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>
This commit is contained in:
parent
8c2320da44
commit
5283d2d265
10 changed files with 1735 additions and 264 deletions
|
|
@ -11,13 +11,21 @@
|
||||||
7D757E1341A0143A2E9EBDF4 /* Pods_PayfritBeacon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F6819A0F2BD2E9D84E7EDB4 /* Pods_PayfritBeacon.framework */; };
|
7D757E1341A0143A2E9EBDF4 /* Pods_PayfritBeacon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F6819A0F2BD2E9D84E7EDB4 /* Pods_PayfritBeacon.framework */; };
|
||||||
D01000000001 /* PayfritBeaconApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02000000001 /* PayfritBeaconApp.swift */; };
|
D01000000001 /* PayfritBeaconApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02000000001 /* PayfritBeaconApp.swift */; };
|
||||||
D01000000002 /* Api.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02000000002 /* Api.swift */; };
|
D01000000002 /* Api.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02000000002 /* Api.swift */; };
|
||||||
|
D01000000003 /* BeaconBanList.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02000000003 /* BeaconBanList.swift */; };
|
||||||
|
D01000000004 /* BeaconScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02000000004 /* BeaconScanner.swift */; };
|
||||||
D01000000005 /* DevBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02000000005 /* DevBanner.swift */; };
|
D01000000005 /* DevBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02000000005 /* DevBanner.swift */; };
|
||||||
D01000000006 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02000000006 /* LoginView.swift */; };
|
D01000000006 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02000000006 /* LoginView.swift */; };
|
||||||
D01000000007 /* BusinessListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02000000007 /* BusinessListView.swift */; };
|
D01000000007 /* BusinessListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02000000007 /* BusinessListView.swift */; };
|
||||||
|
D01000000008 /* ScanView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02000000008 /* ScanView.swift */; };
|
||||||
|
D01000000009 /* QrScanView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02000000009 /* QrScanView.swift */; };
|
||||||
D0100000000A /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0200000000A /* RootView.swift */; };
|
D0100000000A /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0200000000A /* RootView.swift */; };
|
||||||
D01000000060 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D02000000060 /* Assets.xcassets */; };
|
D01000000060 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D02000000060 /* Assets.xcassets */; };
|
||||||
D01000000070 /* payfrit-favicon-light-outlines.svg in Resources */ = {isa = PBXBuildFile; fileRef = D02000000070 /* payfrit-favicon-light-outlines.svg */; };
|
D01000000070 /* payfrit-favicon-light-outlines.svg in Resources */ = {isa = PBXBuildFile; fileRef = D02000000070 /* payfrit-favicon-light-outlines.svg */; };
|
||||||
D01000000080 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = D02000000080 /* InfoPlist.strings */; };
|
D01000000080 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = D02000000080 /* InfoPlist.strings */; };
|
||||||
|
D010000000B1 /* BLEBeaconScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8710F334FDFE10F0625EB86D /* BLEBeaconScanner.swift */; };
|
||||||
|
D010000000B2 /* BeaconProvisioner.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7C275738594E225BE7D5740 /* BeaconProvisioner.swift */; };
|
||||||
|
D010000000B3 /* BeaconShardPool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 964B63D1857877BBEE73F1D1 /* BeaconShardPool.swift */; };
|
||||||
|
F1575ED0F871FE8806035906 /* DebugLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = F100AA6D1E41596FCB1C1A39 /* DebugLog.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
|
|
@ -42,6 +50,7 @@
|
||||||
D02000000070 /* payfrit-favicon-light-outlines.svg */ = {isa = PBXFileReference; lastKnownFileType = text; path = "payfrit-favicon-light-outlines.svg"; sourceTree = "<group>"; };
|
D02000000070 /* payfrit-favicon-light-outlines.svg */ = {isa = PBXFileReference; lastKnownFileType = text; path = "payfrit-favicon-light-outlines.svg"; sourceTree = "<group>"; };
|
||||||
D02000000081 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
D02000000081 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||||
E1775119CBC98A753AE26D84 /* ServicePointListView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ServicePointListView.swift; sourceTree = "<group>"; };
|
E1775119CBC98A753AE26D84 /* ServicePointListView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ServicePointListView.swift; sourceTree = "<group>"; };
|
||||||
|
F100AA6D1E41596FCB1C1A39 /* DebugLog.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DebugLog.swift; path = PayfritBeacon/DebugLog.swift; sourceTree = "<group>"; };
|
||||||
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 = "<group>"; };
|
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 = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
|
|
@ -73,6 +82,7 @@
|
||||||
C05000000009 /* Products */,
|
C05000000009 /* Products */,
|
||||||
04996117E2F5D5BB2D86CD46 /* Pods */,
|
04996117E2F5D5BB2D86CD46 /* Pods */,
|
||||||
EEC06FED6BE78CF9357F3158 /* Frameworks */,
|
EEC06FED6BE78CF9357F3158 /* Frameworks */,
|
||||||
|
F100AA6D1E41596FCB1C1A39 /* DebugLog.swift */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
|
@ -234,11 +244,19 @@
|
||||||
files = (
|
files = (
|
||||||
D01000000001 /* PayfritBeaconApp.swift in Sources */,
|
D01000000001 /* PayfritBeaconApp.swift in Sources */,
|
||||||
D01000000002 /* Api.swift in Sources */,
|
D01000000002 /* Api.swift in Sources */,
|
||||||
|
D01000000003 /* BeaconBanList.swift in Sources */,
|
||||||
|
D01000000004 /* BeaconScanner.swift in Sources */,
|
||||||
D01000000005 /* DevBanner.swift in Sources */,
|
D01000000005 /* DevBanner.swift in Sources */,
|
||||||
D01000000006 /* LoginView.swift in Sources */,
|
D01000000006 /* LoginView.swift in Sources */,
|
||||||
D01000000007 /* BusinessListView.swift in Sources */,
|
D01000000007 /* BusinessListView.swift in Sources */,
|
||||||
|
D01000000008 /* ScanView.swift in Sources */,
|
||||||
|
D01000000009 /* QrScanView.swift in Sources */,
|
||||||
D0100000000A /* RootView.swift in Sources */,
|
D0100000000A /* RootView.swift in Sources */,
|
||||||
|
D010000000B1 /* BLEBeaconScanner.swift in Sources */,
|
||||||
|
D010000000B2 /* BeaconProvisioner.swift in Sources */,
|
||||||
|
D010000000B3 /* BeaconShardPool.swift in Sources */,
|
||||||
281CC856DD918C4CFA00EB67 /* ServicePointListView.swift in Sources */,
|
281CC856DD918C4CFA00EB67 /* ServicePointListView.swift in Sources */,
|
||||||
|
F1575ED0F871FE8806035906 /* DebugLog.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|
@ -256,6 +274,193 @@
|
||||||
/* End PBXVariantGroup section */
|
/* End PBXVariantGroup section */
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration section */
|
/* Begin XCBuildConfiguration section */
|
||||||
|
064DDD2A9238EC6900250593 /* Release-Dev */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = F1B84CA677516C50F6BE2294 /* Pods-PayfritBeacon.release.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
APP_DISPLAY_NAME = "Payfrit Beacon BETA";
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 2;
|
||||||
|
DEVELOPMENT_TEAM = U83YL8VRF3;
|
||||||
|
GENERATE_INFOPLIST_FILE = NO;
|
||||||
|
INFOPLIST_FILE = PayfritBeacon/Info.plist;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = "Payfrit Beacon";
|
||||||
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.business";
|
||||||
|
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||||
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||||
|
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||||
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.payfrit.beacon.dev;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
|
SUPPORTS_MACCATALYST = NO;
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEV;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
};
|
||||||
|
name = "Release-Dev";
|
||||||
|
};
|
||||||
|
672BF8AE9DE36DEAE34D6DA0 /* Debug-Dev */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_TESTABILITY = YES;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
|
GCC_DYNAMIC_NO_PIC = NO;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_OPTIMIZATION_LEVEL = 0;
|
||||||
|
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||||
|
"DEBUG=1",
|
||||||
|
"$(inherited)",
|
||||||
|
);
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
|
MTL_FAST_MATH = YES;
|
||||||
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
};
|
||||||
|
name = "Debug-Dev";
|
||||||
|
};
|
||||||
|
99070AFE15F557412BACA2C9 /* Release-Dev */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
|
MTL_FAST_MATH = YES;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
|
VALIDATE_PRODUCT = YES;
|
||||||
|
};
|
||||||
|
name = "Release-Dev";
|
||||||
|
};
|
||||||
|
B0D496FEA252D8DDA33F57A0 /* Debug-Dev */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = AFF542870BA2862FAB429A86 /* Pods-PayfritBeacon.debug.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
APP_DISPLAY_NAME = "Payfrit Beacon BETA";
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 2;
|
||||||
|
DEVELOPMENT_TEAM = U83YL8VRF3;
|
||||||
|
GENERATE_INFOPLIST_FILE = NO;
|
||||||
|
INFOPLIST_FILE = PayfritBeacon/Info.plist;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = "Payfrit Beacon";
|
||||||
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.business";
|
||||||
|
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||||
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||||
|
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||||
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.payfrit.beacon.dev;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
|
SUPPORTS_MACCATALYST = NO;
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG DEV";
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
};
|
||||||
|
name = "Debug-Dev";
|
||||||
|
};
|
||||||
C0B000000001 /* Debug */ = {
|
C0B000000001 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
|
@ -377,6 +582,7 @@
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = AFF542870BA2862FAB429A86 /* Pods-PayfritBeacon.debug.xcconfig */;
|
baseConfigurationReference = AFF542870BA2862FAB429A86 /* Pods-PayfritBeacon.debug.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
APP_DISPLAY_NAME = "Payfrit Beacon";
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
|
@ -400,6 +606,7 @@
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
SUPPORTS_MACCATALYST = NO;
|
SUPPORTS_MACCATALYST = NO;
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
|
@ -410,6 +617,7 @@
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = F1B84CA677516C50F6BE2294 /* Pods-PayfritBeacon.release.xcconfig */;
|
baseConfigurationReference = F1B84CA677516C50F6BE2294 /* Pods-PayfritBeacon.release.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
APP_DISPLAY_NAME = "Payfrit Beacon";
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
|
@ -433,6 +641,7 @@
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
SUPPORTS_MACCATALYST = NO;
|
SUPPORTS_MACCATALYST = NO;
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
|
@ -447,6 +656,8 @@
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
C0B000000001 /* Debug */,
|
C0B000000001 /* Debug */,
|
||||||
C0B000000002 /* Release */,
|
C0B000000002 /* Release */,
|
||||||
|
672BF8AE9DE36DEAE34D6DA0 /* Debug-Dev */,
|
||||||
|
99070AFE15F557412BACA2C9 /* Release-Dev */,
|
||||||
);
|
);
|
||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
|
|
@ -456,6 +667,8 @@
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
C0B000000003 /* Debug */,
|
C0B000000003 /* Debug */,
|
||||||
C0B000000004 /* Release */,
|
C0B000000004 /* Release */,
|
||||||
|
B0D496FEA252D8DDA33F57A0 /* Debug-Dev */,
|
||||||
|
064DDD2A9238EC6900250593 /* Release-Dev */,
|
||||||
);
|
);
|
||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1620"
|
||||||
|
version = "1.7">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "C06000000001"
|
||||||
|
BuildableName = "PayfritBeacon.app"
|
||||||
|
BlueprintName = "PayfritBeacon"
|
||||||
|
ReferencedContainer = "container:PayfritBeacon.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug-Dev"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
shouldAutocreateTestPlan = "YES">
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug-Dev"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "C06000000001"
|
||||||
|
BuildableName = "PayfritBeacon.app"
|
||||||
|
BlueprintName = "PayfritBeacon"
|
||||||
|
ReferencedContainer = "container:PayfritBeacon.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release-Dev"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "C06000000001"
|
||||||
|
BuildableName = "PayfritBeacon.app"
|
||||||
|
BlueprintName = "PayfritBeacon"
|
||||||
|
ReferencedContainer = "container:PayfritBeacon.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug-Dev">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release-Dev"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
|
|
@ -3,8 +3,12 @@ import Foundation
|
||||||
class Api {
|
class Api {
|
||||||
static let shared = Api()
|
static let shared = Api()
|
||||||
|
|
||||||
// ── DEV toggle: flip to false for production ──
|
// ── DEV toggle: driven by DEV compiler flag (set in build configuration) ──
|
||||||
|
#if DEV
|
||||||
|
static let IS_DEV = true
|
||||||
|
#else
|
||||||
static let IS_DEV = false
|
static let IS_DEV = false
|
||||||
|
#endif
|
||||||
|
|
||||||
private static var BASE_URL: String {
|
private static var BASE_URL: String {
|
||||||
IS_DEV ? "https://dev.payfrit.com/api" : "https://biz.payfrit.com/api"
|
IS_DEV ? "https://dev.payfrit.com/api" : "https://biz.payfrit.com/api"
|
||||||
|
|
@ -280,51 +284,69 @@ class Api {
|
||||||
|
|
||||||
/// Get beacon config for a service point (UUID, Major, Minor to write to beacon)
|
/// Get beacon config for a service point (UUID, Major, Minor to write to beacon)
|
||||||
func getBeaconConfig(businessId: Int, servicePointId: Int) async throws -> BeaconConfigResponse {
|
func getBeaconConfig(businessId: Int, servicePointId: Int) async throws -> BeaconConfigResponse {
|
||||||
|
DebugLog.shared.log("[API] getBeaconConfig businessId=\(businessId) servicePointId=\(servicePointId)")
|
||||||
|
|
||||||
let json = try await postRequest(
|
let json = try await postRequest(
|
||||||
endpoint: "/beacon-sharding/get_beacon_config.cfm",
|
endpoint: "/beacon-sharding/get_beacon_config.cfm",
|
||||||
body: ["BusinessID": businessId, "ServicePointID": servicePointId],
|
body: ["BusinessID": businessId, "ServicePointID": servicePointId],
|
||||||
extraHeaders: ["X-Business-Id": String(businessId)]
|
extraHeaders: ["X-Business-Id": String(businessId)]
|
||||||
)
|
)
|
||||||
|
|
||||||
if !parseBool(json["OK"] ?? json["ok"]) {
|
DebugLog.shared.log("[API] getBeaconConfig response keys: \(json.keys.sorted())")
|
||||||
let error = ((json["ERROR"] ?? json["error"]) as? String) ?? "Failed to get beacon config"
|
DebugLog.shared.log("[API] getBeaconConfig full response: \(json)")
|
||||||
|
|
||||||
|
if !parseBool(json["OK"] ?? json["ok"] ?? json["Ok"]) {
|
||||||
|
let error = ((json["ERROR"] ?? json["error"] ?? json["Error"]) as? String) ?? "Failed to get beacon config"
|
||||||
throw ApiException(error)
|
throw ApiException(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let uuid = (json["UUID"] ?? json["uuid"]) as? String,
|
guard let uuid = (json["UUID"] ?? json["uuid"] ?? json["Uuid"] ?? json["BeaconUUID"] ?? json["BEACONUUID"]) as? String else {
|
||||||
let major = parseIntValue(json["MAJOR"] ?? json["major"]),
|
throw ApiException("Invalid beacon config response - no UUID. Keys: \(json.keys.sorted())")
|
||||||
let minor = parseIntValue(json["MINOR"] ?? json["minor"]) else {
|
|
||||||
throw ApiException("Invalid beacon config response")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
guard let major = parseIntValue(json["MAJOR"] ?? json["major"] ?? json["Major"] ?? json["BeaconMajor"] ?? json["BEACONMAJOR"]) else {
|
||||||
|
throw ApiException("Invalid beacon config response - no Major. Keys: \(json.keys.sorted())")
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let minor = parseIntValue(json["MINOR"] ?? json["minor"] ?? json["Minor"] ?? json["BeaconMinor"] ?? json["BEACONMINOR"]) else {
|
||||||
|
throw ApiException("Invalid beacon config response - no Minor. Keys: \(json.keys.sorted())")
|
||||||
|
}
|
||||||
|
|
||||||
|
DebugLog.shared.log("[API] getBeaconConfig parsed: uuid=\(uuid) major=\(major) minor=\(minor)")
|
||||||
|
|
||||||
return BeaconConfigResponse(
|
return BeaconConfigResponse(
|
||||||
uuid: uuid.replacingOccurrences(of: "-", with: "").uppercased(),
|
uuid: uuid.replacingOccurrences(of: "-", with: "").uppercased(),
|
||||||
major: UInt16(major),
|
major: UInt16(major),
|
||||||
minor: UInt16(minor),
|
minor: UInt16(minor),
|
||||||
txPower: parseIntValue(json["TXPOWER"] ?? json["txPower"]) ?? -59,
|
txPower: parseIntValue(json["TXPOWER"] ?? json["txPower"] ?? json["TxPower"]) ?? -59,
|
||||||
interval: parseIntValue(json["INTERVAL"] ?? json["interval"]) ?? 350
|
interval: parseIntValue(json["INTERVAL"] ?? json["interval"] ?? json["Interval"]) ?? 350
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Register beacon hardware after provisioning
|
/// Register beacon hardware after provisioning
|
||||||
func registerBeaconHardware(businessId: Int, servicePointId: Int, uuid: String, major: UInt16, minor: UInt16, macAddress: String?) async throws -> Bool {
|
func registerBeaconHardware(businessId: Int, servicePointId: Int, uuid: String, major: UInt16, minor: UInt16, hardwareId: String, macAddress: String? = nil) async throws -> Bool {
|
||||||
var body: [String: Any] = [
|
var body: [String: Any] = [
|
||||||
"BusinessID": businessId,
|
"BusinessID": businessId,
|
||||||
"ServicePointID": servicePointId,
|
"ServicePointID": servicePointId,
|
||||||
"UUID": uuid,
|
"UUID": uuid,
|
||||||
"Major": major,
|
"Major": major,
|
||||||
"Minor": minor
|
"Minor": minor,
|
||||||
|
"HardwareID": hardwareId
|
||||||
]
|
]
|
||||||
if let mac = macAddress, !mac.isEmpty {
|
if let mac = macAddress, !mac.isEmpty {
|
||||||
body["MACAddress"] = mac
|
body["MACAddress"] = mac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DebugLog.shared.log("[API] registerBeaconHardware body: \(body)")
|
||||||
|
|
||||||
let json = try await postRequest(
|
let json = try await postRequest(
|
||||||
endpoint: "/beacon-sharding/register_beacon_hardware.cfm",
|
endpoint: "/beacon-sharding/register_beacon_hardware.cfm",
|
||||||
body: body,
|
body: body,
|
||||||
extraHeaders: ["X-Business-Id": String(businessId)]
|
extraHeaders: ["X-Business-Id": String(businessId)]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
DebugLog.shared.log("[API] registerBeaconHardware response: \(json)")
|
||||||
|
|
||||||
if !parseBool(json["OK"] ?? json["ok"]) {
|
if !parseBool(json["OK"] ?? json["ok"]) {
|
||||||
let error = ((json["ERROR"] ?? json["error"]) as? String) ?? "Failed to register beacon"
|
let error = ((json["ERROR"] ?? json["error"]) as? String) ?? "Failed to register beacon"
|
||||||
throw ApiException(error)
|
throw ApiException(error)
|
||||||
|
|
@ -376,7 +398,8 @@ class Api {
|
||||||
|
|
||||||
return BusinessNamespace(
|
return BusinessNamespace(
|
||||||
shardId: parseIntValue(json["ShardID"] ?? json["SHARDID"]) ?? 0,
|
shardId: parseIntValue(json["ShardID"] ?? json["SHARDID"]) ?? 0,
|
||||||
uuid: uuid.replacingOccurrences(of: "-", with: "").uppercased(),
|
uuid: uuid,
|
||||||
|
uuidClean: uuid.replacingOccurrences(of: "-", with: "").uppercased(),
|
||||||
major: UInt16(major),
|
major: UInt16(major),
|
||||||
alreadyAllocated: parseBool(json["AlreadyAllocated"] ?? json["ALREADYALLOCATED"])
|
alreadyAllocated: parseBool(json["AlreadyAllocated"] ?? json["ALREADYALLOCATED"])
|
||||||
)
|
)
|
||||||
|
|
@ -412,12 +435,16 @@ class Api {
|
||||||
body["ServicePointID"] = spId
|
body["ServicePointID"] = spId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DebugLog.shared.log("[API] saveServicePoint businessId=\(businessId) name=\(name) servicePointId=\(String(describing: servicePointId))")
|
||||||
|
|
||||||
let json = try await postRequest(
|
let json = try await postRequest(
|
||||||
endpoint: "/servicepoints/save.cfm",
|
endpoint: "/servicepoints/save.cfm",
|
||||||
body: body,
|
body: body,
|
||||||
extraHeaders: ["X-Business-Id": String(businessId)]
|
extraHeaders: ["X-Business-Id": String(businessId)]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
DebugLog.shared.log("[API] saveServicePoint response: \(json)")
|
||||||
|
|
||||||
if !parseBool(json["OK"] ?? json["ok"]) {
|
if !parseBool(json["OK"] ?? json["ok"]) {
|
||||||
let error = ((json["ERROR"] ?? json["error"]) as? String) ?? "Failed to save service point"
|
let error = ((json["ERROR"] ?? json["error"]) as? String) ?? "Failed to save service point"
|
||||||
throw ApiException(error)
|
throw ApiException(error)
|
||||||
|
|
@ -429,6 +456,12 @@ class Api {
|
||||||
let spId = parseIntValue(sp["ServicePointID"] ?? sp["SERVICEPOINTID"]) ?? servicePointId ?? 0
|
let spId = parseIntValue(sp["ServicePointID"] ?? sp["SERVICEPOINTID"]) ?? servicePointId ?? 0
|
||||||
let beaconMinor = parseIntValue(sp["BeaconMinor"] ?? sp["BEACONMINOR"])
|
let beaconMinor = parseIntValue(sp["BeaconMinor"] ?? sp["BEACONMINOR"])
|
||||||
|
|
||||||
|
DebugLog.shared.log("[API] saveServicePoint parsed: spId=\(spId) beaconMinor=\(String(describing: beaconMinor))")
|
||||||
|
|
||||||
|
if spId == 0 {
|
||||||
|
DebugLog.shared.log("[API] WARNING: servicePointId is 0! Full sp dict: \(sp)")
|
||||||
|
}
|
||||||
|
|
||||||
return ServicePoint(
|
return ServicePoint(
|
||||||
servicePointId: spId,
|
servicePointId: spId,
|
||||||
name: name,
|
name: name,
|
||||||
|
|
@ -442,6 +475,20 @@ class Api {
|
||||||
return try await saveServicePoint(businessId: businessId, name: name)
|
return try await saveServicePoint(businessId: businessId, name: name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Delete a service point
|
||||||
|
func deleteServicePoint(businessId: Int, servicePointId: Int) async throws {
|
||||||
|
let json = try await postRequest(
|
||||||
|
endpoint: "/servicepoints/delete.cfm",
|
||||||
|
body: ["BusinessID": businessId, "ServicePointID": servicePointId],
|
||||||
|
extraHeaders: ["X-Business-Id": String(businessId)]
|
||||||
|
)
|
||||||
|
|
||||||
|
if !parseBool(json["OK"] ?? json["ok"]) {
|
||||||
|
let error = ((json["ERROR"] ?? json["error"]) as? String) ?? "Failed to delete service point"
|
||||||
|
throw ApiException(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// HELPERS
|
// HELPERS
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
@ -550,7 +597,8 @@ struct ServicePoint: Identifiable {
|
||||||
|
|
||||||
struct BusinessNamespace {
|
struct BusinessNamespace {
|
||||||
let shardId: Int
|
let shardId: Int
|
||||||
let uuid: String // 32-char hex, no dashes
|
let uuid: String // Original UUID from API (with dashes, as-is)
|
||||||
|
let uuidClean: String // 32-char hex, no dashes, uppercase (for BLE provisioning)
|
||||||
let major: UInt16
|
let major: UInt16
|
||||||
let alreadyAllocated: Bool
|
let alreadyAllocated: Bool
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import CoreBluetooth
|
import CoreBluetooth
|
||||||
|
|
||||||
/// Beacon type detected by service UUID
|
/// Beacon type detected by service UUID or name
|
||||||
enum BeaconType: String {
|
enum BeaconType: String {
|
||||||
case kbeacon = "KBeacon"
|
case kbeacon = "KBeacon"
|
||||||
|
case dxsmart = "DX-Smart"
|
||||||
case bluecharm = "BlueCharm"
|
case bluecharm = "BlueCharm"
|
||||||
case unknown = "Unknown"
|
case unknown = "Unknown"
|
||||||
}
|
}
|
||||||
|
|
@ -18,8 +19,8 @@ struct DiscoveredBeacon: Identifiable {
|
||||||
var lastSeen: Date
|
var lastSeen: Date
|
||||||
|
|
||||||
var displayName: String {
|
var displayName: String {
|
||||||
if name.isEmpty || name == "Unknown" {
|
if name.isEmpty {
|
||||||
return "\(type.rawValue) (\(id.uuidString.prefix(8))...)"
|
return id.uuidString.prefix(8) + "..."
|
||||||
}
|
}
|
||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
|
|
@ -64,9 +65,9 @@ class BLEBeaconScanner: NSObject, ObservableObject {
|
||||||
options: [CBCentralManagerScanOptionAllowDuplicatesKey: true]
|
options: [CBCentralManagerScanOptionAllowDuplicatesKey: true]
|
||||||
)
|
)
|
||||||
|
|
||||||
// Auto-stop after 10 seconds
|
// Auto-stop after 1 second (beacons advertise every ~200ms so 1s is plenty)
|
||||||
scanTimer?.invalidate()
|
scanTimer?.invalidate()
|
||||||
scanTimer = Timer.scheduledTimer(withTimeInterval: 10.0, repeats: false) { [weak self] _ in
|
scanTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { [weak self] _ in
|
||||||
self?.stopScanning()
|
self?.stopScanning()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -104,35 +105,20 @@ extension BLEBeaconScanner: CBCentralManagerDelegate {
|
||||||
advertisementData: [String: Any], rssi RSSI: NSNumber) {
|
advertisementData: [String: Any], rssi RSSI: NSNumber) {
|
||||||
|
|
||||||
let rssiValue = RSSI.intValue
|
let rssiValue = RSSI.intValue
|
||||||
guard rssiValue > -90 && rssiValue < 0 else { return } // Filter weak signals
|
guard rssiValue > -70 && rssiValue < 0 else { return } // Only show nearby devices
|
||||||
|
|
||||||
let name = peripheral.name ?? advertisementData[CBAdvertisementDataLocalNameKey] as? String ?? ""
|
let name = peripheral.name ?? advertisementData[CBAdvertisementDataLocalNameKey] as? String ?? ""
|
||||||
|
|
||||||
// Determine beacon type from name or advertised services
|
// Best-effort type hint from advertised services (informational only)
|
||||||
var beaconType: BeaconType = .unknown
|
var beaconType: BeaconType = .unknown
|
||||||
|
|
||||||
// Check advertised service UUIDs
|
|
||||||
if let serviceUUIDs = advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID] {
|
if let serviceUUIDs = advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID] {
|
||||||
if serviceUUIDs.contains(BLEBeaconScanner.KBEACON_SERVICE) {
|
if serviceUUIDs.contains(BLEBeaconScanner.KBEACON_SERVICE) {
|
||||||
beaconType = .kbeacon
|
beaconType = .dxsmart
|
||||||
} else if serviceUUIDs.contains(BLEBeaconScanner.BLUECHARM_SERVICE) {
|
} else if serviceUUIDs.contains(BLEBeaconScanner.BLUECHARM_SERVICE) {
|
||||||
beaconType = .bluecharm
|
beaconType = .bluecharm
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also check by name patterns
|
|
||||||
if beaconType == .unknown {
|
|
||||||
let lowerName = name.lowercased()
|
|
||||||
if lowerName.contains("kbeacon") || lowerName.contains("kbpro") || lowerName.hasPrefix("kb") {
|
|
||||||
beaconType = .kbeacon
|
|
||||||
} else if lowerName.contains("bluecharm") || lowerName.contains("bc") || lowerName.hasPrefix("bc") {
|
|
||||||
beaconType = .bluecharm
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only track beacons we can identify
|
|
||||||
guard beaconType != .unknown else { return }
|
|
||||||
|
|
||||||
// Update or add beacon
|
// Update or add beacon
|
||||||
if let index = discoveredBeacons.firstIndex(where: { $0.id == peripheral.identifier }) {
|
if let index = discoveredBeacons.firstIndex(where: { $0.id == peripheral.identifier }) {
|
||||||
discoveredBeacons[index].rssi = rssiValue
|
discoveredBeacons[index].rssi = rssiValue
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
33
PayfritBeacon/DebugLog.swift
Normal file
33
PayfritBeacon/DebugLog.swift
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// Simple in-app debug log — viewable from ScanView
|
||||||
|
class DebugLog: ObservableObject {
|
||||||
|
static let shared = DebugLog()
|
||||||
|
|
||||||
|
@Published var entries: [String] = []
|
||||||
|
|
||||||
|
private let maxEntries = 200
|
||||||
|
|
||||||
|
func log(_ message: String) {
|
||||||
|
let ts = DateFormatter.localizedString(from: Date(), dateStyle: .none, timeStyle: .medium)
|
||||||
|
let entry = "[\(ts)] \(message)"
|
||||||
|
NSLog("[DebugLog] \(message)")
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.entries.append(entry)
|
||||||
|
if self.entries.count > self.maxEntries {
|
||||||
|
self.entries.removeFirst(self.entries.count - self.maxEntries)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func clear() {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.entries.removeAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all entries as a single string for clipboard
|
||||||
|
var allText: String {
|
||||||
|
entries.joined(separator: "\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,15 +11,17 @@
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>Payfrit Beacon</string>
|
<string>$(APP_DISPLAY_NAME)</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>Payfrit Beacon</string>
|
<string>$(APP_DISPLAY_NAME)</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>$(MARKETING_VERSION)</string>
|
<string>$(MARKETING_VERSION)</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
|
<key>NSBluetoothAlwaysUsageDescription</key>
|
||||||
|
<string>Payfrit Beacon uses Bluetooth to discover and configure nearby beacons.</string>
|
||||||
<key>NSFaceIDUsageDescription</key>
|
<key>NSFaceIDUsageDescription</key>
|
||||||
<string>Payfrit Beacon uses Face ID for quick sign-in.</string>
|
<string>Payfrit Beacon uses Face ID for quick sign-in.</string>
|
||||||
<key>UIApplicationSceneManifest</key>
|
<key>UIApplicationSceneManifest</key>
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ struct PayfritBeaconApp: App {
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
RootView()
|
RootView()
|
||||||
|
.preferredColorScheme(.light)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,13 @@ import SwiftUI
|
||||||
struct ScanView: View {
|
struct ScanView: View {
|
||||||
let businessId: Int
|
let businessId: Int
|
||||||
let businessName: String
|
let businessName: String
|
||||||
|
var reprovisionServicePoint: ServicePoint? = nil // If set, we're re-provisioning an existing SP
|
||||||
var onBack: () -> Void
|
var onBack: () -> Void
|
||||||
|
|
||||||
@StateObject private var bleScanner = BLEBeaconScanner()
|
@StateObject private var bleScanner = BLEBeaconScanner()
|
||||||
@StateObject private var provisioner = BeaconProvisioner()
|
@StateObject private var provisioner = BeaconProvisioner()
|
||||||
|
|
||||||
|
@State private var namespace: BusinessNamespace?
|
||||||
@State private var servicePoints: [ServicePoint] = []
|
@State private var servicePoints: [ServicePoint] = []
|
||||||
@State private var nextTableNumber: Int = 1
|
@State private var nextTableNumber: Int = 1
|
||||||
@State private var provisionedCount: Int = 0
|
@State private var provisionedCount: Int = 0
|
||||||
|
|
@ -21,6 +23,18 @@ struct ScanView: View {
|
||||||
@State private var assignName = ""
|
@State private var assignName = ""
|
||||||
@State private var isProvisioning = false
|
@State private var isProvisioning = false
|
||||||
@State private var provisioningProgress = ""
|
@State private var provisioningProgress = ""
|
||||||
|
@State private var provisioningError: String?
|
||||||
|
|
||||||
|
// Action sheet + check config
|
||||||
|
@State private var showBeaconActionSheet = false
|
||||||
|
@State private var showCheckConfigSheet = false
|
||||||
|
@State private var checkConfigData: BeaconCheckResult?
|
||||||
|
@State private var isCheckingConfig = false
|
||||||
|
@State private var checkConfigError: String?
|
||||||
|
|
||||||
|
// Debug log
|
||||||
|
@State private var showDebugLog = false
|
||||||
|
@ObservedObject private var debugLog = DebugLog.shared
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
|
|
@ -38,6 +52,13 @@ struct ScanView: View {
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.payfritGreen)
|
.foregroundColor(.payfritGreen)
|
||||||
}
|
}
|
||||||
|
Button {
|
||||||
|
showDebugLog = true
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "ladybug")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
.background(Color(.systemBackground))
|
.background(Color(.systemBackground))
|
||||||
|
|
@ -48,10 +69,16 @@ struct ScanView: View {
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
Spacer()
|
Spacer()
|
||||||
|
if let sp = reprovisionServicePoint {
|
||||||
|
Text("Re-provision: \(sp.name)")
|
||||||
|
.font(.subheadline.bold())
|
||||||
|
.foregroundColor(.orange)
|
||||||
|
} else {
|
||||||
Text("Next: Table \(nextTableNumber)")
|
Text("Next: Table \(nextTableNumber)")
|
||||||
.font(.subheadline.bold())
|
.font(.subheadline.bold())
|
||||||
.foregroundColor(.payfritGreen)
|
.foregroundColor(.payfritGreen)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
|
|
||||||
|
|
@ -90,7 +117,10 @@ struct ScanView: View {
|
||||||
LazyVStack(spacing: 8) {
|
LazyVStack(spacing: 8) {
|
||||||
ForEach(bleScanner.discoveredBeacons) { beacon in
|
ForEach(bleScanner.discoveredBeacons) { beacon in
|
||||||
beaconRow(beacon)
|
beaconRow(beacon)
|
||||||
.onTapGesture { selectBeacon(beacon) }
|
.onTapGesture {
|
||||||
|
selectedBeacon = beacon
|
||||||
|
showBeaconActionSheet = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
|
|
@ -133,6 +163,33 @@ struct ScanView: View {
|
||||||
.modifier(DevBanner())
|
.modifier(DevBanner())
|
||||||
.overlay(snackOverlay, alignment: .bottom)
|
.overlay(snackOverlay, alignment: .bottom)
|
||||||
.sheet(isPresented: $showAssignSheet) { assignSheet }
|
.sheet(isPresented: $showAssignSheet) { assignSheet }
|
||||||
|
.sheet(isPresented: $showCheckConfigSheet) { checkConfigSheet }
|
||||||
|
.sheet(isPresented: $showDebugLog) { debugLogSheet }
|
||||||
|
.confirmationDialog(
|
||||||
|
selectedBeacon?.displayName ?? "Beacon",
|
||||||
|
isPresented: $showBeaconActionSheet,
|
||||||
|
titleVisibility: .visible
|
||||||
|
) {
|
||||||
|
if let sp = reprovisionServicePoint {
|
||||||
|
Button("Provision for \(sp.name)") {
|
||||||
|
guard selectedBeacon != nil else { return }
|
||||||
|
reprovisionBeacon()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Button("Configure (Assign & Provision)") {
|
||||||
|
guard selectedBeacon != nil else { return }
|
||||||
|
assignName = "Table \(nextTableNumber)"
|
||||||
|
showAssignSheet = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Button("Check Current Config") {
|
||||||
|
guard let beacon = selectedBeacon else { return }
|
||||||
|
checkConfig(beacon)
|
||||||
|
}
|
||||||
|
Button("Cancel", role: .cancel) {
|
||||||
|
selectedBeacon = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
.onAppear { loadServicePoints() }
|
.onAppear { loadServicePoints() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -179,13 +236,15 @@ struct ScanView: View {
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
|
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
|
if beacon.type != .unknown {
|
||||||
Text(beacon.type.rawValue)
|
Text(beacon.type.rawValue)
|
||||||
.font(.caption2.weight(.medium))
|
.font(.caption2.weight(.medium))
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
.padding(.horizontal, 6)
|
.padding(.horizontal, 6)
|
||||||
.padding(.vertical, 2)
|
.padding(.vertical, 2)
|
||||||
.background(beacon.type == .kbeacon ? Color.blue : Color.purple)
|
.background(beacon.type == .kbeacon ? Color.blue : beacon.type == .dxsmart ? Color.orange : Color.purple)
|
||||||
.cornerRadius(4)
|
.cornerRadius(4)
|
||||||
|
}
|
||||||
|
|
||||||
Text("\(beacon.rssi) dBm")
|
Text("\(beacon.rssi) dBm")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
|
|
@ -230,7 +289,7 @@ struct ScanView: View {
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
.padding(.horizontal, 6)
|
.padding(.horizontal, 6)
|
||||||
.padding(.vertical, 2)
|
.padding(.vertical, 2)
|
||||||
.background(beacon.type == .kbeacon ? Color.blue : Color.purple)
|
.background(beacon.type == .kbeacon ? Color.blue : beacon.type == .dxsmart ? Color.orange : Color.purple)
|
||||||
.cornerRadius(4)
|
.cornerRadius(4)
|
||||||
}
|
}
|
||||||
Text("Signal: \(beacon.rssi) dBm")
|
Text("Signal: \(beacon.rssi) dBm")
|
||||||
|
|
@ -251,19 +310,6 @@ struct ScanView: View {
|
||||||
.font(.title3)
|
.font(.title3)
|
||||||
}
|
}
|
||||||
|
|
||||||
// KBeacon warning
|
|
||||||
if beacon.type == .kbeacon {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: "info.circle.fill")
|
|
||||||
.foregroundColor(.infoBlue)
|
|
||||||
Text("KBeacon requires their app for provisioning. Config will be copied to clipboard.")
|
|
||||||
.font(.caption)
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.background(Color.infoBlue.opacity(0.1))
|
|
||||||
.cornerRadius(8)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Provisioning progress
|
// Provisioning progress
|
||||||
if isProvisioning {
|
if isProvisioning {
|
||||||
HStack {
|
HStack {
|
||||||
|
|
@ -277,6 +323,20 @@ struct ScanView: View {
|
||||||
.cornerRadius(8)
|
.cornerRadius(8)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Provisioning error
|
||||||
|
if let error = provisioningError {
|
||||||
|
HStack(alignment: .top) {
|
||||||
|
Image(systemName: "exclamationmark.triangle.fill")
|
||||||
|
.foregroundColor(.red)
|
||||||
|
Text(error)
|
||||||
|
.font(.callout)
|
||||||
|
.foregroundColor(.red)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background(Color.red.opacity(0.1))
|
||||||
|
.cornerRadius(8)
|
||||||
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -303,6 +363,270 @@ struct ScanView: View {
|
||||||
.interactiveDismissDisabled(isProvisioning)
|
.interactiveDismissDisabled(isProvisioning)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Check Config Sheet
|
||||||
|
|
||||||
|
private var checkConfigSheet: some View {
|
||||||
|
NavigationStack {
|
||||||
|
ScrollView {
|
||||||
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
|
if let beacon = selectedBeacon {
|
||||||
|
// Beacon identity
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text("Beacon")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
HStack {
|
||||||
|
Text(beacon.displayName)
|
||||||
|
.font(.headline)
|
||||||
|
Spacer()
|
||||||
|
if beacon.type != .unknown {
|
||||||
|
Text(beacon.type.rawValue)
|
||||||
|
.font(.caption2.weight(.medium))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.padding(.horizontal, 6)
|
||||||
|
.padding(.vertical, 2)
|
||||||
|
.background(beacon.type == .kbeacon ? Color.blue : beacon.type == .dxsmart ? Color.orange : Color.purple)
|
||||||
|
.cornerRadius(4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Text("Signal: \(beacon.rssi) dBm")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background(Color(.systemGray6))
|
||||||
|
.cornerRadius(8)
|
||||||
|
|
||||||
|
// Loading state
|
||||||
|
if isCheckingConfig {
|
||||||
|
HStack {
|
||||||
|
ProgressView()
|
||||||
|
Text(provisioner.progress.isEmpty ? "Connecting..." : provisioner.progress)
|
||||||
|
.font(.callout)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.background(Color(.systemGray6))
|
||||||
|
.cornerRadius(8)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error state
|
||||||
|
if let error = checkConfigError {
|
||||||
|
HStack(alignment: .top) {
|
||||||
|
Image(systemName: "exclamationmark.triangle.fill")
|
||||||
|
.foregroundColor(.orange)
|
||||||
|
Text(error)
|
||||||
|
.font(.callout)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background(Color.orange.opacity(0.1))
|
||||||
|
.cornerRadius(8)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parsed config data
|
||||||
|
if let data = checkConfigData {
|
||||||
|
// iBeacon configuration
|
||||||
|
if data.hasConfig {
|
||||||
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
|
Text("iBeacon Configuration")
|
||||||
|
.font(.subheadline.weight(.semibold))
|
||||||
|
|
||||||
|
if let uuid = data.uuid {
|
||||||
|
configRow("UUID", uuid)
|
||||||
|
}
|
||||||
|
if let major = data.major {
|
||||||
|
configRow("Major", "\(major)")
|
||||||
|
}
|
||||||
|
if let minor = data.minor {
|
||||||
|
configRow("Minor", "\(minor)")
|
||||||
|
}
|
||||||
|
if let name = data.deviceName {
|
||||||
|
configRow("Name", name)
|
||||||
|
}
|
||||||
|
if let rssi = data.rssiAt1m {
|
||||||
|
configRow("RSSI@1m", "\(rssi) dBm")
|
||||||
|
}
|
||||||
|
if let interval = data.advInterval {
|
||||||
|
configRow("Interval", "\(interval)00 ms")
|
||||||
|
}
|
||||||
|
if let tx = data.txPower {
|
||||||
|
configRow("TX Power", "\(tx)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background(Color(.systemGray6))
|
||||||
|
.cornerRadius(8)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Device info
|
||||||
|
if data.battery != nil || data.macAddress != nil {
|
||||||
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
|
Text("Device Info")
|
||||||
|
.font(.subheadline.weight(.semibold))
|
||||||
|
|
||||||
|
if let battery = data.battery {
|
||||||
|
configRow("Battery", "\(battery)%")
|
||||||
|
}
|
||||||
|
if let mac = data.macAddress {
|
||||||
|
configRow("MAC", mac)
|
||||||
|
}
|
||||||
|
if let slots = data.frameSlots {
|
||||||
|
let slotStr = slots.enumerated().map { i, s in
|
||||||
|
"Slot\(i): 0x\(String(format: "%02X", s))"
|
||||||
|
}.joined(separator: " ")
|
||||||
|
configRow("Frames", slotStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background(Color(.systemGray6))
|
||||||
|
.cornerRadius(8)
|
||||||
|
}
|
||||||
|
|
||||||
|
// No config found
|
||||||
|
if !data.hasConfig && data.battery == nil && data.macAddress == nil && data.rawResponses.isEmpty {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "info.circle")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Text("No DX-Smart config data received")
|
||||||
|
.font(.callout)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !data.servicesFound.isEmpty {
|
||||||
|
Text("Services: \(data.servicesFound.joined(separator: ", "))")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background(Color(.systemGray6))
|
||||||
|
.cornerRadius(8)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Raw responses (debug section)
|
||||||
|
if !data.rawResponses.isEmpty {
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
HStack {
|
||||||
|
Text("Raw Responses")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Spacer()
|
||||||
|
Button {
|
||||||
|
let raw = data.rawResponses.joined(separator: "\n")
|
||||||
|
UIPasteboard.general.string = raw
|
||||||
|
showSnack("Copied to clipboard")
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "doc.on.doc")
|
||||||
|
.font(.caption)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Text(data.rawResponses.joined(separator: "\n"))
|
||||||
|
.font(.system(.caption2, design: .monospaced))
|
||||||
|
.textSelection(.enabled)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background(Color(.systemGray6))
|
||||||
|
.cornerRadius(8)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Services/chars discovery info
|
||||||
|
if !data.characteristicsFound.isEmpty {
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text("BLE Discovery")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Text(data.characteristicsFound.joined(separator: "\n"))
|
||||||
|
.font(.system(.caption2, design: .monospaced))
|
||||||
|
.textSelection(.enabled)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background(Color(.systemGray6))
|
||||||
|
.cornerRadius(8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text("No beacon selected")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
.navigationTitle("Check Config")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .cancellationAction) {
|
||||||
|
Button("Done") {
|
||||||
|
showCheckConfigSheet = false
|
||||||
|
selectedBeacon = nil
|
||||||
|
checkConfigData = nil
|
||||||
|
checkConfigError = nil
|
||||||
|
}
|
||||||
|
.disabled(isCheckingConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.presentationDetents([.medium, .large])
|
||||||
|
.interactiveDismissDisabled(isCheckingConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configRow(_ label: String, _ value: String) -> some View {
|
||||||
|
HStack(alignment: .top) {
|
||||||
|
Text(label)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.frame(width: 80, alignment: .leading)
|
||||||
|
Text(value)
|
||||||
|
.font(.system(.caption, design: .monospaced))
|
||||||
|
.textSelection(.enabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Debug Log Sheet
|
||||||
|
|
||||||
|
private var debugLogSheet: some View {
|
||||||
|
NavigationStack {
|
||||||
|
ScrollViewReader { proxy in
|
||||||
|
ScrollView {
|
||||||
|
LazyVStack(alignment: .leading, spacing: 2) {
|
||||||
|
ForEach(Array(debugLog.entries.enumerated()), id: \.offset) { idx, entry in
|
||||||
|
Text(entry)
|
||||||
|
.font(.system(.caption2, design: .monospaced))
|
||||||
|
.textSelection(.enabled)
|
||||||
|
.id(idx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 8)
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
if let last = debugLog.entries.indices.last {
|
||||||
|
proxy.scrollTo(last, anchor: .bottom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("Debug Log")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .cancellationAction) {
|
||||||
|
Button("Done") { showDebugLog = false }
|
||||||
|
}
|
||||||
|
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||||
|
Button {
|
||||||
|
UIPasteboard.general.string = debugLog.allText
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "doc.on.doc")
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
debugLog.clear()
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "trash")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Snack Overlay
|
// MARK: - Snack Overlay
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
|
|
@ -333,7 +657,16 @@ struct ScanView: View {
|
||||||
private func loadServicePoints() {
|
private func loadServicePoints() {
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
servicePoints = try await Api.shared.listServicePoints(businessId: businessId)
|
// Load namespace (UUID + Major) and service points in parallel
|
||||||
|
async let nsTask = Api.shared.allocateBusinessNamespace(businessId: businessId)
|
||||||
|
async let spTask = Api.shared.listServicePoints(businessId: businessId)
|
||||||
|
|
||||||
|
namespace = try await nsTask
|
||||||
|
servicePoints = try await spTask
|
||||||
|
|
||||||
|
DebugLog.shared.log("[ScanView] Loaded namespace: uuid=\(namespace?.uuid ?? "nil") major=\(namespace?.major ?? 0)")
|
||||||
|
DebugLog.shared.log("[ScanView] Loaded \(servicePoints.count) service points")
|
||||||
|
|
||||||
// Find next table number
|
// Find next table number
|
||||||
let maxNumber = servicePoints.compactMap { sp -> Int? in
|
let maxNumber = servicePoints.compactMap { sp -> Int? in
|
||||||
guard let match = sp.name.range(of: #"Table\s+(\d+)"#,
|
guard let match = sp.name.range(of: #"Table\s+(\d+)"#,
|
||||||
|
|
@ -345,7 +678,7 @@ struct ScanView: View {
|
||||||
}.max() ?? 0
|
}.max() ?? 0
|
||||||
nextTableNumber = maxNumber + 1
|
nextTableNumber = maxNumber + 1
|
||||||
} catch {
|
} catch {
|
||||||
// Silently continue
|
DebugLog.shared.log("[ScanView] loadServicePoints error: \(error)")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-start scan
|
// Auto-start scan
|
||||||
|
|
@ -361,10 +694,103 @@ struct ScanView: View {
|
||||||
bleScanner.startScanning()
|
bleScanner.startScanning()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func selectBeacon(_ beacon: DiscoveredBeacon) {
|
private func checkConfig(_ beacon: DiscoveredBeacon) {
|
||||||
selectedBeacon = beacon
|
isCheckingConfig = true
|
||||||
assignName = "Table \(nextTableNumber)"
|
checkConfigError = nil
|
||||||
showAssignSheet = true
|
checkConfigData = nil
|
||||||
|
showCheckConfigSheet = true
|
||||||
|
|
||||||
|
// Stop scanning to avoid BLE interference
|
||||||
|
bleScanner.stopScanning()
|
||||||
|
|
||||||
|
provisioner.readConfig(beacon: beacon) { data, error in
|
||||||
|
Task { @MainActor in
|
||||||
|
isCheckingConfig = false
|
||||||
|
if let data = data {
|
||||||
|
checkConfigData = data
|
||||||
|
}
|
||||||
|
if let error = error {
|
||||||
|
checkConfigError = error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func reprovisionBeacon() {
|
||||||
|
guard let beacon = selectedBeacon, let sp = reprovisionServicePoint else { return }
|
||||||
|
|
||||||
|
guard let ns = namespace else {
|
||||||
|
failProvisioning("Namespace not loaded — go back and try again")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop scanning
|
||||||
|
bleScanner.stopScanning()
|
||||||
|
|
||||||
|
isProvisioning = true
|
||||||
|
provisioningProgress = "Preparing..."
|
||||||
|
provisioningError = nil
|
||||||
|
showAssignSheet = true // Reuse assign sheet to show progress
|
||||||
|
assignName = sp.name
|
||||||
|
|
||||||
|
DebugLog.shared.log("[ScanView] reprovisionBeacon: sp=\(sp.name) spId=\(sp.servicePointId) minor=\(String(describing: sp.beaconMinor)) beacon=\(beacon.displayName)")
|
||||||
|
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
// If SP has no minor, re-fetch to get it
|
||||||
|
var minor = sp.beaconMinor
|
||||||
|
if minor == nil {
|
||||||
|
DebugLog.shared.log("[ScanView] reprovisionBeacon: SP has no minor, re-fetching...")
|
||||||
|
let refreshed = try await Api.shared.listServicePoints(businessId: businessId)
|
||||||
|
minor = refreshed.first(where: { $0.servicePointId == sp.servicePointId })?.beaconMinor
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let beaconMinor = minor else {
|
||||||
|
failProvisioning("Service point has no beacon minor assigned")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build config from namespace + service point (uuidClean for BLE)
|
||||||
|
let deviceName = "PF-\(sp.name)"
|
||||||
|
let beaconConfig = BeaconConfig(
|
||||||
|
uuid: ns.uuidClean,
|
||||||
|
major: ns.major,
|
||||||
|
minor: beaconMinor,
|
||||||
|
txPower: -59,
|
||||||
|
interval: 350,
|
||||||
|
deviceName: deviceName
|
||||||
|
)
|
||||||
|
|
||||||
|
DebugLog.shared.log("[ScanView] reprovisionBeacon: BLE uuid=\(ns.uuidClean) API uuid=\(ns.uuid) major=\(ns.major) minor=\(beaconMinor)")
|
||||||
|
provisioningProgress = "Provisioning beacon..."
|
||||||
|
|
||||||
|
let hardwareId = beacon.id.uuidString // BLE peripheral identifier
|
||||||
|
provisioner.provision(beacon: beacon, config: beaconConfig) { result in
|
||||||
|
Task { @MainActor in
|
||||||
|
switch result {
|
||||||
|
case .success:
|
||||||
|
do {
|
||||||
|
try await Api.shared.registerBeaconHardware(
|
||||||
|
businessId: businessId,
|
||||||
|
servicePointId: sp.servicePointId,
|
||||||
|
uuid: ns.uuid,
|
||||||
|
major: ns.major,
|
||||||
|
minor: beaconMinor,
|
||||||
|
hardwareId: hardwareId
|
||||||
|
)
|
||||||
|
finishProvisioning(name: sp.name)
|
||||||
|
} catch {
|
||||||
|
failProvisioning(error.localizedDescription)
|
||||||
|
}
|
||||||
|
case .failure(let error):
|
||||||
|
failProvisioning(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
failProvisioning(error.localizedDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func saveBeacon() {
|
private func saveBeacon() {
|
||||||
|
|
@ -372,65 +798,78 @@ struct ScanView: View {
|
||||||
let name = assignName.trimmingCharacters(in: .whitespaces)
|
let name = assignName.trimmingCharacters(in: .whitespaces)
|
||||||
guard !name.isEmpty else { return }
|
guard !name.isEmpty else { return }
|
||||||
|
|
||||||
|
guard let ns = namespace else {
|
||||||
|
failProvisioning("Namespace not loaded — go back and try again")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
isProvisioning = true
|
isProvisioning = true
|
||||||
provisioningProgress = "Creating service point..."
|
provisioningProgress = "Preparing..."
|
||||||
|
provisioningError = nil
|
||||||
|
|
||||||
|
// Stop scanning to avoid BLE interference
|
||||||
|
bleScanner.stopScanning()
|
||||||
|
|
||||||
|
DebugLog.shared.log("[ScanView] saveBeacon: name=\(name) beacon=\(beacon.displayName) businessId=\(businessId)")
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
// 1. Create or get service point
|
// 1. Reuse existing service point if name matches, otherwise create new
|
||||||
let servicePoint = try await Api.shared.createServicePoint(businessId: businessId, name: name)
|
var servicePoint: ServicePoint
|
||||||
provisioningProgress = "Getting beacon config..."
|
if let existing = servicePoints.first(where: { $0.name.caseInsensitiveCompare(name) == .orderedSame }) {
|
||||||
|
DebugLog.shared.log("[ScanView] saveBeacon: found existing SP id=\(existing.servicePointId) minor=\(String(describing: existing.beaconMinor))")
|
||||||
|
servicePoint = existing
|
||||||
|
} else {
|
||||||
|
provisioningProgress = "Creating service point..."
|
||||||
|
DebugLog.shared.log("[ScanView] saveBeacon: creating new SP...")
|
||||||
|
servicePoint = try await Api.shared.createServicePoint(businessId: businessId, name: name)
|
||||||
|
DebugLog.shared.log("[ScanView] saveBeacon: created SP id=\(servicePoint.servicePointId) minor=\(String(describing: servicePoint.beaconMinor))")
|
||||||
|
}
|
||||||
|
|
||||||
// 2. Get beacon config from backend
|
// If SP has no minor yet, re-fetch service points to get the allocated minor
|
||||||
let config = try await Api.shared.getBeaconConfig(businessId: businessId, servicePointId: servicePoint.servicePointId)
|
if servicePoint.beaconMinor == nil {
|
||||||
|
DebugLog.shared.log("[ScanView] saveBeacon: SP has no minor, re-fetching service points...")
|
||||||
|
let refreshed = try await Api.shared.listServicePoints(businessId: businessId)
|
||||||
|
if let updated = refreshed.first(where: { $0.servicePointId == servicePoint.servicePointId }) {
|
||||||
|
servicePoint = updated
|
||||||
|
DebugLog.shared.log("[ScanView] saveBeacon: refreshed SP minor=\(String(describing: servicePoint.beaconMinor))")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let minor = servicePoint.beaconMinor else {
|
||||||
|
failProvisioning("Service point has no beacon minor assigned")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Build config from namespace + service point (uuidClean = no dashes, for BLE)
|
||||||
|
let deviceName = "PF-\(name)"
|
||||||
|
let beaconConfig = BeaconConfig(
|
||||||
|
uuid: ns.uuidClean,
|
||||||
|
major: ns.major,
|
||||||
|
minor: minor,
|
||||||
|
txPower: -59,
|
||||||
|
interval: 350,
|
||||||
|
deviceName: deviceName
|
||||||
|
)
|
||||||
|
|
||||||
|
DebugLog.shared.log("[ScanView] saveBeacon: BLE uuid=\(ns.uuidClean) API uuid=\(ns.uuid) major=\(ns.major) minor=\(minor)")
|
||||||
provisioningProgress = "Provisioning beacon..."
|
provisioningProgress = "Provisioning beacon..."
|
||||||
|
|
||||||
// 3. Provision the beacon
|
// 3. Provision the beacon via GATT
|
||||||
let beaconConfig = BeaconConfig(
|
let hardwareId = beacon.id.uuidString // BLE peripheral identifier
|
||||||
uuid: config.uuid,
|
|
||||||
major: config.major,
|
|
||||||
minor: config.minor,
|
|
||||||
txPower: Int8(config.txPower),
|
|
||||||
interval: UInt16(config.interval)
|
|
||||||
)
|
|
||||||
|
|
||||||
if beacon.type == .kbeacon {
|
|
||||||
// Copy config to clipboard for KBeacon app
|
|
||||||
let clipboardText = """
|
|
||||||
UUID: \(formatUuidWithDashes(config.uuid))
|
|
||||||
Major: \(config.major)
|
|
||||||
Minor: \(config.minor)
|
|
||||||
TxPower: \(config.txPower)
|
|
||||||
"""
|
|
||||||
UIPasteboard.general.string = clipboardText
|
|
||||||
showSnack("Config copied! Use KBeacon app to program.")
|
|
||||||
|
|
||||||
// Register in backend
|
|
||||||
try await Api.shared.registerBeaconHardware(
|
|
||||||
businessId: businessId,
|
|
||||||
servicePointId: servicePoint.servicePointId,
|
|
||||||
uuid: config.uuid,
|
|
||||||
major: config.major,
|
|
||||||
minor: config.minor,
|
|
||||||
macAddress: nil
|
|
||||||
)
|
|
||||||
|
|
||||||
finishProvisioning(name: name)
|
|
||||||
} else {
|
|
||||||
// BlueCharm - provision directly via GATT
|
|
||||||
provisioner.provision(beacon: beacon, config: beaconConfig) { result in
|
provisioner.provision(beacon: beacon, config: beaconConfig) { result in
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
switch result {
|
switch result {
|
||||||
case .success:
|
case .success:
|
||||||
// Register in backend
|
// Register in backend (use original UUID from API, not cleaned)
|
||||||
do {
|
do {
|
||||||
try await Api.shared.registerBeaconHardware(
|
try await Api.shared.registerBeaconHardware(
|
||||||
businessId: businessId,
|
businessId: businessId,
|
||||||
servicePointId: servicePoint.servicePointId,
|
servicePointId: servicePoint.servicePointId,
|
||||||
uuid: config.uuid,
|
uuid: ns.uuid,
|
||||||
major: config.major,
|
major: ns.major,
|
||||||
minor: config.minor,
|
minor: minor,
|
||||||
macAddress: nil
|
hardwareId: hardwareId
|
||||||
)
|
)
|
||||||
finishProvisioning(name: name)
|
finishProvisioning(name: name)
|
||||||
} catch {
|
} catch {
|
||||||
|
|
@ -441,7 +880,6 @@ struct ScanView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
failProvisioning(error.localizedDescription)
|
failProvisioning(error.localizedDescription)
|
||||||
}
|
}
|
||||||
|
|
@ -476,9 +914,10 @@ struct ScanView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func failProvisioning(_ error: String) {
|
private func failProvisioning(_ error: String) {
|
||||||
|
DebugLog.shared.log("[ScanView] Provisioning failed: \(error)")
|
||||||
isProvisioning = false
|
isProvisioning = false
|
||||||
provisioningProgress = ""
|
provisioningProgress = ""
|
||||||
showSnack("Error: \(error)")
|
provisioningError = error
|
||||||
}
|
}
|
||||||
|
|
||||||
private func formatUuidWithDashes(_ raw: String) -> String {
|
private func formatUuidWithDashes(_ raw: String) -> String {
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,13 @@ struct ServicePointListView: View {
|
||||||
@State private var newServicePointName = ""
|
@State private var newServicePointName = ""
|
||||||
@State private var isAdding = false
|
@State private var isAdding = false
|
||||||
|
|
||||||
|
// Beacon scan
|
||||||
|
@State private var showScanView = false
|
||||||
|
|
||||||
|
// Re-provision existing service point
|
||||||
|
@State private var tappedServicePoint: ServicePoint?
|
||||||
|
@State private var reprovisionServicePoint: ServicePoint?
|
||||||
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
|
|
@ -94,14 +101,22 @@ struct ServicePointListView: View {
|
||||||
.padding(.vertical, 20)
|
.padding(.vertical, 20)
|
||||||
} else {
|
} else {
|
||||||
ForEach(servicePoints) { sp in
|
ForEach(servicePoints) { sp in
|
||||||
|
Button {
|
||||||
|
tappedServicePoint = sp
|
||||||
|
} label: {
|
||||||
HStack {
|
HStack {
|
||||||
Text(sp.name)
|
Text(sp.name)
|
||||||
|
.foregroundColor(.primary)
|
||||||
Spacer()
|
Spacer()
|
||||||
if let minor = sp.beaconMinor {
|
if let minor = sp.beaconMinor {
|
||||||
Text("Minor: \(minor)")
|
Text("Minor: \(minor)")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
|
Image(systemName: "chevron.right")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -117,7 +132,12 @@ struct ServicePointListView: View {
|
||||||
ToolbarItem(placement: .navigationBarLeading) {
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
Button("Back", action: onBack)
|
Button("Back", action: onBack)
|
||||||
}
|
}
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||||
|
Button {
|
||||||
|
showScanView = true
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "antenna.radiowaves.left.and.right")
|
||||||
|
}
|
||||||
Button {
|
Button {
|
||||||
showAddSheet = true
|
showAddSheet = true
|
||||||
} label: {
|
} label: {
|
||||||
|
|
@ -128,6 +148,49 @@ struct ServicePointListView: View {
|
||||||
}
|
}
|
||||||
.onAppear { loadData() }
|
.onAppear { loadData() }
|
||||||
.sheet(isPresented: $showAddSheet) { addServicePointSheet }
|
.sheet(isPresented: $showAddSheet) { addServicePointSheet }
|
||||||
|
.fullScreenCover(isPresented: $showScanView) {
|
||||||
|
ScanView(
|
||||||
|
businessId: businessId,
|
||||||
|
businessName: businessName,
|
||||||
|
onBack: {
|
||||||
|
showScanView = false
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.fullScreenCover(item: $reprovisionServicePoint) { sp in
|
||||||
|
ScanView(
|
||||||
|
businessId: businessId,
|
||||||
|
businessName: businessName,
|
||||||
|
reprovisionServicePoint: sp,
|
||||||
|
onBack: {
|
||||||
|
reprovisionServicePoint = nil
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.confirmationDialog(
|
||||||
|
tappedServicePoint?.name ?? "Service Point",
|
||||||
|
isPresented: Binding(
|
||||||
|
get: { tappedServicePoint != nil },
|
||||||
|
set: { if !$0 { tappedServicePoint = nil } }
|
||||||
|
),
|
||||||
|
titleVisibility: .visible
|
||||||
|
) {
|
||||||
|
Button("Re-provision Beacon") {
|
||||||
|
reprovisionServicePoint = tappedServicePoint
|
||||||
|
tappedServicePoint = nil
|
||||||
|
}
|
||||||
|
Button("Delete", role: .destructive) {
|
||||||
|
if let sp = tappedServicePoint {
|
||||||
|
deleteServicePoint(sp)
|
||||||
|
}
|
||||||
|
tappedServicePoint = nil
|
||||||
|
}
|
||||||
|
Button("Cancel", role: .cancel) {
|
||||||
|
tappedServicePoint = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Add Sheet
|
// MARK: - Add Sheet
|
||||||
|
|
@ -227,6 +290,17 @@ struct ServicePointListView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func deleteServicePoint(_ sp: ServicePoint) {
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
try await Api.shared.deleteServicePoint(businessId: businessId, servicePointId: sp.servicePointId)
|
||||||
|
servicePoints.removeAll { $0.servicePointId == sp.servicePointId }
|
||||||
|
} catch {
|
||||||
|
errorMessage = error.localizedDescription
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func addServicePoint() {
|
private func addServicePoint() {
|
||||||
let name = newServicePointName.trimmingCharacters(in: .whitespaces)
|
let name = newServicePointName.trimmingCharacters(in: .whitespaces)
|
||||||
guard !name.isEmpty else { return }
|
guard !name.isEmpty else { return }
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue