From 37c4274f4a91cba0479f1c864d9ec2084963f549 Mon Sep 17 00:00:00 2001 From: ijunaid Date: Fri, 20 May 2022 15:15:12 +0500 Subject: [PATCH] Updated base SDK's (#92) -- Updated SDK version to 21.11.0 -- Updated underlying android SDK to 21.11.1 -- Updated underlying iOS SDK version to 21.11.2 --- .versions | 2 +- CHANGELOG.md | 4 + Countly.js | 2 +- package.js | 2 +- package.json | 2 +- plugin.xml | 4 +- src/android/CountlyNative.java | 2 +- src/ios/CountlyNative.m | 2 +- src/ios/CountlyiOS/CHANGELOG.md | 570 ++++++++++++++++++ src/ios/CountlyiOS/Countly-PL.podspec | 43 ++ src/ios/CountlyiOS/Countly.h | 107 ++-- src/ios/CountlyiOS/Countly.m | 175 +++++- src/ios/CountlyiOS/Countly.podspec | 31 + src/ios/CountlyiOS/CountlyCommon.h | 21 +- src/ios/CountlyiOS/CountlyCommon.m | 177 +++--- src/ios/CountlyiOS/CountlyConfig.h | 132 ++-- src/ios/CountlyiOS/CountlyConfig.m | 6 + src/ios/CountlyiOS/CountlyConnectionManager.h | 8 +- src/ios/CountlyiOS/CountlyConnectionManager.m | 150 +++-- src/ios/CountlyiOS/CountlyConsentManager.h | 4 +- src/ios/CountlyiOS/CountlyConsentManager.m | 123 +--- src/ios/CountlyiOS/CountlyDeviceInfo.h | 4 - src/ios/CountlyiOS/CountlyDeviceInfo.m | 89 +-- src/ios/CountlyiOS/CountlyFeedbackWidget.h | 2 + src/ios/CountlyiOS/CountlyFeedbackWidget.m | 25 +- src/ios/CountlyiOS/CountlyFeedbacks.h | 3 + src/ios/CountlyiOS/CountlyFeedbacks.m | 28 +- .../CountlyiOS/CountlyPerformanceMonitoring.m | 5 + src/ios/CountlyiOS/CountlyPersistency.m | 16 +- src/ios/CountlyiOS/CountlyPushNotifications.h | 2 + src/ios/CountlyiOS/CountlyPushNotifications.m | 304 ++-------- src/ios/CountlyiOS/CountlyUserDetails.m | 128 +++- src/ios/CountlyiOS/CountlyViewTracking.h | 2 + src/ios/CountlyiOS/CountlyViewTracking.m | 2 + src/ios/CountlyiOS/LICENSE.md | 19 + src/ios/CountlyiOS/README.md | 59 ++ src/ios/CountlyiOS/SECURITY.md | 3 + src/ios/CountlyiOS/countly_dsym_uploader.sh | 121 ++++ 38 files changed, 1700 insertions(+), 679 deletions(-) create mode 100644 src/ios/CountlyiOS/CHANGELOG.md create mode 100644 src/ios/CountlyiOS/Countly-PL.podspec create mode 100644 src/ios/CountlyiOS/Countly.podspec create mode 100644 src/ios/CountlyiOS/LICENSE.md create mode 100644 src/ios/CountlyiOS/README.md create mode 100644 src/ios/CountlyiOS/SECURITY.md create mode 100755 src/ios/CountlyiOS/countly_dsym_uploader.sh diff --git a/.versions b/.versions index d55efaf..be6e359 100644 --- a/.versions +++ b/.versions @@ -1,2 +1,2 @@ -countly:countly-sdk-js@20.11.3 +countly:countly-sdk-js@21.11.0 meteor@1.9.2 diff --git a/CHANGELOG.md b/CHANGELOG.md index c15ea54..962ad61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 21.11.0 +* Updated underlying android SDK to 21.11.1 +* Updated underlying iOS SDK version to 21.11.2 + ## 20.11.3 * Added COUNTLY_EXCLUDE_PUSHNOTIFICATIONS flag to disable push notifications altogether in order to avoid App Store Connect warnings. * Fixed issues related to Push notification crash when notification recieved from other SDK's/Plugins (not from Countly). diff --git a/Countly.js b/Countly.js index f45c2dc..e1a81f7 100644 --- a/Countly.js +++ b/Countly.js @@ -2,7 +2,7 @@ Countly = {}; Countly.serverUrl = ""; Countly.appKey = ""; Countly.ready = false; -Countly.version = "20.11.3"; +Countly.version = "21.11.0"; Countly.isDebug = false; Countly.isInitCalled = false; if (window.cordova.platformId == "android") { diff --git a/package.js b/package.js index ee80376..d171dc1 100644 --- a/package.js +++ b/package.js @@ -1,6 +1,6 @@ Package.describe({ name: 'countly:countly-sdk-js', - version: '20.11.3', + version: '21.11.0', summary: 'Countly is an innovative, real-time, open source mobile analytics and push notifications platform. It collects data from mobile devices, and visualizes this information to analyze mobile application usage and end-user behavior. There are two parts of Countly: the server that collects and analyzes data, and mobile SDK that sends this data. Both parts are open source with different licensing terms.', git: 'https://github.com/Countly/countly-sdk-cordova.git', documentation: 'README.md' diff --git a/package.json b/package.json index 54335ba..b31ead3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "countly-sdk-js", - "version": "20.11.3", + "version": "21.11.0", "description": "Countly is an innovative, real-time, open source mobile analytics and push notifications platform. It collects data from mobile devices, and visualizes this information to analyze mobile application usage and end-user behavior. There are two parts of Countly: the server that collects and analyzes data, and mobile SDK that sends this data. Both parts are open source with different licensing terms.", "cordova": { "id": "countly-sdk-js", diff --git a/plugin.xml b/plugin.xml index 8cbf4a8..d3533d6 100644 --- a/plugin.xml +++ b/plugin.xml @@ -1,5 +1,5 @@ - + Countly Cordova SDK @@ -141,7 +141,7 @@ - + diff --git a/src/android/CountlyNative.java b/src/android/CountlyNative.java index cf20085..e7317e4 100644 --- a/src/android/CountlyNative.java +++ b/src/android/CountlyNative.java @@ -36,7 +36,7 @@ public class CountlyNative { public static final String TAG = "CountlyCordovaPlugin"; - private String COUNTLY_CORDOVA_SDK_VERSION_STRING = "20.11.3"; + private String COUNTLY_CORDOVA_SDK_VERSION_STRING = "21.11.0"; private String COUNTLY_CORDOVA_SDK_NAME = "js-cordovab-android"; private Countly.CountlyMessagingMode pushTokenTypeVariable = Countly.CountlyMessagingMode.PRODUCTION; diff --git a/src/ios/CountlyNative.m b/src/ios/CountlyNative.m index 0f3680f..5416b62 100644 --- a/src/ios/CountlyNative.m +++ b/src/ios/CountlyNative.m @@ -19,7 +19,7 @@ Boolean isInitialized = false; NSString *const pushPluginApplicationDidBecomeActiveNotification = @"pushPluginApplicationDidBecomeActiveNotification"; -NSString* const kCountlyCordovaSDKVersion = @"20.11.3"; +NSString* const kCountlyCordovaSDKVersion = @"21.11.0"; NSString* const kCountlyCordovaSDKName = @"js-cordovab-ios"; @interface CountlyFeedbackWidget () diff --git a/src/ios/CountlyiOS/CHANGELOG.md b/src/ios/CountlyiOS/CHANGELOG.md new file mode 100644 index 0000000..32f36a7 --- /dev/null +++ b/src/ios/CountlyiOS/CHANGELOG.md @@ -0,0 +1,570 @@ +## 21.11.2 +- Added direct and indirect attribution +- Added platform info to default segmentation of push action events +- Added `recordRatingWidgetWithID:rating:email:comment:userCanBeContacted:` method to be able to manually record rating widgets +- Added macOS version info to `Countly.xcodeproj` (thanks @ntadej) +- Updated sending consent changes to inlude all current consents state +- Excluded Countly-PL.podspec from SPM manifest (thanks @harrisg) +- Fixed possible SecTrustCopyExceptions leak +- Deprecated `presentFeedbackWidgetWithID:completionHandler:` method + + + +## 21.11.1 +- Fixed a crash when some default user detail properties are set to `NSNull` (thanks @lhunath) +- Updated README.md for minimum supported deployment targets + + + +## 21.11.0 +- Updated minimum supported OS versions as `iOS 10.0`, `tvOS 10.0`, `watchOS 4.0` and `macOS 10.14` +- Updated some deprecated API usage to get rid of warnings +- Added configurable internal limits `maxKeyLength`, `maxValueLength` and `maxSegmentationValues` +- Added `enableOrientationTracking` config for disabling automatic user interface orientation tracking +- Added `setNewHost:` method to be able change the host on the go +- Added `shouldIgnoreTrustCheck` config for self-signed certificates (thanks @centrinvest) +- Created additional `Countly-PL.podspec` for avoiding static framework issue on original `Countly.podspec` (thanks @multinerd) +- Implemented cancelling all consents when device ID is changed without a merge +- Implemented by-passing events consent for reserved internal events +- Discarded consent requirement for changing device ID +- Discarded auto metrics for Apple Watch +- Discarded `customHeaderFieldName` and `customHeaderFieldValue` config properties +- Discarded `setCustomHeaderFieldValue:` method +- Fixed missing nullability specifier on `CountlyCommon.h` +- Fixed missing info level logs on `CountlyFeedbackWidget` class +- Fixed missing info level logs on `CountlyUserDetails` class +- Deprecated `userLoggedIn:` and `userLoggedOut` methods +- Deprecated going back to default system device ID + +- Other various improvements + - Updated HeaderDocs, internal logs, inline notes and pragma marks + - Updated Countly project settings for Xcode 13.1 + - Deleted previously deprecated methods and properties + - Refactored `connectionType` method + + + +## 20.11.3 +- Added optional appear and dismiss callbacks for feedback widget presenting +- Added manually displayed and recorded feedback widgets support +- Fixed HTTP method check for feedback widget requests +- Implemented immediately sending of queued events when a widget event is recorded + + + +## 20.11.2 +- Added configurable internal log levels +- Added internal logs for approximate received and sent data size for requests +- Added numbers and boolean value types for custom user details methods +- Added `clearCrashLogs` method for clearing custom crash logs (breadcrumbs) +- Added `navigationItem`'s title as a view title fallback for view tracking +- Added Mac Catalyst support +- Added selector precaution for `CountlyLoggerDelegate` method call +- Added precautions for nil values in custom user details methods +- Updated request successful check to consider response object +- Updated default `eventSendThreshold` as 100 +- Fixed `UIApplicationState` usage for crashes occured on non-main thread +- Fixed clearing custom crash logs +- Fixed missing frameworks for `ns` subspec in `podspec` file +- Fixed CountlyLoggerDelegate methods optionality +- Fixed view tracking exception view checking +- Fixed adding and removing view tracking exceptions on tvOS +- Fixed cast warnings for an APM method internal log + +- Other various improvements + - Updated HeaderDocs, internal logs, inline notes and pragma marks + - Updated Countly project settings for Xcode 12.4 + + + +## 20.11.1 +- Added `loggerDelegate` initial config property for receiving internal logs on production builds +- Fixed manual view tracking state clean up when view tracking consent is cancelled +- Updated `CountlyFeedbackWidget.h` as public header file in Xcode project file for Carthage +- Added nullability specifiers for block parameters + + + +## 20.11.0 +- Added Surveys and NPS feedback widgets +- Added Swift Package Manager support +- Added `replaceAllAppKeysInQueueWithCurrentAppKey` method to replace all app keys in queue with the current app key +- Added `removeDifferentAppKeysFromQueue` method to remove all different app keys from the queue +- Added `deviceIDType` method to be able to check device ID type +- Added precaution and warning for `nil` crash report case +- Added `consents` initial config property +- Added device type metric +- Updated dismiss button design +- Fixed web view autoresizing mask for legacy feedback widgets +- Fixed a missing `CoreLocation` framework import +- Fixed unnecessary recreation of `NSURLSession` instances +- Fixed dismiss button layout +- Changed interface orientation change event consent from `Events` to `UserDetails` +- Changed remote config consent from `Any` to `RemoteConfig` +- Marked `pushTestMode` initial config property as `_Nullable` + +- Other various improvements + - Refactored picture upload data extraction + - Suppressed an internal log for interface orientation change + - Updated some constant key declarations for common use + - Updated HeaderDocs, internal logs, inline notes and pragma marks + + + +## 20.04.3 +- Deprecated `recordLocation:`, `recordCity:andISOCountryCode:`, `recordIP:` methods +- Added new combined `recordLocation:city:ISOCountryCode:IP:` method for recording location related info +- Deprecated `enableAttribution` initial config flag +- Added `attributionID` initial config property +- Added `recordAttributionID:` method +- Discarded IDFA usage on optional attribution feature +- Discarded `COUNTLY_EXCLUDE_IDFA` preprocessor flag +- Updated `PLCrashReporter` subspec dependency version specifier as `~> 1` + +- Other various improvements + - Updated HeaderDocs, internal logs, inline notes and pragma marks + - Updated some initial config property modifiers as `copy` + - Treated empty string `city`, `ISOCountryCode` and `IP` values as `nil` + - Added warnings for the cases where `city` and `ISOCountryCode` code are not set as a pair + + + +## 20.04.2 +- Implemented overriding default metrics and adding custom ones +- Fixed advertising tracking enabled check + +- Other various improvements + - Improved internal logs for pinned certificate check + - Refactored extra slash check using `hasSuffix:` method + - Renamed some app life cycle observing methods for clarity + + + +## 20.04.1 +- Added Application Performance Monitoring (Phase 1) + - Manual network traces + - Manual custom traces + - Semi-automatic app start time trace + - Automatic app foreground time trace + - Automatic app background time trace + - Consent handling for Application Performance Monitoring +- Added `COUNTLY_EXCLUDE_PUSHNOTIFICATIONS` flag to disable push notifications altogether in order to avoid App Store Connect warnings (thanks @grundleborg) +- Fixed an incorrect internal logging on SDK start +- Fixed location consent order to avoid some legacy Countly Server issue with location info being unavailable even after giving consent +- Improved `UIApplicationWillTerminateNotification` behaviour +- Prevented recording empty string as `city`, `ISOCountryCode` and `IP` for location info +- Applied `alwaysUsePOST` flag to feedback widget check requests +- Applied `alwaysUsePOST` flag to remote config requests + +- Other various improvements + - Deleted some unnecessary imports + - Updated HeaderDocs, internal logs, inline notes and pragma marks + - Added missing frameworks to CocoaPods podspec + - Added ability to override SDK name and version for bridge SDKs + + + +## 20.04 +- Added crash reporting feature for tvOS +- Added crash reporting feature for macOS +- Added crash reporting feature for watchOS +- Added optional crash reporting dependency PLCrashReporter for iOS +- Added UI orientation tracking +- Added crash filtering with regex +- Updated dSYM uploader script for accepting custom dSYM paths +- Updated enableAppleWatch flag default value for independent watchOS apps +- Fixed push notification consent method for macOS targets +- Fixed not appearing rich push notification buttons for some cases +- Discarded OpenGL ES version info in crash reports + +- Other various improvements + - Deleted an unnecessary UIKit import + - Added precaution for possible nil lines in backtrace + - Added precaution for possible nil OS name value + - Replaced scheduledTimerWithTimeInterval call with timerWithTimeInterval (thanks @mt-rpranata) + - Updated architerture method for crash reports + - Updated CocoaPods podspec for core subspec approach + - Updated feature, consent and push test mode specifiers as NSString typedefs + - Updated HeaderDocs, internal logs, inline notes and pragma marks + + + +## 19.08 +- Added temporary device ID mode +- Added support for Carthage +- Added custom URL session configuration support +- Added custom segmentation support on view tracking +- Added ability to change app key on the run +- Added ability to flush queues +- Added `pushTestMode` property and discarded `isTestDevice` property +- Fixed `WCSessionDelegate` interception +- Fixed title and message check in push notification payloads +- Fixed binary image name extraction for crash reports +- Fixed missing delegate forwarding for `userNotificationCenter:openSettingsForNotification:` method +- Fixed in-app alerts on iOS10+ devices when a silent notification with alert key arrives +- Discarded device ID persistency on Keychain +- Discarded OpenUDID device ID option +- Discarded IDFA device ID option +- Discarded zero IDFA fix +- Updated default device ID on tvOS as `identifierForVendor` + +- Other various improvements + - Renamed `forceDeviceIDInitialization` flag as `resetStoredDeviceID` + - Added lightweight generics for segmentation parameters + - Added dSYM upload script to preserved paths in Podspec + - Updated dSYM upload script to support paths with spaces + - Changed request cache policy to `NSURLRequestReloadIgnoringLocalCacheData` + - Added battery level for watchOS 4.0+ + - Added JSON validity check before converting objects + - Deleted unused `kCountlyCRKeyLoadAddress` constant + - Improved internal logging in binary images processing for crash reports + - Added persistency for generated `NSUUID` + - Added precaution to prevent invalid requests from being added to queue + - Discarded null check on request queue + - Discarded all APM related files + - Added length check for view tracking view name + - Added length check for view tracking exceptions + - Updated HeaderDocs, internal logs, inline notes and pragma marks + + + +## 19.02 +- Added push notification support for macOS +- Added provisional push notification permission support for iOS12 +- Added remote config feature +- Added `recordPushNotificationToken` method to be used after device ID changes +- Added `clearPushNotificationToken` method to be used after device ID changes +- Discarded `Push Open` event and replaced it with `Push Action` event +- Fixed push notification token not being sent on some iOS12 devices +- Fixed device ID change request delaying issue by discarding delay altogether +- Fixed internal view controller presenting failure when root view controller is not ready yet +- Fixed `openURL` freeze caused by iOS +- Fixed wrong `kCountlyQSKeyLocationIP` key in location info requests +- Fixed missing app key in feedback widget requests +- Fixed feedback widget dismiss button position + +- Other various improvements + - Discarded separate UIWindow usage for presenting feedback widgets + - Added checksum to feedback widget requests + - Improved internal logging for request queue + + + +## 18.08 +- Added feedback widgets support +- Added limit for number of custom crash logs (100 logs) +- Added limit for each custom crash log length (1000 chars) +- Added support for cancelling timed events +- Added support for recording fatal exceptions manually +- Added `userInfo` to crash report custom property +- Added delay before sending change device ID request (server requirement) +- Renamed `isAutoViewTrackingEnabled` as `isAutoViewTrackingActive` +- Fixed Xcode warnings for `askForNotificationPermission` method +- Fixed `UIAlertController` leak in push notification manager +- Fixed `crashSegmentation` availability for manually recorded crashes +- Fixed `openURL:` call thread as main thread +- Updated minimum supported `macOS` version as `10.10` + +- Other various improvements + - Discarded separate `UIWindow` for presenting `UIAlertControllers` + - Refactored `buildUUID` and `executableName` as properties + - Refactored custom crash log array and date formatter + - Updated HeaderDocs, inline notes, pragma marks + + + +## 18.04 +- Added consent management for GDPR compliance +- Exposed device ID to be used for data export and/or removal requests +- Added precautions for SDK start state to prevent re-starting and early method calls +- Added mutability protection for core functions, configuration properties, events and user details +- Added `COUNTLY_EXCLUDE_IDFA` pre-processor flag to exclude IDFA references +- Added API availability checks and warnings for Apple Watch and Push Notifications +- Renamed `reportView:` method as `recordView:` +- Fixed early ending of `UIBackgroundTask` +- Fixed getting file path form local storage URL (thanks @dsmo) +- Fixed not respecting `doNotShowAlertForNotifications` flag on iOS10+ devices +- Fixed not starting requests queue when `manualSessionHandling` is enabled +- Fixed `block implicitly retains self` warning in Star Rating +- Fixed local variable shadowing warnings +- Fixed Japanese language code for Star Rating dialog + +- Other various improvements + - Refactored all location info into Location Manager + - Refactored `checkForAutoAsk` in Star Rating + - Refactored event recording for consents compatibility + - Refactored Apple Watch matching + - Refactored auto view tracking + - Added top view controller finding method + - Replaced asserts with exceptions + - Deleted unneccessary method declarations in Push Notifications + - Deleted unnecessary reference for `WCSession.defaultSession.delegate` + - Deleted unnecessary `TARGET_OS_OSX` definition + - Standardized `nil` checks + - Renamed and reordered some query string constants + - Updated HeaderDocs, inline notes, pragma marks + - Performed whitespace cleaning + + + +## 18.01 + +- Added `attribution` config +- Added recording city and country for GeoLocation +- Added recording explicit IP address for GeoLocation +- Added disabling GeoLocation +- Updated `recordLocation` method to override inital `location` config +- Fixed reserved key for IP address query string +- Fixed a `CoreTelephony` related crash due to an iOS bug +- Replaced `NSUserDefaults` with `NSCachesDirectory` on tvOS for persistency +- Improved auto dSYM uploader script +- Improved performance on stored request limit execution + +- Other various improvements + - Fixed a placeholder type specifier for `NSNumber` + - Deleted unnecessary `CLYMessaging` definition + - Deleted unnecessary strong ownership qualifiers + - Added Hindu translation for star rating dialog + - Added change log file + - Updated user details and star rating reserved keys as constants + - Updated `OpenGLESVersion` method return type as `NSString` + - Updated time related types as `NSTimeInterval` + - Updated HeaderDocs + + + +## 17.09 + +- Updated for Xcode 9 and iOS 11 +- Added symbolication support for crash reports +- Added Automatic dSYM Uploading script +- Added extension subspec for integrating Rich Push Notifications with CocoaPods +- Added nullability specifiers for better Swift compatibility +- Added 28 new system UIViewController subclass exception for Auto View Tracking +- Added convenience method for recording action event for manually handled notifications +- Added convenience method for recording handled exception with stack trace +- Added precaution for invalid event keys +- Added precaution for corrupt request strings +- Made Zero-IDFA fix optional +- Fixed a view tracking duration problem where duration being reported as timestamp +- Replaced `crashLog` method with `recordCrashLog` and added deprecated warning +- Changed dispatch queue type for opening external URLs + +- Other various improvements + - Added Bengali translation for star rating dialog + - Updated metric, event, view tracking and crash report reserved keys as constants + - Deleted unnecessary gitattributes file + - Deleted duplicate Zero-IDFA const + - Rearranged file imports + - Updated HeaderDocs + - Cleaned whitespace + + + +## 17.05 + +- Added Rich Push Notifications support (attachments and custom action buttons) +- Added manual session handling +- Added URL escaping for custom device ID and other user defined properties +- Added support for accidental extra slash in host +- Added architecture, executable name and load address for crash reporting +- Added IP optional parameter +- Added SDK metadata to all request +- Switched to SHA256 for parameter tampering protection +- Discarded `recordUserDetails` method and combined it with `save` method +- Improved `AutoViewTracking` active duration calculation +- Improved Countly payload check in notification dictionary +- Fixed inner event timestamp for 32 bit devices +- Fixed token cleaning when user's permission changes +- Fixed checksum calculation for `zero-IDFA` fix case +- Fixed OS version metric for `tvOS` +- Fixed double `suspend` method call when user kills the app using App Switcher +- Fixed a compiler warning for `macOS` targets +- Fixed `AutoViewTracking` for `macOS` targets +- Fixed showing of multiple alerts in succession + +- Other various improvements + - Refactored picture upload data preparation from request string using `NSURLComponents` + - Refactored `zero-IDFA` check + - Refactored additional info to be sent with begin session request + - Refactored checksum appending + - Refactored URLSession generation + - Refactored opening external URLs on main thread + - Refactored device model identifier method + - Refactored sending crash report into connection manager + - Replaced `__OPTIMIZED__` flag with `DEBUG` flag for push notification test mode detection + - Replaced boundary method with constant string + - Replaced text based dismiss button with cross dismiss button for star-rating + - Redefined request query string keys as constants + - Redefined push notification reserved keys as constants + - Redefined GET request max length as a constant + - Redefined server input endpoint as a constant + - Redefined push notification test mode values as enum + - Standardized some integer types + - Standardized target checking preprocessor macro usage + - Deleted unnecessary `init` override in push manager + - Deleted unnecessary `updateSessionPeriod` property in connection manager + - Deleted unnecessary `starRatingDismissButtonTitle` config property + - Deleted internal crash test methods + - Added Czech and Latvian localization for star-rating dialog + - Changed example host URL for rebranding compatibility + - Updated handling of notification on `iOS9` and older + - Updated alert key handling in push notification payload + - Updated HeaderDocs + - Cleaned whitespace + + + +## 16.12 + +- Refactored push notifications + - Made integration more easy + - Added iOS10 push notifications handling + - Added convenience method for asking push notifications permission covers all iOS versions + - Renamed feature name from `CLYMessaging` to `CLYPushNotifications` + - Added configuration option `doNotShowAlertForNotifications` to disable push triggered alerts + - Discarded complicated `UIUserNotificationCategory` actions + - Added configuration option `sendPushTokenAlways` to record push token always (for sending silent notification to users without notification permission) + - Discarded App Store URL fetching with `NSURLConnection` +- Discarded iOS7 support and deprecated method calls +- Switched to runtime controlled internal logging instead of preprocessor flag +- Added AutoViewTracking support for tvOS +- Added view controller title and custom titleView support for AutoViewTracking +- Improved AutoViewTracking performance and Swift compatibility +- Refactored suspending for crash reporting +- Switched to async file save for suspending +- Added user login and logout convenience methods +- Added configuration option to enable Apple Watch related features +- Moved archiving of queued request into sync block to prevent a very rare occurring crash +- Refactored unsent session duration +- Added completion callback for automatically displayed star-rating dialog +- Partially fixed but temporarily disabled APM feature until server completely supports it +- Fixed too long exception name in crash reports on iOS10 +- Other various improvements + - Refactored starting method + - Switched to separate window based alert controller displaying for push notifications and star-rating dialogs + - Renamed constant kCountlyStarRatingButtonSize to prevent compile time conflicts + - Renamed server input endpoint variable for white label SDK renamer script compatibility + - Updated star-rating reserved event key + - Added internal log for successful initialization with SDK name and version + - Fixed unused `UIAlertViewAssociatedObjectKey` warning for macOS + - Removed old deviceID zero-IDFA fixer redundant request + - Added internal logging for connection type retrieval exception + - Added exception type info to crash reports + - Fixed duplicate exception adding for AutoViewTracking + - Prevented Countly internal view controllers from being tracked by AutoViewTracking + - Prefixed all category methods to prevent possible conflicts + - Changed timer's runloop mode + - Updated timestamp type specifier (thanks to @scottlaw) + - Changed SDK metadata sending to begin_session only + - Replaced empty string checks with length checks + - Cleared nullability specifiers + - Updated HeaderDocs + - Cleaned whitespace + + + +## 16.06.4 + +- Fixed iOS10 zero-IDFA problem +- Fixed TARGET_OS_OSX warning for iOS10 SDK on Xcode 8. +- Fixed ending of background tasks. +- Added parameter tampering protection. +- Added density metric. +- Added alwaysUsePOST config property for using POST method for all requests regardless of the data size. +- Added timezone. +- Switched to millisecond timestamp. +- Disabled server response dictionary check. +- Other minor improvements like better internal logging, standardization, whitespacing, code cleaning, commenting, pragma marking and HeaderDocing + + + +## 16.06.3 + +- Fixed a persistency related crash +- Improved thread safety of request queue and events +- Added Star-Rating, the simplest form of feedback from users, both automatically and manually. +- Improved event recording performance and safety for APM and Auto View Tracking. +- Added custom HTTP header field support for requests, both on initial configuration and later. +- Standardized internal logging grammar and formatting for easier debugging +- Improved headerdocs grammar and formatting for easier integration and usage +- Fixed some static analyzer warnings + + + +## 16.06.2 + +- Added Star-Rating, the simplest form of feedback from users, both automatically and manually. +- Improved event recording performance and safety for APM and Auto View Tracking. +- Added custom HTTP header field support for requests, both on initial configuration and later. +- Standardized internal logging grammar and formatting for easier debugging +- Improved headerdocs grammar and formatting for easier integration and usage +- Fixed some static analyzer warnings + + + +## 16.06.1 + +- Added support for certificate pinning. +- Added deleting of user details properties on server by setting them as NSNull. +- Implemented switching between GET and POST depending on data size on requests. +- Fixed a URL encoding issue which causes problems for Asian languages and some JSON payloads. +- Fixed custom crash log formatter. + + + +## 16.06 + +- Fixed a problem with changing device ID (for system generated device IDs) +- Added isTestDevice flag to mark test devices for Push Notifications +- Improved Auto View Tracking by ignoring non-visible foundation UIViewController subclasses +- Implemented manually adding exception UIViewController subclasses for Auto View Tracking +- Changed default device ID type for tvOS from IDFA to NSUUID +- Added stored requests limit +- Added optional parameters ISOCountryCode, city and location for advanced segmentation +- Discarded timed events persistency +- Added buildUUID and build number to Crash Reports +- Added SDK name (language-origin-platform) to all requests +- Changed default alert title for push messages +- Other minor improvements like better internal logging, standardization, whitespacing, code cleaning, commenting, pragma marking and HeaderDocing + + + +# 16.02.01 + +- Swithed to POST method for all requests by default +- Fixed some issues with Crash Reporting persistency +- Fixed some issues with CocoaPods v1.0.0 +- Other minor fixes and improvements + + + +## 16.02 + +Completely re-written iOS SDK with watchOS, tvOS & OSX support +- APM +- Manual/Auto ViewTracking +- UserDetails modifiers +- watchOS 2 support +- tvOS support +- Configurable starting +- Custom or system provided (IDFA, IDFV, OpenUDID) device ID +- Changing/merging device ID on runtime +- Persistency without CoreData +- Various performance improvements and minor bugfixes + + + +## 15.06.01 + +Updated CocoaPods spec + + + +## 15.06 + +- Added WatchKit support +- Added CrashReporting support +- Fixed minor problems with Messaging +- Added manually ending sessions on background fetch +- Switched to Ubuntu version numbering diff --git a/src/ios/CountlyiOS/Countly-PL.podspec b/src/ios/CountlyiOS/Countly-PL.podspec new file mode 100644 index 0000000..8f8c68b --- /dev/null +++ b/src/ios/CountlyiOS/Countly-PL.podspec @@ -0,0 +1,43 @@ +Pod::Spec.new do |s| + s.name = 'Countly-PL' + s.version = '21.11.2' + s.license = { :type => 'MIT', :file => 'LICENSE.md' } + s.summary = 'Countly is an innovative, real-time, open source mobile analytics platform.' + s.homepage = 'https://github.com/Countly/countly-sdk-ios' + s.social_media_url = 'https://twitter.com/gocountly' + s.author = {'Countly' => 'hello@count.ly'} + s.source = { :git => 'https://github.com/Countly/countly-sdk-ios.git', :tag => s.version.to_s } + + s.requires_arc = true + s.default_subspecs = 'Core' + s.ios.deployment_target = '10.0' + s.osx.deployment_target = '10.14' + s.watchos.deployment_target = '4.0' + s.tvos.deployment_target = '10.0' + + s.subspec 'Core' do |core| + core.source_files = '*.{h,m}' + core.public_header_files = 'Countly.h', 'CountlyUserDetails.h', 'CountlyConfig.h', 'CountlyFeedbackWidget.h' + core.preserve_path = 'countly_dsym_uploader.sh' + core.ios.frameworks = ['Foundation', 'UIKit', 'UserNotifications', 'CoreLocation', 'WebKit', 'CoreTelephony', 'WatchConnectivity'] + end + + s.subspec 'NotificationService' do |ns| + ns.source_files = 'CountlyNotificationService.{m,h}' + ns.ios.deployment_target = '10.0' + ns.ios.frameworks = ['Foundation', 'UserNotifications'] + end + + s.subspec 'PL' do |pl| + pl.platform = :ios + pl.dependency 'Countly/Core' + pl.dependency 'PLCrashReporter', '~> 1' + + # It is not possible to set static_framework attribute on subspecs. + # So, we have to set it on main spec. + # But it affects the main spec even when this subspec is not used. + # Asked this on CocoaPods GitHub page: https://github.com/CocoaPods/CocoaPods/issues/7355#issuecomment-619261908 + s.static_framework = true + end + +end diff --git a/src/ios/CountlyiOS/Countly.h b/src/ios/CountlyiOS/Countly.h index 08541c8..ed8f5e5 100644 --- a/src/ios/CountlyiOS/Countly.h +++ b/src/ios/CountlyiOS/Countly.h @@ -32,22 +32,29 @@ NS_ASSUME_NONNULL_BEGIN - (void)startWithConfig:(CountlyConfig *)config; /** - * Sets new app key to be used in following requests. - * @discussion Before switching to new app key, this method suspends Countly and resumes immediately after. - * @discussion Requests already queued previously will keep using the old app key. - * @discussion New app key needs to be a non-zero length string, otherwise it is ignored. - * @discussion @c recordPushNotificationToken and @c updateRemoteConfigWithCompletionHandler: methods may need to be called again after app key change. - * @param newAppKey New app key + * Sets a new host to be used in requests. + * @discussion Requests already queued previously will also be using the new host. + * @discussion The new host needs to be a non-zero length string, otherwise it is ignored. + * @discussion @c recordPushNotificationToken and @c updateRemoteConfigWithCompletionHandler: methods may need to be called after the host change. + * @param newHost The new host + */ +- (void)setNewHost:(NSString *)newHost; + +/** + * Sets a new app key to be used in new requests. + * @discussion Before switching to the new app key, this method suspends Countly and resumes it immediately after. + * @discussion The requests already queued prior to this method call will keep using the old app key. + * @discussion The new app key needs to be a non-zero length string, otherwise it is ignored. + * @discussion @c recordPushNotificationToken and @c updateRemoteConfigWithCompletionHandler: methods may need to be called again after the app key change. + * @param newAppKey The new app key */ - (void)setNewAppKey:(NSString *)newAppKey; /** - * Sets the value of the custom HTTP header field to be sent with every request if @c customHeaderFieldName is set on initial configuration. - * @discussion If @c customHeaderFieldValue on initial configuration can not be set on app launch, this method can be used to do so later. - * @discussion Requests not started due to missing @c customHeaderFieldValue since app launch will start hereafter. - * @param customHeaderFieldValue Custom header field value + * @c setCustomHeaderFieldValue: method is deprecated. Please use @c URLSessionConfiguration property on @c CountlyConfig instead. + * @discussion Calling this method will have no effect. */ -- (void)setCustomHeaderFieldValue:(NSString *)customHeaderFieldValue; +- (void)setCustomHeaderFieldValue:(NSString *)customHeaderFieldValue DEPRECATED_MSG_ATTRIBUTE("Use 'URLSessionConfiguration' property on CountlyConfig instead!"); /** * Flushes request and event queues. @@ -408,12 +415,6 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)disableLocationInfo; -/** - * @c isGeoLocationEnabled property is deprecated. Please use @c disableLocationInfo method instead. - * @discussion Using this property will have no effect. - */ -@property (nonatomic) BOOL isGeoLocationEnabled DEPRECATED_MSG_ATTRIBUTE("Use 'disableLocationInfo' method instead!"); - #pragma mark - Crash Reporting @@ -453,14 +454,6 @@ NS_ASSUME_NONNULL_BEGIN - (void)clearCrashLogs; -/** - * @c crashLog: method is deprecated. Please use @c recordCrashLog: method instead. - * @discussion Be advised, parameter type changed to plain @c NSString from string format, for better Swift compatibility. - * @discussion Calling this method will have no effect. - */ -- (void)crashLog:(NSString *)format, ... NS_FORMAT_FUNCTION(1,2) DEPRECATED_MSG_ATTRIBUTE("Use 'recordCrashLog:' method instead!"); - - #pragma mark - View Tracking @@ -507,12 +500,6 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic) BOOL isAutoViewTrackingActive; -/** - * @c isAutoViewTrackingEnabled property is deprecated. Please use @c isAutoViewTrackingActive property instead. - * @discussion Using this property will have no effect. - */ -@property (nonatomic) BOOL isAutoViewTrackingEnabled DEPRECATED_MSG_ATTRIBUTE("Use 'isAutoViewTrackingActive' property instead!"); - #endif @@ -531,14 +518,14 @@ NS_ASSUME_NONNULL_BEGIN * @discussion This is just a convenience method that handles setting user ID as new device ID and merging existing data on Countly Server. * @param userID Custom user ID uniquely defining the logged in user */ -- (void)userLoggedIn:(NSString *)userID; +- (void)userLoggedIn:(NSString *)userID DEPRECATED_MSG_ATTRIBUTE("Use 'setNewDeviceID:onServer:' method instead!"); /** * Handles switching from custom user ID to device ID for logged out users * @discussion When a user logs out, all the data can be tracked with default device ID henceforth. * @discussion This is just a convenience method that handles resetting device ID to default one and starting a new session. */ -- (void)userLoggedOut; +- (void)userLoggedOut DEPRECATED_MSG_ATTRIBUTE("Use 'setNewDeviceID:onServer:' method instead!"); @@ -567,7 +554,37 @@ NS_ASSUME_NONNULL_BEGIN * @param widgetID ID of the feedback widget created on Countly Server. * @param completionHandler A completion handler block to be executed when feedback widget is dismissed by user or there is an error. */ -- (void)presentFeedbackWidgetWithID:(NSString *)widgetID completionHandler:(void (^)(NSError * __nullable error))completionHandler; +- (void)presentFeedbackWidgetWithID:(NSString *)widgetID completionHandler:(void (^)(NSError * __nullable error))completionHandler DEPRECATED_MSG_ATTRIBUTE("Use 'presentRatingWidgetWithID:' method instead!"); + +/** + * Presents rating widget with given ID in a WKWebView placed in a UIViewController. + * @discussion First, the availability of the rating widget will be checked asynchronously. + * @discussion If the rating widget with given ID is available, it will be modally presented. + * @discussion Otherwise, @c completionHandler will be executed with an @c NSError. + * @discussion @c completionHandler will also be executed with @c nil when the rating widget is dismissed by user. + * @discussion Calls to this method will be ignored and @c completionHandler will not be executed if: + * @discussion - Consent for @c CLYConsentFeedback is not given, while @c requiresConsent flag is set on initial configuration. + * @discussion - Current device ID is @c CLYTemporaryDeviceID. + * @discussion - @c widgetID is not a non-zero length valid string. + * @discussion This is a legacy method for presenting Rating type feedback widgets only. + * @discussion Passing widget ID's of Survey or NPS type feedback widgets will not work. + * @param widgetID ID of the rating widget created on Countly Server. + * @param completionHandler A completion handler block to be executed when the rating widget is dismissed by user or there is an error. + */ +- (void)presentRatingWidgetWithID:(NSString *)widgetID completionHandler:(void (^)(NSError * __nullable error))completionHandler; + +/** + * Manually records rating widget result with given ID and other info. + * @discussion Calls to this method will be ignored if: + * @discussion - Consent for @c CLYConsentFeedback is not given, while @c requiresConsent flag is set on initial configuration. + * @discussion - @c widgetID is not a non-zero length valid string. + * @param widgetID ID of the rating widget created on Countly Server + * @param rating User's rating + * @param email User's e-mail address (optional) + * @param comment User's comment (optional) + * @param userCanBeContacted User's consent for whether they can be contacted via e-mail or not + */ +- (void)recordRatingWidgetWithID:(NSString *)widgetID rating:(NSInteger)rating email:(NSString * _Nullable)email comment:(NSString * _Nullable)comment userCanBeContacted:(BOOL)userCanBeContacted; /** * Fetches a list of available feedback widgets. @@ -590,11 +607,33 @@ NS_ASSUME_NONNULL_BEGIN * Records attribution ID (IDFA) for campaign attribution. * @discussion This method overrides @c attributionID property specified on initial configuration, and sends an immediate request. * @discussion Also, this attribution ID will be sent with all @c begin_session requests. + * @discussion Calls to this method will be ignored if: + * @discussion - Consent for @c CLYConsentAttribution is not given, while @c requiresConsent flag is set on initial configuration. * @param attributionID Attribution ID (IDFA) */ - (void)recordAttributionID:(NSString *)attributionID; +/** + * Records direct attribution with campaign type and data. + * @discussion Currently supported campaign types are "countly" and "_special_test". + * @discussion Campaign data has to be in `{"cid":"CAMPAIGN_ID", "cuid":"CAMPAIGN_USER_ID"}` format. + * @discussion This method sends an immediate request. + * @discussion Calls to this method will be ignored if: + * @discussion - Consent for @c CLYConsentAttribution is not given, while @c requiresConsent flag is set on initial configuration. + * @param campaignType Campaign Type + * @param campaignData Campaign Data + */ +- (void)recordDirectAttributionWithCampaignType:(NSString *)campaignType andCampaignData:(NSString *)campaignData; +/** + * Records indirect attribution with given key-value pairs. + * @discussion Keys could be a predefined CLYAttributionKey or any non-zero length valid string. + * @discussion This method sends an immediate request. + * @discussion Calls to this method will be ignored if: + * @discussion - Consent for @c CLYConsentAttribution is not given, while @c requiresConsent flag is set on initial configuration. + * @param attribution Attribution key-value pairs + */ +- (void)recordIndirectAttribution:(NSDictionary *)attribution; #pragma mark - Remote Config /** diff --git a/src/ios/CountlyiOS/Countly.m b/src/ios/CountlyiOS/Countly.m index eaa6a75..a0c19c5 100644 --- a/src/ios/CountlyiOS/Countly.m +++ b/src/ios/CountlyiOS/Countly.m @@ -71,8 +71,12 @@ - (void)startWithConfig:(CountlyConfig *)config CountlyCommon.sharedInstance.hasStarted = YES; CountlyCommon.sharedInstance.enableDebug = config.enableDebug; + CountlyCommon.sharedInstance.shouldIgnoreTrustCheck = config.shouldIgnoreTrustCheck; CountlyCommon.sharedInstance.loggerDelegate = config.loggerDelegate; CountlyCommon.sharedInstance.internalLogLevel = config.internalLogLevel; + CountlyCommon.sharedInstance.maxKeyLength = config.maxKeyLength; + CountlyCommon.sharedInstance.maxValueLength = config.maxValueLength; + CountlyCommon.sharedInstance.maxSegmentationValues = config.maxSegmentationValues; CountlyConsentManager.sharedInstance.requiresConsent = config.requiresConsent; @@ -92,11 +96,9 @@ - (void)startWithConfig:(CountlyConfig *)config } CountlyConnectionManager.sharedInstance.appKey = config.appKey; - CountlyConnectionManager.sharedInstance.host = [config.host hasSuffix:@"/"] ? [config.host substringToIndex:config.host.length - 1] : config.host; + CountlyConnectionManager.sharedInstance.host = config.host; CountlyConnectionManager.sharedInstance.alwaysUsePOST = config.alwaysUsePOST; CountlyConnectionManager.sharedInstance.pinnedCertificates = config.pinnedCertificates; - CountlyConnectionManager.sharedInstance.customHeaderFieldName = config.customHeaderFieldName; - CountlyConnectionManager.sharedInstance.customHeaderFieldValue = config.customHeaderFieldValue; CountlyConnectionManager.sharedInstance.secretSalt = config.secretSalt; CountlyConnectionManager.sharedInstance.URLSessionConfiguration = config.URLSessionConfiguration; @@ -105,11 +107,9 @@ - (void)startWithConfig:(CountlyConfig *)config CountlyCommon.sharedInstance.manualSessionHandling = config.manualSessionHandling; - CountlyCommon.sharedInstance.enableAppleWatch = config.enableAppleWatch; - CountlyCommon.sharedInstance.attributionID = config.attributionID; - CountlyDeviceInfo.sharedInstance.customMetrics = config.customMetrics; + CountlyDeviceInfo.sharedInstance.customMetrics = [config.customMetrics cly_truncated:@"Custom metric"]; #if (TARGET_OS_IOS) CountlyFeedbacks.sharedInstance.message = config.starRatingMessage; @@ -145,7 +145,7 @@ - (void)startWithConfig:(CountlyConfig *)config #endif #endif - CountlyCrashReporter.sharedInstance.crashSegmentation = config.crashSegmentation; + CountlyCrashReporter.sharedInstance.crashSegmentation = [config.crashSegmentation cly_truncated:@"Crash segmentation"]; CountlyCrashReporter.sharedInstance.crashLogLimit = MAX(1, config.crashLogLimit); CountlyCrashReporter.sharedInstance.crashFilter = config.crashFilter; CountlyCrashReporter.sharedInstance.shouldUsePLCrashReporter = config.shouldUsePLCrashReporter; @@ -169,8 +169,6 @@ - (void)startWithConfig:(CountlyConfig *)config timer = [NSTimer timerWithTimeInterval:config.updateSessionPeriod target:self selector:@selector(onTimer:) userInfo:nil repeats:YES]; [NSRunLoop.mainRunLoop addTimer:timer forMode:NSRunLoopCommonModes]; - [CountlyCommon.sharedInstance startAppleWatchMatching]; - CountlyRemoteConfig.sharedInstance.isEnabledOnInitialConfig = config.enableRemoteConfig; CountlyRemoteConfig.sharedInstance.remoteConfigCompletionHandler = config.remoteConfigCompletionHandler; [CountlyRemoteConfig.sharedInstance startRemoteConfig]; @@ -178,12 +176,33 @@ - (void)startWithConfig:(CountlyConfig *)config CountlyPerformanceMonitoring.sharedInstance.isEnabledOnInitialConfig = config.enablePerformanceMonitoring; [CountlyPerformanceMonitoring.sharedInstance startPerformanceMonitoring]; + CountlyCommon.sharedInstance.enableOrientationTracking = config.enableOrientationTracking; [CountlyCommon.sharedInstance observeDeviceOrientationChanges]; [CountlyConnectionManager.sharedInstance proceedOnQueue]; if (config.consents) [self giveConsentForFeatures:config.consents]; + + if (config.campaignType && config.campaignData) + [self recordDirectAttributionWithCampaignType:config.campaignType andCampaignData:config.campaignData]; + + if (config.indirectAttribution) + [self recordIndirectAttribution:config.indirectAttribution]; +} + + +- (void)setNewHost:(NSString *)newHost +{ + CLY_LOG_I(@"%s %@", __FUNCTION__, newHost); + + if (!newHost.length) + { + CLY_LOG_W(@"New host is invalid!"); + return; + } + + CountlyConnectionManager.sharedInstance.host = newHost; } - (void)setNewAppKey:(NSString *)newAppKey @@ -191,7 +210,10 @@ - (void)setNewAppKey:(NSString *)newAppKey CLY_LOG_I(@"%s %@", __FUNCTION__, newAppKey); if (!newAppKey.length) + { + CLY_LOG_W(@"New app key is invalid!"); return; + } [self suspend]; @@ -204,10 +226,7 @@ - (void)setNewAppKey:(NSString *)newAppKey - (void)setCustomHeaderFieldValue:(NSString *)customHeaderFieldValue { - CLY_LOG_I(@"%s %@", __FUNCTION__, customHeaderFieldValue); - - CountlyConnectionManager.sharedInstance.customHeaderFieldValue = customHeaderFieldValue.copy; - [CountlyConnectionManager.sharedInstance proceedOnQueue]; + CLY_LOG_W(@"setCustomHeaderFieldValue: method is deprecated. Please use `URLSessionConfiguration` property on `CountlyConfig` instead."); } - (void)flushQueues @@ -400,9 +419,11 @@ - (void)setNewDeviceID:(NSString *)deviceID onServer:(BOOL)onServer if (!CountlyCommon.sharedInstance.hasStarted) return; - if (!CountlyConsentManager.sharedInstance.hasAnyConsent) - return; - + if (!deviceID.length) + { + CLY_LOG_W(@"Passing `CLYDefaultDeviceID` or `nil` or empty string as devie ID is deprecated, and will not be allowed in the future."); + } + [self storeCustomDeviceIDState:deviceID]; deviceID = [CountlyDeviceInfo.sharedInstance ensafeDeviceID:deviceID]; @@ -448,6 +469,8 @@ - (void)setNewDeviceID:(NSString *)deviceID onServer:(BOOL)onServer [CountlyDeviceInfo.sharedInstance initializeDeviceID:deviceID]; + [CountlyConsentManager.sharedInstance cancelConsentForAllFeaturesWithoutSendingConsentsRequest]; + [self resume]; [CountlyPersistency.sharedInstance clearAllTimedEvents]; @@ -559,8 +582,35 @@ - (void)recordEvent:(NSString *)key segmentation:(NSDictionary *)segmentation co { CLY_LOG_I(@"%s %@ %@ %lu %f %f", __FUNCTION__, key, segmentation, (unsigned long)count, sum, duration); - if (!CountlyConsentManager.sharedInstance.consentForEvents) + NSDictionary * reservedEvents = + @{ + kCountlyReservedEventOrientation: @(CountlyConsentManager.sharedInstance.consentForUserDetails), + kCountlyReservedEventStarRating: @(CountlyConsentManager.sharedInstance.consentForFeedback), + kCountlyReservedEventSurvey: @(CountlyConsentManager.sharedInstance.consentForFeedback), + kCountlyReservedEventNPS: @(CountlyConsentManager.sharedInstance.consentForFeedback), + kCountlyReservedEventPushAction: @(CountlyConsentManager.sharedInstance.consentForPushNotifications), + kCountlyReservedEventView: @(CountlyConsentManager.sharedInstance.consentForViewTracking), + }; + + NSNumber* aReservedEvent = reservedEvents[key]; + + if (aReservedEvent) + { + CLY_LOG_V(@"A reserved event detected: %@", key); + + if (!aReservedEvent.boolValue) + { + CLY_LOG_W(@"Specific consent not given for the reserved event! So, it will not be recorded."); + return; + } + + CLY_LOG_V(@"Specific consent given for the reserved event! So, it will be recorded."); + } + else if (!CountlyConsentManager.sharedInstance.consentForEvents) + { + CLY_LOG_W(@"Events consent not given! Event will not be recorded."); return; + } [self recordEvent:key segmentation:segmentation count:count sum:sum duration:duration timestamp:CountlyCommon.sharedInstance.uniqueTimestamp]; } @@ -851,6 +901,8 @@ - (void)userLoggedIn:(NSString *)userID { CLY_LOG_I(@"%s %@", __FUNCTION__, userID); + CLY_LOG_W(@"userLoggedIn: method is deprecated. Please directly use setNewDeviceID:onServer: method instead."); + [self setNewDeviceID:userID onServer:YES]; } @@ -858,6 +910,8 @@ - (void)userLoggedOut { CLY_LOG_I(@"%s", __FUNCTION__); + CLY_LOG_W(@"userLoggedOut method is deprecated. Please directly use setNewDeviceID:onServer: method instead."); + [self setNewDeviceID:CLYDefaultDeviceID onServer:NO]; } @@ -880,6 +934,20 @@ - (void)presentFeedbackWidgetWithID:(NSString *)widgetID completionHandler:(void [CountlyFeedbacks.sharedInstance checkFeedbackWidgetWithID:widgetID completionHandler:completionHandler]; } +- (void)presentRatingWidgetWithID:(NSString *)widgetID completionHandler:(void (^)(NSError * error))completionHandler +{ + CLY_LOG_I(@"%s %@ %@", __FUNCTION__, widgetID, completionHandler); + + [CountlyFeedbacks.sharedInstance checkFeedbackWidgetWithID:widgetID completionHandler:completionHandler]; +} + +- (void)recordRatingWidgetWithID:(NSString *)widgetID rating:(NSInteger)rating email:(NSString * _Nullable)email comment:(NSString * _Nullable)comment userCanBeContacted:(BOOL)userCanBeContacted +{ + CLY_LOG_I(@"%s %@ %ld %@ %@ %d", __FUNCTION__, widgetID, (long)rating, email, comment, userCanBeContacted); + + [CountlyFeedbacks.sharedInstance recordRatingWidgetWithID:widgetID rating:rating email:email comment:comment userCanBeContacted:userCanBeContacted]; +} + - (void)getFeedbackWidgets:(void (^)(NSArray *feedbackWidgets, NSError * error))completionHandler { CLY_LOG_I(@"%s %@", __FUNCTION__, completionHandler); @@ -905,7 +973,80 @@ - (void)recordAttributionID:(NSString *)attributionID [CountlyConnectionManager.sharedInstance sendAttribution]; } +- (void)recordDirectAttributionWithCampaignType:(NSString *)campaignType andCampaignData:(NSString *)campaignData +{ + CLY_LOG_I(@"%s %@ %@", __FUNCTION__, campaignType, campaignData); + + if (!CountlyConsentManager.sharedInstance.consentForAttribution) + return; + + if (!campaignType.length) + { + CLY_LOG_E(@"campaignType must be non-zero length valid string. Method execution will be aborted!"); + return; + } + + if (!campaignData.length) + { + CLY_LOG_E(@"campaignData must be non-zero length valid string. Method execution will be aborted!"); + return; + } + + if ([campaignType isEqualToString:@"_special_test"]) + { + [CountlyConnectionManager.sharedInstance sendAttributionData:campaignData]; + return; + } + if (![campaignType isEqualToString:@"countly"]) + { + CLY_LOG_W(@"Recording direct attribution with a type other than 'countly' is currently not supported. Method execution will be aborted!"); + return; + } + + NSError* error = nil; + NSDictionary* campaignDataDictionary = [NSJSONSerialization JSONObjectWithData:[campaignData cly_dataUTF8] options:0 error:&error]; + if (error) + { + CLY_LOG_E(@"Campaign data is not in expected format. Method execution will be aborted!"); + return; + } + + NSString* campaignID = campaignDataDictionary[@"cid"]; + if (!campaignID.length) + { + CLY_LOG_E(@"Campaign ID must be non-zero length valid string. Method execution will be aborted!"); + return; + } + + NSString* campaignUserID = campaignDataDictionary[@"cuid"]; + if (!campaignUserID.length) + { + CLY_LOG_W(@"Campaign User ID must be non-zero length valid string. It will be ignored!"); + } + + [CountlyConnectionManager.sharedInstance sendDirectAttributionWithCampaignID:campaignID andCampaignUserID:campaignUserID]; +} + +- (void)recordIndirectAttribution:(NSDictionary *)attribution +{ + CLY_LOG_I(@"%s %@", __FUNCTION__, attribution); + + if (!CountlyConsentManager.sharedInstance.consentForAttribution) + return; + + NSMutableDictionary* filtered = attribution.mutableCopy; + [attribution enumerateKeysAndObjectsUsingBlock:^(NSString * key, NSString * value, BOOL * stop) + { + if (!value.length) + [filtered removeObjectForKey:key]; + }]; + + NSDictionary* truncated = [filtered cly_truncated:@"Indirect attribution"]; + NSDictionary* limited = [truncated cly_limited:@"Indirect attribution"]; + + [CountlyConnectionManager.sharedInstance sendIndirectAttribution:limited]; +} #pragma mark - Remote Config diff --git a/src/ios/CountlyiOS/Countly.podspec b/src/ios/CountlyiOS/Countly.podspec new file mode 100644 index 0000000..c620b95 --- /dev/null +++ b/src/ios/CountlyiOS/Countly.podspec @@ -0,0 +1,31 @@ +Pod::Spec.new do |s| + s.name = 'Countly' + s.version = '21.11.2' + s.license = { :type => 'MIT', :file => 'LICENSE.md' } + s.summary = 'Countly is an innovative, real-time, open source mobile analytics platform.' + s.homepage = 'https://github.com/Countly/countly-sdk-ios' + s.social_media_url = 'https://twitter.com/gocountly' + s.author = {'Countly' => 'hello@count.ly'} + s.source = { :git => 'https://github.com/Countly/countly-sdk-ios.git', :tag => s.version.to_s } + + s.requires_arc = true + s.default_subspecs = 'Core' + s.ios.deployment_target = '10.0' + s.osx.deployment_target = '10.14' + s.watchos.deployment_target = '4.0' + s.tvos.deployment_target = '10.0' + + s.subspec 'Core' do |core| + core.source_files = '*.{h,m}' + core.public_header_files = 'Countly.h', 'CountlyUserDetails.h', 'CountlyConfig.h', 'CountlyFeedbackWidget.h' + core.preserve_path = 'countly_dsym_uploader.sh' + core.ios.frameworks = ['Foundation', 'UIKit', 'UserNotifications', 'CoreLocation', 'WebKit', 'CoreTelephony', 'WatchConnectivity'] + end + + s.subspec 'NotificationService' do |ns| + ns.source_files = 'CountlyNotificationService.{m,h}' + ns.ios.deployment_target = '10.0' + ns.ios.frameworks = ['Foundation', 'UserNotifications'] + end + +end diff --git a/src/ios/CountlyiOS/CountlyCommon.h b/src/ios/CountlyiOS/CountlyCommon.h index 68b9fae..94e1a61 100644 --- a/src/ios/CountlyiOS/CountlyCommon.h +++ b/src/ios/CountlyiOS/CountlyCommon.h @@ -31,7 +31,6 @@ #if (TARGET_OS_IOS) #import -#import "WatchConnectivity/WatchConnectivity.h" #endif #if (TARGET_OS_WATCH) @@ -45,8 +44,12 @@ #import +NS_ASSUME_NONNULL_BEGIN + extern NSString* const kCountlyErrorDomain; +extern NSString* const kCountlyReservedEventOrientation; + NS_ERROR_ENUM(kCountlyErrorDomain) { CLYErrorFeedbackWidgetNotAvailable = 10001, @@ -62,11 +65,17 @@ NS_ERROR_ENUM(kCountlyErrorDomain) @property (nonatomic) BOOL hasStarted; @property (nonatomic) BOOL enableDebug; +@property (nonatomic) BOOL shouldIgnoreTrustCheck; @property (nonatomic, weak) id loggerDelegate; @property (nonatomic) CLYInternalLogLevel internalLogLevel; -@property (nonatomic) BOOL enableAppleWatch; @property (nonatomic, copy) NSString* attributionID; @property (nonatomic) BOOL manualSessionHandling; +@property (nonatomic) BOOL enableOrientationTracking; + + +@property (nonatomic) NSUInteger maxKeyLength; +@property (nonatomic) NSUInteger maxValueLength; +@property (nonatomic) NSUInteger maxSegmentationValues; void CountlyInternalLog(CLYInternalLogLevel level, NSString *format, ...) NS_FORMAT_FUNCTION(2, 3); void CountlyPrint(NSString *stringToPrint); @@ -87,8 +96,6 @@ void CountlyPrint(NSString *stringToPrint); - (void)tryPresentingViewController:(UIViewController *)viewController withCompletion:(void (^ __nullable) (void))completion; #endif -- (void)startAppleWatchMatching; - - (void)observeDeviceOrientationChanges; - (BOOL)hasStarted_; @@ -116,6 +123,8 @@ void CountlyPrint(NSString *stringToPrint); - (NSString *)cly_SHA256; - (NSData *)cly_dataUTF8; - (NSString *)cly_valueForQueryStringKey:(NSString *)key; +- (NSString *)cly_truncatedKey:(NSString *)explanation; +- (NSString *)cly_truncatedValue:(NSString *)explanation; @end @interface NSArray (Countly) @@ -124,6 +133,8 @@ void CountlyPrint(NSString *stringToPrint); @interface NSDictionary (Countly) - (NSString *)cly_JSONify; +- (NSDictionary *)cly_truncated:(NSString *)explanation; +- (NSDictionary *)cly_limited:(NSString *)explanation; @end @interface NSData (Countly) @@ -138,3 +149,5 @@ void CountlyPrint(NSString *stringToPrint); @interface CountlyUserDetails (ClearUserDetails) - (void)clearUserDetails; @end + +NS_ASSUME_NONNULL_END diff --git a/src/ios/CountlyiOS/CountlyCommon.m b/src/ios/CountlyiOS/CountlyCommon.m index bfa129d..3985178 100644 --- a/src/ios/CountlyiOS/CountlyCommon.m +++ b/src/ios/CountlyiOS/CountlyCommon.m @@ -10,10 +10,6 @@ NSString* const kCountlyReservedEventOrientation = @"[CLY]_orientation"; NSString* const kCountlyOrientationKeyMode = @"mode"; -@interface CLYWCSessionDelegateInterceptor : CLYDelegateInterceptor -@end - - @interface CountlyCommon () { NSCalendar* gregorianCalendar; @@ -28,16 +24,11 @@ @interface CountlyCommon () #if (TARGET_OS_IOS || TARGET_OS_TV) @property (nonatomic) UIBackgroundTaskIdentifier bgTask; #endif -#if (TARGET_OS_IOS || TARGET_OS_WATCH) -@property (nonatomic) CLYWCSessionDelegateInterceptor* watchDelegate; -#endif @end -NSString* const kCountlySDKVersion = @"20.11.3"; +NSString* const kCountlySDKVersion = @"21.11.2"; NSString* const kCountlySDKName = @"objc-native-ios"; -NSString* const kCountlyParentDeviceIDTransferKey = @"kCountlyParentDeviceIDTransferKey"; - NSString* const kCountlyErrorDomain = @"ly.count.ErrorDomain"; NSString* const kCountlyInternalLogPrefix = @"[Countly] "; @@ -161,40 +152,6 @@ - (NSTimeInterval)uniqueTimestamp return (NSTimeInterval)(self.lastTimestamp / 1000.0); } -#pragma mark - Watch Connectivity - -- (void)startAppleWatchMatching -{ - if (!self.enableAppleWatch) - return; - - if (!CountlyConsentManager.sharedInstance.consentForAppleWatch) - return; - -#if (TARGET_OS_IOS || TARGET_OS_WATCH) - if (@available(iOS 9.0, *)) - { - if (WCSession.isSupported) - { - self.watchDelegate = [CLYWCSessionDelegateInterceptor alloc]; - self.watchDelegate.originalDelegate = WCSession.defaultSession.delegate; - WCSession.defaultSession.delegate = (id)self.watchDelegate; - [WCSession.defaultSession activateSession]; - } - } -#endif - -#if (TARGET_OS_IOS) - if (@available(iOS 9.0, *)) - { - if (WCSession.defaultSession.paired && WCSession.defaultSession.watchAppInstalled) - { - [WCSession.defaultSession transferUserInfo:@{kCountlyParentDeviceIDTransferKey: CountlyDeviceInfo.sharedInstance.deviceID}]; - CLY_LOG_D(@"Transferring parent device ID %@ ...", CountlyDeviceInfo.sharedInstance.deviceID); - } - } -#endif -} #pragma mark - Orientation @@ -207,6 +164,9 @@ - (void)observeDeviceOrientationChanges - (void)deviceOrientationDidChange:(NSNotification *)notification { + if (!self.enableOrientationTracking) + return; + //NOTE: Delay is needed for interface orientation change animation to complete. Otherwise old interface orientation value is returned. [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(recordOrientation) object:nil]; [self performSelector:@selector(recordOrientation) withObject:nil afterDelay:0.5]; @@ -215,15 +175,11 @@ - (void)deviceOrientationDidChange:(NSNotification *)notification - (void)recordOrientation { #if (TARGET_OS_IOS) - UIInterfaceOrientation interfaceOrientation = UIInterfaceOrientationUnknown; - if (@available(iOS 13.0, *)) - { - interfaceOrientation = UIApplication.sharedApplication.keyWindow.windowScene.interfaceOrientation; - } - else - { - interfaceOrientation = UIApplication.sharedApplication.statusBarOrientation; - } + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + UIInterfaceOrientation interfaceOrientation = UIApplication.sharedApplication.statusBarOrientation; +#pragma GCC diagnostic pop NSString* mode = nil; if (UIInterfaceOrientationIsPortrait(interfaceOrientation)) @@ -283,18 +239,21 @@ - (void)finishBackgroundTask #if (TARGET_OS_IOS || TARGET_OS_TV) - (UIViewController *)topViewController { +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" UIViewController* topVC = UIApplication.sharedApplication.keyWindow.rootViewController; +#pragma GCC diagnostic pop while (YES) { if (topVC.presentedViewController) topVC = topVC.presentedViewController; - else if ([topVC isKindOfClass:UINavigationController.class]) - topVC = ((UINavigationController *)topVC).topViewController; - else if ([topVC isKindOfClass:UITabBarController.class]) - topVC = ((UITabBarController *)topVC).selectedViewController; - else - break; + else if ([topVC isKindOfClass:UINavigationController.class]) + topVC = ((UINavigationController *)topVC).topViewController; + else if ([topVC isKindOfClass:UITabBarController.class]) + topVC = ((UITabBarController *)topVC).selectedViewController; + else + break; } return topVC; @@ -394,7 +353,11 @@ - (void)positionToTopRight:(BOOL)shouldConsiderStatusBar { if (@available(iOS 11.0, *)) { +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" CGFloat top = UIApplication.sharedApplication.keyWindow.safeAreaInsets.top; +#pragma GCC diagnostic pop + if (top) { rect.origin.y += top; @@ -435,35 +398,6 @@ - (void)forwardInvocation:(NSInvocation *)invocation @end -#pragma mark - Watch Delegate Proxy -@implementation CLYWCSessionDelegateInterceptor - -#if (TARGET_OS_WATCH) -- (void)session:(WCSession *)session didReceiveUserInfo:(NSDictionary *)userInfo -{ - CLY_LOG_D(@"Watch received user info: \n%@", userInfo); - - NSString* parentDeviceID = userInfo[kCountlyParentDeviceIDTransferKey]; - - if (parentDeviceID && ![parentDeviceID isEqualToString:[CountlyPersistency.sharedInstance retrieveWatchParentDeviceID]]) - { - [CountlyConnectionManager.sharedInstance sendParentDeviceID:parentDeviceID]; - - CLY_LOG_D(@"Parent device ID %@ added to queue.", parentDeviceID); - - [CountlyPersistency.sharedInstance storeWatchParentDeviceID:parentDeviceID]; - } - - if ([self.originalDelegate respondsToSelector:@selector(session:didReceiveUserInfo:)]) - { - CLY_LOG_D(@"Forwarding WCSession user info to original delegate."); - - [self.originalDelegate session:session didReceiveUserInfo:userInfo]; - } -} -#endif -@end - #pragma mark - Categories NSString* CountlyJSONFromObject(id object) @@ -526,6 +460,29 @@ - (NSString *)cly_valueForQueryStringKey:(NSString *)key return nil; } + +- (NSString *)cly_truncatedKey:(NSString *)explanation +{ + if (self.length > CountlyCommon.sharedInstance.maxKeyLength) + { + CLY_LOG_W(@"%@ length is more than the limit (%ld)! So, it will be truncated: %@.", explanation, (long)CountlyCommon.sharedInstance.maxKeyLength, self); + return [self substringToIndex:CountlyCommon.sharedInstance.maxKeyLength]; + } + + return self; +} + +- (NSString *)cly_truncatedValue:(NSString *)explanation +{ + if (self.length > CountlyCommon.sharedInstance.maxValueLength) + { + CLY_LOG_W(@"%@ length is more than the limit (%ld)! So, it will be truncated: %@.", explanation, (long)CountlyCommon.sharedInstance.maxValueLength, self); + return [self substringToIndex:CountlyCommon.sharedInstance.maxValueLength]; + } + + return self; +} + @end @implementation NSArray (Countly) @@ -540,6 +497,50 @@ - (NSString *)cly_JSONify { return [CountlyJSONFromObject(self) cly_URLEscaped]; } + +- (NSDictionary *)cly_truncated:(NSString *)explanation +{ + NSMutableDictionary* truncatedDict = self.mutableCopy; + [self enumerateKeysAndObjectsUsingBlock:^(NSString * key, id obj, BOOL * stop) + { + NSString* truncatedKey = [key cly_truncatedKey:[explanation stringByAppendingString:@" key"]]; + if (![truncatedKey isEqualToString:key]) + { + truncatedDict[truncatedKey] = obj; + [truncatedDict removeObjectForKey:key]; + } + + if ([obj isKindOfClass:NSString.class]) + { + NSString* truncatedValue = [obj cly_truncatedValue:[explanation stringByAppendingString:@" value"]]; + if (![truncatedValue isEqualToString:obj]) + { + truncatedDict[truncatedKey] = truncatedValue; + } + } + }]; + + return truncatedDict.copy; +} + +- (NSDictionary *)cly_limited:(NSString *)explanation +{ + NSArray* allKeys = self.allKeys; + + if (allKeys.count <= CountlyCommon.sharedInstance.maxSegmentationValues) + return self; + + NSMutableArray* excessKeys = allKeys.mutableCopy; + [excessKeys removeObjectsInRange:(NSRange){0, CountlyCommon.sharedInstance.maxSegmentationValues}]; + + CLY_LOG_W(@"Number of key-value pairs in %@ is more than the limit (%ld)! So, some of them will be removed:\n %@", explanation, (long)CountlyCommon.sharedInstance.maxSegmentationValues, [excessKeys description]); + + NSMutableDictionary* limitedDict = self.mutableCopy; + [limitedDict removeObjectsForKeys:excessKeys]; + + return limitedDict.copy; +} + @end @implementation NSData (Countly) diff --git a/src/ios/CountlyiOS/CountlyConfig.h b/src/ios/CountlyiOS/CountlyConfig.h index 676155c..bb40b7e 100644 --- a/src/ios/CountlyiOS/CountlyConfig.h +++ b/src/ios/CountlyiOS/CountlyConfig.h @@ -55,11 +55,6 @@ extern CLYDeviceIDType const CLYDeviceIDTypeTemporary; extern CLYDeviceIDType const CLYDeviceIDTypeIDFV; extern CLYDeviceIDType const CLYDeviceIDTypeNSUUID; -//NOTE: Legacy device ID options -extern NSString* const CLYIDFV DEPRECATED_MSG_ATTRIBUTE("Please use CLYDefaultDeviceID instead!"); -extern NSString* const CLYIDFA DEPRECATED_MSG_ATTRIBUTE("Please use CLYDefaultDeviceID instead!"); -extern NSString* const CLYOpenUDID DEPRECATED_MSG_ATTRIBUTE("Please use CLYDefaultDeviceID instead!"); - //NOTE: Available consents typedef NSString* CLYConsent NS_EXTENSIBLE_STRING_ENUM; extern CLYConsent const CLYConsentSessions; @@ -71,7 +66,7 @@ extern CLYConsent const CLYConsentLocation; extern CLYConsent const CLYConsentViewTracking; extern CLYConsent const CLYConsentAttribution; extern CLYConsent const CLYConsentStarRating DEPRECATED_MSG_ATTRIBUTE("Please use CLYConsentFeedback instead!"); -extern CLYConsent const CLYConsentAppleWatch; +extern CLYConsent const CLYConsentAppleWatch DEPRECATED_MSG_ATTRIBUTE("As automatic metrics for Apple Watch is not supported anymore, 'CLYConsentAppleWatch' is now inoperative!"); extern CLYConsent const CLYConsentPerformanceMonitoring; extern CLYConsent const CLYConsentFeedback; extern CLYConsent const CLYConsentRemoteConfig; @@ -95,6 +90,10 @@ extern CLYMetricKey const CLYMetricKeyLocale; extern CLYMetricKey const CLYMetricKeyHasWatch; extern CLYMetricKey const CLYMetricKeyInstalledWatchApp; +//NOTE: Attribution keys +typedef NSString* CLYAttributionKey NS_EXTENSIBLE_STRING_ENUM; +extern CLYAttributionKey const CLYAttributionKeyIDFA; +extern CLYAttributionKey const CLYAttributionKeyADID; //NOTE: Internal log levels typedef enum : NSUInteger @@ -139,6 +138,13 @@ typedef enum : NSUInteger */ @property (nonatomic) BOOL enableDebug; +/** + * For ignoring all SSL trust checks by setting server trust as exception. + * @discussion Can be used for self-signed certificates. + * @discussion Works only for Development environment where @c DEBUG flag is set in Build Settings. + */ +@property (nonatomic) BOOL shouldIgnoreTrustCheck; + /** * For receiving SDK's internal logs even in production builds. * @discussion If set, SDK will forward its internal logs to this delegate object regardless of @c enableDebug initial config value. @@ -197,12 +203,6 @@ typedef enum : NSUInteger @property (nonatomic, copy) NSArray* consents; #pragma mark - -/** - * @c isTestDevice property is deprecated. Please use @c pushTestMode property instead. - * @discussion Using this property will have no effect. - */ -@property (nonatomic) BOOL isTestDevice DEPRECATED_MSG_ATTRIBUTE("Use 'pushTestMode' property instead!"); - /** * For specifying which test mode Countly Server should use for sending push notifications. * @discussion There are 2 test modes: @@ -221,7 +221,8 @@ typedef enum : NSUInteger /** * For disabling automatically showing of message alerts by @c CLYPushNotifications feature. - * @discussion If set, push notifications that contain a message or a URL visit request will not show alerts automatically. Push Open event will be recorded automatically, but Push Action event needs to be recorded manually, as well as displaying the message manually. + * @discussion If set, push notifications that contain a message or a URL will not show alerts automatically. + * @discussion Push Action event needs to be recorded manually, as well as displaying the message. */ @property (nonatomic) BOOL doNotShowAlertForNotifications; @@ -279,19 +280,6 @@ typedef enum : NSUInteger */ @property (nonatomic) BOOL resetStoredDeviceID; -/** - * @c forceDeviceIDInitialization property is deprecated. Please use @c resetStoredDeviceID property instead. - * @discussion Using this property will have no effect. - */ -@property (nonatomic) BOOL forceDeviceIDInitialization DEPRECATED_MSG_ATTRIBUTE("Use 'resetStoredDeviceID' property instead!"); - -/** - * @c applyZeroIDFAFixFor property is deprecated. - * @discussion As IDFA is not supported anymore, @c applyZeroIDFAFix is now inoperative. - * @discussion Using this property will have no effect. - */ -@property (nonatomic) BOOL applyZeroIDFAFix DEPRECATED_MSG_ATTRIBUTE("As IDFA is not supported anymore, 'applyZeroIDFAFix' is now inoperative!"); - #pragma mark - /** @@ -314,6 +302,43 @@ typedef enum : NSUInteger */ @property (nonatomic) NSUInteger storedRequestsLimit; +/** + * Limit for the length of all string keys. + * @discussion It affects: + * @discussion - event names + * @discussion - view names + * @discussion - APM network trace names + * @discussion - APM custom trace names + * @discussion - APM custom trace metric keys + * @discussion - segmentation keys + * @discussion - custom metric keys + * @discussion - custom user property keys + * @discussion Keys longer than this limit will be truncated. + * @discussion If not set, it will be 128 chars by default. + */ +@property (nonatomic) NSUInteger maxKeyLength; + +/** + * Limit for the length of values in all key-value pairs. + * @discussion It affects: + * @discussion - segmentation values + * @discussion - APM custom trace metric values + * @discussion - custom crash logs + * @discussion - custom metric values + * @discussion - custom user property values + * @discussion Values longer than this limit will be truncated. + * @discussion If not set, it will be 256 chars by default. + */ +@property (nonatomic) NSUInteger maxValueLength; + +/** + * Limit for the number of key-value pairs in segmentations. + * @discussion If there are more key-value pairs than this limit, some of them will be removed. + * @discussion As obviously there is no order among the keys of an NSDictionary, it is not defined which ones will be removed. + * @discussion If not set, it will be 30 by default. + */ +@property (nonatomic) NSUInteger maxSegmentationValues; + /** * For sending all requests using HTTP POST method. * @discussion If set, all requests will be sent using HTTP POST method. Otherwise; only the requests with a file upload or data size more than 2048 bytes will be sent using HTTP POST method. @@ -329,20 +354,38 @@ typedef enum : NSUInteger @property (nonatomic) BOOL manualSessionHandling; /** - * For enabling automatic handling of Apple Watch related features for iOS apps with a watchOS counterpart app. - * @discussion If set on both iOS and watchOS app, Apple Watch related features such as parent device matching, pairing status, and watch app installing status will be handled automatically. - * @discussion This flag should not be set on independent watchOS apps. + * @c enableAppleWatch property is deprecated. + * @discussion As automatic metrics for Apple Watch is not supported anymore, @c enableAppleWatch is now inoperative. + * @discussion Using this property will have no effect. */ -@property (nonatomic) BOOL enableAppleWatch; +@property (nonatomic) BOOL enableAppleWatch DEPRECATED_MSG_ATTRIBUTE("As automatic metrics for Apple Watch is not supported anymore, 'enableAppleWatch' is now inoperative!"); #pragma mark - /** - * For specifying attribution ID (IDFA) for campaign attribution. + * For specifying attribution ID (IDFA). * @discussion If set, this attribution ID will be sent with all @c begin_session requests. */ @property (nonatomic, copy) NSString* attributionID; +/** + * For specifying direct attribution campaign type. + * @discussion Currently supported campaign types are "countly" and "_special_test". + */ +@property (nonatomic, copy) NSString* campaignType; + +/** + * For specifying direct attribution campaign data. + * @discussion Campaign data has to be in `{"cid":"CAMPAIGN_ID", "cuid":"CAMPAIGN_USER_ID"}` format. + */ +@property (nonatomic, copy) NSString* campaignData; + +/** + * For specifying indirect attribution with given key-value pairs. + * @discussion Keys could be a predefined CLYAttributionKey or any non-zero length valid string. + */ +@property (nonatomic, copy) NSDictionary * indirectAttribution; + /** * @c enableAttribution property is deprecated. Please use @c recordAttributionID method instead. * @discussion Using this property will have no effect. @@ -417,18 +460,16 @@ typedef enum : NSUInteger @property (nonatomic, copy) NSArray* pinnedCertificates; /** - * Name of the custom HTTP header field to be sent with every request. - * @discussion e.g. X-My-Secret-Server-Token - * @discussion If set, every request sent to Countly Server will have this custom HTTP header and its value will be @c customHeaderFieldValue property. - * @discussion If @c customHeaderFieldValue is not set when Countly is started, requests will not start until it is set using @c setCustomHeaderFieldValue: method later. + * @c customHeaderFieldName property is deprecated. Please use @c URLSessionConfiguration property instead. + * @discussion Using this property will have no effect. */ -@property (nonatomic, copy) NSString* customHeaderFieldName; +@property (nonatomic, copy) NSString* customHeaderFieldName DEPRECATED_MSG_ATTRIBUTE("Use 'URLSessionConfiguration' property instead!"); /** - * Value of the custom HTTP header field to be sent with every request if @c customHeaderFieldName is set. - * @discussion If not set while @c customHeaderFieldName is set, requests will not start until it is set using @c setCustomHeaderFieldValue: method later. + * @c customHeaderFieldValue property is deprecated. Please use @c URLSessionConfiguration property instead. + * @discussion Using this property will have no effect. */ -@property (nonatomic, copy) NSString* customHeaderFieldValue; +@property (nonatomic, copy) NSString* customHeaderFieldValue DEPRECATED_MSG_ATTRIBUTE("Use 'URLSessionConfiguration' property instead!"); /** * Salt value to be used for parameter tampering protection. @@ -493,6 +534,19 @@ typedef enum : NSUInteger * @discussion If set, Performance Monitoring feature will be started automatically on SDK start. */ @property (nonatomic) BOOL enablePerformanceMonitoring; + +#pragma mark - + +/** + * For enabling automatic user interface orientation tracking. + * @discussion If set, user interface orientation tracking feature will be enabled. + * @discussion An event will be sent whenever user interface orientation changes. + * @discussion Orientation event will not be sent if consent for @c CLYConsentUserDetails is not given, + * while @c requiresConsent flag is set on initial configuration. + * @discussion Automatic user interface orientation tracking is enabled by default. + * @discussion For disabling it, please set this flag to @c NO. + */ +@property (nonatomic) BOOL enableOrientationTracking; NS_ASSUME_NONNULL_END @end diff --git a/src/ios/CountlyiOS/CountlyConfig.m b/src/ios/CountlyiOS/CountlyConfig.m index 0fce334..cfc0b0f 100644 --- a/src/ios/CountlyiOS/CountlyConfig.m +++ b/src/ios/CountlyiOS/CountlyConfig.m @@ -52,11 +52,17 @@ - (instancetype)init self.storedRequestsLimit = 1000; self.crashLogLimit = 100; + self.maxKeyLength = 128; + self.maxValueLength = 256; + self.maxSegmentationValues = 30; + self.location = kCLLocationCoordinate2DInvalid; self.URLSessionConfiguration = NSURLSessionConfiguration.defaultSessionConfiguration; self.internalLogLevel = CLYInternalLogLevelDebug; + + self.enableOrientationTracking = YES; } return self; diff --git a/src/ios/CountlyiOS/CountlyConnectionManager.h b/src/ios/CountlyiOS/CountlyConnectionManager.h index 7daafdd..b8f3b07 100644 --- a/src/ios/CountlyiOS/CountlyConnectionManager.h +++ b/src/ios/CountlyiOS/CountlyConnectionManager.h @@ -28,8 +28,6 @@ extern const NSInteger kCountlyGETRequestMaxLength; @property (nonatomic) NSString* host; @property (nonatomic) NSURLSessionTask* connection; @property (nonatomic) NSArray* pinnedCertificates; -@property (nonatomic) NSString* customHeaderFieldName; -@property (nonatomic) NSString* customHeaderFieldValue; @property (nonatomic) NSString* secretSalt; @property (nonatomic) BOOL alwaysUsePOST; @property (nonatomic) NSURLSessionConfiguration* URLSessionConfiguration; @@ -48,9 +46,11 @@ extern const NSInteger kCountlyGETRequestMaxLength; - (void)sendUserDetails:(NSString *)userDetails; - (void)sendCrashReport:(NSString *)report immediately:(BOOL)immediately; - (void)sendOldDeviceID:(NSString *)oldDeviceID; -- (void)sendParentDeviceID:(NSString *)parentDeviceID; - (void)sendAttribution; -- (void)sendConsentChanges:(NSString *)consentChanges; +- (void)sendDirectAttributionWithCampaignID:(NSString *)campaignID andCampaignUserID:(NSString *)campaignUserID; +- (void)sendAttributionData:(NSString *)attributionData; +- (void)sendIndirectAttribution:(NSDictionary *)attribution; +- (void)sendConsents:(NSString *)consents; - (void)sendPerformanceMonitoringTrace:(NSString *)trace; - (void)proceedOnQueue; diff --git a/src/ios/CountlyiOS/CountlyConnectionManager.m b/src/ios/CountlyiOS/CountlyConnectionManager.m index 87a1c23..6182dab 100644 --- a/src/ios/CountlyiOS/CountlyConnectionManager.m +++ b/src/ios/CountlyiOS/CountlyConnectionManager.m @@ -19,7 +19,6 @@ @interface CountlyConnectionManager () NSString* const kCountlyQSKeyDeviceID = @"device_id"; NSString* const kCountlyQSKeyDeviceIDOld = @"old_device_id"; -NSString* const kCountlyQSKeyDeviceIDParent = @"parent_device_id"; NSString* const kCountlyQSKeyTimestamp = @"timestamp"; NSString* const kCountlyQSKeyTimeZone = @"tz"; @@ -42,18 +41,26 @@ @interface CountlyConnectionManager () NSString* const kCountlyQSKeyLocationCountry = @"country_code"; NSString* const kCountlyQSKeyLocationIP = @"ip_address"; +NSString* const kCountlyQSKeyAttributionID = @"aid"; +NSString* const kCountlyQSKeyIDFA = @"idfa"; +NSString* const kCountlyQSKeyADID = @"adid"; +NSString* const kCountlyQSKeyCampaignID = @"campaign_id"; +NSString* const kCountlyQSKeyCampaignUser = @"campaign_user"; +NSString* const kCountlyQSKeyAttributionData = @"attribution_data"; + NSString* const kCountlyQSKeyMetrics = @"metrics"; NSString* const kCountlyQSKeyEvents = @"events"; NSString* const kCountlyQSKeyUserDetails = @"user_details"; NSString* const kCountlyQSKeyCrash = @"crash"; NSString* const kCountlyQSKeyChecksum256 = @"checksum256"; -NSString* const kCountlyQSKeyAttributionID = @"aid"; -NSString* const kCountlyQSKeyIDFA = @"idfa"; NSString* const kCountlyQSKeyConsent = @"consent"; NSString* const kCountlyQSKeyAPM = @"apm"; NSString* const kCountlyQSKeyMethod = @"method"; +CLYAttributionKey const CLYAttributionKeyIDFA = kCountlyQSKeyIDFA; +CLYAttributionKey const CLYAttributionKeyADID = kCountlyQSKeyADID; + NSString* const kCountlyUploadBoundary = @"0cae04a8b698d63ff6ea55d168993f21"; NSString* const kCountlyEndpointI = @"/i"; //NOTE: input endpoint @@ -88,6 +95,20 @@ - (instancetype)init return self; } +- (void)setHost:(NSString *)host +{ + if ([host hasSuffix:@"/"]) + { + CLY_LOG_W(@"Host has an extra \"/\" at the end! It will be removed by the SDK.\ + But please make sure you fix it to avoid this warning in the future."); + _host = [host substringToIndex:host.length - 1]; + } + else + { + _host = host; + } +} + - (void)proceedOnQueue { CLY_LOG_D(@"Proceeding on queue..."); @@ -110,12 +131,6 @@ - (void)proceedOnQueue return; } - if (self.customHeaderFieldName && !self.customHeaderFieldValue) - { - CLY_LOG_D(@"Proceeding on queue is aborted: customHeaderFieldName specified on config, but customHeaderFieldValue not set yet!"); - return; - } - if (CountlyPersistency.sharedInstance.isQueueBeingModified) { CLY_LOG_D(@"Proceeding on queue is aborted: Queue is being modified!"); @@ -161,9 +176,6 @@ - (void)proceedOnQueue request.HTTPBody = [queryString cly_dataUTF8]; } - if (self.customHeaderFieldName && self.customHeaderFieldValue) - [request setValue:self.customHeaderFieldValue forHTTPHeaderField:self.customHeaderFieldName]; - request.cachePolicy = NSURLRequestReloadIgnoringLocalCacheData; self.connection = [self.URLSession dataTaskWithRequest:request completionHandler:^(NSData * data, NSURLResponse * response, NSError * error) @@ -365,15 +377,6 @@ - (void)sendCrashReport:(NSString *)report immediately:(BOOL)immediately; if (!CountlyCommon.sharedInstance.manualSessionHandling) [self endSession]; - if (self.customHeaderFieldName && !self.customHeaderFieldValue) - { - CLY_LOG_D(@"customHeaderFieldName specified on config, but customHeaderFieldValue not set! Crash report stored to be sent later!"); - - [CountlyPersistency.sharedInstance addToQueue:queryString]; - [CountlyPersistency.sharedInstance saveToFileSync]; - return; - } - if (CountlyDeviceInfo.sharedInstance.isDeviceIDTemporary) { CLY_LOG_D(@"Device ID is set as CLYTemporaryDeviceID! Crash report stored to be sent later!"); @@ -390,9 +393,6 @@ - (void)sendCrashReport:(NSString *)report immediately:(BOOL)immediately; request.HTTPMethod = @"POST"; request.HTTPBody = [[self appendChecksum:queryString] cly_dataUTF8]; - if (self.customHeaderFieldName && self.customHeaderFieldValue) - [request setValue:self.customHeaderFieldValue forHTTPHeaderField:self.customHeaderFieldName]; - dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); [[self.URLSession dataTaskWithRequest:request completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) @@ -427,33 +427,58 @@ - (void)sendOldDeviceID:(NSString *)oldDeviceID [self proceedOnQueue]; } -- (void)sendParentDeviceID:(NSString *)parentDeviceID +- (void)sendAttribution { - NSString* queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%@", - kCountlyQSKeyDeviceIDParent, parentDeviceID.cly_URLEscaped]; + NSString * attributionQueryString = [self attributionQueryString]; + if (!attributionQueryString) + return; + + NSString* queryString = [[self queryEssentials] stringByAppendingString:attributionQueryString]; [CountlyPersistency.sharedInstance addToQueue:queryString]; [self proceedOnQueue]; } -- (void)sendAttribution +- (void)sendDirectAttributionWithCampaignID:(NSString *)campaignID andCampaignUserID:(NSString *)campaignUserID { - NSString * attributionQueryString = [self attributionQueryString]; - if (!attributionQueryString) - return; + NSMutableString* queryString = [self queryEssentials].mutableCopy; + [queryString appendFormat:@"&%@=%@", kCountlyQSKeyCampaignID, campaignID]; - NSString* queryString = [[self queryEssentials] stringByAppendingString:attributionQueryString]; + if (campaignUserID.length) + { + [queryString appendFormat:@"&%@=%@", kCountlyQSKeyCampaignUser, campaignUserID]; + } - [CountlyPersistency.sharedInstance addToQueue:queryString]; + [CountlyPersistency.sharedInstance addToQueue:queryString.copy]; + + [self proceedOnQueue]; +} + +- (void)sendAttributionData:(NSString *)attributionData +{ + NSMutableString* queryString = [self queryEssentials].mutableCopy; + [queryString appendFormat:@"&%@=%@", kCountlyQSKeyAttributionData, [attributionData cly_URLEscaped]]; + + [CountlyPersistency.sharedInstance addToQueue:queryString.copy]; [self proceedOnQueue]; } -- (void)sendConsentChanges:(NSString *)consentChanges +- (void)sendIndirectAttribution:(NSDictionary *)attribution +{ + NSMutableString* queryString = [self queryEssentials].mutableCopy; + [queryString appendFormat:@"&%@=%@", kCountlyQSKeyAttributionID, [attribution cly_JSONify]]; + + [CountlyPersistency.sharedInstance addToQueue:queryString.copy]; + + [self proceedOnQueue]; +} + +- (void)sendConsents:(NSString *)consents { NSString* queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%@", - kCountlyQSKeyConsent, consentChanges]; + kCountlyQSKeyConsent, consents]; [CountlyPersistency.sharedInstance addToQueue:queryString]; @@ -673,22 +698,48 @@ - (NSURLSession *)URLSession - (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler { - SecTrustRef serverTrust = challenge.protectionSpace.serverTrust; - SecKeyRef serverKey = SecTrustCopyPublicKey(serverTrust); SecPolicyRef policy = SecPolicyCreateSSL(true, (__bridge CFStringRef)challenge.protectionSpace.host); + SecTrustRef serverTrust = challenge.protectionSpace.serverTrust; + SecKeyRef serverKey = NULL; + + if (@available(iOS 14.0, tvOS 14.0, macOS 11.0, watchOS 7.0, *)) + { + serverKey = SecTrustCopyKey(serverTrust); + } + else + { +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + serverKey = SecTrustCopyPublicKey(serverTrust); +#pragma GCC diagnostic pop + } __block BOOL isLocalAndServerCertMatch = NO; - for (NSString* certificate in self.pinnedCertificates ) + for (NSString* certificate in self.pinnedCertificates) { NSString* localCertPath = [NSBundle.mainBundle pathForResource:certificate ofType:nil]; + if (!localCertPath) [NSException raise:@"CountlyCertificateNotFoundException" format:@"Bundled certificate can not be found for %@", certificate]; + NSData* localCertData = [NSData dataWithContentsOfFile:localCertPath]; SecCertificateRef localCert = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)localCertData); SecTrustRef localTrust = NULL; SecTrustCreateWithCertificates(localCert, policy, &localTrust); - SecKeyRef localKey = SecTrustCopyPublicKey(localTrust); + SecKeyRef localKey = NULL; + + if (@available(iOS 14.0, tvOS 14.0, macOS 11.0, watchOS 7.0, *)) + { + localKey = SecTrustCopyKey(localTrust); + } + else + { +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + localKey = SecTrustCopyPublicKey(localTrust); +#pragma GCC diagnostic pop + } CFRelease(localCert); CFRelease(localTrust); @@ -702,11 +753,24 @@ - (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticat break; } - if (localKey) CFRelease(localKey); + if (localKey) + CFRelease(localKey); } - + +#if DEBUG + if (CountlyCommon.sharedInstance.shouldIgnoreTrustCheck) + { + CFDataRef exceptions = SecTrustCopyExceptions(serverTrust); + SecTrustSetExceptions(serverTrust, exceptions); + CFRelease(exceptions); + } +#endif + SecTrustResultType serverTrustResult; +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" SecTrustEvaluate(serverTrust, &serverTrustResult); +#pragma GCC diagnostic pop BOOL isServerCertValid = (serverTrustResult == kSecTrustResultUnspecified || serverTrustResult == kSecTrustResultProceed); if (isLocalAndServerCertMatch && isServerCertValid) @@ -726,7 +790,9 @@ - (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticat completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, NULL); } - if (serverKey) CFRelease(serverKey); + if (serverKey) + CFRelease(serverKey); + CFRelease(policy); } diff --git a/src/ios/CountlyiOS/CountlyConsentManager.h b/src/ios/CountlyiOS/CountlyConsentManager.h index 439ac58..ec6314b 100644 --- a/src/ios/CountlyiOS/CountlyConsentManager.h +++ b/src/ios/CountlyiOS/CountlyConsentManager.h @@ -18,7 +18,6 @@ @property (nonatomic, readonly) BOOL consentForLocation; @property (nonatomic, readonly) BOOL consentForViewTracking; @property (nonatomic, readonly) BOOL consentForAttribution; -@property (nonatomic, readonly) BOOL consentForAppleWatch; @property (nonatomic, readonly) BOOL consentForPerformanceMonitoring; @property (nonatomic, readonly) BOOL consentForFeedback; @property (nonatomic, readonly) BOOL consentForRemoteConfig; @@ -28,6 +27,5 @@ - (void)giveConsentForAllFeatures; - (void)cancelConsentForFeatures:(NSArray *)features; - (void)cancelConsentForAllFeatures; -- (BOOL)hasAnyConsent; - +- (void)cancelConsentForAllFeaturesWithoutSendingConsentsRequest; @end diff --git a/src/ios/CountlyiOS/CountlyConsentManager.m b/src/ios/CountlyiOS/CountlyConsentManager.m index 636826c..cb9c73b 100644 --- a/src/ios/CountlyiOS/CountlyConsentManager.m +++ b/src/ios/CountlyiOS/CountlyConsentManager.m @@ -21,10 +21,6 @@ CLYConsent const CLYConsentRemoteConfig = @"remote-config"; -@interface CountlyConsentManager () -@property (nonatomic, strong) NSMutableDictionary* consentChanges; -@end - @implementation CountlyConsentManager @synthesize consentForSessions = _consentForSessions; @@ -35,7 +31,6 @@ @implementation CountlyConsentManager @synthesize consentForLocation = _consentForLocation; @synthesize consentForViewTracking = _consentForViewTracking; @synthesize consentForAttribution = _consentForAttribution; -@synthesize consentForAppleWatch = _consentForAppleWatch; @synthesize consentForPerformanceMonitoring = _consentForPerformanceMonitoring; @synthesize consentForFeedback = _consentForFeedback; @synthesize consentForRemoteConfig = _consentForRemoteConfig; @@ -58,7 +53,7 @@ - (instancetype)init { if (self = [super init]) { - self.consentChanges = NSMutableDictionary.new; + } return self; @@ -108,9 +103,6 @@ - (void)giveConsentForFeatures:(NSArray *)features if ([features containsObject:CLYConsentAttribution] && !self.consentForAttribution) self.consentForAttribution = YES; - if ([features containsObject:CLYConsentAppleWatch] && !self.consentForAppleWatch) - self.consentForAppleWatch = YES; - if ([features containsObject:CLYConsentPerformanceMonitoring] && !self.consentForPerformanceMonitoring) self.consentForPerformanceMonitoring = YES; @@ -120,7 +112,7 @@ - (void)giveConsentForFeatures:(NSArray *)features if ([features containsObject:CLYConsentRemoteConfig] && !self.consentForRemoteConfig) self.consentForRemoteConfig = YES; - [self sendConsentChanges]; + [self sendConsents]; } @@ -130,7 +122,19 @@ - (void)cancelConsentForAllFeatures } +- (void)cancelConsentForAllFeaturesWithoutSendingConsentsRequest +{ + [self cancelConsentForFeatures:[self allFeatures] shouldSkipSendingConsentsRequest:YES]; +} + + - (void)cancelConsentForFeatures:(NSArray *)features +{ + [self cancelConsentForFeatures:features shouldSkipSendingConsentsRequest:NO]; +} + + +- (void)cancelConsentForFeatures:(NSArray *)features shouldSkipSendingConsentsRequest:(BOOL)shouldSkipSendingConsentsRequest { if (!self.requiresConsent) return; @@ -159,9 +163,6 @@ - (void)cancelConsentForFeatures:(NSArray *)features if ([features containsObject:CLYConsentAttribution] && self.consentForAttribution) self.consentForAttribution = NO; - if ([features containsObject:CLYConsentAppleWatch] && self.consentForAppleWatch) - self.consentForAppleWatch = NO; - if ([features containsObject:CLYConsentPerformanceMonitoring] && self.consentForPerformanceMonitoring) self.consentForPerformanceMonitoring = NO; @@ -171,17 +172,28 @@ - (void)cancelConsentForFeatures:(NSArray *)features if ([features containsObject:CLYConsentRemoteConfig] && self.consentForRemoteConfig) self.consentForRemoteConfig = NO; - [self sendConsentChanges]; + if (!shouldSkipSendingConsentsRequest) + [self sendConsents]; } -- (void)sendConsentChanges +- (void)sendConsents { - if (self.consentChanges.allKeys.count) - { - [CountlyConnectionManager.sharedInstance sendConsentChanges:[self.consentChanges cly_JSONify]]; - [self.consentChanges removeAllObjects]; - } + NSDictionary * consents = + @{ + CLYConsentSessions: @(self.consentForSessions), + CLYConsentEvents: @(self.consentForEvents), + CLYConsentUserDetails: @(self.consentForUserDetails), + CLYConsentCrashReporting: @(self.consentForCrashReporting), + CLYConsentPushNotifications: @(self.consentForPushNotifications), + CLYConsentLocation: @(self.consentForLocation), + CLYConsentViewTracking: @(self.consentForViewTracking), + CLYConsentAttribution: @(self.consentForAttribution), + CLYConsentPerformanceMonitoring: @(self.consentForPerformanceMonitoring), + CLYConsentFeedback: @(self.consentForFeedback), + }; + + [CountlyConnectionManager.sharedInstance sendConsents:[consents cly_JSONify]]; } @@ -197,30 +209,11 @@ - (NSArray *)allFeatures CLYConsentLocation, CLYConsentViewTracking, CLYConsentAttribution, - CLYConsentAppleWatch, CLYConsentPerformanceMonitoring, CLYConsentFeedback, ]; } - -- (BOOL)hasAnyConsent -{ - return - self.consentForSessions || - self.consentForEvents || - self.consentForUserDetails || - self.consentForCrashReporting || - self.consentForPushNotifications || - self.consentForLocation || - self.consentForViewTracking || - self.consentForAttribution || - self.consentForAppleWatch || - self.consentForPerformanceMonitoring || - self.consentForFeedback || - self.consentForRemoteConfig; -} - - (BOOL)containsFeedbackOrStarRating:(NSArray *)features { //NOTE: StarRating consent is merged into new Feedback consent. @@ -248,8 +241,6 @@ - (void)setConsentForSessions:(BOOL)consentForSessions { CLY_LOG_D(@"Consent for Session is cancelled."); } - - self.consentChanges[CLYConsentSessions] = @(consentForSessions); } @@ -268,8 +259,6 @@ - (void)setConsentForEvents:(BOOL)consentForEvents [CountlyConnectionManager.sharedInstance sendEvents]; [CountlyPersistency.sharedInstance clearAllTimedEvents]; } - - self.consentChanges[CLYConsentEvents] = @(consentForEvents); } @@ -287,8 +276,6 @@ - (void)setConsentForUserDetails:(BOOL)consentForUserDetails [CountlyUserDetails.sharedInstance clearUserDetails]; } - - self.consentChanges[CLYConsentUserDetails] = @(consentForUserDetails); } @@ -308,8 +295,6 @@ - (void)setConsentForCrashReporting:(BOOL)consentForCrashReporting [CountlyCrashReporter.sharedInstance stopCrashReporting]; } - - self.consentChanges[CLYConsentCrashReporting] = @(consentForCrashReporting); } @@ -334,8 +319,6 @@ - (void)setConsentForPushNotifications:(BOOL)consentForPushNotifications #endif } #endif - - self.consentChanges[CLYConsentPushNotifications] = @(consentForPushNotifications); } @@ -353,8 +336,6 @@ - (void)setConsentForLocation:(BOOL)consentForLocation { CLY_LOG_D(@"Consent for Location is cancelled."); } - - self.consentChanges[CLYConsentLocation] = @(consentForLocation); } @@ -376,8 +357,6 @@ - (void)setConsentForViewTracking:(BOOL)consentForViewTracking [CountlyViewTracking.sharedInstance stopAutoViewTracking]; } #endif - - self.consentChanges[CLYConsentViewTracking] = @(consentForViewTracking); } @@ -395,29 +374,6 @@ - (void)setConsentForAttribution:(BOOL)consentForAttribution { CLY_LOG_D(@"Consent for Attribution is cancelled."); } - - self.consentChanges[CLYConsentAttribution] = @(consentForAttribution); -} - - -- (void)setConsentForAppleWatch:(BOOL)consentForAppleWatch -{ - _consentForAppleWatch = consentForAppleWatch; - -#if (TARGET_OS_IOS || TARGET_OS_WATCH) - if (consentForAppleWatch) - { - CLY_LOG_D(@"Consent for AppleWatch is given."); - - [CountlyCommon.sharedInstance startAppleWatchMatching]; - } - else - { - CLY_LOG_D(@"Consent for AppleWatch is cancelled."); - } -#endif - - self.consentChanges[CLYConsentAppleWatch] = @(consentForAppleWatch); } @@ -439,8 +395,6 @@ - (void)setConsentForPerformanceMonitoring:(BOOL)consentForPerformanceMonitoring [CountlyPerformanceMonitoring.sharedInstance stopPerformanceMonitoring]; } #endif - - self.consentChanges[CLYConsentPerformanceMonitoring] = @(consentForPerformanceMonitoring); } - (void)setConsentForFeedback:(BOOL)consentForFeedback @@ -459,8 +413,6 @@ - (void)setConsentForFeedback:(BOOL)consentForFeedback CLY_LOG_D(@"Consent for Feedback is cancelled."); } #endif - - self.consentChanges[CLYConsentFeedback] = @(consentForFeedback); } - (void)setConsentForRemoteConfig:(BOOL)consentForRemoteConfig @@ -477,8 +429,6 @@ - (void)setConsentForRemoteConfig:(BOOL)consentForRemoteConfig { CLY_LOG_D(@"Consent for RemoteConfig is cancelled."); } - - self.consentChanges[CLYConsentRemoteConfig] = @(consentForRemoteConfig); } #pragma mark - @@ -555,15 +505,6 @@ - (BOOL)consentForAttribution } -- (BOOL)consentForAppleWatch -{ - if (!self.requiresConsent) - return YES; - - return _consentForAppleWatch; -} - - - (BOOL)consentForPerformanceMonitoring { if (!self.requiresConsent) diff --git a/src/ios/CountlyiOS/CountlyDeviceInfo.h b/src/ios/CountlyiOS/CountlyDeviceInfo.h index 9496e66..a406463 100644 --- a/src/ios/CountlyiOS/CountlyDeviceInfo.h +++ b/src/ios/CountlyiOS/CountlyDeviceInfo.h @@ -26,10 +26,6 @@ + (NSString *)locale; + (NSString *)appVersion; + (NSString *)appBuild; -#if (TARGET_OS_IOS) -+ (NSInteger)hasWatch; -+ (NSInteger)installedWatchApp; -#endif + (NSString *)metrics; diff --git a/src/ios/CountlyiOS/CountlyDeviceInfo.m b/src/ios/CountlyiOS/CountlyDeviceInfo.m index 18824ca..f3ea34c 100644 --- a/src/ios/CountlyiOS/CountlyDeviceInfo.m +++ b/src/ios/CountlyiOS/CountlyDeviceInfo.m @@ -148,7 +148,7 @@ + (NSString *)deviceType #if (TARGET_OS_MACCATALYST) return @"desktop"; #else - if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) + if (UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad) return @"tablet"; return @"mobile"; @@ -217,7 +217,10 @@ + (NSString *)carrier { #if (TARGET_OS_IOS) #if (!TARGET_OS_MACCATALYST) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" return CountlyDeviceInfo.sharedInstance.networkInfo.subscriberCellularProvider.carrierName; +#pragma GCC diagnostic pop #endif #endif //NOTE: it is not possible to get carrier info on Apple Watches as CoreTelephony is not available. @@ -272,24 +275,6 @@ + (NSString *)appBuild return [NSBundle.mainBundle objectForInfoDictionaryKey:(NSString*)kCFBundleVersionKey]; } -#if (TARGET_OS_IOS) -+ (NSInteger)hasWatch -{ - if (@available(iOS 9.0, *)) - return (NSInteger)WCSession.defaultSession.paired; - - return 0; -} - -+ (NSInteger)installedWatchApp -{ - if (@available(iOS 9.0, *)) - return (NSInteger)WCSession.defaultSession.watchAppInstalled; - - return 0; -} -#endif - + (NSString *)metrics { NSMutableDictionary* metricsDictionary = NSMutableDictionary.new; @@ -307,25 +292,7 @@ + (NSString *)metrics metricsDictionary[CLYMetricKeyDensity] = CountlyDeviceInfo.density; metricsDictionary[CLYMetricKeyLocale] = CountlyDeviceInfo.locale; -#if (TARGET_OS_IOS) - if (CountlyCommon.sharedInstance.enableAppleWatch) - { - if (CountlyConsentManager.sharedInstance.consentForAppleWatch) - { - metricsDictionary[CLYMetricKeyHasWatch] = @(CountlyDeviceInfo.hasWatch); - metricsDictionary[CLYMetricKeyInstalledWatchApp] = @(CountlyDeviceInfo.installedWatchApp); - } - } -#endif - - if (CountlyDeviceInfo.sharedInstance.customMetrics) - { - [CountlyDeviceInfo.sharedInstance.customMetrics enumerateKeysAndObjectsUsingBlock:^(NSString* key, NSString* value, BOOL* stop) - { - if ([value isKindOfClass:NSString.class]) - metricsDictionary[key] = value; - }]; - } + [metricsDictionary addEntriesFromDictionary:CountlyDeviceInfo.sharedInstance.customMetrics]; return [metricsDictionary cly_JSONify]; } @@ -339,9 +306,6 @@ + (NSUInteger)connectionType CLYConnectionNone, CLYConnectionWiFi, CLYConnectionCellNetwork, - CLYConnectionCellNetwork2G, - CLYConnectionCellNetwork3G, - CLYConnectionCellNetworkLTE } CLYConnectionType; CLYConnectionType connType = CLYConnectionNone; @@ -361,29 +325,6 @@ + (NSUInteger)connectionType if ([[NSString stringWithUTF8String:i->ifa_name] isEqualToString:@"pdp_ip0"]) { connType = CLYConnectionCellNetwork; - -#if (TARGET_OS_IOS) -#if (!TARGET_OS_MACCATALYST) - NSDictionary* connectionTypes = - @{ - CTRadioAccessTechnologyGPRS: @(CLYConnectionCellNetwork2G), - CTRadioAccessTechnologyEdge: @(CLYConnectionCellNetwork2G), - CTRadioAccessTechnologyCDMA1x: @(CLYConnectionCellNetwork2G), - CTRadioAccessTechnologyWCDMA: @(CLYConnectionCellNetwork3G), - CTRadioAccessTechnologyHSDPA: @(CLYConnectionCellNetwork3G), - CTRadioAccessTechnologyHSUPA: @(CLYConnectionCellNetwork3G), - CTRadioAccessTechnologyCDMAEVDORev0: @(CLYConnectionCellNetwork3G), - CTRadioAccessTechnologyCDMAEVDORevA: @(CLYConnectionCellNetwork3G), - CTRadioAccessTechnologyCDMAEVDORevB: @(CLYConnectionCellNetwork3G), - CTRadioAccessTechnologyeHRPD: @(CLYConnectionCellNetwork3G), - CTRadioAccessTechnologyLTE: @(CLYConnectionCellNetworkLTE) - }; - - NSString* radioAccessTech = CountlyDeviceInfo.sharedInstance.networkInfo.currentRadioAccessTechnology; - if (connectionTypes[radioAccessTech]) - connType = [connectionTypes[radioAccessTech] integerValue]; -#endif -#endif } else if ([[NSString stringWithUTF8String:i->ifa_name] isEqualToString:@"en0"]) { @@ -440,14 +381,7 @@ + (NSInteger)batteryLevel UIDevice.currentDevice.batteryMonitoringEnabled = YES; return abs((int)(UIDevice.currentDevice.batteryLevel * 100)); #elif (TARGET_OS_WATCH) - if (@available(watchOS 4.0, *)) - { - return abs((int)(WKInterfaceDevice.currentDevice.batteryLevel * 100)); - } - else - { - return 100; - } + return abs((int)(WKInterfaceDevice.currentDevice.batteryLevel * 100)); #elif (TARGET_OS_OSX) CFTypeRef sourcesInfo = IOPSCopyPowerSourcesInfo(); NSArray *sources = (__bridge NSArray*)IOPSCopyPowerSourcesList(sourcesInfo); @@ -471,13 +405,10 @@ + (NSString *)orientation if (orientation >= 0 && orientation < orientations.count) return orientations[orientation]; #elif (TARGET_OS_WATCH) - if (@available(watchOS 3.0, *)) - { - NSArray *orientations = @[@"CrownLeft", @"CrownRight"]; - WKInterfaceDeviceCrownOrientation orientation = WKInterfaceDevice.currentDevice.crownOrientation; - if (orientation >= 0 && orientation < orientations.count) - return orientations[orientation]; - } + NSArray *orientations = @[@"CrownLeft", @"CrownRight"]; + WKInterfaceDeviceCrownOrientation orientation = WKInterfaceDevice.currentDevice.crownOrientation; + if (orientation >= 0 && orientation < orientations.count) + return orientations[orientation]; #endif return nil; diff --git a/src/ios/CountlyiOS/CountlyFeedbackWidget.h b/src/ios/CountlyiOS/CountlyFeedbackWidget.h index 3cb4e2e..bcdf2eb 100644 --- a/src/ios/CountlyiOS/CountlyFeedbackWidget.h +++ b/src/ios/CountlyiOS/CountlyFeedbackWidget.h @@ -12,6 +12,8 @@ typedef NSString* CLYFeedbackWidgetType NS_EXTENSIBLE_STRING_ENUM; extern CLYFeedbackWidgetType const CLYFeedbackWidgetTypeSurvey; extern CLYFeedbackWidgetType const CLYFeedbackWidgetTypeNPS; +extern NSString* const kCountlyReservedEventSurvey; +extern NSString* const kCountlyReservedEventNPS; @interface CountlyFeedbackWidget : NSObject #if (TARGET_OS_IOS) diff --git a/src/ios/CountlyiOS/CountlyFeedbackWidget.m b/src/ios/CountlyiOS/CountlyFeedbackWidget.m index 9ec1bc6..d694139 100644 --- a/src/ios/CountlyiOS/CountlyFeedbackWidget.m +++ b/src/ios/CountlyiOS/CountlyFeedbackWidget.m @@ -12,7 +12,9 @@ CLYFeedbackWidgetType const CLYFeedbackWidgetTypeSurvey = @"survey"; CLYFeedbackWidgetType const CLYFeedbackWidgetTypeNPS = @"nps"; -NSString* const kCountlyReservedEventPrefix = @"[CLY]_"; //NOTE: This will be used with feedback type. +NSString* const kCountlyReservedEventSurvey = @"[CLY]_survey"; +NSString* const kCountlyReservedEventNPS = @"[CLY]_nps"; + NSString* const kCountlyFBKeyClosed = @"closed"; NSString* const kCountlyFBKeyShown = @"shown"; @@ -38,11 +40,15 @@ + (CountlyFeedbackWidget *)createWithDictionary:(NSDictionary *)dictionary - (void)present { + CLY_LOG_I(@"%s", __FUNCTION__); + [self presentWithAppearBlock:nil andDismissBlock:nil]; } - (void)presentWithAppearBlock:(void(^ __nullable)(void))appearBlock andDismissBlock:(void(^ __nullable)(void))dismissBlock; { + CLY_LOG_I(@"%s %@ %@", __FUNCTION__, appearBlock, dismissBlock); + if (!CountlyConsentManager.sharedInstance.consentForFeedback) return; @@ -78,6 +84,8 @@ - (void)presentWithAppearBlock:(void(^ __nullable)(void))appearBlock andDismissB - (void)getWidgetData:(void (^)(NSDictionary * __nullable widgetData, NSError * __nullable error))completionHandler { + CLY_LOG_I(@"%s %@", __FUNCTION__, completionHandler); + NSURLSessionTask* task = [NSURLSession.sharedSession dataTaskWithRequest:[self dataRequest] completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) { NSDictionary *widgetData = nil; @@ -111,6 +119,8 @@ - (void)getWidgetData:(void (^)(NSDictionary * __nullable widgetData, NSError * - (void)recordResult:(NSDictionary * __nullable)result { + CLY_LOG_I(@"%s %@", __FUNCTION__, result); + if (!result) [self recordReservedEventForDismissing]; else @@ -184,7 +194,18 @@ - (void)recordReservedEventWithSegmentation:(NSDictionary *)segm if (!CountlyConsentManager.sharedInstance.consentForFeedback) return; - NSString* eventName = [kCountlyReservedEventPrefix stringByAppendingString:self.type]; + NSString* eventName = nil; + if ([self.type isEqualToString:CLYFeedbackWidgetTypeSurvey]) + eventName = kCountlyReservedEventSurvey; + else if ([self.type isEqualToString:CLYFeedbackWidgetTypeNPS]) + eventName = kCountlyReservedEventNPS; + + if (!eventName) + { + CLY_LOG_W(@"Unsupported feedback widget type! Event will not be recorded!"); + return; + } + NSMutableDictionary* segmentation = segm.mutableCopy; segmentation[kCountlyFBKeyPlatform] = CountlyDeviceInfo.osName; segmentation[kCountlyFBKeyAppVersion] = CountlyDeviceInfo.appVersion; diff --git a/src/ios/CountlyiOS/CountlyFeedbacks.h b/src/ios/CountlyiOS/CountlyFeedbacks.h index fbf0eae..a411e7b 100644 --- a/src/ios/CountlyiOS/CountlyFeedbacks.h +++ b/src/ios/CountlyiOS/CountlyFeedbacks.h @@ -13,12 +13,15 @@ extern NSString* const kCountlyFBKeyAppVersion; extern NSString* const kCountlyFBKeyWidgetID; extern NSString* const kCountlyFBKeyID; +extern NSString* const kCountlyReservedEventStarRating; + @interface CountlyFeedbacks : NSObject #if (TARGET_OS_IOS) + (instancetype)sharedInstance; - (void)showDialog:(void(^)(NSInteger rating))completion; - (void)checkFeedbackWidgetWithID:(NSString *)widgetID completionHandler:(void (^)(NSError * error))completionHandler; +- (void)recordRatingWidgetWithID:(NSString *)widgetID rating:(NSInteger)rating email:(NSString *)email comment:(NSString *)comment userCanBeContacted:(BOOL)userCanBeContacted; - (void)checkForStarRatingAutoAsk; - (void)getFeedbackWidgets:(void (^)(NSArray *feedbackWidgets, NSError *error))completionHandler; diff --git a/src/ios/CountlyiOS/CountlyFeedbacks.m b/src/ios/CountlyiOS/CountlyFeedbacks.m index fec7b7a..5c68913 100644 --- a/src/ios/CountlyiOS/CountlyFeedbacks.m +++ b/src/ios/CountlyiOS/CountlyFeedbacks.m @@ -35,6 +35,9 @@ @interface CountlyFeedbacks () NSString* const kCountlyFBKeyPhone = @"phone"; NSString* const kCountlyFBKeyTablet = @"tablet"; NSString* const kCountlyFBKeyFeedback = @"feedback"; +NSString* const kCountlyFBKeyEmail = @"email"; +NSString* const kCountlyFBKeyComment = @"comment"; +NSString* const kCountlyFBKeyContactMe = @"contactMe"; const CGFloat kCountlyStarRatingButtonSize = 40.0; @@ -365,14 +368,35 @@ - (NSURL *)widgetDisplayURL:(NSString *)widgetID - (BOOL)isDeviceTargetedByWidget:(NSDictionary *)widgetInfo { - BOOL isTablet = UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad; - BOOL isPhone = UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone; + BOOL isTablet = UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad; + BOOL isPhone = UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPhone; BOOL isTabletTargeted = [widgetInfo[kCountlyFBKeyTargetDevices][kCountlyFBKeyTablet] boolValue]; BOOL isPhoneTargeted = [widgetInfo[kCountlyFBKeyTargetDevices][kCountlyFBKeyPhone] boolValue]; return ((isTablet && isTabletTargeted) || (isPhone && isPhoneTargeted)); } +- (void)recordRatingWidgetWithID:(NSString *)widgetID rating:(NSInteger)rating email:(NSString *)email comment:(NSString *)comment userCanBeContacted:(BOOL)userCanBeContacted +{ + if (!CountlyConsentManager.sharedInstance.consentForFeedback) + return; + + if (!widgetID.length) + return; + + NSMutableDictionary* segmentation = NSMutableDictionary.new; + segmentation[kCountlyFBKeyPlatform] = CountlyDeviceInfo.osName; + segmentation[kCountlyFBKeyAppVersion] = CountlyDeviceInfo.appVersion; + segmentation[kCountlyFBKeyRating] = @(rating); + segmentation[kCountlyFBKeyWidgetID] = widgetID; + segmentation[kCountlyFBKeyEmail] = email; + segmentation[kCountlyFBKeyComment] = comment; + segmentation[kCountlyFBKeyContactMe] = @(userCanBeContacted); + + [Countly.sharedInstance recordReservedEvent:kCountlyReservedEventStarRating segmentation:segmentation]; +} + + #pragma mark - Feedbacks (Surveys, NPS) - (void)getFeedbackWidgets:(void (^)(NSArray *feedbackWidgets, NSError *error))completionHandler diff --git a/src/ios/CountlyiOS/CountlyPerformanceMonitoring.m b/src/ios/CountlyiOS/CountlyPerformanceMonitoring.m index a53eaa8..3c6eb0f 100644 --- a/src/ios/CountlyiOS/CountlyPerformanceMonitoring.m +++ b/src/ios/CountlyiOS/CountlyPerformanceMonitoring.m @@ -180,6 +180,8 @@ - (void)recordNetworkTrace:(NSString *)traceName if (!traceName.length) return; + traceName = [traceName cly_truncatedKey:@"Network trace name"]; + NSDictionary* metrics = @{ kCountlyPMKeyRequestPayloadSize: @(requestPayloadSize), @@ -245,6 +247,9 @@ - (void)endCustomTrace:(NSString *)traceName metrics:(NSDictionary *)metrics return; } + traceName = [traceName cly_truncatedKey:@"Custom trace name"]; + metrics = [metrics cly_truncated:@"Custom trace metric"]; + NSNumber* endTime = @((long long)(CountlyCommon.sharedInstance.uniqueTimestamp * 1000)); NSMutableDictionary* mutableMetrics = metrics.mutableCopy; diff --git a/src/ios/CountlyiOS/CountlyPersistency.m b/src/ios/CountlyiOS/CountlyPersistency.m index 50d2a61..3a25ba1 100644 --- a/src/ios/CountlyiOS/CountlyPersistency.m +++ b/src/ios/CountlyiOS/CountlyPersistency.m @@ -45,7 +45,10 @@ - (instancetype)init if (readData) { +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" NSDictionary* readDict = [NSKeyedUnarchiver unarchiveObjectWithData:readData]; +#pragma GCC diagnostic pop self.queuedRequests = [readDict[kCountlyQueuedRequestsPersistencyKey] mutableCopy]; } @@ -178,6 +181,11 @@ - (void)recordEvent:(CountlyEvent *)event { @synchronized (self.recordedEvents) { + event.key = [event.key cly_truncatedKey:@"Event key"]; + NSDictionary* truncated = [event.segmentation cly_truncated:@"Event segmentation"]; + NSDictionary* limited = [truncated cly_limited:@"Event segmentation"]; + event.segmentation = limited; + [self.recordedEvents addObject:event]; if (self.recordedEvents.count >= self.eventSendThreshold) @@ -373,17 +381,15 @@ - (void)saveToFileSync @synchronized (self) { +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" saveData = [NSKeyedArchiver archivedDataWithRootObject:@{kCountlyQueuedRequestsPersistencyKey: self.queuedRequests}]; +#pragma GCC diagnostic pop } -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wunused-variable" - BOOL writeResult = [saveData writeToFile:[self storageFileURL].path atomically:YES]; CLY_LOG_D(@"Result of writing data to file: %d", writeResult); -#pragma clang diagnostic pop - [CountlyCommon.sharedInstance finishBackgroundTask]; } diff --git a/src/ios/CountlyiOS/CountlyPushNotifications.h b/src/ios/CountlyiOS/CountlyPushNotifications.h index 6c28c60..07293a7 100644 --- a/src/ios/CountlyiOS/CountlyPushNotifications.h +++ b/src/ios/CountlyiOS/CountlyPushNotifications.h @@ -6,6 +6,8 @@ #import +extern NSString* const kCountlyReservedEventPushAction; + @interface CountlyPushNotifications : NSObject #ifndef COUNTLY_EXCLUDE_PUSHNOTIFICATIONS @property (nonatomic) BOOL isEnabledOnInitialConfig; diff --git a/src/ios/CountlyiOS/CountlyPushNotifications.m b/src/ios/CountlyiOS/CountlyPushNotifications.m index d3e5ab5..1e4fbbe 100644 --- a/src/ios/CountlyiOS/CountlyPushNotifications.m +++ b/src/ios/CountlyiOS/CountlyPushNotifications.m @@ -9,6 +9,10 @@ NSString* const kCountlyReservedEventPushAction = @"[CLY]_push_action"; NSString* const kCountlyTokenError = @"kCountlyTokenError"; +NSString* const kCountlyPNKeyPlatform = @"p"; +NSString* const kCountlyPNKeyiOS = @"i"; +NSString* const kCountlyPNKeymacOS = @"m"; + //NOTE: Push Notification Test Modes CLYPushTestMode const CLYPushTestModeDevelopment = @"CLYPushTestModeDevelopment"; CLYPushTestMode const CLYPushTestModeTestFlightOrAdHoc = @"CLYPushTestModeTestFlightOrAdHoc"; @@ -16,7 +20,6 @@ #if (TARGET_OS_IOS || TARGET_OS_OSX) @interface CountlyPushNotifications () @property (nonatomic) NSString* token; -@property (nonatomic, copy) void (^permissionCompletion)(BOOL granted, NSError * error); #else @interface CountlyPushNotifications () #endif @@ -28,9 +31,6 @@ @interface CountlyPushNotifications () #define CLYApplication NSApplication #endif -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - @implementation CountlyPushNotifications #ifndef COUNTLY_EXCLUDE_PUSHNOTIFICATIONS @@ -67,22 +67,16 @@ - (void)startPushNotifications if (!CountlyConsentManager.sharedInstance.consentForPushNotifications) return; - if (@available(iOS 10.0, macOS 10.14, *)) - UNUserNotificationCenter.currentNotificationCenter.delegate = self; + UNUserNotificationCenter.currentNotificationCenter.delegate = self; [self swizzlePushNotificationMethods]; -#if (TARGET_OS_IOS) - [UIApplication.sharedApplication registerForRemoteNotifications]; -#elif (TARGET_OS_OSX) - [NSApplication.sharedApplication registerForRemoteNotificationTypes:NSRemoteNotificationTypeBadge | NSRemoteNotificationTypeAlert | NSRemoteNotificationTypeSound]; + [CLYApplication.sharedApplication registerForRemoteNotifications]; - if (@available(macOS 10.14, *)) - { - UNNotificationResponse* notificationResponse = self.launchNotification.userInfo[NSApplicationLaunchUserNotificationKey]; - if (notificationResponse) - [self userNotificationCenter:UNUserNotificationCenter.currentNotificationCenter didReceiveNotificationResponse:notificationResponse withCompletionHandler:^{}]; - } +#if (TARGET_OS_OSX) + UNNotificationResponse* notificationResponse = self.launchNotification.userInfo[NSApplicationLaunchUserNotificationKey]; + if (notificationResponse) + [self userNotificationCenter:UNUserNotificationCenter.currentNotificationCenter didReceiveNotificationResponse:notificationResponse withCompletionHandler:^{}]; #endif } @@ -91,11 +85,8 @@ - (void)stopPushNotifications if (!self.isEnabledOnInitialConfig) return; - if (@available(iOS 10.0, macOS 10.14, *)) - { - if (UNUserNotificationCenter.currentNotificationCenter.delegate == self) - UNUserNotificationCenter.currentNotificationCenter.delegate = nil; - } + if (UNUserNotificationCenter.currentNotificationCenter.delegate == self) + UNUserNotificationCenter.currentNotificationCenter.delegate = nil; [CLYApplication.sharedApplication unregisterForRemoteNotifications]; } @@ -113,12 +104,6 @@ - (void)swizzlePushNotificationMethods @[ @"application:didRegisterForRemoteNotificationsWithDeviceToken:", @"application:didFailToRegisterForRemoteNotificationsWithError:", -#if (TARGET_OS_IOS) - @"application:didRegisterUserNotificationSettings:", - @"application:didReceiveRemoteNotification:fetchCompletionHandler:", -#elif (TARGET_OS_OSX) - @"application:didReceiveRemoteNotification:", -#endif ]; for (NSString* selectorString in selectors) @@ -146,32 +131,16 @@ - (void)askForNotificationPermissionWithOptions:(NSUInteger)options completionHa if (!CountlyConsentManager.sharedInstance.consentForPushNotifications) return; - if (@available(iOS 10.0, macOS 10.14, *)) - { - if (options == 0) - options = UNAuthorizationOptionBadge | UNAuthorizationOptionSound | UNAuthorizationOptionAlert; + if (options == 0) + options = UNAuthorizationOptionBadge | UNAuthorizationOptionSound | UNAuthorizationOptionAlert; - [UNUserNotificationCenter.currentNotificationCenter requestAuthorizationWithOptions:options completionHandler:^(BOOL granted, NSError* error) - { - if (completionHandler) - completionHandler(granted, error); - - [self sendToken]; - }]; - } -#if (TARGET_OS_IOS) - else + [UNUserNotificationCenter.currentNotificationCenter requestAuthorizationWithOptions:options completionHandler:^(BOOL granted, NSError* error) { - self.permissionCompletion = completionHandler; - - if (options == 0) - options = UIUserNotificationTypeBadge | UIUserNotificationTypeSound | UIUserNotificationTypeAlert; + if (completionHandler) + completionHandler(granted, error); - UIUserNotificationType userNotificationTypes = (UIUserNotificationType)options; - UIUserNotificationSettings* settings = [UIUserNotificationSettings settingsForTypes:userNotificationTypes categories:nil]; - [UIApplication.sharedApplication registerUserNotificationSettings:settings]; - } -#endif + [self sendToken]; + }]; } - (void)sendToken @@ -196,32 +165,15 @@ - (void)sendToken BOOL hasNotificationPermissionBefore = [CountlyPersistency.sharedInstance retrieveNotificationPermission]; - if (@available(iOS 10.0, macOS 10.14, *)) + [UNUserNotificationCenter.currentNotificationCenter getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings* settings) { - [UNUserNotificationCenter.currentNotificationCenter getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings* settings) + BOOL hasProvisionalPermission = NO; + if (@available(iOS 12.0, *)) { - BOOL hasProvisionalPermission = NO; - if (@available(iOS 12.0, *)) - { - hasProvisionalPermission = settings.authorizationStatus == UNAuthorizationStatusProvisional; - } - - if (settings.authorizationStatus == UNAuthorizationStatusAuthorized || hasProvisionalPermission) - { - [CountlyConnectionManager.sharedInstance sendPushToken:self.token]; - [CountlyPersistency.sharedInstance storeNotificationPermission:YES]; - } - else if (hasNotificationPermissionBefore) - { - [self clearToken]; - [CountlyPersistency.sharedInstance storeNotificationPermission:NO]; - } - }]; - } -#if (TARGET_OS_IOS) - else - { - if (UIApplication.sharedApplication.currentUserNotificationSettings.types != UIUserNotificationTypeNone) + hasProvisionalPermission = settings.authorizationStatus == UNAuthorizationStatusProvisional; + } + + if (settings.authorizationStatus == UNAuthorizationStatusAuthorized || hasProvisionalPermission) { [CountlyConnectionManager.sharedInstance sendPushToken:self.token]; [CountlyPersistency.sharedInstance storeNotificationPermission:YES]; @@ -231,8 +183,7 @@ - (void)sendToken [self clearToken]; [CountlyPersistency.sharedInstance storeNotificationPermission:NO]; } - } -#endif + }]; } - (void)clearToken @@ -240,133 +191,6 @@ - (void)clearToken [CountlyConnectionManager.sharedInstance sendPushToken:@""]; } -- (void)handleNotification:(NSDictionary *)notification -{ -#if (TARGET_OS_IOS || TARGET_OS_OSX) - if (!CountlyConsentManager.sharedInstance.consentForPushNotifications) - return; - - CLY_LOG_D(@"Handling remote notification %@", notification); - - NSDictionary* countlyPayload = notification[kCountlyPNKeyCountlyPayload]; - NSString* notificationID = countlyPayload[kCountlyPNKeyNotificationID]; - - if (!notificationID) - { - CLY_LOG_D(@"Countly payload not found in notification dictionary!"); - return; - } - - CLY_LOG_D(@"Countly Push Notification ID: %@", notificationID); -#endif - -#if (TARGET_OS_OSX) - //NOTE: For macOS targets, just record action event. - [self recordActionEvent:notificationID buttonIndex:0]; -#endif - -#if (TARGET_OS_IOS) - if (self.doNotShowAlertForNotifications) - { - CLY_LOG_D(@"doNotShowAlertForNotifications flag is set!"); - return; - } - - if (@available(iOS 10.0, *)) - { - //NOTE: On iOS10+ when a silent notification (content-available: 1) with `alert` key arrives, do not show alert here, as it is shown in UN framework delegate method - CLY_LOG_W(@"A silent notification (content-available: 1) with `alert` key on iOS10+."); - return; - } - - id alert = notification[@"aps"][@"alert"]; - NSString* message = nil; - NSString* title = nil; - - if ([alert isKindOfClass:NSDictionary.class]) - { - message = alert[@"body"]; - title = alert[@"title"]; - } - else - { - message = (NSString*)alert; - title = [NSBundle.mainBundle objectForInfoDictionaryKey:@"CFBundleDisplayName"]; - } - - if (!message && !title) - { - CLY_LOG_W(@"Title and Message are both not found in notification dictionary!"); - return; - } - - - __block UIAlertController* alertController = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert]; - - - CLYButton* defaultButton = nil; - NSString* defaultURL = countlyPayload[kCountlyPNKeyDefaultURL]; - if (defaultURL) - { - defaultButton = [CLYButton buttonWithType:UIButtonTypeCustom]; - defaultButton.frame = alertController.view.bounds; - defaultButton.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; - defaultButton.onClick = ^(id sender) - { - [self recordActionEvent:notificationID buttonIndex:0]; - - [self openURL:defaultURL]; - - [alertController dismissViewControllerAnimated:YES completion:^ - { - alertController = nil; - }]; - }; - [alertController.view addSubview:defaultButton]; - } - - - CLYButton* dismissButton = [CLYButton dismissAlertButton]; - dismissButton.onClick = ^(id sender) - { - [self recordActionEvent:notificationID buttonIndex:0]; - - [alertController dismissViewControllerAnimated:YES completion:^ - { - alertController = nil; - }]; - }; - [alertController.view addSubview:dismissButton]; - [dismissButton positionToTopRight]; - - NSArray* buttons = countlyPayload[kCountlyPNKeyButtons]; - [buttons enumerateObjectsUsingBlock:^(NSDictionary* button, NSUInteger idx, BOOL * stop) - { - //NOTE: Add space to force buttons to be laid out vertically - NSString* actionTitle = [button[kCountlyPNKeyActionButtonTitle] stringByAppendingString:@" "]; - NSString* URL = button[kCountlyPNKeyActionButtonURL]; - - UIAlertAction* visit = [UIAlertAction actionWithTitle:actionTitle style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) - { - [self recordActionEvent:notificationID buttonIndex:idx + 1]; - - [self openURL:URL]; - - alertController = nil; - }]; - - [alertController addAction:visit]; - }]; - - [CountlyCommon.sharedInstance tryPresentingViewController:alertController]; - - const CGFloat kCountlyActionButtonHeight = 44.0; - CGRect tempFrame = defaultButton.frame; - tempFrame.size.height -= buttons.count * kCountlyActionButtonHeight; - defaultButton.frame = tempFrame; -#endif -} - - (void)openURL:(NSString *)URLString { if (!URLString) @@ -375,7 +199,7 @@ - (void)openURL:(NSString *)URLString dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^ { #if (TARGET_OS_IOS) - [UIApplication.sharedApplication openURL:[NSURL URLWithString:URLString]]; + [UIApplication.sharedApplication openURL:[NSURL URLWithString:URLString] options:@{} completionHandler:nil]; #elif (TARGET_OS_OSX) [NSWorkspace.sharedWorkspace openURL:[NSURL URLWithString:URLString]]; #endif @@ -398,10 +222,18 @@ - (void)recordActionEvent:(NSString *)notificationID buttonIndex:(NSInteger)butt if (!notificationID) return; + NSString* platform = @"unknown"; +#if (TARGET_OS_IOS) + platform = kCountlyPNKeyiOS; +#elif (TARGET_OS_OSX) + platform = kCountlyPNKeymacOS; +#endif + NSDictionary* segmentation = @{ kCountlyPNKeyNotificationID: notificationID, - kCountlyPNKeyActionButtonIndex: @(buttonIndex) + kCountlyPNKeyActionButtonIndex: @(buttonIndex), + kCountlyPNKeyPlatform: platform, }; [Countly.sharedInstance recordReservedEvent:kCountlyReservedEventPushAction segmentation:segmentation]; @@ -420,7 +252,21 @@ - (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNot NSString* notificationID = countlyPayload[kCountlyPNKeyNotificationID]; if (notificationID) - completionHandler(UNNotificationPresentationOptionAlert); + { + UNNotificationPresentationOptions presentationOption = UNNotificationPresentationOptionNone; + if (@available(iOS 14.0, tvOS 14.0, macOS 11.0, watchOS 7.0, *)) + { + presentationOption = UNNotificationPresentationOptionList | UNNotificationPresentationOptionBanner; + } + else + { +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + presentationOption = UNNotificationPresentationOptionAlert; +#pragma GCC diagnostic pop + } + completionHandler(presentationOption); + } } id appDelegate = (id)CLYApplication.sharedApplication.delegate; @@ -474,7 +320,7 @@ - (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNoti - (void)userNotificationCenter:(UNUserNotificationCenter *)center openSettingsForNotification:(UNNotification *)notification API_AVAILABLE(ios(12.0), macos(10.14)) { - if (@available(iOS 12.0, macOS 10.14, *)) + if (@available(iOS 12.0, *)) { id appDelegate = (id)CLYApplication.sharedApplication.delegate; @@ -487,15 +333,6 @@ - (void)userNotificationCenter:(UNUserNotificationCenter *)center openSettingsFo - (void)application:(CLYApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken{} - (void)application:(CLYApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error{} -#if (TARGET_OS_IOS) -- (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings{} -- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler -{ - completionHandler(UIBackgroundFetchResultNewData); -} -#elif (TARGET_OS_OSX) -- (void)application:(NSApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo{} -#endif #endif @end @@ -530,40 +367,5 @@ - (void)Countly_application:(CLYApplication *)application didFailToRegisterForRe } #endif -#if (TARGET_OS_IOS) -- (void)Countly_application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings -{ - CLY_LOG_D(@"App didRegisterUserNotificationSettings: %@", notificationSettings); - - [CountlyPushNotifications.sharedInstance sendToken]; - - BOOL granted = UIApplication.sharedApplication.currentUserNotificationSettings.types != UIUserNotificationTypeNone; - - if (CountlyPushNotifications.sharedInstance.permissionCompletion) - CountlyPushNotifications.sharedInstance.permissionCompletion(granted, nil); - - [self Countly_application:application didRegisterUserNotificationSettings:notificationSettings]; -} - -- (void)Countly_application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler; -{ - CLY_LOG_D(@"App didReceiveRemoteNotification:fetchCompletionHandler"); - - [CountlyPushNotifications.sharedInstance handleNotification:userInfo]; - - [self Countly_application:application didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler]; -} - -#elif (TARGET_OS_OSX) -- (void)Countly_application:(NSApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo -{ - CLY_LOG_D(@"App didReceiveRemoteNotification:"); - - [CountlyPushNotifications.sharedInstance handleNotification:userInfo]; - - [self Countly_application:application didReceiveRemoteNotification:userInfo]; -} -#endif #endif @end -#pragma GCC diagnostic pop diff --git a/src/ios/CountlyiOS/CountlyUserDetails.m b/src/ios/CountlyiOS/CountlyUserDetails.m index fe6d042..3ad0dbd 100644 --- a/src/ios/CountlyiOS/CountlyUserDetails.m +++ b/src/ios/CountlyiOS/CountlyUserDetails.m @@ -55,21 +55,44 @@ - (NSString *)serializedUserDetails { NSMutableDictionary* userDictionary = NSMutableDictionary.new; if (self.name) - userDictionary[kCountlyUDKeyName] = self.name; + userDictionary[kCountlyUDKeyName] = + ![self.name isKindOfClass:NSString.class] ? self.name : + [(NSString *)self.name cly_truncatedValue:@"User details name"]; + if (self.username) - userDictionary[kCountlyUDKeyUsername] = self.username; + userDictionary[kCountlyUDKeyUsername] = + ![self.username isKindOfClass:NSString.class] ? self.username : + [(NSString *)self.username cly_truncatedValue:@"User details username"]; + if (self.email) - userDictionary[kCountlyUDKeyEmail] = self.email; + userDictionary[kCountlyUDKeyEmail] = + ![self.email isKindOfClass:NSString.class] ? self.email : + [(NSString *)self.email cly_truncatedValue:@"User details email"]; + if (self.organization) - userDictionary[kCountlyUDKeyOrganization] = self.organization; + userDictionary[kCountlyUDKeyOrganization] = + ![self.organization isKindOfClass:NSString.class] ? self.organization : + [(NSString *)self.organization cly_truncatedValue:@"User details organization"]; + if (self.phone) - userDictionary[kCountlyUDKeyPhone] = self.phone; + userDictionary[kCountlyUDKeyPhone] = + ![self.phone isKindOfClass:NSString.class] ? self.phone : + [(NSString *)self.phone cly_truncatedValue:@"User details phone"]; + if (self.gender) - userDictionary[kCountlyUDKeyGender] = self.gender; + userDictionary[kCountlyUDKeyGender] = + ![self.gender isKindOfClass:NSString.class] ? self.gender : + [(NSString *)self.gender cly_truncatedValue:@"User details gender"]; + if (self.pictureURL) userDictionary[kCountlyUDKeyPicture] = self.pictureURL; + if (self.birthYear) userDictionary[kCountlyUDKeyBirthyear] = self.birthYear; + + if ([self.custom isKindOfClass:NSDictionary.class]) + self.custom = [((NSDictionary *)self.custom) cly_truncated:@"User details custom dictionary"]; + if (self.custom) userDictionary[kCountlyUDKeyCustom] = self.custom; @@ -99,86 +122,133 @@ - (void)clearUserDetails - (void)set:(NSString *)key value:(NSString *)value { + CLY_LOG_I(@"%s %@ %@", __FUNCTION__, key, value); + self.modifications[key] = value.copy; } - (void)set:(NSString *)key numberValue:(NSNumber *)value { + CLY_LOG_I(@"%s %@ %@", __FUNCTION__, key, value); + self.modifications[key] = value.copy; } - (void)set:(NSString *)key boolValue:(BOOL)value { + CLY_LOG_I(@"%s %@ %d", __FUNCTION__, key, value); + self.modifications[key] = @(value); } - (void)setOnce:(NSString *)key value:(NSString *)value { + CLY_LOG_I(@"%s %@ %@", __FUNCTION__, key, value); + if (!value) + { + CLY_LOG_W(@"%s call will be ignored as value is nil!", __FUNCTION__); return; + } self.modifications[key] = @{kCountlyUDKeyModifierSetOnce: value.copy}; } - (void)setOnce:(NSString *)key numberValue:(NSNumber *)value { + CLY_LOG_I(@"%s %@ %@", __FUNCTION__, key, value); + if (!value) + { + CLY_LOG_W(@"%s call will be ignored as value is nil!", __FUNCTION__); return; + } self.modifications[key] = @{kCountlyUDKeyModifierSetOnce: value.copy}; } - (void)setOnce:(NSString *)key boolValue:(BOOL)value; { + CLY_LOG_I(@"%s %@ %d", __FUNCTION__, key, value); + self.modifications[key] = @{kCountlyUDKeyModifierSetOnce: @(value)}; } - (void)unSet:(NSString *)key { + CLY_LOG_I(@"%s %@", __FUNCTION__, key); + self.modifications[key] = NSNull.null; } - (void)increment:(NSString *)key { + CLY_LOG_I(@"%s %@", __FUNCTION__, key); + [self incrementBy:key value:@1]; } - (void)incrementBy:(NSString *)key value:(NSNumber *)value { + CLY_LOG_I(@"%s %@ %@", __FUNCTION__, key, value); + if (!value) + { + CLY_LOG_W(@"%s call will be ignored as value is nil!", __FUNCTION__); return; + } self.modifications[key] = @{kCountlyUDKeyModifierIncrement: value}; } - (void)multiply:(NSString *)key value:(NSNumber *)value { + CLY_LOG_I(@"%s %@ %@", __FUNCTION__, key, value); + if (!value) + { + CLY_LOG_W(@"%s call will be ignored as value is nil!", __FUNCTION__); return; + } self.modifications[key] = @{kCountlyUDKeyModifierMultiply: value}; } - (void)max:(NSString *)key value:(NSNumber *)value { + CLY_LOG_I(@"%s %@ %@", __FUNCTION__, key, value); + if (!value) + { + CLY_LOG_W(@"%s call will be ignored as value is nil!", __FUNCTION__); return; + } self.modifications[key] = @{kCountlyUDKeyModifierMax: value}; } - (void)min:(NSString *)key value:(NSNumber *)value { + CLY_LOG_I(@"%s %@ %@", __FUNCTION__, key, value); + if (!value) + { + CLY_LOG_W(@"%s call will be ignored as value is nil!", __FUNCTION__); return; + } self.modifications[key] = @{kCountlyUDKeyModifierMin: value}; } - (void)push:(NSString *)key value:(NSString *)value { + CLY_LOG_I(@"%s %@ %@", __FUNCTION__, key, value); + if (!value) + { + CLY_LOG_W(@"%s call will be ignored as value is nil!", __FUNCTION__); return; + } self.modifications[key] = @{kCountlyUDKeyModifierPush: value.copy}; } @@ -186,84 +256,130 @@ - (void)push:(NSString *)key value:(NSString *)value - (void)push:(NSString *)key numberValue:(NSNumber *)value; { if (!value) + { + CLY_LOG_W(@"%s call will be ignored as value is nil!", __FUNCTION__); return; + } self.modifications[key] = @{kCountlyUDKeyModifierPush: value.copy}; } - (void)push:(NSString *)key boolValue:(BOOL)value { + CLY_LOG_I(@"%s %@ %d", __FUNCTION__, key, value); + self.modifications[key] = @{kCountlyUDKeyModifierPush: @(value)}; } - (void)push:(NSString *)key values:(NSArray *)value { + CLY_LOG_I(@"%s %@ %@", __FUNCTION__, key, value); + if (!value) + { + CLY_LOG_W(@"%s call will be ignored as value is nil!", __FUNCTION__); return; + } self.modifications[key] = @{kCountlyUDKeyModifierPush: value.copy}; } - (void)pushUnique:(NSString *)key value:(NSString *)value { + CLY_LOG_I(@"%s %@ %@", __FUNCTION__, key, value); + if (!value) + { + CLY_LOG_W(@"%s call will be ignored as value is nil!", __FUNCTION__); return; + } self.modifications[key] = @{kCountlyUDKeyModifierAddToSet: value.copy}; } - (void)pushUnique:(NSString *)key numberValue:(NSNumber *)value { + CLY_LOG_I(@"%s %@ %@", __FUNCTION__, key, value); + if (!value) + { + CLY_LOG_W(@"%s call will be ignored as value is nil!", __FUNCTION__); return; + } self.modifications[key] = @{kCountlyUDKeyModifierAddToSet: value.copy}; } - (void)pushUnique:(NSString *)key boolValue:(BOOL)value { + CLY_LOG_I(@"%s %@ %d", __FUNCTION__, key, value); + self.modifications[key] = @{kCountlyUDKeyModifierAddToSet: @(value)}; } - (void)pushUnique:(NSString *)key values:(NSArray *)value { + CLY_LOG_I(@"%s %@ %@", __FUNCTION__, key, value); + if (!value) + { + CLY_LOG_W(@"%s call will be ignored as value is nil!", __FUNCTION__); return; + } self.modifications[key] = @{kCountlyUDKeyModifierAddToSet: value.copy}; } - (void)pull:(NSString *)key value:(NSString *)value { + CLY_LOG_I(@"%s %@ %@", __FUNCTION__, key, value); + if (!value) + { + CLY_LOG_W(@"%s call will be ignored as value is nil!", __FUNCTION__); return; + } self.modifications[key] = @{kCountlyUDKeyModifierPull: value.copy}; } - (void)pull:(NSString *)key numberValue:(NSNumber *)value { + CLY_LOG_I(@"%s %@ %@", __FUNCTION__, key, value); + if (!value) + { + CLY_LOG_W(@"%s call will be ignored as value is nil!", __FUNCTION__); return; + } self.modifications[key] = @{kCountlyUDKeyModifierPull: value.copy}; } - (void)pull:(NSString *)key boolValue:(BOOL)value { + CLY_LOG_I(@"%s %@ %d", __FUNCTION__, key, value); + self.modifications[key] = @{kCountlyUDKeyModifierPull: @(value)}; } - (void)pull:(NSString *)key values:(NSArray *)value { + CLY_LOG_I(@"%s %@ %@", __FUNCTION__, key, value); + if (!value) + { + CLY_LOG_W(@"%s call will be ignored as value is nil!", __FUNCTION__); return; + } self.modifications[key] = @{kCountlyUDKeyModifierPull: value.copy}; } - (void)save { + CLY_LOG_I(@"%s", __FUNCTION__); + if (!CountlyCommon.sharedInstance.hasStarted) return; diff --git a/src/ios/CountlyiOS/CountlyViewTracking.h b/src/ios/CountlyiOS/CountlyViewTracking.h index 84671c4..76a6a9e 100644 --- a/src/ios/CountlyiOS/CountlyViewTracking.h +++ b/src/ios/CountlyiOS/CountlyViewTracking.h @@ -6,6 +6,8 @@ #import +extern NSString* const kCountlyReservedEventView; + @interface CountlyViewTracking : NSObject @property (nonatomic) BOOL isEnabledOnInitialConfig; diff --git a/src/ios/CountlyiOS/CountlyViewTracking.m b/src/ios/CountlyiOS/CountlyViewTracking.m index af21388..2e3120b 100644 --- a/src/ios/CountlyiOS/CountlyViewTracking.m +++ b/src/ios/CountlyiOS/CountlyViewTracking.m @@ -116,6 +116,8 @@ - (void)startView:(NSString *)viewName customSegmentation:(NSDictionary *)custom CLY_LOG_D(@"View tracking started: %@", viewName); + viewName = [viewName cly_truncatedKey:@"View name"]; + NSMutableDictionary* segmentation = NSMutableDictionary.new; segmentation[kCountlyVTKeyName] = viewName; segmentation[kCountlyVTKeySegment] = CountlyDeviceInfo.osName; diff --git a/src/ios/CountlyiOS/LICENSE.md b/src/ios/CountlyiOS/LICENSE.md new file mode 100644 index 0000000..950c9b1 --- /dev/null +++ b/src/ios/CountlyiOS/LICENSE.md @@ -0,0 +1,19 @@ +Copyright (c) 2012, 2020 Countly + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/ios/CountlyiOS/README.md b/src/ios/CountlyiOS/README.md new file mode 100644 index 0000000..02a7fff --- /dev/null +++ b/src/ios/CountlyiOS/README.md @@ -0,0 +1,59 @@ +# Countly iOS SDK + +[![Platform](https://img.shields.io/cocoapods/p/Countly.svg?style=flat)](https://support.count.ly/hc/en-us/articles/360037753511-iOS-watchOS-tvOS-macOS#supported-system-versions) +[![GitHub license](https://img.shields.io/badge/license-MIT-lightgrey.svg)](https://github.com/Countly/countly-sdk-ios/blob/master/LICENSE.md) +[![GitHub release](https://img.shields.io/github/release/Countly/countly-sdk-ios.svg)](https://github.com/Countly/countly-sdk-ios/releases) +[![CocoaPods Compatible](https://img.shields.io/cocoapods/v/Countly.svg)](https://support.count.ly/hc/en-us/articles/360037753511-iOS-watchOS-tvOS-macOS#cocoapods) +[![Carthage Compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://support.count.ly/hc/en-us/articles/360037753511-iOS-watchOS-tvOS-macOS#carthage) + +## What is Countly? +[Countly](http://count.ly) is a product analytics solution and innovation enabler that helps teams track product performance and customer journey and behavior across [mobile](https://count.ly/mobile-analytics), [web](http://count.ly/web-analytics), and [desktop](https://count.ly/desktop-analytics) applications. [Ensuring privacy by design](https://count.ly/your-data-your-rules), Countly allows you to innovate and enhance your products to provide personalized and customized customer experiences, and meet key business and revenue goals. + +Track, measure, and take action - all without leaving Countly. + +## About this SDK +This repository includes Countly iOS SDK with watchOS, tvOS & macOS support. +The minimum deployment targets are `iOS 10.0`, `watchOS 4.0`, `tvOS 10.0` , `macOS 10.14`, and it requires `Xcode 13.0+`. + +See [Countly iOS SDK documentation](https://support.count.ly/hc/en-us/articles/360037753511-iOS-watchOS-tvOS-macOS) for integration and details. + +## Sample iOS Application +We also have a useful [sample iOS application](https://github.com/Countly/countly-sample-ios) which demonstrates how to use this SDK in depth. +It includes iOS (both Objective-C and Swift), watchOS, tvOS and macOS sample projects. +Feel free to use them as a reference while you develop your application and also for easily testing your Countly Server. + +![iOS-sample-app](https://count.ly/github/countly-ios-sample-app.png) + +## Security + +Security is very important to us. If you discover any issue regarding security, please disclose the information responsibly by sending an email to security@count.ly and **not by creating a GitHub issue**. + +## Other Countly Resources +This SDK needs one of the following counterpart Countly Server editions to work: + +* [Countly Community Edition](https://github.com/Countly/countly-server) (downloadable from GitHub) +* [Countly Enterprise Edition](https://count.ly/product) + +For more information about Countly Enterprise Edition, please see [comparison of Countly editions](https://count.ly/pricing#compare-editions). + +There are also other [Countly SDK repositories](https://support.count.ly/hc/en-us/articles/360037236571-Downloading-Installing-SDKs) both official and community supported. + +## How can I help you with your efforts? +Glad you asked. We need ideas, feedbacks and constructive comments. +All your suggestions will be taken care with upmost importance. +We are on [Twitter](https://twitter.com/gocountly), [Facebook](https://www.facebook.com/Countly) and [YouTube](https://www.youtube.com/user/GoCountly) if you would like to keep up with our fast progress! + +## Badges +If you like Countly, [why not use one of our badges](https://count.ly/brand-assets) and give a link back to us, so others could know about this wonderful platform? + +Countly - Product Analytics + + Countly - Product Analytics + +Countly - Product Analytics + + Countly - Product Analytics + +## Support +Have any questions? +Visit [Countly Community Area](https://support.count.ly/hc/en-us/community/topics "Countly Community Area") or join our [Slack community](https://slack.count.ly). diff --git a/src/ios/CountlyiOS/SECURITY.md b/src/ios/CountlyiOS/SECURITY.md new file mode 100644 index 0000000..515d399 --- /dev/null +++ b/src/ios/CountlyiOS/SECURITY.md @@ -0,0 +1,3 @@ +# Security Policy + +Security is very important to us. If you discover any issue regarding security, please disclose the information responsibly by sending an email to security@count.ly and not by creating a GitHub issue. diff --git a/src/ios/CountlyiOS/countly_dsym_uploader.sh b/src/ios/CountlyiOS/countly_dsym_uploader.sh new file mode 100755 index 0000000..bb06197 --- /dev/null +++ b/src/ios/CountlyiOS/countly_dsym_uploader.sh @@ -0,0 +1,121 @@ +#!/bin/bash + +# countly_dsym_uploader.sh +# +# This code is provided under the MIT License. +# +# Please visit www.count.ly for more information. + + +# For your target, go to `Build Phases` tab and choose `New Run Script Phase` after clicking plus (+) button. +# Add these two lines: +# +# COUNTLY_DSYM_UPLOADER=$(find $SRCROOT -name "countly_dsym_uploader.sh" | head -n 1) +# sh "$COUNTLY_DSYM_UPLOADER" "https://YOUR_COUNTLY_SERVER" "YOUR_APP_KEY" +# +# or if you're using CocoaPods just add this one line: +# +# sh "$(PODS_ROOT)/Countly/countly_dsym_uploader.sh" "https://YOUR_COUNTLY_SERVER" "YOUR_APP_KEY" +# +# Notes: +# Do not forget to replace YOUR_COUNTLY_SERVER and YOUR_APP_KEY with real values. +# If your project setup and/or CI/CD flow requires a custom path for the generated dSYMs, you can specify it as third argument. + + +# Common functions +countly_log () { echo "[Countly] $1"; } + +countly_fail () { countly_log "$1"; exit 0; } + +countly_usage () +{ + countly_log "You must invoke the script as follows:" + echo " sh \"/path/to/.../countly_dsym_uploader.sh\" \"https://YOUR_COUNTLY_SERVER\" \"YOUR_APP_KEY\" [\"/path/to/.../your.dSYM\"]" +} + + +# Reading arguments +HOST="${1}"; +APPKEY="${2}"; +CUSTOM_DSYM_PATH="${3}" + + +# Pre-checks +if [[ -z $HOST ]]; then + countly_usage + countly_fail "Host not specified!" +fi + +if [[ -z $APPKEY ]]; then + countly_usage + countly_fail "App Key not specified!" +fi + +if [[ -z $CUSTOM_DSYM_PATH ]]; then + if [ ! "${DWARF_DSYM_FOLDER_PATH}" ] || [ ! "${DWARF_DSYM_FILE_NAME}" ]; then + countly_usage + countly_fail "Custom dSYM path not specified and Xcode Environment Variables are missing!" + fi + + DSYM_FOLDER_PATH=${DWARF_DSYM_FOLDER_PATH} + DSYM_FILE_NAME=${DWARF_DSYM_FILE_NAME} +else + DSYM_FOLDER_PATH=$(dirname "${CUSTOM_DSYM_PATH}") + DSYM_FILE_NAME=$(basename "${CUSTOM_DSYM_PATH}") +fi + +DSYM_PATH="${DSYM_FOLDER_PATH}/${DSYM_FILE_NAME}"; +if [[ ! -d $DSYM_PATH ]]; then + countly_fail "dSYM path ${DSYM_PATH} does not exist!" +fi + + +# Extracting Build UUIDs from DSYM using dwarfdump +BUILD_UUIDS=$(xcrun dwarfdump --uuid "${DSYM_PATH}" | awk '{print $2}' | xargs | sed 's/ /,/g') +if [ $? -eq 0 ]; then + countly_log "Extracted Build UUIDs: ${BUILD_UUIDS}" +else + countly_fail "Extracting Build UUIDs failed!" +fi + + +# Creating archive of DSYM folder using zip +DSYM_ZIP_PATH="/tmp/$(date +%s)_${DSYM_FILE_NAME}.zip" +pushd "${DSYM_FOLDER_PATH}" > /dev/null +zip -rq "${DSYM_ZIP_PATH}" "${DSYM_FILE_NAME}" +popd > /dev/null +if [ $? -eq 0 ]; then + countly_log "Created archive at $DSYM_ZIP_PATH" +else + countly_fail "Creating archive failed!" +fi + + +# Preparing for upload +ENDPOINT="/i/crash_symbols/upload_symbol" + +PLATFORM="ios" #This value is common for all iOS/iPadOS/watchOS/tvOS/macOS + +EPN=${EFFECTIVE_PLATFORM_NAME:1} +if [[ -z $EPN ]]; then +EPN="macos" +fi + +QUERY="?platform=${PLATFORM}&epn=${EPN}&app_key=${APPKEY}&build=${BUILD_UUIDS}" +URL="${HOST}${ENDPOINT}${QUERY}" +countly_log "Uploading to ${URL}" + + +# Uploading to server using curl +UPLOAD_RESULT=$(curl -s -F "symbols=@${DSYM_ZIP_PATH}" "${URL}") +if [ $? -eq 0 ] && [ "${UPLOAD_RESULT}" == "{\"result\":\"Success\"}" ]; then + countly_log "dSYM upload succesfully completed." +else + countly_fail "dSYM upload failed! ${UPLOAD_RESULT}" +fi + + +# Removing artifacts +rm "${DSYM_ZIP_PATH}" + +exit 0