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.
Files changed (213) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +589 -0
  3. package/SpotnySdk.podspec +35 -0
  4. package/android/build.gradle +70 -0
  5. package/android/src/main/AndroidManifest.xml +33 -0
  6. package/android/src/main/java/com/spotnysdk/SpotnySdkModule.kt +685 -0
  7. package/android/src/main/java/com/spotnysdk/SpotnySdkPackage.kt +31 -0
  8. package/ios/Frameworks/CBORCoding.xcframework/Info.plist +44 -0
  9. package/ios/Frameworks/CBORCoding.xcframework/ios-arm64/CBORCoding.framework/CBORCoding +0 -0
  10. package/ios/Frameworks/CBORCoding.xcframework/ios-arm64/CBORCoding.framework/Headers/CBORCoding-Swift.h +317 -0
  11. package/ios/Frameworks/CBORCoding.xcframework/ios-arm64/CBORCoding.framework/Info.plist +0 -0
  12. package/ios/Frameworks/CBORCoding.xcframework/ios-arm64/CBORCoding.framework/Modules/CBORCoding.swiftmodule/arm64-apple-ios.abi.json +7021 -0
  13. package/ios/Frameworks/CBORCoding.xcframework/ios-arm64/CBORCoding.framework/Modules/CBORCoding.swiftmodule/arm64-apple-ios.private.swiftinterface +193 -0
  14. package/ios/Frameworks/CBORCoding.xcframework/ios-arm64/CBORCoding.framework/Modules/CBORCoding.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
  15. package/ios/Frameworks/CBORCoding.xcframework/ios-arm64/CBORCoding.framework/Modules/CBORCoding.swiftmodule/arm64-apple-ios.swiftinterface +193 -0
  16. package/ios/Frameworks/CBORCoding.xcframework/ios-arm64/CBORCoding.framework/Modules/module.modulemap +4 -0
  17. package/ios/Frameworks/CBORCoding.xcframework/ios-arm64_x86_64-simulator/CBORCoding.framework/CBORCoding +0 -0
  18. package/ios/Frameworks/CBORCoding.xcframework/ios-arm64_x86_64-simulator/CBORCoding.framework/Headers/CBORCoding-Swift.h +630 -0
  19. package/ios/Frameworks/CBORCoding.xcframework/ios-arm64_x86_64-simulator/CBORCoding.framework/Info.plist +0 -0
  20. package/ios/Frameworks/CBORCoding.xcframework/ios-arm64_x86_64-simulator/CBORCoding.framework/Modules/CBORCoding.swiftmodule/arm64-apple-ios-simulator.abi.json +7021 -0
  21. package/ios/Frameworks/CBORCoding.xcframework/ios-arm64_x86_64-simulator/CBORCoding.framework/Modules/CBORCoding.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +193 -0
  22. package/ios/Frameworks/CBORCoding.xcframework/ios-arm64_x86_64-simulator/CBORCoding.framework/Modules/CBORCoding.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  23. package/ios/Frameworks/CBORCoding.xcframework/ios-arm64_x86_64-simulator/CBORCoding.framework/Modules/CBORCoding.swiftmodule/arm64-apple-ios-simulator.swiftinterface +193 -0
  24. package/ios/Frameworks/CBORCoding.xcframework/ios-arm64_x86_64-simulator/CBORCoding.framework/Modules/CBORCoding.swiftmodule/x86_64-apple-ios-simulator.abi.json +7021 -0
  25. package/ios/Frameworks/CBORCoding.xcframework/ios-arm64_x86_64-simulator/CBORCoding.framework/Modules/CBORCoding.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +193 -0
  26. package/ios/Frameworks/CBORCoding.xcframework/ios-arm64_x86_64-simulator/CBORCoding.framework/Modules/CBORCoding.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
  27. package/ios/Frameworks/CBORCoding.xcframework/ios-arm64_x86_64-simulator/CBORCoding.framework/Modules/CBORCoding.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +193 -0
  28. package/ios/Frameworks/CBORCoding.xcframework/ios-arm64_x86_64-simulator/CBORCoding.framework/Modules/module.modulemap +4 -0
  29. package/ios/Frameworks/CBORCoding.xcframework/ios-arm64_x86_64-simulator/CBORCoding.framework/_CodeSignature/CodeResources +267 -0
  30. package/ios/Frameworks/Half.xcframework/Info.plist +44 -0
  31. package/ios/Frameworks/Half.xcframework/ios-arm64/Half.framework/Half +0 -0
  32. package/ios/Frameworks/Half.xcframework/ios-arm64/Half.framework/Headers/Half-Swift.h +317 -0
  33. package/ios/Frameworks/Half.xcframework/ios-arm64/Half.framework/Headers/half.h +95 -0
  34. package/ios/Frameworks/Half.xcframework/ios-arm64/Half.framework/Info.plist +0 -0
  35. package/ios/Frameworks/Half.xcframework/ios-arm64/Half.framework/Modules/Half.swiftmodule/arm64-apple-ios.abi.json +4942 -0
  36. package/ios/Frameworks/Half.xcframework/ios-arm64/Half.framework/Modules/Half.swiftmodule/arm64-apple-ios.private.swiftinterface +650 -0
  37. package/ios/Frameworks/Half.xcframework/ios-arm64/Half.framework/Modules/Half.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
  38. package/ios/Frameworks/Half.xcframework/ios-arm64/Half.framework/Modules/Half.swiftmodule/arm64-apple-ios.swiftinterface +650 -0
  39. package/ios/Frameworks/Half.xcframework/ios-arm64/Half.framework/Modules/module.modulemap +11 -0
  40. package/ios/Frameworks/Half.xcframework/ios-arm64_x86_64-simulator/Half.framework/Half +0 -0
  41. package/ios/Frameworks/Half.xcframework/ios-arm64_x86_64-simulator/Half.framework/Headers/Half-Swift.h +630 -0
  42. package/ios/Frameworks/Half.xcframework/ios-arm64_x86_64-simulator/Half.framework/Headers/half.h +95 -0
  43. package/ios/Frameworks/Half.xcframework/ios-arm64_x86_64-simulator/Half.framework/Info.plist +0 -0
  44. package/ios/Frameworks/Half.xcframework/ios-arm64_x86_64-simulator/Half.framework/Modules/Half.swiftmodule/arm64-apple-ios-simulator.abi.json +4942 -0
  45. package/ios/Frameworks/Half.xcframework/ios-arm64_x86_64-simulator/Half.framework/Modules/Half.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +650 -0
  46. package/ios/Frameworks/Half.xcframework/ios-arm64_x86_64-simulator/Half.framework/Modules/Half.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  47. package/ios/Frameworks/Half.xcframework/ios-arm64_x86_64-simulator/Half.framework/Modules/Half.swiftmodule/arm64-apple-ios-simulator.swiftinterface +650 -0
  48. package/ios/Frameworks/Half.xcframework/ios-arm64_x86_64-simulator/Half.framework/Modules/Half.swiftmodule/x86_64-apple-ios-simulator.abi.json +4973 -0
  49. package/ios/Frameworks/Half.xcframework/ios-arm64_x86_64-simulator/Half.framework/Modules/Half.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +660 -0
  50. package/ios/Frameworks/Half.xcframework/ios-arm64_x86_64-simulator/Half.framework/Modules/Half.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
  51. package/ios/Frameworks/Half.xcframework/ios-arm64_x86_64-simulator/Half.framework/Modules/Half.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +660 -0
  52. package/ios/Frameworks/Half.xcframework/ios-arm64_x86_64-simulator/Half.framework/Modules/module.modulemap +11 -0
  53. package/ios/Frameworks/Half.xcframework/ios-arm64_x86_64-simulator/Half.framework/_CodeSignature/CodeResources +245 -0
  54. package/ios/Frameworks/KontaktSDK.xcframework/Info.plist +44 -0
  55. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Base.lproj/Localizable.strings +0 -0
  56. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/CLBeacon+Kontakt.h +37 -0
  57. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKAction.h +90 -0
  58. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKActionContent.h +80 -0
  59. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKBeaconManager.h +326 -0
  60. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKBeaconRegion.h +102 -0
  61. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKCloudClient.h +241 -0
  62. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKCloudClientJSONResponseSerializer.h +20 -0
  63. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKCloudClientResponseSerializer.h +35 -0
  64. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKCloudClientSessionManager.h +207 -0
  65. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKCloudDefinitions.h +13 -0
  66. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKCloudModel.h +36 -0
  67. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKDevice.h +179 -0
  68. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKDeviceCCOperationDelegate.h +13 -0
  69. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKDeviceConfiguration.h +329 -0
  70. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKDeviceConfigurationGPIO.h +24 -0
  71. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKDeviceConnection.h +379 -0
  72. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKDeviceConnectionOperation.h +65 -0
  73. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKDeviceDataLoggerConnection.h +56 -0
  74. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKDeviceDataLoggerReading.h +49 -0
  75. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKDeviceDefinitions.h +853 -0
  76. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKDeviceGATTDefinitions.h +106 -0
  77. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKDeviceGATTOperation.h +68 -0
  78. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKDeviceGatewayConfigurationType.h +43 -0
  79. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKDeviceGatewayConnection.h +101 -0
  80. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKDeviceGatewayDiagnostic.h +61 -0
  81. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKDeviceGatewayWiFiNetwork.h +76 -0
  82. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKDeviceKontaktRecognitionBox.h +26 -0
  83. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKDeviceListenOperation.h +20 -0
  84. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKDeviceNotifyOperation.h +34 -0
  85. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKDevicePowerSaving.h +86 -0
  86. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKDeviceWriteOperation.h +51 -0
  87. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKDevicesManager.h +182 -0
  88. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKEddystone.h +101 -0
  89. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKEddystoneFrame.h +47 -0
  90. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKEddystoneManager.h +160 -0
  91. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKEddystoneRegion.h +85 -0
  92. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKEddystoneTLM.h +49 -0
  93. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKEddystoneUID.h +55 -0
  94. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKEddystoneURL.h +33 -0
  95. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKEddystoneURLValueTransformer.h +13 -0
  96. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKFirmware.h +98 -0
  97. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKGroupOperation.h +100 -0
  98. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKKontaktResponse.h +85 -0
  99. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKManager.h +121 -0
  100. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKNearbyDevice.h +135 -0
  101. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKNearbyDeviceTelemetry.h +513 -0
  102. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKOperation.h +116 -0
  103. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKPersonPosition.h +22 -0
  104. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKSecureBeaconRegion.h +86 -0
  105. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKSecureEddystoneRegion.h +59 -0
  106. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKTrigger.h +94 -0
  107. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKTriggerContext.h +79 -0
  108. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KTKVenue.h +105 -0
  109. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/Kontakt.h +187 -0
  110. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KontaktSDK-Swift.h +668 -0
  111. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/KontaktSDK.h +43 -0
  112. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/NSData+Kontakt.h +115 -0
  113. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/NSError+Kontakt.h +26 -0
  114. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/NSString+Kontakt.h +25 -0
  115. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Headers/NSURLRequest+Kontakt.h +44 -0
  116. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Info.plist +0 -0
  117. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/KontaktSDK +0 -0
  118. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Modules/KontaktSDK.swiftmodule/arm64-apple-ios.abi.json +14106 -0
  119. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Modules/KontaktSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +428 -0
  120. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Modules/KontaktSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
  121. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Modules/KontaktSDK.swiftmodule/arm64-apple-ios.swiftinterface +428 -0
  122. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/Modules/module.modulemap +11 -0
  123. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64/KontaktSDK.framework/strip-frameworks.sh +72 -0
  124. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Base.lproj/Localizable.strings +0 -0
  125. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/CLBeacon+Kontakt.h +37 -0
  126. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKAction.h +90 -0
  127. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKActionContent.h +80 -0
  128. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKBeaconManager.h +326 -0
  129. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKBeaconRegion.h +102 -0
  130. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKCloudClient.h +241 -0
  131. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKCloudClientJSONResponseSerializer.h +20 -0
  132. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKCloudClientResponseSerializer.h +35 -0
  133. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKCloudClientSessionManager.h +207 -0
  134. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKCloudDefinitions.h +13 -0
  135. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKCloudModel.h +36 -0
  136. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKDevice.h +179 -0
  137. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKDeviceCCOperationDelegate.h +13 -0
  138. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKDeviceConfiguration.h +329 -0
  139. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKDeviceConfigurationGPIO.h +24 -0
  140. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKDeviceConnection.h +379 -0
  141. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKDeviceConnectionOperation.h +65 -0
  142. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKDeviceDataLoggerConnection.h +56 -0
  143. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKDeviceDataLoggerReading.h +49 -0
  144. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKDeviceDefinitions.h +853 -0
  145. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKDeviceGATTDefinitions.h +106 -0
  146. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKDeviceGATTOperation.h +68 -0
  147. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKDeviceGatewayConfigurationType.h +43 -0
  148. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKDeviceGatewayConnection.h +101 -0
  149. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKDeviceGatewayDiagnostic.h +61 -0
  150. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKDeviceGatewayWiFiNetwork.h +76 -0
  151. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKDeviceKontaktRecognitionBox.h +26 -0
  152. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKDeviceListenOperation.h +20 -0
  153. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKDeviceNotifyOperation.h +34 -0
  154. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKDevicePowerSaving.h +86 -0
  155. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKDeviceWriteOperation.h +51 -0
  156. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKDevicesManager.h +182 -0
  157. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKEddystone.h +101 -0
  158. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKEddystoneFrame.h +47 -0
  159. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKEddystoneManager.h +160 -0
  160. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKEddystoneRegion.h +85 -0
  161. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKEddystoneTLM.h +49 -0
  162. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKEddystoneUID.h +55 -0
  163. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKEddystoneURL.h +33 -0
  164. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKEddystoneURLValueTransformer.h +13 -0
  165. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKFirmware.h +98 -0
  166. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKGroupOperation.h +100 -0
  167. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKKontaktResponse.h +85 -0
  168. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKManager.h +121 -0
  169. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKNearbyDevice.h +135 -0
  170. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKNearbyDeviceTelemetry.h +513 -0
  171. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKOperation.h +116 -0
  172. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKPersonPosition.h +22 -0
  173. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKSecureBeaconRegion.h +86 -0
  174. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKSecureEddystoneRegion.h +59 -0
  175. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKTrigger.h +94 -0
  176. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKTriggerContext.h +79 -0
  177. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KTKVenue.h +105 -0
  178. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/Kontakt.h +187 -0
  179. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KontaktSDK-Swift.h +1332 -0
  180. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/KontaktSDK.h +43 -0
  181. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/NSData+Kontakt.h +115 -0
  182. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/NSError+Kontakt.h +26 -0
  183. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/NSString+Kontakt.h +25 -0
  184. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Headers/NSURLRequest+Kontakt.h +44 -0
  185. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Info.plist +0 -0
  186. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/KontaktSDK +0 -0
  187. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Modules/KontaktSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +14106 -0
  188. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Modules/KontaktSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +428 -0
  189. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Modules/KontaktSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  190. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Modules/KontaktSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +428 -0
  191. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Modules/KontaktSDK.swiftmodule/x86_64-apple-ios-simulator.abi.json +14106 -0
  192. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Modules/KontaktSDK.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +428 -0
  193. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Modules/KontaktSDK.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
  194. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Modules/KontaktSDK.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +428 -0
  195. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/Modules/module.modulemap +11 -0
  196. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/_CodeSignature/CodeResources +894 -0
  197. package/ios/Frameworks/KontaktSDK.xcframework/ios-arm64_x86_64-simulator/KontaktSDK.framework/strip-frameworks.sh +72 -0
  198. package/ios/SpotnyBeaconScanner.swift +938 -0
  199. package/ios/SpotnySdk.h +10 -0
  200. package/ios/SpotnySdk.mm +127 -0
  201. package/lib/module/NativeSpotnySdk.js +5 -0
  202. package/lib/module/NativeSpotnySdk.js.map +1 -0
  203. package/lib/module/index.js +96 -0
  204. package/lib/module/index.js.map +1 -0
  205. package/lib/module/package.json +1 -0
  206. package/lib/typescript/package.json +1 -0
  207. package/lib/typescript/src/NativeSpotnySdk.d.ts +18 -0
  208. package/lib/typescript/src/NativeSpotnySdk.d.ts.map +1 -0
  209. package/lib/typescript/src/index.d.ts +81 -0
  210. package/lib/typescript/src/index.d.ts.map +1 -0
  211. package/package.json +169 -0
  212. package/src/NativeSpotnySdk.ts +29 -0
  213. 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
+ }