From c0da34c52898a1d354576da0d18f049da9870add Mon Sep 17 00:00:00 2001 From: Delirium Date: Fri, 10 Nov 2023 10:32:55 +0100 Subject: [PATCH] feat(ios): add Ios source (#1640) * feat: add ios and rn source * fix(ios): remove testing keys * fix(tracker): change default path --- tracker/tracker-ios/.gitignore | 3 + tracker/tracker-ios/LICENSE.md | 44 ++ tracker/tracker-ios/ORTracker.podspec | 14 + tracker/tracker-ios/Package.resolved | 32 + tracker/tracker-ios/Package.swift | 38 + tracker/tracker-ios/Podfile | 8 + tracker/tracker-ios/README.md | 89 +++ .../Sources/ORTracker/Extensions/Data.swift | 230 ++++++ .../Sources/ORTracker/Extensions/Date.swift | 17 + .../Sources/ORTracker/Extensions/String.swift | 10 + .../ORTracker/Extensions/UIImage.swift | 24 + .../Sources/ORTracker/Extensions/UIView.swift | 26 + .../ORTracker/Listeners/Analytics.swift | 288 ++++++++ .../Sources/ORTracker/Listeners/Crash.swift | 64 ++ .../ORTracker/Listeners/LogsListener.swift | 47 ++ .../ORTracker/Listeners/NetworkListener.swift | 150 ++++ .../Listeners/PerformanceListener.swift | 177 +++++ .../ORTracker/Managers/DebugUtils.swift | 16 + .../ORTracker/Managers/MessageCollector.swift | 168 +++++ .../ORTracker/Managers/NetworkManager.swift | 195 +++++ .../ORTracker/Managers/ORUserDefaults.swift | 33 + .../ORTracker/Managers/Swizzling.swift | 22 + .../Sources/ORTracker/Models/ORMessage.swift | 42 ++ .../Sources/ORTracker/Models/OROptions.swift | 27 + .../Sources/ORTracker/Models/ORRecord.swift | 9 + .../Sources/ORTracker/Models/ORSession.swift | 84 +++ .../ORTracker/Models/script/ORMessages.swift | 499 +++++++++++++ .../ORTracker/Models/script/messages.erb | 37 + .../ORTracker/Models/script/messages.rb | 129 ++++ .../Sources/ORTracker/Models/script/run.rb | 114 +++ .../Sources/ORTracker/ORTracker.swift | 162 +++++ .../ScreenCapture/ScreenCapture.swift | 286 ++++++++ .../Tests/ORTrackerTests/ORTrackerTests.swift | 11 + tracker/tracker-reactnative/.editorconfig | 15 + tracker/tracker-reactnative/.gitignore | 71 ++ tracker/tracker-reactnative/.nvmrc | 1 + tracker/tracker-reactnative/.watchmanconfig | 1 + tracker/tracker-reactnative/.yarnrc | 3 + .../tracker-reactnative/CODE_OF_CONDUCT.md | 133 ++++ tracker/tracker-reactnative/CONTRIBUTING.md | 114 +++ tracker/tracker-reactnative/LICENSE | 44 ++ tracker/tracker-reactnative/README.md | 32 + .../tracker-reactnative/android/build.gradle | 108 +++ .../android/gradle.properties | 5 + .../android/src/main/AndroidManifest.xml | 3 + .../android/src/main/AndroidManifestNew.xml | 2 + .../java/com/rntracker/RntrackerPackage.kt | 17 + .../com/rntracker/RntrackerViewManager.kt | 20 + tracker/tracker-reactnative/babel.config.js | 3 + .../example/.bundle/config | 2 + .../tracker-reactnative/example/.gitignore | 1 + .../example/.watchmanconfig | 1 + tracker/tracker-reactnative/example/Gemfile | 6 + tracker/tracker-reactnative/example/README.md | 79 ++ .../example/android/app/build.gradle | 123 ++++ .../example/android/app/debug.keystore | Bin 0 -> 2257 bytes .../example/android/app/proguard-rules.pro | 10 + .../android/app/src/debug/AndroidManifest.xml | 13 + .../rntrackerexample/ReactNativeFlipper.java | 75 ++ .../android/app/src/main/AndroidManifest.xml | 25 + .../com/rntrackerexample/MainActivity.java | 32 + .../com/rntrackerexample/MainApplication.java | 62 ++ .../res/drawable/rn_edit_text_material.xml | 36 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3056 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 5024 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2096 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2858 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4569 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 7098 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 6464 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 10676 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 9250 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 15523 bytes .../app/src/main/res/values/strings.xml | 3 + .../app/src/main/res/values/styles.xml | 9 + .../rntrackerexample/ReactNativeFlipper.java | 20 + .../example/android/build.gradle | 21 + .../example/android/gradle.properties | 44 ++ .../android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 61574 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + .../example/android/gradlew | 244 +++++++ .../example/android/gradlew.bat | 92 +++ .../example/android/settings.gradle | 4 + tracker/tracker-reactnative/example/app.json | 4 + .../example/babel.config.js | 17 + tracker/tracker-reactnative/example/index.js | 5 + .../example/ios/File.swift | 6 + .../tracker-reactnative/example/ios/Podfile | 66 ++ .../example/ios/Podfile.lock | 671 +++++++++++++++++ .../ios/RntrackerExample-Bridging-Header.h | 3 + .../project.pbxproj | 686 ++++++++++++++++++ .../xcschemes/RntrackerExample.xcscheme | 88 +++ .../contents.xcworkspacedata | 10 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../ios/RntrackerExample/AppDelegate.h | 6 + .../ios/RntrackerExample/AppDelegate.mm | 26 + .../AppIcon.appiconset/Contents.json | 53 ++ .../Images.xcassets/Contents.json | 6 + .../example/ios/RntrackerExample/Info.plist | 55 ++ .../RntrackerExample/LaunchScreen.storyboard | 47 ++ .../example/ios/RntrackerExample/main.m | 10 + .../ios/RntrackerExampleTests/Info.plist | 24 + .../RntrackerExampleTests.m | 66 ++ .../example/jest.config.js | 3 + .../example/metro.config.js | 45 ++ .../tracker-reactnative/example/package.json | 26 + .../example/react-native.config.js | 10 + .../tracker-reactnative/example/src/App.tsx | 77 ++ .../ios/Rntracker-Bridging-Header.h | 2 + .../ios/Rntracker.xcodeproj/project.pbxproj | 283 ++++++++ .../ios/RntrackerConnector-Bridging-Header.h | 1 + .../ios/RntrackerConnector.m | 12 + .../ios/RntrackerConnector.swift | 67 ++ .../ios/RntrackerInputManager.m | 4 + .../ios/RntrackerInputManager.swift | 24 + .../ios/RntrackerSanitizedViewManager.m | 6 + .../ios/RntrackerSanitizedViewManager.swift | 23 + .../ios/RntrackerTouchManager.m | 6 + .../ios/RntrackerTouchManager.swift | 44 ++ .../ios/RntrackerViewManager.m | 6 + .../ios/RntrackerViewManager.swift | 37 + tracker/tracker-reactnative/lefthook.yml | 16 + .../openreplay-reactnative.podspec | 45 ++ tracker/tracker-reactnative/package.json | 163 +++++ .../tracker-reactnative/scripts/bootstrap.js | 29 + .../src/__tests__/index.test.tsx | 1 + tracker/tracker-reactnative/src/index.tsx | 92 +++ .../tracker-reactnative/tsconfig.build.json | 5 + tracker/tracker-reactnative/tsconfig.json | 28 + tracker/tracker-reactnative/turbo.json | 34 + 130 files changed, 7521 insertions(+) create mode 100644 tracker/tracker-ios/.gitignore create mode 100644 tracker/tracker-ios/LICENSE.md create mode 100644 tracker/tracker-ios/ORTracker.podspec create mode 100644 tracker/tracker-ios/Package.resolved create mode 100644 tracker/tracker-ios/Package.swift create mode 100644 tracker/tracker-ios/Podfile create mode 100644 tracker/tracker-ios/README.md create mode 100644 tracker/tracker-ios/Sources/ORTracker/Extensions/Data.swift create mode 100644 tracker/tracker-ios/Sources/ORTracker/Extensions/Date.swift create mode 100644 tracker/tracker-ios/Sources/ORTracker/Extensions/String.swift create mode 100644 tracker/tracker-ios/Sources/ORTracker/Extensions/UIImage.swift create mode 100644 tracker/tracker-ios/Sources/ORTracker/Extensions/UIView.swift create mode 100644 tracker/tracker-ios/Sources/ORTracker/Listeners/Analytics.swift create mode 100644 tracker/tracker-ios/Sources/ORTracker/Listeners/Crash.swift create mode 100644 tracker/tracker-ios/Sources/ORTracker/Listeners/LogsListener.swift create mode 100644 tracker/tracker-ios/Sources/ORTracker/Listeners/NetworkListener.swift create mode 100644 tracker/tracker-ios/Sources/ORTracker/Listeners/PerformanceListener.swift create mode 100644 tracker/tracker-ios/Sources/ORTracker/Managers/DebugUtils.swift create mode 100644 tracker/tracker-ios/Sources/ORTracker/Managers/MessageCollector.swift create mode 100644 tracker/tracker-ios/Sources/ORTracker/Managers/NetworkManager.swift create mode 100644 tracker/tracker-ios/Sources/ORTracker/Managers/ORUserDefaults.swift create mode 100644 tracker/tracker-ios/Sources/ORTracker/Managers/Swizzling.swift create mode 100644 tracker/tracker-ios/Sources/ORTracker/Models/ORMessage.swift create mode 100644 tracker/tracker-ios/Sources/ORTracker/Models/OROptions.swift create mode 100644 tracker/tracker-ios/Sources/ORTracker/Models/ORRecord.swift create mode 100644 tracker/tracker-ios/Sources/ORTracker/Models/ORSession.swift create mode 100644 tracker/tracker-ios/Sources/ORTracker/Models/script/ORMessages.swift create mode 100644 tracker/tracker-ios/Sources/ORTracker/Models/script/messages.erb create mode 100644 tracker/tracker-ios/Sources/ORTracker/Models/script/messages.rb create mode 100644 tracker/tracker-ios/Sources/ORTracker/Models/script/run.rb create mode 100644 tracker/tracker-ios/Sources/ORTracker/ORTracker.swift create mode 100644 tracker/tracker-ios/Sources/ORTracker/ScreenCapture/ScreenCapture.swift create mode 100644 tracker/tracker-ios/Tests/ORTrackerTests/ORTrackerTests.swift create mode 100644 tracker/tracker-reactnative/.editorconfig create mode 100644 tracker/tracker-reactnative/.gitignore create mode 100644 tracker/tracker-reactnative/.nvmrc create mode 100644 tracker/tracker-reactnative/.watchmanconfig create mode 100644 tracker/tracker-reactnative/.yarnrc create mode 100644 tracker/tracker-reactnative/CODE_OF_CONDUCT.md create mode 100644 tracker/tracker-reactnative/CONTRIBUTING.md create mode 100644 tracker/tracker-reactnative/LICENSE create mode 100644 tracker/tracker-reactnative/README.md create mode 100644 tracker/tracker-reactnative/android/build.gradle create mode 100644 tracker/tracker-reactnative/android/gradle.properties create mode 100644 tracker/tracker-reactnative/android/src/main/AndroidManifest.xml create mode 100644 tracker/tracker-reactnative/android/src/main/AndroidManifestNew.xml create mode 100644 tracker/tracker-reactnative/android/src/main/java/com/rntracker/RntrackerPackage.kt create mode 100644 tracker/tracker-reactnative/android/src/main/java/com/rntracker/RntrackerViewManager.kt create mode 100644 tracker/tracker-reactnative/babel.config.js create mode 100644 tracker/tracker-reactnative/example/.bundle/config create mode 100644 tracker/tracker-reactnative/example/.gitignore create mode 100644 tracker/tracker-reactnative/example/.watchmanconfig create mode 100644 tracker/tracker-reactnative/example/Gemfile create mode 100644 tracker/tracker-reactnative/example/README.md create mode 100644 tracker/tracker-reactnative/example/android/app/build.gradle create mode 100644 tracker/tracker-reactnative/example/android/app/debug.keystore create mode 100644 tracker/tracker-reactnative/example/android/app/proguard-rules.pro create mode 100644 tracker/tracker-reactnative/example/android/app/src/debug/AndroidManifest.xml create mode 100644 tracker/tracker-reactnative/example/android/app/src/debug/java/com/rntrackerexample/ReactNativeFlipper.java create mode 100644 tracker/tracker-reactnative/example/android/app/src/main/AndroidManifest.xml create mode 100644 tracker/tracker-reactnative/example/android/app/src/main/java/com/rntrackerexample/MainActivity.java create mode 100644 tracker/tracker-reactnative/example/android/app/src/main/java/com/rntrackerexample/MainApplication.java create mode 100644 tracker/tracker-reactnative/example/android/app/src/main/res/drawable/rn_edit_text_material.xml create mode 100644 tracker/tracker-reactnative/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 tracker/tracker-reactnative/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 tracker/tracker-reactnative/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 tracker/tracker-reactnative/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 tracker/tracker-reactnative/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 tracker/tracker-reactnative/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 tracker/tracker-reactnative/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 tracker/tracker-reactnative/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 tracker/tracker-reactnative/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 tracker/tracker-reactnative/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 tracker/tracker-reactnative/example/android/app/src/main/res/values/strings.xml create mode 100644 tracker/tracker-reactnative/example/android/app/src/main/res/values/styles.xml create mode 100644 tracker/tracker-reactnative/example/android/app/src/release/java/com/rntrackerexample/ReactNativeFlipper.java create mode 100644 tracker/tracker-reactnative/example/android/build.gradle create mode 100644 tracker/tracker-reactnative/example/android/gradle.properties create mode 100644 tracker/tracker-reactnative/example/android/gradle/wrapper/gradle-wrapper.jar create mode 100644 tracker/tracker-reactnative/example/android/gradle/wrapper/gradle-wrapper.properties create mode 100755 tracker/tracker-reactnative/example/android/gradlew create mode 100644 tracker/tracker-reactnative/example/android/gradlew.bat create mode 100644 tracker/tracker-reactnative/example/android/settings.gradle create mode 100644 tracker/tracker-reactnative/example/app.json create mode 100644 tracker/tracker-reactnative/example/babel.config.js create mode 100644 tracker/tracker-reactnative/example/index.js create mode 100644 tracker/tracker-reactnative/example/ios/File.swift create mode 100644 tracker/tracker-reactnative/example/ios/Podfile create mode 100644 tracker/tracker-reactnative/example/ios/Podfile.lock create mode 100644 tracker/tracker-reactnative/example/ios/RntrackerExample-Bridging-Header.h create mode 100644 tracker/tracker-reactnative/example/ios/RntrackerExample.xcodeproj/project.pbxproj create mode 100644 tracker/tracker-reactnative/example/ios/RntrackerExample.xcodeproj/xcshareddata/xcschemes/RntrackerExample.xcscheme create mode 100644 tracker/tracker-reactnative/example/ios/RntrackerExample.xcworkspace/contents.xcworkspacedata create mode 100644 tracker/tracker-reactnative/example/ios/RntrackerExample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 tracker/tracker-reactnative/example/ios/RntrackerExample/AppDelegate.h create mode 100644 tracker/tracker-reactnative/example/ios/RntrackerExample/AppDelegate.mm create mode 100644 tracker/tracker-reactnative/example/ios/RntrackerExample/Images.xcassets/AppIcon.appiconset/Contents.json create mode 100644 tracker/tracker-reactnative/example/ios/RntrackerExample/Images.xcassets/Contents.json create mode 100644 tracker/tracker-reactnative/example/ios/RntrackerExample/Info.plist create mode 100644 tracker/tracker-reactnative/example/ios/RntrackerExample/LaunchScreen.storyboard create mode 100644 tracker/tracker-reactnative/example/ios/RntrackerExample/main.m create mode 100644 tracker/tracker-reactnative/example/ios/RntrackerExampleTests/Info.plist create mode 100644 tracker/tracker-reactnative/example/ios/RntrackerExampleTests/RntrackerExampleTests.m create mode 100644 tracker/tracker-reactnative/example/jest.config.js create mode 100644 tracker/tracker-reactnative/example/metro.config.js create mode 100644 tracker/tracker-reactnative/example/package.json create mode 100644 tracker/tracker-reactnative/example/react-native.config.js create mode 100644 tracker/tracker-reactnative/example/src/App.tsx create mode 100644 tracker/tracker-reactnative/ios/Rntracker-Bridging-Header.h create mode 100644 tracker/tracker-reactnative/ios/Rntracker.xcodeproj/project.pbxproj create mode 100644 tracker/tracker-reactnative/ios/RntrackerConnector-Bridging-Header.h create mode 100644 tracker/tracker-reactnative/ios/RntrackerConnector.m create mode 100644 tracker/tracker-reactnative/ios/RntrackerConnector.swift create mode 100644 tracker/tracker-reactnative/ios/RntrackerInputManager.m create mode 100644 tracker/tracker-reactnative/ios/RntrackerInputManager.swift create mode 100644 tracker/tracker-reactnative/ios/RntrackerSanitizedViewManager.m create mode 100644 tracker/tracker-reactnative/ios/RntrackerSanitizedViewManager.swift create mode 100644 tracker/tracker-reactnative/ios/RntrackerTouchManager.m create mode 100644 tracker/tracker-reactnative/ios/RntrackerTouchManager.swift create mode 100644 tracker/tracker-reactnative/ios/RntrackerViewManager.m create mode 100644 tracker/tracker-reactnative/ios/RntrackerViewManager.swift create mode 100644 tracker/tracker-reactnative/lefthook.yml create mode 100644 tracker/tracker-reactnative/openreplay-reactnative.podspec create mode 100644 tracker/tracker-reactnative/package.json create mode 100644 tracker/tracker-reactnative/scripts/bootstrap.js create mode 100644 tracker/tracker-reactnative/src/__tests__/index.test.tsx create mode 100644 tracker/tracker-reactnative/src/index.tsx create mode 100644 tracker/tracker-reactnative/tsconfig.build.json create mode 100644 tracker/tracker-reactnative/tsconfig.json create mode 100644 tracker/tracker-reactnative/turbo.json diff --git a/tracker/tracker-ios/.gitignore b/tracker/tracker-ios/.gitignore new file mode 100644 index 000000000..536ad5539 --- /dev/null +++ b/tracker/tracker-ios/.gitignore @@ -0,0 +1,3 @@ +.build +.swiftpm +.DS_Store \ No newline at end of file diff --git a/tracker/tracker-ios/LICENSE.md b/tracker/tracker-ios/LICENSE.md new file mode 100644 index 000000000..6a52dfa68 --- /dev/null +++ b/tracker/tracker-ios/LICENSE.md @@ -0,0 +1,44 @@ +Elastic License 2.0 (ELv2) + +**Acceptance** +By using the software, you agree to all of the terms and conditions below. + +**Copyright License** +The licensor grants you a non-exclusive, royalty-free, worldwide, non-sublicensable, non-transferable license to use, copy, distribute, make available, and prepare derivative works of the software, in each case subject to the limitations and conditions below + +**Limitations** +You may not provide the software to third parties as a hosted or managed service, where the service provides users with access to any substantial set of the features or functionality of the software. + +You may not move, change, disable, or circumvent the license key functionality in the software, and you may not remove or obscure any functionality in the software that is protected by the license key. + +You may not alter, remove, or obscure any licensing, copyright, or other notices of the licensor in the software. Any use of the licensor’s trademarks is subject to applicable law. + +**Patents** +The licensor grants you a license, under any patent claims the licensor can license, or becomes able to license, to make, have made, use, sell, offer for sale, import and have imported the software, in each case subject to the limitations and conditions in this license. This license does not cover any patent claims that you cause to be infringed by modifications or additions to the software. If you or your company make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company. + +**Notices** +You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms. + +If you modify the software, you must include in any modified copies of the software prominent notices stating that you have modified the software. + +**No Other Rights** +These terms do not imply any licenses other than those expressly granted in these terms. + +**Termination** +If you use the software in violation of these terms, such use is not licensed, and your licenses will automatically terminate. If the licensor provides you with a notice of your violation, and you cease all violation of this license no later than 30 days after you receive that notice, your licenses will be reinstated retroactively. However, if you violate these terms after such reinstatement, any additional violation of these terms will cause your licenses to terminate automatically and permanently. + +**No Liability** +As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim. + +**Definitions** +The *licensor* is the entity offering these terms, and the *software* is the software the licensor makes available under these terms, including any portion of it. + +*you* refers to the individual or entity agreeing to these terms. + +*your company* is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. *control* means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect. + +*your licenses* are all the licenses granted to you for the software under these terms. + +*use* means anything you do with the software requiring one of your licenses. + +*trademark* means trademarks, service marks, and similar rights. diff --git a/tracker/tracker-ios/ORTracker.podspec b/tracker/tracker-ios/ORTracker.podspec new file mode 100644 index 000000000..ec8f84902 --- /dev/null +++ b/tracker/tracker-ios/ORTracker.podspec @@ -0,0 +1,14 @@ +Pod::Spec.new do |s| + s.name = 'ORTracker' + s.version = '0.1.0' + s.summary = 'A short description of ORTracker.' + s.homepage = 'https://github.com/openreplay/openreplay/tracker/tracker-ios' + s.license = { :type => 'ELv2', :file => 'LICENSE.md' } + s.author = { 'Nick Delirium' => 'nick.delirium@proton.me' } + s.source = { :git => 'https://github.com/openreplay/openreplay/tracker/tracker-ios', :tag => s.version.to_s } + s.ios.deployment_target = '13.0' + s.swift_version = '5.0' + s.source_files = 'Sources/ORTracker/**/*' + s.dependency 'SWCompression' + s.dependency 'DeviceKit' +end diff --git a/tracker/tracker-ios/Package.resolved b/tracker/tracker-ios/Package.resolved new file mode 100644 index 000000000..d7fbe221e --- /dev/null +++ b/tracker/tracker-ios/Package.resolved @@ -0,0 +1,32 @@ +{ + "pins" : [ + { + "identity" : "bitbytedata", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tsolomko/BitByteData", + "state" : { + "revision" : "36df26fe4586b4f23d76cfd8b47076998343a2b2", + "version" : "2.0.3" + } + }, + { + "identity" : "devicekit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/devicekit/DeviceKit.git", + "state" : { + "revision" : "d37e70cb2646666dcf276d7d3d4a9760a41ff8a6", + "version" : "4.9.0" + } + }, + { + "identity" : "swcompression", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tsolomko/SWCompression.git", + "state" : { + "revision" : "cd39ca0a3b269173bab06f68b182b72fa690765c", + "version" : "4.8.5" + } + } + ], + "version" : 2 +} diff --git a/tracker/tracker-ios/Package.swift b/tracker/tracker-ios/Package.swift new file mode 100644 index 000000000..5dcfa5256 --- /dev/null +++ b/tracker/tracker-ios/Package.swift @@ -0,0 +1,38 @@ +// swift-tools-version: 5.8 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "ORTracker", + platforms: [ + .iOS(.v13) + ], + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "ORTracker", + targets: ["ORTracker"] + ), + ], + dependencies: [ + .package(url: "https://github.com/devicekit/DeviceKit.git", from: "4.0.0"), + .package(url: "https://github.com/tsolomko/SWCompression.git", .upToNextMajor(from: "4.8.5")), + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "ORTracker", + dependencies: [ + .product(name: "SWCompression", package: "SWCompression"), + .product(name: "DeviceKit", package: "DeviceKit"), + + ] + ), + .testTarget( + name: "ORTrackerTests", + dependencies: ["ORTracker"] + ), + ] +) diff --git a/tracker/tracker-ios/Podfile b/tracker/tracker-ios/Podfile new file mode 100644 index 000000000..054e24c7d --- /dev/null +++ b/tracker/tracker-ios/Podfile @@ -0,0 +1,8 @@ +platform :ios, '13.0' + +target 'ORTracker' do + use_frameworks! + + pod 'SWCompression', '~> 4.8' + pod 'DeviceKit', '~> 5.1' +end diff --git a/tracker/tracker-ios/README.md b/tracker/tracker-ios/README.md new file mode 100644 index 000000000..c63cedbc8 --- /dev/null +++ b/tracker/tracker-ios/README.md @@ -0,0 +1,89 @@ +Setting up tracker + + +```swift +// AppDelegate.swift +import ORTracker + +//... + +class AppDelegate: UIResponder, UIApplicationDelegate { + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + + ORTracker.shared.serverURL = "https://your.instance.com/ingest" + ORTracker.shared.start(projectKey: "projectkey", options: .defaults) + + // ... + return true + } +``` + +Options (default all `true`) + +```swift +let crashes: Bool +let analytics: Bool +let performances: Bool +let logs: Bool +let screen: Bool +let wifiOnly: Bool +``` + +Setting up touches listener + +```swift +// SceneDelegate.Swift +import ORTracker + +// ... + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + let contentView = ContentView() + .environmentObject(TodoStore()) + + if let windowScene = scene as? UIWindowScene { + let window = TouchTrackingWindow(windowScene: windowScene) // <<<< here + window.rootViewController = UIHostingController(rootView: contentView) + self.window = window + window.makeKeyAndVisible() + } + } +``` + +Adding sensitive views (will be blurred in replay) + +```swift +import ORTracker + +// swiftUI +Text("Very important sensitive text") + .sensitive() + +// UIKit +ORTracker.shared.addIgnoredView(view) +``` + +Adding tracked inputs + +```swift + +// swiftUI +TextField("Input", text: $text) + .observeInput(text: $text, label: "tracker input #1", masked: Bool) + +// UIKit will use placeholder as label and sender.isSecureTextEntry to mask the input +Analytics.shared.addObservedInput(inputEl) +``` + +Observing views + +```swift +// swiftUI +TextField("Test") + .observeView(title: "Screen title", viewName: "test input name") + +// UIKit +Analytics.shared.addObservedView(view: inputEl, title: "Screen title", viewName: "test input name") +``` + +will send IOSScreenEnter and IOSScreenLeave when view appears/dissapears on/from screen diff --git a/tracker/tracker-ios/Sources/ORTracker/Extensions/Data.swift b/tracker/tracker-ios/Sources/ORTracker/Extensions/Data.swift new file mode 100644 index 000000000..aabb3a6bc --- /dev/null +++ b/tracker/tracker-ios/Sources/ORTracker/Extensions/Data.swift @@ -0,0 +1,230 @@ +import UIKit +import CommonCrypto + +protocol NSObjectCoding: NSCoding, NSObject {} + +extension NSObjectCoding { + static func from(data: Data, offset: inout Int) throws -> Self { + let valueData = try data.readData(offset: &offset) + guard let result = try NSKeyedUnarchiver.unarchivedObject(ofClass: self, from: valueData) + else { throw NSError(domain: "ErrorDomain", code: 0, userInfo: [NSLocalizedDescriptionKey: "Error reading NSCoding"]) } + return result + } +} + +extension Data { + mutating func appendString(_ string: String) { + if let data = string.data(using: .utf8) { + append(data) + } + } + + func sha256() -> String { + var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) + self.withUnsafeBytes { + _ = CC_SHA256($0.baseAddress, CC_LONG(self.count), &hash) + } + return Data(hash).hexEncodedString() + } + + func hexEncodedString() -> String { + return map { String(format: "%02hhx", $0) }.joined() + } + + func subdata(start: Int, length: Int) -> Data? { + let start = startIndex.advanced(by: start) + let end = start.advanced(by: length) + guard start >= 0, end <= count else { return nil } + return subdata(in: start..(offset: inout Int) throws -> T { + if T.self == CGFloat.self { + return CGFloat(try readPrimary(offset: &offset) as Double) as! T + } + if T.self == UInt64.self { + return try readUint(offset: &offset) as! T + } + if T.self == Int64.self { + return try readInt(offset: &offset) as! T + } + if T.self == Bool.self { + return try readBoolean(offset: &offset) as! T + } + let valueSize = MemoryLayout.size + guard let data = subdata(start: offset, length: valueSize) else { throw "Error reading primary value" } + let result = data.withUnsafeBytes { + $0.load(as: T.self) + } + offset += data.count + return result + } + + func readData(offset: inout Int) throws -> Data { + let length = try readUint(offset: &offset) + guard let data = subdata(start: offset, length: Int(length)), + length == data.count else { throw "Error reading data" } + + offset += Int(length) + return data + } + + func readString(offset: inout Int) throws -> String { + let data = try readData(offset: &offset) + guard let result = String(data: data, encoding: .utf8) + else { throw "Error reading string" } + return result + } + + private func readByte(offset: inout Int) throws -> UInt8 { + guard offset < count else { throw "Error reading byte" } + let b = self[offset] + offset += 1 + return b + } + + private func readUint(offset: inout Int) throws -> UInt64 { + var x: UInt64 = 0 + var s: Int = 0 + var i: Int = 0 + while true { + let b = try readByte(offset: &offset) + if b < 0x80 { + if i > 9 || i == 9 && b > 1 { + throw "Invalid UInt" + } + return x | UInt64(b)< Int64 { + let ux = try readUint(offset: &offset) + var x = Int64(ux >> 1) + if ux&1 != 0 { + x = ~x + } + return x + } + + private func readBoolean(offset: inout Int) throws -> Bool { + return try readByte(offset: &offset) == 1 + } +} + +extension Data { + init(value: Any?) { + self.init() + if let value = value { + writeValue(value: value) + } + } + + init(values: Any...) { + self.init() + values.forEach { writeValue(value: $0) } + } + + mutating func writeValues(values: Any...) { + values.forEach { writeValue(value: $0) } + } + + mutating func writeValue(value: Any) { + let oldLength = count + switch value { + case is NSNull: break + case let parsed as Data: writeData(parsed, sizePrefix: true) + case let parsed as Int64: writeInt(parsed) + case let parsed as UInt64: writeUint(parsed) + case let parsed as Int: writePrimary(parsed) + case let parsed as UInt8: writePrimary(parsed) + case let parsed as UInt16: writePrimary(parsed) + case let parsed as UInt32: writePrimary(parsed) + case let parsed as Float: writePrimary(parsed) + case let parsed as CGFloat: writePrimary(Double(parsed)) + case let parsed as Double: writePrimary(parsed) + case let parsed as Bool: writeBoolean(parsed) + case let parsed as UIEdgeInsets: + writeValues(values: parsed.top, parsed.bottom, parsed.left, parsed.right) + case let parsed as CGRect: + writeValues(values: parsed.origin.x, parsed.origin.y, parsed.size.width, parsed.size.height) + case let parsed as CGPoint: writeValues(values: parsed.x, parsed.y) + case let parsed as CGSize: writeValues(values: parsed.width, parsed.height) + case let parsed as UIColor: + var red: CGFloat = 0 + var green: CGFloat = 0 + var blue: CGFloat = 0 + var alpha: CGFloat = 0 + parsed.getRed(&red, green: &green, blue: &blue, alpha: &alpha) + writeValues(values: red, green, blue, alpha) + case let parsed as String: writeString(parsed) + case let parsed as UIFont: writeNSCoding(parsed.fontDescriptor) + case let parsed as NSAttributedString: writeNSCoding(parsed) + default: break + } + if CFGetTypeID(value as CFTypeRef) == CGColor.typeID { + writeValue(value: UIColor(cgColor: value as! CGColor)) + } + let length = count - oldLength + if length == 0 && !(value is NSNull) { + print("Nothing was written for \(String(describing: type(of: value))):\(value) ") + } + } + + private mutating func writePrimary(_ value: T) { + writeData(Swift.withUnsafeBytes(of: value) { Data($0) }, sizePrefix: false) + } + + private mutating func writeString(_ string: String) { + let stringData = string.data(using: .utf8, allowLossyConversion: true) ?? Data() + writeData(stringData, sizePrefix: true) + } + + private mutating func writeNSCoding(_ coding: NSCoding) { + do { + let valueData = try NSKeyedArchiver.archivedData(withRootObject: coding, requiringSecureCoding: true) + writeData(valueData, sizePrefix: true) + } catch { + print("Unexpected error: \(error).") + } + } + + private mutating func writeData(_ data: Data, sizePrefix: Bool) { + if sizePrefix { + writeValue(value: UInt64(data.count)) + } + append(data) + } + + private mutating func writeUint(_ input: UInt64) { + var v = input + while v >= 0x80 { + append(UInt8(v.littleEndian & 0x7F) | 0x80) // v.littleEndian ? + v >>= 7 + } + append(UInt8(v)) + } + + private mutating func writeInt(_ v: Int64) { + var uv = UInt64(v) << 1 + if v < 0 { + uv = ~uv + } + writeUint(uv) + } + + private mutating func writeBoolean(_ v: Bool) { + append(v ? 1 : 0) + } +} + +extension Encodable { + func toJSONData() -> Data? { try? JSONEncoder().encode(self) } +} + +extension String: Error {} diff --git a/tracker/tracker-ios/Sources/ORTracker/Extensions/Date.swift b/tracker/tracker-ios/Sources/ORTracker/Extensions/Date.swift new file mode 100644 index 000000000..373623744 --- /dev/null +++ b/tracker/tracker-ios/Sources/ORTracker/Extensions/Date.swift @@ -0,0 +1,17 @@ +import Foundation + +extension Date { + func format(_ format: String) -> String { + let formatter = DateFormatter() + formatter.locale = Locale(identifier: "en_US") + formatter.dateFormat = format + return formatter.string(from: self) + } + + static func parse(dateStr: String, format: String) -> Date? { + let formatter = DateFormatter() + formatter.locale = Locale(identifier: "en_US") + formatter.dateFormat = format + return formatter.date(from: dateStr) + } +} diff --git a/tracker/tracker-ios/Sources/ORTracker/Extensions/String.swift b/tracker/tracker-ios/Sources/ORTracker/Extensions/String.swift new file mode 100644 index 000000000..ae7078111 --- /dev/null +++ b/tracker/tracker-ios/Sources/ORTracker/Extensions/String.swift @@ -0,0 +1,10 @@ +import Foundation + +extension String { + + func contains(regex: String) -> Bool { + guard let regex = try? NSRegularExpression(pattern: regex) else { return false } + let range = NSRange(location: 0, length: self.utf16.count) + return regex.firstMatch(in: self, options: [], range: range) != nil + } +} diff --git a/tracker/tracker-ios/Sources/ORTracker/Extensions/UIImage.swift b/tracker/tracker-ios/Sources/ORTracker/Extensions/UIImage.swift new file mode 100644 index 000000000..3f3f97af8 --- /dev/null +++ b/tracker/tracker-ios/Sources/ORTracker/Extensions/UIImage.swift @@ -0,0 +1,24 @@ +import UIKit + +extension UIImage { + func applyBlurWithRadius(_ blurRadius: CGFloat) -> UIImage? { + if (size.width < 1 || size.height < 1) { + return nil + } + guard let inputCGImage = self.cgImage else { + return nil + } + let inputImage = CIImage(cgImage: inputCGImage) + let filter = CIFilter(name: "CIGaussianBlur") + filter?.setValue(inputImage, forKey: kCIInputImageKey) + filter?.setValue(blurRadius, forKey: kCIInputRadiusKey) + guard let outputImage = filter?.outputImage else { + return nil + } + let context = CIContext(options: nil) + guard let outputCGImage = context.createCGImage(outputImage, from: inputImage.extent) else { + return nil + } + return UIImage(cgImage: outputCGImage) + } +} diff --git a/tracker/tracker-ios/Sources/ORTracker/Extensions/UIView.swift b/tracker/tracker-ios/Sources/ORTracker/Extensions/UIView.swift new file mode 100644 index 000000000..c3a782fbb --- /dev/null +++ b/tracker/tracker-ios/Sources/ORTracker/Extensions/UIView.swift @@ -0,0 +1,26 @@ +import UIKit + +private var viewCounter = 0 +private var shortIds = [String: String]() + +extension UIView: Sanitizable { + public var identifier: String { + let longId = longIdentifier + if let existingId = shortIds[longId] { + return existingId + } + let shortId = "\(viewCounter)" + viewCounter += 1 + shortIds[longId] = shortId + return shortId + } + + public var longIdentifier: String { + return String(describing: type(of: self)) + "-" + Unmanaged.passUnretained(self).toOpaque().debugDescription + } + + public var frameInWindow: CGRect? { + return self.window == nil ? nil : self.convert(self.bounds, to: self.window) + } +} + diff --git a/tracker/tracker-ios/Sources/ORTracker/Listeners/Analytics.swift b/tracker/tracker-ios/Sources/ORTracker/Listeners/Analytics.swift new file mode 100644 index 000000000..aa48a8f2b --- /dev/null +++ b/tracker/tracker-ios/Sources/ORTracker/Listeners/Analytics.swift @@ -0,0 +1,288 @@ +import UIKit +import SwiftUI +import Combine +import ObjectiveC + +open class Analytics: NSObject { + public static let shared = Analytics() + public var enabled = false + public var observedInputs: [UITextField] = [] + public var observedViews: [UIView] = [] + private override init() { + super.init() + } + + public func start() { + enabled = true + UIViewController.swizzleLifecycleMethods() + } + + public func stop() { + observedViews.removeAll() + observedInputs.removeAll() + enabled = false + // Unswizzle (reverse the swizzling) if needed in the + } + + @objc private func handleTap(gesture: UITapGestureRecognizer) { + let location = gesture.location(in: nil) + DebugUtils.log("Tap detected at: \(location)") + } + + @objc public func addObservedInput(_ element: UITextField) { + observedInputs.append(element) + element.addTarget(self, action: #selector(textInputFinished), for: .editingDidEnd) + } + + @objc public func addObservedView(view: UIView, screenName: String, viewName: String) { + view.orScreenName = screenName + view.orViewName = viewName + observedViews.append(view) + } + + @objc public func sendClick(label: String, x: UInt64, y: UInt64) { + let message = ORIOSClickEvent(label: label, x: x, y: y) + + if Analytics.shared.enabled { + MessageCollector.shared.sendMessage(message) + } + } + + @objc public func sendSwipe(label: String, x: UInt64, y: UInt64, direction: String) { + let message = ORIOSSwipeEvent(label: label, x: x,y: y, direction: direction) + + if Analytics.shared.enabled { + MessageCollector.shared.sendMessage(message) + } + } + + @objc func textInputFinished(_ sender: UITextField) { + #if DEBUG + DebugUtils.log(">>>>>Text finish \(sender.text ?? "no_text") \(sender.placeholder ?? "no_placeholder")") + #endif + var sentText = sender.text + if sender.isSecureTextEntry { + sentText = "***" + } + MessageCollector.shared.sendMessage(ORIOSInputEvent(value: sentText ?? "", valueMasked: sender.isSecureTextEntry, label: sender.placeholder ?? "")) + } +} + +extension UIViewController { + + static func swizzleLifecycleMethods() { + DebugUtils.log(">>>>> ORTracker: swizzle UIViewController") + + Self.swizzle(original: #selector(viewDidAppear(_:)), swizzled: #selector(swizzledViewDidAppear(_:))) + Self.swizzle(original: #selector(viewDidDisappear(_:)), swizzled: #selector(swizzledViewDidDisappear(_:))) + } + + static private func swizzle(original: Selector, swizzled: Selector) { + if let originalMethod = class_getInstanceMethod(self, original), + let swizzledMethod = class_getInstanceMethod(self, swizzled) { + method_exchangeImplementations(originalMethod, swizzledMethod) + } + } + + @objc private func swizzledViewDidAppear(_ animated: Bool) { + self.swizzledViewDidAppear(animated) + + if let (screenName, viewName) = isViewOrSubviewObservedEnter() { + let message = ORIOSViewComponentEvent(screenName: screenName, viewName: viewName, visible: true) + if Analytics.shared.enabled { + MessageCollector.shared.sendMessage(message) + } + } + } + + @objc private func swizzledViewDidDisappear(_ animated: Bool) { + self.swizzledViewDidDisappear(animated) + + if let (screenName, viewName) = isViewOrSubviewObservedEnter() { + let message = ORIOSViewComponentEvent(screenName: screenName, viewName: viewName, visible: false) + if Analytics.shared.enabled { + MessageCollector.shared.sendMessage(message) + } + } + } + + private func isViewOrSubviewObservedEnter() -> (screenName: String, viewName: String)? { + var viewsToCheck: [UIView] = [self.view] + while !viewsToCheck.isEmpty { + let view = viewsToCheck.removeFirst() + if let observed = Analytics.shared.observedViews.first(where: { $0 == view }) { + let screenName = observed.orScreenName ?? "Unknown ScreenName" + let viewName = observed.orViewName ?? "Unknown View" + + return (screenName, viewName) + } + viewsToCheck.append(contentsOf: view.subviews) + } + return nil + } +} + +public class TouchTrackingWindow: UIWindow { + var touchStart: CGPoint? + + public override func sendEvent(_ event: UIEvent) { + super.sendEvent(event) + + guard let touches = event.allTouches else { return } + + for touch in touches { + switch touch.phase { + case .began: + touchStart = touch.location(in: self) + case .ended: + let location = touch.location(in: self) + let isSwipe = touchStart!.distance(to: location) > 10 + var event: ORMessage + let description = getViewDescription(touch.view) ?? "UIView" + if isSwipe { + DebugUtils.log("Swipe from \(touchStart ?? CGPoint(x: 0, y: 0)) to \(location)") + event = ORIOSSwipeEvent(label: description, x: UInt64(location.x),y: UInt64(location.y), direction: detectSwipeDirection(from: touchStart!, to: location)) + } else { + event = ORIOSClickEvent(label: description, x: UInt64(location.x), y: UInt64(location.y)) + DebugUtils.log("Touch from \(touchStart ?? CGPoint(x: 0, y: 0)) to \(location)") + } + touchStart = nil + MessageCollector.shared.sendMessage(event) + default: + break + } + } + } + + + private func getViewDescription(_ view: UIView?) -> String? { + guard let view = view else { + return nil + } + + if let textField = view as? UITextField { + return "UITextField '\(textField.placeholder ?? "No Placeholder")'" + } else if let label = view as? UILabel { + return "UILabel '\(label.text ?? "No Text")'" + } else if let button = view as? UIButton { + return "UIButton '\(button.currentTitle ?? "No Title")'" + } else if let textView = view as? UITextView { + return "UITextView '\(textView.text ?? "No Text")'" + } else { + return "\(type(of: view))" + } + } + + + private func detectSwipeDirection(from start: CGPoint, to end: CGPoint) -> String { + let deltaX = end.x - start.x + let deltaY = end.y - start.y + + if abs(deltaX) > abs(deltaY) { + if deltaX > 0 { + return "right" + } else { + return "left" + } + } else if abs(deltaY) > abs(deltaX) { + if deltaY > 0 { + return "down" + } else { + return "up" + } + } + + return "right" + } +} + + + +extension CGPoint { + func distance(to point: CGPoint) -> CGFloat { + return hypot(point.x - x, point.y - y) + } +} + +public struct ObservedInputModifier: ViewModifier { + @Binding var text: String + let label: String? + let masked: Bool? + + public func body(content: Content) -> some View { + content + .onReceive(text.publisher.collect()) { value in + let stringValue = String(value) + textInputFinished(value: stringValue, label: label, masked: masked) + } + } + + private func textInputFinished(value: String, label: String?, masked: Bool?) { + guard !value.isEmpty else { return } + var sentValue = value + if masked ?? false { + sentValue = "****" + } + MessageCollector.shared.sendDebouncedMessage(ORIOSInputEvent(value: sentValue, valueMasked: masked ?? false, label: label ?? "")) + } +} + +public struct ViewLifecycleModifier: ViewModifier { + let screenName: String + let viewName: String + + public func body(content: Content) -> some View { + content + .onAppear { + DebugUtils.log("<><><>view appear \(viewName)") + let message = ORIOSViewComponentEvent(screenName: screenName, viewName: viewName, visible: true) + if Analytics.shared.enabled { + MessageCollector.shared.sendMessage(message) + } + + } + .onDisappear { + DebugUtils.log("<><><>disappear view \(viewName)") + let message = ORIOSViewComponentEvent(screenName: screenName, viewName: viewName, visible: false) + if Analytics.shared.enabled { + MessageCollector.shared.sendMessage(message) + } + } + } +} + + +public extension View { + func observeView(screenName: String, viewName: String) -> some View { + self.modifier(ViewLifecycleModifier(screenName: screenName, viewName: viewName)) + } + + func observeInput(text: Binding, label: String?, masked: Bool?) -> some View { + self.modifier(ObservedInputModifier(text: text, label: label, masked: masked)) + } +} + +extension UIView { + private struct AssociatedKeys { + static var orScreenName: String = "OR: screenName" + static var orViewName: String = "OR: viewName" + } + + var orScreenName: String? { + get { + return objc_getAssociatedObject(self, &AssociatedKeys.orScreenName) as? String + } + set { + objc_setAssociatedObject(self, &AssociatedKeys.orScreenName, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } + + var orViewName: String? { + get { + return objc_getAssociatedObject(self, &AssociatedKeys.orViewName) as? String + } + set { + objc_setAssociatedObject(self, &AssociatedKeys.orViewName, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } +} diff --git a/tracker/tracker-ios/Sources/ORTracker/Listeners/Crash.swift b/tracker/tracker-ios/Sources/ORTracker/Listeners/Crash.swift new file mode 100644 index 000000000..556dcb577 --- /dev/null +++ b/tracker/tracker-ios/Sources/ORTracker/Listeners/Crash.swift @@ -0,0 +1,64 @@ +import UIKit + +public class Crashs: NSObject { + public static let shared = Crashs() + private static var fileUrl: URL? = nil + private var isActive = false + + private override init() { + Crashs.fileUrl = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first?.appendingPathComponent("ASCrash.dat") + if let fileUrl = Crashs.fileUrl, + FileManager.default.fileExists(atPath: fileUrl.path), + let crashData = try? Data(contentsOf: fileUrl) { + NetworkManager.shared.sendLateMessage(content: crashData) { (success) in + guard success else { return } + if FileManager.default.fileExists(atPath: fileUrl.path) { + try? FileManager.default.removeItem(at: fileUrl) + } + } + } + } + + public func start() { + NSSetUncaughtExceptionHandler { (exception) in + print("<><> captured crash \(exception)") + let message = ORIOSCrash(name: exception.name.rawValue, + reason: exception.reason ?? "", + stacktrace: exception.callStackSymbols.joined(separator: "\n")) + let messageData = message.contentData() + if let fileUrl = Crashs.fileUrl { + try? messageData.write(to: fileUrl) + } + NetworkManager.shared.sendMessage(content: messageData) { (success) in + guard success else { return } + if let fileUrl = Crashs.fileUrl, + FileManager.default.fileExists(atPath: fileUrl.path) { + try? FileManager.default.removeItem(at: fileUrl) + } + } + } + isActive = true + } + + public func sendLateError(exception: NSException) { + let message = ORIOSCrash(name: exception.name.rawValue, + reason: exception.reason ?? "", + stacktrace: exception.callStackSymbols.joined(separator: "\n") + ) + NetworkManager.shared.sendLateMessage(content: message.contentData()) { (success) in + guard success else { return } + if let fileUrl = Crashs.fileUrl, + FileManager.default.fileExists(atPath: fileUrl.path) { + try? FileManager.default.removeItem(at: fileUrl) + } + } + } + + public func stop() { + if isActive { + NSSetUncaughtExceptionHandler(nil) + isActive = false + } + + } +} diff --git a/tracker/tracker-ios/Sources/ORTracker/Listeners/LogsListener.swift b/tracker/tracker-ios/Sources/ORTracker/Listeners/LogsListener.swift new file mode 100644 index 000000000..838a2ba45 --- /dev/null +++ b/tracker/tracker-ios/Sources/ORTracker/Listeners/LogsListener.swift @@ -0,0 +1,47 @@ +import UIKit + +class LogsListener: NSObject { + static let shared = LogsListener() + private let outputListener = Listener(fileHandle: FileHandle.standardOutput, severity: "info") + private let errorListener = Listener(fileHandle: FileHandle.standardError, severity: "error") + + func start() { + outputListener.start() + errorListener.start() + } + + class Listener: NSObject { + let inputPipe = Pipe() + let outputPipe = Pipe() + let fileHandle: FileHandle + let severity: String + + init(fileHandle: FileHandle, severity: String) { + self.fileHandle = fileHandle + self.severity = severity + super.init() + + inputPipe.fileHandleForReading.readabilityHandler = { [weak self] fileHandle in + guard let strongSelf = self else { return } + + let data = fileHandle.availableData + if let string = String(data: data, encoding: String.Encoding.utf8) { + let message = ORIOSLog(severity: severity, content: string) + MessageCollector.shared.sendMessage(message) + } + + strongSelf.outputPipe.fileHandleForWriting.write(data) + } + } + + func start() { + dup2(fileHandle.fileDescriptor, outputPipe.fileHandleForWriting.fileDescriptor) + dup2(inputPipe.fileHandleForWriting.fileDescriptor, fileHandle.fileDescriptor) + } + + func stop() { + + } + + } +} diff --git a/tracker/tracker-ios/Sources/ORTracker/Listeners/NetworkListener.swift b/tracker/tracker-ios/Sources/ORTracker/Listeners/NetworkListener.swift new file mode 100644 index 000000000..4baea64c9 --- /dev/null +++ b/tracker/tracker-ios/Sources/ORTracker/Listeners/NetworkListener.swift @@ -0,0 +1,150 @@ +import UIKit + +open class NetworkListener: NSObject { + private let startTime: UInt64 + private var url: String = "" + private var method: String = "" + private var requestBody: String? + private var requestHeaders: [String: String]? + var ignoredKeys = ["password"] + var ignoredHeaders = ["Authentication", "Auth"] + + public override init() { + startTime = UInt64(Date().timeIntervalSince1970 * 1000) + } + + public convenience init(request: URLRequest) { + self.init() + start(request: request) + } + + public convenience init(task: URLSessionTask) { + self.init() + start(task: task) + } + + open func start(request: URLRequest) { + url = request.url?.absoluteString ?? "" + method = request.httpMethod ?? "GET" + requestHeaders = request.allHTTPHeaderFields + + if let body = request.httpBody { + requestBody = String(data: body, encoding: .utf8) + } else { + requestBody = "" + DebugUtils.log("error getting request body (start request)") + } + } + + open func start(task: URLSessionTask) { + if let request = task.currentRequest { + start(request: request) + } else { + DebugUtils.log("error getting request body (start task)") + } + } + + open func finish(response: URLResponse?, data: Data?) { + let endTime = UInt64(Date().timeIntervalSince1970 * 1000) + let httpResponse = response as? HTTPURLResponse + + var responseBody: String? = nil + if let data = data { + responseBody = String(data: data, encoding: .utf8) + } else { + DebugUtils.log("error getting request body (finish)") + } + + let requestContent: [String: Any?] = [ + "body": sanitizeBody(body: requestBody), + "headers": sanitizeHeaders(headers: requestHeaders) + ] + + var responseContent: [String: Any?] + if let httpResponse = httpResponse { + let headers = transformHeaders(httpResponse.allHeaderFields) + responseContent = [ + "body": sanitizeBody(body: responseBody), + "headers": sanitizeHeaders(headers: headers) + ] + } else { + responseContent = [ + "body": "", + "headers": "" + ] + } + + + let requestJSON = convertDictionaryToJSONString(dictionary: requestContent) ?? "" + let responseJSON = convertDictionaryToJSONString(dictionary: responseContent) ?? "" + + let status = httpResponse?.statusCode ?? 0 + let message = ORIOSNetworkCall( + type: "request", + method: method, + URL: url, + request: requestJSON, + response: responseJSON, + status: UInt64(status), + duration: endTime - startTime + ) + + MessageCollector.shared.sendMessage(message) + } + + private func sanitizeHeaders(headers: [String: String]?) -> [String: String]? { + guard let headerContent = headers else { return nil } + + var sanitizedHeaders = headerContent + for key in ignoredKeys { + if sanitizedHeaders.keys.contains(key) { + sanitizedHeaders[key] = "***" + } + } + return sanitizedHeaders + } + + + private func sanitizeBody(body: String?) -> String? { + guard let bodyContent = body else { return nil } + + var sanitizedBody = bodyContent + for key in ignoredKeys { + if let range = sanitizedBody.range(of: "\"\(key)\":\"[^\"]*\"", options: .regularExpression) { + sanitizedBody.replaceSubrange(range, with: "\"\(key)\":\"***\"") + } + } + return sanitizedBody + } +} + +func convertDictionaryToJSONString(dictionary: [String: Any?]) -> String? { + if let jsonData = try? JSONSerialization.data(withJSONObject: dictionary, options: []) { + return String(data: jsonData, encoding: .utf8) + } + return nil +} + +func transformHeaders(_ headers: [AnyHashable: Any]) -> [String: String] { + var stringHeaders: [String: String] = [:] + for (key, value) in headers { + if let stringKey = key.base as? String, let stringValue = value as? String { + stringHeaders[stringKey] = stringValue + } + } + return stringHeaders +} + + +func isJSONString(string: String) -> Bool { + if let data = string.data(using: .utf8) { + do { + _ = try JSONSerialization.jsonObject(with: data, options: []) + return true + } catch { + DebugUtils.log("Error: \(error)") + return false + } + } + return false +} diff --git a/tracker/tracker-ios/Sources/ORTracker/Listeners/PerformanceListener.swift b/tracker/tracker-ios/Sources/ORTracker/Listeners/PerformanceListener.swift new file mode 100644 index 000000000..f01c63588 --- /dev/null +++ b/tracker/tracker-ios/Sources/ORTracker/Listeners/PerformanceListener.swift @@ -0,0 +1,177 @@ +import UIKit + +open class PerformanceListener: NSObject { + public static let shared = PerformanceListener() + private var cpuTimer: Timer? + private var cpuIteration = 0 + private var memTimer: Timer? + public var isActive = false + + func start() { + #warning("Can interfere with client usage") + UIDevice.current.isBatteryMonitoringEnabled = true + UIDevice.current.beginGeneratingDeviceOrientationNotifications() + + let observe: (Notification.Name) -> Void = { + NotificationCenter.default.addObserver(self, selector: #selector(self.notified(_:)), name: $0, object: nil) + } + observe(.NSBundleResourceRequestLowDiskSpace) + observe(.NSProcessInfoPowerStateDidChange) + observe(ProcessInfo.thermalStateDidChangeNotification) + observe(UIApplication.didReceiveMemoryWarningNotification) + observe(UIDevice.batteryLevelDidChangeNotification) + observe(UIDevice.batteryStateDidChangeNotification) + observe(UIDevice.orientationDidChangeNotification) + + getCpuMessage() + getMemoryMessage() + + cpuTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true, block: { (_) in + self.getCpuMessage() + }) + + memTimer = Timer.scheduledTimer(withTimeInterval: 10, repeats: true, block: { (_) in + self.getMemoryMessage() + }) + isActive = true + + + NotificationCenter.default.addObserver(self, selector: #selector(pause), name: UIApplication.didEnterBackgroundNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(resume), name: UIApplication.willEnterForegroundNotification, object: nil) + } + + @objc func resume() { + #if DEBUG + DebugUtils.log("Resume") + #endif + getCpuMessage() + getMemoryMessage() + MessageCollector.shared.sendMessage(ORIOSPerformanceEvent(name: "background", value: UInt64(0))) + } + + @objc func pause() { + #if DEBUG + DebugUtils.log("Background") + #endif + MessageCollector.shared.sendMessage(ORIOSPerformanceEvent(name: "background", value: UInt64(1))) + } + + func getCpuMessage() { + if let cpu = self.cpuUsage() { + MessageCollector.shared.sendMessage(ORIOSPerformanceEvent(name: "mainThreadCPU", value: UInt64(cpu))) + } + } + + func getMemoryMessage() { + if let mem = self.memoryUsage() { + MessageCollector.shared.sendMessage(ORIOSPerformanceEvent(name: "memoryUsage", value: UInt64(mem))) + } + } + + func stop() { + if isActive { + UIDevice.current.isBatteryMonitoringEnabled = false + UIDevice.current.endGeneratingDeviceOrientationNotifications() + + NotificationCenter.default.removeObserver(self, name: .NSBundleResourceRequestLowDiskSpace, object: nil) + NotificationCenter.default.removeObserver(self, name: .NSProcessInfoPowerStateDidChange, object: nil) + NotificationCenter.default.removeObserver(self, name: ProcessInfo.thermalStateDidChangeNotification, object: nil) + NotificationCenter.default.removeObserver(self, name: UIApplication.didReceiveMemoryWarningNotification, object: nil) + NotificationCenter.default.removeObserver(self, name: UIDevice.batteryLevelDidChangeNotification, object: nil) + NotificationCenter.default.removeObserver(self, name: UIDevice.batteryStateDidChangeNotification, object: nil) + NotificationCenter.default.removeObserver(self, name: UIDevice.orientationDidChangeNotification, object: nil) + NotificationCenter.default.removeObserver(self, name: UIApplication.didEnterBackgroundNotification, object: nil) + NotificationCenter.default.removeObserver(self, name: UIApplication.willEnterForegroundNotification, object: nil) + + cpuTimer?.invalidate() + cpuTimer = nil + memTimer?.invalidate() + memTimer = nil + + isActive = false + } + } + + public func sendBattery() { + let message = ORIOSPerformanceEvent(name: "batteryLevel", value: 20) + + MessageCollector.shared.sendMessage(message) + } + + public func sendThermal() { + let message2 = ORIOSPerformanceEvent(name: "thermalState", value: 2) + MessageCollector.shared.sendMessage(message2) + } + + @objc func notified(_ notification: Notification) { + var message: ORIOSPerformanceEvent? = nil + switch notification.name { + case .NSBundleResourceRequestLowDiskSpace: + message = ORIOSPerformanceEvent(name: "lowDiskSpace", value: 0) + case .NSProcessInfoPowerStateDidChange: + message = ORIOSPerformanceEvent(name: "isLowPowerModeEnabled", value: ProcessInfo.processInfo.isLowPowerModeEnabled ? 1 : 0) + case ProcessInfo.thermalStateDidChangeNotification: + message = ORIOSPerformanceEvent(name: "thermalState", value: UInt64(ProcessInfo.processInfo.thermalState.rawValue)) + case UIApplication.didReceiveMemoryWarningNotification: + message = ORIOSPerformanceEvent(name: "memoryWarning", value: 0) + case UIDevice.batteryLevelDidChangeNotification: + message = ORIOSPerformanceEvent(name: "batteryLevel", value: UInt64(max(0.0, UIDevice.current.batteryLevel)*100)) + case UIDevice.batteryStateDidChangeNotification: + message = ORIOSPerformanceEvent(name: "batteryState", value: UInt64(UIDevice.current.batteryState.rawValue)) + case UIDevice.orientationDidChangeNotification: + message = ORIOSPerformanceEvent(name: "orientation", value: UInt64(UIDevice.current.orientation.rawValue)) + default: break + } + if let message = message { + MessageCollector.shared.sendMessage(message) + } + } + + func networkStateChange(_ state: UInt64) { + let message = ORIOSPerformanceEvent(name: "networkState", value: state) + MessageCollector.shared.sendMessage(message) + } + + func memoryUsage() -> UInt64? { + var taskInfo = task_vm_info_data_t() + var count = mach_msg_type_number_t(MemoryLayout.size) / 4 + let result: kern_return_t = withUnsafeMutablePointer(to: &taskInfo) { + $0.withMemoryRebound(to: integer_t.self, capacity: 1) { + task_info(mach_task_self_, task_flavor_t(TASK_VM_INFO), $0, &count) + } + } + + guard result == KERN_SUCCESS else { + return nil + } + return UInt64(taskInfo.phys_footprint) + } + + func cpuUsage() -> Double? { + var threadsListContainer: thread_act_array_t? + var threadsCount = mach_msg_type_number_t(0) + let threadsResult = withUnsafeMutablePointer(to: &threadsListContainer) { + return $0.withMemoryRebound(to: thread_act_array_t?.self, capacity: 1) { + task_threads(mach_task_self_, $0, &threadsCount) + } + } + defer { + vm_deallocate(mach_task_self_, vm_address_t(UInt(bitPattern: threadsListContainer)), vm_size_t(Int(threadsCount) * MemoryLayout.stride)) + } + + guard threadsCount > 0, threadsResult == KERN_SUCCESS, let threadsList = threadsListContainer else { + return nil + } + var threadInfo = thread_basic_info() + var threadInfoCount = mach_msg_type_number_t(THREAD_INFO_MAX) + let infoResult = withUnsafeMutablePointer(to: &threadInfo) { + $0.withMemoryRebound(to: integer_t.self, capacity: 1) { + thread_info(threadsList[0], thread_flavor_t(THREAD_BASIC_INFO), $0, &threadInfoCount) + } + } + + let threadBasicInfo = threadInfo as thread_basic_info + guard infoResult == KERN_SUCCESS, threadBasicInfo.flags & TH_FLAGS_IDLE == 0 else { return nil } + return Double(threadBasicInfo.cpu_usage) / Double(TH_USAGE_SCALE) * 100.0 + } +} diff --git a/tracker/tracker-ios/Sources/ORTracker/Managers/DebugUtils.swift b/tracker/tracker-ios/Sources/ORTracker/Managers/DebugUtils.swift new file mode 100644 index 000000000..689e3bb4e --- /dev/null +++ b/tracker/tracker-ios/Sources/ORTracker/Managers/DebugUtils.swift @@ -0,0 +1,16 @@ +import Foundation + +class DebugUtils: NSObject { + + static func error(_ str: String) { + // TODO: fix this one +// MessageCollector.shared.sendMessage(ASIOSInternalError(content: str)) + log(str) + } + + static func log(_ str: String) { + #if DEBUG + print(str) + #endif + } +} diff --git a/tracker/tracker-ios/Sources/ORTracker/Managers/MessageCollector.swift b/tracker/tracker-ios/Sources/ORTracker/Managers/MessageCollector.swift new file mode 100644 index 000000000..824e5b41e --- /dev/null +++ b/tracker/tracker-ios/Sources/ORTracker/Managers/MessageCollector.swift @@ -0,0 +1,168 @@ +import UIKit + +struct BatchArch { + var name: String + var data: Data +} + +class MessageCollector: NSObject { + public static let shared = MessageCollector() + private var imagesWaiting = [BatchArch]() + private var imagesSending = [BatchArch]() + private var messagesWaiting = [Data]() + private var nextMessageIndex = 0 + private var sendingLastMessages = false + private let maxMessagesSize = 500_000 + private let messagesQueue = OperationQueue() + private let lateMessagesFile: URL? + private var sendInderval: Timer? + + override init() { + lateMessagesFile = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first?.appendingPathComponent("/lateMessages.dat") + super.init() + } + + func start() { + sendInderval = Timer.scheduledTimer(withTimeInterval: 5, repeats: true, block: { [weak self] _ in + self?.flush() + }) + NotificationCenter.default.addObserver(self, selector: #selector(terminate), name: UIApplication.willResignActiveNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(terminate), name: UIApplication.willTerminateNotification, object: nil) + messagesQueue.maxConcurrentOperationCount = 1 + + if let fileUrl = lateMessagesFile, + FileManager.default.fileExists(atPath: fileUrl.path), + let lateData = try? Data(contentsOf: fileUrl) { + NetworkManager.shared.sendLateMessage(content: lateData) { (success) in + guard success else { return } + try? FileManager.default.removeItem(at: fileUrl) + } + } + } + + func stop() { + sendInderval?.invalidate() + NotificationCenter.default.removeObserver(self, name: UIApplication.willResignActiveNotification, object: nil) + NotificationCenter.default.removeObserver(self, name: UIApplication.willTerminateNotification, object: nil) + self.terminate() + } + + func sendImagesBatch(batch: Data, fileName: String) { + self.imagesWaiting.append(BatchArch(name: fileName, data: batch)) + messagesQueue.addOperation { + self.flushImages() + } + } + + @objc func terminate() { + guard !sendingLastMessages else { return } + messagesQueue.addOperation { + self.sendingLastMessages = true + self.flushMessages() + } + } + + @objc func flush() { + messagesQueue.addOperation { + self.flushMessages() + self.flushImages() + } + } + + private func flushImages() { + let images = imagesWaiting.first + guard !imagesWaiting.isEmpty, let images = images, let projectKey = ORTracker.shared.projectKey else { return } + imagesWaiting.remove(at: 0) + + imagesSending.append(images) + + DebugUtils.log("Sending images \(images.name) \(images.data.count)") + NetworkManager.shared.sendImages(projectKey: projectKey, images: images.data, name: images.name) { (success) in + self.imagesSending.removeAll { (waiting) -> Bool in + images.name == waiting.name + } + guard success else { + self.imagesWaiting.append(images) + return + } + } + } + + func sendMessage(_ message: ORMessage) { + let data = message.contentData() + #if DEBUG + if !message.description.contains("IOSLog") && !message.description.contains("IOSNetworkCall") { + DebugUtils.log(message.description) + } + if let networkCallMessage = message as? ORIOSNetworkCall { + DebugUtils.log("-->> IOSNetworkCall(105): \(networkCallMessage.method) \(networkCallMessage.URL)") + } + #endif + self.sendRawMessage(data) + } + + private var debounceTimer: Timer? + private var debouncedMessage: ORMessage? + func sendDebouncedMessage(_ message: ORMessage) { + debounceTimer?.invalidate() + + debouncedMessage = message + debounceTimer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false) { [weak self] _ in + if let debouncedMessage = self?.debouncedMessage { + self?.sendMessage(debouncedMessage) + self?.debouncedMessage = nil + } + } + } + + func sendRawMessage(_ data: Data) { + messagesQueue.addOperation { + if data.count > self.maxMessagesSize { + DebugUtils.log("<><><>Single message size exceeded limit") + return + } + self.messagesWaiting.append(data) + var totalWaitingSize = 0 + self.messagesWaiting.forEach { totalWaitingSize += $0.count } + if totalWaitingSize > Int(Double(self.maxMessagesSize) * 0.8) { + self.flushMessages() + } + } + } + + private func flushMessages() { + var messages = [Data]() + var sentSize = 0 + while let message = messagesWaiting.first, sentSize + message.count <= maxMessagesSize { + messages.append(message) + messagesWaiting.remove(at: 0) + sentSize += message.count + } + guard !messages.isEmpty else { return } + var content = Data() + let index = ORIOSBatchMeta(firstIndex: UInt64(nextMessageIndex)) + content.append(index.contentData()) + DebugUtils.log(index.description) + messages.forEach { (message) in + content.append(message) + } + if sendingLastMessages, let fileUrl = lateMessagesFile { + try? content.write(to: fileUrl) + } + nextMessageIndex += messages.count + DebugUtils.log("messages batch \(content)") + NetworkManager.shared.sendMessage(content: content) { (success) in + guard success else { + DebugUtils.log("<><>re-sending failed batch<><>") + self.messagesWaiting.insert(contentsOf: messages, at: 0) + return + } + if self.sendingLastMessages { + self.sendingLastMessages = false + if let fileUrl = self.lateMessagesFile, FileManager.default.fileExists(atPath: fileUrl.path) { + try? FileManager.default.removeItem(at: fileUrl) + } + } + } + } +} diff --git a/tracker/tracker-ios/Sources/ORTracker/Managers/NetworkManager.swift b/tracker/tracker-ios/Sources/ORTracker/Managers/NetworkManager.swift new file mode 100644 index 000000000..6c5ae0f1a --- /dev/null +++ b/tracker/tracker-ios/Sources/ORTracker/Managers/NetworkManager.swift @@ -0,0 +1,195 @@ +import UIKit +import SWCompression + +let START_URL = "/v1/mobile/start" +let INGEST_URL = "/v1/mobile/i" +let LATE_URL = "/v1/mobile/late" +let IMAGES_URL = "/v1/mobile/images" + +class NetworkManager: NSObject { + static let shared = NetworkManager() + var baseUrl = "https://api.openreplay.com/ingest" + public var sessionId: String? = nil + private var token: String? = nil + public var writeToFile = false + #if DEBUG + private let localFilePath = "/Users/nikitamelnikov/Desktop/session.dat" + #endif + + override init() { + #if DEBUG + if writeToFile, FileManager.default.fileExists(atPath: localFilePath) { + try? FileManager.default.removeItem(at: URL(fileURLWithPath: localFilePath)) + } + #endif + } + + private func createRequest(method: String, path: String) -> URLRequest { + let url = URL(string: baseUrl+path)! + var request = URLRequest(url: url) + request.httpMethod = method + return request + } + + private func callAPI(request: URLRequest, + onSuccess: @escaping (Data) -> Void, + onError: @escaping (Error?) -> Void) { + guard !writeToFile else { return } + let task = URLSession.shared.dataTask(with: request) { (data, response, error) in + + DebugUtils.log(">>>\(request.httpMethod ?? ""):\(request.url?.absoluteString ?? "")\n<<<\(String(data: data ?? Data(), encoding: .utf8) ?? "")") + + DispatchQueue.main.async { + guard let data = data, + let httpResponse = response as? HTTPURLResponse, + (200...299).contains(httpResponse.statusCode) else { + DebugUtils.error(">>>>>> Error in call \(request.url?.absoluteString ?? "") : \(error?.localizedDescription ?? "N/A")") + if (response as? HTTPURLResponse)?.statusCode == 401 { + self.token = nil + ORTracker.shared.startSession(projectKey: ORTracker.shared.projectKey ?? "", options: ORTracker.shared.options) + } + onError(error) + return + } + onSuccess(data) + } + } + task.resume() + } + + func createSession(params: [String: AnyHashable], completion: @escaping (ORSessionResponse?) -> Void) { + guard !writeToFile else { + self.token = "writeToFile" + return + } + var request = createRequest(method: "POST", path: START_URL) + guard let jsonData = try? JSONSerialization.data(withJSONObject: params, options: []) else { + completion(nil) + return + } + request.httpBody = jsonData + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + callAPI(request: request) { (data) in + do { + let session = try JSONDecoder().decode(ORSessionResponse.self, from: data) + + self.token = session.token + self.sessionId = session.sessionID + ORUserDefaults.shared.lastToken = self.token + + completion(session) + } catch { + DebugUtils.log("Can't unwrap session start resp: \(error)") + } + } onError: { _ in + completion(nil) + } + } + + func sendMessage(content: Data, completion: @escaping (Bool) -> Void) { + guard !writeToFile else { + appendLocalFile(data: content) + return + } + var request = createRequest(method: "POST", path: INGEST_URL) + guard let token = token else { + completion(false) + return + } + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + + var compressedContent = content + let oldSize = compressedContent.count + var newSize = oldSize + do { + let compressed = try GzipArchive.archive(data: content) + compressedContent = compressed + newSize = compressed.count + request.setValue("gzip", forHTTPHeaderField: "Content-Encoding") + request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type") + DebugUtils.log(">>>>Compress batch file \(oldSize)>\(newSize)") + } catch { + DebugUtils.log("Error with compression: \(error)") + } + + request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type") + + request.httpBody = compressedContent + callAPI(request: request) { (data) in + completion(true) + } onError: { _ in + completion(false) + } + } + + func sendLateMessage(content: Data, completion: @escaping (Bool) -> Void) { + DebugUtils.log(">>>sending late messages") + var request = createRequest(method: "POST", path: LATE_URL) + guard let token = ORUserDefaults.shared.lastToken else { + completion(false) + DebugUtils.log("! No last token found") + return + } + print(token) + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + request.httpBody = content + callAPI(request: request) { (data) in + completion(true) + DebugUtils.log("<<< late messages sent") + } onError: { _ in + completion(false) + } + } + + func sendImages(projectKey: String, images: Data, name: String, completion: @escaping (Bool) -> Void) { + var request = createRequest(method: "POST", path: IMAGES_URL) + guard let token = token else { + completion(false) + return + } + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + let boundary = "Boundary-\(NSUUID().uuidString)" + request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") + + var body = Data() + let parameters = ["projectKey": projectKey] + for (key, value) in parameters { + body.appendString("--\(boundary)\r\n") + body.appendString("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n") + body.appendString("\(value)\r\n") + } + + body.appendString("--\(boundary)\r\n") + body.appendString("Content-Disposition: form-data; name=\"batch\"; filename=\"\(name)\"\r\n") + body.appendString("Content-Type: gzip\r\n\r\n") + body.append(images) + body.appendString("\r\n") + + body.appendString("--\(boundary)--\r\n") + DebugUtils.log(">>>>>> sending \(body.count) bytes") + request.httpBody = body + + callAPI(request: request) { (data) in + completion(true) + } onError: { _ in + completion(false) + } + } + + private func appendLocalFile(data: Data) { + #if DEBUG + DebugUtils.log("appendInFile \(data.count) bytes") + + let fileURL = URL(fileURLWithPath: localFilePath) + if let fileHandle = try? FileHandle(forWritingTo: fileURL) { + defer { + fileHandle.closeFile() + } + fileHandle.seekToEndOfFile() + fileHandle.write(data) + } else { + try? data.write(to: fileURL, options: .atomic) + } + #endif + } +} diff --git a/tracker/tracker-ios/Sources/ORTracker/Managers/ORUserDefaults.swift b/tracker/tracker-ios/Sources/ORTracker/Managers/ORUserDefaults.swift new file mode 100644 index 000000000..a5174f001 --- /dev/null +++ b/tracker/tracker-ios/Sources/ORTracker/Managers/ORUserDefaults.swift @@ -0,0 +1,33 @@ +import UIKit + +class ORUserDefaults: NSObject { + public static let shared = ORUserDefaults() + private let userDefaults: UserDefaults? + + override init() { + userDefaults = UserDefaults(suiteName: "io.orenreplay.openreplaytr-defaults") + } + + var userUUID: String { + get { + if let savedUUID = userDefaults?.string(forKey: "userUUID") { + return savedUUID + } + let newUUID = UUID().uuidString + self.userUUID = newUUID + return newUUID + } + set { + userDefaults?.set(newValue, forKey: "userUUID") + } + } + + var lastToken: String? { + get { + return userDefaults?.string(forKey: "lastToken") + } + set { + userDefaults?.set(newValue, forKey: "lastToken") + } + } +} diff --git a/tracker/tracker-ios/Sources/ORTracker/Managers/Swizzling.swift b/tracker/tracker-ios/Sources/ORTracker/Managers/Swizzling.swift new file mode 100644 index 000000000..83de65e75 --- /dev/null +++ b/tracker/tracker-ios/Sources/ORTracker/Managers/Swizzling.swift @@ -0,0 +1,22 @@ + +import UIKit + +class Swizzling: NSObject { + static func swizzle(cls: AnyClass, original: Selector, swizzled: Selector) { + if let originalMethod = class_getInstanceMethod(cls, original), + let swizzledMethod = class_getInstanceMethod(cls, swizzled) { + method_exchangeImplementations(originalMethod, swizzledMethod) + } + } + static func swizzleIfPresent(cls: AnyClass, original: Selector, swizzled: Selector) { + if let originalMethod = class_getInstanceMethod(cls, original), + let swizzledMethod = class_getInstanceMethod(cls, swizzled) { + let didAddMethod = class_addMethod(self, original, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)) + if didAddMethod { + class_replaceMethod(self, swizzled, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)) + } else { + method_exchangeImplementations(originalMethod, swizzledMethod) + } + } + } +} diff --git a/tracker/tracker-ios/Sources/ORTracker/Models/ORMessage.swift b/tracker/tracker-ios/Sources/ORTracker/Models/ORMessage.swift new file mode 100644 index 000000000..0df951aad --- /dev/null +++ b/tracker/tracker-ios/Sources/ORTracker/Models/ORMessage.swift @@ -0,0 +1,42 @@ +import UIKit + +struct GenericMessage { + let typeRaw: UInt64 + let type: ORMessageType? + let timestamp: UInt64 + let body: Data + + init?(data: Data, offset: inout Int) { + do { + typeRaw = try data.readPrimary(offset: &offset) + type = ORMessageType(rawValue: typeRaw) + timestamp = try data.readPrimary(offset: &offset) + body = try data.readData(offset: &offset) + } catch { + return nil + } + } +} + +class ORMessage: NSObject { + + let messageRaw: UInt64 + let message: ORMessageType? + let timestamp: UInt64 + + init(messageType: ORMessageType) { + self.messageRaw = messageType.rawValue + self.message = messageType + self.timestamp = UInt64(Date().timeIntervalSince1970 * 1000) + } + + init?(genericMessage: GenericMessage) { + self.messageRaw = genericMessage.typeRaw + self.message = genericMessage.type + self.timestamp = genericMessage.timestamp + } + + func contentData() -> Data { + fatalError("This method should be overridden") + } +} diff --git a/tracker/tracker-ios/Sources/ORTracker/Models/OROptions.swift b/tracker/tracker-ios/Sources/ORTracker/Models/OROptions.swift new file mode 100644 index 000000000..429ba8a48 --- /dev/null +++ b/tracker/tracker-ios/Sources/ORTracker/Models/OROptions.swift @@ -0,0 +1,27 @@ +import Foundation + +@objc public enum RecordingQuality: Int { + case Low + case Standard + case High +} + +open class OROptions: NSObject { + let crashes: Bool + let analytics: Bool + let performances: Bool + let logs: Bool + let screen: Bool + let wifiOnly: Bool + + public static let defaults = OROptions(crashes: true, analytics: true, performances: true, logs: true, screen: true, wifiOnly: true) + + @objc public init(crashes: Bool, analytics: Bool, performances: Bool, logs: Bool, screen: Bool, wifiOnly: Bool) { + self.crashes = crashes + self.analytics = analytics + self.performances = performances + self.logs = logs + self.screen = screen + self.wifiOnly = wifiOnly + } +} diff --git a/tracker/tracker-ios/Sources/ORTracker/Models/ORRecord.swift b/tracker/tracker-ios/Sources/ORTracker/Models/ORRecord.swift new file mode 100644 index 000000000..f568ae081 --- /dev/null +++ b/tracker/tracker-ios/Sources/ORTracker/Models/ORRecord.swift @@ -0,0 +1,9 @@ +import UIKit + + +struct ORRecord: Codable { + let img: String + let originX: CGFloat + let originY: CGFloat + let timestamp: UInt64 +} diff --git a/tracker/tracker-ios/Sources/ORTracker/Models/ORSession.swift b/tracker/tracker-ios/Sources/ORTracker/Models/ORSession.swift new file mode 100644 index 000000000..4b651f95c --- /dev/null +++ b/tracker/tracker-ios/Sources/ORTracker/Models/ORSession.swift @@ -0,0 +1,84 @@ +import UIKit +import DeviceKit + +class ORSessionRequest: NSObject { + private static var params = [String: AnyHashable]() + + static func create( completion: @escaping (ORSessionResponse?) -> Void) { + guard let projectKey = ORTracker.shared.projectKey else { return print("Openreplay: no project key added") } + #warning("Can interfere with client usage") + UIDevice.current.isBatteryMonitoringEnabled = true + UIDevice.current.beginGeneratingDeviceOrientationNotifications() + + let performances: [String: UInt64] = [ + "physicalMemory": UInt64(ProcessInfo.processInfo.physicalMemory), + "processorCount": UInt64(ProcessInfo.processInfo.processorCount), + "activeProcessorCount": UInt64(ProcessInfo.processInfo.activeProcessorCount), + "systemUptime": UInt64(ProcessInfo.processInfo.systemUptime), + "isLowPowerModeEnabled": UInt64(ProcessInfo.processInfo.isLowPowerModeEnabled ? 1 : 0), + "thermalState": UInt64(ProcessInfo.processInfo.thermalState.rawValue), + "batteryLevel": UInt64(max(0.0, UIDevice.current.batteryLevel)*100), + "batteryState": UInt64(UIDevice.current.batteryState.rawValue), + "orientation": UInt64(UIDevice.current.orientation.rawValue), + ] + + let device = Device.current + var deviceModel = "" + var deviceSafeName = "" + + if device.isSimulator { + deviceSafeName = "iPhone 14 Pro" + deviceModel = "iPhone14,8" + } else { + deviceSafeName = device.safeDescription + deviceModel = Device.identifier + } + + DebugUtils.log(">>>> device \(device) type \(device.safeDescription) mem \(UInt64(ProcessInfo.processInfo.physicalMemory / 1024))") + params = [ + "projectKey": projectKey, + "trackerVersion": Bundle(for: ORTracker.shared.classForCoder).object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "N/A", + "revID": Bundle(for: ORTracker.shared.classForCoder).object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "N/A", + "userUUID": ORUserDefaults.shared.userUUID, + "userOSVersion": UIDevice.current.systemVersion, + "userDevice": deviceModel, + "userDeviceType": deviceSafeName, + "timestamp": UInt64(Date().timeIntervalSince1970 * 1000), + "performances": performances, + "deviceMemory": UInt64(ProcessInfo.processInfo.physicalMemory / 1024), + "timezone": getTimezone(), + ] + callAPI(completion: completion) + } + + private static func callAPI(completion: @escaping (ORSessionResponse?) -> Void) { + guard !params.isEmpty else { return } + NetworkManager.shared.createSession(params: params) { (sessionResponse) in + guard let sessionResponse = sessionResponse else { + DispatchQueue.global().asyncAfter(deadline: .now() + 5) { + callAPI(completion: completion) + } + return + } + DebugUtils.log(">>>> Starting session : \(sessionResponse.sessionID)") + return completion(sessionResponse) + } + } +} + +struct ORSessionResponse: Decodable { + let userUUID: String + let token: String + let imagesHashList: [String]? + let sessionID: String + let fps: Int + let quality: String +} + +func getTimezone() -> String { + let offset = TimeZone.current.secondsFromGMT() + let sign = offset >= 0 ? "+" : "-" + let hours = abs(offset) / 3600 + let minutes = (abs(offset) % 3600) / 60 + return String(format: "UTC%@%02d:%02d", sign, hours, minutes) +} diff --git a/tracker/tracker-ios/Sources/ORTracker/Models/script/ORMessages.swift b/tracker/tracker-ios/Sources/ORTracker/Models/script/ORMessages.swift new file mode 100644 index 000000000..9835baebf --- /dev/null +++ b/tracker/tracker-ios/Sources/ORTracker/Models/script/ORMessages.swift @@ -0,0 +1,499 @@ + +// Auto-generated, do not edit +import UIKit + +enum ORMessageType: UInt64 { + case iOSMetadata = 92 + case iOSEvent = 93 + case iOSUserID = 94 + case iOSUserAnonymousID = 95 + case iOSScreenChanges = 96 + case iOSCrash = 97 + case iOSViewComponentEvent = 98 + case iOSClickEvent = 100 + case iOSInputEvent = 101 + case iOSPerformanceEvent = 102 + case iOSLog = 103 + case iOSInternalError = 104 + case iOSNetworkCall = 105 + case iOSSwipeEvent = 106 + case iOSBatchMeta = 107 +} + +class ORIOSMetadata: ORMessage { + let key: String + let value: String + + init(key: String, value: String) { + self.key = key + self.value = value + super.init(messageType: .iOSMetadata) + } + + override init?(genericMessage: GenericMessage) { + do { + var offset = 0 + self.key = try genericMessage.body.readString(offset: &offset) + self.value = try genericMessage.body.readString(offset: &offset) + super.init(genericMessage: genericMessage) + } catch { + return nil + } + } + + override func contentData() -> Data { + return Data(values: UInt64(92), timestamp, Data(values: key, value)) + } + + override var description: String { + return "-->> IOSMetadata(92): timestamp:\(timestamp) key:\(key) value:\(value)"; + } +} + +class ORIOSEvent: ORMessage { + let name: String + let payload: String + + init(name: String, payload: String) { + self.name = name + self.payload = payload + super.init(messageType: .iOSEvent) + } + + override init?(genericMessage: GenericMessage) { + do { + var offset = 0 + self.name = try genericMessage.body.readString(offset: &offset) + self.payload = try genericMessage.body.readString(offset: &offset) + super.init(genericMessage: genericMessage) + } catch { + return nil + } + } + + override func contentData() -> Data { + return Data(values: UInt64(93), timestamp, Data(values: name, payload)) + } + + override var description: String { + return "-->> IOSEvent(93): timestamp:\(timestamp) name:\(name) payload:\(payload)"; + } +} + +class ORIOSUserID: ORMessage { + let iD: String + + init(iD: String) { + self.iD = iD + super.init(messageType: .iOSUserID) + } + + override init?(genericMessage: GenericMessage) { + do { + var offset = 0 + self.iD = try genericMessage.body.readString(offset: &offset) + super.init(genericMessage: genericMessage) + } catch { + return nil + } + } + + override func contentData() -> Data { + return Data(values: UInt64(94), timestamp, Data(values: iD)) + } + + override var description: String { + return "-->> IOSUserID(94): timestamp:\(timestamp) iD:\(iD)"; + } +} + +class ORIOSUserAnonymousID: ORMessage { + let iD: String + + init(iD: String) { + self.iD = iD + super.init(messageType: .iOSUserAnonymousID) + } + + override init?(genericMessage: GenericMessage) { + do { + var offset = 0 + self.iD = try genericMessage.body.readString(offset: &offset) + super.init(genericMessage: genericMessage) + } catch { + return nil + } + } + + override func contentData() -> Data { + return Data(values: UInt64(95), timestamp, Data(values: iD)) + } + + override var description: String { + return "-->> IOSUserAnonymousID(95): timestamp:\(timestamp) iD:\(iD)"; + } +} + +class ORIOSScreenChanges: ORMessage { + let x: UInt64 + let y: UInt64 + let width: UInt64 + let height: UInt64 + + init(x: UInt64, y: UInt64, width: UInt64, height: UInt64) { + self.x = x + self.y = y + self.width = width + self.height = height + super.init(messageType: .iOSScreenChanges) + } + + override init?(genericMessage: GenericMessage) { + do { + var offset = 0 + self.x = try genericMessage.body.readPrimary(offset: &offset) + self.y = try genericMessage.body.readPrimary(offset: &offset) + self.width = try genericMessage.body.readPrimary(offset: &offset) + self.height = try genericMessage.body.readPrimary(offset: &offset) + super.init(genericMessage: genericMessage) + } catch { + return nil + } + } + + override func contentData() -> Data { + return Data(values: UInt64(96), timestamp, Data(values: x, y, width, height)) + } + + override var description: String { + return "-->> IOSScreenChanges(96): timestamp:\(timestamp) x:\(x) y:\(y) width:\(width) height:\(height)"; + } +} + +class ORIOSCrash: ORMessage { + let name: String + let reason: String + let stacktrace: String + + init(name: String, reason: String, stacktrace: String) { + self.name = name + self.reason = reason + self.stacktrace = stacktrace + super.init(messageType: .iOSCrash) + } + + override init?(genericMessage: GenericMessage) { + do { + var offset = 0 + self.name = try genericMessage.body.readString(offset: &offset) + self.reason = try genericMessage.body.readString(offset: &offset) + self.stacktrace = try genericMessage.body.readString(offset: &offset) + super.init(genericMessage: genericMessage) + } catch { + return nil + } + } + + override func contentData() -> Data { + return Data(values: UInt64(97), timestamp, Data(values: name, reason, stacktrace)) + } + + override var description: String { + return "-->> IOSCrash(97): timestamp:\(timestamp) name:\(name) reason:\(reason) stacktrace:\(stacktrace)"; + } +} + +class ORIOSViewComponentEvent: ORMessage { + let screenName: String + let viewName: String + let visible: Bool + + init(screenName: String, viewName: String, visible: Bool) { + self.screenName = screenName + self.viewName = viewName + self.visible = visible + super.init(messageType: .iOSViewComponentEvent) + } + + override init?(genericMessage: GenericMessage) { + do { + var offset = 0 + self.screenName = try genericMessage.body.readString(offset: &offset) + self.viewName = try genericMessage.body.readString(offset: &offset) + self.visible = try genericMessage.body.readPrimary(offset: &offset) + super.init(genericMessage: genericMessage) + } catch { + return nil + } + } + + override func contentData() -> Data { + return Data(values: UInt64(98), timestamp, Data(values: screenName, viewName, visible)) + } + + override var description: String { + return "-->> IOSViewComponentEvent(98): timestamp:\(timestamp) screenName:\(screenName) viewName:\(viewName) visible:\(visible)"; + } +} + +class ORIOSClickEvent: ORMessage { + let label: String + let x: UInt64 + let y: UInt64 + + init(label: String, x: UInt64, y: UInt64) { + self.label = label + self.x = x + self.y = y + super.init(messageType: .iOSClickEvent) + } + + override init?(genericMessage: GenericMessage) { + do { + var offset = 0 + self.label = try genericMessage.body.readString(offset: &offset) + self.x = try genericMessage.body.readPrimary(offset: &offset) + self.y = try genericMessage.body.readPrimary(offset: &offset) + super.init(genericMessage: genericMessage) + } catch { + return nil + } + } + + override func contentData() -> Data { + return Data(values: UInt64(100), timestamp, Data(values: label, x, y)) + } + + override var description: String { + return "-->> IOSClickEvent(100): timestamp:\(timestamp) label:\(label) x:\(x) y:\(y)"; + } +} + +class ORIOSInputEvent: ORMessage { + let value: String + let valueMasked: Bool + let label: String + + init(value: String, valueMasked: Bool, label: String) { + self.value = value + self.valueMasked = valueMasked + self.label = label + super.init(messageType: .iOSInputEvent) + } + + override init?(genericMessage: GenericMessage) { + do { + var offset = 0 + self.value = try genericMessage.body.readString(offset: &offset) + self.valueMasked = try genericMessage.body.readPrimary(offset: &offset) + self.label = try genericMessage.body.readString(offset: &offset) + super.init(genericMessage: genericMessage) + } catch { + return nil + } + } + + override func contentData() -> Data { + return Data(values: UInt64(101), timestamp, Data(values: value, valueMasked, label)) + } + + override var description: String { + return "-->> IOSInputEvent(101): timestamp:\(timestamp) value:\(value) valueMasked:\(valueMasked) label:\(label)"; + } +} + +class ORIOSPerformanceEvent: ORMessage { + let name: String + let value: UInt64 + + init(name: String, value: UInt64) { + self.name = name + self.value = value + super.init(messageType: .iOSPerformanceEvent) + } + + override init?(genericMessage: GenericMessage) { + do { + var offset = 0 + self.name = try genericMessage.body.readString(offset: &offset) + self.value = try genericMessage.body.readPrimary(offset: &offset) + super.init(genericMessage: genericMessage) + } catch { + return nil + } + } + + override func contentData() -> Data { + return Data(values: UInt64(102), timestamp, Data(values: name, value)) + } + + override var description: String { + return "-->> IOSPerformanceEvent(102): timestamp:\(timestamp) name:\(name) value:\(value)"; + } +} + +class ORIOSLog: ORMessage { + let severity: String + let content: String + + init(severity: String, content: String) { + self.severity = severity + self.content = content + super.init(messageType: .iOSLog) + } + + override init?(genericMessage: GenericMessage) { + do { + var offset = 0 + self.severity = try genericMessage.body.readString(offset: &offset) + self.content = try genericMessage.body.readString(offset: &offset) + super.init(genericMessage: genericMessage) + } catch { + return nil + } + } + + override func contentData() -> Data { + return Data(values: UInt64(103), timestamp, Data(values: severity, content)) + } + + override var description: String { + return "-->> IOSLog(103): timestamp:\(timestamp) severity:\(severity) content:\(content)"; + } +} + +class ORIOSInternalError: ORMessage { + let content: String + + init(content: String) { + self.content = content + super.init(messageType: .iOSInternalError) + } + + override init?(genericMessage: GenericMessage) { + do { + var offset = 0 + self.content = try genericMessage.body.readString(offset: &offset) + super.init(genericMessage: genericMessage) + } catch { + return nil + } + } + + override func contentData() -> Data { + return Data(values: UInt64(104), timestamp, Data(values: content)) + } + + override var description: String { + return "-->> IOSInternalError(104): timestamp:\(timestamp) content:\(content)"; + } +} + +class ORIOSNetworkCall: ORMessage { + let type: String + let method: String + let URL: String + let request: String + let response: String + let status: UInt64 + let duration: UInt64 + + init(type: String, method: String, URL: String, request: String, response: String, status: UInt64, duration: UInt64) { + self.type = type + self.method = method + self.URL = URL + self.request = request + self.response = response + self.status = status + self.duration = duration + super.init(messageType: .iOSNetworkCall) + } + + override init?(genericMessage: GenericMessage) { + do { + var offset = 0 + self.type = try genericMessage.body.readString(offset: &offset) + self.method = try genericMessage.body.readString(offset: &offset) + self.URL = try genericMessage.body.readString(offset: &offset) + self.request = try genericMessage.body.readString(offset: &offset) + self.response = try genericMessage.body.readString(offset: &offset) + self.status = try genericMessage.body.readPrimary(offset: &offset) + self.duration = try genericMessage.body.readPrimary(offset: &offset) + super.init(genericMessage: genericMessage) + } catch { + return nil + } + } + + override func contentData() -> Data { + return Data(values: UInt64(105), timestamp, Data(values: type, method, URL, request, response, status, duration)) + } + + override var description: String { + return "-->> IOSNetworkCall(105): timestamp:\(timestamp) type:\(type) method:\(method) URL:\(URL) request:\(request) response:\(response) status:\(status) duration:\(duration)"; + } +} + +class ORIOSSwipeEvent: ORMessage { + let label: String + let x: UInt64 + let y: UInt64 + let direction: String + + init(label: String, x: UInt64, y: UInt64, direction: String) { + self.label = label + self.x = x + self.y = y + self.direction = direction + super.init(messageType: .iOSSwipeEvent) + } + + override init?(genericMessage: GenericMessage) { + do { + var offset = 0 + self.label = try genericMessage.body.readString(offset: &offset) + self.x = try genericMessage.body.readPrimary(offset: &offset) + self.y = try genericMessage.body.readPrimary(offset: &offset) + self.direction = try genericMessage.body.readString(offset: &offset) + super.init(genericMessage: genericMessage) + } catch { + return nil + } + } + + override func contentData() -> Data { + return Data(values: UInt64(106), timestamp, Data(values: label, x, y, direction)) + } + + override var description: String { + return "-->> IOSSwipeEvent(106): timestamp:\(timestamp) label:\(label) x:\(x) y:\(y) direction:\(direction)"; + } +} + +class ORIOSBatchMeta: ORMessage { + let firstIndex: UInt64 + + init(firstIndex: UInt64) { + self.firstIndex = firstIndex + super.init(messageType: .iOSBatchMeta) + } + + override init?(genericMessage: GenericMessage) { + do { + var offset = 0 + self.firstIndex = try genericMessage.body.readPrimary(offset: &offset) + super.init(genericMessage: genericMessage) + } catch { + return nil + } + } + + override func contentData() -> Data { + return Data(values: UInt64(107), timestamp, Data(values: firstIndex)) + } + + override var description: String { + return "-->> IOSBatchMeta(107): timestamp:\(timestamp) firstIndex:\(firstIndex)"; + } +} + diff --git a/tracker/tracker-ios/Sources/ORTracker/Models/script/messages.erb b/tracker/tracker-ios/Sources/ORTracker/Models/script/messages.erb new file mode 100644 index 000000000..974c0f68f --- /dev/null +++ b/tracker/tracker-ios/Sources/ORTracker/Models/script/messages.erb @@ -0,0 +1,37 @@ + +// Auto-generated, do not edit +import UIKit + +enum ORMessageType: UInt64 { +<%= $messages.map { |msg| " case #{msg.name.first_lower} = #{msg.id}" }.join "\n" %> +} +<% $messages.each do |msg| %> +class OR<%= msg.name.to_s.camel_case %>: ORMessage { +<%= msg.attributes[2..-1].map { |attr| " let #{attr.property}: #{attr.type_swift}" }.join "\n" %> + + init(<%= msg.attributes[2..-1].map { |attr| "#{attr.property}: #{attr.type_swift}" }.join ", " %>) { +<%= msg.attributes[2..-1].map { |attr| " self.#{attr.property} = #{attr.property}" }.join "\n" %> + super.init(messageType: .<%= "#{msg.name.first_lower}" %>) + } + + override init?(genericMessage: GenericMessage) { + <% if msg.attributes.length > 2 %> do { + var offset = 0 +<%= msg.attributes[2..-1].map { |attr| " self.#{attr.property} = try genericMessage.body.read#{attr.type_swift_read}(offset: &offset)" }.join "\n" %> + super.init(genericMessage: genericMessage) + } catch { + return nil + } + <% else %> + super.init(genericMessage: genericMessage) + <% end %>} + + override func contentData() -> Data { + return Data(values: UInt64(<%= "#{msg.id}"%>), timestamp<% if msg.attributes.length > 2 %>, Data(values: <%= msg.attributes[2..-1].map { |attr| attr.property }.join ", "%>)<% end %>) + } + + override var description: String { + return "-->> <%= msg.name.to_s.camel_case %>(<%= "#{msg.id}"%>): timestamp:\(timestamp) <%= msg.attributes[2..-1].map { |attr| "#{attr.property}:\\(#{attr.property})" }.join " "%>"; + } +} +<% end %> diff --git a/tracker/tracker-ios/Sources/ORTracker/Models/script/messages.rb b/tracker/tracker-ios/Sources/ORTracker/Models/script/messages.rb new file mode 100644 index 000000000..b163323ae --- /dev/null +++ b/tracker/tracker-ios/Sources/ORTracker/Models/script/messages.rb @@ -0,0 +1,129 @@ +message 92, 'IOSMetadata' do + uint 'Timestamp' + uint 'Length' + string 'Key' + string 'Value' +end + +message 93, 'IOSEvent' do + uint 'Timestamp' + uint 'Length' + string 'Name' + string 'Payload' +end + +message 94, 'IOSUserID' do + uint 'Timestamp' + uint 'Length' + string 'ID' +end + +message 95, 'IOSUserAnonymousID' do + uint 'Timestamp' + uint 'Length' + string 'ID' +end + +message 96, 'IOSScreenChanges' do + uint 'Timestamp' + uint 'Length' + uint 'X' + uint 'Y' + uint 'Width' + uint 'Height' +end + +message 97, 'IOSCrash' do + uint 'Timestamp' + uint 'Length' + string 'Name' + string 'Reason' + string 'Stacktrace' +end + +message 98, 'IOSViewComponentEvent' do + uint 'Timestamp' + uint 'Length' + string 'ScreenName' + string 'ViewName' + boolean 'Visible' +end + + +message 100, 'IOSClickEvent' do + uint 'Timestamp' + uint 'Length' + string 'Label' + uint 'X' + uint 'Y' +end + +message 101, 'IOSInputEvent' do + uint 'Timestamp' + uint 'Length' + string 'Value' + boolean 'ValueMasked' + string 'Label' +end + +=begin +Name/Value may be : +"physicalMemory": Total memory in bytes +"processorCount": Total processors in device +"activeProcessorCount": Number of currently used processors +"systemUptime": Elapsed time (in seconds) since last boot +"isLowPowerModeEnabled": Possible values (1 or 0) +"thermalState": Possible values (0:nominal 1:fair 2:serious 3:critical) +"batteryLevel": Possible values (0 .. 100) +"batteryState": Possible values (0:unknown 1:unplugged 2:charging 3:full) +"orientation": Possible values (0unknown 1:portrait 2:portraitUpsideDown 3:landscapeLeft 4:landscapeRight 5:faceUp 6:faceDown) +"mainThreadCPU": Possible values (0 .. 100) +"memoryUsage": Used memory in bytes +"fps": Frames per second +=end +message 102, 'IOSPerformanceEvent' do + uint 'Timestamp' + uint 'Length' + string 'Name' + uint 'Value' +end + +message 103, 'IOSLog' do + uint 'Timestamp' + uint 'Length' + string 'Severity' # Possible values ("info", "error") + string 'Content' +end + +message 104, 'IOSInternalError' do + uint 'Timestamp' + uint 'Length' + string 'Content' +end + +message 105, 'IOSNetworkCall' do + uint 'Timestamp' + uint 'Length' + string 'Type' + string 'Method' + string 'URL' + string 'Request' + string 'Response' + uint 'Status' + uint 'Duration' +end + +message 106, 'IOSSwipeEvent' do + uint 'Timestamp' + uint 'Length' + string 'Label' + uint 'X' + uint 'Y' + string 'Direction' +end + +message 107, 'IOSBatchMeta' do + uint 'Timestamp' + uint 'Length' + uint 'FirstIndex' +end diff --git a/tracker/tracker-ios/Sources/ORTracker/Models/script/run.rb b/tracker/tracker-ios/Sources/ORTracker/Models/script/run.rb new file mode 100644 index 000000000..3efccf957 --- /dev/null +++ b/tracker/tracker-ios/Sources/ORTracker/Models/script/run.rb @@ -0,0 +1,114 @@ +require 'erb' + +class String + def camel_case + return self if self !~ /_/ && self =~ /[A-Z]+.*/ + split('_').map{|e| e.capitalize}.join.upperize + end + + def camel_case_lower + self.split('_').inject([]) do |buffer, e| + word = if e.downcase == "url" + "URL" + else + buffer.empty? ? e : e.capitalize + end + buffer.push(word) + end.join.upperize + end + + def upperize + self.sub('Id', 'ID').sub('Url', 'URL').sub('url', 'URL') + end + + def first_lower + return "URL" if self == "URL" + "" if self == "" + self[0].downcase + self[1..-1] + end + + def underscore + self.gsub(/::/, '/'). + gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2'). + gsub(/([a-z\d])([A-Z])/,'\1_\2'). + tr("-", "_"). + downcase + end +end + +class Attribute + attr_reader :name, :type + def initialize(name:, type:) + @name = name + @type = type + end + + def property + @name.first_lower + end + + def type_swift + case @type + when :string + "String" + when :data + "Data" + when :uint + "UInt64" + when :boolean + "Bool" + else + "Primary" + end + end + + def type_swift_read + case @type + when :string + "String" + when :data + "Data" + else + "Primary" + end + end +end + +class Message + attr_reader :id, :name, :js, :replayer, :attributes + def initialize(name:, id:, js: true, replayer: true, &block) + @id = id + @name = name + @js = js + @replayer = replayer + @attributes = [] + instance_eval &block + end + + %i(uint string data boolean).each do |type| + define_method type do |name, opts = {}| + opts.merge!( + name: name, + type: type, + ) + @attributes << Attribute.new(opts) + end + end +end + +$ids = [] +$messages = [] +def message(id, name, opts = {}, &block) + raise "id duplicated #{name}" if $ids.include? id + raise "id is too big #{name}" if id > 120 + $ids << id + opts[:id] = id + opts[:name] = name + msg = Message.new(opts, &block) + $messages << msg +end + +require './messages.rb' + +e = ERB.new(File.read('./messages.erb')) +File.write('ORMessages.swift', e.result) diff --git a/tracker/tracker-ios/Sources/ORTracker/ORTracker.swift b/tracker/tracker-ios/Sources/ORTracker/ORTracker.swift new file mode 100644 index 000000000..295c730a9 --- /dev/null +++ b/tracker/tracker-ios/Sources/ORTracker/ORTracker.swift @@ -0,0 +1,162 @@ +import UIKit +import Network + +public enum CheckState { + case unchecked + case canStart + case cantStart +} + +open class ORTracker: NSObject { + @objc public static let shared = ORTracker() + public let userDefaults = UserDefaults(suiteName: "io.asayer.AsayerSDK-defaults") + public var projectKey: String? + public var trackerState = CheckState.unchecked + private var networkCheckTimer: Timer? + public var serverURL: String { + get { NetworkManager.shared.baseUrl } + set { NetworkManager.shared.baseUrl = newValue } + } + public var options: OROptions = OROptions.defaults + + @objc open func start(projectKey: String, options: OROptions) { + self.options = options + let monitor = NWPathMonitor() + let q = DispatchQueue.global(qos: .background) + + self.projectKey = projectKey + + monitor.start(queue: q) + + monitor.pathUpdateHandler = { path in + if path.usesInterfaceType(.wifi) { + if PerformanceListener.shared.isActive { + PerformanceListener.shared.networkStateChange(1) + } + self.trackerState = CheckState.canStart + } else if path.usesInterfaceType(.cellular) { + if PerformanceListener.shared.isActive { + PerformanceListener.shared.networkStateChange(0) + } + if options.wifiOnly { + self.trackerState = CheckState.cantStart + print("Connected to Cellular and options.wifiOnly is true. ORTracker will not start.") + } else { + self.trackerState = CheckState.canStart + } + } else { + self.trackerState = CheckState.cantStart + print("Not connected to either WiFi or Cellular. ORTracker will not start.") + } + } + + networkCheckTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true, block: { (_) in + if self.trackerState == CheckState.canStart { + self.startSession(projectKey: projectKey, options: options) + self.networkCheckTimer?.invalidate() + } + if self.trackerState == CheckState.cantStart { + self.networkCheckTimer?.invalidate() + } + }) + } + + @objc open func startSession(projectKey: String, options: OROptions) { + self.projectKey = projectKey + ORSessionRequest.create() { sessionResponse in + guard let sessionResponse = sessionResponse else { return print("Openreplay: no response from /start request") } + let captureSettings = getCaptureSettings(fps: sessionResponse.fps, quality: sessionResponse.quality) + ScreenshotManager.shared.setSettings(settings: captureSettings) + + MessageCollector.shared.start() + + if options.logs { + LogsListener.shared.start() + } + + if options.crashes { + Crashs.shared.start() + } + + if options.performances { + PerformanceListener.shared.start() + } + + if options.screen { + ScreenshotManager.shared.start() + } + + if options.analytics { + Analytics.shared.start() + } + } + } + + @objc open func stop() { + MessageCollector.shared.stop() + ScreenshotManager.shared.stop() + Crashs.shared.stop() + PerformanceListener.shared.stop() + Analytics.shared.stop() + } + + @objc open func addIgnoredView(_ view: UIView) { + ScreenshotManager.shared.addSanitizedElement(view) + } + + @objc open func setMetadata(key: String, value: String) { + let message = ORIOSMetadata(key: key, value: value) + MessageCollector.shared.sendMessage(message) + } + + @objc open func event(name: String, object: NSObject?) { + event(name: name, payload: object as? Encodable) + } + + open func event(name: String, payload: Encodable?) { + var json = "" + if let payload = payload, + let data = payload.toJSONData(), + let jsonStr = String(data: data, encoding: .utf8) { + json = jsonStr + } + let message = ORIOSEvent(name: name, payload: json) + MessageCollector.shared.sendMessage(message) + } + + open func eventStr(name: String, payload: String?) { + let message = ORIOSEvent(name: name, payload: payload ?? "") + MessageCollector.shared.sendMessage(message) + } + + @objc open func setUserID(_ userID: String) { + let message = ORIOSUserID(iD: userID) + MessageCollector.shared.sendMessage(message) + } + + @objc open func userAnonymousID(_ userID: String) { + let message = ORIOSUserAnonymousID(iD: userID) + MessageCollector.shared.sendMessage(message) + } +} + + + +func getCaptureSettings(fps: Int, quality: String) -> (captureRate: Double, imgCompression: Double) { + let limitedFPS = min(max(fps, 1), 99) + let captureRate = 1.0 / Double(limitedFPS) + + var imgCompression: Double + switch quality.lowercased() { + case "low": + imgCompression = 0.4 + case "standard": + imgCompression = 0.5 + case "high": + imgCompression = 0.6 + default: + imgCompression = 0.5 // default to standard if quality string is not recognized + } + + return (captureRate: captureRate, imgCompression: imgCompression) +} diff --git a/tracker/tracker-ios/Sources/ORTracker/ScreenCapture/ScreenCapture.swift b/tracker/tracker-ios/Sources/ORTracker/ScreenCapture/ScreenCapture.swift new file mode 100644 index 000000000..34f3a65c6 --- /dev/null +++ b/tracker/tracker-ios/Sources/ORTracker/ScreenCapture/ScreenCapture.swift @@ -0,0 +1,286 @@ +import UIKit +import Foundation +import SwiftUI +import SWCompression + +// MARK: - screenshot manager +open class ScreenshotManager { + public static let shared = ScreenshotManager() + private let messagesQueue = OperationQueue() + + private var timer: Timer? + private var sendTimer: Timer? + + private var sanitizedElements: [Sanitizable] = [] + private var observedInputs: [UITextField] = [] + private var screenshots: [Data] = [] + private var lastIndex = 0 + // MARK: capture settings + // should we blur out sensitive views, or place a solid box on top + private var isBlurMode = true + private var blurRadius = 2.5 + // this affects how big the image will be compared to real phone screan. + // we also can use default UIScreen.main.scale which is around 3.0 (dense pixel screen) + private var screenScale = 1.25 + private var settings: (captureRate: Double, imgCompression: Double) = (captureRate: 0.33, imgCompression: 0.5) + + private init() { } + + func start() { + startTakingScreenshots(every: settings.captureRate) + } + + func setSettings(settings: (captureRate: Double, imgCompression: Double)) { + self.settings = settings + } + + func stop() { + timer?.invalidate() + timer = nil + lastIndex = 0 + screenshots.removeAll() + } + + func startTakingScreenshots(every interval: TimeInterval) { + takeScreenshot() + + timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in + self?.takeScreenshot() + } + } + + public func addSanitizedElement(_ element: Sanitizable) { + #if DEBUG + DebugUtils.log("called add") + #endif + sanitizedElements.append(element) + } + + public func removeSanitizedElement(_ element: Sanitizable) { + #if DEBUG + DebugUtils.log("called remove") + #endif + sanitizedElements.removeAll { $0 as AnyObject === element as AnyObject } + } + + // MARK: - UI Capturing + func takeScreenshot() { + let window = UIApplication.shared.windows.first { $0.isKeyWindow } + let size = window?.frame.size ?? CGSize.zero + UIGraphicsBeginImageContextWithOptions(size, false, screenScale) + guard let context = UIGraphicsGetCurrentContext() else { return } + + // Rendering current window in custom context + // 2nd option looks to be more precise +// window?.layer.render(in: context) + #warning("Can slow down the app depending on complexity of the UI tree") + window?.drawHierarchy(in: window?.bounds ?? CGRect.zero, afterScreenUpdates: false) + + // MARK: sanitize + // Sanitizing sensitive elements + if isBlurMode { + let stripeWidth: CGFloat = 5.0 + let stripeSpacing: CGFloat = 15.0 + let stripeColor: UIColor = .gray.withAlphaComponent(0.7) + + for element in sanitizedElements { + if let frame = element.frameInWindow { + let totalWidth = frame.size.width + let totalHeight = frame.size.height + let convertedFrame = CGRect( + x: frame.origin.x, + y: frame.origin.y, + width: frame.size.width, + height: frame.size.height + ) + let cropFrame = CGRect( + x: frame.origin.x * screenScale, + y: frame.origin.y * screenScale, + width: frame.size.width * screenScale, + height: frame.size.height * screenScale + ) + if let regionImage = UIGraphicsGetImageFromCurrentImageContext()?.cgImage?.cropping(to: cropFrame) { + let imageToBlur = UIImage(cgImage: regionImage, scale: screenScale, orientation: .up) + let blurredImage = imageToBlur.applyBlurWithRadius(blurRadius) + blurredImage?.draw(in: convertedFrame) + + context.saveGState() + UIRectClip(convertedFrame) + + // Draw diagonal lines within the clipped region + for x in stride(from: -totalHeight, to: totalWidth, by: stripeSpacing + stripeWidth) { + context.move(to: CGPoint(x: x + convertedFrame.minX, y: convertedFrame.minY)) + context.addLine(to: CGPoint(x: x + totalHeight + convertedFrame.minX, y: totalHeight + convertedFrame.minY)) + } + + context.setLineWidth(stripeWidth) + stripeColor.setStroke() + context.strokePath() + context.restoreGState() + + #if DEBUG + context.setStrokeColor(UIColor.black.cgColor) + context.setLineWidth(1) + context.stroke(convertedFrame) + #endif + } + } else { + removeSanitizedElement(element) + } + } + } else { + context.setFillColor(UIColor.blue.cgColor) + for element in sanitizedElements { + if let frame = element.frameInWindow { + context.fill(frame) + } + } + } + + // Get the resulting image + if let image = UIGraphicsGetImageFromCurrentImageContext() { + if let compressedData = image.jpegData(compressionQuality: self.settings.imgCompression) { + screenshots.append(compressedData) + if screenshots.count >= 10 { + self.sendScreenshots() + } + } + } + UIGraphicsEndImageContext() + } + + // Not using this because no idea how to sync it with the replay fps rn + //func onError() { + // takeScreenshot() + //} + + // MARK: - sending screenshots + func sendScreenshots() { + guard let sessionId = NetworkManager.shared.sessionId else { + return + } + let localFilePath = "/Users/nikitamelnikov/Desktop/session/" + let desktopURL = URL(fileURLWithPath: localFilePath) + var archiveName = "\(sessionId)-\(String(format: "%06d", self.lastIndex)).tar.gz" + let archiveURL = desktopURL.appendingPathComponent(archiveName) + + // Ensure the directory exists + let fileManager = FileManager.default + if !fileManager.fileExists(atPath: localFilePath) { + try? fileManager.createDirectory(at: desktopURL, withIntermediateDirectories: true, attributes: nil) + } + var combinedData = Data() + let images = screenshots + for (index, imageData) in screenshots.enumerated() { + combinedData.append(imageData) + #if DEBUG + let filename = "\(lastIndex)_\(index).jpeg" + let fileURL = desktopURL.appendingPathComponent(filename) + + do { + try imageData.write(to: fileURL) + } catch { + DebugUtils.log("Unexpected error: \(error).") + } + #endif + } + #if DEBUG + DebugUtils.log("saved image files in \(localFilePath)") + #endif + + messagesQueue.addOperation { + var entries: [TarEntry] = [] + for imageData in images { + let filename = "\(String(format: "%06d", self.lastIndex)).jpeg" + var tarEntry = TarContainer.Entry(info: .init(name: filename, type: .regular), data: imageData) + tarEntry.info.permissions = Permissions(rawValue: 420) + tarEntry.info.creationTime = Date() + tarEntry.info.modificationTime = Date() + + entries.append(tarEntry) + self.lastIndex+=1 + } + do { + let gzData = try GzipArchive.archive(data: TarContainer.create(from: entries)) + + #if DEBUG + try gzData.write(to: archiveURL) + DebugUtils.log("Archive saved to \(archiveURL.path)") + MessageCollector.shared.sendImagesBatch(batch: gzData, fileName: archiveName) + #else + MessageCollector.shared.sendImagesBatch(batch: gzData, fileName: archiveName) + #endif + } catch { + DebugUtils.log("Error writing tar.gz data: \(error)") + } + } + screenshots.removeAll() + } +} + +// MARK: making extensions for UI +struct SensitiveViewWrapperRepresentable: UIViewRepresentable { + @Binding var viewWrapper: SensitiveViewWrapper? + + func makeUIView(context: Context) -> SensitiveViewWrapper { + let wrapper = SensitiveViewWrapper() + viewWrapper = wrapper + return wrapper + } + + func updateUIView(_ uiView: SensitiveViewWrapper, context: Context) { } +} + +struct SensitiveModifier: ViewModifier { + @State private var viewWrapper: SensitiveViewWrapper? + + func body(content: Content) -> some View { + content + .background(SensitiveViewWrapperRepresentable(viewWrapper: $viewWrapper)) + } +} + +public extension View { + func sensitive() -> some View { + self.modifier(SensitiveModifier()) + } +} + +class SensitiveViewWrapper: UIView { + override func didMoveToSuperview() { + super.didMoveToSuperview() + if self.superview != nil { + ScreenshotManager.shared.addSanitizedElement(self) + } else { + ScreenshotManager.shared.removeSanitizedElement(self) + } + } +} + +class SensitiveTextField: UITextField { + override func didMoveToWindow() { + super.didMoveToWindow() + if self.window != nil { + ScreenshotManager.shared.addSanitizedElement(self) + } else { + ScreenshotManager.shared.removeSanitizedElement(self) + } + } +} + +// Protocol to make a UIView sanitizable +public protocol Sanitizable { + var frameInWindow: CGRect? { get } +} + + +func getCaptureSettings(for quality: RecordingQuality) -> (captureRate: Double, imgCompression: Double) { + switch quality { + case .Low: + return (captureRate: 1, imgCompression: 0.4) + case .Standard: + return (captureRate: 0.33, imgCompression: 0.5) + case .High: + return (captureRate: 0.20, imgCompression: 0.55) + } +} diff --git a/tracker/tracker-ios/Tests/ORTrackerTests/ORTrackerTests.swift b/tracker/tracker-ios/Tests/ORTrackerTests/ORTrackerTests.swift new file mode 100644 index 000000000..b31069599 --- /dev/null +++ b/tracker/tracker-ios/Tests/ORTrackerTests/ORTrackerTests.swift @@ -0,0 +1,11 @@ +import XCTest +@testable import ORTracker + +final class ORTrackerTests: XCTestCase { + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct + // results. +// XCTAssertEqual(ORTracker().text, "Hello, World!") + } +} diff --git a/tracker/tracker-reactnative/.editorconfig b/tracker/tracker-reactnative/.editorconfig new file mode 100644 index 000000000..65365be68 --- /dev/null +++ b/tracker/tracker-reactnative/.editorconfig @@ -0,0 +1,15 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + +[*] + +indent_style = space +indent_size = 2 + +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/tracker/tracker-reactnative/.gitignore b/tracker/tracker-reactnative/.gitignore new file mode 100644 index 000000000..9d17fc5dd --- /dev/null +++ b/tracker/tracker-reactnative/.gitignore @@ -0,0 +1,71 @@ +# OSX +# +.DS_Store + +# XDE +.expo/ +.env +example/.env +# VSCode +.vscode/ +jsconfig.json + +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate +project.xcworkspace + +# Android/IJ +# +.classpath +.cxx +.gradle +.idea +.project +.settings +local.properties +android.iml + +# Cocoapods +# +example/ios/Pods +Pods +# Ruby +example/vendor/ + +# node.js +# +node_modules/ +npm-debug.log +yarn-debug.log +yarn-error.log + +# BUCK +buck-out/ +\.buckd/ +android/app/libs +android/keystores/debug.keystore + +# Expo +.expo/ + +# Turborepo +.turbo/ + +# generated by bob +lib/ diff --git a/tracker/tracker-reactnative/.nvmrc b/tracker/tracker-reactnative/.nvmrc new file mode 100644 index 000000000..3f430af82 --- /dev/null +++ b/tracker/tracker-reactnative/.nvmrc @@ -0,0 +1 @@ +v18 diff --git a/tracker/tracker-reactnative/.watchmanconfig b/tracker/tracker-reactnative/.watchmanconfig new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/tracker/tracker-reactnative/.watchmanconfig @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/tracker/tracker-reactnative/.yarnrc b/tracker/tracker-reactnative/.yarnrc new file mode 100644 index 000000000..fedc0f117 --- /dev/null +++ b/tracker/tracker-reactnative/.yarnrc @@ -0,0 +1,3 @@ +# Override Yarn command so we can automatically setup the repo on running `yarn` + +yarn-path "scripts/bootstrap.js" diff --git a/tracker/tracker-reactnative/CODE_OF_CONDUCT.md b/tracker/tracker-reactnative/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..45d257b29 --- /dev/null +++ b/tracker/tracker-reactnative/CODE_OF_CONDUCT.md @@ -0,0 +1,133 @@ + +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +[INSERT CONTACT METHOD]. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/tracker/tracker-reactnative/CONTRIBUTING.md b/tracker/tracker-reactnative/CONTRIBUTING.md new file mode 100644 index 000000000..e4e8ec906 --- /dev/null +++ b/tracker/tracker-reactnative/CONTRIBUTING.md @@ -0,0 +1,114 @@ +# Contributing + +Contributions are always welcome, no matter how large or small! + +We want this community to be friendly and respectful to each other. Please follow it in all your interactions with the project. Before contributing, please read the [code of conduct](./CODE_OF_CONDUCT.md). + +## Development workflow + +To get started with the project, run `yarn` in the root directory to install the required dependencies for each package: + +```sh +yarn +``` + +> While it's possible to use [`npm`](https://github.com/npm/cli), the tooling is built around [`yarn`](https://classic.yarnpkg.com/), so you'll have an easier time if you use `yarn` for development. + +While developing, you can run the [example app](/example/) to test your changes. Any changes you make in your library's JavaScript code will be reflected in the example app without a rebuild. If you change any native code, then you'll need to rebuild the example app. + +To start the packager: + +```sh +yarn example start +``` + +To run the example app on Android: + +```sh +yarn example android +``` + +To run the example app on iOS: + +```sh +yarn example ios +``` + +Make sure your code passes TypeScript and ESLint. Run the following to verify: + +```sh +yarn typecheck +yarn lint +``` + +To fix formatting errors, run the following: + +```sh +yarn lint --fix +``` + +Remember to add tests for your change if possible. Run the unit tests by: + +```sh +yarn test +``` + +To edit the Objective-C or Swift files, open `example/ios/RntrackerExample.xcworkspace` in XCode and find the source files at `Pods > Development Pods > openreplay-reactnative`. + +To edit the Java or Kotlin files, open `example/android` in Android studio and find the source files at `openreplay-reactnative` under `Android`. + + +### Commit message convention + +We follow the [conventional commits specification](https://www.conventionalcommits.org/en) for our commit messages: + +- `fix`: bug fixes, e.g. fix crash due to deprecated method. +- `feat`: new features, e.g. add new method to the module. +- `refactor`: code refactor, e.g. migrate from class components to hooks. +- `docs`: changes into documentation, e.g. add usage example for the module.. +- `test`: adding or updating tests, e.g. add integration tests using detox. +- `chore`: tooling changes, e.g. change CI config. + +Our pre-commit hooks verify that your commit message matches this format when committing. + +### Linting and tests + +[ESLint](https://eslint.org/), [Prettier](https://prettier.io/), [TypeScript](https://www.typescriptlang.org/) + +We use [TypeScript](https://www.typescriptlang.org/) for type checking, [ESLint](https://eslint.org/) with [Prettier](https://prettier.io/) for linting and formatting the code, and [Jest](https://jestjs.io/) for testing. + +Our pre-commit hooks verify that the linter and tests pass when committing. + +### Publishing to npm + +We use [release-it](https://github.com/release-it/release-it) to make it easier to publish new versions. It handles common tasks like bumping version based on semver, creating tags and releases etc. + +To publish new versions, run the following: + +```sh +yarn release +``` + +### Scripts + +The `package.json` file contains various scripts for common tasks: + +- `yarn bootstrap`: setup project by installing all dependencies and pods. +- `yarn typecheck`: type-check files with TypeScript. +- `yarn lint`: lint files with ESLint. +- `yarn test`: run unit tests with Jest. +- `yarn example start`: start the Metro server for the example app. +- `yarn example android`: run the example app on Android. +- `yarn example ios`: run the example app on iOS. + +### Sending a pull request + +> **Working on your first pull request?** You can learn how from this _free_ series: [How to Contribute to an Open Source Project on GitHub](https://app.egghead.io/playlists/how-to-contribute-to-an-open-source-project-on-github). + +When you're sending a pull request: + +- Prefer small pull requests focused on one change. +- Verify that linters and tests are passing. +- Review the documentation to make sure it looks good. +- Follow the pull request template when opening a pull request. +- For pull requests that change the API or implementation, discuss with maintainers first by opening an issue. diff --git a/tracker/tracker-reactnative/LICENSE b/tracker/tracker-reactnative/LICENSE new file mode 100644 index 000000000..6a52dfa68 --- /dev/null +++ b/tracker/tracker-reactnative/LICENSE @@ -0,0 +1,44 @@ +Elastic License 2.0 (ELv2) + +**Acceptance** +By using the software, you agree to all of the terms and conditions below. + +**Copyright License** +The licensor grants you a non-exclusive, royalty-free, worldwide, non-sublicensable, non-transferable license to use, copy, distribute, make available, and prepare derivative works of the software, in each case subject to the limitations and conditions below + +**Limitations** +You may not provide the software to third parties as a hosted or managed service, where the service provides users with access to any substantial set of the features or functionality of the software. + +You may not move, change, disable, or circumvent the license key functionality in the software, and you may not remove or obscure any functionality in the software that is protected by the license key. + +You may not alter, remove, or obscure any licensing, copyright, or other notices of the licensor in the software. Any use of the licensor’s trademarks is subject to applicable law. + +**Patents** +The licensor grants you a license, under any patent claims the licensor can license, or becomes able to license, to make, have made, use, sell, offer for sale, import and have imported the software, in each case subject to the limitations and conditions in this license. This license does not cover any patent claims that you cause to be infringed by modifications or additions to the software. If you or your company make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company. + +**Notices** +You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms. + +If you modify the software, you must include in any modified copies of the software prominent notices stating that you have modified the software. + +**No Other Rights** +These terms do not imply any licenses other than those expressly granted in these terms. + +**Termination** +If you use the software in violation of these terms, such use is not licensed, and your licenses will automatically terminate. If the licensor provides you with a notice of your violation, and you cease all violation of this license no later than 30 days after you receive that notice, your licenses will be reinstated retroactively. However, if you violate these terms after such reinstatement, any additional violation of these terms will cause your licenses to terminate automatically and permanently. + +**No Liability** +As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim. + +**Definitions** +The *licensor* is the entity offering these terms, and the *software* is the software the licensor makes available under these terms, including any portion of it. + +*you* refers to the individual or entity agreeing to these terms. + +*your company* is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. *control* means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect. + +*your licenses* are all the licenses granted to you for the software under these terms. + +*use* means anything you do with the software requiring one of your licenses. + +*trademark* means trademarks, service marks, and similar rights. diff --git a/tracker/tracker-reactnative/README.md b/tracker/tracker-reactnative/README.md new file mode 100644 index 000000000..5a1f5e841 --- /dev/null +++ b/tracker/tracker-reactnative/README.md @@ -0,0 +1,32 @@ +# @openreplay/react-native + +Only iOS devices are supported right now. Regular View and empty callbacks are used for android devices, +so its safe to use with crossplatform apps. + +## Installation + +```sh +npm install @openreplay/react-native +``` + +## Usage + +```js +import { RntrackerView } from "@openreplay/react-native"; + +// ... + + +``` + +## Contributing + +See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow. + +## License + +MIT + +--- + +Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob) diff --git a/tracker/tracker-reactnative/android/build.gradle b/tracker/tracker-reactnative/android/build.gradle new file mode 100644 index 000000000..322fc69d0 --- /dev/null +++ b/tracker/tracker-reactnative/android/build.gradle @@ -0,0 +1,108 @@ +buildscript { + // Buildscript is evaluated before everything else so we can't use getExtOrDefault + def kotlin_version = rootProject.ext.has("kotlinVersion") ? rootProject.ext.get("kotlinVersion") : project.properties["Rntracker_kotlinVersion"] + + repositories { + google() + mavenCentral() + } + + dependencies { + classpath "com.android.tools.build:gradle:7.2.1" + // noinspection DifferentKotlinGradleVersion + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +def isNewArchitectureEnabled() { + return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true" +} + +apply plugin: "com.android.library" +apply plugin: "kotlin-android" + + +def appProject = rootProject.allprojects.find { it.plugins.hasPlugin('com.android.application') } + +if (isNewArchitectureEnabled()) { + apply plugin: "com.facebook.react" +} + +def getExtOrDefault(name) { + return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["Rntracker_" + name] +} + +def getExtOrIntegerDefault(name) { + return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["Rntracker_" + name]).toInteger() +} + +def supportsNamespace() { + def parsed = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.') + def major = parsed[0].toInteger() + def minor = parsed[1].toInteger() + + // Namespace support was added in 7.3.0 + if (major == 7 && minor >= 3) { + return true + } + + return major >= 8 +} + +android { + if (supportsNamespace()) { + namespace "com.rntracker" + + sourceSets { + main { + manifest.srcFile "src/main/AndroidManifestNew.xml" + } + } + } + + compileSdkVersion getExtOrIntegerDefault("compileSdkVersion") + + defaultConfig { + minSdkVersion getExtOrIntegerDefault("minSdkVersion") + targetSdkVersion getExtOrIntegerDefault("targetSdkVersion") + buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() + } + buildTypes { + release { + minifyEnabled false + } + } + + lintOptions { + disable "GradleCompatible" + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + +} + +repositories { + mavenCentral() + google() +} + +def kotlin_version = getExtOrDefault("kotlinVersion") + +dependencies { + // For < 0.71, this will be from the local maven repo + // For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin + //noinspection GradleDynamicVersion + implementation "com.facebook.react:react-native:+" + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" +} + +if (isNewArchitectureEnabled()) { + react { + jsRootDir = file("../src/") + libraryName = "RntrackerView" + codegenJavaPackageName = "com.rntracker" + } +} diff --git a/tracker/tracker-reactnative/android/gradle.properties b/tracker/tracker-reactnative/android/gradle.properties new file mode 100644 index 000000000..02ebfbf24 --- /dev/null +++ b/tracker/tracker-reactnative/android/gradle.properties @@ -0,0 +1,5 @@ +Rntracker_kotlinVersion=1.7.0 +Rntracker_minSdkVersion=21 +Rntracker_targetSdkVersion=31 +Rntracker_compileSdkVersion=31 +Rntracker_ndkversion=21.4.7075529 diff --git a/tracker/tracker-reactnative/android/src/main/AndroidManifest.xml b/tracker/tracker-reactnative/android/src/main/AndroidManifest.xml new file mode 100644 index 000000000..cf4c92684 --- /dev/null +++ b/tracker/tracker-reactnative/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/tracker/tracker-reactnative/android/src/main/AndroidManifestNew.xml b/tracker/tracker-reactnative/android/src/main/AndroidManifestNew.xml new file mode 100644 index 000000000..a2f47b605 --- /dev/null +++ b/tracker/tracker-reactnative/android/src/main/AndroidManifestNew.xml @@ -0,0 +1,2 @@ + + diff --git a/tracker/tracker-reactnative/android/src/main/java/com/rntracker/RntrackerPackage.kt b/tracker/tracker-reactnative/android/src/main/java/com/rntracker/RntrackerPackage.kt new file mode 100644 index 000000000..3d400e886 --- /dev/null +++ b/tracker/tracker-reactnative/android/src/main/java/com/rntracker/RntrackerPackage.kt @@ -0,0 +1,17 @@ +package com.rntracker + +import com.facebook.react.ReactPackage +import com.facebook.react.bridge.NativeModule +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.uimanager.ViewManager + + +class RntrackerPackage : ReactPackage { + override fun createNativeModules(reactContext: ReactApplicationContext): List { + return emptyList() + } + + override fun createViewManagers(reactContext: ReactApplicationContext): List> { + return listOf(RntrackerViewManager()) + } +} diff --git a/tracker/tracker-reactnative/android/src/main/java/com/rntracker/RntrackerViewManager.kt b/tracker/tracker-reactnative/android/src/main/java/com/rntracker/RntrackerViewManager.kt new file mode 100644 index 000000000..ba8f40c7e --- /dev/null +++ b/tracker/tracker-reactnative/android/src/main/java/com/rntracker/RntrackerViewManager.kt @@ -0,0 +1,20 @@ +package com.rntracker + +import android.graphics.Color +import android.view.View +import com.facebook.react.uimanager.SimpleViewManager +import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.uimanager.annotations.ReactProp + +class RntrackerViewManager : SimpleViewManager() { + override fun getName() = "RntrackerView" + + override fun createViewInstance(reactContext: ThemedReactContext): View { + return View(reactContext) + } + + @ReactProp(name = "color") + fun setColor(view: View, color: String) { + view.setBackgroundColor(Color.parseColor(color)) + } +} diff --git a/tracker/tracker-reactnative/babel.config.js b/tracker/tracker-reactnative/babel.config.js new file mode 100644 index 000000000..f842b77fc --- /dev/null +++ b/tracker/tracker-reactnative/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + presets: ['module:metro-react-native-babel-preset'], +}; diff --git a/tracker/tracker-reactnative/example/.bundle/config b/tracker/tracker-reactnative/example/.bundle/config new file mode 100644 index 000000000..848943bb5 --- /dev/null +++ b/tracker/tracker-reactnative/example/.bundle/config @@ -0,0 +1,2 @@ +BUNDLE_PATH: "vendor/bundle" +BUNDLE_FORCE_RUBY_PLATFORM: 1 diff --git a/tracker/tracker-reactnative/example/.gitignore b/tracker/tracker-reactnative/example/.gitignore new file mode 100644 index 000000000..4c49bd78f --- /dev/null +++ b/tracker/tracker-reactnative/example/.gitignore @@ -0,0 +1 @@ +.env diff --git a/tracker/tracker-reactnative/example/.watchmanconfig b/tracker/tracker-reactnative/example/.watchmanconfig new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/tracker/tracker-reactnative/example/.watchmanconfig @@ -0,0 +1 @@ +{} diff --git a/tracker/tracker-reactnative/example/Gemfile b/tracker/tracker-reactnative/example/Gemfile new file mode 100644 index 000000000..1fa2c2e1a --- /dev/null +++ b/tracker/tracker-reactnative/example/Gemfile @@ -0,0 +1,6 @@ +source 'https://rubygems.org' + +# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version +ruby ">= 2.6.10" + +gem 'cocoapods', '~> 1.12' diff --git a/tracker/tracker-reactnative/example/README.md b/tracker/tracker-reactnative/example/README.md new file mode 100644 index 000000000..12470c30e --- /dev/null +++ b/tracker/tracker-reactnative/example/README.md @@ -0,0 +1,79 @@ +This is a new [**React Native**](https://reactnative.dev) project, bootstrapped using [`@react-native-community/cli`](https://github.com/react-native-community/cli). + +# Getting Started + +>**Note**: Make sure you have completed the [React Native - Environment Setup](https://reactnative.dev/docs/environment-setup) instructions till "Creating a new application" step, before proceeding. + +## Step 1: Start the Metro Server + +First, you will need to start **Metro**, the JavaScript _bundler_ that ships _with_ React Native. + +To start Metro, run the following command from the _root_ of your React Native project: + +```bash +# using npm +npm start + +# OR using Yarn +yarn start +``` + +## Step 2: Start your Application + +Let Metro Bundler run in its _own_ terminal. Open a _new_ terminal from the _root_ of your React Native project. Run the following command to start your _Android_ or _iOS_ app: + +### For Android + +```bash +# using npm +npm run android + +# OR using Yarn +yarn android +``` + +### For iOS + +```bash +# using npm +npm run ios + +# OR using Yarn +yarn ios +``` + +If everything is set up _correctly_, you should see your new app running in your _Android Emulator_ or _iOS Simulator_ shortly provided you have set up your emulator/simulator correctly. + +This is one way to run your app — you can also run it directly from within Android Studio and Xcode respectively. + +## Step 3: Modifying your App + +Now that you have successfully run the app, let's modify it. + +1. Open `App.tsx` in your text editor of choice and edit some lines. +2. For **Android**: Press the R key twice or select **"Reload"** from the **Developer Menu** (Ctrl + M (on Window and Linux) or Cmd ⌘ + M (on macOS)) to see your changes! + + For **iOS**: Hit Cmd ⌘ + R in your iOS Simulator to reload the app and see your changes! + +## Congratulations! :tada: + +You've successfully run and modified your React Native App. :partying_face: + +### Now what? + +- If you want to add this new React Native code to an existing application, check out the [Integration guide](https://reactnative.dev/docs/integration-with-existing-apps). +- If you're curious to learn more about React Native, check out the [Introduction to React Native](https://reactnative.dev/docs/getting-started). + +# Troubleshooting + +If you can't get this to work, see the [Troubleshooting](https://reactnative.dev/docs/troubleshooting) page. + +# Learn More + +To learn more about React Native, take a look at the following resources: + +- [React Native Website](https://reactnative.dev) - learn more about React Native. +- [Getting Started](https://reactnative.dev/docs/environment-setup) - an **overview** of React Native and how setup your environment. +- [Learn the Basics](https://reactnative.dev/docs/getting-started) - a **guided tour** of the React Native **basics**. +- [Blog](https://reactnative.dev/blog) - read the latest official React Native **Blog** posts. +- [`@facebook/react-native`](https://github.com/facebook/react-native) - the Open Source; GitHub **repository** for React Native. diff --git a/tracker/tracker-reactnative/example/android/app/build.gradle b/tracker/tracker-reactnative/example/android/app/build.gradle new file mode 100644 index 000000000..943a22761 --- /dev/null +++ b/tracker/tracker-reactnative/example/android/app/build.gradle @@ -0,0 +1,123 @@ +apply plugin: "com.android.application" +apply plugin: "com.facebook.react" + +/** + * This is the configuration block to customize your React Native Android app. + * By default you don't need to apply any configuration, just uncomment the lines you need. + */ +react { + /* Folders */ + // The root of your project, i.e. where "package.json" lives. Default is '..' + // root = file("../") + // The folder where the react-native NPM package is. Default is ../node_modules/react-native + // reactNativeDir = file("../node_modules/react-native") + // The folder where the react-native Codegen package is. Default is ../node_modules/@react-native/codegen + // codegenDir = file("../node_modules/@react-native/codegen") + // The cli.js file which is the React Native CLI entrypoint. Default is ../node_modules/react-native/cli.js + // cliFile = file("../node_modules/react-native/cli.js") + + /* Variants */ + // The list of variants to that are debuggable. For those we're going to + // skip the bundling of the JS bundle and the assets. By default is just 'debug'. + // If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants. + // debuggableVariants = ["liteDebug", "prodDebug"] + + /* Bundling */ + // A list containing the node command and its flags. Default is just 'node'. + // nodeExecutableAndArgs = ["node"] + // + // The command to run when bundling. By default is 'bundle' + // bundleCommand = "ram-bundle" + // + // The path to the CLI configuration file. Default is empty. + // bundleConfig = file(../rn-cli.config.js) + // + // The name of the generated asset file containing your JS bundle + // bundleAssetName = "MyApplication.android.bundle" + // + // The entry file for bundle generation. Default is 'index.android.js' or 'index.js' + // entryFile = file("../js/MyApplication.android.js") + // + // A list of extra flags to pass to the 'bundle' commands. + // See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle + // extraPackagerArgs = [] + + /* Hermes Commands */ + // The hermes compiler command to run. By default it is 'hermesc' + // hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc" + // + // The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map" + // hermesFlags = ["-O", "-output-source-map"] +} + +/** + * Set this to true to Run Proguard on Release builds to minify the Java bytecode. + */ +def enableProguardInReleaseBuilds = false + +/** + * The preferred build flavor of JavaScriptCore (JSC) + * + * For example, to use the international variant, you can use: + * `def jscFlavor = 'org.webkit:android-jsc-intl:+'` + * + * The international variant includes ICU i18n library and necessary data + * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that + * give correct results when using with locales other than en-US. Note that + * this variant is about 6MiB larger per architecture than default. + */ +def jscFlavor = 'org.webkit:android-jsc:+' + +android { + ndkVersion rootProject.ext.ndkVersion + + compileSdkVersion rootProject.ext.compileSdkVersion + + namespace "com.rntrackerexample" + defaultConfig { + applicationId "com.rntrackerexample" + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + versionCode 1 + versionName "1.0" + } + signingConfigs { + debug { + storeFile file('debug.keystore') + storePassword 'android' + keyAlias 'androiddebugkey' + keyPassword 'android' + } + } + buildTypes { + debug { + signingConfig signingConfigs.debug + } + release { + // Caution! In production, you need to generate your own keystore file. + // see https://reactnative.dev/docs/signed-apk-android. + signingConfig signingConfigs.debug + minifyEnabled enableProguardInReleaseBuilds + proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" + } + } +} + +dependencies { + // The version of react-native is set by the React Native Gradle Plugin + implementation("com.facebook.react:react-android") + + debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") + debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") { + exclude group:'com.squareup.okhttp3', module:'okhttp' + } + + debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}") + if (hermesEnabled.toBoolean()) { + implementation("com.facebook.react:hermes-android") + } else { + implementation jscFlavor + } +} + +apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project) diff --git a/tracker/tracker-reactnative/example/android/app/debug.keystore b/tracker/tracker-reactnative/example/android/app/debug.keystore new file mode 100644 index 0000000000000000000000000000000000000000..364e105ed39fbfd62001429a68140672b06ec0de GIT binary patch literal 2257 zcmchYXEfYt8;7T1^dLH$VOTZ%2NOdOH5j5LYLtZ0q7x-V8_6gU5)#7dkq{HTmsfNq zB3ZqcAxeY^G10@?efK?Q&)M(qInVv!xjx+IKEL}p*K@LYvIzo#AZG>st5|P)KF1_Z;y){W{<7K{nl!CPuE z_^(!C(Ol0n8 zK13*rzAtW>(wULKPRYLd7G18F8#1P`V*9`(Poj26eOXYyBVZPno~Cvvhx7vPjAuZo zF?VD!zB~QG(!zbw#qsxT8%BSpqMZ4f70ZPn-3y$L8{EVbbN9$H`B&Z1quk9tgp5FM zuxp3pJ0b8u|3+#5bkJ4SRnCF2l7#DyLYXYY8*?OuAwK4E6J{0N=O3QNVzQ$L#FKkR zi-c@&!nDvezOV$i$Lr}iF$XEcwnybQ6WZrMKuw8gCL^U#D;q3t&HpTbqyD%vG=TeDlzCT~MXUPC|Leb-Uk+ z=vnMd(|>ld?Fh>V8poP;q;;nc@en$|rnP0ytzD&fFkCeUE^kG9Kx4wUh!!rpjwKDP zyw_e|a^x_w3E zP}}@$g>*LLJ4i0`Gx)qltL}@;mDv}D*xR^oeWcWdPkW@Uu)B^X&4W1$p6}ze!zudJ zyiLg@uggoMIArBr*27EZV7djDg@W1MaL+rcZ-lrANJQ%%>u8)ZMWU@R2qtnmG(acP z0d_^!t>}5W zpT`*2NR+0+SpTHb+6Js4b;%LJB;B_-ChhnU5py}iJtku*hm5F0!iql8Hrpcy1aYbT z1*dKC5ua6pMX@@iONI?Hpr%h;&YaXp9n!ND7-=a%BD7v&g zOO41M6EbE24mJ#S$Ui0-brR5ML%@|ndz^)YLMMV1atna{Fw<;TF@>d&F|!Z>8eg>>hkFrV)W+uv=`^F9^e zzzM2*oOjT9%gLoub%(R57p-`TXFe#oh1_{&N-YN z<}artH|m=d8TQuKSWE)Z%puU|g|^^NFwC#N=@dPhasyYjoy(fdEVfKR@cXKHZV-`06HsP`|Ftx;8(YD$fFXumLWbGnu$GMqRncXYY9mwz9$ap zQtfZB^_BeNYITh^hA7+(XNFox5WMeG_LtJ%*Q}$8VKDI_p8^pqX)}NMb`0e|wgF7D zuQACY_Ua<1ri{;Jwt@_1sW9zzdgnyh_O#8y+C;LcZq6=4e^cs6KvmK@$vVpKFGbQ= z$)Eux5C|Fx;Gtmv9^#Y-g@7Rt7*eLp5n!gJmn7&B_L$G?NCN`AP>cXQEz}%F%K;vUs{+l4Q{}eWW;ATe2 zqvXzxoIDy(u;F2q1JH7Sf;{jy_j})F+cKlIOmNfjBGHoG^CN zM|Ho&&X|L-36f}Q-obEACz`sI%2f&k>z5c$2TyTSj~vmO)BW~+N^kt`Jt@R|s!){H ze1_eCrlNaPkJQhL$WG&iRvF*YG=gXd1IyYQ9ew|iYn7r~g!wOnw;@n42>enAxBv*A zEmV*N#sxdicyNM=A4|yaOC5MByts}s_Hpfj|y<6G=o=!3S@eIFKDdpR7|FY>L&Wat&oW&cm&X~ z5Bt>Fcq(fgnvlvLSYg&o6>&fY`ODg4`V^lWWD=%oJ#Kbad2u~! zLECFS*??>|vDsNR&pH=Ze0Eo`sC_G`OjoEKVHY|wmwlX&(XBE<@sx3Hd^gtd-fNwUHsylg06p`U2y_={u}Bc + + + + + + + + diff --git a/tracker/tracker-reactnative/example/android/app/src/debug/java/com/rntrackerexample/ReactNativeFlipper.java b/tracker/tracker-reactnative/example/android/app/src/debug/java/com/rntrackerexample/ReactNativeFlipper.java new file mode 100644 index 000000000..039e90e3c --- /dev/null +++ b/tracker/tracker-reactnative/example/android/app/src/debug/java/com/rntrackerexample/ReactNativeFlipper.java @@ -0,0 +1,75 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + *

This source code is licensed under the MIT license found in the LICENSE file in the root + * directory of this source tree. + */ +package com.rntrackerexample; + +import android.content.Context; +import com.facebook.flipper.android.AndroidFlipperClient; +import com.facebook.flipper.android.utils.FlipperUtils; +import com.facebook.flipper.core.FlipperClient; +import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin; +import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin; +import com.facebook.flipper.plugins.fresco.FrescoFlipperPlugin; +import com.facebook.flipper.plugins.inspector.DescriptorMapping; +import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin; +import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor; +import com.facebook.flipper.plugins.network.NetworkFlipperPlugin; +import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin; +import com.facebook.react.ReactInstanceEventListener; +import com.facebook.react.ReactInstanceManager; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.modules.network.NetworkingModule; +import okhttp3.OkHttpClient; + +/** + * Class responsible of loading Flipper inside your React Native application. This is the debug + * flavor of it. Here you can add your own plugins and customize the Flipper setup. + */ +public class ReactNativeFlipper { + public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) { + if (FlipperUtils.shouldEnableFlipper(context)) { + final FlipperClient client = AndroidFlipperClient.getInstance(context); + + client.addPlugin(new InspectorFlipperPlugin(context, DescriptorMapping.withDefaults())); + client.addPlugin(new DatabasesFlipperPlugin(context)); + client.addPlugin(new SharedPreferencesFlipperPlugin(context)); + client.addPlugin(CrashReporterPlugin.getInstance()); + + NetworkFlipperPlugin networkFlipperPlugin = new NetworkFlipperPlugin(); + NetworkingModule.setCustomClientBuilder( + new NetworkingModule.CustomClientBuilder() { + @Override + public void apply(OkHttpClient.Builder builder) { + builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin)); + } + }); + client.addPlugin(networkFlipperPlugin); + client.start(); + + // Fresco Plugin needs to ensure that ImagePipelineFactory is initialized + // Hence we run if after all native modules have been initialized + ReactContext reactContext = reactInstanceManager.getCurrentReactContext(); + if (reactContext == null) { + reactInstanceManager.addReactInstanceEventListener( + new ReactInstanceEventListener() { + @Override + public void onReactContextInitialized(ReactContext reactContext) { + reactInstanceManager.removeReactInstanceEventListener(this); + reactContext.runOnNativeModulesQueueThread( + new Runnable() { + @Override + public void run() { + client.addPlugin(new FrescoFlipperPlugin()); + } + }); + } + }); + } else { + client.addPlugin(new FrescoFlipperPlugin()); + } + } + } +} diff --git a/tracker/tracker-reactnative/example/android/app/src/main/AndroidManifest.xml b/tracker/tracker-reactnative/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..4122f36a5 --- /dev/null +++ b/tracker/tracker-reactnative/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + diff --git a/tracker/tracker-reactnative/example/android/app/src/main/java/com/rntrackerexample/MainActivity.java b/tracker/tracker-reactnative/example/android/app/src/main/java/com/rntrackerexample/MainActivity.java new file mode 100644 index 000000000..aa9fa89e0 --- /dev/null +++ b/tracker/tracker-reactnative/example/android/app/src/main/java/com/rntrackerexample/MainActivity.java @@ -0,0 +1,32 @@ +package com.rntrackerexample; + +import com.facebook.react.ReactActivity; +import com.facebook.react.ReactActivityDelegate; +import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint; +import com.facebook.react.defaults.DefaultReactActivityDelegate; + +public class MainActivity extends ReactActivity { + + /** + * Returns the name of the main component registered from JavaScript. This is used to schedule + * rendering of the component. + */ + @Override + protected String getMainComponentName() { + return "RntrackerExample"; + } + + /** + * Returns the instance of the {@link ReactActivityDelegate}. Here we use a util class {@link + * DefaultReactActivityDelegate} which allows you to easily enable Fabric and Concurrent React + * (aka React 18) with two boolean flags. + */ + @Override + protected ReactActivityDelegate createReactActivityDelegate() { + return new DefaultReactActivityDelegate( + this, + getMainComponentName(), + // If you opted-in for the New Architecture, we enable the Fabric Renderer. + DefaultNewArchitectureEntryPoint.getFabricEnabled()); + } +} diff --git a/tracker/tracker-reactnative/example/android/app/src/main/java/com/rntrackerexample/MainApplication.java b/tracker/tracker-reactnative/example/android/app/src/main/java/com/rntrackerexample/MainApplication.java new file mode 100644 index 000000000..b693aa653 --- /dev/null +++ b/tracker/tracker-reactnative/example/android/app/src/main/java/com/rntrackerexample/MainApplication.java @@ -0,0 +1,62 @@ +package com.rntrackerexample; + +import android.app.Application; +import com.facebook.react.PackageList; +import com.facebook.react.ReactApplication; +import com.facebook.react.ReactNativeHost; +import com.facebook.react.ReactPackage; +import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint; +import com.facebook.react.defaults.DefaultReactNativeHost; +import com.facebook.soloader.SoLoader; +import java.util.List; + +public class MainApplication extends Application implements ReactApplication { + + private final ReactNativeHost mReactNativeHost = + new DefaultReactNativeHost(this) { + @Override + public boolean getUseDeveloperSupport() { + return BuildConfig.DEBUG; + } + + @Override + protected List getPackages() { + @SuppressWarnings("UnnecessaryLocalVariable") + List packages = new PackageList(this).getPackages(); + // Packages that cannot be autolinked yet can be added manually here, for example: + // packages.add(new MyReactNativePackage()); + return packages; + } + + @Override + protected String getJSMainModuleName() { + return "index"; + } + + @Override + protected boolean isNewArchEnabled() { + return BuildConfig.IS_NEW_ARCHITECTURE_ENABLED; + } + + @Override + protected Boolean isHermesEnabled() { + return BuildConfig.IS_HERMES_ENABLED; + } + }; + + @Override + public ReactNativeHost getReactNativeHost() { + return mReactNativeHost; + } + + @Override + public void onCreate() { + super.onCreate(); + SoLoader.init(this, /* native exopackage */ false); + if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { + // If you opted-in for the New Architecture, we load the native entry point for this app. + DefaultNewArchitectureEntryPoint.load(); + } + ReactNativeFlipper.initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); + } +} diff --git a/tracker/tracker-reactnative/example/android/app/src/main/res/drawable/rn_edit_text_material.xml b/tracker/tracker-reactnative/example/android/app/src/main/res/drawable/rn_edit_text_material.xml new file mode 100644 index 000000000..73b37e4d9 --- /dev/null +++ b/tracker/tracker-reactnative/example/android/app/src/main/res/drawable/rn_edit_text_material.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + diff --git a/tracker/tracker-reactnative/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/tracker/tracker-reactnative/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..a2f5908281d070150700378b64a84c7db1f97aa1 GIT binary patch literal 3056 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 axPAxe%7rDusX+w*00006jj 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⁣ zwiG+x#%j}{0gU~J8;<|!B1@-XaB@{KORFwrYg_8rOv({b0EO#DbeQRm;B6_9=mXGf z-x|VL{zd`)#@yN}HkCSJbjbNlE|zL3Wm9Q8HY`sV)}3%pgN>cL^67{Z;PPL(*wT8N zUjXU{@|*hvm}({wsAC=x0^ok0%UAz0;sogW{B!nDqk|JJ5x~4NfTDgP49^zeu`csl?5mY@JdQdISc zFs!E{^grmkLnUk9 zny~m)1vws@5BFI<-0Tuo2JWX(0v`W|t(wg;s--L47WTvTMz-8l#TL^=OJNRS2?_Qj z3AKT+gvbyBi#H*-tJ%tWD|>EV3wy|8qxfzS!5RW;Jpl5*zo&^UBU=fG#2}UvRyNkK zA06Dy9;K1ca@r2T>yThYgI!ont$(G{6q#2QT+00r_x0(b)gsE`lBB?2gr55gq^D3Fi&p%E(p9>U%bv zkg1Jco(RbyTX7FDHOnl7-O@ zI$AaIl?9NJKPm(WiBP`1-#CB1QzU>&hKm)fpa5DKE{2$X0hGz-0uZ?cyTk(YC!Y&| zL=1VrNERSA5NA2jq7FACfX4JfPyj5XXl1yv0>~s;eF7L2$>&oMqeTFT2m$y7FlkON z_yurD1yIOvA;5C6016pyxBznGUt0kJ&k5r#;&>Jow`r)sp9R~PmK~lz$3xH%LT*1U zJdOyABZ3!FvNoR*vN$5ykHS8f`jA4zV+|L}i1C4`B2c{R0;UdYxaU|H)2avz@ z=mEYc|2S<+(B2Tj+FkX+2D+yFI!k9lWMA61DJ{)e;lum$(;O87?vGJJe!KtK04+N_ zI*P~t@dUb>9Xh{dbyl{-ZQ(UMgz7$|QfL5XSPkskt^NgctYC#;4WcZB1@%@wy@2t3 z2z0DI7&%b$*Aw~abe?GxE`ez@+6hOh-6*8fHRV{1os$EL@}uUZeG4h1&Be`98q*7j z=3-v+lhIjfWVo12!<>%V^a6lTgW3+_#W6n|p*~==zOH7z$0{LSZk(Tpd7EaD04hnA zL;#fxS0aD{`5^&D`}>0Uq?byDD-l2=!wm_bLcUl4gc(% za1p|itVANvFF>hghAS07Im1;IK;|b*W)}VDyI;BIp2=K*yu2a)j?B|f<44NI$NbmJ z#dE0>jI$fMr&@>4kN8MLFb4&2O9fEKaQg%(QO$4_1rVQywG^CmBLh#}_7gKW3vd?| z2?1^&KWq8}8I^_S0|)MowU_pw$q@nl@Nkn$z>BQq_KA^9yaR`(R3u{{Ig;cwt z@AJ^{ODQCm^neroM9nKNUAXi9RCK`OsP_LuR0PUR(YZCCX5dNF6VzcoK&=b^r`W?ltt|*F zpkoae%ZT{C1h~EcFui~b7fF`vb<<~j_VquuUA$}QqIKYELPp#;{u?q8Dz}WAG-(3; zjrm$i%7UbyZMM(Y{>!uJ#vNB?R~B{6Htp=>e*<{fQQ5W7V(1coCWlOON!MzZxhum| ztZBQpGR z;~#ur^&PockKdV{Q6R>o`Pl{0x!DEbpZ7y9Y;*ZvE!*gU`V1W3znva{f=?WO5I&>B z&hw6}tjECtaghm5z|C#%M;Yf_*pI^};h}Vl=^r9EN=tVDj86D;C$jIJ?K7VP+00000NkvXXu0mjf D5i!M* literal 0 HcmV?d00001 diff --git a/tracker/tracker-reactnative/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/tracker/tracker-reactnative/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..459ca609d3ae0d3943ab44cdc27feef9256dc6d7 GIT binary patch literal 7098 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 literal 0 HcmV?d00001 diff --git a/tracker/tracker-reactnative/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/tracker/tracker-reactnative/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..8ca12fe024be86e868d14e91120a6902f8e88ac6 GIT binary patch literal 6464 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 B0*L?s literal 0 HcmV?d00001 diff --git a/tracker/tracker-reactnative/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/tracker/tracker-reactnative/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..8e19b410a1b15ff180f3dacac19395fe3046cdec GIT binary patch literal 10676 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 literal 0 HcmV?d00001 diff --git a/tracker/tracker-reactnative/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/tracker/tracker-reactnative/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..4c19a13c239cb67b8a2134ddd5f325db1d2d5bee GIT binary patch literal 15523 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 BIP(Ai literal 0 HcmV?d00001 diff --git a/tracker/tracker-reactnative/example/android/app/src/main/res/values/strings.xml b/tracker/tracker-reactnative/example/android/app/src/main/res/values/strings.xml new file mode 100644 index 000000000..ca009c7cc --- /dev/null +++ b/tracker/tracker-reactnative/example/android/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + RntrackerExample + diff --git a/tracker/tracker-reactnative/example/android/app/src/main/res/values/styles.xml b/tracker/tracker-reactnative/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 000000000..7ba83a2ad --- /dev/null +++ b/tracker/tracker-reactnative/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/tracker/tracker-reactnative/example/android/app/src/release/java/com/rntrackerexample/ReactNativeFlipper.java b/tracker/tracker-reactnative/example/android/app/src/release/java/com/rntrackerexample/ReactNativeFlipper.java new file mode 100644 index 000000000..1db5f1297 --- /dev/null +++ b/tracker/tracker-reactnative/example/android/app/src/release/java/com/rntrackerexample/ReactNativeFlipper.java @@ -0,0 +1,20 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + *

This source code is licensed under the MIT license found in the LICENSE file in the root + * directory of this source tree. + */ +package com.rntrackerexample; + +import android.content.Context; +import com.facebook.react.ReactInstanceManager; + +/** + * Class responsible of loading Flipper inside your React Native application. This is the release + * flavor of it so it's empty as we don't want to load Flipper. + */ +public class ReactNativeFlipper { + public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) { + // Do nothing as we don't want to initialize Flipper on Release. + } +} diff --git a/tracker/tracker-reactnative/example/android/build.gradle b/tracker/tracker-reactnative/example/android/build.gradle new file mode 100644 index 000000000..34ea71819 --- /dev/null +++ b/tracker/tracker-reactnative/example/android/build.gradle @@ -0,0 +1,21 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + ext { + buildToolsVersion = "33.0.0" + minSdkVersion = 21 + compileSdkVersion = 33 + targetSdkVersion = 33 + + // We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP. + ndkVersion = "23.1.7779620" + } + repositories { + google() + mavenCentral() + } + dependencies { + classpath("com.android.tools.build:gradle") + classpath("com.facebook.react:react-native-gradle-plugin") + } +} diff --git a/tracker/tracker-reactnative/example/android/gradle.properties b/tracker/tracker-reactnative/example/android/gradle.properties new file mode 100644 index 000000000..a3b2fa124 --- /dev/null +++ b/tracker/tracker-reactnative/example/android/gradle.properties @@ -0,0 +1,44 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m +org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true + +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Automatically convert third-party libraries to use AndroidX +android.enableJetifier=true + +# Version of flipper SDK to use with React Native +FLIPPER_VERSION=0.182.0 + +# Use this property to specify which architecture you want to build. +# You can also override it from the CLI using +# ./gradlew -PreactNativeArchitectures=x86_64 +reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 + +# Use this property to enable support to the new architecture. +# This will allow you to use TurboModules and the Fabric render in +# your application. You should enable this flag either if you want +# to write custom TurboModules/Fabric components OR use libraries that +# are providing them. +newArchEnabled=false + +# Use this property to enable or disable the Hermes JS engine. +# If set to false, you will be using JSC instead. +hermesEnabled=true diff --git a/tracker/tracker-reactnative/example/android/gradle/wrapper/gradle-wrapper.jar b/tracker/tracker-reactnative/example/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..943f0cbfa754578e88a3dae77fce6e3dea56edbf GIT binary patch literal 61574 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 ScNgau(U?ep-K_E5zy1%ZQTdPn literal 0 HcmV?d00001 diff --git a/tracker/tracker-reactnative/example/android/gradle/wrapper/gradle-wrapper.properties b/tracker/tracker-reactnative/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..6ec1567a0 --- /dev/null +++ b/tracker/tracker-reactnative/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.1-all.zip +networkTimeout=10000 +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/tracker/tracker-reactnative/example/android/gradlew b/tracker/tracker-reactnative/example/android/gradlew new file mode 100755 index 000000000..65dcd68d6 --- /dev/null +++ b/tracker/tracker-reactnative/example/android/gradlew @@ -0,0 +1,244 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/tracker/tracker-reactnative/example/android/gradlew.bat b/tracker/tracker-reactnative/example/android/gradlew.bat new file mode 100644 index 000000000..6689b85be --- /dev/null +++ b/tracker/tracker-reactnative/example/android/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/tracker/tracker-reactnative/example/android/settings.gradle b/tracker/tracker-reactnative/example/android/settings.gradle new file mode 100644 index 000000000..0ba6d84a1 --- /dev/null +++ b/tracker/tracker-reactnative/example/android/settings.gradle @@ -0,0 +1,4 @@ +rootProject.name = 'RntrackerExample' +apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings) +include ':app' +includeBuild('../node_modules/@react-native/gradle-plugin') diff --git a/tracker/tracker-reactnative/example/app.json b/tracker/tracker-reactnative/example/app.json new file mode 100644 index 000000000..32df6c498 --- /dev/null +++ b/tracker/tracker-reactnative/example/app.json @@ -0,0 +1,4 @@ +{ + "name": "RntrackerExample", + "displayName": "RntrackerExample" +} diff --git a/tracker/tracker-reactnative/example/babel.config.js b/tracker/tracker-reactnative/example/babel.config.js new file mode 100644 index 000000000..adea77bdb --- /dev/null +++ b/tracker/tracker-reactnative/example/babel.config.js @@ -0,0 +1,17 @@ +const path = require('path'); +const pak = require('../package.json'); + +module.exports = { + presets: ['module:metro-react-native-babel-preset'], + plugins: [ + [ + 'module-resolver', + { + extensions: ['.tsx', '.ts', '.js', '.json'], + alias: { + [pak.name]: path.join(__dirname, '..', pak.source), + }, + }, + ], + ], +}; diff --git a/tracker/tracker-reactnative/example/index.js b/tracker/tracker-reactnative/example/index.js new file mode 100644 index 000000000..117ddcae4 --- /dev/null +++ b/tracker/tracker-reactnative/example/index.js @@ -0,0 +1,5 @@ +import { AppRegistry } from 'react-native'; +import App from './src/App'; +import { name as appName } from './app.json'; + +AppRegistry.registerComponent(appName, () => App); diff --git a/tracker/tracker-reactnative/example/ios/File.swift b/tracker/tracker-reactnative/example/ios/File.swift new file mode 100644 index 000000000..3b914f6be --- /dev/null +++ b/tracker/tracker-reactnative/example/ios/File.swift @@ -0,0 +1,6 @@ +// +// File.swift +// RntrackerExample +// + +import Foundation diff --git a/tracker/tracker-reactnative/example/ios/Podfile b/tracker/tracker-reactnative/example/ios/Podfile new file mode 100644 index 000000000..01f5e11d6 --- /dev/null +++ b/tracker/tracker-reactnative/example/ios/Podfile @@ -0,0 +1,66 @@ +# Resolve react_native_pods.rb with node to allow for hoisting +require Pod::Executable.execute_command('node', ['-p', + 'require.resolve( + "react-native/scripts/react_native_pods.rb", + {paths: [process.argv[1]]}, + )', __dir__]).strip + +platform :ios, '13.0' +prepare_react_native_project! + +# If you are using a `react-native-flipper` your iOS build will fail when `NO_FLIPPER=1` is set. +# because `react-native-flipper` depends on (FlipperKit,...) that will be excluded +# +# To fix this you can also exclude `react-native-flipper` using a `react-native.config.js` +# ```js +# module.exports = { +# dependencies: { +# ...(process.env.NO_FLIPPER ? { 'react-native-flipper': { platforms: { ios: null } } } : {}), +# ``` +flipper_config = ENV['NO_FLIPPER'] == "1" ? FlipperConfiguration.disabled : FlipperConfiguration.enabled + +linkage = ENV['USE_FRAMEWORKS'] +if linkage != nil + Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green + use_frameworks! :linkage => linkage.to_sym +end + +target 'RntrackerExample' do + use_frameworks! :linkage => :dynamic + config = use_native_modules! + + # Flags change depending on the env values. + flags = get_default_flags() + + use_react_native!( + :path => config[:reactNativePath], + # Hermes is now enabled by default. Disable by setting this flag to false. + :hermes_enabled => flags[:hermes_enabled], + :fabric_enabled => flags[:fabric_enabled], + # Enables Flipper. + # + # Note that if you have use_frameworks! enabled, Flipper will not work and + # you should disable the next line. + #:flipper_configuration => flipper_config, + # An absolute path to your application root. + :app_path => "#{Pod::Config.instance.installation_root}/.." + ) + Dir['../../'] + pod 'ORTracker', :path => '../../../tracker-ios' + + + target 'RntrackerExampleTests' do + inherit! :complete + # Pods for testing + end + + post_install do |installer| + # https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202 + react_native_post_install( + installer, + config[:reactNativePath], + :mac_catalyst_enabled => false + ) + __apply_Xcode_12_5_M1_post_install_workaround(installer) + end +end diff --git a/tracker/tracker-reactnative/example/ios/Podfile.lock b/tracker/tracker-reactnative/example/ios/Podfile.lock new file mode 100644 index 000000000..9325c7459 --- /dev/null +++ b/tracker/tracker-reactnative/example/ios/Podfile.lock @@ -0,0 +1,671 @@ +PODS: + - BitByteData (2.0.3) + - boost (1.76.0) + - DeviceKit (5.1.0) + - DoubleConversion (1.1.6) + - FBLazyVector (0.72.4) + - FBReactNativeSpec (0.72.4): + - RCT-Folly (= 2021.07.22.00) + - RCTRequired (= 0.72.4) + - RCTTypeSafety (= 0.72.4) + - React-Core (= 0.72.4) + - React-jsi (= 0.72.4) + - ReactCommon/turbomodule/core (= 0.72.4) + - fmt (6.2.1) + - glog (0.3.5) + - hermes-engine (0.72.4): + - hermes-engine/Pre-built (= 0.72.4) + - hermes-engine/Pre-built (0.72.4) + - libevent (2.1.12) + - openreplay-reactnative (0.1.0): + - ORTracker + - RCT-Folly (= 2021.07.22.00) + - React-Core + - ORTracker (0.1.0): + - DeviceKit + - SWCompression + - RCT-Folly (2021.07.22.00): + - boost + - DoubleConversion + - fmt (~> 6.2.1) + - glog + - RCT-Folly/Default (= 2021.07.22.00) + - RCT-Folly/Default (2021.07.22.00): + - boost + - DoubleConversion + - fmt (~> 6.2.1) + - glog + - RCT-Folly/Futures (2021.07.22.00): + - boost + - DoubleConversion + - fmt (~> 6.2.1) + - glog + - libevent + - RCTRequired (0.72.4) + - RCTTypeSafety (0.72.4): + - FBLazyVector (= 0.72.4) + - RCTRequired (= 0.72.4) + - React-Core (= 0.72.4) + - React (0.72.4): + - React-Core (= 0.72.4) + - React-Core/DevSupport (= 0.72.4) + - React-Core/RCTWebSocket (= 0.72.4) + - React-RCTActionSheet (= 0.72.4) + - React-RCTAnimation (= 0.72.4) + - React-RCTBlob (= 0.72.4) + - React-RCTImage (= 0.72.4) + - React-RCTLinking (= 0.72.4) + - React-RCTNetwork (= 0.72.4) + - React-RCTSettings (= 0.72.4) + - React-RCTText (= 0.72.4) + - React-RCTVibration (= 0.72.4) + - React-callinvoker (0.72.4) + - React-Codegen (0.72.4): + - DoubleConversion + - FBReactNativeSpec + - glog + - hermes-engine + - RCT-Folly + - RCTRequired + - RCTTypeSafety + - React-Core + - React-jsi + - React-jsiexecutor + - React-NativeModulesApple + - React-rncore + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - React-Core (0.72.4): + - glog + - hermes-engine + - RCT-Folly (= 2021.07.22.00) + - React-Core/Default (= 0.72.4) + - React-cxxreact + - React-hermes + - React-jsi + - React-jsiexecutor + - React-perflogger + - React-runtimeexecutor + - React-utils + - SocketRocket (= 0.6.1) + - Yoga + - React-Core/CoreModulesHeaders (0.72.4): + - glog + - hermes-engine + - RCT-Folly (= 2021.07.22.00) + - React-Core/Default + - React-cxxreact + - React-hermes + - React-jsi + - React-jsiexecutor + - React-perflogger + - React-runtimeexecutor + - React-utils + - SocketRocket (= 0.6.1) + - Yoga + - React-Core/Default (0.72.4): + - glog + - hermes-engine + - RCT-Folly (= 2021.07.22.00) + - React-cxxreact + - React-hermes + - React-jsi + - React-jsiexecutor + - React-perflogger + - React-runtimeexecutor + - React-utils + - SocketRocket (= 0.6.1) + - Yoga + - React-Core/DevSupport (0.72.4): + - glog + - hermes-engine + - RCT-Folly (= 2021.07.22.00) + - React-Core/Default (= 0.72.4) + - React-Core/RCTWebSocket (= 0.72.4) + - React-cxxreact + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector (= 0.72.4) + - React-perflogger + - React-runtimeexecutor + - React-utils + - SocketRocket (= 0.6.1) + - Yoga + - React-Core/RCTActionSheetHeaders (0.72.4): + - glog + - hermes-engine + - RCT-Folly (= 2021.07.22.00) + - React-Core/Default + - React-cxxreact + - React-hermes + - React-jsi + - React-jsiexecutor + - React-perflogger + - React-runtimeexecutor + - React-utils + - SocketRocket (= 0.6.1) + - Yoga + - React-Core/RCTAnimationHeaders (0.72.4): + - glog + - hermes-engine + - RCT-Folly (= 2021.07.22.00) + - React-Core/Default + - React-cxxreact + - React-hermes + - React-jsi + - React-jsiexecutor + - React-perflogger + - React-runtimeexecutor + - React-utils + - SocketRocket (= 0.6.1) + - Yoga + - React-Core/RCTBlobHeaders (0.72.4): + - glog + - hermes-engine + - RCT-Folly (= 2021.07.22.00) + - React-Core/Default + - React-cxxreact + - React-hermes + - React-jsi + - React-jsiexecutor + - React-perflogger + - React-runtimeexecutor + - React-utils + - SocketRocket (= 0.6.1) + - Yoga + - React-Core/RCTImageHeaders (0.72.4): + - glog + - hermes-engine + - RCT-Folly (= 2021.07.22.00) + - React-Core/Default + - React-cxxreact + - React-hermes + - React-jsi + - React-jsiexecutor + - React-perflogger + - React-runtimeexecutor + - React-utils + - SocketRocket (= 0.6.1) + - Yoga + - React-Core/RCTLinkingHeaders (0.72.4): + - glog + - hermes-engine + - RCT-Folly (= 2021.07.22.00) + - React-Core/Default + - React-cxxreact + - React-hermes + - React-jsi + - React-jsiexecutor + - React-perflogger + - React-runtimeexecutor + - React-utils + - SocketRocket (= 0.6.1) + - Yoga + - React-Core/RCTNetworkHeaders (0.72.4): + - glog + - hermes-engine + - RCT-Folly (= 2021.07.22.00) + - React-Core/Default + - React-cxxreact + - React-hermes + - React-jsi + - React-jsiexecutor + - React-perflogger + - React-runtimeexecutor + - React-utils + - SocketRocket (= 0.6.1) + - Yoga + - React-Core/RCTSettingsHeaders (0.72.4): + - glog + - hermes-engine + - RCT-Folly (= 2021.07.22.00) + - React-Core/Default + - React-cxxreact + - React-hermes + - React-jsi + - React-jsiexecutor + - React-perflogger + - React-runtimeexecutor + - React-utils + - SocketRocket (= 0.6.1) + - Yoga + - React-Core/RCTTextHeaders (0.72.4): + - glog + - hermes-engine + - RCT-Folly (= 2021.07.22.00) + - React-Core/Default + - React-cxxreact + - React-hermes + - React-jsi + - React-jsiexecutor + - React-perflogger + - React-runtimeexecutor + - React-utils + - SocketRocket (= 0.6.1) + - Yoga + - React-Core/RCTVibrationHeaders (0.72.4): + - glog + - hermes-engine + - RCT-Folly (= 2021.07.22.00) + - React-Core/Default + - React-cxxreact + - React-hermes + - React-jsi + - React-jsiexecutor + - React-perflogger + - React-runtimeexecutor + - React-utils + - SocketRocket (= 0.6.1) + - Yoga + - React-Core/RCTWebSocket (0.72.4): + - glog + - hermes-engine + - RCT-Folly (= 2021.07.22.00) + - React-Core/Default (= 0.72.4) + - React-cxxreact + - React-hermes + - React-jsi + - React-jsiexecutor + - React-perflogger + - React-runtimeexecutor + - React-utils + - SocketRocket (= 0.6.1) + - Yoga + - React-CoreModules (0.72.4): + - RCT-Folly (= 2021.07.22.00) + - RCTTypeSafety (= 0.72.4) + - React-Codegen (= 0.72.4) + - React-Core/CoreModulesHeaders (= 0.72.4) + - React-jsi (= 0.72.4) + - React-RCTBlob + - React-RCTImage (= 0.72.4) + - ReactCommon/turbomodule/core (= 0.72.4) + - SocketRocket (= 0.6.1) + - React-cxxreact (0.72.4): + - boost (= 1.76.0) + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2021.07.22.00) + - React-callinvoker (= 0.72.4) + - React-debug (= 0.72.4) + - React-jsi (= 0.72.4) + - React-jsinspector (= 0.72.4) + - React-logger (= 0.72.4) + - React-perflogger (= 0.72.4) + - React-runtimeexecutor (= 0.72.4) + - React-debug (0.72.4) + - React-hermes (0.72.4): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2021.07.22.00) + - RCT-Folly/Futures (= 2021.07.22.00) + - React-cxxreact (= 0.72.4) + - React-jsi + - React-jsiexecutor (= 0.72.4) + - React-jsinspector (= 0.72.4) + - React-perflogger (= 0.72.4) + - React-jsi (0.72.4): + - boost (= 1.76.0) + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2021.07.22.00) + - React-jsiexecutor (0.72.4): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2021.07.22.00) + - React-cxxreact (= 0.72.4) + - React-jsi (= 0.72.4) + - React-perflogger (= 0.72.4) + - React-jsinspector (0.72.4) + - React-logger (0.72.4): + - glog + - React-NativeModulesApple (0.72.4): + - hermes-engine + - React-callinvoker + - React-Core + - React-cxxreact + - React-jsi + - React-runtimeexecutor + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - React-perflogger (0.72.4) + - React-RCTActionSheet (0.72.4): + - React-Core/RCTActionSheetHeaders (= 0.72.4) + - React-RCTAnimation (0.72.4): + - RCT-Folly (= 2021.07.22.00) + - RCTTypeSafety (= 0.72.4) + - React-Codegen (= 0.72.4) + - React-Core/RCTAnimationHeaders (= 0.72.4) + - React-jsi (= 0.72.4) + - ReactCommon/turbomodule/core (= 0.72.4) + - React-RCTAppDelegate (0.72.4): + - RCT-Folly + - RCTRequired + - RCTTypeSafety + - React-Core + - React-CoreModules + - React-hermes + - React-NativeModulesApple + - React-RCTImage + - React-RCTNetwork + - React-runtimescheduler + - ReactCommon/turbomodule/core + - React-RCTBlob (0.72.4): + - hermes-engine + - RCT-Folly (= 2021.07.22.00) + - React-Codegen (= 0.72.4) + - React-Core/RCTBlobHeaders (= 0.72.4) + - React-Core/RCTWebSocket (= 0.72.4) + - React-jsi (= 0.72.4) + - React-RCTNetwork (= 0.72.4) + - ReactCommon/turbomodule/core (= 0.72.4) + - React-RCTImage (0.72.4): + - RCT-Folly (= 2021.07.22.00) + - RCTTypeSafety (= 0.72.4) + - React-Codegen (= 0.72.4) + - React-Core/RCTImageHeaders (= 0.72.4) + - React-jsi (= 0.72.4) + - React-RCTNetwork (= 0.72.4) + - ReactCommon/turbomodule/core (= 0.72.4) + - React-RCTLinking (0.72.4): + - React-Codegen (= 0.72.4) + - React-Core/RCTLinkingHeaders (= 0.72.4) + - React-jsi (= 0.72.4) + - ReactCommon/turbomodule/core (= 0.72.4) + - React-RCTNetwork (0.72.4): + - RCT-Folly (= 2021.07.22.00) + - RCTTypeSafety (= 0.72.4) + - React-Codegen (= 0.72.4) + - React-Core/RCTNetworkHeaders (= 0.72.4) + - React-jsi (= 0.72.4) + - ReactCommon/turbomodule/core (= 0.72.4) + - React-RCTSettings (0.72.4): + - RCT-Folly (= 2021.07.22.00) + - RCTTypeSafety (= 0.72.4) + - React-Codegen (= 0.72.4) + - React-Core/RCTSettingsHeaders (= 0.72.4) + - React-jsi (= 0.72.4) + - ReactCommon/turbomodule/core (= 0.72.4) + - React-RCTText (0.72.4): + - React-Core/RCTTextHeaders (= 0.72.4) + - React-RCTVibration (0.72.4): + - RCT-Folly (= 2021.07.22.00) + - React-Codegen (= 0.72.4) + - React-Core/RCTVibrationHeaders (= 0.72.4) + - React-jsi (= 0.72.4) + - ReactCommon/turbomodule/core (= 0.72.4) + - React-rncore (0.72.4) + - React-runtimeexecutor (0.72.4): + - React-jsi (= 0.72.4) + - React-runtimescheduler (0.72.4): + - glog + - hermes-engine + - RCT-Folly (= 2021.07.22.00) + - React-callinvoker + - React-debug + - React-jsi + - React-runtimeexecutor + - React-utils (0.72.4): + - glog + - RCT-Folly (= 2021.07.22.00) + - React-debug + - ReactCommon/turbomodule/bridging (0.72.4): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2021.07.22.00) + - React-callinvoker (= 0.72.4) + - React-cxxreact (= 0.72.4) + - React-jsi (= 0.72.4) + - React-logger (= 0.72.4) + - React-perflogger (= 0.72.4) + - ReactCommon/turbomodule/core (0.72.4): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2021.07.22.00) + - React-callinvoker (= 0.72.4) + - React-cxxreact (= 0.72.4) + - React-jsi (= 0.72.4) + - React-logger (= 0.72.4) + - React-perflogger (= 0.72.4) + - SocketRocket (0.6.1) + - SWCompression (4.8.5): + - BitByteData (~> 2.0) + - SWCompression/BZip2 (= 4.8.5) + - SWCompression/Deflate (= 4.8.5) + - SWCompression/GZip (= 4.8.5) + - SWCompression/LZ4 (= 4.8.5) + - SWCompression/LZMA (= 4.8.5) + - SWCompression/LZMA2 (= 4.8.5) + - SWCompression/SevenZip (= 4.8.5) + - SWCompression/TAR (= 4.8.5) + - SWCompression/XZ (= 4.8.5) + - SWCompression/ZIP (= 4.8.5) + - SWCompression/Zlib (= 4.8.5) + - SWCompression/BZip2 (4.8.5): + - BitByteData (~> 2.0) + - SWCompression/Deflate (4.8.5): + - BitByteData (~> 2.0) + - SWCompression/GZip (4.8.5): + - BitByteData (~> 2.0) + - SWCompression/Deflate + - SWCompression/LZ4 (4.8.5): + - BitByteData (~> 2.0) + - SWCompression/LZMA (4.8.5): + - BitByteData (~> 2.0) + - SWCompression/LZMA2 (4.8.5): + - BitByteData (~> 2.0) + - SWCompression/LZMA + - SWCompression/SevenZip (4.8.5): + - BitByteData (~> 2.0) + - SWCompression/LZMA2 + - SWCompression/TAR (4.8.5): + - BitByteData (~> 2.0) + - SWCompression/XZ (4.8.5): + - BitByteData (~> 2.0) + - SWCompression/LZMA2 + - SWCompression/ZIP (4.8.5): + - BitByteData (~> 2.0) + - SWCompression/Deflate + - SWCompression/Zlib (4.8.5): + - BitByteData (~> 2.0) + - SWCompression/Deflate + - Yoga (1.14.0) + +DEPENDENCIES: + - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) + - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) + - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) + - FBReactNativeSpec (from `../node_modules/react-native/React/FBReactNativeSpec`) + - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`) + - hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) + - libevent (~> 2.1.12) + - openreplay-reactnative (from `../..`) + - ORTracker (from `../../../tracker-ios`) + - RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) + - RCTRequired (from `../node_modules/react-native/Libraries/RCTRequired`) + - RCTTypeSafety (from `../node_modules/react-native/Libraries/TypeSafety`) + - React (from `../node_modules/react-native/`) + - React-callinvoker (from `../node_modules/react-native/ReactCommon/callinvoker`) + - React-Codegen (from `build/generated/ios`) + - React-Core (from `../node_modules/react-native/`) + - React-Core/RCTWebSocket (from `../node_modules/react-native/`) + - React-CoreModules (from `../node_modules/react-native/React/CoreModules`) + - React-cxxreact (from `../node_modules/react-native/ReactCommon/cxxreact`) + - React-debug (from `../node_modules/react-native/ReactCommon/react/debug`) + - React-hermes (from `../node_modules/react-native/ReactCommon/hermes`) + - React-jsi (from `../node_modules/react-native/ReactCommon/jsi`) + - React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`) + - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`) + - React-logger (from `../node_modules/react-native/ReactCommon/logger`) + - React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`) + - React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`) + - React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`) + - React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`) + - React-RCTAppDelegate (from `../node_modules/react-native/Libraries/AppDelegate`) + - React-RCTBlob (from `../node_modules/react-native/Libraries/Blob`) + - React-RCTImage (from `../node_modules/react-native/Libraries/Image`) + - React-RCTLinking (from `../node_modules/react-native/Libraries/LinkingIOS`) + - React-RCTNetwork (from `../node_modules/react-native/Libraries/Network`) + - React-RCTSettings (from `../node_modules/react-native/Libraries/Settings`) + - React-RCTText (from `../node_modules/react-native/Libraries/Text`) + - React-RCTVibration (from `../node_modules/react-native/Libraries/Vibration`) + - React-rncore (from `../node_modules/react-native/ReactCommon`) + - React-runtimeexecutor (from `../node_modules/react-native/ReactCommon/runtimeexecutor`) + - React-runtimescheduler (from `../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler`) + - React-utils (from `../node_modules/react-native/ReactCommon/react/utils`) + - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) + - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) + +SPEC REPOS: + trunk: + - BitByteData + - DeviceKit + - fmt + - libevent + - SocketRocket + - SWCompression + +EXTERNAL SOURCES: + boost: + :podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec" + DoubleConversion: + :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" + FBLazyVector: + :path: "../node_modules/react-native/Libraries/FBLazyVector" + FBReactNativeSpec: + :path: "../node_modules/react-native/React/FBReactNativeSpec" + glog: + :podspec: "../node_modules/react-native/third-party-podspecs/glog.podspec" + hermes-engine: + :podspec: "../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec" + :tag: hermes-2023-08-07-RNv0.72.4-813b2def12bc9df02654b3e3653ae4a68d0572e0 + openreplay-reactnative: + :path: "../.." + ORTracker: + :path: "../../../tracker-ios" + RCT-Folly: + :podspec: "../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec" + RCTRequired: + :path: "../node_modules/react-native/Libraries/RCTRequired" + RCTTypeSafety: + :path: "../node_modules/react-native/Libraries/TypeSafety" + React: + :path: "../node_modules/react-native/" + React-callinvoker: + :path: "../node_modules/react-native/ReactCommon/callinvoker" + React-Codegen: + :path: build/generated/ios + React-Core: + :path: "../node_modules/react-native/" + React-CoreModules: + :path: "../node_modules/react-native/React/CoreModules" + React-cxxreact: + :path: "../node_modules/react-native/ReactCommon/cxxreact" + React-debug: + :path: "../node_modules/react-native/ReactCommon/react/debug" + React-hermes: + :path: "../node_modules/react-native/ReactCommon/hermes" + React-jsi: + :path: "../node_modules/react-native/ReactCommon/jsi" + React-jsiexecutor: + :path: "../node_modules/react-native/ReactCommon/jsiexecutor" + React-jsinspector: + :path: "../node_modules/react-native/ReactCommon/jsinspector" + React-logger: + :path: "../node_modules/react-native/ReactCommon/logger" + React-NativeModulesApple: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios" + React-perflogger: + :path: "../node_modules/react-native/ReactCommon/reactperflogger" + React-RCTActionSheet: + :path: "../node_modules/react-native/Libraries/ActionSheetIOS" + React-RCTAnimation: + :path: "../node_modules/react-native/Libraries/NativeAnimation" + React-RCTAppDelegate: + :path: "../node_modules/react-native/Libraries/AppDelegate" + React-RCTBlob: + :path: "../node_modules/react-native/Libraries/Blob" + React-RCTImage: + :path: "../node_modules/react-native/Libraries/Image" + React-RCTLinking: + :path: "../node_modules/react-native/Libraries/LinkingIOS" + React-RCTNetwork: + :path: "../node_modules/react-native/Libraries/Network" + React-RCTSettings: + :path: "../node_modules/react-native/Libraries/Settings" + React-RCTText: + :path: "../node_modules/react-native/Libraries/Text" + React-RCTVibration: + :path: "../node_modules/react-native/Libraries/Vibration" + React-rncore: + :path: "../node_modules/react-native/ReactCommon" + React-runtimeexecutor: + :path: "../node_modules/react-native/ReactCommon/runtimeexecutor" + React-runtimescheduler: + :path: "../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler" + React-utils: + :path: "../node_modules/react-native/ReactCommon/react/utils" + ReactCommon: + :path: "../node_modules/react-native/ReactCommon" + Yoga: + :path: "../node_modules/react-native/ReactCommon/yoga" + +SPEC CHECKSUMS: + BitByteData: 95b9b252c3b8d936df5e87750301f7a60542069d + boost: 57d2868c099736d80fcd648bf211b4431e51a558 + DeviceKit: bcce5c26d9502e77e3a9fa5d1282877db388d922 + DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54 + FBLazyVector: 5d4a3b7f411219a45a6d952f77d2c0a6c9989da5 + FBReactNativeSpec: 3fc2d478e1c4b08276f9dd9128f80ec6d5d85c1f + fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 + glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b + hermes-engine: 81191603c4eaa01f5e4ae5737a9efcf64756c7b2 + libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 + openreplay-reactnative: 195b9e120d1aed7bdde7d4728a2f23b37b8d5266 + ORTracker: 5b993fad6248d9e5605c28f4af6454022f43934f + RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1 + RCTRequired: c0569ecc035894e4a68baecb30fe6a7ea6e399f9 + RCTTypeSafety: e90354072c21236e0bcf1699011e39acd25fea2f + React: a1be3c6dc0a6e949ccd3e659781aa47bbae1868f + React-callinvoker: 1020b33f6cb1a1824f9ca2a86609fbce2a73c6ed + React-Codegen: d3cabb5a8b9de7b0fc5928c0cf478e73a178d728 + React-Core: 34da8c952844e21f5a69bf46438fb6bf063900d0 + React-CoreModules: 621243df8863055ff30f3bb2ff71573ffa7b16c8 + React-cxxreact: 4ad1cc861e32fb533dad6ff7a4ea25680fa1c994 + React-debug: cc0911f6e8ef0b198724c1d8d2c114eee7a059ec + React-hermes: 37377d0a56aa0cf55c65248271866ce3268cde3f + React-jsi: 6de8b0ccc6b765b58e4eee9ee38049dbeaf5c221 + React-jsiexecutor: c7f826e40fa9cab5d37cab6130b1af237332b594 + React-jsinspector: aaed4cf551c4a1c98092436518c2d267b13a673f + React-logger: da1ebe05ae06eb6db4b162202faeafac4b435e77 + React-NativeModulesApple: 38a88481feb279e37deb740414abc732c6768c93 + React-perflogger: 496a1a3dc6737f964107cb3ddae7f9e265ddda58 + React-RCTActionSheet: 02904b932b50e680f4e26e7a686b33ebf7ef3c00 + React-RCTAnimation: e44fe5053708c5be762943087c78dc5a6a3c645c + React-RCTAppDelegate: 75566e142d0c325f603790943a38f0e297bcbab3 + React-RCTBlob: 5577c3542fafbd9241f505bda541f6762996b0eb + React-RCTImage: f48859ae6a1e5c2c8659f860efdfcb8b0cb60cde + React-RCTLinking: 9b145f380c6b5910e3219bef73e1d7024381ad54 + React-RCTNetwork: 179f50a80c01fa7d6a515552a24ad2e294f341a1 + React-RCTSettings: cdc9f538f0f8e72e6f3de7c971fb4fb2f49caff9 + React-RCTText: 9b9f5589d9b649d7246c3f336e116496df28cfe6 + React-RCTVibration: 35eec7201c8ffdaccdd908a5ec874b7055f975f3 + React-rncore: 46133d523155fc84572338bd6a7c461c1d873efc + React-runtimeexecutor: d465ba0c47ef3ed8281143f59605cacc2244d5c7 + React-runtimescheduler: b5bc431f8bbc35d6112b541086bba72061bf7c1d + React-utils: 23a3a714dec3c5727c07a017c37993c3a492b441 + ReactCommon: d615a572332dd5b915b10a6fe7006b1bc67ee849 + SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 + SWCompression: 15e38b06c37077399a1b60bfecc1c2cd71f0ee99 + Yoga: 3efc43e0d48686ce2e8c60f99d4e6bd349aff981 + +PODFILE CHECKSUM: f12bcbf8dfa25c96bb19ba57e6413b29c875f421 + +COCOAPODS: 1.12.1 diff --git a/tracker/tracker-reactnative/example/ios/RntrackerExample-Bridging-Header.h b/tracker/tracker-reactnative/example/ios/RntrackerExample-Bridging-Header.h new file mode 100644 index 000000000..e11d920b1 --- /dev/null +++ b/tracker/tracker-reactnative/example/ios/RntrackerExample-Bridging-Header.h @@ -0,0 +1,3 @@ +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// diff --git a/tracker/tracker-reactnative/example/ios/RntrackerExample.xcodeproj/project.pbxproj b/tracker/tracker-reactnative/example/ios/RntrackerExample.xcodeproj/project.pbxproj new file mode 100644 index 000000000..76ee919f1 --- /dev/null +++ b/tracker/tracker-reactnative/example/ios/RntrackerExample.xcodeproj/project.pbxproj @@ -0,0 +1,686 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 00E356F31AD99517003FC87E /* RntrackerExampleTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* RntrackerExampleTests.m */; }; + 13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.mm */; }; + 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; + 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; + 60ADCB349F6D4931641744DE /* Pods_RntrackerExample.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5EB70A6C522028E464541BDD /* Pods_RntrackerExample.framework */; }; + 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; + 97DF8FD3FE651CA8010130D7 /* Pods_RntrackerExample_RntrackerExampleTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2D22699E0FFA1C22F7AE3448 /* Pods_RntrackerExample_RntrackerExampleTests.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 00E356F41AD99517003FC87E /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 13B07F861A680F5B00A75B9A; + remoteInfo = RntrackerExample; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 00E356EE1AD99517003FC87E /* RntrackerExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RntrackerExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 00E356F21AD99517003FC87E /* RntrackerExampleTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RntrackerExampleTests.m; sourceTree = ""; }; + 13B07F961A680F5B00A75B9A /* RntrackerExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = RntrackerExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = RntrackerExample/AppDelegate.h; sourceTree = ""; }; + 13B07FB01A68108700A75B9A /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = RntrackerExample/AppDelegate.mm; sourceTree = ""; }; + 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = RntrackerExample/Images.xcassets; sourceTree = ""; }; + 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = RntrackerExample/Info.plist; sourceTree = ""; }; + 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = RntrackerExample/main.m; sourceTree = ""; }; + 2D22699E0FFA1C22F7AE3448 /* Pods_RntrackerExample_RntrackerExampleTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RntrackerExample_RntrackerExampleTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 5EB70A6C522028E464541BDD /* Pods_RntrackerExample.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RntrackerExample.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = RntrackerExample/LaunchScreen.storyboard; sourceTree = ""; }; + 9473576378E546844F1249CB /* Pods-RntrackerExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RntrackerExample.release.xcconfig"; path = "Target Support Files/Pods-RntrackerExample/Pods-RntrackerExample.release.xcconfig"; sourceTree = ""; }; + B3398286B848055D501F1B7C /* Pods-RntrackerExample-RntrackerExampleTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RntrackerExample-RntrackerExampleTests.debug.xcconfig"; path = "Target Support Files/Pods-RntrackerExample-RntrackerExampleTests/Pods-RntrackerExample-RntrackerExampleTests.debug.xcconfig"; sourceTree = ""; }; + C668E804196FA22A7C3B2356 /* Pods-RntrackerExample-RntrackerExampleTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RntrackerExample-RntrackerExampleTests.release.xcconfig"; path = "Target Support Files/Pods-RntrackerExample-RntrackerExampleTests/Pods-RntrackerExample-RntrackerExampleTests.release.xcconfig"; sourceTree = ""; }; + ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; + EDE6F6808B65834562C20EBE /* Pods-RntrackerExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RntrackerExample.debug.xcconfig"; path = "Target Support Files/Pods-RntrackerExample/Pods-RntrackerExample.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 00E356EB1AD99517003FC87E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 97DF8FD3FE651CA8010130D7 /* Pods_RntrackerExample_RntrackerExampleTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 13B07F8C1A680F5B00A75B9A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 60ADCB349F6D4931641744DE /* Pods_RntrackerExample.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 00E356EF1AD99517003FC87E /* RntrackerExampleTests */ = { + isa = PBXGroup; + children = ( + 00E356F21AD99517003FC87E /* RntrackerExampleTests.m */, + 00E356F01AD99517003FC87E /* Supporting Files */, + ); + path = RntrackerExampleTests; + sourceTree = ""; + }; + 00E356F01AD99517003FC87E /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 00E356F11AD99517003FC87E /* Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 13B07FAE1A68108700A75B9A /* RntrackerExample */ = { + isa = PBXGroup; + children = ( + 13B07FAF1A68108700A75B9A /* AppDelegate.h */, + 13B07FB01A68108700A75B9A /* AppDelegate.mm */, + 13B07FB51A68108700A75B9A /* Images.xcassets */, + 13B07FB61A68108700A75B9A /* Info.plist */, + 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */, + 13B07FB71A68108700A75B9A /* main.m */, + ); + name = RntrackerExample; + sourceTree = ""; + }; + 2D16E6871FA4F8E400B85C8A /* Frameworks */ = { + isa = PBXGroup; + children = ( + ED297162215061F000B7C4FE /* JavaScriptCore.framework */, + 5EB70A6C522028E464541BDD /* Pods_RntrackerExample.framework */, + 2D22699E0FFA1C22F7AE3448 /* Pods_RntrackerExample_RntrackerExampleTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 832341AE1AAA6A7D00B99B32 /* Libraries */ = { + isa = PBXGroup; + children = ( + ); + name = Libraries; + sourceTree = ""; + }; + 83CBB9F61A601CBA00E9B192 = { + isa = PBXGroup; + children = ( + 13B07FAE1A68108700A75B9A /* RntrackerExample */, + 832341AE1AAA6A7D00B99B32 /* Libraries */, + 00E356EF1AD99517003FC87E /* RntrackerExampleTests */, + 83CBBA001A601CBA00E9B192 /* Products */, + 2D16E6871FA4F8E400B85C8A /* Frameworks */, + BBD78D7AC51CEA395F1C20DB /* Pods */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + usesTabs = 0; + }; + 83CBBA001A601CBA00E9B192 /* Products */ = { + isa = PBXGroup; + children = ( + 13B07F961A680F5B00A75B9A /* RntrackerExample.app */, + 00E356EE1AD99517003FC87E /* RntrackerExampleTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + BBD78D7AC51CEA395F1C20DB /* Pods */ = { + isa = PBXGroup; + children = ( + EDE6F6808B65834562C20EBE /* Pods-RntrackerExample.debug.xcconfig */, + 9473576378E546844F1249CB /* Pods-RntrackerExample.release.xcconfig */, + B3398286B848055D501F1B7C /* Pods-RntrackerExample-RntrackerExampleTests.debug.xcconfig */, + C668E804196FA22A7C3B2356 /* Pods-RntrackerExample-RntrackerExampleTests.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 00E356ED1AD99517003FC87E /* RntrackerExampleTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "RntrackerExampleTests" */; + buildPhases = ( + C3DF83E17741CA620498EBE1 /* [CP] Check Pods Manifest.lock */, + 00E356EA1AD99517003FC87E /* Sources */, + 00E356EB1AD99517003FC87E /* Frameworks */, + 00E356EC1AD99517003FC87E /* Resources */, + 81B05CC246621391DA66D200 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 00E356F51AD99517003FC87E /* PBXTargetDependency */, + ); + name = RntrackerExampleTests; + productName = RntrackerExampleTests; + productReference = 00E356EE1AD99517003FC87E /* RntrackerExampleTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 13B07F861A680F5B00A75B9A /* RntrackerExample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "RntrackerExample" */; + buildPhases = ( + A6D6377E2D3186BB6736D56A /* [CP] Check Pods Manifest.lock */, + FD10A7F022414F080027D42C /* Start Packager */, + 13B07F871A680F5B00A75B9A /* Sources */, + 13B07F8C1A680F5B00A75B9A /* Frameworks */, + 13B07F8E1A680F5B00A75B9A /* Resources */, + 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, + BA34C2DCDBAC12D7C0BE1BE6 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = RntrackerExample; + productName = RntrackerExample; + productReference = 13B07F961A680F5B00A75B9A /* RntrackerExample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 83CBB9F71A601CBA00E9B192 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1210; + TargetAttributes = { + 00E356ED1AD99517003FC87E = { + CreatedOnToolsVersion = 6.2; + TestTargetID = 13B07F861A680F5B00A75B9A; + }; + 13B07F861A680F5B00A75B9A = { + LastSwiftMigration = 1120; + }; + }; + }; + buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "RntrackerExample" */; + compatibilityVersion = "Xcode 12.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 83CBB9F61A601CBA00E9B192; + productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 13B07F861A680F5B00A75B9A /* RntrackerExample */, + 00E356ED1AD99517003FC87E /* RntrackerExampleTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 00E356EC1AD99517003FC87E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 13B07F8E1A680F5B00A75B9A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */, + 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "$(SRCROOT)/.xcode.env.local", + "$(SRCROOT)/.xcode.env", + ); + name = "Bundle React Native code and images"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "set -e\n\nWITH_ENVIRONMENT=\"../node_modules/react-native/scripts/xcode/with-environment.sh\"\nREACT_NATIVE_XCODE=\"../node_modules/react-native/scripts/react-native-xcode.sh\"\n\n/bin/sh -c \"$WITH_ENVIRONMENT $REACT_NATIVE_XCODE\"\n"; + }; + 81B05CC246621391DA66D200 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-RntrackerExample-RntrackerExampleTests/Pods-RntrackerExample-RntrackerExampleTests-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-RntrackerExample-RntrackerExampleTests/Pods-RntrackerExample-RntrackerExampleTests-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-RntrackerExample-RntrackerExampleTests/Pods-RntrackerExample-RntrackerExampleTests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + A6D6377E2D3186BB6736D56A /* [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-RntrackerExample-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; + }; + BA34C2DCDBAC12D7C0BE1BE6 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-RntrackerExample/Pods-RntrackerExample-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-RntrackerExample/Pods-RntrackerExample-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-RntrackerExample/Pods-RntrackerExample-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + C3DF83E17741CA620498EBE1 /* [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-RntrackerExample-RntrackerExampleTests-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; + }; + FD10A7F022414F080027D42C /* Start Packager */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Start Packager"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "export RCT_METRO_PORT=\"${RCT_METRO_PORT:=8081}\"\necho \"export RCT_METRO_PORT=${RCT_METRO_PORT}\" > \"${SRCROOT}/../node_modules/react-native/scripts/.packager.env\"\nif [ -z \"${RCT_NO_LAUNCH_PACKAGER+xxx}\" ] ; then\n if nc -w 5 -z localhost ${RCT_METRO_PORT} ; then\n if ! curl -s \"http://localhost:${RCT_METRO_PORT}/status\" | grep -q \"packager-status:running\" ; then\n echo \"Port ${RCT_METRO_PORT} already in use, packager is either not running or not running correctly\"\n exit 2\n fi\n else\n open \"$SRCROOT/../node_modules/react-native/scripts/launchPackager.command\" || echo \"Can't start packager automatically\"\n fi\nfi\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 00E356EA1AD99517003FC87E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 00E356F31AD99517003FC87E /* RntrackerExampleTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 13B07F871A680F5B00A75B9A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */, + 13B07FC11A68108700A75B9A /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 00E356F51AD99517003FC87E /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 13B07F861A680F5B00A75B9A /* RntrackerExample */; + targetProxy = 00E356F41AD99517003FC87E /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 00E356F61AD99517003FC87E /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = B3398286B848055D501F1B7C /* Pods-RntrackerExample-RntrackerExampleTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + INFOPLIST_FILE = RntrackerExampleTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + OTHER_LDFLAGS = ( + "-ObjC", + "-lc++", + "$(inherited)", + ); + PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/RntrackerExample.app/RntrackerExample"; + }; + name = Debug; + }; + 00E356F71AD99517003FC87E /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = C668E804196FA22A7C3B2356 /* Pods-RntrackerExample-RntrackerExampleTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + COPY_PHASE_STRIP = NO; + INFOPLIST_FILE = RntrackerExampleTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + OTHER_LDFLAGS = ( + "-ObjC", + "-lc++", + "$(inherited)", + ); + PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/RntrackerExample.app/RntrackerExample"; + }; + name = Release; + }; + 13B07F941A680F5B00A75B9A /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = EDE6F6808B65834562C20EBE /* Pods-RntrackerExample.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = 1; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = RntrackerExample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + OTHER_LDFLAGS = ( + "$(inherited)", + "-ObjC", + "-lc++", + ); + PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = RntrackerExample; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 13B07F951A680F5B00A75B9A /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9473576378E546844F1249CB /* Pods-RntrackerExample.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = 1; + INFOPLIST_FILE = RntrackerExample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + OTHER_LDFLAGS = ( + "$(inherited)", + "-ObjC", + "-lc++", + ); + PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = RntrackerExample; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; + 83CBBA201A601CBA00E9B192 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "c++17"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = 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_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_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + 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; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers/react/nativemodule/core", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers/platform/ios", + "${PODS_CONFIGURATION_BUILD_DIR}/React-NativeModulesApple/React_NativeModulesApple.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers/react/renderer/graphics/platform/ios", + ); + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + /usr/lib/swift, + "$(inherited)", + ); + LIBRARY_SEARCH_PATHS = ( + "\"$(SDKROOT)/usr/lib/swift\"", + "\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"", + "\"$(inherited)\"", + ); + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + OTHER_CFLAGS = "$(inherited)"; + OTHER_CPLUSPLUSFLAGS = ( + "$(OTHER_CFLAGS)", + "-DFOLLY_NO_CONFIG", + "-DFOLLY_MOBILE=1", + "-DFOLLY_USE_LIBCPP=1", + ); + REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 83CBBA211A601CBA00E9B192 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "c++17"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = 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_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_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers/react/nativemodule/core", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers/platform/ios", + "${PODS_CONFIGURATION_BUILD_DIR}/React-NativeModulesApple/React_NativeModulesApple.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers/react/renderer/graphics/platform/ios", + ); + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + /usr/lib/swift, + "$(inherited)", + ); + LIBRARY_SEARCH_PATHS = ( + "\"$(SDKROOT)/usr/lib/swift\"", + "\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"", + "\"$(inherited)\"", + ); + MTL_ENABLE_DEBUG_INFO = NO; + OTHER_CFLAGS = "$(inherited)"; + OTHER_CPLUSPLUSFLAGS = ( + "$(OTHER_CFLAGS)", + "-DFOLLY_NO_CONFIG", + "-DFOLLY_MOBILE=1", + "-DFOLLY_USE_LIBCPP=1", + ); + REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "RntrackerExampleTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 00E356F61AD99517003FC87E /* Debug */, + 00E356F71AD99517003FC87E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "RntrackerExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 13B07F941A680F5B00A75B9A /* Debug */, + 13B07F951A680F5B00A75B9A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "RntrackerExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 83CBBA201A601CBA00E9B192 /* Debug */, + 83CBBA211A601CBA00E9B192 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */; +} diff --git a/tracker/tracker-reactnative/example/ios/RntrackerExample.xcodeproj/xcshareddata/xcschemes/RntrackerExample.xcscheme b/tracker/tracker-reactnative/example/ios/RntrackerExample.xcodeproj/xcshareddata/xcschemes/RntrackerExample.xcscheme new file mode 100644 index 000000000..393671026 --- /dev/null +++ b/tracker/tracker-reactnative/example/ios/RntrackerExample.xcodeproj/xcshareddata/xcschemes/RntrackerExample.xcscheme @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tracker/tracker-reactnative/example/ios/RntrackerExample.xcworkspace/contents.xcworkspacedata b/tracker/tracker-reactnative/example/ios/RntrackerExample.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..208941c09 --- /dev/null +++ b/tracker/tracker-reactnative/example/ios/RntrackerExample.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/tracker/tracker-reactnative/example/ios/RntrackerExample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/tracker/tracker-reactnative/example/ios/RntrackerExample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/tracker/tracker-reactnative/example/ios/RntrackerExample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/tracker/tracker-reactnative/example/ios/RntrackerExample/AppDelegate.h b/tracker/tracker-reactnative/example/ios/RntrackerExample/AppDelegate.h new file mode 100644 index 000000000..5d2808256 --- /dev/null +++ b/tracker/tracker-reactnative/example/ios/RntrackerExample/AppDelegate.h @@ -0,0 +1,6 @@ +#import +#import + +@interface AppDelegate : RCTAppDelegate + +@end diff --git a/tracker/tracker-reactnative/example/ios/RntrackerExample/AppDelegate.mm b/tracker/tracker-reactnative/example/ios/RntrackerExample/AppDelegate.mm new file mode 100644 index 000000000..a533cc9c4 --- /dev/null +++ b/tracker/tracker-reactnative/example/ios/RntrackerExample/AppDelegate.mm @@ -0,0 +1,26 @@ +#import "AppDelegate.h" + +#import + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + self.moduleName = @"RntrackerExample"; + // You can add your custom initial props in the dictionary below. + // They will be passed down to the ViewController used by React Native. + self.initialProps = @{}; + + return [super application:application didFinishLaunchingWithOptions:launchOptions]; +} + +- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge +{ +#if DEBUG + return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"]; +#else + return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; +#endif +} + +@end diff --git a/tracker/tracker-reactnative/example/ios/RntrackerExample/Images.xcassets/AppIcon.appiconset/Contents.json b/tracker/tracker-reactnative/example/ios/RntrackerExample/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..81213230d --- /dev/null +++ b/tracker/tracker-reactnative/example/ios/RntrackerExample/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,53 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/tracker/tracker-reactnative/example/ios/RntrackerExample/Images.xcassets/Contents.json b/tracker/tracker-reactnative/example/ios/RntrackerExample/Images.xcassets/Contents.json new file mode 100644 index 000000000..2d92bd53f --- /dev/null +++ b/tracker/tracker-reactnative/example/ios/RntrackerExample/Images.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/tracker/tracker-reactnative/example/ios/RntrackerExample/Info.plist b/tracker/tracker-reactnative/example/ios/RntrackerExample/Info.plist new file mode 100644 index 000000000..750172826 --- /dev/null +++ b/tracker/tracker-reactnative/example/ios/RntrackerExample/Info.plist @@ -0,0 +1,55 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + RntrackerExample + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleSignature + ???? + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + LSRequiresIPhoneOS + + NSAppTransportSecurity + + NSExceptionDomains + + localhost + + NSExceptionAllowsInsecureHTTPLoads + + + + + NSLocationWhenInUseUsageDescription + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/tracker/tracker-reactnative/example/ios/RntrackerExample/LaunchScreen.storyboard b/tracker/tracker-reactnative/example/ios/RntrackerExample/LaunchScreen.storyboard new file mode 100644 index 000000000..297605086 --- /dev/null +++ b/tracker/tracker-reactnative/example/ios/RntrackerExample/LaunchScreen.storyboard @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tracker/tracker-reactnative/example/ios/RntrackerExample/main.m b/tracker/tracker-reactnative/example/ios/RntrackerExample/main.m new file mode 100644 index 000000000..d645c7246 --- /dev/null +++ b/tracker/tracker-reactnative/example/ios/RntrackerExample/main.m @@ -0,0 +1,10 @@ +#import + +#import "AppDelegate.h" + +int main(int argc, char *argv[]) +{ + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/tracker/tracker-reactnative/example/ios/RntrackerExampleTests/Info.plist b/tracker/tracker-reactnative/example/ios/RntrackerExampleTests/Info.plist new file mode 100644 index 000000000..ba72822e8 --- /dev/null +++ b/tracker/tracker-reactnative/example/ios/RntrackerExampleTests/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/tracker/tracker-reactnative/example/ios/RntrackerExampleTests/RntrackerExampleTests.m b/tracker/tracker-reactnative/example/ios/RntrackerExampleTests/RntrackerExampleTests.m new file mode 100644 index 000000000..0de7f9f8d --- /dev/null +++ b/tracker/tracker-reactnative/example/ios/RntrackerExampleTests/RntrackerExampleTests.m @@ -0,0 +1,66 @@ +#import +#import + +#import +#import + +#define TIMEOUT_SECONDS 600 +#define TEXT_TO_LOOK_FOR @"Welcome to React" + +@interface RntrackerExampleTests : XCTestCase + +@end + +@implementation RntrackerExampleTests + +- (BOOL)findSubviewInView:(UIView *)view matching:(BOOL (^)(UIView *view))test +{ + if (test(view)) { + return YES; + } + for (UIView *subview in [view subviews]) { + if ([self findSubviewInView:subview matching:test]) { + return YES; + } + } + return NO; +} + +- (void)testRendersWelcomeScreen +{ + UIViewController *vc = [[[RCTSharedApplication() delegate] window] rootViewController]; + NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; + BOOL foundElement = NO; + + __block NSString *redboxError = nil; +#ifdef DEBUG + RCTSetLogFunction( + ^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) { + if (level >= RCTLogLevelError) { + redboxError = message; + } + }); +#endif + + while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) { + [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; + [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; + + foundElement = [self findSubviewInView:vc.view + matching:^BOOL(UIView *view) { + if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) { + return YES; + } + return NO; + }]; + } + +#ifdef DEBUG + RCTSetLogFunction(RCTDefaultLogFunction); +#endif + + XCTAssertNil(redboxError, @"RedBox error: %@", redboxError); + XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS); +} + +@end diff --git a/tracker/tracker-reactnative/example/jest.config.js b/tracker/tracker-reactnative/example/jest.config.js new file mode 100644 index 000000000..8eb675e9b --- /dev/null +++ b/tracker/tracker-reactnative/example/jest.config.js @@ -0,0 +1,3 @@ +module.exports = { + preset: 'react-native', +}; diff --git a/tracker/tracker-reactnative/example/metro.config.js b/tracker/tracker-reactnative/example/metro.config.js new file mode 100644 index 000000000..b82321455 --- /dev/null +++ b/tracker/tracker-reactnative/example/metro.config.js @@ -0,0 +1,45 @@ +const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config'); +const path = require('path'); +const escape = require('escape-string-regexp'); +const exclusionList = require('metro-config/src/defaults/exclusionList'); +const pak = require('../package.json'); + +const root = path.resolve(__dirname, '..'); +const modules = Object.keys({ ...pak.peerDependencies }); + +/** + * Metro configuration + * https://facebook.github.io/metro/docs/configuration + * + * @type {import('metro-config').MetroConfig} + */ +const config = { + watchFolders: [root], + + // We need to make sure that only one version is loaded for peerDependencies + // So we block them at the root, and alias them to the versions in example's node_modules + resolver: { + blacklistRE: exclusionList( + modules.map( + (m) => + new RegExp(`^${escape(path.join(root, 'node_modules', m))}\\/.*$`) + ) + ), + + extraNodeModules: modules.reduce((acc, name) => { + acc[name] = path.join(__dirname, 'node_modules', name); + return acc; + }, {}), + }, + + transformer: { + getTransformOptions: async () => ({ + transform: { + experimentalImportSupport: false, + inlineRequires: true, + }, + }), + }, +}; + +module.exports = mergeConfig(getDefaultConfig(__dirname), config); diff --git a/tracker/tracker-reactnative/example/package.json b/tracker/tracker-reactnative/example/package.json new file mode 100644 index 000000000..537899ad7 --- /dev/null +++ b/tracker/tracker-reactnative/example/package.json @@ -0,0 +1,26 @@ +{ + "name": "RntrackerExample", + "version": "0.0.1", + "private": true, + "scripts": { + "android": "react-native run-android", + "ios": "react-native run-ios", + "start": "react-native start", + "pods": "pod-install --quiet" + }, + "dependencies": { + "react": "18.2.0", + "react-native": "0.72.4" + }, + "devDependencies": { + "@babel/core": "^7.20.0", + "@babel/preset-env": "^7.20.0", + "@babel/runtime": "^7.20.0", + "@react-native/metro-config": "^0.72.11", + "metro-react-native-babel-preset": "0.76.8", + "babel-plugin-module-resolver": "^5.0.0" + }, + "engines": { + "node": ">=16" + } +} \ No newline at end of file diff --git a/tracker/tracker-reactnative/example/react-native.config.js b/tracker/tracker-reactnative/example/react-native.config.js new file mode 100644 index 000000000..a5166956f --- /dev/null +++ b/tracker/tracker-reactnative/example/react-native.config.js @@ -0,0 +1,10 @@ +const path = require('path'); +const pak = require('../package.json'); + +module.exports = { + dependencies: { + [pak.name]: { + root: path.join(__dirname, '..'), + }, + }, +}; diff --git a/tracker/tracker-reactnative/example/src/App.tsx b/tracker/tracker-reactnative/example/src/App.tsx new file mode 100644 index 000000000..a81051d49 --- /dev/null +++ b/tracker/tracker-reactnative/example/src/App.tsx @@ -0,0 +1,77 @@ +import * as React from 'react'; + +import { StyleSheet, View, Text, TouchableOpacity } from 'react-native'; +import Openreplay from '@openreplay/react-native'; + +export default function App() { + const [number, onChangeNumber] = React.useState(''); + + const start = () => { + Openreplay.tracker.startSession( + process.env.REACT_APP_KEY!, + {}, + process.env.REACT_APP_INGEST + ); + console.log('test', Openreplay.tracker, 123123); + }; + + const setMedatada = () => { + Openreplay.tracker.setMetadata('test', 'data'); + }; + + const event = () => { + Openreplay.tracker.event('test', JSON.stringify({ value: 'keyv' })); + }; + + const setID = () => { + Openreplay.tracker.setUserID('react-native@connector.me'); + }; + + return ( + + + + This is a tracker view with text + + + This is a sanitized view + + + Start + + + Set medatada + + + event + + + + Set user id + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + }, + box: { + width: 60, + height: 60, + marginVertical: 20, + }, + input: { height: 30, width: 100, borderWidth: 1 }, +}); diff --git a/tracker/tracker-reactnative/ios/Rntracker-Bridging-Header.h b/tracker/tracker-reactnative/ios/Rntracker-Bridging-Header.h new file mode 100644 index 000000000..755dfcf87 --- /dev/null +++ b/tracker/tracker-reactnative/ios/Rntracker-Bridging-Header.h @@ -0,0 +1,2 @@ +#import +#import diff --git a/tracker/tracker-reactnative/ios/Rntracker.xcodeproj/project.pbxproj b/tracker/tracker-reactnative/ios/Rntracker.xcodeproj/project.pbxproj new file mode 100644 index 000000000..a6ae48cad --- /dev/null +++ b/tracker/tracker-reactnative/ios/Rntracker.xcodeproj/project.pbxproj @@ -0,0 +1,283 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 5E555C0D2413F4C50049A1A2 /* Rntracker.mm in Sources */ = {isa = PBXBuildFile; fileRef = B3E7B5891CC2AC0600A0062D /* Rntracker.mm */; }; + F4FF95D7245B92E800C19C63 /* Rntracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4FF95D6245B92E800C19C63 /* Rntracker.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 58B511D91A9E6C8500147676 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "include/$(PRODUCT_NAME)"; + dstSubfolderSpec = 16; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 134814201AA4EA6300B7C361 /* libRntracker.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRntracker.a; sourceTree = BUILT_PRODUCTS_DIR; }; + B3E7B5891CC2AC0600A0062D /* Rntracker.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Rntracker.mm; sourceTree = ""; }; + F4FF95D5245B92E700C19C63 /* Rntracker-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Rntracker-Bridging-Header.h"; sourceTree = ""; }; + F4FF95D6245B92E800C19C63 /* Rntracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Rntracker.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 58B511D81A9E6C8500147676 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 134814211AA4EA7D00B7C361 /* Products */ = { + isa = PBXGroup; + children = ( + 134814201AA4EA6300B7C361 /* libRntracker.a */, + ); + name = Products; + sourceTree = ""; + }; + 58B511D21A9E6C8500147676 = { + isa = PBXGroup; + children = ( + F4FF95D6245B92E800C19C63 /* Rntracker.swift */, + B3E7B5891CC2AC0600A0062D /* Rntracker.mm */, + F4FF95D5245B92E700C19C63 /* Rntracker-Bridging-Header.h */, + 134814211AA4EA7D00B7C361 /* Products */, + ); + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 58B511DA1A9E6C8500147676 /* Rntracker */ = { + isa = PBXNativeTarget; + buildConfigurationList = 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "Rntracker" */; + buildPhases = ( + 58B511D71A9E6C8500147676 /* Sources */, + 58B511D81A9E6C8500147676 /* Frameworks */, + 58B511D91A9E6C8500147676 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Rntracker; + productName = RCTDataManager; + productReference = 134814201AA4EA6300B7C361 /* libRntracker.a */; + productType = "com.apple.product-type.library.static"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 58B511D31A9E6C8500147676 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0920; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 58B511DA1A9E6C8500147676 = { + CreatedOnToolsVersion = 6.1.1; + }; + }; + }; + buildConfigurationList = 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "Rntracker" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + English, + en, + ); + mainGroup = 58B511D21A9E6C8500147676; + productRefGroup = 58B511D21A9E6C8500147676; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 58B511DA1A9E6C8500147676 /* Rntracker */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + 58B511D71A9E6C8500147676 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F4FF95D7245B92E800C19C63 /* Rntracker.swift in Sources */, + B3E7B58A1CC2AC0600A0062D /* Rntracker.mm in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 58B511ED1A9E6C8500147676 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + "EXCLUDED_ARCHS[sdk=*]" = arm64; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + 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 = 8.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 58B511EE1A9E6C8500147676 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + "EXCLUDED_ARCHS[sdk=*]" = arm64; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 58B511F01A9E6C8500147676 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../../React/**", + "$(SRCROOT)/../../react-native/React/**", + ); + LIBRARY_SEARCH_PATHS = "$(inherited)"; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = Rntracker; + SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "Rntracker-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 58B511F11A9E6C8500147676 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../../React/**", + "$(SRCROOT)/../../react-native/React/**", + ); + LIBRARY_SEARCH_PATHS = "$(inherited)"; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = Rntracker; + SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "Rntracker-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "Rntracker" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 58B511ED1A9E6C8500147676 /* Debug */, + 58B511EE1A9E6C8500147676 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "Rntracker" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 58B511F01A9E6C8500147676 /* Debug */, + 58B511F11A9E6C8500147676 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 58B511D31A9E6C8500147676 /* Project object */; +} diff --git a/tracker/tracker-reactnative/ios/RntrackerConnector-Bridging-Header.h b/tracker/tracker-reactnative/ios/RntrackerConnector-Bridging-Header.h new file mode 100644 index 000000000..5fff35c15 --- /dev/null +++ b/tracker/tracker-reactnative/ios/RntrackerConnector-Bridging-Header.h @@ -0,0 +1 @@ +#import diff --git a/tracker/tracker-reactnative/ios/RntrackerConnector.m b/tracker/tracker-reactnative/ios/RntrackerConnector.m new file mode 100644 index 000000000..7f1790180 --- /dev/null +++ b/tracker/tracker-reactnative/ios/RntrackerConnector.m @@ -0,0 +1,12 @@ +#import + +@interface RCT_EXTERN_MODULE(ORTrackerConnector, NSObject) +RCT_EXTERN_METHOD(start:(NSString *)projectKey optionsDict:(NSDictionary *)optionsDict projectUrl:(NSString *)projectUrl) +RCT_EXTERN_METHOD(startSession:(NSString *)projectKey optionsDict:(NSDictionary *)optionsDict projectUrl:(NSString *)projectUrl) +RCT_EXTERN_METHOD(stop) +RCT_EXTERN_METHOD(setMetadata:(NSString *)key value:(NSString *)value) +RCT_EXTERN_METHOD(event:(NSString *)name object:(NSString *)object) +RCT_EXTERN_METHOD(setUserID:(NSString *)userID) +RCT_EXTERN_METHOD(userAnonymousID:(NSString *)userID) +@end + diff --git a/tracker/tracker-reactnative/ios/RntrackerConnector.swift b/tracker/tracker-reactnative/ios/RntrackerConnector.swift new file mode 100644 index 000000000..c17452cdf --- /dev/null +++ b/tracker/tracker-reactnative/ios/RntrackerConnector.swift @@ -0,0 +1,67 @@ +import ORTracker + +@objc(ORTrackerConnector) +public class ORTrackerConnector: NSObject { + @objc public static func moduleName() -> String { + return "ORTrackerConnector" + } + + @objc public static func requiresMainQueueSetup() -> Bool { + return true + } + + @objc(start:optionsDict:projectUrl:) + open func start(_ projectKey: String, optionsDict: NSDictionary, projectUrl: String?) { + let options = OROptions( + crashes: optionsDict["crashes"] as? Bool ?? true, + analytics: optionsDict["analytics"] as? Bool ?? true, + performances: optionsDict["performances"] as? Bool ?? true, + logs: optionsDict["logs"] as? Bool ?? true, + screen: optionsDict["screen"] as? Bool ?? true, + wifiOnly: optionsDict["wifiOnly"] as? Bool ?? true + ) + ORTracker.shared.serverURL = projectUrl ?? "https://app.openreplay.com/ingest" + ORTracker.shared.start(projectKey: projectKey, options: options) + print("Starting for \(options) \(projectKey) \(projectUrl ?? "no url")") + } + + @objc open func stop() { + ORTracker.shared.stop() + } + + @objc(startSession:optionsDict:projectUrl:) + open func startSession(_ projectKey: String, optionsDict: NSDictionary, projectUrl: String?) { + let options = OROptions( + crashes: optionsDict["crashes"] as? Bool ?? true, + analytics: optionsDict["analytics"] as? Bool ?? true, + performances: optionsDict["performances"] as? Bool ?? true, + logs: optionsDict["logs"] as? Bool ?? true, + screen: optionsDict["screen"] as? Bool ?? true, + wifiOnly: false + ) + ORTracker.shared.serverURL = projectUrl ?? "https://app.openreplay.com/ingest" + ORTracker.shared.startSession(projectKey: projectKey, options: options) + print("Starting for \(options) \(projectKey) \(projectUrl ?? "no url")") + } + + @objc(setMetadata:value:) + open func setMetadata(_ key: String, value: String) { + ORTracker.shared.setMetadata(key: key, value: value) + } + + @objc(event:object:) + open func event(_ name: String, object: String?) { + ORTracker.shared.eventStr(name: name, payload: object) + } + + + @objc(setUserID:) + open func setUserID(_ userID: String) { + ORTracker.shared.setUserID(userID) + } + + @objc(userAnonymousID:) + open func userAnonymousID(_ userID: String) { + ORTracker.shared.userAnonymousID(userID) + } +} diff --git a/tracker/tracker-reactnative/ios/RntrackerInputManager.m b/tracker/tracker-reactnative/ios/RntrackerInputManager.m new file mode 100644 index 000000000..9423bb050 --- /dev/null +++ b/tracker/tracker-reactnative/ios/RntrackerInputManager.m @@ -0,0 +1,4 @@ +#import + +@interface RCT_EXTERN_MODULE(RntrackerInputManager, RCTViewManager) +@end diff --git a/tracker/tracker-reactnative/ios/RntrackerInputManager.swift b/tracker/tracker-reactnative/ios/RntrackerInputManager.swift new file mode 100644 index 000000000..12084f0ff --- /dev/null +++ b/tracker/tracker-reactnative/ios/RntrackerInputManager.swift @@ -0,0 +1,24 @@ +import ORTracker + +@objc(RntrackerInputManager) +class RntrackerInputManager: RCTViewManager { + + override func view() -> (RntrackerInput) { + return RntrackerInput() + } + + @objc override static func requiresMainQueueSetup() -> Bool { + return true + } +} + + +class RntrackerInput : UITextField { + override func didMoveToSuperview() { + super.didMoveToSuperview() + + if superview != nil { + Analytics.shared.addObservedInput(self) + } + } +} diff --git a/tracker/tracker-reactnative/ios/RntrackerSanitizedViewManager.m b/tracker/tracker-reactnative/ios/RntrackerSanitizedViewManager.m new file mode 100644 index 000000000..5fb2f8cf1 --- /dev/null +++ b/tracker/tracker-reactnative/ios/RntrackerSanitizedViewManager.m @@ -0,0 +1,6 @@ + +#import + +@interface RCT_EXTERN_MODULE(RntrackerSanitizedViewManager, RCTViewManager) + +@end diff --git a/tracker/tracker-reactnative/ios/RntrackerSanitizedViewManager.swift b/tracker/tracker-reactnative/ios/RntrackerSanitizedViewManager.swift new file mode 100644 index 000000000..c1a88d468 --- /dev/null +++ b/tracker/tracker-reactnative/ios/RntrackerSanitizedViewManager.swift @@ -0,0 +1,23 @@ +import ORTracker + +@objc(RntrackerSanitizedViewManager) +class RntrackerSanitizedViewManager: RCTViewManager { + + override func view() -> (RntrackerSanitizedView) { + return RntrackerSanitizedView() + } + + @objc override static func requiresMainQueueSetup() -> Bool { + return true + } +} + +class RntrackerSanitizedView : UIView { + override func didMoveToSuperview() { + super.didMoveToSuperview() + + if superview != nil { + ScreenshotManager.shared.addSanitizedElement(self) + } + } +} diff --git a/tracker/tracker-reactnative/ios/RntrackerTouchManager.m b/tracker/tracker-reactnative/ios/RntrackerTouchManager.m new file mode 100644 index 000000000..193e4f232 --- /dev/null +++ b/tracker/tracker-reactnative/ios/RntrackerTouchManager.m @@ -0,0 +1,6 @@ + +#import + +@interface RCT_EXTERN_MODULE(RntrackerTouchManager, RCTViewManager) + +@end diff --git a/tracker/tracker-reactnative/ios/RntrackerTouchManager.swift b/tracker/tracker-reactnative/ios/RntrackerTouchManager.swift new file mode 100644 index 000000000..998dd3c24 --- /dev/null +++ b/tracker/tracker-reactnative/ios/RntrackerTouchManager.swift @@ -0,0 +1,44 @@ +import ORTracker + +@objc(RntrackerTouchManager) +class RntrackerTouchManager: RCTViewManager { + + override func view() -> (RntrackerTouchView) { + return RntrackerTouchView() + } + + @objc override static func requiresMainQueueSetup() -> Bool { + return true + } +} + +class RntrackerTouchView : UIView { + var touchStart: CGPoint? + + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + super.touchesBegan(touches, with: event) + + if let touch = touches.first { + touchStart = touch.location(in: self) + } + } + + override func touchesEnded(_ touches: Set, with event: UIEvent?) { + super.touchesEnded(touches, with: event) + + guard let touch = touches.first, let startPoint = touchStart else { return } + let endPoint = touch.location(in: self) + + let deltaX = endPoint.x - startPoint.x + let deltaY = endPoint.y - startPoint.y + + let distance = sqrt(deltaX * deltaX + deltaY * deltaY) + + if distance > 10 { + let direction = abs(deltaX) > abs(deltaY) ? (deltaX > 0 ? "right" : "left") : (deltaY > 0 ? "down" : "up") + Analytics.shared.sendSwipe(label: "React-Native View", x: UInt64(endPoint.x), y: UInt64(endPoint.x), direction: direction) + } else { + Analytics.shared.sendClick(label: "React-Native View", x: UInt64(endPoint.x), y: UInt64(endPoint.y)) + } + } +} diff --git a/tracker/tracker-reactnative/ios/RntrackerViewManager.m b/tracker/tracker-reactnative/ios/RntrackerViewManager.m new file mode 100644 index 000000000..f94411da5 --- /dev/null +++ b/tracker/tracker-reactnative/ios/RntrackerViewManager.m @@ -0,0 +1,6 @@ +#import + +@interface RCT_EXTERN_MODULE(RntrackerViewManager, RCTViewManager) +RCT_EXPORT_VIEW_PROPERTY(viewName, NSString) +RCT_EXPORT_VIEW_PROPERTY(screenName, NSString) +@end diff --git a/tracker/tracker-reactnative/ios/RntrackerViewManager.swift b/tracker/tracker-reactnative/ios/RntrackerViewManager.swift new file mode 100644 index 000000000..3ec8c27c1 --- /dev/null +++ b/tracker/tracker-reactnative/ios/RntrackerViewManager.swift @@ -0,0 +1,37 @@ +import ORTracker + +@objc(RntrackerViewManager) +class RntrackerViewManager: RCTViewManager { + + override func view() -> (RntrackerView) { + return RntrackerView() + } + + @objc override static func requiresMainQueueSetup() -> Bool { + return true + } +} + +class RntrackerView : UIView { + var _orViewName: String = "" + var _orScreenName: String = "" + + @objc var viewName: String = "" { + didSet { + self._orViewName = viewName + } + } + @objc var screenName: String = "" { + didSet { + self._orScreenName = screenName + } + } + + override func didMoveToSuperview() { + super.didMoveToSuperview() + + if superview != nil { + Analytics.shared.addObservedView(view: self, screenName: self._orScreenName, viewName: self._orViewName) + } + } +} diff --git a/tracker/tracker-reactnative/lefthook.yml b/tracker/tracker-reactnative/lefthook.yml new file mode 100644 index 000000000..065a491b0 --- /dev/null +++ b/tracker/tracker-reactnative/lefthook.yml @@ -0,0 +1,16 @@ +pre-commit: + parallel: true + commands: + lint: + files: git diff --name-only @{push} + glob: "*.{js,ts,jsx,tsx}" + run: npx eslint {files} + types: + files: git diff --name-only @{push} + glob: "*.{js,ts, jsx, tsx}" + run: npx tsc --noEmit +commit-msg: + parallel: true + commands: + commitlint: + run: npx commitlint --edit diff --git a/tracker/tracker-reactnative/openreplay-reactnative.podspec b/tracker/tracker-reactnative/openreplay-reactnative.podspec new file mode 100644 index 000000000..b5cb8fb81 --- /dev/null +++ b/tracker/tracker-reactnative/openreplay-reactnative.podspec @@ -0,0 +1,45 @@ +require "json" + +package = JSON.parse(File.read(File.join(__dir__, "package.json"))) +folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32' + +Pod::Spec.new do |s| + s.name = "openreplay-reactnative" + s.version = package["version"] + s.summary = package["description"] + s.homepage = package["homepage"] + s.license = package["license"] + s.authors = package["author"] + + s.platforms = { :ios => "13.0" } + s.source = { :git => "https://github.com/openreplay/openreplay/tracker/tracker-reactnative", :tag => "#{s.version}" } + + s.source_files = "ios/**/*.{h,m,mm,swift}" + + # Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0. + # See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79. + if respond_to?(:install_modules_dependencies, true) + s.dependency "ORTracker" + install_modules_dependencies(s) + else + s.dependency "React-Core" + s.dependency "ORTracker" + + # Don't install the dependencies when we run `pod install` in the old architecture. + if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then + s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1" + s.pod_target_xcconfig = { + "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"", + "OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1", + "CLANG_CXX_LANGUAGE_STANDARD" => "c++17" + } + s.dependency "React-RCTFabric" + s.dependency "React-Codegen" + s.dependency "RCT-Folly" + s.dependency "ORTracker" + s.dependency "RCTRequired" + s.dependency "RCTTypeSafety" + s.dependency "ReactCommon/turbomodule/core" + end + end +end diff --git a/tracker/tracker-reactnative/package.json b/tracker/tracker-reactnative/package.json new file mode 100644 index 000000000..588c1c2b3 --- /dev/null +++ b/tracker/tracker-reactnative/package.json @@ -0,0 +1,163 @@ +{ + "name": "@openreplay/react-native", + "version": "0.1.0", + "description": "Openreplay React-native connector for iOS applications", + "main": "lib/commonjs/index", + "module": "lib/module/index", + "types": "lib/typescript/src/index.d.ts", + "react-native": "src/index", + "source": "src/index", + "files": [ + "src", + "lib", + "android", + "ios", + "cpp", + "*.podspec", + "!lib/typescript/example", + "!ios/build", + "!android/build", + "!android/gradle", + "!android/gradlew", + "!android/gradlew.bat", + "!android/local.properties", + "!**/__tests__", + "!**/__fixtures__", + "!**/__mocks__", + "!**/.*" + ], + "scripts": { + "test": "jest", + "typecheck": "tsc --noEmit", + "lint": "eslint \"**/*.{js,ts,tsx}\"", + "prepack": "bob build", + "release": "release-it", + "example": "yarn --cwd example", + "build:android": "cd example/android && ./gradlew assembleDebug --no-daemon --console=plain -PreactNativeArchitectures=arm64-v8a", + "build:ios": "cd example/ios && xcodebuild -workspace RntrackerExample.xcworkspace -scheme RntrackerExample -configuration Debug -sdk iphonesimulator CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ GCC_OPTIMIZATION_LEVEL=0 GCC_PRECOMPILE_PREFIX_HEADER=YES ASSETCATALOG_COMPILER_OPTIMIZATION=time DEBUG_INFORMATION_FORMAT=dwarf COMPILER_INDEX_STORE_ENABLE=NO", + "bootstrap": "yarn example && yarn install && yarn example pods", + "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build" + }, + "keywords": [ + "react-native", + "ios", + "android" + ], + "repository": "https://github.com/openreplay/openreplay/tracker/tracker-reactnative", + "author": "nick-delirium (https://github.com/nick-delirium)", + "license": "MIT", + "bugs": { + "url": "https://github.com/openreplay/openreplay/issues" + }, + "homepage": "https://github.com/openreplay/openreplay/tracker/tracker-reactnative", + "publishConfig": { + "registry": "https://registry.npmjs.org/" + }, + "devDependencies": { + "@commitlint/config-conventional": "^17.0.2", + "@evilmartians/lefthook": "^1.2.2", + "@react-native/eslint-config": "^0.72.2", + "@release-it/conventional-changelog": "^5.0.0", + "@types/jest": "^28.1.2", + "@types/react": "~17.0.21", + "@types/react-native": "0.70.0", + "commitlint": "^17.0.2", + "del-cli": "^5.0.0", + "eslint": "^8.4.1", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-prettier": "^4.0.0", + "jest": "^28.1.1", + "pod-install": "^0.1.0", + "prettier": "^2.0.5", + "react": "18.2.0", + "react-native": "0.72.4", + "react-native-builder-bob": "^0.22.0", + "release-it": "^15.0.0", + "turbo": "^1.10.7", + "typescript": "^5.0.2" + }, + "resolutions": { + "@types/react": "17.0.21" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + }, + "engines": { + "node": ">= 18.0.0" + }, + "packageManager": "^yarn@1.22.15", + "jest": { + "preset": "react-native", + "modulePathIgnorePatterns": [ + "/example/node_modules", + "/lib/" + ] + }, + "commitlint": { + "extends": [ + "@commitlint/config-conventional" + ] + }, + "release-it": { + "git": { + "commitMessage": "chore: release ${version}", + "tagName": "v${version}" + }, + "npm": { + "publish": true + }, + "github": { + "release": true + }, + "plugins": { + "@release-it/conventional-changelog": { + "preset": "angular" + } + } + }, + "eslintConfig": { + "root": true, + "extends": [ + "@react-native", + "prettier" + ], + "rules": { + "prettier/prettier": [ + "error", + { + "quoteProps": "consistent", + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "useTabs": false + } + ] + } + }, + "eslintIgnore": [ + "node_modules/", + "lib/" + ], + "prettier": { + "quoteProps": "consistent", + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "useTabs": false + }, + "react-native-builder-bob": { + "source": "src", + "output": "lib", + "targets": [ + "commonjs", + "module", + [ + "typescript", + { + "project": "tsconfig.build.json" + } + ] + ] + } +} diff --git a/tracker/tracker-reactnative/scripts/bootstrap.js b/tracker/tracker-reactnative/scripts/bootstrap.js new file mode 100644 index 000000000..172918947 --- /dev/null +++ b/tracker/tracker-reactnative/scripts/bootstrap.js @@ -0,0 +1,29 @@ +const os = require('os'); +const path = require('path'); +const child_process = require('child_process'); + +const root = path.resolve(__dirname, '..'); +const args = process.argv.slice(2); +const options = { + cwd: process.cwd(), + env: process.env, + stdio: 'inherit', + encoding: 'utf-8', +}; + +if (os.type() === 'Windows_NT') { + options.shell = true; +} + +let result; + +if (process.cwd() !== root || args.length) { + // We're not in the root of the project, or additional arguments were passed + // In this case, forward the command to `yarn` + result = child_process.spawnSync('yarn', args, options); +} else { + // If `yarn` is run without arguments, perform bootstrap + result = child_process.spawnSync('yarn', ['bootstrap'], options); +} + +process.exitCode = result.status; diff --git a/tracker/tracker-reactnative/src/__tests__/index.test.tsx b/tracker/tracker-reactnative/src/__tests__/index.test.tsx new file mode 100644 index 000000000..bf84291a5 --- /dev/null +++ b/tracker/tracker-reactnative/src/__tests__/index.test.tsx @@ -0,0 +1 @@ +it.todo('write a test'); diff --git a/tracker/tracker-reactnative/src/index.tsx b/tracker/tracker-reactnative/src/index.tsx new file mode 100644 index 000000000..580f40572 --- /dev/null +++ b/tracker/tracker-reactnative/src/index.tsx @@ -0,0 +1,92 @@ +import { + requireNativeComponent, + UIManager, + Platform, + NativeModules, + View, + TextInput, +} from 'react-native'; +import type { ViewProps, TextInputProps } from 'react-native'; + +const { ORTrackerConnector } = NativeModules; + +const LINKING_ERROR = + `The package '@openreplay/react-native' doesn't seem to be linked. Make sure: \n\n` + + Platform.select({ ios: "- You have run 'pod install'\n", default: '' }) + + '- You rebuilt the app after installing the package\n' + + '- You are not using Expo Go\n'; + +type RntrackerProps = ViewProps & { + viewName: string; + screenName: string; + children: any; +}; + +const RntrackerSanitizedView = + UIManager.getViewManagerConfig('RntrackerSanitizedView') != null + ? requireNativeComponent('RntrackerSanitizedView') + : () => { + throw new Error('RntrackerSanitizedView; ' + LINKING_ERROR); + }; + +const RntrackerInput = + UIManager.getViewManagerConfig('RntrackerInput') != null + ? requireNativeComponent('RntrackerInput') + : () => { + throw new Error('RntrackerInput; ' + LINKING_ERROR); + }; + +const RntrackerView = + UIManager.getViewManagerConfig('RntrackerView') != null + ? requireNativeComponent('RntrackerView') + : () => { + throw new Error('RntrackerView; ' + LINKING_ERROR); + }; + +const RntrackerTouchTrackingView = + UIManager.getViewManagerConfig('RntrackerTouch') != null + ? requireNativeComponent('RntrackerTouch') + : () => { + throw new Error('RntrackerTouchTrackingView; ' + LINKING_ERROR); + }; + +interface Options { + crashes?: boolean; + analytics?: boolean; + performances?: boolean; + logs?: boolean; + screen?: boolean; +} + +interface IORTrackerConnector { + startSession: ( + projectKey: string, + optionsDict: Options, + projectUrl?: string + ) => void; + setMetadata: (key: string, value: string) => void; + event: (name: string, payload?: string) => void; + setUserID: (userID: string) => void; + userAnonymousID: (userID: string) => void; +} + +const emptyShell = { + startSession: () => + console.log('Openreplay: SDK only supports iOS at the moment'), + setMetadata: () => null, + event: () => null, + setUserID: () => null, + userAnonymousID: () => null, +}; + +export default { + tracker: + Platform.OS === 'ios' + ? (ORTrackerConnector as IORTrackerConnector) + : emptyShell, + ORTouchTrackingView: + Platform.OS === 'ios' ? RntrackerTouchTrackingView : View, + ORSanitizedView: Platform.OS === 'ios' ? RntrackerSanitizedView : View, + ORTrackedInput: Platform.OS === 'ios' ? RntrackerInput : TextInput, + ORAnalyticsView: Platform.OS === 'ios' ? RntrackerView : View, +}; diff --git a/tracker/tracker-reactnative/tsconfig.build.json b/tracker/tracker-reactnative/tsconfig.build.json new file mode 100644 index 000000000..999d3f3c8 --- /dev/null +++ b/tracker/tracker-reactnative/tsconfig.build.json @@ -0,0 +1,5 @@ + +{ + "extends": "./tsconfig", + "exclude": ["example"] +} diff --git a/tracker/tracker-reactnative/tsconfig.json b/tracker/tracker-reactnative/tsconfig.json new file mode 100644 index 000000000..57bbded5f --- /dev/null +++ b/tracker/tracker-reactnative/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "rootDir": ".", + "paths": { + "@openreplay/react-native": ["./src/index"] + }, + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "lib": ["esnext"], + "module": "esnext", + "moduleResolution": "node", + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "noImplicitUseStrict": false, + "noStrictGenericChecks": false, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "esnext", + "verbatimModuleSyntax": true + } +} diff --git a/tracker/tracker-reactnative/turbo.json b/tracker/tracker-reactnative/turbo.json new file mode 100644 index 000000000..331e28900 --- /dev/null +++ b/tracker/tracker-reactnative/turbo.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://turbo.build/schema.json", + "pipeline": { + "build:android": { + "inputs": [ + "package.json", + "android", + "!android/build", + "src/*.ts", + "src/*.tsx", + "example/package.json", + "example/android", + "!example/android/.gradle", + "!example/android/build", + "!example/android/app/build" + ], + "outputs": [] + }, + "build:ios": { + "inputs": [ + "package.json", + "*.podspec", + "ios", + "src/*.ts", + "src/*.tsx", + "example/package.json", + "example/ios", + "!example/ios/build", + "!example/ios/Pods" + ], + "outputs": [] + } + } +}