From ced06f9eb6f3483ed723583029bdaa452569ebaa Mon Sep 17 00:00:00 2001 From: Stefan Lange-Hegermann Date: Wed, 5 Nov 2025 11:13:40 +0100 Subject: [PATCH] ads tracking --- Cable.xcodeproj/project.pbxproj | 234 +- Cable.xcworkspace/contents.xcworkspacedata | 10 + Cable/AppDelegate.swift | 24 + Cable/AppIcon.icon/icon.json | 2 +- Cable/Batteries/BatteriesView.swift | 71 +- Cable/Batteries/BatteryEditorView.swift | 6 +- Cable/CableApp.swift | 8 +- Cable/Chargers/ChargersView.swift | 78 +- Cable/Loads/CalculatorView.swift | 6 +- Cable/Loads/LoadsView.swift | 398 ++- Cable/Overview/SystemOverviewView.swift | 22 +- Cable/Paywall/CableProPaywallView.swift | 25 +- Cable/SettingsView.swift | 74 +- Cable/StatsHeaderContainer.swift | 48 + Cable/StoreKitManager.swift | 221 ++ Cable/Systems/SystemsOnboardingView.swift | 5 + Cable/Systems/SystemsView.swift | 121 +- Podfile | 23 + Podfile.lock | 16 + Pods/Manifest.lock | 16 + Pods/Pods.xcodeproj/project.pbxproj | 2254 +++++++++++++++++ .../Pods-Cable-CableUITests.xcscheme | 58 + ...Pods-Cable-CableUITestsScreenshot.xcscheme | 58 + .../xcschemes/Pods-Cable.xcscheme | 58 + .../xcschemes/Pods-CableTests.xcscheme | 58 + .../xcschemes/PostHog-PostHog.xcscheme | 58 + .../xcschemes/PostHog.xcscheme | 58 + .../xcschemes/xcschememanagement.plist | 41 + Pods/PostHog/LICENSE | 21 + .../ApplicationLifecyclePublisher.swift | 199 ++ .../PostHogAppLifeCycleIntegration.swift | 214 ++ .../ApplicationViewLayoutPublisher.swift | 159 ++ .../AutocaptureEventProcessing.swift | 14 + .../ForwardingPickerViewDelegate.swift | 71 + .../PostHogAutocaptureEventTracker.swift | 606 +++++ .../PostHogAutocaptureIntegration.swift | 147 ++ .../SwiftUI/View+PostHogLabel.swift | 156 ++ .../Autocapture/UIView+PostHogLabel.swift | 29 + Pods/PostHog/PostHog/DI.swift | 28 + .../PostHog/PostHog/Models/PostHogEvent.swift | 102 + .../Surveys/PostHogSurvey+Display.swift | 122 + .../Models/Surveys/PostHogSurvey.swift | 46 + .../Surveys/PostHogSurveyAppearance.swift | 38 + .../Surveys/PostHogSurveyConditions.swift | 47 + .../Models/Surveys/PostHogSurveyEnums.swift | 280 ++ .../Surveys/PostHogSurveyQuestion.swift | 247 ++ Pods/PostHog/PostHog/PostHog.h | 60 + Pods/PostHog/PostHog/PostHogApi.swift | 337 +++ .../PostHog/PostHogBatchUploadInfo.swift | 13 + Pods/PostHog/PostHog/PostHogConfig.swift | 276 ++ .../PostHog/PostHogConsumerPayload.swift | 13 + Pods/PostHog/PostHog/PostHogContext.swift | 411 +++ Pods/PostHog/PostHog/PostHogExtensions.swift | 20 + .../PostHog/PostHogFileBackedQueue.swift | 114 + Pods/PostHog/PostHog/PostHogIntegration.swift | 59 + Pods/PostHog/PostHog/PostHogLegacyQueue.swift | 45 + .../PostHog/PostHogPersonProfiles.swift | 20 + .../PostHog/PostHogPropertiesSanitizer.swift | 34 + Pods/PostHog/PostHog/PostHogQueue.swift | 285 +++ .../PostHog/PostHog/PostHogRemoteConfig.swift | 636 +++++ Pods/PostHog/PostHog/PostHogSDK.swift | 1499 +++++++++++ .../PostHog/PostHogSessionManager.swift | 271 ++ Pods/PostHog/PostHog/PostHogStorage.swift | 419 +++ .../PostHog/PostHogStorageManager.swift | 163 ++ Pods/PostHog/PostHog/PostHogSwizzler.swift | 14 + Pods/PostHog/PostHog/PostHogVersion.swift | 16 + .../Replay/ApplicationEventPublisher.swift | 130 + .../PostHog/PostHog/Replay/CGColor+Util.swift | 33 + Pods/PostHog/PostHog/Replay/CGSize+Util.swift | 19 + Pods/PostHog/PostHog/Replay/Date+Util.swift | 18 + Pods/PostHog/PostHog/Replay/Float+Util.swift | 20 + .../PostHog/Replay/MethodSwizzler.swift | 120 + .../PostHog/Replay/NetworkSample.swift | 58 + .../PostHog/Replay/Optional+Util.swift | 12 + .../PostHogConsoleLogInterceptor.swift | 136 + .../Console Logs/PostHogLogEntry.swift | 37 + .../Console Logs/PostHogLogLevel.swift | 22 + ...ostHogSessionReplayConsoleLogsPlugin.swift | 86 + .../PostHogSessionReplayNetworkPlugin.swift | 89 + .../Plugins/Network/URLSessionExtension.swift | 231 ++ .../Network/URLSessionInterceptor.swift | 163 ++ .../Plugins/Network/URLSessionSwizzler.swift | 251 ++ .../Plugins/PostHogSessionReplayPlugin.swift | 43 + .../Replay/PostHogReplayIntegration.swift | 865 +++++++ .../Replay/PostHogSessionReplayConfig.swift | 93 + ...PostHogSessionReplayConsoleLogConfig.swift | 73 + Pods/PostHog/PostHog/Replay/RRStyle.swift | 96 + Pods/PostHog/PostHog/Replay/RRWireframe.swift | 133 + Pods/PostHog/PostHog/Replay/String+Util.swift | 14 + .../PostHog/PostHog/Replay/UIColor+Util.swift | 17 + .../PostHog/PostHog/Replay/UIImage+Util.swift | 33 + .../Replay/UITextInputTraits+Util.swift | 36 + Pods/PostHog/PostHog/Replay/UIView+Util.swift | 70 + .../Replay/ViewTreeSnapshotStatus.swift | 15 + .../PostHog/Resources/PrivacyInfo.xcprivacy | 44 + .../ApplicationScreenViewPublisher.swift | 172 ++ .../PostHogScreenViewIntegration.swift | 79 + .../PostHog/Surveys/BottomSection.swift | 23 + .../PostHog/Surveys/ConfirmationMessage.swift | 42 + .../Surveys/Models/PostHogDisplaySurvey.swift | 57 + .../PostHogDisplaySurveyAppearance.swift | 88 + .../Models/PostHogDisplaySurveyQuestion.swift | 150 ++ .../Models/PostHogNextSurveyQuestion.swift | 23 + .../Models/PostHogSurveyResponse.swift | 107 + .../Surveys/PostHogSurveyIntegration.swift | 937 +++++++ .../Surveys/PostHogSurveysConfig.swift | 53 + .../PostHogSurveysDefaultDelegate.swift | 70 + .../PostHog/Surveys/QuestionHeader.swift | 49 + .../PostHog/Surveys/QuestionTypes.swift | 243 ++ .../Surveys/SurveyDisplayController.swift | 56 + .../PostHog/PostHog/Surveys/SurveySheet.swift | 249 ++ .../PostHog/Surveys/SurveysRootView.swift | 44 + .../PostHog/Surveys/SurveysWindow.swift | 41 + .../PostHog/Surveys/Utils/EdgeBorder.swift | 26 + .../PostHog/Surveys/Utils/EmojiRating.swift | 115 + .../Surveys/Utils/MultipleChoiceOptions.swift | 160 ++ .../PostHog/Surveys/Utils/NumberRating.swift | 115 + .../PostHog/Surveys/Utils/Resources.swift | 431 ++++ .../Surveys/Utils/SegmentedControl.swift | 95 + .../PostHog/Surveys/Utils/Survey+Util.swift | 306 +++ .../PostHog/Surveys/Utils/SurveyButton.swift | 37 + ...rveyPresentationDetentsRepresentable.swift | 122 + .../PostHog/Surveys/Utils/SwiftUI+Util.swift | 105 + .../SwiftUI/PostHogMaskViewModifier.swift | 53 + .../SwiftUI/PostHogNoMaskViewModifier.swift | 54 + .../SwiftUI/PostHogSwiftUIViewModifiers.swift | 68 + .../SwiftUI/PostHogTagViewModifier.swift | 371 +++ Pods/PostHog/PostHog/UIViewController.swift | 26 + .../PostHog/Utils/AssociatedKeys.swift | 17 + Pods/PostHog/PostHog/Utils/Data+Gzip.swift | 301 +++ Pods/PostHog/PostHog/Utils/DateUtils.swift | 43 + Pods/PostHog/PostHog/Utils/DictUtils.swift | 43 + Pods/PostHog/PostHog/Utils/Errors.swift | 16 + Pods/PostHog/PostHog/Utils/FileUtils.swift | 33 + Pods/PostHog/PostHog/Utils/Hedgelog.swift | 20 + Pods/PostHog/PostHog/Utils/Reachability.swift | 406 +++ .../PostHog/PostHog/Utils/ReadWriteLock.swift | 67 + .../Utils/TimeBasedEpochGenerator.swift | 86 + .../PostHog/Utils/UIApplication+.swift | 43 + Pods/PostHog/PostHog/Utils/UIImage+WebP.swift | 138 + Pods/PostHog/PostHog/Utils/UIWindow+.swift | 50 + Pods/PostHog/PostHog/Utils/UUIDUtils.swift | 17 + Pods/PostHog/README.md | 15 + .../Pods-Cable-CableUITests-Info.plist | 26 + ...ble-CableUITests-acknowledgements.markdown | 28 + ...-Cable-CableUITests-acknowledgements.plist | 60 + .../Pods-Cable-CableUITests-dummy.m | 5 + ...ts-frameworks-Debug-input-files.xcfilelist | 2 + ...s-frameworks-Debug-output-files.xcfilelist | 1 + ...-frameworks-Release-input-files.xcfilelist | 2 + ...frameworks-Release-output-files.xcfilelist | 1 + .../Pods-Cable-CableUITests-frameworks.sh | 186 ++ .../Pods-Cable-CableUITests-umbrella.h | 16 + .../Pods-Cable-CableUITests.debug.xcconfig | 16 + .../Pods-Cable-CableUITests.modulemap | 6 + .../Pods-Cable-CableUITests.release.xcconfig | 16 + ...ds-Cable-CableUITestsScreenshot-Info.plist | 26 + ...ITestsScreenshot-acknowledgements.markdown | 28 + ...leUITestsScreenshot-acknowledgements.plist | 60 + .../Pods-Cable-CableUITestsScreenshot-dummy.m | 5 + ...ot-frameworks-Debug-input-files.xcfilelist | 2 + ...t-frameworks-Debug-output-files.xcfilelist | 1 + ...-frameworks-Release-input-files.xcfilelist | 2 + ...frameworks-Release-output-files.xcfilelist | 1 + ...Cable-CableUITestsScreenshot-frameworks.sh | 186 ++ ...ds-Cable-CableUITestsScreenshot-umbrella.h | 16 + ...able-CableUITestsScreenshot.debug.xcconfig | 16 + ...ods-Cable-CableUITestsScreenshot.modulemap | 6 + ...le-CableUITestsScreenshot.release.xcconfig | 16 + .../Pods-Cable/Pods-Cable-Info.plist | 26 + .../Pods-Cable-acknowledgements.markdown | 28 + .../Pods-Cable-acknowledgements.plist | 60 + .../Pods-Cable/Pods-Cable-dummy.m | 5 + ...le-frameworks-Debug-input-files.xcfilelist | 2 + ...e-frameworks-Debug-output-files.xcfilelist | 1 + ...-frameworks-Release-input-files.xcfilelist | 2 + ...frameworks-Release-output-files.xcfilelist | 1 + .../Pods-Cable/Pods-Cable-frameworks.sh | 186 ++ .../Pods-Cable/Pods-Cable-umbrella.h | 16 + .../Pods-Cable/Pods-Cable.debug.xcconfig | 16 + .../Pods-Cable/Pods-Cable.modulemap | 6 + .../Pods-Cable/Pods-Cable.release.xcconfig | 16 + .../Pods-CableTests-Info.plist | 26 + .../Pods-CableTests-acknowledgements.markdown | 3 + .../Pods-CableTests-acknowledgements.plist | 29 + .../Pods-CableTests/Pods-CableTests-dummy.m | 5 + .../Pods-CableTests-umbrella.h | 16 + .../Pods-CableTests.debug.xcconfig | 11 + .../Pods-CableTests/Pods-CableTests.modulemap | 6 + .../Pods-CableTests.release.xcconfig | 11 + .../PostHog/PostHog-Info.plist | 26 + .../PostHog/PostHog-dummy.m | 5 + .../PostHog/PostHog-prefix.pch | 12 + .../PostHog/PostHog-umbrella.h | 61 + .../PostHog/PostHog.debug.xcconfig | 15 + .../PostHog/PostHog.modulemap | 6 + .../PostHog/PostHog.release.xcconfig | 15 + .../ResourceBundle-PostHog-PostHog-Info.plist | 24 + 198 files changed, 21205 insertions(+), 262 deletions(-) create mode 100644 Cable.xcworkspace/contents.xcworkspacedata create mode 100644 Cable/AppDelegate.swift create mode 100644 Cable/StatsHeaderContainer.swift create mode 100644 Cable/StoreKitManager.swift create mode 100644 Podfile create mode 100644 Podfile.lock create mode 100644 Pods/Manifest.lock create mode 100644 Pods/Pods.xcodeproj/project.pbxproj create mode 100644 Pods/Pods.xcodeproj/xcuserdata/lange-hegermann.xcuserdatad/xcschemes/Pods-Cable-CableUITests.xcscheme create mode 100644 Pods/Pods.xcodeproj/xcuserdata/lange-hegermann.xcuserdatad/xcschemes/Pods-Cable-CableUITestsScreenshot.xcscheme create mode 100644 Pods/Pods.xcodeproj/xcuserdata/lange-hegermann.xcuserdatad/xcschemes/Pods-Cable.xcscheme create mode 100644 Pods/Pods.xcodeproj/xcuserdata/lange-hegermann.xcuserdatad/xcschemes/Pods-CableTests.xcscheme create mode 100644 Pods/Pods.xcodeproj/xcuserdata/lange-hegermann.xcuserdatad/xcschemes/PostHog-PostHog.xcscheme create mode 100644 Pods/Pods.xcodeproj/xcuserdata/lange-hegermann.xcuserdatad/xcschemes/PostHog.xcscheme create mode 100644 Pods/Pods.xcodeproj/xcuserdata/lange-hegermann.xcuserdatad/xcschemes/xcschememanagement.plist create mode 100644 Pods/PostHog/LICENSE create mode 100644 Pods/PostHog/PostHog/App Life Cycle/ApplicationLifecyclePublisher.swift create mode 100644 Pods/PostHog/PostHog/App Life Cycle/PostHogAppLifeCycleIntegration.swift create mode 100644 Pods/PostHog/PostHog/ApplicationViewLayoutPublisher.swift create mode 100644 Pods/PostHog/PostHog/Autocapture/AutocaptureEventProcessing.swift create mode 100644 Pods/PostHog/PostHog/Autocapture/ForwardingPickerViewDelegate.swift create mode 100644 Pods/PostHog/PostHog/Autocapture/PostHogAutocaptureEventTracker.swift create mode 100644 Pods/PostHog/PostHog/Autocapture/PostHogAutocaptureIntegration.swift create mode 100644 Pods/PostHog/PostHog/Autocapture/SwiftUI/View+PostHogLabel.swift create mode 100644 Pods/PostHog/PostHog/Autocapture/UIView+PostHogLabel.swift create mode 100644 Pods/PostHog/PostHog/DI.swift create mode 100644 Pods/PostHog/PostHog/Models/PostHogEvent.swift create mode 100644 Pods/PostHog/PostHog/Models/Surveys/PostHogSurvey+Display.swift create mode 100644 Pods/PostHog/PostHog/Models/Surveys/PostHogSurvey.swift create mode 100644 Pods/PostHog/PostHog/Models/Surveys/PostHogSurveyAppearance.swift create mode 100644 Pods/PostHog/PostHog/Models/Surveys/PostHogSurveyConditions.swift create mode 100644 Pods/PostHog/PostHog/Models/Surveys/PostHogSurveyEnums.swift create mode 100644 Pods/PostHog/PostHog/Models/Surveys/PostHogSurveyQuestion.swift create mode 100644 Pods/PostHog/PostHog/PostHog.h create mode 100644 Pods/PostHog/PostHog/PostHogApi.swift create mode 100644 Pods/PostHog/PostHog/PostHogBatchUploadInfo.swift create mode 100644 Pods/PostHog/PostHog/PostHogConfig.swift create mode 100644 Pods/PostHog/PostHog/PostHogConsumerPayload.swift create mode 100644 Pods/PostHog/PostHog/PostHogContext.swift create mode 100644 Pods/PostHog/PostHog/PostHogExtensions.swift create mode 100644 Pods/PostHog/PostHog/PostHogFileBackedQueue.swift create mode 100644 Pods/PostHog/PostHog/PostHogIntegration.swift create mode 100644 Pods/PostHog/PostHog/PostHogLegacyQueue.swift create mode 100644 Pods/PostHog/PostHog/PostHogPersonProfiles.swift create mode 100644 Pods/PostHog/PostHog/PostHogPropertiesSanitizer.swift create mode 100644 Pods/PostHog/PostHog/PostHogQueue.swift create mode 100644 Pods/PostHog/PostHog/PostHogRemoteConfig.swift create mode 100644 Pods/PostHog/PostHog/PostHogSDK.swift create mode 100644 Pods/PostHog/PostHog/PostHogSessionManager.swift create mode 100644 Pods/PostHog/PostHog/PostHogStorage.swift create mode 100644 Pods/PostHog/PostHog/PostHogStorageManager.swift create mode 100644 Pods/PostHog/PostHog/PostHogSwizzler.swift create mode 100644 Pods/PostHog/PostHog/PostHogVersion.swift create mode 100644 Pods/PostHog/PostHog/Replay/ApplicationEventPublisher.swift create mode 100644 Pods/PostHog/PostHog/Replay/CGColor+Util.swift create mode 100644 Pods/PostHog/PostHog/Replay/CGSize+Util.swift create mode 100644 Pods/PostHog/PostHog/Replay/Date+Util.swift create mode 100644 Pods/PostHog/PostHog/Replay/Float+Util.swift create mode 100644 Pods/PostHog/PostHog/Replay/MethodSwizzler.swift create mode 100644 Pods/PostHog/PostHog/Replay/NetworkSample.swift create mode 100644 Pods/PostHog/PostHog/Replay/Optional+Util.swift create mode 100644 Pods/PostHog/PostHog/Replay/Plugins/Console Logs/PostHogConsoleLogInterceptor.swift create mode 100644 Pods/PostHog/PostHog/Replay/Plugins/Console Logs/PostHogLogEntry.swift create mode 100644 Pods/PostHog/PostHog/Replay/Plugins/Console Logs/PostHogLogLevel.swift create mode 100644 Pods/PostHog/PostHog/Replay/Plugins/Console Logs/PostHogSessionReplayConsoleLogsPlugin.swift create mode 100644 Pods/PostHog/PostHog/Replay/Plugins/Network/PostHogSessionReplayNetworkPlugin.swift create mode 100644 Pods/PostHog/PostHog/Replay/Plugins/Network/URLSessionExtension.swift create mode 100644 Pods/PostHog/PostHog/Replay/Plugins/Network/URLSessionInterceptor.swift create mode 100644 Pods/PostHog/PostHog/Replay/Plugins/Network/URLSessionSwizzler.swift create mode 100644 Pods/PostHog/PostHog/Replay/Plugins/PostHogSessionReplayPlugin.swift create mode 100644 Pods/PostHog/PostHog/Replay/PostHogReplayIntegration.swift create mode 100644 Pods/PostHog/PostHog/Replay/PostHogSessionReplayConfig.swift create mode 100644 Pods/PostHog/PostHog/Replay/PostHogSessionReplayConsoleLogConfig.swift create mode 100644 Pods/PostHog/PostHog/Replay/RRStyle.swift create mode 100644 Pods/PostHog/PostHog/Replay/RRWireframe.swift create mode 100644 Pods/PostHog/PostHog/Replay/String+Util.swift create mode 100644 Pods/PostHog/PostHog/Replay/UIColor+Util.swift create mode 100644 Pods/PostHog/PostHog/Replay/UIImage+Util.swift create mode 100644 Pods/PostHog/PostHog/Replay/UITextInputTraits+Util.swift create mode 100644 Pods/PostHog/PostHog/Replay/UIView+Util.swift create mode 100644 Pods/PostHog/PostHog/Replay/ViewTreeSnapshotStatus.swift create mode 100644 Pods/PostHog/PostHog/Resources/PrivacyInfo.xcprivacy create mode 100644 Pods/PostHog/PostHog/Screen Views/ApplicationScreenViewPublisher.swift create mode 100644 Pods/PostHog/PostHog/Screen Views/PostHogScreenViewIntegration.swift create mode 100644 Pods/PostHog/PostHog/Surveys/BottomSection.swift create mode 100644 Pods/PostHog/PostHog/Surveys/ConfirmationMessage.swift create mode 100644 Pods/PostHog/PostHog/Surveys/Models/PostHogDisplaySurvey.swift create mode 100644 Pods/PostHog/PostHog/Surveys/Models/PostHogDisplaySurveyAppearance.swift create mode 100644 Pods/PostHog/PostHog/Surveys/Models/PostHogDisplaySurveyQuestion.swift create mode 100644 Pods/PostHog/PostHog/Surveys/Models/PostHogNextSurveyQuestion.swift create mode 100644 Pods/PostHog/PostHog/Surveys/Models/PostHogSurveyResponse.swift create mode 100644 Pods/PostHog/PostHog/Surveys/PostHogSurveyIntegration.swift create mode 100644 Pods/PostHog/PostHog/Surveys/PostHogSurveysConfig.swift create mode 100644 Pods/PostHog/PostHog/Surveys/PostHogSurveysDefaultDelegate.swift create mode 100644 Pods/PostHog/PostHog/Surveys/QuestionHeader.swift create mode 100644 Pods/PostHog/PostHog/Surveys/QuestionTypes.swift create mode 100644 Pods/PostHog/PostHog/Surveys/SurveyDisplayController.swift create mode 100644 Pods/PostHog/PostHog/Surveys/SurveySheet.swift create mode 100644 Pods/PostHog/PostHog/Surveys/SurveysRootView.swift create mode 100644 Pods/PostHog/PostHog/Surveys/SurveysWindow.swift create mode 100644 Pods/PostHog/PostHog/Surveys/Utils/EdgeBorder.swift create mode 100644 Pods/PostHog/PostHog/Surveys/Utils/EmojiRating.swift create mode 100644 Pods/PostHog/PostHog/Surveys/Utils/MultipleChoiceOptions.swift create mode 100644 Pods/PostHog/PostHog/Surveys/Utils/NumberRating.swift create mode 100644 Pods/PostHog/PostHog/Surveys/Utils/Resources.swift create mode 100644 Pods/PostHog/PostHog/Surveys/Utils/SegmentedControl.swift create mode 100644 Pods/PostHog/PostHog/Surveys/Utils/Survey+Util.swift create mode 100644 Pods/PostHog/PostHog/Surveys/Utils/SurveyButton.swift create mode 100644 Pods/PostHog/PostHog/Surveys/Utils/SurveyPresentationDetentsRepresentable.swift create mode 100644 Pods/PostHog/PostHog/Surveys/Utils/SwiftUI+Util.swift create mode 100644 Pods/PostHog/PostHog/SwiftUI/PostHogMaskViewModifier.swift create mode 100644 Pods/PostHog/PostHog/SwiftUI/PostHogNoMaskViewModifier.swift create mode 100644 Pods/PostHog/PostHog/SwiftUI/PostHogSwiftUIViewModifiers.swift create mode 100644 Pods/PostHog/PostHog/SwiftUI/PostHogTagViewModifier.swift create mode 100644 Pods/PostHog/PostHog/UIViewController.swift create mode 100644 Pods/PostHog/PostHog/Utils/AssociatedKeys.swift create mode 100644 Pods/PostHog/PostHog/Utils/Data+Gzip.swift create mode 100644 Pods/PostHog/PostHog/Utils/DateUtils.swift create mode 100644 Pods/PostHog/PostHog/Utils/DictUtils.swift create mode 100644 Pods/PostHog/PostHog/Utils/Errors.swift create mode 100644 Pods/PostHog/PostHog/Utils/FileUtils.swift create mode 100644 Pods/PostHog/PostHog/Utils/Hedgelog.swift create mode 100644 Pods/PostHog/PostHog/Utils/Reachability.swift create mode 100644 Pods/PostHog/PostHog/Utils/ReadWriteLock.swift create mode 100644 Pods/PostHog/PostHog/Utils/TimeBasedEpochGenerator.swift create mode 100644 Pods/PostHog/PostHog/Utils/UIApplication+.swift create mode 100644 Pods/PostHog/PostHog/Utils/UIImage+WebP.swift create mode 100644 Pods/PostHog/PostHog/Utils/UIWindow+.swift create mode 100644 Pods/PostHog/PostHog/Utils/UUIDUtils.swift create mode 100644 Pods/PostHog/README.md create mode 100644 Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests-Info.plist create mode 100644 Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests-acknowledgements.markdown create mode 100644 Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests-acknowledgements.plist create mode 100644 Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests-dummy.m create mode 100644 Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests-frameworks-Debug-input-files.xcfilelist create mode 100644 Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests-frameworks-Debug-output-files.xcfilelist create mode 100644 Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests-frameworks-Release-input-files.xcfilelist create mode 100644 Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests-frameworks-Release-output-files.xcfilelist create mode 100755 Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests-frameworks.sh create mode 100644 Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests-umbrella.h create mode 100644 Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests.debug.xcconfig create mode 100644 Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests.modulemap create mode 100644 Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests.release.xcconfig create mode 100644 Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot-Info.plist create mode 100644 Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot-acknowledgements.markdown create mode 100644 Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot-acknowledgements.plist create mode 100644 Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot-dummy.m create mode 100644 Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot-frameworks-Debug-input-files.xcfilelist create mode 100644 Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot-frameworks-Debug-output-files.xcfilelist create mode 100644 Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot-frameworks-Release-input-files.xcfilelist create mode 100644 Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot-frameworks-Release-output-files.xcfilelist create mode 100755 Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot-frameworks.sh create mode 100644 Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot-umbrella.h create mode 100644 Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot.debug.xcconfig create mode 100644 Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot.modulemap create mode 100644 Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot.release.xcconfig create mode 100644 Pods/Target Support Files/Pods-Cable/Pods-Cable-Info.plist create mode 100644 Pods/Target Support Files/Pods-Cable/Pods-Cable-acknowledgements.markdown create mode 100644 Pods/Target Support Files/Pods-Cable/Pods-Cable-acknowledgements.plist create mode 100644 Pods/Target Support Files/Pods-Cable/Pods-Cable-dummy.m create mode 100644 Pods/Target Support Files/Pods-Cable/Pods-Cable-frameworks-Debug-input-files.xcfilelist create mode 100644 Pods/Target Support Files/Pods-Cable/Pods-Cable-frameworks-Debug-output-files.xcfilelist create mode 100644 Pods/Target Support Files/Pods-Cable/Pods-Cable-frameworks-Release-input-files.xcfilelist create mode 100644 Pods/Target Support Files/Pods-Cable/Pods-Cable-frameworks-Release-output-files.xcfilelist create mode 100755 Pods/Target Support Files/Pods-Cable/Pods-Cable-frameworks.sh create mode 100644 Pods/Target Support Files/Pods-Cable/Pods-Cable-umbrella.h create mode 100644 Pods/Target Support Files/Pods-Cable/Pods-Cable.debug.xcconfig create mode 100644 Pods/Target Support Files/Pods-Cable/Pods-Cable.modulemap create mode 100644 Pods/Target Support Files/Pods-Cable/Pods-Cable.release.xcconfig create mode 100644 Pods/Target Support Files/Pods-CableTests/Pods-CableTests-Info.plist create mode 100644 Pods/Target Support Files/Pods-CableTests/Pods-CableTests-acknowledgements.markdown create mode 100644 Pods/Target Support Files/Pods-CableTests/Pods-CableTests-acknowledgements.plist create mode 100644 Pods/Target Support Files/Pods-CableTests/Pods-CableTests-dummy.m create mode 100644 Pods/Target Support Files/Pods-CableTests/Pods-CableTests-umbrella.h create mode 100644 Pods/Target Support Files/Pods-CableTests/Pods-CableTests.debug.xcconfig create mode 100644 Pods/Target Support Files/Pods-CableTests/Pods-CableTests.modulemap create mode 100644 Pods/Target Support Files/Pods-CableTests/Pods-CableTests.release.xcconfig create mode 100644 Pods/Target Support Files/PostHog/PostHog-Info.plist create mode 100644 Pods/Target Support Files/PostHog/PostHog-dummy.m create mode 100644 Pods/Target Support Files/PostHog/PostHog-prefix.pch create mode 100644 Pods/Target Support Files/PostHog/PostHog-umbrella.h create mode 100644 Pods/Target Support Files/PostHog/PostHog.debug.xcconfig create mode 100644 Pods/Target Support Files/PostHog/PostHog.modulemap create mode 100644 Pods/Target Support Files/PostHog/PostHog.release.xcconfig create mode 100644 Pods/Target Support Files/PostHog/ResourceBundle-PostHog-PostHog-Info.plist diff --git a/Cable.xcodeproj/project.pbxproj b/Cable.xcodeproj/project.pbxproj index c9efbf7..5d9a343 100644 --- a/Cable.xcodeproj/project.pbxproj +++ b/Cable.xcodeproj/project.pbxproj @@ -6,6 +6,13 @@ objectVersion = 77; objects = { +/* Begin PBXBuildFile section */ + 0DBC1CAB8BE5C690AE39630C /* Pods_Cable.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2B59C6617C5C88811F972C70 /* Pods_Cable.framework */; }; + 156FA26BC2A070D3E79DBC53 /* Pods_Cable_CableUITestsScreenshot.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 838FF4DAE13C48C1BCC63760 /* Pods_Cable_CableUITestsScreenshot.framework */; }; + 4472B945421CAB58A81AAF03 /* Pods_Cable_CableUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BCB07623E3B49D249C01C67E /* Pods_Cable_CableUITests.framework */; }; + 85A2E22A9DF253A619C833B2 /* Pods_CableTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 251C4DF01338D1FECB418EE7 /* Pods_CableTests.framework */; }; +/* End PBXBuildFile section */ + /* Begin PBXContainerItemProxy section */ 3E37F65B2E93FB6F00836187 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; @@ -31,10 +38,22 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 0A51CE1631634DF868118C1B /* Pods-CableTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CableTests.debug.xcconfig"; path = "Target Support Files/Pods-CableTests/Pods-CableTests.debug.xcconfig"; sourceTree = ""; }; + 10D59C9B7039F7390CB71DAA /* Pods-CableTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CableTests.release.xcconfig"; path = "Target Support Files/Pods-CableTests/Pods-CableTests.release.xcconfig"; sourceTree = ""; }; + 251C4DF01338D1FECB418EE7 /* Pods_CableTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_CableTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 2B59C6617C5C88811F972C70 /* Pods_Cable.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Cable.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 340C908BC5784DC053266DDB /* Pods-Cable-CableUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Cable-CableUITests.debug.xcconfig"; path = "Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests.debug.xcconfig"; sourceTree = ""; }; 3E37F6552E93FB6F00836187 /* CableUITestsScreenshot.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CableUITestsScreenshot.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3E5C0BCC2E72C0FD00247EC8 /* Cable.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Cable.app; sourceTree = BUILT_PRODUCTS_DIR; }; 3E5C0BDD2E72C0FE00247EC8 /* CableTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CableTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3E5C0BE72E72C0FE00247EC8 /* CableUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CableUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 4017B33DF440FA2BC612E06E /* Pods-Cable-CableUITestsScreenshot.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Cable-CableUITestsScreenshot.release.xcconfig"; path = "Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot.release.xcconfig"; sourceTree = ""; }; + 838FF4DAE13C48C1BCC63760 /* Pods_Cable_CableUITestsScreenshot.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Cable_CableUITestsScreenshot.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 83D6CB62ED3959EC1EC8027D /* Pods-Cable-CableUITestsScreenshot.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Cable-CableUITestsScreenshot.debug.xcconfig"; path = "Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot.debug.xcconfig"; sourceTree = ""; }; + B5E79A38FD11ED9D9A21BB7E /* Pods-Cable.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Cable.debug.xcconfig"; path = "Target Support Files/Pods-Cable/Pods-Cable.debug.xcconfig"; sourceTree = ""; }; + BCB07623E3B49D249C01C67E /* Pods_Cable_CableUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Cable_CableUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + BED2A9D04FDB84725E0725E9 /* Pods-Cable.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Cable.release.xcconfig"; path = "Target Support Files/Pods-Cable/Pods-Cable.release.xcconfig"; sourceTree = ""; }; + F4D8F0C9760202BC765B4260 /* Pods-Cable-CableUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Cable-CableUITests.release.xcconfig"; path = "Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ @@ -78,6 +97,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 156FA26BC2A070D3E79DBC53 /* Pods_Cable_CableUITestsScreenshot.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -85,6 +105,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 0DBC1CAB8BE5C690AE39630C /* Pods_Cable.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -92,6 +113,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 85A2E22A9DF253A619C833B2 /* Pods_CableTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -99,6 +121,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 4472B945421CAB58A81AAF03 /* Pods_Cable_CableUITests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -113,6 +136,8 @@ 3E5C0BEA2E72C0FE00247EC8 /* CableUITests */, 3E37F6562E93FB6F00836187 /* CableUITestsScreenshot */, 3E5C0BCD2E72C0FD00247EC8 /* Products */, + 57738E9B07763CFA62681EEE /* Pods */, + 9D16D1FE8C8B34C13C51D389 /* Frameworks */, ); sourceTree = ""; }; @@ -127,6 +152,32 @@ name = Products; sourceTree = ""; }; + 57738E9B07763CFA62681EEE /* Pods */ = { + isa = PBXGroup; + children = ( + B5E79A38FD11ED9D9A21BB7E /* Pods-Cable.debug.xcconfig */, + BED2A9D04FDB84725E0725E9 /* Pods-Cable.release.xcconfig */, + 340C908BC5784DC053266DDB /* Pods-Cable-CableUITests.debug.xcconfig */, + F4D8F0C9760202BC765B4260 /* Pods-Cable-CableUITests.release.xcconfig */, + 83D6CB62ED3959EC1EC8027D /* Pods-Cable-CableUITestsScreenshot.debug.xcconfig */, + 4017B33DF440FA2BC612E06E /* Pods-Cable-CableUITestsScreenshot.release.xcconfig */, + 0A51CE1631634DF868118C1B /* Pods-CableTests.debug.xcconfig */, + 10D59C9B7039F7390CB71DAA /* Pods-CableTests.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + 9D16D1FE8C8B34C13C51D389 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 2B59C6617C5C88811F972C70 /* Pods_Cable.framework */, + BCB07623E3B49D249C01C67E /* Pods_Cable_CableUITests.framework */, + 838FF4DAE13C48C1BCC63760 /* Pods_Cable_CableUITestsScreenshot.framework */, + 251C4DF01338D1FECB418EE7 /* Pods_CableTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -134,9 +185,11 @@ isa = PBXNativeTarget; buildConfigurationList = 3E37F65D2E93FB6F00836187 /* Build configuration list for PBXNativeTarget "CableUITestsScreenshot" */; buildPhases = ( + ECF8C5947A59DAC9118AE4F4 /* [CP] Check Pods Manifest.lock */, 3E37F6512E93FB6F00836187 /* Sources */, 3E37F6522E93FB6F00836187 /* Frameworks */, 3E37F6532E93FB6F00836187 /* Resources */, + 611809BC8E1F9DF30E9C4629 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -147,8 +200,6 @@ 3E37F6562E93FB6F00836187 /* CableUITestsScreenshot */, ); name = CableUITestsScreenshot; - packageProductDependencies = ( - ); productName = CableUITestsScreenshot; productReference = 3E37F6552E93FB6F00836187 /* CableUITestsScreenshot.xctest */; productType = "com.apple.product-type.bundle.ui-testing"; @@ -157,9 +208,11 @@ isa = PBXNativeTarget; buildConfigurationList = 3E5C0BF02E72C0FE00247EC8 /* Build configuration list for PBXNativeTarget "Cable" */; buildPhases = ( + 3585B809C20C4D3B1FE82C78 /* [CP] Check Pods Manifest.lock */, 3E5C0BC82E72C0FD00247EC8 /* Sources */, 3E5C0BC92E72C0FD00247EC8 /* Frameworks */, 3E5C0BCA2E72C0FD00247EC8 /* Resources */, + E8C196B44C4F00DA4E300C55 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -169,8 +222,6 @@ 3E5C0BCE2E72C0FD00247EC8 /* Cable */, ); name = Cable; - packageProductDependencies = ( - ); productName = Cable; productReference = 3E5C0BCC2E72C0FD00247EC8 /* Cable.app */; productType = "com.apple.product-type.application"; @@ -179,6 +230,7 @@ isa = PBXNativeTarget; buildConfigurationList = 3E5C0BF52E72C0FE00247EC8 /* Build configuration list for PBXNativeTarget "CableTests" */; buildPhases = ( + 3D80694CE29BD68AE168E8DF /* [CP] Check Pods Manifest.lock */, 3E5C0BD92E72C0FE00247EC8 /* Sources */, 3E5C0BDA2E72C0FE00247EC8 /* Frameworks */, 3E5C0BDB2E72C0FE00247EC8 /* Resources */, @@ -192,8 +244,6 @@ 3E5C0BE02E72C0FE00247EC8 /* CableTests */, ); name = CableTests; - packageProductDependencies = ( - ); productName = CableTests; productReference = 3E5C0BDD2E72C0FE00247EC8 /* CableTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; @@ -202,9 +252,11 @@ isa = PBXNativeTarget; buildConfigurationList = 3E5C0BF82E72C0FE00247EC8 /* Build configuration list for PBXNativeTarget "CableUITests" */; buildPhases = ( + D1A689F595A65E0530AAACB0 /* [CP] Check Pods Manifest.lock */, 3E5C0BE32E72C0FE00247EC8 /* Sources */, 3E5C0BE42E72C0FE00247EC8 /* Frameworks */, 3E5C0BE52E72C0FE00247EC8 /* Resources */, + 3808009BC0D951592701EA88 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -215,8 +267,6 @@ 3E5C0BEA2E72C0FE00247EC8 /* CableUITests */, ); name = CableUITests; - packageProductDependencies = ( - ); productName = CableUITests; productReference = 3E5C0BE72E72C0FE00247EC8 /* CableUITests.xctest */; productType = "com.apple.product-type.bundle.ui-testing"; @@ -305,6 +355,160 @@ }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + 3585B809C20C4D3B1FE82C78 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Cable-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 3808009BC0D951592701EA88 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 3D80694CE29BD68AE168E8DF /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-CableTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 611809BC8E1F9DF30E9C4629 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + D1A689F595A65E0530AAACB0 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Cable-CableUITests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + E8C196B44C4F00DA4E300C55 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Cable/Pods-Cable-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Cable/Pods-Cable-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Cable/Pods-Cable-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + ECF8C5947A59DAC9118AE4F4 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Cable-CableUITestsScreenshot-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ 3E37F6512E93FB6F00836187 /* Sources */ = { isa = PBXSourcesBuildPhase; @@ -357,6 +561,7 @@ /* Begin XCBuildConfiguration section */ 3E37F65E2E93FB6F00836187 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 83D6CB62ED3959EC1EC8027D /* Pods-Cable-CableUITestsScreenshot.debug.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; @@ -378,6 +583,7 @@ }; 3E37F65F2E93FB6F00836187 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 4017B33DF440FA2BC612E06E /* Pods-Cable-CableUITestsScreenshot.release.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; @@ -399,16 +605,18 @@ }; 3E5C0BF12E72C0FE00247EC8 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = B5E79A38FD11ED9D9A21BB7E /* Pods-Cable.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = Cable/Cable.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 34; + CURRENT_PROJECT_VERSION = 39; DEVELOPMENT_TEAM = RE4FXQ754N; ENABLE_APP_SANDBOX = YES; ENABLE_PREVIEWS = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; ENABLE_USER_SELECTED_FILES = readonly; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Cable/Info.plist; @@ -434,16 +642,18 @@ }; 3E5C0BF22E72C0FE00247EC8 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = BED2A9D04FDB84725E0725E9 /* Pods-Cable.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = Cable/Cable.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 34; + CURRENT_PROJECT_VERSION = 39; DEVELOPMENT_TEAM = RE4FXQ754N; ENABLE_APP_SANDBOX = YES; ENABLE_PREVIEWS = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; ENABLE_USER_SELECTED_FILES = readonly; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Cable/Info.plist; @@ -592,6 +802,7 @@ }; 3E5C0BF62E72C0FE00247EC8 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 0A51CE1631634DF868118C1B /* Pods-CableTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -611,6 +822,7 @@ }; 3E5C0BF72E72C0FE00247EC8 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 10D59C9B7039F7390CB71DAA /* Pods-CableTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -630,6 +842,7 @@ }; 3E5C0BF92E72C0FE00247EC8 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 340C908BC5784DC053266DDB /* Pods-Cable-CableUITests.debug.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; @@ -647,6 +860,7 @@ }; 3E5C0BFA2E72C0FE00247EC8 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = F4D8F0C9760202BC765B4260 /* Pods-Cable-CableUITests.release.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; diff --git a/Cable.xcworkspace/contents.xcworkspacedata b/Cable.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..7552daa --- /dev/null +++ b/Cable.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/Cable/AppDelegate.swift b/Cable/AppDelegate.swift new file mode 100644 index 0000000..a8ee51e --- /dev/null +++ b/Cable/AppDelegate.swift @@ -0,0 +1,24 @@ +// +// AppDelegate.swift +// Cable +// +// Created by Stefan Lange-Hegermann on 01.11.25. +// + + +import Foundation +import PostHog +import UIKit + +class AppDelegate: NSObject, UIApplicationDelegate { + func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { + let POSTHOG_API_KEY = "phc_icZY61N3vdg4Sr3lzz9DNAqCRh6hCorVJbytduWORO9" + let POSTHOG_HOST = "https://eu.i.posthog.com" + + let config = PostHogConfig(apiKey: POSTHOG_API_KEY, host: POSTHOG_HOST) + + PostHogSDK.shared.setup(config) + NSLog("Launched") + return true + } +} diff --git a/Cable/AppIcon.icon/icon.json b/Cable/AppIcon.icon/icon.json index 111641b..b35405b 100644 --- a/Cable/AppIcon.icon/icon.json +++ b/Cable/AppIcon.icon/icon.json @@ -44,7 +44,7 @@ "fill-specializations" : [ { "value" : { - "solid" : "display-p3:0.31765,0.56494,0.59766,1.00000" + "solid" : "display-p3:0.31765,0.56471,0.59608,1.00000" } }, { diff --git a/Cable/Batteries/BatteriesView.swift b/Cable/Batteries/BatteriesView.swift index 4c55f5c..a67aa0d 100644 --- a/Cable/Batteries/BatteriesView.swift +++ b/Cable/Batteries/BatteriesView.swift @@ -154,26 +154,7 @@ struct BatteriesView: View { emptyState .frame(maxWidth: .infinity, maxHeight: .infinity) } else { - summarySection - - List { - ForEach(batteries) { battery in - Button { - onEdit(battery) - } label: { - batteryRow(for: battery) - } - .buttonStyle(.plain) - .disabled(editMode == .active) - .listRowInsets(.init(top: 12, leading: 16, bottom: 12, trailing: 16)) - .listRowSeparator(.hidden) - .listRowBackground(Color.clear) - } - .onDelete(perform: onDelete) - } - .listStyle(.plain) - .scrollContentBackground(.hidden) - .environment(\.editMode, $editMode) + batteriesListWithHeader } } .frame(maxWidth: .infinity, maxHeight: .infinity) @@ -197,7 +178,13 @@ struct BatteriesView: View { } } - private var summarySection: some View { + private var batteryStatsHeader: some View { + StatsHeaderContainer { + batterySummaryContent + } + } + + private var batterySummaryContent: some View { VStack(spacing: 0) { VStack(alignment: .leading, spacing: 10) { HStack(alignment: .firstTextBaseline) { @@ -225,15 +212,47 @@ struct BatteriesView: View { .buttonStyle(.plain) } } - .padding(.horizontal, 16) - .padding(.vertical, 10) - .background(Color(.systemGroupedBackground)) - Divider() - .background(Color(.separator)) } } + @ViewBuilder + private var batteriesListWithHeader: some View { + if #available(iOS 26.0, *) { + baseBatteriesList + .scrollEdgeEffectStyle(.soft, for: .top) + .safeAreaInset(edge: .top, spacing: 0) { + batteryStatsHeader + } + } else { + baseBatteriesList + .safeAreaInset(edge: .top, spacing: 0) { + batteryStatsHeader + } + } + } + + private var baseBatteriesList: some View { + List { + ForEach(batteries) { battery in + Button { + onEdit(battery) + } label: { + batteryRow(for: battery) + } + .buttonStyle(.plain) + .disabled(editMode == .active) + .listRowInsets(.init(top: 12, leading: 16, bottom: 12, trailing: 16)) + .listRowSeparator(.hidden) + .listRowBackground(Color.clear) + } + .onDelete(perform: onDelete) + } + .listStyle(.plain) + .scrollContentBackground(.hidden) + .environment(\.editMode, $editMode) + } + private func batteryRow(for battery: SavedBattery) -> some View { VStack(alignment: .leading, spacing: 14) { HStack(spacing: 12) { diff --git a/Cable/Batteries/BatteryEditorView.swift b/Cable/Batteries/BatteryEditorView.swift index cd5aceb..2415521 100644 --- a/Cable/Batteries/BatteryEditorView.swift +++ b/Cable/Batteries/BatteryEditorView.swift @@ -9,6 +9,7 @@ struct BatteryEditorView: View { @State private var minimumTemperatureInput: String = "" @State private var maximumTemperatureInput: String = "" @State private var showingAppearanceEditor = false + @EnvironmentObject private var storeKitManager: StoreKitManager @State private var hasActiveProSubscription = false let onSave: (BatteryConfiguration) -> Void @@ -532,7 +533,10 @@ struct BatteryEditorView: View { CableProPaywallView(isPresented: $showingProUpsell) } .task { - hasActiveProSubscription = (await SettingsView.fetchProStatus()) != nil + hasActiveProSubscription = storeKitManager.isProUnlocked + } + .onReceive(storeKitManager.$status) { _ in + hasActiveProSubscription = storeKitManager.isProUnlocked } .alert( NSLocalizedString( diff --git a/Cable/CableApp.swift b/Cable/CableApp.swift index 969d6df..b651c32 100644 --- a/Cable/CableApp.swift +++ b/Cable/CableApp.swift @@ -10,7 +10,9 @@ import SwiftData @main struct CableApp: App { - @StateObject private var unitSettings = UnitSystemSettings() + @UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate + @StateObject private var unitSettings: UnitSystemSettings + @StateObject private var storeKitManager: StoreKitManager var sharedModelContainer: ModelContainer = { do { @@ -31,6 +33,9 @@ struct CableApp: App { }() init() { + let unitSettings = UnitSystemSettings() + _unitSettings = StateObject(wrappedValue: unitSettings) + _storeKitManager = StateObject(wrappedValue: StoreKitManager(unitSettings: unitSettings)) #if DEBUG UITestSampleData.prepareIfNeeded(container: sharedModelContainer) #endif @@ -40,6 +45,7 @@ struct CableApp: App { WindowGroup { ContentView() .environmentObject(unitSettings) + .environmentObject(storeKitManager) } .modelContainer(sharedModelContainer) } diff --git a/Cable/Chargers/ChargersView.swift b/Cable/Chargers/ChargersView.swift index 2a4ef20..603aa5c 100644 --- a/Cable/Chargers/ChargersView.swift +++ b/Cable/Chargers/ChargersView.swift @@ -110,34 +110,20 @@ struct ChargersView: View { emptyState .frame(maxWidth: .infinity, maxHeight: .infinity) } else { - summarySection - - List { - ForEach(chargers) { charger in - Button { - onEdit(charger) - } label: { - chargerRow(for: charger) - } - .buttonStyle(.plain) - .disabled(editMode == .active) - .listRowInsets(.init(top: 12, leading: 16, bottom: 12, trailing: 16)) - .listRowSeparator(.hidden) - .listRowBackground(Color.clear) - } - .onDelete(perform: onDelete) - } - .listStyle(.plain) - .scrollContentBackground(.hidden) - .environment(\.editMode, $editMode) - .accessibilityIdentifier("chargers-list") + chargersListWithHeader } } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color(.systemGroupedBackground)) } - private var summarySection: some View { + private var chargerStatsHeader: some View { + StatsHeaderContainer { + chargerSummaryContent + } + } + + private var chargerSummaryContent: some View { VStack(spacing: 0) { VStack(alignment: .leading, spacing: 10) { HStack(alignment: .firstTextBaseline) { @@ -157,18 +143,50 @@ struct ChargersView: View { ) } } - .padding(.trailing, 16) + .padding(.horizontal, 2) } - .scrollClipDisabled(true) + .scrollClipDisabled(false) } - .padding(.horizontal, 16) - .padding(.vertical, 10) - Divider() - .background(Color(.separator)) - .padding(.leading, 0) } - .background(Color(.systemGroupedBackground)) + } + + @ViewBuilder + private var chargersListWithHeader: some View { + if #available(iOS 26.0, *) { + baseChargersList + .scrollEdgeEffectStyle(.soft, for: .top) + .safeAreaInset(edge: .top, spacing: 0) { + chargerStatsHeader + } + } else { + baseChargersList + .safeAreaInset(edge: .top, spacing: 0) { + chargerStatsHeader + } + } + } + + private var baseChargersList: some View { + List { + ForEach(chargers) { charger in + Button { + onEdit(charger) + } label: { + chargerRow(for: charger) + } + .buttonStyle(.plain) + .disabled(editMode == .active) + .listRowInsets(.init(top: 12, leading: 16, bottom: 12, trailing: 16)) + .listRowSeparator(.hidden) + .listRowBackground(Color.clear) + } + .onDelete(perform: onDelete) + } + .listStyle(.plain) + .scrollContentBackground(.hidden) + .environment(\.editMode, $editMode) + .accessibilityIdentifier("chargers-list") } private var summaryMetrics: [SummaryMetric] { diff --git a/Cable/Loads/CalculatorView.swift b/Cable/Loads/CalculatorView.swift index f6d0503..03a7592 100644 --- a/Cable/Loads/CalculatorView.swift +++ b/Cable/Loads/CalculatorView.swift @@ -29,6 +29,7 @@ struct CalculatorView: View { @State private var presentedAffiliateLink: AffiliateLinkInfo? @State private var completedItemIDs: Set @State private var isAdvancedExpanded = false + @EnvironmentObject private var storeKitManager: StoreKitManager @State private var hasActiveProSubscription = false let savedLoad: SavedLoad? @@ -80,7 +81,10 @@ struct CalculatorView: View { ) ) .task { - hasActiveProSubscription = (await SettingsView.fetchProStatus()) != nil + hasActiveProSubscription = storeKitManager.isProUnlocked + } + .onReceive(storeKitManager.$status) { _ in + hasActiveProSubscription = storeKitManager.isProUnlocked } } diff --git a/Cable/Loads/LoadsView.swift b/Cable/Loads/LoadsView.swift index 262e91b..7cea401 100644 --- a/Cable/Loads/LoadsView.swift +++ b/Cable/Loads/LoadsView.swift @@ -8,6 +8,7 @@ import SwiftUI import SwiftData +import PostHog struct LoadsView: View { @Environment(\.modelContext) private var modelContext @@ -134,7 +135,7 @@ struct LoadsView: View { .toolbar { ToolbarItem(placement: .principal) { Button(action: { - showingSystemEditor = true + presentSystemEditor(source: "toolbar") }) { HStack(spacing: 8) { ZStack { @@ -258,13 +259,20 @@ struct LoadsView: View { if presentSystemEditorOnAppear && !hasPresentedSystemEditorOnAppear { hasPresentedSystemEditorOnAppear = true DispatchQueue.main.async { - showingSystemEditor = true + presentSystemEditor(source: "auto") } } if let loadToOpen = loadToOpenOnAppear, !hasOpenedLoadOnAppear { hasOpenedLoadOnAppear = true DispatchQueue.main.async { + PostHogSDK.shared.capture( + "Load Opened", + properties: [ + "mode": loadToOpen.isWattMode ? "watt" : "amp", + "system": system.name + ] + ) newLoadToEdit = loadToOpen } } @@ -287,109 +295,116 @@ struct LoadsView: View { onSelectBatteries: { selectedComponentTab = .batteries }, onSelectChargers: { selectedComponentTab = .chargers }, onCreateLoad: { createNewLoad() }, - onBrowseLibrary: { showingComponentLibrary = true }, - onShowBillOfMaterials: { showingSystemBOM = true }, + onBrowseLibrary: { openComponentLibrary(source: "overview") }, + onShowBillOfMaterials: { openBillOfMaterials() }, onCreateBattery: { startBatteryConfiguration() }, onCreateCharger: { startChargerConfiguration() } ) .accessibilityIdentifier("system-overview") } - private var summarySection: some View { - ZStack(alignment: .bottomTrailing) { - VStack(alignment: .leading, spacing: 10) { - HStack(alignment: .firstTextBaseline) { - Text(loadsSummaryTitle) - .font(.headline.weight(.semibold)) - Spacer() + private var loadsStatsHeader: some View { + StatsHeaderContainer { + loadsSummaryContent + } + } + + private var loadsSummaryContent: some View { + VStack(alignment: .leading, spacing: 10) { + HStack(alignment: .firstTextBaseline) { + Text(loadsSummaryTitle) + .font(.headline.weight(.semibold)) + Spacer() + } + + ViewThatFits(in: .horizontal) { + HStack(spacing: 16) { + summaryMetric( + icon: "square.stack.3d.up", + label: loadsCountLabel, + value: "\(savedLoads.count)", + tint: .blue + ) + summaryMetric( + icon: "bolt.fill", + label: loadsCurrentLabel, + value: formattedCurrent(totalCurrent), + tint: .orange + ) + summaryMetric( + icon: "gauge.medium", + label: loadsPowerLabel, + value: formattedPower(totalPower), + tint: .green + ) } - ViewThatFits(in: .horizontal) { - HStack(spacing: 16) { - summaryMetric( - icon: "square.stack.3d.up", - label: loadsCountLabel, - value: "\(savedLoads.count)", - tint: .blue - ) - summaryMetric( - icon: "bolt.fill", - label: loadsCurrentLabel, - value: formattedCurrent(totalCurrent), - tint: .orange - ) - summaryMetric( - icon: "gauge.medium", - label: loadsPowerLabel, - value: formattedPower(totalPower), - tint: .green - ) - } - - VStack(alignment: .leading, spacing: 12) { - summaryMetric( - icon: "square.stack.3d.up", - label: loadsCountLabel, - value: "\(savedLoads.count)", - tint: .blue - ) - summaryMetric( - icon: "bolt.fill", - label: loadsCurrentLabel, - value: formattedCurrent(totalCurrent), - tint: .orange - ) - summaryMetric( - icon: "gauge.medium", - label: loadsPowerLabel, - value: formattedPower(totalPower), - tint: .green - ) - } - } - - if let status = loadStatus { - Button { - activeStatus = status - } label: { - statusBanner(for: status) - } - .buttonStyle(.plain) + VStack(alignment: .leading, spacing: 12) { + summaryMetric( + icon: "square.stack.3d.up", + label: loadsCountLabel, + value: "\(savedLoads.count)", + tint: .blue + ) + summaryMetric( + icon: "bolt.fill", + label: loadsCurrentLabel, + value: formattedCurrent(totalCurrent), + tint: .orange + ) + summaryMetric( + icon: "gauge.medium", + label: loadsPowerLabel, + value: formattedPower(totalPower), + tint: .green + ) } } - .padding(.horizontal, 16) - .padding(.vertical, 10) - .background(Color(.systemGroupedBackground)) - Divider() - .background(Color(.separator)) - - libraryButton - .padding(.trailing, 16) - .padding(.bottom, 6) + if let status = loadStatus { + Button { + activeStatus = status + } label: { + statusBanner(for: status) + } + .buttonStyle(.plain) + } } } private var libraryButton: some View { Button { - showingComponentLibrary = true + openComponentLibrary(source: "library-button") } label: { - Label( - String( - localized: "loads.library.button", - bundle: .main, - comment: "Button title to open component library" - ), - systemImage: "books.vertical" - ) - .font(.footnote.weight(.semibold)) - .padding(.horizontal, 14) - .padding(.vertical, 10) - .background(.ultraThinMaterial, in: Capsule(style: .continuous)) + Group { + if #available(iOS 26.0, *) { + libraryButtonLabel + .padding(.horizontal, 18) + .padding(.vertical, 12) + .glassEffect(.regular, in: .capsule) + } else { + libraryButtonLabel + .padding(.horizontal, 14) + .padding(.vertical, 10) + .background(.ultraThinMaterial, in: Capsule(style: .continuous)) + .shadow(color: Color.black.opacity(0.08), radius: 16, x: 0, y: 8) + } + } } .buttonStyle(.plain) .tint(.accentColor) - .shadow(color: Color.black.opacity(0.08), radius: 16, x: 0, y: 8) + } + + private var libraryButtonLabel: some View { + Label( + String( + localized: "loads.library.button", + bundle: .main, + comment: "Button title to open component library" + ), + systemImage: "books.vertical" + ) + .font(.footnote.weight(.semibold)) } private var componentsTab: some View { @@ -398,37 +413,69 @@ struct LoadsView: View { OnboardingInfoView( configuration: .loads(), onPrimaryAction: { createNewLoad() }, - onSecondaryAction: { showingComponentLibrary = true } + onSecondaryAction: { openComponentLibrary(source: "components-onboarding") } ) .padding(.horizontal, 0) } else { - summarySection - - List { - ForEach(savedLoads) { load in - Button { - selectLoad(load) - } label: { - loadRow(for: load) - } - .buttonStyle(.plain) - .disabled(editMode == .active) - .listRowInsets(.init(top: 12, leading: 16, bottom: 12, trailing: 16)) - .listRowSeparator(.hidden) - .listRowBackground(Color.clear) - } - .onDelete(perform: deleteLoads) - } - .listStyle(.plain) - .scrollContentBackground(.hidden) - .accessibilityIdentifier("loads-list") - .environment(\.editMode, $editMode) + loadsListWithHeader } } .background(Color(.systemGroupedBackground)) } + @ViewBuilder + private var loadsListWithHeader: some View { + Group { + if #available(iOS 26.0, *) { + baseLoadsList + .scrollEdgeEffectStyle(.soft, for: .top) + .safeAreaInset(edge: .top, spacing: 0) { + loadsStatsHeader + } + } else { + baseLoadsList + .safeAreaInset(edge: .top, spacing: 0) { + loadsStatsHeader + } + } + } + .overlay(alignment: .bottomTrailing) { + libraryButton + .padding(.trailing, 24) + .padding(.bottom, 24) + } + } + + private var baseLoadsList: some View { + List { + ForEach(savedLoads) { load in + Button { + selectLoad(load) + } label: { + loadRow(for: load) + } + .buttonStyle(.plain) + .disabled(editMode == .active) + .listRowInsets(.init(top: 12, leading: 16, bottom: 12, trailing: 16)) + .listRowSeparator(.hidden) + .listRowBackground(Color.clear) + } + .onDelete(perform: deleteLoads) + } + .listStyle(.plain) + .scrollContentBackground(.hidden) + .accessibilityIdentifier("loads-list") + .environment(\.editMode, $editMode) + } + private func selectLoad(_ load: SavedLoad) { + PostHogSDK.shared.capture( + "Load Opened", + properties: [ + "mode": load.isWattMode ? "watt" : "amp", + "system": system.name + ] + ) newLoadToEdit = load } @@ -699,14 +746,6 @@ struct LoadsView: View { ) } - private func deleteLoads(offsets: IndexSet) { - withAnimation { - for index in offsets { - modelContext.delete(savedLoads[index]) - } - } - } - private func handlePrimaryAction() { switch selectedComponentTab { case .overview: @@ -719,6 +758,54 @@ struct LoadsView: View { startChargerConfiguration() } } + + private func presentSystemEditor(source: String) { + PostHogSDK.shared.capture( + "System Editor Opened", + properties: [ + "source": source, + "system": system.name + ] + ) + showingSystemEditor = true + } + + private func openComponentLibrary(source: String) { + PostHogSDK.shared.capture( + "Component Library Opened", + properties: [ + "source": source, + "system": system.name + ] + ) + showingComponentLibrary = true + } + + private func openBillOfMaterials() { + PostHogSDK.shared.capture( + "Bill Of Materials Opened", + properties: [ + "system": system.name + ] + ) + showingSystemBOM = true + } + + private func deleteLoads(offsets: IndexSet) { + let loadsToDelete = offsets.map { savedLoads[$0] } + withAnimation { + for load in loadsToDelete { + PostHogSDK.shared.capture( + "Load Deleted", + properties: [ + "name": load.name, + "system": system.name + ] + ) + modelContext.delete(load) + } + } + } private func createNewLoad() { let newLoad = SystemComponentsPersistence.createDefaultLoad( @@ -728,10 +815,24 @@ struct LoadsView: View { existingBatteries: savedBatteries, existingChargers: savedChargers ) + PostHogSDK.shared.capture( + "Load Created", + properties: [ + "name": newLoad.name, + "system": system.name + ] + ) newLoadToEdit = newLoad } private func startBatteryConfiguration() { + PostHogSDK.shared.capture( + "Battery Editor Opened", + properties: [ + "source": "create", + "system": system.name + ] + ) batteryDraft = SystemComponentsPersistence.makeBatteryDraft( for: system, existingLoads: savedLoads, @@ -741,20 +842,46 @@ struct LoadsView: View { } private func saveBattery(_ configuration: BatteryConfiguration) { + let isExisting = savedBatteries.contains { $0.id == configuration.id } SystemComponentsPersistence.saveBattery( configuration, for: system, existingBatteries: savedBatteries, in: modelContext ) + let eventName = isExisting ? "Battery Updated" : "Battery Created" + PostHogSDK.shared.capture( + eventName, + properties: [ + "name": configuration.name, + "system": system.name + ] + ) } private func editBattery(_ battery: SavedBattery) { + PostHogSDK.shared.capture( + "Battery Editor Opened", + properties: [ + "source": "edit", + "system": system.name + ] + ) batteryDraft = BatteryConfiguration(savedBattery: battery, system: system) } private func deleteBatteries(_ offsets: IndexSet) { + let batteriesToDelete = offsets.map { savedBatteries[$0] } withAnimation { + for battery in batteriesToDelete { + PostHogSDK.shared.capture( + "Battery Deleted", + properties: [ + "name": battery.name, + "system": system.name + ] + ) + } SystemComponentsPersistence.deleteBatteries( at: offsets, from: savedBatteries, @@ -764,6 +891,13 @@ struct LoadsView: View { } private func startChargerConfiguration() { + PostHogSDK.shared.capture( + "Charger Editor Opened", + properties: [ + "source": "create", + "system": system.name + ] + ) chargerDraft = SystemComponentsPersistence.makeChargerDraft( for: system, existingLoads: savedLoads, @@ -773,20 +907,46 @@ struct LoadsView: View { } private func saveCharger(_ configuration: ChargerConfiguration) { + let isExisting = savedChargers.contains { $0.id == configuration.id } SystemComponentsPersistence.saveCharger( configuration, for: system, existingChargers: savedChargers, in: modelContext ) + let eventName = isExisting ? "Charger Updated" : "Charger Created" + PostHogSDK.shared.capture( + eventName, + properties: [ + "name": configuration.name, + "system": system.name + ] + ) } private func editCharger(_ charger: SavedCharger) { + PostHogSDK.shared.capture( + "Charger Editor Opened", + properties: [ + "source": "edit", + "system": system.name + ] + ) chargerDraft = ChargerConfiguration(savedCharger: charger, system: system) } private func deleteChargers(_ offsets: IndexSet) { + let chargersToDelete = offsets.map { savedChargers[$0] } withAnimation { + for charger in chargersToDelete { + PostHogSDK.shared.capture( + "Charger Deleted", + properties: [ + "name": charger.name, + "system": system.name + ] + ) + } SystemComponentsPersistence.deleteChargers( at: offsets, from: savedChargers, @@ -804,6 +964,14 @@ struct LoadsView: View { existingBatteries: savedBatteries, existingChargers: savedChargers ) + PostHogSDK.shared.capture( + "Library Load Added", + properties: [ + "id": item.id, + "name": item.localizedName, + "system": system.name + ] + ) newLoadToEdit = newLoad } diff --git a/Cable/Overview/SystemOverviewView.swift b/Cable/Overview/SystemOverviewView.swift index b183122..7c41db2 100644 --- a/Cable/Overview/SystemOverviewView.swift +++ b/Cable/Overview/SystemOverviewView.swift @@ -592,6 +592,16 @@ struct SystemOverviewView: View { } } + private var totalAverageLoadPower: Double { + loads.reduce(0) { result, load in + let power = max(load.power, 0) + guard power > 0 else { return result } + let dutyCycleFraction = max(min(load.dutyCyclePercent, 100), 0) / 100 + let usageFraction = max(min(load.dailyUsageHours, 24), 0) / 24 + return result + power * dutyCycleFraction * usageFraction + } + } + private var totalCapacity: Double { batteries.reduce(0) { result, battery in result + battery.capacityAmpHours @@ -714,9 +724,11 @@ struct SystemOverviewView: View { } private var completedBOMItemCount: Int { - settledLoads.reduce(into: Set()) { partialResult, load in - load.bomCompletedItemIDs.forEach { partialResult.insert($0) } - }.count + settledLoads.reduce(0) { result, load in + let uniqueItems = Set(load.bomCompletedItemIDs) + let cappedCount = min(uniqueItems.count, Self.bomItemsPerLoad) + return result + cappedCount + } } private var bomItemsCount: Int { @@ -794,8 +806,8 @@ struct SystemOverviewView: View { } private var estimatedRuntimeHours: Double? { - guard totalPower > 0, totalUsableEnergy > 0 else { return nil } - let hours = totalUsableEnergy / totalPower + guard totalAverageLoadPower > 0, totalUsableEnergy > 0 else { return nil } + let hours = totalUsableEnergy / totalAverageLoadPower return hours.isFinite && hours > 0 ? hours : nil } diff --git a/Cable/Paywall/CableProPaywallView.swift b/Cable/Paywall/CableProPaywallView.swift index 4d827bb..93cee4c 100644 --- a/Cable/Paywall/CableProPaywallView.swift +++ b/Cable/Paywall/CableProPaywallView.swift @@ -118,7 +118,9 @@ final class CableProPaywallViewModel: ObservableObject { for await result in Transaction.currentEntitlements { switch result { case .verified(let transaction): - unlocked.insert(transaction.productID) + if productIdentifiers.contains(transaction.productID) { + unlocked.insert(transaction.productID) + } case .unverified: continue } @@ -132,14 +134,12 @@ struct CableProPaywallView: View { @Environment(\.dismiss) private var dismiss @Binding var isPresented: Bool @EnvironmentObject private var unitSettings: UnitSystemSettings + @EnvironmentObject private var storeKitManager: StoreKitManager @StateObject private var viewModel: CableProPaywallViewModel @State private var alertInfo: PaywallAlert? - private static let defaultProductIds = [ - "app.voltplan.cable.weekly", - "app.voltplan.cable.yearly" - ] + private static let defaultProductIds = StoreKitManager.subscriptionProductIDs init(isPresented: Binding, productIdentifiers: [String] = CableProPaywallView.defaultProductIds) { _isPresented = isPresented @@ -168,9 +168,12 @@ struct CableProPaywallView: View { } .task { await viewModel.loadProducts(force: true) - unitSettings.isProUnlocked = !viewModel.purchasedProductIDs.isEmpty + await storeKitManager.refreshEntitlements() + } + .refreshable { + await viewModel.loadProducts(force: true) + await storeKitManager.refreshEntitlements() } - .refreshable { await viewModel.loadProducts(force: true) } } .onChange(of: viewModel.alert) { newValue in alertInfo = newValue @@ -186,7 +189,7 @@ struct CableProPaywallView: View { ) } .onChange(of: viewModel.purchasedProductIDs) { newValue in - unitSettings.isProUnlocked = !newValue.isEmpty + Task { await storeKitManager.refreshEntitlements() } } } @@ -535,5 +538,9 @@ struct PaywallAlert: Identifiable, Equatable { } #Preview { - CableProPaywallView(isPresented: .constant(true)) + let unitSettings = UnitSystemSettings() + let manager = StoreKitManager(unitSettings: unitSettings) + return CableProPaywallView(isPresented: .constant(true)) + .environmentObject(unitSettings) + .environmentObject(manager) } diff --git a/Cable/SettingsView.swift b/Cable/SettingsView.swift index 13c1783..70b6103 100644 --- a/Cable/SettingsView.swift +++ b/Cable/SettingsView.swift @@ -7,17 +7,14 @@ import SwiftUI -import SwiftData -import StoreKit struct SettingsView: View { @EnvironmentObject var unitSettings: UnitSystemSettings + @EnvironmentObject private var storeKitManager: StoreKitManager @Environment(\.dismiss) private var dismiss @Environment(\.openURL) private var openURL @State private var showingProPaywall = false - @State private var isLoadingProStatus = true - @State private var proStatus: ProSubscriptionStatus? var body: some View { NavigationStack { @@ -75,29 +72,28 @@ struct SettingsView: View { } } } - .task { await loadProStatus() } .sheet(isPresented: $showingProPaywall) { CableProPaywallView(isPresented: $showingProPaywall) } .onChange(of: showingProPaywall) { isPresented in if !isPresented { - Task { await loadProStatus() } + Task { await storeKitManager.refreshEntitlements() } } } .onAppear { - Task { await loadProStatus() } + Task { await storeKitManager.refreshEntitlements() } } } @ViewBuilder private var proSectionContent: some View { - if isLoadingProStatus { + if storeKitManager.isRefreshing && storeKitManager.status == nil { HStack { Spacer() ProgressView() Spacer() } - } else if let status = proStatus { + } else if let status = storeKitManager.status { VStack(alignment: .leading, spacing: 8) { Label(status.displayName, systemImage: "checkmark.seal.fill") .font(.headline) @@ -113,6 +109,18 @@ struct SettingsView: View { .font(.footnote) .foregroundStyle(.secondary) } + + if status.isInGracePeriod { + Text(localizedString("settings.pro.grace_period", defaultValue: "We're retrying your last payment; access remains during the grace period.")) + .font(.footnote) + .foregroundStyle(.secondary) + } + + if let isAutoRenewEnabled = status.isAutoRenewEnabled, !isAutoRenewEnabled { + Text(localizedString("settings.pro.autorenew.off", defaultValue: "Auto-renew is off—consider renewing to keep Cable PRO.")) + .font(.footnote) + .foregroundStyle(.secondary) + } Text(localizedString("settings.pro.instructions", defaultValue: "Manage or cancel your subscription in the App Store.")) .font(.footnote) @@ -149,15 +157,6 @@ struct SettingsView: View { } } - @MainActor - private func loadProStatus() async { - isLoadingProStatus = true - defer { isLoadingProStatus = false } - let status = await SettingsView.fetchProStatus() - proStatus = status - unitSettings.isProUnlocked = status != nil - } - private func renewalText(for date: Date) -> String { let formatter = DateFormatter() formatter.dateStyle = .medium @@ -168,7 +167,7 @@ struct SettingsView: View { return String(format: template, dateString) } - private func trialMessage(for status: ProSubscriptionStatus) -> String? { + private func trialMessage(for status: StoreKitManager.SubscriptionStatus) -> String? { guard status.isInTrial, let endDate = status.trialEndDate else { return nil } let days = max(Calendar.autoupdatingCurrent.dateComponents([.day], from: Date(), to: endDate).day ?? 0, 0) if days > 0 { @@ -199,48 +198,15 @@ struct SettingsView: View { return formatter.string(from: NSNumber(value: value)) ?? String(value) } - static func fetchProStatus() async -> ProSubscriptionStatus? { - let productIDs = Set(["app.voltplan.cable.weekly", "app.voltplan.cable.yearly"]) - for await result in Transaction.currentEntitlements { - guard case .verified(let transaction) = result, - productIDs.contains(transaction.productID) else { continue } - - let product = try? await Product.products(for: [transaction.productID]).first - let displayName = product?.displayName ?? transaction.productID - let renewalDate = transaction.expirationDate - - let hasIntroOffer = transaction.offerType == .introductory - let paymentMode = product?.subscription?.introductoryOffer?.paymentMode - let isInTrial = hasIntroOffer && paymentMode == .freeTrial - let trialEndDate = isInTrial ? transaction.expirationDate : nil - - return ProSubscriptionStatus( - productId: transaction.productID, - displayName: displayName, - renewalDate: renewalDate, - isInTrial: isInTrial, - trialEndDate: trialEndDate - ) - } - - return nil - } - private func localizedString(_ key: String, defaultValue: String) -> String { NSLocalizedString(key, tableName: nil, bundle: .main, value: defaultValue, comment: "") } - - struct ProSubscriptionStatus { - let productId: String - let displayName: String - let renewalDate: Date? - let isInTrial: Bool - let trialEndDate: Date? - } } #Preview("Settings (Default)") { let settings = UnitSystemSettings() + let manager = StoreKitManager(unitSettings: settings) return SettingsView() .environmentObject(settings) + .environmentObject(manager) } diff --git a/Cable/StatsHeaderContainer.swift b/Cable/StatsHeaderContainer.swift new file mode 100644 index 0000000..28d3829 --- /dev/null +++ b/Cable/StatsHeaderContainer.swift @@ -0,0 +1,48 @@ +import SwiftUI + +/// Reusable wrapper that applies the system overview stats card styling to a header view. +struct StatsHeaderContainer: View { + private let content: Content + + init(@ViewBuilder content: () -> Content) { + self.content = content() + } + + var body: some View { + Group { + if #available(iOS 26.0, *) { + card + .glassEffect(.regular, in: .rect(cornerRadius: 20)) + .padding(.horizontal, 16) + .padding(.top, 12) + .padding(.bottom, 12) + } else { + card + .padding(.horizontal, 16) + .padding(.top, 20) + .padding(.bottom, 16) + .background(Color(.systemGroupedBackground)) + .overlay( + RoundedRectangle(cornerRadius: 20, style: .continuous) + .strokeBorder(.white.opacity(0.15)) + ) + } + } + } + + private var card: some View { + content + .padding(.vertical, 18) + .padding(.horizontal, 20) + .frame(maxWidth: .infinity, alignment: .leading) + .background( + RoundedRectangle(cornerRadius: 20, style: .continuous) + .fill(Color(red: 81 / 255, green: 144 / 255, blue: 152 / 255).opacity(0.12)) + .overlay( + RoundedRectangle(cornerRadius: 20, style: .continuous) + .stroke(Color(.separator).opacity(0.18), lineWidth: 1) + ) + ) + .clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous)) + } +} diff --git a/Cable/StoreKitManager.swift b/Cable/StoreKitManager.swift new file mode 100644 index 0000000..f9c540d --- /dev/null +++ b/Cable/StoreKitManager.swift @@ -0,0 +1,221 @@ +import Foundation +import StoreKit + +@MainActor +final class StoreKitManager: ObservableObject { + struct SubscriptionStatus: Equatable { + let productId: String + let displayName: String + let renewalDate: Date? + let isInTrial: Bool + let trialEndDate: Date? + let isInGracePeriod: Bool + let isAutoRenewEnabled: Bool? + } + + nonisolated static let subscriptionProductIDs: [String] = [ + "app.voltplan.cable.weekly", + "app.voltplan.cable.yearly" + ] + + @Published private(set) var status: SubscriptionStatus? + @Published private(set) var isRefreshing = false + + var isProUnlocked: Bool { + status != nil + } + + private let productIDs: Set + private weak var unitSettings: UnitSystemSettings? + private var updatesTask: Task? + private var productCache: [String: Product] = [:] + + init( + productIDs: [String] = StoreKitManager.subscriptionProductIDs, + unitSettings: UnitSystemSettings? = nil + ) { + self.productIDs = Set(productIDs) + self.unitSettings = unitSettings + + updatesTask = Task { [weak self] in + await self?.observeTransactionUpdates() + } + + Task { [weak self] in + await self?.finishUnfinishedTransactions() + await self?.refreshEntitlements() + } + } + + deinit { + updatesTask?.cancel() + } + + func attachUnitSettings(_ settings: UnitSystemSettings) { + unitSettings = settings + Task { [weak self] in + await self?.refreshEntitlements() + } + } + + func refreshEntitlements() async { + guard !isRefreshing else { return } + + isRefreshing = true + defer { isRefreshing = false } + + let resolvedStatus = await loadCurrentStatus() + status = resolvedStatus + unitSettings?.isProUnlocked = resolvedStatus != nil + } + + private func loadCurrentStatus() async -> SubscriptionStatus? { + if let entitlementStatus = await statusFromCurrentEntitlements() { + return entitlementStatus + } + + return await statusFromLatestTransactions() + } + + private func statusFromCurrentEntitlements() async -> SubscriptionStatus? { + var newestTransaction: StoreKit.Transaction? + + for await result in StoreKit.Transaction.currentEntitlements { + guard case .verified(let transaction) = result, + productIDs.contains(transaction.productID), + transaction.revocationDate == nil, + !isExpired(transaction) else { continue } + + if let existing = newestTransaction { + let existingExpiration = existing.expirationDate ?? .distantPast + let candidateExpiration = transaction.expirationDate ?? .distantPast + if candidateExpiration > existingExpiration { + newestTransaction = transaction + } + } else { + newestTransaction = transaction + } + } + + guard let activeTransaction = newestTransaction else { return nil } + return await status(for: activeTransaction) + } + + private func statusFromLatestTransactions() async -> SubscriptionStatus? { + var newestTransaction: StoreKit.Transaction? + + for productID in productIDs { + guard let latestResult = await StoreKit.Transaction.latest(for: productID) else { continue } + guard case .verified(let transaction) = latestResult, + transaction.revocationDate == nil, + !isExpired(transaction) else { continue } + + if let existing = newestTransaction { + let existingExpiration = existing.expirationDate ?? .distantPast + let candidateExpiration = transaction.expirationDate ?? .distantPast + if candidateExpiration > existingExpiration { + newestTransaction = transaction + } + } else { + newestTransaction = transaction + } + } + + guard let activeTransaction = newestTransaction else { return nil } + return await status(for: activeTransaction) + } + + private func observeTransactionUpdates() async { + for await result in StoreKit.Transaction.updates { + guard !Task.isCancelled else { return } + + switch result { + case .verified(let transaction): + await transaction.finish() + await refreshEntitlements() + case .unverified: + continue + } + } + } + + private func finishUnfinishedTransactions() async { + for await result in StoreKit.Transaction.unfinished { + guard case .verified(let transaction) = result else { continue } + await transaction.finish() + } + } + + private func status(for transaction: StoreKit.Transaction) async -> SubscriptionStatus? { + let product = await product(for: transaction.productID) + let displayName = product?.displayName ?? transaction.productID + + var isInGracePeriod = false + var isAutoRenewEnabled: Bool? + var isInTrial = false + var trialEndDate: Date? + + if let currentStatus = await transaction.subscriptionStatus { + if currentStatus.state == .inGracePeriod { + isInGracePeriod = true + } + + if case .verified(let renewalInfo) = currentStatus.renewalInfo { + isAutoRenewEnabled = renewalInfo.willAutoRenew + + if renewalInfo.gracePeriodExpirationDate != nil { + isInGracePeriod = true + } + + if #available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) { + if let offer = renewalInfo.offer, offer.type == .introductory { + isInTrial = true + trialEndDate = transaction.expirationDate + } + } else { +#if compiler(>=5.3) + if renewalInfo.offerType == .introductory { + isInTrial = true + trialEndDate = transaction.expirationDate + } +#endif + } + } else if case .verified(let statusTransaction) = currentStatus.transaction { + if let offer = statusTransaction.offer, offer.type == .introductory { + isInTrial = true + trialEndDate = statusTransaction.expirationDate ?? transaction.expirationDate + } + } + } else if let offer = transaction.offer, offer.type == .introductory { + isInTrial = true + trialEndDate = transaction.expirationDate + } + + return SubscriptionStatus( + productId: transaction.productID, + displayName: displayName, + renewalDate: transaction.expirationDate, + isInTrial: isInTrial, + trialEndDate: trialEndDate, + isInGracePeriod: isInGracePeriod, + isAutoRenewEnabled: isAutoRenewEnabled + ) + } + + private func isExpired(_ transaction: StoreKit.Transaction) -> Bool { + if let expirationDate = transaction.expirationDate { + return expirationDate < Date() + } + return false + } + + private func product(for id: String) async -> Product? { + if let cached = productCache[id] { + return cached + } + + guard let product = try? await Product.products(for: [id]).first else { return nil } + productCache[id] = product + return product + } +} diff --git a/Cable/Systems/SystemsOnboardingView.swift b/Cable/Systems/SystemsOnboardingView.swift index 64ce52b..6cd83c1 100644 --- a/Cable/Systems/SystemsOnboardingView.swift +++ b/Cable/Systems/SystemsOnboardingView.swift @@ -1,4 +1,5 @@ import SwiftUI +import PostHog struct SystemsOnboardingView: View { @State private var systemName: String = String(localized: "default.system.name", comment: "Default placeholder name for a system") @@ -92,6 +93,9 @@ struct SystemsOnboardingView: View { .background(Color(.systemGroupedBackground)) .onAppear(perform: resetState) .onReceive(timer) { _ in advanceCarousel() } + .task { + PostHogSDK.shared.capture("Launched") + } } private func resetState() { @@ -102,6 +106,7 @@ struct SystemsOnboardingView: View { private func createSystem() { isFieldFocused = false let trimmed = systemName.trimmingCharacters(in: .whitespacesAndNewlines) + PostHogSDK.shared.capture("System Created", properties: ["name": trimmed]) guard !trimmed.isEmpty else { return } onCreate(trimmed) } diff --git a/Cable/Systems/SystemsView.swift b/Cable/Systems/SystemsView.swift index d58bbd7..94c68c7 100644 --- a/Cable/Systems/SystemsView.swift +++ b/Cable/Systems/SystemsView.swift @@ -8,6 +8,7 @@ import SwiftUI import SwiftData +import PostHog struct SystemsView: View { @Environment(\.modelContext) private var modelContext @@ -107,6 +108,17 @@ struct SystemsView: View { } .padding(.vertical, 4) } + .simultaneousGesture( + TapGesture().onEnded { + PostHogSDK.shared.capture( + "System Opened", + properties: [ + "name": system.name, + "source": "list" + ] + ) + } + ) } .onDelete(perform: deleteSystems) } @@ -117,7 +129,7 @@ struct SystemsView: View { .toolbar { ToolbarItem(placement: .navigationBarLeading) { Button { - showingSettings = true + openSettings() } label: { Image(systemName: "gearshape") } @@ -125,6 +137,7 @@ struct SystemsView: View { ToolbarItem(placement: .navigationBarTrailing) { HStack { Button(action: { + PostHogSDK.shared.capture("System Create Navigation") createNewSystem() }) { Image(systemName: "plus") @@ -160,15 +173,44 @@ struct SystemsView: View { createOnboardingSystem(named: name) } } + + private func openSettings() { + PostHogSDK.shared.capture("Settings Opened") + showingSettings = true + } private func createNewSystem() { let system = makeSystem() - navigateToSystem(system, presentSystemEditor: true, loadToOpen: nil) + PostHogSDK.shared.capture( + "System Created", + properties: [ + "name": system.name, + "source": "toolbar" + ] + ) + navigateToSystem( + system, + presentSystemEditor: true, + loadToOpen: nil, + source: "created" + ) } private func createNewSystem(named name: String) { let system = makeSystem(preferredName: name) - navigateToSystem(system, presentSystemEditor: true, loadToOpen: nil) + PostHogSDK.shared.capture( + "System Created", + properties: [ + "name": system.name, + "source": "named" + ] + ) + navigateToSystem( + system, + presentSystemEditor: true, + loadToOpen: nil, + source: "created-named" + ) } private func createOnboardingSystem(named name: String) { @@ -176,10 +218,29 @@ struct SystemsView: View { preferredName: name, colorName: randomSystemColorName() ) - navigateToSystem(system, presentSystemEditor: false, loadToOpen: nil) + navigateToSystem( + system, + presentSystemEditor: false, + loadToOpen: nil, + source: "onboarding" + ) } - private func navigateToSystem(_ system: ElectricalSystem, presentSystemEditor: Bool, loadToOpen: SavedLoad?, animated: Bool = true) { + private func navigateToSystem( + _ system: ElectricalSystem, + presentSystemEditor: Bool, + loadToOpen: SavedLoad?, + animated: Bool = true, + source: String = "programmatic" + ) { + PostHogSDK.shared.capture( + "System Opened", + properties: [ + "name": system.name, + "source": source, + "loads": loads(for: system).count + ] + ) let target = SystemNavigationTarget( system: system, presentSystemEditor: presentSystemEditor, @@ -228,13 +289,40 @@ struct SystemsView: View { hasPerformedInitialAutoNavigation = true guard systems.count == 1, let system = systems.first else { return } - navigateToSystem(system, presentSystemEditor: false, loadToOpen: nil, animated: false) + navigateToSystem( + system, + presentSystemEditor: false, + loadToOpen: nil, + animated: false, + source: "auto" + ) } private func addComponentFromLibrary(_ item: ComponentLibraryItem) { let system = makeSystem() + PostHogSDK.shared.capture( + "System Created", + properties: [ + "name": system.name, + "source": "library" + ] + ) let load = createLoad(from: item, in: system) - navigateToSystem(system, presentSystemEditor: false, loadToOpen: load, animated: false) + PostHogSDK.shared.capture( + "Library Load Added", + properties: [ + "id": item.id, + "name": item.localizedName, + "system": system.name + ] + ) + navigateToSystem( + system, + presentSystemEditor: false, + loadToOpen: load, + animated: false, + source: "library" + ) } private func createLoad(from item: ComponentLibraryItem, in system: ElectricalSystem) -> SavedLoad { @@ -306,9 +394,16 @@ struct SystemsView: View { } private func deleteSystems(offsets: IndexSet) { + let systemsToDelete = offsets.map { systems[$0] } withAnimation { - for index in offsets { - let system = systems[index] + for system in systemsToDelete { + PostHogSDK.shared.capture( + "System Deleted", + properties: [ + "name": system.name, + "loads": loads(for: system).count + ] + ) deleteLoads(for: system) modelContext.delete(system) } @@ -319,6 +414,14 @@ struct SystemsView: View { let descriptor = FetchDescriptor() if let loads = try? modelContext.fetch(descriptor) { for load in loads where load.system == system { + PostHogSDK.shared.capture( + "Load Deleted", + properties: [ + "name": load.name, + "system": system.name, + "source": "system-delete" + ] + ) modelContext.delete(load) } } diff --git a/Podfile b/Podfile new file mode 100644 index 0000000..3a9e543 --- /dev/null +++ b/Podfile @@ -0,0 +1,23 @@ +# Uncomment the next line to define a global platform for your project +# platform :ios, '9.0' +platform :ios, '17.6' +target 'Cable' do + # Comment the next line if you don't want to use dynamic frameworks + use_frameworks! + + # Pods for Cable + pod "PostHog", "~> 3.0" + target 'CableTests' do + inherit! :search_paths + # Pods for testing + end + + target 'CableUITests' do + # Pods for testing + end + + target 'CableUITestsScreenshot' do + # Pods for testing + end + +end diff --git a/Podfile.lock b/Podfile.lock new file mode 100644 index 0000000..506b37a --- /dev/null +++ b/Podfile.lock @@ -0,0 +1,16 @@ +PODS: + - PostHog (3.34.0) + +DEPENDENCIES: + - PostHog (~> 3.0) + +SPEC REPOS: + trunk: + - PostHog + +SPEC CHECKSUMS: + PostHog: bbb7eaecb2f5a286d9da3c833cbb18ae08799655 + +PODFILE CHECKSUM: b59bd921fb9a91e981795c18b6ff238370434172 + +COCOAPODS: 1.16.2 diff --git a/Pods/Manifest.lock b/Pods/Manifest.lock new file mode 100644 index 0000000..506b37a --- /dev/null +++ b/Pods/Manifest.lock @@ -0,0 +1,16 @@ +PODS: + - PostHog (3.34.0) + +DEPENDENCIES: + - PostHog (~> 3.0) + +SPEC REPOS: + trunk: + - PostHog + +SPEC CHECKSUMS: + PostHog: bbb7eaecb2f5a286d9da3c833cbb18ae08799655 + +PODFILE CHECKSUM: b59bd921fb9a91e981795c18b6ff238370434172 + +COCOAPODS: 1.16.2 diff --git a/Pods/Pods.xcodeproj/project.pbxproj b/Pods/Pods.xcodeproj/project.pbxproj new file mode 100644 index 0000000..8c7866a --- /dev/null +++ b/Pods/Pods.xcodeproj/project.pbxproj @@ -0,0 +1,2254 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 01C82EF77CAD48D420201E5041F71981 /* backward_references_enc.c in Sources */ = {isa = PBXBuildFile; fileRef = 873422C94FE37539EBA965E98C7A3636 /* backward_references_enc.c */; }; + 01EB398AE7FB8D1AA43CCC4BE25256A1 /* PostHogTagViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 267A77D966A9CB080522F80053CB2597 /* PostHogTagViewModifier.swift */; }; + 0357CEDE60497FDDDFF4206D50D84D32 /* dec_sse41.c in Sources */ = {isa = PBXBuildFile; fileRef = 8CECA788D22A8D210E1F126F087FA049 /* dec_sse41.c */; }; + 0572679AE85C33C43DE798C06F3E1534 /* ph_huffman_encode_utils.h in Headers */ = {isa = PBXBuildFile; fileRef = F115DBC1009A4AE8B1E2EEACECEE686D /* ph_huffman_encode_utils.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 06F57CD4670CD1DAE91524790BFC6EC1 /* EdgeBorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = BADD4B93BF926C2B8CA6E8BAC40F9EED /* EdgeBorder.swift */; }; + 07169E6F462DE20D301856B85FB0C74F /* rescaler_neon.c in Sources */ = {isa = PBXBuildFile; fileRef = 8B72D15908356213E83617F069F28778 /* rescaler_neon.c */; }; + 0867541398BCAD5D8D499F66BB8887FB /* RRStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EC9E8EDC00E76181958B8A45361E2E3 /* RRStyle.swift */; }; + 0A24C32E44D0415EF08780849822BCEE /* yuv_sse41.c in Sources */ = {isa = PBXBuildFile; fileRef = 30238EC8C0FAB5B9682951C8C40A56BC /* yuv_sse41.c */; }; + 0AE01E9EDF9EC4A863872FB71F99983B /* View+PostHogLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9145EC49215D8AD4B67DC4C8A3FFCD24 /* View+PostHogLabel.swift */; }; + 0B03D2F73EAB1910FB04EFA201F7FA59 /* enc_neon.c in Sources */ = {isa = PBXBuildFile; fileRef = E634ED9E4F291E94869F8F535E2FB59D /* enc_neon.c */; }; + 0BE2AAAE30AE1B29B23B1E55C819D7A7 /* PostHogPersonProfiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED8BAA4BA5693210FDA9BE42AA76F93E /* PostHogPersonProfiles.swift */; }; + 0C4DEC31440C7333BC97E7B78D67BC72 /* tree_enc.c in Sources */ = {isa = PBXBuildFile; fileRef = B4AE78BA77D7992329434D3A2D3370F6 /* tree_enc.c */; }; + 0E16356A19CF2C4ED7FE29BBB4F700BC /* random_utils.c in Sources */ = {isa = PBXBuildFile; fileRef = 276E633F3287EC40CFDCCA704F85A7BB /* random_utils.c */; }; + 0E3BEE1E611D26371DAF50C23E7AC1BB /* UIImage+Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = 497BF2253E737EA49DACF8B6460E188A /* UIImage+Util.swift */; }; + 0EF0A53B84657EF4A4A6C6E11C409DF3 /* yuv_neon.c in Sources */ = {isa = PBXBuildFile; fileRef = 235F09390CCC154EF3CE1AF84501B9D5 /* yuv_neon.c */; }; + 0F409AE2D56C546B8A1155D37CCA80D5 /* PostHogConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C042A1CBFE280735CE602F869E5E16E /* PostHogConfig.swift */; }; + 1089A5E22CB40877785F1B4D5B8337D8 /* dec_sse2.c in Sources */ = {isa = PBXBuildFile; fileRef = 1B3EF635F2042C38745AADD3B049F9D1 /* dec_sse2.c */; }; + 125C8B61C19411B4E222E2D4670EDA7C /* ReadWriteLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F97F6C297E221C6D17A28FD64FD56C9 /* ReadWriteLock.swift */; }; + 13D9AE4C00CE2535E0DEF135306CB8BF /* PostHogDisplaySurveyQuestion.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2CD1BA30ECCBAFA941F6D593A91CE5A /* PostHogDisplaySurveyQuestion.swift */; }; + 13F0B4871E8EB793200587DF5C1C4246 /* PostHogSessionReplayPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B180FB52B54B3D7CE6A1EB92AF8CD1F /* PostHogSessionReplayPlugin.swift */; }; + 141DFED4583A49BE8122CF85D9C530B2 /* Pods-Cable-CableUITestsScreenshot-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 701F2C64F76028BD63C8BC57E763B989 /* Pods-Cable-CableUITestsScreenshot-dummy.m */; }; + 144C6DD1A247AC1EDF9AD57981DCB922 /* ph_palette.h in Headers */ = {isa = PBXBuildFile; fileRef = A97D7894341E91B3BF09503921D83C16 /* ph_palette.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 152739ADAFEA0909584A96C79E07C151 /* PostHogReplayIntegration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36EBA9BB4949682419A03F0FE285AFDD /* PostHogReplayIntegration.swift */; }; + 15F7984FF736D8AF019F417DA8901036 /* PostHog-PostHog in Resources */ = {isa = PBXBuildFile; fileRef = 42D87DCAF52175DC2D5C6B44419636A8 /* PostHog-PostHog */; }; + 16053DB437B9398112A24A047C55D124 /* PostHogApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A8552B95757A4F529060DCD92FA9EA8 /* PostHogApi.swift */; }; + 183E1F870F4FDDEA7C57FEC6BD70ED5B /* near_lossless_enc.c in Sources */ = {isa = PBXBuildFile; fileRef = B3892D9BAABC94C6D1D38A736617D726 /* near_lossless_enc.c */; }; + 18BF9FAC9C9D784C748A0AFEE36F20E1 /* PostHogBatchUploadInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DAC933623085F073616F04099AF6703 /* PostHogBatchUploadInfo.swift */; }; + 19D82E01983174C6A45C7EE85AA81795 /* UITextInputTraits+Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DB59137CCCF0AB605EA3C16F30B47C7 /* UITextInputTraits+Util.swift */; }; + 1C54E6F4D075181AE6E195DC2CBA737D /* QuestionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A84BF685A521744850F13C55520AAB70 /* QuestionHeader.swift */; }; + 1ED830E9C2C477B7C3C88115DEB77BA4 /* ph_decode.h in Headers */ = {isa = PBXBuildFile; fileRef = D396A5F4E5EA6A631CFA1F5BA73BE979 /* ph_decode.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 236BD01A81BCD619F470D17D00FA2440 /* UIWindow+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D1B502E8A8D8A7CF0F160485F27D94 /* UIWindow+.swift */; }; + 2468C99062231929D4FA5A274E16CB72 /* AssociatedKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15861BEA1F90094B56DC2CD840469B3D /* AssociatedKeys.swift */; }; + 24DEE46DD19D816066C36A6F5AFF3D20 /* PostHog-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 74853741E05F6EF1BEB0A51503A2FAAF /* PostHog-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 252C54D4211DE3406B82DC8206E354DA /* PostHogLogLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C720DA9EA026D5E7449597140583E063 /* PostHogLogLevel.swift */; }; + 2599E595E56867C2CC371352E74E5CB0 /* PostHogSurveyResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73BBDBBF3DD175220822CBE95FE0B6DA /* PostHogSurveyResponse.swift */; }; + 273B9F6A4399EA87F762570917E52D34 /* Hedgelog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C3B8263979EC31683158AC2B8D195C7 /* Hedgelog.swift */; }; + 287E75C60B7FB8A9A9E6A4D5E2465701 /* upsampling.c in Sources */ = {isa = PBXBuildFile; fileRef = 5C1E2A40BEFB3E27559CF75D3AF598D0 /* upsampling.c */; }; + 2B18064A9F0190519F509DD5891B81A0 /* PostHogRemoteConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05688B486C05767CC332F7BF18F63FA6 /* PostHogRemoteConfig.swift */; }; + 2C34DA75C8E0FD9A4A61AF56826CE149 /* predictor_enc.c in Sources */ = {isa = PBXBuildFile; fileRef = 7309703A36FD4345F1782408988D9A2A /* predictor_enc.c */; }; + 2C4358D6D544B725F9B1EB1A3FF6D35B /* rescaler_sse2.c in Sources */ = {isa = PBXBuildFile; fileRef = 3EA95D0670D5D47F7BC69002970C0E15 /* rescaler_sse2.c */; }; + 308067A90C6D4714A6625855CF82A55C /* ph_bit_reader_utils.h in Headers */ = {isa = PBXBuildFile; fileRef = 114279FBFB8CEFB0597BCACAFD61F21C /* ph_bit_reader_utils.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 30C93CCDE28363680E50EA3A91FE8B0A /* Optional+Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC5EDD8394DA064EC7C83ACD7ECCD71E /* Optional+Util.swift */; }; + 3155F7F3D2B67132C14891B5A2E8F65B /* alpha_processing_neon.c in Sources */ = {isa = PBXBuildFile; fileRef = 6293FFF4CAA83AC8938A5AF805A16B4E /* alpha_processing_neon.c */; }; + 32AECF33E41A6D0971F70B1220A182F5 /* URLSessionInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 439F6F8B57A9D0DC1E4C4669A0B6BD70 /* URLSessionInterceptor.swift */; }; + 32BD682BFDA6241281CEBA7904C5A5A1 /* PostHogContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94407C342DD9E167C5D75E705899A981 /* PostHogContext.swift */; }; + 3491FB1468B98D7D43A3D900A0B00281 /* sharpyuv.c in Sources */ = {isa = PBXBuildFile; fileRef = DF2A3A811E0015722FF7E34E9338B448 /* sharpyuv.c */; }; + 360ABBEBC1B15FDCF4DA237E7647F2BE /* sharpyuv_csp.c in Sources */ = {isa = PBXBuildFile; fileRef = B2FFDD72D120FD8743D882769EF18E9B /* sharpyuv_csp.c */; }; + 36C07E870675759BC50B2A25CCC281C5 /* Pods-CableTests-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 85828150034429DD2DBC8A14669BDF37 /* Pods-CableTests-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 38177A6BBDA55B9E965610C950A24204 /* PostHogSDK.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF48F37324DDA42ABC08BC0E63AE23FC /* PostHogSDK.swift */; }; + 38AF18D906A0862245B23AA744DEFE31 /* lossless.c in Sources */ = {isa = PBXBuildFile; fileRef = F553AE255E9F4CBC202ED457EF76082F /* lossless.c */; }; + 38E6D9105E78ED4CC0E127C62F53B331 /* Pods-Cable-CableUITests-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 51F57C7D4BAC17BBF066E3F6D4C4B59A /* Pods-Cable-CableUITests-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 395B15A53215A65E19F9C6A19D422D3A /* ph_random_utils.h in Headers */ = {isa = PBXBuildFile; fileRef = A64CAB6E8B5412C7633F7B8565B8651D /* ph_random_utils.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 39FD34007D0F0D009C7F332C7DC5E187 /* sharpyuv_cpu.c in Sources */ = {isa = PBXBuildFile; fileRef = 79C664EEE1DC1B7E4C1AD3FFBCE62DCF /* sharpyuv_cpu.c */; }; + 3A184F0C63E5C112D93A58B9032A6B58 /* ph_webpi_dec.h in Headers */ = {isa = PBXBuildFile; fileRef = 44A2B199B29D72CB41D7DCC66EE77960 /* ph_webpi_dec.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 3B1D530F1C85B1227913F0026BFD0BDC /* UUIDUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39451BDDC73A8B6E24FFE97D8F418255 /* UUIDUtils.swift */; }; + 3C960D07046A6486F7DC7FD20FC938FA /* ph_sharpyuv_gamma.h in Headers */ = {isa = PBXBuildFile; fileRef = C1C1F47ADEAE9E1F0B9810DF4E04BD56 /* ph_sharpyuv_gamma.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 3E0A9BA7670824E8A542BB0238965308 /* URLSessionSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CACEF186F8DF80FAFD17F7D33BC5D71 /* URLSessionSwizzler.swift */; }; + 3E4221A803DEDC8750348BA20A791EEE /* PostHogSessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D888AE3CE3943C819B306C6E7A8FF6BC /* PostHogSessionManager.swift */; }; + 4006CE8FE027629C962E1BFB2A56F858 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 384DDA2CB25005BD6479B5987C619DD4 /* Foundation.framework */; }; + 40BD75D3EE82B288BCAA04D3DACEBD99 /* Survey+Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26FE4D61E0E5E088E0A627B0FDFC294E /* Survey+Util.swift */; }; + 4238E88AE8BA8624075729D326C0F4A9 /* alpha_enc.c in Sources */ = {isa = PBXBuildFile; fileRef = 02F6988AFBC0B2AF511F86755F935B13 /* alpha_enc.c */; }; + 438E9D989048FFC3DF68443837BF6398 /* PostHogExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7C8924423D336E936B9B8D7528391B5 /* PostHogExtensions.swift */; }; + 4391AA0D63D56F3F6233972901D35B62 /* bit_writer_utils.c in Sources */ = {isa = PBXBuildFile; fileRef = 9FBABCFADE0E13362BC515109844B0B7 /* bit_writer_utils.c */; }; + 43DBAA9AC5E2E63021F065F66FCBC065 /* ApplicationEventPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 400531B7575C215F76B0965A3F16DDA0 /* ApplicationEventPublisher.swift */; }; + 44058A91D10EDA3415018DE7925C348B /* SurveySheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6304E8E5FDED5863B9DCE550E79F2F13 /* SurveySheet.swift */; }; + 450E0503FAA2B07BD4D1F2C3CB11B122 /* picture_rescale_enc.c in Sources */ = {isa = PBXBuildFile; fileRef = F3BBC9AA886853862CBA71BF1966D0F2 /* picture_rescale_enc.c */; }; + 453AF00CBDEE42C069369E1F1BB482FC /* ph_common_sse41.h in Headers */ = {isa = PBXBuildFile; fileRef = 3B8A9E04574F84317E10956DD24EE006 /* ph_common_sse41.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 475FD727732636A7758AA528CA362744 /* ph_huffman_utils.h in Headers */ = {isa = PBXBuildFile; fileRef = DE79FAF47ADBBDA1FD96FA4EB80CD2D4 /* ph_huffman_utils.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 4805E842E48C745A9E6A4C06D4731373 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = AC3ADEA740D3607618548A1451E8EE35 /* PrivacyInfo.xcprivacy */; }; + 48B8B5B751C3E55843A697C3792C7837 /* palette.c in Sources */ = {isa = PBXBuildFile; fileRef = 4B69F8B2559E5EE459C85C21EFEE836B /* palette.c */; }; + 493D47C4E0ADF0AA3C30D3FB6C2F9C57 /* CGSize+Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CAF2DEC195826ABDDD705FA32B3BAC /* CGSize+Util.swift */; }; + 4A2589F3E16BE0902709FF2A3630BE17 /* SurveyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 262A25E1DBCEB2502F06974ED0FE5FBD /* SurveyButton.swift */; }; + 4BB3C8C41EA289FB29DCEC068F7FCD76 /* UIImage+WebP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5131F1FFA6BB84612F7867F86C51FCBE /* UIImage+WebP.swift */; }; + 4DE8C331A0B2A6959482C25ACB77E609 /* ph_sharpyuv_cpu.h in Headers */ = {isa = PBXBuildFile; fileRef = BCC15FDAC7750FBA4BA88CC539B5231E /* ph_sharpyuv_cpu.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 4E1473B9FF22300F21A2ABCA3911A03B /* picture_csp_enc.c in Sources */ = {isa = PBXBuildFile; fileRef = 94288A5DC18B54C6DC2DD8C571B0F0F6 /* picture_csp_enc.c */; }; + 4E33D63DE814B6144A257A9CBE8B695C /* lossless_neon.c in Sources */ = {isa = PBXBuildFile; fileRef = A296C0A01AD7CFE6D2C8A9367A6C2A67 /* lossless_neon.c */; }; + 4EA5892CD434E01818878C81F1452F65 /* ph_sharpyuv_dsp.h in Headers */ = {isa = PBXBuildFile; fileRef = EEA4A4F055CC07589E72A35CB0E1EF28 /* ph_sharpyuv_dsp.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 4ED6FC8835C4F8D158F713E6D7E8BB76 /* sharpyuv_neon.c in Sources */ = {isa = PBXBuildFile; fileRef = 77C6B393EA1E57769EC0389F1AD05B9C /* sharpyuv_neon.c */; }; + 4F5F04A349551F176FFCF2A5F318055D /* token_enc.c in Sources */ = {isa = PBXBuildFile; fileRef = 04F316A4B2CB95903F1476FD8F6E5506 /* token_enc.c */; }; + 4F61F0B687C035589D0FD4E29A73B0DF /* ph_neon.h in Headers */ = {isa = PBXBuildFile; fileRef = A4B8C487F55A8DC54688605DFF2114A6 /* ph_neon.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 4FA4C53C4992B0098AACCF9805B840B3 /* UIView+PostHogLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 394497CE62FB8F5E127D55A66D718803 /* UIView+PostHogLabel.swift */; }; + 50B1BD836429AB01BD316C26265CD71C /* huffman_encode_utils.c in Sources */ = {isa = PBXBuildFile; fileRef = 2D78E84DEDA315BA0155AF3658637CFC /* huffman_encode_utils.c */; }; + 51557FD624B2D8D939BD15D6D67DAA9F /* AutocaptureEventProcessing.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0B671EE629623603942CB46DA763609 /* AutocaptureEventProcessing.swift */; }; + 51F949044932E2740E23BC026B3CB070 /* DI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DB3C2029FEE43C9492E48815A749CD8 /* DI.swift */; }; + 526DF460F26A30E9A4C72AE94B898DAC /* BottomSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50567BD13E8812DF3257A08243491EA /* BottomSection.swift */; }; + 52D4C546D7A305CC6D9B29EBBE6BA5DA /* DateUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 812FDF6DD4F545B16DF167DAEA77C36B /* DateUtils.swift */; }; + 538F223AE0B8D0CA9A7DFF6C1BCDF61A /* PostHogConsoleLogInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE54094ABB405AD044CBC194CBC3787A /* PostHogConsoleLogInterceptor.swift */; }; + 54E24A21DBE22F50FF2C8ED9FE8C1920 /* ph_cost_enc.h in Headers */ = {isa = PBXBuildFile; fileRef = FBA8E6C91C9871D781A894E9021E767F /* ph_cost_enc.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 550CB5E2CAB7E6F88A94256F8D4E1AD0 /* lossless_enc.c in Sources */ = {isa = PBXBuildFile; fileRef = E0D796ED35657AF32721999BD872B40E /* lossless_enc.c */; }; + 560F19C3A96258BB787771F53438277C /* PostHogSurveysDefaultDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57AED642173AA14A5AB8433E3D4B85ED /* PostHogSurveysDefaultDelegate.swift */; }; + 569413C3DF6867A0AF74DE0A54B21317 /* ph_vp8li_enc.h in Headers */ = {isa = PBXBuildFile; fileRef = 268E4C94970868F0267B55DD268C4FDC /* ph_vp8li_enc.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 570EB0858F1AFE99A8B7CA45C6C3BAEE /* PostHogScreenViewIntegration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49AA24A61F055E7B69C6A5D8C6B2AD78 /* PostHogScreenViewIntegration.swift */; }; + 580FDEB8A8A152A39A502A40A58724C2 /* PostHogStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B7E943BABB7F3EAC5DE3E570F21D845 /* PostHogStorage.swift */; }; + 5A6216FF081F6510212318F7654826F2 /* huffman_utils.c in Sources */ = {isa = PBXBuildFile; fileRef = 16D25A3C7A45F1954C4E6B068C58B7EA /* huffman_utils.c */; }; + 5C24BD9E95BD5410843BD372C028EF2F /* cpu.c in Sources */ = {isa = PBXBuildFile; fileRef = A7467F12C852FA55CE696623D0C49893 /* cpu.c */; }; + 5C603F51B177ED3F49DA5B71B25253F4 /* bit_reader_utils.c in Sources */ = {isa = PBXBuildFile; fileRef = 143960F19300CE4777F0CC55F8DD8E31 /* bit_reader_utils.c */; }; + 5CC284C9FBACD9B6424307C1E8EE2618 /* PostHog-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 9B2AAEABCF4BAAC96E0099538E6FAEA5 /* PostHog-dummy.m */; }; + 5E514467B3F4FB8469C113819DE25FF2 /* String+Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98C872681D85E83B48A177D688728D6E /* String+Util.swift */; }; + 5F8EF5FE4D73E4F038E2DEA4D5C6A146 /* PostHogQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED3CF8C4E1C8C5343DE62E4B8A2043AD /* PostHogQueue.swift */; }; + 6045482673A7946DF31DA4DE7CBC35F6 /* PostHogSurvey.swift in Sources */ = {isa = PBXBuildFile; fileRef = C84990ADFAD35F17E5D87A70DE6B2768 /* PostHogSurvey.swift */; }; + 615D7CAA8FFEFF59CC50B16C3981EBE4 /* PostHogStorageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFB8864DE15913957BF2887CC886727E /* PostHogStorageManager.swift */; }; + 627DC06DB4D42F2FB9605AF95A528471 /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34EBB44BB3ED427636A0A0A673F61914 /* Errors.swift */; }; + 62B8676E90C8E48016693783C0036D8F /* ssim_sse2.c in Sources */ = {isa = PBXBuildFile; fileRef = 71C01AE5C1556B7FCEB70598A2359C57 /* ssim_sse2.c */; }; + 645BFE97A3A8E993F08B87285951AB82 /* TimeBasedEpochGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74828AFF764C4FBA1E8273C81B8E961D /* TimeBasedEpochGenerator.swift */; }; + 64C0E1A516D383EA3EC23A7BFC448FCE /* EmojiRating.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07ACC03496AD0255B5AF70CE5B4D3739 /* EmojiRating.swift */; }; + 65FEDD05D8424912681558FCA1154C7D /* filters_utils.c in Sources */ = {isa = PBXBuildFile; fileRef = 262DE51E90B8F78B18D4D167AAC6C4B4 /* filters_utils.c */; }; + 6899E938FC1259735FECEDCCBD5DCB12 /* muxinternal.c in Sources */ = {isa = PBXBuildFile; fileRef = 58890CE1C13F1D218F91B8F1B662297C /* muxinternal.c */; }; + 6979FEA56C2EE86990E4473D5F0085CF /* ApplicationLifecyclePublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F788308CE59B5B07BDA5820491EC32B /* ApplicationLifecyclePublisher.swift */; }; + 6A604351BC7DAE8F47EADA54FA67BA92 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 384DDA2CB25005BD6479B5987C619DD4 /* Foundation.framework */; }; + 6B8EC9F9C886019474D30A2A38FF68B2 /* ConfirmationMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AC357F49D750FDC096D0A38EF084E0F /* ConfirmationMessage.swift */; }; + 6CCFABEA9067B84D78A97723B7424B2A /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 384DDA2CB25005BD6479B5987C619DD4 /* Foundation.framework */; }; + 6CD6C7D137F96E4B377097CC311E07A9 /* sharpyuv_dsp.c in Sources */ = {isa = PBXBuildFile; fileRef = DB9CEA743E2F233A85546C94DD807354 /* sharpyuv_dsp.c */; }; + 6D1C160E221E65E31F2C2C1FB79BF4B3 /* rescaler_utils.c in Sources */ = {isa = PBXBuildFile; fileRef = 85583DB1C0D12DF398032FE155609A9E /* rescaler_utils.c */; }; + 6DCDBF153AE370C69768E7A7B57F0748 /* PostHogSurvey+Display.swift in Sources */ = {isa = PBXBuildFile; fileRef = 752389685141ECE7C426B3DEB3D9F601 /* PostHogSurvey+Display.swift */; }; + 7203FB71B522DF771D5BF42CFE86CE87 /* utils.c in Sources */ = {isa = PBXBuildFile; fileRef = 24177CAF8EE40225DC590E24E98B5907 /* utils.c */; }; + 7206DFEB5BB955754C39FC17011FF545 /* PostHogSurveyIntegration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B69967DBE2F79C655769D9E766052B9 /* PostHogSurveyIntegration.swift */; }; + 7241253A695D3360662518FABF8A18F9 /* Pods-Cable-CableUITests-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = C77B0E69679F739AA5F6EF3472F81B3E /* Pods-Cable-CableUITests-dummy.m */; }; + 73FCA47CCEAFE7E4B070B112F21A9C7E /* PostHogSurveyEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = 701958AB5C27BF0CCC2736BED2525ADA /* PostHogSurveyEnums.swift */; }; + 7460FA92770379998EFE3064CC0A9530 /* ph_sharpyuv.h in Headers */ = {isa = PBXBuildFile; fileRef = E71BC82ADF0D9EC4F9A390C9D17DAEB7 /* ph_sharpyuv.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 74A2933F21E1826BFCE98767611A1F2B /* PostHogAutocaptureIntegration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3B1285FF54F13BB960970B7370E7941 /* PostHogAutocaptureIntegration.swift */; }; + 74B9900ECC81F957E50D152BBBED824E /* NetworkSample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516914075585022AAE4D7BBA974FC678 /* NetworkSample.swift */; }; + 7597FE04F73E19981054D053749CBC7C /* picture_enc.c in Sources */ = {isa = PBXBuildFile; fileRef = 856534F765DD23D7992D3EC872FE93B7 /* picture_enc.c */; }; + 76A22346D2892A75D2C605B1E46C9C01 /* ph_encode.h in Headers */ = {isa = PBXBuildFile; fileRef = 5A40B8F5231A7944996ACCC1E340899D /* ph_encode.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 76A53A0EB8A7F9C73557B3B32D669BBB /* PostHogSwiftUIViewModifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9D50B5F78458141D8272AA365385479 /* PostHogSwiftUIViewModifiers.swift */; }; + 77EB7B90D005993B65D9AD7A2533B13D /* enc_sse41.c in Sources */ = {isa = PBXBuildFile; fileRef = 399AFF4367999AB195B1A4861DE2E3E1 /* enc_sse41.c */; }; + 790E4BF42B672B3EA4E21B66CB0DB05B /* lossless_sse2.c in Sources */ = {isa = PBXBuildFile; fileRef = 9E7A2B7242409A2E9C0F0B5B9DBCC3A8 /* lossless_sse2.c */; }; + 7A4857DA3F9C646CCC878F9329D19E60 /* Pods-CableTests-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = D07D529105A58FA284174316608207E1 /* Pods-CableTests-dummy.m */; }; + 7DA49ED9EF36CCDB27E476FFFC6D5660 /* PostHogFileBackedQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 866330A74D6DEBA7354A80F57B4FB378 /* PostHogFileBackedQueue.swift */; }; + 7E015D3260F0BFD29E3F0690B1E59E0D /* ApplicationViewLayoutPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C60B359AA89658785332F5EB04C61112 /* ApplicationViewLayoutPublisher.swift */; }; + 7FDBA2F04F5750B61E6DF8FA25F849BA /* iterator_enc.c in Sources */ = {isa = PBXBuildFile; fileRef = 4282C330B006663023D6DA60644E9C71 /* iterator_enc.c */; }; + 80DA5FB9AFF890ABE611B5A044CDB036 /* rescaler.c in Sources */ = {isa = PBXBuildFile; fileRef = 2DE0CB3567095D87C771243576EF9353 /* rescaler.c */; }; + 81A20054B797FF0F766333BE4913DBCC /* alpha_processing_sse41.c in Sources */ = {isa = PBXBuildFile; fileRef = 676CFD3B35C9F8564C51E1B617A3937C /* alpha_processing_sse41.c */; }; + 81A74112BDB883335465416F2DDD4F73 /* PostHogSessionReplayConsoleLogsPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEA18A14AB5E18A4522909D739010F8B /* PostHogSessionReplayConsoleLogsPlugin.swift */; }; + 822476AFB7FFB4F8605DC4FA8A19D926 /* PostHogSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69AC865B00E170495D08F65246F26683 /* PostHogSwizzler.swift */; }; + 82AAEEC1837B795CD013636BAEF14DA9 /* yuv_sse2.c in Sources */ = {isa = PBXBuildFile; fileRef = B233AD5FB3C464116639B4C4A446D5A9 /* yuv_sse2.c */; }; + 83FEE6E290BF6CA38102DFB7009F6E4B /* ph_lossless_common.h in Headers */ = {isa = PBXBuildFile; fileRef = 44B2D98D590BDDF72420B7AC9EE530EF /* ph_lossless_common.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 847080B8D7683BBBEF894EE6EA38912E /* PostHogSurveysConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15D68B2FA6D4A041D8698FC67954D8CF /* PostHogSurveysConfig.swift */; }; + 8494528FA0360A1CAFFAC147C9849375 /* ApplicationScreenViewPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3B5BAECFF77239E331B522C7107C1A0 /* ApplicationScreenViewPublisher.swift */; }; + 858173DFD0E9446CB773D1CBB00418BD /* dec_neon.c in Sources */ = {isa = PBXBuildFile; fileRef = 0093492C1008E66439CF39ACE752BD1B /* dec_neon.c */; }; + 85A31731349118825F11B6BD97570A72 /* Pods-Cable-CableUITestsScreenshot-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = C94C1436FB9990B1C8458A4890A74FDD /* Pods-Cable-CableUITestsScreenshot-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 85E00607DEA0B702A88256B889A0AE46 /* ph_color_cache_utils.h in Headers */ = {isa = PBXBuildFile; fileRef = 10FE6C33488F83B12D9A37C71584E723 /* ph_color_cache_utils.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 866CAB0B3150FEF773928C86AD9171E1 /* ViewTreeSnapshotStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAA1A0EEEA8915D7ED932898580D55DA /* ViewTreeSnapshotStatus.swift */; }; + 8753E838E3006A1E2F72DF5F6AC2C67A /* Date+Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07439B205D54D4542249407BDC340D8B /* Date+Util.swift */; }; + 889631E71D504EF98E0F26B2E5E45DD9 /* MultipleChoiceOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF7956F6F3924F58644A57ECA19C3559 /* MultipleChoiceOptions.swift */; }; + 89AB8E555A39C1D168505C89C8CF5777 /* UIViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A884B1BB87102903507CF0084AE903C /* UIViewController.swift */; }; + 89D364405A06C6C8B06DA285AC3E5A5F /* filter_enc.c in Sources */ = {isa = PBXBuildFile; fileRef = 8C1E609130982209FE02FE99B78BDC88 /* filter_enc.c */; }; + 8A2EA1FD1DE278407880880388CC7863 /* thread_utils.c in Sources */ = {isa = PBXBuildFile; fileRef = BCE3D06D9F5BD48177BA8DD707F3B037 /* thread_utils.c */; }; + 8A7B3562B53BEF18CBED9C9821C6A371 /* ph_format_constants.h in Headers */ = {isa = PBXBuildFile; fileRef = FB3C8E706E1BB31142E0F7F3BDAAD7A3 /* ph_format_constants.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8B95C63A2E2B72090AE5A5AB08AA2F26 /* ForwardingPickerViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BC3CBC81194A98EBD7798839CE8D8C4 /* ForwardingPickerViewDelegate.swift */; }; + 8CC38A2ED3E279B2AFF414782AF6EF05 /* PostHogSessionReplayNetworkPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52C45951D83FB2A2320DCC090D6EE882 /* PostHogSessionReplayNetworkPlugin.swift */; }; + 8D7AFBBCADCA7780494700C5EE031A7A /* SurveysWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A8771BEB422C0EBA1669CCBCC77482 /* SurveysWindow.swift */; }; + 8E6CE9ACB1DEA1CB2ED4E8C43A6CB1DF /* picture_psnr_enc.c in Sources */ = {isa = PBXBuildFile; fileRef = FD4EE07A95F242B51F29985FC791B86C /* picture_psnr_enc.c */; }; + 8EC01D8A4D04C123025FD6958165F4F5 /* ph_quant_levels_utils.h in Headers */ = {isa = PBXBuildFile; fileRef = 0C5591E49085B223152BC20087DBC2F5 /* ph_quant_levels_utils.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 900853FFC431D50BA4A0E6B015873734 /* quant_levels_utils.c in Sources */ = {isa = PBXBuildFile; fileRef = 8460AF919A0E871BF4A73C742BE5F688 /* quant_levels_utils.c */; }; + 918561BED43D6D9A55D7ED87A4233FAA /* quant_levels_dec_utils.c in Sources */ = {isa = PBXBuildFile; fileRef = D601464067F9E5B1E5D5620C8924912F /* quant_levels_dec_utils.c */; }; + 91A181721A95245C3A19DD7DF8D3ED20 /* quant_enc.c in Sources */ = {isa = PBXBuildFile; fileRef = 58B2390554BF82073BFFF56330375E16 /* quant_enc.c */; }; + 923C6E3BE82D64E5623625D81D82FBA8 /* ph_types.h in Headers */ = {isa = PBXBuildFile; fileRef = 81A9CA6BA8912B85E00744D072E0AF98 /* ph_types.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 932C39898E846B7343F58C87A7C8A18D /* upsampling_sse2.c in Sources */ = {isa = PBXBuildFile; fileRef = 58E3377D17B30CB16D0310778D68FF1B /* upsampling_sse2.c */; }; + 93F25171D356A06B938299A8DC1B01A3 /* PostHogAutocaptureEventTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 770559699B6C4F893D1E7A02F2CD0026 /* PostHogAutocaptureEventTracker.swift */; }; + 944E6DAB32560E951BAE16F5157F0D35 /* PostHogNextSurveyQuestion.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3261BD495FD61B20BC8BA98578865B7 /* PostHogNextSurveyQuestion.swift */; }; + 947857B02E90224ED7E680233305DB5E /* PostHogSurveyConditions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B45CA8B0658CABEFE16DEF11945D4C96 /* PostHogSurveyConditions.swift */; }; + 95D55CBE1ACFF50FD1EBC6D4936B2E94 /* PostHogConsumerPayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FDB8663680B7CBA9F5E40A296878638 /* PostHogConsumerPayload.swift */; }; + 96B479CE48E29ED5239A38806540AFA6 /* enc.c in Sources */ = {isa = PBXBuildFile; fileRef = B73ECF10580052B7EADF1C8126C1F5D4 /* enc.c */; }; + 9748FF5103EE1045677BDAED5F00DFC9 /* PostHogSessionReplayConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 103153A1783959BDADE5A086BAEE932E /* PostHogSessionReplayConfig.swift */; }; + 97F67C732B893F315EFDD5D598C3F71E /* config_enc.c in Sources */ = {isa = PBXBuildFile; fileRef = 738AC56CBEF0F5C46F68B5F6C393CE26 /* config_enc.c */; }; + 9ADEA2121D351229E95318A512FB3E0F /* cost.c in Sources */ = {isa = PBXBuildFile; fileRef = B24A16C60C8C5673028974678594E6E8 /* cost.c */; }; + 9B3694B60BA51B86814BC4B1C0AADE57 /* Reachability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E80C814BD248103DB298C992AB07604 /* Reachability.swift */; }; + 9B7B7D98445B36EC45CFE01B99DC85AB /* lossless_enc_sse41.c in Sources */ = {isa = PBXBuildFile; fileRef = EFBDDE4003994362E3FE58381610B087 /* lossless_enc_sse41.c */; }; + 9C0BAA41861364D4FF410FA385BA695D /* NumberRating.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B5738CAC80AE5D72B75358EEE9D7559 /* NumberRating.swift */; }; + 9C1AF5AC07E7ED9CCAFECF84C27E73BD /* PostHogPropertiesSanitizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8ACAC7F2DBEC2E6F6C08E461831738E /* PostHogPropertiesSanitizer.swift */; }; + 9C1C48333A30CF28A6F8F918216615FB /* UIColor+Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC7A62B6B9F7CF772B7553E3B238AAB3 /* UIColor+Util.swift */; }; + 9C95E9E96A19763B1F0B805841F064BF /* ph_histogram_enc.h in Headers */ = {isa = PBXBuildFile; fileRef = 1DA4E5C73A12E0A6898B70F3A9CED1A6 /* ph_histogram_enc.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 9DAE58DDF04BB8AC6700C178740CA4A2 /* webp_enc.c in Sources */ = {isa = PBXBuildFile; fileRef = 46FD7041B91F02D8C44A8B23724610E8 /* webp_enc.c */; }; + 9DFA8122176106229CC767FEAF4B3304 /* lossless_enc_neon.c in Sources */ = {isa = PBXBuildFile; fileRef = 69AF4F959032087EDA8FC0D0A5B6209D /* lossless_enc_neon.c */; }; + 9E27DE50AA66E7023C06263FB25B71AE /* filters_sse2.c in Sources */ = {isa = PBXBuildFile; fileRef = 13CD7EE11BBD992CE37375834390CA29 /* filters_sse2.c */; }; + 9E32FD9B51892EE4CDE615D15D2FC878 /* ph_vp8li_dec.h in Headers */ = {isa = PBXBuildFile; fileRef = 229D517849A145DB966B04AE150168FB /* ph_vp8li_dec.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 9FAB5C450670E109F781D6C53E56D2AF /* PostHog.h in Headers */ = {isa = PBXBuildFile; fileRef = 67DFC8AB8EF46C4E3F28229F5DCE1C8C /* PostHog.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 9FF301A6E7FAA2AAE00ACD5E80FD4C9B /* ph_cpu.h in Headers */ = {isa = PBXBuildFile; fileRef = 93E56AB9DB0FA2E618B09B988C467DD9 /* ph_cpu.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A086FB83D5610E0968532EF591FDC242 /* ph_mux.h in Headers */ = {isa = PBXBuildFile; fileRef = FC51F4877491548B2CE8612D8EBE8994 /* ph_mux.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A1C65033EA26B7634A850B9635138B45 /* PostHogSurveyQuestion.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA884DB8D0FD6DE10EC0E09ECBCB0FCC /* PostHogSurveyQuestion.swift */; }; + A2111B640365DEB1D1262861FB526BB6 /* Data+Gzip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CFF0D65E2D3133D28C9527F218B033B /* Data+Gzip.swift */; }; + A23F680D118045C38E24C79BCA87F3F8 /* MethodSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DCCB7DFFA938AEFD75591B645CDDDF0 /* MethodSwizzler.swift */; }; + A25DB2959F2533A76503BA4FA1C3D8A8 /* ph_bit_writer_utils.h in Headers */ = {isa = PBXBuildFile; fileRef = C15956FAD29CD446AED83B28072F258A /* ph_bit_writer_utils.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A4351828FD656D27899119C43282A46A /* Pods-Cable-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 3B075A62E32AE782E63CB647C2425908 /* Pods-Cable-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A49FA4C1772B8C68D42DD34E69C76134 /* lossless_sse41.c in Sources */ = {isa = PBXBuildFile; fileRef = 8F310530386C455D52673B41DA4A2B76 /* lossless_sse41.c */; }; + A7E4B6A307B6D11D3216D24DFF540A76 /* upsampling_neon.c in Sources */ = {isa = PBXBuildFile; fileRef = C52A7E8E1B3DA2B543DA937C3BE7162B /* upsampling_neon.c */; }; + A87193AFE5CEF7AA673BC80BA4FB9C9B /* UIApplication+.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07415FC3293C535290E997DFFF959FD /* UIApplication+.swift */; }; + A881E1170145D6E36C8AD2E62BDCBB9A /* ph_common_dec.h in Headers */ = {isa = PBXBuildFile; fileRef = D22E0F8ACB0AADC33D8564D1D4E72C93 /* ph_common_dec.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A923905603728FF1F5D63FA3383A7647 /* upsampling_sse41.c in Sources */ = {isa = PBXBuildFile; fileRef = C87BE5C48779C989CE62FA8282E8B3D5 /* upsampling_sse41.c */; }; + A958EABDF4F3CF594A315F89E9E5CB16 /* PostHogDisplaySurvey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66AFFADA1464DFC165AF775D6EDDE652 /* PostHogDisplaySurvey.swift */; }; + A9947D06E0701770A3D401BD4B4DA0D1 /* ph_muxi.h in Headers */ = {isa = PBXBuildFile; fileRef = E20DB8D1A1DFFDC41590109620887074 /* ph_muxi.h */; settings = {ATTRIBUTES = (Public, ); }; }; + AC8C4D813E979741EB84B24C111118F0 /* PostHogMaskViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = D88D282FD37C24951D615E881B2D8B5E /* PostHogMaskViewModifier.swift */; }; + ACF234EB88FF3EFA147BF0AB0A317A4D /* ph_vp8_dec.h in Headers */ = {isa = PBXBuildFile; fileRef = AC8C2B6DA50936FEAAA8D7CF5AD97F60 /* ph_vp8_dec.h */; settings = {ATTRIBUTES = (Public, ); }; }; + AD26200A6DEF1516F1C192768115BC55 /* filters.c in Sources */ = {isa = PBXBuildFile; fileRef = 22394C97F8C3BED5DFED7C780C1CE564 /* filters.c */; }; + AF9FF002C9339CCCD33ADA5476D20C72 /* ph_vp8i_dec.h in Headers */ = {isa = PBXBuildFile; fileRef = 63BDD7A067C75995C9F44A5BD44EE5E7 /* ph_vp8i_dec.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B1F2CAAAD6999BC7C09AE72FB869D703 /* CGColor+Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF0D333280C1AA92C268AF36D289B8B /* CGColor+Util.swift */; }; + B4A5799A36D3D50E81703A45BAEE77A3 /* ph_common_sse2.h in Headers */ = {isa = PBXBuildFile; fileRef = F4C64C6E9AA1FDB5C402603F7B39F3BC /* ph_common_sse2.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B511EA85DAC18FD6205B26B71DE1050C /* UIView+Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3D701A5BB86EA2FBA69F57589340667 /* UIView+Util.swift */; }; + B9342CDA79BCBDBEAB3D05BB11EDDBFD /* Pods-Cable-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = A2428DC0690BFC906FB6BBC9517C91B8 /* Pods-Cable-dummy.m */; }; + B94F9615E1406645D9976C740A12F52E /* RRWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDA36D795E2D2304CF2F39068E99EED8 /* RRWireframe.swift */; }; + B959C06622003BB1462ABA53313D422B /* PostHogSessionReplayConsoleLogConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA4A2E54DC65E09C0702AC19D93C51D0 /* PostHogSessionReplayConsoleLogConfig.swift */; }; + BD645B55581F18C45D3A077C7120F169 /* dec_clip_tables.c in Sources */ = {isa = PBXBuildFile; fileRef = 801F6B74D8E565882EB6463915EE29D9 /* dec_clip_tables.c */; }; + BDE90F6901C6138EF6CB8F5ADA8F6061 /* ph_thread_utils.h in Headers */ = {isa = PBXBuildFile; fileRef = 52ABD4159B83070BF140003F701169DE /* ph_thread_utils.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BF00726324688B38AB498AB8C0D6A9CC /* PostHogVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = A40B6975FA38F82CFAD982D745DC1177 /* PostHogVersion.swift */; }; + BFF5E04D16488F3C122C6F2099D16702 /* PostHogIntegration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58006037934279EEB6F765806CE31D38 /* PostHogIntegration.swift */; }; + C085F172DD546D7A954F9E7CFA6549AF /* Float+Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = F091F375187698A52912BC097838826A /* Float+Util.swift */; }; + C10F91F1C306F86A8DE647E9392EAD5B /* PostHogAppLifeCycleIntegration.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBAA59A93C40F18F756F62E7761DDDA2 /* PostHogAppLifeCycleIntegration.swift */; }; + C1D7ABBD432849EC886D9852B1A06CC4 /* SurveyPresentationDetentsRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AE8485816F0A8CEE5E8A00890CA324A /* SurveyPresentationDetentsRepresentable.swift */; }; + C20E16F51EE280913ECD5C8939075863 /* picture_tools_enc.c in Sources */ = {isa = PBXBuildFile; fileRef = CDACF4D658D473E1D0C0BC475C6B6D9F /* picture_tools_enc.c */; }; + C2D9AAF9E55A505AAEA9DDC14B0BED48 /* color_cache_utils.c in Sources */ = {isa = PBXBuildFile; fileRef = DF049858415B4235BCDBB079C3581BA4 /* color_cache_utils.c */; }; + C38E5112CB2F46733703F641350364B7 /* enc_sse2.c in Sources */ = {isa = PBXBuildFile; fileRef = E7F5A16999097A6591248430D5EE1DD5 /* enc_sse2.c */; }; + C3B88263FFF28B2C4B1C34BBFA2487CB /* ph_backward_references_enc.h in Headers */ = {isa = PBXBuildFile; fileRef = 80A1861E13FC4397C27E11FB9264C784 /* ph_backward_references_enc.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C40C37140BA4AC6DCF085AF14D6CBF2A /* ph_mux_types.h in Headers */ = {isa = PBXBuildFile; fileRef = E851D31270CCB1CA24F49C52015B7F59 /* ph_mux_types.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C41DEDAF518B62C012E82A92168E2F71 /* sharpyuv_gamma.c in Sources */ = {isa = PBXBuildFile; fileRef = ECD1B7D205F7FC63D45533FC2BAE3410 /* sharpyuv_gamma.c */; }; + C4A5285EB3B6FA8347156384E0693E4F /* ph_utils.h in Headers */ = {isa = PBXBuildFile; fileRef = AFC508491302293623F9C9FE123DF31D /* ph_utils.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C8B7CAFDAF0B31D48040478F71C0EBE2 /* cost_enc.c in Sources */ = {isa = PBXBuildFile; fileRef = 02AE2A2A0467EA70DB5D9C6CB93B7CBE /* cost_enc.c */; }; + C9FDB67918170E2DD508541BA216560E /* sharpyuv_sse2.c in Sources */ = {isa = PBXBuildFile; fileRef = CD9B87A72D9E03E8531BE4E64C96BBAA /* sharpyuv_sse2.c */; }; + CA4046A0C463B283FDEF14E88E615622 /* Resources.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30358A66B1E325E7BF8FACB77136DCBD /* Resources.swift */; }; + CA716D34DD2DC32B06C1C0B0B2C3C5C2 /* ph_filters_utils.h in Headers */ = {isa = PBXBuildFile; fileRef = 450735A51B25E2BC879F80FE3EF683E2 /* ph_filters_utils.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CCEB5CA997D8A5E48BD7E73B6F4EB71D /* ssim.c in Sources */ = {isa = PBXBuildFile; fileRef = 06902488BC3ACDB66C4CC8E3DA9B2BF1 /* ssim.c */; }; + CDCFCFCE3B5007033F6B1E3AD1C1545E /* DictUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 881B0B1AA8FD87B6D79DB655742B357B /* DictUtils.swift */; }; + CE53F772FBCCABD2B04587B2BD4FD177 /* FileUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13289E1D4EAA79792BF87E290976C758 /* FileUtils.swift */; }; + CEEA0DE4B861387A64E05F48B5A7CEE5 /* yuv.c in Sources */ = {isa = PBXBuildFile; fileRef = CBF4E22F61B73B539ABF63E8ADAAB87E /* yuv.c */; }; + D16B60C05E3B60104BB3BD49495D0879 /* histogram_enc.c in Sources */ = {isa = PBXBuildFile; fileRef = A9410C5F8607F2FC658BCBEDAD5EEFBE /* histogram_enc.c */; }; + D246F10610362DEE01F847DA6180E08C /* filters_neon.c in Sources */ = {isa = PBXBuildFile; fileRef = 86706A03E7D1636BA28748C6D1F0740A /* filters_neon.c */; }; + D4A6686C68814AF2106A2CAB329530DB /* muxread.c in Sources */ = {isa = PBXBuildFile; fileRef = 42AC22285670786E35A95F104334D436 /* muxread.c */; }; + D4AB0395E8CB9D1D7677DAE14B6B5F59 /* SurveyDisplayController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 635FCA4635E4DDDA1A3FC3953FC4C9E9 /* SurveyDisplayController.swift */; }; + D5AC0B15E162B74F89BE5C08EAD54621 /* URLSessionExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 905D11210787EACE6D48E66560631096 /* URLSessionExtension.swift */; }; + D76EBEDD6E19898C194F1FADF177664C /* alpha_processing.c in Sources */ = {isa = PBXBuildFile; fileRef = F45BEC0B42FB9EFA2C1B6AADA6AAF9E4 /* alpha_processing.c */; }; + D9BF8B39F7A90185A2AE69B33D1F91C2 /* SwiftUI+Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28E5B198F689C1C063C1DE0BF9B7ABAF /* SwiftUI+Util.swift */; }; + DA6006155C3D0397BC53C971B6764235 /* analysis_enc.c in Sources */ = {isa = PBXBuildFile; fileRef = DDC359F157B7B3B8BA8CEDD5389D7EC4 /* analysis_enc.c */; }; + DF22F1DD5882402BECE6B91CD1A227E8 /* SurveysRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AF87321A55BD1044FFD6A604445B369 /* SurveysRootView.swift */; }; + DFA1A5698507154A30CAA31FD6BD1A85 /* alpha_processing_sse2.c in Sources */ = {isa = PBXBuildFile; fileRef = 795E63F5459422366F9C7A22AEE66832 /* alpha_processing_sse2.c */; }; + E0749957A6BB7594DC26469DB77B0C7B /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 384DDA2CB25005BD6479B5987C619DD4 /* Foundation.framework */; }; + E1A545A284C243E666880CB425BA9F20 /* ph_vp8i_enc.h in Headers */ = {isa = PBXBuildFile; fileRef = 27378C145AF70BB39C853D63F329CD6D /* ph_vp8i_enc.h */; settings = {ATTRIBUTES = (Public, ); }; }; + E2A195E40A0C3853004F3B1C7E6ECD1C /* ph_quant.h in Headers */ = {isa = PBXBuildFile; fileRef = 7F3CFCC8A376939FB12B819B27393F00 /* ph_quant.h */; settings = {ATTRIBUTES = (Public, ); }; }; + E2BB8174AB27B7FC6D610F3D6D2E55C9 /* PostHogDisplaySurveyAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56F211460A3349D6478176319237385E /* PostHogDisplaySurveyAppearance.swift */; }; + E317933C3FAD82DD8D0C65EA369EF44A /* ph_dsp.h in Headers */ = {isa = PBXBuildFile; fileRef = 4BFDFC30C1DB0FBE9E1AAB4CA5D5E990 /* ph_dsp.h */; settings = {ATTRIBUTES = (Public, ); }; }; + E3C0C764F23ABB7818F31999FE8856BF /* SegmentedControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB852437A4FE639A36D1C941F94F307A /* SegmentedControl.swift */; }; + E617B7F0DBC07F3490AE7811551911D5 /* ph_rescaler_utils.h in Headers */ = {isa = PBXBuildFile; fileRef = C95684E80F2639489418BE63DB2999CD /* ph_rescaler_utils.h */; settings = {ATTRIBUTES = (Public, ); }; }; + E6285A5C5201781908B0FA8CC012CF82 /* ph_endian_inl_utils.h in Headers */ = {isa = PBXBuildFile; fileRef = 9F7478CDF58D2E47C91E9AEA4F3BFA36 /* ph_endian_inl_utils.h */; settings = {ATTRIBUTES = (Public, ); }; }; + E641EDF95A76D284807990177906C9C7 /* frame_enc.c in Sources */ = {isa = PBXBuildFile; fileRef = ED013EDE4144AF0F72EA8632E927919E /* frame_enc.c */; }; + E645C1A88AD4148D5B873D3811EA3BE0 /* QuestionTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C816478A7A89278F0B9617CD33B4ABB /* QuestionTypes.swift */; }; + E68C5CF478D5211B525ABCD7A11F346A /* PostHogLogEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03F6F40CA78AF34A91BC6DEC8595DF55 /* PostHogLogEntry.swift */; }; + E6E78E2A322D4E7736D55ADCE1D73BF6 /* syntax_enc.c in Sources */ = {isa = PBXBuildFile; fileRef = 390E5F5970C7EFFE8A499DFEFE839B86 /* syntax_enc.c */; }; + E7475B260DBC82214488EF1C5712B807 /* ph_sharpyuv_csp.h in Headers */ = {isa = PBXBuildFile; fileRef = 1E3A1E4CB22BAA257379DDFFB6654CC3 /* ph_sharpyuv_csp.h */; settings = {ATTRIBUTES = (Public, ); }; }; + E9576BFC34B3253843CC4778948ADC07 /* ph_yuv.h in Headers */ = {isa = PBXBuildFile; fileRef = 6F05DC32B52DAD2AA8964AB408980A1F /* ph_yuv.h */; settings = {ATTRIBUTES = (Public, ); }; }; + E9F821F9B181137CF9756E7B5DB290F8 /* PostHogSurveyAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AB66D01B3E08701E5AD477ECC306070 /* PostHogSurveyAppearance.swift */; }; + EAEEFEB5F2F8EDC54B52B4FEE8E0021E /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 384DDA2CB25005BD6479B5987C619DD4 /* Foundation.framework */; }; + EB51376036E66B13F5DA94861714AF5A /* ph_lossless.h in Headers */ = {isa = PBXBuildFile; fileRef = 6ABF4049707FD7850C0863E5B99FC269 /* ph_lossless.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EB6484265BE63031CDDCB790CD983769 /* lossless_enc_sse2.c in Sources */ = {isa = PBXBuildFile; fileRef = 53A2AF1A2EBA24407AC0E0E425DED2AB /* lossless_enc_sse2.c */; }; + EBDF53B9DD3C2058CE110560E30904D6 /* vp8l_enc.c in Sources */ = {isa = PBXBuildFile; fileRef = 1900B13AA3B133ACDED32587C775ABA2 /* vp8l_enc.c */; }; + EC0A7D763F31745069FD68B9549C9A9A /* dec.c in Sources */ = {isa = PBXBuildFile; fileRef = 9DC16A20D6C39A6FE6C700DE88890FC6 /* dec.c */; }; + EF9115BBB1E249A72211587F17B96835 /* muxedit.c in Sources */ = {isa = PBXBuildFile; fileRef = 172A611FED667B809856910CFB1F8DDA /* muxedit.c */; }; + F149A1F7278934A1B4C094EBF060D2B6 /* PostHogEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8213698CA3EB61F4236A6C8E334099A /* PostHogEvent.swift */; }; + F395CAD4C36A94E525379C352F11ADF4 /* cost_sse2.c in Sources */ = {isa = PBXBuildFile; fileRef = F85D4CA294CDFB70EE341652BCF427BC /* cost_sse2.c */; }; + F632525798590D31F88730CB5B08A136 /* backward_references_cost_enc.c in Sources */ = {isa = PBXBuildFile; fileRef = 716805E0DE609F449A51C263A653D182 /* backward_references_cost_enc.c */; }; + F64FD409AECB0B56E3D10EC2B87A36EE /* PostHogLegacyQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E6E0620A6F6472F44291ECF29967CCB /* PostHogLegacyQueue.swift */; }; + F8222B205BE0E6FCCBE3ACE42940599A /* cost_neon.c in Sources */ = {isa = PBXBuildFile; fileRef = 3BBA09ECA091215738C1BE2FFE5F091C /* cost_neon.c */; }; + FDDD950364CB85A70790993AD79EFB17 /* PostHogNoMaskViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 905362F80FAC60800B3E20073D217B0C /* PostHogNoMaskViewModifier.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 0D8CAE93284D0305C41A4E72F6A07403 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 8879D5F28A55518ACFB247594F87F75A; + remoteInfo = PostHog; + }; + 61238D0FC731DB2BBAA0E8FC812EACC8 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 8012054959C338A62834AD9706977FB0; + remoteInfo = "Pods-Cable"; + }; + 62E12AA42617D0B8EC3B5959449CF6A7 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 8879D5F28A55518ACFB247594F87F75A; + remoteInfo = PostHog; + }; + BF702A62A6C910D491628C2736B02269 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 8879D5F28A55518ACFB247594F87F75A; + remoteInfo = PostHog; + }; + F71674A1D40932D753945F87EC1C5BB1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = E326EE08AE4CF9FA8C947B96B6F8AB07; + remoteInfo = "PostHog-PostHog"; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 0093492C1008E66439CF39ACE752BD1B /* dec_neon.c */ = {isa = PBXFileReference; includeInIndex = 1; name = dec_neon.c; path = vendor/libwebp/dec_neon.c; sourceTree = ""; }; + 00B098547C4C7CC1269E78AD6477793A /* Pods-Cable-CableUITestsScreenshot.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = "Pods-Cable-CableUITestsScreenshot.modulemap"; sourceTree = ""; }; + 02AE2A2A0467EA70DB5D9C6CB93B7CBE /* cost_enc.c */ = {isa = PBXFileReference; includeInIndex = 1; name = cost_enc.c; path = vendor/libwebp/cost_enc.c; sourceTree = ""; }; + 02F6988AFBC0B2AF511F86755F935B13 /* alpha_enc.c */ = {isa = PBXFileReference; includeInIndex = 1; name = alpha_enc.c; path = vendor/libwebp/alpha_enc.c; sourceTree = ""; }; + 03F6F40CA78AF34A91BC6DEC8595DF55 /* PostHogLogEntry.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostHogLogEntry.swift; path = "PostHog/Replay/Plugins/Console Logs/PostHogLogEntry.swift"; sourceTree = ""; }; + 04F316A4B2CB95903F1476FD8F6E5506 /* token_enc.c */ = {isa = PBXFileReference; includeInIndex = 1; name = token_enc.c; path = vendor/libwebp/token_enc.c; sourceTree = ""; }; + 05688B486C05767CC332F7BF18F63FA6 /* PostHogRemoteConfig.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostHogRemoteConfig.swift; path = PostHog/PostHogRemoteConfig.swift; sourceTree = ""; }; + 06902488BC3ACDB66C4CC8E3DA9B2BF1 /* ssim.c */ = {isa = PBXFileReference; includeInIndex = 1; name = ssim.c; path = vendor/libwebp/ssim.c; sourceTree = ""; }; + 07439B205D54D4542249407BDC340D8B /* Date+Util.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Date+Util.swift"; path = "PostHog/Replay/Date+Util.swift"; sourceTree = ""; }; + 07ACC03496AD0255B5AF70CE5B4D3739 /* EmojiRating.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = EmojiRating.swift; path = PostHog/Surveys/Utils/EmojiRating.swift; sourceTree = ""; }; + 0B5738CAC80AE5D72B75358EEE9D7559 /* NumberRating.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NumberRating.swift; path = PostHog/Surveys/Utils/NumberRating.swift; sourceTree = ""; }; + 0B79B981CBC521AEB9B5F19CEE6486D8 /* Pods-Cable-CableUITests-acknowledgements.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-Cable-CableUITests-acknowledgements.plist"; sourceTree = ""; }; + 0C5591E49085B223152BC20087DBC2F5 /* ph_quant_levels_utils.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = ph_quant_levels_utils.h; path = vendor/libwebp/ph_quant_levels_utils.h; sourceTree = ""; }; + 0CFF0D65E2D3133D28C9527F218B033B /* Data+Gzip.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Data+Gzip.swift"; path = "PostHog/Utils/Data+Gzip.swift"; sourceTree = ""; }; + 0E6E0620A6F6472F44291ECF29967CCB /* PostHogLegacyQueue.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostHogLegacyQueue.swift; path = PostHog/PostHogLegacyQueue.swift; sourceTree = ""; }; + 103153A1783959BDADE5A086BAEE932E /* PostHogSessionReplayConfig.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostHogSessionReplayConfig.swift; path = PostHog/Replay/PostHogSessionReplayConfig.swift; sourceTree = ""; }; + 10FE6C33488F83B12D9A37C71584E723 /* ph_color_cache_utils.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = ph_color_cache_utils.h; path = vendor/libwebp/ph_color_cache_utils.h; sourceTree = ""; }; + 114279FBFB8CEFB0597BCACAFD61F21C /* ph_bit_reader_utils.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = ph_bit_reader_utils.h; path = vendor/libwebp/ph_bit_reader_utils.h; sourceTree = ""; }; + 13289E1D4EAA79792BF87E290976C758 /* FileUtils.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FileUtils.swift; path = PostHog/Utils/FileUtils.swift; sourceTree = ""; }; + 13CD7EE11BBD992CE37375834390CA29 /* filters_sse2.c */ = {isa = PBXFileReference; includeInIndex = 1; name = filters_sse2.c; path = vendor/libwebp/filters_sse2.c; sourceTree = ""; }; + 143960F19300CE4777F0CC55F8DD8E31 /* bit_reader_utils.c */ = {isa = PBXFileReference; includeInIndex = 1; name = bit_reader_utils.c; path = vendor/libwebp/bit_reader_utils.c; sourceTree = ""; }; + 15861BEA1F90094B56DC2CD840469B3D /* AssociatedKeys.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AssociatedKeys.swift; path = PostHog/Utils/AssociatedKeys.swift; sourceTree = ""; }; + 15D68B2FA6D4A041D8698FC67954D8CF /* PostHogSurveysConfig.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostHogSurveysConfig.swift; path = PostHog/Surveys/PostHogSurveysConfig.swift; sourceTree = ""; }; + 1615B66B1E3F08A9CCE5056265CA80A8 /* PostHog.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = PostHog.modulemap; sourceTree = ""; }; + 16D25A3C7A45F1954C4E6B068C58B7EA /* huffman_utils.c */ = {isa = PBXFileReference; includeInIndex = 1; name = huffman_utils.c; path = vendor/libwebp/huffman_utils.c; sourceTree = ""; }; + 172A611FED667B809856910CFB1F8DDA /* muxedit.c */ = {isa = PBXFileReference; includeInIndex = 1; name = muxedit.c; path = vendor/libwebp/muxedit.c; sourceTree = ""; }; + 1900B13AA3B133ACDED32587C775ABA2 /* vp8l_enc.c */ = {isa = PBXFileReference; includeInIndex = 1; name = vp8l_enc.c; path = vendor/libwebp/vp8l_enc.c; sourceTree = ""; }; + 1A884B1BB87102903507CF0084AE903C /* UIViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = UIViewController.swift; path = PostHog/UIViewController.swift; sourceTree = ""; }; + 1B3EF635F2042C38745AADD3B049F9D1 /* dec_sse2.c */ = {isa = PBXFileReference; includeInIndex = 1; name = dec_sse2.c; path = vendor/libwebp/dec_sse2.c; sourceTree = ""; }; + 1B69967DBE2F79C655769D9E766052B9 /* PostHogSurveyIntegration.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostHogSurveyIntegration.swift; path = PostHog/Surveys/PostHogSurveyIntegration.swift; sourceTree = ""; }; + 1B7E943BABB7F3EAC5DE3E570F21D845 /* PostHogStorage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostHogStorage.swift; path = PostHog/PostHogStorage.swift; sourceTree = ""; }; + 1BC3CBC81194A98EBD7798839CE8D8C4 /* ForwardingPickerViewDelegate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ForwardingPickerViewDelegate.swift; path = PostHog/Autocapture/ForwardingPickerViewDelegate.swift; sourceTree = ""; }; + 1CACEF186F8DF80FAFD17F7D33BC5D71 /* URLSessionSwizzler.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = URLSessionSwizzler.swift; path = PostHog/Replay/Plugins/Network/URLSessionSwizzler.swift; sourceTree = ""; }; + 1DA4E5C73A12E0A6898B70F3A9CED1A6 /* ph_histogram_enc.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = ph_histogram_enc.h; path = vendor/libwebp/ph_histogram_enc.h; sourceTree = ""; }; + 1DB59137CCCF0AB605EA3C16F30B47C7 /* UITextInputTraits+Util.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "UITextInputTraits+Util.swift"; path = "PostHog/Replay/UITextInputTraits+Util.swift"; sourceTree = ""; }; + 1E3A1E4CB22BAA257379DDFFB6654CC3 /* ph_sharpyuv_csp.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = ph_sharpyuv_csp.h; path = vendor/libwebp/ph_sharpyuv_csp.h; sourceTree = ""; }; + 1F788308CE59B5B07BDA5820491EC32B /* ApplicationLifecyclePublisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ApplicationLifecyclePublisher.swift; path = "PostHog/App Life Cycle/ApplicationLifecyclePublisher.swift"; sourceTree = ""; }; + 1F97F6C297E221C6D17A28FD64FD56C9 /* ReadWriteLock.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ReadWriteLock.swift; path = PostHog/Utils/ReadWriteLock.swift; sourceTree = ""; }; + 1FDB8663680B7CBA9F5E40A296878638 /* PostHogConsumerPayload.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostHogConsumerPayload.swift; path = PostHog/PostHogConsumerPayload.swift; sourceTree = ""; }; + 22394C97F8C3BED5DFED7C780C1CE564 /* filters.c */ = {isa = PBXFileReference; includeInIndex = 1; name = filters.c; path = vendor/libwebp/filters.c; sourceTree = ""; }; + 229D517849A145DB966B04AE150168FB /* ph_vp8li_dec.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = ph_vp8li_dec.h; path = vendor/libwebp/ph_vp8li_dec.h; sourceTree = ""; }; + 235F09390CCC154EF3CE1AF84501B9D5 /* yuv_neon.c */ = {isa = PBXFileReference; includeInIndex = 1; name = yuv_neon.c; path = vendor/libwebp/yuv_neon.c; sourceTree = ""; }; + 24177CAF8EE40225DC590E24E98B5907 /* utils.c */ = {isa = PBXFileReference; includeInIndex = 1; name = utils.c; path = vendor/libwebp/utils.c; sourceTree = ""; }; + 262A25E1DBCEB2502F06974ED0FE5FBD /* SurveyButton.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SurveyButton.swift; path = PostHog/Surveys/Utils/SurveyButton.swift; sourceTree = ""; }; + 262DE51E90B8F78B18D4D167AAC6C4B4 /* filters_utils.c */ = {isa = PBXFileReference; includeInIndex = 1; name = filters_utils.c; path = vendor/libwebp/filters_utils.c; sourceTree = ""; }; + 267A77D966A9CB080522F80053CB2597 /* PostHogTagViewModifier.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostHogTagViewModifier.swift; path = PostHog/SwiftUI/PostHogTagViewModifier.swift; sourceTree = ""; }; + 268E4C94970868F0267B55DD268C4FDC /* ph_vp8li_enc.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = ph_vp8li_enc.h; path = vendor/libwebp/ph_vp8li_enc.h; sourceTree = ""; }; + 26FE4D61E0E5E088E0A627B0FDFC294E /* Survey+Util.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Survey+Util.swift"; path = "PostHog/Surveys/Utils/Survey+Util.swift"; sourceTree = ""; }; + 2735D5533C9516CBB656FC0E44B60D7B /* Pods-Cable-frameworks.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "Pods-Cable-frameworks.sh"; sourceTree = ""; }; + 27378C145AF70BB39C853D63F329CD6D /* ph_vp8i_enc.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = ph_vp8i_enc.h; path = vendor/libwebp/ph_vp8i_enc.h; sourceTree = ""; }; + 276E633F3287EC40CFDCCA704F85A7BB /* random_utils.c */ = {isa = PBXFileReference; includeInIndex = 1; name = random_utils.c; path = vendor/libwebp/random_utils.c; sourceTree = ""; }; + 28E5B198F689C1C063C1DE0BF9B7ABAF /* SwiftUI+Util.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "SwiftUI+Util.swift"; path = "PostHog/Surveys/Utils/SwiftUI+Util.swift"; sourceTree = ""; }; + 2AB66D01B3E08701E5AD477ECC306070 /* PostHogSurveyAppearance.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostHogSurveyAppearance.swift; path = PostHog/Models/Surveys/PostHogSurveyAppearance.swift; sourceTree = ""; }; + 2D78E84DEDA315BA0155AF3658637CFC /* huffman_encode_utils.c */ = {isa = PBXFileReference; includeInIndex = 1; name = huffman_encode_utils.c; path = vendor/libwebp/huffman_encode_utils.c; sourceTree = ""; }; + 2DE0CB3567095D87C771243576EF9353 /* rescaler.c */ = {isa = PBXFileReference; includeInIndex = 1; name = rescaler.c; path = vendor/libwebp/rescaler.c; sourceTree = ""; }; + 30238EC8C0FAB5B9682951C8C40A56BC /* yuv_sse41.c */ = {isa = PBXFileReference; includeInIndex = 1; name = yuv_sse41.c; path = vendor/libwebp/yuv_sse41.c; sourceTree = ""; }; + 30358A66B1E325E7BF8FACB77136DCBD /* Resources.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Resources.swift; path = PostHog/Surveys/Utils/Resources.swift; sourceTree = ""; }; + 34EBB44BB3ED427636A0A0A673F61914 /* Errors.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Errors.swift; path = PostHog/Utils/Errors.swift; sourceTree = ""; }; + 36A60AA62169A7D58D9D78E917A84BD6 /* PostHog */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = PostHog; path = PostHog.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 36EBA9BB4949682419A03F0FE285AFDD /* PostHogReplayIntegration.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostHogReplayIntegration.swift; path = PostHog/Replay/PostHogReplayIntegration.swift; sourceTree = ""; }; + 384DDA2CB25005BD6479B5987C619DD4 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; }; + 390E5F5970C7EFFE8A499DFEFE839B86 /* syntax_enc.c */ = {isa = PBXFileReference; includeInIndex = 1; name = syntax_enc.c; path = vendor/libwebp/syntax_enc.c; sourceTree = ""; }; + 394497CE62FB8F5E127D55A66D718803 /* UIView+PostHogLabel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "UIView+PostHogLabel.swift"; path = "PostHog/Autocapture/UIView+PostHogLabel.swift"; sourceTree = ""; }; + 39451BDDC73A8B6E24FFE97D8F418255 /* UUIDUtils.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = UUIDUtils.swift; path = PostHog/Utils/UUIDUtils.swift; sourceTree = ""; }; + 399AFF4367999AB195B1A4861DE2E3E1 /* enc_sse41.c */ = {isa = PBXFileReference; includeInIndex = 1; name = enc_sse41.c; path = vendor/libwebp/enc_sse41.c; sourceTree = ""; }; + 39C96D15D41A39D9CA442FE17E0C0C24 /* Pods-Cable-acknowledgements.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-Cable-acknowledgements.plist"; sourceTree = ""; }; + 3AE65FACEDE1F85D4627DDF23A372ED1 /* Pods-CableTests-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-CableTests-Info.plist"; sourceTree = ""; }; + 3B075A62E32AE782E63CB647C2425908 /* Pods-Cable-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Pods-Cable-umbrella.h"; sourceTree = ""; }; + 3B8A9E04574F84317E10956DD24EE006 /* ph_common_sse41.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = ph_common_sse41.h; path = vendor/libwebp/ph_common_sse41.h; sourceTree = ""; }; + 3BBA09ECA091215738C1BE2FFE5F091C /* cost_neon.c */ = {isa = PBXFileReference; includeInIndex = 1; name = cost_neon.c; path = vendor/libwebp/cost_neon.c; sourceTree = ""; }; + 3C816478A7A89278F0B9617CD33B4ABB /* QuestionTypes.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = QuestionTypes.swift; path = PostHog/Surveys/QuestionTypes.swift; sourceTree = ""; }; + 3EA95D0670D5D47F7BC69002970C0E15 /* rescaler_sse2.c */ = {isa = PBXFileReference; includeInIndex = 1; name = rescaler_sse2.c; path = vendor/libwebp/rescaler_sse2.c; sourceTree = ""; }; + 400531B7575C215F76B0965A3F16DDA0 /* ApplicationEventPublisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ApplicationEventPublisher.swift; path = PostHog/Replay/ApplicationEventPublisher.swift; sourceTree = ""; }; + 4282C330B006663023D6DA60644E9C71 /* iterator_enc.c */ = {isa = PBXFileReference; includeInIndex = 1; name = iterator_enc.c; path = vendor/libwebp/iterator_enc.c; sourceTree = ""; }; + 42AC22285670786E35A95F104334D436 /* muxread.c */ = {isa = PBXFileReference; includeInIndex = 1; name = muxread.c; path = vendor/libwebp/muxread.c; sourceTree = ""; }; + 42D87DCAF52175DC2D5C6B44419636A8 /* PostHog-PostHog */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; name = "PostHog-PostHog"; path = PostHog.bundle; sourceTree = BUILT_PRODUCTS_DIR; }; + 439F6F8B57A9D0DC1E4C4669A0B6BD70 /* URLSessionInterceptor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = URLSessionInterceptor.swift; path = PostHog/Replay/Plugins/Network/URLSessionInterceptor.swift; sourceTree = ""; }; + 44A2B199B29D72CB41D7DCC66EE77960 /* ph_webpi_dec.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = ph_webpi_dec.h; path = vendor/libwebp/ph_webpi_dec.h; sourceTree = ""; }; + 44B2D98D590BDDF72420B7AC9EE530EF /* ph_lossless_common.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = ph_lossless_common.h; path = vendor/libwebp/ph_lossless_common.h; sourceTree = ""; }; + 450735A51B25E2BC879F80FE3EF683E2 /* ph_filters_utils.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = ph_filters_utils.h; path = vendor/libwebp/ph_filters_utils.h; sourceTree = ""; }; + 46FD7041B91F02D8C44A8B23724610E8 /* webp_enc.c */ = {isa = PBXFileReference; includeInIndex = 1; name = webp_enc.c; path = vendor/libwebp/webp_enc.c; sourceTree = ""; }; + 497BF2253E737EA49DACF8B6460E188A /* UIImage+Util.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "UIImage+Util.swift"; path = "PostHog/Replay/UIImage+Util.swift"; sourceTree = ""; }; + 49AA24A61F055E7B69C6A5D8C6B2AD78 /* PostHogScreenViewIntegration.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostHogScreenViewIntegration.swift; path = "PostHog/Screen Views/PostHogScreenViewIntegration.swift"; sourceTree = ""; }; + 4AF87321A55BD1044FFD6A604445B369 /* SurveysRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SurveysRootView.swift; path = PostHog/Surveys/SurveysRootView.swift; sourceTree = ""; }; + 4B180FB52B54B3D7CE6A1EB92AF8CD1F /* PostHogSessionReplayPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostHogSessionReplayPlugin.swift; path = PostHog/Replay/Plugins/PostHogSessionReplayPlugin.swift; sourceTree = ""; }; + 4B69F8B2559E5EE459C85C21EFEE836B /* palette.c */ = {isa = PBXFileReference; includeInIndex = 1; name = palette.c; path = vendor/libwebp/palette.c; sourceTree = ""; }; + 4BFDFC30C1DB0FBE9E1AAB4CA5D5E990 /* ph_dsp.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = ph_dsp.h; path = vendor/libwebp/ph_dsp.h; sourceTree = ""; }; + 5131F1FFA6BB84612F7867F86C51FCBE /* UIImage+WebP.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "UIImage+WebP.swift"; path = "PostHog/Utils/UIImage+WebP.swift"; sourceTree = ""; }; + 516914075585022AAE4D7BBA974FC678 /* NetworkSample.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NetworkSample.swift; path = PostHog/Replay/NetworkSample.swift; sourceTree = ""; }; + 51F57C7D4BAC17BBF066E3F6D4C4B59A /* Pods-Cable-CableUITests-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Pods-Cable-CableUITests-umbrella.h"; sourceTree = ""; }; + 52ABD4159B83070BF140003F701169DE /* ph_thread_utils.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = ph_thread_utils.h; path = vendor/libwebp/ph_thread_utils.h; sourceTree = ""; }; + 52C45951D83FB2A2320DCC090D6EE882 /* PostHogSessionReplayNetworkPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostHogSessionReplayNetworkPlugin.swift; path = PostHog/Replay/Plugins/Network/PostHogSessionReplayNetworkPlugin.swift; sourceTree = ""; }; + 53A2AF1A2EBA24407AC0E0E425DED2AB /* lossless_enc_sse2.c */ = {isa = PBXFileReference; includeInIndex = 1; name = lossless_enc_sse2.c; path = vendor/libwebp/lossless_enc_sse2.c; sourceTree = ""; }; + 54A7FFEE9A3DE4A9543516D400145E56 /* Pods-CableTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-CableTests.release.xcconfig"; sourceTree = ""; }; + 54D1B502E8A8D8A7CF0F160485F27D94 /* UIWindow+.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "UIWindow+.swift"; path = "PostHog/Utils/UIWindow+.swift"; sourceTree = ""; }; + 5633C72981EE1D3DFED81A8579AA5217 /* Pods-CableTests-acknowledgements.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-CableTests-acknowledgements.plist"; sourceTree = ""; }; + 56F211460A3349D6478176319237385E /* PostHogDisplaySurveyAppearance.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostHogDisplaySurveyAppearance.swift; path = PostHog/Surveys/Models/PostHogDisplaySurveyAppearance.swift; sourceTree = ""; }; + 5757A0552803FBE7A020E16BD23EF91B /* Pods-Cable-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-Cable-Info.plist"; sourceTree = ""; }; + 57AED642173AA14A5AB8433E3D4B85ED /* PostHogSurveysDefaultDelegate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostHogSurveysDefaultDelegate.swift; path = PostHog/Surveys/PostHogSurveysDefaultDelegate.swift; sourceTree = ""; }; + 58006037934279EEB6F765806CE31D38 /* PostHogIntegration.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostHogIntegration.swift; path = PostHog/PostHogIntegration.swift; sourceTree = ""; }; + 58890CE1C13F1D218F91B8F1B662297C /* muxinternal.c */ = {isa = PBXFileReference; includeInIndex = 1; name = muxinternal.c; path = vendor/libwebp/muxinternal.c; sourceTree = ""; }; + 58B2390554BF82073BFFF56330375E16 /* quant_enc.c */ = {isa = PBXFileReference; includeInIndex = 1; name = quant_enc.c; path = vendor/libwebp/quant_enc.c; sourceTree = ""; }; + 58E3377D17B30CB16D0310778D68FF1B /* upsampling_sse2.c */ = {isa = PBXFileReference; includeInIndex = 1; name = upsampling_sse2.c; path = vendor/libwebp/upsampling_sse2.c; sourceTree = ""; }; + 5A40B8F5231A7944996ACCC1E340899D /* ph_encode.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = ph_encode.h; path = vendor/libwebp/ph_encode.h; sourceTree = ""; }; + 5A8552B95757A4F529060DCD92FA9EA8 /* PostHogApi.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostHogApi.swift; path = PostHog/PostHogApi.swift; sourceTree = ""; }; + 5A93DEA6097B2F04643E499901EC91A6 /* Pods-Cable-CableUITests-frameworks.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "Pods-Cable-CableUITests-frameworks.sh"; sourceTree = ""; }; + 5C042A1CBFE280735CE602F869E5E16E /* PostHogConfig.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostHogConfig.swift; path = PostHog/PostHogConfig.swift; sourceTree = ""; }; + 5C1E2A40BEFB3E27559CF75D3AF598D0 /* upsampling.c */ = {isa = PBXFileReference; includeInIndex = 1; name = upsampling.c; path = vendor/libwebp/upsampling.c; sourceTree = ""; }; + 5DF75AE42F75E0133CA974B0A992AF64 /* Pods-Cable-CableUITests */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = "Pods-Cable-CableUITests"; path = Pods_Cable_CableUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 6293FFF4CAA83AC8938A5AF805A16B4E /* alpha_processing_neon.c */ = {isa = PBXFileReference; includeInIndex = 1; name = alpha_processing_neon.c; path = vendor/libwebp/alpha_processing_neon.c; sourceTree = ""; }; + 6304E8E5FDED5863B9DCE550E79F2F13 /* SurveySheet.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SurveySheet.swift; path = PostHog/Surveys/SurveySheet.swift; sourceTree = ""; }; + 635FCA4635E4DDDA1A3FC3953FC4C9E9 /* SurveyDisplayController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SurveyDisplayController.swift; path = PostHog/Surveys/SurveyDisplayController.swift; sourceTree = ""; }; + 63BDD7A067C75995C9F44A5BD44EE5E7 /* ph_vp8i_dec.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = ph_vp8i_dec.h; path = vendor/libwebp/ph_vp8i_dec.h; sourceTree = ""; }; + 64AC4C5BA1754D85126569A476612795 /* Pods-Cable.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = "Pods-Cable.modulemap"; sourceTree = ""; }; + 66AFFADA1464DFC165AF775D6EDDE652 /* PostHogDisplaySurvey.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostHogDisplaySurvey.swift; path = PostHog/Surveys/Models/PostHogDisplaySurvey.swift; sourceTree = ""; }; + 676CFD3B35C9F8564C51E1B617A3937C /* alpha_processing_sse41.c */ = {isa = PBXFileReference; includeInIndex = 1; name = alpha_processing_sse41.c; path = vendor/libwebp/alpha_processing_sse41.c; sourceTree = ""; }; + 67914AAFEBAC9A2F1F127DE54D715E79 /* Pods-Cable-CableUITests.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = "Pods-Cable-CableUITests.modulemap"; sourceTree = ""; }; + 67DFC8AB8EF46C4E3F28229F5DCE1C8C /* PostHog.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = PostHog.h; path = PostHog/PostHog.h; sourceTree = ""; }; + 69AC865B00E170495D08F65246F26683 /* PostHogSwizzler.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostHogSwizzler.swift; path = PostHog/PostHogSwizzler.swift; sourceTree = ""; }; + 69AF4F959032087EDA8FC0D0A5B6209D /* lossless_enc_neon.c */ = {isa = PBXFileReference; includeInIndex = 1; name = lossless_enc_neon.c; path = vendor/libwebp/lossless_enc_neon.c; sourceTree = ""; }; + 6ABF4049707FD7850C0863E5B99FC269 /* ph_lossless.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = ph_lossless.h; path = vendor/libwebp/ph_lossless.h; sourceTree = ""; }; + 6C3B8263979EC31683158AC2B8D195C7 /* Hedgelog.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Hedgelog.swift; path = PostHog/Utils/Hedgelog.swift; sourceTree = ""; }; + 6DB3C2029FEE43C9492E48815A749CD8 /* DI.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DI.swift; path = PostHog/DI.swift; sourceTree = ""; }; + 6E80C814BD248103DB298C992AB07604 /* Reachability.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Reachability.swift; path = PostHog/Utils/Reachability.swift; sourceTree = ""; }; + 6EC9E8EDC00E76181958B8A45361E2E3 /* RRStyle.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RRStyle.swift; path = PostHog/Replay/RRStyle.swift; sourceTree = ""; }; + 6F05DC32B52DAD2AA8964AB408980A1F /* ph_yuv.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = ph_yuv.h; path = vendor/libwebp/ph_yuv.h; sourceTree = ""; }; + 6F5F4E6D26C2B48557ACEBE8D608BC49 /* ResourceBundle-PostHog-PostHog-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "ResourceBundle-PostHog-PostHog-Info.plist"; sourceTree = ""; }; + 701958AB5C27BF0CCC2736BED2525ADA /* PostHogSurveyEnums.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostHogSurveyEnums.swift; path = PostHog/Models/Surveys/PostHogSurveyEnums.swift; sourceTree = ""; }; + 701F2C64F76028BD63C8BC57E763B989 /* Pods-Cable-CableUITestsScreenshot-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Pods-Cable-CableUITestsScreenshot-dummy.m"; sourceTree = ""; }; + 716805E0DE609F449A51C263A653D182 /* backward_references_cost_enc.c */ = {isa = PBXFileReference; includeInIndex = 1; name = backward_references_cost_enc.c; path = vendor/libwebp/backward_references_cost_enc.c; sourceTree = ""; }; + 71C01AE5C1556B7FCEB70598A2359C57 /* ssim_sse2.c */ = {isa = PBXFileReference; includeInIndex = 1; name = ssim_sse2.c; path = vendor/libwebp/ssim_sse2.c; sourceTree = ""; }; + 7309703A36FD4345F1782408988D9A2A /* predictor_enc.c */ = {isa = PBXFileReference; includeInIndex = 1; name = predictor_enc.c; path = vendor/libwebp/predictor_enc.c; sourceTree = ""; }; + 738AC56CBEF0F5C46F68B5F6C393CE26 /* config_enc.c */ = {isa = PBXFileReference; includeInIndex = 1; name = config_enc.c; path = vendor/libwebp/config_enc.c; sourceTree = ""; }; + 73BBDBBF3DD175220822CBE95FE0B6DA /* PostHogSurveyResponse.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostHogSurveyResponse.swift; path = PostHog/Surveys/Models/PostHogSurveyResponse.swift; sourceTree = ""; }; + 74828AFF764C4FBA1E8273C81B8E961D /* TimeBasedEpochGenerator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TimeBasedEpochGenerator.swift; path = PostHog/Utils/TimeBasedEpochGenerator.swift; sourceTree = ""; }; + 74853741E05F6EF1BEB0A51503A2FAAF /* PostHog-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "PostHog-umbrella.h"; sourceTree = ""; }; + 752389685141ECE7C426B3DEB3D9F601 /* PostHogSurvey+Display.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "PostHogSurvey+Display.swift"; path = "PostHog/Models/Surveys/PostHogSurvey+Display.swift"; sourceTree = ""; }; + 770559699B6C4F893D1E7A02F2CD0026 /* PostHogAutocaptureEventTracker.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostHogAutocaptureEventTracker.swift; path = PostHog/Autocapture/PostHogAutocaptureEventTracker.swift; sourceTree = ""; }; + 773CC3FBB2958F1D7CE0C0170B523B93 /* PostHog-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "PostHog-Info.plist"; sourceTree = ""; }; + 77C6B393EA1E57769EC0389F1AD05B9C /* sharpyuv_neon.c */ = {isa = PBXFileReference; includeInIndex = 1; name = sharpyuv_neon.c; path = vendor/libwebp/sharpyuv_neon.c; sourceTree = ""; }; + 795E63F5459422366F9C7A22AEE66832 /* alpha_processing_sse2.c */ = {isa = PBXFileReference; includeInIndex = 1; name = alpha_processing_sse2.c; path = vendor/libwebp/alpha_processing_sse2.c; sourceTree = ""; }; + 79C664EEE1DC1B7E4C1AD3FFBCE62DCF /* sharpyuv_cpu.c */ = {isa = PBXFileReference; includeInIndex = 1; name = sharpyuv_cpu.c; path = vendor/libwebp/sharpyuv_cpu.c; sourceTree = ""; }; + 7F3CFCC8A376939FB12B819B27393F00 /* ph_quant.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = ph_quant.h; path = vendor/libwebp/ph_quant.h; sourceTree = ""; }; + 801F6B74D8E565882EB6463915EE29D9 /* dec_clip_tables.c */ = {isa = PBXFileReference; includeInIndex = 1; name = dec_clip_tables.c; path = vendor/libwebp/dec_clip_tables.c; sourceTree = ""; }; + 80A1861E13FC4397C27E11FB9264C784 /* ph_backward_references_enc.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = ph_backward_references_enc.h; path = vendor/libwebp/ph_backward_references_enc.h; sourceTree = ""; }; + 812FDF6DD4F545B16DF167DAEA77C36B /* DateUtils.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DateUtils.swift; path = PostHog/Utils/DateUtils.swift; sourceTree = ""; }; + 81A9CA6BA8912B85E00744D072E0AF98 /* ph_types.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = ph_types.h; path = vendor/libwebp/ph_types.h; sourceTree = ""; }; + 8460AF919A0E871BF4A73C742BE5F688 /* quant_levels_utils.c */ = {isa = PBXFileReference; includeInIndex = 1; name = quant_levels_utils.c; path = vendor/libwebp/quant_levels_utils.c; sourceTree = ""; }; + 84CAF2DEC195826ABDDD705FA32B3BAC /* CGSize+Util.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "CGSize+Util.swift"; path = "PostHog/Replay/CGSize+Util.swift"; sourceTree = ""; }; + 85583DB1C0D12DF398032FE155609A9E /* rescaler_utils.c */ = {isa = PBXFileReference; includeInIndex = 1; name = rescaler_utils.c; path = vendor/libwebp/rescaler_utils.c; sourceTree = ""; }; + 856534F765DD23D7992D3EC872FE93B7 /* picture_enc.c */ = {isa = PBXFileReference; includeInIndex = 1; name = picture_enc.c; path = vendor/libwebp/picture_enc.c; sourceTree = ""; }; + 85828150034429DD2DBC8A14669BDF37 /* Pods-CableTests-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Pods-CableTests-umbrella.h"; sourceTree = ""; }; + 85D33F85427449A3CF37371439A6C570 /* Pods-Cable-CableUITestsScreenshot-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-Cable-CableUITestsScreenshot-Info.plist"; sourceTree = ""; }; + 866330A74D6DEBA7354A80F57B4FB378 /* PostHogFileBackedQueue.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostHogFileBackedQueue.swift; path = PostHog/PostHogFileBackedQueue.swift; sourceTree = ""; }; + 86706A03E7D1636BA28748C6D1F0740A /* filters_neon.c */ = {isa = PBXFileReference; includeInIndex = 1; name = filters_neon.c; path = vendor/libwebp/filters_neon.c; sourceTree = ""; }; + 873422C94FE37539EBA965E98C7A3636 /* backward_references_enc.c */ = {isa = PBXFileReference; includeInIndex = 1; name = backward_references_enc.c; path = vendor/libwebp/backward_references_enc.c; sourceTree = ""; }; + 881B0B1AA8FD87B6D79DB655742B357B /* DictUtils.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DictUtils.swift; path = PostHog/Utils/DictUtils.swift; sourceTree = ""; }; + 8B72D15908356213E83617F069F28778 /* rescaler_neon.c */ = {isa = PBXFileReference; includeInIndex = 1; name = rescaler_neon.c; path = vendor/libwebp/rescaler_neon.c; sourceTree = ""; }; + 8C1E609130982209FE02FE99B78BDC88 /* filter_enc.c */ = {isa = PBXFileReference; includeInIndex = 1; name = filter_enc.c; path = vendor/libwebp/filter_enc.c; sourceTree = ""; }; + 8CECA788D22A8D210E1F126F087FA049 /* dec_sse41.c */ = {isa = PBXFileReference; includeInIndex = 1; name = dec_sse41.c; path = vendor/libwebp/dec_sse41.c; sourceTree = ""; }; + 8DAC933623085F073616F04099AF6703 /* PostHogBatchUploadInfo.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostHogBatchUploadInfo.swift; path = PostHog/PostHogBatchUploadInfo.swift; sourceTree = ""; }; + 8DCCB7DFFA938AEFD75591B645CDDDF0 /* MethodSwizzler.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MethodSwizzler.swift; path = PostHog/Replay/MethodSwizzler.swift; sourceTree = ""; }; + 8F310530386C455D52673B41DA4A2B76 /* lossless_sse41.c */ = {isa = PBXFileReference; includeInIndex = 1; name = lossless_sse41.c; path = vendor/libwebp/lossless_sse41.c; sourceTree = ""; }; + 8F5ADA47F04EABAB81527ABBF841D2AC /* Pods-CableTests */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = "Pods-CableTests"; path = Pods_CableTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 905362F80FAC60800B3E20073D217B0C /* PostHogNoMaskViewModifier.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostHogNoMaskViewModifier.swift; path = PostHog/SwiftUI/PostHogNoMaskViewModifier.swift; sourceTree = ""; }; + 905D11210787EACE6D48E66560631096 /* URLSessionExtension.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = URLSessionExtension.swift; path = PostHog/Replay/Plugins/Network/URLSessionExtension.swift; sourceTree = ""; }; + 9145EC49215D8AD4B67DC4C8A3FFCD24 /* View+PostHogLabel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "View+PostHogLabel.swift"; path = "PostHog/Autocapture/SwiftUI/View+PostHogLabel.swift"; sourceTree = ""; }; + 93E56AB9DB0FA2E618B09B988C467DD9 /* ph_cpu.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = ph_cpu.h; path = vendor/libwebp/ph_cpu.h; sourceTree = ""; }; + 94288A5DC18B54C6DC2DD8C571B0F0F6 /* picture_csp_enc.c */ = {isa = PBXFileReference; includeInIndex = 1; name = picture_csp_enc.c; path = vendor/libwebp/picture_csp_enc.c; sourceTree = ""; }; + 94407C342DD9E167C5D75E705899A981 /* PostHogContext.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostHogContext.swift; path = PostHog/PostHogContext.swift; sourceTree = ""; }; + 9567F7671610CAD55DFF871A5F19A42D /* Pods-Cable-CableUITestsScreenshot.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-Cable-CableUITestsScreenshot.debug.xcconfig"; sourceTree = ""; }; + 9833327F3E1576D80701526ECD9F245B /* Pods-CableTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-CableTests.debug.xcconfig"; sourceTree = ""; }; + 98C872681D85E83B48A177D688728D6E /* String+Util.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "String+Util.swift"; path = "PostHog/Replay/String+Util.swift"; sourceTree = ""; }; + 99F4A385A81C30B48330AC1106D13082 /* Pods-Cable */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = "Pods-Cable"; path = Pods_Cable.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 9AC357F49D750FDC096D0A38EF084E0F /* ConfirmationMessage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConfirmationMessage.swift; path = PostHog/Surveys/ConfirmationMessage.swift; sourceTree = ""; }; + 9AE8485816F0A8CEE5E8A00890CA324A /* SurveyPresentationDetentsRepresentable.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SurveyPresentationDetentsRepresentable.swift; path = PostHog/Surveys/Utils/SurveyPresentationDetentsRepresentable.swift; sourceTree = ""; }; + 9B2AAEABCF4BAAC96E0099538E6FAEA5 /* PostHog-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "PostHog-dummy.m"; sourceTree = ""; }; + 9C442AD9B5648643BF9F8C662F4B32A7 /* Pods-Cable-CableUITests-acknowledgements.markdown */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = "Pods-Cable-CableUITests-acknowledgements.markdown"; sourceTree = ""; }; + 9D940727FF8FB9C785EB98E56350EF41 /* Podfile */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; indentWidth = 2; lastKnownFileType = text; name = Podfile; path = ../Podfile; sourceTree = SOURCE_ROOT; tabWidth = 2; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; + 9DC16A20D6C39A6FE6C700DE88890FC6 /* dec.c */ = {isa = PBXFileReference; includeInIndex = 1; name = dec.c; path = vendor/libwebp/dec.c; sourceTree = ""; }; + 9E7A2B7242409A2E9C0F0B5B9DBCC3A8 /* lossless_sse2.c */ = {isa = PBXFileReference; includeInIndex = 1; name = lossless_sse2.c; path = vendor/libwebp/lossless_sse2.c; sourceTree = ""; }; + 9F7478CDF58D2E47C91E9AEA4F3BFA36 /* ph_endian_inl_utils.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = ph_endian_inl_utils.h; path = vendor/libwebp/ph_endian_inl_utils.h; sourceTree = ""; }; + 9FBABCFADE0E13362BC515109844B0B7 /* bit_writer_utils.c */ = {isa = PBXFileReference; includeInIndex = 1; name = bit_writer_utils.c; path = vendor/libwebp/bit_writer_utils.c; sourceTree = ""; }; + 9FF0D333280C1AA92C268AF36D289B8B /* CGColor+Util.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "CGColor+Util.swift"; path = "PostHog/Replay/CGColor+Util.swift"; sourceTree = ""; }; + A2428DC0690BFC906FB6BBC9517C91B8 /* Pods-Cable-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Pods-Cable-dummy.m"; sourceTree = ""; }; + A296C0A01AD7CFE6D2C8A9367A6C2A67 /* lossless_neon.c */ = {isa = PBXFileReference; includeInIndex = 1; name = lossless_neon.c; path = vendor/libwebp/lossless_neon.c; sourceTree = ""; }; + A3B5BAECFF77239E331B522C7107C1A0 /* ApplicationScreenViewPublisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ApplicationScreenViewPublisher.swift; path = "PostHog/Screen Views/ApplicationScreenViewPublisher.swift"; sourceTree = ""; }; + A40B6975FA38F82CFAD982D745DC1177 /* PostHogVersion.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostHogVersion.swift; path = PostHog/PostHogVersion.swift; sourceTree = ""; }; + A4B8C487F55A8DC54688605DFF2114A6 /* ph_neon.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = ph_neon.h; path = vendor/libwebp/ph_neon.h; sourceTree = ""; }; + A50567BD13E8812DF3257A08243491EA /* BottomSection.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BottomSection.swift; path = PostHog/Surveys/BottomSection.swift; sourceTree = ""; }; + A64CAB6E8B5412C7633F7B8565B8651D /* ph_random_utils.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = ph_random_utils.h; path = vendor/libwebp/ph_random_utils.h; sourceTree = ""; }; + A7467F12C852FA55CE696623D0C49893 /* cpu.c */ = {isa = PBXFileReference; includeInIndex = 1; name = cpu.c; path = vendor/libwebp/cpu.c; sourceTree = ""; }; + A84BF685A521744850F13C55520AAB70 /* QuestionHeader.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = QuestionHeader.swift; path = PostHog/Surveys/QuestionHeader.swift; sourceTree = ""; }; + A9410C5F8607F2FC658BCBEDAD5EEFBE /* histogram_enc.c */ = {isa = PBXFileReference; includeInIndex = 1; name = histogram_enc.c; path = vendor/libwebp/histogram_enc.c; sourceTree = ""; }; + A97D7894341E91B3BF09503921D83C16 /* ph_palette.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = ph_palette.h; path = vendor/libwebp/ph_palette.h; sourceTree = ""; }; + AC3ADEA740D3607618548A1451E8EE35 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = PostHog/Resources/PrivacyInfo.xcprivacy; sourceTree = ""; }; + AC8C2B6DA50936FEAAA8D7CF5AD97F60 /* ph_vp8_dec.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = ph_vp8_dec.h; path = vendor/libwebp/ph_vp8_dec.h; sourceTree = ""; }; + AFC508491302293623F9C9FE123DF31D /* ph_utils.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = ph_utils.h; path = vendor/libwebp/ph_utils.h; sourceTree = ""; }; + B1784FF53C3EB6951476A12994ECAE5A /* Pods-CableTests-acknowledgements.markdown */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = "Pods-CableTests-acknowledgements.markdown"; sourceTree = ""; }; + B1C3DCD4E3C2C0AE65F78D9A92443B29 /* Pods-Cable-CableUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-Cable-CableUITests.release.xcconfig"; sourceTree = ""; }; + B233AD5FB3C464116639B4C4A446D5A9 /* yuv_sse2.c */ = {isa = PBXFileReference; includeInIndex = 1; name = yuv_sse2.c; path = vendor/libwebp/yuv_sse2.c; sourceTree = ""; }; + B24A16C60C8C5673028974678594E6E8 /* cost.c */ = {isa = PBXFileReference; includeInIndex = 1; name = cost.c; path = vendor/libwebp/cost.c; sourceTree = ""; }; + B2CD1BA30ECCBAFA941F6D593A91CE5A /* PostHogDisplaySurveyQuestion.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostHogDisplaySurveyQuestion.swift; path = PostHog/Surveys/Models/PostHogDisplaySurveyQuestion.swift; sourceTree = ""; }; + B2E0C12DA5F3B1E2AD47FD377B6B55AD /* Pods-Cable-CableUITestsScreenshot-frameworks.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "Pods-Cable-CableUITestsScreenshot-frameworks.sh"; sourceTree = ""; }; + B2FFDD72D120FD8743D882769EF18E9B /* sharpyuv_csp.c */ = {isa = PBXFileReference; includeInIndex = 1; name = sharpyuv_csp.c; path = vendor/libwebp/sharpyuv_csp.c; sourceTree = ""; }; + B3892D9BAABC94C6D1D38A736617D726 /* near_lossless_enc.c */ = {isa = PBXFileReference; includeInIndex = 1; name = near_lossless_enc.c; path = vendor/libwebp/near_lossless_enc.c; sourceTree = ""; }; + B45CA8B0658CABEFE16DEF11945D4C96 /* PostHogSurveyConditions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostHogSurveyConditions.swift; path = PostHog/Models/Surveys/PostHogSurveyConditions.swift; sourceTree = ""; }; + B4AE78BA77D7992329434D3A2D3370F6 /* tree_enc.c */ = {isa = PBXFileReference; includeInIndex = 1; name = tree_enc.c; path = vendor/libwebp/tree_enc.c; sourceTree = ""; }; + B73ECF10580052B7EADF1C8126C1F5D4 /* enc.c */ = {isa = PBXFileReference; includeInIndex = 1; name = enc.c; path = vendor/libwebp/enc.c; sourceTree = ""; }; + BA4A2E54DC65E09C0702AC19D93C51D0 /* PostHogSessionReplayConsoleLogConfig.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostHogSessionReplayConsoleLogConfig.swift; path = PostHog/Replay/PostHogSessionReplayConsoleLogConfig.swift; sourceTree = ""; }; + BADD4B93BF926C2B8CA6E8BAC40F9EED /* EdgeBorder.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = EdgeBorder.swift; path = PostHog/Surveys/Utils/EdgeBorder.swift; sourceTree = ""; }; + BBAA59A93C40F18F756F62E7761DDDA2 /* PostHogAppLifeCycleIntegration.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostHogAppLifeCycleIntegration.swift; path = "PostHog/App Life Cycle/PostHogAppLifeCycleIntegration.swift"; sourceTree = ""; }; + BC5EDD8394DA064EC7C83ACD7ECCD71E /* Optional+Util.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Optional+Util.swift"; path = "PostHog/Replay/Optional+Util.swift"; sourceTree = ""; }; + BCC15FDAC7750FBA4BA88CC539B5231E /* ph_sharpyuv_cpu.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = ph_sharpyuv_cpu.h; path = vendor/libwebp/ph_sharpyuv_cpu.h; sourceTree = ""; }; + BCE3D06D9F5BD48177BA8DD707F3B037 /* thread_utils.c */ = {isa = PBXFileReference; includeInIndex = 1; name = thread_utils.c; path = vendor/libwebp/thread_utils.c; sourceTree = ""; }; + BE54094ABB405AD044CBC194CBC3787A /* PostHogConsoleLogInterceptor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostHogConsoleLogInterceptor.swift; path = "PostHog/Replay/Plugins/Console Logs/PostHogConsoleLogInterceptor.swift"; sourceTree = ""; }; + BEA18A14AB5E18A4522909D739010F8B /* PostHogSessionReplayConsoleLogsPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostHogSessionReplayConsoleLogsPlugin.swift; path = "PostHog/Replay/Plugins/Console Logs/PostHogSessionReplayConsoleLogsPlugin.swift"; sourceTree = ""; }; + C07415FC3293C535290E997DFFF959FD /* UIApplication+.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "UIApplication+.swift"; path = "PostHog/Utils/UIApplication+.swift"; sourceTree = ""; }; + C0B671EE629623603942CB46DA763609 /* AutocaptureEventProcessing.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AutocaptureEventProcessing.swift; path = PostHog/Autocapture/AutocaptureEventProcessing.swift; sourceTree = ""; }; + C15956FAD29CD446AED83B28072F258A /* ph_bit_writer_utils.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = ph_bit_writer_utils.h; path = vendor/libwebp/ph_bit_writer_utils.h; sourceTree = ""; }; + C1C1F47ADEAE9E1F0B9810DF4E04BD56 /* ph_sharpyuv_gamma.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = ph_sharpyuv_gamma.h; path = vendor/libwebp/ph_sharpyuv_gamma.h; sourceTree = ""; }; + C3B1285FF54F13BB960970B7370E7941 /* PostHogAutocaptureIntegration.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostHogAutocaptureIntegration.swift; path = PostHog/Autocapture/PostHogAutocaptureIntegration.swift; sourceTree = ""; }; + C52A7E8E1B3DA2B543DA937C3BE7162B /* upsampling_neon.c */ = {isa = PBXFileReference; includeInIndex = 1; name = upsampling_neon.c; path = vendor/libwebp/upsampling_neon.c; sourceTree = ""; }; + C60B359AA89658785332F5EB04C61112 /* ApplicationViewLayoutPublisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ApplicationViewLayoutPublisher.swift; path = PostHog/ApplicationViewLayoutPublisher.swift; sourceTree = ""; }; + C720DA9EA026D5E7449597140583E063 /* PostHogLogLevel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostHogLogLevel.swift; path = "PostHog/Replay/Plugins/Console Logs/PostHogLogLevel.swift"; sourceTree = ""; }; + C77B0E69679F739AA5F6EF3472F81B3E /* Pods-Cable-CableUITests-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Pods-Cable-CableUITests-dummy.m"; sourceTree = ""; }; + C7C8924423D336E936B9B8D7528391B5 /* PostHogExtensions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostHogExtensions.swift; path = PostHog/PostHogExtensions.swift; sourceTree = ""; }; + C84990ADFAD35F17E5D87A70DE6B2768 /* PostHogSurvey.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostHogSurvey.swift; path = PostHog/Models/Surveys/PostHogSurvey.swift; sourceTree = ""; }; + C87BE5C48779C989CE62FA8282E8B3D5 /* upsampling_sse41.c */ = {isa = PBXFileReference; includeInIndex = 1; name = upsampling_sse41.c; path = vendor/libwebp/upsampling_sse41.c; sourceTree = ""; }; + C94C1436FB9990B1C8458A4890A74FDD /* Pods-Cable-CableUITestsScreenshot-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Pods-Cable-CableUITestsScreenshot-umbrella.h"; sourceTree = ""; }; + C95684E80F2639489418BE63DB2999CD /* ph_rescaler_utils.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = ph_rescaler_utils.h; path = vendor/libwebp/ph_rescaler_utils.h; sourceTree = ""; }; + C9A8771BEB422C0EBA1669CCBCC77482 /* SurveysWindow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SurveysWindow.swift; path = PostHog/Surveys/SurveysWindow.swift; sourceTree = ""; }; + CB852437A4FE639A36D1C941F94F307A /* SegmentedControl.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SegmentedControl.swift; path = PostHog/Surveys/Utils/SegmentedControl.swift; sourceTree = ""; }; + CBF4E22F61B73B539ABF63E8ADAAB87E /* yuv.c */ = {isa = PBXFileReference; includeInIndex = 1; name = yuv.c; path = vendor/libwebp/yuv.c; sourceTree = ""; }; + CD9B87A72D9E03E8531BE4E64C96BBAA /* sharpyuv_sse2.c */ = {isa = PBXFileReference; includeInIndex = 1; name = sharpyuv_sse2.c; path = vendor/libwebp/sharpyuv_sse2.c; sourceTree = ""; }; + CDACF4D658D473E1D0C0BC475C6B6D9F /* picture_tools_enc.c */ = {isa = PBXFileReference; includeInIndex = 1; name = picture_tools_enc.c; path = vendor/libwebp/picture_tools_enc.c; sourceTree = ""; }; + CF7956F6F3924F58644A57ECA19C3559 /* MultipleChoiceOptions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MultipleChoiceOptions.swift; path = PostHog/Surveys/Utils/MultipleChoiceOptions.swift; sourceTree = ""; }; + CFB8864DE15913957BF2887CC886727E /* PostHogStorageManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostHogStorageManager.swift; path = PostHog/PostHogStorageManager.swift; sourceTree = ""; }; + D07D529105A58FA284174316608207E1 /* Pods-CableTests-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Pods-CableTests-dummy.m"; sourceTree = ""; }; + D08666942BF9AA9C3313A5D3639E9A56 /* Pods-Cable-CableUITestsScreenshot-acknowledgements.markdown */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = "Pods-Cable-CableUITestsScreenshot-acknowledgements.markdown"; sourceTree = ""; }; + D22E0F8ACB0AADC33D8564D1D4E72C93 /* ph_common_dec.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = ph_common_dec.h; path = vendor/libwebp/ph_common_dec.h; sourceTree = ""; }; + D3261BD495FD61B20BC8BA98578865B7 /* PostHogNextSurveyQuestion.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostHogNextSurveyQuestion.swift; path = PostHog/Surveys/Models/PostHogNextSurveyQuestion.swift; sourceTree = ""; }; + D396A5F4E5EA6A631CFA1F5BA73BE979 /* ph_decode.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = ph_decode.h; path = vendor/libwebp/ph_decode.h; sourceTree = ""; }; + D5DD39201B51B24F484813D108EB3C2C /* Pods-Cable-CableUITestsScreenshot-acknowledgements.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-Cable-CableUITestsScreenshot-acknowledgements.plist"; sourceTree = ""; }; + D601464067F9E5B1E5D5620C8924912F /* quant_levels_dec_utils.c */ = {isa = PBXFileReference; includeInIndex = 1; name = quant_levels_dec_utils.c; path = vendor/libwebp/quant_levels_dec_utils.c; sourceTree = ""; }; + D8213698CA3EB61F4236A6C8E334099A /* PostHogEvent.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostHogEvent.swift; path = PostHog/Models/PostHogEvent.swift; sourceTree = ""; }; + D888AE3CE3943C819B306C6E7A8FF6BC /* PostHogSessionManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostHogSessionManager.swift; path = PostHog/PostHogSessionManager.swift; sourceTree = ""; }; + D88D282FD37C24951D615E881B2D8B5E /* PostHogMaskViewModifier.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostHogMaskViewModifier.swift; path = PostHog/SwiftUI/PostHogMaskViewModifier.swift; sourceTree = ""; }; + D8AAD180580FCC6EE9AEFFBE6AEDA671 /* Pods-Cable-CableUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-Cable-CableUITests.debug.xcconfig"; sourceTree = ""; }; + D8ACAC7F2DBEC2E6F6C08E461831738E /* PostHogPropertiesSanitizer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostHogPropertiesSanitizer.swift; path = PostHog/PostHogPropertiesSanitizer.swift; sourceTree = ""; }; + DA9D1BD5EFA6A8CA76078ADFBB131D2D /* PostHog.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = PostHog.debug.xcconfig; sourceTree = ""; }; + DB9CEA743E2F233A85546C94DD807354 /* sharpyuv_dsp.c */ = {isa = PBXFileReference; includeInIndex = 1; name = sharpyuv_dsp.c; path = vendor/libwebp/sharpyuv_dsp.c; sourceTree = ""; }; + DCDFFA010640D140217DEAF48821ABD1 /* PostHog.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = PostHog.release.xcconfig; sourceTree = ""; }; + DD8F109A9D1262A0B31CFFE54558EF8E /* Pods-Cable.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-Cable.debug.xcconfig"; sourceTree = ""; }; + DDC359F157B7B3B8BA8CEDD5389D7EC4 /* analysis_enc.c */ = {isa = PBXFileReference; includeInIndex = 1; name = analysis_enc.c; path = vendor/libwebp/analysis_enc.c; sourceTree = ""; }; + DE79FAF47ADBBDA1FD96FA4EB80CD2D4 /* ph_huffman_utils.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = ph_huffman_utils.h; path = vendor/libwebp/ph_huffman_utils.h; sourceTree = ""; }; + DE9FD3D6919DF785A204354C43DE839D /* Pods-Cable.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-Cable.release.xcconfig"; sourceTree = ""; }; + DF049858415B4235BCDBB079C3581BA4 /* color_cache_utils.c */ = {isa = PBXFileReference; includeInIndex = 1; name = color_cache_utils.c; path = vendor/libwebp/color_cache_utils.c; sourceTree = ""; }; + DF2A3A811E0015722FF7E34E9338B448 /* sharpyuv.c */ = {isa = PBXFileReference; includeInIndex = 1; name = sharpyuv.c; path = vendor/libwebp/sharpyuv.c; sourceTree = ""; }; + E0D796ED35657AF32721999BD872B40E /* lossless_enc.c */ = {isa = PBXFileReference; includeInIndex = 1; name = lossless_enc.c; path = vendor/libwebp/lossless_enc.c; sourceTree = ""; }; + E20DB8D1A1DFFDC41590109620887074 /* ph_muxi.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = ph_muxi.h; path = vendor/libwebp/ph_muxi.h; sourceTree = ""; }; + E3D701A5BB86EA2FBA69F57589340667 /* UIView+Util.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "UIView+Util.swift"; path = "PostHog/Replay/UIView+Util.swift"; sourceTree = ""; }; + E634ED9E4F291E94869F8F535E2FB59D /* enc_neon.c */ = {isa = PBXFileReference; includeInIndex = 1; name = enc_neon.c; path = vendor/libwebp/enc_neon.c; sourceTree = ""; }; + E71BC82ADF0D9EC4F9A390C9D17DAEB7 /* ph_sharpyuv.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = ph_sharpyuv.h; path = vendor/libwebp/ph_sharpyuv.h; sourceTree = ""; }; + E72753E6126AC46A7475C95A3F9DE5A7 /* Pods-CableTests.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = "Pods-CableTests.modulemap"; sourceTree = ""; }; + E7F5A16999097A6591248430D5EE1DD5 /* enc_sse2.c */ = {isa = PBXFileReference; includeInIndex = 1; name = enc_sse2.c; path = vendor/libwebp/enc_sse2.c; sourceTree = ""; }; + E851D31270CCB1CA24F49C52015B7F59 /* ph_mux_types.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = ph_mux_types.h; path = vendor/libwebp/ph_mux_types.h; sourceTree = ""; }; + EAA1A0EEEA8915D7ED932898580D55DA /* ViewTreeSnapshotStatus.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ViewTreeSnapshotStatus.swift; path = PostHog/Replay/ViewTreeSnapshotStatus.swift; sourceTree = ""; }; + EC7A62B6B9F7CF772B7553E3B238AAB3 /* UIColor+Util.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "UIColor+Util.swift"; path = "PostHog/Replay/UIColor+Util.swift"; sourceTree = ""; }; + ECD1B7D205F7FC63D45533FC2BAE3410 /* sharpyuv_gamma.c */ = {isa = PBXFileReference; includeInIndex = 1; name = sharpyuv_gamma.c; path = vendor/libwebp/sharpyuv_gamma.c; sourceTree = ""; }; + ED013EDE4144AF0F72EA8632E927919E /* frame_enc.c */ = {isa = PBXFileReference; includeInIndex = 1; name = frame_enc.c; path = vendor/libwebp/frame_enc.c; sourceTree = ""; }; + ED3CF8C4E1C8C5343DE62E4B8A2043AD /* PostHogQueue.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostHogQueue.swift; path = PostHog/PostHogQueue.swift; sourceTree = ""; }; + ED8BAA4BA5693210FDA9BE42AA76F93E /* PostHogPersonProfiles.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostHogPersonProfiles.swift; path = PostHog/PostHogPersonProfiles.swift; sourceTree = ""; }; + EDA36D795E2D2304CF2F39068E99EED8 /* RRWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RRWireframe.swift; path = PostHog/Replay/RRWireframe.swift; sourceTree = ""; }; + EDFECD65E342D7E6925201E956D8FBDA /* PostHog-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "PostHog-prefix.pch"; sourceTree = ""; }; + EEA4A4F055CC07589E72A35CB0E1EF28 /* ph_sharpyuv_dsp.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = ph_sharpyuv_dsp.h; path = vendor/libwebp/ph_sharpyuv_dsp.h; sourceTree = ""; }; + EF48F37324DDA42ABC08BC0E63AE23FC /* PostHogSDK.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostHogSDK.swift; path = PostHog/PostHogSDK.swift; sourceTree = ""; }; + EF645AFD51E32C0A7B929FE3710B3EC9 /* Pods-Cable-acknowledgements.markdown */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = "Pods-Cable-acknowledgements.markdown"; sourceTree = ""; }; + EFBDDE4003994362E3FE58381610B087 /* lossless_enc_sse41.c */ = {isa = PBXFileReference; includeInIndex = 1; name = lossless_enc_sse41.c; path = vendor/libwebp/lossless_enc_sse41.c; sourceTree = ""; }; + F091F375187698A52912BC097838826A /* Float+Util.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Float+Util.swift"; path = "PostHog/Replay/Float+Util.swift"; sourceTree = ""; }; + F115DBC1009A4AE8B1E2EEACECEE686D /* ph_huffman_encode_utils.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = ph_huffman_encode_utils.h; path = vendor/libwebp/ph_huffman_encode_utils.h; sourceTree = ""; }; + F3BBC9AA886853862CBA71BF1966D0F2 /* picture_rescale_enc.c */ = {isa = PBXFileReference; includeInIndex = 1; name = picture_rescale_enc.c; path = vendor/libwebp/picture_rescale_enc.c; sourceTree = ""; }; + F45BEC0B42FB9EFA2C1B6AADA6AAF9E4 /* alpha_processing.c */ = {isa = PBXFileReference; includeInIndex = 1; name = alpha_processing.c; path = vendor/libwebp/alpha_processing.c; sourceTree = ""; }; + F48FB46A5EECE30CA2951FE024EEE7C6 /* Pods-Cable-CableUITestsScreenshot */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = "Pods-Cable-CableUITestsScreenshot"; path = Pods_Cable_CableUITestsScreenshot.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + F4C64C6E9AA1FDB5C402603F7B39F3BC /* ph_common_sse2.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = ph_common_sse2.h; path = vendor/libwebp/ph_common_sse2.h; sourceTree = ""; }; + F553AE255E9F4CBC202ED457EF76082F /* lossless.c */ = {isa = PBXFileReference; includeInIndex = 1; name = lossless.c; path = vendor/libwebp/lossless.c; sourceTree = ""; }; + F6DC89C77E43B360E6ED162E84069CF6 /* Pods-Cable-CableUITestsScreenshot.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-Cable-CableUITestsScreenshot.release.xcconfig"; sourceTree = ""; }; + F85D4CA294CDFB70EE341652BCF427BC /* cost_sse2.c */ = {isa = PBXFileReference; includeInIndex = 1; name = cost_sse2.c; path = vendor/libwebp/cost_sse2.c; sourceTree = ""; }; + F9D50B5F78458141D8272AA365385479 /* PostHogSwiftUIViewModifiers.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostHogSwiftUIViewModifiers.swift; path = PostHog/SwiftUI/PostHogSwiftUIViewModifiers.swift; sourceTree = ""; }; + FA884DB8D0FD6DE10EC0E09ECBCB0FCC /* PostHogSurveyQuestion.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostHogSurveyQuestion.swift; path = PostHog/Models/Surveys/PostHogSurveyQuestion.swift; sourceTree = ""; }; + FB3C8E706E1BB31142E0F7F3BDAAD7A3 /* ph_format_constants.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = ph_format_constants.h; path = vendor/libwebp/ph_format_constants.h; sourceTree = ""; }; + FBA8E6C91C9871D781A894E9021E767F /* ph_cost_enc.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = ph_cost_enc.h; path = vendor/libwebp/ph_cost_enc.h; sourceTree = ""; }; + FC51F4877491548B2CE8612D8EBE8994 /* ph_mux.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = ph_mux.h; path = vendor/libwebp/ph_mux.h; sourceTree = ""; }; + FD4EE07A95F242B51F29985FC791B86C /* picture_psnr_enc.c */ = {isa = PBXFileReference; includeInIndex = 1; name = picture_psnr_enc.c; path = vendor/libwebp/picture_psnr_enc.c; sourceTree = ""; }; + FE3276BC9DD9343402F74F0411ACBAD3 /* Pods-Cable-CableUITests-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-Cable-CableUITests-Info.plist"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 3890B9438DD6516F1EA5E3CAACCE8FFE /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 6CCFABEA9067B84D78A97723B7424B2A /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 464D3A2144371ABCE15B36C2A6A868B9 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + EAEEFEB5F2F8EDC54B52B4FEE8E0021E /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6914A7F0DD12E15F1B99EAE8F381191A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 6A604351BC7DAE8F47EADA54FA67BA92 /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 73D22CAADE4FCD69C0A715C06D35F9B8 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + A26CDBEA01200B96B68F314398EEE38C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4006CE8FE027629C962E1BFB2A56F858 /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EA42357D9D8899D007F3BE4D5BAB8571 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + E0749957A6BB7594DC26469DB77B0C7B /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 0223C71E4F7A4926A35DE003CAC38985 /* PostHog */ = { + isa = PBXGroup; + children = ( + 02F6988AFBC0B2AF511F86755F935B13 /* alpha_enc.c */, + F45BEC0B42FB9EFA2C1B6AADA6AAF9E4 /* alpha_processing.c */, + 6293FFF4CAA83AC8938A5AF805A16B4E /* alpha_processing_neon.c */, + 795E63F5459422366F9C7A22AEE66832 /* alpha_processing_sse2.c */, + 676CFD3B35C9F8564C51E1B617A3937C /* alpha_processing_sse41.c */, + DDC359F157B7B3B8BA8CEDD5389D7EC4 /* analysis_enc.c */, + 400531B7575C215F76B0965A3F16DDA0 /* ApplicationEventPublisher.swift */, + 1F788308CE59B5B07BDA5820491EC32B /* ApplicationLifecyclePublisher.swift */, + A3B5BAECFF77239E331B522C7107C1A0 /* ApplicationScreenViewPublisher.swift */, + C60B359AA89658785332F5EB04C61112 /* ApplicationViewLayoutPublisher.swift */, + 15861BEA1F90094B56DC2CD840469B3D /* AssociatedKeys.swift */, + C0B671EE629623603942CB46DA763609 /* AutocaptureEventProcessing.swift */, + 716805E0DE609F449A51C263A653D182 /* backward_references_cost_enc.c */, + 873422C94FE37539EBA965E98C7A3636 /* backward_references_enc.c */, + 143960F19300CE4777F0CC55F8DD8E31 /* bit_reader_utils.c */, + 9FBABCFADE0E13362BC515109844B0B7 /* bit_writer_utils.c */, + A50567BD13E8812DF3257A08243491EA /* BottomSection.swift */, + 9FF0D333280C1AA92C268AF36D289B8B /* CGColor+Util.swift */, + 84CAF2DEC195826ABDDD705FA32B3BAC /* CGSize+Util.swift */, + DF049858415B4235BCDBB079C3581BA4 /* color_cache_utils.c */, + 738AC56CBEF0F5C46F68B5F6C393CE26 /* config_enc.c */, + 9AC357F49D750FDC096D0A38EF084E0F /* ConfirmationMessage.swift */, + B24A16C60C8C5673028974678594E6E8 /* cost.c */, + 02AE2A2A0467EA70DB5D9C6CB93B7CBE /* cost_enc.c */, + 3BBA09ECA091215738C1BE2FFE5F091C /* cost_neon.c */, + F85D4CA294CDFB70EE341652BCF427BC /* cost_sse2.c */, + A7467F12C852FA55CE696623D0C49893 /* cpu.c */, + 0CFF0D65E2D3133D28C9527F218B033B /* Data+Gzip.swift */, + 07439B205D54D4542249407BDC340D8B /* Date+Util.swift */, + 812FDF6DD4F545B16DF167DAEA77C36B /* DateUtils.swift */, + 9DC16A20D6C39A6FE6C700DE88890FC6 /* dec.c */, + 801F6B74D8E565882EB6463915EE29D9 /* dec_clip_tables.c */, + 0093492C1008E66439CF39ACE752BD1B /* dec_neon.c */, + 1B3EF635F2042C38745AADD3B049F9D1 /* dec_sse2.c */, + 8CECA788D22A8D210E1F126F087FA049 /* dec_sse41.c */, + 6DB3C2029FEE43C9492E48815A749CD8 /* DI.swift */, + 881B0B1AA8FD87B6D79DB655742B357B /* DictUtils.swift */, + BADD4B93BF926C2B8CA6E8BAC40F9EED /* EdgeBorder.swift */, + 07ACC03496AD0255B5AF70CE5B4D3739 /* EmojiRating.swift */, + B73ECF10580052B7EADF1C8126C1F5D4 /* enc.c */, + E634ED9E4F291E94869F8F535E2FB59D /* enc_neon.c */, + E7F5A16999097A6591248430D5EE1DD5 /* enc_sse2.c */, + 399AFF4367999AB195B1A4861DE2E3E1 /* enc_sse41.c */, + 34EBB44BB3ED427636A0A0A673F61914 /* Errors.swift */, + 13289E1D4EAA79792BF87E290976C758 /* FileUtils.swift */, + 8C1E609130982209FE02FE99B78BDC88 /* filter_enc.c */, + 22394C97F8C3BED5DFED7C780C1CE564 /* filters.c */, + 86706A03E7D1636BA28748C6D1F0740A /* filters_neon.c */, + 13CD7EE11BBD992CE37375834390CA29 /* filters_sse2.c */, + 262DE51E90B8F78B18D4D167AAC6C4B4 /* filters_utils.c */, + F091F375187698A52912BC097838826A /* Float+Util.swift */, + 1BC3CBC81194A98EBD7798839CE8D8C4 /* ForwardingPickerViewDelegate.swift */, + ED013EDE4144AF0F72EA8632E927919E /* frame_enc.c */, + 6C3B8263979EC31683158AC2B8D195C7 /* Hedgelog.swift */, + A9410C5F8607F2FC658BCBEDAD5EEFBE /* histogram_enc.c */, + 2D78E84DEDA315BA0155AF3658637CFC /* huffman_encode_utils.c */, + 16D25A3C7A45F1954C4E6B068C58B7EA /* huffman_utils.c */, + 4282C330B006663023D6DA60644E9C71 /* iterator_enc.c */, + F553AE255E9F4CBC202ED457EF76082F /* lossless.c */, + E0D796ED35657AF32721999BD872B40E /* lossless_enc.c */, + 69AF4F959032087EDA8FC0D0A5B6209D /* lossless_enc_neon.c */, + 53A2AF1A2EBA24407AC0E0E425DED2AB /* lossless_enc_sse2.c */, + EFBDDE4003994362E3FE58381610B087 /* lossless_enc_sse41.c */, + A296C0A01AD7CFE6D2C8A9367A6C2A67 /* lossless_neon.c */, + 9E7A2B7242409A2E9C0F0B5B9DBCC3A8 /* lossless_sse2.c */, + 8F310530386C455D52673B41DA4A2B76 /* lossless_sse41.c */, + 8DCCB7DFFA938AEFD75591B645CDDDF0 /* MethodSwizzler.swift */, + CF7956F6F3924F58644A57ECA19C3559 /* MultipleChoiceOptions.swift */, + 172A611FED667B809856910CFB1F8DDA /* muxedit.c */, + 58890CE1C13F1D218F91B8F1B662297C /* muxinternal.c */, + 42AC22285670786E35A95F104334D436 /* muxread.c */, + B3892D9BAABC94C6D1D38A736617D726 /* near_lossless_enc.c */, + 516914075585022AAE4D7BBA974FC678 /* NetworkSample.swift */, + 0B5738CAC80AE5D72B75358EEE9D7559 /* NumberRating.swift */, + BC5EDD8394DA064EC7C83ACD7ECCD71E /* Optional+Util.swift */, + 4B69F8B2559E5EE459C85C21EFEE836B /* palette.c */, + 80A1861E13FC4397C27E11FB9264C784 /* ph_backward_references_enc.h */, + 114279FBFB8CEFB0597BCACAFD61F21C /* ph_bit_reader_utils.h */, + C15956FAD29CD446AED83B28072F258A /* ph_bit_writer_utils.h */, + 10FE6C33488F83B12D9A37C71584E723 /* ph_color_cache_utils.h */, + D22E0F8ACB0AADC33D8564D1D4E72C93 /* ph_common_dec.h */, + F4C64C6E9AA1FDB5C402603F7B39F3BC /* ph_common_sse2.h */, + 3B8A9E04574F84317E10956DD24EE006 /* ph_common_sse41.h */, + FBA8E6C91C9871D781A894E9021E767F /* ph_cost_enc.h */, + 93E56AB9DB0FA2E618B09B988C467DD9 /* ph_cpu.h */, + D396A5F4E5EA6A631CFA1F5BA73BE979 /* ph_decode.h */, + 4BFDFC30C1DB0FBE9E1AAB4CA5D5E990 /* ph_dsp.h */, + 5A40B8F5231A7944996ACCC1E340899D /* ph_encode.h */, + 9F7478CDF58D2E47C91E9AEA4F3BFA36 /* ph_endian_inl_utils.h */, + 450735A51B25E2BC879F80FE3EF683E2 /* ph_filters_utils.h */, + FB3C8E706E1BB31142E0F7F3BDAAD7A3 /* ph_format_constants.h */, + 1DA4E5C73A12E0A6898B70F3A9CED1A6 /* ph_histogram_enc.h */, + F115DBC1009A4AE8B1E2EEACECEE686D /* ph_huffman_encode_utils.h */, + DE79FAF47ADBBDA1FD96FA4EB80CD2D4 /* ph_huffman_utils.h */, + 6ABF4049707FD7850C0863E5B99FC269 /* ph_lossless.h */, + 44B2D98D590BDDF72420B7AC9EE530EF /* ph_lossless_common.h */, + FC51F4877491548B2CE8612D8EBE8994 /* ph_mux.h */, + E851D31270CCB1CA24F49C52015B7F59 /* ph_mux_types.h */, + E20DB8D1A1DFFDC41590109620887074 /* ph_muxi.h */, + A4B8C487F55A8DC54688605DFF2114A6 /* ph_neon.h */, + A97D7894341E91B3BF09503921D83C16 /* ph_palette.h */, + 7F3CFCC8A376939FB12B819B27393F00 /* ph_quant.h */, + 0C5591E49085B223152BC20087DBC2F5 /* ph_quant_levels_utils.h */, + A64CAB6E8B5412C7633F7B8565B8651D /* ph_random_utils.h */, + C95684E80F2639489418BE63DB2999CD /* ph_rescaler_utils.h */, + E71BC82ADF0D9EC4F9A390C9D17DAEB7 /* ph_sharpyuv.h */, + BCC15FDAC7750FBA4BA88CC539B5231E /* ph_sharpyuv_cpu.h */, + 1E3A1E4CB22BAA257379DDFFB6654CC3 /* ph_sharpyuv_csp.h */, + EEA4A4F055CC07589E72A35CB0E1EF28 /* ph_sharpyuv_dsp.h */, + C1C1F47ADEAE9E1F0B9810DF4E04BD56 /* ph_sharpyuv_gamma.h */, + 52ABD4159B83070BF140003F701169DE /* ph_thread_utils.h */, + 81A9CA6BA8912B85E00744D072E0AF98 /* ph_types.h */, + AFC508491302293623F9C9FE123DF31D /* ph_utils.h */, + AC8C2B6DA50936FEAAA8D7CF5AD97F60 /* ph_vp8_dec.h */, + 63BDD7A067C75995C9F44A5BD44EE5E7 /* ph_vp8i_dec.h */, + 27378C145AF70BB39C853D63F329CD6D /* ph_vp8i_enc.h */, + 229D517849A145DB966B04AE150168FB /* ph_vp8li_dec.h */, + 268E4C94970868F0267B55DD268C4FDC /* ph_vp8li_enc.h */, + 44A2B199B29D72CB41D7DCC66EE77960 /* ph_webpi_dec.h */, + 6F05DC32B52DAD2AA8964AB408980A1F /* ph_yuv.h */, + 94288A5DC18B54C6DC2DD8C571B0F0F6 /* picture_csp_enc.c */, + 856534F765DD23D7992D3EC872FE93B7 /* picture_enc.c */, + FD4EE07A95F242B51F29985FC791B86C /* picture_psnr_enc.c */, + F3BBC9AA886853862CBA71BF1966D0F2 /* picture_rescale_enc.c */, + CDACF4D658D473E1D0C0BC475C6B6D9F /* picture_tools_enc.c */, + 67DFC8AB8EF46C4E3F28229F5DCE1C8C /* PostHog.h */, + 5A8552B95757A4F529060DCD92FA9EA8 /* PostHogApi.swift */, + BBAA59A93C40F18F756F62E7761DDDA2 /* PostHogAppLifeCycleIntegration.swift */, + 770559699B6C4F893D1E7A02F2CD0026 /* PostHogAutocaptureEventTracker.swift */, + C3B1285FF54F13BB960970B7370E7941 /* PostHogAutocaptureIntegration.swift */, + 8DAC933623085F073616F04099AF6703 /* PostHogBatchUploadInfo.swift */, + 5C042A1CBFE280735CE602F869E5E16E /* PostHogConfig.swift */, + BE54094ABB405AD044CBC194CBC3787A /* PostHogConsoleLogInterceptor.swift */, + 1FDB8663680B7CBA9F5E40A296878638 /* PostHogConsumerPayload.swift */, + 94407C342DD9E167C5D75E705899A981 /* PostHogContext.swift */, + 66AFFADA1464DFC165AF775D6EDDE652 /* PostHogDisplaySurvey.swift */, + 56F211460A3349D6478176319237385E /* PostHogDisplaySurveyAppearance.swift */, + B2CD1BA30ECCBAFA941F6D593A91CE5A /* PostHogDisplaySurveyQuestion.swift */, + D8213698CA3EB61F4236A6C8E334099A /* PostHogEvent.swift */, + C7C8924423D336E936B9B8D7528391B5 /* PostHogExtensions.swift */, + 866330A74D6DEBA7354A80F57B4FB378 /* PostHogFileBackedQueue.swift */, + 58006037934279EEB6F765806CE31D38 /* PostHogIntegration.swift */, + 0E6E0620A6F6472F44291ECF29967CCB /* PostHogLegacyQueue.swift */, + 03F6F40CA78AF34A91BC6DEC8595DF55 /* PostHogLogEntry.swift */, + C720DA9EA026D5E7449597140583E063 /* PostHogLogLevel.swift */, + D88D282FD37C24951D615E881B2D8B5E /* PostHogMaskViewModifier.swift */, + D3261BD495FD61B20BC8BA98578865B7 /* PostHogNextSurveyQuestion.swift */, + 905362F80FAC60800B3E20073D217B0C /* PostHogNoMaskViewModifier.swift */, + ED8BAA4BA5693210FDA9BE42AA76F93E /* PostHogPersonProfiles.swift */, + D8ACAC7F2DBEC2E6F6C08E461831738E /* PostHogPropertiesSanitizer.swift */, + ED3CF8C4E1C8C5343DE62E4B8A2043AD /* PostHogQueue.swift */, + 05688B486C05767CC332F7BF18F63FA6 /* PostHogRemoteConfig.swift */, + 36EBA9BB4949682419A03F0FE285AFDD /* PostHogReplayIntegration.swift */, + 49AA24A61F055E7B69C6A5D8C6B2AD78 /* PostHogScreenViewIntegration.swift */, + EF48F37324DDA42ABC08BC0E63AE23FC /* PostHogSDK.swift */, + D888AE3CE3943C819B306C6E7A8FF6BC /* PostHogSessionManager.swift */, + 103153A1783959BDADE5A086BAEE932E /* PostHogSessionReplayConfig.swift */, + BA4A2E54DC65E09C0702AC19D93C51D0 /* PostHogSessionReplayConsoleLogConfig.swift */, + BEA18A14AB5E18A4522909D739010F8B /* PostHogSessionReplayConsoleLogsPlugin.swift */, + 52C45951D83FB2A2320DCC090D6EE882 /* PostHogSessionReplayNetworkPlugin.swift */, + 4B180FB52B54B3D7CE6A1EB92AF8CD1F /* PostHogSessionReplayPlugin.swift */, + 1B7E943BABB7F3EAC5DE3E570F21D845 /* PostHogStorage.swift */, + CFB8864DE15913957BF2887CC886727E /* PostHogStorageManager.swift */, + C84990ADFAD35F17E5D87A70DE6B2768 /* PostHogSurvey.swift */, + 752389685141ECE7C426B3DEB3D9F601 /* PostHogSurvey+Display.swift */, + 2AB66D01B3E08701E5AD477ECC306070 /* PostHogSurveyAppearance.swift */, + B45CA8B0658CABEFE16DEF11945D4C96 /* PostHogSurveyConditions.swift */, + 701958AB5C27BF0CCC2736BED2525ADA /* PostHogSurveyEnums.swift */, + 1B69967DBE2F79C655769D9E766052B9 /* PostHogSurveyIntegration.swift */, + FA884DB8D0FD6DE10EC0E09ECBCB0FCC /* PostHogSurveyQuestion.swift */, + 73BBDBBF3DD175220822CBE95FE0B6DA /* PostHogSurveyResponse.swift */, + 15D68B2FA6D4A041D8698FC67954D8CF /* PostHogSurveysConfig.swift */, + 57AED642173AA14A5AB8433E3D4B85ED /* PostHogSurveysDefaultDelegate.swift */, + F9D50B5F78458141D8272AA365385479 /* PostHogSwiftUIViewModifiers.swift */, + 69AC865B00E170495D08F65246F26683 /* PostHogSwizzler.swift */, + 267A77D966A9CB080522F80053CB2597 /* PostHogTagViewModifier.swift */, + A40B6975FA38F82CFAD982D745DC1177 /* PostHogVersion.swift */, + 7309703A36FD4345F1782408988D9A2A /* predictor_enc.c */, + 58B2390554BF82073BFFF56330375E16 /* quant_enc.c */, + D601464067F9E5B1E5D5620C8924912F /* quant_levels_dec_utils.c */, + 8460AF919A0E871BF4A73C742BE5F688 /* quant_levels_utils.c */, + A84BF685A521744850F13C55520AAB70 /* QuestionHeader.swift */, + 3C816478A7A89278F0B9617CD33B4ABB /* QuestionTypes.swift */, + 276E633F3287EC40CFDCCA704F85A7BB /* random_utils.c */, + 6E80C814BD248103DB298C992AB07604 /* Reachability.swift */, + 1F97F6C297E221C6D17A28FD64FD56C9 /* ReadWriteLock.swift */, + 2DE0CB3567095D87C771243576EF9353 /* rescaler.c */, + 8B72D15908356213E83617F069F28778 /* rescaler_neon.c */, + 3EA95D0670D5D47F7BC69002970C0E15 /* rescaler_sse2.c */, + 85583DB1C0D12DF398032FE155609A9E /* rescaler_utils.c */, + 30358A66B1E325E7BF8FACB77136DCBD /* Resources.swift */, + 6EC9E8EDC00E76181958B8A45361E2E3 /* RRStyle.swift */, + EDA36D795E2D2304CF2F39068E99EED8 /* RRWireframe.swift */, + CB852437A4FE639A36D1C941F94F307A /* SegmentedControl.swift */, + DF2A3A811E0015722FF7E34E9338B448 /* sharpyuv.c */, + 79C664EEE1DC1B7E4C1AD3FFBCE62DCF /* sharpyuv_cpu.c */, + B2FFDD72D120FD8743D882769EF18E9B /* sharpyuv_csp.c */, + DB9CEA743E2F233A85546C94DD807354 /* sharpyuv_dsp.c */, + ECD1B7D205F7FC63D45533FC2BAE3410 /* sharpyuv_gamma.c */, + 77C6B393EA1E57769EC0389F1AD05B9C /* sharpyuv_neon.c */, + CD9B87A72D9E03E8531BE4E64C96BBAA /* sharpyuv_sse2.c */, + 06902488BC3ACDB66C4CC8E3DA9B2BF1 /* ssim.c */, + 71C01AE5C1556B7FCEB70598A2359C57 /* ssim_sse2.c */, + 98C872681D85E83B48A177D688728D6E /* String+Util.swift */, + 26FE4D61E0E5E088E0A627B0FDFC294E /* Survey+Util.swift */, + 262A25E1DBCEB2502F06974ED0FE5FBD /* SurveyButton.swift */, + 635FCA4635E4DDDA1A3FC3953FC4C9E9 /* SurveyDisplayController.swift */, + 9AE8485816F0A8CEE5E8A00890CA324A /* SurveyPresentationDetentsRepresentable.swift */, + 6304E8E5FDED5863B9DCE550E79F2F13 /* SurveySheet.swift */, + 4AF87321A55BD1044FFD6A604445B369 /* SurveysRootView.swift */, + C9A8771BEB422C0EBA1669CCBCC77482 /* SurveysWindow.swift */, + 28E5B198F689C1C063C1DE0BF9B7ABAF /* SwiftUI+Util.swift */, + 390E5F5970C7EFFE8A499DFEFE839B86 /* syntax_enc.c */, + BCE3D06D9F5BD48177BA8DD707F3B037 /* thread_utils.c */, + 74828AFF764C4FBA1E8273C81B8E961D /* TimeBasedEpochGenerator.swift */, + 04F316A4B2CB95903F1476FD8F6E5506 /* token_enc.c */, + B4AE78BA77D7992329434D3A2D3370F6 /* tree_enc.c */, + C07415FC3293C535290E997DFFF959FD /* UIApplication+.swift */, + EC7A62B6B9F7CF772B7553E3B238AAB3 /* UIColor+Util.swift */, + 497BF2253E737EA49DACF8B6460E188A /* UIImage+Util.swift */, + 5131F1FFA6BB84612F7867F86C51FCBE /* UIImage+WebP.swift */, + 1DB59137CCCF0AB605EA3C16F30B47C7 /* UITextInputTraits+Util.swift */, + 394497CE62FB8F5E127D55A66D718803 /* UIView+PostHogLabel.swift */, + E3D701A5BB86EA2FBA69F57589340667 /* UIView+Util.swift */, + 1A884B1BB87102903507CF0084AE903C /* UIViewController.swift */, + 54D1B502E8A8D8A7CF0F160485F27D94 /* UIWindow+.swift */, + 5C1E2A40BEFB3E27559CF75D3AF598D0 /* upsampling.c */, + C52A7E8E1B3DA2B543DA937C3BE7162B /* upsampling_neon.c */, + 58E3377D17B30CB16D0310778D68FF1B /* upsampling_sse2.c */, + C87BE5C48779C989CE62FA8282E8B3D5 /* upsampling_sse41.c */, + 905D11210787EACE6D48E66560631096 /* URLSessionExtension.swift */, + 439F6F8B57A9D0DC1E4C4669A0B6BD70 /* URLSessionInterceptor.swift */, + 1CACEF186F8DF80FAFD17F7D33BC5D71 /* URLSessionSwizzler.swift */, + 24177CAF8EE40225DC590E24E98B5907 /* utils.c */, + 39451BDDC73A8B6E24FFE97D8F418255 /* UUIDUtils.swift */, + 9145EC49215D8AD4B67DC4C8A3FFCD24 /* View+PostHogLabel.swift */, + EAA1A0EEEA8915D7ED932898580D55DA /* ViewTreeSnapshotStatus.swift */, + 1900B13AA3B133ACDED32587C775ABA2 /* vp8l_enc.c */, + 46FD7041B91F02D8C44A8B23724610E8 /* webp_enc.c */, + CBF4E22F61B73B539ABF63E8ADAAB87E /* yuv.c */, + 235F09390CCC154EF3CE1AF84501B9D5 /* yuv_neon.c */, + B233AD5FB3C464116639B4C4A446D5A9 /* yuv_sse2.c */, + 30238EC8C0FAB5B9682951C8C40A56BC /* yuv_sse41.c */, + 2D5E9A1C5E8CEAE871AA6E0BECC9314A /* Resources */, + 2F453DBFE7C2D6DF985DB9E1B233D9AC /* Support Files */, + ); + name = PostHog; + path = PostHog; + sourceTree = ""; + }; + 1484FEE6BEED2D255564FAF6215F4252 /* Pods-CableTests */ = { + isa = PBXGroup; + children = ( + E72753E6126AC46A7475C95A3F9DE5A7 /* Pods-CableTests.modulemap */, + B1784FF53C3EB6951476A12994ECAE5A /* Pods-CableTests-acknowledgements.markdown */, + 5633C72981EE1D3DFED81A8579AA5217 /* Pods-CableTests-acknowledgements.plist */, + D07D529105A58FA284174316608207E1 /* Pods-CableTests-dummy.m */, + 3AE65FACEDE1F85D4627DDF23A372ED1 /* Pods-CableTests-Info.plist */, + 85828150034429DD2DBC8A14669BDF37 /* Pods-CableTests-umbrella.h */, + 9833327F3E1576D80701526ECD9F245B /* Pods-CableTests.debug.xcconfig */, + 54A7FFEE9A3DE4A9543516D400145E56 /* Pods-CableTests.release.xcconfig */, + ); + name = "Pods-CableTests"; + path = "Target Support Files/Pods-CableTests"; + sourceTree = ""; + }; + 1CA9B4AF47F09AED5B697E849F55B43E /* Targets Support Files */ = { + isa = PBXGroup; + children = ( + ED72A752F3C64B6966D7D367BFAE6679 /* Pods-Cable */, + EB416C5BECB04A8A0EE73DFB48A2DD95 /* Pods-Cable-CableUITests */, + 383A41246B730B9633A2A1E341D6324B /* Pods-Cable-CableUITestsScreenshot */, + 1484FEE6BEED2D255564FAF6215F4252 /* Pods-CableTests */, + ); + name = "Targets Support Files"; + sourceTree = ""; + }; + 2D5E9A1C5E8CEAE871AA6E0BECC9314A /* Resources */ = { + isa = PBXGroup; + children = ( + AC3ADEA740D3607618548A1451E8EE35 /* PrivacyInfo.xcprivacy */, + ); + name = Resources; + sourceTree = ""; + }; + 2F453DBFE7C2D6DF985DB9E1B233D9AC /* Support Files */ = { + isa = PBXGroup; + children = ( + 1615B66B1E3F08A9CCE5056265CA80A8 /* PostHog.modulemap */, + 9B2AAEABCF4BAAC96E0099538E6FAEA5 /* PostHog-dummy.m */, + 773CC3FBB2958F1D7CE0C0170B523B93 /* PostHog-Info.plist */, + EDFECD65E342D7E6925201E956D8FBDA /* PostHog-prefix.pch */, + 74853741E05F6EF1BEB0A51503A2FAAF /* PostHog-umbrella.h */, + DA9D1BD5EFA6A8CA76078ADFBB131D2D /* PostHog.debug.xcconfig */, + DCDFFA010640D140217DEAF48821ABD1 /* PostHog.release.xcconfig */, + 6F5F4E6D26C2B48557ACEBE8D608BC49 /* ResourceBundle-PostHog-PostHog-Info.plist */, + ); + name = "Support Files"; + path = "../Target Support Files/PostHog"; + sourceTree = ""; + }; + 383A41246B730B9633A2A1E341D6324B /* Pods-Cable-CableUITestsScreenshot */ = { + isa = PBXGroup; + children = ( + 00B098547C4C7CC1269E78AD6477793A /* Pods-Cable-CableUITestsScreenshot.modulemap */, + D08666942BF9AA9C3313A5D3639E9A56 /* Pods-Cable-CableUITestsScreenshot-acknowledgements.markdown */, + D5DD39201B51B24F484813D108EB3C2C /* Pods-Cable-CableUITestsScreenshot-acknowledgements.plist */, + 701F2C64F76028BD63C8BC57E763B989 /* Pods-Cable-CableUITestsScreenshot-dummy.m */, + B2E0C12DA5F3B1E2AD47FD377B6B55AD /* Pods-Cable-CableUITestsScreenshot-frameworks.sh */, + 85D33F85427449A3CF37371439A6C570 /* Pods-Cable-CableUITestsScreenshot-Info.plist */, + C94C1436FB9990B1C8458A4890A74FDD /* Pods-Cable-CableUITestsScreenshot-umbrella.h */, + 9567F7671610CAD55DFF871A5F19A42D /* Pods-Cable-CableUITestsScreenshot.debug.xcconfig */, + F6DC89C77E43B360E6ED162E84069CF6 /* Pods-Cable-CableUITestsScreenshot.release.xcconfig */, + ); + name = "Pods-Cable-CableUITestsScreenshot"; + path = "Target Support Files/Pods-Cable-CableUITestsScreenshot"; + sourceTree = ""; + }; + 6BC74FA425F6B6D9BB727803CF26F0A2 /* Products */ = { + isa = PBXGroup; + children = ( + 99F4A385A81C30B48330AC1106D13082 /* Pods-Cable */, + 5DF75AE42F75E0133CA974B0A992AF64 /* Pods-Cable-CableUITests */, + F48FB46A5EECE30CA2951FE024EEE7C6 /* Pods-Cable-CableUITestsScreenshot */, + 8F5ADA47F04EABAB81527ABBF841D2AC /* Pods-CableTests */, + 36A60AA62169A7D58D9D78E917A84BD6 /* PostHog */, + 42D87DCAF52175DC2D5C6B44419636A8 /* PostHog-PostHog */, + ); + name = Products; + sourceTree = ""; + }; + CF1408CF629C7361332E53B88F7BD30C = { + isa = PBXGroup; + children = ( + 9D940727FF8FB9C785EB98E56350EF41 /* Podfile */, + D210D550F4EA176C3123ED886F8F87F5 /* Frameworks */, + DF772DFEFA18716AA6009EF1D16D2D03 /* Pods */, + 6BC74FA425F6B6D9BB727803CF26F0A2 /* Products */, + 1CA9B4AF47F09AED5B697E849F55B43E /* Targets Support Files */, + ); + sourceTree = ""; + }; + D210D550F4EA176C3123ED886F8F87F5 /* Frameworks */ = { + isa = PBXGroup; + children = ( + E4801F62A6B08CD9B5410329F1A18FDE /* iOS */, + ); + name = Frameworks; + sourceTree = ""; + }; + DF772DFEFA18716AA6009EF1D16D2D03 /* Pods */ = { + isa = PBXGroup; + children = ( + 0223C71E4F7A4926A35DE003CAC38985 /* PostHog */, + ); + name = Pods; + sourceTree = ""; + }; + E4801F62A6B08CD9B5410329F1A18FDE /* iOS */ = { + isa = PBXGroup; + children = ( + 384DDA2CB25005BD6479B5987C619DD4 /* Foundation.framework */, + ); + name = iOS; + sourceTree = ""; + }; + EB416C5BECB04A8A0EE73DFB48A2DD95 /* Pods-Cable-CableUITests */ = { + isa = PBXGroup; + children = ( + 67914AAFEBAC9A2F1F127DE54D715E79 /* Pods-Cable-CableUITests.modulemap */, + 9C442AD9B5648643BF9F8C662F4B32A7 /* Pods-Cable-CableUITests-acknowledgements.markdown */, + 0B79B981CBC521AEB9B5F19CEE6486D8 /* Pods-Cable-CableUITests-acknowledgements.plist */, + C77B0E69679F739AA5F6EF3472F81B3E /* Pods-Cable-CableUITests-dummy.m */, + 5A93DEA6097B2F04643E499901EC91A6 /* Pods-Cable-CableUITests-frameworks.sh */, + FE3276BC9DD9343402F74F0411ACBAD3 /* Pods-Cable-CableUITests-Info.plist */, + 51F57C7D4BAC17BBF066E3F6D4C4B59A /* Pods-Cable-CableUITests-umbrella.h */, + D8AAD180580FCC6EE9AEFFBE6AEDA671 /* Pods-Cable-CableUITests.debug.xcconfig */, + B1C3DCD4E3C2C0AE65F78D9A92443B29 /* Pods-Cable-CableUITests.release.xcconfig */, + ); + name = "Pods-Cable-CableUITests"; + path = "Target Support Files/Pods-Cable-CableUITests"; + sourceTree = ""; + }; + ED72A752F3C64B6966D7D367BFAE6679 /* Pods-Cable */ = { + isa = PBXGroup; + children = ( + 64AC4C5BA1754D85126569A476612795 /* Pods-Cable.modulemap */, + EF645AFD51E32C0A7B929FE3710B3EC9 /* Pods-Cable-acknowledgements.markdown */, + 39C96D15D41A39D9CA442FE17E0C0C24 /* Pods-Cable-acknowledgements.plist */, + A2428DC0690BFC906FB6BBC9517C91B8 /* Pods-Cable-dummy.m */, + 2735D5533C9516CBB656FC0E44B60D7B /* Pods-Cable-frameworks.sh */, + 5757A0552803FBE7A020E16BD23EF91B /* Pods-Cable-Info.plist */, + 3B075A62E32AE782E63CB647C2425908 /* Pods-Cable-umbrella.h */, + DD8F109A9D1262A0B31CFFE54558EF8E /* Pods-Cable.debug.xcconfig */, + DE9FD3D6919DF785A204354C43DE839D /* Pods-Cable.release.xcconfig */, + ); + name = "Pods-Cable"; + path = "Target Support Files/Pods-Cable"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 03BF60FAC1B81EED9E2FE19023955AD6 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 38E6D9105E78ED4CC0E127C62F53B331 /* Pods-Cable-CableUITests-umbrella.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 09E6F859468F2F38E7B69BC67E00FB8D /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 85A31731349118825F11B6BD97570A72 /* Pods-Cable-CableUITestsScreenshot-umbrella.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 518F67BEE435AFD553D53E7E3449A09E /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + A4351828FD656D27899119C43282A46A /* Pods-Cable-umbrella.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6DBD685B887A4AF77120554981EA58B1 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + C3B88263FFF28B2C4B1C34BBFA2487CB /* ph_backward_references_enc.h in Headers */, + 308067A90C6D4714A6625855CF82A55C /* ph_bit_reader_utils.h in Headers */, + A25DB2959F2533A76503BA4FA1C3D8A8 /* ph_bit_writer_utils.h in Headers */, + 85E00607DEA0B702A88256B889A0AE46 /* ph_color_cache_utils.h in Headers */, + A881E1170145D6E36C8AD2E62BDCBB9A /* ph_common_dec.h in Headers */, + B4A5799A36D3D50E81703A45BAEE77A3 /* ph_common_sse2.h in Headers */, + 453AF00CBDEE42C069369E1F1BB482FC /* ph_common_sse41.h in Headers */, + 54E24A21DBE22F50FF2C8ED9FE8C1920 /* ph_cost_enc.h in Headers */, + 9FF301A6E7FAA2AAE00ACD5E80FD4C9B /* ph_cpu.h in Headers */, + 1ED830E9C2C477B7C3C88115DEB77BA4 /* ph_decode.h in Headers */, + E317933C3FAD82DD8D0C65EA369EF44A /* ph_dsp.h in Headers */, + 76A22346D2892A75D2C605B1E46C9C01 /* ph_encode.h in Headers */, + E6285A5C5201781908B0FA8CC012CF82 /* ph_endian_inl_utils.h in Headers */, + CA716D34DD2DC32B06C1C0B0B2C3C5C2 /* ph_filters_utils.h in Headers */, + 8A7B3562B53BEF18CBED9C9821C6A371 /* ph_format_constants.h in Headers */, + 9C95E9E96A19763B1F0B805841F064BF /* ph_histogram_enc.h in Headers */, + 0572679AE85C33C43DE798C06F3E1534 /* ph_huffman_encode_utils.h in Headers */, + 475FD727732636A7758AA528CA362744 /* ph_huffman_utils.h in Headers */, + EB51376036E66B13F5DA94861714AF5A /* ph_lossless.h in Headers */, + 83FEE6E290BF6CA38102DFB7009F6E4B /* ph_lossless_common.h in Headers */, + A086FB83D5610E0968532EF591FDC242 /* ph_mux.h in Headers */, + C40C37140BA4AC6DCF085AF14D6CBF2A /* ph_mux_types.h in Headers */, + A9947D06E0701770A3D401BD4B4DA0D1 /* ph_muxi.h in Headers */, + 4F61F0B687C035589D0FD4E29A73B0DF /* ph_neon.h in Headers */, + 144C6DD1A247AC1EDF9AD57981DCB922 /* ph_palette.h in Headers */, + E2A195E40A0C3853004F3B1C7E6ECD1C /* ph_quant.h in Headers */, + 8EC01D8A4D04C123025FD6958165F4F5 /* ph_quant_levels_utils.h in Headers */, + 395B15A53215A65E19F9C6A19D422D3A /* ph_random_utils.h in Headers */, + E617B7F0DBC07F3490AE7811551911D5 /* ph_rescaler_utils.h in Headers */, + 7460FA92770379998EFE3064CC0A9530 /* ph_sharpyuv.h in Headers */, + 4DE8C331A0B2A6959482C25ACB77E609 /* ph_sharpyuv_cpu.h in Headers */, + E7475B260DBC82214488EF1C5712B807 /* ph_sharpyuv_csp.h in Headers */, + 4EA5892CD434E01818878C81F1452F65 /* ph_sharpyuv_dsp.h in Headers */, + 3C960D07046A6486F7DC7FD20FC938FA /* ph_sharpyuv_gamma.h in Headers */, + BDE90F6901C6138EF6CB8F5ADA8F6061 /* ph_thread_utils.h in Headers */, + 923C6E3BE82D64E5623625D81D82FBA8 /* ph_types.h in Headers */, + C4A5285EB3B6FA8347156384E0693E4F /* ph_utils.h in Headers */, + ACF234EB88FF3EFA147BF0AB0A317A4D /* ph_vp8_dec.h in Headers */, + AF9FF002C9339CCCD33ADA5476D20C72 /* ph_vp8i_dec.h in Headers */, + E1A545A284C243E666880CB425BA9F20 /* ph_vp8i_enc.h in Headers */, + 9E32FD9B51892EE4CDE615D15D2FC878 /* ph_vp8li_dec.h in Headers */, + 569413C3DF6867A0AF74DE0A54B21317 /* ph_vp8li_enc.h in Headers */, + 3A184F0C63E5C112D93A58B9032A6B58 /* ph_webpi_dec.h in Headers */, + E9576BFC34B3253843CC4778948ADC07 /* ph_yuv.h in Headers */, + 9FAB5C450670E109F781D6C53E56D2AF /* PostHog.h in Headers */, + 24DEE46DD19D816066C36A6F5AFF3D20 /* PostHog-umbrella.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B2D249709949995053781C643A85F693 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 36C07E870675759BC50B2A25CCC281C5 /* Pods-CableTests-umbrella.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 52F449E691F258410D3E74F5BAFD41CD /* Pods-CableTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = E5E901805E2933A10C49FF4056309D27 /* Build configuration list for PBXNativeTarget "Pods-CableTests" */; + buildPhases = ( + B2D249709949995053781C643A85F693 /* Headers */, + EA62F0FCF8722BEA7EC3D4085650078A /* Sources */, + 6914A7F0DD12E15F1B99EAE8F381191A /* Frameworks */, + E7D6DA7D9DC3C67ABE6122EC8F4157A4 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + FC844A347CA7E4B13AF939C15B0255D7 /* PBXTargetDependency */, + ); + name = "Pods-CableTests"; + productName = Pods_CableTests; + productReference = 8F5ADA47F04EABAB81527ABBF841D2AC /* Pods-CableTests */; + productType = "com.apple.product-type.framework"; + }; + 6835ABF5E9176D16603D3FAED02C1229 /* Pods-Cable-CableUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7F875C92A0064FEA41DD432FB3D2B9F5 /* Build configuration list for PBXNativeTarget "Pods-Cable-CableUITests" */; + buildPhases = ( + 03BF60FAC1B81EED9E2FE19023955AD6 /* Headers */, + 11B9C3062484A76F522B08C02943691A /* Sources */, + 464D3A2144371ABCE15B36C2A6A868B9 /* Frameworks */, + 72E199819E43E2956927C51BECA30F80 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + D3486A4DBC5DEF58B6DE24FC89B6DB2A /* PBXTargetDependency */, + ); + name = "Pods-Cable-CableUITests"; + productName = Pods_Cable_CableUITests; + productReference = 5DF75AE42F75E0133CA974B0A992AF64 /* Pods-Cable-CableUITests */; + productType = "com.apple.product-type.framework"; + }; + 8012054959C338A62834AD9706977FB0 /* Pods-Cable */ = { + isa = PBXNativeTarget; + buildConfigurationList = F787CC961FDD06E4AD7797CC39088604 /* Build configuration list for PBXNativeTarget "Pods-Cable" */; + buildPhases = ( + 518F67BEE435AFD553D53E7E3449A09E /* Headers */, + FAA54168123200C7B9668A497C6A5801 /* Sources */, + EA42357D9D8899D007F3BE4D5BAB8571 /* Frameworks */, + B9793F0F8CA1592CFBDA2158363BEE43 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + D061B5DE671E2844D48A74C5E35051EF /* PBXTargetDependency */, + ); + name = "Pods-Cable"; + productName = Pods_Cable; + productReference = 99F4A385A81C30B48330AC1106D13082 /* Pods-Cable */; + productType = "com.apple.product-type.framework"; + }; + 86C5E834AAC4A69D5D37D96BAF8B8330 /* Pods-Cable-CableUITestsScreenshot */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4B0A632BAB21EA6EFE88E8B7CF12F513 /* Build configuration list for PBXNativeTarget "Pods-Cable-CableUITestsScreenshot" */; + buildPhases = ( + 09E6F859468F2F38E7B69BC67E00FB8D /* Headers */, + C77F8BDE60E6E297A97F81D0D98F9ACB /* Sources */, + 3890B9438DD6516F1EA5E3CAACCE8FFE /* Frameworks */, + 01BFA279DAE745D11102F6375A2AA1AD /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 732576E2FD2FBF077D0F21C041585F88 /* PBXTargetDependency */, + ); + name = "Pods-Cable-CableUITestsScreenshot"; + productName = Pods_Cable_CableUITestsScreenshot; + productReference = F48FB46A5EECE30CA2951FE024EEE7C6 /* Pods-Cable-CableUITestsScreenshot */; + productType = "com.apple.product-type.framework"; + }; + 8879D5F28A55518ACFB247594F87F75A /* PostHog */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3EA171DF69C6597F9BFFD3F4672C5A70 /* Build configuration list for PBXNativeTarget "PostHog" */; + buildPhases = ( + 6DBD685B887A4AF77120554981EA58B1 /* Headers */, + 9E80A9B36A41B08E0176D40615787A6F /* Sources */, + A26CDBEA01200B96B68F314398EEE38C /* Frameworks */, + 185F1A104E0899D7A77085F3320B8948 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 723D01296E2FD4600B1D936335ABA7C3 /* PBXTargetDependency */, + ); + name = PostHog; + productName = PostHog; + productReference = 36A60AA62169A7D58D9D78E917A84BD6 /* PostHog */; + productType = "com.apple.product-type.framework"; + }; + E326EE08AE4CF9FA8C947B96B6F8AB07 /* PostHog-PostHog */ = { + isa = PBXNativeTarget; + buildConfigurationList = CE81BF29562F4A5FB5765C0AAFFE4A53 /* Build configuration list for PBXNativeTarget "PostHog-PostHog" */; + buildPhases = ( + 97346091A7C7DDCF5BE5DA549CF27240 /* Sources */, + 73D22CAADE4FCD69C0A715C06D35F9B8 /* Frameworks */, + A32586E3BC3E86EF8B3D882DD9339221 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "PostHog-PostHog"; + productName = PostHog; + productReference = 42D87DCAF52175DC2D5C6B44419636A8 /* PostHog-PostHog */; + productType = "com.apple.product-type.bundle"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + BFDFE7DC352907FC980B868725387E98 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1600; + LastUpgradeCheck = 1600; + }; + buildConfigurationList = 4821239608C13582E20E6DA73FD5F1F9 /* Build configuration list for PBXProject "Pods" */; + compatibilityVersion = "Xcode 16.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + Base, + en, + ); + mainGroup = CF1408CF629C7361332E53B88F7BD30C; + minimizedProjectReferenceProxies = 0; + preferredProjectObjectVersion = 77; + productRefGroup = 6BC74FA425F6B6D9BB727803CF26F0A2 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 8012054959C338A62834AD9706977FB0 /* Pods-Cable */, + 6835ABF5E9176D16603D3FAED02C1229 /* Pods-Cable-CableUITests */, + 86C5E834AAC4A69D5D37D96BAF8B8330 /* Pods-Cable-CableUITestsScreenshot */, + 52F449E691F258410D3E74F5BAFD41CD /* Pods-CableTests */, + 8879D5F28A55518ACFB247594F87F75A /* PostHog */, + E326EE08AE4CF9FA8C947B96B6F8AB07 /* PostHog-PostHog */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 01BFA279DAE745D11102F6375A2AA1AD /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 185F1A104E0899D7A77085F3320B8948 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 15F7984FF736D8AF019F417DA8901036 /* PostHog-PostHog in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 72E199819E43E2956927C51BECA30F80 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + A32586E3BC3E86EF8B3D882DD9339221 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4805E842E48C745A9E6A4C06D4731373 /* PrivacyInfo.xcprivacy in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B9793F0F8CA1592CFBDA2158363BEE43 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E7D6DA7D9DC3C67ABE6122EC8F4157A4 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 11B9C3062484A76F522B08C02943691A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7241253A695D3360662518FABF8A18F9 /* Pods-Cable-CableUITests-dummy.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97346091A7C7DDCF5BE5DA549CF27240 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 9E80A9B36A41B08E0176D40615787A6F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4238E88AE8BA8624075729D326C0F4A9 /* alpha_enc.c in Sources */, + D76EBEDD6E19898C194F1FADF177664C /* alpha_processing.c in Sources */, + 3155F7F3D2B67132C14891B5A2E8F65B /* alpha_processing_neon.c in Sources */, + DFA1A5698507154A30CAA31FD6BD1A85 /* alpha_processing_sse2.c in Sources */, + 81A20054B797FF0F766333BE4913DBCC /* alpha_processing_sse41.c in Sources */, + DA6006155C3D0397BC53C971B6764235 /* analysis_enc.c in Sources */, + 43DBAA9AC5E2E63021F065F66FCBC065 /* ApplicationEventPublisher.swift in Sources */, + 6979FEA56C2EE86990E4473D5F0085CF /* ApplicationLifecyclePublisher.swift in Sources */, + 8494528FA0360A1CAFFAC147C9849375 /* ApplicationScreenViewPublisher.swift in Sources */, + 7E015D3260F0BFD29E3F0690B1E59E0D /* ApplicationViewLayoutPublisher.swift in Sources */, + 2468C99062231929D4FA5A274E16CB72 /* AssociatedKeys.swift in Sources */, + 51557FD624B2D8D939BD15D6D67DAA9F /* AutocaptureEventProcessing.swift in Sources */, + F632525798590D31F88730CB5B08A136 /* backward_references_cost_enc.c in Sources */, + 01C82EF77CAD48D420201E5041F71981 /* backward_references_enc.c in Sources */, + 5C603F51B177ED3F49DA5B71B25253F4 /* bit_reader_utils.c in Sources */, + 4391AA0D63D56F3F6233972901D35B62 /* bit_writer_utils.c in Sources */, + 526DF460F26A30E9A4C72AE94B898DAC /* BottomSection.swift in Sources */, + B1F2CAAAD6999BC7C09AE72FB869D703 /* CGColor+Util.swift in Sources */, + 493D47C4E0ADF0AA3C30D3FB6C2F9C57 /* CGSize+Util.swift in Sources */, + C2D9AAF9E55A505AAEA9DDC14B0BED48 /* color_cache_utils.c in Sources */, + 97F67C732B893F315EFDD5D598C3F71E /* config_enc.c in Sources */, + 6B8EC9F9C886019474D30A2A38FF68B2 /* ConfirmationMessage.swift in Sources */, + 9ADEA2121D351229E95318A512FB3E0F /* cost.c in Sources */, + C8B7CAFDAF0B31D48040478F71C0EBE2 /* cost_enc.c in Sources */, + F8222B205BE0E6FCCBE3ACE42940599A /* cost_neon.c in Sources */, + F395CAD4C36A94E525379C352F11ADF4 /* cost_sse2.c in Sources */, + 5C24BD9E95BD5410843BD372C028EF2F /* cpu.c in Sources */, + A2111B640365DEB1D1262861FB526BB6 /* Data+Gzip.swift in Sources */, + 8753E838E3006A1E2F72DF5F6AC2C67A /* Date+Util.swift in Sources */, + 52D4C546D7A305CC6D9B29EBBE6BA5DA /* DateUtils.swift in Sources */, + EC0A7D763F31745069FD68B9549C9A9A /* dec.c in Sources */, + BD645B55581F18C45D3A077C7120F169 /* dec_clip_tables.c in Sources */, + 858173DFD0E9446CB773D1CBB00418BD /* dec_neon.c in Sources */, + 1089A5E22CB40877785F1B4D5B8337D8 /* dec_sse2.c in Sources */, + 0357CEDE60497FDDDFF4206D50D84D32 /* dec_sse41.c in Sources */, + 51F949044932E2740E23BC026B3CB070 /* DI.swift in Sources */, + CDCFCFCE3B5007033F6B1E3AD1C1545E /* DictUtils.swift in Sources */, + 06F57CD4670CD1DAE91524790BFC6EC1 /* EdgeBorder.swift in Sources */, + 64C0E1A516D383EA3EC23A7BFC448FCE /* EmojiRating.swift in Sources */, + 96B479CE48E29ED5239A38806540AFA6 /* enc.c in Sources */, + 0B03D2F73EAB1910FB04EFA201F7FA59 /* enc_neon.c in Sources */, + C38E5112CB2F46733703F641350364B7 /* enc_sse2.c in Sources */, + 77EB7B90D005993B65D9AD7A2533B13D /* enc_sse41.c in Sources */, + 627DC06DB4D42F2FB9605AF95A528471 /* Errors.swift in Sources */, + CE53F772FBCCABD2B04587B2BD4FD177 /* FileUtils.swift in Sources */, + 89D364405A06C6C8B06DA285AC3E5A5F /* filter_enc.c in Sources */, + AD26200A6DEF1516F1C192768115BC55 /* filters.c in Sources */, + D246F10610362DEE01F847DA6180E08C /* filters_neon.c in Sources */, + 9E27DE50AA66E7023C06263FB25B71AE /* filters_sse2.c in Sources */, + 65FEDD05D8424912681558FCA1154C7D /* filters_utils.c in Sources */, + C085F172DD546D7A954F9E7CFA6549AF /* Float+Util.swift in Sources */, + 8B95C63A2E2B72090AE5A5AB08AA2F26 /* ForwardingPickerViewDelegate.swift in Sources */, + E641EDF95A76D284807990177906C9C7 /* frame_enc.c in Sources */, + 273B9F6A4399EA87F762570917E52D34 /* Hedgelog.swift in Sources */, + D16B60C05E3B60104BB3BD49495D0879 /* histogram_enc.c in Sources */, + 50B1BD836429AB01BD316C26265CD71C /* huffman_encode_utils.c in Sources */, + 5A6216FF081F6510212318F7654826F2 /* huffman_utils.c in Sources */, + 7FDBA2F04F5750B61E6DF8FA25F849BA /* iterator_enc.c in Sources */, + 38AF18D906A0862245B23AA744DEFE31 /* lossless.c in Sources */, + 550CB5E2CAB7E6F88A94256F8D4E1AD0 /* lossless_enc.c in Sources */, + 9DFA8122176106229CC767FEAF4B3304 /* lossless_enc_neon.c in Sources */, + EB6484265BE63031CDDCB790CD983769 /* lossless_enc_sse2.c in Sources */, + 9B7B7D98445B36EC45CFE01B99DC85AB /* lossless_enc_sse41.c in Sources */, + 4E33D63DE814B6144A257A9CBE8B695C /* lossless_neon.c in Sources */, + 790E4BF42B672B3EA4E21B66CB0DB05B /* lossless_sse2.c in Sources */, + A49FA4C1772B8C68D42DD34E69C76134 /* lossless_sse41.c in Sources */, + A23F680D118045C38E24C79BCA87F3F8 /* MethodSwizzler.swift in Sources */, + 889631E71D504EF98E0F26B2E5E45DD9 /* MultipleChoiceOptions.swift in Sources */, + EF9115BBB1E249A72211587F17B96835 /* muxedit.c in Sources */, + 6899E938FC1259735FECEDCCBD5DCB12 /* muxinternal.c in Sources */, + D4A6686C68814AF2106A2CAB329530DB /* muxread.c in Sources */, + 183E1F870F4FDDEA7C57FEC6BD70ED5B /* near_lossless_enc.c in Sources */, + 74B9900ECC81F957E50D152BBBED824E /* NetworkSample.swift in Sources */, + 9C0BAA41861364D4FF410FA385BA695D /* NumberRating.swift in Sources */, + 30C93CCDE28363680E50EA3A91FE8B0A /* Optional+Util.swift in Sources */, + 48B8B5B751C3E55843A697C3792C7837 /* palette.c in Sources */, + 4E1473B9FF22300F21A2ABCA3911A03B /* picture_csp_enc.c in Sources */, + 7597FE04F73E19981054D053749CBC7C /* picture_enc.c in Sources */, + 8E6CE9ACB1DEA1CB2ED4E8C43A6CB1DF /* picture_psnr_enc.c in Sources */, + 450E0503FAA2B07BD4D1F2C3CB11B122 /* picture_rescale_enc.c in Sources */, + C20E16F51EE280913ECD5C8939075863 /* picture_tools_enc.c in Sources */, + 5CC284C9FBACD9B6424307C1E8EE2618 /* PostHog-dummy.m in Sources */, + 16053DB437B9398112A24A047C55D124 /* PostHogApi.swift in Sources */, + C10F91F1C306F86A8DE647E9392EAD5B /* PostHogAppLifeCycleIntegration.swift in Sources */, + 93F25171D356A06B938299A8DC1B01A3 /* PostHogAutocaptureEventTracker.swift in Sources */, + 74A2933F21E1826BFCE98767611A1F2B /* PostHogAutocaptureIntegration.swift in Sources */, + 18BF9FAC9C9D784C748A0AFEE36F20E1 /* PostHogBatchUploadInfo.swift in Sources */, + 0F409AE2D56C546B8A1155D37CCA80D5 /* PostHogConfig.swift in Sources */, + 538F223AE0B8D0CA9A7DFF6C1BCDF61A /* PostHogConsoleLogInterceptor.swift in Sources */, + 95D55CBE1ACFF50FD1EBC6D4936B2E94 /* PostHogConsumerPayload.swift in Sources */, + 32BD682BFDA6241281CEBA7904C5A5A1 /* PostHogContext.swift in Sources */, + A958EABDF4F3CF594A315F89E9E5CB16 /* PostHogDisplaySurvey.swift in Sources */, + E2BB8174AB27B7FC6D610F3D6D2E55C9 /* PostHogDisplaySurveyAppearance.swift in Sources */, + 13D9AE4C00CE2535E0DEF135306CB8BF /* PostHogDisplaySurveyQuestion.swift in Sources */, + F149A1F7278934A1B4C094EBF060D2B6 /* PostHogEvent.swift in Sources */, + 438E9D989048FFC3DF68443837BF6398 /* PostHogExtensions.swift in Sources */, + 7DA49ED9EF36CCDB27E476FFFC6D5660 /* PostHogFileBackedQueue.swift in Sources */, + BFF5E04D16488F3C122C6F2099D16702 /* PostHogIntegration.swift in Sources */, + F64FD409AECB0B56E3D10EC2B87A36EE /* PostHogLegacyQueue.swift in Sources */, + E68C5CF478D5211B525ABCD7A11F346A /* PostHogLogEntry.swift in Sources */, + 252C54D4211DE3406B82DC8206E354DA /* PostHogLogLevel.swift in Sources */, + AC8C4D813E979741EB84B24C111118F0 /* PostHogMaskViewModifier.swift in Sources */, + 944E6DAB32560E951BAE16F5157F0D35 /* PostHogNextSurveyQuestion.swift in Sources */, + FDDD950364CB85A70790993AD79EFB17 /* PostHogNoMaskViewModifier.swift in Sources */, + 0BE2AAAE30AE1B29B23B1E55C819D7A7 /* PostHogPersonProfiles.swift in Sources */, + 9C1AF5AC07E7ED9CCAFECF84C27E73BD /* PostHogPropertiesSanitizer.swift in Sources */, + 5F8EF5FE4D73E4F038E2DEA4D5C6A146 /* PostHogQueue.swift in Sources */, + 2B18064A9F0190519F509DD5891B81A0 /* PostHogRemoteConfig.swift in Sources */, + 152739ADAFEA0909584A96C79E07C151 /* PostHogReplayIntegration.swift in Sources */, + 570EB0858F1AFE99A8B7CA45C6C3BAEE /* PostHogScreenViewIntegration.swift in Sources */, + 38177A6BBDA55B9E965610C950A24204 /* PostHogSDK.swift in Sources */, + 3E4221A803DEDC8750348BA20A791EEE /* PostHogSessionManager.swift in Sources */, + 9748FF5103EE1045677BDAED5F00DFC9 /* PostHogSessionReplayConfig.swift in Sources */, + B959C06622003BB1462ABA53313D422B /* PostHogSessionReplayConsoleLogConfig.swift in Sources */, + 81A74112BDB883335465416F2DDD4F73 /* PostHogSessionReplayConsoleLogsPlugin.swift in Sources */, + 8CC38A2ED3E279B2AFF414782AF6EF05 /* PostHogSessionReplayNetworkPlugin.swift in Sources */, + 13F0B4871E8EB793200587DF5C1C4246 /* PostHogSessionReplayPlugin.swift in Sources */, + 580FDEB8A8A152A39A502A40A58724C2 /* PostHogStorage.swift in Sources */, + 615D7CAA8FFEFF59CC50B16C3981EBE4 /* PostHogStorageManager.swift in Sources */, + 6045482673A7946DF31DA4DE7CBC35F6 /* PostHogSurvey.swift in Sources */, + 6DCDBF153AE370C69768E7A7B57F0748 /* PostHogSurvey+Display.swift in Sources */, + E9F821F9B181137CF9756E7B5DB290F8 /* PostHogSurveyAppearance.swift in Sources */, + 947857B02E90224ED7E680233305DB5E /* PostHogSurveyConditions.swift in Sources */, + 73FCA47CCEAFE7E4B070B112F21A9C7E /* PostHogSurveyEnums.swift in Sources */, + 7206DFEB5BB955754C39FC17011FF545 /* PostHogSurveyIntegration.swift in Sources */, + A1C65033EA26B7634A850B9635138B45 /* PostHogSurveyQuestion.swift in Sources */, + 2599E595E56867C2CC371352E74E5CB0 /* PostHogSurveyResponse.swift in Sources */, + 847080B8D7683BBBEF894EE6EA38912E /* PostHogSurveysConfig.swift in Sources */, + 560F19C3A96258BB787771F53438277C /* PostHogSurveysDefaultDelegate.swift in Sources */, + 76A53A0EB8A7F9C73557B3B32D669BBB /* PostHogSwiftUIViewModifiers.swift in Sources */, + 822476AFB7FFB4F8605DC4FA8A19D926 /* PostHogSwizzler.swift in Sources */, + 01EB398AE7FB8D1AA43CCC4BE25256A1 /* PostHogTagViewModifier.swift in Sources */, + BF00726324688B38AB498AB8C0D6A9CC /* PostHogVersion.swift in Sources */, + 2C34DA75C8E0FD9A4A61AF56826CE149 /* predictor_enc.c in Sources */, + 91A181721A95245C3A19DD7DF8D3ED20 /* quant_enc.c in Sources */, + 918561BED43D6D9A55D7ED87A4233FAA /* quant_levels_dec_utils.c in Sources */, + 900853FFC431D50BA4A0E6B015873734 /* quant_levels_utils.c in Sources */, + 1C54E6F4D075181AE6E195DC2CBA737D /* QuestionHeader.swift in Sources */, + E645C1A88AD4148D5B873D3811EA3BE0 /* QuestionTypes.swift in Sources */, + 0E16356A19CF2C4ED7FE29BBB4F700BC /* random_utils.c in Sources */, + 9B3694B60BA51B86814BC4B1C0AADE57 /* Reachability.swift in Sources */, + 125C8B61C19411B4E222E2D4670EDA7C /* ReadWriteLock.swift in Sources */, + 80DA5FB9AFF890ABE611B5A044CDB036 /* rescaler.c in Sources */, + 07169E6F462DE20D301856B85FB0C74F /* rescaler_neon.c in Sources */, + 2C4358D6D544B725F9B1EB1A3FF6D35B /* rescaler_sse2.c in Sources */, + 6D1C160E221E65E31F2C2C1FB79BF4B3 /* rescaler_utils.c in Sources */, + CA4046A0C463B283FDEF14E88E615622 /* Resources.swift in Sources */, + 0867541398BCAD5D8D499F66BB8887FB /* RRStyle.swift in Sources */, + B94F9615E1406645D9976C740A12F52E /* RRWireframe.swift in Sources */, + E3C0C764F23ABB7818F31999FE8856BF /* SegmentedControl.swift in Sources */, + 3491FB1468B98D7D43A3D900A0B00281 /* sharpyuv.c in Sources */, + 39FD34007D0F0D009C7F332C7DC5E187 /* sharpyuv_cpu.c in Sources */, + 360ABBEBC1B15FDCF4DA237E7647F2BE /* sharpyuv_csp.c in Sources */, + 6CD6C7D137F96E4B377097CC311E07A9 /* sharpyuv_dsp.c in Sources */, + C41DEDAF518B62C012E82A92168E2F71 /* sharpyuv_gamma.c in Sources */, + 4ED6FC8835C4F8D158F713E6D7E8BB76 /* sharpyuv_neon.c in Sources */, + C9FDB67918170E2DD508541BA216560E /* sharpyuv_sse2.c in Sources */, + CCEB5CA997D8A5E48BD7E73B6F4EB71D /* ssim.c in Sources */, + 62B8676E90C8E48016693783C0036D8F /* ssim_sse2.c in Sources */, + 5E514467B3F4FB8469C113819DE25FF2 /* String+Util.swift in Sources */, + 40BD75D3EE82B288BCAA04D3DACEBD99 /* Survey+Util.swift in Sources */, + 4A2589F3E16BE0902709FF2A3630BE17 /* SurveyButton.swift in Sources */, + D4AB0395E8CB9D1D7677DAE14B6B5F59 /* SurveyDisplayController.swift in Sources */, + C1D7ABBD432849EC886D9852B1A06CC4 /* SurveyPresentationDetentsRepresentable.swift in Sources */, + 44058A91D10EDA3415018DE7925C348B /* SurveySheet.swift in Sources */, + DF22F1DD5882402BECE6B91CD1A227E8 /* SurveysRootView.swift in Sources */, + 8D7AFBBCADCA7780494700C5EE031A7A /* SurveysWindow.swift in Sources */, + D9BF8B39F7A90185A2AE69B33D1F91C2 /* SwiftUI+Util.swift in Sources */, + E6E78E2A322D4E7736D55ADCE1D73BF6 /* syntax_enc.c in Sources */, + 8A2EA1FD1DE278407880880388CC7863 /* thread_utils.c in Sources */, + 645BFE97A3A8E993F08B87285951AB82 /* TimeBasedEpochGenerator.swift in Sources */, + 4F5F04A349551F176FFCF2A5F318055D /* token_enc.c in Sources */, + 0C4DEC31440C7333BC97E7B78D67BC72 /* tree_enc.c in Sources */, + A87193AFE5CEF7AA673BC80BA4FB9C9B /* UIApplication+.swift in Sources */, + 9C1C48333A30CF28A6F8F918216615FB /* UIColor+Util.swift in Sources */, + 0E3BEE1E611D26371DAF50C23E7AC1BB /* UIImage+Util.swift in Sources */, + 4BB3C8C41EA289FB29DCEC068F7FCD76 /* UIImage+WebP.swift in Sources */, + 19D82E01983174C6A45C7EE85AA81795 /* UITextInputTraits+Util.swift in Sources */, + 4FA4C53C4992B0098AACCF9805B840B3 /* UIView+PostHogLabel.swift in Sources */, + B511EA85DAC18FD6205B26B71DE1050C /* UIView+Util.swift in Sources */, + 89AB8E555A39C1D168505C89C8CF5777 /* UIViewController.swift in Sources */, + 236BD01A81BCD619F470D17D00FA2440 /* UIWindow+.swift in Sources */, + 287E75C60B7FB8A9A9E6A4D5E2465701 /* upsampling.c in Sources */, + A7E4B6A307B6D11D3216D24DFF540A76 /* upsampling_neon.c in Sources */, + 932C39898E846B7343F58C87A7C8A18D /* upsampling_sse2.c in Sources */, + A923905603728FF1F5D63FA3383A7647 /* upsampling_sse41.c in Sources */, + D5AC0B15E162B74F89BE5C08EAD54621 /* URLSessionExtension.swift in Sources */, + 32AECF33E41A6D0971F70B1220A182F5 /* URLSessionInterceptor.swift in Sources */, + 3E0A9BA7670824E8A542BB0238965308 /* URLSessionSwizzler.swift in Sources */, + 7203FB71B522DF771D5BF42CFE86CE87 /* utils.c in Sources */, + 3B1D530F1C85B1227913F0026BFD0BDC /* UUIDUtils.swift in Sources */, + 0AE01E9EDF9EC4A863872FB71F99983B /* View+PostHogLabel.swift in Sources */, + 866CAB0B3150FEF773928C86AD9171E1 /* ViewTreeSnapshotStatus.swift in Sources */, + EBDF53B9DD3C2058CE110560E30904D6 /* vp8l_enc.c in Sources */, + 9DAE58DDF04BB8AC6700C178740CA4A2 /* webp_enc.c in Sources */, + CEEA0DE4B861387A64E05F48B5A7CEE5 /* yuv.c in Sources */, + 0EF0A53B84657EF4A4A6C6E11C409DF3 /* yuv_neon.c in Sources */, + 82AAEEC1837B795CD013636BAEF14DA9 /* yuv_sse2.c in Sources */, + 0A24C32E44D0415EF08780849822BCEE /* yuv_sse41.c in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C77F8BDE60E6E297A97F81D0D98F9ACB /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 141DFED4583A49BE8122CF85D9C530B2 /* Pods-Cable-CableUITestsScreenshot-dummy.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EA62F0FCF8722BEA7EC3D4085650078A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7A4857DA3F9C646CCC878F9329D19E60 /* Pods-CableTests-dummy.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FAA54168123200C7B9668A497C6A5801 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B9342CDA79BCBDBEAB3D05BB11EDDBFD /* Pods-Cable-dummy.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 723D01296E2FD4600B1D936335ABA7C3 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = "PostHog-PostHog"; + target = E326EE08AE4CF9FA8C947B96B6F8AB07 /* PostHog-PostHog */; + targetProxy = F71674A1D40932D753945F87EC1C5BB1 /* PBXContainerItemProxy */; + }; + 732576E2FD2FBF077D0F21C041585F88 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = PostHog; + target = 8879D5F28A55518ACFB247594F87F75A /* PostHog */; + targetProxy = 0D8CAE93284D0305C41A4E72F6A07403 /* PBXContainerItemProxy */; + }; + D061B5DE671E2844D48A74C5E35051EF /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = PostHog; + target = 8879D5F28A55518ACFB247594F87F75A /* PostHog */; + targetProxy = BF702A62A6C910D491628C2736B02269 /* PBXContainerItemProxy */; + }; + D3486A4DBC5DEF58B6DE24FC89B6DB2A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = PostHog; + target = 8879D5F28A55518ACFB247594F87F75A /* PostHog */; + targetProxy = 62E12AA42617D0B8EC3B5959449CF6A7 /* PBXContainerItemProxy */; + }; + FC844A347CA7E4B13AF939C15B0255D7 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = "Pods-Cable"; + target = 8012054959C338A62834AD9706977FB0 /* Pods-Cable */; + targetProxy = 61238D0FC731DB2BBAA0E8FC812EACC8 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 066BC42F1D3638A5C6146DB138A5E4C8 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = DD8F109A9D1262A0B31CFFE54558EF8E /* Pods-Cable.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + INFOPLIST_FILE = "Target Support Files/Pods-Cable/Pods-Cable-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MODULEMAP_FILE = "Target Support Files/Pods-Cable/Pods-Cable.modulemap"; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 24B82364850CD61A83AC39AB834AB0C5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = DA9D1BD5EFA6A8CA76078ADFBB131D2D /* PostHog.debug.xcconfig */; + buildSettings = { + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_PREFIX_HEADER = "Target Support Files/PostHog/PostHog-prefix.pch"; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = "Target Support Files/PostHog/PostHog-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MODULEMAP_FILE = "Target Support Files/PostHog/PostHog.modulemap"; + PRODUCT_MODULE_NAME = PostHog; + PRODUCT_NAME = PostHog; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_INSTALL_OBJC_HEADER = YES; + SWIFT_VERSION = 5.3; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 2790FD60DE7909879F2E49B80999A418 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = DE9FD3D6919DF785A204354C43DE839D /* Pods-Cable.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + INFOPLIST_FILE = "Target Support Files/Pods-Cable/Pods-Cable-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MODULEMAP_FILE = "Target Support Files/Pods-Cable/Pods-Cable.modulemap"; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 393E6A9D498B59105CC15A51FD45FA2F /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = DCDFFA010640D140217DEAF48821ABD1 /* PostHog.release.xcconfig */; + buildSettings = { + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_PREFIX_HEADER = "Target Support Files/PostHog/PostHog-prefix.pch"; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = "Target Support Files/PostHog/PostHog-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MODULEMAP_FILE = "Target Support Files/PostHog/PostHog.modulemap"; + PRODUCT_MODULE_NAME = PostHog; + PRODUCT_NAME = PostHog; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_INSTALL_OBJC_HEADER = YES; + SWIFT_VERSION = 5.3; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 5814376CBA6AD9C93845A394E4A7DDCD /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = DA9D1BD5EFA6A8CA76078ADFBB131D2D /* PostHog.debug.xcconfig */; + buildSettings = { + CODE_SIGNING_ALLOWED = NO; + CONFIGURATION_BUILD_DIR = "$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/PostHog"; + IBSC_MODULE = PostHog; + INFOPLIST_FILE = "Target Support Files/PostHog/ResourceBundle-PostHog-PostHog-Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + PRODUCT_NAME = PostHog; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + WRAPPER_EXTENSION = bundle; + }; + name = Debug; + }; + 5F5AE8F49433A59A9392F2677BEB6317 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D8AAD180580FCC6EE9AEFFBE6AEDA671 /* Pods-Cable-CableUITests.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + INFOPLIST_FILE = "Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MODULEMAP_FILE = "Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests.modulemap"; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 6C71F7B4D69110348654FDB3B0D188E2 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F6DC89C77E43B360E6ED162E84069CF6 /* Pods-Cable-CableUITestsScreenshot.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + INFOPLIST_FILE = "Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MODULEMAP_FILE = "Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot.modulemap"; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 7B9BF991C60D017C403B3AB25A3D9D21 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = B1C3DCD4E3C2C0AE65F78D9A92443B29 /* Pods-Cable-CableUITests.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + INFOPLIST_FILE = "Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MODULEMAP_FILE = "Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests.modulemap"; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 7D418BF305962BD394B69D9080B4BFFA /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = DCDFFA010640D140217DEAF48821ABD1 /* PostHog.release.xcconfig */; + buildSettings = { + CODE_SIGNING_ALLOWED = NO; + CONFIGURATION_BUILD_DIR = "$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/PostHog"; + IBSC_MODULE = PostHog; + INFOPLIST_FILE = "Target Support Files/PostHog/ResourceBundle-PostHog-PostHog-Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + PRODUCT_NAME = PostHog; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + WRAPPER_EXTENSION = bundle; + }; + name = Release; + }; + 8C9963745FD01B6B3EF95F9CE58D9F10 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "POD_CONFIGURATION_DEBUG=1", + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRIP_INSTALLED_PRODUCT = NO; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + SYMROOT = "${SRCROOT}/../build"; + }; + name = Debug; + }; + 92492A978D4AC63ABB84F6977386188B /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9567F7671610CAD55DFF871A5F19A42D /* Pods-Cable-CableUITestsScreenshot.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + INFOPLIST_FILE = "Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MODULEMAP_FILE = "Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot.modulemap"; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 97EC72F28306B21CC7F052446D53670C /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9833327F3E1576D80701526ECD9F245B /* Pods-CableTests.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + INFOPLIST_FILE = "Target Support Files/Pods-CableTests/Pods-CableTests-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MODULEMAP_FILE = "Target Support Files/Pods-CableTests/Pods-CableTests.modulemap"; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + B9F3557045385CA606F5AF548D58B58A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_PREPROCESSOR_DEFINITIONS = ( + "POD_CONFIGURATION_RELEASE=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRIP_INSTALLED_PRODUCT = NO; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + SYMROOT = "${SRCROOT}/../build"; + }; + name = Release; + }; + CB70384C563F6BB137294A0E0712EF1C /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 54A7FFEE9A3DE4A9543516D400145E56 /* Pods-CableTests.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + INFOPLIST_FILE = "Target Support Files/Pods-CableTests/Pods-CableTests-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MODULEMAP_FILE = "Target Support Files/Pods-CableTests/Pods-CableTests.modulemap"; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 3EA171DF69C6597F9BFFD3F4672C5A70 /* Build configuration list for PBXNativeTarget "PostHog" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 24B82364850CD61A83AC39AB834AB0C5 /* Debug */, + 393E6A9D498B59105CC15A51FD45FA2F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 4821239608C13582E20E6DA73FD5F1F9 /* Build configuration list for PBXProject "Pods" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8C9963745FD01B6B3EF95F9CE58D9F10 /* Debug */, + B9F3557045385CA606F5AF548D58B58A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 4B0A632BAB21EA6EFE88E8B7CF12F513 /* Build configuration list for PBXNativeTarget "Pods-Cable-CableUITestsScreenshot" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 92492A978D4AC63ABB84F6977386188B /* Debug */, + 6C71F7B4D69110348654FDB3B0D188E2 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7F875C92A0064FEA41DD432FB3D2B9F5 /* Build configuration list for PBXNativeTarget "Pods-Cable-CableUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5F5AE8F49433A59A9392F2677BEB6317 /* Debug */, + 7B9BF991C60D017C403B3AB25A3D9D21 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + CE81BF29562F4A5FB5765C0AAFFE4A53 /* Build configuration list for PBXNativeTarget "PostHog-PostHog" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5814376CBA6AD9C93845A394E4A7DDCD /* Debug */, + 7D418BF305962BD394B69D9080B4BFFA /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + E5E901805E2933A10C49FF4056309D27 /* Build configuration list for PBXNativeTarget "Pods-CableTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97EC72F28306B21CC7F052446D53670C /* Debug */, + CB70384C563F6BB137294A0E0712EF1C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F787CC961FDD06E4AD7797CC39088604 /* Build configuration list for PBXNativeTarget "Pods-Cable" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 066BC42F1D3638A5C6146DB138A5E4C8 /* Debug */, + 2790FD60DE7909879F2E49B80999A418 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = BFDFE7DC352907FC980B868725387E98 /* Project object */; +} diff --git a/Pods/Pods.xcodeproj/xcuserdata/lange-hegermann.xcuserdatad/xcschemes/Pods-Cable-CableUITests.xcscheme b/Pods/Pods.xcodeproj/xcuserdata/lange-hegermann.xcuserdatad/xcschemes/Pods-Cable-CableUITests.xcscheme new file mode 100644 index 0000000..77eaac9 --- /dev/null +++ b/Pods/Pods.xcodeproj/xcuserdata/lange-hegermann.xcuserdatad/xcschemes/Pods-Cable-CableUITests.xcscheme @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Pods/Pods.xcodeproj/xcuserdata/lange-hegermann.xcuserdatad/xcschemes/Pods-Cable-CableUITestsScreenshot.xcscheme b/Pods/Pods.xcodeproj/xcuserdata/lange-hegermann.xcuserdatad/xcschemes/Pods-Cable-CableUITestsScreenshot.xcscheme new file mode 100644 index 0000000..303c7af --- /dev/null +++ b/Pods/Pods.xcodeproj/xcuserdata/lange-hegermann.xcuserdatad/xcschemes/Pods-Cable-CableUITestsScreenshot.xcscheme @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Pods/Pods.xcodeproj/xcuserdata/lange-hegermann.xcuserdatad/xcschemes/Pods-Cable.xcscheme b/Pods/Pods.xcodeproj/xcuserdata/lange-hegermann.xcuserdatad/xcschemes/Pods-Cable.xcscheme new file mode 100644 index 0000000..59c1334 --- /dev/null +++ b/Pods/Pods.xcodeproj/xcuserdata/lange-hegermann.xcuserdatad/xcschemes/Pods-Cable.xcscheme @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Pods/Pods.xcodeproj/xcuserdata/lange-hegermann.xcuserdatad/xcschemes/Pods-CableTests.xcscheme b/Pods/Pods.xcodeproj/xcuserdata/lange-hegermann.xcuserdatad/xcschemes/Pods-CableTests.xcscheme new file mode 100644 index 0000000..a5eb9e0 --- /dev/null +++ b/Pods/Pods.xcodeproj/xcuserdata/lange-hegermann.xcuserdatad/xcschemes/Pods-CableTests.xcscheme @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Pods/Pods.xcodeproj/xcuserdata/lange-hegermann.xcuserdatad/xcschemes/PostHog-PostHog.xcscheme b/Pods/Pods.xcodeproj/xcuserdata/lange-hegermann.xcuserdatad/xcschemes/PostHog-PostHog.xcscheme new file mode 100644 index 0000000..1f38c86 --- /dev/null +++ b/Pods/Pods.xcodeproj/xcuserdata/lange-hegermann.xcuserdatad/xcschemes/PostHog-PostHog.xcscheme @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Pods/Pods.xcodeproj/xcuserdata/lange-hegermann.xcuserdatad/xcschemes/PostHog.xcscheme b/Pods/Pods.xcodeproj/xcuserdata/lange-hegermann.xcuserdatad/xcschemes/PostHog.xcscheme new file mode 100644 index 0000000..fc0d05a --- /dev/null +++ b/Pods/Pods.xcodeproj/xcuserdata/lange-hegermann.xcuserdatad/xcschemes/PostHog.xcscheme @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Pods/Pods.xcodeproj/xcuserdata/lange-hegermann.xcuserdatad/xcschemes/xcschememanagement.plist b/Pods/Pods.xcodeproj/xcuserdata/lange-hegermann.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..55a472f --- /dev/null +++ b/Pods/Pods.xcodeproj/xcuserdata/lange-hegermann.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,41 @@ + + + + + SchemeUserState + + Pods-Cable-CableUITests.xcscheme + + isShown + + + Pods-Cable-CableUITestsScreenshot.xcscheme + + isShown + + + Pods-Cable.xcscheme + + isShown + + + Pods-CableTests.xcscheme + + isShown + + + PostHog-PostHog.xcscheme + + isShown + + + PostHog.xcscheme + + isShown + + + + SuppressBuildableAutocreation + + + diff --git a/Pods/PostHog/LICENSE b/Pods/PostHog/LICENSE new file mode 100644 index 0000000..7e799e3 --- /dev/null +++ b/Pods/PostHog/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) [2023] [PostHog] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Pods/PostHog/PostHog/App Life Cycle/ApplicationLifecyclePublisher.swift b/Pods/PostHog/PostHog/App Life Cycle/ApplicationLifecyclePublisher.swift new file mode 100644 index 0000000..987fe65 --- /dev/null +++ b/Pods/PostHog/PostHog/App Life Cycle/ApplicationLifecyclePublisher.swift @@ -0,0 +1,199 @@ +// +// ApplicationLifecyclePublisher.swift +// PostHog +// +// Created by Yiannis Josephides on 16/12/2024. +// + +#if os(iOS) || os(tvOS) || os(visionOS) + import UIKit +#elseif os(macOS) + import AppKit +#elseif os(watchOS) + import WatchKit +#endif + +typealias AppLifecycleHandler = () -> Void + +protocol AppLifecyclePublishing: AnyObject { + /// Registers a callback for the `didBecomeActive` event. + func onDidBecomeActive(_ callback: @escaping AppLifecycleHandler) -> RegistrationToken + /// Registers a callback for the `didEnterBackground` event. + func onDidEnterBackground(_ callback: @escaping AppLifecycleHandler) -> RegistrationToken + /// Registers a callback for the `didFinishLaunching` event. + func onDidFinishLaunching(_ callback: @escaping AppLifecycleHandler) -> RegistrationToken +} + +/** + A publisher that handles application lifecycle events and allows registering callbacks for them. + + This class provides a way to observe application lifecycle events like when the app becomes active, + enters background, or finishes launching. Callbacks can be registered for each event type and will + be automatically unregistered when their registration token is deallocated. + + Example usage: + ``` + let token = ApplicationLifecyclePublisher.shared.onDidBecomeActive { + // App became active logic + } + // Keep `token` in memory to keep the registration active + // When token is deallocated, the callback will be automatically unregistered + ``` + */ +final class ApplicationLifecyclePublisher: BaseApplicationLifecyclePublisher { + /// Shared instance to allow easy access across the app. + static let shared = ApplicationLifecyclePublisher() + + override private init() { + super.init() + + let defaultCenter = NotificationCenter.default + + #if os(iOS) || os(tvOS) + defaultCenter.addObserver(self, + selector: #selector(appDidFinishLaunching), + name: UIApplication.didFinishLaunchingNotification, + object: nil) + defaultCenter.addObserver(self, + selector: #selector(appDidEnterBackground), + name: UIApplication.didEnterBackgroundNotification, + object: nil) + defaultCenter.addObserver(self, + selector: #selector(appDidBecomeActive), + name: UIApplication.didBecomeActiveNotification, + object: nil) + #elseif os(visionOS) + defaultCenter.addObserver(self, + selector: #selector(appDidFinishLaunching), + name: UIApplication.didFinishLaunchingNotification, + object: nil) + defaultCenter.addObserver(self, + selector: #selector(appDidEnterBackground), + name: UIScene.willDeactivateNotification, + object: nil) + defaultCenter.addObserver(self, + selector: #selector(appDidBecomeActive), + name: UIScene.didActivateNotification, + object: nil) + #elseif os(macOS) + defaultCenter.addObserver(self, + selector: #selector(appDidFinishLaunching), + name: NSApplication.didFinishLaunchingNotification, + object: nil) + // macOS does not have didEnterBackgroundNotification, so we use didResignActiveNotification + defaultCenter.addObserver(self, + selector: #selector(appDidEnterBackground), + name: NSApplication.didResignActiveNotification, + object: nil) + defaultCenter.addObserver(self, + selector: #selector(appDidBecomeActive), + name: NSApplication.didBecomeActiveNotification, + object: nil) + #elseif os(watchOS) + if #available(watchOS 7.0, *) { + NotificationCenter.default.addObserver(self, + selector: #selector(appDidBecomeActive), + name: WKApplication.didBecomeActiveNotification, + object: nil) + } else { + NotificationCenter.default.addObserver(self, + selector: #selector(appDidBecomeActive), + name: .init("UIApplicationDidBecomeActiveNotification"), + object: nil) + } + #endif + } + + // MARK: - Handlers + + @objc private func appDidEnterBackground() { + notifyHandlers(didEnterBackgroundHandlers) + } + + @objc private func appDidBecomeActive() { + notifyHandlers(didBecomeActiveHandlers) + } + + @objc private func appDidFinishLaunching() { + notifyHandlers(didFinishLaunchingHandlers) + } + + private func notifyHandlers(_ handlers: [AppLifecycleHandler]) { + for handler in handlers { + notifyHander(handler) + } + } + + private func notifyHander(_ handler: @escaping AppLifecycleHandler) { + if Thread.isMainThread { + handler() + } else { + DispatchQueue.main.async(execute: handler) + } + } +} + +class BaseApplicationLifecyclePublisher: AppLifecyclePublishing { + private let registrationLock = NSLock() + + private var didBecomeActiveCallbacks: [UUID: AppLifecycleHandler] = [:] + private var didEnterBackgroundCallbacks: [UUID: AppLifecycleHandler] = [:] + private var didFinishLaunchingCallbacks: [UUID: AppLifecycleHandler] = [:] + + var didBecomeActiveHandlers: [AppLifecycleHandler] { + registrationLock.withLock { Array(didBecomeActiveCallbacks.values) } + } + + var didEnterBackgroundHandlers: [AppLifecycleHandler] { + registrationLock.withLock { Array(didEnterBackgroundCallbacks.values) } + } + + var didFinishLaunchingHandlers: [AppLifecycleHandler] { + registrationLock.withLock { Array(didFinishLaunchingCallbacks.values) } + } + + /// Registers a callback for the `didBecomeActive` event. + func onDidBecomeActive(_ callback: @escaping AppLifecycleHandler) -> RegistrationToken { + register(handler: callback, on: \.didBecomeActiveCallbacks) + } + + /// Registers a callback for the `didEnterBackground` event. + func onDidEnterBackground(_ callback: @escaping AppLifecycleHandler) -> RegistrationToken { + register(handler: callback, on: \.didEnterBackgroundCallbacks) + } + + /// Registers a callback for the `didFinishLaunching` event. + func onDidFinishLaunching(_ callback: @escaping AppLifecycleHandler) -> RegistrationToken { + register(handler: callback, on: \.didFinishLaunchingCallbacks) + } + + func register( + handler callback: @escaping AppLifecycleHandler, + on keyPath: ReferenceWritableKeyPath + ) -> RegistrationToken { + let id = UUID() + registrationLock.withLock { + self[keyPath: keyPath][id] = callback + } + + return RegistrationToken { [weak self] in + // Registration token deallocated here + guard let self else { return } + self.registrationLock.withLock { + self[keyPath: keyPath][id] = nil + } + } + } +} + +final class RegistrationToken { + private let onDealloc: () -> Void + + init(_ onDealloc: @escaping () -> Void) { + self.onDealloc = onDealloc + } + + deinit { + onDealloc() + } +} diff --git a/Pods/PostHog/PostHog/App Life Cycle/PostHogAppLifeCycleIntegration.swift b/Pods/PostHog/PostHog/App Life Cycle/PostHogAppLifeCycleIntegration.swift new file mode 100644 index 0000000..b423326 --- /dev/null +++ b/Pods/PostHog/PostHog/App Life Cycle/PostHogAppLifeCycleIntegration.swift @@ -0,0 +1,214 @@ +// +// PostHogAppLifeCycleIntegration.swift +// PostHog +// +// Created by Ioannis Josephides on 19/02/2025. +// + +import Foundation + +/** + Add capability to capture application lifecycle events. + + This integration: + - captures an `App Installed` event on the first launch of the app + - captures an `App Updated` event on any subsequent launch with a different version + - captures an `App Opened` event when the app is opened (including the first launch) + - captures an `App Backgrounded` event when the app moves to the background + */ +final class PostHogAppLifeCycleIntegration: PostHogIntegration { + var requiresSwizzling: Bool { false } + + private static var integrationInstalledLock = NSLock() + private static var integrationInstalled = false + private static var didCaptureAppInstallOrUpdate = false + + private weak var postHog: PostHogSDK? + + // True if the app is launched for the first time + private var isFreshAppLaunch = true + // Manually maintained flag to determine background status of the app + private var isAppBackgrounded: Bool = true + + private var didBecomeActiveToken: RegistrationToken? + private var didEnterBackgroundToken: RegistrationToken? + private var didFinishLaunchingToken: RegistrationToken? + + func install(_ postHog: PostHogSDK) throws { + try PostHogAppLifeCycleIntegration.integrationInstalledLock.withLock { + if PostHogAppLifeCycleIntegration.integrationInstalled { + throw InternalPostHogError(description: "App life cycle integration already installed to another PostHogSDK instance.") + } + PostHogAppLifeCycleIntegration.integrationInstalled = true + } + + self.postHog = postHog + + start() + captureAppInstallOrUpdated() + } + + func uninstall(_ postHog: PostHogSDK) { + // uninstall only for integration instance + if self.postHog === postHog || self.postHog == nil { + stop() + self.postHog = nil + PostHogAppLifeCycleIntegration.integrationInstalledLock.withLock { + PostHogAppLifeCycleIntegration.integrationInstalled = false + } + } + } + + /** + Start capturing app lifecycles events + */ + func start() { + let publisher = DI.main.appLifecyclePublisher + didFinishLaunchingToken = publisher.onDidFinishLaunching { [weak self] in + self?.captureAppInstallOrUpdated() + } + didBecomeActiveToken = publisher.onDidBecomeActive { [weak self] in + self?.captureAppOpened() + } + didEnterBackgroundToken = publisher.onDidEnterBackground { [weak self] in + self?.captureAppBackgrounded() + } + } + + /** + Stop capturing app lifecycle events + */ + func stop() { + didFinishLaunchingToken = nil + didBecomeActiveToken = nil + didEnterBackgroundToken = nil + } + + private func captureAppInstallOrUpdated() { + // Check if Application Installed or Application Updated was already checked in the lifecycle of this app + // This can be called multiple times in case of optOut, multiple instances or start/stop integration + guard let postHog, !PostHogAppLifeCycleIntegration.didCaptureAppInstallOrUpdate else { return } + + PostHogAppLifeCycleIntegration.didCaptureAppInstallOrUpdate = true + + if !postHog.config.captureApplicationLifecycleEvents { + hedgeLog("Skipping Application Installed/Application Updated event - captureApplicationLifecycleEvents is disabled in configuration") + return + } + + let bundle = Bundle.main + + let versionName = bundle.infoDictionary?["CFBundleShortVersionString"] as? String + let versionCode = bundle.infoDictionary?["CFBundleVersion"] as? String + + // capture app installed/updated + let userDefaults = UserDefaults.standard + + let previousVersion = userDefaults.string(forKey: "PHGVersionKey") + let previousVersionCode = userDefaults.string(forKey: "PHGBuildKeyV2") + + var props: [String: Any] = [:] + var event: String + if previousVersionCode == nil { + // installed + event = "Application Installed" + } else { + event = "Application Updated" + + // Do not send version updates if its the same + if previousVersionCode == versionCode { + return + } + + if previousVersion != nil { + props["previous_version"] = previousVersion + } + props["previous_build"] = previousVersionCode + } + + var syncDefaults = false + if versionName != nil { + props["version"] = versionName + userDefaults.setValue(versionName, forKey: "PHGVersionKey") + syncDefaults = true + } + + if versionCode != nil { + props["build"] = versionCode + userDefaults.setValue(versionCode, forKey: "PHGBuildKeyV2") + syncDefaults = true + } + + if syncDefaults { + userDefaults.synchronize() + } + + postHog.capture(event, properties: props) + } + + private func captureAppOpened() { + guard let postHog else { return } + + guard isAppBackgrounded else { + hedgeLog("Skipping Application Opened event - app already in foreground") + return + } + + isAppBackgrounded = false + + if !postHog.config.captureApplicationLifecycleEvents { + hedgeLog("Skipping Application Opened event - captureApplicationLifecycleEvents is disabled in configuration") + return + } + + var props: [String: Any] = [:] + props["from_background"] = !isFreshAppLaunch + + if isFreshAppLaunch { + let bundle = Bundle.main + + let versionName = bundle.infoDictionary?["CFBundleShortVersionString"] as? String + let versionCode = bundle.infoDictionary?["CFBundleVersion"] as? String + + if versionName != nil { + props["version"] = versionName + } + if versionCode != nil { + props["build"] = versionCode + } + + isFreshAppLaunch = false + } + + postHog.capture("Application Opened", properties: props) + } + + private func captureAppBackgrounded() { + guard let postHog else { return } + + guard !isAppBackgrounded else { + hedgeLog("Skipping Application Opened event - app already in background") + return + } + + isAppBackgrounded = true + + if !postHog.config.captureApplicationLifecycleEvents { + hedgeLog("Skipping Application Backgrounded event - captureApplicationLifecycleEvents is disabled in configuration") + return + } + + postHog.capture("Application Backgrounded") + } +} + +#if TESTING + extension PostHogAppLifeCycleIntegration { + static func clearInstalls() { + PostHogAppLifeCycleIntegration.didCaptureAppInstallOrUpdate = false + integrationInstalledLock.withLock { + integrationInstalled = false + } + } + } +#endif diff --git a/Pods/PostHog/PostHog/ApplicationViewLayoutPublisher.swift b/Pods/PostHog/PostHog/ApplicationViewLayoutPublisher.swift new file mode 100644 index 0000000..7855a3f --- /dev/null +++ b/Pods/PostHog/PostHog/ApplicationViewLayoutPublisher.swift @@ -0,0 +1,159 @@ +// +// ApplicationViewLayoutPublisher.swift +// PostHog +// +// Created by Ioannis Josephides on 19/03/2025. +// + +#if os(iOS) || os(tvOS) + import UIKit + + typealias ApplicationViewLayoutHandler = () -> Void + + protocol ViewLayoutPublishing: AnyObject { + /// Registers a callback for getting notified when a UIView is laid out. + /// Note: callback guaranteed to be called on main thread + func onViewLayout(throttle: TimeInterval, _ callback: @escaping ApplicationViewLayoutHandler) -> RegistrationToken + } + + final class ApplicationViewLayoutPublisher: BaseApplicationViewLayoutPublisher { + static let shared = ApplicationViewLayoutPublisher() + + private var hasSwizzled: Bool = false + + func start() { + swizzleLayoutSubviews() + } + + func stop() { + unswizzleLayoutSubviews() + } + + func swizzleLayoutSubviews() { + guard !hasSwizzled else { return } + hasSwizzled = true + + swizzle( + forClass: UIView.self, + original: #selector(UIView.layoutSublayers(of:)), + new: #selector(UIView.ph_swizzled_layoutSublayers(of:)) + ) + } + + func unswizzleLayoutSubviews() { + guard hasSwizzled else { return } + hasSwizzled = false + + // swizzling twice will exchange implementations back to original + swizzle( + forClass: UIView.self, + original: #selector(UIView.layoutSublayers(of:)), + new: #selector(UIView.ph_swizzled_layoutSublayers(of:)) + ) + } + + override func onViewLayout(throttle interval: TimeInterval, _ callback: @escaping ApplicationViewLayoutHandler) -> RegistrationToken { + let id = UUID() + registrationLock.withLock { + self.onViewLayoutCallbacks[id] = ThrottledHandler(handler: callback, interval: interval) + } + + // start on first callback registration + if !hasSwizzled { + start() + } + + return RegistrationToken { [weak self] in + // Registration token deallocated here + guard let self else { return } + let handlerCount = self.registrationLock.withLock { + self.onViewLayoutCallbacks[id] = nil + return self.onViewLayoutCallbacks.values.count + } + + // stop when there are no more callbacks + if handlerCount <= 0 { + self.stop() + } + } + } + + // Called from swizzled `UIView.layoutSubviews` + fileprivate func layoutSubviews() { + notifyHandlers() + } + + #if TESTING + func simulateLayoutSubviews() { + layoutSubviews() + } + #endif + } + + class BaseApplicationViewLayoutPublisher: ViewLayoutPublishing { + fileprivate let registrationLock = NSLock() + + var onViewLayoutCallbacks: [UUID: ThrottledHandler] = [:] + + final class ThrottledHandler { + static let throttleQueue = DispatchQueue(label: "com.posthog.ThrottledHandler", + target: .global(qos: .utility)) + + let interval: TimeInterval + let handler: ApplicationViewLayoutHandler + + private var lastFired: Date = .distantPast + + init(handler: @escaping ApplicationViewLayoutHandler, interval: TimeInterval) { + self.handler = handler + self.interval = interval + } + + func throttleHandler() { + let now = now() + let timeSinceLastFired = now.timeIntervalSince(lastFired) + + if timeSinceLastFired >= interval { + lastFired = now + // notify on main + DispatchQueue.main.async(execute: handler) + } + } + } + + func onViewLayout(throttle interval: TimeInterval, _ callback: @escaping ApplicationViewLayoutHandler) -> RegistrationToken { + let id = UUID() + registrationLock.withLock { + self.onViewLayoutCallbacks[id] = ThrottledHandler( + handler: callback, + interval: interval + ) + } + + return RegistrationToken { [weak self] in + // Registration token deallocated here + guard let self else { return } + self.registrationLock.withLock { + self.onViewLayoutCallbacks[id] = nil + } + } + } + + func notifyHandlers() { + ThrottledHandler.throttleQueue.async { + // Don't lock on main + let handlers = self.registrationLock.withLock { self.onViewLayoutCallbacks.values } + for handler in handlers { + handler.throttleHandler() + } + } + } + } + + extension UIView { + @objc func ph_swizzled_layoutSublayers(of layer: CALayer) { + ph_swizzled_layoutSublayers(of: layer) // call original, not altering execution logic + ApplicationViewLayoutPublisher.shared.layoutSubviews() + } + } +#endif diff --git a/Pods/PostHog/PostHog/Autocapture/AutocaptureEventProcessing.swift b/Pods/PostHog/PostHog/Autocapture/AutocaptureEventProcessing.swift new file mode 100644 index 0000000..121db12 --- /dev/null +++ b/Pods/PostHog/PostHog/Autocapture/AutocaptureEventProcessing.swift @@ -0,0 +1,14 @@ +// +// AutocaptureEventProcessing.swift +// PostHog +// +// Created by Yiannis Josephides on 30/10/2024. +// + +#if os(iOS) || targetEnvironment(macCatalyst) + import Foundation + + protocol AutocaptureEventProcessing: AnyObject { + func process(source: PostHogAutocaptureEventTracker.EventSource, event: PostHogAutocaptureEventTracker.EventData) + } +#endif diff --git a/Pods/PostHog/PostHog/Autocapture/ForwardingPickerViewDelegate.swift b/Pods/PostHog/PostHog/Autocapture/ForwardingPickerViewDelegate.swift new file mode 100644 index 0000000..771ce61 --- /dev/null +++ b/Pods/PostHog/PostHog/Autocapture/ForwardingPickerViewDelegate.swift @@ -0,0 +1,71 @@ +// +// ForwardingPickerViewDelegate.swift +// PostHog +// +// Created by Yiannis Josephides on 24/10/2024. +// + +#if os(iOS) || targetEnvironment(macCatalyst) + import UIKit + + final class ForwardingPickerViewDelegate: NSObject, UIPickerViewDelegate, UIPickerViewDataSource { + // this needs to be weak since `actualDelegate` will hold a strong reference to `ForwardingPickerViewDelegate` + weak var actualDelegate: UIPickerViewDelegate? + private var valueChangedCallback: (() -> Void)? + + // We respond to the same selectors that the original delegate responds to + override func responds(to aSelector: Selector!) -> Bool { + actualDelegate?.responds(to: aSelector) ?? false + } + + init(delegate: UIPickerViewDelegate?, onValueChanged: @escaping () -> Void) { + actualDelegate = delegate + valueChangedCallback = onValueChanged + } + + // MARK: - UIPickerViewDataSource + + func numberOfComponents(in pickerView: UIPickerView) -> Int { + (actualDelegate as? UIPickerViewDataSource)?.numberOfComponents(in: pickerView) ?? 0 + } + + func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { + (actualDelegate as? UIPickerViewDataSource)?.pickerView(pickerView, numberOfRowsInComponent: component) ?? 0 + } + + // MARK: - UIPickerViewDelegate + + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + valueChangedCallback?() + actualDelegate?.pickerView?(pickerView, didSelectRow: row, inComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView { + actualDelegate?.pickerView?(pickerView, viewForRow: row, forComponent: component, reusing: view) ?? UIView() + } + + func pickerView(_ pickerView: UIPickerView, widthForComponent component: Int) -> CGFloat { + actualDelegate?.pickerView?(pickerView, widthForComponent: component) ?? .zero + } + + func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat { + actualDelegate?.pickerView?(pickerView, rowHeightForComponent: component) ?? .zero + } + + func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { + actualDelegate?.pickerView?(pickerView, titleForRow: row, forComponent: component) + } + + func pickerView(_ pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? { + actualDelegate?.pickerView?(pickerView, attributedTitleForRow: row, forComponent: component) + } + } + + extension UIPickerViewDelegate { + var ph_forwardingDelegate: UIPickerViewDelegate? { + get { objc_getAssociatedObject(self, &AssociatedKeys.phForwardingDelegate) as? UIPickerViewDelegate } + set { objc_setAssociatedObject(self, &AssociatedKeys.phForwardingDelegate, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } + } + } + +#endif diff --git a/Pods/PostHog/PostHog/Autocapture/PostHogAutocaptureEventTracker.swift b/Pods/PostHog/PostHog/Autocapture/PostHogAutocaptureEventTracker.swift new file mode 100644 index 0000000..bc38f0c --- /dev/null +++ b/Pods/PostHog/PostHog/Autocapture/PostHogAutocaptureEventTracker.swift @@ -0,0 +1,606 @@ +// +// PostHogAutocaptureEventTracker.swift +// PostHog +// +// Created by Yiannis Josephides on 14/10/2024. +// + +#if os(iOS) || targetEnvironment(macCatalyst) + import UIKit + + class PostHogAutocaptureEventTracker { + struct EventData { + let touchCoordinates: CGPoint? + let value: String? + let screenName: String? + let viewHierarchy: [Element] + // values >0 means that this event will be debounced for `debounceInterval` + let debounceInterval: TimeInterval + } + + struct Element { + let text: String + let targetClass: String + let baseClass: String? + let label: String? + + var elementsChainEntry: String { + var attributes = [String]() + + if !text.isEmpty { + attributes.append("text=\(text.quoted)") + } + if let baseClass, !baseClass.isEmpty { + attributes.append("attr__class=\(baseClass.quoted)") + } + if let label, !label.isEmpty { + attributes.append("attr_id=\(label.quoted)") + } + + return attributes.isEmpty ? targetClass : "\(targetClass):\(attributes.joined())" + } + } + + enum EventSource { + case notification(name: String) + case actionMethod(description: String) + case gestureRecognizer(description: String) + } + + static var eventProcessor: (any AutocaptureEventProcessing)? { + willSet { + if newValue != nil { + swizzle() + } else { + unswizzle() + } + } + } + + private static var hasSwizzled: Bool = false + private static func swizzle() { + guard !hasSwizzled else { return } + hasSwizzled = true + swizzleMethods() + registerNotifications() + } + + private static func unswizzle() { + guard hasSwizzled else { return } + hasSwizzled = false + swizzleMethods() // swizzling again will exchange implementations back to original + unregisterNotifications() + } + + private static func swizzleMethods() { + PostHog.swizzle( + forClass: UIApplication.self, + original: #selector(UIApplication.sendAction), + new: #selector(UIApplication.ph_swizzled_uiapplication_sendAction) + ) + + PostHog.swizzle( + forClass: UIGestureRecognizer.self, + original: #selector(setter: UIGestureRecognizer.state), + new: #selector(UIGestureRecognizer.ph_swizzled_uigesturerecognizer_state_Setter) + ) + + PostHog.swizzle( + forClass: UIScrollView.self, + original: #selector(setter: UIScrollView.contentOffset), + new: #selector(UIScrollView.ph_swizzled_setContentOffset_Setter) + ) + + PostHog.swizzle( + forClass: UIPickerView.self, + original: #selector(setter: UIPickerView.delegate), + new: #selector(UIPickerView.ph_swizzled_setDelegate) + ) + } + + private static func registerNotifications() { + NotificationCenter.default.addObserver( + PostHogAutocaptureEventTracker.self, + selector: #selector(didEndEditing), + name: UITextField.textDidEndEditingNotification, + object: nil + ) + NotificationCenter.default.addObserver( + PostHogAutocaptureEventTracker.self, + selector: #selector(didEndEditing), + name: UITextView.textDidEndEditingNotification, + object: nil + ) + } + + private static func unregisterNotifications() { + NotificationCenter.default.removeObserver(PostHogAutocaptureEventTracker.self, name: UITextField.textDidEndEditingNotification, object: nil) + NotificationCenter.default.removeObserver(PostHogAutocaptureEventTracker.self, name: UITextView.textDidEndEditingNotification, object: nil) + } + + // `UITextField` or `UITextView` did end editing notification + @objc static func didEndEditing(_ notification: NSNotification) { + guard let view = notification.object as? UIView, let eventData = view.eventData else { return } + + eventProcessor?.process(source: .notification(name: "change"), event: eventData) + } + } + + extension UIApplication { + @objc func ph_swizzled_uiapplication_sendAction(_ action: Selector, to target: Any?, from sender: Any?, for event: UIEvent?) -> Bool { + defer { + // Currently, the action methods pointing to a SwiftUI target are blocked. + let targetClass = String(describing: object_getClassName(target)) + if targetClass.contains("SwiftUI") { + hedgeLog("Action methods on SwiftUI targets are not yet supported.") + } else if let control = sender as? UIControl, + control.ph_shouldTrack(action, for: target), + let eventData = control.eventData, + let eventDescription = control.event(for: action, to: target)?.description(forControl: control) + { + PostHogAutocaptureEventTracker.eventProcessor?.process(source: .actionMethod(description: eventDescription), event: eventData) + } + } + + // first, call original method + return ph_swizzled_uiapplication_sendAction(action, to: target, from: sender, for: event) + } + } + + extension UIGestureRecognizer { + // swiftlint:disable:next cyclomatic_complexity + @objc func ph_swizzled_uigesturerecognizer_state_Setter(_ state: UIGestureRecognizer.State) { + // first, call original method + ph_swizzled_uigesturerecognizer_state_Setter(state) + + guard state == .ended, let view, shouldTrack(view) else { return } + + // block scroll and zoom gestures for `UIScrollView`. + if let scrollView = view as? UIScrollView { + if self === scrollView.panGestureRecognizer { + return + } + #if !os(tvOS) + if self === scrollView.pinchGestureRecognizer { + return + } + #endif + } + + // block all gestures for `UISwitch` (already captured via `.valueChanged` action) + if String(describing: type(of: view)).starts(with: "UISwitch") { + return + } + // ignore gestures in `UIPickerColumnView` + if String(describing: type(of: view)) == "UIPickerColumnView" { + return + } + + let gestureDescription: String? + switch self { + case is UITapGestureRecognizer: + gestureDescription = EventType.kTouch + case is UISwipeGestureRecognizer: + gestureDescription = EventType.kSwipe + case is UIPanGestureRecognizer: + gestureDescription = EventType.kPan + case is UILongPressGestureRecognizer: + gestureDescription = EventType.kLongPress + #if !os(tvOS) + case is UIPinchGestureRecognizer: + gestureDescription = EventType.kPinch + case is UIRotationGestureRecognizer: + gestureDescription = EventType.kRotation + case is UIScreenEdgePanGestureRecognizer: + gestureDescription = EventType.kPan + #endif + default: + gestureDescription = nil + } + + guard let gestureDescription else { return } + + if let eventData = view.eventData { + PostHogAutocaptureEventTracker.eventProcessor?.process(source: .gestureRecognizer(description: gestureDescription), event: eventData) + } + } + } + + extension UIScrollView { + @objc func ph_swizzled_setContentOffset_Setter(_ newContentOffset: CGPoint) { + // first, call original method + ph_swizzled_setContentOffset_Setter(newContentOffset) + + guard shouldTrack(self) else { + return + } + + // ignore all keyboard events + if let window, window.isKeyboardWindow { + return + } + + // scrollview did not scroll (contentOffset didn't change) + guard contentOffset != newContentOffset else { + return + } + + // block scrolls on UIPickerTableView. (captured via a forwarding delegate implementation) + if String(describing: type(of: self)) == "UIPickerTableView" { + return + } + + if let eventData { + PostHogAutocaptureEventTracker.eventProcessor?.process(source: .gestureRecognizer(description: EventType.kScroll), event: eventData) + } + } + } + + extension UIPickerView { + @objc func ph_swizzled_setDelegate(_ delegate: (any UIPickerViewDelegate)?) { + guard let delegate else { + // this just removes the delegate + return ph_swizzled_setDelegate(delegate) + } + + // if delegate doesn't respond to this selector, then we can't intercept selection changes + guard delegate.responds(to: #selector(UIPickerViewDelegate.pickerView(_:didSelectRow:inComponent:))) else { + return ph_swizzled_setDelegate(delegate) + } + + // wrap in a forwarding delegate so we can intercept calls + let forwardingDelegate = ForwardingPickerViewDelegate(delegate: delegate) { [weak self] in + if let data = self?.eventData { + PostHogAutocaptureEventTracker.eventProcessor?.process(source: .gestureRecognizer(description: EventType.kValueChange), event: data) + } + } + + // Need to keep a strong reference to keep this forwarding delegate instance alive + delegate.ph_forwardingDelegate = forwardingDelegate + + // call original setter + ph_swizzled_setDelegate(forwardingDelegate) + } + } + + extension UIView { + var eventData: PostHogAutocaptureEventTracker.EventData? { + guard shouldTrack(self) else { return nil } + return PostHogAutocaptureEventTracker.EventData( + touchCoordinates: nil, + value: ph_autocaptureText + .map(sanitizeText), + screenName: nearestViewController + .flatMap(UIViewController.ph_topViewController) + .flatMap(UIViewController.getViewControllerName), + viewHierarchy: sequence(first: self, next: \.superview) + .map(\.toElement), + debounceInterval: ph_autocaptureDebounceInterval + ) + } + } + + private extension UIView { + var toElement: PostHogAutocaptureEventTracker.Element { + PostHogAutocaptureEventTracker.Element( + text: ph_autocaptureText.map(sanitizeText) ?? "", + targetClass: descriptiveTypeName, + baseClass: baseTypeName, + label: postHogLabel + ) + } + } + + extension UIControl { + func event(for action: Selector, to target: Any?) -> UIControl.Event? { + var events: [UIControl.Event] = [ + .valueChanged, + .touchDown, + .touchDownRepeat, + .touchDragInside, + .touchDragOutside, + .touchDragEnter, + .touchDragExit, + .touchUpInside, + .touchUpOutside, + .touchCancel, + .editingDidBegin, + .editingChanged, + .editingDidEnd, + .editingDidEndOnExit, + .primaryActionTriggered, + ] + + if #available(iOS 14.0, tvOS 14.0, macCatalyst 14.0, *) { + events.append(.menuActionTriggered) + } + + // latest event for action + return events.first { event in + self.actions(forTarget: target, forControlEvent: event)?.contains(action.description) ?? false + } + } + } + + extension UIControl.Event { + // swiftlint:disable:next cyclomatic_complexity + func description(forControl control: UIControl) -> String? { + if self == .primaryActionTriggered { + if control is UIButton { + return EventType.kTouch // UIButton triggers primaryAction with a touch interaction + } else if control is UISegmentedControl { + return EventType.kValueChange // UISegmentedControl changes its value + } else if control is UITextField { + return EventType.kSubmit // UITextField uses this for submit-like behavior + } else if control is UISwitch { + return EventType.kToggle + } else if control is UIDatePicker { + return EventType.kValueChange + } else if control is UIStepper { + return EventType.kValueChange + } else { + return EventType.kPrimaryAction + } + } + + // General event descriptions + if UIControl.Event.allTouchEvents.contains(self) { + return EventType.kTouch + } else if UIControl.Event.allEditingEvents.contains(self) { + return EventType.kChange + } else if self == .valueChanged { + if control is UISwitch { + // toggle better describes a value chagne in a switch control + return EventType.kToggle + } + return EventType.kValueChange + } else if #available(iOS 14.0, tvOS 14.0, macCatalyst 14.0, *), self == .menuActionTriggered { + return EventType.kMenuAction + } + + return nil + } + } + + extension UIViewController { + class func ph_topViewController(base: UIViewController? = UIApplication.getCurrentWindow()?.rootViewController) -> UIViewController? { + if let nav = base as? UINavigationController { + return ph_topViewController(base: nav.visibleViewController) + + } else if let tab = base as? UITabBarController, let selected = tab.selectedViewController { + return ph_topViewController(base: selected) + + } else if let presented = base?.presentedViewController { + return ph_topViewController(base: presented) + } + return base + } + } + + extension UIResponder { + var nearestViewController: UIViewController? { + self as? UIViewController ?? next?.nearestViewController + } + } + + private func typeName(of type: AnyClass) -> String { + let typeName = String(describing: type) + if let match = typeName.range(of: "^[^<]+", options: .regularExpression) { + // Extracts everything before the first '<' to deal with generics + return String(typeName[match]) + } + return typeName + } + + // common base types in UIKit that should not be captured + private let excludedBaseTypes: [AnyClass] = [ + NSObject.self, + UIResponder.self, + UIControl.self, + UIView.self, + UIScrollView.self, + ] + + extension NSObject { + var descriptiveTypeName: String { + typeName(of: type(of: self)) + } + + var baseTypeName: String? { + guard + let superclass = type(of: self).superclass(), + !excludedBaseTypes.contains(where: { $0 == superclass }) + else { + return nil + } + return typeName(of: superclass) + } + } + + protocol AutoCapturable { + var ph_autocaptureText: String? { get } + var ph_autocaptureEvents: UIControl.Event { get } + var ph_autocaptureDebounceInterval: TimeInterval { get } + func ph_shouldTrack(_ action: Selector, for target: Any?) -> Bool + } + + extension UIView: AutoCapturable { + @objc var ph_autocaptureEvents: UIControl.Event { .touchUpInside } + @objc var ph_autocaptureText: String? { nil } + @objc var ph_autocaptureDebounceInterval: TimeInterval { 0 } + @objc func ph_shouldTrack(_: Selector, for _: Any?) -> Bool { + false // by default views are not tracked. Can be overridden in subclasses + } + } + + extension UIButton { + override var ph_autocaptureText: String? { title(for: .normal) ?? title(for: .selected) } + } + + extension UIControl { + @objc override func ph_shouldTrack(_ action: Selector, for target: Any?) -> Bool { + guard shouldTrack(self) else { return false } + return actions(forTarget: target, forControlEvent: ph_autocaptureEvents)?.contains(action.description) ?? false + } + } + + extension UIScrollView { + override var ph_autocaptureDebounceInterval: TimeInterval { 0.4 } + } + + extension UISegmentedControl { + override var ph_autocaptureEvents: UIControl.Event { .valueChanged } + override var ph_autocaptureText: String? { + // -1 if no segment is selected + if (0 ..< numberOfSegments) ~= selectedSegmentIndex { + return titleForSegment(at: selectedSegmentIndex) + } + return nil + } + } + + extension UIPageControl { + override var ph_autocaptureEvents: UIControl.Event { .valueChanged } + } + + extension UISearchBar { + override var ph_autocaptureEvents: UIControl.Event { .editingDidEnd } + } + + extension UIToolbar { + override var ph_autocaptureEvents: UIControl.Event { + if #available(iOS 14.0, *) { .menuActionTriggered } else { .primaryActionTriggered } + } + } + + extension UITextField { + override var ph_autocaptureText: String? { text ?? attributedText?.string ?? placeholder } + override func ph_shouldTrack(_: Selector, for _: Any?) -> Bool { + // Just making sure that in the future we don't intercept UIControl.Ecent (even though it's not currently emited) + // Tracked via `UITextField.textDidEndEditingNotification` + false + } + } + + extension UITextView { + override var ph_autocaptureText: String? { text ?? attributedText?.string } + override func ph_shouldTrack(_: Selector, for _: Any?) -> Bool { + shouldTrack(self) + } + } + + extension UIStepper { + override var ph_autocaptureEvents: UIControl.Event { .valueChanged } + override var ph_autocaptureText: String? { "\(value)" } + } + + extension UISlider { + override var ph_autocaptureDebounceInterval: TimeInterval { 0.3 } + override var ph_autocaptureEvents: UIControl.Event { .valueChanged } + override var ph_autocaptureText: String? { "\(value)" } + } + + extension UISwitch { + @objc override var ph_autocaptureEvents: UIControl.Event { .valueChanged } + override var ph_autocaptureText: String? { "\(isOn)" } + } + + extension UIPickerView { + override var ph_autocaptureText: String? { + (0 ..< numberOfComponents).reduce("") { result, component in + // -1 if no row is selected + let selectedRow = selectedRow(inComponent: component) + let rowCount = numberOfRows(inComponent: component) + + if (0 ..< rowCount) ~= selectedRow { + if let title = delegate?.pickerView?(self, titleForRow: selectedRow, forComponent: component) { + return result.isEmpty ? title : "\(result) \(title)" + } else if let title = delegate?.pickerView?(self, attributedTitleForRow: selectedRow, forComponent: component) { + return result.isEmpty ? title.string : "\(result) \(title.string)" + } + } + + return result + } + } + } + + #if !os(tvOS) + extension UIDatePicker { + override var ph_autocaptureEvents: UIControl.Event { .valueChanged } + } + #endif + + private func shouldTrack(_ view: UIView) -> Bool { + if view.isHidden { return false } + if !view.isUserInteractionEnabled { return false } + if view.isNoCapture() { return false } + if view.window?.isKeyboardWindow == true { return false } + + if let textField = view as? UITextField, textField.isSensitiveText() { + return false + } + if let textView = view as? UITextView, textView.isSensitiveText() { + return false + } + + // check view hierarchy up + if let superview = view.superview { + return shouldTrack(superview) + } + + return true + } + + // TODO: Filter out or obfuscate strings that look like sensitive data + // see: https://github.com/PostHog/posthog-js/blob/0cfffcac9bdf1da3fbb9478c1a51170a325bd57f/src/autocapture-utils.ts#L389 + private func sanitizeText(_ title: String) -> String { + title + .trimmingCharacters(in: .whitespacesAndNewlines) // trim + .replacingOccurrences( // sequence of spaces, returns and line breaks + of: "[ \\r\\n]+", + with: " ", + options: .regularExpression + ) + .replacingOccurrences( // sanitize zero-width unicode characters + of: "[\\u{200B}\\u{200C}\\u{200D}\\u{FEFF}]", + with: "", + options: .regularExpression + ) + .limit(to: 255) + } + + enum EventType { + static let kValueChange = "value_changed" + static let kSubmit = "submit" + static let kToggle = "toggle" + static let kPrimaryAction = "primary_action" + static let kMenuAction = "menu_action" + static let kChange = "change" + + static let kTouch = "touch" + static let kSwipe = "swipe" + static let kPinch = "pinch" + static let kPan = "pan" + static let kScroll = "scroll" + static let kRotation = "rotation" + static let kLongPress = "long_press" + } + + // MARK: - Helpers + + private extension String { + func limit(to length: Int) -> String { + if count > length { + let index = index(startIndex, offsetBy: length) + return String(self[.. 0 { + debounceTimers[eventHash]?.invalidate() // Keep cancelling existing + debounceTimers[eventHash] = Timer.scheduledTimer(withTimeInterval: event.debounceInterval, repeats: false) { [weak self] _ in + self?.handleEventProcessing(source: source, event: event) + self?.debounceTimers.removeValue(forKey: eventHash) // Clean up once fired + } + } else { + handleEventProcessing(source: source, event: event) + } + } + + /** + Handles the processing of autocapture events by extracting event details, building properties, and sending them to PostHog. + + - Parameters: + - source: The source of the event (action method, gesture, or notification). Values are already mapped to `$event_type` earlier in the chain + - event: The event data including view hierarchy, screen name, and other metadata. + + This function extracts event details such as the event type, view hierarchy, and touch coordinates. + It creates a structured payload with relevant properties (e.g., tag_name, elements, element_chain) and sends it to the + associated PostHog instance for further processing. + */ + private func handleEventProcessing(source: PostHogAutocaptureEventTracker.EventSource, event: PostHogAutocaptureEventTracker.EventData) { + guard let postHog else { + return + } + + let eventType: String = switch source { + case let .actionMethod(description): description + case let .gestureRecognizer(description): description + case let .notification(name): name + } + + var properties: [String: Any] = [:] + + if let screenName = event.screenName { + properties["$screen_name"] = screenName + } + + let elementsChain = event.viewHierarchy + .map(\.elementsChainEntry) + .joined(separator: elementsChainDelimiter) + + if let coordinates = event.touchCoordinates { + properties["$touch_x"] = coordinates.x + properties["$touch_y"] = coordinates.y + } + + postHog.autocapture( + eventType: eventType, + elementsChain: elementsChain, + properties: properties + ) + } + } + + #if TESTING + extension PostHogAutocaptureIntegration { + static func clearInstalls() { + integrationInstalledLock.withLock { + integrationInstalled = false + } + } + } + #endif +#endif diff --git a/Pods/PostHog/PostHog/Autocapture/SwiftUI/View+PostHogLabel.swift b/Pods/PostHog/PostHog/Autocapture/SwiftUI/View+PostHogLabel.swift new file mode 100644 index 0000000..262308e --- /dev/null +++ b/Pods/PostHog/PostHog/Autocapture/SwiftUI/View+PostHogLabel.swift @@ -0,0 +1,156 @@ +// +// View+PostHogLabel.swift +// PostHog +// +// Created by Yiannis Josephides on 04/12/2024. +// + +#if os(iOS) || targetEnvironment(macCatalyst) + import SwiftUI + + public extension View { + /** + Adds a custom label to this view for use with PostHog's auto-capture functionality. + + By setting a custom label, you can easily identify and filter interactions with this specific element in your analytics data. + + ### Usage + ```swift + struct ContentView: View { + var body: some View { + Button("Login") { + ... + } + .postHogLabel("loginButton") + } + } + ``` + + - Parameter label: A custom label that uniquely identifies the element for analytics purposes. + */ + func postHogLabel(_ label: String?) -> some View { + modifier(PostHogLabelTaggerViewModifier(label: label)) + } + } + + private struct PostHogLabelTaggerViewModifier: ViewModifier { + let label: String? + + func body(content: Content) -> some View { + content + .background(viewTagger) + } + + @ViewBuilder + private var viewTagger: some View { + if let label { + PostHogLabelViewTagger(label: label) + } + } + } + + private struct PostHogLabelViewTagger: UIViewRepresentable { + let label: String + + func makeUIView(context _: Context) -> PostHogLabelTaggerView { + PostHogLabelTaggerView(label: label) + } + + func updateUIView(_: PostHogLabelTaggerView, context _: Context) { + // nothing + } + } + + private class PostHogLabelTaggerView: UIView { + private let label: String + weak var taggedView: UIView? + + init(label: String) { + self.label = label + super.init(frame: .zero) + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + label = "" + super.init(frame: .zero) + } + + override func layoutSubviews() { + super.didMoveToWindow() + + // try to find a "taggable" cousin view in hierarchy + // + // ### Why cousin view? + // + // Because of SwiftUI-to-UIKit view bridging: + // + // OriginalView (SwiftUI) + // L SwiftUITextFieldRepresentable (ViewRepresentable) + // L UITextField (UIControl) <- we tag here + // L PostHogLabelViewTagger (ViewRepresentable) + // L PostHogLabelTaggerView (UIView) <- we are here + // + if let view = findCousinView(of: PostHogSwiftUITaggable.self) { + taggedView = view + view.postHogLabel = label + } else { + // just tag grandparent view + // + // ### Why grandparent view? + // + // Because of SwiftUI-to-UIKit view bridging: + // OriginalView (SwiftUI) <- we tag here + // L PostHogLabelViewTagger (ViewRepresentable) + // L PostHogLabelTaggerView (UIView) <- we are here + // + taggedView = superview?.superview + superview?.superview?.postHogLabel = label + } + } + + override func removeFromSuperview() { + super.removeFromSuperview() + // remove custom label when removed from hierarchy + taggedView?.postHogLabel = nil + taggedView = nil + } + + private func findCousinView(of _: T.Type) -> T? { + for sibling in superview?.siblings() ?? [] { + if let match = sibling.child(of: T.self) { + return match + } + } + return nil + } + } + + // MARK: - Helpers + + private extension UIView { + func siblings() -> [UIView] { + superview?.subviews.reduce(into: []) { result, current in + if current !== self { result.append(current) } + } ?? [] + } + + func child(of type: T.Type) -> T? { + for child in subviews { + if let curT = child as? T ?? child.child(of: type) { + return curT + } + } + return nil + } + } + + protocol PostHogSwiftUITaggable: UIView { /**/ } + + extension UIControl: PostHogSwiftUITaggable { /**/ } + extension UIPickerView: PostHogSwiftUITaggable { /**/ } + extension UITextView: PostHogSwiftUITaggable { /**/ } + extension UICollectionView: PostHogSwiftUITaggable { /**/ } + extension UITableView: PostHogSwiftUITaggable { /**/ } + +#endif diff --git a/Pods/PostHog/PostHog/Autocapture/UIView+PostHogLabel.swift b/Pods/PostHog/PostHog/Autocapture/UIView+PostHogLabel.swift new file mode 100644 index 0000000..3f47d56 --- /dev/null +++ b/Pods/PostHog/PostHog/Autocapture/UIView+PostHogLabel.swift @@ -0,0 +1,29 @@ +// +// UIView+PostHogLabel.swift +// PostHog +// +// Created by Yiannis Josephides on 04/12/2024. +// + +#if os(iOS) || targetEnvironment(macCatalyst) + import UIKit + + public extension UIView { + /** + Adds a custom label to this view for use with PostHog's auto-capture functionality. + + By setting a custom label, you can easily identify and filter interactions with this specific element in your analytics data. + + ### Usage + ```swift + let myView = UIView() + myView.postHogLabel = "customLabel" + ``` + */ + var postHogLabel: String? { + get { objc_getAssociatedObject(self, &AssociatedKeys.phLabel) as? String } + set { objc_setAssociatedObject(self, &AssociatedKeys.phLabel, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } + } + } + +#endif diff --git a/Pods/PostHog/PostHog/DI.swift b/Pods/PostHog/PostHog/DI.swift new file mode 100644 index 0000000..03c4a05 --- /dev/null +++ b/Pods/PostHog/PostHog/DI.swift @@ -0,0 +1,28 @@ +// +// DI.swift +// PostHog +// +// Created by Yiannis Josephides on 17/12/2024. +// + +// swiftlint:disable:next type_name +enum DI { + static var main = Container() + + final class Container { + // publishes global app lifecycle events + lazy var appLifecyclePublisher: AppLifecyclePublishing = ApplicationLifecyclePublisher.shared + // publishes global screen view events (UIViewController.viewDidAppear) + lazy var screenViewPublisher: ScreenViewPublishing = ApplicationScreenViewPublisher.shared + + #if os(iOS) || os(tvOS) + // publishes global application events (UIApplication.sendEvent) + lazy var applicationEventPublisher: ApplicationEventPublishing = ApplicationEventPublisher.shared + #endif + + #if os(iOS) + // publishes global view layout events within a throttle interval (UIView.layoutSubviews) + lazy var viewLayoutPublisher: ViewLayoutPublishing = ApplicationViewLayoutPublisher.shared + #endif + } +} diff --git a/Pods/PostHog/PostHog/Models/PostHogEvent.swift b/Pods/PostHog/PostHog/Models/PostHogEvent.swift new file mode 100644 index 0000000..178f0f2 --- /dev/null +++ b/Pods/PostHog/PostHog/Models/PostHogEvent.swift @@ -0,0 +1,102 @@ +// +// PostHogEvent.swift +// PostHog +// +// Created by Manoel Aranda Neto on 13.10.23. +// + +import Foundation + +@objc(PostHogEvent) public class PostHogEvent: NSObject { + @objc public var event: String + @objc public var distinctId: String + @objc public var properties: [String: Any] + @objc public var timestamp: Date + @objc public private(set) var uuid: UUID + // Only used for Replay + var apiKey: String? + + init(event: String, distinctId: String, properties: [String: Any]? = nil, timestamp: Date = Date(), uuid: UUID = UUID.v7(), apiKey: String? = nil) { + self.event = event + self.distinctId = distinctId + self.properties = properties ?? [:] + self.timestamp = timestamp + self.uuid = uuid + self.apiKey = apiKey + } + + // NOTE: Ideally we would use the NSCoding behaviour but it gets needlessly complex + // given we only need this for sending to the API + static func fromJSON(_ data: Data) -> PostHogEvent? { + guard let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else { + return nil + } + + return fromJSON(json) + } + + static func fromJSON(_ json: [String: Any]) -> PostHogEvent? { + guard let event = json["event"] as? String else { return nil } + + let timestamp = json["timestamp"] as? String ?? toISO8601String(Date()) + + let timestampDate = toISO8601Date(timestamp) ?? Date() + + var properties = (json["properties"] as? [String: Any]) ?? [:] + + // back compatibility with v2 + let setProps = json["$set"] as? [String: Any] + if setProps != nil { + properties["$set"] = setProps + } + + guard let distinctId = (json["distinct_id"] as? String) ?? (properties["distinct_id"] as? String) else { return nil } + + let uuid = ((json["uuid"] as? String) ?? (json["message_id"] as? String)) ?? UUID.v7().uuidString + let uuidObj = UUID(uuidString: uuid) ?? UUID.v7() + + let apiKey = json["api_key"] as? String + + return PostHogEvent( + event: event, + distinctId: distinctId, + properties: properties, + timestamp: timestampDate, + uuid: uuidObj, + apiKey: apiKey + ) + } + + func toJSON() -> [String: Any] { + var json: [String: Any] = [ + "event": event, + "distinct_id": distinctId, + "properties": properties, + "timestamp": toISO8601String(timestamp), + "uuid": uuid.uuidString, + ] + + if let apiKey { + json["api_key"] = apiKey + } + + return json + } +} + +enum PostHogKnownUnsafeEditableEvent: String { + case snapshot = "$snapshot" + case screen = "$screen" + case set = "$set" + case surveyDismissed = "survey dismissed" + case surveySent = "survey sent" + case surveyShown = "survey shown" + case identify = "$identify" + case groupidentify = "$groupidentify" + case createAlias = "$create_alias" + case featureFlagCalled = "$feature_flag_called" + + static func contains(_ name: String) -> Bool { + PostHogKnownUnsafeEditableEvent(rawValue: name) != nil + } +} diff --git a/Pods/PostHog/PostHog/Models/Surveys/PostHogSurvey+Display.swift b/Pods/PostHog/PostHog/Models/Surveys/PostHogSurvey+Display.swift new file mode 100644 index 0000000..784bda1 --- /dev/null +++ b/Pods/PostHog/PostHog/Models/Surveys/PostHogSurvey+Display.swift @@ -0,0 +1,122 @@ +#if os(iOS) || TESTING + import Foundation + + extension PostHogSurvey { + func toDisplaySurvey() -> PostHogDisplaySurvey { + PostHogDisplaySurvey( + id: id, + name: name, + questions: questions.compactMap { $0.toDisplayQuestion() }, + appearance: appearance?.toDisplayAppearance(), + startDate: startDate, + endDate: endDate + ) + } + } + + extension PostHogSurveyQuestion { + func toDisplayQuestion() -> PostHogDisplaySurveyQuestion? { + switch self { + case let .open(question): + return PostHogDisplayOpenQuestion( + id: question.id, + question: question.question, + questionDescription: question.description, + questionDescriptionContentType: question.descriptionContentType?.toDisplayContentType(), + isOptional: question.optional ?? false, + buttonText: question.buttonText + ) + + case let .link(question): + return PostHogDisplayLinkQuestion( + id: question.id, + question: question.question, + questionDescription: question.description, + questionDescriptionContentType: question.descriptionContentType?.toDisplayContentType(), + isOptional: question.optional ?? false, + buttonText: question.buttonText, + link: question.link ?? "" + ) + + case let .rating(question): + return PostHogDisplayRatingQuestion( + id: question.id, + question: question.question, + questionDescription: question.description, + questionDescriptionContentType: question.descriptionContentType?.toDisplayContentType(), + isOptional: question.optional ?? false, + buttonText: question.buttonText, + ratingType: question.display.toDisplayRatingType(), + scaleLowerBound: question.scale.range.lowerBound, + scaleUpperBound: question.scale.range.upperBound, + lowerBoundLabel: question.lowerBoundLabel, + upperBoundLabel: question.upperBoundLabel + ) + + case let .singleChoice(question), let .multipleChoice(question): + return PostHogDisplayChoiceQuestion( + id: question.id, + question: question.question, + questionDescription: question.description, + questionDescriptionContentType: question.descriptionContentType?.toDisplayContentType(), + isOptional: question.optional ?? false, + buttonText: question.buttonText, + choices: question.choices, + hasOpenChoice: question.hasOpenChoice ?? false, + shuffleOptions: question.shuffleOptions ?? false, + isMultipleChoice: isMultipleChoice + ) + + default: + return nil + } + } + + private var isMultipleChoice: Bool { + switch self { + case .multipleChoice: return true + default: return false + } + } + } + + extension PostHogSurveyTextContentType { + func toDisplayContentType() -> PostHogDisplaySurveyTextContentType { + if case .html = self { + return .html + } + return .text + } + } + + extension PostHogSurveyRatingDisplayType { + func toDisplayRatingType() -> PostHogDisplaySurveyRatingType { + if case .emoji = self { + return .emoji + } + return .number + } + } + + extension PostHogSurveyAppearance { + func toDisplayAppearance() -> PostHogDisplaySurveyAppearance { + PostHogDisplaySurveyAppearance( + fontFamily: fontFamily, + backgroundColor: backgroundColor, + borderColor: borderColor, + submitButtonColor: submitButtonColor, + submitButtonText: submitButtonText, + submitButtonTextColor: submitButtonTextColor, + descriptionTextColor: descriptionTextColor, + ratingButtonColor: ratingButtonColor, + ratingButtonActiveColor: ratingButtonActiveColor, + placeholder: placeholder, + displayThankYouMessage: displayThankYouMessage ?? true, + thankYouMessageHeader: thankYouMessageHeader, + thankYouMessageDescription: thankYouMessageDescription, + thankYouMessageDescriptionContentType: thankYouMessageDescriptionContentType?.toDisplayContentType(), + thankYouMessageCloseButtonText: thankYouMessageCloseButtonText + ) + } + } +#endif diff --git a/Pods/PostHog/PostHog/Models/Surveys/PostHogSurvey.swift b/Pods/PostHog/PostHog/Models/Surveys/PostHogSurvey.swift new file mode 100644 index 0000000..8b045fe --- /dev/null +++ b/Pods/PostHog/PostHog/Models/Surveys/PostHogSurvey.swift @@ -0,0 +1,46 @@ +// +// PostHogSurvey.swift +// PostHog +// +// Created by Yiannis Josephides on 20/01/2025. +// + +import Foundation + +/// Represents the main survey object containing metadata, questions, conditions, and appearance settings. +/// see: posthog-js/posthog-surveys-types.ts +struct PostHogSurvey: Decodable, Identifiable { + /// The unique identifier for the survey + let id: String + /// The name of the survey + let name: String + /// Type of the survey (e.g., "popover") + let type: PostHogSurveyType + /// The questions asked in the survey + let questions: [PostHogSurveyQuestion] + /// Multiple feature flag keys. Must all (AND) evaluate to true for the survey to be shown (optional) + let featureFlagKeys: [PostHogSurveyFeatureFlagKeyValue]? + /// Linked feature flag key. Must evaluate to true for the survey to be shown (optional) + let linkedFlagKey: String? + /// Targeting feature flag key. Must evaluate to true for the survey to be shown (optional) + let targetingFlagKey: String? + /// Internal targeting flag key. Must evaluate to true for the survey to be shown (optional) + let internalTargetingFlagKey: String? + /// Conditions for displaying the survey (optional) + let conditions: PostHogSurveyConditions? + /// Appearance settings for the survey (optional) + let appearance: PostHogSurveyAppearance? + /// The iteration number for the survey (optional) + let currentIteration: Int? + /// The start date for the current iteration of the survey (optional) + let currentIterationStartDate: Date? + /// Start date of the survey (optional) + let startDate: Date? + /// End date of the survey (optional) + let endDate: Date? +} + +struct PostHogSurveyFeatureFlagKeyValue: Equatable, Decodable { + let key: String + let value: String? +} diff --git a/Pods/PostHog/PostHog/Models/Surveys/PostHogSurveyAppearance.swift b/Pods/PostHog/PostHog/Models/Surveys/PostHogSurveyAppearance.swift new file mode 100644 index 0000000..95d213c --- /dev/null +++ b/Pods/PostHog/PostHog/Models/Surveys/PostHogSurveyAppearance.swift @@ -0,0 +1,38 @@ +// +// PostHogSurveyAppearance.swift +// PostHog +// +// Created by Ioannis Josephides on 08/04/2025. +// + +import Foundation + +/// Represents the appearance settings for the survey, such as colors, fonts, and layout +struct PostHogSurveyAppearance: Decodable { + let position: PostHogSurveyAppearancePosition? + let fontFamily: String? + let backgroundColor: String? + let submitButtonColor: String? + let submitButtonText: String? + let submitButtonTextColor: String? + let descriptionTextColor: String? + let ratingButtonColor: String? + let ratingButtonActiveColor: String? + let ratingButtonHoverColor: String? + let whiteLabel: Bool? + let autoDisappear: Bool? + let displayThankYouMessage: Bool? + let thankYouMessageHeader: String? + let thankYouMessageDescription: String? + let thankYouMessageDescriptionContentType: PostHogSurveyTextContentType? + let thankYouMessageCloseButtonText: String? + let borderColor: String? + let placeholder: String? + let shuffleQuestions: Bool? + let surveyPopupDelaySeconds: TimeInterval? + // widget options + let widgetType: PostHogSurveyAppearanceWidgetType? + let widgetSelector: String? + let widgetLabel: String? + let widgetColor: String? +} diff --git a/Pods/PostHog/PostHog/Models/Surveys/PostHogSurveyConditions.swift b/Pods/PostHog/PostHog/Models/Surveys/PostHogSurveyConditions.swift new file mode 100644 index 0000000..0cf3cb1 --- /dev/null +++ b/Pods/PostHog/PostHog/Models/Surveys/PostHogSurveyConditions.swift @@ -0,0 +1,47 @@ +// +// PostHogSurveyConditions.swift +// PostHog +// +// Created by Ioannis Josephides on 08/04/2025. +// + +import Foundation + +/// Represents conditions for displaying the survey, such as URL or event-based triggers +struct PostHogSurveyConditions: Decodable { + /// Target URL for the survey (optional) + let url: String? + /// The match type for the url condition (optional) + let urlMatchType: PostHogSurveyMatchType? + /// CSS selector for displaying the survey (optional) + let selector: String? + /// Device type based conditions for displaying the survey (optional) + let deviceTypes: [String]? + /// The match type for the device type condition (optional) + let deviceTypesMatchType: PostHogSurveyMatchType? + /// Minimum wait period before showing the survey again (optional) + let seenSurveyWaitPeriodInDays: Int? + /// Event-based conditions for displaying the survey (optional) + let events: PostHogSurveyEventConditions? + /// Action-based conditions for displaying the survey (optional) + let actions: PostHogSurveyActionsConditions? +} + +/// Represents event-based conditions for displaying the survey +struct PostHogSurveyEventConditions: Decodable { + let repeatedActivation: Bool? + /// List of events that trigger the survey + let values: [PostHogEventCondition] +} + +/// Represents action-based conditions for displaying the survey +struct PostHogSurveyActionsConditions: Decodable { + /// List of events that trigger the survey + let values: [PostHogEventCondition] +} + +/// Represents a single event condition used in survey targeting +struct PostHogEventCondition: Decodable, Equatable { + /// Name of the event (e.g., "content loaded") + let name: String +} diff --git a/Pods/PostHog/PostHog/Models/Surveys/PostHogSurveyEnums.swift b/Pods/PostHog/PostHog/Models/Surveys/PostHogSurveyEnums.swift new file mode 100644 index 0000000..6615401 --- /dev/null +++ b/Pods/PostHog/PostHog/Models/Surveys/PostHogSurveyEnums.swift @@ -0,0 +1,280 @@ +// +// PostHogSurveyEnums.swift +// PostHog +// +// Created by Ioannis Josephides on 08/04/2025. +// + +import Foundation + +// MARK: - Supporting Types + +enum PostHogSurveyType: Decodable, Equatable { + case popover + case api + case widget + case unknown(type: String) + + init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + let typeString = try container.decode(String.self) + + switch typeString { + case "popover": + self = .popover + case "api": + self = .api + case "widget": + self = .widget + default: + self = .unknown(type: typeString) + } + } +} + +enum PostHogSurveyQuestionType: Decodable, Equatable { + case open + case link + case rating + case multipleChoice + case singleChoice + case unknown(type: String) + + init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + let typeString = try container.decode(String.self) + + switch typeString { + case "open": + self = .open + case "link": + self = .link + case "rating": + self = .rating + case "multiple_choice": + self = .multipleChoice + case "single_choice": + self = .singleChoice + default: + self = .unknown(type: typeString) + } + } +} + +enum PostHogSurveyTextContentType: Decodable, Equatable { + case html + case text + case unknown(type: String) + + init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + let typeString = try container.decode(String.self) + + switch typeString { + case "html": + self = .html + case "text": + self = .text + default: + self = .unknown(type: typeString) + } + } +} + +enum PostHogSurveyMatchType: Decodable, Equatable { + case regex + case notRegex + case exact + case isNot + case iContains + case notIContains + case unknown(value: String) + + init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + let valueString = try container.decode(String.self) + + switch valueString { + case "regex": + self = .regex + case "not_regex": + self = .notRegex + case "exact": + self = .exact + case "is_not": + self = .isNot + case "icontains": + self = .iContains + case "not_icontains": + self = .notIContains + default: + self = .unknown(value: valueString) + } + } +} + +enum PostHogSurveyAppearancePosition: Decodable, Equatable { + case topLeft + case topCenter + case topRight + case middleLeft + case middleCenter + case middleRight + case left + case right + case center + case unknown(position: String) + + init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + let positionString = try container.decode(String.self) + + switch positionString { + case "top_left": + self = .topLeft + case "top_center": + self = .topCenter + case "top_right": + self = .topRight + case "middle_left": + self = .middleLeft + case "middle_center": + self = .middleCenter + case "middle_right": + self = .middleRight + case "left": + self = .left + case "right": + self = .right + case "center": + self = .center + default: + self = .unknown(position: positionString) + } + } +} + +enum PostHogSurveyAppearanceWidgetType: Decodable, Equatable { + case button + case tab + case selector + case unknown(type: String) + + init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + let typeString = try container.decode(String.self) + + switch typeString { + case "button": + self = .button + case "tab": + self = .tab + case "selector": + self = .selector + default: + self = .unknown(type: typeString) + } + } +} + +enum PostHogSurveyRatingDisplayType: Decodable, Equatable { + case number + case emoji + case unknown(type: String) + + init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + let typeString = try container.decode(String.self) + + switch typeString { + case "number": + self = .number + case "emoji": + self = .emoji + default: + self = .unknown(type: typeString) + } + } +} + +enum PostHogSurveyRatingScale: Decodable, Equatable { + case threePoint + case fivePoint + case sevenPoint + case tenPoint + case unknown(scale: Int) + + var rawValue: Int { + switch self { + case .threePoint: 3 + case .fivePoint: 5 + case .sevenPoint: 7 + case .tenPoint: 10 + case let .unknown(scale): scale + } + } + + var range: ClosedRange { + switch self { + case .threePoint: 1 ... 3 + case .fivePoint: 1 ... 5 + case .sevenPoint: 1 ... 7 + case .tenPoint: 0 ... 10 + case let .unknown(scale): 1 ... scale + } + } + + init(range: ClosedRange) { + switch range { + case 1 ... 3: self = .threePoint + case 1 ... 5: self = .fivePoint + case 1 ... 7: self = .sevenPoint + case 0 ... 10: self = .tenPoint + default: self = .unknown(scale: range.upperBound) + } + } + + init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + let scaleInt = try container.decode(Int.self) + + switch scaleInt { + case 3: + self = .threePoint + case 5: + self = .fivePoint + case 7: + self = .sevenPoint + case 10: + self = .tenPoint + default: + self = .unknown(scale: scaleInt) + } + } +} + +enum PostHogSurveyQuestionBranchingType: Decodable, Equatable { + case nextQuestion + case end + case responseBased + case specificQuestion + case unknown(type: String) + + init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + let typeString = try container.decode(String.self) + + switch typeString { + case "next_question": + self = .nextQuestion + case "end": + self = .end + case "response_based": + self = .responseBased + case "specific_question": + self = .specificQuestion + default: + self = .unknown(type: typeString) + } + } +} diff --git a/Pods/PostHog/PostHog/Models/Surveys/PostHogSurveyQuestion.swift b/Pods/PostHog/PostHog/Models/Surveys/PostHogSurveyQuestion.swift new file mode 100644 index 0000000..d974b56 --- /dev/null +++ b/Pods/PostHog/PostHog/Models/Surveys/PostHogSurveyQuestion.swift @@ -0,0 +1,247 @@ +// +// PostHogSurveyQuestion.swift +// PostHog +// +// Created by Ioannis Josephides on 08/04/2025. +// + +import Foundation + +// MARK: - Question Models + +/// Protocol defining common properties for all survey question types +protocol PostHogSurveyQuestionProperties { + /// Question ID, empty if none + var id: String { get } + /// Question text + var question: String { get } + /// Additional description or instructions (optional) + var description: String? { get } + /// Content type of the description (e.g., "text", "html") (optional) + var descriptionContentType: PostHogSurveyTextContentType? { get } + /// Indicates if this question is optional (optional) + var optional: Bool? { get } + /// Text for the main CTA associated with this question (optional) + var buttonText: String? { get } + /// Original index of the question in the survey (optional) + var originalQuestionIndex: Int? { get } + /// Question branching logic if any (optional) + var branching: PostHogSurveyQuestionBranching? { get } +} + +/// Represents different types of survey questions with their associated data +enum PostHogSurveyQuestion: PostHogSurveyQuestionProperties, Decodable { + case open(PostHogOpenSurveyQuestion) + case link(PostHogLinkSurveyQuestion) + case rating(PostHogRatingSurveyQuestion) + case singleChoice(PostHogMultipleSurveyQuestion) + case multipleChoice(PostHogMultipleSurveyQuestion) + case unknown(type: String) + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let type = try container.decode(PostHogSurveyQuestionType.self, forKey: .type) + + switch type { + case .open: + self = try .open(PostHogOpenSurveyQuestion(from: decoder)) + case .link: + self = try .link(PostHogLinkSurveyQuestion(from: decoder)) + case .rating: + self = try .rating(PostHogRatingSurveyQuestion(from: decoder)) + case .singleChoice: + self = try .singleChoice(PostHogMultipleSurveyQuestion(from: decoder)) + case .multipleChoice: + self = try .multipleChoice(PostHogMultipleSurveyQuestion(from: decoder)) + case let .unknown(type): + self = .unknown(type: type) + } + } + + var id: String { + wrappedQuestion?.id ?? "" + } + + var question: String { + wrappedQuestion?.question ?? "" + } + + var description: String? { + wrappedQuestion?.description + } + + var descriptionContentType: PostHogSurveyTextContentType? { + wrappedQuestion?.descriptionContentType + } + + var optional: Bool? { + wrappedQuestion?.optional + } + + var buttonText: String? { + wrappedQuestion?.buttonText + } + + var originalQuestionIndex: Int? { + wrappedQuestion?.originalQuestionIndex + } + + var branching: PostHogSurveyQuestionBranching? { + wrappedQuestion?.branching + } + + private var wrappedQuestion: PostHogSurveyQuestionProperties? { + switch self { + case let .open(question): question + case let .link(question): question + case let .rating(question): question + case let .singleChoice(question): question + case let .multipleChoice(question): question + case .unknown: nil + } + } + + private enum CodingKeys: CodingKey { + case type + } +} + +/// Represents a basic open-ended survey question +struct PostHogOpenSurveyQuestion: PostHogSurveyQuestionProperties, Decodable { + let id: String + let question: String + let description: String? + let descriptionContentType: PostHogSurveyTextContentType? + let optional: Bool? + let buttonText: String? + let originalQuestionIndex: Int? + let branching: PostHogSurveyQuestionBranching? +} + +/// Represents a survey question with an associated link +struct PostHogLinkSurveyQuestion: PostHogSurveyQuestionProperties, Decodable { + let id: String + let question: String + let description: String? + let descriptionContentType: PostHogSurveyTextContentType? + let optional: Bool? + let buttonText: String? + let originalQuestionIndex: Int? + let branching: PostHogSurveyQuestionBranching? + /// URL link associated with the question + let link: String? +} + +/// Represents a rating-based survey question +struct PostHogRatingSurveyQuestion: PostHogSurveyQuestionProperties, Decodable { + let id: String + let question: String + let description: String? + let descriptionContentType: PostHogSurveyTextContentType? + let optional: Bool? + let buttonText: String? + let originalQuestionIndex: Int? + let branching: PostHogSurveyQuestionBranching? + /// Display type for the rating ("number" or "emoji") + let display: PostHogSurveyRatingDisplayType + /// Scale of the rating (3, 5, 7, or 10) + let scale: PostHogSurveyRatingScale + let lowerBoundLabel: String + let upperBoundLabel: String +} + +/// Represents a multiple-choice or single-choice survey question +struct PostHogMultipleSurveyQuestion: PostHogSurveyQuestionProperties, Decodable { + let id: String + let question: String + let description: String? + let descriptionContentType: PostHogSurveyTextContentType? + let optional: Bool? + let buttonText: String? + let originalQuestionIndex: Int? + let branching: PostHogSurveyQuestionBranching? + /// List of choices for multiple-choice or single-choice questions + let choices: [String] + /// Indicates if there is an open choice option (optional) + let hasOpenChoice: Bool? + /// Indicates if choices should be shuffled or not (optional) + let shuffleOptions: Bool? +} + +/// Represents branching logic for a question based on user responses +enum PostHogSurveyQuestionBranching: Decodable { + case next + case end + case responseBased(responseValues: [String: Any]) + case specificQuestion(index: Int) + case unknown(type: String) + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let type = try container.decode(PostHogSurveyQuestionBranchingType.self, forKey: .type) + + switch type { + case .nextQuestion: + self = .next + case .end: + self = .end + case .responseBased: + do { + let responseValues = try container.decode(JSON.self, forKey: .responseValues) + guard let dict = responseValues.value as? [String: Any] else { + throw DecodingError.typeMismatch( + [String: Any].self, + DecodingError.Context( + codingPath: container.codingPath, + debugDescription: "Expected responseValues to be a dictionary" + ) + ) + } + self = .responseBased(responseValues: dict) + } catch { + throw DecodingError.dataCorruptedError( + forKey: .responseValues, + in: container, + debugDescription: "responseValues is not a valid JSON object" + ) + } + case .specificQuestion: + self = try .specificQuestion(index: container.decode(Int.self, forKey: .index)) + case let .unknown(type): + self = .unknown(type: type) + } + } + + private enum CodingKeys: CodingKey { + case type, responseValues, index + } +} + +/// A helper type for decoding JSON values, which may be nested objects, arrays, strings, numbers, booleans, or nulls. +private struct JSON: Decodable { + let value: Any + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + + if container.decodeNil() { + value = NSNull() + } else if let object = try? container.decode([String: JSON].self) { + value = object.mapValues { $0.value } + } else if let array = try? container.decode([JSON].self) { + value = array.map(\.value) + } else if let string = try? container.decode(String.self) { + value = string + } else if let bool = try? container.decode(Bool.self) { + value = bool + } else if let number = try? container.decode(Double.self) { + value = NSNumber(value: number) + } else if let number = try? container.decode(Int.self) { + value = NSNumber(value: number) + } else { + throw DecodingError.dataCorruptedError( + in: container, debugDescription: "Invalid JSON value" + ) + } + } +} diff --git a/Pods/PostHog/PostHog/PostHog.h b/Pods/PostHog/PostHog/PostHog.h new file mode 100644 index 0000000..79e356a --- /dev/null +++ b/Pods/PostHog/PostHog/PostHog.h @@ -0,0 +1,60 @@ +// +// PostHog.h +// PostHog +// +// Created by Ben White on 10.01.23. +// + +#import + +//! Project version number for PostHog. +FOUNDATION_EXPORT double PostHogVersionNumber; + +//! Project version string for PostHog. +FOUNDATION_EXPORT const unsigned char PostHogVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import diff --git a/Pods/PostHog/PostHog/PostHogApi.swift b/Pods/PostHog/PostHog/PostHogApi.swift new file mode 100644 index 0000000..53bee88 --- /dev/null +++ b/Pods/PostHog/PostHog/PostHogApi.swift @@ -0,0 +1,337 @@ +// +// PostHogApi.swift +// PostHog +// +// Created by Ben White on 06.02.23. +// + +import Foundation + +class PostHogApi { + private let config: PostHogConfig + + // default is 60s but we do 10s + private let defaultTimeout: TimeInterval = 10 + + init(_ config: PostHogConfig) { + self.config = config + } + + func sessionConfig() -> URLSessionConfiguration { + let config = URLSessionConfiguration.default + + config.httpAdditionalHeaders = [ + "Content-Type": "application/json; charset=utf-8", + "User-Agent": "\(postHogSdkName)/\(postHogVersion)", + ] + + return config + } + + private func getURLRequest(_ url: URL) -> URLRequest { + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.timeoutInterval = defaultTimeout + return request + } + + private func getEndpointURL( + _ endpoint: String, + queryItems: URLQueryItem..., + relativeTo baseUrl: URL + ) -> URL? { + guard var components = URLComponents( + url: baseUrl, + resolvingAgainstBaseURL: true + ) else { + return nil + } + let path = "\(components.path)/\(endpoint)" + .replacingOccurrences(of: "/+", with: "/", options: .regularExpression) + components.path = path + components.queryItems = queryItems + return components.url + } + + private func getRemoteConfigRequest() -> URLRequest? { + guard let baseUrl: URL = switch config.host.absoluteString { + case "https://us.i.posthog.com": + URL(string: "https://us-assets.i.posthog.com") + case "https://eu.i.posthog.com": + URL(string: "https://eu-assets.i.posthog.com") + default: + config.host + } else { + return nil + } + + let url = baseUrl.appendingPathComponent("/array/\(config.apiKey)/config") + + var request = URLRequest(url: url) + request.httpMethod = "GET" + request.timeoutInterval = defaultTimeout + return request + } + + func batch(events: [PostHogEvent], completion: @escaping (PostHogBatchUploadInfo) -> Void) { + guard let url = getEndpointURL("/batch", relativeTo: config.host) else { + hedgeLog("Malformed batch URL error.") + return completion(PostHogBatchUploadInfo(statusCode: nil, error: nil)) + } + + let config = sessionConfig() + var headers = config.httpAdditionalHeaders ?? [:] + headers["Accept-Encoding"] = "gzip" + headers["Content-Encoding"] = "gzip" + config.httpAdditionalHeaders = headers + + let request = getURLRequest(url) + + let toSend: [String: Any] = [ + "api_key": self.config.apiKey, + "batch": events.map { $0.toJSON() }, + "sent_at": toISO8601String(Date()), + ] + + var data: Data? + + do { + data = try JSONSerialization.data(withJSONObject: toSend) + } catch { + hedgeLog("Error parsing the batch body: \(error)") + return completion(PostHogBatchUploadInfo(statusCode: nil, error: error)) + } + + var gzippedPayload: Data? + do { + gzippedPayload = try data!.gzipped() + } catch { + hedgeLog("Error gzipping the batch body: \(error).") + return completion(PostHogBatchUploadInfo(statusCode: nil, error: error)) + } + + URLSession(configuration: config).uploadTask(with: request, from: gzippedPayload!) { data, response, error in + if error != nil { + hedgeLog("Error calling the batch API: \(String(describing: error)).") + return completion(PostHogBatchUploadInfo(statusCode: nil, error: error)) + } + + let httpResponse = response as! HTTPURLResponse + + if !(200 ... 299 ~= httpResponse.statusCode) { + let jsonBody = String(describing: try? JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? [String: Any]) + let errorMessage = "Error sending events to batch API: status: \(jsonBody)." + hedgeLog(errorMessage) + } else { + hedgeLog("Events sent successfully.") + } + + return completion(PostHogBatchUploadInfo(statusCode: httpResponse.statusCode, error: error)) + }.resume() + } + + func snapshot(events: [PostHogEvent], completion: @escaping (PostHogBatchUploadInfo) -> Void) { + guard let url = getEndpointURL(config.snapshotEndpoint, relativeTo: config.host) else { + hedgeLog("Malformed snapshot URL error.") + return completion(PostHogBatchUploadInfo(statusCode: nil, error: nil)) + } + + for event in events { + event.apiKey = self.config.apiKey + } + + let config = sessionConfig() + var headers = config.httpAdditionalHeaders ?? [:] + headers["Accept-Encoding"] = "gzip" + headers["Content-Encoding"] = "gzip" + config.httpAdditionalHeaders = headers + + let request = getURLRequest(url) + + let toSend = events.map { $0.toJSON() } + + var data: Data? + + do { + data = try JSONSerialization.data(withJSONObject: toSend) +// remove it only for debugging +// if let newData = data { +// let convertedString = String(data: newData, encoding: .utf8) +// hedgeLog("snapshot body: \(convertedString ?? "")") +// } + } catch { + hedgeLog("Error parsing the snapshot body: \(error)") + return completion(PostHogBatchUploadInfo(statusCode: nil, error: error)) + } + + var gzippedPayload: Data? + do { + gzippedPayload = try data!.gzipped() + } catch { + hedgeLog("Error gzipping the snapshot body: \(error).") + return completion(PostHogBatchUploadInfo(statusCode: nil, error: error)) + } + + URLSession(configuration: config).uploadTask(with: request, from: gzippedPayload!) { data, response, error in + if error != nil { + hedgeLog("Error calling the snapshot API: \(String(describing: error)).") + return completion(PostHogBatchUploadInfo(statusCode: nil, error: error)) + } + + let httpResponse = response as! HTTPURLResponse + + if !(200 ... 299 ~= httpResponse.statusCode) { + let jsonBody = String(describing: try? JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? [String: Any]) + let errorMessage = "Error sending events to snapshot API: status: \(httpResponse.statusCode), body: \(jsonBody)." + hedgeLog(errorMessage) + } else { + hedgeLog("Snapshots sent successfully.") + } + + return completion(PostHogBatchUploadInfo(statusCode: httpResponse.statusCode, error: error)) + }.resume() + } + + func flags( + distinctId: String, + anonymousId: String?, + groups: [String: String], + personProperties: [String: Any], + groupProperties: [String: [String: Any]]? = nil, + completion: @escaping ([String: Any]?, _ error: Error?) -> Void + ) { + let url = getEndpointURL( + "/flags", + queryItems: URLQueryItem(name: "v", value: "2"), URLQueryItem(name: "config", value: "true"), + relativeTo: config.host + ) + + guard let url else { + hedgeLog("Malformed flags URL error.") + return completion(nil, nil) + } + + let config = sessionConfig() + + let request = getURLRequest(url) + + var toSend: [String: Any] = [ + "api_key": self.config.apiKey, + "distinct_id": distinctId, + "$groups": groups, + ] + + if let anonymousId { + toSend["$anon_distinct_id"] = anonymousId + } + + if !personProperties.isEmpty { + toSend["person_properties"] = personProperties + } + + if let groupProperties, !groupProperties.isEmpty { + toSend["group_properties"] = groupProperties + } + + if let evaluationEnvironments = self.config.evaluationEnvironments, !evaluationEnvironments.isEmpty { + toSend["evaluation_environments"] = evaluationEnvironments + } + + var data: Data? + + do { + data = try JSONSerialization.data(withJSONObject: toSend) + } catch { + hedgeLog("Error parsing the flags body: \(error)") + return completion(nil, error) + } + + URLSession(configuration: config).uploadTask(with: request, from: data!) { data, response, error in + if error != nil { + hedgeLog("Error calling the flags API: \(String(describing: error))") + return completion(nil, error) + } + + let httpResponse = response as! HTTPURLResponse + + if !(200 ... 299 ~= httpResponse.statusCode) { + let jsonBody = String(describing: try? JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? [String: Any]) + let errorMessage = "Error calling flags API: status: \(httpResponse.statusCode), body: \(jsonBody)." + hedgeLog(errorMessage) + + return completion(nil, + InternalPostHogError(description: errorMessage)) + } else { + hedgeLog("Flags called successfully.") + } + + do { + let jsonData = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? [String: Any] + completion(jsonData, nil) + } catch { + hedgeLog("Error parsing the flags response: \(error)") + completion(nil, error) + } + }.resume() + } + + func remoteConfig( + completion: @escaping ([String: Any]?, _ error: Error?) -> Void + ) { + guard let request = getRemoteConfigRequest() else { + hedgeLog("Error calling the remote config API: unable to create request") + return + } + + let config = sessionConfig() + + let task = URLSession(configuration: config).dataTask(with: request) { data, response, error in + if let error { + hedgeLog("Error calling the remote config API: \(error.localizedDescription)") + return completion(nil, error) + } + + let httpResponse = response as! HTTPURLResponse + + if !(200 ... 299 ~= httpResponse.statusCode) { + let jsonBody = String(describing: try? JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? [String: Any]) + let errorMessage = "Error calling the remote config API: status: \(httpResponse.statusCode), body: \(jsonBody)." + hedgeLog(errorMessage) + + return completion(nil, + InternalPostHogError(description: errorMessage)) + } else { + hedgeLog("Remote config called successfully.") + } + + do { + let jsonData = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? [String: Any] + completion(jsonData, nil) + } catch { + hedgeLog("Error parsing the remote config response: \(error)") + completion(nil, error) + } + } + + task.resume() + } +} + +extension PostHogApi { + static var jsonDecoder: JSONDecoder = { + let decoder = JSONDecoder() + + decoder.dateDecodingStrategy = .custom { decoder in + let container = try decoder.singleValueContainer() + let dateString = try container.decode(String.self) + guard let date = apiDateFormatter.date(from: dateString) else { + throw DecodingError.dataCorruptedError( + in: container, debugDescription: "Invalid date format" + ) + } + return date + } + decoder.keyDecodingStrategy = .convertFromSnakeCase + return decoder + }() +} diff --git a/Pods/PostHog/PostHog/PostHogBatchUploadInfo.swift b/Pods/PostHog/PostHog/PostHogBatchUploadInfo.swift new file mode 100644 index 0000000..9b4d607 --- /dev/null +++ b/Pods/PostHog/PostHog/PostHogBatchUploadInfo.swift @@ -0,0 +1,13 @@ +// +// PostHogBatchUploadInfo.swift +// PostHog +// +// Created by Manoel Aranda Neto on 13.10.23. +// + +import Foundation + +struct PostHogBatchUploadInfo { + let statusCode: Int? + let error: Error? +} diff --git a/Pods/PostHog/PostHog/PostHogConfig.swift b/Pods/PostHog/PostHog/PostHogConfig.swift new file mode 100644 index 0000000..2542efd --- /dev/null +++ b/Pods/PostHog/PostHog/PostHogConfig.swift @@ -0,0 +1,276 @@ +// +// PostHogConfig.swift +// PostHog +// +// Created by Ben White on 07.02.23. +// +import Foundation + +public typealias BeforeSendBlock = (PostHogEvent) -> PostHogEvent? + +@objc public final class BoxedBeforeSendBlock: NSObject { + @objc public let block: BeforeSendBlock + + @objc(block:) + public init(block: @escaping BeforeSendBlock) { + self.block = block + } +} + +@objc(PostHogConfig) public class PostHogConfig: NSObject { + enum Defaults { + #if os(tvOS) + static let flushAt: Int = 5 + static let maxQueueSize: Int = 100 + #else + static let flushAt: Int = 20 + static let maxQueueSize: Int = 1000 + #endif + static let maxBatchSize: Int = 50 + static let flushIntervalSeconds: TimeInterval = 30 + } + + @objc(PostHogDataMode) public enum PostHogDataMode: Int { + case wifi + case cellular + case any + } + + @objc public let host: URL + @objc public let apiKey: String + @objc public var flushAt: Int = Defaults.flushAt + @objc public var maxQueueSize: Int = Defaults.maxQueueSize + @objc public var maxBatchSize: Int = Defaults.maxBatchSize + @objc public var flushIntervalSeconds: TimeInterval = Defaults.flushIntervalSeconds + @objc public var dataMode: PostHogDataMode = .any + @objc public var sendFeatureFlagEvent: Bool = true + @objc public var preloadFeatureFlags: Bool = true + + /// Preload PostHog remote config automatically + /// Default: true + /// + /// Note: Surveys rely on remote config. Disabling this will also disable Surveys + @objc public var remoteConfig: Bool = true + + @objc public var captureApplicationLifecycleEvents: Bool = true + @objc public var captureScreenViews: Bool = true + + /// Enable method swizzling for SDK functionality that depends on it + /// + /// When disabled, functionality that require swizzling (like autocapture, screen views, session replay, surveys) will not be installed. + /// + /// Note: Disabling swizzling will limit session rotation logic to only detect application open and background events. + /// Session rotation will still work, just with reduced granularity for detecting user activity. + /// + /// Default: true + @objc public var enableSwizzling: Bool = true + + #if os(iOS) || targetEnvironment(macCatalyst) + /// Enable autocapture for iOS + /// Default: false + @objc public var captureElementInteractions: Bool = false + #endif + @objc public var debug: Bool = false + @objc public var optOut: Bool = false + @objc public var getAnonymousId: ((UUID) -> UUID) = { uuid in uuid } + + /// Flag to reuse the anonymous Id between `reset()` and next `identify()` calls + /// + /// If enabled, the anonymous Id will be reused for all anonymous users on this device, + /// essentially creating a "Guest user Id" as long as this option is enabled. + /// + /// Note: + /// Events captured *before* call to *identify()* won't be linked to the identified user + /// Events captured *after* call to *reset()* won't be linked to the identified user + /// + /// Defaults to false. + @objc public var reuseAnonymousId: Bool = false + + /// Hook that allows to sanitize the event properties + /// The hook is called before the event is cached or sent over the wire + @available(*, deprecated, message: "Use beforeSend instead") + @objc public var propertiesSanitizer: PostHogPropertiesSanitizer? + /// Determines the behavior for processing user profiles. + @objc public var personProfiles: PostHogPersonProfiles = .identifiedOnly + + /// Automatically set common device and app properties as person properties for feature flag evaluation. + /// + /// When enabled, the SDK will automatically set the following person properties: + /// - $app_version: App version from bundle + /// - $app_build: App build number from bundle + /// - $os_name: Operating system name (iOS, macOS, etc.) + /// - $os_version: Operating system version + /// - $device_type: Device type (Mobile, Tablet, Desktop, etc.) + /// - $locale: User's current locale + /// + /// This helps ensure feature flags that rely on these properties work correctly + /// without waiting for server-side processing of identify() calls. + /// + /// Default: true + @objc public var setDefaultPersonProperties: Bool = true + + /// Evaluation environments for feature flags. + /// + /// When configured, only feature flags that have at least one matching evaluation tag + /// will be evaluated. Feature flags with no evaluation tags will always be evaluated + /// for backward compatibility. + /// + /// Example usage: + /// ```swift + /// config.evaluationEnvironments = ["production", "web", "checkout"] + /// ``` + /// + /// This helps ensure feature flags are only evaluated in the appropriate environments + /// for your SDK instance. + /// + /// Default: nil (all flags are evaluated) + @objc public var evaluationEnvironments: [String]? + + /// The identifier of the App Group that should be used to store shared analytics data. + /// PostHog will try to get the physical location of the App Group’s shared container, otherwise fallback to the default location + /// Default: nil + @objc public var appGroupIdentifier: String? + + /// Internal + /// Do not modify it, this flag is read and updated by the SDK via feature flags + @objc public var snapshotEndpoint: String = "/s/" + + /// or EU Host: 'https://eu.i.posthog.com' + public static let defaultHost: String = "https://us.i.posthog.com" + + #if os(iOS) + /// Enable Recording of Session Replays for iOS + /// Default: false + @objc public var sessionReplay: Bool = false + /// Session Replay configuration + @objc public let sessionReplayConfig: PostHogSessionReplayConfig = .init() + #endif + + /// Enable mobile surveys + /// + /// Default: true + /// + /// Note: Event triggers will only work with the instance that first enables surveys. + /// In case of multiple instances, please make sure you are capturing events on the instance that has config.surveys = true + @available(iOS 15.0, *) + @available(watchOS, unavailable, message: "Surveys are only available on iOS 15+") + @available(macOS, unavailable, message: "Surveys are only available on iOS 15+") + @available(tvOS, unavailable, message: "Surveys are only available on iOS 15+") + @available(visionOS, unavailable, message: "Surveys are only available on iOS 15+") + @objc public var surveys: Bool { + get { _surveys } + set { setSurveys(newValue) } + } + + @available(iOS 15.0, *) + @available(watchOS, unavailable, message: "Surveys are only available on iOS 15+") + @available(macOS, unavailable, message: "Surveys are only available on iOS 15+") + @available(tvOS, unavailable, message: "Surveys are only available on iOS 15+") + @available(visionOS, unavailable, message: "Surveys are only available on iOS 15+") + @objc public var surveysConfig: PostHogSurveysConfig { + get { _surveysConfig } + set { setSurveysConfig(newValue) } + } + + // only internal + var disableReachabilityForTesting: Bool = false + var disableQueueTimerForTesting: Bool = false + // internal + public var storageManager: PostHogStorageManager? + + @objc(apiKey:) + public init( + apiKey: String + ) { + self.apiKey = apiKey + host = URL(string: PostHogConfig.defaultHost)! + } + + @objc(apiKey:host:) + public init( + apiKey: String, + host: String = defaultHost + ) { + self.apiKey = apiKey + self.host = URL(string: host) ?? URL(string: PostHogConfig.defaultHost)! + } + + /// Returns an array of integrations to be installed based on current configuration + func getIntegrations() -> [PostHogIntegration] { + var integrations: [PostHogIntegration] = [] + + if captureScreenViews { + integrations.append(PostHogScreenViewIntegration()) + } + + if captureApplicationLifecycleEvents { + integrations.append(PostHogAppLifeCycleIntegration()) + } + + #if os(iOS) + if sessionReplay { + integrations.append(PostHogReplayIntegration()) + } + + if _surveys { + integrations.append(PostHogSurveyIntegration()) + } + + #endif + + #if os(iOS) || targetEnvironment(macCatalyst) + if captureElementInteractions { + integrations.append(PostHogAutocaptureIntegration()) + } + #endif + + return integrations + } + + var _surveys: Bool = true // swiftlint:disable:this identifier_name + private func setSurveys(_ value: Bool) { + // protection against objc API availability warning instead of error + // Unlike swift, which enforces stricter safety rules, objc just displays a warning + if #available(iOS 15.0, *) { + _surveys = value + } + } + + var _surveysConfig: PostHogSurveysConfig = .init() // swiftlint:disable:this identifier_name + private func setSurveysConfig(_ value: PostHogSurveysConfig) { + // protection against objc API availability warning instead of error + // Unlike swift, which enforces stricter safety rules, objc just displays a warning + if #available(iOS 15.0, *) { + _surveysConfig = value + } + } + + /// Hook that allows to sanitize the event + /// The hook is called before the event is cached or sent over the wire + private var beforeSend: BeforeSendBlock = { $0 } + + private static func buildBeforeSendBlock(_ blocks: [BeforeSendBlock]) -> BeforeSendBlock { + { event in + blocks.reduce(event) { event, block in + event.flatMap(block) + } + } + } + + public func setBeforeSend(_ blocks: [BeforeSendBlock]) { + beforeSend = Self.buildBeforeSendBlock(blocks) + } + + public func setBeforeSend(_ blocks: BeforeSendBlock...) { + setBeforeSend(blocks) + } + + @available(*, unavailable, message: "Use setBeforeSend(_ blocks: BeforeSendBlock...) instead") + @objc public func setBeforeSend(_ blocks: [BoxedBeforeSendBlock]) { + setBeforeSend(blocks.map(\.block)) + } + + func runBeforeSend(_ event: PostHogEvent) -> PostHogEvent? { + beforeSend(event) + } +} diff --git a/Pods/PostHog/PostHog/PostHogConsumerPayload.swift b/Pods/PostHog/PostHog/PostHogConsumerPayload.swift new file mode 100644 index 0000000..923752f --- /dev/null +++ b/Pods/PostHog/PostHog/PostHogConsumerPayload.swift @@ -0,0 +1,13 @@ +// +// PostHogConsumerPayload.swift +// PostHog +// +// Created by Manoel Aranda Neto on 13.10.23. +// + +import Foundation + +struct PostHogConsumerPayload { + let events: [PostHogEvent] + let completion: (Bool) -> Void +} diff --git a/Pods/PostHog/PostHog/PostHogContext.swift b/Pods/PostHog/PostHog/PostHogContext.swift new file mode 100644 index 0000000..db69b84 --- /dev/null +++ b/Pods/PostHog/PostHog/PostHogContext.swift @@ -0,0 +1,411 @@ +// +// PostHogContext.swift +// PostHog +// +// Created by Manoel Aranda Neto on 16.10.23. +// + +import Foundation + +#if os(iOS) || os(tvOS) || os(visionOS) + import UIKit +#elseif os(macOS) + import AppKit +#elseif os(watchOS) + import WatchKit +#endif + +class PostHogContext { + @ReadWriteLock + private var screenSize: CGSize? + + #if !os(watchOS) + private let reachability: Reachability? + #endif + + private lazy var theStaticContext: [String: Any] = { + // Properties that do not change over the lifecycle of an application + var properties: [String: Any] = [:] + + let infoDictionary = Bundle.main.infoDictionary + + if let appName = infoDictionary?[kCFBundleNameKey as String] { + properties["$app_name"] = appName + } else if let appName = infoDictionary?["CFBundleDisplayName"] { + properties["$app_name"] = appName + } + if let appVersion = infoDictionary?["CFBundleShortVersionString"] { + properties["$app_version"] = appVersion + } + if let appBuild = infoDictionary?["CFBundleVersion"] { + properties["$app_build"] = appBuild + } + + if Bundle.main.bundleIdentifier != nil { + properties["$app_namespace"] = Bundle.main.bundleIdentifier + } + properties["$device_manufacturer"] = "Apple" + properties["$device_model"] = platform() + + if let deviceType = PostHogContext.deviceType { + properties["$device_type"] = deviceType + } + + properties["$is_emulator"] = PostHogContext.isSimulator + + let isIOSAppOnMac = PostHogContext.isIOSAppOnMac + let isMacCatalystApp = PostHogContext.isMacCatalystApp + + properties["$is_ios_running_on_mac"] = isIOSAppOnMac + properties["$is_mac_catalyst_app"] = isMacCatalystApp + + #if os(iOS) || os(tvOS) || os(visionOS) + let device = UIDevice.current + // use https://github.com/devicekit/DeviceKit + let processInfo = ProcessInfo.processInfo + + if isMacCatalystApp || isIOSAppOnMac { + let underlyingOS = device.systemName + let underlyingOSVersion = device.systemVersion + let macOSVersion = processInfo.operatingSystemVersionString + + if isMacCatalystApp { + let osVersion = ProcessInfo.processInfo.operatingSystemVersion + properties["$os_version"] = "\(osVersion.majorVersion).\(osVersion.minorVersion).\(osVersion.patchVersion)" + } else { + let osVersionString = processInfo.operatingSystemVersionString + if let versionRange = osVersionString.range(of: #"\d+\.\d+\.\d+"#, options: .regularExpression) { + properties["$os_version"] = osVersionString[versionRange] + } else { + // fallback to full version string in case formatting changes + properties["$os_version"] = osVersionString + } + } + // device.userInterfaceIdiom reports .pad here, so we use a static value instead + // - For an app deployable on iPad, the idiom type is always .pad (instead of .mac) + // + // Source: https://developer.apple.com/documentation/apple-silicon/adapting-ios-code-to-run-in-the-macos-environment#Handle-unknown-device-types-gracefully + properties["$os_name"] = "macOS" + properties["$device_name"] = processInfo.hostName + } else { + // use https://github.com/devicekit/DeviceKit + properties["$os_name"] = device.systemName + properties["$os_version"] = device.systemVersion + properties["$device_name"] = device.model + } + #elseif os(macOS) + let deviceName = Host.current().localizedName + if (deviceName?.isEmpty) != nil { + properties["$device_name"] = deviceName + } + let processInfo = ProcessInfo.processInfo + properties["$os_name"] = "macOS" + let osVersion = processInfo.operatingSystemVersion + properties["$os_version"] = "\(osVersion.majorVersion).\(osVersion.minorVersion).\(osVersion.patchVersion)" + #endif + + return properties + }() + + #if !os(watchOS) + init(_ reachability: Reachability?) { + self.reachability = reachability + registerNotifications() + } + #else + init() { + if #available(watchOS 7.0, *) { + registerNotifications() + } else { + onShouldUpdateScreenSize() + } + } + #endif + + deinit { + #if !os(watchOS) + unregisterNotifications() + #else + if #available(watchOS 7.0, *) { + unregisterNotifications() + } + #endif + } + + private lazy var theSdkInfo: [String: Any] = { + var sdkInfo: [String: Any] = [:] + sdkInfo["$lib"] = postHogSdkName + sdkInfo["$lib_version"] = postHogVersion + return sdkInfo + }() + + func staticContext() -> [String: Any] { + theStaticContext + } + + func sdkInfo() -> [String: Any] { + theSdkInfo + } + + private func platform() -> String { + var sysctlName = "hw.machine" + + // In case of mac catalyst or iOS running on mac: + // - "hw.machine" returns underlying iPad/iPhone model + // - "hw.model" returns mac model + #if targetEnvironment(macCatalyst) + sysctlName = "hw.model" + #elseif os(iOS) || os(visionOS) + if #available(iOS 14.0, *) { + if ProcessInfo.processInfo.isiOSAppOnMac { + sysctlName = "hw.model" + } + } + #endif + + var size = 0 + sysctlbyname(sysctlName, nil, &size, nil, 0) + var machine = [CChar](repeating: 0, count: size) + sysctlbyname(sysctlName, &machine, &size, nil, 0) + return String(cString: machine) + } + + func dynamicContext() -> [String: Any] { + var properties: [String: Any] = [:] + + if let screenSize { + properties["$screen_width"] = Float(screenSize.width) + properties["$screen_height"] = Float(screenSize.height) + } + + if #available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) { + if let languageCode = Locale.current.language.languageCode { + properties["$locale"] = languageCode.identifier + } + } else { + if Locale.current.languageCode != nil { + properties["$locale"] = Locale.current.languageCode + } + } + properties["$timezone"] = TimeZone.current.identifier + + #if !os(watchOS) + if reachability != nil { + properties["$network_wifi"] = reachability?.connection == .wifi + properties["$network_cellular"] = reachability?.connection == .cellular + } + #endif + + return properties + } + + /// Returns person properties context by extracting relevant properties from static context. + /// This centralizes the logic for determining which properties should be used as person properties. + func personPropertiesContext() -> [String: Any] { + let staticCtx = staticContext() + var personProperties: [String: Any] = [:] + + // App information + if let appVersion = staticCtx["$app_version"] { + personProperties["$app_version"] = appVersion + } + if let appBuild = staticCtx["$app_build"] { + personProperties["$app_build"] = appBuild + } + + // Operating system information + if let osName = staticCtx["$os_name"] { + personProperties["$os_name"] = osName + } + if let osVersion = staticCtx["$os_version"] { + personProperties["$os_version"] = osVersion + } + + // Device information + if let deviceType = staticCtx["$device_type"] { + personProperties["$device_type"] = deviceType + } + if let deviceManufacturer = staticCtx["$device_manufacturer"] { + personProperties["$device_manufacturer"] = deviceManufacturer + } + if let deviceModel = staticCtx["$device_model"] { + personProperties["$device_model"] = deviceModel + } + + // Localization - read directly to avoid expensive dynamicContext call + if #available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) { + if let languageCode = Locale.current.language.languageCode { + personProperties["$locale"] = languageCode.identifier + } + } else { + if let languageCode = Locale.current.languageCode { + personProperties["$locale"] = languageCode + } + } + + return personProperties + } + + private func registerNotifications() { + #if os(iOS) || os(tvOS) || os(visionOS) + #if os(iOS) + NotificationCenter.default.addObserver(self, + selector: #selector(onOrientationDidChange), + name: UIDevice.orientationDidChangeNotification, + object: nil) + #endif + NotificationCenter.default.addObserver(self, + selector: #selector(onShouldUpdateScreenSize), + name: UIWindow.didBecomeKeyNotification, + object: nil) + #elseif os(macOS) + NotificationCenter.default.addObserver(self, + selector: #selector(onShouldUpdateScreenSize), + name: NSWindow.didBecomeKeyNotification, + object: nil) + NotificationCenter.default.addObserver(self, + selector: #selector(onShouldUpdateScreenSize), + name: NSWindow.didChangeScreenNotification, + object: nil) + NotificationCenter.default.addObserver(self, + selector: #selector(onShouldUpdateScreenSize), + name: NSApplication.didBecomeActiveNotification, + object: nil) + #elseif os(watchOS) + if #available(watchOS 7.0, *) { + NotificationCenter.default.addObserver(self, + selector: #selector(onShouldUpdateScreenSize), + name: WKApplication.didBecomeActiveNotification, + object: nil) + } + #endif + } + + private func unregisterNotifications() { + #if os(iOS) || os(tvOS) || os(visionOS) + #if os(iOS) + NotificationCenter.default.removeObserver(self, + name: UIDevice.orientationDidChangeNotification, + object: nil) + #endif + NotificationCenter.default.removeObserver(self, + name: UIWindow.didBecomeKeyNotification, + object: nil) + + #elseif os(macOS) + NotificationCenter.default.removeObserver(self, + name: NSWindow.didBecomeKeyNotification, + object: nil) + NotificationCenter.default.removeObserver(self, + name: NSWindow.didChangeScreenNotification, + object: nil) + NotificationCenter.default.removeObserver(self, + name: NSApplication.didBecomeActiveNotification, + object: nil) + #elseif os(watchOS) + if #available(watchOS 7.0, *) { + NotificationCenter.default.removeObserver(self, + name: WKApplication.didBecomeActiveNotification, + object: nil) + } + #endif + } + + /// Retrieves the current screen size of the application window based on platform + private func getScreenSize() -> CGSize? { + #if os(iOS) || os(tvOS) || os(visionOS) + return UIApplication.getCurrentWindow(filterForegrounded: false)?.bounds.size + #elseif os(macOS) + // NSScreen.frame represents the full screen rectangle and includes any space occupied by menu, dock or camera bezel + return NSApplication.shared.windows.first { $0.isKeyWindow }?.screen?.frame.size + #elseif os(watchOS) + return WKInterfaceDevice.current().screenBounds.size + #else + return nil + #endif + } + + #if os(iOS) + // Special treatment for `orientationDidChangeNotification` since the notification seems to be _sometimes_ called early, before screen bounds are flipped + @objc private func onOrientationDidChange() { + updateScreenSize { + self.getScreenSize().map { size in + // manually set width and height based on device orientation. (Needed for fast orientation changes) + if UIDevice.current.orientation.isLandscape { + CGSize(width: max(size.width, size.height), height: min(size.height, size.width)) + } else { + CGSize(width: min(size.width, size.height), height: max(size.height, size.width)) + } + } + } + } + #endif + + @objc private func onShouldUpdateScreenSize() { + updateScreenSize(getScreenSize) + } + + private func updateScreenSize(_ getSize: @escaping () -> CGSize?) { + let block = { + self.screenSize = getSize() + } + // ensure block is executed on `main` since closure accesses non thread-safe UI objects like UIApplication + if Thread.isMainThread { + block() + } else { + DispatchQueue.main.async(execute: block) + } + } + + static let deviceType: String? = { + #if os(iOS) || os(tvOS) + if isMacCatalystApp || isIOSAppOnMac { + return "Desktop" + } else { + switch UIDevice.current.userInterfaceIdiom { + case UIUserInterfaceIdiom.phone: + return "Mobile" + case UIUserInterfaceIdiom.pad: + return "Tablet" + case UIUserInterfaceIdiom.tv: + return "TV" + case UIUserInterfaceIdiom.carPlay: + return "CarPlay" + case UIUserInterfaceIdiom.mac: + return "Desktop" + case UIUserInterfaceIdiom.vision: + return "Vision" + default: + return nil + } + } + #elseif os(macOS) + return "Desktop" + #else + return nil + #endif + }() + + static let isIOSAppOnMac: Bool = { + if #available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) { + return ProcessInfo.processInfo.isiOSAppOnMac + } + return false + }() + + static let isMacCatalystApp: Bool = { + #if targetEnvironment(macCatalyst) + true + #else + false + #endif + }() + + static let isSimulator: Bool = { + #if targetEnvironment(simulator) + true + #else + false + #endif + }() +} diff --git a/Pods/PostHog/PostHog/PostHogExtensions.swift b/Pods/PostHog/PostHog/PostHogExtensions.swift new file mode 100644 index 0000000..4f701e6 --- /dev/null +++ b/Pods/PostHog/PostHog/PostHogExtensions.swift @@ -0,0 +1,20 @@ +// +// PostHogExtensions.swift +// PostHog +// +// Created by Manoel Aranda Neto on 13.10.23. +// + +import Foundation + +/** + # Notifications + + This helper module encapsulates all notifications that we trigger from within the SDK. + + */ + +public extension PostHogSDK { + @objc static let didStartNotification = Notification.Name("PostHogDidStart") // object: nil + @objc static let didReceiveFeatureFlags = Notification.Name("PostHogDidReceiveFeatureFlags") // object: nil +} diff --git a/Pods/PostHog/PostHog/PostHogFileBackedQueue.swift b/Pods/PostHog/PostHog/PostHogFileBackedQueue.swift new file mode 100644 index 0000000..4b6edfb --- /dev/null +++ b/Pods/PostHog/PostHog/PostHogFileBackedQueue.swift @@ -0,0 +1,114 @@ +// +// PostHogFileBackedQueue.swift +// PostHog +// +// Created by Manoel Aranda Neto on 13.10.23. +// + +import Foundation + +class PostHogFileBackedQueue { + let queue: URL + @ReadWriteLock + private var items = [String]() + + var depth: Int { + items.count + } + + init(queue: URL, oldQueue: URL? = nil) { + self.queue = queue + setup(oldQueue: oldQueue) + } + + private func setup(oldQueue: URL?) { + do { + try FileManager.default.createDirectory(atPath: queue.path, withIntermediateDirectories: true) + } catch { + hedgeLog("Error trying to create caching folder \(error)") + } + + if oldQueue != nil { + migrateOldQueue(queue: queue, oldQueue: oldQueue!) + } + + do { + items = try FileManager.default.contentsOfDirectory(atPath: queue.path) + items.sort { Double($0)! < Double($1)! } + } catch { + hedgeLog("Failed to load files for queue \(error)") + // failed to read directory – bad permissions, perhaps? + } + } + + func peek(_ count: Int) -> [Data] { + loadFiles(count) + } + + func delete(index: Int) { + if items.isEmpty { return } + let removed = items.remove(at: index) + + deleteSafely(queue.appendingPathComponent(removed)) + } + + func pop(_ count: Int) { + deleteFiles(count) + } + + func add(_ contents: Data) { + do { + let filename = "\(Date().timeIntervalSince1970)" + try contents.write(to: queue.appendingPathComponent(filename)) + items.append(filename) + } catch { + hedgeLog("Could not write file \(error)") + } + } + + /// Internal, used for testing + func clear() { + deleteSafely(queue) + setup(oldQueue: nil) + } + + private func loadFiles(_ count: Int) -> [Data] { + var results = [Data]() + + for item in items { + let itemURL = queue.appendingPathComponent(item) + do { + if !FileManager.default.fileExists(atPath: itemURL.path) { + hedgeLog("File \(itemURL) does not exist") + continue + } + let contents = try Data(contentsOf: itemURL) + + results.append(contents) + } catch { + hedgeLog("File \(itemURL) is corrupted \(error)") + + deleteSafely(itemURL) + } + + if results.count == count { + return results + } + } + + return results + } + + private func deleteFiles(_ count: Int) { + for _ in 0 ..< count { + if let removed: String = _items.mutate({ items in + if items.isEmpty { + return nil + } + return items.remove(at: 0) // We always remove from the top of the queue + }) { + deleteSafely(queue.appendingPathComponent(removed)) + } + } + } +} diff --git a/Pods/PostHog/PostHog/PostHogIntegration.swift b/Pods/PostHog/PostHog/PostHogIntegration.swift new file mode 100644 index 0000000..85c4ae4 --- /dev/null +++ b/Pods/PostHog/PostHog/PostHogIntegration.swift @@ -0,0 +1,59 @@ +// +// PostHogIntegration.swift +// PostHog +// +// Created by Ioannis Josephides on 25/02/2025. +// +import Foundation + +protocol PostHogIntegration { + /** + * Indicates whether this integration requires method swizzling to function. + * + * When `enableSwizzling` is set to `false` in PostHogConfig, integrations + * that return `true` for this property will be skipped during installation. + */ + var requiresSwizzling: Bool { get } + + /** + * Installs and initializes the integration with a PostHogSDK instance. + * + * This method should: + * 1. Run checks if needed to ensure that the integration is only installed once + * 2. Initialize any required resources + * 3. Start the integration's functionality + * + * - Parameter postHog: The PostHogSDK instance to integrate with + * - Throws: InternalPostHogError if installation fails (e.g., already installed) + */ + func install(_ postHog: PostHogSDK) throws + + /** + * Uninstalls the integration from a specific PostHogSDK instance. + * + * This method should: + * 1. Stop all integration functionality + * 2. Clean up any resources + * 3. Remove references to the PostHog instance + * + * - Parameter postHog: The PostHog SDK instance to uninstall from + */ + func uninstall(_ postHog: PostHogSDK) + + /** + * Starts the integration's functionality. + * + * Note: This is typically called automatically during installation + * but may be called manually to restart a stopped integration. + */ + func start() + + /** + * Stops the integration's functionality without uninstalling. + * + * Note: This is typically called automatically during uninstallation + * but may be called manually to temporarily suspend the integration + * while maintaining its installation status (e.g manual start/stop for session recording) + */ + func stop() +} diff --git a/Pods/PostHog/PostHog/PostHogLegacyQueue.swift b/Pods/PostHog/PostHog/PostHogLegacyQueue.swift new file mode 100644 index 0000000..f0938c7 --- /dev/null +++ b/Pods/PostHog/PostHog/PostHogLegacyQueue.swift @@ -0,0 +1,45 @@ +// +// PostHogLegacyQueue.swift +// PostHog +// +// Created by Manoel Aranda Neto on 30.10.23. +// + +import Foundation + +// Migrates the Old Queue (v2) to the new Queue (v3) +func migrateOldQueue(queue: URL, oldQueue: URL) { + if !FileManager.default.fileExists(atPath: oldQueue.path) { + return + } + + defer { + deleteSafely(oldQueue) + } + + do { + let data = try Data(contentsOf: oldQueue) + let array = try JSONSerialization.jsonObject(with: data) as? [Any] + + if array == nil { + return + } + + for item in array! { + guard let event = item as? [String: Any] else { + continue + } + let timestamp = event["timestamp"] as? String ?? toISO8601String(Date()) + + let timestampDate = toISO8601Date(timestamp) ?? Date() + + let filename = "\(timestampDate.timeIntervalSince1970)" + + let contents = try JSONSerialization.data(withJSONObject: event) + + try contents.write(to: queue.appendingPathComponent(filename)) + } + } catch { + hedgeLog("Failed to migrate queue \(error)") + } +} diff --git a/Pods/PostHog/PostHog/PostHogPersonProfiles.swift b/Pods/PostHog/PostHog/PostHogPersonProfiles.swift new file mode 100644 index 0000000..ddcaa52 --- /dev/null +++ b/Pods/PostHog/PostHog/PostHogPersonProfiles.swift @@ -0,0 +1,20 @@ +// +// PostHogPersonProfiles.swift +// PostHog +// +// Created by Manoel Aranda Neto on 09.09.24. +// + +import Foundation + +/// Determines the behavior for processing user profiles. +/// - `never`: We won't process persons for any event. This means that anonymous users will not be merged once +/// they sign up or login, so you lose the ability to create funnels that track users from anonymous to identified. +/// All events (including `$identify`) will be sent with `$process_person_profile: False`. +/// - `always`: We will process persons data for all events. +/// - `identifiedOnly`: (default): we will only process persons when you call `identify`, `alias`, and `group`, Anonymous users won't get person profiles. +@objc(PostHogPersonProfiles) public enum PostHogPersonProfiles: Int { + case never + case always + case identifiedOnly +} diff --git a/Pods/PostHog/PostHog/PostHogPropertiesSanitizer.swift b/Pods/PostHog/PostHog/PostHogPropertiesSanitizer.swift new file mode 100644 index 0000000..789f197 --- /dev/null +++ b/Pods/PostHog/PostHog/PostHogPropertiesSanitizer.swift @@ -0,0 +1,34 @@ +// +// PostHogPropertiesSanitizer.swift +// PostHog +// +// Created by Manoel Aranda Neto on 06.08.24. +// + +import Foundation + +/// Protocol to sanitize the event properties +@objc(PostHogPropertiesSanitizer) public protocol PostHogPropertiesSanitizer { + /// Sanitizes the event properties + /// - Parameter properties: the event properties to sanitize + /// - Returns: the sanitized properties + /// + /// Obs: `inout` cannot be used in Swift protocols, so you need to clone the properties + /// + /// ```swift + /// private class ExampleSanitizer: PostHogPropertiesSanitizer { + /// public func sanitize(_ properties: [String: Any]) -> [String: Any] { + /// var sanitizedProperties = properties + /// // Perform sanitization + /// // For example, removing keys with empty values + /// for (key, value) in properties { + /// if let stringValue = value as? String, stringValue.isEmpty { + /// sanitizedProperties.removeValue(forKey: key) + /// } + /// } + /// return sanitizedProperties + /// } + /// } + /// ``` + @objc func sanitize(_ properties: [String: Any]) -> [String: Any] +} diff --git a/Pods/PostHog/PostHog/PostHogQueue.swift b/Pods/PostHog/PostHog/PostHogQueue.swift new file mode 100644 index 0000000..bd3fd3b --- /dev/null +++ b/Pods/PostHog/PostHog/PostHogQueue.swift @@ -0,0 +1,285 @@ +// +// PostHogQueue.swift +// PostHog +// +// Created by Ben White on 06.02.23. +// + +import Foundation + +/** + # Queue + + The queue uses File persistence. This allows us to + 1. Only send events when we have a network connection + 2. Ensure that we can survive app closing or offline situations + 3. Not hold too much in memory + + */ + +class PostHogQueue { + enum PostHogApiEndpoint: Int { + case batch + case snapshot + } + + private let config: PostHogConfig + private let api: PostHogApi + private var paused: Bool = false + private let pausedLock = NSLock() + private var pausedUntil: Date? + private var retryCount: TimeInterval = 0 + #if !os(watchOS) + private let reachability: Reachability? + #endif + + private var isFlushing = false + private let isFlushingLock = NSLock() + private var timer: Timer? + private let timerLock = NSLock() + private let endpoint: PostHogApiEndpoint + private let dispatchQueue: DispatchQueue + + /// Internal, used for testing + var depth: Int { + fileQueue.depth + } + + private let fileQueue: PostHogFileBackedQueue + + #if !os(watchOS) + init(_ config: PostHogConfig, _ storage: PostHogStorage, _ api: PostHogApi, _ endpoint: PostHogApiEndpoint, _ reachability: Reachability?) { + self.config = config + self.api = api + self.reachability = reachability + self.endpoint = endpoint + + switch endpoint { + case .batch: + fileQueue = PostHogFileBackedQueue(queue: storage.url(forKey: .queue), oldQueue: storage.url(forKey: .oldQeueue)) + dispatchQueue = DispatchQueue(label: "com.posthog.Queue", target: .global(qos: .utility)) + case .snapshot: + fileQueue = PostHogFileBackedQueue(queue: storage.url(forKey: .replayQeueue)) + dispatchQueue = DispatchQueue(label: "com.posthog.ReplayQueue", target: .global(qos: .utility)) + } + } + #else + init(_ config: PostHogConfig, _ storage: PostHogStorage, _ api: PostHogApi, _ endpoint: PostHogApiEndpoint) { + self.config = config + self.api = api + self.endpoint = endpoint + + switch endpoint { + case .batch: + fileQueue = PostHogFileBackedQueue(queue: storage.url(forKey: .queue), oldQueue: storage.url(forKey: .oldQeueue)) + dispatchQueue = DispatchQueue(label: "com.posthog.Queue", target: .global(qos: .utility)) + case .snapshot: + fileQueue = PostHogFileBackedQueue(queue: storage.url(forKey: .replayQeueue)) + dispatchQueue = DispatchQueue(label: "com.posthog.ReplayQueue", target: .global(qos: .utility)) + } + } + #endif + + private func eventHandler(_ payload: PostHogConsumerPayload) { + hedgeLog("Sending batch of \(payload.events.count) events to PostHog") + + switch endpoint { + case .batch: + api.batch(events: payload.events) { result in + self.handleResult(result, payload) + } + case .snapshot: + api.snapshot(events: payload.events) { result in + self.handleResult(result, payload) + } + } + } + + private func handleResult(_ result: PostHogBatchUploadInfo, _ payload: PostHogConsumerPayload) { + // -1 means its not anything related to the API but rather network or something else, so we try again + let statusCode = result.statusCode ?? -1 + + var shouldRetry = false + if 300 ... 399 ~= statusCode || statusCode == -1 { + shouldRetry = true + } + + // TODO: https://github.com/PostHog/posthog-android/pull/130 + // fix: reduce batch size if API returns 413 + + if shouldRetry { + retryCount += 1 + let delay = min(retryCount * retryDelay, maxRetryDelay) + pauseFor(seconds: delay) + hedgeLog("Pausing queue consumption for \(delay) seconds due to \(retryCount) API failure(s).") + } else { + retryCount = 0 + } + + payload.completion(!shouldRetry) + } + + func start(disableReachabilityForTesting: Bool, + disableQueueTimerForTesting: Bool) + { + if !disableReachabilityForTesting { + // Setup the monitoring of network status for the queue + #if !os(watchOS) + reachability?.whenReachable = { reachability in + self.pausedLock.withLock { + if self.config.dataMode == .wifi, reachability.connection != .wifi { + hedgeLog("Queue is paused because its not in WiFi mode") + self.paused = true + } else { + self.paused = false + } + } + + // Always trigger a flush when we are on wifi + if reachability.connection == .wifi { + if !self.isFlushing { + self.flush() + } + } + } + + reachability?.whenUnreachable = { _ in + self.pausedLock.withLock { + hedgeLog("Queue is paused because network is unreachable") + self.paused = true + } + } + + do { + try reachability?.startNotifier() + } catch { + hedgeLog("Error: Unable to monitor network reachability: \(error)") + } + #endif + } + + if !disableQueueTimerForTesting { + timerLock.withLock { + DispatchQueue.main.async { + self.timer = Timer.scheduledTimer(withTimeInterval: self.config.flushIntervalSeconds, repeats: true, block: { _ in + if !self.isFlushing { + self.flush() + } + }) + } + } + } + } + + /// Internal, used for testing + func clear() { + fileQueue.clear() + } + + func stop() { + timerLock.withLock { + timer?.invalidate() + timer = nil + } + } + + func flush() { + if !canFlush() { + return + } + + take(config.maxBatchSize) { payload in + if !payload.events.isEmpty { + self.eventHandler(payload) + } else { + // there's nothing to be sent + payload.completion(true) + } + } + } + + private func flushIfOverThreshold() { + if fileQueue.depth >= config.flushAt { + flush() + } + } + + func add(_ event: PostHogEvent) { + if fileQueue.depth >= config.maxQueueSize { + hedgeLog("Queue is full, dropping oldest event") + // first is always oldest + fileQueue.delete(index: 0) + } + + var data: Data? + do { + data = try JSONSerialization.data(withJSONObject: event.toJSON()) + } catch { + hedgeLog("Tried to queue unserialisable PostHogEvent \(error)") + return + } + + fileQueue.add(data!) + hedgeLog("Queued event '\(event.event)'. Depth: \(fileQueue.depth)") + flushIfOverThreshold() + } + + private func take(_ count: Int, completion: @escaping (PostHogConsumerPayload) -> Void) { + dispatchQueue.async { + self.isFlushingLock.withLock { + if self.isFlushing { + return + } + self.isFlushing = true + } + + let items = self.fileQueue.peek(count) + + var processing = [PostHogEvent]() + + for item in items { + // each element is a PostHogEvent if fromJSON succeeds + guard let event = PostHogEvent.fromJSON(item) else { + continue + } + processing.append(event) + } + + completion(PostHogConsumerPayload(events: processing) { success in + if success, items.count > 0 { + self.fileQueue.pop(items.count) + hedgeLog("Completed!") + } + + self.isFlushingLock.withLock { + self.isFlushing = false + } + }) + } + } + + private func pauseFor(seconds: TimeInterval) { + pausedUntil = Date().addingTimeInterval(seconds) + } + + private func canFlush() -> Bool { + if isFlushing { + hedgeLog("Already flushing") + return false + } + + if paused { + // We don't flush data if the queue is paused + hedgeLog("The queue is paused due to the reachability check") + return false + } + + if pausedUntil != nil, pausedUntil! > Date() { + // We don't flush data if the queue is temporarily paused + hedgeLog("The queue is paused until `\(pausedUntil!)`") + return false + } + + return true + } +} diff --git a/Pods/PostHog/PostHog/PostHogRemoteConfig.swift b/Pods/PostHog/PostHog/PostHogRemoteConfig.swift new file mode 100644 index 0000000..ae346a1 --- /dev/null +++ b/Pods/PostHog/PostHog/PostHogRemoteConfig.swift @@ -0,0 +1,636 @@ +// +// PostHogRemoteConfig.swift +// PostHog +// +// Created by Manoel Aranda Neto on 10.10.23. +// + +import Foundation + +class PostHogRemoteConfig { + private let hasFeatureFlagsKey = "hasFeatureFlags" + + private let config: PostHogConfig + private let storage: PostHogStorage + private let api: PostHogApi + private let getDefaultPersonProperties: () -> [String: Any] + + private let loadingFeatureFlagsLock = NSLock() + private let featureFlagsLock = NSLock() + private var loadingFeatureFlags = false + private var sessionReplayFlagActive = false + + private var flags: [String: Any]? + private var featureFlags: [String: Any]? + + private var remoteConfigLock = NSLock() + private let loadingRemoteConfigLock = NSLock() + private var loadingRemoteConfig = false + private var remoteConfig: [String: Any]? + private var remoteConfigDidFetch: Bool = false + private var featureFlagPayloads: [String: Any]? + private var requestId: String? + + private let personPropertiesForFlagsLock = NSLock() + private var personPropertiesForFlags: [String: Any] = [:] + + private let groupPropertiesForFlagsLock = NSLock() + private var groupPropertiesForFlags: [String: [String: Any]] = [:] + + /// Internal, only used for testing + var canReloadFlagsForTesting = true + + var onRemoteConfigLoaded: (([String: Any]?) -> Void)? + var onFeatureFlagsLoaded: (([String: Any]?) -> Void)? + + private let dispatchQueue = DispatchQueue(label: "com.posthog.RemoteConfig", + target: .global(qos: .utility)) + + var lastRequestId: String? { + featureFlagsLock.withLock { + requestId ?? storage.getString(forKey: .requestId) + } + } + + init(_ config: PostHogConfig, + _ storage: PostHogStorage, + _ api: PostHogApi, + _ getDefaultPersonProperties: @escaping () -> [String: Any]) + { + self.config = config + self.storage = storage + self.api = api + self.getDefaultPersonProperties = getDefaultPersonProperties + + // Load cached person and group properties for flags + loadCachedPropertiesForFlags() + + preloadSessionReplayFlag() + + if config.remoteConfig { + preloadRemoteConfig() + } else if config.preloadFeatureFlags { + preloadFeatureFlags() + } + } + + private func preloadRemoteConfig() { + remoteConfigLock.withLock { + // load disk cached config to memory + _ = getCachedRemoteConfig() + } + + // may have already beed fetched from `loadFeatureFlags` call + if remoteConfigLock.withLock({ + self.remoteConfig == nil || !self.remoteConfigDidFetch + }) { + dispatchQueue.async { + self.reloadRemoteConfig { [weak self] remoteConfig in + guard let self else { return } + + // if there's no remote config response, skip + guard let remoteConfig else { + hedgeLog("Remote config response is missing, skipping loading flags") + notifyFeatureFlags(nil) + return + } + + // Check if the server explicitly responded with hasFeatureFlags key + if let hasFeatureFlagsBoolValue = remoteConfig[self.hasFeatureFlagsKey] as? Bool, !hasFeatureFlagsBoolValue { + hedgeLog("hasFeatureFlags is false, clearing flags and skipping loading flags") + // Server responded with explicit hasFeatureFlags: false, meaning no active flags on the account + clearFeatureFlags() + // need to notify cause people may be waiting for flags to load + notifyFeatureFlags([:]) + } else if self.config.preloadFeatureFlags { + // If we reach here, hasFeatureFlags is either true, nil or not a boolean value + // Note: notifyFeatureFlags() will be eventually called inside preloadFeatureFlags() + self.preloadFeatureFlags() + } + } + } + } + } + + private func preloadFeatureFlags() { + featureFlagsLock.withLock { + // load disk cached config to memory + _ = getCachedFeatureFlags() + } + + if config.preloadFeatureFlags { + dispatchQueue.async { + self.reloadFeatureFlags() + } + } + } + + func reloadRemoteConfig( + callback: (([String: Any]?) -> Void)? = nil + ) { + guard config.remoteConfig else { + callback?(nil) + return + } + + loadingRemoteConfigLock.withLock { + if self.loadingRemoteConfig { + return + } + self.loadingRemoteConfig = true + } + + api.remoteConfig { config, _ in + if let config { + // cache config + self.remoteConfigLock.withLock { + self.remoteConfig = config + self.storage.setDictionary(forKey: .remoteConfig, contents: config) + } + + // process session replay config + #if os(iOS) + let featureFlags = self.featureFlagsLock.withLock { self.featureFlags } + self.processSessionRecordingConfig(config, featureFlags: featureFlags ?? [:]) + #endif + + // notify + DispatchQueue.main.async { + self.onRemoteConfigLoaded?(config) + } + } + + self.loadingRemoteConfigLock.withLock { + self.remoteConfigDidFetch = true + self.loadingRemoteConfig = false + } + + callback?(config) + } + } + + func reloadFeatureFlags( + callback: (([String: Any]?) -> Void)? = nil + ) { + guard canReloadFlagsForTesting else { + return + } + + guard let storageManager = config.storageManager else { + hedgeLog("No PostHogStorageManager found in config, skipping loading feature flags") + callback?(nil) + return + } + + let groups = featureFlagsLock.withLock { getGroups() } + let distinctId = storageManager.getDistinctId() + let anonymousId = config.reuseAnonymousId == false ? storageManager.getAnonymousId() : nil + + loadFeatureFlags( + distinctId: distinctId, + anonymousId: anonymousId, + groups: groups, + callback: callback ?? { _ in } + ) + } + + private func preloadSessionReplayFlag() { + var sessionReplay: [String: Any]? + var featureFlags: [String: Any]? + featureFlagsLock.withLock { + sessionReplay = self.storage.getDictionary(forKey: .sessionReplay) as? [String: Any] + featureFlags = self.getCachedFeatureFlags() + } + + if let sessionReplay = sessionReplay { + sessionReplayFlagActive = isRecordingActive(featureFlags ?? [:], sessionReplay) + + if let endpoint = sessionReplay["endpoint"] as? String { + config.snapshotEndpoint = endpoint + } + } + } + + private func isRecordingActive(_ featureFlags: [String: Any], _ sessionRecording: [String: Any]) -> Bool { + var recordingActive = true + + // check for boolean flags + if let linkedFlag = sessionRecording["linkedFlag"] as? String { + let value = featureFlags[linkedFlag] + + if let boolValue = value as? Bool { + // boolean flag with value + recordingActive = boolValue + } else if value is String { + // its a multi-variant flag linked to "any" + recordingActive = true + } else { + // disable recording if the flag does not exist/quota limited + recordingActive = false + } + // check for specific flag variant + } else if let linkedFlag = sessionRecording["linkedFlag"] as? [String: Any] { + let flag = linkedFlag["flag"] as? String + let variant = linkedFlag["variant"] as? String + + if let flag, let variant { + let value = featureFlags[flag] as? String + recordingActive = value == variant + } else { + // disable recording if the flag does not exist/quota limited + recordingActive = false + } + } + // check for multi flag variant (any) + // if let linkedFlag = sessionRecording["linkedFlag"] as? String, + // featureFlags[linkedFlag] != nil + // is also a valid check but since we cannot check the value of the flag, + // we consider session recording is active + + return recordingActive + } + + func loadFeatureFlags( + distinctId: String, + anonymousId: String?, + groups: [String: String], + callback: @escaping ([String: Any]?) -> Void + ) { + loadingFeatureFlagsLock.withLock { + if self.loadingFeatureFlags { + return + } + self.loadingFeatureFlags = true + } + + let personProperties = getPersonPropertiesForFlags() + let groupProperties = getGroupPropertiesForFlags() + + api.flags(distinctId: distinctId, + anonymousId: anonymousId, + groups: groups, + personProperties: personProperties, + groupProperties: groupProperties.isEmpty ? nil : groupProperties) + { data, _ in + self.dispatchQueue.async { + // Check for quota limitation first + if let quotaLimited = data?["quotaLimited"] as? [String], + quotaLimited.contains("feature_flags") + { + // swiftlint:disable:next line_length + hedgeLog("Warning: Feature flags quota limit reached - clearing all feature flags and payloads. See https://posthog.com/docs/billing/limits-alerts for more information.") + + self.clearFeatureFlags() + self.notifyFeatureFlagsAndRelease([:]) + return callback([:]) + } + + // Safely handle optional data + guard var data = data else { + hedgeLog("Error: Flags response data is nil") + self.notifyFeatureFlagsAndRelease(nil) + return callback(nil) + } + + self.normalizeResponse(&data) + + let flagsV4 = data["flags"] as? [String: Any] + + guard let featureFlags = data["featureFlags"] as? [String: Any], + let featureFlagPayloads = data["featureFlagPayloads"] as? [String: Any] + else { + hedgeLog("Error: Flags response missing correct featureFlags format") + self.notifyFeatureFlagsAndRelease(nil) + return callback(nil) + } + + #if os(iOS) + self.processSessionRecordingConfig(data, featureFlags: featureFlags) + #endif + + // Grab the request ID from the response + let requestId = data["requestId"] as? String + let errorsWhileComputingFlags = data["errorsWhileComputingFlags"] as? Bool ?? false + var loadedFeatureFlags: [String: Any]? + + self.featureFlagsLock.withLock { + if let requestId { + // Store the request ID in the storage. + self.setCachedRequestId(requestId) + } + + if errorsWhileComputingFlags { + // v4 cached flags which contains metadata about each flag. + let cachedFlags = self.getCachedFlags() ?? [:] + + // The following two aren't necessarily needed for v4, but we'll keep them for now + // for back compatibility for existing v3 users who might already have cached flag data. + let cachedFeatureFlags = self.getCachedFeatureFlags() ?? [:] + let cachedFeatureFlagsPayloads = self.getCachedFeatureFlagPayload() ?? [:] + + let newFeatureFlags = cachedFeatureFlags.merging(featureFlags) { _, new in new } + let newFeatureFlagsPayloads = cachedFeatureFlagsPayloads.merging(featureFlagPayloads) { _, new in new } + + // if not all flags were computed, we upsert flags instead of replacing them + loadedFeatureFlags = newFeatureFlags + if let flagsV4 { + let newFlags = cachedFlags.merging(flagsV4) { _, new in new } + // if not all flags were computed, we upsert flags instead of replacing them + self.setCachedFlags(newFlags) + } + self.setCachedFeatureFlags(newFeatureFlags) + self.setCachedFeatureFlagPayload(newFeatureFlagsPayloads) + self.notifyFeatureFlagsAndRelease(newFeatureFlags) + } else { + loadedFeatureFlags = featureFlags + if let flagsV4 { + self.setCachedFlags(flagsV4) + } + self.setCachedFeatureFlags(featureFlags) + self.setCachedFeatureFlagPayload(featureFlagPayloads) + self.notifyFeatureFlagsAndRelease(featureFlags) + } + } + + return callback(loadedFeatureFlags) + } + } + } + + #if os(iOS) + private func processSessionRecordingConfig(_ data: [String: Any]?, featureFlags: [String: Any]) { + if let sessionRecording = data?["sessionRecording"] as? Bool { + sessionReplayFlagActive = sessionRecording + + // its always false here anyway + if !sessionRecording { + storage.remove(key: .sessionReplay) + } + + } else if let sessionRecording = data?["sessionRecording"] as? [String: Any] { + // keeps the value from config.sessionReplay since having sessionRecording + // means its enabled on the project settings, but its only enabled + // when local replay integration is enabled/active + if let endpoint = sessionRecording["endpoint"] as? String { + config.snapshotEndpoint = endpoint + } + sessionReplayFlagActive = isRecordingActive(featureFlags, sessionRecording) + storage.setDictionary(forKey: .sessionReplay, contents: sessionRecording) + } + } + #endif + + private func notifyFeatureFlags(_ featureFlags: [String: Any]?) { + DispatchQueue.main.async { + self.onFeatureFlagsLoaded?(featureFlags) + NotificationCenter.default.post(name: PostHogSDK.didReceiveFeatureFlags, object: nil) + } + } + + private func notifyFeatureFlagsAndRelease(_ featureFlags: [String: Any]?) { + notifyFeatureFlags(featureFlags) + + loadingFeatureFlagsLock.withLock { + self.loadingFeatureFlags = false + } + } + + func getFeatureFlags() -> [String: Any]? { + featureFlagsLock.withLock { getCachedFeatureFlags() } + } + + func getFeatureFlag(_ key: String) -> Any? { + var flags: [String: Any]? + featureFlagsLock.withLock { + flags = self.getCachedFeatureFlags() + } + + return flags?[key] + } + + func getFeatureFlagDetails(_ key: String) -> Any? { + var flags: [String: Any]? + featureFlagsLock.withLock { + flags = self.getCachedFlags() + } + + return flags?[key] + } + + // To be called after acquiring `featureFlagsLock` + private func getCachedFeatureFlagPayload() -> [String: Any]? { + if featureFlagPayloads == nil { + featureFlagPayloads = storage.getDictionary(forKey: .enabledFeatureFlagPayloads) as? [String: Any] + } + return featureFlagPayloads + } + + // To be called after acquiring `featureFlagsLock` + private func setCachedFeatureFlagPayload(_ featureFlagPayloads: [String: Any]) { + self.featureFlagPayloads = featureFlagPayloads + storage.setDictionary(forKey: .enabledFeatureFlagPayloads, contents: featureFlagPayloads) + } + + // To be called after acquiring `featureFlagsLock` + private func getCachedFeatureFlags() -> [String: Any]? { + if featureFlags == nil { + featureFlags = storage.getDictionary(forKey: .enabledFeatureFlags) as? [String: Any] + } + return featureFlags + } + + // To be called after acquiring `featureFlagsLock` + private func setCachedFeatureFlags(_ featureFlags: [String: Any]) { + self.featureFlags = featureFlags + storage.setDictionary(forKey: .enabledFeatureFlags, contents: featureFlags) + } + + // To be called after acquiring `featureFlagsLock` + private func setCachedFlags(_ flags: [String: Any]) { + self.flags = flags + storage.setDictionary(forKey: .flags, contents: flags) + } + + // To be called after acquiring `featureFlagsLock` + private func getCachedFlags() -> [String: Any]? { + if flags == nil { + flags = storage.getDictionary(forKey: .flags) as? [String: Any] + } + return flags + } + + func setPersonPropertiesForFlags(_ properties: [String: Any]) { + personPropertiesForFlagsLock.withLock { + // Merge properties additively, similar to JS SDK behavior + personPropertiesForFlags.merge(properties, uniquingKeysWith: { _, new in new }) + // Persist to disk + storage.setDictionary(forKey: .personPropertiesForFlags, contents: personPropertiesForFlags) + } + } + + func resetPersonPropertiesForFlags() { + personPropertiesForFlagsLock.withLock { + personPropertiesForFlags.removeAll() + // Clear from disk + storage.setDictionary(forKey: .personPropertiesForFlags, contents: personPropertiesForFlags) + } + } + + func setGroupPropertiesForFlags(_ groupType: String, properties: [String: Any]) { + groupPropertiesForFlagsLock.withLock { + // Merge properties additively for this group type + groupPropertiesForFlags[groupType, default: [:]].merge(properties) { _, new in new } + // Persist to disk + storage.setDictionary(forKey: .groupPropertiesForFlags, contents: groupPropertiesForFlags) + } + } + + func resetGroupPropertiesForFlags(_ groupType: String? = nil) { + groupPropertiesForFlagsLock.withLock { + if let groupType = groupType { + groupPropertiesForFlags.removeValue(forKey: groupType) + } else { + groupPropertiesForFlags.removeAll() + } + // Persist changes to disk + storage.setDictionary(forKey: .groupPropertiesForFlags, contents: groupPropertiesForFlags) + } + } + + private func getGroupPropertiesForFlags() -> [String: [String: Any]] { + groupPropertiesForFlagsLock.withLock { + groupPropertiesForFlags + } + } + + private func getPersonPropertiesForFlags() -> [String: Any] { + personPropertiesForFlagsLock.withLock { + var properties = personPropertiesForFlags + + // Always include fresh default properties if enabled + if config.setDefaultPersonProperties { + let defaultProperties = getDefaultPersonProperties() + // User-set properties override default properties + properties = defaultProperties.merging(properties) { _, userValue in userValue } + } + + return properties + } + } + + private func loadCachedPropertiesForFlags() { + personPropertiesForFlagsLock.withLock { + if let cachedPersonProperties = storage.getDictionary(forKey: .personPropertiesForFlags) as? [String: Any] { + personPropertiesForFlags = cachedPersonProperties + } + } + + groupPropertiesForFlagsLock.withLock { + if let cachedGroupProperties = storage.getDictionary(forKey: .groupPropertiesForFlags) as? [String: [String: Any]] { + groupPropertiesForFlags = cachedGroupProperties + } + } + } + + func getFeatureFlagPayload(_ key: String) -> Any? { + var flags: [String: Any]? + featureFlagsLock.withLock { + flags = getCachedFeatureFlagPayload() + } + + let value = flags?[key] + + guard let stringValue = value as? String else { + return value + } + + do { + // The payload value is stored as a string and is not pre-parsed... + // We need to mimic the JSON.parse of JS which is what posthog-js uses + return try JSONSerialization.jsonObject(with: stringValue.data(using: .utf8)!, options: .fragmentsAllowed) + } catch { + hedgeLog("Error parsing the object \(String(describing: value)): \(error)") + } + + // fallback to original value if not possible to serialize + return value + } + + // To be called after acquiring `featureFlagsLock` + private func setCachedRequestId(_ value: String?) { + requestId = value + if let value { + storage.setString(forKey: .requestId, contents: value) + } else { + storage.remove(key: .requestId) + } + } + + private func normalizeResponse(_ data: inout [String: Any]) { + if let flagsV4 = data["flags"] as? [String: Any] { + var featureFlags = [String: Any]() + var featureFlagsPayloads = [String: Any]() + for (key, value) in flagsV4 { + if let flag = value as? [String: Any] { + if let variant = flag["variant"] as? String { + featureFlags[key] = variant + // If there's a variant, the flag is enabled, so we can store the payload + if let metadata = flag["metadata"] as? [String: Any], + let payload = metadata["payload"] + { + featureFlagsPayloads[key] = payload + } + } else { + let enabled = flag["enabled"] as? Bool + featureFlags[key] = enabled + + // Only store payload if the flag is enabled + if enabled == true, + let metadata = flag["metadata"] as? [String: Any], + let payload = metadata["payload"] + { + featureFlagsPayloads[key] = payload + } + } + } + } + data["featureFlags"] = featureFlags + data["featureFlagPayloads"] = featureFlagsPayloads + } + } + + private func clearFeatureFlags() { + featureFlagsLock.withLock { + setCachedFlags([:]) + setCachedFeatureFlags([:]) + setCachedFeatureFlagPayload([:]) + setCachedRequestId(nil) // requestId no longer valid + } + } + + #if os(iOS) + func isSessionReplayFlagActive() -> Bool { + sessionReplayFlagActive + } + #endif + + private func getGroups() -> [String: String] { + guard let groups = storage.getDictionary(forKey: .groups) as? [String: String] else { + return [:] + } + return groups + } + + // MARK: Remote Config + + func getRemoteConfig() -> [String: Any]? { + remoteConfigLock.withLock { getCachedRemoteConfig() } + } + + private func getCachedRemoteConfig() -> [String: Any]? { + if remoteConfig == nil { + remoteConfig = storage.getDictionary(forKey: .remoteConfig) as? [String: Any] + } + return remoteConfig + } +} diff --git a/Pods/PostHog/PostHog/PostHogSDK.swift b/Pods/PostHog/PostHog/PostHogSDK.swift new file mode 100644 index 0000000..182ac02 --- /dev/null +++ b/Pods/PostHog/PostHog/PostHogSDK.swift @@ -0,0 +1,1499 @@ +// swiftlint:disable file_length cyclomatic_complexity + +// +// PostHogSDK.swift +// PostHogSDK +// +// Created by Ben White on 07.02.23. +// + +import Foundation + +#if os(iOS) || os(tvOS) + import UIKit +#elseif os(macOS) + import AppKit +#elseif os(watchOS) + import WatchKit +#endif + +let retryDelay = 5.0 +let maxRetryDelay = 30.0 + +// renamed to PostHogSDK due to https://github.com/apple/swift/issues/56573 +@objc public class PostHogSDK: NSObject { + private(set) var config: PostHogConfig + + private init(_ config: PostHogConfig) { + self.config = config + } + + private var enabled = false + private let setupLock = NSLock() + private let optOutLock = NSLock() + private let groupsLock = NSLock() + private let flagCallReportedLock = NSLock() + private let personPropsLock = NSLock() + + private var queue: PostHogQueue? + private var replayQueue: PostHogQueue? + private(set) var storage: PostHogStorage? + #if !os(watchOS) + private var reachability: Reachability? + #endif + private var flagCallReported = Set() + private(set) var remoteConfig: PostHogRemoteConfig? + private var context: PostHogContext? + private static var apiKeys = Set() + private var installedIntegrations: [PostHogIntegration] = [] + let sessionManager = PostHogSessionManager() + + #if os(iOS) + private weak var replayIntegration: PostHogReplayIntegration? + private weak var surveysIntegration: PostHogSurveyIntegration? + #endif + + // nonisolated(unsafe) is introduced in Swift 5.10 + #if swift(>=5.10) + @objc public nonisolated(unsafe) static let shared: PostHogSDK = { + let instance = PostHogSDK(PostHogConfig(apiKey: "")) + return instance + }() + #else + @objc public static let shared: PostHogSDK = { + let instance = PostHogSDK(PostHogConfig(apiKey: "")) + return instance + }() + #endif + + deinit { + #if !os(watchOS) + self.reachability?.stopNotifier() + #endif + + uninstallIntegrations() + } + + @objc public func debug(_ enabled: Bool = true) { + if !isEnabled() { + return + } + + toggleHedgeLog(enabled) + } + + @objc public func setup(_ config: PostHogConfig) { + setupLock.withLock { + toggleHedgeLog(config.debug) + if enabled { + hedgeLog("Setup called despite already being setup!") + return + } + + if PostHogSDK.apiKeys.contains(config.apiKey) { + hedgeLog("API Key: \(config.apiKey) already has a PostHog instance.") + } else { + PostHogSDK.apiKeys.insert(config.apiKey) + } + + enabled = true + self.config = config + let theStorage = PostHogStorage(config) + storage = theStorage + let api = PostHogApi(config) + + config.storageManager = config.storageManager ?? PostHogStorageManager(config) + remoteConfig = PostHogRemoteConfig(config, theStorage, api) { [weak self] in + self?.getDefaultPersonProperties() ?? [:] + } + + #if !os(watchOS) + do { + reachability = try Reachability() + } catch { + // ignored + } + context = PostHogContext(reachability) + #else + context = PostHogContext() + #endif + + optOutLock.withLock { + let optOut = theStorage.getBool(forKey: .optOut) + config.optOut = optOut ?? config.optOut + } + + #if !os(watchOS) + queue = PostHogQueue(config, theStorage, api, .batch, reachability) + replayQueue = PostHogQueue(config, theStorage, api, .snapshot, reachability) + #else + queue = PostHogQueue(config, theStorage, api, .batch) + replayQueue = PostHogQueue(config, theStorage, api, .snapshot) + #endif + + queue?.start(disableReachabilityForTesting: config.disableReachabilityForTesting, + disableQueueTimerForTesting: config.disableQueueTimerForTesting) + + replayQueue?.start(disableReachabilityForTesting: config.disableReachabilityForTesting, + disableQueueTimerForTesting: config.disableQueueTimerForTesting) + + // Create session manager instance for this PostHogSDK instance + sessionManager.setup(config: config) + sessionManager.startSession() + + if !config.optOut { + // don't install integrations if in opt-out state + installIntegrations() + } + + DispatchQueue.main.async { + NotificationCenter.default.post(name: PostHogSDK.didStartNotification, object: nil) + } + } + } + + @objc public func getDistinctId() -> String { + if !isEnabled() { + return "" + } + + return config.storageManager?.getDistinctId() ?? "" + } + + @objc public func getAnonymousId() -> String { + if !isEnabled() { + return "" + } + + return config.storageManager?.getAnonymousId() ?? "" + } + + @objc public func getSessionId() -> String? { + if !isEnabled() { + return nil + } + + return sessionManager.getSessionId(readOnly: true) + } + + @objc public func startSession() { + if !isEnabled() { + return + } + + sessionManager.startSession() + } + + @objc public func endSession() { + if !isEnabled() { + return + } + + sessionManager.endSession() + } + + // EVENT CAPTURE + + private func dynamicContext() -> [String: Any] { + var properties = getRegisteredProperties() + + var groups: [String: String]? + groupsLock.withLock { + groups = getGroups() + } + if groups != nil, !groups!.isEmpty { + properties["$groups"] = groups! + } + + guard let flags = remoteConfig?.getFeatureFlags() as? [String: Any] else { + return properties + } + + var keys: [String] = [] + for (key, value) in flags { + properties["$feature/\(key)"] = value + + var active = true + let boolValue = value as? Bool + if boolValue != nil { + active = boolValue! + } else { + active = true + } + + if active { + keys.append(key) + } + } + + if !keys.isEmpty { + properties["$active_feature_flags"] = keys + } + + return properties + } + + private func hasPersonProcessing() -> Bool { + !( + config.personProfiles == .never || + ( + config.personProfiles == .identifiedOnly && + config.storageManager?.isIdentified() == false && + config.storageManager?.isPersonProcessing() == false + ) + ) + } + + @discardableResult + private func requirePersonProcessing() -> Bool { + if config.personProfiles == .never { + hedgeLog("personProfiles is set to `never`. This call will be ignored.") + return false + } + config.storageManager?.setPersonProcessing(true) + return true + } + + private func buildProperties(distinctId: String, + properties: [String: Any]?, + userProperties: [String: Any]? = nil, + userPropertiesSetOnce: [String: Any]? = nil, + groups: [String: String]? = nil, + appendSharedProps: Bool = true, + timestamp: Date? = nil) -> [String: Any] + { + var props: [String: Any] = [:] + + if appendSharedProps { + let staticCtx = context?.staticContext() + let dynamicCtx = context?.dynamicContext() + let localDynamicCtx = dynamicContext() + + if staticCtx != nil { + props = props.merging(staticCtx ?? [:]) { current, _ in current } + } + if dynamicCtx != nil { + props = props.merging(dynamicCtx ?? [:]) { current, _ in current } + } + props = props.merging(localDynamicCtx) { current, _ in current } + if userProperties != nil { + props["$set"] = (userProperties ?? [:]) + } + if userPropertiesSetOnce != nil { + props["$set_once"] = (userPropertiesSetOnce ?? [:]) + } + if groups != nil { + // $groups are also set via the dynamicContext + let currentGroups = props["$groups"] as? [String: String] ?? [:] + let mergedGroups = currentGroups.merging(groups ?? [:]) { current, _ in current } + props["$groups"] = mergedGroups + } + + if let isIdentified = config.storageManager?.isIdentified() { + props["$is_identified"] = isIdentified + } + + props["$process_person_profile"] = hasPersonProcessing() + } + + let sdkInfo = context?.sdkInfo() + if sdkInfo != nil { + props = props.merging(sdkInfo ?? [:]) { current, _ in current } + } + + // use existing session id if already present in properties (from params) + // for session replay, we attach the session id on the event as early as possible to avoid sending snapshots to a wrong session + // if not present, get a current or new session id at event timestamp + let propSessionId = properties?["$session_id"] as? String + let sessionId: String? = propSessionId.isNilOrEmpty + ? sessionManager.getSessionId(at: timestamp ?? now()) + : propSessionId + + if let sessionId { + if propSessionId.isNilOrEmpty { + props["$session_id"] = sessionId + } + // only Session replay requires $window_id, so we set as the same as $session_id. + // the backend might fallback to $session_id if $window_id is not present next. + #if os(iOS) + if !appendSharedProps, isSessionReplayActive() { + props["$window_id"] = sessionId + } + #endif + } + + // only Session Replay needs distinct_id also in the props + // remove after https://github.com/PostHog/posthog/issues/23275 gets merged + let propDistinctId = properties?["distinct_id"] as? String + if !appendSharedProps, propDistinctId == nil || propDistinctId?.isEmpty == true { + props["distinct_id"] = distinctId + } + + props = props.merging(properties ?? [:]) { current, _ in current } + + return props + } + + @objc public func flush() { + if !isEnabled() { + return + } + + queue?.flush() + replayQueue?.flush() + } + + @objc public func reset() { + if !isEnabled() { + return + } + + // storage also removes all feature flags + storage?.reset(keepAnonymousId: config.reuseAnonymousId) + config.storageManager?.reset(keepAnonymousId: config.reuseAnonymousId) + flagCallReportedLock.withLock { + flagCallReported.removeAll() + } + sessionManager.reset() + + // Clear person and group properties for flags + remoteConfig?.resetPersonPropertiesForFlags() + remoteConfig?.resetGroupPropertiesForFlags() + + // reload flags as anon user + remoteConfig?.reloadFeatureFlags() + } + + private func getGroups() -> [String: String] { + guard let groups = storage?.getDictionary(forKey: .groups) as? [String: String] else { + return [:] + } + return groups + } + + private func getRegisteredProperties() -> [String: Any] { + guard let props = storage?.getDictionary(forKey: .registerProperties) as? [String: Any] else { + return [:] + } + return props + } + + // register is a reserved word in ObjC + @objc(registerProperties:) + public func register(_ properties: [String: Any]) { + if !isEnabled() { + return + } + + let sanitizedProps = sanitizeDictionary(properties) + if sanitizedProps == nil { + return + } + + personPropsLock.withLock { + let props = getRegisteredProperties() + let mergedProps = props.merging(sanitizedProps!) { _, new in new } + storage?.setDictionary(forKey: .registerProperties, contents: mergedProps) + } + } + + @objc(unregisterProperties:) + public func unregister(_ key: String) { + if !isEnabled() { + return + } + + personPropsLock.withLock { + var props = getRegisteredProperties() + props.removeValue(forKey: key) + storage?.setDictionary(forKey: .registerProperties, contents: props) + } + } + + @objc public func identify(_ distinctId: String) { + identify(distinctId, userProperties: nil, userPropertiesSetOnce: nil) + } + + @objc(identifyWithDistinctId:userProperties:) + public func identify(_ distinctId: String, + userProperties: [String: Any]? = nil) + { + identify(distinctId, userProperties: userProperties, userPropertiesSetOnce: nil) + } + + @objc(identifyWithDistinctId:userProperties:userPropertiesSetOnce:) + public func identify(_ distinctId: String, + userProperties: [String: Any]? = nil, + userPropertiesSetOnce: [String: Any]? = nil) + { + if !isEnabled() { + return + } + + if distinctId.isEmpty { + hedgeLog("identify call not allowed, distinctId is invalid: \(distinctId)") + return + } + + if isOptOutState() { + return + } + + if !requirePersonProcessing() { + return + } + + guard let queue, let storageManager = config.storageManager else { + return + } + let oldDistinctId = getDistinctId() + + let isIdentified = storageManager.isIdentified() + + let hasDifferentDistinctId = distinctId != oldDistinctId + + if hasDifferentDistinctId, !isIdentified { + var props: [String: Any] = ["distinct_id": distinctId] + + if !config.reuseAnonymousId { + // We keep the AnonymousId to be used by flags calls and identify to link the previousId + storageManager.setAnonymousId(oldDistinctId) + props["$anon_distinct_id"] = oldDistinctId + } + + storageManager.setDistinctId(distinctId) + storageManager.setIdentified(true) + + let properties = buildProperties( + distinctId: distinctId, + properties: props, + userProperties: sanitizeDictionary(userProperties), + userPropertiesSetOnce: sanitizeDictionary(userPropertiesSetOnce) + ) + + guard let event = buildEvent(event: "$identify", distinctId: distinctId, properties: properties) else { + return + } + + queue.add(event) + + // Automatically set person properties for feature flags during identify() call + setPersonPropertiesForFlagsIfNeeded(userProperties, userPropertiesSetOnce: userPropertiesSetOnce) + + remoteConfig?.reloadFeatureFlags() + + // we need to make sure the user props update is for the same user + // otherwise they have to reset and identify again + } else if !hasDifferentDistinctId, !(userProperties?.isEmpty ?? true) || !(userPropertiesSetOnce?.isEmpty ?? true) { + capture("$set", + distinctId: distinctId, + userProperties: userProperties, + userPropertiesSetOnce: userPropertiesSetOnce) + + // Automatically set person properties for feature flags during user property updates + setPersonPropertiesForFlagsIfNeeded(userProperties, userPropertiesSetOnce: userPropertiesSetOnce) + + // Note we don't reload flags on property changes as these get processed async + + } else { + hedgeLog("already identified with id: \(oldDistinctId)") + } + } + + private func isOptOutState() -> Bool { + if config.optOut { + hedgeLog("PostHog is in OptOut state.") + return true + } + return false + } + + private func setPersonPropertiesForFlagsIfNeeded( + _ userProperties: [String: Any]?, + userPropertiesSetOnce: [String: Any]? = nil + ) { + guard hasPersonProcessing() else { + return + } + + let sanitizedUserProperties = sanitizeDictionary(userProperties) ?? [:] + let sanitizedUserPropertiesSetOnce = sanitizeDictionary(userPropertiesSetOnce) ?? [:] + + // Combine both types of properties for feature flag evaluation + let allProperties = sanitizedUserProperties.merging(sanitizedUserPropertiesSetOnce) { current, _ in current } + + guard !allProperties.isEmpty else { + return + } + + remoteConfig?.setPersonPropertiesForFlags(allProperties) + } + + private func setGroupPropertiesForFlagsIfNeeded( + type: String, + groupProperties: [String: Any]? + ) { + guard hasPersonProcessing() else { + return + } + + let sanitizedGroupProperties = sanitizeDictionary(groupProperties) ?? [:] + + guard !sanitizedGroupProperties.isEmpty else { + return + } + + remoteConfig?.setGroupPropertiesForFlags(type, properties: sanitizedGroupProperties) + } + + /// Returns fresh default device and app properties for feature flag evaluation. + /// This ensures feature flags can use current properties like $app_version, $os_version, etc. + /// These properties are computed fresh each time they're needed. + private func getDefaultPersonProperties() -> [String: Any] { + guard config.setDefaultPersonProperties else { return [:] } + guard isEnabled() else { return [:] } + + return context?.personPropertiesContext() ?? [:] + } + + @objc public func capture(_ event: String) { + capture(event, distinctId: nil, properties: nil, userProperties: nil, userPropertiesSetOnce: nil, groups: nil) + } + + @objc(captureWithEvent:properties:) + public func capture(_ event: String, + properties: [String: Any]? = nil) + { + capture(event, distinctId: nil, properties: properties, userProperties: nil, userPropertiesSetOnce: nil, groups: nil) + } + + @objc(captureWithEvent:properties:userProperties:) + public func capture(_ event: String, + properties: [String: Any]? = nil, + userProperties: [String: Any]? = nil) + { + capture(event, distinctId: nil, properties: properties, userProperties: userProperties, userPropertiesSetOnce: nil, groups: nil) + } + + @objc(captureWithEvent:properties:userProperties:userPropertiesSetOnce:) + public func capture(_ event: String, + properties: [String: Any]? = nil, + userProperties: [String: Any]? = nil, + userPropertiesSetOnce: [String: Any]? = nil) + { + capture(event, distinctId: nil, properties: properties, userProperties: userProperties, userPropertiesSetOnce: userPropertiesSetOnce, groups: nil) + } + + @objc(captureWithEvent:properties:userProperties:userPropertiesSetOnce:groups:) + public func capture(_ event: String, + properties: [String: Any]? = nil, + userProperties: [String: Any]? = nil, + userPropertiesSetOnce: [String: Any]? = nil, + groups: [String: String]? = nil) + { + capture(event, distinctId: nil, properties: properties, userProperties: userProperties, userPropertiesSetOnce: userPropertiesSetOnce, groups: groups) + } + + @objc(captureWithEvent:distinctId:properties:userProperties:userPropertiesSetOnce:groups:) + public func capture(_ event: String, + distinctId: String? = nil, + properties: [String: Any]? = nil, + userProperties: [String: Any]? = nil, + userPropertiesSetOnce: [String: Any]? = nil, + groups: [String: String]? = nil) + { + capture(event, + distinctId: distinctId, + properties: properties, + userProperties: userProperties, + userPropertiesSetOnce: userPropertiesSetOnce, + groups: groups, + timestamp: nil) + } + + @objc(captureWithEvent:distinctId:properties:userProperties:userPropertiesSetOnce:groups:timestamp:) + public func capture(_ event: String, + distinctId: String? = nil, + properties: [String: Any]? = nil, + userProperties: [String: Any]? = nil, + userPropertiesSetOnce: [String: Any]? = nil, + groups: [String: String]? = nil, + timestamp: Date? = nil) + { + if !isEnabled() { + return + } + + if isOptOutState() { + return + } + + guard let queue else { + return + } + + var isSnapshotEvent = event == "$snapshot" + let eventTimestamp = timestamp ?? now() + let eventDistinctId = distinctId ?? getDistinctId() + + // if the user isn't identified but passed userProperties, userPropertiesSetOnce or groups, + // we should still enable person processing since this is intentional + if userProperties?.isEmpty == false || userPropertiesSetOnce?.isEmpty == false || groups?.isEmpty == false { + requirePersonProcessing() + } + + let properties = buildProperties(distinctId: eventDistinctId, + properties: sanitizeDictionary(properties), + userProperties: sanitizeDictionary(userProperties), + userPropertiesSetOnce: sanitizeDictionary(userPropertiesSetOnce), + groups: groups, + appendSharedProps: !isSnapshotEvent, + timestamp: timestamp) + + // Sanitize is now called in buildEvent + let posthogEvent = buildEvent( + event: event, + distinctId: eventDistinctId, + properties: properties, + timestamp: eventTimestamp + ) + + guard let posthogEvent else { + return + } + + // Reevaluate if this is a snapshot event because the event might have been updated by the beforeSend hook + isSnapshotEvent = posthogEvent.event == "$snapshot" + + // if this is a $snapshot event and $session_id is missing, don't process then event + if isSnapshotEvent, posthogEvent.properties["$session_id"] == nil { + return + } + + // Session Replay has its own queue + let targetQueue = isSnapshotEvent ? replayQueue : queue + + targetQueue?.add(posthogEvent) + + // Automatically set person properties for feature flags during capture event + setPersonPropertiesForFlagsIfNeeded(userProperties, userPropertiesSetOnce: userPropertiesSetOnce) + + #if os(iOS) + surveysIntegration?.onEvent(event: posthogEvent.event) + #endif + } + + @objc public func screen(_ screenTitle: String) { + screen(screenTitle, properties: nil) + } + + @objc(screenWithTitle:properties:) + public func screen(_ screenTitle: String, properties: [String: Any]? = nil) { + if !isEnabled() { + return + } + + if isOptOutState() { + return + } + + guard let queue else { + return + } + + let props = [ + "$screen_name": screenTitle, + ].merging(sanitizeDictionary(properties) ?? [:]) { prop, _ in prop } + + let distinctId = getDistinctId() + + let properties = buildProperties(distinctId: distinctId, properties: props) + + guard let event = buildEvent(event: "$screen", distinctId: distinctId, properties: properties) else { + return + } + + queue.add(event) + } + + func autocapture( + eventType: String, + elementsChain: String, + properties: [String: Any] + ) { + if !isEnabled() { + return + } + + if isOptOutState() { + return + } + + guard let queue else { + return + } + + let props = [ + "$event_type": eventType, + "$elements_chain": elementsChain, + ].merging(sanitizeDictionary(properties) ?? [:]) { prop, _ in prop } + + let distinctId = getDistinctId() + + let properties = buildProperties(distinctId: distinctId, properties: props) + + guard let event = buildEvent(event: "$autocapture", distinctId: distinctId, properties: properties) else { + return + } + + queue.add(event) + } + + private func sanitizeProperties(_ properties: [String: Any]) -> [String: Any] { + if let sanitizer = config.propertiesSanitizer { + return sanitizer.sanitize(properties) + } + return properties + } + + @objc public func alias(_ alias: String) { + if !isEnabled() { + return + } + + if isOptOutState() { + return + } + + if !requirePersonProcessing() { + return + } + + guard let queue else { + return + } + + let props = ["alias": alias] + + let distinctId = getDistinctId() + + let properties = buildProperties(distinctId: distinctId, properties: props) + + guard let event = buildEvent(event: "$create_alias", distinctId: distinctId, properties: properties) else { + return + } + + queue.add(event) + } + + private func groups(_ newGroups: [String: String]) -> [String: String] { + guard let storage else { + return [:] + } + + var groups: [String: String]? + var mergedGroups: [String: String]? + groupsLock.withLock { + groups = getGroups() + mergedGroups = groups?.merging(newGroups) { _, new in new } + + storage.setDictionary(forKey: .groups, contents: mergedGroups ?? [:]) + } + + var shouldReloadFlags = false + + for (key, value) in newGroups where groups?[key] != value { + shouldReloadFlags = true + break + } + + if shouldReloadFlags { + remoteConfig?.reloadFeatureFlags() + } + + return mergedGroups ?? [:] + } + + private func groupIdentify(type: String, key: String, groupProperties: [String: Any]? = nil) { + if !isEnabled() { + return + } + + if isOptOutState() { + return + } + + guard let queue else { + return + } + + var props: [String: Any] = ["$group_type": type, + "$group_key": key] + + let groupProps = sanitizeDictionary(groupProperties) + + if groupProps != nil { + props["$group_set"] = groupProps + } + + // Automatically set group properties for feature flags + setGroupPropertiesForFlagsIfNeeded(type: type, groupProperties: groupProperties) + + // Same as .group but without associating the current user with the group + let distinctId = getDistinctId() + + let properties = buildProperties(distinctId: distinctId, properties: props) + + guard let event = buildEvent(event: "$groupidentify", distinctId: distinctId, properties: properties) else { + return + } + + queue.add(event) + } + + func buildEvent(event eventName: String, distinctId: String, properties: [String: Any], timestamp: Date = Date()) -> PostHogEvent? { + let sanitizedProperties = sanitizeProperties(properties) + + let event = PostHogEvent( + event: eventName, + distinctId: distinctId, + properties: sanitizedProperties, + timestamp: timestamp + ) + + let resultEvent = config.runBeforeSend(event) + + if resultEvent == nil { + let originalMessage = "PostHog event \(eventName) was dropped" + let message = PostHogKnownUnsafeEditableEvent.contains(eventName) + ? "\(originalMessage). This can cause unexpected behavior." + : originalMessage + + hedgeLog(message) + } + + return resultEvent + } + + @objc(groupWithType:key:) + public func group(type: String, key: String) { + group(type: type, key: key, groupProperties: nil) + } + + @objc(groupWithType:key:groupProperties:) + public func group(type: String, key: String, groupProperties: [String: Any]? = nil) { + if !isEnabled() { + return + } + + if isOptOutState() { + return + } + + if !requirePersonProcessing() { + return + } + + _ = groups([type: key]) + + groupIdentify(type: type, key: key, groupProperties: sanitizeDictionary(groupProperties)) + } + + // FEATURE FLAGS + + /// Sets person properties that will be included in feature flag evaluation requests. + /// + /// This method allows you to override server-side person properties for immediate feature flag evaluation, + /// solving the race condition where person properties from `identify()` calls may not have been processed + /// by the server yet. + /// + /// Properties are merged additively with existing properties. Feature flags are automatically reloaded + /// after setting properties. + /// + /// ## Example Usage + /// ```swift + /// // Set properties and automatically reload flags + /// PostHogSDK.shared.setPersonPropertiesForFlags([ + /// "$app_version": "2.93.0", + /// "plan": "premium" + /// ]) + /// + /// // Now feature flags will be evaluated with these properties + /// let flagValue = PostHogSDK.shared.isFeatureEnabled("new_feature") + /// ``` + /// + /// - Parameter properties: Dictionary of person properties to include in flag evaluation + /// - SeeAlso: `setPersonPropertiesForFlags(_:reloadFeatureFlags:)` to control flag reloading behavior + @objc public func setPersonPropertiesForFlags(_ properties: [String: Any]) { + setPersonPropertiesForFlags(properties, reloadFeatureFlags: true) + } + + /// Sets person properties that will be included in feature flag evaluation requests. + /// + /// This method allows you to override server-side person properties for immediate feature flag evaluation, + /// solving the race condition where person properties from `identify()` calls may not have been processed + /// by the server yet. + /// + /// Properties are merged additively with existing properties. + /// + /// ## Example Usage + /// ```swift + /// // Set properties without automatically reloading flags + /// PostHogSDK.shared.setPersonPropertiesForFlags([ + /// "$app_version": "2.93.0", + /// "plan": "premium" + /// ], reloadFeatureFlags: false) + /// + /// // Manually reload flags later + /// PostHogSDK.shared.reloadFeatureFlags() + /// ``` + /// + /// - Parameters: + /// - properties: Dictionary of person properties to include in flag evaluation + /// - reloadFeatureFlags: Whether to automatically reload feature flags after setting properties + @objc(setPersonPropertiesForFlagsWithProperties:reloadFeatureFlags:) + public func setPersonPropertiesForFlags(_ properties: [String: Any], reloadFeatureFlags: Bool = true) { + if !isEnabled() { + return + } + + guard hasPersonProcessing() else { + return + } + + let sanitizedProperties = sanitizeDictionary(properties) ?? [:] + guard !sanitizedProperties.isEmpty else { return } + remoteConfig?.setPersonPropertiesForFlags(sanitizedProperties) + + if reloadFeatureFlags { + remoteConfig?.reloadFeatureFlags() + } + } + + /// Resets all person properties that were set for feature flag evaluation. + /// + /// After calling this method, feature flag evaluation will only use server-side person properties + /// and will not include any locally overridden properties. + /// + /// ## Example Usage + /// ```swift + /// // Clear all locally set person properties for flags + /// PostHogSDK.shared.resetPersonPropertiesForFlags() + /// + /// // Feature flags will now use only server-side properties + /// let flagValue = PostHogSDK.shared.isFeatureEnabled("feature") + /// ``` + /// + /// - Note: This method does not automatically reload feature flags. Call `reloadFeatureFlags()` + /// after resetting if you want to immediately refresh flags with the cleared properties. + @objc public func resetPersonPropertiesForFlags() { + if !isEnabled() { + return + } + + guard hasPersonProcessing() else { + return + } + + remoteConfig?.resetPersonPropertiesForFlags() + } + + /// Sets properties for a specific group type to include when evaluating feature flags. + /// These properties supplement the standard group information sent to PostHog for flag evaluation, + /// providing additional context that can be used in flag targeting conditions. + /// + /// ## Example Usage + /// ```swift + /// PostHogSDK.shared.setGroupPropertiesForFlags("organization", properties: [ + /// "plan": "enterprise", + /// "seats": 50, + /// "industry": "technology" + /// ]) + /// ``` + /// + /// - Parameters: + /// - groupType: The group type identifier (e.g., "organization", "team") + /// - properties: Dictionary of properties to set for this group type + /// - Note: This method automatically reloads feature flags to apply the new properties. + /// - SeeAlso: `setGroupPropertiesForFlags(_:properties:reloadFeatureFlags:)` to control flag reloading behavior + @objc(setGroupPropertiesForFlags:properties:) + public func setGroupPropertiesForFlags(_ groupType: String, properties: [String: Any]) { + setGroupPropertiesForFlags(groupType, properties: properties, reloadFeatureFlags: true) + } + + /// Sets properties for a specific group type to include when evaluating feature flags. + /// These properties supplement the standard group information sent to PostHog for flag evaluation, + /// providing additional context that can be used in flag targeting conditions. + /// + /// ## Example Usage + /// ```swift + /// // Set properties without automatically reloading flags + /// PostHogSDK.shared.setGroupPropertiesForFlags("organization", properties: [ + /// "plan": "enterprise", + /// "seats": 50 + /// ], reloadFeatureFlags: false) + /// + /// // Manually reload flags later + /// PostHogSDK.shared.reloadFeatureFlags() + /// ``` + /// + /// - Parameters: + /// - groupType: The group type identifier (e.g., "organization", "team") + /// - properties: Dictionary of properties to set for this group type + /// - reloadFeatureFlags: Whether to automatically reload feature flags after setting properties + @objc(setGroupPropertiesForFlags:properties:reloadFeatureFlags:) + public func setGroupPropertiesForFlags(_ groupType: String, properties: [String: Any], reloadFeatureFlags: Bool) { + if !isEnabled() { + return + } + + guard hasPersonProcessing() else { + return + } + + let sanitizedProperties = sanitizeDictionary(properties) ?? [:] + guard !sanitizedProperties.isEmpty else { return } + remoteConfig?.setGroupPropertiesForFlags(groupType, properties: sanitizedProperties) + + if reloadFeatureFlags { + remoteConfig?.reloadFeatureFlags() + } + } + + /// Clears all group properties for feature flag evaluation. + /// + /// ## Example Usage + /// ```swift + /// // Clear all group properties + /// PostHogSDK.shared.resetGroupPropertiesForFlags() + /// ``` + /// + /// - Note: This method does not automatically reload feature flags. Call `reloadFeatureFlags()` + /// after resetting if you want to immediately refresh flags with the cleared properties. + @objc public func resetGroupPropertiesForFlags() { + resetGroupPropertiesForFlags(groupType: nil) + } + + /// Clears group properties for feature flag evaluation for a specific group type. + /// + /// ## Example Usage + /// ```swift + /// // Clear properties for specific group type + /// PostHogSDK.shared.resetGroupPropertiesForFlags("organization") + /// ``` + /// + /// - Parameter groupType: The group type to clear properties for + /// - Note: This method does not automatically reload feature flags. Call `reloadFeatureFlags()` + /// after resetting if you want to immediately refresh flags with the cleared properties. + @objc(resetGroupPropertiesForFlagsWithGroupType:) + public func resetGroupPropertiesForFlags(_ groupType: String) { + resetGroupPropertiesForFlags(groupType: groupType) + } + + /// Internal implementation for resetting group properties. + private func resetGroupPropertiesForFlags(groupType: String?) { + if !isEnabled() { + return + } + + guard hasPersonProcessing() else { + return + } + + remoteConfig?.resetGroupPropertiesForFlags(groupType) + } + + @objc public func reloadFeatureFlags() { + reloadFeatureFlags { + // No use case + } + } + + @objc(reloadFeatureFlagsWithCallback:) + public func reloadFeatureFlags(_ callback: @escaping () -> Void) { + if !isEnabled() { + return + } + + remoteConfig?.reloadFeatureFlags { _ in + self.flagCallReportedLock.withLock { + self.flagCallReported.removeAll() + } + callback() + } + } + + @objc public func getFeatureFlag(_ key: String) -> Any? { + if !isEnabled() { + return nil + } + + guard let remoteConfig else { + return nil + } + + let value = remoteConfig.getFeatureFlag(key) + + if config.sendFeatureFlagEvent { + reportFeatureFlagCalled(flagKey: key, flagValue: value) + } + + return value + } + + @objc public func isFeatureEnabled(_ key: String) -> Bool { + let result = getFeatureFlag(key) + return result is String ? true : (result as? Bool) ?? false + } + + @objc public func getFeatureFlagPayload(_ key: String) -> Any? { + if !isEnabled() { + return nil + } + + guard let remoteConfig else { + return nil + } + + return remoteConfig.getFeatureFlagPayload(key) + } + + private func reportFeatureFlagCalled(flagKey: String, flagValue: Any?) { + var shouldCapture = false + + flagCallReportedLock.withLock { + if !flagCallReported.contains(flagKey) { + flagCallReported.insert(flagKey) + shouldCapture = true + } + } + + if shouldCapture { + let requestId = remoteConfig?.lastRequestId ?? "" + let details = remoteConfig?.getFeatureFlagDetails(flagKey) + + var properties = [ + "$feature_flag": flagKey, + "$feature_flag_response": flagValue ?? NSNull(), + "$feature_flag_request_id": requestId, + ] + + if let details = details as? [String: Any] { + if let reason = details["reason"] as? [String: Any] { + properties["$feature_flag_reason"] = reason["description"] ?? NSNull() + } + + if let metadata = details["metadata"] as? [String: Any] { + properties["$feature_flag_id"] = metadata["id"] ?? NSNull() + properties["$feature_flag_version"] = metadata["version"] ?? NSNull() + } + } + + capture("$feature_flag_called", properties: properties) + } + } + + private func isEnabled() -> Bool { + if !enabled { + hedgeLog("PostHog method was called without `setup` being complete. Call wil be ignored.") + } + return enabled + } + + @objc public func optIn() { + if !isEnabled() { + return + } + + if !isOptOut() { + return + } + + optOutLock.withLock { + config.optOut = false + storage?.setBool(forKey: .optOut, contents: false) + } + + setupLock.withLock { + installIntegrations() + } + } + + @objc public func optOut() { + if !isEnabled() { + return + } + + if isOptOut() { + return + } + + optOutLock.withLock { + config.optOut = true + storage?.setBool(forKey: .optOut, contents: true) + } + + setupLock.withLock { + uninstallIntegrations() + } + } + + @objc public func isOptOut() -> Bool { + if !isEnabled() { + return true + } + + return config.optOut + } + + @objc public func close() { + if !isEnabled() { + return + } + + setupLock.withLock { + enabled = false + PostHogSDK.apiKeys.remove(config.apiKey) + + queue?.stop() + replayQueue?.stop() + + queue = nil + replayQueue = nil + config.storageManager?.reset(keepAnonymousId: config.reuseAnonymousId) + config.storageManager = nil + config = PostHogConfig(apiKey: "") + remoteConfig = nil + storage = nil + #if !os(watchOS) + self.reachability?.stopNotifier() + reachability = nil + #endif + flagCallReportedLock.withLock { + flagCallReported.removeAll() + } + context = nil + sessionManager.endSession() + toggleHedgeLog(false) + + uninstallIntegrations() + } + } + + #if os(iOS) + /** + Starts session recording. + This method will have no effect if PostHog is not enabled, or if session replay is disabled in your project settings + + ## Note: + - Calling this method will resume the current session or create a new one if it doesn't exist + */ + @objc(startSessionRecording) + public func startSessionRecording() { + startSessionRecording(resumeCurrent: true) + } + + /** + Starts session recording. + This method will have no effect if PostHog is not enabled, or if session replay is disabled in your project settings + + - Parameter resumeCurrent: + Whether to resume recording of current session (true) or start a new session (false). + */ + @objc(startSessionRecordingWithResumeCurrent:) + public func startSessionRecording(resumeCurrent: Bool) { + if !isEnabled() { + return + } + + if isOptOutState() { + return + } + + if replayIntegration == nil { + // replay integration was not previously installed (probably disabled in config), attempt to install it + setupLock.withLock { + installReplayIntegration() + } + } + + guard let replayIntegration else { + return + } + + if resumeCurrent, replayIntegration.isActive() { + // nothing to resume, already active + return + } + + guard let remoteConfig, remoteConfig.isSessionReplayFlagActive() else { + return hedgeLog("Could not start recording. Session replay feature flag is disabled.") + } + + let sessionId = resumeCurrent + ? sessionManager.getSessionId() + : sessionManager.getNextSessionId() + + guard let sessionId else { + return hedgeLog("Could not start recording. Missing session id.") + } + + replayIntegration.start() + hedgeLog("Session replay recording started. Session id is \(sessionId)") + } + + /** + Stops the current session recording if one is in progress. + + This method will have no effect if PostHog is not enabled + */ + @objc public func stopSessionRecording() { + if !isEnabled() { + return + } + + guard let replayIntegration, replayIntegration.isActive() else { + return + } + + replayIntegration.stop() + hedgeLog("Session replay recording stopped.") + } + #endif + + @objc public static func with(_ config: PostHogConfig) -> PostHogSDK { + let postHog = PostHogSDK(config) + postHog.setup(config) + return postHog + } + + #if os(iOS) + @objc public func isSessionReplayActive() -> Bool { + if !isEnabled() { + return false + } + + guard let replayIntegration, let remoteConfig else { + return false + } + + return replayIntegration.isActive() + && !sessionManager.getSessionId(readOnly: true).isNilOrEmpty + && remoteConfig.isSessionReplayFlagActive() + } + #endif + + #if os(iOS) || targetEnvironment(macCatalyst) + @objc public func isAutocaptureActive() -> Bool { + isEnabled() && config.captureElementInteractions + } + #endif + + private func installIntegrations() { + guard installedIntegrations.isEmpty else { + hedgeLog("Integrations already installed. Call uninstallIntegrations() first.") + return + } + + let integrations = config.getIntegrations() + var installed: [PostHogIntegration] = [] + + for integration in integrations { + // Skip integrations that require swizzling when swizzling is disabled + if integration.requiresSwizzling, !config.enableSwizzling { + hedgeLog("Integration \(type(of: integration)) skipped. Integration requires swizzling but enableSwizzling is disabled in config") + continue + } + + do { + try integration.install(self) + installed.append(integration) + + #if os(iOS) + // TODO: Decouple these two integrations from PostHogSDK intance + if let replayIntegration = integration as? PostHogReplayIntegration { + self.replayIntegration = replayIntegration + } + + if let surveysIntegration = integration as? PostHogSurveyIntegration { + self.surveysIntegration = surveysIntegration + } + #endif + + hedgeLog("Integration \(type(of: integration)) installed") + } catch { + hedgeLog("Integration \(type(of: integration)) failed to install: \(error)") + } + } + + installedIntegrations = installed + } + + #if os(iOS) + private func installReplayIntegration() { + guard replayIntegration == nil else { return } + + let integration = PostHogReplayIntegration() + do { + try integration.install(self) + installedIntegrations.append(integration) + replayIntegration = integration + + hedgeLog("Integration \(type(of: integration)) installed") + } catch { + hedgeLog("Integration \(type(of: integration)) failed to install: \(error)") + } + } + #endif + + private func uninstallIntegrations() { + for integration in installedIntegrations { + integration.uninstall(self) + hedgeLog("Integration \(type(of: integration)) uninstalled") + } + installedIntegrations = [] + + #if os(iOS) + replayIntegration = nil + surveysIntegration = nil + #endif + } +} + +#if TESTING + extension PostHogSDK { + #if os(iOS) || targetEnvironment(macCatalyst) + func getAutocaptureIntegration() -> PostHogAutocaptureIntegration? { + installedIntegrations.compactMap { + $0 as? PostHogAutocaptureIntegration + }.first + } + #endif + + #if os(iOS) + func getReplayIntegration() -> PostHogReplayIntegration? { + installedIntegrations.compactMap { + $0 as? PostHogReplayIntegration + }.first + } + #endif + + func getSessionManager() -> PostHogSessionManager? { + sessionManager + } + + func getAppLifeCycleIntegration() -> PostHogAppLifeCycleIntegration? { + installedIntegrations.compactMap { + $0 as? PostHogAppLifeCycleIntegration + }.first + } + + func getScreenViewIntegration() -> PostHogScreenViewIntegration? { + installedIntegrations.compactMap { + $0 as? PostHogScreenViewIntegration + }.first + } + } +#endif + +// swiftlint:enable file_length cyclomatic_complexity diff --git a/Pods/PostHog/PostHog/PostHogSessionManager.swift b/Pods/PostHog/PostHog/PostHogSessionManager.swift new file mode 100644 index 0000000..07de4bc --- /dev/null +++ b/Pods/PostHog/PostHog/PostHogSessionManager.swift @@ -0,0 +1,271 @@ +// +// PostHogSessionManager.swift +// PostHog +// +// Created by Manoel Aranda Neto on 28.08.24. +// + +import Foundation + +// only for internal use +// Do we need to expose this as public API? Could be internal static instead? +@objc public class PostHogSessionManager: NSObject { + enum SessionIDChangeReason: String { + case sessionIdEmpty = "Session id was empty" + case sessionStart = "Session started" + case sessionEnd = "Session ended" + case sessionReset = "Session was reset" + case sessionTimeout = "Session timed out" + case sessionPastMaximumLength = "Session past maximum length" + case customSessionId = "Custom session set" + } + + @objc public static var shared: PostHogSessionManager { + PostHogSDK.shared.sessionManager + } + + private var config: PostHogConfig? + + override init() { + super.init() + } + + func setup(config: PostHogConfig) { + self.config = config + didBecomeActiveToken = nil + didEnterBackgroundToken = nil + applicationEventToken = nil + registerNotifications() + registerApplicationSendEvent() + } + + func reset() { + resetSession() + didBecomeActiveToken = nil + didEnterBackgroundToken = nil + applicationEventToken = nil + } + + private let queue = DispatchQueue(label: "com.posthog.PostHogSessionManager", target: .global(qos: .utility)) + private var sessionId: String? + private var sessionStartTimestamp: TimeInterval? + private var sessionActivityTimestamp: TimeInterval? + private let sessionLock = NSLock() + private var isAppInBackground = true + // 30 minutes in seconds + private let sessionActivityThreshold: TimeInterval = 60 * 30 + // 24 hours in seconds + private let sessionMaxLengthThreshold: TimeInterval = 24 * 60 * 60 + // Called when session id is cleared or changes + var onSessionIdChanged: () -> Void = {} + + @objc public func setSessionId(_ sessionId: String) { + setSessionIdInternal(sessionId, at: now(), reason: .customSessionId) + } + + private func isNotReactNative() -> Bool { + // for the RN SDK, the session is handled by the RN SDK itself + postHogSdkName != "posthog-react-native" + } + + /** + Returns the current session id, and manages id rotation logic + + In addition, this method handles core session cycling logic including: + - Creates a new session id when none exists (but only if app is foregrounded) + - if `readOnly` is false + - Rotates session after *30 minutes* of inactivity + - Clears session after *30 minutes* of inactivity (when app is backgrounded) + - Enforces a maximum session duration of *24 hours* + + - Parameters: + - timeNow: Reference timestamp used for evaluating session expiry rules. + Defaults to current system time. + - readOnly: When true, bypasses all session management logic and returns + the current session id without modifications. + Defaults to false. + + - Returns: Returns the existing session id, or a new one after performing validity checks + */ + func getSessionId( + at timeNow: Date = now(), + readOnly: Bool = false + ) -> String? { + let timestamp = timeNow.timeIntervalSince1970 + let (currentSessionId, lastActive, sessionStart, isBackgrounded) = sessionLock.withLock { + (sessionId, sessionActivityTimestamp, sessionStartTimestamp, isAppInBackground) + } + + // RN manages its own session, just return session id + guard isNotReactNative(), !readOnly else { + return currentSessionId + } + + // Create a new session id if empty + if currentSessionId.isNilOrEmpty, !isBackgrounded { + return rotateSession(force: true, at: timeNow, reason: .sessionIdEmpty) + } + + // Check if session has passed maximum inactivity length + if let lastActive, isExpired(timestamp, lastActive, sessionActivityThreshold) { + return isBackgrounded + ? clearSession(reason: .sessionTimeout) + : rotateSession(at: timeNow, reason: .sessionTimeout) + } + + // Check if session has passed maximum session length + if let sessionStart, isExpired(timestamp, sessionStart, sessionMaxLengthThreshold) { + return isBackgrounded + ? clearSession(reason: .sessionPastMaximumLength) + : rotateSession(at: timeNow, reason: .sessionPastMaximumLength) + } + + return currentSessionId + } + + func getNextSessionId() -> String? { + // if this is RN, return the current session id + guard isNotReactNative() else { + return sessionLock.withLock { sessionId } + } + + return rotateSession(force: true, at: now(), reason: .sessionStart) + } + + /// Creates a new session id and sets timestamps + func startSession(_ completion: (() -> Void)? = nil) { + guard isNotReactNative() else { return } + + rotateSession(force: true, at: now(), reason: .sessionStart) + completion?() + } + + /// Clears current session id and timestamps + func endSession(_ completion: (() -> Void)? = nil) { + guard isNotReactNative() else { return } + + clearSession(reason: .sessionEnd) + completion?() + } + + /// Resets current session id and timestamps + func resetSession() { + guard isNotReactNative() else { return } + + rotateSession(force: true, at: now(), reason: .sessionReset) + } + + /// Call this method to mark any user activity on this session + func touchSession() { + guard isNotReactNative() else { return } + + let (currentSessionId, lastActive) = sessionLock.withLock { + (sessionId, sessionActivityTimestamp) + } + + guard currentSessionId != nil else { return } + + let timeNow = now() + let timestamp = timeNow.timeIntervalSince1970 + + // Check if session has passed maximum inactivity length between user activity marks + if let lastActive, isExpired(timestamp, lastActive, sessionActivityThreshold) { + rotateSession(at: timeNow, reason: .sessionTimeout) + } else { + sessionLock.withLock { + sessionActivityTimestamp = timestamp + } + } + } + + /** + Rotates the current session id + + - Parameters: + - force: When true, creates a new session ID if current one is empty + - reason: The underlying reason behind this session ID rotation + - Returns: a new session id + */ + @discardableResult private func rotateSession(force: Bool = false, at timestamp: Date, reason: SessionIDChangeReason) -> String? { + // only rotate when session is empty + if !force { + let currentSessionId = sessionLock.withLock { sessionId } + if currentSessionId.isNilOrEmpty { + return currentSessionId + } + } + + let newSessionId = UUID.v7().uuidString + setSessionIdInternal(newSessionId, at: timestamp, reason: reason) + return newSessionId + } + + @discardableResult private func clearSession(reason: SessionIDChangeReason) -> String? { + setSessionIdInternal(nil, at: nil, reason: reason) + return nil + } + + private func setSessionIdInternal(_ sessionId: String?, at timestamp: Date?, reason: SessionIDChangeReason) { + let timestamp = timestamp?.timeIntervalSince1970 + + sessionLock.withLock { + self.sessionId = sessionId + self.sessionStartTimestamp = timestamp + self.sessionActivityTimestamp = timestamp + } + + onSessionIdChanged() + + if let sessionId { + hedgeLog("New session id created \(sessionId) (\(reason))") + } else { + hedgeLog("Session id cleared - reason: (\(reason))") + } + } + + private var didBecomeActiveToken: RegistrationToken? + private var didEnterBackgroundToken: RegistrationToken? + + private func registerNotifications() { + let lifecyclePublisher = DI.main.appLifecyclePublisher + didBecomeActiveToken = lifecyclePublisher.onDidBecomeActive { [weak self] in + guard let self, sessionLock.withLock({ self.isAppInBackground }) else { + return + } + + // we consider foregrounding an app an activity on the current session + touchSession() + sessionLock.withLock { self.isAppInBackground = false } + } + didEnterBackgroundToken = lifecyclePublisher.onDidEnterBackground { [weak self] in + guard let self, !sessionLock.withLock({ self.isAppInBackground }) else { + return + } + + // we consider backgrounding the app an activity on the current session + touchSession() + sessionLock.withLock { self.isAppInBackground = true } + } + } + + private var applicationEventToken: RegistrationToken? + + private func registerApplicationSendEvent() { + #if os(iOS) || os(tvOS) + guard let config, config.enableSwizzling else { + return + } + applicationEventToken = DI.main.applicationEventPublisher.onApplicationEvent { [weak self] _, _ in + // update "last active" session + // we want to keep track of the idle time, so we need to maintain a timestamp on the last interactions of the user with the app. UIEvents are a good place to do so since it means that the user is actively interacting with the app (e.g not just noise background activity) + self?.queue.async { + self?.touchSession() + } + } + #endif + } + + private func isExpired(_ timeNow: TimeInterval, _ timeThen: TimeInterval, _ threshold: TimeInterval) -> Bool { + max(timeNow - timeThen, 0) > threshold + } +} diff --git a/Pods/PostHog/PostHog/PostHogStorage.swift b/Pods/PostHog/PostHog/PostHogStorage.swift new file mode 100644 index 0000000..5bde9bd --- /dev/null +++ b/Pods/PostHog/PostHog/PostHogStorage.swift @@ -0,0 +1,419 @@ +// +// PostHogStorage.swift +// PostHog +// +// Created by Ben White on 08.02.23. +// + +import Foundation + +/** + # Storage + + Note for tvOS: + As tvOS restricts access to persisted Application Support directory, we use Library/Caches instead for storage + + If needed, we can use UserDefaults for lightweight data - according to Apple, you can use UserDefaults to persist up to 500KB of data on tvOS + see: https://developer.apple.com/forums/thread/16967?answerId=50696022#50696022 + */ +func applicationSupportDirectoryURL() -> URL { + #if os(tvOS) + // tvOS restricts access to Application Support directory on physical devices + // Use Library/Caches directory which may have less frequent eviction behavior than temp (which is purged when the app quits) + let searchPath: FileManager.SearchPathDirectory = .cachesDirectory + #else + let searchPath: FileManager.SearchPathDirectory = .applicationSupportDirectory + #endif + + let url = FileManager.default.urls(for: searchPath, in: .userDomainMask).first! + let bundleIdentifier = getBundleIdentifier() + + return url.appendingPathComponent(bundleIdentifier) +} + +/** + + From Apple Docs: + In iOS, the value is nil when the group identifier is invalid. In macOS, a URL of the expected form is always + returned, even if the app group is invalid, so be sure to test that you can access the underlying directory + before attempting to use it. + + MacOS: The system also creates the Library/Application Support, Library/Caches, and Library/Preferences + subdirectories inside the group directory the first time you use it + iOS: The system creates only the Library/Caches subdirectory automatically + + see: https://developer.apple.com/documentation/foundation/filemanager/1412643-containerurl/ + */ +func appGroupContainerUrl(config: PostHogConfig) -> URL? { + guard let appGroupIdentifier = config.appGroupIdentifier else { return nil } + + #if os(tvOS) + // tvOS: Due to stricter sandbox rules, creating "Application Support" directory is not possible on tvOS + let librarySubPath = "Library/Caches/" + #else + let librarySubPath = "Library/Application Support/" + #endif + + let libraryUrl = FileManager.default + .containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier)? + .appendingPathComponent(librarySubPath) + + guard let url = libraryUrl?.appendingPathComponent(appGroupIdentifier) else { return nil } + + createDirectoryAtURLIfNeeded(url: url) + + // Merges a legacy container (using bundleIdentifier) into the new container using appGroupIdentifier + mergeLegacyContainerIfNeeded(within: libraryUrl, to: url) + + return directoryExists(url) ? url : nil +} + +func getBundleIdentifier() -> String { + #if TESTING // only visible to test targets + return Bundle.main.bundleIdentifier ?? "com.posthog.test" + #else + return Bundle.main.bundleIdentifier! + #endif +} + +/** + Merges content from a legacy container directory into the current app group container. + + This function handles the migration of PostHog data from the old storage location (using `bundleIdentifier`) + to the new app group shared container location (using `appGroupIdentifier`). + + Migration rules: + - Files that already exist at the destination are skipped (no overwrite) + - The anonymousId from the first processed container (legacy or current) is preserved to maintain user identity + - Successfully migrated files are deleted from the source + - Empty directories are cleaned up after migration + - The entire folder structure is preserved during migration + + - Parameters: + - libraryUrl: The base library URL where both legacy and new containers might exist + - destinationUrl: The target app group container URL where files should be migrated + */ +func mergeLegacyContainerIfNeeded(within libraryUrl: URL?, to destinationUrl: URL) { + let bundleIdentifier = getBundleIdentifier() + guard let sourceUrl = libraryUrl?.appendingPathComponent(bundleIdentifier), directoryExists(sourceUrl) else { + return + } + + hedgeLog("Legacy folder found at \(sourceUrl), merging...") + + // Migrate all contents from the legacy container + migrateDirectoryContents(from: sourceUrl, to: destinationUrl) + + // Try to remove the source directory if it's empty + if removeIfEmpty(sourceUrl) { + hedgeLog("Successfully migrated and removed legacy folder at \(sourceUrl)") + } +} + +/** + Removes a directory if it's empty. + + - Parameters: + - url: The directory URL to potentially remove + - Returns: `true` if the directory was removed, `false` otherwise + */ +@discardableResult +func removeIfEmpty(_ url: URL) -> Bool { + let remainingItems = try? FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: []) + if remainingItems?.isEmpty == true { + do { + try FileManager.default.removeItem(at: url) + return true + } catch { + hedgeLog("Failed to remove empty directory at \(url.path): \(error)") + } + } + return false +} + +/** + Migrates a single file from source to destination. + + Migration rules: + - If the file doesn't exist at destination, it's copied and then deleted from source + - If the file already exists at destination, only the source file is deleted + + - Parameters: + - sourceFile: The source file URL + - destinationFile: The destination file URL + - Throws: Any errors that occur during file operations + */ +func migrateFile(from sourceFile: URL, to destinationFile: URL) throws { + if !FileManager.default.fileExists(atPath: destinationFile.path) { + try FileManager.default.copyItem(at: sourceFile, to: destinationFile) + } + // Always delete source file after processing (whether copied or skipped) + try FileManager.default.removeItem(at: sourceFile) +} + +/** + Recursively migrates all contents from a source directory to a destination directory. + + - Parameters: + - sourceDir: The source directory URL + - destinationDir: The destination directory URL + */ +func migrateDirectoryContents(from sourceDir: URL, to destinationDir: URL) { + do { + // Create destination directory if it doesn't exist (we need to call this here again as the function is recursive) + createDirectoryAtURLIfNeeded(url: destinationDir) + + // Get all items in source directory + let items = try FileManager.default.contentsOfDirectory(at: sourceDir, includingPropertiesForKeys: nil, options: []) + + for item in items { + let destinationItem = destinationDir.appendingPathComponent(item.lastPathComponent) + + // Check if it's a directory + var isDirectory: ObjCBool = false + if FileManager.default.fileExists(atPath: item.path, isDirectory: &isDirectory) { + if isDirectory.boolValue { + // Recursively migrate subdirectory (preserving the folder structure) + migrateDirectoryContents(from: item, to: destinationItem) + // Remove empty directory after migration + removeIfEmpty(item) + } else { + // Migrate file + do { + try migrateFile(from: item, to: destinationItem) + } catch { + hedgeLog("Failed to migrate file from \(item.path) to \(destinationItem.path): \(error)") + } + } + } + } + } catch { + hedgeLog("Error reading directory contents at \(sourceDir.path): \(error)") + } +} + +class PostHogStorage { + // when adding or removing items here, make sure to update the reset method + enum StorageKey: String, CaseIterable { + case distinctId = "posthog.distinctId" + case anonymousId = "posthog.anonymousId" + case queue = "posthog.queueFolder" // NOTE: This is different to posthog-ios v2 + case oldQeueue = "posthog.queue.plist" + case replayQeueue = "posthog.replayFolder" + case enabledFeatureFlags = "posthog.enabledFeatureFlags" + case enabledFeatureFlagPayloads = "posthog.enabledFeatureFlagPayloads" + case flags = "posthog.flags" + case groups = "posthog.groups" + case registerProperties = "posthog.registerProperties" + case optOut = "posthog.optOut" + case sessionReplay = "posthog.sessionReplay" + case isIdentified = "posthog.isIdentified" + case personProcessingEnabled = "posthog.enabledPersonProcessing" + case remoteConfig = "posthog.remoteConfig" + case surveySeen = "posthog.surveySeen" + case requestId = "posthog.requestId" + case personPropertiesForFlags = "posthog.personPropertiesForFlags" + case groupPropertiesForFlags = "posthog.groupPropertiesForFlags" + } + + // The location for storing data that we always want to keep + let appFolderUrl: URL + + init(_ config: PostHogConfig) { + appFolderUrl = Self.getAppFolderUrl(from: config) + + // migrate legacy storage if needed + Self.migrateLegacyStorage(from: config, to: appFolderUrl) + } + + func url(forKey key: StorageKey) -> URL { + appFolderUrl.appendingPathComponent(key.rawValue) + } + + // The "data" methods are the core for storing data and differ between Modes + // All other typed storage methods call these + private func getData(forKey: StorageKey) -> Data? { + let url = url(forKey: forKey) + + do { + if FileManager.default.fileExists(atPath: url.path) { + return try Data(contentsOf: url) + } + } catch { + hedgeLog("Error reading data from key \(forKey): \(error)") + } + return nil + } + + private func setData(forKey: StorageKey, contents: Data?) { + var url = url(forKey: forKey) + + do { + if contents == nil { + deleteSafely(url) + return + } + + try contents?.write(to: url) + + var resourceValues = URLResourceValues() + resourceValues.isExcludedFromBackup = true + try url.setResourceValues(resourceValues) + } catch { + hedgeLog("Failed to write data for key '\(forKey)' error: \(error)") + } + } + + private func getJson(forKey key: StorageKey) -> Any? { + guard let data = getData(forKey: key) else { return nil } + + do { + return try JSONSerialization.jsonObject(with: data) + } catch { + hedgeLog("Failed to serialize key '\(key)' error: \(error)") + } + return nil + } + + private func setJson(forKey key: StorageKey, json: Any) { + var jsonObject: Any? + + if let dictionary = json as? [AnyHashable: Any] { + jsonObject = dictionary + } else if let array = json as? [Any] { + jsonObject = array + } else { + // TRICKY: This is weird legacy behaviour storing the data as a dictionary + jsonObject = [key.rawValue: json] + } + + var data: Data? + do { + data = try JSONSerialization.data(withJSONObject: jsonObject!) + } catch { + hedgeLog("Failed to serialize key '\(key)' error: \(error)") + } + setData(forKey: key, contents: data) + } + + /** + There are cases where applications using posthog-ios want to share analytics data between host app and + an app extension, Widget or App Clip. If there's a defined `appGroupIdentifier` in configuration, + we want to use a shared container for storing data so that extensions correctly identify a user (and batch process events) + */ + private static func getBaseAppFolderUrl(from configuration: PostHogConfig) -> URL { + appGroupContainerUrl(config: configuration) ?? applicationSupportDirectoryURL() + } + + private static func migrateItem(at sourceUrl: URL, to destinationUrl: URL, fileManager: FileManager) throws { + guard fileManager.fileExists(atPath: sourceUrl.path) else { return } + // Copy file or directory over (if it doesn't exist) + if !fileManager.fileExists(atPath: destinationUrl.path) { + try fileManager.copyItem(at: sourceUrl, to: destinationUrl) + } + } + + private static func migrateLegacyStorage(from configuration: PostHogConfig, to apiDir: URL) { + let legacyUrl = getBaseAppFolderUrl(from: configuration) + if directoryExists(legacyUrl) { + let fileManager = FileManager.default + + // Migrate old files that correspond to StorageKey values + for storageKey in StorageKey.allCases { + let legacyFileUrl = legacyUrl.appendingPathComponent(storageKey.rawValue) + let newFileUrl = apiDir.appendingPathComponent(storageKey.rawValue) + + do { + // Migrate the item and its contents if it exists + try migrateItem(at: legacyFileUrl, to: newFileUrl, fileManager: fileManager) + } catch { + hedgeLog("Error during storage migration for file \(storageKey.rawValue) at path \(legacyFileUrl.path): \(error)") + } + + // Remove the legacy item after successful migration + if fileManager.fileExists(atPath: legacyFileUrl.path) { + do { + try fileManager.removeItem(at: legacyFileUrl) + } catch { + hedgeLog("Could not delete file \(storageKey.rawValue) at path \(legacyFileUrl.path): \(error)") + } + } + } + } + } + + private static func getAppFolderUrl(from configuration: PostHogConfig) -> URL { + let apiDir = getBaseAppFolderUrl(from: configuration) + .appendingPathComponent(configuration.apiKey) + + createDirectoryAtURLIfNeeded(url: apiDir) + + return apiDir + } + + func reset(keepAnonymousId: Bool = false) { + // sadly the StorageKey.allCases does not work here + deleteSafely(url(forKey: .distinctId)) + if !keepAnonymousId { + deleteSafely(url(forKey: .anonymousId)) + } + // .queue, .replayQeueue not needed since it'll be deleted by the queue.clear() + deleteSafely(url(forKey: .oldQeueue)) + deleteSafely(url(forKey: .flags)) + deleteSafely(url(forKey: .enabledFeatureFlags)) + deleteSafely(url(forKey: .enabledFeatureFlagPayloads)) + deleteSafely(url(forKey: .groups)) + deleteSafely(url(forKey: .registerProperties)) + deleteSafely(url(forKey: .optOut)) + deleteSafely(url(forKey: .sessionReplay)) + deleteSafely(url(forKey: .isIdentified)) + deleteSafely(url(forKey: .personProcessingEnabled)) + deleteSafely(url(forKey: .remoteConfig)) + deleteSafely(url(forKey: .surveySeen)) + deleteSafely(url(forKey: .requestId)) + deleteSafely(url(forKey: .personPropertiesForFlags)) + deleteSafely(url(forKey: .groupPropertiesForFlags)) + } + + func remove(key: StorageKey) { + let url = url(forKey: key) + + deleteSafely(url) + } + + func getString(forKey key: StorageKey) -> String? { + let value = getJson(forKey: key) + if let stringValue = value as? String { + return stringValue + } else if let dictValue = value as? [String: String] { + return dictValue[key.rawValue] + } + return nil + } + + func setString(forKey key: StorageKey, contents: String) { + setJson(forKey: key, json: contents) + } + + func getDictionary(forKey key: StorageKey) -> [AnyHashable: Any]? { + getJson(forKey: key) as? [AnyHashable: Any] + } + + func setDictionary(forKey key: StorageKey, contents: [AnyHashable: Any]) { + setJson(forKey: key, json: contents) + } + + func getBool(forKey key: StorageKey) -> Bool? { + let value = getJson(forKey: key) + if let boolValue = value as? Bool { + return boolValue + } else if let dictValue = value as? [String: Bool] { + return dictValue[key.rawValue] + } + return nil + } + + func setBool(forKey key: StorageKey, contents: Bool) { + setJson(forKey: key, json: contents) + } +} diff --git a/Pods/PostHog/PostHog/PostHogStorageManager.swift b/Pods/PostHog/PostHog/PostHogStorageManager.swift new file mode 100644 index 0000000..685473e --- /dev/null +++ b/Pods/PostHog/PostHog/PostHogStorageManager.swift @@ -0,0 +1,163 @@ +// +// PostHogStorageManager.swift +// PostHog +// +// Created by Ben White on 08.02.23. +// + +import Foundation + +// Internal class to manage the storage metadata of the PostHog SDK +public class PostHogStorageManager { + private let storage: PostHogStorage! + + private let anonLock = NSLock() + private let distinctLock = NSLock() + private let identifiedLock = NSLock() + private let personProcessingLock = NSLock() + private let idGen: (UUID) -> UUID + + private var distinctId: String? + private var cachedDistinctId = false + private var anonymousId: String? + private var isIdentifiedValue: Bool? + private var personProcessingEnabled: Bool? + + init(_ config: PostHogConfig) { + storage = PostHogStorage(config) + idGen = config.getAnonymousId + } + + public func getAnonymousId() -> String { + anonLock.withLock { + if anonymousId == nil { + var anonymousId = storage.getString(forKey: .anonymousId) + + if anonymousId == nil { + let uuid = UUID.v7() + anonymousId = idGen(uuid).uuidString + setAnonId(anonymousId ?? "") + } else { + // update the memory value + self.anonymousId = anonymousId + } + } + } + + return anonymousId ?? "" + } + + public func setAnonymousId(_ id: String) { + anonLock.withLock { + setAnonId(id) + } + } + + private func setAnonId(_ id: String) { + anonymousId = id + storage.setString(forKey: .anonymousId, contents: id) + } + + public func getDistinctId() -> String { + var distinctId: String? + distinctLock.withLock { + if self.distinctId == nil { + // since distinctId is nil until its identified, no need to read from + // cache every single time, otherwise anon users will never used the + // cached values + if !cachedDistinctId { + distinctId = storage.getString(forKey: .distinctId) + cachedDistinctId = true + } + + // do this to not assign the AnonymousId to the DistinctId, its just a fallback + if distinctId == nil { + distinctId = getAnonymousId() + } else { + // update the memory value + self.distinctId = distinctId + } + } else { + // read from memory + distinctId = self.distinctId + } + } + return distinctId ?? "" + } + + public func setDistinctId(_ id: String) { + distinctLock.withLock { + distinctId = id + storage.setString(forKey: .distinctId, contents: id) + } + } + + public func isIdentified() -> Bool { + identifiedLock.withLock { + if isIdentifiedValue == nil { + isIdentifiedValue = storage.getBool(forKey: .isIdentified) ?? (getDistinctId() != getAnonymousId()) + } + } + return isIdentifiedValue ?? false + } + + public func setIdentified(_ isIdentified: Bool) { + identifiedLock.withLock { + isIdentifiedValue = isIdentified + storage.setBool(forKey: .isIdentified, contents: isIdentified) + } + } + + public func isPersonProcessing() -> Bool { + personProcessingLock.withLock { + if personProcessingEnabled == nil { + personProcessingEnabled = storage.getBool(forKey: .personProcessingEnabled) ?? false + } + } + return personProcessingEnabled ?? false + } + + public func setPersonProcessing(_ enable: Bool) { + personProcessingLock.withLock { + // only set if its different to avoid IO since this is called more often + if self.personProcessingEnabled != enable { + self.personProcessingEnabled = enable + storage.setBool(forKey: .personProcessingEnabled, contents: enable) + } + } + } + + public func reset(keepAnonymousId: Bool = false, _ resetStorage: Bool = false) { + // resetStorage is only used for testing, when the reset method is called, + // the storage is also cleared, so we don't do here to not do it twice. + distinctLock.withLock { + distinctId = nil + cachedDistinctId = false + if resetStorage { + storage.remove(key: .distinctId) + } + } + + if !keepAnonymousId { + anonLock.withLock { + anonymousId = nil + if resetStorage { + storage.remove(key: .anonymousId) + } + } + } + + identifiedLock.withLock { + isIdentifiedValue = nil + if resetStorage { + storage.remove(key: .isIdentified) + } + } + personProcessingLock.withLock { + personProcessingEnabled = nil + if resetStorage { + storage.remove(key: .personProcessingEnabled) + } + } + } +} diff --git a/Pods/PostHog/PostHog/PostHogSwizzler.swift b/Pods/PostHog/PostHog/PostHogSwizzler.swift new file mode 100644 index 0000000..7aaf73c --- /dev/null +++ b/Pods/PostHog/PostHog/PostHogSwizzler.swift @@ -0,0 +1,14 @@ +// +// PostHogSwizzler.swift +// PostHog +// +// Created by Manoel Aranda Neto on 26.03.24. +// + +import Foundation + +func swizzle(forClass: AnyClass, original: Selector, new: Selector) { + guard let originalMethod = class_getInstanceMethod(forClass, original) else { return } + guard let swizzledMethod = class_getInstanceMethod(forClass, new) else { return } + method_exchangeImplementations(originalMethod, swizzledMethod) +} diff --git a/Pods/PostHog/PostHog/PostHogVersion.swift b/Pods/PostHog/PostHog/PostHogVersion.swift new file mode 100644 index 0000000..8943742 --- /dev/null +++ b/Pods/PostHog/PostHog/PostHogVersion.swift @@ -0,0 +1,16 @@ +// +// PostHogVersion.swift +// PostHog +// +// Created by Manoel Aranda Neto on 13.10.23. +// + +import Foundation + +// if you change this, make sure to also change it in the podspec and check if the script scripts/bump-version.sh still works +// This property is internal only +public var postHogVersion = "3.34.0" + +public let postHogiOSSdkName = "posthog-ios" +// This property is internal only +public var postHogSdkName = postHogiOSSdkName diff --git a/Pods/PostHog/PostHog/Replay/ApplicationEventPublisher.swift b/Pods/PostHog/PostHog/Replay/ApplicationEventPublisher.swift new file mode 100644 index 0000000..98c5782 --- /dev/null +++ b/Pods/PostHog/PostHog/Replay/ApplicationEventPublisher.swift @@ -0,0 +1,130 @@ +// +// ApplicationEventPublisher.swift +// PostHog +// +// Created by Ioannis Josephides on 24/02/2025. +// + +import Foundation + +#if os(iOS) || os(tvOS) + import UIKit + + typealias ApplicationEventHandler = (_ event: UIEvent, _ date: Date) -> Void + + protocol ApplicationEventPublishing: AnyObject { + /// Registers a callback for a `UIApplication.sendEvent` + func onApplicationEvent(_ callback: @escaping ApplicationEventHandler) -> RegistrationToken + } + + final class ApplicationEventPublisher: BaseApplicationEventPublisher { + static let shared = ApplicationEventPublisher() + + private var hasSwizzled: Bool = false + + func start() { + swizzleSendEvent() + } + + func stop() { + unswizzleSendEvent() + } + + func swizzleSendEvent() { + guard !hasSwizzled else { return } + hasSwizzled = true + + swizzle( + forClass: UIApplication.self, + original: #selector(UIApplication.sendEvent(_:)), + new: #selector(UIApplication.sendEventOverride) + ) + } + + func unswizzleSendEvent() { + guard hasSwizzled else { return } + hasSwizzled = false + + // swizzling twice will exchange implementations back to original + swizzle( + forClass: UIApplication.self, + original: #selector(UIApplication.sendEvent(_:)), + new: #selector(UIApplication.sendEventOverride) + ) + } + + override func onApplicationEvent(_ callback: @escaping ApplicationEventHandler) -> RegistrationToken { + let id = UUID() + registrationLock.withLock { + self.onApplicationEventCallbacks[id] = callback + } + + // start on first callback registration + if !hasSwizzled { + start() + } + + return RegistrationToken { [weak self] in + // Registration token deallocated here + guard let self else { return } + let handlerCount = self.registrationLock.withLock { + self.onApplicationEventCallbacks[id] = nil + return self.onApplicationEventCallbacks.values.count + } + + // stop when there are no more callbacks + if handlerCount <= 0 { + self.stop() + } + } + } + + // Called from swizzled `UIApplication.sendEvent` + fileprivate func sendEvent(event: UIEvent, date: Date) { + notifyHandlers(uiEvent: event, date: date) + } + } + + class BaseApplicationEventPublisher: ApplicationEventPublishing { + fileprivate let registrationLock = NSLock() + + var onApplicationEventCallbacks: [UUID: ApplicationEventHandler] = [:] + + func onApplicationEvent(_ callback: @escaping ApplicationEventHandler) -> RegistrationToken { + let id = UUID() + registrationLock.withLock { + self.onApplicationEventCallbacks[id] = callback + } + + return RegistrationToken { [weak self] in + // Registration token deallocated here + guard let self else { return } + self.registrationLock.withLock { + self.onApplicationEventCallbacks[id] = nil + } + } + } + + func notifyHandlers(uiEvent: UIEvent, date: Date) { + let handlers = registrationLock.withLock { onApplicationEventCallbacks.values } + for handler in handlers { + notifyHander(handler, uiEvent: uiEvent, date: date) + } + } + + private func notifyHander(_ handler: @escaping ApplicationEventHandler, uiEvent: UIEvent, date: Date) { + if Thread.isMainThread { + handler(uiEvent, date) + } else { + DispatchQueue.main.async { handler(uiEvent, date) } + } + } + } + + extension UIApplication { + @objc func sendEventOverride(_ event: UIEvent) { + sendEventOverride(event) + ApplicationEventPublisher.shared.sendEvent(event: event, date: Date()) + } + } +#endif diff --git a/Pods/PostHog/PostHog/Replay/CGColor+Util.swift b/Pods/PostHog/PostHog/Replay/CGColor+Util.swift new file mode 100644 index 0000000..9fee8b5 --- /dev/null +++ b/Pods/PostHog/PostHog/Replay/CGColor+Util.swift @@ -0,0 +1,33 @@ +// +// CGColor+Util.swift +// PostHog +// +// Created by Manoel Aranda Neto on 21.03.24. +// + +#if os(iOS) + + import Foundation + import UIKit + + extension CGColor { + func toRGBString() -> String? { + // see dicussion: https://github.com/PostHog/posthog-ios/issues/226 + // Allow only CGColors with an intiialized value of `numberOfComponents` with a value in 3...4 range + // Loading dynamic colors from storyboard sometimes leads to some random values for numberOfComponents like `105553118884896` which crashes the app + guard + 3 ... 4 ~= numberOfComponents, // check range + let components = components, // we now assume it's safe to access `components` + components.count >= 3 + else { + return nil + } + + let red = Int(components[0] * 255) + let green = Int(components[1] * 255) + let blue = Int(components[2] * 255) + + return String(format: "#%02X%02X%02X", red, green, blue) + } + } +#endif diff --git a/Pods/PostHog/PostHog/Replay/CGSize+Util.swift b/Pods/PostHog/PostHog/Replay/CGSize+Util.swift new file mode 100644 index 0000000..2fb8380 --- /dev/null +++ b/Pods/PostHog/PostHog/Replay/CGSize+Util.swift @@ -0,0 +1,19 @@ +// +// CGSize+Util.swift +// PostHog +// +// Created by Manoel Aranda Neto on 24.07.24. +// + +#if os(iOS) + import Foundation + + extension CGSize { + func hasSize() -> Bool { + if width == 0 || height == 0 { + return false + } + return true + } + } +#endif diff --git a/Pods/PostHog/PostHog/Replay/Date+Util.swift b/Pods/PostHog/PostHog/Replay/Date+Util.swift new file mode 100644 index 0000000..4cb98d9 --- /dev/null +++ b/Pods/PostHog/PostHog/Replay/Date+Util.swift @@ -0,0 +1,18 @@ +// +// Date+Util.swift +// PostHog +// +// Created by Manoel Aranda Neto on 21.03.24. +// + +import Foundation + +extension Date { + func toMillis() -> Int64 { + Int64(timeIntervalSince1970 * 1000) + } +} + +public func dateToMillis(_ date: Date) -> Int64 { + date.toMillis() +} diff --git a/Pods/PostHog/PostHog/Replay/Float+Util.swift b/Pods/PostHog/PostHog/Replay/Float+Util.swift new file mode 100644 index 0000000..ee3eabe --- /dev/null +++ b/Pods/PostHog/PostHog/Replay/Float+Util.swift @@ -0,0 +1,20 @@ +// +// Float+Util.swift +// PostHog +// +// Created by Yiannis Josephides on 07/02/2025. +// + +import Foundation + +extension CGFloat { + func toInt() -> Int { + NSNumber(value: rounded()).intValue + } +} + +extension Double { + func toInt() -> Int { + NSNumber(value: rounded()).intValue + } +} diff --git a/Pods/PostHog/PostHog/Replay/MethodSwizzler.swift b/Pods/PostHog/PostHog/Replay/MethodSwizzler.swift new file mode 100644 index 0000000..1aa6c44 --- /dev/null +++ b/Pods/PostHog/PostHog/Replay/MethodSwizzler.swift @@ -0,0 +1,120 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +#if os(iOS) + import Foundation + + class MethodSwizzler { + struct FoundMethod: Hashable { + let method: Method + private let klass: AnyClass + + fileprivate init(method: Method, klass: AnyClass) { + self.method = method + self.klass = klass + } + + static func == (lhs: FoundMethod, rhs: FoundMethod) -> Bool { + let methodParity = (lhs.method == rhs.method) + let classParity = (NSStringFromClass(lhs.klass) == NSStringFromClass(rhs.klass)) + return methodParity && classParity + } + + func hash(into hasher: inout Hasher) { + let methodName = NSStringFromSelector(method_getName(method)) + let klassName = NSStringFromClass(klass) + let identifier = "\(methodName)|||\(klassName)" + hasher.combine(identifier) + } + } + + private var implementationCache: [FoundMethod: IMP] = [:] + var swizzledMethods: [FoundMethod] { + Array(implementationCache.keys) + } + + static func findMethod(with selector: Selector, in klass: AnyClass) throws -> FoundMethod { + /// NOTE: RUMM-452 as we never add/remove methods/classes at runtime, + /// search operation doesn't have to wrapped in sync {...} although it's visible in the interface + var headKlass: AnyClass? = klass + while let someKlass = headKlass { + if let foundMethod = findMethod(with: selector, in: someKlass) { + return FoundMethod(method: foundMethod, klass: someKlass) + } + headKlass = class_getSuperclass(headKlass) + } + throw InternalPostHogError(description: "\(NSStringFromSelector(selector)) is not found in \(NSStringFromClass(klass))") + } + + func originalImplementation(of found: FoundMethod) -> TypedIMP { + sync { + let originalImp: IMP = implementationCache[found] ?? method_getImplementation(found.method) + return unsafeBitCast(originalImp, to: TypedIMP.self) + } + } + + func swizzle( + _ foundMethod: FoundMethod, + impProvider: (TypedIMP) -> TypedBlockIMP + ) { + sync { + let currentIMP = method_getImplementation(foundMethod.method) + let currentTypedIMP = unsafeBitCast(currentIMP, to: TypedIMP.self) + let newImpBlock: TypedBlockIMP = impProvider(currentTypedIMP) + let newImp: IMP = imp_implementationWithBlock(newImpBlock) + + set(newIMP: newImp, for: foundMethod) + } + } + + /// Removes swizzling and resets the method to its original implementation. + func unswizzle() { + for foundMethod in swizzledMethods { + let originalTypedIMP = originalImplementation(of: foundMethod) + let originalIMP: IMP = unsafeBitCast(originalTypedIMP, to: IMP.self) + method_setImplementation(foundMethod.method, originalIMP) + } + } + + // MARK: - Private methods + + @discardableResult + private func sync(block: () -> T) -> T { + objc_sync_enter(self) + defer { objc_sync_exit(self) } + return block() + } + + private static func findMethod(with selector: Selector, in klass: AnyClass) -> Method? { + var methodsCount: UInt32 = 0 + let methodsCountPtr = withUnsafeMutablePointer(to: &methodsCount) { $0 } + guard let methods: UnsafeMutablePointer = class_copyMethodList(klass, methodsCountPtr) else { + return nil + } + defer { + free(methods) + } + for index in 0 ..< Int(methodsCount) { + let method = methods.advanced(by: index).pointee + if method_getName(method) == selector { + return method + } + } + return nil + } + + private func set(newIMP: IMP, for found: FoundMethod) { + if implementationCache[found] == nil { + implementationCache[found] = method_getImplementation(found.method) + } + method_setImplementation(found.method, newIMP) + } + } + + extension MethodSwizzler.FoundMethod { + var swizzlingName: String { "\(klass).\(method_getName(method))" } + } +#endif diff --git a/Pods/PostHog/PostHog/Replay/NetworkSample.swift b/Pods/PostHog/PostHog/Replay/NetworkSample.swift new file mode 100644 index 0000000..5c6bf4d --- /dev/null +++ b/Pods/PostHog/PostHog/Replay/NetworkSample.swift @@ -0,0 +1,58 @@ +// +// NetworkSample.swift +// PostHog +// +// Created by Manoel Aranda Neto on 26.03.24. +// + +#if os(iOS) + import Foundation + + struct NetworkSample { + let sessionId: String + let timeOrigin: Date + let entryType = "resource" + var name: String? + var responseStatus: Int? + var initiatorType = "fetch" + var httpMethod: String? + var duration: Int64? + var decodedBodySize: Int64? + + init(sessionId: String, timeOrigin: Date, url: String? = nil) { + self.timeOrigin = timeOrigin + self.sessionId = sessionId + name = url + } + + func toDict() -> [String: Any] { + var dict: [String: Any] = [ + "timestamp": timeOrigin.toMillis(), + "entryType": entryType, + "initiatorType": initiatorType, + ] + + if let name = name { + dict["name"] = name + } + + if let responseStatus = responseStatus { + dict["responseStatus"] = responseStatus + } + + if let httpMethod = httpMethod { + dict["method"] = httpMethod + } + + if let duration = duration { + dict["duration"] = duration + } + + if let decodedBodySize = decodedBodySize { + dict["transferSize"] = decodedBodySize + } + + return dict + } + } +#endif diff --git a/Pods/PostHog/PostHog/Replay/Optional+Util.swift b/Pods/PostHog/PostHog/Replay/Optional+Util.swift new file mode 100644 index 0000000..65a6038 --- /dev/null +++ b/Pods/PostHog/PostHog/Replay/Optional+Util.swift @@ -0,0 +1,12 @@ +// +// Optional+Util.swift +// PostHog +// +// Created by Yiannis Josephides on 20/01/2025. +// + +extension Optional where Wrapped: Collection { + var isNilOrEmpty: Bool { + self?.isEmpty ?? true + } +} diff --git a/Pods/PostHog/PostHog/Replay/Plugins/Console Logs/PostHogConsoleLogInterceptor.swift b/Pods/PostHog/PostHog/Replay/Plugins/Console Logs/PostHogConsoleLogInterceptor.swift new file mode 100644 index 0000000..eef36ad --- /dev/null +++ b/Pods/PostHog/PostHog/Replay/Plugins/Console Logs/PostHogConsoleLogInterceptor.swift @@ -0,0 +1,136 @@ +// +// PostHogConsoleLogInterceptor.swift +// PostHog +// +// Created by Ioannis Josephides on 05/05/2025. +// + +#if os(iOS) + import Foundation + + final class PostHogConsoleLogInterceptor { + private let maxLogStringSize = 2000 // Maximum number of characters allowed in a string + + struct ConsoleOutput { + let timestamp: Date + let text: String + let level: PostHogLogLevel + } + + static let shared = PostHogConsoleLogInterceptor() + + // Pipe redirection properties + private var stdoutPipe: Pipe? + private var stderrPipe: Pipe? + private var originalStdout: Int32 = -1 + private var originalStderr: Int32 = -1 + + private init() { /* Singleton */ } + + func startCapturing(config: PostHogConfig, callback: @escaping (ConsoleOutput) -> Void) { + stopCapturing() // cleanup + setupPipeRedirection(config: config, callback: callback) + } + + private func setupPipeRedirection(config: PostHogConfig, callback: @escaping (ConsoleOutput) -> Void) { + // Set stdout/stderr to unbuffered mode (_IONBF) to ensure real-time output capture. + // Without this, output might be buffered and only flushed when the buffer is full or + // when explicitly flushed, which is especially problematic without an attached debugger + setvbuf(stdout, nil, _IONBF, 0) + setvbuf(stderr, nil, _IONBF, 0) + + // Save original file descriptors + originalStdout = dup(STDOUT_FILENO) + originalStderr = dup(STDERR_FILENO) + + stdoutPipe = Pipe() + stderrPipe = Pipe() + + guard let stdoutPipe = stdoutPipe, let stderrPipe = stderrPipe else { return } + + // Redirect stdout and stderr to our pipes + dup2(stdoutPipe.fileHandleForWriting.fileDescriptor, STDOUT_FILENO) + dup2(stderrPipe.fileHandleForWriting.fileDescriptor, STDERR_FILENO) + + // Setup and handle pipe output + setupPipeSource(for: originalStdout, fileHandle: stdoutPipe.fileHandleForReading, config: config, callback: callback) + setupPipeSource(for: originalStderr, fileHandle: stderrPipe.fileHandleForReading, config: config, callback: callback) + } + + private func setupPipeSource(for originalFd: Int32, fileHandle: FileHandle, config: PostHogConfig, callback: @escaping (ConsoleOutput) -> Void) { + fileHandle.readabilityHandler = { [weak self] handle in + let data = handle.availableData + guard !data.isEmpty, + let output = String(data: data, encoding: .utf8), + let self = self else { return } + + // Write to original file descriptor, so logs appear normally + if originalFd != -1 { + if let data = output.data(using: .utf8) { + _ = data.withUnsafeBytes { ptr in + write(originalFd, ptr.baseAddress, ptr.count) + } + } + } + + self.processOutput(output, config: config, callback: callback) + } + } + + private func processOutput(_ output: String, config: PostHogConfig, callback: @escaping (ConsoleOutput) -> Void) { + // Skip internal logs and empty lines + // Note: Need to skip internal logs because `config.debug` may be enabled. If that's the case, then + // the process of capturing logs, will generate more logs, leading to an infinite loop. This relies on hedgeLog() format which should + // be okay, even not ideal + guard !output.contains("[PostHog]"), !output.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { + return + } + + // Process log entries from config + let entries = output + .components(separatedBy: CharacterSet.newlines) // split by line + .lazy + .filter { !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } // Skip empty strings and new lines + .compactMap(config.sessionReplayConfig.captureLogsConfig.logSanitizer) + + for entry in entries where shouldCaptureLog(entry: entry, config: config) { + callback(ConsoleOutput(timestamp: Date(), text: truncatedOutput(entry.message), level: entry.level)) + } + } + + /// Determines if the log message should be captured, based on config + private func shouldCaptureLog(entry: PostHogLogEntry, config: PostHogConfig) -> Bool { + entry.level.rawValue >= config.sessionReplayConfig.captureLogsConfig.minLogLevel.rawValue + } + + /// Console logs can be really large. + /// This function returns a truncated version of the console output if it exceeds `maxLogStringSize` + private func truncatedOutput(_ output: String) -> String { + guard output.count > maxLogStringSize else { return output } + return "\(output.prefix(maxLogStringSize))...[truncated]" + } + + func stopCapturing() { + // Restore original file descriptors + if originalStdout != -1 { + dup2(originalStdout, STDOUT_FILENO) + close(originalStdout) + originalStdout = -1 + } + + if originalStderr != -1 { + dup2(originalStderr, STDERR_FILENO) + close(originalStderr) + originalStderr = -1 + } + + // remove pipes + stdoutPipe?.fileHandleForReading.readabilityHandler = nil + stderrPipe?.fileHandleForReading.readabilityHandler = nil + stdoutPipe?.fileHandleForReading.closeFile() + stderrPipe?.fileHandleForReading.closeFile() + stdoutPipe = nil + stderrPipe = nil + } + } +#endif diff --git a/Pods/PostHog/PostHog/Replay/Plugins/Console Logs/PostHogLogEntry.swift b/Pods/PostHog/PostHog/Replay/Plugins/Console Logs/PostHogLogEntry.swift new file mode 100644 index 0000000..1d9ed8f --- /dev/null +++ b/Pods/PostHog/PostHog/Replay/Plugins/Console Logs/PostHogLogEntry.swift @@ -0,0 +1,37 @@ +// +// PostHogLogEntry.swift +// PostHog +// +// Created by Ioannis Josephides on 09/05/2025. +// + +#if os(iOS) + import Foundation + + /** + A model representing a processed console log entry for session replay. + + Describes a single console log entry after it has been processed by `PostHogSessionReplayConsoleLogConfig.logSanitizer`. + Each instance contains the log message content and its determined severity level. + */ + @objc public class PostHogLogEntry: NSObject { + /// The severity level of the log entry. + /// This determines how the log will be displayed in the session replay and + /// whether it will be captured based on `minLogLevel` setting. + @objc public let level: PostHogLogLevel + + /// The actual content of the log message. + /// This is the processed and sanitized log message + @objc public let message: String + + /// Creates a new console log result. + /// - Parameters: + /// - level: The severity level of the log entry + /// - message: The processed log message content + @objc public init(level: PostHogLogLevel, message: String) { + self.level = level + self.message = message + super.init() + } + } +#endif diff --git a/Pods/PostHog/PostHog/Replay/Plugins/Console Logs/PostHogLogLevel.swift b/Pods/PostHog/PostHog/Replay/Plugins/Console Logs/PostHogLogLevel.swift new file mode 100644 index 0000000..ff3f23d --- /dev/null +++ b/Pods/PostHog/PostHog/Replay/Plugins/Console Logs/PostHogLogLevel.swift @@ -0,0 +1,22 @@ +// +// PostHogLogLevel.swift +// PostHog +// +// Created by Ioannis Josephides on 09/05/2025. +// + +#if os(iOS) + import Foundation + + /// The severity level of a console log entry. + /// + /// Used to categorize logs by their severity in session replay. + @objc public enum PostHogLogLevel: Int { + /// Informational messages, debugging output, and general logs + case info + /// Warning messages indicating potential issues or deprecation notices + case warn + /// Error messages indicating failures or critical issues + case error + } +#endif diff --git a/Pods/PostHog/PostHog/Replay/Plugins/Console Logs/PostHogSessionReplayConsoleLogsPlugin.swift b/Pods/PostHog/PostHog/Replay/Plugins/Console Logs/PostHogSessionReplayConsoleLogsPlugin.swift new file mode 100644 index 0000000..a7f39ad --- /dev/null +++ b/Pods/PostHog/PostHog/Replay/Plugins/Console Logs/PostHogSessionReplayConsoleLogsPlugin.swift @@ -0,0 +1,86 @@ +// +// PostHogSessionReplayConsoleLogsPlugin.swift +// PostHog +// +// Created by Ioannis Josephides on 09/05/2025. +// + +#if os(iOS) + import Foundation + + final class PostHogSessionReplayConsoleLogsPlugin: PostHogSessionReplayPlugin { + private weak var postHog: PostHogSDK? + private var isActive = false + + func start(postHog: PostHogSDK) { + self.postHog = postHog + isActive = true + PostHogConsoleLogInterceptor.shared.startCapturing(config: postHog.config) { [weak self] output in + self?.handleConsoleLog(output) + } + hedgeLog("[Session Replay] Console logs plugin started") + } + + func stop() { + postHog = nil + isActive = false + PostHogConsoleLogInterceptor.shared.stopCapturing() + hedgeLog("[Session Replay] Console logs plugin stopped") + } + + func resume() { + guard !isActive, let postHog else { return } + isActive = true + PostHogConsoleLogInterceptor.shared.startCapturing(config: postHog.config) { [weak self] output in + self?.handleConsoleLog(output) + } + hedgeLog("[Session Replay] Console logs plugin resumed") + } + + func pause() { + guard isActive else { return } + isActive = false + PostHogConsoleLogInterceptor.shared.stopCapturing() + hedgeLog("[Session Replay] Console logs plugin paused") + } + + private func handleConsoleLog(_ output: PostHogConsoleLogInterceptor.ConsoleOutput) { + guard + isActive, + let postHog, + postHog.isSessionReplayActive(), + let sessionId = postHog.sessionManager.getSessionId(at: output.timestamp) + else { + return + } + + // `PostHogLogLevel`` needs to be an Int enum for objc interop + // So we need to convert this to a String before sending upstream + let level = switch output.level { + case .error: "error" + case .info: "info" + case .warn: "warn" + } + + var snapshotsData: [Any] = [] + let payloadData: [String: Any] = ["level": level, "payload": output.text] + let pluginData: [String: Any] = ["plugin": "rrweb/console@1", "payload": payloadData] + + snapshotsData.append([ + "type": 6, + "data": pluginData, + "timestamp": output.timestamp.toMillis(), + ]) + + postHog.capture( + "$snapshot", + properties: [ + "$snapshot_source": "mobile", + "$snapshot_data": snapshotsData, + "$session_id": sessionId, + ], + timestamp: output.timestamp + ) + } + } +#endif diff --git a/Pods/PostHog/PostHog/Replay/Plugins/Network/PostHogSessionReplayNetworkPlugin.swift b/Pods/PostHog/PostHog/Replay/Plugins/Network/PostHogSessionReplayNetworkPlugin.swift new file mode 100644 index 0000000..af31790 --- /dev/null +++ b/Pods/PostHog/PostHog/Replay/Plugins/Network/PostHogSessionReplayNetworkPlugin.swift @@ -0,0 +1,89 @@ +// +// PostHogSessionReplayNetworkPlugin.swift +// PostHog +// +// Created by Ioannis Josephides on 28/05/2025. +// + +#if os(iOS) + import Foundation + + /// Session replay plugin that captures network requests using URLSession swizzling. + class PostHogSessionReplayNetworkPlugin: PostHogSessionReplayPlugin { + private var sessionSwizzler: URLSessionSwizzler? + private var postHog: PostHogSDK? + private var isActive = false + + func start(postHog: PostHogSDK) { + self.postHog = postHog + do { + sessionSwizzler = try URLSessionSwizzler( + shouldCapture: shouldCaptureNetworkSample, + onCapture: handleNetworkSample, + getSessionId: { [weak self] date in + self?.postHog?.sessionManager.getSessionId(at: date) + } + ) + sessionSwizzler?.swizzle() + hedgeLog("[Session Replay] Network telemetry plugin started") + isActive = true + } catch { + hedgeLog("[Session Replay] Failed to initialize network telemetry: \(error)") + } + } + + func stop() { + sessionSwizzler?.unswizzle() + sessionSwizzler = nil + postHog = nil + isActive = false + hedgeLog("[Session Replay] Network telemetry plugin stopped") + } + + func resume() { + guard !isActive else { return } + isActive = true + hedgeLog("[Session Replay] Network telemetry plugin resumed") + } + + func pause() { + guard isActive else { return } + isActive = false + hedgeLog("[Session Replay] Network telemetry plugin paused") + } + + private func shouldCaptureNetworkSample() -> Bool { + guard let postHog else { return false } + return isActive && postHog.config.sessionReplayConfig.captureNetworkTelemetry && postHog.isSessionReplayActive() + } + + private func handleNetworkSample(sample: NetworkSample) { + guard let postHog else { return } + + let timestamp = sample.timeOrigin + + var snapshotsData: [Any] = [] + + let requestsData = [sample.toDict()] + let payloadData: [String: Any] = ["requests": requestsData] + let pluginData: [String: Any] = ["plugin": "rrweb/network@1", "payload": payloadData] + + let data: [String: Any] = [ + "type": 6, + "data": pluginData, + "timestamp": timestamp.toMillis(), + ] + snapshotsData.append(data) + + postHog.capture( + "$snapshot", + properties: [ + "$snapshot_source": "mobile", + "$snapshot_data": snapshotsData, + "$session_id": sample.sessionId, + ], + timestamp: sample.timeOrigin + ) + } + } +#endif diff --git a/Pods/PostHog/PostHog/Replay/Plugins/Network/URLSessionExtension.swift b/Pods/PostHog/PostHog/Replay/Plugins/Network/URLSessionExtension.swift new file mode 100644 index 0000000..fa43ed6 --- /dev/null +++ b/Pods/PostHog/PostHog/Replay/Plugins/Network/URLSessionExtension.swift @@ -0,0 +1,231 @@ +#if os(iOS) + import Foundation + + public extension URLSession { + private func getMonotonicTimeInMilliseconds() -> UInt64 { + // Get the raw mach time + let machTime = mach_absolute_time() + + // Get timebase info to convert to nanoseconds + var timebaseInfo = mach_timebase_info_data_t() + mach_timebase_info(&timebaseInfo) + + // Convert mach time to nanoseconds + let nanoTime = machTime * UInt64(timebaseInfo.numer) / UInt64(timebaseInfo.denom) + + // Convert nanoseconds to milliseconds + let milliTime = nanoTime / 1_000_000 + + return milliTime + } + + private func executeRequest(request: URLRequest? = nil, + action: () async throws -> (Data, URLResponse), + postHog: PostHogSDK?) async throws -> (Data, URLResponse) + { + let timestamp = Date() + let startMillis = getMonotonicTimeInMilliseconds() + var endMillis: UInt64? + let sessionId = postHog?.sessionManager.getSessionId(at: timestamp) + do { + let (data, response) = try await action() + endMillis = getMonotonicTimeInMilliseconds() + captureData(request: request, + response: response, + sessionId: sessionId, + timestamp: timestamp, + start: startMillis, + end: endMillis, + postHog: postHog) + return (data, response) + } catch { + captureData(request: request, + response: nil, + sessionId: sessionId, + timestamp: timestamp, + start: startMillis, + end: endMillis, + postHog: postHog) + throw error + } + } + + private func executeRequest(request: URLRequest? = nil, + action: () async throws -> (URL, URLResponse), + postHog: PostHogSDK?) async throws -> (URL, URLResponse) + { + let timestamp = Date() + let startMillis = getMonotonicTimeInMilliseconds() + var endMillis: UInt64? + let sessionId = postHog?.sessionManager.getSessionId(at: timestamp) + do { + let (url, response) = try await action() + endMillis = getMonotonicTimeInMilliseconds() + captureData(request: request, + response: response, + sessionId: sessionId, + timestamp: timestamp, + start: startMillis, + end: endMillis, + postHog: postHog) + return (url, response) + } catch { + captureData(request: request, + response: nil, + sessionId: sessionId, + timestamp: timestamp, + start: startMillis, + end: endMillis, + postHog: postHog) + throw error + } + } + + func postHogData(for request: URLRequest, postHog: PostHogSDK? = nil) async throws -> (Data, URLResponse) { + try await executeRequest(request: request, action: { try await data(for: request) }, postHog: postHog) + } + + func postHogData(from url: URL, postHog: PostHogSDK? = nil) async throws -> (Data, URLResponse) { + try await executeRequest(action: { try await data(from: url) }, postHog: postHog) + } + + func postHogUpload( + for request: URLRequest, + fromFile fileURL: URL, + postHog: PostHogSDK? = nil + ) async throws -> (Data, URLResponse) { + try await executeRequest(request: request, action: { try await upload(for: request, fromFile: fileURL) }, postHog: postHog) + } + + func postHogUpload( + for request: URLRequest, + from bodyData: Data, + postHog: PostHogSDK? = nil + ) async throws -> (Data, URLResponse) { + try await executeRequest(request: request, action: { try await upload(for: request, from: bodyData) }, postHog: postHog) + } + + @available(iOS 15.0, *) + func postHogData( + for request: URLRequest, + delegate: (any URLSessionTaskDelegate)? = nil, + postHog: PostHogSDK? = nil + ) async throws -> (Data, URLResponse) { + try await executeRequest(request: request, action: { try await data(for: request, delegate: delegate) }, postHog: postHog) + } + + @available(iOS 15.0, *) + func postHogData( + from url: URL, + delegate: (any URLSessionTaskDelegate)? = nil, + postHog: PostHogSDK? = nil + ) async throws -> (Data, URLResponse) { + try await executeRequest(action: { try await data(from: url, delegate: delegate) }, postHog: postHog) + } + + @available(iOS 15.0, *) + func postHogUpload( + for request: URLRequest, + fromFile fileURL: URL, + delegate: (any URLSessionTaskDelegate)? = nil, + postHog: PostHogSDK? = nil + ) async throws -> (Data, URLResponse) { + try await executeRequest(request: request, action: { try await upload(for: request, fromFile: fileURL, delegate: delegate) }, postHog: postHog) + } + + @available(iOS 15.0, *) + func postHogUpload( + for request: URLRequest, + from bodyData: Data, + delegate: (any URLSessionTaskDelegate)? = nil, + postHog: PostHogSDK? = nil + ) async throws -> (Data, URLResponse) { + try await executeRequest(request: request, action: { try await upload(for: request, from: bodyData, delegate: delegate) }, postHog: postHog) + } + + @available(iOS 15.0, *) + func postHogDownload( + for request: URLRequest, + delegate: (any URLSessionTaskDelegate)? = nil, + postHog: PostHogSDK? = nil + ) async throws -> (URL, URLResponse) { + try await executeRequest(request: request, action: { try await download(for: request, delegate: delegate) }, postHog: postHog) + } + + @available(iOS 15.0, *) + func postHogDownload( + from url: URL, + delegate: (any URLSessionTaskDelegate)? = nil, + postHog: PostHogSDK? = nil + ) async throws -> (URL, URLResponse) { + try await executeRequest(action: { try await download(from: url, delegate: delegate) }, postHog: postHog) + } + + @available(iOS 15.0, *) + func postHogDownload( + resumeFrom resumeData: Data, + delegate: (any URLSessionTaskDelegate)? = nil, + postHog: PostHogSDK? = nil + ) async throws -> (URL, URLResponse) { + try await executeRequest(action: { try await download(resumeFrom: resumeData, delegate: delegate) }, postHog: postHog) + } + + // MARK: Private methods + + private func captureData( + request: URLRequest? = nil, + response: URLResponse? = nil, + sessionId: String?, + timestamp: Date, + start: UInt64, + end: UInt64? = nil, + postHog: PostHogSDK? + ) { + let instance = postHog ?? PostHogSDK.shared + + // we don't check config.sessionReplayConfig.captureNetworkTelemetry here since this extension + // has to be called manually anyway + guard let sessionId, instance.isSessionReplayActive() else { + return + } + let currentEnd = end ?? getMonotonicTimeInMilliseconds() + + PostHogReplayIntegration.dispatchQueue.async { + var snapshotsData: [Any] = [] + + var requestsData: [String: Any] = ["duration": currentEnd - start, + "method": request?.httpMethod ?? "GET", + "name": request?.url?.absoluteString ?? (response?.url?.absoluteString ?? ""), + "initiatorType": "fetch", + "entryType": "resource", + "timestamp": timestamp.toMillis()] + + // the UI special case if the transferSize is 0 as coming from cache + let transferSize = Int64(request?.httpBody?.count ?? 0) + (response?.expectedContentLength ?? 0) + if transferSize > 0 { + requestsData["transferSize"] = transferSize + } + + if let urlResponse = response as? HTTPURLResponse { + requestsData["responseStatus"] = urlResponse.statusCode + } + + let payloadData: [String: Any] = ["requests": [requestsData]] + let pluginData: [String: Any] = ["plugin": "rrweb/network@1", "payload": payloadData] + + let recordingData: [String: Any] = ["type": 6, "data": pluginData, "timestamp": timestamp.toMillis()] + snapshotsData.append(recordingData) + + instance.capture( + "$snapshot", + properties: [ + "$snapshot_source": "mobile", + "$snapshot_data": snapshotsData, + "$session_id": sessionId, + ], + timestamp: timestamp + ) + } + } + } +#endif diff --git a/Pods/PostHog/PostHog/Replay/Plugins/Network/URLSessionInterceptor.swift b/Pods/PostHog/PostHog/Replay/Plugins/Network/URLSessionInterceptor.swift new file mode 100644 index 0000000..b073e80 --- /dev/null +++ b/Pods/PostHog/PostHog/Replay/Plugins/Network/URLSessionInterceptor.swift @@ -0,0 +1,163 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +#if os(iOS) + + import Foundation + + class URLSessionInterceptor { + private let tasksLock = NSLock() + private let shouldCapture: () -> Bool + private let onCapture: (NetworkSample) -> Void + private let getSessionId: (Date) -> String? + + init(shouldCapture: @escaping () -> Bool, onCapture: @escaping (NetworkSample) -> Void, getSessionId: @escaping (Date) -> String?) { + self.shouldCapture = shouldCapture + self.onCapture = onCapture + self.getSessionId = getSessionId + } + + /// An internal queue for synchronising the access to `samplesByTask`. + private let queue = DispatchQueue(label: "com.posthog.URLSessionInterceptor", target: .global(qos: .utility)) + private var samplesByTask: [URLSessionTask: NetworkSample] = [:] + + // MARK: - Interception Flow + + /// Notifies the `URLSessionTask` creation. + /// This method should be called as soon as the task was created. + /// - Parameter task: the task object obtained from `URLSession`. + func taskCreated(task: URLSessionTask, session _: URLSession? = nil) { + guard shouldCapture() else { + return + } + + guard let request = task.originalRequest else { + return + } + + guard let url = request.url else { + return + } + + let date = now() + + guard let sessionId = getSessionId(date) else { + return + } + + queue.async { + let sample = NetworkSample( + sessionId: sessionId, + timeOrigin: date, + url: url.absoluteString + ) + + self.tasksLock.withLock { + self.samplesByTask[task] = sample + } + + self.finishAll() + } + } + + /// Notifies the `URLSessionTask` completion. + /// This method should be called as soon as the task was completed. + /// - Parameter task: the task object obtained from `URLSession`. + /// - Parameter error: optional `Error` if the task completed with error. + func taskCompleted(task: URLSessionTask, error _: Error?) { + guard shouldCapture() else { + return + } + + let date = Date() + + queue.async { + var sampleTask: NetworkSample? + self.tasksLock.withLock { + sampleTask = self.samplesByTask[task] + } + + guard var sample = sampleTask else { + return + } + + self.finish(task: task, sample: &sample, date: date) + + self.finishAll() + } + } + + private func finish(task: URLSessionTask, sample: inout NetworkSample, date: Date? = nil) { + // only safe guard, should not happen + guard let request = task.originalRequest else { + tasksLock.withLock { + _ = samplesByTask.removeValue(forKey: task) + } + return + } + + let responseStatusCode = urlResponseStatusCode(response: task.response) + + if responseStatusCode != -1 { + sample.responseStatus = responseStatusCode + } + + sample.httpMethod = request.httpMethod + sample.initiatorType = "fetch" + // instrumented requests that dont use the completion handler wont have the duration set + if let date = date { + sample.duration = (date.toMillis() - sample.timeOrigin.toMillis()) + } + + // the UI special case if the transferSize is 0 as coming from cache + let transferSize = Int64(request.httpBody?.count ?? 0) + (task.response?.expectedContentLength ?? 0) + if transferSize > 0 { + sample.decodedBodySize = transferSize + } + + finish(task: task, sample: sample) + } + + // MARK: - Private + + private func urlResponseStatusCode(response: URLResponse?) -> Int { + if let urlResponse = response as? HTTPURLResponse { + return urlResponse.statusCode + } + return -1 + } + + private func finish(task: URLSessionTask, sample: NetworkSample) { + if shouldCapture() { + onCapture(sample) + } + + tasksLock.withLock { + _ = samplesByTask.removeValue(forKey: task) + } + } + + private func finishAll() { + var completedTasks: [URLSessionTask: NetworkSample] = [:] + tasksLock.withLock { + for item in samplesByTask where item.key.state == .completed { + completedTasks[item.key] = item.value + } + } + + for item in completedTasks { + var value = item.value + finish(task: item.key, sample: &value) + } + } + + func stop() { + tasksLock.withLock { + samplesByTask.removeAll() + } + } + } +#endif diff --git a/Pods/PostHog/PostHog/Replay/Plugins/Network/URLSessionSwizzler.swift b/Pods/PostHog/PostHog/Replay/Plugins/Network/URLSessionSwizzler.swift new file mode 100644 index 0000000..529f8e0 --- /dev/null +++ b/Pods/PostHog/PostHog/Replay/Plugins/Network/URLSessionSwizzler.swift @@ -0,0 +1,251 @@ +// swiftlint:disable nesting + +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +#if os(iOS) + + import Foundation + + class URLSessionSwizzler { + /// `URLSession.dataTask(with:completionHandler:)` (for `URLRequest`) swizzling. + private let dataTaskWithURLRequestAndCompletion: DataTaskWithURLRequestAndCompletion + /// `URLSession.dataTask(with:)` (for `URLRequest`) swizzling. + private let dataTaskWithURLRequest: DataTaskWithURLRequest + + /// `URLSession.dataTask(with:completionHandler:)` (for `URL`) swizzling. Only applied on iOS 13 and above. + private let dataTaskWithURLAndCompletion: DataTaskWithURLAndCompletion? + /// `URLSession.dataTask(with:)` (for `URL`) swizzling. Only applied on iOS 13 and above. + private let dataTaskWithURL: DataTaskWithURL? + + private let interceptor: URLSessionInterceptor + + private var hasSwizzled = false + + init(shouldCapture: @escaping () -> Bool, onCapture: @escaping (NetworkSample) -> Void, getSessionId: @escaping (Date) -> String?) throws { + interceptor = URLSessionInterceptor( + shouldCapture: shouldCapture, + onCapture: onCapture, + getSessionId: getSessionId + ) + + dataTaskWithURLAndCompletion = try DataTaskWithURLAndCompletion.build(interceptor: interceptor) + dataTaskWithURL = try DataTaskWithURL.build(interceptor: interceptor) + + dataTaskWithURLRequestAndCompletion = try DataTaskWithURLRequestAndCompletion.build(interceptor: interceptor) + dataTaskWithURLRequest = try DataTaskWithURLRequest.build(interceptor: interceptor) + } + + func swizzle() { + dataTaskWithURLRequestAndCompletion.swizzle() + dataTaskWithURLAndCompletion?.swizzle() + dataTaskWithURLRequest.swizzle() + dataTaskWithURL?.swizzle() + hasSwizzled = true + } + + func unswizzle() { + if !hasSwizzled { + return + } + dataTaskWithURLRequestAndCompletion.unswizzle() + dataTaskWithURLRequest.unswizzle() + dataTaskWithURLAndCompletion?.unswizzle() + dataTaskWithURL?.unswizzle() + hasSwizzled = false + } + + // MARK: - Swizzlings + + typealias CompletionHandler = (Data?, URLResponse?, Error?) -> Void + + /// Swizzles the `URLSession.dataTask(with:completionHandler:)` for `URLRequest`. + class DataTaskWithURLRequestAndCompletion: MethodSwizzler< + @convention(c) (URLSession, Selector, URLRequest, CompletionHandler?) -> URLSessionDataTask, + @convention(block) (URLSession, URLRequest, CompletionHandler?) -> URLSessionDataTask + > { + private static let selector = #selector( + URLSession.dataTask(with:completionHandler:) as (URLSession) -> (URLRequest, @escaping CompletionHandler) -> URLSessionDataTask + ) + + private let method: FoundMethod + private let interceptor: URLSessionInterceptor + + static func build(interceptor: URLSessionInterceptor) throws -> DataTaskWithURLRequestAndCompletion { + try DataTaskWithURLRequestAndCompletion( + selector: selector, + klass: URLSession.self, + interceptor: interceptor + ) + } + + private init(selector: Selector, klass: AnyClass, interceptor: URLSessionInterceptor) throws { + method = try Self.findMethod(with: selector, in: klass) + self.interceptor = interceptor + super.init() + } + + func swizzle() { + typealias Signature = @convention(block) (URLSession, URLRequest, CompletionHandler?) -> URLSessionDataTask + swizzle(method) { previousImplementation -> Signature in { session, urlRequest, completionHandler -> URLSessionDataTask in + let task: URLSessionDataTask + if completionHandler != nil { + var taskReference: URLSessionDataTask? + let newCompletionHandler: CompletionHandler = { data, response, error in + if let task = taskReference { // sanity check, should always succeed + self.interceptor.taskCompleted(task: task, error: error) + } + completionHandler?(data, response, error) + } + + task = previousImplementation(session, Self.selector, urlRequest, newCompletionHandler) + taskReference = task + } else { + // The `completionHandler` can be `nil` in two cases: + // - on iOS 11 or 12, where `dataTask(with:)` (for `URL` and `URLRequest`) calls + // the `dataTask(with:completionHandler:)` (for `URLRequest`) internally by nullifying the completion block. + // - when `[session dataTaskWithURL:completionHandler:]` is called in Objective-C with explicitly passing + // `nil` as the `completionHandler` (it produces a warning, but compiles). + task = previousImplementation(session, Self.selector, urlRequest, completionHandler) + } + self.interceptor.taskCreated(task: task, session: session) + return task + } + } + } + } + + /// Swizzles the `URLSession.dataTask(with:completionHandler:)` for `URL`. + class DataTaskWithURLAndCompletion: MethodSwizzler< + @convention(c) (URLSession, Selector, URL, CompletionHandler?) -> URLSessionDataTask, + @convention(block) (URLSession, URL, CompletionHandler?) -> URLSessionDataTask + > { + private static let selector = #selector( + URLSession.dataTask(with:completionHandler:) as (URLSession) -> (URL, @escaping CompletionHandler) -> URLSessionDataTask + ) + + private let method: FoundMethod + private let interceptor: URLSessionInterceptor + + static func build(interceptor: URLSessionInterceptor) throws -> DataTaskWithURLAndCompletion { + try DataTaskWithURLAndCompletion( + selector: selector, + klass: URLSession.self, + interceptor: interceptor + ) + } + + private init(selector: Selector, klass: AnyClass, interceptor: URLSessionInterceptor) throws { + method = try Self.findMethod(with: selector, in: klass) + self.interceptor = interceptor + super.init() + } + + func swizzle() { + typealias Signature = @convention(block) (URLSession, URL, CompletionHandler?) -> URLSessionDataTask + swizzle(method) { previousImplementation -> Signature in { session, url, completionHandler -> URLSessionDataTask in + let task: URLSessionDataTask + if completionHandler != nil { + var taskReference: URLSessionDataTask? + let newCompletionHandler: CompletionHandler = { data, response, error in + if let task = taskReference { // sanity check, should always succeed + self.interceptor.taskCompleted(task: task, error: error) + } + completionHandler?(data, response, error) + } + task = previousImplementation(session, Self.selector, url, newCompletionHandler) + taskReference = task + } else { + // The `completionHandler` can be `nil` in one case: + // - when `[session dataTaskWithURL:completionHandler:]` is called in Objective-C with explicitly passing + // `nil` as the `completionHandler` (it produces a warning, but compiles). + task = previousImplementation(session, Self.selector, url, completionHandler) + } + self.interceptor.taskCreated(task: task, session: session) + return task + } + } + } + } + + /// Swizzles the `URLSession.dataTask(with:)` for `URLRequest`. + class DataTaskWithURLRequest: MethodSwizzler< + @convention(c) (URLSession, Selector, URLRequest) -> URLSessionDataTask, + @convention(block) (URLSession, URLRequest) -> URLSessionDataTask + > { + private static let selector = #selector( + URLSession.dataTask(with:) as (URLSession) -> (URLRequest) -> URLSessionDataTask + ) + + private let method: FoundMethod + private let interceptor: URLSessionInterceptor + + static func build(interceptor: URLSessionInterceptor) throws -> DataTaskWithURLRequest { + try DataTaskWithURLRequest( + selector: selector, + klass: URLSession.self, + interceptor: interceptor + ) + } + + private init(selector: Selector, klass: AnyClass, interceptor: URLSessionInterceptor) throws { + method = try Self.findMethod(with: selector, in: klass) + self.interceptor = interceptor + super.init() + } + + func swizzle() { + typealias Signature = @convention(block) (URLSession, URLRequest) -> URLSessionDataTask + swizzle(method) { previousImplementation -> Signature in { session, urlRequest -> URLSessionDataTask in + let task = previousImplementation(session, Self.selector, urlRequest) + self.interceptor.taskCreated(task: task, session: session) + return task + } + } + } + } + + /// Swizzles the `URLSession.dataTask(with:)` for `URL`. + class DataTaskWithURL: MethodSwizzler< + @convention(c) (URLSession, Selector, URL) -> URLSessionDataTask, + @convention(block) (URLSession, URL) -> URLSessionDataTask + > { + private static let selector = #selector( + URLSession.dataTask(with:) as (URLSession) -> (URL) -> URLSessionDataTask + ) + + private let method: FoundMethod + private let interceptor: URLSessionInterceptor + + static func build(interceptor: URLSessionInterceptor) throws -> DataTaskWithURL { + try DataTaskWithURL( + selector: selector, + klass: URLSession.self, + interceptor: interceptor + ) + } + + private init(selector: Selector, klass: AnyClass, interceptor: URLSessionInterceptor) throws { + method = try Self.findMethod(with: selector, in: klass) + self.interceptor = interceptor + super.init() + } + + func swizzle() { + typealias Signature = @convention(block) (URLSession, URL) -> URLSessionDataTask + swizzle(method) { previousImplementation -> Signature in { session, url -> URLSessionDataTask in + let task = previousImplementation(session, Self.selector, url) + self.interceptor.taskCreated(task: task, session: session) + return task + } + } + } + } + } + +#endif + +// swiftlint:enable nesting diff --git a/Pods/PostHog/PostHog/Replay/Plugins/PostHogSessionReplayPlugin.swift b/Pods/PostHog/PostHog/Replay/Plugins/PostHogSessionReplayPlugin.swift new file mode 100644 index 0000000..49cbde8 --- /dev/null +++ b/Pods/PostHog/PostHog/Replay/Plugins/PostHogSessionReplayPlugin.swift @@ -0,0 +1,43 @@ +// +// PostHogSessionReplayPlugin.swift +// PostHog +// +// Created by Ioannis Josephides on 12/05/2025. +// + +#if os(iOS) + import Foundation + + /// Session replay plugins are used to capture specific types of meta data during a session, + /// such as console logs, network requests and user interactions. Each plugin is responsible + /// for managing its own capture lifecycle and sending data to PostHog. + /// + /// Plugins are installed automatically based on the session replay configuration. + protocol PostHogSessionReplayPlugin { + /// Starts the plugin and begins data capture. + /// + /// Called when session replay is started. The plugin should set up any required + /// resources and begin capturing data. + /// + /// - Parameter postHog: The PostHog SDK instance to use for sending data + func start(postHog: PostHogSDK) + + /// Stops the plugin and cleans up resources. + /// + /// Called when session replay is stopped. The plugin should clean up any resources + /// and stop capturing data. + func stop() + + /// Temporarily pauses data capture. + /// + /// Called by session replay integration when plugin is requested to temporarily pause capturing data + /// The plugin should pause data capture but maintain its state. + func pause() + + /// Resumes data capture after being paused. + /// + /// Called by session replay integration when plugin is requested to resume normal capturing data + /// The plugin should resume data capture from its previous state. + func resume() + } +#endif diff --git a/Pods/PostHog/PostHog/Replay/PostHogReplayIntegration.swift b/Pods/PostHog/PostHog/Replay/PostHogReplayIntegration.swift new file mode 100644 index 0000000..d3511fc --- /dev/null +++ b/Pods/PostHog/PostHog/Replay/PostHogReplayIntegration.swift @@ -0,0 +1,865 @@ +// swiftlint:disable cyclomatic_complexity + +// +// PostHogReplayIntegration.swift +// PostHog +// +// Created by Manoel Aranda Neto on 19.03.24. +// +#if os(iOS) + import Foundation + import PhotosUI + import SwiftUI + import UIKit + import WebKit + + class PostHogReplayIntegration: PostHogIntegration { + var requiresSwizzling: Bool { true } + + private static var integrationInstalledLock = NSLock() + private static var integrationInstalled = false + + private var config: PostHogConfig? { + postHog?.config + } + + private weak var postHog: PostHogSDK? + + private var isEnabled: Bool = false + + private let windowViewsLock = NSLock() + private let windowViews = NSMapTable.weakToStrongObjects() + private var applicationEventToken: RegistrationToken? + private var applicationBackgroundedToken: RegistrationToken? + private var applicationForegroundedToken: RegistrationToken? + private var viewLayoutToken: RegistrationToken? + private var installedPlugins: [PostHogSessionReplayPlugin] = [] + + /** + ### Mapping of SwiftUI Views to UIKit + + This section summarizes findings on how SwiftUI views map to UIKit components + + #### Image-Based Views + - **`AsyncImage` and `Image`** + - Both views have a `CALayer` of type `SwiftUI.ImageLayer`. + - The associated `UIView` is of type `SwiftUI._UIGraphicsView`. + + #### Graphic-based Views + - **`Color`, `Divider`, `Gradient` etc + - These are backed by `SwiftUI._UIGraphicsView` but have a different layer type than images + + #### Text-Based Views + - **`Text`, `Button`, and `TextEditor`** + - These views are backed by a `UIView` of type `SwiftUI.CGDrawingView`, which is a subclass of `SwiftUI._UIGraphicsView`. + - CoreGraphics (`CG`) is used for rendering text content directly, making it challenging to access the value programmatically. + + #### UIKit-Mapped Views + - **Views Hosted by `UIViewRepresentable`** + - Some SwiftUI views map directly to UIKit classes or to a subclass: + - **Control Images** (e.g., in `Picker` drop-downs) may map to `UIImageView`. + - **Buttons** map to `SwiftUI.UIKitIconPreferringButton` (a subclass of `UIButton`). + - **Toggle** maps to `UISwitch` (the toggle itself, excluding its label). + - **Picker** with wheel style maps to `UIPickerView`. Other styles use combinations of image-based and text-based views. + + #### Layout and Structure Views + - **`Spacer`, `VStack`, `HStack`, `ZStack`, and Lazy Stacks** + - These views do not correspond to specific a `UIView`. Instead, they translate directly into layout constraints. + + #### List-Based Views + - **`List` and Scrollable Container Views** + - Backed by a subclass of `UICollectionView` + + #### Other SwiftUI Views + - Most other SwiftUI views are *compositions* of the views described above + + SwiftUI Image Types: + - [StackOverflow: Subviews of a Window or View in SwiftUI](https://stackoverflow.com/questions/57554590/how-to-get-all-the-subviews-of-a-window-or-view-in-latest-swiftui-app) + - [StackOverflow: Detect SwiftUI Usage Programmatically](https://stackoverflow.com/questions/58336045/how-to-detect-swiftui-usage-programmatically-in-an-ios-application) + */ + + /// `AsyncImage` and `Image` + private let swiftUIImageLayerTypes = [ + "SwiftUI.ImageLayer", + ].compactMap(NSClassFromString) + + /// `Text`, `Button`, `TextEditor` views + private let swiftUITextBasedViewTypes = [ + "SwiftUI.CGDrawingView", // Text, Button + "SwiftUI.TextEditorTextView", // TextEditor + "SwiftUI.VerticalTextView", // TextField, vertical axis + ].compactMap(NSClassFromString) + + private let swiftUIGenericTypes = [ + "_TtC7SwiftUIP33_A34643117F00277B93DEBAB70EC0697122_UIShapeHitTestingView", + ].compactMap(NSClassFromString) + + private let reactNativeTextView: AnyClass? = NSClassFromString("RCTTextView") + private let reactNativeImageView: AnyClass? = NSClassFromString("RCTImageView") + // These are usually views that don't belong to the current process and are most likely sensitive + private let systemSandboxedView: AnyClass? = NSClassFromString("_UIRemoteView") + + // These layer types should be safe to ignore while masking + private let swiftUISafeLayerTypes: [AnyClass] = [ + "SwiftUI.GradientLayer", // Views like LinearGradient, RadialGradient, or AngularGradient + ].compactMap(NSClassFromString) + + static let dispatchQueue = DispatchQueue(label: "com.posthog.PostHogReplayIntegration", + target: .global(qos: .utility)) + + private func isNotFlutter() -> Bool { + // for the Flutter SDK, screen recordings are managed by Flutter SDK itself + postHogSdkName != "posthog-flutter" + } + + func install(_ postHog: PostHogSDK) throws { + try PostHogReplayIntegration.integrationInstalledLock.withLock { + if PostHogReplayIntegration.integrationInstalled { + throw InternalPostHogError(description: "Replay integration already installed to another PostHogSDK instance.") + } + PostHogReplayIntegration.integrationInstalled = true + } + + self.postHog = postHog + + start() + } + + func uninstall(_ postHog: PostHogSDK) { + if self.postHog === postHog || self.postHog == nil { + stop() + self.postHog = nil + PostHogReplayIntegration.integrationInstalledLock.withLock { + PostHogReplayIntegration.integrationInstalled = false + } + } + } + + func start() { + guard let postHog, !isEnabled else { + return + } + + isEnabled = true + // reset views when session id changes (or is cleared) so we can re-send new metadata (or full snapshot in the future) + postHog.sessionManager.onSessionIdChanged = { [weak self] in + self?.resetViews() + } + + // flutter captures snapshots, so we don't need to capture them here + if isNotFlutter() { + let interval = postHog.config.sessionReplayConfig.throttleDelay + viewLayoutToken = DI.main.viewLayoutPublisher.onViewLayout(throttle: interval) { [weak self] in + // called on main thread + self?.snapshot() + } + } + + // start listening to `UIApplication.sendEvent` + let applicationEventPublisher = DI.main.applicationEventPublisher + applicationEventToken = applicationEventPublisher.onApplicationEvent { [weak self] event, date in + self?.handleApplicationEvent(event: event, date: date) + } + + // Install plugins + let plugins = postHog.config.sessionReplayConfig.getPlugins() + installedPlugins = [] + for plugin in plugins { + plugin.start(postHog: postHog) + installedPlugins.append(plugin) + } + + // Start listening to application background events and pause all plugins + let applicationLifecyclePublisher = DI.main.appLifecyclePublisher + applicationBackgroundedToken = applicationLifecyclePublisher.onDidEnterBackground { [weak self] in + self?.pauseAllPlugins() + } + + // Start listening to application foreground events and resume all plugins + applicationForegroundedToken = applicationLifecyclePublisher.onDidBecomeActive { [weak self] in + self?.resumeAllPlugins() + } + } + + func stop() { + guard isEnabled else { return } + isEnabled = false + resetViews() + postHog?.sessionManager.onSessionIdChanged = {} + + // stop listening to `UIApplication.sendEvent` + applicationEventToken = nil + // stop listening to Application lifecycle events + applicationBackgroundedToken = nil + applicationForegroundedToken = nil + // stop listening to `UIView.layoutSubviews` events + viewLayoutToken = nil + + // stop plugins + for plugin in installedPlugins { + plugin.stop() + } + installedPlugins = [] + } + + func isActive() -> Bool { + isEnabled + } + + private func resetViews() { + // Ensure thread-safe access to windowViews + windowViewsLock.withLock { + windowViews.removeAllObjects() + } + } + + private func pauseAllPlugins() { + for plugin in installedPlugins { + plugin.pause() + } + } + + private func resumeAllPlugins() { + for plugin in installedPlugins { + plugin.resume() + } + } + + private func handleApplicationEvent(event: UIEvent, date: Date) { + guard let postHog, postHog.isSessionReplayActive() else { + return + } + + guard event.type == .touches else { + return + } + + guard let window = UIApplication.getCurrentWindow() else { + return + } + + guard let touches = event.touches(for: window) else { + return + } + + // capture necessary touch information on the main thread before performing any asynchronous operations + // - this ensures that UITouch associated objects like UIView, UIWindow, or [UIGestureRecognizer] are still valid. + // - these objects may be released or erased by the system if accessed asynchronously, resulting in invalid/zeroed-out touch coordinates + let touchInfo = touches.map { + (phase: $0.phase, location: $0.location(in: window)) + } + + PostHogReplayIntegration.dispatchQueue.async { [touchInfo, weak postHog = postHog] in + // always make sure we have a fresh session id as early as possible + guard let sessionId = postHog?.sessionManager.getSessionId(at: date) else { + return + } + + // captured weakly since integration may have uninstalled by now + guard let postHog else { return } + + var snapshotsData: [Any] = [] + for touch in touchInfo { + let phase = touch.phase + + let type: Int + if phase == .began { + type = 7 + } else if phase == .ended { + type = 9 + } else { + continue + } + + // we keep a failsafe here just in case, but this will likely never be triggered + guard touch.location != .zero else { + continue + } + + let posX = touch.location.x.toInt() + let posY = touch.location.y.toInt() + + // if the id is 0, BE transformer will set it to the virtual bodyId + let touchData: [String: Any] = ["id": 0, "pointerType": 2, "source": 2, "type": type, "x": posX, "y": posY] + + let data: [String: Any] = ["type": 3, "data": touchData, "timestamp": date.toMillis()] + snapshotsData.append(data) + } + if !snapshotsData.isEmpty { + postHog.capture( + "$snapshot", + properties: [ + "$snapshot_source": "mobile", + "$snapshot_data": snapshotsData, + "$session_id": sessionId, + ], + timestamp: date + ) + } + } + } + + private func generateSnapshot(_ window: UIWindow, _ screenName: String? = nil, postHog: PostHogSDK) { + var hasChanges = false + + guard let wireframe = postHog.config.sessionReplayConfig.screenshotMode ? toScreenshotWireframe(window) : toWireframe(window) else { + return + } + + // capture timestamp after snapshot was taken + let timestampDate = Date() + let timestamp = timestampDate.toMillis() + + let snapshotStatus = windowViewsLock.withLock { + windowViews.object(forKey: window) ?? ViewTreeSnapshotStatus() + } + + var snapshotsData: [Any] = [] + + if !snapshotStatus.sentMetaEvent { + let size = window.bounds.size + let width = size.width.toInt() + let height = size.height.toInt() + + var data: [String: Any] = ["width": width, "height": height] + + if let screenName = screenName { + data["href"] = screenName + } + + let snapshotData: [String: Any] = ["type": 4, "data": data, "timestamp": timestamp] + snapshotsData.append(snapshotData) + snapshotStatus.sentMetaEvent = true + hasChanges = true + } + + if hasChanges { + windowViewsLock.withLock { + windowViews.setObject(snapshotStatus, forKey: window) + } + } + + // TODO: IncrementalSnapshot, type=2 + + PostHogReplayIntegration.dispatchQueue.async { + // always make sure we have a fresh session id at correct timestamp + guard let sessionId = postHog.sessionManager.getSessionId(at: timestampDate) else { + return + } + + var wireframes: [Any] = [] + wireframes.append(wireframe.toDict()) + let initialOffset = ["top": 0, "left": 0] + let data: [String: Any] = ["initialOffset": initialOffset, "wireframes": wireframes] + let snapshotData: [String: Any] = ["type": 2, "data": data, "timestamp": timestamp] + snapshotsData.append(snapshotData) + + postHog.capture( + "$snapshot", + properties: [ + "$snapshot_source": "mobile", + "$snapshot_data": snapshotsData, + "$session_id": sessionId, + ], + timestamp: timestampDate + ) + } + } + + private func setAlignment(_ alignment: NSTextAlignment, _ style: RRStyle) { + if alignment == .center { + style.verticalAlign = "center" + style.horizontalAlign = "center" + } else if alignment == .right { + style.horizontalAlign = "right" + } else if alignment == .left { + style.horizontalAlign = "left" + } + } + + private func setPadding(_ insets: UIEdgeInsets, _ style: RRStyle) { + style.paddingTop = insets.top.toInt() + style.paddingRight = insets.right.toInt() + style.paddingBottom = insets.bottom.toInt() + style.paddingLeft = insets.left.toInt() + } + + private func createBasicWireframe(_ view: UIView) -> RRWireframe { + let wireframe = RRWireframe() + + // since FE will render each node of the wireframe with position: fixed + // we need to convert bounds to global screen coordinates + // otherwise each view of depth > 1 will likely have an origin of 0,0 (which is the local origin) + let frame = view.toAbsoluteRect(view.window) + + wireframe.id = view.hash + wireframe.posX = frame.origin.x.toInt() + wireframe.posY = frame.origin.y.toInt() + wireframe.width = frame.size.width.toInt() + wireframe.height = frame.size.height.toInt() + + return wireframe + } + + private func findMaskableWidgets(_ view: UIView, _ window: UIWindow, _ maskableWidgets: inout [CGRect], _ maskChildren: inout Bool) { + // User explicitly marked this view (and its subviews) as non-maskable through `.postHogNoMask()` view modifier + if view.postHogNoMask { + return + } + + if let textView = view as? UITextView { // TextEditor, SwiftUI.TextEditorTextView, SwiftUI.UIKitTextView + if isTextViewSensitive(textView) { + maskableWidgets.append(view.toAbsoluteRect(window)) + return + } + } + + /// SwiftUI: `TextField`, `SecureField` will land here + if let textField = view as? UITextField { + if isTextFieldSensitive(textField) { + maskableWidgets.append(view.toAbsoluteRect(window)) + return + } + } + + if let reactNativeTextView = reactNativeTextView { + if view.isKind(of: reactNativeTextView), config?.sessionReplayConfig.maskAllTextInputs == true { + maskableWidgets.append(view.toAbsoluteRect(window)) + return + } + } + + /// SwiftUI: Some control images like the ones in `Picker` view may land here + if let image = view as? UIImageView { + if isImageViewSensitive(image) { + maskableWidgets.append(view.toAbsoluteRect(window)) + return + } + } + + if let reactNativeImageView = reactNativeImageView { + if view.isKind(of: reactNativeImageView), config?.sessionReplayConfig.maskAllImages == true { + maskableWidgets.append(view.toAbsoluteRect(window)) + return + } + } + + if let label = view as? UILabel { // Text, this code might never be reachable in SwiftUI, see swiftUIImageTypes instead + if isLabelSensitive(label) { + maskableWidgets.append(view.toAbsoluteRect(window)) + return + } + } + + if let webView = view as? WKWebView { // Link, this code might never be reachable in SwiftUI, see swiftUIImageTypes instead + // since we cannot mask the webview content, if masking texts or images are enabled + // we mask the whole webview as well + if isAnyInputSensitive(webView) { + maskableWidgets.append(view.toAbsoluteRect(window)) + return + } + } + + /// SwiftUI: `SwiftUI.UIKitIconPreferringButton` and other subclasses will land here + if let button = view as? UIButton { + if isButtonSensitive(button) { + maskableWidgets.append(view.toAbsoluteRect(window)) + return + } + } + + /// SwiftUI: `Toggle` (no text, labels are just rendered to Text (swiftUIImageTypes)) + if let theSwitch = view as? UISwitch { + if isSwitchSensitive(theSwitch) { + maskableWidgets.append(view.toAbsoluteRect(window)) + return + } + } + + // detect any views that don't belong to the current process (likely system views) + if config?.sessionReplayConfig.maskAllSandboxedViews == true, + let systemSandboxedView, + view.isKind(of: systemSandboxedView) + { + maskableWidgets.append(view.toAbsoluteRect(window)) + return + } + + // if its a generic type and has subviews, subviews have to be checked first + let hasSubViews = !view.subviews.isEmpty + + /// SwiftUI: `Picker` with .pickerStyle(.wheel) will land here + if let picker = view as? UIPickerView { + if isTextInputSensitive(picker), !hasSubViews { + maskableWidgets.append(picker.toAbsoluteRect(window)) + return + } + } + + /// SwiftUI: Text based views like `Text`, `Button`, `TextEditor` + if swiftUITextBasedViewTypes.contains(where: view.isKind(of:)) { + if isTextInputSensitive(view), !hasSubViews { + maskableWidgets.append(view.toAbsoluteRect(window)) + return + } + } + + /// SwiftUI: Image based views like `Image`, `AsyncImage`. (Note: We check the layer type here) + if swiftUIImageLayerTypes.contains(where: view.layer.isKind(of:)) { + if isSwiftUIImageSensitive(view), !hasSubViews { + maskableWidgets.append(view.toAbsoluteRect(window)) + return + } + } + + // this can be anything, so better to be conservative + if swiftUIGenericTypes.contains(where: { view.isKind(of: $0) }), !isSwiftUILayerSafe(view.layer) { + if isTextInputSensitive(view), !hasSubViews { + maskableWidgets.append(view.toAbsoluteRect(window)) + return + } + } + + // manually masked views through `.postHogMask()` view modifier + if view.postHogNoCapture { + maskableWidgets.append(view.toAbsoluteRect(window)) + return + } + + // on RN, lots get converted to RCTRootContentView, RCTRootView, RCTView and sometimes its just the whole screen, we dont want to mask + // in such cases + if view.isNoCapture() || maskChildren { + let viewRect = view.toAbsoluteRect(window) + let windowRect = window.frame + + // Check if the rectangles do not match + if !viewRect.equalTo(windowRect) { + maskableWidgets.append(view.toAbsoluteRect(window)) + } else { + maskChildren = true + } + } + + if !view.subviews.isEmpty { + for child in view.subviews { + if !child.isVisible() { + continue + } + + findMaskableWidgets(child, window, &maskableWidgets, &maskChildren) + } + } + maskChildren = false + } + + private func toScreenshotWireframe(_ window: UIWindow) -> RRWireframe? { + // this will bail on view controller animations (interactive or not) + if !window.isVisible() || isAnimatingTransition(window) { + return nil + } + + var maskableWidgets: [CGRect] = [] + var maskChildren = false + findMaskableWidgets(window, window, &maskableWidgets, &maskChildren) + + let wireframe = createBasicWireframe(window) + + if let image = window.toImage() { + if !image.size.hasSize() { + return nil + } + + wireframe.maskableWidgets = maskableWidgets + + wireframe.image = image + } + wireframe.type = "screenshot" + return wireframe + } + + /// Check if any view controller in the hierarchy is animating a transition + private func isAnimatingTransition(_ window: UIWindow) -> Bool { + guard let rootViewController = window.rootViewController else { return false } + return isAnimatingTransition(rootViewController) + } + + private func isAnimatingTransition(_ viewController: UIViewController) -> Bool { + // Check if this view controller is animating + if viewController.transitionCoordinator?.isAnimated ?? false { + return true + } + + // Check if presented view controller is animating + if let presented = viewController.presentedViewController, isAnimatingTransition(presented) { + return true + } + + // Check if any of the child view controllers is animating + if viewController.children.first(where: isAnimatingTransition) != nil { + return true + } + + return false + } + + private func isAssetsImage(_ image: UIImage) -> Bool { + // https://github.com/daydreamboy/lldb_scripts#9-pimage + // do not mask if its an asset image, likely not PII anyway + image.imageAsset?.value(forKey: "_containingBundle") != nil + } + + private func isAnyInputSensitive(_ view: UIView) -> Bool { + isTextInputSensitive(view) || config?.sessionReplayConfig.maskAllImages == true + } + + private func isTextInputSensitive(_ view: UIView) -> Bool { + config?.sessionReplayConfig.maskAllTextInputs == true || view.isNoCapture() + } + + private func isLabelSensitive(_ view: UILabel) -> Bool { + isTextInputSensitive(view) && hasText(view.text) + } + + private func isButtonSensitive(_ view: UIButton) -> Bool { + isTextInputSensitive(view) && hasText(view.titleLabel?.text) + } + + private func isTextViewSensitive(_ view: UITextView) -> Bool { + (isTextInputSensitive(view) || view.isSensitiveText()) && hasText(view.text) + } + + private func isSwitchSensitive(_ view: UISwitch) -> Bool { + var containsText = true + if #available(iOS 14.0, *) { + containsText = hasText(view.title) + } + + return isTextInputSensitive(view) && containsText + } + + private func isTextFieldSensitive(_ view: UITextField) -> Bool { + (isTextInputSensitive(view) || view.isSensitiveText()) && (hasText(view.text) || hasText(view.placeholder)) + } + + private func isSwiftUILayerSafe(_ layer: CALayer) -> Bool { + swiftUISafeLayerTypes.contains(where: { layer.isKind(of: $0) }) + } + + private func hasText(_ text: String?) -> Bool { + if let text = text, !text.isEmpty { + return true + } else { + // if there's no text, there's nothing to mask + return false + } + } + + private func isSwiftUIImageSensitive(_ view: UIView) -> Bool { + // No way of checking if this is an asset image or not + // No way of checking if there's actual content in the image or not + config?.sessionReplayConfig.maskAllImages == true || view.isNoCapture() + } + + private func isImageViewSensitive(_ view: UIImageView) -> Bool { + // if there's no image, there's nothing to mask + guard let image = view.image else { return false } + + // sensitive, regardless + if view.isNoCapture() { + return true + } + + // asset images are probably not sensitive + if isAssetsImage(image) { + return false + } + + // symbols are probably not sensitive + if image.isSymbolImage { + return false + } + + return config?.sessionReplayConfig.maskAllImages == true + } + + private func toWireframe(_ view: UIView) -> RRWireframe? { + if !view.isVisible() { + return nil + } + + let wireframe = createBasicWireframe(view) + + let style = RRStyle() + + if let textView = view as? UITextView { + wireframe.type = "text" + wireframe.text = isTextViewSensitive(textView) ? textView.text.mask() : textView.text + wireframe.disabled = !textView.isEditable + style.color = textView.textColor?.toRGBString() + style.fontFamily = textView.font?.familyName + if let fontSize = textView.font?.pointSize.toInt() { + style.fontSize = fontSize + } + setAlignment(textView.textAlignment, style) + setPadding(textView.textContainerInset, style) + } + + if let textField = view as? UITextField { + wireframe.type = "input" + wireframe.inputType = "text_area" + let isSensitive = isTextFieldSensitive(textField) + if let text = textField.text { + wireframe.value = isSensitive ? text.mask() : text + } else { + if let text = textField.placeholder { + wireframe.value = isSensitive ? text.mask() : text + } + } + wireframe.disabled = !textField.isEnabled + style.color = textField.textColor?.toRGBString() + style.fontFamily = textField.font?.familyName + if let fontSize = textField.font?.pointSize.toInt() { + style.fontSize = fontSize + } + setAlignment(textField.textAlignment, style) + } + + if view is UIPickerView { + wireframe.type = "input" + wireframe.inputType = "select" + // set wireframe.value from selected row + } + + if let theSwitch = view as? UISwitch { + wireframe.type = "input" + wireframe.inputType = "toggle" + wireframe.checked = theSwitch.isOn + if #available(iOS 14.0, *) { + if let text = theSwitch.title { + wireframe.label = isSwitchSensitive(theSwitch) ? text.mask() : text + } + } + } + + if let imageView = view as? UIImageView { + wireframe.type = "image" + if let image = imageView.image { + if !isImageViewSensitive(imageView) { + wireframe.image = image + } + } + } + + if let button = view as? UIButton { + wireframe.type = "input" + wireframe.inputType = "button" + wireframe.disabled = !button.isEnabled + + if let text = button.titleLabel?.text { + // NOTE: this will create a ghosting effect since text will also be captured in child UILabel + // We also may be masking this UIButton but child UILabel may remain unmasked + wireframe.value = isButtonSensitive(button) ? text.mask() : text + } + } + + if let label = view as? UILabel { + wireframe.type = "text" + if let text = label.text { + wireframe.text = isLabelSensitive(label) ? text.mask() : text + } + wireframe.disabled = !label.isEnabled + style.color = label.textColor?.toRGBString() + style.fontFamily = label.font?.familyName + if let fontSize = label.font?.pointSize.toInt() { + style.fontSize = fontSize + } + setAlignment(label.textAlignment, style) + } + + if view is WKWebView { + wireframe.type = "web_view" + } + + if let progressView = view as? UIProgressView { + wireframe.type = "input" + wireframe.inputType = "progress" + wireframe.value = progressView.progress + wireframe.max = 1 + // UIProgressView theres not circular format, only custom view or swiftui + style.bar = "horizontal" + } + + if view is UIActivityIndicatorView { + wireframe.type = "input" + wireframe.inputType = "progress" + style.bar = "circular" + } + + // TODO: props: backgroundImage (probably not needed) + // TODO: componenets: UITabBar, UINavigationBar, UISlider, UIStepper, UIDatePicker + + style.backgroundColor = view.backgroundColor?.toRGBString() + let layer = view.layer + style.borderWidth = layer.borderWidth.toInt() + style.borderRadius = layer.cornerRadius.toInt() + style.borderColor = layer.borderColor?.toRGBString() + + wireframe.style = style + + if !view.subviews.isEmpty { + var childWireframes: [RRWireframe] = [] + for subview in view.subviews { + if let child = toWireframe(subview) { + childWireframes.append(child) + } + } + wireframe.childWireframes = childWireframes + } + + return wireframe + } + + @objc private func snapshot() { + guard let postHog, postHog.isSessionReplayActive() else { + return + } + + guard let window = UIApplication.getCurrentWindow() else { + return + } + + var screenName: String? + if let controller = window.rootViewController { + // SwiftUI only supported with screenshotMode + if controller is AnyObjectUIHostingViewController, !postHog.config.sessionReplayConfig.screenshotMode { + hedgeLog("SwiftUI snapshot not supported, enable screenshotMode.") + return + // screen name only makes sense if we are not using SwiftUI + } else if !postHog.config.sessionReplayConfig.screenshotMode { + screenName = UIViewController.getViewControllerName(controller) + } + } + + // this cannot run off of the main thread because most properties require to be called within the main thread + // this method has to be fast and do as little as possible + generateSnapshot(window, screenName, postHog: postHog) + } + } + + private protocol AnyObjectUIHostingViewController: AnyObject {} + + extension UIHostingController: AnyObjectUIHostingViewController {} + + #if TESTING + extension PostHogReplayIntegration { + static func clearInstalls() { + integrationInstalledLock.withLock { + integrationInstalled = false + } + } + } + #endif + +#endif + +// swiftlint:enable cyclomatic_complexity diff --git a/Pods/PostHog/PostHog/Replay/PostHogSessionReplayConfig.swift b/Pods/PostHog/PostHog/Replay/PostHogSessionReplayConfig.swift new file mode 100644 index 0000000..9f27abf --- /dev/null +++ b/Pods/PostHog/PostHog/Replay/PostHogSessionReplayConfig.swift @@ -0,0 +1,93 @@ +// +// PostHogSessionReplayConfig.swift +// PostHog +// +// Created by Manoel Aranda Neto on 19.03.24. +// +#if os(iOS) + import Foundation + + @objc(PostHogSessionReplayConfig) public class PostHogSessionReplayConfig: NSObject { + /// Enable masking of all text and text input fields + /// Default: true + @objc public var maskAllTextInputs: Bool = true + + /// Enable masking of all images to a placeholder + /// Default: true + @objc public var maskAllImages: Bool = true + + /// Enable masking of all sandboxed system views + /// These may include UIImagePickerController, PHPickerViewController and CNContactPickerViewController + /// Default: true + @objc public var maskAllSandboxedViews: Bool = true + + /// Enable masking of images that likely originated from user's photo library (UIKit only) + /// Default: false + /// + /// - Note: Deprecated + @available(*, deprecated, message: "This property has no effect and will be removed in the next major release. To learn how to manually mask user photos please see our Privacy controls documentation: https://posthog.com/docs/session-replay/privacy?tab=iOS") + @objc public var maskPhotoLibraryImages: Bool = false + + /// Enable capturing network telemetry + /// Default: true + @objc public var captureNetworkTelemetry: Bool = true + + /// By default Session replay will capture all the views on the screen as a wireframe, + /// By enabling this option, PostHog will capture the screenshot of the screen. + /// The screenshot may contain sensitive information, use with caution. + /// Default: false + @objc public var screenshotMode: Bool = false + + /// Debouncer delay used to reduce the number of snapshots captured and reduce performance impact + /// This is used for capturing the view as a wireframe or screenshot + /// The lower the number more snapshots will be captured but higher the performance impact + /// Defaults to 1s + @available(*, deprecated, message: "Deprecated in favor of 'throttleDelay' which provides identical functionality. Will be removed in the next major release.") + @objc public var debouncerDelay: TimeInterval { + get { throttleDelay } + set { throttleDelay = newValue } + } + + /// Throttle delay used to reduce the number of snapshots captured and reduce performance impact + /// This is used for capturing the view as a wireframe or screenshot + /// The lower the number more snapshots will be captured but higher the performance impact + /// Defaults to 1s + /// + /// Note: Previously `debouncerDelay` + @objc public var throttleDelay: TimeInterval = 1 + + /// Enable capturing console output for session replay. + /// + /// When enabled, logs from the following sources will be captured: + /// - Standard output (stdout) + /// - Standard error (stderr) + /// - OSLog messages + /// - NSLog messages + /// + /// Each log entry will be tagged with a level (info/warning/error) based on the message content + /// and the source. + /// + /// Defaults to `false` + @objc public var captureLogs: Bool = false + + /// Further configuration for capturing console output + @objc public var captureLogsConfig: PostHogSessionReplayConsoleLogConfig = .init() + + // TODO: sessionRecording config such as networkPayloadCapture, sampleRate, etc + + /// Returns an array of plugins to be installed based on current configuration + func getPlugins() -> [PostHogSessionReplayPlugin] { + var plugins: [PostHogSessionReplayPlugin] = [] + + if captureLogs { + plugins.append(PostHogSessionReplayConsoleLogsPlugin()) + } + + if captureNetworkTelemetry { + plugins.append(PostHogSessionReplayNetworkPlugin()) + } + + return plugins + } + } +#endif diff --git a/Pods/PostHog/PostHog/Replay/PostHogSessionReplayConsoleLogConfig.swift b/Pods/PostHog/PostHog/Replay/PostHogSessionReplayConsoleLogConfig.swift new file mode 100644 index 0000000..fb9908b --- /dev/null +++ b/Pods/PostHog/PostHog/Replay/PostHogSessionReplayConsoleLogConfig.swift @@ -0,0 +1,73 @@ +// +// PostHogSessionReplayConsoleLogConfig.swift +// PostHog +// +// Created by Ioannis Josephides on 09/05/2025. +// + +#if os(iOS) + import Foundation + + @objc public class PostHogSessionReplayConsoleLogConfig: NSObject { + /// Block to process and format captured console output for session replay. + /// + /// This block is called whenever console output is captured. It allows you to: + /// 1. Filter or modify log messages before they are sent to session replay + /// 2. Determine the appropriate log level (info/warn/error) for each message + /// 3. Format, sanitize or skip a log messages (e.g. remove sensitive data or PII) + /// + /// The default implementation: + /// - Detect log level (best effort) + /// - Process OSLog messages to remove metadata + /// + /// - Parameter output: The raw console output to process + /// - Returns: Array of `PostHogConsoleLogResult` objects, one for each processed log entry. Return an empty array to skip a log output + @objc public var logSanitizer: ((String) -> PostHogLogEntry?) = PostHogSessionReplayConsoleLogConfig.defaultLogSanitizer + + /// The minimum log level to capture in session replay. + /// Only log messages with this level or higher will be captured. + /// For example, if set to `.warn`: + /// - `.error` messages will be captured + /// - `.warn` messages will be captured + /// - `.info` messages will be skipped + /// + /// Defaults to `.error` to minimize noise in session replays. + @objc public var minLogLevel: PostHogLogLevel = .error + + /// Default implementation for processing console output. + static func defaultLogSanitizer(_ message: String) -> PostHogLogEntry? { + let message = String(message) + // Determine console log level + let level: PostHogLogLevel = { + if message.range(of: logMessageWarningPattern, options: .regularExpression) != nil { return .warn } + if message.range(of: logMessageErrorPattern, options: .regularExpression) != nil { return .error } + return .info + }() + + // For OSLog messages, extract just the log message part + let sanitizedMessage = message.contains("OSLOG-") ? { + if let tabIndex = message.lastIndex(of: "\t") { + return String(message[message.index(after: tabIndex)...]) + } + return message + }() : message + + return PostHogLogEntry(level: level, message: sanitizedMessage) + } + + /// Default regular expression pattern used to identify error-level log messages. + /// + /// By default, it matches common error indicators such as: + /// - The word "error", "exception", "fail" or "failed" + /// - OSLog messages with type "Error" or "Fault" + private static let logMessageErrorPattern = "(error|exception|fail(ed)?|OSLOG-.*type:\"Error\"|OSLOG-.*type:\"Fault\")" + + /// Default regular expression pattern used to identify warning-level log messages. + /// + /// By default, it matches common warning indicators such as: + /// - The words "warning", "warn", "caution", or "deprecated" + /// - OSLog messages with type "Warning" + /// + private static let logMessageWarningPattern = "(warn(ing)?|caution|deprecated|OSLOG-.*type:\"Warning\")" + } +#endif diff --git a/Pods/PostHog/PostHog/Replay/RRStyle.swift b/Pods/PostHog/PostHog/Replay/RRStyle.swift new file mode 100644 index 0000000..43624fd --- /dev/null +++ b/Pods/PostHog/PostHog/Replay/RRStyle.swift @@ -0,0 +1,96 @@ +// swiftlint:disable cyclomatic_complexity + +// +// RRStyle.swift +// PostHog +// +// Created by Manoel Aranda Neto on 21.03.24. +// + +import Foundation + +class RRStyle { + var color: String? + var backgroundColor: String? + var backgroundImage: String? + var borderWidth: Int? + var borderRadius: Int? + var borderColor: String? + var fontSize: Int? + var fontFamily: String? + var horizontalAlign: String? + var verticalAlign: String? + var paddingTop: Int? + var paddingBottom: Int? + var paddingLeft: Int? + var paddingRight: Int? + var bar: String? + + func toDict() -> [String: Any] { + var dict: [String: Any] = [:] + + if let color = color { + dict["color"] = color + } + + if let backgroundColor = backgroundColor { + dict["backgroundColor"] = backgroundColor + } + + if let backgroundImage = backgroundImage { + dict["backgroundImage"] = backgroundImage + } + + if let borderWidth = borderWidth { + dict["borderWidth"] = borderWidth + } + + if let borderRadius = borderRadius { + dict["borderRadius"] = borderRadius + } + + if let borderColor = borderColor { + dict["borderColor"] = borderColor + } + + if let fontSize = fontSize { + dict["fontSize"] = fontSize + } + + if let fontFamily = fontFamily { + dict["fontFamily"] = fontFamily + } + + if let horizontalAlign = horizontalAlign { + dict["horizontalAlign"] = horizontalAlign + } + + if let verticalAlign = verticalAlign { + dict["verticalAlign"] = verticalAlign + } + + if let paddingTop = paddingTop { + dict["paddingTop"] = paddingTop + } + + if let paddingBottom = paddingBottom { + dict["paddingBottom"] = paddingBottom + } + + if let paddingLeft = paddingLeft { + dict["paddingLeft"] = paddingLeft + } + + if let paddingRight = paddingRight { + dict["paddingRight"] = paddingRight + } + + if let bar = bar { + dict["bar"] = bar + } + + return dict + } +} + +// swiftlint:enable cyclomatic_complexity diff --git a/Pods/PostHog/PostHog/Replay/RRWireframe.swift b/Pods/PostHog/PostHog/Replay/RRWireframe.swift new file mode 100644 index 0000000..bd6553a --- /dev/null +++ b/Pods/PostHog/PostHog/Replay/RRWireframe.swift @@ -0,0 +1,133 @@ +// swiftlint:disable cyclomatic_complexity + +// +// RRWireframe.swift +// PostHog +// +// Created by Manoel Aranda Neto on 21.03.24. +// + +import Foundation +#if os(iOS) + import UIKit +#endif + +class RRWireframe { + var id: Int = 0 + var posX: Int = 0 + var posY: Int = 0 + var width: Int = 0 + var height: Int = 0 + var childWireframes: [RRWireframe]? + var type: String? // text|image|rectangle|input|div|screenshot + var inputType: String? + var text: String? + var label: String? + var value: Any? // string or number + #if os(iOS) + var image: UIImage? + var maskableWidgets: [CGRect]? + #endif + var base64: String? + var style: RRStyle? + var disabled: Bool? + var checked: Bool? + var options: [String]? + var max: Int? + // internal + var parentId: Int? + + #if os(iOS) + private func maskImage() -> UIImage? { + if let image = image { + // the scale also affects the image size/resolution, from usually 100kb to 15kb each + let redactedImage = UIGraphicsImageRenderer(size: image.size, format: .init(for: .init(displayScale: 1))).image { context in + context.cgContext.interpolationQuality = .none + image.draw(at: .zero) + + if let maskableWidgets = maskableWidgets { + for rect in maskableWidgets { + let path = UIBezierPath(roundedRect: rect, cornerRadius: 10) + UIColor.black.setFill() + path.fill() + } + } + } + return redactedImage + } + return nil + } + #endif + + func toDict() -> [String: Any] { + var dict: [String: Any] = [ + "id": id, + "x": posX, + "y": posY, + "width": width, + "height": height, + ] + + if let childWireframes = childWireframes { + dict["childWireframes"] = childWireframes.map { $0.toDict() } + } + + if let type = type { + dict["type"] = type + } + + if let inputType = inputType { + dict["inputType"] = inputType + } + + if let text = text { + dict["text"] = text + } + + if let label = label { + dict["label"] = label + } + + if let value = value { + dict["value"] = value + } + + #if os(iOS) + if let image = image { + if let maskedImage = maskImage() { + base64 = maskedImage.toBase64() + } else { + base64 = image.toBase64() + } + } + #endif + + if let base64 = base64 { + dict["base64"] = base64 + } + + if let style = style { + dict["style"] = style.toDict() + } + + if let disabled = disabled { + dict["disabled"] = disabled + } + + if let checked = checked { + dict["checked"] = checked + } + + if let options = options { + dict["options"] = options + } + + if let max = max { + dict["max"] = max + } + + return dict + } +} + +// swiftlint:enable cyclomatic_complexity diff --git a/Pods/PostHog/PostHog/Replay/String+Util.swift b/Pods/PostHog/PostHog/Replay/String+Util.swift new file mode 100644 index 0000000..ea8d1fe --- /dev/null +++ b/Pods/PostHog/PostHog/Replay/String+Util.swift @@ -0,0 +1,14 @@ +// +// String+Util.swift +// PostHog +// +// Created by Manoel Aranda Neto on 21.03.24. +// + +import Foundation + +extension String { + func mask() -> String { + String(repeating: "*", count: count) + } +} diff --git a/Pods/PostHog/PostHog/Replay/UIColor+Util.swift b/Pods/PostHog/PostHog/Replay/UIColor+Util.swift new file mode 100644 index 0000000..b3d0463 --- /dev/null +++ b/Pods/PostHog/PostHog/Replay/UIColor+Util.swift @@ -0,0 +1,17 @@ +// +// UIColor+Util.swift +// PostHog +// +// Created by Manoel Aranda Neto on 21.03.24. +// +#if os(iOS) + + import Foundation + import UIKit + + extension UIColor { + func toRGBString() -> String? { + cgColor.toRGBString() + } + } +#endif diff --git a/Pods/PostHog/PostHog/Replay/UIImage+Util.swift b/Pods/PostHog/PostHog/Replay/UIImage+Util.swift new file mode 100644 index 0000000..cd067a7 --- /dev/null +++ b/Pods/PostHog/PostHog/Replay/UIImage+Util.swift @@ -0,0 +1,33 @@ +// +// UIImage+Util.swift +// PostHog +// +// Created by Manoel Aranda Neto on 27.11.24. +// + +#if os(iOS) + import Foundation + import UIKit + + extension UIImage { + func toBase64(_ compressionQuality: CGFloat = 0.3) -> String? { + toWebPBase64(compressionQuality) ?? toJpegBase64(compressionQuality) + } + + private func toWebPBase64(_ compressionQuality: CGFloat) -> String? { + webpData(compressionQuality: compressionQuality).map { data in + "data:image/webp;base64,\(data.base64EncodedString())" + } + } + + private func toJpegBase64(_ compressionQuality: CGFloat) -> String? { + jpegData(compressionQuality: compressionQuality).map { data in + "data:image/jpeg;base64,\(data.base64EncodedString())" + } + } + } + + public func imageToBase64(_ image: UIImage, _ compressionQuality: CGFloat = 0.3) -> String? { + image.toBase64(compressionQuality) + } +#endif diff --git a/Pods/PostHog/PostHog/Replay/UITextInputTraits+Util.swift b/Pods/PostHog/PostHog/Replay/UITextInputTraits+Util.swift new file mode 100644 index 0000000..ce54821 --- /dev/null +++ b/Pods/PostHog/PostHog/Replay/UITextInputTraits+Util.swift @@ -0,0 +1,36 @@ +// +// UITextInputTraits+Util.swift +// PostHog +// +// Created by Manoel Aranda Neto on 21.03.24. +// + +#if os(iOS) + import Foundation + import UIKit + + private let sensibleTypes: [UITextContentType] = [ + .newPassword, .oneTimeCode, .creditCardNumber, + .telephoneNumber, .emailAddress, .password, + .username, .URL, .name, .nickname, + .middleName, .familyName, .nameSuffix, + .namePrefix, .organizationName, .location, + .fullStreetAddress, .streetAddressLine1, + .streetAddressLine2, .addressCity, .addressState, + .addressCityAndState, .postalCode, + ] + + extension UITextInputTraits { + func isSensitiveText() -> Bool { + if isSecureTextEntry ?? false { + return true + } + + if let contentType = textContentType, let contentType = contentType { + return sensibleTypes.contains(contentType) + } + + return false + } + } +#endif diff --git a/Pods/PostHog/PostHog/Replay/UIView+Util.swift b/Pods/PostHog/PostHog/Replay/UIView+Util.swift new file mode 100644 index 0000000..6818996 --- /dev/null +++ b/Pods/PostHog/PostHog/Replay/UIView+Util.swift @@ -0,0 +1,70 @@ +// +// UIView+Util.swift +// PostHog +// +// Created by Manoel Aranda Neto on 21.03.24. +// + +#if os(iOS) + import Foundation + import UIKit + + extension UIView { + func isVisible() -> Bool { + if isHidden || alpha == 0 || frame == .zero { + return false + } + return true + } + + func isNoCapture() -> Bool { + var isNoCapture = false + if let identifier = accessibilityIdentifier { + isNoCapture = checkLabel(identifier) + } + // read accessibilityLabel from the parent's view to skip the RCTRecursiveAccessibilityLabel on RN which is slow and may cause an endless loop + // see https://github.com/facebook/react-native/issues/33084 + if let label = super.accessibilityLabel, !isNoCapture { + isNoCapture = checkLabel(label) + } + + return isNoCapture + } + + private func checkLabel(_ label: String) -> Bool { + label.lowercased().contains("ph-no-capture") + } + + func toImage() -> UIImage? { + // Avoid Rendering Offscreen Views + let bounds = superview?.bounds ?? bounds + let size = bounds.intersection(bounds).size + + if !size.hasSize() { + return nil + } + + let rendererFormat = UIGraphicsImageRendererFormat.default() + + // This can significantly improve rendering performance because the renderer won't need to + // process transparency. + rendererFormat.opaque = isOpaque + // Another way to improve rendering performance is to scale the renderer's content. + // rendererFormat.scale = 0.5 + let renderer = UIGraphicsImageRenderer(size: size, format: rendererFormat) + + let image = renderer.image { _ in + /// Note: Always `false` for `afterScreenUpdates` since this will cause the screen to flicker when a sensitive text field is visible on screen + /// This can potentially affect capturing a snapshot during a screen transition but we want the lesser of the two evils here + drawHierarchy(in: bounds, afterScreenUpdates: false) + } + + return image + } + + // you need this because of SwiftUI otherwise the coordinates always zeroed for some reason + func toAbsoluteRect(_ window: UIWindow?) -> CGRect { + convert(bounds, to: window) + } + } +#endif diff --git a/Pods/PostHog/PostHog/Replay/ViewTreeSnapshotStatus.swift b/Pods/PostHog/PostHog/Replay/ViewTreeSnapshotStatus.swift new file mode 100644 index 0000000..2806dc2 --- /dev/null +++ b/Pods/PostHog/PostHog/Replay/ViewTreeSnapshotStatus.swift @@ -0,0 +1,15 @@ +// +// ViewTreeSnapshotStatus.swift +// PostHog +// +// Created by Manoel Aranda Neto on 20.03.24. +// + +import Foundation + +class ViewTreeSnapshotStatus { + var sentFullSnapshot: Bool = false + var sentMetaEvent: Bool = false + var keyboardVisible: Bool = false + var lastSnapshot: Bool = false +} diff --git a/Pods/PostHog/PostHog/Resources/PrivacyInfo.xcprivacy b/Pods/PostHog/PostHog/Resources/PrivacyInfo.xcprivacy new file mode 100644 index 0000000..8379bfd --- /dev/null +++ b/Pods/PostHog/PostHog/Resources/PrivacyInfo.xcprivacy @@ -0,0 +1,44 @@ + + + + + NSPrivacyCollectedDataTypes + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeProductInteraction + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAnalytics + + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeOtherUsageData + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAnalytics + + + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryUserDefaults + NSPrivacyAccessedAPITypeReasons + + CA92.1 + + + + + diff --git a/Pods/PostHog/PostHog/Screen Views/ApplicationScreenViewPublisher.swift b/Pods/PostHog/PostHog/Screen Views/ApplicationScreenViewPublisher.swift new file mode 100644 index 0000000..dcb59dc --- /dev/null +++ b/Pods/PostHog/PostHog/Screen Views/ApplicationScreenViewPublisher.swift @@ -0,0 +1,172 @@ +// +// ApplicationScreenViewPublisher.swift +// PostHog +// +// Created by Ioannis Josephides on 20/02/2025. +// + +import Foundation + +#if os(iOS) || os(tvOS) + import UIKit +#endif + +typealias ScreenViewHandler = (String) -> Void + +protocol ScreenViewPublishing: AnyObject { + /// Registers a callback for a view appeared event + func onScreenView(_ callback: @escaping ScreenViewHandler) -> RegistrationToken +} + +final class ApplicationScreenViewPublisher: BaseScreenViewPublisher { + static let shared = ApplicationScreenViewPublisher() + + private var hasSwizzled: Bool = false + + func start() { + // no-op if not UIKit + #if os(iOS) || os(tvOS) + swizzleViewDidAppear() + #endif + } + + func stop() { + // no-op if not UIKit + #if os(iOS) || os(tvOS) + unswizzleViewDidAppear() + #endif + } + + override func onScreenView(_ callback: @escaping ScreenViewHandler) -> RegistrationToken { + let id = UUID() + registrationLock.withLock { + self.onScreenViewCallbacks[id] = callback + } + + // start on first callback registration + if !hasSwizzled { + start() + } + + return RegistrationToken { [weak self] in + // Registration token deallocated here + guard let self else { return } + let handlerCount = self.registrationLock.withLock { + self.onScreenViewCallbacks[id] = nil + return self.onScreenViewCallbacks.values.count + } + // stop when there are no more callbacks + if handlerCount <= 0 { + stop() + } + } + } + + #if os(iOS) || os(tvOS) + func swizzleViewDidAppear() { + guard !hasSwizzled else { return } + hasSwizzled = true + swizzle( + forClass: UIViewController.self, + original: #selector(UIViewController.viewDidAppear(_:)), + new: #selector(UIViewController.viewDidAppearOverride) + ) + } + + func unswizzleViewDidAppear() { + guard hasSwizzled else { return } + hasSwizzled = false + swizzle( + forClass: UIViewController.self, + original: #selector(UIViewController.viewDidAppearOverride), + new: #selector(UIViewController.viewDidAppear(_:)) + ) + } + + // Called from swizzled `viewDidAppearOverride` + fileprivate func viewDidAppear(in viewController: UIViewController?) { + // ignore views from keyboard window + guard let window = viewController?.viewIfLoaded?.window, !window.isKeyboardWindow else { + return + } + + guard let top = findVisibleViewController(viewController) else { return } + + if let name = UIViewController.getViewControllerName(top) { + notifyHandlers(screen: name) + } + } + + private func findVisibleViewController(_ controller: UIViewController?) -> UIViewController? { + if let navigationController = controller as? UINavigationController { + return findVisibleViewController(navigationController.visibleViewController) + } + if let tabController = controller as? UITabBarController { + if let selected = tabController.selectedViewController { + return findVisibleViewController(selected) + } + } + if let presented = controller?.presentedViewController { + return findVisibleViewController(presented) + } + return controller + } + #endif +} + +class BaseScreenViewPublisher: ScreenViewPublishing { + fileprivate let registrationLock = NSLock() + + var onScreenViewCallbacks: [UUID: ScreenViewHandler] = [:] + + func onScreenView(_ callback: @escaping ScreenViewHandler) -> RegistrationToken { + let id = UUID() + registrationLock.withLock { + self.onScreenViewCallbacks[id] = callback + } + + return RegistrationToken { [weak self] in + // Registration token deallocated here + guard let self else { return } + self.registrationLock.withLock { + self.onScreenViewCallbacks[id] = nil + } + } + } + + func notifyHandlers(screen: String) { + let handlers = registrationLock.withLock { onScreenViewCallbacks.values } + for handler in handlers { + notifyHander(handler, screen: screen) + } + } + + private func notifyHander(_ handler: @escaping ScreenViewHandler, screen: String) { + if Thread.isMainThread { + handler(screen) + } else { + DispatchQueue.main.async { handler(screen) } + } + } +} + +#if os(iOS) || os(tvOS) + private extension UIViewController { + @objc func viewDidAppearOverride(animated: Bool) { + ApplicationScreenViewPublisher.shared.viewDidAppear(in: activeController) + + // it looks like we're calling ourselves, but we're actually + // calling the original implementation of viewDidAppear since it's been swizzled. + viewDidAppearOverride(animated: animated) + } + + private var activeController: UIViewController? { + // if a view is being dismissed, this will return nil + if let root = viewIfLoaded?.window?.rootViewController { + return root + } + // TODO: handle container controllers (see ph_topViewController) + return UIApplication.getCurrentWindow()?.rootViewController + } + } +#endif diff --git a/Pods/PostHog/PostHog/Screen Views/PostHogScreenViewIntegration.swift b/Pods/PostHog/PostHog/Screen Views/PostHogScreenViewIntegration.swift new file mode 100644 index 0000000..9f5f6a3 --- /dev/null +++ b/Pods/PostHog/PostHog/Screen Views/PostHogScreenViewIntegration.swift @@ -0,0 +1,79 @@ +// +// PostHogScreenViewIntegration.swift +// PostHog +// +// Created by Ioannis Josephides on 20/02/2025. +// + +import Foundation + +final class PostHogScreenViewIntegration: PostHogIntegration { + var requiresSwizzling: Bool { true } + + private static var integrationInstalledLock = NSLock() + private static var integrationInstalled = false + + private weak var postHog: PostHogSDK? + private var screenViewToken: RegistrationToken? + + func install(_ postHog: PostHogSDK) throws { + try PostHogScreenViewIntegration.integrationInstalledLock.withLock { + if PostHogScreenViewIntegration.integrationInstalled { + throw InternalPostHogError(description: "Autocapture integration already installed to another PostHogSDK instance.") + } + PostHogScreenViewIntegration.integrationInstalled = true + } + + self.postHog = postHog + + start() + } + + func uninstall(_ postHog: PostHogSDK) { + // uninstall only for integration instance + if self.postHog === postHog || self.postHog == nil { + stop() + self.postHog = nil + PostHogScreenViewIntegration.integrationInstalledLock.withLock { + PostHogScreenViewIntegration.integrationInstalled = false + } + } + } + + /** + Start capturing screen view events + */ + func start() { + let screenViewPublisher = DI.main.screenViewPublisher + screenViewToken = screenViewPublisher.onScreenView { [weak self] screen in + self?.captureScreenView(screen: screen) + } + } + + /** + Stop capturing screen view events + */ + func stop() { + screenViewToken = nil + } + + private func captureScreenView(screen screenName: String) { + guard let postHog else { return } + + if postHog.config.captureScreenViews { + postHog.screen(screenName) + } else { + hedgeLog("Skipping $screen event - captureScreenViews is disabled in configuration") + } + } +} + +#if TESTING + extension PostHogScreenViewIntegration { + static func clearInstalls() { + integrationInstalledLock.withLock { + integrationInstalled = false + } + } + } +#endif diff --git a/Pods/PostHog/PostHog/Surveys/BottomSection.swift b/Pods/PostHog/PostHog/Surveys/BottomSection.swift new file mode 100644 index 0000000..646d2fd --- /dev/null +++ b/Pods/PostHog/PostHog/Surveys/BottomSection.swift @@ -0,0 +1,23 @@ +// +// BottomSection.swift +// PostHog +// +// Created by Ioannis Josephides on 18/03/2025. +// + +#if os(iOS) + import SwiftUI + + @available(iOS 15.0, *) + struct BottomSection: View { + let label: String + let action: () -> Void + + var body: some View { + Button(label, action: action) + .buttonStyle(SurveyButtonStyle()) + .padding(.bottom, 16) + } + } + +#endif diff --git a/Pods/PostHog/PostHog/Surveys/ConfirmationMessage.swift b/Pods/PostHog/PostHog/Surveys/ConfirmationMessage.swift new file mode 100644 index 0000000..1cdfd45 --- /dev/null +++ b/Pods/PostHog/PostHog/Surveys/ConfirmationMessage.swift @@ -0,0 +1,42 @@ +// +// ConfirmationMessage.swift +// PostHog +// +// Created by Ioannis Josephides on 13/03/2025. +// + +#if os(iOS) + import SwiftUI + + @available(iOS 15.0, *) + struct ConfirmationMessage: View { + @Environment(\.surveyAppearance) private var appearance + + let onClose: () -> Void + + var body: some View { + VStack(spacing: 16) { + Text(appearance.thankYouMessageHeader) + .font(.body.bold()) + .foregroundStyle(foregroundTextColor) + if let description = appearance.thankYouMessageDescription, appearance.thankYouMessageDescriptionContentType == .text { + Text(description) + .font(.body) + .foregroundStyle(foregroundTextColor) + } + + BottomSection(label: appearance.thankYouMessageCloseButtonText, action: onClose) + .padding(.top, 20) + } + } + + private var foregroundTextColor: Color { + appearance.backgroundColor.getContrastingTextColor() + } + } + + @available(iOS 15.0, *) + #Preview { + ConfirmationMessage {} + } +#endif diff --git a/Pods/PostHog/PostHog/Surveys/Models/PostHogDisplaySurvey.swift b/Pods/PostHog/PostHog/Surveys/Models/PostHogDisplaySurvey.swift new file mode 100644 index 0000000..8f2345f --- /dev/null +++ b/Pods/PostHog/PostHog/Surveys/Models/PostHogDisplaySurvey.swift @@ -0,0 +1,57 @@ +// +// PostHogDisplaySurvey.swift +// PostHog +// +// Created by Ioannis Josephides on 18/06/2025. +// + +import Foundation + +/// A model representing a PostHog survey to be displayed to users +@objc public class PostHogDisplaySurvey: NSObject, Identifiable { + /// Unique identifier for the survey + public let id: String + /// Name of the survey + public let name: String + /// Array of questions to be presented in the survey + public let questions: [PostHogDisplaySurveyQuestion] + /// Optional appearance configuration for customizing the survey's look and feel + public let appearance: PostHogDisplaySurveyAppearance? + /// Optional date indicating when the survey should start being shown + public let startDate: Date? + /// Optional date indicating when the survey should stop being shown + public let endDate: Date? + + init( + id: String, + name: String, + questions: [PostHogDisplaySurveyQuestion], + appearance: PostHogDisplaySurveyAppearance?, + startDate: Date?, + endDate: Date? + ) { + self.id = id + self.name = name + self.questions = questions + self.appearance = appearance + self.startDate = startDate + self.endDate = endDate + super.init() + } +} + +/// Type of rating display for survey rating questions +@objc public enum PostHogDisplaySurveyRatingType: Int { + /// Display numeric rating options + case number + /// Display emoji rating options + case emoji +} + +/// Content type for text-based survey elements +@objc public enum PostHogDisplaySurveyTextContentType: Int { + /// Content should be rendered as HTML + case html + /// Content should be rendered as plain text + case text +} diff --git a/Pods/PostHog/PostHog/Surveys/Models/PostHogDisplaySurveyAppearance.swift b/Pods/PostHog/PostHog/Surveys/Models/PostHogDisplaySurveyAppearance.swift new file mode 100644 index 0000000..0af1bd3 --- /dev/null +++ b/Pods/PostHog/PostHog/Surveys/Models/PostHogDisplaySurveyAppearance.swift @@ -0,0 +1,88 @@ +// +// PostHogDisplaySurveyAppearance.swift +// PostHog +// +// Created by Ioannis Josephides on 19/06/2025. +// + +import Foundation + +/// Model that describes the appearance customization of a PostHog survey +@objc public class PostHogDisplaySurveyAppearance: NSObject { + // General + /// Optional font family to use throughout the survey + public let fontFamily: String? + /// Optional background color as web color (e.g. "#FFFFFF" or "white") + public let backgroundColor: String? + /// Optional border color as web color + public let borderColor: String? + + // Submit button + /// Optional background color for the submit button as web color + public let submitButtonColor: String? + /// Optional custom text for the submit button + public let submitButtonText: String? + /// Optional text color for the submit button as web color + public let submitButtonTextColor: String? + + // Text colors + /// Optional color for description text as web color + public let descriptionTextColor: String? + + // Rating buttons + /// Optional color for rating buttons as web color + public let ratingButtonColor: String? + /// Optional color for active/selected rating buttons as web color + public let ratingButtonActiveColor: String? + + // Input + /// Optional placeholder text for input fields + public let placeholder: String? + + // Thank you message + /// Whether to show a thank you message after survey completion + public let displayThankYouMessage: Bool + /// Optional header text for the thank you message + public let thankYouMessageHeader: String? + /// Optional description text for the thank you message + public let thankYouMessageDescription: String? + /// Optional content type for the thank you message description + public let thankYouMessageDescriptionContentType: PostHogDisplaySurveyTextContentType? + /// Optional text for the close button in the thank you message + public let thankYouMessageCloseButtonText: String? + + init( + fontFamily: String?, + backgroundColor: String?, + borderColor: String?, + submitButtonColor: String?, + submitButtonText: String?, + submitButtonTextColor: String?, + descriptionTextColor: String?, + ratingButtonColor: String?, + ratingButtonActiveColor: String?, + placeholder: String?, + displayThankYouMessage: Bool, + thankYouMessageHeader: String?, + thankYouMessageDescription: String?, + thankYouMessageDescriptionContentType: PostHogDisplaySurveyTextContentType?, + thankYouMessageCloseButtonText: String? + ) { + self.fontFamily = fontFamily + self.backgroundColor = backgroundColor + self.borderColor = borderColor + self.submitButtonColor = submitButtonColor + self.submitButtonText = submitButtonText + self.submitButtonTextColor = submitButtonTextColor + self.descriptionTextColor = descriptionTextColor + self.ratingButtonColor = ratingButtonColor + self.ratingButtonActiveColor = ratingButtonActiveColor + self.placeholder = placeholder + self.displayThankYouMessage = displayThankYouMessage + self.thankYouMessageHeader = thankYouMessageHeader + self.thankYouMessageDescription = thankYouMessageDescription + self.thankYouMessageDescriptionContentType = thankYouMessageDescriptionContentType + self.thankYouMessageCloseButtonText = thankYouMessageCloseButtonText + super.init() + } +} diff --git a/Pods/PostHog/PostHog/Surveys/Models/PostHogDisplaySurveyQuestion.swift b/Pods/PostHog/PostHog/Surveys/Models/PostHogDisplaySurveyQuestion.swift new file mode 100644 index 0000000..bd59ed8 --- /dev/null +++ b/Pods/PostHog/PostHog/Surveys/Models/PostHogDisplaySurveyQuestion.swift @@ -0,0 +1,150 @@ +// +// PostHogDisplaySurveyQuestion.swift +// PostHog +// +// Created by Ioannis Josephides on 19/06/2025. +// + +import Foundation + +/// Base class for all survey question types +@objc public class PostHogDisplaySurveyQuestion: NSObject { + /// The question ID, empty if none + @objc public let id: String + /// The main question text to display + @objc public let question: String + /// Optional additional description or context for the question + @objc public let questionDescription: String? + /// Content type for the question description (HTML or plain text) + @objc public let questionDescriptionContentType: PostHogDisplaySurveyTextContentType + /// Whether the question can be skipped + @objc public let isOptional: Bool + /// Optional custom text for the question's action button + @objc public let buttonText: String? + + init( + id: String, + question: String, + questionDescription: String?, + questionDescriptionContentType: PostHogDisplaySurveyTextContentType?, + isOptional: Bool, + buttonText: String? + ) { + self.id = id + self.question = question + self.questionDescription = questionDescription + self.questionDescriptionContentType = questionDescriptionContentType ?? .text + self.isOptional = isOptional + self.buttonText = buttonText + super.init() + } +} + +/// Represents an open-ended question where users can input free-form text +@objc public class PostHogDisplayOpenQuestion: PostHogDisplaySurveyQuestion { /**/ } + +/// Represents a question with a clickable link +@objc public class PostHogDisplayLinkQuestion: PostHogDisplaySurveyQuestion { + /// The URL that will be opened when the link is clicked + public let link: String? + + init( + id: String, + question: String, + questionDescription: String?, + questionDescriptionContentType: PostHogDisplaySurveyTextContentType?, + isOptional: Bool, + buttonText: String?, + link: String? + ) { + self.link = link + super.init( + id: id, + question: question, + questionDescription: questionDescription, + questionDescriptionContentType: questionDescriptionContentType, + isOptional: isOptional, + buttonText: buttonText + ) + } +} + +/// Represents a rating question where users can select a rating from a scale +@objc public class PostHogDisplayRatingQuestion: PostHogDisplaySurveyQuestion { + /// The type of rating scale (numbers, emoji) + public let ratingType: PostHogDisplaySurveyRatingType + /// The lower bound of the rating scale + public let scaleLowerBound: Int + /// The upper bound of the rating scale + public let scaleUpperBound: Int + /// The label for the lower bound of the rating scale + public let lowerBoundLabel: String + /// The label for the upper bound of the rating scale + public let upperBoundLabel: String + + init( + id: String, + question: String, + questionDescription: String?, + questionDescriptionContentType: PostHogDisplaySurveyTextContentType?, + isOptional: Bool, + buttonText: String?, + ratingType: PostHogDisplaySurveyRatingType, + scaleLowerBound: Int, + scaleUpperBound: Int, + lowerBoundLabel: String, + upperBoundLabel: String + ) { + self.ratingType = ratingType + self.scaleLowerBound = scaleLowerBound + self.scaleUpperBound = scaleUpperBound + self.lowerBoundLabel = lowerBoundLabel + self.upperBoundLabel = upperBoundLabel + super.init( + id: id, + question: question, + questionDescription: questionDescription, + questionDescriptionContentType: questionDescriptionContentType, + isOptional: isOptional, + buttonText: buttonText + ) + } +} + +/// Represents a multiple or single choice question where users can select one or more options +@objc public class PostHogDisplayChoiceQuestion: PostHogDisplaySurveyQuestion { + /// The list of options for the user to choose from + public let choices: [String] + /// Whether the question includes an "other" option for users to input free-form text + public let hasOpenChoice: Bool + /// Whether the options should be shuffled to randomize the order + public let shuffleOptions: Bool + /// Whether the user can select multiple options + public let isMultipleChoice: Bool + + init( + id: String, + question: String, + questionDescription: String?, + questionDescriptionContentType: PostHogDisplaySurveyTextContentType?, + isOptional: Bool, + buttonText: String?, + choices: [String], + hasOpenChoice: Bool, + shuffleOptions: Bool, + isMultipleChoice: Bool + ) { + self.choices = choices + self.hasOpenChoice = hasOpenChoice + self.shuffleOptions = shuffleOptions + self.isMultipleChoice = isMultipleChoice + super.init( + id: id, + question: question, + questionDescription: questionDescription, + questionDescriptionContentType: questionDescriptionContentType, + isOptional: isOptional, + buttonText: buttonText + ) + } +} diff --git a/Pods/PostHog/PostHog/Surveys/Models/PostHogNextSurveyQuestion.swift b/Pods/PostHog/PostHog/Surveys/Models/PostHogNextSurveyQuestion.swift new file mode 100644 index 0000000..4e09350 --- /dev/null +++ b/Pods/PostHog/PostHog/Surveys/Models/PostHogNextSurveyQuestion.swift @@ -0,0 +1,23 @@ +// +// PostHogNextSurveyQuestion.swift +// PostHog +// +// Created by Ioannis Josephides on 19/06/2025. +// + +import Foundation + +/// A model representing the next state of the survey progression. +@objc public class PostHogNextSurveyQuestion: NSObject { + /// The index of the next question to be displayed (0-based) + public let questionIndex: Int + /// Whether all questions have been answered and the survey is complete + /// Depending on the survey appearance configuration, you may want to show the "Thank you" message or dismiss the survey at this point + public let isSurveyCompleted: Bool + + init(questionIndex: Int, isSurveyCompleted: Bool) { + self.questionIndex = questionIndex + self.isSurveyCompleted = isSurveyCompleted + super.init() + } +} diff --git a/Pods/PostHog/PostHog/Surveys/Models/PostHogSurveyResponse.swift b/Pods/PostHog/PostHog/Surveys/Models/PostHogSurveyResponse.swift new file mode 100644 index 0000000..567b6c9 --- /dev/null +++ b/Pods/PostHog/PostHog/Surveys/Models/PostHogSurveyResponse.swift @@ -0,0 +1,107 @@ +// +// PostHogSurveyResponse.swift +// PostHog +// +// Created by Ioannis Josephides on 19/06/2025. +// + +import Foundation + +/// A model representing a user's response to a survey question +@objc @objcMembers +public class PostHogSurveyResponse: NSObject { + /// The type of response (link, rating, text, or multiple choice) + public let type: PostHogSurveyResponseType + /// Whether a link was clicked (for link questions) + public let linkClicked: Bool? + /// The numeric rating value (for rating questions) + public let ratingValue: Int? + /// The text response (for open questions) + public let textValue: String? + /// The selected options (for multiple or single choice questions) + public let selectedOptions: [String]? + + private init( + type: PostHogSurveyResponseType, + linkClicked: Bool? = nil, + ratingValue: Int? = nil, + textValue: String? = nil, + multipleChoiceValues: [String]? = nil + ) { + self.type = type + self.linkClicked = linkClicked + self.ratingValue = ratingValue + self.textValue = textValue + selectedOptions = multipleChoiceValues + } + + /// Creates a response for a link question + /// - Parameter clicked: Whether the link was clicked + public static func link(_ clicked: Bool) -> PostHogSurveyResponse { + PostHogSurveyResponse( + type: .link, + linkClicked: clicked, + ratingValue: nil, + textValue: nil, + multipleChoiceValues: nil + ) + } + + /// Creates a response for a rating question + /// - Parameter rating: The selected rating value + public static func rating(_ rating: Int?) -> PostHogSurveyResponse { + PostHogSurveyResponse( + type: .rating, + linkClicked: nil, + ratingValue: rating, + textValue: nil, + multipleChoiceValues: nil + ) + } + + /// Creates a response for an open-ended question + /// - Parameter openEnded: The text response + public static func openEnded(_ openEnded: String?) -> PostHogSurveyResponse { + PostHogSurveyResponse( + type: .openEnded, + linkClicked: nil, + ratingValue: nil, + textValue: openEnded, + multipleChoiceValues: nil + ) + } + + /// Creates a response for a single-choice question + /// - Parameter singleChoice: The selected option + public static func singleChoice(_ singleChoice: String?) -> PostHogSurveyResponse { + PostHogSurveyResponse( + type: .singleChoice, + linkClicked: nil, + ratingValue: nil, + textValue: nil, + multipleChoiceValues: { + if let singleChoice { [singleChoice] } else { nil } + }() + ) + } + + /// Creates a response for a multiple-choice question + /// - Parameter multipleChoice: The selected options + public static func multipleChoice(_ multipleChoice: [String]?) -> PostHogSurveyResponse { + PostHogSurveyResponse( + type: .multipleChoice, + linkClicked: nil, + ratingValue: nil, + textValue: nil, + multipleChoiceValues: multipleChoice + ) + } +} + +@objc public enum PostHogSurveyResponseType: Int { + case link + case rating + case openEnded + case singleChoice + case multipleChoice +} diff --git a/Pods/PostHog/PostHog/Surveys/PostHogSurveyIntegration.swift b/Pods/PostHog/PostHog/Surveys/PostHogSurveyIntegration.swift new file mode 100644 index 0000000..40b80fd --- /dev/null +++ b/Pods/PostHog/PostHog/Surveys/PostHogSurveyIntegration.swift @@ -0,0 +1,937 @@ +// +// PostHogSurveyIntegration.swift +// PostHog +// +// Created by Ioannis Josephides on 20/02/2025. +// + +#if os(iOS) || TESTING + + import Foundation + #if os(iOS) + import UIKit + #endif + + final class PostHogSurveyIntegration: PostHogIntegration { + var requiresSwizzling: Bool { true } + + private static var integrationInstalledLock = NSLock() + private static var integrationInstalled = false + + typealias SurveyCallback = (_ surveys: [PostHogSurvey]) -> Void + + private let kSurveySeenKeyPrefix = "seenSurvey_" + private let kSurveyResponseKey = "$survey_response" + + private var postHog: PostHogSDK? + private var config: PostHogConfig? { postHog?.config } + private var storage: PostHogStorage? { postHog?.storage } + private var remoteConfig: PostHogRemoteConfig? { postHog?.remoteConfig } + + private var allSurveysLock = NSLock() + private var allSurveys: [PostHogSurvey]? + + private var eventsToSurveysLock = NSLock() + private var eventsToSurveys: [String: [String]] = [:] + + private var seenSurveyKeysLock = NSLock() + private var seenSurveyKeys: [AnyHashable: Any]? + + private var eventActivatedSurveysLock = NSLock() + private var eventActivatedSurveys: Set = [] + + private var didBecomeActiveToken: RegistrationToken? + private var didLayoutViewToken: RegistrationToken? + + private var activeSurveyLock = NSLock() + private var activeSurvey: PostHogSurvey? + private var activeSurveyResponses: [String: PostHogSurveyResponse] = [:] // keyed by question identifier + private var activeSurveyCompleted: Bool = false + private var activeSurveyQuestionIndex: Int = 0 + + func install(_ postHog: PostHogSDK) throws { + try PostHogSurveyIntegration.integrationInstalledLock.withLock { + if PostHogSurveyIntegration.integrationInstalled { + throw InternalPostHogError(description: "Replay integration already installed to another PostHogSDK instance.") + } + PostHogSurveyIntegration.integrationInstalled = true + } + + self.postHog = postHog + start() + } + + func uninstall(_ postHog: PostHogSDK) { + if self.postHog === postHog || self.postHog == nil { + stop() + self.postHog = nil + PostHogSurveyIntegration.integrationInstalledLock.withLock { + PostHogSurveyIntegration.integrationInstalled = false + } + } + } + + func start() { + #if os(iOS) + // TODO: listen to screen view events + didLayoutViewToken = DI.main.viewLayoutPublisher.onViewLayout(throttle: 5) { [weak self] in + self?.showNextSurvey() + } + + didBecomeActiveToken = DI.main.appLifecyclePublisher.onDidBecomeActive { [weak self] in + self?.showNextSurvey() + } + #endif + } + + func stop() { + didBecomeActiveToken = nil + didLayoutViewToken = nil + #if os(iOS) + if #available(iOS 15.0, *) { + config?.surveysConfig.surveysDelegate.cleanupSurveys() + } + #endif + } + + /// Get surveys enabled for the current user + func getActiveMatchingSurveys( + forceReload: Bool = false, + callback: @escaping SurveyCallback + ) { + getSurveys(forceReload: forceReload) { [weak self] surveys in + guard let self else { return } + + let matchingSurveys = surveys + .lazy + .filter { // 1. unseen surveys, + !self.getSurveySeen(survey: $0) + } + .filter(\.isActive) // 2. that are active, + .filter { survey in // 3. and match display conditions, + // TODO: Check screen conditions + // TODO: Check event conditions + let deviceTypeCheck = self.doesSurveyDeviceTypesMatch(survey: survey) + return deviceTypeCheck + } + .filter { survey in // 4. and match linked flags + let allKeys: [String?] = [ + [survey.linkedFlagKey], + [survey.targetingFlagKey], + // we check internal targeting flags only if this survey cannot be activated repeatedly + [survey.canActivateRepeatedly ? nil : survey.internalTargetingFlagKey], + survey.featureFlagKeys?.compactMap { kvp in + kvp.key.isEmpty ? nil : kvp.value + } ?? [], + ] + .joined() + .compactMap { $0 } + .filter { !$0.isEmpty } + + // all keys must be enabled + return Set(allKeys) + .allSatisfy(self.isSurveyFeatureFlagEnabled) + } + .filter { survey in // 5. and if event-based, have been activated by that event + survey.hasEvents ? self.isSurveyEventActivated(survey: survey) : true + } + + callback(Array(matchingSurveys)) + } + } + + // TODO: Decouple PostHogSDK and use registration handlers instead + /// Called from PostHogSDK instance when an event is captured + func onEvent(event: String) { + let activatedSurveys = eventsToSurveysLock.withLock { eventsToSurveys[event] } ?? [] + guard !activatedSurveys.isEmpty else { return } + + eventActivatedSurveysLock.withLock { + for survey in activatedSurveys { + eventActivatedSurveys.insert(survey) + } + } + + DispatchQueue.main.async { + self.showNextSurvey() + } + } + + private func getSurveys(forceReload: Bool = false, callback: @escaping SurveyCallback) { + guard let remoteConfig else { + return + } + + guard let config = config, config._surveys else { + hedgeLog("Surveys disabled. Not loading surveys.") + return callback([]) + } + + // mem cache + let allSurveys = allSurveysLock.withLock { self.allSurveys } + + if let allSurveys, !forceReload { + callback(allSurveys) + } else { + // first or force load + getRemoteConfig(remoteConfig, forceReload: forceReload) { [weak self] config in + self?.getFeatureFlags(remoteConfig, forceReload: forceReload) { [weak self] _ in + self?.decodeAndSetSurveys(remoteConfig: config, callback: callback) + } + } + } + } + + private func getRemoteConfig( + _ remoteConfig: PostHogRemoteConfig, + forceReload: Bool = false, + callback: (([String: Any]?) -> Void)? = nil + ) { + let cached = remoteConfig.getRemoteConfig() + if cached == nil || forceReload { + remoteConfig.reloadRemoteConfig(callback: callback) + } else { + callback?(cached) + } + } + + private func getFeatureFlags( + _ remoteConfig: PostHogRemoteConfig, + forceReload: Bool = false, + callback: (([String: Any]?) -> Void)? = nil + ) { + let cached = remoteConfig.getFeatureFlags() + if cached == nil || forceReload { + remoteConfig.reloadFeatureFlags(callback: callback) + } else { + callback?(cached) + } + } + + private func decodeAndSetSurveys(remoteConfig: [String: Any]?, callback: @escaping SurveyCallback) { + let loadedSurveys: [PostHogSurvey] = decodeSurveys(from: remoteConfig ?? [:]) + + let eventMap = loadedSurveys.reduce(into: [String: [String]]()) { result, current in + if let surveyEvents = current.conditions?.events?.values.map(\.name) { + for event in surveyEvents { + result[event, default: []].append(current.id) + } + } + } + + allSurveysLock.withLock { + self.allSurveys = loadedSurveys + } + eventsToSurveysLock.withLock { + self.eventsToSurveys = eventMap + } + + callback(loadedSurveys) + } + + private func decodeSurveys(from remoteConfig: [String: Any]) -> [PostHogSurvey] { + guard let surveysJSON = remoteConfig["surveys"] as? [[String: Any]] else { + // surveys not json, disabled + return [] + } + + do { + let jsonData = try JSONSerialization.data(withJSONObject: surveysJSON) + return try PostHogApi.jsonDecoder.decode([PostHogSurvey].self, from: jsonData) + } catch { + hedgeLog("Error decoding Surveys: \(error)") + return [] + } + } + + private func isSurveyFeatureFlagEnabled(flagKey: String?) -> Bool { + guard let flagKey, let postHog else { + return false + } + + return postHog.isFeatureEnabled(flagKey) + } + + private func canRenderSurvey(survey: PostHogSurvey) -> Bool { + // only render popover surveys for now + survey.type == .popover + } + + /// Shows next survey in queue. No-op if a survey is already being shown + private func showNextSurvey() { + #if os(iOS) + guard #available(iOS 15.0, *) else { + hedgeLog("[Surveys] Surveys can be rendered only on iOS 15+") + return + } + + guard canShowNextSurvey() else { return } + + // Check if there is a new popover surveys to be displayed + getActiveMatchingSurveys { activeSurveys in + if let survey = activeSurveys.first(where: self.canRenderSurvey) { + // set survey as active + self.setActiveSurvey(survey: survey) + + DispatchQueue.main.async { [weak self] in + if let self { + // render the survey + self.postHog?.config.surveysConfig.surveysDelegate.renderSurvey( + survey.toDisplaySurvey(), + onSurveyShown: self.handleSurveyShown, + onSurveyResponse: self.handleSurveyResponse, + onSurveyClosed: self.handleSurveyClosed + ) + } + } + } + } + #endif + } + + /// Returns the computed storage key for a given survey + private func getSurveySeenKey(_ survey: PostHogSurvey) -> String { + let surveySeenKey = "\(kSurveySeenKeyPrefix)\(survey.id)" + if let currentIteration = survey.currentIteration, currentIteration > 0 { + return "\(surveySeenKey)_\(currentIteration)" + } + return surveySeenKey + } + + /// Checks storage for seenSurvey_ key and returns its value + /// + /// Note: if the survey can be repeatedly activated by its events, or if the key is missing, this value will default to false + private func getSurveySeen(survey: PostHogSurvey) -> Bool { + if survey.canActivateRepeatedly { + // if this survey can activate repeatedly, we override this return value + return false + } + + let key = getSurveySeenKey(survey) + let surveysSeen = getSeenSurveyKeys() + let surveySeen = surveysSeen[key] as? Bool ?? false + + return surveySeen + } + + /// Mark a survey as seen + private func setSurveySeen(survey: PostHogSurvey) { + let key = getSurveySeenKey(survey) + let seenKeys = seenSurveyKeysLock.withLock { + seenSurveyKeys?[key] = true + return seenSurveyKeys + } + + storage?.setDictionary(forKey: .surveySeen, contents: seenKeys ?? [:]) + } + + /// Returns survey seen list (and mem-cache from disk if needed) + private func getSeenSurveyKeys() -> [AnyHashable: Any] { + seenSurveyKeysLock.withLock { + if seenSurveyKeys == nil { + seenSurveyKeys = storage?.getDictionary(forKey: .surveySeen) ?? [:] + } + return seenSurveyKeys ?? [:] + } + } + + /// Returns given match type or default value if nil + private func getMatchTypeOrDefault(_ matchType: PostHogSurveyMatchType?) -> PostHogSurveyMatchType { + matchType ?? .iContains + } + + /// Checks if a survey with a device type condition matches the current device type + private func doesSurveyDeviceTypesMatch(survey: PostHogSurvey) -> Bool { + guard + let conditions = survey.conditions, + let deviceTypes = conditions.deviceTypes, deviceTypes.count > 0 + else { + // not device type restrictions, assume true + return true + } + + guard + let deviceType = PostHogContext.deviceType + else { + // if we don't know the current device type, we assume it is not a match + return false + } + + let matchType = getMatchTypeOrDefault(conditions.deviceTypesMatchType) + + return matchType.matches(targets: deviceTypes, value: deviceType) + } + + /// Checks if a survey has been previously activated by an associated event + private func isSurveyEventActivated(survey: PostHogSurvey) -> Bool { + eventActivatedSurveysLock.withLock { + eventActivatedSurveys.contains(survey.id) + } + } + + /// Handle a survey that is shown + private func handleSurveyShown(survey: PostHogDisplaySurvey) { + let activeSurvey = activeSurveyLock.withLock { self.activeSurvey } + + guard let activeSurvey, survey.id == activeSurvey.id else { + hedgeLog("[Surveys] Received a show event for a non-active survey") + return + } + + sendSurveyShownEvent(survey: activeSurvey) + + // clear up event-activated surveys + if activeSurvey.hasEvents { + eventActivatedSurveysLock.withLock { + _ = eventActivatedSurveys.remove(activeSurvey.id) + } + } + } + + /// Handle a survey response + /// Processes a user's response to a survey question and determines the next question to display + /// - Parameters: + /// - survey: The currently displayed survey + /// - index: The index of the current question being answered + /// - response: The user's response to the current question + /// - Returns: The next question to display based on branching logic, or nil if there was an error + private func handleSurveyResponse(survey: PostHogDisplaySurvey, index: Int, response: PostHogSurveyResponse) -> PostHogNextSurveyQuestion? { + let (activeSurvey, activeSurveyQuestionIndex) = activeSurveyLock.withLock { (self.activeSurvey, self.activeSurveyQuestionIndex) } + + guard let activeSurvey, survey.id == activeSurvey.id else { + hedgeLog("[Surveys] Received a response event for a non-active survey") + return nil + } + + // TODO: ideally the handleSurveyResponse should pass the question ID as param but it would break the Flutter SDK for older versions + let questionId: String + if index < survey.questions.count { + let question = survey.questions[index] + questionId = question.id + } else { + // this should not happen, its only for back compatibility + questionId = "" + } + + // 2. Get next step + let nextStep = getNextSurveyStep( + survey: activeSurvey, + questionIndex: activeSurveyQuestionIndex, + response: response + ) + + let (isCompleted, nextIndex) = switch nextStep { + case let .index(nextIndex): (false, nextIndex) + case .end: (true, activeSurveyQuestionIndex) + } + + let nextSurveyQuestion = PostHogNextSurveyQuestion( + questionIndex: nextIndex, + isSurveyCompleted: isCompleted + ) + + // update response, next question index and survey completion + let allResponses = setActiveSurveyResponse(id: questionId, index: index, response: response, nextQuestion: nextSurveyQuestion) + + // send event if needed + // TODO: Partial responses + if isCompleted { + sendSurveySentEvent(survey: activeSurvey, responses: allResponses) + } + + return nextSurveyQuestion + } + + /// Handle a survey dismiss + private func handleSurveyClosed(survey: PostHogDisplaySurvey) { + let (activeSurvey, activeSurveyCompleted) = activeSurveyLock.withLock { (self.activeSurvey, self.activeSurveyCompleted) } + + guard let activeSurvey, survey.id == activeSurvey.id else { + hedgeLog("Received a close event for a non-active survey") + return + } + + // send survey dismissed event if needed + if !activeSurveyCompleted { + sendSurveyDismissedEvent(survey: activeSurvey) + } + + // mark as seen + setSurveySeen(survey: activeSurvey) + + // clear active survey + clearActiveSurvey() + + // show next survey in queue, if any, after a short delay + DispatchQueue.main.asyncAfter(deadline: .now() + 0.75) { + self.showNextSurvey() + } + } + + /// Sends a `survey shown` event to PostHog instance + private func sendSurveyShownEvent(survey: PostHogSurvey) { + sendSurveyEvent( + event: "survey shown", + survey: survey + ) + } + + /// Sends a `survey sent` event to PostHog instance + /// Sends a survey completion event to PostHog with all collected responses + /// - Parameters: + /// - survey: The completed survey + /// - responses: Dictionary of collected responses for each question + private func sendSurveySentEvent(survey: PostHogSurvey, responses: [String: PostHogSurveyResponse]) { + let responsesProperties: [String: Any] = responses.compactMapValues { resp in + switch resp.type { + case .link: resp.linkClicked == true ? "link clicked" : nil + case .multipleChoice: resp.selectedOptions + case .singleChoice: resp.selectedOptions?.first + case .openEnded: resp.textValue + case .rating: resp.ratingValue.map { "\($0)" } + } + } + + let surveyQuestions = survey.questions.enumerated().map { index, question in + let responseKey = question.id.isEmpty ? getOldResponseKey(for: index) : getNewResponseKey(for: question.id) + var questionData: [String: Any] = [ + "id": question.id, + "question": question.question, + ] + + if let response = responsesProperties[responseKey] { + questionData["response"] = response + } + + return questionData + } + + let questionProperties: [String: Any] = [ + "$survey_questions": surveyQuestions, + "$set": [getSurveyInteractionProperty(survey: survey, property: "responded"): true], + ] + + // TODO: Should be doing some validation before sending the event? + + let additionalProperties = questionProperties.merging(responsesProperties, uniquingKeysWith: { _, new in new }) + + sendSurveyEvent( + event: "survey sent", + survey: survey, + additionalProperties: additionalProperties + ) + } + + /// Sends a `survey dismissed` event to PostHog instance + private func sendSurveyDismissedEvent(survey: PostHogSurvey) { + let additionalProperties: [String: Any] = [ + "$set": [ + getSurveyInteractionProperty(survey: survey, property: "dismissed"): true, + ], + ] + + sendSurveyEvent( + event: "survey dismissed", + survey: survey, + additionalProperties: additionalProperties + ) + } + + private func sendSurveyEvent(event: String, survey: PostHogSurvey, additionalProperties: [String: Any] = [:]) { + guard let postHog else { + hedgeLog("[\(event)] event not captured, PostHog instance not found.") + return + } + + var properties = getBaseSurveyEventProperties(for: survey) + properties.merge(additionalProperties) { _, new in new } + + postHog.capture(event, properties: properties) + } + + private func getBaseSurveyEventProperties(for survey: PostHogSurvey) -> [String: Any] { + // TODO: Add session replay screen name + let props: [String: Any?] = [ + "$survey_name": survey.name, + "$survey_id": survey.id, + "$survey_iteration": survey.currentIteration, + "$survey_iteration_start_date": survey.currentIterationStartDate.map(toISO8601String), + ] + return props.compactMapValues { $0 } + } + + private func getSurveyInteractionProperty(survey: PostHogSurvey, property: String) -> String { + var surveyProperty = "$survey_\(property)/\(survey.id)" + + if let currentIteration = survey.currentIteration, currentIteration > 0 { + surveyProperty = "$survey_\(property)/\(survey.id)/\(currentIteration)" + } + + return surveyProperty + } + + private func setActiveSurvey(survey: PostHogSurvey) { + activeSurveyLock.withLock { + if activeSurvey == nil { + activeSurvey = survey + activeSurveyCompleted = false + activeSurveyResponses = [:] + activeSurveyQuestionIndex = 0 + } + } + } + + private func clearActiveSurvey() { + activeSurveyLock.withLock { + activeSurvey = nil + activeSurveyCompleted = false + activeSurveyResponses = [:] + activeSurveyQuestionIndex = 0 + } + } + + /// Stores a response for the current question in the active survey, and returns updated responses + /// - Parameters: + /// - id: The question ID, empty if none + /// - index: The index of the question being answered + /// - response: The user's response to store + /// - nextQuestion: The next question index and completion info + private func setActiveSurveyResponse( + id: String, + index: Int, + response: PostHogSurveyResponse, + nextQuestion: PostHogNextSurveyQuestion + ) -> [String: PostHogSurveyResponse] { + activeSurveyLock.withLock { + // keeping the old response key format for back compatibility + activeSurveyResponses[getOldResponseKey(for: index)] = response + if !id.isEmpty { + // setting the new response key format + activeSurveyResponses[getNewResponseKey(for: id)] = response + } + activeSurveyQuestionIndex = nextQuestion.questionIndex + activeSurveyCompleted = nextQuestion.isSurveyCompleted + return activeSurveyResponses + } + } + + /// Returns next question index + /// - Parameters: + /// - survey: The survey which contains the question + /// - questionIndex: The current question index + /// - response: The current question response + /// - Returns: The next question `.index()` if found, or `.end` survey reach the end + private func getNextSurveyStep( + survey: PostHogSurvey, + questionIndex: Int, + response: PostHogSurveyResponse + ) -> NextSurveyQuestion { + let question = survey.questions[questionIndex] + let nextQuestionIndex = min(questionIndex + 1, survey.questions.count - 1) + + guard let branching = question.branching else { + return questionIndex == survey.questions.count - 1 ? .end : .index(nextQuestionIndex) + } + + switch branching { + case .end: + return .end + + case let .specificQuestion(index): + return .index(min(index, survey.questions.count - 1)) + + case let .responseBased(responseValues): + return getResponseBasedNextQuestionIndex( + survey: survey, + question: question, + response: response, + responseValues: responseValues + ) ?? .index(nextQuestionIndex) + + case .next, .unknown: + return .index(nextQuestionIndex) + } + } + + /// Returns next question index based on response value (from responseValues dictionary) + /// + /// - Parameters: + /// - survey: The survey which contains the question + /// - question: The current question + /// - response: The response to the current question + /// - responseValues: The response values dictionary + /// - Returns: The next index if found in the `responseValues` + private func getResponseBasedNextQuestionIndex( + survey: PostHogSurvey, + question: PostHogSurveyQuestion, + response: PostHogSurveyResponse?, + responseValues: [String: Any] + ) -> NextSurveyQuestion? { + guard let response else { + hedgeLog("[Surveys] Got response based branching, but missing the actual response.") + return nil + } + + switch (question, response.type) { + case let (.singleChoice(singleChoiceQuestion), .singleChoice): + let singleChoiceResponse = response.selectedOptions?.first + var responseIndex = singleChoiceQuestion.choices.firstIndex(of: singleChoiceResponse ?? "") + + if responseIndex == nil, singleChoiceQuestion.hasOpenChoice == true { + // if the response is not found in the choices, it must be the open choice, which is always the last choice + responseIndex = singleChoiceQuestion.choices.count - 1 + } + + if let responseIndex, let nextIndex = responseValues["\(responseIndex)"] { + return processBranchingStep(nextIndex: nextIndex, totalQuestions: survey.questions.count) + } + + hedgeLog("[Surveys] Could not find response index for specific question.") + return nil + + case let (.rating(ratingQuestion), .rating): + if let responseInt = response.ratingValue, + let ratingBucket = getRatingBucketForResponseValue(scale: ratingQuestion.scale, value: responseInt), + let nextIndex = responseValues[ratingBucket] + { + return processBranchingStep(nextIndex: nextIndex, totalQuestions: survey.questions.count) + } + hedgeLog("[Surveys] Could not get response bucket for rating question.") + return nil + + default: + hedgeLog("[Surveys] Got response based branching for an unsupported question type.") + return nil + } + } + + /// Returns next question index based on a branching step result + /// - Parameters: + /// - nextIndex: The next index to process + /// - totalQuestions: The total number of questions in the survey + /// - Returns: The next question index if found, or nil if not + private func processBranchingStep(nextIndex: Any, totalQuestions: Int) -> NextSurveyQuestion? { + if let nextIndex = nextIndex as? Int { + return .index(min(nextIndex, totalQuestions - 1)) + } + if let nextIndex = nextIndex as? String, nextIndex.lowercased() == "end" { + return .end + } + return nil + } + + // Gets the response bucket for a given rating response value, given the scale. + // For example, for a scale of 3, the buckets are "negative", "neutral" and "positive". + private func getRatingBucketForResponseValue(scale: PostHogSurveyRatingScale, value: Int) -> String? { + // swiftlint:disable:previous cyclomatic_complexity + // Validate input ranges + switch scale { + case .threePoint where RatingBucket.threePointRange.contains(value): + switch value { + case BucketThresholds.ThreePoint.negatives: return RatingBucket.negative + case BucketThresholds.ThreePoint.neutrals: return RatingBucket.neutral + default: return RatingBucket.positive + } + + case .fivePoint where RatingBucket.fivePointRange.contains(value): + switch value { + case BucketThresholds.FivePoint.negatives: return RatingBucket.negative + case BucketThresholds.FivePoint.neutrals: return RatingBucket.neutral + default: return RatingBucket.positive + } + + case .sevenPoint where RatingBucket.sevenPointRange.contains(value): + switch value { + case BucketThresholds.SevenPoint.negatives: return RatingBucket.negative + case BucketThresholds.SevenPoint.neutrals: return RatingBucket.neutral + default: return RatingBucket.positive + } + + case .tenPoint where RatingBucket.tenPointRange.contains(value): + switch value { + case BucketThresholds.TenPoint.detractors: return RatingBucket.detractors + case BucketThresholds.TenPoint.passives: return RatingBucket.passives + default: return RatingBucket.promoters + } + + default: + hedgeLog("[Surveys] Cannot get rating bucket for invalid scale: \(scale). The scale must be one of: 3 (1-3), 5 (1-5), 7 (1-7), 10 (0-10).") + return nil + } + } + + // Returns the old survey response key for a specific question index + private func getOldResponseKey(for index: Int) -> String { + index == 0 ? kSurveyResponseKey : "\(kSurveyResponseKey)_\(index)" + } + + // Returns the new survey response key for a specific question id + private func getNewResponseKey(for questionId: String) -> String { + "\(kSurveyResponseKey)_\(questionId)" + } + + func canShowNextSurvey() -> Bool { + activeSurveyLock.withLock { activeSurvey == nil } + } + } + + enum NextSurveyQuestion { + case index(Int) + case end + } + + extension PostHogSurvey: CustomStringConvertible { + var description: String { + "\(name) [\(id)]" + } + } + + extension PostHogSurvey { + var isActive: Bool { + startDate != nil && endDate == nil + } + + var hasEvents: Bool { + conditions?.events?.values.count ?? 0 > 0 + } + + var canActivateRepeatedly: Bool { + conditions?.events?.repeatedActivation == true && hasEvents + } + } + + private extension PostHogSurveyMatchType { + func matches(targets: [String], value: String) -> Bool { + switch self { + // any of the targets contain the value (matched lowercase) + case .iContains: + targets.contains { target in + target.lowercased().contains(value.lowercased()) + } + // *none* of the targets contain the value (matched lowercase) + case .notIContains: + targets.allSatisfy { target in + !target.lowercased().contains(value.lowercased()) + } + // any of the targets match with regex + case .regex: + targets.contains { target in + target.range(of: value, options: .regularExpression) != nil + } + // *none* if the targets match with regex + case .notRegex: + targets.allSatisfy { target in + target.range(of: value, options: .regularExpression) == nil + } + // any of the targets is an exact match + case .exact: + targets.contains { target in + target == value + } + // *none* of the targets is an exact match + case .isNot: + targets.allSatisfy { target in + target != value + } + case .unknown: + false + } + } + } + + private enum RatingBucket { + // Bucket names + static let negative = "negative" + static let neutral = "neutral" + static let positive = "positive" + static let detractors = "detractors" + static let passives = "passives" + static let promoters = "promoters" + + // Scale ranges + static let threePointRange = 1 ... 3 + static let fivePointRange = 1 ... 5 + static let sevenPointRange = 1 ... 7 + static let tenPointRange = 0 ... 10 + } + + private enum BucketThresholds { + enum ThreePoint { + static let negatives = 1 ... 1 + static let neutrals = 2 ... 2 + } + + enum FivePoint { + static let negatives = 1 ... 2 + static let neutrals = 3 ... 3 + } + + enum SevenPoint { + static let negatives = 1 ... 3 + static let neutrals = 4 ... 4 + } + + enum TenPoint { + static let detractors = 0 ... 6 + static let passives = 7 ... 8 + } + } + + #if TESTING + extension PostHogSurveyMatchType { + var matchFunction: (_ targets: [String], _ value: String) -> Bool { + matches + } + } + + extension PostHogSurveyIntegration { + func setSurveys(_ surveys: [PostHogSurvey]) { + allSurveys = surveys + } + + func setShownSurvey(_ survey: PostHogSurvey) { + clearActiveSurvey() + setActiveSurvey(survey: survey) + } + + func getNextQuestion(index: Int, response: PostHogSurveyResponse) -> (Int, Bool)? { + guard let activeSurvey else { return nil } + activeSurveyQuestionIndex = index + if let next = handleSurveyResponse(survey: activeSurvey.toDisplaySurvey(), index: index, response: response) { + return (next.questionIndex, next.isSurveyCompleted) + } + return nil + } + + func testSendSurveyShownEvent(survey: PostHogSurvey) { + sendSurveyShownEvent(survey: survey) + } + + func testSendSurveySentEvent(survey: PostHogSurvey, responses: [String: PostHogSurveyResponse]) { + sendSurveySentEvent(survey: survey, responses: responses) + } + + func testSendSurveyDismissedEvent(survey: PostHogSurvey) { + sendSurveyDismissedEvent(survey: survey) + } + + func testGetBaseSurveyEventProperties(for survey: PostHogSurvey) -> [String: Any] { + getBaseSurveyEventProperties(for: survey) + } + + func testGetSurveyInteractionProperty(survey: PostHogSurvey, property: String) -> String { + getSurveyInteractionProperty(survey: survey, property: property) + } + + func testGetResponseKey(questionId: String) -> String { + getNewResponseKey(for: questionId) + } + + static func clearInstalls() { + integrationInstalledLock.withLock { + integrationInstalled = false + } + } + } + #endif +#endif diff --git a/Pods/PostHog/PostHog/Surveys/PostHogSurveysConfig.swift b/Pods/PostHog/PostHog/Surveys/PostHogSurveysConfig.swift new file mode 100644 index 0000000..776f2ec --- /dev/null +++ b/Pods/PostHog/PostHog/Surveys/PostHogSurveysConfig.swift @@ -0,0 +1,53 @@ +// +// PostHogSurveysConfig.swift +// PostHog +// +// Created by Ioannis Josephides on 24/04/2025. +// + +import Foundation + +@objc public class PostHogSurveysConfig: NSObject { + /// Delegate responsible for managing survey presentation in your app. + /// Handles survey rendering, response collection, and lifecycle events. + /// You can provide your own delegate for a custom survey presentation. + /// + /// Defaults to `PostHogSurveysDefaultDelegate` which provides a standard survey UI. + public var surveysDelegate: PostHogSurveysDelegate = PostHogSurveysDefaultDelegate() +} + +/// To be called when a survey is successfully shown to the user +/// - Parameter survey: The survey that was displayed +public typealias OnPostHogSurveyShown = (_ survey: PostHogDisplaySurvey) -> Void + +/// To be called when a user responds to a survey question +/// - Parameters: +/// - survey: The current survey being displayed +/// - index: The index of the question being answered +/// - response: The user's response to the question +/// - Returns: The next question state (next question index and completion flag) +public typealias OnPostHogSurveyResponse = (_ survey: PostHogDisplaySurvey, _ index: Int, _ response: PostHogSurveyResponse) -> PostHogNextSurveyQuestion? + +/// To be called when a survey is dismissed +/// - Parameter survey: The survey that was closed +public typealias OnPostHogSurveyClosed = (_ survey: PostHogDisplaySurvey) -> Void + +@objc public protocol PostHogSurveysDelegate { + /// Called when an activated PostHog survey needs to be rendered on the app's UI + /// + /// - Parameters: + /// - survey: The survey to be displayed to the user + /// - onSurveyShown: To be called when the survey is successfully displayed to the user. + /// - onSurveyResponse: To be called the user submits a response to a question. + /// - onSurveyClosed: To be called when the survey is dismissed + @objc func renderSurvey( + _ survey: PostHogDisplaySurvey, + onSurveyShown: @escaping OnPostHogSurveyShown, + onSurveyResponse: @escaping OnPostHogSurveyResponse, + onSurveyClosed: @escaping OnPostHogSurveyClosed + ) + + /// Called when surveys are stopped to clean up any UI elements and reset the survey display state. + /// This method should handle the dismissal of any active surveys and cleanup of associated resources. + @objc func cleanupSurveys() +} diff --git a/Pods/PostHog/PostHog/Surveys/PostHogSurveysDefaultDelegate.swift b/Pods/PostHog/PostHog/Surveys/PostHogSurveysDefaultDelegate.swift new file mode 100644 index 0000000..6c854c5 --- /dev/null +++ b/Pods/PostHog/PostHog/Surveys/PostHogSurveysDefaultDelegate.swift @@ -0,0 +1,70 @@ +// +// PostHogSurveysDefaultDelegate.swift +// PostHog +// +// Created by Ioannis Josephides on 18/06/2025. +// + +#if os(iOS) + import UIKit +#else + import Foundation +#endif + +final class PostHogSurveysDefaultDelegate: PostHogSurveysDelegate { + #if os(iOS) + private var surveysWindow: UIWindow? + private var displayController: SurveyDisplayController? + #endif + + func renderSurvey( + _ survey: PostHogDisplaySurvey, + onSurveyShown: @escaping OnPostHogSurveyShown, + onSurveyResponse: @escaping OnPostHogSurveyResponse, + onSurveyClosed: @escaping OnPostHogSurveyClosed + ) { + #if os(iOS) + guard #available(iOS 15.0, *) else { return } + + if surveysWindow == nil { + // setup window for first-time display + setupWindow() + } + + // Setup handlers + displayController?.onSurveyShown = onSurveyShown + displayController?.onSurveyResponse = onSurveyResponse + displayController?.onSurveyClosed = onSurveyClosed + + // Display survey + displayController?.showSurvey(survey) + #endif + } + + func cleanupSurveys() { + #if os(iOS) + displayController?.dismissSurvey() // dismiss any active surveys + surveysWindow?.rootViewController?.dismiss(animated: true) { + self.surveysWindow?.isHidden = true + self.surveysWindow = nil + self.displayController = nil + } + #endif + } + + #if os(iOS) + @available(iOS 15.0, *) + private func setupWindow() { + if let activeWindow = UIApplication.getCurrentWindow(), let activeScene = activeWindow.windowScene { + let controller = SurveyDisplayController() + displayController = controller + surveysWindow = SurveysWindow( + controller: controller, + scene: activeScene + ) + surveysWindow?.isHidden = false + surveysWindow?.windowLevel = activeWindow.windowLevel + 1 + } + } + #endif +} diff --git a/Pods/PostHog/PostHog/Surveys/QuestionHeader.swift b/Pods/PostHog/PostHog/Surveys/QuestionHeader.swift new file mode 100644 index 0000000..362d0a7 --- /dev/null +++ b/Pods/PostHog/PostHog/Surveys/QuestionHeader.swift @@ -0,0 +1,49 @@ +// +// QuestionHeader.swift +// PostHog +// +// Created by Ioannis Josephides on 13/03/2025. +// + +#if os(iOS) + import SwiftUI + + @available(iOS 15.0, *) + struct QuestionHeader: View { + @Environment(\.surveyAppearance) private var appearance + + let question: String + let description: String? + let contentType: PostHogDisplaySurveyTextContentType + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + Text(question) + .font(.body.bold()) + .foregroundColor(foregroundTextColor) + .multilineTextAlignment(.leading) + if let description, !description.isEmpty, contentType == .text { + Text(description) + .font(.callout) + .foregroundColor(foregroundTextColor) + .multilineTextAlignment(.leading) + } + } + .padding(.top, 16) + .frame(maxWidth: .infinity, alignment: .leading) + } + + private var foregroundTextColor: Color { + appearance.backgroundColor.getContrastingTextColor() + } + } + + @available(iOS 15.0, *) + #Preview { + QuestionHeader( + question: "What can we do to improve our product?", + description: "Any feedback will be helpful!", + contentType: .text + ) + } +#endif diff --git a/Pods/PostHog/PostHog/Surveys/QuestionTypes.swift b/Pods/PostHog/PostHog/Surveys/QuestionTypes.swift new file mode 100644 index 0000000..b55b059 --- /dev/null +++ b/Pods/PostHog/PostHog/Surveys/QuestionTypes.swift @@ -0,0 +1,243 @@ +// +// QuestionTypes.swift +// PostHog +// +// Created by Ioannis Josephides on 13/03/2025. +// + +#if os(iOS) + import SwiftUI + + @available(iOS 15.0, *) + struct OpenTextQuestionView: View { + @Environment(\.surveyAppearance) private var appearance + + let question: PostHogDisplayOpenQuestion + let onNextQuestion: (String?) -> Void + + @State private var text: String = "" + + var body: some View { + VStack(spacing: 16) { + QuestionHeader( + question: question.question, + description: question.questionDescription, + contentType: question.questionDescriptionContentType + ) + + TextEditor(text: $text) + .frame(height: 80) + .overlay( + Group { + if text.isEmpty { + Text(appearance.placeholder ?? "Start typing...") + .foregroundColor(.secondary) + .offset(x: 5, y: 8) + } + }, + alignment: .topLeading + ) + .padding(8) + .tint(.black) + .background( + RoundedRectangle(cornerRadius: 6) + .stroke(Color(uiColor: .secondaryLabel), lineWidth: 1) + .background(Color.white) + ) + + BottomSection(label: question.buttonText ?? appearance.submitButtonText) { + let resp = text.trimmingCharacters(in: .whitespaces) + onNextQuestion(resp.isEmpty ? nil : text) + } + .disabled(!canSubmit) + } + } + + private var canSubmit: Bool { + if question.isOptional { return true } + return !text.isEmpty + } + } + + @available(iOS 15.0, *) + struct LinkQuestionView: View { + @Environment(\.surveyAppearance) private var appearance + + let question: PostHogDisplayLinkQuestion + let onNextQuestion: (Bool) -> Void + + var body: some View { + VStack(spacing: 16) { + QuestionHeader( + question: question.question, + description: question.questionDescription, + contentType: question.questionDescriptionContentType + ) + + BottomSection(label: question.buttonText ?? appearance.submitButtonText) { + onNextQuestion(true) + if let link, UIApplication.shared.canOpenURL(link) { + UIApplication.shared.open(link) + } + } + } + } + + private var link: URL? { + if let link = question.link { + return URL(string: link) + } + return nil + } + } + + @available(iOS 15.0, *) + struct RatingQuestionView: View { + @Environment(\.surveyAppearance) private var appearance + + let question: PostHogDisplayRatingQuestion + let onNextQuestion: (Int?) -> Void + @State var rating: Int? + + var body: some View { + VStack(spacing: 16) { + QuestionHeader( + question: question.question, + description: question.questionDescription, + contentType: question.questionDescriptionContentType + ) + + if question.ratingType == .emoji { + EmojiRating( + selectedValue: $rating, + scale: scale, + lowerBoundLabel: question.lowerBoundLabel, + upperBoundLabel: question.upperBoundLabel + ) + } else { + NumberRating( + selectedValue: $rating, + scale: scale, + lowerBoundLabel: question.lowerBoundLabel, + upperBoundLabel: question.upperBoundLabel + ) + } + + BottomSection(label: question.buttonText ?? appearance.submitButtonText) { + onNextQuestion(rating) + } + .disabled(!canSubmit) + } + } + + private var canSubmit: Bool { + if question.isOptional { return true } + return rating != nil + } + + private var scale: PostHogSurveyRatingScale { + PostHogSurveyRatingScale(range: question.scaleLowerBound ... question.scaleUpperBound) + } + } + + @available(iOS 15.0, *) + struct SingleChoiceQuestionView: View { + @Environment(\.surveyAppearance) private var appearance + + let question: PostHogDisplayChoiceQuestion + let onNextQuestion: (String?) -> Void + + @State private var selectedChoices: Set = [] + @State private var openChoiceInput: String = "" + + var body: some View { + VStack(spacing: 16) { + QuestionHeader( + question: question.question, + description: question.questionDescription, + contentType: question.questionDescriptionContentType + ) + + MultipleChoiceOptions( + allowsMultipleSelection: false, + hasOpenChoiceQuestion: question.hasOpenChoice, + options: question.choices, + selectedOptions: $selectedChoices, + openChoiceInput: $openChoiceInput + ) + + BottomSection(label: question.buttonText ?? appearance.submitButtonText) { + let response = selectedChoices.first + let openChoiceInput = openChoiceInput.trimmingCharacters(in: .whitespaces) + onNextQuestion(response == openChoice ? openChoiceInput : response) + } + .disabled(!canSubmit) + } + } + + private var canSubmit: Bool { + if question.isOptional { return true } + return selectedChoices.count == 1 && (hasOpenChoiceSelected ? !openChoiceInput.isEmpty : true) + } + + private var hasOpenChoiceSelected: Bool { + guard let openChoice else { return false } + return selectedChoices.contains(openChoice) + } + + private var openChoice: String? { + guard question.hasOpenChoice == true else { return nil } + return question.choices.last + } + } + + @available(iOS 15.0, *) + struct MultipleChoiceQuestionView: View { + @Environment(\.surveyAppearance) private var appearance + + let question: PostHogDisplayChoiceQuestion + let onNextQuestion: ([String]?) -> Void + + @State private var selectedChoices: Set = [] + @State private var openChoiceInput: String = "" + + var body: some View { + VStack(spacing: 16) { + QuestionHeader( + question: question.question, + description: question.questionDescription, + contentType: question.questionDescriptionContentType + ) + + MultipleChoiceOptions( + allowsMultipleSelection: true, + hasOpenChoiceQuestion: question.hasOpenChoice, + options: question.choices, + selectedOptions: $selectedChoices, + openChoiceInput: $openChoiceInput + ) + + BottomSection(label: question.buttonText ?? appearance.submitButtonText) { + let resp = selectedChoices.map { $0 == openChoice ? openChoiceInput : $0 } + onNextQuestion(resp.isEmpty ? nil : resp) + } + .disabled(!canSubmit) + } + } + + private var canSubmit: Bool { + if question.isOptional { return true } + return !selectedChoices.isEmpty && (hasOpenChoiceSelected ? !openChoiceInput.isEmpty : true) + } + + private var hasOpenChoiceSelected: Bool { + guard let openChoice else { return false } + return selectedChoices.contains(openChoice) + } + + private var openChoice: String? { + guard question.hasOpenChoice == true else { return nil } + return question.choices.last + } + } +#endif diff --git a/Pods/PostHog/PostHog/Surveys/SurveyDisplayController.swift b/Pods/PostHog/PostHog/Surveys/SurveyDisplayController.swift new file mode 100644 index 0000000..fad2804 --- /dev/null +++ b/Pods/PostHog/PostHog/Surveys/SurveyDisplayController.swift @@ -0,0 +1,56 @@ +// +// SurveyDisplayController.swift +// PostHog +// +// Created by Ioannis Josephides on 07/03/2025. +// + +#if os(iOS) || Testing + import SwiftUI + + final class SurveyDisplayController: ObservableObject { + @Published var displayedSurvey: PostHogDisplaySurvey? + @Published var isSurveyCompleted: Bool = false + @Published var currentQuestionIndex: Int = 0 + + var onSurveyShown: OnPostHogSurveyShown? + var onSurveyResponse: OnPostHogSurveyResponse? + var onSurveyClosed: OnPostHogSurveyClosed? + + func showSurvey(_ survey: PostHogDisplaySurvey) { + guard displayedSurvey == nil else { + hedgeLog("[Surveys] Already displaying a survey. Skipping") + return + } + + displayedSurvey = survey + isSurveyCompleted = false + currentQuestionIndex = 0 + onSurveyShown?(survey) + } + + func onNextQuestion(index: Int, response: PostHogSurveyResponse) { + guard let displayedSurvey else { return } + guard let next = onSurveyResponse?(displayedSurvey, index, response) else { return } + + currentQuestionIndex = next.questionIndex + isSurveyCompleted = next.isSurveyCompleted + + // auto-dismiss survey when completed + if isSurveyCompleted, displayedSurvey.appearance?.displayThankYouMessage == false { + dismissSurvey() + } + } + + // User dismissed survey + func dismissSurvey() { + if let survey = displayedSurvey { + onSurveyClosed?(survey) + } + displayedSurvey = nil + isSurveyCompleted = false + currentQuestionIndex = 0 + } + } + +#endif diff --git a/Pods/PostHog/PostHog/Surveys/SurveySheet.swift b/Pods/PostHog/PostHog/Surveys/SurveySheet.swift new file mode 100644 index 0000000..24a1c7a --- /dev/null +++ b/Pods/PostHog/PostHog/Surveys/SurveySheet.swift @@ -0,0 +1,249 @@ +// +// SurveySheet.swift +// PostHog +// +// Created by Ioannis Josephides on 12/03/2025. +// + +#if os(iOS) + + import SwiftUI + + @available(iOS 15, *) + struct SurveySheet: View { + let survey: PostHogDisplaySurvey + let isSurveyCompleted: Bool + let currentQuestionIndex: Int + let onClose: () -> Void + let onNextQuestionClicked: (_ index: Int, _ response: PostHogSurveyResponse) -> Void + + @State private var sheetHeight: CGFloat = .zero + + var body: some View { + surveyContent + .animation(.linear(duration: 0.25), value: currentQuestionIndex) + .readFrame(in: .named("survey-scroll-view")) { frame in + sheetHeight = frame.height + } + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + SurveyDismissButton(action: onClose) + } + } + .surveyBottomSheet(height: sheetHeight) + .environment(\.surveyAppearance, appearance) + } + + @ViewBuilder + private var surveyContent: some View { + if isSurveyCompleted, appearance.displayThankYouMessage { + ConfirmationMessage(onClose: onClose) + } else if let currentQuestion { + switch currentQuestion { + case let currentQuestion as PostHogDisplayOpenQuestion: + OpenTextQuestionView(question: currentQuestion) { resp in + onNextQuestionClicked(currentQuestionIndex, .openEnded(resp)) + } + case let currentQuestion as PostHogDisplayLinkQuestion: + LinkQuestionView(question: currentQuestion) { resp in + onNextQuestionClicked(currentQuestionIndex, .link(resp)) + } + case let currentQuestion as PostHogDisplayRatingQuestion: + RatingQuestionView(question: currentQuestion) { resp in + onNextQuestionClicked(currentQuestionIndex, .rating(resp)) + } + case let currentQuestion as PostHogDisplayChoiceQuestion: + if currentQuestion.isMultipleChoice { + MultipleChoiceQuestionView(question: currentQuestion) { resp in + onNextQuestionClicked(currentQuestionIndex, .multipleChoice(resp)) + } + } else { + SingleChoiceQuestionView(question: currentQuestion) { resp in + onNextQuestionClicked(currentQuestionIndex, .singleChoice(resp)) + } + } + default: + EmptyView() + } + } + } + + private var currentQuestion: PostHogDisplaySurveyQuestion? { + guard currentQuestionIndex <= survey.questions.count - 1 else { + return nil + } + return survey.questions[currentQuestionIndex] + } + + private var appearance: SwiftUISurveyAppearance { + .getAppearanceWithDefaults(survey.appearance) + } + } + + @available(iOS 15, *) + private struct SurveyDismissButton: View { + let action: () -> Void + + var body: some View { + Button(action: action) { + Image(systemName: "xmark") + .font(.body) + .foregroundColor(Color(uiColor: .label)) + } + .buttonStyle(.borderless) + } + } + + extension View { + @available(iOS 15, *) + func surveyBottomSheet(height: CGFloat) -> some View { + modifier( + SurveyBottomSheetWithWithDetents(height: height) + ) + } + } + + @available(iOS 15.0, *) + private struct SurveyBottomSheetWithWithDetents: ViewModifier { + @Environment(\.surveyAppearance) private var appearance + + @State private var sheetHeight: CGFloat = .zero + @State private var safeAreaInsetsTop: CGFloat = .zero + + let height: CGFloat + + func body(content: Content) -> some View { + NavigationView { + scrolledContent(with: content) + .background(appearance.backgroundColor) + .navigationBarTitleDisplayMode(.inline) + .readSafeAreaInsets { insets in + DispatchQueue.main.async { + if safeAreaInsetsTop == .zero { + safeAreaInsetsTop = insets.top + } + } + } + } + .interactiveDismissDisabled() + .background( + SurveyPresentationDetentsRepresentable(detents: sheetDetents) + ) + } + + @ViewBuilder + private func scrolledContent(with content: Content) -> some View { + if #available(iOS 16.4, *) { + ScrollView { + content + .padding(.horizontal, 16) + } + .coordinateSpace(name: "survey-scroll-view") + .scrollBounceBehavior(.basedOnSize) + .scrollDismissesKeyboard(.interactively) + } else { + ScrollView { + content + .padding(.horizontal, 16) + } + .coordinateSpace(name: "survey-scroll-view") + } + } + + private var sheetDetents: [SurveyPresentationDetentsRepresentable.Detent] { + if adjustedSheetHeight >= UIScreen.main.bounds.height { + return [.medium, .large] + } + return [.height(adjustedSheetHeight)] + } + + var adjustedSheetHeight: CGFloat { + height + safeAreaInsetsTop + } + } + + struct SwiftUISurveyAppearance { + var fontFamily: Font + var backgroundColor: Color + var submitButtonColor: Color + var submitButtonText: String + var submitButtonTextColor: Color + var descriptionTextColor: Color + var ratingButtonColor: Color? + var ratingButtonActiveColor: Color? + var displayThankYouMessage: Bool + var thankYouMessageHeader: String + var thankYouMessageDescription: String? + var thankYouMessageDescriptionContentType: PostHogDisplaySurveyTextContentType = .text + var thankYouMessageCloseButtonText: String + var borderColor: Color + var placeholder: String? + } + + @available(iOS 15.0, *) + private struct SurveyAppearanceEnvironmentKey: EnvironmentKey { + static let defaultValue: SwiftUISurveyAppearance = .getAppearanceWithDefaults() + } + + extension EnvironmentValues { + @available(iOS 15.0, *) + var surveyAppearance: SwiftUISurveyAppearance { + get { self[SurveyAppearanceEnvironmentKey.self] } + set { self[SurveyAppearanceEnvironmentKey.self] = newValue } + } + } + + extension SwiftUISurveyAppearance { + @available(iOS 15.0, *) + static func getAppearanceWithDefaults(_ appearance: PostHogDisplaySurveyAppearance? = nil) -> SwiftUISurveyAppearance { + SwiftUISurveyAppearance( + fontFamily: Font.customFont(family: appearance?.fontFamily ?? "") ?? Font.body, + backgroundColor: colorFrom(css: appearance?.backgroundColor, defaultColor: .tertiarySystemBackground), + submitButtonColor: colorFrom(css: appearance?.submitButtonColor, defaultColor: .black), + submitButtonText: appearance?.submitButtonText ?? "Submit", + submitButtonTextColor: colorFrom(css: appearance?.submitButtonTextColor, defaultColor: .white), + descriptionTextColor: colorFrom(css: appearance?.descriptionTextColor, defaultColor: .secondaryLabel), + ratingButtonColor: colorFrom(css: appearance?.ratingButtonColor), + ratingButtonActiveColor: colorFrom(css: appearance?.ratingButtonActiveColor), + displayThankYouMessage: appearance?.displayThankYouMessage ?? true, + thankYouMessageHeader: appearance?.thankYouMessageHeader ?? "Thank you for your feedback!", + thankYouMessageDescriptionContentType: appearance?.thankYouMessageDescriptionContentType ?? .text, + thankYouMessageCloseButtonText: appearance?.thankYouMessageCloseButtonText ?? "Close", + borderColor: colorFrom(css: appearance?.borderColor, defaultColor: .systemFill) + ) + } + + @available(iOS 15.0, *) + private static func colorFrom(css hex: String?, defaultColor: UIColor) -> Color { + hex.map { Color(uiColor: UIColor(hex: $0)) } ?? Color(uiColor: defaultColor) + } + + @available(iOS 15.0, *) + private static func colorFrom(css hex: String?) -> Color? { + hex.map { Color(uiColor: UIColor(hex: $0)) } + } + } + + @available(iOS 16.0, *) + extension PresentationDetent { + /// Same as .large detent but without shrinking the source view + static let almostLarge = Self.custom(AlmostLarge.self) + } + + @available(iOS 16.0, *) + struct AlmostLarge: CustomPresentationDetent { + static func height(in context: Context) -> CGFloat? { + context.maxDetentValue - 0.5 + } + } + + extension Font { + static func customFont(family: String) -> Font? { + if let uiFont = UIFont(name: family, size: UIFont.systemFontSize) { + return Font(uiFont) + } + return nil + } + } + +#endif diff --git a/Pods/PostHog/PostHog/Surveys/SurveysRootView.swift b/Pods/PostHog/PostHog/Surveys/SurveysRootView.swift new file mode 100644 index 0000000..7fc4a58 --- /dev/null +++ b/Pods/PostHog/PostHog/Surveys/SurveysRootView.swift @@ -0,0 +1,44 @@ +// +// SurveysRootView.swift +// PostHog +// +// Created by Ioannis Josephides on 07/03/2025. +// + +#if os(iOS) + import SwiftUI + + @available(iOS 15.0, *) + struct SurveysRootView: View { + @EnvironmentObject private var displayManager: SurveyDisplayController + + var body: some View { + Color.clear + .allowsHitTesting(false) + .sheet(item: displayBinding) { survey in + SurveySheet( + survey: survey, + isSurveyCompleted: displayManager.isSurveyCompleted, + currentQuestionIndex: displayManager.currentQuestionIndex, + onClose: displayManager.dismissSurvey, + onNextQuestionClicked: displayManager.onNextQuestion + ) + .environment(\.colorScheme, .light) // enforce light theme for now + } + } + + private var displayBinding: Binding { + .init( + get: { + displayManager.displayedSurvey + }, + set: { newValue in + // in case interactive dismiss is allowed + if newValue == nil { + displayManager.dismissSurvey() + } + } + ) + } + } +#endif diff --git a/Pods/PostHog/PostHog/Surveys/SurveysWindow.swift b/Pods/PostHog/PostHog/Surveys/SurveysWindow.swift new file mode 100644 index 0000000..e810ce2 --- /dev/null +++ b/Pods/PostHog/PostHog/Surveys/SurveysWindow.swift @@ -0,0 +1,41 @@ +// +// SurveysWindow.swift +// PostHog +// +// Created by Ioannis Josephides on 06/03/2025. +// + +#if os(iOS) + import SwiftUI + import UIKit + + @available(iOS 15.0, *) + final class SurveysWindow: PassthroughWindow { + init(controller: SurveyDisplayController, scene: UIWindowScene) { + super.init(windowScene: scene) + let rootView = SurveysRootView().environmentObject(controller) + let hostingController = UIHostingController(rootView: rootView) + hostingController.view.backgroundColor = .clear + rootViewController = hostingController + } + + required init?(coder _: NSCoder) { + super.init(frame: .zero) + } + } + + class PassthroughWindow: UIWindow { + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + guard + let hitView = super.hitTest(point, with: event), + let rootView = rootViewController?.view + else { + return nil + } + + // if test comes back as our own view, ignore (this is the passthrough part) + return hitView == rootView ? nil : hitView + } + } + +#endif diff --git a/Pods/PostHog/PostHog/Surveys/Utils/EdgeBorder.swift b/Pods/PostHog/PostHog/Surveys/Utils/EdgeBorder.swift new file mode 100644 index 0000000..d03196c --- /dev/null +++ b/Pods/PostHog/PostHog/Surveys/Utils/EdgeBorder.swift @@ -0,0 +1,26 @@ +// +// EdgeBorder.swift +// PostHog +// +// Created by Ioannis Josephides on 22/03/2025. +// + +#if os(iOS) + import SwiftUI + + struct EdgeBorder: Shape { + var lineWidth: CGFloat + var edges: [Edge] + + func path(in rect: CGRect) -> Path { + edges.map { edge -> Path in + switch edge { + case .top: return Path(.init(x: rect.minX, y: rect.minY, width: rect.width, height: lineWidth)) + case .bottom: return Path(.init(x: rect.minX, y: rect.maxY - lineWidth, width: rect.width, height: lineWidth)) + case .leading: return Path(.init(x: rect.minX, y: rect.minY, width: lineWidth, height: rect.height)) + case .trailing: return Path(.init(x: rect.maxX - lineWidth, y: rect.minY, width: lineWidth, height: rect.height)) + } + }.reduce(into: Path()) { $0.addPath($1) } + } + } +#endif diff --git a/Pods/PostHog/PostHog/Surveys/Utils/EmojiRating.swift b/Pods/PostHog/PostHog/Surveys/Utils/EmojiRating.swift new file mode 100644 index 0000000..337be9b --- /dev/null +++ b/Pods/PostHog/PostHog/Surveys/Utils/EmojiRating.swift @@ -0,0 +1,115 @@ +// +// EmojiRating.swift +// PostHog +// +// Created by Ioannis Josephides on 11/03/2025. +// + +#if os(iOS) + import SwiftUI + + @available(iOS 15.0, *) + struct EmojiRating: View { + @Environment(\.surveyAppearance) private var appearance + @Binding var selectedValue: Int? + + let scale: PostHogSurveyRatingScale + let lowerBoundLabel: String + let upperBoundLabel: String + + var body: some View { + VStack { + HStack { + ForEach(scale.range, id: \.self) { value in + Button { + withAnimation(.linear(duration: 0.25)) { + selectedValue = selectedValue == value ? nil : value + } + } label: { + let isSelected = selectedValue == value + emoji(for: value) + .frame(width: 48, height: 48) + .font(.body.bold()) + .foregroundColor(foregroundColor(selected: isSelected)) + + if value != scale.range.upperBound { + Spacer() + } + } + } + } + + HStack(spacing: 0) { + Text(lowerBoundLabel) + .foregroundStyle(appearance.descriptionTextColor) + .frame(alignment: .leading) + Spacer() + Text(upperBoundLabel) + .foregroundStyle(appearance.descriptionTextColor) + .frame(alignment: .trailing) + } + } + } + + // swiftlint:disable:next cyclomatic_complexity + @ViewBuilder private func emoji(for value: Int) -> some View { + switch scale { + case .threePoint: + switch value { + case 1: DissatisfiedEmoji() + case 2: NeutralEmoji() + case 3: SatisfiedEmoji() + default: EmptyView() + } + case .fivePoint: + switch value { + case 1: VeryDissatisfiedEmoji() + case 2: DissatisfiedEmoji() + case 3: NeutralEmoji() + case 4: SatisfiedEmoji() + case 5: VerySatisfiedEmoji() + default: EmptyView() + } + default: EmptyView() + } + } + + private func foregroundColor(selected: Bool) -> Color { + selected ? Color(uiColor: .label) : Color(uiColor: .tertiaryLabel) + } + + private var ratingButtonActiveColor: Color { + appearance.ratingButtonActiveColor ?? .black + } + } + + #if DEBUG + @available(iOS 18.0, *) + private struct TestView: View { + @State var selectedValue: Int? + + var body: some View { + NavigationView { + VStack(spacing: 40) { + EmojiRating( + selectedValue: $selectedValue, + scale: .fivePoint, + lowerBoundLabel: "Unlikely", + upperBoundLabel: "Very likely" + ) + .padding(.horizontal, 20) + } + } + .navigationBarTitle(Text("Emoji Rating")) + .environment(\.surveyAppearance.ratingButtonColor, .green.opacity(0.3)) + .environment(\.surveyAppearance.ratingButtonActiveColor, .green) + .environment(\.surveyAppearance.descriptionTextColor, .orange) + } + } + + @available(iOS 18.0, *) + #Preview { + TestView() + } + #endif +#endif diff --git a/Pods/PostHog/PostHog/Surveys/Utils/MultipleChoiceOptions.swift b/Pods/PostHog/PostHog/Surveys/Utils/MultipleChoiceOptions.swift new file mode 100644 index 0000000..1eb39ce --- /dev/null +++ b/Pods/PostHog/PostHog/Surveys/Utils/MultipleChoiceOptions.swift @@ -0,0 +1,160 @@ +// +// MultipleChoiceOptions.swift +// PostHog +// +// Created by Ioannis Josephides on 11/03/2025. +// + +#if os(iOS) + import SwiftUI + + @available(iOS 15.0, *) + struct MultipleChoiceOptions: View { + let allowsMultipleSelection: Bool + let hasOpenChoiceQuestion: Bool + let options: [String] + + @Binding var selectedOptions: Set + @Binding var openChoiceInput: String + @State private var textFieldRect: CGRect = .zero + @FocusState private var isTextFieldFocused: Bool + + var body: some View { + VStack { + ForEach(options, id: \.self) { option in + let isSelected = isSelected(option) + + Button { + withAnimation(.linear(duration: 0.15)) { + setSelected(!isSelected, option: option) + } + } label: { + if isOpenChoice(option) { + VStack(alignment: .leading) { + Text("\(option):") + .multilineTextAlignment(.leading) + // Invisible text for calculating TextField placement + Text("text-field-placeholder") + .opacity(0) + .frame(maxWidth: .infinity) + .multilineTextAlignment(.leading) + .readFrame(in: .named("SurveyButton")) { frame in + textFieldRect = frame + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .modifier(SurveyOptionStyle(isChecked: isSelected)) + .coordinateSpace(name: "SurveyButton") + } else { + Text(option) + .modifier(SurveyOptionStyle(isChecked: isSelected)) + .multilineTextAlignment(.leading) + } + } + // text field needs to overlay the Button so it can receive touches first when enabled + .overlay(openChoiceField(option), alignment: .topLeading) + } + } + } + + private func isOpenChoice(_ option: String) -> Bool { + hasOpenChoiceQuestion && options.last == option + } + + private func isSelected(_ option: String) -> Bool { + selectedOptions.contains(option) + } + + private func setSelected(_ selected: Bool, option: String) { + if selected { + if allowsMultipleSelection { + selectedOptions.insert(option) + } else { + selectedOptions = [option] + } + + let isOpenChoice = self.isOpenChoice(option) + // requires a small delay since textfield is enabled/disabled based on `selectedOptions` state update + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + isTextFieldFocused = isOpenChoice + } + } else { + selectedOptions.remove(option) + } + } + + @ViewBuilder + private func openChoiceField(_ option: String) -> some View { + if isOpenChoice(option) { + TextField("", text: $openChoiceInput) + .focused($isTextFieldFocused) + .foregroundColor(isSelected(option) ? Color.black : Color.black.opacity(0.5)) + .frame(maxWidth: .infinity, alignment: .leading) + .frame(maxWidth: textFieldRect.size.width) + .disabled(!isSelected(option)) + .offset( + x: textFieldRect.origin.x, + y: textFieldRect.origin.y + ) + } + } + } + + @available(iOS 15.0, *) + private struct SurveyOptionStyle: ViewModifier { + let isChecked: Bool + + func body(content: Content) -> some View { + HStack(alignment: .center, spacing: 8) { + content + .frame(maxWidth: .infinity, alignment: .leading) + .font(isChecked ? .body.bold() : .body) + .animation(.linear(duration: 0.15), value: isChecked) + + if isChecked { + CheckIcon() + .frame(width: 16, height: 12) + } + } + .contentShape(Rectangle()) + .padding(10) + .frame(minHeight: 48) + .background( + RoundedRectangle(cornerRadius: 4) + .stroke(isChecked ? Color.black : Color.black.opacity(0.5), lineWidth: 1) + ) + .foregroundColor(isChecked ? Color.black : Color.black.opacity(0.5)) + .contentShape(Rectangle()) + } + } + + #if DEBUG + @available(iOS 18.0, *) + private struct TestView: View { + @State var selectedOptions: Set = [] + @State var openChoiceInput = "" + + var body: some View { + MultipleChoiceOptions( + allowsMultipleSelection: true, + hasOpenChoiceQuestion: true, + options: [ + "Tutorials", + "Customer case studies", + "Product announcements", + "Other", + ], + selectedOptions: $selectedOptions, + openChoiceInput: $openChoiceInput + ) + .colorScheme(.dark) + .padding() + } + } + + @available(iOS 18.0, *) + #Preview { + TestView() + } + #endif +#endif diff --git a/Pods/PostHog/PostHog/Surveys/Utils/NumberRating.swift b/Pods/PostHog/PostHog/Surveys/Utils/NumberRating.swift new file mode 100644 index 0000000..4e9aa76 --- /dev/null +++ b/Pods/PostHog/PostHog/Surveys/Utils/NumberRating.swift @@ -0,0 +1,115 @@ +// +// NumberRating.swift +// PostHog +// +// Created by Ioannis Josephides on 11/03/2025. +// + +#if os(iOS) + import SwiftUI + + @available(iOS 15.0, *) + struct NumberRating: View { + @Environment(\.surveyAppearance) private var appearance + + @Binding var selectedValue: Int? + let scale: PostHogSurveyRatingScale + let lowerBoundLabel: String + let upperBoundLabel: String + + var body: some View { + VStack { + SegmentedControl( + range: scale.range, + height: 45, + selectedValue: $selectedValue + ) { value, selected in + Text("\(value)") + .font(.body.bold()) + .foregroundColor( + foregroundTextColor(selected: selected) + ) + } separatorView: { value, _ in + if value != scale.range.upperBound { + EdgeBorder(lineWidth: 1, edges: [.trailing]) + .foregroundStyle(appearance.borderColor) + } + } indicatorView: { size in + Rectangle() + .fill(ratingButtonActiveColor) + .frame(height: size.height) + .frame(maxHeight: .infinity, alignment: .bottom) + } + .background(ratingButtonColor) + .clipShape(RoundedRectangle(cornerRadius: 6)) + .overlay( + RoundedRectangle(cornerRadius: 6) + .stroke(appearance.borderColor, lineWidth: 2) + ) + HStack { + Text(lowerBoundLabel) + .font(.callout) + .foregroundColor(appearance.descriptionTextColor) + .frame(alignment: .leading) + Spacer() + Text(upperBoundLabel) + .font(.callout) + .foregroundColor(appearance.descriptionTextColor) + .frame(alignment: .trailing) + } + } + + .padding(2) + } + + private func foregroundTextColor(selected: Bool) -> Color { + backgroundColor(selected: selected) + .getContrastingTextColor() + .opacity(foregroundTextOpacity(selected: selected)) + } + + private func foregroundTextOpacity(selected: Bool) -> Double { + selected ? 1 : 0.5 + } + + private func backgroundColor(selected: Bool) -> Color { + selected ? ratingButtonActiveColor : ratingButtonColor + } + + private var ratingButtonColor: Color { + appearance.ratingButtonColor ?? Color(uiColor: .secondarySystemBackground) + } + + private var ratingButtonActiveColor: Color { + appearance.ratingButtonActiveColor ?? .black + } + } + + #if DEBUG + @available(iOS 18.0, *) + private struct TestView: View { + @State var selectedValue: Int? + + var body: some View { + NavigationView { + VStack(spacing: 15) { + NumberRating( + selectedValue: $selectedValue, + scale: .tenPoint, + lowerBoundLabel: "Unlikely", + upperBoundLabel: "Very Likely" + ) + } + .padding() + } + .navigationBarTitle(Text("Number Rating")) + .environment(\.colorScheme, .light) + } + } + + @available(iOS 18.0, *) + #Preview { + TestView() + } + #endif +#endif diff --git a/Pods/PostHog/PostHog/Surveys/Utils/Resources.swift b/Pods/PostHog/PostHog/Surveys/Utils/Resources.swift new file mode 100644 index 0000000..8aa59b6 --- /dev/null +++ b/Pods/PostHog/PostHog/Surveys/Utils/Resources.swift @@ -0,0 +1,431 @@ +// +// Resources.swift +// PostHog +// +// Created by Ioannis Josephides on 10/03/2025. +// + +// see: https://github.com/bring-shrubbery/SVG-to-SwiftUI + +// swiftlint:disable line_length +#if os(iOS) + import SwiftUI + + struct VeryDissatisfiedEmoji: Shape { + func path(in rect: CGRect) -> Path { + var path = Path() + let width = rect.size.width + let height = rect.size.height + path.move(to: CGPoint(x: 0.5 * width, y: -0.43438 * height)) + path.addQuadCurve(to: CGPoint(x: 0.37344 * width, y: -0.39531 * height), control: CGPoint(x: 0.43021 * width, y: -0.43438 * height)) + path.addQuadCurve(to: CGPoint(x: 0.28958 * width, y: -0.29167 * height), control: CGPoint(x: 0.31667 * width, y: -0.35625 * height)) + path.addLine(to: CGPoint(x: 0.71042 * width, y: -0.29167 * height)) + path.addQuadCurve(to: CGPoint(x: 0.62708 * width, y: -0.39583 * height), control: CGPoint(x: 0.68437 * width, y: -0.35729 * height)) + path.addQuadCurve(to: CGPoint(x: 0.5 * width, y: -0.43438 * height), control: CGPoint(x: 0.56979 * width, y: -0.43438 * height)) + path.closeSubpath() + path.move(to: CGPoint(x: 0.30938 * width, y: -0.50938 * height)) + path.addLine(to: CGPoint(x: 0.36146 * width, y: -0.55625 * height)) + path.addLine(to: CGPoint(x: 0.40833 * width, y: -0.50938 * height)) + path.addLine(to: CGPoint(x: 0.44062 * width, y: -0.54688 * height)) + path.addLine(to: CGPoint(x: 0.39375 * width, y: -0.59375 * height)) + path.addLine(to: CGPoint(x: 0.44062 * width, y: -0.64063 * height)) + path.addLine(to: CGPoint(x: 0.40833 * width, y: -0.67812 * height)) + path.addLine(to: CGPoint(x: 0.36146 * width, y: -0.63125 * height)) + path.addLine(to: CGPoint(x: 0.30938 * width, y: -0.67812 * height)) + path.addLine(to: CGPoint(x: 0.27708 * width, y: -0.64063 * height)) + path.addLine(to: CGPoint(x: 0.32396 * width, y: -0.59375 * height)) + path.addLine(to: CGPoint(x: 0.27708 * width, y: -0.54688 * height)) + path.addLine(to: CGPoint(x: 0.30938 * width, y: -0.50938 * height)) + path.closeSubpath() + path.move(to: CGPoint(x: 0.59271 * width, y: -0.50938 * height)) + path.addLine(to: CGPoint(x: 0.63854 * width, y: -0.55625 * height)) + path.addLine(to: CGPoint(x: 0.69167 * width, y: -0.50938 * height)) + path.addLine(to: CGPoint(x: 0.72396 * width, y: -0.54688 * height)) + path.addLine(to: CGPoint(x: 0.67708 * width, y: -0.59375 * height)) + path.addLine(to: CGPoint(x: 0.72396 * width, y: -0.64063 * height)) + path.addLine(to: CGPoint(x: 0.69167 * width, y: -0.67812 * height)) + path.addLine(to: CGPoint(x: 0.63854 * width, y: -0.63125 * height)) + path.addLine(to: CGPoint(x: 0.59271 * width, y: -0.67812 * height)) + path.addLine(to: CGPoint(x: 0.56042 * width, y: -0.64063 * height)) + path.addLine(to: CGPoint(x: 0.60625 * width, y: -0.59375 * height)) + path.addLine(to: CGPoint(x: 0.56042 * width, y: -0.54688 * height)) + path.addLine(to: CGPoint(x: 0.59271 * width, y: -0.50938 * height)) + path.closeSubpath() + path.move(to: CGPoint(x: 0.5 * width, y: -0.08333 * height)) + path.addQuadCurve(to: CGPoint(x: 0.3375 * width, y: -0.11615 * height), control: CGPoint(x: 0.41354 * width, y: -0.08333 * height)) + path.addQuadCurve(to: CGPoint(x: 0.20521 * width, y: -0.20521 * height), control: CGPoint(x: 0.26146 * width, y: -0.14896 * height)) + path.addQuadCurve(to: CGPoint(x: 0.11615 * width, y: -0.3375 * height), control: CGPoint(x: 0.14896 * width, y: -0.26146 * height)) + path.addQuadCurve(to: CGPoint(x: 0.08333 * width, y: -0.5 * height), control: CGPoint(x: 0.08333 * width, y: -0.41354 * height)) + path.addQuadCurve(to: CGPoint(x: 0.11615 * width, y: -0.6625 * height), control: CGPoint(x: 0.08333 * width, y: -0.58646 * height)) + path.addQuadCurve(to: CGPoint(x: 0.20521 * width, y: -0.79479 * height), control: CGPoint(x: 0.14896 * width, y: -0.73854 * height)) + path.addQuadCurve(to: CGPoint(x: 0.3375 * width, y: -0.88385 * height), control: CGPoint(x: 0.26146 * width, y: -0.85104 * height)) + path.addQuadCurve(to: CGPoint(x: 0.5 * width, y: -0.91667 * height), control: CGPoint(x: 0.41354 * width, y: -0.91667 * height)) + path.addQuadCurve(to: CGPoint(x: 0.6625 * width, y: -0.88385 * height), control: CGPoint(x: 0.58646 * width, y: -0.91667 * height)) + path.addQuadCurve(to: CGPoint(x: 0.79479 * width, y: -0.79479 * height), control: CGPoint(x: 0.73854 * width, y: -0.85104 * height)) + path.addQuadCurve(to: CGPoint(x: 0.88385 * width, y: -0.6625 * height), control: CGPoint(x: 0.85104 * width, y: -0.73854 * height)) + path.addQuadCurve(to: CGPoint(x: 0.91667 * width, y: -0.5 * height), control: CGPoint(x: 0.91667 * width, y: -0.58646 * height)) + path.addQuadCurve(to: CGPoint(x: 0.88385 * width, y: -0.3375 * height), control: CGPoint(x: 0.91667 * width, y: -0.41354 * height)) + path.addQuadCurve(to: CGPoint(x: 0.79479 * width, y: -0.20521 * height), control: CGPoint(x: 0.85104 * width, y: -0.26146 * height)) + path.addQuadCurve(to: CGPoint(x: 0.6625 * width, y: -0.11615 * height), control: CGPoint(x: 0.73854 * width, y: -0.14896 * height)) + path.addQuadCurve(to: CGPoint(x: 0.5 * width, y: -0.08333 * height), control: CGPoint(x: 0.58646 * width, y: -0.08333 * height)) + path.closeSubpath() + path.move(to: CGPoint(x: 0.5 * width, y: -0.5 * height)) + path.closeSubpath() + path.move(to: CGPoint(x: 0.5 * width, y: -0.14583 * height)) + path.addQuadCurve(to: CGPoint(x: 0.75104 * width, y: -0.24896 * height), control: CGPoint(x: 0.64792 * width, y: -0.14583 * height)) + path.addQuadCurve(to: CGPoint(x: 0.85417 * width, y: -0.5 * height), control: CGPoint(x: 0.85417 * width, y: -0.35208 * height)) + path.addQuadCurve(to: CGPoint(x: 0.75104 * width, y: -0.75104 * height), control: CGPoint(x: 0.85417 * width, y: -0.64792 * height)) + path.addQuadCurve(to: CGPoint(x: 0.5 * width, y: -0.85417 * height), control: CGPoint(x: 0.64792 * width, y: -0.85417 * height)) + path.addQuadCurve(to: CGPoint(x: 0.24896 * width, y: -0.75104 * height), control: CGPoint(x: 0.35208 * width, y: -0.85417 * height)) + path.addQuadCurve(to: CGPoint(x: 0.14583 * width, y: -0.5 * height), control: CGPoint(x: 0.14583 * width, y: -0.64792 * height)) + path.addQuadCurve(to: CGPoint(x: 0.24896 * width, y: -0.24896 * height), control: CGPoint(x: 0.14583 * width, y: -0.35208 * height)) + path.addQuadCurve(to: CGPoint(x: 0.5 * width, y: -0.14583 * height), control: CGPoint(x: 0.35208 * width, y: -0.14583 * height)) + path.closeSubpath() + return path.offsetBy(dx: 0, dy: height) + } + } + + struct VerySatisfiedEmoji: Shape { + func path(in rect: CGRect) -> Path { + var path = Path() + let width = rect.size.width + let height = rect.size.height + path.move(to: CGPoint(x: 0.49948 * width, y: -0.27187 * height)) + path.addQuadCurve(to: CGPoint(x: 0.6099 * width, y: -0.29896 * height), control: CGPoint(x: 0.55937 * width, y: -0.27187 * height)) + path.addQuadCurve(to: CGPoint(x: 0.69167 * width, y: -0.37437 * height), control: CGPoint(x: 0.66042 * width, y: -0.32604 * height)) + path.addQuadCurve(to: CGPoint(x: 0.69089 * width, y: -0.39792 * height), control: CGPoint(x: 0.69792 * width, y: -0.38646 * height)) + path.addQuadCurve(to: CGPoint(x: 0.66979 * width, y: -0.40937 * height), control: CGPoint(x: 0.68385 * width, y: -0.40937 * height)) + path.addLine(to: CGPoint(x: 0.33012 * width, y: -0.40937 * height)) + path.addQuadCurve(to: CGPoint(x: 0.30885 * width, y: -0.39792 * height), control: CGPoint(x: 0.31562 * width, y: -0.40937 * height)) + path.addQuadCurve(to: CGPoint(x: 0.30833 * width, y: -0.37437 * height), control: CGPoint(x: 0.30208 * width, y: -0.38646 * height)) + path.addQuadCurve(to: CGPoint(x: 0.3901 * width, y: -0.29896 * height), control: CGPoint(x: 0.33958 * width, y: -0.32604 * height)) + path.addQuadCurve(to: CGPoint(x: 0.49948 * width, y: -0.27187 * height), control: CGPoint(x: 0.44062 * width, y: -0.27187 * height)) + path.closeSubpath() + path.move(to: CGPoint(x: 0.36146 * width, y: -0.60208 * height)) + path.addLine(to: CGPoint(x: 0.38958 * width, y: -0.57396 * height)) + path.addQuadCurve(to: CGPoint(x: 0.40814 * width, y: -0.56563 * height), control: CGPoint(x: 0.39754 * width, y: -0.56563 * height)) + path.addQuadCurve(to: CGPoint(x: 0.42708 * width, y: -0.57396 * height), control: CGPoint(x: 0.41875 * width, y: -0.56563 * height)) + path.addQuadCurve(to: CGPoint(x: 0.43542 * width, y: -0.59271 * height), control: CGPoint(x: 0.43542 * width, y: -0.58229 * height)) + path.addQuadCurve(to: CGPoint(x: 0.42708 * width, y: -0.61146 * height), control: CGPoint(x: 0.43542 * width, y: -0.60313 * height)) + path.addLine(to: CGPoint(x: 0.38333 * width, y: -0.65521 * height)) + path.addQuadCurve(to: CGPoint(x: 0.36156 * width, y: -0.66458 * height), control: CGPoint(x: 0.37417 * width, y: -0.66458 * height)) + path.addQuadCurve(to: CGPoint(x: 0.33958 * width, y: -0.65521 * height), control: CGPoint(x: 0.34896 * width, y: -0.66458 * height)) + path.addLine(to: CGPoint(x: 0.29583 * width, y: -0.61146 * height)) + path.addQuadCurve(to: CGPoint(x: 0.2875 * width, y: -0.5929 * height), control: CGPoint(x: 0.2875 * width, y: -0.6035 * height)) + path.addQuadCurve(to: CGPoint(x: 0.29583 * width, y: -0.57396 * height), control: CGPoint(x: 0.2875 * width, y: -0.58229 * height)) + path.addQuadCurve(to: CGPoint(x: 0.31458 * width, y: -0.56563 * height), control: CGPoint(x: 0.30417 * width, y: -0.56563 * height)) + path.addQuadCurve(to: CGPoint(x: 0.33333 * width, y: -0.57396 * height), control: CGPoint(x: 0.325 * width, y: -0.56563 * height)) + path.addLine(to: CGPoint(x: 0.36146 * width, y: -0.60208 * height)) + path.closeSubpath() + path.move(to: CGPoint(x: 0.63958 * width, y: -0.60208 * height)) + path.addLine(to: CGPoint(x: 0.66771 * width, y: -0.57396 * height)) + path.addQuadCurve(to: CGPoint(x: 0.68646 * width, y: -0.56563 * height), control: CGPoint(x: 0.67574 * width, y: -0.56563 * height)) + path.addQuadCurve(to: CGPoint(x: 0.70521 * width, y: -0.57396 * height), control: CGPoint(x: 0.69717 * width, y: -0.56563 * height)) + path.addQuadCurve(to: CGPoint(x: 0.71354 * width, y: -0.59252 * height), control: CGPoint(x: 0.71354 * width, y: -0.58191 * height)) + path.addQuadCurve(to: CGPoint(x: 0.70521 * width, y: -0.61146 * height), control: CGPoint(x: 0.71354 * width, y: -0.60313 * height)) + path.addLine(to: CGPoint(x: 0.66146 * width, y: -0.65521 * height)) + path.addQuadCurve(to: CGPoint(x: 0.63969 * width, y: -0.66458 * height), control: CGPoint(x: 0.65229 * width, y: -0.66458 * height)) + path.addQuadCurve(to: CGPoint(x: 0.61771 * width, y: -0.65521 * height), control: CGPoint(x: 0.62708 * width, y: -0.66458 * height)) + path.addLine(to: CGPoint(x: 0.57396 * width, y: -0.61146 * height)) + path.addQuadCurve(to: CGPoint(x: 0.56563 * width, y: -0.59271 * height), control: CGPoint(x: 0.56563 * width, y: -0.60342 * height)) + path.addQuadCurve(to: CGPoint(x: 0.57396 * width, y: -0.57396 * height), control: CGPoint(x: 0.56563 * width, y: -0.58199 * height)) + path.addQuadCurve(to: CGPoint(x: 0.59252 * width, y: -0.56563 * height), control: CGPoint(x: 0.58191 * width, y: -0.56563 * height)) + path.addQuadCurve(to: CGPoint(x: 0.61146 * width, y: -0.57396 * height), control: CGPoint(x: 0.60313 * width, y: -0.56563 * height)) + path.addLine(to: CGPoint(x: 0.63958 * width, y: -0.60208 * height)) + path.closeSubpath() + path.move(to: CGPoint(x: 0.5 * width, y: -0.08333 * height)) + path.addQuadCurve(to: CGPoint(x: 0.3375 * width, y: -0.11615 * height), control: CGPoint(x: 0.41354 * width, y: -0.08333 * height)) + path.addQuadCurve(to: CGPoint(x: 0.20521 * width, y: -0.20521 * height), control: CGPoint(x: 0.26146 * width, y: -0.14896 * height)) + path.addQuadCurve(to: CGPoint(x: 0.11615 * width, y: -0.3375 * height), control: CGPoint(x: 0.14896 * width, y: -0.26146 * height)) + path.addQuadCurve(to: CGPoint(x: 0.08333 * width, y: -0.5 * height), control: CGPoint(x: 0.08333 * width, y: -0.41354 * height)) + path.addQuadCurve(to: CGPoint(x: 0.11615 * width, y: -0.6625 * height), control: CGPoint(x: 0.08333 * width, y: -0.58646 * height)) + path.addQuadCurve(to: CGPoint(x: 0.20521 * width, y: -0.79479 * height), control: CGPoint(x: 0.14896 * width, y: -0.73854 * height)) + path.addQuadCurve(to: CGPoint(x: 0.3375 * width, y: -0.88385 * height), control: CGPoint(x: 0.26146 * width, y: -0.85104 * height)) + path.addQuadCurve(to: CGPoint(x: 0.5 * width, y: -0.91667 * height), control: CGPoint(x: 0.41354 * width, y: -0.91667 * height)) + path.addQuadCurve(to: CGPoint(x: 0.6625 * width, y: -0.88385 * height), control: CGPoint(x: 0.58646 * width, y: -0.91667 * height)) + path.addQuadCurve(to: CGPoint(x: 0.79479 * width, y: -0.79479 * height), control: CGPoint(x: 0.73854 * width, y: -0.85104 * height)) + path.addQuadCurve(to: CGPoint(x: 0.88385 * width, y: -0.6625 * height), control: CGPoint(x: 0.85104 * width, y: -0.73854 * height)) + path.addQuadCurve(to: CGPoint(x: 0.91667 * width, y: -0.5 * height), control: CGPoint(x: 0.91667 * width, y: -0.58646 * height)) + path.addQuadCurve(to: CGPoint(x: 0.88385 * width, y: -0.3375 * height), control: CGPoint(x: 0.91667 * width, y: -0.41354 * height)) + path.addQuadCurve(to: CGPoint(x: 0.79479 * width, y: -0.20521 * height), control: CGPoint(x: 0.85104 * width, y: -0.26146 * height)) + path.addQuadCurve(to: CGPoint(x: 0.6625 * width, y: -0.11615 * height), control: CGPoint(x: 0.73854 * width, y: -0.14896 * height)) + path.addQuadCurve(to: CGPoint(x: 0.5 * width, y: -0.08333 * height), control: CGPoint(x: 0.58646 * width, y: -0.08333 * height)) + path.closeSubpath() + path.move(to: CGPoint(x: 0.5 * width, y: -0.5 * height)) + path.closeSubpath() + path.move(to: CGPoint(x: 0.5 * width, y: -0.14583 * height)) + path.addQuadCurve(to: CGPoint(x: 0.75124 * width, y: -0.24876 * height), control: CGPoint(x: 0.64831 * width, y: -0.14583 * height)) + path.addQuadCurve(to: CGPoint(x: 0.85417 * width, y: -0.5 * height), control: CGPoint(x: 0.85417 * width, y: -0.35169 * height)) + path.addQuadCurve(to: CGPoint(x: 0.75124 * width, y: -0.75124 * height), control: CGPoint(x: 0.85417 * width, y: -0.64831 * height)) + path.addQuadCurve(to: CGPoint(x: 0.5 * width, y: -0.85417 * height), control: CGPoint(x: 0.64831 * width, y: -0.85417 * height)) + path.addQuadCurve(to: CGPoint(x: 0.24876 * width, y: -0.75124 * height), control: CGPoint(x: 0.35169 * width, y: -0.85417 * height)) + path.addQuadCurve(to: CGPoint(x: 0.14583 * width, y: -0.5 * height), control: CGPoint(x: 0.14583 * width, y: -0.64831 * height)) + path.addQuadCurve(to: CGPoint(x: 0.24876 * width, y: -0.24876 * height), control: CGPoint(x: 0.14583 * width, y: -0.35169 * height)) + path.addQuadCurve(to: CGPoint(x: 0.5 * width, y: -0.14583 * height), control: CGPoint(x: 0.35169 * width, y: -0.14583 * height)) + path.closeSubpath() + return path.offsetBy(dx: 0, dy: height) + } + } + + struct DissatisfiedEmoji: Shape { + func path(in rect: CGRect) -> Path { + var path = Path() + let width = rect.size.width + let height = rect.size.height + path.move(to: CGPoint(x: 0.65208 * width, y: -0.55521 * height)) + path.addQuadCurve(to: CGPoint(x: 0.69193 * width, y: -0.57161 * height), control: CGPoint(x: 0.67552 * width, y: -0.55521 * height)) + path.addQuadCurve(to: CGPoint(x: 0.70833 * width, y: -0.61146 * height), control: CGPoint(x: 0.70833 * width, y: -0.58802 * height)) + path.addQuadCurve(to: CGPoint(x: 0.69193 * width, y: -0.6513 * height), control: CGPoint(x: 0.70833 * width, y: -0.6349 * height)) + path.addQuadCurve(to: CGPoint(x: 0.65208 * width, y: -0.66771 * height), control: CGPoint(x: 0.67552 * width, y: -0.66771 * height)) + path.addQuadCurve(to: CGPoint(x: 0.61224 * width, y: -0.6513 * height), control: CGPoint(x: 0.62865 * width, y: -0.66771 * height)) + path.addQuadCurve(to: CGPoint(x: 0.59583 * width, y: -0.61146 * height), control: CGPoint(x: 0.59583 * width, y: -0.6349 * height)) + path.addQuadCurve(to: CGPoint(x: 0.61224 * width, y: -0.57161 * height), control: CGPoint(x: 0.59583 * width, y: -0.58802 * height)) + path.addQuadCurve(to: CGPoint(x: 0.65208 * width, y: -0.55521 * height), control: CGPoint(x: 0.62865 * width, y: -0.55521 * height)) + path.closeSubpath() + path.move(to: CGPoint(x: 0.34792 * width, y: -0.55521 * height)) + path.addQuadCurve(to: CGPoint(x: 0.38776 * width, y: -0.57161 * height), control: CGPoint(x: 0.37135 * width, y: -0.55521 * height)) + path.addQuadCurve(to: CGPoint(x: 0.40417 * width, y: -0.61146 * height), control: CGPoint(x: 0.40417 * width, y: -0.58802 * height)) + path.addQuadCurve(to: CGPoint(x: 0.38776 * width, y: -0.6513 * height), control: CGPoint(x: 0.40417 * width, y: -0.6349 * height)) + path.addQuadCurve(to: CGPoint(x: 0.34792 * width, y: -0.66771 * height), control: CGPoint(x: 0.37135 * width, y: -0.66771 * height)) + path.addQuadCurve(to: CGPoint(x: 0.30807 * width, y: -0.6513 * height), control: CGPoint(x: 0.32448 * width, y: -0.66771 * height)) + path.addQuadCurve(to: CGPoint(x: 0.29167 * width, y: -0.61146 * height), control: CGPoint(x: 0.29167 * width, y: -0.6349 * height)) + path.addQuadCurve(to: CGPoint(x: 0.30807 * width, y: -0.57161 * height), control: CGPoint(x: 0.29167 * width, y: -0.58802 * height)) + path.addQuadCurve(to: CGPoint(x: 0.34792 * width, y: -0.55521 * height), control: CGPoint(x: 0.32448 * width, y: -0.55521 * height)) + path.closeSubpath() + path.move(to: CGPoint(x: 0.50018 * width, y: -0.43438 * height)) + path.addQuadCurve(to: CGPoint(x: 0.37344 * width, y: -0.39531 * height), control: CGPoint(x: 0.43021 * width, y: -0.43438 * height)) + path.addQuadCurve(to: CGPoint(x: 0.28958 * width, y: -0.29167 * height), control: CGPoint(x: 0.31667 * width, y: -0.35625 * height)) + path.addLine(to: CGPoint(x: 0.34479 * width, y: -0.29167 * height)) + path.addQuadCurve(to: CGPoint(x: 0.40956 * width, y: -0.35938 * height), control: CGPoint(x: 0.36771 * width, y: -0.33542 * height)) + path.addQuadCurve(to: CGPoint(x: 0.5007 * width, y: -0.38333 * height), control: CGPoint(x: 0.4514 * width, y: -0.38333 * height)) + path.addQuadCurve(to: CGPoint(x: 0.59115 * width, y: -0.35885 * height), control: CGPoint(x: 0.55 * width, y: -0.38333 * height)) + path.addQuadCurve(to: CGPoint(x: 0.65625 * width, y: -0.29167 * height), control: CGPoint(x: 0.63229 * width, y: -0.33437 * height)) + path.addLine(to: CGPoint(x: 0.71042 * width, y: -0.29167 * height)) + path.addQuadCurve(to: CGPoint(x: 0.62726 * width, y: -0.39583 * height), control: CGPoint(x: 0.68437 * width, y: -0.35729 * height)) + path.addQuadCurve(to: CGPoint(x: 0.50018 * width, y: -0.43438 * height), control: CGPoint(x: 0.57015 * width, y: -0.43438 * height)) + path.closeSubpath() + path.move(to: CGPoint(x: 0.5 * width, y: -0.08333 * height)) + path.addQuadCurve(to: CGPoint(x: 0.3375 * width, y: -0.11615 * height), control: CGPoint(x: 0.41354 * width, y: -0.08333 * height)) + path.addQuadCurve(to: CGPoint(x: 0.20521 * width, y: -0.20521 * height), control: CGPoint(x: 0.26146 * width, y: -0.14896 * height)) + path.addQuadCurve(to: CGPoint(x: 0.11615 * width, y: -0.3375 * height), control: CGPoint(x: 0.14896 * width, y: -0.26146 * height)) + path.addQuadCurve(to: CGPoint(x: 0.08333 * width, y: -0.5 * height), control: CGPoint(x: 0.08333 * width, y: -0.41354 * height)) + path.addQuadCurve(to: CGPoint(x: 0.11615 * width, y: -0.6625 * height), control: CGPoint(x: 0.08333 * width, y: -0.58646 * height)) + path.addQuadCurve(to: CGPoint(x: 0.20521 * width, y: -0.79479 * height), control: CGPoint(x: 0.14896 * width, y: -0.73854 * height)) + path.addQuadCurve(to: CGPoint(x: 0.3375 * width, y: -0.88385 * height), control: CGPoint(x: 0.26146 * width, y: -0.85104 * height)) + path.addQuadCurve(to: CGPoint(x: 0.5 * width, y: -0.91667 * height), control: CGPoint(x: 0.41354 * width, y: -0.91667 * height)) + path.addQuadCurve(to: CGPoint(x: 0.6625 * width, y: -0.88385 * height), control: CGPoint(x: 0.58646 * width, y: -0.91667 * height)) + path.addQuadCurve(to: CGPoint(x: 0.79479 * width, y: -0.79479 * height), control: CGPoint(x: 0.73854 * width, y: -0.85104 * height)) + path.addQuadCurve(to: CGPoint(x: 0.88385 * width, y: -0.6625 * height), control: CGPoint(x: 0.85104 * width, y: -0.73854 * height)) + path.addQuadCurve(to: CGPoint(x: 0.91667 * width, y: -0.5 * height), control: CGPoint(x: 0.91667 * width, y: -0.58646 * height)) + path.addQuadCurve(to: CGPoint(x: 0.88385 * width, y: -0.3375 * height), control: CGPoint(x: 0.91667 * width, y: -0.41354 * height)) + path.addQuadCurve(to: CGPoint(x: 0.79479 * width, y: -0.20521 * height), control: CGPoint(x: 0.85104 * width, y: -0.26146 * height)) + path.addQuadCurve(to: CGPoint(x: 0.6625 * width, y: -0.11615 * height), control: CGPoint(x: 0.73854 * width, y: -0.14896 * height)) + path.addQuadCurve(to: CGPoint(x: 0.5 * width, y: -0.08333 * height), control: CGPoint(x: 0.58646 * width, y: -0.08333 * height)) + path.closeSubpath() + path.move(to: CGPoint(x: 0.5 * width, y: -0.5 * height)) + path.closeSubpath() + path.move(to: CGPoint(x: 0.5 * width, y: -0.14583 * height)) + path.addQuadCurve(to: CGPoint(x: 0.75124 * width, y: -0.24876 * height), control: CGPoint(x: 0.64831 * width, y: -0.14583 * height)) + path.addQuadCurve(to: CGPoint(x: 0.85417 * width, y: -0.5 * height), control: CGPoint(x: 0.85417 * width, y: -0.35169 * height)) + path.addQuadCurve(to: CGPoint(x: 0.75124 * width, y: -0.75124 * height), control: CGPoint(x: 0.85417 * width, y: -0.64831 * height)) + path.addQuadCurve(to: CGPoint(x: 0.5 * width, y: -0.85417 * height), control: CGPoint(x: 0.64831 * width, y: -0.85417 * height)) + path.addQuadCurve(to: CGPoint(x: 0.24876 * width, y: -0.75124 * height), control: CGPoint(x: 0.35169 * width, y: -0.85417 * height)) + path.addQuadCurve(to: CGPoint(x: 0.14583 * width, y: -0.5 * height), control: CGPoint(x: 0.14583 * width, y: -0.64831 * height)) + path.addQuadCurve(to: CGPoint(x: 0.24876 * width, y: -0.24876 * height), control: CGPoint(x: 0.14583 * width, y: -0.35169 * height)) + path.addQuadCurve(to: CGPoint(x: 0.5 * width, y: -0.14583 * height), control: CGPoint(x: 0.35169 * width, y: -0.14583 * height)) + path.closeSubpath() + return path.offsetBy(dx: 0, dy: height) + } + } + + struct NeutralEmoji: Shape { + func path(in rect: CGRect) -> Path { + var path = Path() + let width = rect.size.width + let height = rect.size.height + path.move(to: CGPoint(x: 0.65208 * width, y: -0.55521 * height)) + path.addQuadCurve(to: CGPoint(x: 0.69193 * width, y: -0.57161 * height), control: CGPoint(x: 0.67552 * width, y: -0.55521 * height)) + path.addQuadCurve(to: CGPoint(x: 0.70833 * width, y: -0.61146 * height), control: CGPoint(x: 0.70833 * width, y: -0.58802 * height)) + path.addQuadCurve(to: CGPoint(x: 0.69193 * width, y: -0.6513 * height), control: CGPoint(x: 0.70833 * width, y: -0.6349 * height)) + path.addQuadCurve(to: CGPoint(x: 0.65208 * width, y: -0.66771 * height), control: CGPoint(x: 0.67552 * width, y: -0.66771 * height)) + path.addQuadCurve(to: CGPoint(x: 0.61224 * width, y: -0.6513 * height), control: CGPoint(x: 0.62865 * width, y: -0.66771 * height)) + path.addQuadCurve(to: CGPoint(x: 0.59583 * width, y: -0.61146 * height), control: CGPoint(x: 0.59583 * width, y: -0.6349 * height)) + path.addQuadCurve(to: CGPoint(x: 0.61224 * width, y: -0.57161 * height), control: CGPoint(x: 0.59583 * width, y: -0.58802 * height)) + path.addQuadCurve(to: CGPoint(x: 0.65208 * width, y: -0.55521 * height), control: CGPoint(x: 0.62865 * width, y: -0.55521 * height)) + path.closeSubpath() + path.move(to: CGPoint(x: 0.34792 * width, y: -0.55521 * height)) + path.addQuadCurve(to: CGPoint(x: 0.38776 * width, y: -0.57161 * height), control: CGPoint(x: 0.37135 * width, y: -0.55521 * height)) + path.addQuadCurve(to: CGPoint(x: 0.40417 * width, y: -0.61146 * height), control: CGPoint(x: 0.40417 * width, y: -0.58802 * height)) + path.addQuadCurve(to: CGPoint(x: 0.38776 * width, y: -0.6513 * height), control: CGPoint(x: 0.40417 * width, y: -0.6349 * height)) + path.addQuadCurve(to: CGPoint(x: 0.34792 * width, y: -0.66771 * height), control: CGPoint(x: 0.37135 * width, y: -0.66771 * height)) + path.addQuadCurve(to: CGPoint(x: 0.30807 * width, y: -0.6513 * height), control: CGPoint(x: 0.32448 * width, y: -0.66771 * height)) + path.addQuadCurve(to: CGPoint(x: 0.29167 * width, y: -0.61146 * height), control: CGPoint(x: 0.29167 * width, y: -0.6349 * height)) + path.addQuadCurve(to: CGPoint(x: 0.30807 * width, y: -0.57161 * height), control: CGPoint(x: 0.29167 * width, y: -0.58802 * height)) + path.addQuadCurve(to: CGPoint(x: 0.34792 * width, y: -0.55521 * height), control: CGPoint(x: 0.32448 * width, y: -0.55521 * height)) + path.closeSubpath() + path.move(to: CGPoint(x: 0.36875 * width, y: -0.35313 * height)) + path.addLine(to: CGPoint(x: 0.63229 * width, y: -0.35313 * height)) + path.addLine(to: CGPoint(x: 0.63229 * width, y: -0.40417 * height)) + path.addLine(to: CGPoint(x: 0.36875 * width, y: -0.40417 * height)) + path.addLine(to: CGPoint(x: 0.36875 * width, y: -0.35313 * height)) + path.closeSubpath() + path.move(to: CGPoint(x: 0.5 * width, y: -0.08333 * height)) + path.addQuadCurve(to: CGPoint(x: 0.3375 * width, y: -0.11615 * height), control: CGPoint(x: 0.41354 * width, y: -0.08333 * height)) + path.addQuadCurve(to: CGPoint(x: 0.20521 * width, y: -0.20521 * height), control: CGPoint(x: 0.26146 * width, y: -0.14896 * height)) + path.addQuadCurve(to: CGPoint(x: 0.11615 * width, y: -0.3375 * height), control: CGPoint(x: 0.14896 * width, y: -0.26146 * height)) + path.addQuadCurve(to: CGPoint(x: 0.08333 * width, y: -0.5 * height), control: CGPoint(x: 0.08333 * width, y: -0.41354 * height)) + path.addQuadCurve(to: CGPoint(x: 0.11615 * width, y: -0.6625 * height), control: CGPoint(x: 0.08333 * width, y: -0.58646 * height)) + path.addQuadCurve(to: CGPoint(x: 0.20521 * width, y: -0.79479 * height), control: CGPoint(x: 0.14896 * width, y: -0.73854 * height)) + path.addQuadCurve(to: CGPoint(x: 0.3375 * width, y: -0.88385 * height), control: CGPoint(x: 0.26146 * width, y: -0.85104 * height)) + path.addQuadCurve(to: CGPoint(x: 0.5 * width, y: -0.91667 * height), control: CGPoint(x: 0.41354 * width, y: -0.91667 * height)) + path.addQuadCurve(to: CGPoint(x: 0.6625 * width, y: -0.88385 * height), control: CGPoint(x: 0.58646 * width, y: -0.91667 * height)) + path.addQuadCurve(to: CGPoint(x: 0.79479 * width, y: -0.79479 * height), control: CGPoint(x: 0.73854 * width, y: -0.85104 * height)) + path.addQuadCurve(to: CGPoint(x: 0.88385 * width, y: -0.6625 * height), control: CGPoint(x: 0.85104 * width, y: -0.73854 * height)) + path.addQuadCurve(to: CGPoint(x: 0.91667 * width, y: -0.5 * height), control: CGPoint(x: 0.91667 * width, y: -0.58646 * height)) + path.addQuadCurve(to: CGPoint(x: 0.88385 * width, y: -0.3375 * height), control: CGPoint(x: 0.91667 * width, y: -0.41354 * height)) + path.addQuadCurve(to: CGPoint(x: 0.79479 * width, y: -0.20521 * height), control: CGPoint(x: 0.85104 * width, y: -0.26146 * height)) + path.addQuadCurve(to: CGPoint(x: 0.6625 * width, y: -0.11615 * height), control: CGPoint(x: 0.73854 * width, y: -0.14896 * height)) + path.addQuadCurve(to: CGPoint(x: 0.5 * width, y: -0.08333 * height), control: CGPoint(x: 0.58646 * width, y: -0.08333 * height)) + path.closeSubpath() + path.move(to: CGPoint(x: 0.5 * width, y: -0.5 * height)) + path.closeSubpath() + path.move(to: CGPoint(x: 0.5 * width, y: -0.14583 * height)) + path.addQuadCurve(to: CGPoint(x: 0.75124 * width, y: -0.24876 * height), control: CGPoint(x: 0.64831 * width, y: -0.14583 * height)) + path.addQuadCurve(to: CGPoint(x: 0.85417 * width, y: -0.5 * height), control: CGPoint(x: 0.85417 * width, y: -0.35169 * height)) + path.addQuadCurve(to: CGPoint(x: 0.75124 * width, y: -0.75124 * height), control: CGPoint(x: 0.85417 * width, y: -0.64831 * height)) + path.addQuadCurve(to: CGPoint(x: 0.5 * width, y: -0.85417 * height), control: CGPoint(x: 0.64831 * width, y: -0.85417 * height)) + path.addQuadCurve(to: CGPoint(x: 0.24876 * width, y: -0.75124 * height), control: CGPoint(x: 0.35169 * width, y: -0.85417 * height)) + path.addQuadCurve(to: CGPoint(x: 0.14583 * width, y: -0.5 * height), control: CGPoint(x: 0.14583 * width, y: -0.64831 * height)) + path.addQuadCurve(to: CGPoint(x: 0.24876 * width, y: -0.24876 * height), control: CGPoint(x: 0.14583 * width, y: -0.35169 * height)) + path.addQuadCurve(to: CGPoint(x: 0.5 * width, y: -0.14583 * height), control: CGPoint(x: 0.35169 * width, y: -0.14583 * height)) + path.closeSubpath() + return path.offsetBy(dx: 0, dy: height) + } + } + + struct SatisfiedEmoji: Shape { + func path(in rect: CGRect) -> Path { + var path = Path() + let width = rect.size.width + let height = rect.size.height + path.move(to: CGPoint(x: 0.65208 * width, y: -0.55521 * height)) + path.addQuadCurve(to: CGPoint(x: 0.69193 * width, y: -0.57161 * height), control: CGPoint(x: 0.67552 * width, y: -0.55521 * height)) + path.addQuadCurve(to: CGPoint(x: 0.70833 * width, y: -0.61146 * height), control: CGPoint(x: 0.70833 * width, y: -0.58802 * height)) + path.addQuadCurve(to: CGPoint(x: 0.69193 * width, y: -0.6513 * height), control: CGPoint(x: 0.70833 * width, y: -0.6349 * height)) + path.addQuadCurve(to: CGPoint(x: 0.65208 * width, y: -0.66771 * height), control: CGPoint(x: 0.67552 * width, y: -0.66771 * height)) + path.addQuadCurve(to: CGPoint(x: 0.61224 * width, y: -0.6513 * height), control: CGPoint(x: 0.62865 * width, y: -0.66771 * height)) + path.addQuadCurve(to: CGPoint(x: 0.59583 * width, y: -0.61146 * height), control: CGPoint(x: 0.59583 * width, y: -0.6349 * height)) + path.addQuadCurve(to: CGPoint(x: 0.61224 * width, y: -0.57161 * height), control: CGPoint(x: 0.59583 * width, y: -0.58802 * height)) + path.addQuadCurve(to: CGPoint(x: 0.65208 * width, y: -0.55521 * height), control: CGPoint(x: 0.62865 * width, y: -0.55521 * height)) + path.closeSubpath() + path.move(to: CGPoint(x: 0.34792 * width, y: -0.55521 * height)) + path.addQuadCurve(to: CGPoint(x: 0.38776 * width, y: -0.57161 * height), control: CGPoint(x: 0.37135 * width, y: -0.55521 * height)) + path.addQuadCurve(to: CGPoint(x: 0.40417 * width, y: -0.61146 * height), control: CGPoint(x: 0.40417 * width, y: -0.58802 * height)) + path.addQuadCurve(to: CGPoint(x: 0.38776 * width, y: -0.6513 * height), control: CGPoint(x: 0.40417 * width, y: -0.6349 * height)) + path.addQuadCurve(to: CGPoint(x: 0.34792 * width, y: -0.66771 * height), control: CGPoint(x: 0.37135 * width, y: -0.66771 * height)) + path.addQuadCurve(to: CGPoint(x: 0.30807 * width, y: -0.6513 * height), control: CGPoint(x: 0.32448 * width, y: -0.66771 * height)) + path.addQuadCurve(to: CGPoint(x: 0.29167 * width, y: -0.61146 * height), control: CGPoint(x: 0.29167 * width, y: -0.6349 * height)) + path.addQuadCurve(to: CGPoint(x: 0.30807 * width, y: -0.57161 * height), control: CGPoint(x: 0.29167 * width, y: -0.58802 * height)) + path.addQuadCurve(to: CGPoint(x: 0.34792 * width, y: -0.55521 * height), control: CGPoint(x: 0.32448 * width, y: -0.55521 * height)) + path.closeSubpath() + path.move(to: CGPoint(x: 0.5 * width, y: -0.27187 * height)) + path.addQuadCurve(to: CGPoint(x: 0.62656 * width, y: -0.30885 * height), control: CGPoint(x: 0.56875 * width, y: -0.27187 * height)) + path.addQuadCurve(to: CGPoint(x: 0.71042 * width, y: -0.40937 * height), control: CGPoint(x: 0.68437 * width, y: -0.34583 * height)) + path.addLine(to: CGPoint(x: 0.65625 * width, y: -0.40937 * height)) + path.addQuadCurve(to: CGPoint(x: 0.59062 * width, y: -0.34531 * height), control: CGPoint(x: 0.63229 * width, y: -0.36771 * height)) + path.addQuadCurve(to: CGPoint(x: 0.50052 * width, y: -0.32292 * height), control: CGPoint(x: 0.54896 * width, y: -0.32292 * height)) + path.addQuadCurve(to: CGPoint(x: 0.4099 * width, y: -0.34479 * height), control: CGPoint(x: 0.45208 * width, y: -0.32292 * height)) + path.addQuadCurve(to: CGPoint(x: 0.34479 * width, y: -0.40937 * height), control: CGPoint(x: 0.36771 * width, y: -0.36667 * height)) + path.addLine(to: CGPoint(x: 0.28958 * width, y: -0.40937 * height)) + path.addQuadCurve(to: CGPoint(x: 0.37396 * width, y: -0.30885 * height), control: CGPoint(x: 0.31667 * width, y: -0.34583 * height)) + path.addQuadCurve(to: CGPoint(x: 0.5 * width, y: -0.27187 * height), control: CGPoint(x: 0.43125 * width, y: -0.27187 * height)) + path.closeSubpath() + path.move(to: CGPoint(x: 0.5 * width, y: -0.08333 * height)) + path.addQuadCurve(to: CGPoint(x: 0.3375 * width, y: -0.11615 * height), control: CGPoint(x: 0.41354 * width, y: -0.08333 * height)) + path.addQuadCurve(to: CGPoint(x: 0.20521 * width, y: -0.20521 * height), control: CGPoint(x: 0.26146 * width, y: -0.14896 * height)) + path.addQuadCurve(to: CGPoint(x: 0.11615 * width, y: -0.3375 * height), control: CGPoint(x: 0.14896 * width, y: -0.26146 * height)) + path.addQuadCurve(to: CGPoint(x: 0.08333 * width, y: -0.5 * height), control: CGPoint(x: 0.08333 * width, y: -0.41354 * height)) + path.addQuadCurve(to: CGPoint(x: 0.11615 * width, y: -0.6625 * height), control: CGPoint(x: 0.08333 * width, y: -0.58646 * height)) + path.addQuadCurve(to: CGPoint(x: 0.20521 * width, y: -0.79479 * height), control: CGPoint(x: 0.14896 * width, y: -0.73854 * height)) + path.addQuadCurve(to: CGPoint(x: 0.3375 * width, y: -0.88385 * height), control: CGPoint(x: 0.26146 * width, y: -0.85104 * height)) + path.addQuadCurve(to: CGPoint(x: 0.5 * width, y: -0.91667 * height), control: CGPoint(x: 0.41354 * width, y: -0.91667 * height)) + path.addQuadCurve(to: CGPoint(x: 0.6625 * width, y: -0.88385 * height), control: CGPoint(x: 0.58646 * width, y: -0.91667 * height)) + path.addQuadCurve(to: CGPoint(x: 0.79479 * width, y: -0.79479 * height), control: CGPoint(x: 0.73854 * width, y: -0.85104 * height)) + path.addQuadCurve(to: CGPoint(x: 0.88385 * width, y: -0.6625 * height), control: CGPoint(x: 0.85104 * width, y: -0.73854 * height)) + path.addQuadCurve(to: CGPoint(x: 0.91667 * width, y: -0.5 * height), control: CGPoint(x: 0.91667 * width, y: -0.58646 * height)) + path.addQuadCurve(to: CGPoint(x: 0.88385 * width, y: -0.3375 * height), control: CGPoint(x: 0.91667 * width, y: -0.41354 * height)) + path.addQuadCurve(to: CGPoint(x: 0.79479 * width, y: -0.20521 * height), control: CGPoint(x: 0.85104 * width, y: -0.26146 * height)) + path.addQuadCurve(to: CGPoint(x: 0.6625 * width, y: -0.11615 * height), control: CGPoint(x: 0.73854 * width, y: -0.14896 * height)) + path.addQuadCurve(to: CGPoint(x: 0.5 * width, y: -0.08333 * height), control: CGPoint(x: 0.58646 * width, y: -0.08333 * height)) + path.closeSubpath() + path.move(to: CGPoint(x: 0.5 * width, y: -0.5 * height)) + path.closeSubpath() + path.move(to: CGPoint(x: 0.5 * width, y: -0.14583 * height)) + path.addQuadCurve(to: CGPoint(x: 0.75124 * width, y: -0.24876 * height), control: CGPoint(x: 0.64831 * width, y: -0.14583 * height)) + path.addQuadCurve(to: CGPoint(x: 0.85417 * width, y: -0.5 * height), control: CGPoint(x: 0.85417 * width, y: -0.35169 * height)) + path.addQuadCurve(to: CGPoint(x: 0.75124 * width, y: -0.75124 * height), control: CGPoint(x: 0.85417 * width, y: -0.64831 * height)) + path.addQuadCurve(to: CGPoint(x: 0.5 * width, y: -0.85417 * height), control: CGPoint(x: 0.64831 * width, y: -0.85417 * height)) + path.addQuadCurve(to: CGPoint(x: 0.24876 * width, y: -0.75124 * height), control: CGPoint(x: 0.35169 * width, y: -0.85417 * height)) + path.addQuadCurve(to: CGPoint(x: 0.14583 * width, y: -0.5 * height), control: CGPoint(x: 0.14583 * width, y: -0.64831 * height)) + path.addQuadCurve(to: CGPoint(x: 0.24876 * width, y: -0.24876 * height), control: CGPoint(x: 0.14583 * width, y: -0.35169 * height)) + path.addQuadCurve(to: CGPoint(x: 0.5 * width, y: -0.14583 * height), control: CGPoint(x: 0.35169 * width, y: -0.14583 * height)) + path.closeSubpath() + return path.offsetBy(dx: 0, dy: height) + } + } + + struct CheckIcon: Shape { + func path(in rect: CGRect) -> Path { + var path = Path() + let width = rect.size.width + let height = rect.size.height + path.move(to: CGPoint(x: 0.33173 * width, y: 0.89102 * height)) + path.addLine(to: CGPoint(x: 0.29858 * width, y: 0.93522 * height)) + path.addCurve(to: CGPoint(x: 0.33173 * width, y: 0.95352 * height), control1: CGPoint(x: 0.30738 * width, y: 0.94694 * height), control2: CGPoint(x: 0.3193 * width, y: 0.95352 * height)) + path.addCurve(to: CGPoint(x: 0.36488 * width, y: 0.93522 * height), control1: CGPoint(x: 0.34416 * width, y: 0.95352 * height), control2: CGPoint(x: 0.35609 * width, y: 0.94694 * height)) + path.addLine(to: CGPoint(x: 0.33173 * width, y: 0.89102 * height)) + path.closeSubpath() + path.move(to: CGPoint(x: 0.97064 * width, y: 0.12753 * height)) + path.addCurve(to: CGPoint(x: 0.97064 * width, y: 0.03914 * height), control1: CGPoint(x: 0.98895 * width, y: 0.10312 * height), control2: CGPoint(x: 0.98895 * width, y: 0.06355 * height)) + path.addCurve(to: CGPoint(x: 0.90436 * width, y: 0.03914 * height), control1: CGPoint(x: 0.95234 * width, y: 0.01473 * height), control2: CGPoint(x: 0.92266 * width, y: 0.01473 * height)) + path.addLine(to: CGPoint(x: 0.97064 * width, y: 0.12753 * height)) + path.closeSubpath() + path.move(to: CGPoint(x: 0.09565 * width, y: 0.48786 * height)) + path.addCurve(to: CGPoint(x: 0.02935 * width, y: 0.48786 * height), control1: CGPoint(x: 0.07734 * width, y: 0.46345 * height), control2: CGPoint(x: 0.04766 * width, y: 0.46345 * height)) + path.addCurve(to: CGPoint(x: 0.02935 * width, y: 0.57625 * height), control1: CGPoint(x: 0.01105 * width, y: 0.51226 * height), control2: CGPoint(x: 0.01105 * width, y: 0.55184 * height)) + path.addLine(to: CGPoint(x: 0.09565 * width, y: 0.48786 * height)) + path.closeSubpath() + path.move(to: CGPoint(x: 0.36488 * width, y: 0.93522 * height)) + path.addLine(to: CGPoint(x: 0.97064 * width, y: 0.12753 * height)) + path.addLine(to: CGPoint(x: 0.90436 * width, y: 0.03914 * height)) + path.addLine(to: CGPoint(x: 0.29858 * width, y: 0.84683 * height)) + path.addLine(to: CGPoint(x: 0.36488 * width, y: 0.93522 * height)) + path.closeSubpath() + path.move(to: CGPoint(x: 0.02935 * width, y: 0.57625 * height)) + path.addLine(to: CGPoint(x: 0.29858 * width, y: 0.93522 * height)) + path.addLine(to: CGPoint(x: 0.36488 * width, y: 0.84683 * height)) + path.addLine(to: CGPoint(x: 0.09565 * width, y: 0.48786 * height)) + path.addLine(to: CGPoint(x: 0.02935 * width, y: 0.57625 * height)) + path.closeSubpath() + return path + } + } + + #Preview { + VStack { + HStack { + VeryDissatisfiedEmoji() + .frame(width: 48, height: 48) + .foregroundColor(.blue) + DissatisfiedEmoji().frame(width: 48, height: 48) + NeutralEmoji().frame(width: 48, height: 48) + SatisfiedEmoji().frame(width: 48, height: 48) + VerySatisfiedEmoji().frame(width: 48, height: 48) + } + HStack { + CheckIcon().frame(width: 16, height: 12) + } + } + } +#endif +// swiftlint:enable line_length diff --git a/Pods/PostHog/PostHog/Surveys/Utils/SegmentedControl.swift b/Pods/PostHog/PostHog/Surveys/Utils/SegmentedControl.swift new file mode 100644 index 0000000..2e152aa --- /dev/null +++ b/Pods/PostHog/PostHog/Surveys/Utils/SegmentedControl.swift @@ -0,0 +1,95 @@ +// +// SegmentedControl.swift +// PostHog +// +// Created by Ioannis Josephides on 11/03/2025. +// + +#if os(iOS) + import SwiftUI + + struct SegmentedControl: View { + var range: ClosedRange + var height: CGFloat = 45 + + @Binding var selectedValue: Int? + @ViewBuilder var segmentView: (_ value: Int, _ selected: Bool) -> Segment + @ViewBuilder var separatorView: (_ value: Int, _ selected: Bool) -> Separator + @ViewBuilder var indicatorView: (CGSize) -> Indicator + + @State private var minX: CGFloat = .zero + + var body: some View { + GeometryReader { + let size = $0.size + let containerWidthForEachTab = size.width / CGFloat(range.count) + + HStack(spacing: 0) { + ForEach(range, id: \.self) { value in + let isSelected = selectedValue == value + Button { + if selectedValue == value { + withAnimation(.snappy(duration: 0.25, extraBounce: 0)) { + selectedValue = nil + } + + } else { + let index = value - range.lowerBound + if selectedValue == nil { + minX = containerWidthForEachTab * CGFloat(index) + withAnimation(.snappy(duration: 0.25, extraBounce: 0)) { + selectedValue = value + } + } else { + selectedValue = selectedValue == value ? nil : value + withAnimation(.snappy(duration: 0.25, extraBounce: 0)) { + minX = containerWidthForEachTab * CGFloat(index) + } + } + } + + } label: { + segmentView(value, isSelected) + .contentShape(.rect) + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + .buttonStyle(.borderless) + .animation(.snappy, value: selectedValue) + .background( + Group { + if value == range.lowerBound, selectedValue != nil { + GeometryReader { + let size = $0.size + indicatorView(size) + .frame(width: size.width, height: size.height, alignment: .leading) + .offset(x: minX) + } + } + }, + alignment: .leading + ) + .overlay( + separatorView(value, isSelected) + ) + } + } + .preference(key: SizeKey.self, value: size) + .onPreferenceChange(SizeKey.self) { _ in + if let selectedValue { + let index = selectedValue - range.lowerBound + minX = containerWidthForEachTab * CGFloat(index) + } + } + } + .frame(height: height) + } + } + + private struct SizeKey: PreferenceKey { + static var defaultValue: CGSize = .zero + static func reduce(value: inout CGSize, nextValue: () -> CGSize) { + value = nextValue() + } + } + +#endif diff --git a/Pods/PostHog/PostHog/Surveys/Utils/Survey+Util.swift b/Pods/PostHog/PostHog/Surveys/Utils/Survey+Util.swift new file mode 100644 index 0000000..f2f4fdc --- /dev/null +++ b/Pods/PostHog/PostHog/Surveys/Utils/Survey+Util.swift @@ -0,0 +1,306 @@ +// https://gist.github.com/nbasham/3b2de0566d5f716894fc +// +// Survey+Util.swift +// previously Color+HexAndCSSColorNames.swift +// +// Created by Norman Basham on 12/8/15. +// Copyright ©2018 Black Labs. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if os(iOS) + import SwiftUI + import UIKit + + // swiftlint:disable identifier_name + public extension UIColor { + /** + Creates an immuatble UIColor instance specified by a hex string, CSS color name, or nil. + + - parameter hexString: A case insensitive String? representing a hex or CSS value e.g. + + - **"abc"** + - **"abc7"** + - **"#abc7"** + - **"00FFFF"** + - **"#00FFFF"** + - **"00FFFF77"** + - **"Orange", "Azure", "Tomato"** Modern browsers support 140 color names () + - **"Clear"** [UIColor clearColor] + - **"Transparent"** [UIColor clearColor] + - **nil** [UIColor clearColor] + - **empty string** [UIColor clearColor] + */ + convenience init(hex: String?) { + let normalizedHexString: String = UIColor.normalize(hex) + var c: CUnsignedLongLong = 0 + Scanner(string: normalizedHexString).scanHexInt64(&c) + self.init( + red: UIColorMasks.redValue(CUnsignedInt(c)), + green: UIColorMasks.greenValue(CUnsignedInt(c)), + blue: UIColorMasks.blueValue(CUnsignedInt(c)), + alpha: UIColorMasks.alphaValue(CUnsignedInt(c)) + ) + } + + /** + Returns a hex equivalent of this UIColor. + + - Parameter includeAlpha: Optional parameter to include the alpha hex. + + color.hexDescription() -> "ff0000" + + color.hexDescription(true) -> "ff0000aa" + + - Returns: A new string with `String` with the color's hexidecimal value. + */ + func hexDescription(_ includeAlpha: Bool = false) -> String { + guard cgColor.numberOfComponents == 4 else { + return "Color not RGB." + } + let a = cgColor.components!.map { Int($0 * CGFloat(255)) } + let color = String(format: "%02x%02x%02x", a[0], a[1], a[2]) + if includeAlpha { + let alpha = String(format: "%02x", a[3]) + return "\(color)\(alpha)" + } + return color + } + + fileprivate enum UIColorMasks: CUnsignedInt { + case redMask = 0xFF000000 + case greenMask = 0x00FF0000 + case blueMask = 0x0000FF00 + case alphaMask = 0x000000FF + + static func redValue(_ value: CUnsignedInt) -> CGFloat { + CGFloat((value & redMask.rawValue) >> 24) / 255.0 + } + + static func greenValue(_ value: CUnsignedInt) -> CGFloat { + CGFloat((value & greenMask.rawValue) >> 16) / 255.0 + } + + static func blueValue(_ value: CUnsignedInt) -> CGFloat { + CGFloat((value & blueMask.rawValue) >> 8) / 255.0 + } + + static func alphaValue(_ value: CUnsignedInt) -> CGFloat { + CGFloat(value & alphaMask.rawValue) / 255.0 + } + } + + fileprivate static func normalize(_ hex: String?) -> String { + guard var hexString = hex else { + return "00000000" + } + if let cssColor = cssToHexDictionary[hexString.uppercased()] { + return cssColor.count == 8 ? cssColor : cssColor + "ff" + } + if hexString.hasPrefix("#") { + hexString = String(hexString.dropFirst()) + } + if hexString.count == 3 || hexString.count == 4 { + hexString = hexString.map { "\($0)\($0)" }.joined() + } + let hasAlpha = hexString.count > 7 + if !hasAlpha { + hexString += "ff" + } + return hexString + } + + /** + All modern browsers support the following 140 color names (see http://www.w3schools.com/cssref/css_colornames.asp) + */ + fileprivate static func hexFromCssName(_ cssName: String) -> String { + let key = cssName.uppercased() + if let hex = cssToHexDictionary[key] { + return hex + } + return cssName + } + + fileprivate static let cssToHexDictionary: [String: String] = [ + "CLEAR": "00000000", + "TRANSPARENT": "00000000", + "": "00000000", + "ALICEBLUE": "F0F8FF", + "ANTIQUEWHITE": "FAEBD7", + "AQUA": "00FFFF", + "AQUAMARINE": "7FFFD4", + "AZURE": "F0FFFF", + "BEIGE": "F5F5DC", + "BISQUE": "FFE4C4", + "BLACK": "000000", + "BLANCHEDALMOND": "FFEBCD", + "BLUE": "0000FF", + "BLUEVIOLET": "8A2BE2", + "BROWN": "A52A2A", + "BURLYWOOD": "DEB887", + "CADETBLUE": "5F9EA0", + "CHARTREUSE": "7FFF00", + "CHOCOLATE": "D2691E", + "CORAL": "FF7F50", + "CORNFLOWERBLUE": "6495ED", + "CORNSILK": "FFF8DC", + "CRIMSON": "DC143C", + "CYAN": "00FFFF", + "DARKBLUE": "00008B", + "DARKCYAN": "008B8B", + "DARKGOLDENROD": "B8860B", + "DARKGRAY": "A9A9A9", + "DARKGREY": "A9A9A9", + "DARKGREEN": "006400", + "DARKKHAKI": "BDB76B", + "DARKMAGENTA": "8B008B", + "DARKOLIVEGREEN": "556B2F", + "DARKORANGE": "FF8C00", + "DARKORCHID": "9932CC", + "DARKRED": "8B0000", + "DARKSALMON": "E9967A", + "DARKSEAGREEN": "8FBC8F", + "DARKSLATEBLUE": "483D8B", + "DARKSLATEGRAY": "2F4F4F", + "DARKSLATEGREY": "2F4F4F", + "DARKTURQUOISE": "00CED1", + "DARKVIOLET": "9400D3", + "DEEPPINK": "FF1493", + "DEEPSKYBLUE": "00BFFF", + "DIMGRAY": "696969", + "DIMGREY": "696969", + "DODGERBLUE": "1E90FF", + "FIREBRICK": "B22222", + "FLORALWHITE": "FFFAF0", + "FORESTGREEN": "228B22", + "FUCHSIA": "FF00FF", + "GAINSBORO": "DCDCDC", + "GHOSTWHITE": "F8F8FF", + "GOLD": "FFD700", + "GOLDENROD": "DAA520", + "GRAY": "808080", + "GREY": "808080", + "GREEN": "008000", + "GREENYELLOW": "ADFF2F", + "HONEYDEW": "F0FFF0", + "HOTPINK": "FF69B4", + "INDIANRED": "CD5C5C", + "INDIGO": "4B0082", + "IVORY": "FFFFF0", + "KHAKI": "F0E68C", + "LAVENDER": "E6E6FA", + "LAVENDERBLUSH": "FFF0F5", + "LAWNGREEN": "7CFC00", + "LEMONCHIFFON": "FFFACD", + "LIGHTBLUE": "ADD8E6", + "LIGHTCORAL": "F08080", + "LIGHTCYAN": "E0FFFF", + "LIGHTGOLDENRODYELLOW": "FAFAD2", + "LIGHTGRAY": "D3D3D3", + "LIGHTGREY": "D3D3D3", + "LIGHTGREEN": "90EE90", + "LIGHTPINK": "FFB6C1", + "LIGHTSALMON": "FFA07A", + "LIGHTSEAGREEN": "20B2AA", + "LIGHTSKYBLUE": "87CEFA", + "LIGHTSLATEGRAY": "778899", + "LIGHTSLATEGREY": "778899", + "LIGHTSTEELBLUE": "B0C4DE", + "LIGHTYELLOW": "FFFFE0", + "LIME": "00FF00", + "LIMEGREEN": "32CD32", + "LINEN": "FAF0E6", + "MAGENTA": "FF00FF", + "MAROON": "800000", + "MEDIUMAQUAMARINE": "66CDAA", + "MEDIUMBLUE": "0000CD", + "MEDIUMORCHID": "BA55D3", + "MEDIUMPURPLE": "9370DB", + "MEDIUMSEAGREEN": "3CB371", + "MEDIUMSLATEBLUE": "7B68EE", + "MEDIUMSPRINGGREEN": "00FA9A", + "MEDIUMTURQUOISE": "48D1CC", + "MEDIUMVIOLETRED": "C71585", + "MIDNIGHTBLUE": "191970", + "MINTCREAM": "F5FFFA", + "MISTYROSE": "FFE4E1", + "MOCCASIN": "FFE4B5", + "NAVAJOWHITE": "FFDEAD", + "NAVY": "000080", + "OLDLACE": "FDF5E6", + "OLIVE": "808000", + "OLIVEDRAB": "6B8E23", + "ORANGE": "FFA500", + "ORANGERED": "FF4500", + "ORCHID": "DA70D6", + "PALEGOLDENROD": "EEE8AA", + "PALEGREEN": "98FB98", + "PALETURQUOISE": "AFEEEE", + "PALEVIOLETRED": "DB7093", + "PAPAYAWHIP": "FFEFD5", + "PEACHPUFF": "FFDAB9", + "PERU": "CD853F", + "PINK": "FFC0CB", + "PLUM": "DDA0DD", + "POWDERBLUE": "B0E0E6", + "PURPLE": "800080", + "RED": "FF0000", + "ROSYBROWN": "BC8F8F", + "ROYALBLUE": "4169E1", + "SADDLEBROWN": "8B4513", + "SALMON": "FA8072", + "SANDYBROWN": "F4A460", + "SEAGREEN": "2E8B57", + "SEASHELL": "FFF5EE", + "SIENNA": "A0522D", + "SILVER": "C0C0C0", + "SKYBLUE": "87CEEB", + "SLATEBLUE": "6A5ACD", + "SLATEGRAY": "708090", + "SLATEGREY": "708090", + "SNOW": "FFFAFA", + "SPRINGGREEN": "00FF7F", + "STEELBLUE": "4682B4", + "TAN": "D2B48C", + "TEAL": "008080", + "THISTLE": "D8BFD8", + "TOMATO": "FF6347", + "TURQUOISE": "40E0D0", + "VIOLET": "EE82EE", + "WHEAT": "F5DEB3", + "WHITE": "FFFFFF", + "WHITESMOKE": "F5F5F5", + "YELLOW": "FFFF00", + "YELLOWGREEN": "9ACD32", + ] + } + + extension Color { + @available(iOS 15.0, *) + func getContrastingTextColor() -> Color { + var r, g, b, a: CGFloat + (r, g, b, a) = (0, 0, 0, 0) + UIColor(self).getRed(&r, green: &g, blue: &b, alpha: &a) + let luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b + return luminance < 0.6 ? .white : .black + } + } + + // swiftlint:enable identifier_name +#endif diff --git a/Pods/PostHog/PostHog/Surveys/Utils/SurveyButton.swift b/Pods/PostHog/PostHog/Surveys/Utils/SurveyButton.swift new file mode 100644 index 0000000..7ac992f --- /dev/null +++ b/Pods/PostHog/PostHog/Surveys/Utils/SurveyButton.swift @@ -0,0 +1,37 @@ +// +// SurveyButton.swift +// PostHog +// +// Created by Ioannis Josephides on 11/03/2025. +// + +#if os(iOS) + + import SwiftUI + + @available(iOS 15.0, *) + struct SurveyButtonStyle: ButtonStyle { + @Environment(\.surveyAppearance) private var appearance + @Environment(\.isEnabled) private var isEnabled + + func makeBody(configuration: Configuration) -> some View { + configuration.label + .font(.body.bold()) + .frame(maxWidth: .infinity) + .shadow(color: Color.black.opacity(0.12), radius: 0, x: 0, y: -1) // Text shadow + .padding(12) + .foregroundStyle(appearance.submitButtonTextColor) + .background( + RoundedRectangle(cornerRadius: 6) + .fill(appearance.submitButtonColor) + .shadow(color: Color.black.opacity(0.15), radius: 2, x: 0, y: 2) // Box shadow + ) + .contentShape(Rectangle()) + .opacity(configuration.isPressed ? 0.80 : opacity) + } + + private var opacity: Double { + isEnabled ? 1.0 : 0.5 + } + } +#endif diff --git a/Pods/PostHog/PostHog/Surveys/Utils/SurveyPresentationDetentsRepresentable.swift b/Pods/PostHog/PostHog/Surveys/Utils/SurveyPresentationDetentsRepresentable.swift new file mode 100644 index 0000000..d9b311a --- /dev/null +++ b/Pods/PostHog/PostHog/Surveys/Utils/SurveyPresentationDetentsRepresentable.swift @@ -0,0 +1,122 @@ +// +// SurveyPresentationDetentsRepresentable.swift +// PostHog +// +// Created by Ioannis Josephides on 22/03/2025. +// + +#if os(iOS) + + import SwiftUI + + @available(iOS 15.0, *) + struct SurveyPresentationDetentsRepresentable: UIViewControllerRepresentable { + enum Detent: Hashable, Identifiable, Comparable { + case medium + case large + case height(_ value: CGFloat) + + var toPresentationDetents: UISheetPresentationController.Detent { + switch self { + case .medium: .medium() + case .large: + if #available(iOS 16.0, *) { + // almost large detent, so that background view is not scaled + .custom(identifier: id, resolver: { context in context.maximumDetentValue - 0.5 }) + } else { + .large() + } + case let .height(value): + if #available(iOS 16.0, *) { + if value > 0 { + .custom(identifier: id, resolver: { _ in value }) + } else { + .medium() + } + } else { + .medium() + } + } + } + + var id: UISheetPresentationController.Detent.Identifier { + switch self { + case .medium: .init("com.apple.UIKit.medium") + case .large: + if #available(iOS 16.0, *) { + .init("posthog.detent.almostLarge") + } else { + .init("com.apple.UIKit.large") + } + case let .height(value): + if #available(iOS 16.0, *) { + if value > 0 { + .init("posthog.detent.customHeight.\(value)") + } else { + .init("com.apple.UIKit.medium") + } + } else { + .init("com.apple.UIKit.medium") + } + } + } + } + + let detents: [Detent] + + func makeUIViewController(context _: Context) -> Controller { + Controller(detents: detents) + } + + func updateUIViewController(_ controller: Controller, context _: Context) { + controller.detents = detents + DispatchQueue.main.async(execute: controller.update) + } + + final class Controller: UIViewController, UISheetPresentationControllerDelegate { + var detents: [Detent] + + init(detents: [Detent]) { + self.detents = detents + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + detents = [] + super.init(nibName: nil, bundle: nil) + } + + func update() { + let newDetents = detents.map(\.toPresentationDetents) + + if let controller = sheetPresentationController { + controller.detents = newDetents + + // present as bottom sheet on compact-size (e.g landscape) + if #available(iOS 16.0, *) { + controller.prefersEdgeAttachedInCompactHeight = true + controller.widthFollowsPreferredContentSizeWhenEdgeAttached = true + } else { + // Getting some weird crash on iOS 15.5 when setting this to true. Disable for now + // This means that on iOS 15.0 landscape mode presentation will be full screen + controller.prefersEdgeAttachedInCompactHeight = false + controller.widthFollowsPreferredContentSizeWhenEdgeAttached = false + } + // scrolling with expand the bottom sheet if needed + controller.prefersScrollingExpandsWhenScrolledToEdge = true + // show drag indicator if bottom sheet is expandable + controller.prefersGrabberVisible = detents.count > 1 + // always dim background + controller.presentingViewController.view?.tintAdjustmentMode = .dimmed + } + } + + override func viewWillTransition(to size: CGSize, with coordinator: any UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: coordinator) + DispatchQueue.main.async(execute: update) + } + } + } + +#endif diff --git a/Pods/PostHog/PostHog/Surveys/Utils/SwiftUI+Util.swift b/Pods/PostHog/PostHog/Surveys/Utils/SwiftUI+Util.swift new file mode 100644 index 0000000..5773836 --- /dev/null +++ b/Pods/PostHog/PostHog/Surveys/Utils/SwiftUI+Util.swift @@ -0,0 +1,105 @@ +// +// SwiftUI+Util.swift +// PostHog +// +// Created by Ioannis Josephides on 10/03/2025. +// + +#if os(iOS) + import SwiftUI + + extension View { + /// Reads frame changes of current view in a coordinate space (default global) + @available(iOS 14.0, *) + func readFrame( + in coordinateSpace: CoordinateSpace = .global, + onFrame: @escaping (CGRect) -> Void + ) -> some View { + modifier( + ReadFrameModifier( + coordinateSpace: coordinateSpace, + onFrame: onFrame + ) + ) + } + + /// Reads current view's safe area insets + @available(iOS 14.0, *) + func readSafeAreaInsets( + onSafeAreaInsets: @escaping (EdgeInsets) -> Void + ) -> some View { + modifier( + ReadSafeAreaInsetsModifier( + onSafeAreaInsets: onSafeAreaInsets + ) + ) + } + + /// Type-erases a View + var erasedToAnyView: AnyView { + AnyView(self) + } + } + + @available(iOS 14.0, *) + private struct ReadFrameModifier: ViewModifier { + /// Helper for notifying parents for child view frame changes + struct FramePreferenceKey: PreferenceKey { + static var defaultValue: CGRect = .zero + static func reduce(value _: inout CGRect, nextValue _: () -> CGRect) { + // nothing + } + } + + let coordinateSpace: CoordinateSpace + let onFrame: (CGRect) -> Void + + func body(content: Content) -> some View { + content + .background( + GeometryReader { proxy in + Color.clear + .preference( + key: FramePreferenceKey.self, + value: proxy.frame(in: coordinateSpace) + ) + } + ) + .onPreferenceChange(FramePreferenceKey.self, perform: onFrame) + } + } + + @available(iOS 14.0, *) + private struct ReadSafeAreaInsetsModifier: ViewModifier { + /// Helper for notifying parents for child view's safe area insets + struct SafeAreaInsetsPreferenceKey: PreferenceKey { + static var defaultValue: EdgeInsets = .init() + static func reduce(value _: inout EdgeInsets, nextValue _: () -> EdgeInsets) { + // nothing + } + } + + let onSafeAreaInsets: (EdgeInsets) -> Void + + @State private var safeAreaInsets: EdgeInsets = .init() + + func body(content: Content) -> some View { + ZStack { + content + .background( + GeometryReader { proxy in + Color.clear + .onChange(of: proxy.safeAreaInsets) { size in + safeAreaInsets = size + } + .preference( + key: SafeAreaInsetsPreferenceKey.self, + value: safeAreaInsets + ) + } + ) + } + .onPreferenceChange(SafeAreaInsetsPreferenceKey.self, perform: onSafeAreaInsets) + } + } +#endif diff --git a/Pods/PostHog/PostHog/SwiftUI/PostHogMaskViewModifier.swift b/Pods/PostHog/PostHog/SwiftUI/PostHogMaskViewModifier.swift new file mode 100644 index 0000000..227674e --- /dev/null +++ b/Pods/PostHog/PostHog/SwiftUI/PostHogMaskViewModifier.swift @@ -0,0 +1,53 @@ +// +// PostHogMaskViewModifier.swift +// PostHog +// +// Created by Yiannis Josephides on 09/10/2024. +// + +#if os(iOS) && canImport(SwiftUI) + + import SwiftUI + + public extension View { + /** + Marks a SwiftUI View to be masked in PostHog session replay recordings. + + Because of the nature of how we intercept SwiftUI view hierarchy (and how it maps to UIKit), + we can't always be 100% confident that a view should be masked and may accidentally mark a + sensitive view as non-sensitive instead. + + Use this modifier to explicitly mask sensitive views in session replay recordings. + + For example: + ```swift + // This view will be masked in recordings + SensitiveDataView() + .postHogMask() + + // Conditionally mask based on a flag + SensitiveDataView() + .postHogMask(shouldMask) + ``` + + - Parameter isEnabled: Whether masking should be enabled. Defaults to true. + - Returns: A modified view that will be masked in session replay recordings when enabled + */ + func postHogMask(_ isEnabled: Bool = true) -> some View { + modifier( + PostHogTagViewModifier { uiViews in + uiViews.forEach { $0.postHogNoCapture = isEnabled } + } onRemove: { uiViews in + uiViews.forEach { $0.postHogNoCapture = false } + } + ) + } + } + + extension UIView { + var postHogNoCapture: Bool { + get { objc_getAssociatedObject(self, &AssociatedKeys.phNoCapture) as? Bool ?? false } + set { objc_setAssociatedObject(self, &AssociatedKeys.phNoCapture, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } + } + } +#endif diff --git a/Pods/PostHog/PostHog/SwiftUI/PostHogNoMaskViewModifier.swift b/Pods/PostHog/PostHog/SwiftUI/PostHogNoMaskViewModifier.swift new file mode 100644 index 0000000..e4c1c45 --- /dev/null +++ b/Pods/PostHog/PostHog/SwiftUI/PostHogNoMaskViewModifier.swift @@ -0,0 +1,54 @@ +// +// PostHogNoMaskViewModifier.swift +// PostHog +// +// Created by Yiannis Josephides on 09/10/2024. +// + +#if os(iOS) && canImport(SwiftUI) + + import SwiftUI + + public extension View { + /** + Marks a SwiftUI View to be excluded from masking in PostHog session replay recordings. + + There are cases where PostHog SDK will unintentionally mask some SwiftUI views. + + Because of the nature of how we intercept SwiftUI view hierarchy (and how it maps to UIKit), + we can't always be 100% confident that a view should be masked. For that reason, we prefer to + take a proactive and prefer to mask views if we're not sure. + + Use this modifier to prevent views from being masked in session replay recordings. + + For example: + ```swift + // This view may be accidentally masked by PostHog SDK + SomeSafeView() + + // This custom view (and all its subviews) will not be masked in recordings + SomeSafeView() + .postHogNoMask() + ``` + + - Returns: A modified view that will not be masked in session replay recordings + */ + func postHogNoMask() -> some View { + modifier( + PostHogTagViewModifier { uiViews in + uiViews.forEach { $0.postHogNoMask = true } + } onRemove: { uiViews in + uiViews.forEach { $0.postHogNoMask = false } + } + ) + } + } + + extension UIView { + var postHogNoMask: Bool { + get { objc_getAssociatedObject(self, &AssociatedKeys.phNoMask) as? Bool ?? false } + set { objc_setAssociatedObject(self, &AssociatedKeys.phNoMask, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } + } + } + +#endif diff --git a/Pods/PostHog/PostHog/SwiftUI/PostHogSwiftUIViewModifiers.swift b/Pods/PostHog/PostHog/SwiftUI/PostHogSwiftUIViewModifiers.swift new file mode 100644 index 0000000..b809426 --- /dev/null +++ b/Pods/PostHog/PostHog/SwiftUI/PostHogSwiftUIViewModifiers.swift @@ -0,0 +1,68 @@ +// +// PostHogSwiftUIViewModifiers.swift +// PostHog +// +// Created by Manoel Aranda Neto on 05.09.24. +// + +#if canImport(SwiftUI) + import Foundation + import SwiftUI + + public extension View { + /** + Marks a SwiftUI View to be tracked as a $screen event in PostHog when onAppear is called. + + - Parameters: + - screenName: The name of the screen. Defaults to the type of the view. + - properties: Additional properties to be tracked with the screen. + - postHog: The instance to be used when sending the $screen event + - Returns: A modified view that will be tracked as a screen in PostHog. + */ + func postHogScreenView(_ screenName: String? = nil, + _ properties: [String: Any]? = nil, + postHog: PostHogSDK? = nil) -> some View + { + let viewEventName = screenName ?? "\(type(of: self))" + return modifier(PostHogSwiftUIViewModifier(viewEventName: viewEventName, + screenEvent: true, + properties: properties, + postHog: postHog)) + } + + func postHogViewSeen(_ event: String, + _ properties: [String: Any]? = nil, + postHog: PostHogSDK? = nil) -> some View + { + modifier(PostHogSwiftUIViewModifier(viewEventName: event, + screenEvent: false, + properties: properties, + postHog: postHog)) + } + } + + private struct PostHogSwiftUIViewModifier: ViewModifier { + let viewEventName: String + + let screenEvent: Bool + + let properties: [String: Any]? + + let postHog: PostHogSDK? + + func body(content: Content) -> some View { + content.onAppear { + if screenEvent { + instance.screen(viewEventName, properties: properties) + } else { + instance.capture(viewEventName, properties: properties) + } + } + } + + private var instance: PostHogSDK { + postHog ?? PostHogSDK.shared + } + } + +#endif diff --git a/Pods/PostHog/PostHog/SwiftUI/PostHogTagViewModifier.swift b/Pods/PostHog/PostHog/SwiftUI/PostHogTagViewModifier.swift new file mode 100644 index 0000000..68cfefb --- /dev/null +++ b/Pods/PostHog/PostHog/SwiftUI/PostHogTagViewModifier.swift @@ -0,0 +1,371 @@ +// +// PostHogTagViewModifier.swift +// PostHog +// +// Created by Yiannis Josephides on 19/12/2024. +// + +// Inspired from: https://github.com/siteline/swiftui-introspect + +#if os(iOS) && canImport(SwiftUI) + import SwiftUI + + typealias PostHogTagViewHandler = ([UIView]) -> Void + + /** + This is a helper view modifier for retrieving a list of underlying UIKit views for the current SwiftUI view. + + This implementation injects two hidden views into the SwiftUI view hierarchy, with the purpose of using them to retrieve the generated UIKit views for this SwiftUI view. + + The two injected views basically sandwich the current SwiftUI view: + - The first view is an anchor view, which defines how far **down** we need to traverse the view hierarchy (added as a background view). + - The second view is a tagger view, which defines how far **up** we traverse the view hierarchy (added as an overlay view). + - Any view in between the two should be the generated UIKit views that correspond to the current View + + ``` + View Hierarchy Tree: + + UIHostingController + │ + ▼ + _UIHostingView (Common ancestor) + │ + ┌──────┴──────┐ + ▼ ▼ + UnrelatedView | + │ + PostHogTagView + (overlay) + │ + ▼ + _UIGeneratedView (e.g generated views in an HStack) + │ + ▼ + _UIGeneratedView (e.g generated views in an HStack) + │ + ▼ + PostHogTagAnchorView + (background) + + The general approach is: + + 1. PostHogTagAnchorView injected as background (bottom boundary) + 2. PostHogTagView injected as overlay (top boundary) + 3. System renders SwiftUI view hierarchy in UIKit + 4. Find the common ancestor of the PostHogTagAnchorView and PostHogTagView (e.g _UIHostingView) + 5. Retrieve all of the descendants of common ancestor that are between PostHogTagView and PostHogTagAnchorView (excluding tagged views) + + This logic is implemented in the `getTargetViews` function, which is called from PostHogTagView. + + ``` + */ + struct PostHogTagViewModifier: ViewModifier { + private let id = UUID() + + let onChange: PostHogTagViewHandler + let onRemove: PostHogTagViewHandler + + /** + This is a helper view modifier for retrieving a list of underlying UIKit views for the current SwiftUI view. + + If, for example, this modifier is applied on an instance of an HStack, the returned list will contain the underlying UIKit views embedded in the HStack. + For single views, the returned list will contain a single element, the view itself. + + - Parameters: + - onChange: called when the underlying UIKit views are detected, or when they are layed out. + - onRemove: called when the underlying UIKit views are removed from the view hierarchy, for cleanup. + */ + init(onChange: @escaping PostHogTagViewHandler, onRemove: @escaping PostHogTagViewHandler) { + self.onChange = onChange + self.onRemove = onRemove + } + + func body(content: Content) -> some View { + content + .background( + PostHogTagAnchorView(id: id) + .accessibility(hidden: true) + .frame(width: 0, height: 0) + ) + .overlay( + PostHogTagView(id: id, onChange: onChange, onRemove: onRemove) + .accessibility(hidden: true) + .frame(width: 0, height: 0) + ) + } + } + + struct PostHogTagView: UIViewRepresentable { + final class Coordinator { + var onChangeHandler: PostHogTagViewHandler? + var onRemoveHandler: PostHogTagViewHandler? + + private var _targets: [Weak] + var cachedTargets: [UIView] { + get { _targets.compactMap(\.value) } + set { _targets = newValue.map(Weak.init) } + } + + init( + onRemove: PostHogTagViewHandler? + ) { + _targets = [] + onRemoveHandler = onRemove + } + } + + @Binding + private var observed: Void // workaround for state changes not triggering view updates + private let id: UUID + private let onChangeHandler: PostHogTagViewHandler? + private let onRemoveHandler: PostHogTagViewHandler? + + init( + id: UUID, + onChange: PostHogTagViewHandler?, + onRemove: PostHogTagViewHandler? + ) { + _observed = .constant(()) + self.id = id + onChangeHandler = onChange + onRemoveHandler = onRemove + } + + func makeCoordinator() -> Coordinator { + // dismantleUIView is Static, so we need to store the onRemoveHandler + // somewhere where we can access it during view distruction + Coordinator(onRemove: onRemoveHandler) + } + + func makeUIView(context: Context) -> PostHogTagUIView { + let view = PostHogTagUIView(id: id) { controller in + let targets = getTargetViews(from: controller) + if !targets.isEmpty { + context.coordinator.cachedTargets = targets + onChangeHandler?(targets) + } + } + + return view + } + + func updateUIView(_: PostHogTagUIView, context _: Context) { + // + } + + static func dismantleUIView(_ uiView: PostHogTagUIView, coordinator: Coordinator) { + // using cached targets should be good here + let targets = coordinator.cachedTargets.isEmpty + ? getTargetViews(from: uiView) + : coordinator.cachedTargets + + if !targets.isEmpty { + coordinator.onRemoveHandler?(targets) + } + + uiView.postHogTagView = nil + uiView.handler = nil + } + } + + private let swiftUIIgnoreTypes: [AnyClass] = [ + // .clipShape or .clipped SwiftUI modifiers will add this to view hierarchy + // Not sure of its functionality, but it seems to be just a wrapper view with no visual impact + // + // We can safely ignore from list of descendant views, since it's sometimes being tagged + // for replay masking unintentionally + "SwiftUI._UIInheritedView", + ].compactMap(NSClassFromString) + + func getTargetViews(from taggerView: UIView) -> [UIView] { + guard + let anchorView = taggerView.postHogAnchor, + let commonAncestor = anchorView.nearestCommonAncestor(with: taggerView) + else { + return [] + } + + return commonAncestor + .allDescendants(between: anchorView, and: taggerView) + .lazy + .filter { + // ignore some system SwiftUI views + !swiftUIIgnoreTypes.contains(where: $0.isKind(of:)) + } + .filter { + // exclude injected views + !$0.postHogView + } + } + + private struct PostHogTagAnchorView: UIViewRepresentable { + var id: UUID + + func makeUIView(context _: Context) -> some UIView { + PostHogTagAnchorUIView(id: id) + } + + func updateUIView(_: UIViewType, context _: Context) { + // + } + } + + private class PostHogTagAnchorUIView: UIView { + let id: UUID + + init(id: UUID) { + self.id = id + super.init(frame: .zero) + TaggingStore.shared[id, default: .init()].anchor = self + postHogView = true + } + + required init?(coder _: NSCoder) { + id = UUID() + super.init(frame: .zero) + } + } + + final class PostHogTagUIView: UIView { + let id: UUID + var handler: (() -> Void)? + + init( + id: UUID, + handler: ((PostHogTagUIView) -> Void)? + ) { + self.id = id + super.init(frame: .zero) + self.handler = { [weak self] in + guard let self else { + return + } + handler?(self) + } + + TaggingStore.shared[id, default: .init()].tagger = self + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + id = UUID() + super.init(frame: .zero) + } + + override func didMoveToSuperview() { + super.didMoveToSuperview() + postHogTagView = self + postHogView = true + handler?() + } + + override func didMoveToWindow() { + super.didMoveToWindow() + handler?() + } + + override func layoutSubviews() { + super.layoutSubviews() + handler?() + } + } + + private extension UIView { + var postHogTagView: PostHogTagUIView? { + get { objc_getAssociatedObject(self, &AssociatedKeys.phTagView) as? PostHogTagUIView } + set { objc_setAssociatedObject(self, &AssociatedKeys.phTagView, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } + } + + var postHogView: Bool { + get { objc_getAssociatedObject(self, &AssociatedKeys.phView) as? Bool ?? false } + set { objc_setAssociatedObject(self, &AssociatedKeys.phView, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } + } + + func allDescendants(between bottomEntity: UIView, and topEntity: UIView) -> some Sequence { + descendants + .lazy + .drop(while: { $0 !== bottomEntity }) + .prefix(while: { $0 !== topEntity }) + } + + var ancestors: some Sequence { + sequence(first: self, next: { $0.superview }).dropFirst() + } + + var descendants: some Sequence { + recursiveSequence([self], children: { $0.subviews }).dropFirst() + } + + func isDescendant(of other: UIView) -> Bool { + ancestors.contains(other) + } + + func nearestCommonAncestor(with other: UIView) -> UIView? { + var nearestAncestor: UIView? = self + + while let currentEntity = nearestAncestor, !other.isDescendant(of: currentEntity) { + nearestAncestor = currentEntity.superview + } + + return nearestAncestor + } + + var postHogAnchor: UIView? { + if let tagView = postHogTagView { + return TaggingStore.shared[tagView.id]?.anchor + } + return nil + } + } + + /** + A helper store for storing reference pairs between anchor and tagger views + */ + @MainActor private enum TaggingStore { + static var shared: [UUID: Pair] = [:] + + struct Pair { + weak var anchor: PostHogTagAnchorUIView? + weak var tagger: PostHogTagUIView? + } + } + + /** + Recursively iterates over a sequence of elements, applying a function to each element to get its children. + + - Parameters: + - sequence: The sequence of elements to iterate over. + - children: A function that takes an element and returns a sequence of its children. + - Returns: An AnySequence that iterates over all elements and their children. + */ + private func recursiveSequence(_ sequence: S, children: @escaping (S.Element) -> S) -> AnySequence { + AnySequence { + var mainIterator = sequence.makeIterator() + // Current iterator, or `nil` if all sequences are exhausted: + var iterator: AnyIterator? + + return AnyIterator { + guard let iterator, let element = iterator.next() else { + if let element = mainIterator.next() { + iterator = recursiveSequence(children(element), children: children).makeIterator() + return element + } + return nil + } + return element + } + } + } + + /** + Boxing a weak reference to a reference type. + */ + final class Weak { + weak var value: T? + + init(_ wrappedValue: T? = nil) { + value = wrappedValue + } + } + +#endif diff --git a/Pods/PostHog/PostHog/UIViewController.swift b/Pods/PostHog/PostHog/UIViewController.swift new file mode 100644 index 0000000..6def956 --- /dev/null +++ b/Pods/PostHog/PostHog/UIViewController.swift @@ -0,0 +1,26 @@ +// +// UIViewController.swift +// PostHog +// +// Inspired by +// https://raw.githubusercontent.com/segmentio/analytics-swift/e613e09aa1b97144126a923ec408374f914a6f2e/Examples/other_plugins/UIKitScreenTracking.swift +// +// Created by Manoel Aranda Neto on 23.10.23. +// + +#if os(iOS) || os(tvOS) + import Foundation + import UIKit + + extension UIViewController { + static func getViewControllerName(_ viewController: UIViewController) -> String? { + var title: String? = String(describing: viewController.classForCoder).replacingOccurrences(of: "ViewController", with: "") + + if title?.isEmpty == true { + title = viewController.title ?? nil + } + + return title + } + } +#endif diff --git a/Pods/PostHog/PostHog/Utils/AssociatedKeys.swift b/Pods/PostHog/PostHog/Utils/AssociatedKeys.swift new file mode 100644 index 0000000..c8d0da6 --- /dev/null +++ b/Pods/PostHog/PostHog/Utils/AssociatedKeys.swift @@ -0,0 +1,17 @@ +// +// AssociatedKeys.swift +// PostHog +// +// Created by Yiannis Josephides on 04/12/2024. +// + +import Foundation + +enum AssociatedKeys { + static var phForwardingDelegate: UInt8 = 0 + static var phNoCapture: UInt8 = 0 + static var phNoMask: UInt8 = 0 + static var phTagView: UInt8 = 0 + static var phView: UInt8 = 0 + static var phLabel: UInt8 = 0 +} diff --git a/Pods/PostHog/PostHog/Utils/Data+Gzip.swift b/Pods/PostHog/PostHog/Utils/Data+Gzip.swift new file mode 100644 index 0000000..09498a6 --- /dev/null +++ b/Pods/PostHog/PostHog/Utils/Data+Gzip.swift @@ -0,0 +1,301 @@ +// +// Data+Gzip.swift +// +// https://github.com/1024jp/GzipSwift/blob/731037f6cc2be2ec01562f6597c1d0aa3fe6fd05/Sources/Gzip/Data%2BGzip.swift + +/* + The MIT License (MIT) + + © 2014-2023 1024jp + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +// issues importing scoped classes +// import struct Foundation.Data +import Foundation + +#if os(Linux) + import zlibLinux +#else + import zlib +#endif + +public enum Gzip { + /// Maximum value for windowBits (`MAX_WBITS`) + public static let maxWindowBits = MAX_WBITS +} + +/// Compression level whose rawValue is based on the zlib's constants. +public struct CompressionLevel: RawRepresentable, Sendable { + /// Compression level in the range of `0` (no compression) to `9` (maximum compression). + public let rawValue: Int32 + + public static let noCompression = Self(Z_NO_COMPRESSION) + public static let bestSpeed = Self(Z_BEST_SPEED) + public static let bestCompression = Self(Z_BEST_COMPRESSION) + + public static let defaultCompression = Self(Z_DEFAULT_COMPRESSION) + + public init(rawValue: Int32) { + self.rawValue = rawValue + } + + public init(_ rawValue: Int32) { + self.rawValue = rawValue + } +} + +/// Errors on gzipping/gunzipping based on the zlib error codes. +public struct GzipError: Swift.Error, Sendable { + // cf. http://www.zlib.net/manual.html + + public enum Kind: Equatable, Sendable { + /// The stream structure was inconsistent. + /// + /// - underlying zlib error: `Z_STREAM_ERROR` (-2) + case stream + + /// The input data was corrupted + /// (input stream not conforming to the zlib format or incorrect check value). + /// + /// - underlying zlib error: `Z_DATA_ERROR` (-3) + case data + + /// There was not enough memory. + /// + /// - underlying zlib error: `Z_MEM_ERROR` (-4) + case memory + + /// No progress is possible or there was not enough room in the output buffer. + /// + /// - underlying zlib error: `Z_BUF_ERROR` (-5) + case buffer + + /// The zlib library version is incompatible with the version assumed by the caller. + /// + /// - underlying zlib error: `Z_VERSION_ERROR` (-6) + case version + + /// An unknown error occurred. + /// + /// - parameter code: return error by zlib + case unknown(code: Int) + } + + /// Error kind. + public let kind: Kind + + /// Returned message by zlib. + public let message: String + + init(code: Int32, msg: UnsafePointer?) { + message = msg.flatMap(String.init(validatingUTF8:)) ?? "Unknown gzip error" + kind = Kind(code: code) + } + + public var localizedDescription: String { + message + } +} + +private extension GzipError.Kind { + init(code: Int32) { + switch code { + case Z_STREAM_ERROR: + self = .stream + case Z_DATA_ERROR: + self = .data + case Z_MEM_ERROR: + self = .memory + case Z_BUF_ERROR: + self = .buffer + case Z_VERSION_ERROR: + self = .version + default: + self = .unknown(code: Int(code)) + } + } +} + +extension Data { + /// Whether the receiver is compressed in gzip format. + var isGzipped: Bool { + starts(with: [0x1F, 0x8B]) // check magic number + } + + /// Create a new `Data` instance by compressing the receiver using zlib. + /// Throws an error if compression failed. + /// + /// The `wBits` parameter allows for managing the size of the history buffer. The possible values are: + /// + /// Value Window size logarithm Input + /// +9 to +15 Base 2 Includes zlib header and trailer + /// -9 to -15 Absolute value of wbits No header and trailer + /// +25 to +31 Low 4 bits of the value Includes gzip header and trailing checksum + /// + /// - Parameter level: Compression level. + /// - Parameter wBits: Manage the size of the history buffer. + /// - Returns: Gzip-compressed `Data` instance. + /// - Throws: `GzipError` + func gzipped(level: CompressionLevel = .defaultCompression, wBits: Int32 = Gzip.maxWindowBits + 16) throws -> Data { + guard !isEmpty else { + return Data() + } + + var stream = z_stream() + var status: Int32 + + status = deflateInit2_(&stream, level.rawValue, Z_DEFLATED, wBits, MAX_MEM_LEVEL, Z_DEFAULT_STRATEGY, ZLIB_VERSION, Int32(DataSize.stream)) + + guard status == Z_OK else { + // deflateInit2 returns: + // Z_VERSION_ERROR The zlib library version is incompatible with the version assumed by the caller. + // Z_MEM_ERROR There was not enough memory. + // Z_STREAM_ERROR A parameter is invalid. + + throw GzipError(code: status, msg: stream.msg) + } + + var data = Data(capacity: DataSize.chunk) + repeat { + if Int(stream.total_out) >= data.count { + data.count += DataSize.chunk + } + + let inputCount = count + let outputCount = data.count + + withUnsafeBytes { (inputPointer: UnsafeRawBufferPointer) in + stream.next_in = UnsafeMutablePointer(mutating: inputPointer.bindMemory(to: Bytef.self).baseAddress!).advanced(by: Int(stream.total_in)) + stream.avail_in = uInt(inputCount) - uInt(stream.total_in) + + data.withUnsafeMutableBytes { (outputPointer: UnsafeMutableRawBufferPointer) in + stream.next_out = outputPointer.bindMemory(to: Bytef.self).baseAddress!.advanced(by: Int(stream.total_out)) + stream.avail_out = uInt(outputCount) - uInt(stream.total_out) + + status = deflate(&stream, Z_FINISH) + + stream.next_out = nil + } + + stream.next_in = nil + } + + } while stream.avail_out == 0 && status != Z_STREAM_END + + guard deflateEnd(&stream) == Z_OK, status == Z_STREAM_END else { + throw GzipError(code: status, msg: stream.msg) + } + + data.count = Int(stream.total_out) + + return data + } + + /// Create a new `Data` instance by decompressing the receiver using zlib. + /// Throws an error if decompression failed. + /// + /// The `wBits` parameter allows for managing the size of the history buffer. The possible values are: + /// + /// Value Window size logarithm Input + /// +8 to +15 Base 2 Includes zlib header and trailer + /// -8 to -15 Absolute value of wbits Raw stream with no header and trailer + /// +24 to +31 = 16 + (8 to 15) Low 4 bits of the value Includes gzip header and trailer + /// +40 to +47 = 32 + (8 to 15) Low 4 bits of the value zlib or gzip format + /// + /// - Parameter wBits: Manage the size of the history buffer. + /// - Returns: Gzip-decompressed `Data` instance. + /// - Throws: `GzipError` + func gunzipped(wBits: Int32 = Gzip.maxWindowBits + 32) throws -> Data { + guard !isEmpty else { + return Data() + } + + var data = Data(capacity: count * 2) + var totalIn: uLong = 0 + var totalOut: uLong = 0 + + repeat { + var stream = z_stream() + var status: Int32 + + status = inflateInit2_(&stream, wBits, ZLIB_VERSION, Int32(DataSize.stream)) + + guard status == Z_OK else { + // inflateInit2 returns: + // Z_VERSION_ERROR The zlib library version is incompatible with the version assumed by the caller. + // Z_MEM_ERROR There was not enough memory. + // Z_STREAM_ERROR A parameters are invalid. + + throw GzipError(code: status, msg: stream.msg) + } + + repeat { + if Int(totalOut + stream.total_out) >= data.count { + data.count += count / 2 + } + + let inputCount = count + let outputCount = data.count + + withUnsafeBytes { (inputPointer: UnsafeRawBufferPointer) in + let inputStartPosition = totalIn + stream.total_in + stream.next_in = UnsafeMutablePointer(mutating: inputPointer.bindMemory(to: Bytef.self).baseAddress!).advanced(by: Int(inputStartPosition)) + stream.avail_in = uInt(inputCount) - uInt(inputStartPosition) + + data.withUnsafeMutableBytes { (outputPointer: UnsafeMutableRawBufferPointer) in + let outputStartPosition = totalOut + stream.total_out + stream.next_out = outputPointer.bindMemory(to: Bytef.self).baseAddress!.advanced(by: Int(outputStartPosition)) + stream.avail_out = uInt(outputCount) - uInt(outputStartPosition) + + status = inflate(&stream, Z_SYNC_FLUSH) + + stream.next_out = nil + } + + stream.next_in = nil + } + } while status == Z_OK + + totalIn += stream.total_in + + guard inflateEnd(&stream) == Z_OK, status == Z_STREAM_END else { + // inflate returns: + // Z_DATA_ERROR The input data was corrupted (input stream not conforming to the zlib format or incorrect check value). + // Z_STREAM_ERROR The stream structure was inconsistent (for example if next_in or next_out was NULL). + // Z_MEM_ERROR There was not enough memory. + // Z_BUF_ERROR No progress is possible or there was not enough room in the output buffer when Z_FINISH is used. + throw GzipError(code: status, msg: stream.msg) + } + + totalOut += stream.total_out + + } while totalIn < count + + data.count = Int(totalOut) + + return data + } +} + +private enum DataSize { + static let chunk = 1 << 14 + static let stream = MemoryLayout.size +} diff --git a/Pods/PostHog/PostHog/Utils/DateUtils.swift b/Pods/PostHog/PostHog/Utils/DateUtils.swift new file mode 100644 index 0000000..f874b1b --- /dev/null +++ b/Pods/PostHog/PostHog/Utils/DateUtils.swift @@ -0,0 +1,43 @@ +// +// DateUtils.swift +// PostHog +// +// Created by Manoel Aranda Neto on 27.10.23. +// + +import Foundation + +final class PostHogAPIDateFormatter { + private static func getFormatter(with format: String) -> DateFormatter { + let dateFormatter = DateFormatter() + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + dateFormatter.timeZone = TimeZone(abbreviation: "UTC") + dateFormatter.dateFormat = format + return dateFormatter + } + + private let dateFormatterWithMilliseconds = getFormatter(with: "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") + + private let dateFormatterWithSeconds = getFormatter(with: "yyyy-MM-dd'T'HH:mm:ss'Z'") + + func string(from date: Date) -> String { + dateFormatterWithMilliseconds.string(from: date) + } + + func date(from string: String) -> Date? { + dateFormatterWithMilliseconds.date(from: string) + ?? dateFormatterWithSeconds.date(from: string) + } +} + +let apiDateFormatter = PostHogAPIDateFormatter() + +public func toISO8601String(_ date: Date) -> String { + apiDateFormatter.string(from: date) +} + +public func toISO8601Date(_ date: String) -> Date? { + apiDateFormatter.date(from: date) +} + +var now: () -> Date = { Date() } diff --git a/Pods/PostHog/PostHog/Utils/DictUtils.swift b/Pods/PostHog/PostHog/Utils/DictUtils.swift new file mode 100644 index 0000000..e0d0612 --- /dev/null +++ b/Pods/PostHog/PostHog/Utils/DictUtils.swift @@ -0,0 +1,43 @@ +// +// DictUtils.swift +// PostHog +// +// Created by Manoel Aranda Neto on 27.10.23. +// + +import Foundation + +public func sanitizeDictionary(_ dict: [String: Any]?) -> [String: Any]? { + if dict == nil || dict!.isEmpty { + return nil + } + + var newDict = dict! + + for (key, value) in newDict where !isValidObject(value) { + if value is URL { + newDict[key] = (value as! URL).absoluteString + continue + } + if value is Date { + newDict[key] = ISO8601DateFormatter().string(from: (value as! Date)) + continue + } + + newDict.removeValue(forKey: key) + hedgeLog("property: \(key) isn't serializable, dropping the item") + } + + return newDict +} + +private func isValidObject(_ object: Any) -> Bool { + if object is String || object is Bool || object is any Numeric || object is NSNumber { + return true + } + if object is [Any?] || object is [String: Any?] { + return JSONSerialization.isValidJSONObject(object) + } + // workaround [object] since isValidJSONObject only accepts an Array or Dict + return JSONSerialization.isValidJSONObject([object]) +} diff --git a/Pods/PostHog/PostHog/Utils/Errors.swift b/Pods/PostHog/PostHog/Utils/Errors.swift new file mode 100644 index 0000000..35eb31c --- /dev/null +++ b/Pods/PostHog/PostHog/Utils/Errors.swift @@ -0,0 +1,16 @@ +// +// Errors.swift +// PostHog +// +// Created by Ben White on 21.03.23. +// + +import Foundation + +struct InternalPostHogError: Error, CustomStringConvertible { + let description: String + + init(description: String, fileID: StaticString = #fileID, line: UInt = #line) { + self.description = "\(description) (\(fileID):\(line))" + } +} diff --git a/Pods/PostHog/PostHog/Utils/FileUtils.swift b/Pods/PostHog/PostHog/Utils/FileUtils.swift new file mode 100644 index 0000000..1bb9b20 --- /dev/null +++ b/Pods/PostHog/PostHog/Utils/FileUtils.swift @@ -0,0 +1,33 @@ +// +// FileUtils.swift +// PostHog +// +// Created by Manoel Aranda Neto on 30.10.23. +// + +import Foundation + +public func deleteSafely(_ file: URL) { + if FileManager.default.fileExists(atPath: file.path) { + do { + try FileManager.default.removeItem(at: file) + } catch { + hedgeLog("Error trying to delete file \(file.path) \(error)") + } + } +} + +/// Check if provided directory exists +func directoryExists(_ directory: URL) -> Bool { + var isDirectory: ObjCBool = false + return FileManager.default.fileExists(atPath: directory.path, isDirectory: &isDirectory) && isDirectory.boolValue +} + +func createDirectoryAtURLIfNeeded(url: URL) { + if FileManager.default.fileExists(atPath: url.path) { return } + do { + try FileManager.default.createDirectory(atPath: url.path, withIntermediateDirectories: true) + } catch { + hedgeLog("Error creating storage directory: \(error)") + } +} diff --git a/Pods/PostHog/PostHog/Utils/Hedgelog.swift b/Pods/PostHog/PostHog/Utils/Hedgelog.swift new file mode 100644 index 0000000..f53ad86 --- /dev/null +++ b/Pods/PostHog/PostHog/Utils/Hedgelog.swift @@ -0,0 +1,20 @@ +// +// Hedgelog.swift +// PostHog +// +// Created by Ben White on 07.02.23. +// + +import Foundation + +var hedgeLogEnabled = false + +func toggleHedgeLog(_ enabled: Bool) { + hedgeLogEnabled = enabled +} + +// Meant for internally logging PostHog related things +func hedgeLog(_ message: String) { + if !hedgeLogEnabled { return } + print("[PostHog] \(message)") +} diff --git a/Pods/PostHog/PostHog/Utils/Reachability.swift b/Pods/PostHog/PostHog/Utils/Reachability.swift new file mode 100644 index 0000000..fe840e8 --- /dev/null +++ b/Pods/PostHog/PostHog/Utils/Reachability.swift @@ -0,0 +1,406 @@ +/* + Copyright (c) 2014, Ashley Mills + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + */ + +import Foundation + +#if !os(watchOS) + import SystemConfiguration + + public enum ReachabilityError: Error { + case failedToCreateWithAddress(sockaddr, Int32) + case failedToCreateWithHostname(String, Int32) + case unableToSetCallback(Int32) + case unableToSetDispatchQueue(Int32) + case unableToGetFlags(Int32) + } + + @available(*, unavailable, renamed: "Notification.Name.reachabilityChanged") + public let ReachabilityChangedNotification = NSNotification.Name("ReachabilityChangedNotification") + + extension Notification.Name { + static let reachabilityChanged = Notification.Name("reachabilityChanged") + } + + public class Reachability { + public typealias NetworkReachable = (Reachability) -> Void + public typealias NetworkUnreachable = (Reachability) -> Void + + @available(*, unavailable, renamed: "Connection") + public enum NetworkStatus: CustomStringConvertible { + case notReachable, reachableViaWiFi, reachableViaWWAN + public var description: String { + switch self { + case .reachableViaWWAN: return "Cellular" + case .reachableViaWiFi: return "WiFi" + case .notReachable: return "No Connection" + } + } + } + + public enum Connection: CustomStringConvertible { + case unavailable, wifi, cellular + public var description: String { + switch self { + case .cellular: return "Cellular" + case .wifi: return "WiFi" + case .unavailable: return "No Connection" + } + } + + @available(*, deprecated, renamed: "unavailable") + public static let none: Connection = .unavailable + } + + public var whenReachable: NetworkReachable? + public var whenUnreachable: NetworkUnreachable? + + @available(*, deprecated, renamed: "allowsCellularConnection") + public let reachableOnWWAN: Bool = true + + /// Set to `false` to force Reachability.connection to .none when on cellular connection (default value `true`) + public var allowsCellularConnection: Bool + + // The notification center on which "reachability changed" events are being posted + public var notificationCenter: NotificationCenter = .default + + @available(*, deprecated, renamed: "connection.description") + public var currentReachabilityString: String { + "\(connection)" + } + + @available(*, unavailable, renamed: "connection") + public var currentReachabilityStatus: Connection { + connection + } + + public var connection: Connection { + if flags == nil { + try? setReachabilityFlags() + } + + switch flags?.connection { + case .unavailable?, nil: return .unavailable + case .cellular?: return allowsCellularConnection ? .cellular : .unavailable + case .wifi?: return .wifi + } + } + + private(set) var notifierRunning = false + private let reachabilityRef: SCNetworkReachability + private let reachabilitySerialQueue: DispatchQueue + private let notificationQueue: DispatchQueue? + private(set) var flags: SCNetworkReachabilityFlags? { + didSet { + guard flags != oldValue else { return } + notifyReachabilityChanged() + } + } + + public required init(reachabilityRef: SCNetworkReachability, + queueQoS: DispatchQoS = .default, + targetQueue: DispatchQueue? = nil, + notificationQueue: DispatchQueue? = .main) + { + allowsCellularConnection = true + self.reachabilityRef = reachabilityRef + reachabilitySerialQueue = DispatchQueue(label: "uk.co.ashleymills.reachability", qos: queueQoS, target: targetQueue) + self.notificationQueue = notificationQueue + } + + public convenience init(hostname: String, + queueQoS: DispatchQoS = .default, + targetQueue: DispatchQueue? = nil, + notificationQueue: DispatchQueue? = .main) throws + { + guard let ref = SCNetworkReachabilityCreateWithName(nil, hostname) else { + throw ReachabilityError.failedToCreateWithHostname(hostname, SCError()) + } + self.init(reachabilityRef: ref, queueQoS: queueQoS, targetQueue: targetQueue, notificationQueue: notificationQueue) + } + + public convenience init(queueQoS: DispatchQoS = .default, + targetQueue: DispatchQueue? = nil, + notificationQueue: DispatchQueue? = .main) throws + { + var zeroAddress = sockaddr() + zeroAddress.sa_len = UInt8(MemoryLayout.size) + zeroAddress.sa_family = sa_family_t(AF_INET) + + guard let ref = SCNetworkReachabilityCreateWithAddress(nil, &zeroAddress) else { + throw ReachabilityError.failedToCreateWithAddress(zeroAddress, SCError()) + } + + self.init(reachabilityRef: ref, queueQoS: queueQoS, targetQueue: targetQueue, notificationQueue: notificationQueue) + } + + deinit { + stopNotifier() + } + } + + extension Reachability { + // MARK: - *** Notifier methods *** + + func startNotifier() throws { + guard !notifierRunning else { return } + + let callback: SCNetworkReachabilityCallBack = { _, flags, info in + guard let info = info else { return } + + // `weakifiedReachability` is guaranteed to exist by virtue of our + // retain/release callbacks which we provided to the `SCNetworkReachabilityContext`. + let weakifiedReachability = Unmanaged.fromOpaque(info).takeUnretainedValue() + + // The weak `reachability` _may_ no longer exist if the `Reachability` + // object has since been deallocated but a callback was already in flight. + weakifiedReachability.reachability?.flags = flags + } + + let weakifiedReachability = ReachabilityWeakifier(reachability: self) + let opaqueWeakifiedReachability = Unmanaged.passUnretained(weakifiedReachability).toOpaque() + + var context = SCNetworkReachabilityContext( + version: 0, + info: UnsafeMutableRawPointer(opaqueWeakifiedReachability), + retain: { (info: UnsafeRawPointer) -> UnsafeRawPointer in + let unmanagedWeakifiedReachability = Unmanaged.fromOpaque(info) + _ = unmanagedWeakifiedReachability.retain() + return UnsafeRawPointer(unmanagedWeakifiedReachability.toOpaque()) + }, + release: { (info: UnsafeRawPointer) in + let unmanagedWeakifiedReachability = Unmanaged.fromOpaque(info) + unmanagedWeakifiedReachability.release() + }, + copyDescription: { (info: UnsafeRawPointer) -> Unmanaged in + let unmanagedWeakifiedReachability = Unmanaged.fromOpaque(info) + let weakifiedReachability = unmanagedWeakifiedReachability.takeUnretainedValue() + let description = weakifiedReachability.reachability?.description ?? "nil" + return Unmanaged.passRetained(description as CFString) + } + ) + + if !SCNetworkReachabilitySetCallback(reachabilityRef, callback, &context) { + stopNotifier() + throw ReachabilityError.unableToSetCallback(SCError()) + } + + if !SCNetworkReachabilitySetDispatchQueue(reachabilityRef, reachabilitySerialQueue) { + stopNotifier() + throw ReachabilityError.unableToSetDispatchQueue(SCError()) + } + + // Perform an initial check + try setReachabilityFlags() + + notifierRunning = true + } + + func stopNotifier() { + defer { notifierRunning = false } + + SCNetworkReachabilitySetCallback(reachabilityRef, nil, nil) + SCNetworkReachabilitySetDispatchQueue(reachabilityRef, nil) + } + + // MARK: - *** Connection test methods *** + + @available(*, deprecated, message: "Please use `connection != .none`") + var isReachable: Bool { + connection != .unavailable + } + + @available(*, deprecated, message: "Please use `connection == .cellular`") + var isReachableViaWWAN: Bool { + // Check we're not on the simulator, we're REACHABLE and check we're on WWAN + connection == .cellular + } + + @available(*, deprecated, message: "Please use `connection == .wifi`") + var isReachableViaWiFi: Bool { + connection == .wifi + } + + var description: String { + flags?.description ?? "unavailable flags" + } + } + + private extension Reachability { + func setReachabilityFlags() throws { + try reachabilitySerialQueue.sync { [unowned self] in + var flags = SCNetworkReachabilityFlags() + if !SCNetworkReachabilityGetFlags(reachabilityRef, &flags) { + stopNotifier() + throw ReachabilityError.unableToGetFlags(SCError()) + } + + self.flags = flags + } + } + + func notifyReachabilityChanged() { + let notify = { [weak self] in + guard let self = self else { return } + self.connection != .unavailable ? self.whenReachable?(self) : self.whenUnreachable?(self) + self.notificationCenter.post(name: .reachabilityChanged, object: self) + } + + // notify on the configured `notificationQueue`, or the caller's (i.e. `reachabilitySerialQueue`) + notificationQueue?.async(execute: notify) ?? notify() + } + } + + extension SCNetworkReachabilityFlags { + typealias Connection = Reachability.Connection + + var connection: Connection { + guard isReachableFlagSet else { return .unavailable } + + // If we're reachable, but not on an iOS device (i.e. simulator), we must be on WiFi + #if targetEnvironment(simulator) + return .wifi + #else + var connection = Connection.unavailable + + if !isConnectionRequiredFlagSet { + connection = .wifi + } + + if isConnectionOnTrafficOrDemandFlagSet { + if !isInterventionRequiredFlagSet { + connection = .wifi + } + } + + if isOnWWANFlagSet { + connection = .cellular + } + + return connection + #endif + } + + var isOnWWANFlagSet: Bool { + #if os(iOS) || os(visionOS) + return contains(.isWWAN) + #else + return false + #endif + } + + var isReachableFlagSet: Bool { + contains(.reachable) + } + + var isConnectionRequiredFlagSet: Bool { + contains(.connectionRequired) + } + + var isInterventionRequiredFlagSet: Bool { + contains(.interventionRequired) + } + + var isConnectionOnTrafficFlagSet: Bool { + contains(.connectionOnTraffic) + } + + var isConnectionOnDemandFlagSet: Bool { + contains(.connectionOnDemand) + } + + var isConnectionOnTrafficOrDemandFlagSet: Bool { + !intersection([.connectionOnTraffic, .connectionOnDemand]).isEmpty + } + + var isTransientConnectionFlagSet: Bool { + contains(.transientConnection) + } + + var isLocalAddressFlagSet: Bool { + contains(.isLocalAddress) + } + + var isDirectFlagSet: Bool { + contains(.isDirect) + } + + var description: String { + let W = isOnWWANFlagSet ? "W" : "-" + let R = isReachableFlagSet ? "R" : "-" + let c = isConnectionRequiredFlagSet ? "c" : "-" + let t = isTransientConnectionFlagSet ? "t" : "-" + let i = isInterventionRequiredFlagSet ? "i" : "-" + let C = isConnectionOnTrafficFlagSet ? "C" : "-" + let D = isConnectionOnDemandFlagSet ? "D" : "-" + let l = isLocalAddressFlagSet ? "l" : "-" + let d = isDirectFlagSet ? "d" : "-" + + return "\(W)\(R) \(c)\(t)\(i)\(C)\(D)\(l)\(d)" + } + } + + /** + `ReachabilityWeakifier` weakly wraps the `Reachability` class + in order to break retain cycles when interacting with CoreFoundation. + + CoreFoundation callbacks expect a pair of retain/release whenever an + opaque `info` parameter is provided. These callbacks exist to guard + against memory management race conditions when invoking the callbacks. + + #### Race Condition + + If we passed `SCNetworkReachabilitySetCallback` a direct reference to our + `Reachability` class without also providing corresponding retain/release + callbacks, then a race condition can lead to crashes when: + - `Reachability` is deallocated on thread X + - A `SCNetworkReachability` callback(s) is already in flight on thread Y + + #### Retain Cycle + + If we pass `Reachability` to CoreFoundtion while also providing retain/ + release callbacks, we would create a retain cycle once CoreFoundation + retains our `Reachability` class. This fixes the crashes and his how + CoreFoundation expects the API to be used, but doesn't play nicely with + Swift/ARC. This cycle would only be broken after manually calling + `stopNotifier()` — `deinit` would never be called. + + #### ReachabilityWeakifier + + By providing both retain/release callbacks and wrapping `Reachability` in + a weak wrapper, we: + - interact correctly with CoreFoundation, thereby avoiding a crash. + See "Memory Management Programming Guide for Core Foundation". + - don't alter the public API of `Reachability.swift` in any way + - still allow for automatic stopping of the notifier on `deinit`. + */ + private class ReachabilityWeakifier { + weak var reachability: Reachability? + init(reachability: Reachability) { + self.reachability = reachability + } + } +#endif diff --git a/Pods/PostHog/PostHog/Utils/ReadWriteLock.swift b/Pods/PostHog/PostHog/Utils/ReadWriteLock.swift new file mode 100644 index 0000000..c782a15 --- /dev/null +++ b/Pods/PostHog/PostHog/Utils/ReadWriteLock.swift @@ -0,0 +1,67 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import Foundation + +/// A property wrapper using a fair, POSIX conforming reader-writer lock for atomic +/// access to the value. It is optimised for concurrent reads and exclusive writes. +/// +/// The wrapper is a class to prevent copying the lock, it creates and initilaizes a `pthread_rwlock_t`. +/// An additional method `mutate` allow to safely mutate the value in-place (to read it +/// and write it while obtaining the lock only once). +@propertyWrapper +public final class ReadWriteLock { + /// The wrapped value. + private var value: Value + + /// The lock object. + private var rwlock = pthread_rwlock_t() + + public init(wrappedValue value: Value) { + pthread_rwlock_init(&rwlock, nil) + self.value = value + } + + deinit { + pthread_rwlock_destroy(&rwlock) + } + + /// The wrapped value. + /// + /// The `get` will acquire the lock for reading while the `set` will acquire for + /// writing. + public var wrappedValue: Value { + get { + pthread_rwlock_rdlock(&rwlock) + defer { pthread_rwlock_unlock(&rwlock) } + return value + } + set { + pthread_rwlock_wrlock(&rwlock) + value = newValue + pthread_rwlock_unlock(&rwlock) + } + } + + /// Provides a non-escaping closure for mutation. + /// The lock will be acquired once for writing before invoking the closure. + /// + /// - Parameter closure: The closure with the mutable value. + @discardableResult + public func mutate(_ closure: (inout Value) -> T) -> T { + pthread_rwlock_wrlock(&rwlock) + defer { + pthread_rwlock_unlock(&rwlock) + } + return closure(&value) + } +} + +func synchronized(_ lock: Any, closure: () -> Void) { + objc_sync_enter(lock) + closure() + objc_sync_exit(lock) +} diff --git a/Pods/PostHog/PostHog/Utils/TimeBasedEpochGenerator.swift b/Pods/PostHog/PostHog/Utils/TimeBasedEpochGenerator.swift new file mode 100644 index 0000000..5c6c159 --- /dev/null +++ b/Pods/PostHog/PostHog/Utils/TimeBasedEpochGenerator.swift @@ -0,0 +1,86 @@ +// +// TimeBasedEpochGenerator.swift +// PostHog +// +// Created by Manoel Aranda Neto on 17.06.24. +// + +import Foundation + +class TimeBasedEpochGenerator { + static let shared = TimeBasedEpochGenerator() + + // Private initializer to prevent multiple instances + private init() {} + + private var lastEntropy = [UInt8](repeating: 0, count: 10) + private var lastTimestamp: UInt64 = 0 + + private let lock = NSLock() + + func v7() -> UUID { + var uuid: UUID? + + lock.withLock { + uuid = generateUUID() + } + + // or fallback to UUID v4 + return uuid ?? UUID() + } + + private func generateUUID() -> UUID? { + let timestamp = Date().timeIntervalSince1970 + let unixTimeMilliseconds = UInt64(timestamp * 1000) + + var uuidBytes = [UInt8]() + + let timeBytes = unixTimeMilliseconds.bigEndianData.suffix(6) // First 6 bytes for the timestamp + uuidBytes.append(contentsOf: timeBytes) + + if unixTimeMilliseconds == lastTimestamp { + var check = true + for index in (0 ..< 10).reversed() where check { + var temp = lastEntropy[index] + temp = temp &+ 0x01 + check = lastEntropy[index] == 0xFF + lastEntropy[index] = temp + } + } else { + lastTimestamp = unixTimeMilliseconds + + // Prepare the random part (10 bytes to complete the UUID) + let status = SecRandomCopyBytes(kSecRandomDefault, lastEntropy.count, &lastEntropy) + // If we can't generate secure random bytes, use a fallback + if status != errSecSuccess { + let randomData = (0 ..< 10).map { _ in UInt8.random(in: 0 ... 255) } + lastEntropy = randomData + } + } + uuidBytes.append(contentsOf: lastEntropy) + + // Set version (7) in the version byte + uuidBytes[6] = (uuidBytes[6] & 0x0F) | 0x70 + + // Set the UUID variant (10xx for standard UUIDs) + uuidBytes[8] = (uuidBytes[8] & 0x3F) | 0x80 + + // Ensure we have a total of 16 bytes + if uuidBytes.count == 16 { + return UUID(uuid: (uuidBytes[0], uuidBytes[1], uuidBytes[2], uuidBytes[3], + uuidBytes[4], uuidBytes[5], uuidBytes[6], uuidBytes[7], + uuidBytes[8], uuidBytes[9], uuidBytes[10], uuidBytes[11], + uuidBytes[12], uuidBytes[13], uuidBytes[14], uuidBytes[15])) + } + + return nil + } +} + +extension UInt64 { + // Correctly generate Data representation in big endian format + var bigEndianData: Data { + var bigEndianValue = bigEndian + return Data(bytes: &bigEndianValue, count: MemoryLayout.size) + } +} diff --git a/Pods/PostHog/PostHog/Utils/UIApplication+.swift b/Pods/PostHog/PostHog/Utils/UIApplication+.swift new file mode 100644 index 0000000..afe9b2b --- /dev/null +++ b/Pods/PostHog/PostHog/Utils/UIApplication+.swift @@ -0,0 +1,43 @@ +// +// UIApplication+.swift +// PostHog +// +// Created by Yiannis Josephides on 11/11/2024. +// + +#if os(iOS) || os(tvOS) || os(visionOS) + import UIKit + + extension UIApplication { + static func getCurrentWindow(filterForegrounded: Bool = true) -> UIWindow? { + let windowScenes = UIApplication.shared + .connectedScenes + .compactMap { $0 as? UIWindowScene } + .filter { + !filterForegrounded || $0.activationState == .foregroundActive + } + + for scene in windowScenes { + // attempt to retrieve directly from UIWindowScene + if #available(iOS 15.0, tvOS 15.0, *) { + if let keyWindow = scene.keyWindow { + return keyWindow + } + } else { + // check scene.windows.isKeyWindow + for window in scene.windows where window.isKeyWindow { + return window + } + } + + // check scene.delegate.window property + let sceneDelegate = scene.delegate as? UIWindowSceneDelegate + if let target = sceneDelegate, let window = target.window { + return window + } + } + + return nil + } + } +#endif diff --git a/Pods/PostHog/PostHog/Utils/UIImage+WebP.swift b/Pods/PostHog/PostHog/Utils/UIImage+WebP.swift new file mode 100644 index 0000000..377f0bd --- /dev/null +++ b/Pods/PostHog/PostHog/Utils/UIImage+WebP.swift @@ -0,0 +1,138 @@ +// +// UIImage+WebP.swift +// PostHog +// +// Created by Yiannis Josephides on 09/12/2024. +// +// Adapted from: https://github.com/SDWebImage/SDWebImageWebPCoder/blob/master/SDWebImageWebPCoder/Classes/SDImageWebPCoder.m + +#if os(iOS) + import Accelerate + import CoreGraphics + import Foundation + #if canImport(phlibwebp) + // SPM package is linked via a lib since mix-code is not yet supported + + // `internal import`: added in Swift 5.9 and it's the "official" feature. Should replace when we switch to swift-tools-version:5.9 + // see: (https://github.com/swiftlang/swift-evolution/blob/main/proposals/0409-access-level-on-imports.md) + + // @_implementationOnly: available since Swift 5.1 + @_implementationOnly import phlibwebp + #endif + import UIKit + + extension UIImage { + /** + Returns a data object that contains the image in WebP format. + + - Parameters: + - compressionQuality: desired compression quality [0...1] (0=max/lowest quality, 1=low/high quality) + - Returns: A data object containing the WebP data, or nil if there’s a problem generating the data. + */ + func webpData(compressionQuality: CGFloat) -> Data? { + // Early exit if image is missing + guard let cgImage = cgImage else { + return nil + } + + // validate dimensions + let width = Int(cgImage.width) + let height = Int(cgImage.height) + + guard width > 0, width <= WEBP_MAX_DIMENSION, height > 0, height <= WEBP_MAX_DIMENSION else { + return nil + } + + let bitmapInfo = cgImage.bitmapInfo + let alphaInfo = CGImageAlphaInfo(rawValue: bitmapInfo.rawValue & CGBitmapInfo.alphaInfoMask.rawValue) + + // Prepare destination format + + let hasAlpha = !( + alphaInfo == CGImageAlphaInfo.none || + alphaInfo == CGImageAlphaInfo.noneSkipFirst || + alphaInfo == CGImageAlphaInfo.noneSkipLast + ) + + // try to use image color space if ~rgb + let colorSpace: CGColorSpace = cgImage.colorSpace?.model == .rgb + ? cgImage.colorSpace! // safe from previous check + : CGColorSpace(name: CGColorSpace.linearSRGB)! + let renderingIntent = cgImage.renderingIntent + + guard let destFormat = vImage_CGImageFormat( + bitsPerComponent: 8, + bitsPerPixel: hasAlpha ? 32 : 24, // RGB888/RGBA8888 + colorSpace: colorSpace, + bitmapInfo: hasAlpha + ? CGBitmapInfo(rawValue: CGImageAlphaInfo.last.rawValue | CGBitmapInfo.byteOrderDefault.rawValue) + : CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue | CGBitmapInfo.byteOrderDefault.rawValue), + renderingIntent: renderingIntent + ) else { + return nil + } + + guard let dest = try? vImage_Buffer(cgImage: cgImage, format: destFormat, flags: .noFlags) else { + hedgeLog("Error initializing WebP image buffer") + return nil + } + defer { dest.data?.deallocate() } + + guard let rgba = dest.data else { // byte array + hedgeLog("Could not get rgba byte array from destination format") + return nil + } + let bytesPerRow = dest.rowBytes + + let quality = Float(compressionQuality * 100) // WebP quality is 0-100 + + var config = WebPConfig() + var picture = WebPPicture() + var writer = WebPMemoryWriter() + + // get present... + guard WebPConfigPreset(&config, WEBP_PRESET_DEFAULT, quality) != 0, WebPPictureInit(&picture) != 0 else { + hedgeLog("Error initializing WebPPicture") + return nil + } + + withUnsafeMutablePointer(to: &writer) { writerPointer in + picture.use_argb = 1 // Lossy encoding uses YUV for internal bitstream + picture.width = Int32(width) + picture.height = Int32(height) + picture.writer = WebPMemoryWrite + picture.custom_ptr = UnsafeMutableRawPointer(writerPointer) + } + + WebPMemoryWriterInit(&writer) + + defer { + WebPMemoryWriterClear(&writer) + WebPPictureFree(&picture) + } + + let result: Int32 + if hasAlpha { + // RGBA8888 - 4 channels + result = WebPPictureImportRGBA(&picture, rgba.bindMemory(to: UInt8.self, capacity: 4), Int32(bytesPerRow)) + } else { + // RGB888 - 3 channels + result = WebPPictureImportRGB(&picture, rgba.bindMemory(to: UInt8.self, capacity: 3), Int32(bytesPerRow)) + } + + if result == 0 { + hedgeLog("Could not read WebPPicture") + return nil + } + + if WebPEncode(&config, &picture) == 0 { + hedgeLog("Could not encode WebP image") + return nil + } + + let webpData = Data(bytes: writer.mem, count: writer.size) + + return webpData + } + } +#endif diff --git a/Pods/PostHog/PostHog/Utils/UIWindow+.swift b/Pods/PostHog/PostHog/Utils/UIWindow+.swift new file mode 100644 index 0000000..f119708 --- /dev/null +++ b/Pods/PostHog/PostHog/Utils/UIWindow+.swift @@ -0,0 +1,50 @@ +// +// UIWindow+.swift +// PostHog +// +// Created by Yiannis Josephides on 03/12/2024. +// + +#if os(iOS) || os(tvOS) + import Foundation + import UIKit + + /** + Known keyboard window (private) types + + ## UIRemoteKeyboardWindow + This is the window that manages the actual keyboard + + The following system view controllers were observed to be presented in a UIRemoteKeyboardWindow window + - UIInputWindowController + - UICompatibilityInputViewController + - UISystemInputAssistantViewController + - UIPredictionViewController + - UISystemKeyboardDockController + - TUIEmojiSearchInputViewController + - STKPrewarmingViewController + - STKStickerRemoteSearchViewController + - _UIRemoteInputViewController + - _UISceneHostingViewController + - STKEmojiAndStickerCollectionViewController + + ## UITextEffectsWindow + Hosts system components like the magnifying glass for text selection, predictive text suggestions, copy/paste menus, input accessory views etc. + + The following system view controllers were observed to be presented in a UITextEffectsWindow window + - UIInputWindowController + - UICompatibilityInputViewController + + These view controllers should not appear in a $screen event. If they do, then it means that they are presented in a UIWindow not listed below + */ + private let knownKeyboardWindowTypes: [String] = [ + "UIRemoteKeyboardWindow", + "UITextEffectsWindow", + ] + + extension UIWindow { + var isKeyboardWindow: Bool { + knownKeyboardWindowTypes.contains(String(describing: type(of: self))) + } + } +#endif diff --git a/Pods/PostHog/PostHog/Utils/UUIDUtils.swift b/Pods/PostHog/PostHog/Utils/UUIDUtils.swift new file mode 100644 index 0000000..66c3dd2 --- /dev/null +++ b/Pods/PostHog/PostHog/Utils/UUIDUtils.swift @@ -0,0 +1,17 @@ +// +// UUIDUtils.swift +// PostHog +// +// Created by Manoel Aranda Neto on 17.06.24. +// + +// Inspired and adapted from https://github.com/nthState/UUIDV7/blob/main/Sources/UUIDV7/UUIDV7.swift +// but using SecRandomCopyBytes + +import Foundation + +extension UUID { + static func v7() -> Self { + TimeBasedEpochGenerator.shared.v7() + } +} diff --git a/Pods/PostHog/README.md b/Pods/PostHog/README.md new file mode 100644 index 0000000..2be037b --- /dev/null +++ b/Pods/PostHog/README.md @@ -0,0 +1,15 @@ +[![Build](https://img.shields.io/github/actions/workflow/status/PostHog/posthog-ios/build.yml?branch=main)](https://github.com/PostHog/posthog-ios/actions/workflows/build.yml?query=branch%3Amain) +[![CocoaPods compadible](https://img.shields.io/cocoapods/v/PostHog.svg)](https://cocoapods.org/pods/PostHog) +[![SwiftPM compatible](https://img.shields.io/badge/spm-compatible-brightgreen.svg?style=flat)](https://swift.org/package-manager) +![platforms](https://img.shields.io/cocoapods/p/PostHog.svg?style=flat) +[![Swift Package Index](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FPostHog%2Fposthog-ios%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/PostHog/posthog-ios) + +# PostHog iOS + +Please see the main [PostHog docs](https://posthog.com/docs). + +Specifically, the [iOS docs](https://posthog.com/docs/libraries/ios) details. + +## Questions? + +### [Check out our community page.](https://posthog.com/posts) diff --git a/Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests-Info.plist b/Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests-Info.plist new file mode 100644 index 0000000..19cf209 --- /dev/null +++ b/Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests-Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + ${PODS_DEVELOPMENT_LANGUAGE} + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + ${PRODUCT_BUNDLE_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0.0 + CFBundleSignature + ???? + CFBundleVersion + ${CURRENT_PROJECT_VERSION} + NSPrincipalClass + + + diff --git a/Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests-acknowledgements.markdown b/Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests-acknowledgements.markdown new file mode 100644 index 0000000..b81fb24 --- /dev/null +++ b/Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests-acknowledgements.markdown @@ -0,0 +1,28 @@ +# Acknowledgements +This application makes use of the following third party libraries: + +## PostHog + +MIT License + +Copyright (c) [2023] [PostHog] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +Generated by CocoaPods - https://cocoapods.org diff --git a/Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests-acknowledgements.plist b/Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests-acknowledgements.plist new file mode 100644 index 0000000..5daadd9 --- /dev/null +++ b/Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests-acknowledgements.plist @@ -0,0 +1,60 @@ + + + + + PreferenceSpecifiers + + + FooterText + This application makes use of the following third party libraries: + Title + Acknowledgements + Type + PSGroupSpecifier + + + FooterText + MIT License + +Copyright (c) [2023] [PostHog] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + License + MIT + Title + PostHog + Type + PSGroupSpecifier + + + FooterText + Generated by CocoaPods - https://cocoapods.org + Title + + Type + PSGroupSpecifier + + + StringsTable + Acknowledgements + Title + Acknowledgements + + diff --git a/Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests-dummy.m b/Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests-dummy.m new file mode 100644 index 0000000..50e395c --- /dev/null +++ b/Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests-dummy.m @@ -0,0 +1,5 @@ +#import +@interface PodsDummy_Pods_Cable_CableUITests : NSObject +@end +@implementation PodsDummy_Pods_Cable_CableUITests +@end diff --git a/Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests-frameworks-Debug-input-files.xcfilelist b/Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests-frameworks-Debug-input-files.xcfilelist new file mode 100644 index 0000000..f4a8c94 --- /dev/null +++ b/Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests-frameworks-Debug-input-files.xcfilelist @@ -0,0 +1,2 @@ +${PODS_ROOT}/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests-frameworks.sh +${BUILT_PRODUCTS_DIR}/PostHog/PostHog.framework \ No newline at end of file diff --git a/Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests-frameworks-Debug-output-files.xcfilelist b/Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests-frameworks-Debug-output-files.xcfilelist new file mode 100644 index 0000000..d4451ab --- /dev/null +++ b/Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests-frameworks-Debug-output-files.xcfilelist @@ -0,0 +1 @@ +${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PostHog.framework \ No newline at end of file diff --git a/Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests-frameworks-Release-input-files.xcfilelist b/Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests-frameworks-Release-input-files.xcfilelist new file mode 100644 index 0000000..f4a8c94 --- /dev/null +++ b/Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests-frameworks-Release-input-files.xcfilelist @@ -0,0 +1,2 @@ +${PODS_ROOT}/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests-frameworks.sh +${BUILT_PRODUCTS_DIR}/PostHog/PostHog.framework \ No newline at end of file diff --git a/Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests-frameworks-Release-output-files.xcfilelist b/Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests-frameworks-Release-output-files.xcfilelist new file mode 100644 index 0000000..d4451ab --- /dev/null +++ b/Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests-frameworks-Release-output-files.xcfilelist @@ -0,0 +1 @@ +${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PostHog.framework \ No newline at end of file diff --git a/Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests-frameworks.sh b/Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests-frameworks.sh new file mode 100755 index 0000000..3d77bb1 --- /dev/null +++ b/Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests-frameworks.sh @@ -0,0 +1,186 @@ +#!/bin/sh +set -e +set -u +set -o pipefail + +function on_error { + echo "$(realpath -mq "${0}"):$1: error: Unexpected failure" +} +trap 'on_error $LINENO' ERR + +if [ -z ${FRAMEWORKS_FOLDER_PATH+x} ]; then + # If FRAMEWORKS_FOLDER_PATH is not set, then there's nowhere for us to copy + # frameworks to, so exit 0 (signalling the script phase was successful). + exit 0 +fi + +echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" +mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + +COCOAPODS_PARALLEL_CODE_SIGN="${COCOAPODS_PARALLEL_CODE_SIGN:-false}" +SWIFT_STDLIB_PATH="${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" +BCSYMBOLMAP_DIR="BCSymbolMaps" + + +# This protects against multiple targets copying the same framework dependency at the same time. The solution +# was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html +RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????") + +# Copies and strips a vendored framework +install_framework() +{ + if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then + local source="${BUILT_PRODUCTS_DIR}/$1" + elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then + local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")" + elif [ -r "$1" ]; then + local source="$1" + fi + + local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + + if [ -L "${source}" ]; then + echo "Symlinked..." + source="$(readlink -f "${source}")" + fi + + if [ -d "${source}/${BCSYMBOLMAP_DIR}" ]; then + # Locate and install any .bcsymbolmaps if present, and remove them from the .framework before the framework is copied + find "${source}/${BCSYMBOLMAP_DIR}" -name "*.bcsymbolmap"|while read f; do + echo "Installing $f" + install_bcsymbolmap "$f" "$destination" + rm "$f" + done + rmdir "${source}/${BCSYMBOLMAP_DIR}" + fi + + # Use filter instead of exclude so missing patterns don't throw errors. + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\"" + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}" + + local basename + basename="$(basename -s .framework "$1")" + binary="${destination}/${basename}.framework/${basename}" + + if ! [ -r "$binary" ]; then + binary="${destination}/${basename}" + elif [ -L "${binary}" ]; then + echo "Destination binary is symlinked..." + dirname="$(dirname "${binary}")" + binary="${dirname}/$(readlink "${binary}")" + fi + + # Strip invalid architectures so "fat" simulator / device frameworks work on device + if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then + strip_invalid_archs "$binary" + fi + + # Resign the code if required by the build settings to avoid unstable apps + code_sign_if_enabled "${destination}/$(basename "$1")" + + # Embed linked Swift runtime libraries. No longer necessary as of Xcode 7. + if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then + local swift_runtime_libs + swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u) + for lib in $swift_runtime_libs; do + echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\"" + rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}" + code_sign_if_enabled "${destination}/${lib}" + done + fi +} +# Copies and strips a vendored dSYM +install_dsym() { + local source="$1" + warn_missing_arch=${2:-true} + if [ -r "$source" ]; then + # Copy the dSYM into the targets temp dir. + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${DERIVED_FILES_DIR}\"" + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${DERIVED_FILES_DIR}" + + local basename + basename="$(basename -s .dSYM "$source")" + binary_name="$(ls "$source/Contents/Resources/DWARF")" + binary="${DERIVED_FILES_DIR}/${basename}.dSYM/Contents/Resources/DWARF/${binary_name}" + + # Strip invalid architectures from the dSYM. + if [[ "$(file "$binary")" == *"Mach-O "*"dSYM companion"* ]]; then + strip_invalid_archs "$binary" "$warn_missing_arch" + fi + if [[ $STRIP_BINARY_RETVAL == 0 ]]; then + # Move the stripped file into its final destination. + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${DERIVED_FILES_DIR}/${basename}.framework.dSYM\" \"${DWARF_DSYM_FOLDER_PATH}\"" + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${DERIVED_FILES_DIR}/${basename}.dSYM" "${DWARF_DSYM_FOLDER_PATH}" + else + # The dSYM was not stripped at all, in this case touch a fake folder so the input/output paths from Xcode do not reexecute this script because the file is missing. + mkdir -p "${DWARF_DSYM_FOLDER_PATH}" + touch "${DWARF_DSYM_FOLDER_PATH}/${basename}.dSYM" + fi + fi +} + +# Used as a return value for each invocation of `strip_invalid_archs` function. +STRIP_BINARY_RETVAL=0 + +# Strip invalid architectures +strip_invalid_archs() { + binary="$1" + warn_missing_arch=${2:-true} + # Get architectures for current target binary + binary_archs="$(lipo -info "$binary" | rev | cut -d ':' -f1 | awk '{$1=$1;print}' | rev)" + # Intersect them with the architectures we are building for + intersected_archs="$(echo ${ARCHS[@]} ${binary_archs[@]} | tr ' ' '\n' | sort | uniq -d)" + # If there are no archs supported by this binary then warn the user + if [[ -z "$intersected_archs" ]]; then + if [[ "$warn_missing_arch" == "true" ]]; then + echo "warning: [CP] Vendored binary '$binary' contains architectures ($binary_archs) none of which match the current build architectures ($ARCHS)." + fi + STRIP_BINARY_RETVAL=1 + return + fi + stripped="" + for arch in $binary_archs; do + if ! [[ "${ARCHS}" == *"$arch"* ]]; then + # Strip non-valid architectures in-place + lipo -remove "$arch" -output "$binary" "$binary" + stripped="$stripped $arch" + fi + done + if [[ "$stripped" ]]; then + echo "Stripped $binary of architectures:$stripped" + fi + STRIP_BINARY_RETVAL=0 +} + +# Copies the bcsymbolmap files of a vendored framework +install_bcsymbolmap() { + local bcsymbolmap_path="$1" + local destination="${BUILT_PRODUCTS_DIR}" + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}"" + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}" +} + +# Signs a framework with the provided identity +code_sign_if_enabled() { + if [ -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" -a "${CODE_SIGNING_REQUIRED:-}" != "NO" -a "${CODE_SIGNING_ALLOWED}" != "NO" ]; then + # Use the current code_sign_identity + echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}" + local code_sign_cmd="/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS:-} --preserve-metadata=identifier,entitlements '$1'" + + if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then + code_sign_cmd="$code_sign_cmd &" + fi + echo "$code_sign_cmd" + eval "$code_sign_cmd" + fi +} + +if [[ "$CONFIGURATION" == "Debug" ]]; then + install_framework "${BUILT_PRODUCTS_DIR}/PostHog/PostHog.framework" +fi +if [[ "$CONFIGURATION" == "Release" ]]; then + install_framework "${BUILT_PRODUCTS_DIR}/PostHog/PostHog.framework" +fi +if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then + wait +fi diff --git a/Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests-umbrella.h b/Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests-umbrella.h new file mode 100644 index 0000000..265363e --- /dev/null +++ b/Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests-umbrella.h @@ -0,0 +1,16 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + + +FOUNDATION_EXPORT double Pods_Cable_CableUITestsVersionNumber; +FOUNDATION_EXPORT const unsigned char Pods_Cable_CableUITestsVersionString[]; + diff --git a/Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests.debug.xcconfig b/Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests.debug.xcconfig new file mode 100644 index 0000000..976867a --- /dev/null +++ b/Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests.debug.xcconfig @@ -0,0 +1,16 @@ +ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/PostHog" +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/PostHog/PostHog.framework/Headers" +LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift "$(PLATFORM_DIR)/Developer/Library/Frameworks" '@executable_path/Frameworks' '@loader_path/Frameworks' +LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift +OTHER_LDFLAGS = $(inherited) -framework "Foundation" -framework "PostHog" +OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/PostHog" +OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests.modulemap b/Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests.modulemap new file mode 100644 index 0000000..187c319 --- /dev/null +++ b/Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests.modulemap @@ -0,0 +1,6 @@ +framework module Pods_Cable_CableUITests { + umbrella header "Pods-Cable-CableUITests-umbrella.h" + + export * + module * { export * } +} diff --git a/Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests.release.xcconfig b/Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests.release.xcconfig new file mode 100644 index 0000000..976867a --- /dev/null +++ b/Pods/Target Support Files/Pods-Cable-CableUITests/Pods-Cable-CableUITests.release.xcconfig @@ -0,0 +1,16 @@ +ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/PostHog" +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/PostHog/PostHog.framework/Headers" +LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift "$(PLATFORM_DIR)/Developer/Library/Frameworks" '@executable_path/Frameworks' '@loader_path/Frameworks' +LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift +OTHER_LDFLAGS = $(inherited) -framework "Foundation" -framework "PostHog" +OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/PostHog" +OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot-Info.plist b/Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot-Info.plist new file mode 100644 index 0000000..19cf209 --- /dev/null +++ b/Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot-Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + ${PODS_DEVELOPMENT_LANGUAGE} + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + ${PRODUCT_BUNDLE_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0.0 + CFBundleSignature + ???? + CFBundleVersion + ${CURRENT_PROJECT_VERSION} + NSPrincipalClass + + + diff --git a/Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot-acknowledgements.markdown b/Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot-acknowledgements.markdown new file mode 100644 index 0000000..b81fb24 --- /dev/null +++ b/Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot-acknowledgements.markdown @@ -0,0 +1,28 @@ +# Acknowledgements +This application makes use of the following third party libraries: + +## PostHog + +MIT License + +Copyright (c) [2023] [PostHog] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +Generated by CocoaPods - https://cocoapods.org diff --git a/Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot-acknowledgements.plist b/Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot-acknowledgements.plist new file mode 100644 index 0000000..5daadd9 --- /dev/null +++ b/Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot-acknowledgements.plist @@ -0,0 +1,60 @@ + + + + + PreferenceSpecifiers + + + FooterText + This application makes use of the following third party libraries: + Title + Acknowledgements + Type + PSGroupSpecifier + + + FooterText + MIT License + +Copyright (c) [2023] [PostHog] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + License + MIT + Title + PostHog + Type + PSGroupSpecifier + + + FooterText + Generated by CocoaPods - https://cocoapods.org + Title + + Type + PSGroupSpecifier + + + StringsTable + Acknowledgements + Title + Acknowledgements + + diff --git a/Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot-dummy.m b/Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot-dummy.m new file mode 100644 index 0000000..f400376 --- /dev/null +++ b/Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot-dummy.m @@ -0,0 +1,5 @@ +#import +@interface PodsDummy_Pods_Cable_CableUITestsScreenshot : NSObject +@end +@implementation PodsDummy_Pods_Cable_CableUITestsScreenshot +@end diff --git a/Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot-frameworks-Debug-input-files.xcfilelist b/Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot-frameworks-Debug-input-files.xcfilelist new file mode 100644 index 0000000..dd3a87a --- /dev/null +++ b/Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot-frameworks-Debug-input-files.xcfilelist @@ -0,0 +1,2 @@ +${PODS_ROOT}/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot-frameworks.sh +${BUILT_PRODUCTS_DIR}/PostHog/PostHog.framework \ No newline at end of file diff --git a/Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot-frameworks-Debug-output-files.xcfilelist b/Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot-frameworks-Debug-output-files.xcfilelist new file mode 100644 index 0000000..d4451ab --- /dev/null +++ b/Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot-frameworks-Debug-output-files.xcfilelist @@ -0,0 +1 @@ +${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PostHog.framework \ No newline at end of file diff --git a/Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot-frameworks-Release-input-files.xcfilelist b/Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot-frameworks-Release-input-files.xcfilelist new file mode 100644 index 0000000..dd3a87a --- /dev/null +++ b/Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot-frameworks-Release-input-files.xcfilelist @@ -0,0 +1,2 @@ +${PODS_ROOT}/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot-frameworks.sh +${BUILT_PRODUCTS_DIR}/PostHog/PostHog.framework \ No newline at end of file diff --git a/Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot-frameworks-Release-output-files.xcfilelist b/Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot-frameworks-Release-output-files.xcfilelist new file mode 100644 index 0000000..d4451ab --- /dev/null +++ b/Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot-frameworks-Release-output-files.xcfilelist @@ -0,0 +1 @@ +${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PostHog.framework \ No newline at end of file diff --git a/Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot-frameworks.sh b/Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot-frameworks.sh new file mode 100755 index 0000000..3d77bb1 --- /dev/null +++ b/Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot-frameworks.sh @@ -0,0 +1,186 @@ +#!/bin/sh +set -e +set -u +set -o pipefail + +function on_error { + echo "$(realpath -mq "${0}"):$1: error: Unexpected failure" +} +trap 'on_error $LINENO' ERR + +if [ -z ${FRAMEWORKS_FOLDER_PATH+x} ]; then + # If FRAMEWORKS_FOLDER_PATH is not set, then there's nowhere for us to copy + # frameworks to, so exit 0 (signalling the script phase was successful). + exit 0 +fi + +echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" +mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + +COCOAPODS_PARALLEL_CODE_SIGN="${COCOAPODS_PARALLEL_CODE_SIGN:-false}" +SWIFT_STDLIB_PATH="${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" +BCSYMBOLMAP_DIR="BCSymbolMaps" + + +# This protects against multiple targets copying the same framework dependency at the same time. The solution +# was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html +RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????") + +# Copies and strips a vendored framework +install_framework() +{ + if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then + local source="${BUILT_PRODUCTS_DIR}/$1" + elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then + local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")" + elif [ -r "$1" ]; then + local source="$1" + fi + + local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + + if [ -L "${source}" ]; then + echo "Symlinked..." + source="$(readlink -f "${source}")" + fi + + if [ -d "${source}/${BCSYMBOLMAP_DIR}" ]; then + # Locate and install any .bcsymbolmaps if present, and remove them from the .framework before the framework is copied + find "${source}/${BCSYMBOLMAP_DIR}" -name "*.bcsymbolmap"|while read f; do + echo "Installing $f" + install_bcsymbolmap "$f" "$destination" + rm "$f" + done + rmdir "${source}/${BCSYMBOLMAP_DIR}" + fi + + # Use filter instead of exclude so missing patterns don't throw errors. + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\"" + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}" + + local basename + basename="$(basename -s .framework "$1")" + binary="${destination}/${basename}.framework/${basename}" + + if ! [ -r "$binary" ]; then + binary="${destination}/${basename}" + elif [ -L "${binary}" ]; then + echo "Destination binary is symlinked..." + dirname="$(dirname "${binary}")" + binary="${dirname}/$(readlink "${binary}")" + fi + + # Strip invalid architectures so "fat" simulator / device frameworks work on device + if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then + strip_invalid_archs "$binary" + fi + + # Resign the code if required by the build settings to avoid unstable apps + code_sign_if_enabled "${destination}/$(basename "$1")" + + # Embed linked Swift runtime libraries. No longer necessary as of Xcode 7. + if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then + local swift_runtime_libs + swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u) + for lib in $swift_runtime_libs; do + echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\"" + rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}" + code_sign_if_enabled "${destination}/${lib}" + done + fi +} +# Copies and strips a vendored dSYM +install_dsym() { + local source="$1" + warn_missing_arch=${2:-true} + if [ -r "$source" ]; then + # Copy the dSYM into the targets temp dir. + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${DERIVED_FILES_DIR}\"" + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${DERIVED_FILES_DIR}" + + local basename + basename="$(basename -s .dSYM "$source")" + binary_name="$(ls "$source/Contents/Resources/DWARF")" + binary="${DERIVED_FILES_DIR}/${basename}.dSYM/Contents/Resources/DWARF/${binary_name}" + + # Strip invalid architectures from the dSYM. + if [[ "$(file "$binary")" == *"Mach-O "*"dSYM companion"* ]]; then + strip_invalid_archs "$binary" "$warn_missing_arch" + fi + if [[ $STRIP_BINARY_RETVAL == 0 ]]; then + # Move the stripped file into its final destination. + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${DERIVED_FILES_DIR}/${basename}.framework.dSYM\" \"${DWARF_DSYM_FOLDER_PATH}\"" + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${DERIVED_FILES_DIR}/${basename}.dSYM" "${DWARF_DSYM_FOLDER_PATH}" + else + # The dSYM was not stripped at all, in this case touch a fake folder so the input/output paths from Xcode do not reexecute this script because the file is missing. + mkdir -p "${DWARF_DSYM_FOLDER_PATH}" + touch "${DWARF_DSYM_FOLDER_PATH}/${basename}.dSYM" + fi + fi +} + +# Used as a return value for each invocation of `strip_invalid_archs` function. +STRIP_BINARY_RETVAL=0 + +# Strip invalid architectures +strip_invalid_archs() { + binary="$1" + warn_missing_arch=${2:-true} + # Get architectures for current target binary + binary_archs="$(lipo -info "$binary" | rev | cut -d ':' -f1 | awk '{$1=$1;print}' | rev)" + # Intersect them with the architectures we are building for + intersected_archs="$(echo ${ARCHS[@]} ${binary_archs[@]} | tr ' ' '\n' | sort | uniq -d)" + # If there are no archs supported by this binary then warn the user + if [[ -z "$intersected_archs" ]]; then + if [[ "$warn_missing_arch" == "true" ]]; then + echo "warning: [CP] Vendored binary '$binary' contains architectures ($binary_archs) none of which match the current build architectures ($ARCHS)." + fi + STRIP_BINARY_RETVAL=1 + return + fi + stripped="" + for arch in $binary_archs; do + if ! [[ "${ARCHS}" == *"$arch"* ]]; then + # Strip non-valid architectures in-place + lipo -remove "$arch" -output "$binary" "$binary" + stripped="$stripped $arch" + fi + done + if [[ "$stripped" ]]; then + echo "Stripped $binary of architectures:$stripped" + fi + STRIP_BINARY_RETVAL=0 +} + +# Copies the bcsymbolmap files of a vendored framework +install_bcsymbolmap() { + local bcsymbolmap_path="$1" + local destination="${BUILT_PRODUCTS_DIR}" + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}"" + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}" +} + +# Signs a framework with the provided identity +code_sign_if_enabled() { + if [ -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" -a "${CODE_SIGNING_REQUIRED:-}" != "NO" -a "${CODE_SIGNING_ALLOWED}" != "NO" ]; then + # Use the current code_sign_identity + echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}" + local code_sign_cmd="/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS:-} --preserve-metadata=identifier,entitlements '$1'" + + if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then + code_sign_cmd="$code_sign_cmd &" + fi + echo "$code_sign_cmd" + eval "$code_sign_cmd" + fi +} + +if [[ "$CONFIGURATION" == "Debug" ]]; then + install_framework "${BUILT_PRODUCTS_DIR}/PostHog/PostHog.framework" +fi +if [[ "$CONFIGURATION" == "Release" ]]; then + install_framework "${BUILT_PRODUCTS_DIR}/PostHog/PostHog.framework" +fi +if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then + wait +fi diff --git a/Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot-umbrella.h b/Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot-umbrella.h new file mode 100644 index 0000000..b06e7e5 --- /dev/null +++ b/Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot-umbrella.h @@ -0,0 +1,16 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + + +FOUNDATION_EXPORT double Pods_Cable_CableUITestsScreenshotVersionNumber; +FOUNDATION_EXPORT const unsigned char Pods_Cable_CableUITestsScreenshotVersionString[]; + diff --git a/Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot.debug.xcconfig b/Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot.debug.xcconfig new file mode 100644 index 0000000..976867a --- /dev/null +++ b/Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot.debug.xcconfig @@ -0,0 +1,16 @@ +ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/PostHog" +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/PostHog/PostHog.framework/Headers" +LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift "$(PLATFORM_DIR)/Developer/Library/Frameworks" '@executable_path/Frameworks' '@loader_path/Frameworks' +LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift +OTHER_LDFLAGS = $(inherited) -framework "Foundation" -framework "PostHog" +OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/PostHog" +OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot.modulemap b/Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot.modulemap new file mode 100644 index 0000000..6c55d28 --- /dev/null +++ b/Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot.modulemap @@ -0,0 +1,6 @@ +framework module Pods_Cable_CableUITestsScreenshot { + umbrella header "Pods-Cable-CableUITestsScreenshot-umbrella.h" + + export * + module * { export * } +} diff --git a/Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot.release.xcconfig b/Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot.release.xcconfig new file mode 100644 index 0000000..976867a --- /dev/null +++ b/Pods/Target Support Files/Pods-Cable-CableUITestsScreenshot/Pods-Cable-CableUITestsScreenshot.release.xcconfig @@ -0,0 +1,16 @@ +ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/PostHog" +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/PostHog/PostHog.framework/Headers" +LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift "$(PLATFORM_DIR)/Developer/Library/Frameworks" '@executable_path/Frameworks' '@loader_path/Frameworks' +LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift +OTHER_LDFLAGS = $(inherited) -framework "Foundation" -framework "PostHog" +OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/PostHog" +OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/Pods/Target Support Files/Pods-Cable/Pods-Cable-Info.plist b/Pods/Target Support Files/Pods-Cable/Pods-Cable-Info.plist new file mode 100644 index 0000000..19cf209 --- /dev/null +++ b/Pods/Target Support Files/Pods-Cable/Pods-Cable-Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + ${PODS_DEVELOPMENT_LANGUAGE} + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + ${PRODUCT_BUNDLE_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0.0 + CFBundleSignature + ???? + CFBundleVersion + ${CURRENT_PROJECT_VERSION} + NSPrincipalClass + + + diff --git a/Pods/Target Support Files/Pods-Cable/Pods-Cable-acknowledgements.markdown b/Pods/Target Support Files/Pods-Cable/Pods-Cable-acknowledgements.markdown new file mode 100644 index 0000000..b81fb24 --- /dev/null +++ b/Pods/Target Support Files/Pods-Cable/Pods-Cable-acknowledgements.markdown @@ -0,0 +1,28 @@ +# Acknowledgements +This application makes use of the following third party libraries: + +## PostHog + +MIT License + +Copyright (c) [2023] [PostHog] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +Generated by CocoaPods - https://cocoapods.org diff --git a/Pods/Target Support Files/Pods-Cable/Pods-Cable-acknowledgements.plist b/Pods/Target Support Files/Pods-Cable/Pods-Cable-acknowledgements.plist new file mode 100644 index 0000000..5daadd9 --- /dev/null +++ b/Pods/Target Support Files/Pods-Cable/Pods-Cable-acknowledgements.plist @@ -0,0 +1,60 @@ + + + + + PreferenceSpecifiers + + + FooterText + This application makes use of the following third party libraries: + Title + Acknowledgements + Type + PSGroupSpecifier + + + FooterText + MIT License + +Copyright (c) [2023] [PostHog] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + License + MIT + Title + PostHog + Type + PSGroupSpecifier + + + FooterText + Generated by CocoaPods - https://cocoapods.org + Title + + Type + PSGroupSpecifier + + + StringsTable + Acknowledgements + Title + Acknowledgements + + diff --git a/Pods/Target Support Files/Pods-Cable/Pods-Cable-dummy.m b/Pods/Target Support Files/Pods-Cable/Pods-Cable-dummy.m new file mode 100644 index 0000000..c10074c --- /dev/null +++ b/Pods/Target Support Files/Pods-Cable/Pods-Cable-dummy.m @@ -0,0 +1,5 @@ +#import +@interface PodsDummy_Pods_Cable : NSObject +@end +@implementation PodsDummy_Pods_Cable +@end diff --git a/Pods/Target Support Files/Pods-Cable/Pods-Cable-frameworks-Debug-input-files.xcfilelist b/Pods/Target Support Files/Pods-Cable/Pods-Cable-frameworks-Debug-input-files.xcfilelist new file mode 100644 index 0000000..74e1f97 --- /dev/null +++ b/Pods/Target Support Files/Pods-Cable/Pods-Cable-frameworks-Debug-input-files.xcfilelist @@ -0,0 +1,2 @@ +${PODS_ROOT}/Target Support Files/Pods-Cable/Pods-Cable-frameworks.sh +${BUILT_PRODUCTS_DIR}/PostHog/PostHog.framework \ No newline at end of file diff --git a/Pods/Target Support Files/Pods-Cable/Pods-Cable-frameworks-Debug-output-files.xcfilelist b/Pods/Target Support Files/Pods-Cable/Pods-Cable-frameworks-Debug-output-files.xcfilelist new file mode 100644 index 0000000..d4451ab --- /dev/null +++ b/Pods/Target Support Files/Pods-Cable/Pods-Cable-frameworks-Debug-output-files.xcfilelist @@ -0,0 +1 @@ +${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PostHog.framework \ No newline at end of file diff --git a/Pods/Target Support Files/Pods-Cable/Pods-Cable-frameworks-Release-input-files.xcfilelist b/Pods/Target Support Files/Pods-Cable/Pods-Cable-frameworks-Release-input-files.xcfilelist new file mode 100644 index 0000000..74e1f97 --- /dev/null +++ b/Pods/Target Support Files/Pods-Cable/Pods-Cable-frameworks-Release-input-files.xcfilelist @@ -0,0 +1,2 @@ +${PODS_ROOT}/Target Support Files/Pods-Cable/Pods-Cable-frameworks.sh +${BUILT_PRODUCTS_DIR}/PostHog/PostHog.framework \ No newline at end of file diff --git a/Pods/Target Support Files/Pods-Cable/Pods-Cable-frameworks-Release-output-files.xcfilelist b/Pods/Target Support Files/Pods-Cable/Pods-Cable-frameworks-Release-output-files.xcfilelist new file mode 100644 index 0000000..d4451ab --- /dev/null +++ b/Pods/Target Support Files/Pods-Cable/Pods-Cable-frameworks-Release-output-files.xcfilelist @@ -0,0 +1 @@ +${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PostHog.framework \ No newline at end of file diff --git a/Pods/Target Support Files/Pods-Cable/Pods-Cable-frameworks.sh b/Pods/Target Support Files/Pods-Cable/Pods-Cable-frameworks.sh new file mode 100755 index 0000000..3d77bb1 --- /dev/null +++ b/Pods/Target Support Files/Pods-Cable/Pods-Cable-frameworks.sh @@ -0,0 +1,186 @@ +#!/bin/sh +set -e +set -u +set -o pipefail + +function on_error { + echo "$(realpath -mq "${0}"):$1: error: Unexpected failure" +} +trap 'on_error $LINENO' ERR + +if [ -z ${FRAMEWORKS_FOLDER_PATH+x} ]; then + # If FRAMEWORKS_FOLDER_PATH is not set, then there's nowhere for us to copy + # frameworks to, so exit 0 (signalling the script phase was successful). + exit 0 +fi + +echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" +mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + +COCOAPODS_PARALLEL_CODE_SIGN="${COCOAPODS_PARALLEL_CODE_SIGN:-false}" +SWIFT_STDLIB_PATH="${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" +BCSYMBOLMAP_DIR="BCSymbolMaps" + + +# This protects against multiple targets copying the same framework dependency at the same time. The solution +# was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html +RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????") + +# Copies and strips a vendored framework +install_framework() +{ + if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then + local source="${BUILT_PRODUCTS_DIR}/$1" + elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then + local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")" + elif [ -r "$1" ]; then + local source="$1" + fi + + local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + + if [ -L "${source}" ]; then + echo "Symlinked..." + source="$(readlink -f "${source}")" + fi + + if [ -d "${source}/${BCSYMBOLMAP_DIR}" ]; then + # Locate and install any .bcsymbolmaps if present, and remove them from the .framework before the framework is copied + find "${source}/${BCSYMBOLMAP_DIR}" -name "*.bcsymbolmap"|while read f; do + echo "Installing $f" + install_bcsymbolmap "$f" "$destination" + rm "$f" + done + rmdir "${source}/${BCSYMBOLMAP_DIR}" + fi + + # Use filter instead of exclude so missing patterns don't throw errors. + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\"" + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}" + + local basename + basename="$(basename -s .framework "$1")" + binary="${destination}/${basename}.framework/${basename}" + + if ! [ -r "$binary" ]; then + binary="${destination}/${basename}" + elif [ -L "${binary}" ]; then + echo "Destination binary is symlinked..." + dirname="$(dirname "${binary}")" + binary="${dirname}/$(readlink "${binary}")" + fi + + # Strip invalid architectures so "fat" simulator / device frameworks work on device + if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then + strip_invalid_archs "$binary" + fi + + # Resign the code if required by the build settings to avoid unstable apps + code_sign_if_enabled "${destination}/$(basename "$1")" + + # Embed linked Swift runtime libraries. No longer necessary as of Xcode 7. + if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then + local swift_runtime_libs + swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u) + for lib in $swift_runtime_libs; do + echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\"" + rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}" + code_sign_if_enabled "${destination}/${lib}" + done + fi +} +# Copies and strips a vendored dSYM +install_dsym() { + local source="$1" + warn_missing_arch=${2:-true} + if [ -r "$source" ]; then + # Copy the dSYM into the targets temp dir. + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${DERIVED_FILES_DIR}\"" + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${DERIVED_FILES_DIR}" + + local basename + basename="$(basename -s .dSYM "$source")" + binary_name="$(ls "$source/Contents/Resources/DWARF")" + binary="${DERIVED_FILES_DIR}/${basename}.dSYM/Contents/Resources/DWARF/${binary_name}" + + # Strip invalid architectures from the dSYM. + if [[ "$(file "$binary")" == *"Mach-O "*"dSYM companion"* ]]; then + strip_invalid_archs "$binary" "$warn_missing_arch" + fi + if [[ $STRIP_BINARY_RETVAL == 0 ]]; then + # Move the stripped file into its final destination. + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${DERIVED_FILES_DIR}/${basename}.framework.dSYM\" \"${DWARF_DSYM_FOLDER_PATH}\"" + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${DERIVED_FILES_DIR}/${basename}.dSYM" "${DWARF_DSYM_FOLDER_PATH}" + else + # The dSYM was not stripped at all, in this case touch a fake folder so the input/output paths from Xcode do not reexecute this script because the file is missing. + mkdir -p "${DWARF_DSYM_FOLDER_PATH}" + touch "${DWARF_DSYM_FOLDER_PATH}/${basename}.dSYM" + fi + fi +} + +# Used as a return value for each invocation of `strip_invalid_archs` function. +STRIP_BINARY_RETVAL=0 + +# Strip invalid architectures +strip_invalid_archs() { + binary="$1" + warn_missing_arch=${2:-true} + # Get architectures for current target binary + binary_archs="$(lipo -info "$binary" | rev | cut -d ':' -f1 | awk '{$1=$1;print}' | rev)" + # Intersect them with the architectures we are building for + intersected_archs="$(echo ${ARCHS[@]} ${binary_archs[@]} | tr ' ' '\n' | sort | uniq -d)" + # If there are no archs supported by this binary then warn the user + if [[ -z "$intersected_archs" ]]; then + if [[ "$warn_missing_arch" == "true" ]]; then + echo "warning: [CP] Vendored binary '$binary' contains architectures ($binary_archs) none of which match the current build architectures ($ARCHS)." + fi + STRIP_BINARY_RETVAL=1 + return + fi + stripped="" + for arch in $binary_archs; do + if ! [[ "${ARCHS}" == *"$arch"* ]]; then + # Strip non-valid architectures in-place + lipo -remove "$arch" -output "$binary" "$binary" + stripped="$stripped $arch" + fi + done + if [[ "$stripped" ]]; then + echo "Stripped $binary of architectures:$stripped" + fi + STRIP_BINARY_RETVAL=0 +} + +# Copies the bcsymbolmap files of a vendored framework +install_bcsymbolmap() { + local bcsymbolmap_path="$1" + local destination="${BUILT_PRODUCTS_DIR}" + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}"" + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}" +} + +# Signs a framework with the provided identity +code_sign_if_enabled() { + if [ -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" -a "${CODE_SIGNING_REQUIRED:-}" != "NO" -a "${CODE_SIGNING_ALLOWED}" != "NO" ]; then + # Use the current code_sign_identity + echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}" + local code_sign_cmd="/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS:-} --preserve-metadata=identifier,entitlements '$1'" + + if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then + code_sign_cmd="$code_sign_cmd &" + fi + echo "$code_sign_cmd" + eval "$code_sign_cmd" + fi +} + +if [[ "$CONFIGURATION" == "Debug" ]]; then + install_framework "${BUILT_PRODUCTS_DIR}/PostHog/PostHog.framework" +fi +if [[ "$CONFIGURATION" == "Release" ]]; then + install_framework "${BUILT_PRODUCTS_DIR}/PostHog/PostHog.framework" +fi +if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then + wait +fi diff --git a/Pods/Target Support Files/Pods-Cable/Pods-Cable-umbrella.h b/Pods/Target Support Files/Pods-Cable/Pods-Cable-umbrella.h new file mode 100644 index 0000000..ec384fd --- /dev/null +++ b/Pods/Target Support Files/Pods-Cable/Pods-Cable-umbrella.h @@ -0,0 +1,16 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + + +FOUNDATION_EXPORT double Pods_CableVersionNumber; +FOUNDATION_EXPORT const unsigned char Pods_CableVersionString[]; + diff --git a/Pods/Target Support Files/Pods-Cable/Pods-Cable.debug.xcconfig b/Pods/Target Support Files/Pods-Cable/Pods-Cable.debug.xcconfig new file mode 100644 index 0000000..3c85c0d --- /dev/null +++ b/Pods/Target Support Files/Pods-Cable/Pods-Cable.debug.xcconfig @@ -0,0 +1,16 @@ +ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/PostHog" +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/PostHog/PostHog.framework/Headers" +LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/Frameworks' '@loader_path/Frameworks' +LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift +OTHER_LDFLAGS = $(inherited) -framework "Foundation" -framework "PostHog" +OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/PostHog" +OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/Pods/Target Support Files/Pods-Cable/Pods-Cable.modulemap b/Pods/Target Support Files/Pods-Cable/Pods-Cable.modulemap new file mode 100644 index 0000000..006ee7c --- /dev/null +++ b/Pods/Target Support Files/Pods-Cable/Pods-Cable.modulemap @@ -0,0 +1,6 @@ +framework module Pods_Cable { + umbrella header "Pods-Cable-umbrella.h" + + export * + module * { export * } +} diff --git a/Pods/Target Support Files/Pods-Cable/Pods-Cable.release.xcconfig b/Pods/Target Support Files/Pods-Cable/Pods-Cable.release.xcconfig new file mode 100644 index 0000000..3c85c0d --- /dev/null +++ b/Pods/Target Support Files/Pods-Cable/Pods-Cable.release.xcconfig @@ -0,0 +1,16 @@ +ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/PostHog" +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/PostHog/PostHog.framework/Headers" +LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/Frameworks' '@loader_path/Frameworks' +LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift +OTHER_LDFLAGS = $(inherited) -framework "Foundation" -framework "PostHog" +OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/PostHog" +OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/Pods/Target Support Files/Pods-CableTests/Pods-CableTests-Info.plist b/Pods/Target Support Files/Pods-CableTests/Pods-CableTests-Info.plist new file mode 100644 index 0000000..19cf209 --- /dev/null +++ b/Pods/Target Support Files/Pods-CableTests/Pods-CableTests-Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + ${PODS_DEVELOPMENT_LANGUAGE} + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + ${PRODUCT_BUNDLE_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0.0 + CFBundleSignature + ???? + CFBundleVersion + ${CURRENT_PROJECT_VERSION} + NSPrincipalClass + + + diff --git a/Pods/Target Support Files/Pods-CableTests/Pods-CableTests-acknowledgements.markdown b/Pods/Target Support Files/Pods-CableTests/Pods-CableTests-acknowledgements.markdown new file mode 100644 index 0000000..102af75 --- /dev/null +++ b/Pods/Target Support Files/Pods-CableTests/Pods-CableTests-acknowledgements.markdown @@ -0,0 +1,3 @@ +# Acknowledgements +This application makes use of the following third party libraries: +Generated by CocoaPods - https://cocoapods.org diff --git a/Pods/Target Support Files/Pods-CableTests/Pods-CableTests-acknowledgements.plist b/Pods/Target Support Files/Pods-CableTests/Pods-CableTests-acknowledgements.plist new file mode 100644 index 0000000..7acbad1 --- /dev/null +++ b/Pods/Target Support Files/Pods-CableTests/Pods-CableTests-acknowledgements.plist @@ -0,0 +1,29 @@ + + + + + PreferenceSpecifiers + + + FooterText + This application makes use of the following third party libraries: + Title + Acknowledgements + Type + PSGroupSpecifier + + + FooterText + Generated by CocoaPods - https://cocoapods.org + Title + + Type + PSGroupSpecifier + + + StringsTable + Acknowledgements + Title + Acknowledgements + + diff --git a/Pods/Target Support Files/Pods-CableTests/Pods-CableTests-dummy.m b/Pods/Target Support Files/Pods-CableTests/Pods-CableTests-dummy.m new file mode 100644 index 0000000..f0988bc --- /dev/null +++ b/Pods/Target Support Files/Pods-CableTests/Pods-CableTests-dummy.m @@ -0,0 +1,5 @@ +#import +@interface PodsDummy_Pods_CableTests : NSObject +@end +@implementation PodsDummy_Pods_CableTests +@end diff --git a/Pods/Target Support Files/Pods-CableTests/Pods-CableTests-umbrella.h b/Pods/Target Support Files/Pods-CableTests/Pods-CableTests-umbrella.h new file mode 100644 index 0000000..cd71761 --- /dev/null +++ b/Pods/Target Support Files/Pods-CableTests/Pods-CableTests-umbrella.h @@ -0,0 +1,16 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + + +FOUNDATION_EXPORT double Pods_CableTestsVersionNumber; +FOUNDATION_EXPORT const unsigned char Pods_CableTestsVersionString[]; + diff --git a/Pods/Target Support Files/Pods-CableTests/Pods-CableTests.debug.xcconfig b/Pods/Target Support Files/Pods-CableTests/Pods-CableTests.debug.xcconfig new file mode 100644 index 0000000..990bb22 --- /dev/null +++ b/Pods/Target Support Files/Pods-CableTests/Pods-CableTests.debug.xcconfig @@ -0,0 +1,11 @@ +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/PostHog" +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/PostHog/PostHog.framework/Headers" +OTHER_LDFLAGS = $(inherited) -framework "Foundation" -framework "PostHog" +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/Pods/Target Support Files/Pods-CableTests/Pods-CableTests.modulemap b/Pods/Target Support Files/Pods-CableTests/Pods-CableTests.modulemap new file mode 100644 index 0000000..cc996e3 --- /dev/null +++ b/Pods/Target Support Files/Pods-CableTests/Pods-CableTests.modulemap @@ -0,0 +1,6 @@ +framework module Pods_CableTests { + umbrella header "Pods-CableTests-umbrella.h" + + export * + module * { export * } +} diff --git a/Pods/Target Support Files/Pods-CableTests/Pods-CableTests.release.xcconfig b/Pods/Target Support Files/Pods-CableTests/Pods-CableTests.release.xcconfig new file mode 100644 index 0000000..990bb22 --- /dev/null +++ b/Pods/Target Support Files/Pods-CableTests/Pods-CableTests.release.xcconfig @@ -0,0 +1,11 @@ +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/PostHog" +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/PostHog/PostHog.framework/Headers" +OTHER_LDFLAGS = $(inherited) -framework "Foundation" -framework "PostHog" +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/Pods/Target Support Files/PostHog/PostHog-Info.plist b/Pods/Target Support Files/PostHog/PostHog-Info.plist new file mode 100644 index 0000000..879a061 --- /dev/null +++ b/Pods/Target Support Files/PostHog/PostHog-Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + ${PODS_DEVELOPMENT_LANGUAGE} + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + ${PRODUCT_BUNDLE_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + FMWK + CFBundleShortVersionString + 3.34.0 + CFBundleSignature + ???? + CFBundleVersion + ${CURRENT_PROJECT_VERSION} + NSPrincipalClass + + + diff --git a/Pods/Target Support Files/PostHog/PostHog-dummy.m b/Pods/Target Support Files/PostHog/PostHog-dummy.m new file mode 100644 index 0000000..d35e138 --- /dev/null +++ b/Pods/Target Support Files/PostHog/PostHog-dummy.m @@ -0,0 +1,5 @@ +#import +@interface PodsDummy_PostHog : NSObject +@end +@implementation PodsDummy_PostHog +@end diff --git a/Pods/Target Support Files/PostHog/PostHog-prefix.pch b/Pods/Target Support Files/PostHog/PostHog-prefix.pch new file mode 100644 index 0000000..beb2a24 --- /dev/null +++ b/Pods/Target Support Files/PostHog/PostHog-prefix.pch @@ -0,0 +1,12 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + diff --git a/Pods/Target Support Files/PostHog/PostHog-umbrella.h b/Pods/Target Support Files/PostHog/PostHog-umbrella.h new file mode 100644 index 0000000..4e7c5bc --- /dev/null +++ b/Pods/Target Support Files/PostHog/PostHog-umbrella.h @@ -0,0 +1,61 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + +#import "PostHog.h" +#import "ph_backward_references_enc.h" +#import "ph_bit_reader_utils.h" +#import "ph_bit_writer_utils.h" +#import "ph_color_cache_utils.h" +#import "ph_common_dec.h" +#import "ph_common_sse2.h" +#import "ph_common_sse41.h" +#import "ph_cost_enc.h" +#import "ph_cpu.h" +#import "ph_decode.h" +#import "ph_dsp.h" +#import "ph_encode.h" +#import "ph_endian_inl_utils.h" +#import "ph_filters_utils.h" +#import "ph_format_constants.h" +#import "ph_histogram_enc.h" +#import "ph_huffman_encode_utils.h" +#import "ph_huffman_utils.h" +#import "ph_lossless.h" +#import "ph_lossless_common.h" +#import "ph_mux.h" +#import "ph_muxi.h" +#import "ph_mux_types.h" +#import "ph_neon.h" +#import "ph_palette.h" +#import "ph_quant.h" +#import "ph_quant_levels_utils.h" +#import "ph_random_utils.h" +#import "ph_rescaler_utils.h" +#import "ph_sharpyuv.h" +#import "ph_sharpyuv_cpu.h" +#import "ph_sharpyuv_csp.h" +#import "ph_sharpyuv_dsp.h" +#import "ph_sharpyuv_gamma.h" +#import "ph_thread_utils.h" +#import "ph_types.h" +#import "ph_utils.h" +#import "ph_vp8i_dec.h" +#import "ph_vp8i_enc.h" +#import "ph_vp8li_dec.h" +#import "ph_vp8li_enc.h" +#import "ph_vp8_dec.h" +#import "ph_webpi_dec.h" +#import "ph_yuv.h" + +FOUNDATION_EXPORT double PostHogVersionNumber; +FOUNDATION_EXPORT const unsigned char PostHogVersionString[]; + diff --git a/Pods/Target Support Files/PostHog/PostHog.debug.xcconfig b/Pods/Target Support Files/PostHog/PostHog.debug.xcconfig new file mode 100644 index 0000000..6de2d78 --- /dev/null +++ b/Pods/Target Support Files/PostHog/PostHog.debug.xcconfig @@ -0,0 +1,15 @@ +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/PostHog +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift +OTHER_LDFLAGS = $(inherited) -framework "Foundation" +OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} +PODS_ROOT = ${SRCROOT} +PODS_TARGET_SRCROOT = ${PODS_ROOT}/PostHog +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} +SKIP_INSTALL = YES +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/Pods/Target Support Files/PostHog/PostHog.modulemap b/Pods/Target Support Files/PostHog/PostHog.modulemap new file mode 100644 index 0000000..6657cd0 --- /dev/null +++ b/Pods/Target Support Files/PostHog/PostHog.modulemap @@ -0,0 +1,6 @@ +framework module PostHog { + umbrella header "PostHog-umbrella.h" + + export * + module * { export * } +} diff --git a/Pods/Target Support Files/PostHog/PostHog.release.xcconfig b/Pods/Target Support Files/PostHog/PostHog.release.xcconfig new file mode 100644 index 0000000..6de2d78 --- /dev/null +++ b/Pods/Target Support Files/PostHog/PostHog.release.xcconfig @@ -0,0 +1,15 @@ +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/PostHog +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift +OTHER_LDFLAGS = $(inherited) -framework "Foundation" +OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} +PODS_ROOT = ${SRCROOT} +PODS_TARGET_SRCROOT = ${PODS_ROOT}/PostHog +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} +SKIP_INSTALL = YES +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/Pods/Target Support Files/PostHog/ResourceBundle-PostHog-PostHog-Info.plist b/Pods/Target Support Files/PostHog/ResourceBundle-PostHog-PostHog-Info.plist new file mode 100644 index 0000000..bdc424a --- /dev/null +++ b/Pods/Target Support Files/PostHog/ResourceBundle-PostHog-PostHog-Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + ${PODS_DEVELOPMENT_LANGUAGE} + CFBundleIdentifier + ${PRODUCT_BUNDLE_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + BNDL + CFBundleShortVersionString + 3.34.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + NSPrincipalClass + + +