spotny-sdk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/README.md +589 -0
- package/SpotnySdk.podspec +35 -0
- package/android/build.gradle +70 -0
- package/android/src/main/AndroidManifest.xml +33 -0
- package/android/src/main/java/com/spotnysdk/SpotnySdkModule.kt +685 -0
- package/android/src/main/java/com/spotnysdk/SpotnySdkPackage.kt +31 -0
- package/ios/Frameworks/CBORCoding.xcframework/Info.plist +44 -0
- package/ios/Frameworks/CBORCoding.xcframework/ios-arm64/CBORCoding.framework/CBORCoding +0 -0
- package/ios/Frameworks/CBORCoding.xcframework/ios-arm64/CBORCoding.framework/Headers/CBORCoding-Swift.h +317 -0
- package/ios/Frameworks/CBORCoding.xcframework/ios-arm64/CBORCoding.framework/Info.plist +0 -0
- package/ios/Frameworks/CBORCoding.xcframework/ios-arm64/CBORCoding.framework/Modules/CBORCoding.swiftmodule/arm64-apple-ios.abi.json +7021 -0
- package/ios/Frameworks/CBORCoding.xcframework/ios-arm64/CBORCoding.framework/Modules/CBORCoding.swiftmodule/arm64-apple-ios.private.swiftinterface +193 -0
- package/ios/Frameworks/CBORCoding.xcframework/ios-arm64/CBORCoding.framework/Modules/CBORCoding.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
- package/ios/Frameworks/CBORCoding.xcframework/ios-arm64/CBORCoding.framework/Modules/CBORCoding.swiftmodule/arm64-apple-ios.swiftinterface +193 -0
- package/ios/Frameworks/CBORCoding.xcframework/ios-arm64/CBORCoding.framework/Modules/module.modulemap +4 -0
- package/ios/Frameworks/CBORCoding.xcframework/ios-arm64_x86_64-simulator/CBORCoding.framework/CBORCoding +0 -0
- package/ios/Frameworks/CBORCoding.xcframework/ios-arm64_x86_64-simulator/CBORCoding.framework/Headers/CBORCoding-Swift.h +630 -0
- package/ios/Frameworks/CBORCoding.xcframework/ios-arm64_x86_64-simulator/CBORCoding.framework/Info.plist +0 -0
- package/ios/Frameworks/CBORCoding.xcframework/ios-arm64_x86_64-simulator/CBORCoding.framework/Modules/CBORCoding.swiftmodule/arm64-apple-ios-simulator.abi.json +7021 -0
- package/ios/Frameworks/CBORCoding.xcframework/ios-arm64_x86_64-simulator/CBORCoding.framework/Modules/CBORCoding.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +193 -0
- package/ios/Frameworks/CBORCoding.xcframework/ios-arm64_x86_64-simulator/CBORCoding.framework/Modules/CBORCoding.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
- package/ios/Frameworks/CBORCoding.xcframework/ios-arm64_x86_64-simulator/CBORCoding.framework/Modules/CBORCoding.swiftmodule/arm64-apple-ios-simulator.swiftinterface +193 -0
- package/ios/Frameworks/CBORCoding.xcframework/ios-arm64_x86_64-simulator/CBORCoding.framework/Modules/CBORCoding.swiftmodule/x86_64-apple-ios-simulator.abi.json +7021 -0
- package/ios/Frameworks/CBORCoding.xcframework/ios-arm64_x86_64-simulator/CBORCoding.framework/Modules/CBORCoding.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +193 -0
- package/ios/Frameworks/CBORCoding.xcframework/ios-arm64_x86_64-simulator/CBORCoding.framework/Modules/CBORCoding.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
- package/ios/Frameworks/CBORCoding.xcframework/ios-arm64_x86_64-simulator/CBORCoding.framework/Modules/CBORCoding.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +193 -0
- package/ios/Frameworks/CBORCoding.xcframework/ios-arm64_x86_64-simulator/CBORCoding.framework/Modules/module.modulemap +4 -0
- package/ios/Frameworks/CBORCoding.xcframework/ios-arm64_x86_64-simulator/CBORCoding.framework/_CodeSignature/CodeResources +267 -0
- package/ios/Frameworks/Half.xcframework/Info.plist +44 -0
- package/ios/Frameworks/Half.xcframework/ios-arm64/Half.framework/Half +0 -0
- package/ios/Frameworks/Half.xcframework/ios-arm64/Half.framework/Headers/Half-Swift.h +317 -0
- package/ios/Frameworks/Half.xcframework/ios-arm64/Half.framework/Headers/half.h +95 -0
- package/ios/Frameworks/Half.xcframework/ios-arm64/Half.framework/Info.plist +0 -0
- package/ios/Frameworks/Half.xcframework/ios-arm64/Half.framework/Modules/Half.swiftmodule/arm64-apple-ios.abi.json +4942 -0
- package/ios/Frameworks/Half.xcframework/ios-arm64/Half.framework/Modules/Half.swiftmodule/arm64-apple-ios.private.swiftinterface +650 -0
- package/ios/Frameworks/Half.xcframework/ios-arm64/Half.framework/Modules/Half.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
- package/ios/Frameworks/Half.xcframework/ios-arm64/Half.framework/Modules/Half.swiftmodule/arm64-apple-ios.swiftinterface +650 -0
- package/ios/Frameworks/Half.xcframework/ios-arm64/Half.framework/Modules/module.modulemap +11 -0
- package/ios/Frameworks/Half.xcframework/ios-arm64_x86_64-simulator/Half.framework/Half +0 -0
- package/ios/Frameworks/Half.xcframework/ios-arm64_x86_64-simulator/Half.framework/Headers/Half-Swift.h +630 -0
- package/ios/Frameworks/Half.xcframework/ios-arm64_x86_64-simulator/Half.framework/Headers/half.h +95 -0
- package/ios/Frameworks/Half.xcframework/ios-arm64_x86_64-simulator/Half.framework/Info.plist +0 -0
- package/ios/Frameworks/Half.xcframework/ios-arm64_x86_64-simulator/Half.framework/Modules/Half.swiftmodule/arm64-apple-ios-simulator.abi.json +4942 -0
- package/ios/Frameworks/Half.xcframework/ios-arm64_x86_64-simulator/Half.framework/Modules/Half.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +650 -0
- package/ios/Frameworks/Half.xcframework/ios-arm64_x86_64-simulator/Half.framework/Modules/Half.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
- package/ios/Frameworks/Half.xcframework/ios-arm64_x86_64-simulator/Half.framework/Modules/Half.swiftmodule/arm64-apple-ios-simulator.swiftinterface +650 -0
- package/ios/Frameworks/Half.xcframework/ios-arm64_x86_64-simulator/Half.framework/Modules/Half.swiftmodule/x86_64-apple-ios-simulator.abi.json +4973 -0
- package/ios/Frameworks/Half.xcframework/ios-arm64_x86_64-simulator/Half.framework/Modules/Half.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +660 -0
- package/ios/Frameworks/Half.xcframework/ios-arm64_x86_64-simulator/Half.framework/Modules/Half.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
- package/ios/Frameworks/Half.xcframework/ios-arm64_x86_64-simulator/Half.framework/Modules/Half.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +660 -0
- package/ios/Frameworks/Half.xcframework/ios-arm64_x86_64-simulator/Half.framework/Modules/module.modulemap +11 -0
- package/ios/Frameworks/Half.xcframework/ios-arm64_x86_64-simulator/Half.framework/_CodeSignature/CodeResources +245 -0
- package/ios/Frameworks/KontaktSDK.xcframework/Info.plist +44 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Base.lproj/Localizable.strings +0 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/CLBeacon+Kontakt.h +37 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKAction.h +90 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKActionContent.h +80 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKBeaconManager.h +326 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKBeaconRegion.h +102 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKCloudClient.h +241 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKCloudClientJSONResponseSerializer.h +20 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKCloudClientResponseSerializer.h +35 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKCloudClientSessionManager.h +207 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKCloudDefinitions.h +13 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKCloudModel.h +36 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKDevice.h +179 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKDeviceCCOperationDelegate.h +13 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKDeviceConfiguration.h +329 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKDeviceConfigurationGPIO.h +24 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKDeviceConnection.h +379 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKDeviceConnectionOperation.h +65 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKDeviceDataLoggerConnection.h +56 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKDeviceDataLoggerReading.h +49 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKDeviceDefinitions.h +853 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKDeviceGATTDefinitions.h +106 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKDeviceGATTOperation.h +68 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKDeviceGatewayConfigurationType.h +43 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKDeviceGatewayConnection.h +101 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKDeviceGatewayDiagnostic.h +61 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKDeviceGatewayWiFiNetwork.h +76 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKDeviceKontaktRecognitionBox.h +26 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKDeviceListenOperation.h +20 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKDeviceNotifyOperation.h +34 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKDevicePowerSaving.h +86 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKDeviceWriteOperation.h +51 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKDevicesManager.h +182 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKEddystone.h +101 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKEddystoneFrame.h +47 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKEddystoneManager.h +160 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKEddystoneRegion.h +85 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKEddystoneTLM.h +49 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKEddystoneUID.h +55 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKEddystoneURL.h +33 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKEddystoneURLValueTransformer.h +13 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKFirmware.h +98 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKGroupOperation.h +100 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKKontaktResponse.h +85 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKManager.h +121 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKNearbyDevice.h +135 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKNearbyDeviceTelemetry.h +513 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKOperation.h +116 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKPersonPosition.h +22 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKSecureBeaconRegion.h +86 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKSecureEddystoneRegion.h +59 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKTrigger.h +94 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKTriggerContext.h +79 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKVenue.h +105 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/Kontakt.h +187 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KontaktSDK-Swift.h +668 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KontaktSDK.h +43 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/NSData+Kontakt.h +115 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/NSError+Kontakt.h +26 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/NSString+Kontakt.h +25 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/NSURLRequest+Kontakt.h +44 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Info.plist +0 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/KontaktSDK +0 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Modules/KontaktSDK.swiftmodule/arm64-apple-ios.abi.json +14106 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Modules/KontaktSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +428 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Modules/KontaktSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Modules/KontaktSDK.swiftmodule/arm64-apple-ios.swiftinterface +428 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Modules/module.modulemap +11 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/strip-frameworks.sh +72 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Base.lproj/Localizable.strings +0 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/CLBeacon+Kontakt.h +37 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKAction.h +90 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKActionContent.h +80 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKBeaconManager.h +326 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKBeaconRegion.h +102 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKCloudClient.h +241 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKCloudClientJSONResponseSerializer.h +20 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKCloudClientResponseSerializer.h +35 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKCloudClientSessionManager.h +207 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKCloudDefinitions.h +13 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKCloudModel.h +36 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKDevice.h +179 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKDeviceCCOperationDelegate.h +13 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKDeviceConfiguration.h +329 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKDeviceConfigurationGPIO.h +24 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKDeviceConnection.h +379 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKDeviceConnectionOperation.h +65 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKDeviceDataLoggerConnection.h +56 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKDeviceDataLoggerReading.h +49 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKDeviceDefinitions.h +853 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKDeviceGATTDefinitions.h +106 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKDeviceGATTOperation.h +68 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKDeviceGatewayConfigurationType.h +43 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKDeviceGatewayConnection.h +101 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKDeviceGatewayDiagnostic.h +61 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKDeviceGatewayWiFiNetwork.h +76 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKDeviceKontaktRecognitionBox.h +26 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKDeviceListenOperation.h +20 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKDeviceNotifyOperation.h +34 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKDevicePowerSaving.h +86 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKDeviceWriteOperation.h +51 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKDevicesManager.h +182 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKEddystone.h +101 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKEddystoneFrame.h +47 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKEddystoneManager.h +160 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKEddystoneRegion.h +85 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKEddystoneTLM.h +49 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKEddystoneUID.h +55 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKEddystoneURL.h +33 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKEddystoneURLValueTransformer.h +13 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKFirmware.h +98 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKGroupOperation.h +100 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKKontaktResponse.h +85 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKManager.h +121 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKNearbyDevice.h +135 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKNearbyDeviceTelemetry.h +513 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKOperation.h +116 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKPersonPosition.h +22 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKSecureBeaconRegion.h +86 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKSecureEddystoneRegion.h +59 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKTrigger.h +94 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKTriggerContext.h +79 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKVenue.h +105 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/Kontakt.h +187 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KontaktSDK-Swift.h +1332 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KontaktSDK.h +43 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/NSData+Kontakt.h +115 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/NSError+Kontakt.h +26 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/NSString+Kontakt.h +25 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/NSURLRequest+Kontakt.h +44 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Info.plist +0 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/KontaktSDK +0 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Modules/KontaktSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +14106 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Modules/KontaktSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +428 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Modules/KontaktSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Modules/KontaktSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +428 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Modules/KontaktSDK.swiftmodule/x86_64-apple-ios-simulator.abi.json +14106 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Modules/KontaktSDK.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +428 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Modules/KontaktSDK.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Modules/KontaktSDK.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +428 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Modules/module.modulemap +11 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/_CodeSignature/CodeResources +894 -0
- package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/strip-frameworks.sh +72 -0
- package/ios/SpotnyBeaconScanner.swift +938 -0
- package/ios/SpotnySdk.h +10 -0
- package/ios/SpotnySdk.mm +127 -0
- package/lib/module/NativeSpotnySdk.js +5 -0
- package/lib/module/NativeSpotnySdk.js.map +1 -0
- package/lib/module/index.js +96 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativeSpotnySdk.d.ts +18 -0
- package/lib/typescript/src/NativeSpotnySdk.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +81 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/package.json +169 -0
- package/src/NativeSpotnySdk.ts +29 -0
- package/src/index.tsx +151 -0
|
@@ -0,0 +1,938 @@
|
|
|
1
|
+
//
|
|
2
|
+
// SpotnyBeaconScanner.swift
|
|
3
|
+
// SpotnySdk
|
|
4
|
+
//
|
|
5
|
+
// Core iBeacon scanning logic using Kontakt.io SDK.
|
|
6
|
+
// Handles campaign fetching and proximity / impression tracking against
|
|
7
|
+
// the Spotny backend.
|
|
8
|
+
//
|
|
9
|
+
|
|
10
|
+
import Foundation
|
|
11
|
+
import CoreLocation
|
|
12
|
+
import KontaktSDK
|
|
13
|
+
import UserNotifications
|
|
14
|
+
import Security
|
|
15
|
+
|
|
16
|
+
// MARK: - Public ObjC-visible typealias for the event callback block
|
|
17
|
+
|
|
18
|
+
public typealias SpotnyEventCallback = @convention(block) (_ name: String, _ body: [String: Any]) -> Void
|
|
19
|
+
|
|
20
|
+
// MARK: - Internal data structures
|
|
21
|
+
|
|
22
|
+
private struct CampaignData {
|
|
23
|
+
let campaignId: Int? // nil when no active campaign
|
|
24
|
+
let screenId: Int
|
|
25
|
+
let sessionId: String? // set after first proximity response
|
|
26
|
+
let inQueue: Bool // campaign is queued — skip impressions
|
|
27
|
+
let major: Int
|
|
28
|
+
let minor: Int
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// MARK: - SpotnyBeaconScanner
|
|
32
|
+
|
|
33
|
+
@objc(SpotnyBeaconScanner)
|
|
34
|
+
public class SpotnyBeaconScanner: NSObject {
|
|
35
|
+
|
|
36
|
+
// ── Callback to send events back to SpotnySdk.mm ─────────────────────────
|
|
37
|
+
private var eventCallback: SpotnyEventCallback?
|
|
38
|
+
|
|
39
|
+
// ── Managers ──────────────────────────────────────────────────────────────
|
|
40
|
+
private var beaconManager: KTKBeaconManager!
|
|
41
|
+
private var locationManager: CLLocationManager!
|
|
42
|
+
|
|
43
|
+
// ── Session state ─────────────────────────────────────────────────────────
|
|
44
|
+
private var scanning: Bool = false
|
|
45
|
+
/// Authenticated user ID — required at initialize(). Embedded in the JWT via /api/app/sdk/verify.
|
|
46
|
+
private var userId: String?
|
|
47
|
+
|
|
48
|
+
// ── JS event deduplication ─────────────────────────────────────────
|
|
49
|
+
/// Signature of the last emitted `onBeaconsRanged` payload (sorted major_minor_proximity).
|
|
50
|
+
/// Event is only emitted when this changes, or when debounceInterval has elapsed.
|
|
51
|
+
private var lastRangedSignature: String = ""
|
|
52
|
+
private var lastRangedEmit: Date = .distantPast
|
|
53
|
+
|
|
54
|
+
// ── Configuration ────────────────────────────────────────────────────────────
|
|
55
|
+
// backendURL and kontaktAPIKey are fixed — not overridable by consumers.
|
|
56
|
+
private let backendURL: String = "https://api.spotny.app"
|
|
57
|
+
private let kontaktAPIKey: String = "mgrz08TOKNHafeY02cWIs9mxUHbynNQJ"
|
|
58
|
+
private var maxDetectionDistance: Double = 8.0
|
|
59
|
+
/// Multiplier applied to raw RSSI-derived distance to compensate for low TX power.
|
|
60
|
+
/// Overridable via configure(). Default 0.5 matches Kontakt.io -12 dBm beacons.
|
|
61
|
+
private var distanceCorrectionFactor: Double = 0.5
|
|
62
|
+
/// API key identifying the SDK consumer (e.g. "nike"). Set during initialize().
|
|
63
|
+
private var apiKey: String?
|
|
64
|
+
/// JWT returned by /api/app/sdk/verify. Injected as Authorization header on all subsequent calls.
|
|
65
|
+
private var sdkToken: String?
|
|
66
|
+
/// Expiry date of the current JWT. When exceeded, a new JWT is automatically fetched before every call.
|
|
67
|
+
private var sdkTokenExpiry: Date?
|
|
68
|
+
/// Original SDK token — persisted so JWT can be refreshed without calling initialize() again.
|
|
69
|
+
private var sdkCredential: String?
|
|
70
|
+
// JWT refresh coordination — queues concurrent callers while a single refresh is in-flight
|
|
71
|
+
private var jwtRefreshInProgress = false
|
|
72
|
+
private var jwtRefreshQueue: [(Error?) -> Void] = []
|
|
73
|
+
|
|
74
|
+
// ── Session TTL ────────────────────────────────────────────────────────────
|
|
75
|
+
/// Sessions older than this when the app resumes are considered stale (crash / force-quit).
|
|
76
|
+
private let sessionTTL: TimeInterval = 24 * 3600 // 24 hours
|
|
77
|
+
private var lastSessionHeartbeat: Date = .distantPast
|
|
78
|
+
|
|
79
|
+
// ── Beacon UUID (standard Kontakt.io default) ─────────────────────────────
|
|
80
|
+
private let beaconUUID = UUID(uuidString: "f7826da6-4fa2-4e98-8024-bc5b71e0893e")!
|
|
81
|
+
|
|
82
|
+
// ── Timing constants ──────────────────────────────────────────────────────
|
|
83
|
+
private let campaignFetchCooldown: TimeInterval = 5.0
|
|
84
|
+
private let proximityDistanceThreshold: Double = 0.75
|
|
85
|
+
private let impressionEventInterval: TimeInterval = 10.0
|
|
86
|
+
private let impressionDistance: Double = 2.0
|
|
87
|
+
private var debounceInterval: TimeInterval = 5.0
|
|
88
|
+
|
|
89
|
+
// ── Per-beacon tracking state ──────────────────────────────────────────────
|
|
90
|
+
private var activeCampaigns: [String: CampaignData] = [:]
|
|
91
|
+
private var lastProximityEventSent: [String: Date] = [:]
|
|
92
|
+
private var lastProximityDistance: [String: Double] = [:]
|
|
93
|
+
private var lastImpressionEventSent: [String: Date] = [:]
|
|
94
|
+
private var lastCampaignFetchAttempt: [String: Date] = [:]
|
|
95
|
+
private var fetchInProgress: [String: Bool] = [:]
|
|
96
|
+
private var fetchRetryCount: [String: Int] = [:]
|
|
97
|
+
private var proximityEventInProgress: [String: Bool] = [:]
|
|
98
|
+
private var impressionEventInProgress: [String: Bool] = [:]
|
|
99
|
+
|
|
100
|
+
// MARK: - Init
|
|
101
|
+
|
|
102
|
+
@objc
|
|
103
|
+
public init(eventCallback: @escaping SpotnyEventCallback) {
|
|
104
|
+
self.eventCallback = eventCallback
|
|
105
|
+
super.init()
|
|
106
|
+
setupManagers()
|
|
107
|
+
resumeStoredSession()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// MARK: - Setup
|
|
111
|
+
|
|
112
|
+
private func setupManagers() {
|
|
113
|
+
// Initialise the Kontakt.io SDK with the API key before creating any manager
|
|
114
|
+
Kontakt.setAPIKey(kontaktAPIKey)
|
|
115
|
+
print("✅ SpotnySDK: Kontakt.io SDK initialised")
|
|
116
|
+
|
|
117
|
+
locationManager = CLLocationManager()
|
|
118
|
+
locationManager.delegate = self
|
|
119
|
+
// allowsBackgroundLocationUpdates = true requires the app to declare the
|
|
120
|
+
// "location" UIBackgroundModes entry. Setting it without that entitlement
|
|
121
|
+
// raises NSInternalInconsistencyException and terminates the app.
|
|
122
|
+
if let modes = Bundle.main.object(forInfoDictionaryKey: "UIBackgroundModes") as? [String],
|
|
123
|
+
modes.contains("location") {
|
|
124
|
+
locationManager.allowsBackgroundLocationUpdates = true
|
|
125
|
+
locationManager.pausesLocationUpdatesAutomatically = false
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
beaconManager = KTKBeaconManager(delegate: self)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private func resumeStoredSession() {
|
|
132
|
+
// Resume scanning only if a valid session was previously active
|
|
133
|
+
guard let ts = UserDefaults.standard.object(forKey: "SpotnySDK_sessionTimestamp") as? Double else { return }
|
|
134
|
+
let age = Date().timeIntervalSince1970 - ts
|
|
135
|
+
if age > sessionTTL {
|
|
136
|
+
print("⚠️ SpotnySDK: Stale session (\(Int(age / 3600))h old) — discarding")
|
|
137
|
+
clearStoredSession()
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
// Restore persisted JWT + credentials so all API calls work without initialize()
|
|
141
|
+
sdkToken = keychainRead(key: "SpotnySDK_jwt")
|
|
142
|
+
sdkCredential = keychainRead(key: "SpotnySDK_sdkCredential")
|
|
143
|
+
apiKey = keychainRead(key: "SpotnySDK_apiKey")
|
|
144
|
+
userId = keychainRead(key: "SpotnySDK_userId")
|
|
145
|
+
if let expiryStr = keychainRead(key: "SpotnySDK_jwtExpiry"), let ts = Double(expiryStr) {
|
|
146
|
+
sdkTokenExpiry = Date(timeIntervalSince1970: ts)
|
|
147
|
+
}
|
|
148
|
+
print("🔄 SpotnySDK: Resuming session (device: \(getDeviceId()))")
|
|
149
|
+
startPersistentScanning()
|
|
150
|
+
scanning = true
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private func clearStoredSession() {
|
|
154
|
+
// Only clears the scanning session — identity (Keychain) is intentionally kept
|
|
155
|
+
UserDefaults.standard.removeObject(forKey: "SpotnySDK_sessionTimestamp")
|
|
156
|
+
UserDefaults.standard.synchronize()
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// MARK: - ObjC-Exposed Methods (called from SpotnySdk.mm)
|
|
160
|
+
|
|
161
|
+
@objc
|
|
162
|
+
public func startScanner(
|
|
163
|
+
withResolve resolve: @escaping (Any?) -> Void,
|
|
164
|
+
reject: @escaping (String?, String?, Error?) -> Void
|
|
165
|
+
) {
|
|
166
|
+
if scanning {
|
|
167
|
+
resolve("Already scanning")
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
UserDefaults.standard.set(Date().timeIntervalSince1970, forKey: "SpotnySDK_sessionTimestamp")
|
|
172
|
+
UserDefaults.standard.synchronize()
|
|
173
|
+
|
|
174
|
+
let status = locationManager.authorizationStatus
|
|
175
|
+
if status == .notDetermined {
|
|
176
|
+
locationManager.requestAlwaysAuthorization()
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
startPersistentScanning()
|
|
180
|
+
scanning = true
|
|
181
|
+
print("✅ SpotnySDK: Started scanning (device: \(getDeviceId()))")
|
|
182
|
+
resolve("Scanning started")
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
@objc
|
|
186
|
+
public func stopScanner(
|
|
187
|
+
withResolve resolve: @escaping (Any?) -> Void,
|
|
188
|
+
reject: @escaping (String?, String?, Error?) -> Void
|
|
189
|
+
) {
|
|
190
|
+
beaconManager.stopMonitoringForAllRegions()
|
|
191
|
+
beaconManager.stopRangingBeaconsInAllRegions()
|
|
192
|
+
cleanupAllProximityState()
|
|
193
|
+
|
|
194
|
+
clearStoredSession()
|
|
195
|
+
|
|
196
|
+
scanning = false
|
|
197
|
+
|
|
198
|
+
print("⏹️ SpotnySDK: Stopped scanning")
|
|
199
|
+
resolve("Scanning stopped")
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
@objc
|
|
203
|
+
public func isScanning(
|
|
204
|
+
withResolve resolve: @escaping (Any?) -> Void,
|
|
205
|
+
reject: @escaping (String?, String?, Error?) -> Void
|
|
206
|
+
) {
|
|
207
|
+
resolve(scanning)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
@objc
|
|
211
|
+
public func initialize(
|
|
212
|
+
with config: NSDictionary,
|
|
213
|
+
resolve: @escaping (Any?) -> Void,
|
|
214
|
+
reject: @escaping (String?, String?, Error?) -> Void
|
|
215
|
+
) {
|
|
216
|
+
// Apply config values
|
|
217
|
+
if let dist = config["maxDetectionDistance"] as? Double {
|
|
218
|
+
maxDetectionDistance = dist
|
|
219
|
+
print("⚙️ SpotnySDK: maxDetectionDistance = \(dist)m")
|
|
220
|
+
}
|
|
221
|
+
if let key = config["apiKey"] as? String {
|
|
222
|
+
apiKey = key
|
|
223
|
+
print("⚙️ SpotnySDK: apiKey = \(key)")
|
|
224
|
+
}
|
|
225
|
+
if let factor = config["distanceCorrectionFactor"] as? Double, factor > 0 {
|
|
226
|
+
distanceCorrectionFactor = factor
|
|
227
|
+
print("⚙️ SpotnySDK: distanceCorrectionFactor = \(factor)")
|
|
228
|
+
}
|
|
229
|
+
if let uid = config["userId"] as? String, !uid.isEmpty {
|
|
230
|
+
userId = uid
|
|
231
|
+
}
|
|
232
|
+
guard let token = config["token"] as? String, !token.isEmpty else {
|
|
233
|
+
reject("MISSING_TOKEN", "initialize() requires a token", nil)
|
|
234
|
+
return
|
|
235
|
+
}
|
|
236
|
+
guard let key = apiKey, !key.isEmpty else {
|
|
237
|
+
reject("MISSING_API_KEY", "initialize() requires an apiKey", nil)
|
|
238
|
+
return
|
|
239
|
+
}
|
|
240
|
+
guard let uid = userId, !uid.isEmpty else {
|
|
241
|
+
reject("MISSING_USER_ID", "initialize() requires a userId", nil)
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Send token + apiKey + userId to backend — response returns a JWT used for all subsequent calls
|
|
246
|
+
// sdkToken is nil here so no Authorization header is injected on this call
|
|
247
|
+
let verifyPayload: [String: Any] = ["token": token, "api_key": key, "user_id": uid]
|
|
248
|
+
post(endpoint: "/api/app/sdk/verify", payload: verifyPayload) { [weak self] result in
|
|
249
|
+
guard let self = self else { return }
|
|
250
|
+
switch result {
|
|
251
|
+
case .success(let (status, data)):
|
|
252
|
+
if 200...299 ~= status {
|
|
253
|
+
guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
254
|
+
let dataObj = json["data"] as? [String: Any],
|
|
255
|
+
let jwt = dataObj["jwt"] as? String, !jwt.isEmpty else {
|
|
256
|
+
reject("VERIFY_FAILED", "SDK verification response missing JWT", nil)
|
|
257
|
+
return
|
|
258
|
+
}
|
|
259
|
+
self.sdkToken = jwt
|
|
260
|
+
self.sdkCredential = token
|
|
261
|
+
if let ts = dataObj["expires_at"] as? Double {
|
|
262
|
+
self.sdkTokenExpiry = Date(timeIntervalSince1970: ts)
|
|
263
|
+
} else if let i = dataObj["expires_at"] as? Int {
|
|
264
|
+
self.sdkTokenExpiry = Date(timeIntervalSince1970: Double(i))
|
|
265
|
+
}
|
|
266
|
+
self.persistJWT()
|
|
267
|
+
let expiryDesc = self.sdkTokenExpiry?.description ?? "no expiry"
|
|
268
|
+
print("\u{2705} SpotnySDK: Initialized \u{2014} JWT stored, expires: \(expiryDesc)")
|
|
269
|
+
resolve("SDK initialized")
|
|
270
|
+
} else if status == 401 || status == 403 {
|
|
271
|
+
print("❌ SpotnySDK: Token unauthorized (\(status))")
|
|
272
|
+
reject("UNAUTHORIZED", "Invalid or expired SDK token", nil)
|
|
273
|
+
} else {
|
|
274
|
+
print("❌ SpotnySDK: Verification failed (\(status))")
|
|
275
|
+
reject("VERIFY_FAILED", "SDK verification returned status \(status)", nil)
|
|
276
|
+
}
|
|
277
|
+
case .failure(let error):
|
|
278
|
+
print("❌ SpotnySDK: Verification network error — \(error.localizedDescription)")
|
|
279
|
+
reject("VERIFY_ERROR", error.localizedDescription, error)
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
@objc
|
|
285
|
+
public func requestNotificationPermissions(
|
|
286
|
+
withResolve resolve: @escaping (Any?) -> Void,
|
|
287
|
+
reject: @escaping (String?, String?, Error?) -> Void
|
|
288
|
+
) {
|
|
289
|
+
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
|
|
290
|
+
DispatchQueue.main.async {
|
|
291
|
+
if let error = error {
|
|
292
|
+
reject("PERMISSION_ERROR", error.localizedDescription, error)
|
|
293
|
+
} else {
|
|
294
|
+
resolve(granted ? "granted" : "denied")
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
@objc
|
|
301
|
+
public func setDebounceInterval(
|
|
302
|
+
_ interval: Double,
|
|
303
|
+
resolve: @escaping (Any?) -> Void,
|
|
304
|
+
reject: @escaping (String?, String?, Error?) -> Void
|
|
305
|
+
) {
|
|
306
|
+
debounceInterval = interval
|
|
307
|
+
resolve("Debounce interval set to \(interval)s")
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
@objc
|
|
311
|
+
public func clearDebounceCache(
|
|
312
|
+
withResolve resolve: @escaping (Any?) -> Void,
|
|
313
|
+
reject: @escaping (String?, String?, Error?) -> Void
|
|
314
|
+
) {
|
|
315
|
+
lastCampaignFetchAttempt.removeAll()
|
|
316
|
+
fetchInProgress.removeAll()
|
|
317
|
+
fetchRetryCount.removeAll()
|
|
318
|
+
resolve("Debounce cache cleared")
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// MARK: - Debounce Status
|
|
322
|
+
|
|
323
|
+
@objc
|
|
324
|
+
public func getDebounceStatus(
|
|
325
|
+
withResolve resolve: @escaping (Any?) -> Void,
|
|
326
|
+
reject: @escaping (String?, String?, Error?) -> Void
|
|
327
|
+
) {
|
|
328
|
+
var status: [String: Any] = [:]
|
|
329
|
+
for (key, _) in activeCampaigns {
|
|
330
|
+
var entry: [String: Any] = [:]
|
|
331
|
+
if let lastFetch = lastCampaignFetchAttempt[key] {
|
|
332
|
+
entry["lastFetchAttempt"] = lastFetch.timeIntervalSince1970
|
|
333
|
+
}
|
|
334
|
+
if let inProg = fetchInProgress[key] {
|
|
335
|
+
entry["fetchInProgress"] = inProg
|
|
336
|
+
}
|
|
337
|
+
if let lastProx = lastProximityEventSent[key] {
|
|
338
|
+
entry["lastProximityEvent"] = lastProx.timeIntervalSince1970
|
|
339
|
+
}
|
|
340
|
+
if let lastImp = lastImpressionEventSent[key] {
|
|
341
|
+
entry["lastImpressionEvent"] = lastImp.timeIntervalSince1970
|
|
342
|
+
}
|
|
343
|
+
status[key] = entry
|
|
344
|
+
}
|
|
345
|
+
resolve(status)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// MARK: - Debug Logging
|
|
349
|
+
|
|
350
|
+
@objc
|
|
351
|
+
public func getDebugLogs(
|
|
352
|
+
withResolve resolve: @escaping (Any?) -> Void,
|
|
353
|
+
reject: @escaping (String?, String?, Error?) -> Void
|
|
354
|
+
) {
|
|
355
|
+
guard let docsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
|
|
356
|
+
reject("ERROR", "Cannot access documents directory", nil); return
|
|
357
|
+
}
|
|
358
|
+
let logURL = docsURL.appendingPathComponent("spotny_beacon_debug.log")
|
|
359
|
+
resolve((try? String(contentsOf: logURL, encoding: .utf8)) ?? "No logs found")
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
@objc
|
|
363
|
+
public func clearDebugLogs(
|
|
364
|
+
withResolve resolve: @escaping (Any?) -> Void,
|
|
365
|
+
reject: @escaping (String?, String?, Error?) -> Void
|
|
366
|
+
) {
|
|
367
|
+
guard let docsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
|
|
368
|
+
reject("ERROR", "Cannot access documents directory", nil); return
|
|
369
|
+
}
|
|
370
|
+
try? FileManager.default.removeItem(at: docsURL.appendingPathComponent("spotny_beacon_debug.log"))
|
|
371
|
+
resolve("Logs cleared")
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// MARK: - Scanning Setup
|
|
375
|
+
|
|
376
|
+
private func startPersistentScanning() {
|
|
377
|
+
print("🚀 SpotnySDK: Starting persistent beacon scanning…")
|
|
378
|
+
|
|
379
|
+
// General region — monitors all beacons with the Kontakt.io UUID
|
|
380
|
+
let generalRegion = KTKBeaconRegion(proximityUUID: beaconUUID, identifier: "SpotnySDK_GeneralRegion")
|
|
381
|
+
generalRegion.notifyOnEntry = true
|
|
382
|
+
generalRegion.notifyOnExit = true
|
|
383
|
+
generalRegion.notifyEntryStateOnDisplay = true
|
|
384
|
+
|
|
385
|
+
beaconManager.startMonitoring(for: generalRegion)
|
|
386
|
+
beaconManager.startRangingBeacons(in: generalRegion)
|
|
387
|
+
|
|
388
|
+
print("🎯 SpotnySDK: Monitoring UUID \(beaconUUID.uuidString)")
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// MARK: - Helpers
|
|
392
|
+
|
|
393
|
+
private func beaconKey(major: Int, minor: Int) -> String { "\(major)_\(minor)" }
|
|
394
|
+
|
|
395
|
+
// ── Keychain helpers (device ID survives uninstall/reinstall) ──────────────
|
|
396
|
+
private let keychainService = "app.spotny.sdk"
|
|
397
|
+
|
|
398
|
+
private func keychainRead(key: String) -> String? {
|
|
399
|
+
let query: [CFString: Any] = [
|
|
400
|
+
kSecClass: kSecClassGenericPassword,
|
|
401
|
+
kSecAttrService: keychainService,
|
|
402
|
+
kSecAttrAccount: key,
|
|
403
|
+
kSecReturnData: true,
|
|
404
|
+
kSecMatchLimit: kSecMatchLimitOne
|
|
405
|
+
]
|
|
406
|
+
var result: AnyObject?
|
|
407
|
+
guard SecItemCopyMatching(query as CFDictionary, &result) == errSecSuccess,
|
|
408
|
+
let data = result as? Data else { return nil }
|
|
409
|
+
return String(data: data, encoding: .utf8)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
private func keychainWrite(key: String, value: String) {
|
|
413
|
+
guard let data = value.data(using: .utf8) else { return }
|
|
414
|
+
let query: [CFString: Any] = [
|
|
415
|
+
kSecClass: kSecClassGenericPassword,
|
|
416
|
+
kSecAttrService: keychainService,
|
|
417
|
+
kSecAttrAccount: key
|
|
418
|
+
]
|
|
419
|
+
if SecItemCopyMatching(query as CFDictionary, nil) == errSecSuccess {
|
|
420
|
+
SecItemUpdate(query as CFDictionary, [kSecValueData: data] as CFDictionary)
|
|
421
|
+
} else {
|
|
422
|
+
var item = query; item[kSecValueData] = data
|
|
423
|
+
SecItemAdd(item as CFDictionary, nil)
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
private func keychainDelete(key: String) {
|
|
428
|
+
let query: [CFString: Any] = [
|
|
429
|
+
kSecClass: kSecClassGenericPassword,
|
|
430
|
+
kSecAttrService: keychainService,
|
|
431
|
+
kSecAttrAccount: key
|
|
432
|
+
]
|
|
433
|
+
SecItemDelete(query as CFDictionary)
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
private func getDeviceId() -> String {
|
|
437
|
+
let kcKey = "SpotnySDK_deviceId"
|
|
438
|
+
// 1. Keychain — survives uninstall
|
|
439
|
+
if let stored = keychainRead(key: kcKey) { return stored }
|
|
440
|
+
// 2. Migrate existing UserDefaults value on first run after upgrade
|
|
441
|
+
if let legacy = UserDefaults.standard.string(forKey: kcKey) {
|
|
442
|
+
keychainWrite(key: kcKey, value: legacy)
|
|
443
|
+
UserDefaults.standard.removeObject(forKey: kcKey)
|
|
444
|
+
return legacy
|
|
445
|
+
}
|
|
446
|
+
// 3. Generate new ID
|
|
447
|
+
let id = UIDevice.current.identifierForVendor?.uuidString ?? UUID().uuidString
|
|
448
|
+
keychainWrite(key: kcKey, value: id)
|
|
449
|
+
return id
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
private func proximityLabel(from distance: Double) -> String {
|
|
453
|
+
if distance < 0 { return "unknown" }
|
|
454
|
+
if distance < 0.5 { return "immediate" }
|
|
455
|
+
if distance < 3.0 { return "near" }
|
|
456
|
+
return "far"
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
private func logToFile(_ message: String) {
|
|
460
|
+
guard let docsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return }
|
|
461
|
+
let logURL = docsURL.appendingPathComponent("spotny_beacon_debug.log")
|
|
462
|
+
let ts = DateFormatter.localizedString(from: Date(), dateStyle: .short, timeStyle: .medium)
|
|
463
|
+
let entry = "[\(ts)] \(message)\n"
|
|
464
|
+
guard let data = entry.data(using: .utf8) else { return }
|
|
465
|
+
if FileManager.default.fileExists(atPath: logURL.path),
|
|
466
|
+
let handle = try? FileHandle(forWritingTo: logURL) {
|
|
467
|
+
handle.seekToEndOfFile(); handle.write(data); handle.closeFile()
|
|
468
|
+
} else {
|
|
469
|
+
try? data.write(to: logURL)
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// MARK: - Backend API
|
|
474
|
+
|
|
475
|
+
private func isTokenExpired() -> Bool {
|
|
476
|
+
guard sdkToken != nil else { return true }
|
|
477
|
+
guard let expiry = sdkTokenExpiry else { return false } // no expiry field = treat as valid
|
|
478
|
+
return Date() >= expiry
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
private func persistJWT() {
|
|
482
|
+
if let jwt = sdkToken { keychainWrite(key: "SpotnySDK_jwt", value: jwt) }
|
|
483
|
+
if let exp = sdkTokenExpiry { keychainWrite(key: "SpotnySDK_jwtExpiry", value: String(exp.timeIntervalSince1970)) }
|
|
484
|
+
if let cred = sdkCredential { keychainWrite(key: "SpotnySDK_sdkCredential", value: cred) }
|
|
485
|
+
if let key = apiKey { keychainWrite(key: "SpotnySDK_apiKey", value: key) }
|
|
486
|
+
if let uid = userId { keychainWrite(key: "SpotnySDK_userId", value: uid) }
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/// Refresh the JWT. Concurrent callers are queued and flushed together when the single
|
|
490
|
+
/// in-flight request completes — avoids sending N parallel verify requests.
|
|
491
|
+
private func refreshJWT(completion: @escaping (Error?) -> Void) {
|
|
492
|
+
if jwtRefreshInProgress {
|
|
493
|
+
jwtRefreshQueue.append(completion)
|
|
494
|
+
return
|
|
495
|
+
}
|
|
496
|
+
guard let credential = sdkCredential, let key = apiKey else {
|
|
497
|
+
completion(NSError(domain: "SpotnySDK", code: -3,
|
|
498
|
+
userInfo: [NSLocalizedDescriptionKey: "Cannot refresh JWT — call initialize() first"]))
|
|
499
|
+
return
|
|
500
|
+
}
|
|
501
|
+
jwtRefreshInProgress = true
|
|
502
|
+
let refreshPayload: [String: Any] = ["token": credential, "api_key": key, "user_id": userId ?? ""]
|
|
503
|
+
performPost(endpoint: "/api/app/sdk/verify", payload: refreshPayload) { [weak self] result in
|
|
504
|
+
guard let self = self else { return }
|
|
505
|
+
let flush: (Error?) -> Void = { err in
|
|
506
|
+
let queue = self.jwtRefreshQueue
|
|
507
|
+
self.jwtRefreshQueue.removeAll()
|
|
508
|
+
self.jwtRefreshInProgress = false
|
|
509
|
+
completion(err)
|
|
510
|
+
queue.forEach { $0(err) }
|
|
511
|
+
}
|
|
512
|
+
switch result {
|
|
513
|
+
case .success(let (status, data)):
|
|
514
|
+
if 200...299 ~= status,
|
|
515
|
+
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
516
|
+
let dataObj = json["data"] as? [String: Any],
|
|
517
|
+
let jwt = dataObj["jwt"] as? String, !jwt.isEmpty {
|
|
518
|
+
self.sdkToken = jwt
|
|
519
|
+
if let ts = dataObj["expires_at"] as? Double {
|
|
520
|
+
self.sdkTokenExpiry = Date(timeIntervalSince1970: ts)
|
|
521
|
+
} else if let i = dataObj["expires_at"] as? Int {
|
|
522
|
+
self.sdkTokenExpiry = Date(timeIntervalSince1970: Double(i))
|
|
523
|
+
}
|
|
524
|
+
self.persistJWT()
|
|
525
|
+
let expiryDesc = self.sdkTokenExpiry?.description ?? "no expiry"
|
|
526
|
+
print("\u{1F504} SpotnySDK: JWT refreshed, expires: \(expiryDesc)")
|
|
527
|
+
flush(nil)
|
|
528
|
+
} else {
|
|
529
|
+
flush(NSError(domain: "SpotnySDK", code: -4,
|
|
530
|
+
userInfo: [NSLocalizedDescriptionKey: "JWT refresh failed — status \(status)"]))
|
|
531
|
+
}
|
|
532
|
+
case .failure(let error):
|
|
533
|
+
print("❌ SpotnySDK: JWT refresh error — \(error.localizedDescription)")
|
|
534
|
+
flush(error)
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/// Entry point — checks JWT expiry and auto-refreshes before every call except verify itself.
|
|
540
|
+
private func post(
|
|
541
|
+
endpoint: String,
|
|
542
|
+
payload: [String: Any],
|
|
543
|
+
completion: @escaping (Result<(Int, Data), Error>) -> Void
|
|
544
|
+
) {
|
|
545
|
+
guard endpoint == "/api/app/sdk/verify" || !isTokenExpired() else {
|
|
546
|
+
refreshJWT { [weak self] error in
|
|
547
|
+
if let error = error { completion(.failure(error)); return }
|
|
548
|
+
self?.performPost(endpoint: endpoint, payload: payload, completion: completion)
|
|
549
|
+
}
|
|
550
|
+
return
|
|
551
|
+
}
|
|
552
|
+
performPost(endpoint: endpoint, payload: payload, completion: completion)
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
private func performPost(
|
|
556
|
+
endpoint: String,
|
|
557
|
+
payload: [String: Any],
|
|
558
|
+
completion: @escaping (Result<(Int, Data), Error>) -> Void
|
|
559
|
+
) {
|
|
560
|
+
guard let url = URL(string: "\(backendURL)\(endpoint)") else {
|
|
561
|
+
completion(.failure(NSError(domain: "SpotnySDK", code: -1,
|
|
562
|
+
userInfo: [NSLocalizedDescriptionKey: "Invalid URL: \(backendURL)\(endpoint)"])))
|
|
563
|
+
return
|
|
564
|
+
}
|
|
565
|
+
var req = URLRequest(url: url)
|
|
566
|
+
req.httpMethod = "POST"
|
|
567
|
+
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
568
|
+
if let token = sdkToken { req.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") }
|
|
569
|
+
req.timeoutInterval = 10.0
|
|
570
|
+
|
|
571
|
+
var bg: UIBackgroundTaskIdentifier = .invalid
|
|
572
|
+
bg = UIApplication.shared.beginBackgroundTask { UIApplication.shared.endBackgroundTask(bg); bg = .invalid }
|
|
573
|
+
|
|
574
|
+
do {
|
|
575
|
+
req.httpBody = try JSONSerialization.data(withJSONObject: payload)
|
|
576
|
+
} catch {
|
|
577
|
+
UIApplication.shared.endBackgroundTask(bg)
|
|
578
|
+
completion(.failure(error)); return
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
URLSession.shared.dataTask(with: req) { data, response, error in
|
|
582
|
+
DispatchQueue.main.async {
|
|
583
|
+
if bg != .invalid { UIApplication.shared.endBackgroundTask(bg); bg = .invalid }
|
|
584
|
+
if let error = error { completion(.failure(error)); return }
|
|
585
|
+
guard let http = response as? HTTPURLResponse, let data = data else {
|
|
586
|
+
completion(.failure(NSError(domain: "SpotnySDK", code: -2,
|
|
587
|
+
userInfo: [NSLocalizedDescriptionKey: "Bad response"]))); return
|
|
588
|
+
}
|
|
589
|
+
completion(.success((http.statusCode, data)))
|
|
590
|
+
}
|
|
591
|
+
}.resume()
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// MARK: - Campaign Fetching
|
|
595
|
+
|
|
596
|
+
/// Exponential cooldown: 5 s → 15 s → 45 s. After 3 failures the key is
|
|
597
|
+
/// considered permanently failed until the beacon region is re-entered.
|
|
598
|
+
private func fetchCooldown(for key: String) -> TimeInterval {
|
|
599
|
+
let retries = fetchRetryCount[key] ?? 0
|
|
600
|
+
return campaignFetchCooldown * pow(3.0, Double(min(retries, 3)))
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
private func fetchCampaign(major: Int, minor: Int) {
|
|
604
|
+
let key = beaconKey(major: major, minor: minor)
|
|
605
|
+
guard activeCampaigns[key] == nil else { return }
|
|
606
|
+
guard fetchInProgress[key] != true else { return }
|
|
607
|
+
// Give up after 3 consecutive failures (cooldown would be 135 s+)
|
|
608
|
+
if (fetchRetryCount[key] ?? 0) > 3 { return }
|
|
609
|
+
|
|
610
|
+
if let last = lastCampaignFetchAttempt[key],
|
|
611
|
+
Date().timeIntervalSince(last) < fetchCooldown(for: key) { return }
|
|
612
|
+
|
|
613
|
+
lastCampaignFetchAttempt[key] = Date()
|
|
614
|
+
fetchInProgress[key] = true
|
|
615
|
+
|
|
616
|
+
let payload: [String: Any] = ["beacon_id": key]
|
|
617
|
+
|
|
618
|
+
post(endpoint: "/api/app/campaigns/beacon", payload: payload) { [weak self] result in
|
|
619
|
+
guard let self = self else { return }
|
|
620
|
+
defer { self.fetchInProgress[key] = false }
|
|
621
|
+
|
|
622
|
+
guard case .success(let (status, data)) = result, status == 200 else {
|
|
623
|
+
if case .success(let (s, _)) = result {
|
|
624
|
+
print("❌ SpotnySDK: Campaign fetch status \(s) for beacon \(key)")
|
|
625
|
+
}
|
|
626
|
+
// Increment retry count so next attempt uses a longer cooldown
|
|
627
|
+
self.fetchRetryCount[key] = (self.fetchRetryCount[key] ?? 0) + 1
|
|
628
|
+
logToFile("❌ Campaign fetch failed for beacon \(key) (retry \(self.fetchRetryCount[key] ?? 0))")
|
|
629
|
+
return
|
|
630
|
+
}
|
|
631
|
+
do {
|
|
632
|
+
guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
633
|
+
let dataObj = json["data"] as? [String: Any],
|
|
634
|
+
let screen = dataObj["screen"] as? [String: Any],
|
|
635
|
+
let screenId = screen["id"] as? Int else {
|
|
636
|
+
print("⚠️ SpotnySDK: Unexpected campaign response format for \(key)")
|
|
637
|
+
return
|
|
638
|
+
}
|
|
639
|
+
var campaignId: Int?
|
|
640
|
+
var inQueue = false
|
|
641
|
+
if let campaignObj = dataObj["campaign"] as? [String: Any],
|
|
642
|
+
let cid = campaignObj["id"] as? Int {
|
|
643
|
+
campaignId = cid
|
|
644
|
+
inQueue = campaignObj["inQueue"] as? Bool ?? false
|
|
645
|
+
}
|
|
646
|
+
self.activeCampaigns[key] = CampaignData(
|
|
647
|
+
campaignId: campaignId, screenId: screenId,
|
|
648
|
+
sessionId: nil, inQueue: inQueue, major: major, minor: minor)
|
|
649
|
+
self.fetchRetryCount.removeValue(forKey: key) // reset on success
|
|
650
|
+
logToFile("✅ Campaign loaded for beacon \(key) — screenId=\(screenId)")
|
|
651
|
+
print("✅ SpotnySDK: Campaign loaded for beacon \(key) — screenId=\(screenId)")
|
|
652
|
+
} catch {
|
|
653
|
+
print("❌ SpotnySDK: JSON parse error for beacon \(key): \(error)")
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// MARK: - Tracking
|
|
659
|
+
|
|
660
|
+
private func sendTracking(
|
|
661
|
+
eventType: String,
|
|
662
|
+
key: String,
|
|
663
|
+
distance: Double,
|
|
664
|
+
endpoint: String
|
|
665
|
+
) {
|
|
666
|
+
let isImpression = eventType == "IMPRESSION_HEARTBEAT"
|
|
667
|
+
let inProg = isImpression ? impressionEventInProgress[key] : proximityEventInProgress[key]
|
|
668
|
+
guard inProg != true else { return }
|
|
669
|
+
|
|
670
|
+
if isImpression { impressionEventInProgress[key] = true }
|
|
671
|
+
else { proximityEventInProgress[key] = true }
|
|
672
|
+
|
|
673
|
+
guard let campaign = activeCampaigns[key] else {
|
|
674
|
+
if isImpression { impressionEventInProgress[key] = false }
|
|
675
|
+
else { proximityEventInProgress[key] = false }
|
|
676
|
+
return
|
|
677
|
+
}
|
|
678
|
+
if isImpression {
|
|
679
|
+
guard let _ = campaign.campaignId, !campaign.inQueue else {
|
|
680
|
+
impressionEventInProgress[key] = false; return
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
var payload: [String: Any] = [
|
|
685
|
+
"event_type": eventType,
|
|
686
|
+
"distance": distance,
|
|
687
|
+
"screen_id": campaign.screenId
|
|
688
|
+
]
|
|
689
|
+
if let cid = campaign.campaignId { payload["campaign_id"] = cid }
|
|
690
|
+
if let sid = campaign.sessionId { payload["session_id"] = sid }
|
|
691
|
+
|
|
692
|
+
post(endpoint: endpoint, payload: payload) { [weak self] result in
|
|
693
|
+
guard let self = self else { return }
|
|
694
|
+
switch result {
|
|
695
|
+
case .success(let (status, data)):
|
|
696
|
+
if 200...299 ~= status {
|
|
697
|
+
print("✅ SpotnySDK: \(eventType) sent — distance \(String(format: "%.2f", distance))m")
|
|
698
|
+
logToFile("✅ \(eventType) sent — beacon \(key) @ \(String(format: "%.2f", distance))m")
|
|
699
|
+
if !isImpression, campaign.sessionId == nil,
|
|
700
|
+
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
701
|
+
let dObj = json["data"] as? [String: Any],
|
|
702
|
+
let ev = dObj["event"] as? [String: Any],
|
|
703
|
+
let sid = ev["session_id"] as? String {
|
|
704
|
+
let updated = CampaignData(
|
|
705
|
+
campaignId: campaign.campaignId, screenId: campaign.screenId,
|
|
706
|
+
sessionId: sid, inQueue: campaign.inQueue,
|
|
707
|
+
major: campaign.major, minor: campaign.minor)
|
|
708
|
+
self.activeCampaigns[key] = updated
|
|
709
|
+
print("✅ SpotnySDK: session_id = \(sid)")
|
|
710
|
+
}
|
|
711
|
+
} else if status == 429 {
|
|
712
|
+
let penalty = Date().addingTimeInterval(10)
|
|
713
|
+
if isImpression { self.lastImpressionEventSent[key] = penalty }
|
|
714
|
+
else { self.lastProximityEventSent[key] = penalty }
|
|
715
|
+
print("⚠️ SpotnySDK: \(eventType) rate-limited (429)")
|
|
716
|
+
} else {
|
|
717
|
+
print("❌ SpotnySDK: \(eventType) failed — status \(status)")
|
|
718
|
+
}
|
|
719
|
+
case .failure(let error):
|
|
720
|
+
print("❌ SpotnySDK: \(eventType) error — \(error.localizedDescription)")
|
|
721
|
+
}
|
|
722
|
+
if isImpression { self.impressionEventInProgress[key] = false }
|
|
723
|
+
else { self.proximityEventInProgress[key] = false }
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
private func sendProximity(eventType: String, key: String, distance: Double) {
|
|
728
|
+
sendTracking(eventType: eventType, key: key, distance: distance,
|
|
729
|
+
endpoint: "/api/app/impressions/proximity")
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
private func sendImpression(key: String, distance: Double) {
|
|
733
|
+
sendTracking(eventType: "IMPRESSION_HEARTBEAT", key: key, distance: distance,
|
|
734
|
+
endpoint: "/api/app/impressions/track")
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// MARK: - State Cleanup
|
|
738
|
+
|
|
739
|
+
private func cleanupBeacon(_ key: String, distance: Double = 0) {
|
|
740
|
+
sendProximity(eventType: "PROXIMITY_EXIT", key: key, distance: distance)
|
|
741
|
+
activeCampaigns.removeValue(forKey: key)
|
|
742
|
+
lastProximityEventSent.removeValue(forKey: key)
|
|
743
|
+
lastProximityDistance.removeValue(forKey: key)
|
|
744
|
+
lastImpressionEventSent.removeValue(forKey: key)
|
|
745
|
+
lastCampaignFetchAttempt.removeValue(forKey: key)
|
|
746
|
+
fetchInProgress.removeValue(forKey: key)
|
|
747
|
+
fetchRetryCount.removeValue(forKey: key)
|
|
748
|
+
proximityEventInProgress.removeValue(forKey: key)
|
|
749
|
+
impressionEventInProgress.removeValue(forKey: key)
|
|
750
|
+
print("🧹 SpotnySDK: Cleaned up state for beacon \(key)")
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
private func cleanupAllProximityState() {
|
|
754
|
+
// Stagger exit events to avoid firing N simultaneous network requests
|
|
755
|
+
let keys = Array(activeCampaigns.keys)
|
|
756
|
+
for (index, key) in keys.enumerated() {
|
|
757
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + Double(index) * 0.3) { [weak self] in
|
|
758
|
+
self?.cleanupBeacon(key)
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// MARK: - CLLocationManagerDelegate
|
|
765
|
+
|
|
766
|
+
extension SpotnyBeaconScanner: CLLocationManagerDelegate {
|
|
767
|
+
public func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
|
|
768
|
+
switch status {
|
|
769
|
+
case .authorizedAlways: print("✅ SpotnySDK: Location → ALWAYS")
|
|
770
|
+
case .authorizedWhenInUse: print("⚠️ SpotnySDK: Location → WHEN IN USE (limited background)")
|
|
771
|
+
case .denied, .restricted: print("❌ SpotnySDK: Location → DENIED")
|
|
772
|
+
case .notDetermined: print("⏳ SpotnySDK: Location → not determined")
|
|
773
|
+
@unknown default: break
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// MARK: - KTKBeaconManagerDelegate
|
|
779
|
+
|
|
780
|
+
extension SpotnyBeaconScanner: KTKBeaconManagerDelegate {
|
|
781
|
+
|
|
782
|
+
public func beaconManager(
|
|
783
|
+
_ manager: KTKBeaconManager,
|
|
784
|
+
didRangeBeacons beacons: [CLBeacon],
|
|
785
|
+
in region: KTKBeaconRegion
|
|
786
|
+
) {
|
|
787
|
+
let now = Date()
|
|
788
|
+
|
|
789
|
+
// Refresh session heartbeat every 60 s to prevent TTL expiry on a live session
|
|
790
|
+
if now.timeIntervalSince(lastSessionHeartbeat) >= 60 {
|
|
791
|
+
UserDefaults.standard.set(now.timeIntervalSince1970, forKey: "SpotnySDK_sessionTimestamp")
|
|
792
|
+
lastSessionHeartbeat = now
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Build the JS event payload for ALL ranged beacons
|
|
796
|
+
let beaconPayload: [[String: Any]] = beacons.compactMap { beacon in
|
|
797
|
+
let raw = beacon.accuracy
|
|
798
|
+
let adjusted = raw * distanceCorrectionFactor
|
|
799
|
+
guard adjusted > 0 && adjusted <= maxDetectionDistance else { return nil }
|
|
800
|
+
return [
|
|
801
|
+
"uuid": beacon.proximityUUID.uuidString,
|
|
802
|
+
"major": beacon.major.intValue,
|
|
803
|
+
"minor": beacon.minor.intValue,
|
|
804
|
+
"distance": adjusted,
|
|
805
|
+
"rssi": beacon.rssi,
|
|
806
|
+
"proximity": proximityLabel(from: adjusted)
|
|
807
|
+
]
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// Only emit onBeaconsRanged when proximity state changed, or after debounceInterval
|
|
811
|
+
if !beaconPayload.isEmpty {
|
|
812
|
+
let signature = beaconPayload
|
|
813
|
+
.map { "\($0["major"]!)_\($0["minor"]!)_\($0["proximity"]!)" }
|
|
814
|
+
.sorted().joined(separator: ",")
|
|
815
|
+
let stale = now.timeIntervalSince(lastRangedEmit) >= debounceInterval
|
|
816
|
+
if signature != lastRangedSignature || stale {
|
|
817
|
+
eventCallback?("onBeaconsRanged", ["beacons": beaconPayload, "region": region.identifier])
|
|
818
|
+
lastRangedSignature = signature
|
|
819
|
+
lastRangedEmit = now
|
|
820
|
+
}
|
|
821
|
+
} else if lastRangedSignature != "" {
|
|
822
|
+
// All beacons left range — emit empty to let JS clear its state
|
|
823
|
+
eventCallback?("onBeaconsRanged", ["beacons": [], "region": region.identifier])
|
|
824
|
+
lastRangedSignature = ""
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// Per-beacon campaign & proximity logic
|
|
828
|
+
for beacon in beacons {
|
|
829
|
+
let major = beacon.major.intValue
|
|
830
|
+
let minor = beacon.minor.intValue
|
|
831
|
+
let key = beaconKey(major: major, minor: minor)
|
|
832
|
+
let raw = beacon.accuracy
|
|
833
|
+
let distance = raw * distanceCorrectionFactor
|
|
834
|
+
|
|
835
|
+
guard distance > 0 && distance <= maxDetectionDistance else { continue }
|
|
836
|
+
|
|
837
|
+
if let campaign = activeCampaigns[key] {
|
|
838
|
+
let isFirst = lastProximityEventSent[key] == nil
|
|
839
|
+
|
|
840
|
+
if isFirst {
|
|
841
|
+
sendProximity(eventType: "NEARBY", key: key, distance: distance)
|
|
842
|
+
lastProximityDistance[key] = distance
|
|
843
|
+
lastProximityEventSent[key] = now
|
|
844
|
+
} else if distance >= 1.0,
|
|
845
|
+
let lastDist = lastProximityDistance[key],
|
|
846
|
+
abs(distance - lastDist) >= proximityDistanceThreshold {
|
|
847
|
+
sendProximity(eventType: "NEARBY", key: key, distance: distance)
|
|
848
|
+
lastProximityDistance[key] = distance
|
|
849
|
+
lastProximityEventSent[key] = now
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// Impression heartbeat when user is very close
|
|
853
|
+
if let _ = campaign.campaignId, !campaign.inQueue, distance <= impressionDistance {
|
|
854
|
+
if let last = lastImpressionEventSent[key] {
|
|
855
|
+
if now.timeIntervalSince(last) >= impressionEventInterval {
|
|
856
|
+
sendImpression(key: key, distance: distance)
|
|
857
|
+
lastImpressionEventSent[key] = now
|
|
858
|
+
}
|
|
859
|
+
} else {
|
|
860
|
+
sendImpression(key: key, distance: distance)
|
|
861
|
+
lastImpressionEventSent[key] = now
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
} else {
|
|
865
|
+
fetchCampaign(major: major, minor: minor)
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
public func beaconManager(_ manager: KTKBeaconManager, didEnter region: KTKBeaconRegion) {
|
|
871
|
+
print("🎯 SpotnySDK: Entered region \(region.identifier)")
|
|
872
|
+
eventCallback?("onBeaconRegionEvent", ["region": region.identifier, "event": "enter"])
|
|
873
|
+
|
|
874
|
+
var bg: UIBackgroundTaskIdentifier = .invalid
|
|
875
|
+
bg = UIApplication.shared.beginBackgroundTask { UIApplication.shared.endBackgroundTask(bg); bg = .invalid }
|
|
876
|
+
defer {
|
|
877
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
|
|
878
|
+
if bg != .invalid { UIApplication.shared.endBackgroundTask(bg); bg = .invalid }
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// Parse major/minor from named regions (e.g. "SpotnySDK_52885_35127")
|
|
883
|
+
let parts = region.identifier.components(separatedBy: "_")
|
|
884
|
+
if parts.count == 3, let major = Int(parts[1]), let minor = Int(parts[2]) {
|
|
885
|
+
let key = beaconKey(major: major, minor: minor)
|
|
886
|
+
// Reset fetch state on re-entry so retry backoff starts fresh
|
|
887
|
+
lastCampaignFetchAttempt.removeValue(forKey: key)
|
|
888
|
+
fetchRetryCount.removeValue(forKey: key)
|
|
889
|
+
if activeCampaigns[key] == nil {
|
|
890
|
+
fetchCampaign(major: major, minor: minor)
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
public func beaconManager(_ manager: KTKBeaconManager, didExitRegion region: KTKBeaconRegion) {
|
|
896
|
+
print("🚪 SpotnySDK: Exited region \(region.identifier)")
|
|
897
|
+
eventCallback?("onBeaconRegionEvent", ["region": region.identifier, "event": "exit"])
|
|
898
|
+
|
|
899
|
+
var bg: UIBackgroundTaskIdentifier = .invalid
|
|
900
|
+
bg = UIApplication.shared.beginBackgroundTask { UIApplication.shared.endBackgroundTask(bg); bg = .invalid }
|
|
901
|
+
defer {
|
|
902
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
|
|
903
|
+
if bg != .invalid { UIApplication.shared.endBackgroundTask(bg); bg = .invalid }
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
let parts = region.identifier.components(separatedBy: "_")
|
|
908
|
+
if parts.count == 3, let major = Int(parts[1]), let minor = Int(parts[2]) {
|
|
909
|
+
cleanupBeacon(beaconKey(major: major, minor: minor))
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
beaconManager.stopRangingBeacons(in: region)
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
public func beaconManager(
|
|
916
|
+
_ manager: KTKBeaconManager,
|
|
917
|
+
didDetermineState state: CLRegionState,
|
|
918
|
+
for region: KTKBeaconRegion
|
|
919
|
+
) {
|
|
920
|
+
let label: String
|
|
921
|
+
switch state {
|
|
922
|
+
case .inside: label = "inside"
|
|
923
|
+
case .outside: label = "outside"
|
|
924
|
+
case .unknown: label = "unknown"
|
|
925
|
+
@unknown default: label = "unknown"
|
|
926
|
+
}
|
|
927
|
+
eventCallback?("onBeaconRegionEvent", ["region": region.identifier, "event": "determined", "state": label])
|
|
928
|
+
print("📊 SpotnySDK: Region \(region.identifier) → \(label)")
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
public func beaconManager(
|
|
932
|
+
_ manager: KTKBeaconManager,
|
|
933
|
+
monitoringDidFailFor region: KTKBeaconRegion?,
|
|
934
|
+
withError error: Error?
|
|
935
|
+
) {
|
|
936
|
+
print("❌ SpotnySDK: Monitoring failed for \(region?.identifier ?? "?") — \(error?.localizedDescription ?? "unknown error")")
|
|
937
|
+
}
|
|
938
|
+
}
|