From 07f97614e933285aed9e5b241ff102bf7b86d712 Mon Sep 17 00:00:00 2001 From: Brad Hesse Date: Mon, 16 Apr 2018 13:46:01 -0700 Subject: [PATCH 1/7] Privacy Consent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Adds the capability to require user privacy consent before the SDK can be initialized --- .../OneSignalDevApp/AppDelegate.m | 3 +- .../Base.lproj/Main.storyboard | 30 +++ .../OneSignalDevApp/Info.plist | 2 + .../OneSignalDevApp/ViewController.m | 8 + .../OneSignal.xcodeproj/project.pbxproj | 10 + .../Source/DelayedInitializationParameters.h | 41 ++++ .../Source/DelayedInitializationParameters.m | 41 ++++ iOS_SDK/OneSignalSDK/Source/OneSignal.h | 4 + iOS_SDK/OneSignalSDK/Source/OneSignal.m | 202 +++++++++++++++++- .../Source/OneSignalCommonDefines.h | 4 + 10 files changed, 335 insertions(+), 10 deletions(-) create mode 100644 iOS_SDK/OneSignalSDK/Source/DelayedInitializationParameters.h create mode 100644 iOS_SDK/OneSignalSDK/Source/DelayedInitializationParameters.m diff --git a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/AppDelegate.m b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/AppDelegate.m index 45b0570fc..101ad0792 100644 --- a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/AppDelegate.m +++ b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/AppDelegate.m @@ -41,9 +41,8 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( [FIRApp configure]; NSLog(@"Bundle URL: %@", [[NSBundle mainBundle] bundleURL]); - NSLog(@"[[NSUUID alloc] initWithUUIDString:nil]: %@", [[NSUUID alloc] initWithUUIDString:nil]); - [OneSignal setLogLevel:ONE_S_LL_VERBOSE visualLevel:ONE_S_LL_WARN]; + [OneSignal setLogLevel:ONE_S_LL_VERBOSE visualLevel:ONE_S_LL_ERROR]; OneSignal.inFocusDisplayType = OSNotificationDisplayTypeInAppAlert; diff --git a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/Base.lproj/Main.storyboard b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/Base.lproj/Main.storyboard index a7c9f0421..f8652c39b 100644 --- a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/Base.lproj/Main.storyboard +++ b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/Base.lproj/Main.storyboard @@ -6,6 +6,7 @@ + @@ -71,24 +72,53 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/Info.plist b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/Info.plist index 7e2d08241..03a5960c3 100644 --- a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/Info.plist +++ b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/Info.plist @@ -2,6 +2,8 @@ + Onesignal_require_privacy_consent + CFBundleDevelopmentRegion en CFBundleExecutable diff --git a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/ViewController.m b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/ViewController.m index 684ebd334..7744fc6ef 100644 --- a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/ViewController.m +++ b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/ViewController.m @@ -35,6 +35,7 @@ @interface ViewController () @property (weak, nonatomic) IBOutlet UITextField *textField; @property (weak, nonatomic) IBOutlet UIActivityIndicatorView *activityIndicatorView; +@property (weak, nonatomic) IBOutlet UISegmentedControl *consentSegmentedControl; @end @@ -45,6 +46,8 @@ - (void)viewDidLoad { // Do any additional setup after loading the view, typically from a nib. self.activityIndicatorView.hidden = true; + + self.consentSegmentedControl.selectedSegmentIndex = (NSInteger)![OneSignal requiresUserPrivacyConsent]; } - (void)changeAnimationState:(BOOL)animating { @@ -121,5 +124,10 @@ - (void)didReceiveMemoryWarning { // Dispose of any resources that can be recreated. } +- (IBAction)consentSegmentedControlValueChanged:(UISegmentedControl *)sender { + NSLog(@"View controller consent granted: %i", (int)sender.selectedSegmentIndex); + [OneSignal consentGranted:(bool)sender.selectedSegmentIndex]; +} + @end diff --git a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj index c0f310453..bd3d7ef81 100644 --- a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj +++ b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj @@ -153,6 +153,9 @@ CAABF34B205B15780042F8E5 /* OneSignalExtensionBadgeHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = CAABF34A205B15780042F8E5 /* OneSignalExtensionBadgeHandler.m */; }; CAABF34C205B157B0042F8E5 /* OneSignalExtensionBadgeHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = CAABF34A205B15780042F8E5 /* OneSignalExtensionBadgeHandler.m */; }; CAABF34D205B157B0042F8E5 /* OneSignalExtensionBadgeHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = CAABF34A205B15780042F8E5 /* OneSignalExtensionBadgeHandler.m */; }; + CAB4112920852E48005A70D1 /* DelayedInitializationParameters.m in Sources */ = {isa = PBXBuildFile; fileRef = CAB4112820852E48005A70D1 /* DelayedInitializationParameters.m */; }; + CAB4112A20852E4C005A70D1 /* DelayedInitializationParameters.m in Sources */ = {isa = PBXBuildFile; fileRef = CAB4112820852E48005A70D1 /* DelayedInitializationParameters.m */; }; + CAB4112B20852E4C005A70D1 /* DelayedInitializationParameters.m in Sources */ = {isa = PBXBuildFile; fileRef = CAB4112820852E48005A70D1 /* DelayedInitializationParameters.m */; }; CAEA1C66202BB3C600FBFE9E /* OSEmailSubscription.h in Headers */ = {isa = PBXBuildFile; fileRef = CA810FCF202BA97300A60FED /* OSEmailSubscription.h */; }; /* End PBXBuildFile section */ @@ -292,6 +295,8 @@ CAA4ED0020646762005BD59B /* BadgeTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BadgeTests.m; sourceTree = ""; }; CAABF349205B15780042F8E5 /* OneSignalExtensionBadgeHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OneSignalExtensionBadgeHandler.h; sourceTree = ""; }; CAABF34A205B15780042F8E5 /* OneSignalExtensionBadgeHandler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OneSignalExtensionBadgeHandler.m; sourceTree = ""; }; + CAB4112720852E48005A70D1 /* DelayedInitializationParameters.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DelayedInitializationParameters.h; sourceTree = ""; }; + CAB4112820852E48005A70D1 /* DelayedInitializationParameters.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DelayedInitializationParameters.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -443,6 +448,8 @@ 91C7725D1E7CCE1000D612D0 /* OneSignalInternal.h */, 912411F01E73342200E41FD7 /* OneSignal.h */, 912411F11E73342200E41FD7 /* OneSignal.m */, + CAB4112720852E48005A70D1 /* DelayedInitializationParameters.h */, + CAB4112820852E48005A70D1 /* DelayedInitializationParameters.m */, CA70E3332023D51000019273 /* OneSignalSetEmailParameters.h */, CA70E3342023D51000019273 /* OneSignalSetEmailParameters.m */, 912411F41E73342200E41FD7 /* OneSignalHelper.h */, @@ -731,6 +738,7 @@ 9124123A1E73342200E41FD7 /* OneSignalWebView.m in Sources */, 9124123E1E73342200E41FD7 /* UIApplicationDelegate+OneSignal.m in Sources */, 912412261E73342200E41FD7 /* OneSignalMobileProvision.m in Sources */, + CAB4112920852E48005A70D1 /* DelayedInitializationParameters.m in Sources */, 454F94F21FAD218000D74CCF /* OneSignalNotificationServiceExtensionHandler.m in Sources */, 912412321E73342200E41FD7 /* OneSignalTracker.m in Sources */, CA70E3352023D51000019273 /* OneSignalSetEmailParameters.m in Sources */, @@ -769,6 +777,7 @@ 0338566B1FBBD2270002F7C1 /* OSNotificationPayload.m in Sources */, 912412431E73342200E41FD7 /* UNUserNotificationCenter+OneSignal.m in Sources */, 9124123B1E73342200E41FD7 /* OneSignalWebView.m in Sources */, + CAB4112A20852E4C005A70D1 /* DelayedInitializationParameters.m in Sources */, 9124123F1E73342200E41FD7 /* UIApplicationDelegate+OneSignal.m in Sources */, 0338566C1FBBDB150002F7C1 /* OneSignalNotificationServiceExtensionHandler.m in Sources */, CA70E3362023D51300019273 /* OneSignalSetEmailParameters.m in Sources */, @@ -809,6 +818,7 @@ 912412491E73369800E41FD7 /* OneSignalHelper.m in Sources */, 4529DEE41FA82C6200CEAB1D /* NSURLSessionOverrider.m in Sources */, 4529DED21FA81EA800CEAB1D /* NSObjectOverrider.m in Sources */, + CAB4112B20852E4C005A70D1 /* DelayedInitializationParameters.m in Sources */, 912412341E73342200E41FD7 /* OneSignalTracker.m in Sources */, 912412101E73342200E41FD7 /* OneSignal.m in Sources */, 9124122C1E73342200E41FD7 /* OneSignalReachability.m in Sources */, diff --git a/iOS_SDK/OneSignalSDK/Source/DelayedInitializationParameters.h b/iOS_SDK/OneSignalSDK/Source/DelayedInitializationParameters.h new file mode 100644 index 000000000..17db8b43f --- /dev/null +++ b/iOS_SDK/OneSignalSDK/Source/DelayedInitializationParameters.h @@ -0,0 +1,41 @@ +/** + * Modified MIT License + * + * Copyright 2017 OneSignal + * + * 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: + * + * 1. The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * 2. All copies of substantial portions of the Software may only be used in connection + * with services provided by OneSignal. + * + * 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. + */ + +#import +#import "OneSignal.h" + +@interface DelayedInitializationParameters : NSObject + +-(instancetype)initWithLaunchOptions:(NSDictionary *)launchOptions withAppId:(NSString *)appId withHandleNotificationReceivedBlock:(OSHandleNotificationReceivedBlock)received withHandleNotificationActionBlock:(OSHandleNotificationActionBlock)action withSettings:(NSDictionary *)settings; + +@property (strong, nonatomic) NSDictionary *launchOptions; +@property (strong, nonatomic) NSString *appId; +@property (strong, nonatomic) NSDictionary *settings; +@property (nonatomic) OSHandleNotificationReceivedBlock receivedBlock; +@property (nonatomic) OSHandleNotificationActionBlock actionBlock; + +@end diff --git a/iOS_SDK/OneSignalSDK/Source/DelayedInitializationParameters.m b/iOS_SDK/OneSignalSDK/Source/DelayedInitializationParameters.m new file mode 100644 index 000000000..7b117e8ef --- /dev/null +++ b/iOS_SDK/OneSignalSDK/Source/DelayedInitializationParameters.m @@ -0,0 +1,41 @@ +/** + * Modified MIT License + * + * Copyright 2017 OneSignal + * + * 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: + * + * 1. The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * 2. All copies of substantial portions of the Software may only be used in connection + * with services provided by OneSignal. + * + * 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. + */ + +#import "DelayedInitializationParameters.h" + +@implementation DelayedInitializationParameters + +-(instancetype)initWithLaunchOptions:(NSDictionary *)launchOptions withAppId:(NSString *)appId withHandleNotificationReceivedBlock:(OSHandleNotificationReceivedBlock)received withHandleNotificationActionBlock:(OSHandleNotificationActionBlock)action withSettings:(NSDictionary *)settings { + self.launchOptions = launchOptions; + self.appId = appId; + self.receivedBlock = received; + self.actionBlock = action; + self.settings = settings; + return self; +} + +@end diff --git a/iOS_SDK/OneSignalSDK/Source/OneSignal.h b/iOS_SDK/OneSignalSDK/Source/OneSignal.h index 4ce7aac90..7dc81698e 100755 --- a/iOS_SDK/OneSignalSDK/Source/OneSignal.h +++ b/iOS_SDK/OneSignalSDK/Source/OneSignal.h @@ -340,6 +340,10 @@ typedef NS_ENUM(NSUInteger, ONE_S_LOG_LEVEL) { + (id)initWithLaunchOptions:(NSDictionary*)launchOptions appId:(NSString*)appId handleNotificationAction:(OSHandleNotificationActionBlock)actionCallback settings:(NSDictionary*)settings; + (id)initWithLaunchOptions:(NSDictionary*)launchOptions appId:(NSString*)appId handleNotificationReceived:(OSHandleNotificationReceivedBlock)receivedCallback handleNotificationAction:(OSHandleNotificationActionBlock)actionCallback settings:(NSDictionary*)settings; +// - Privacy ++ (void)consentGranted:(BOOL)granted; ++ (BOOL)requiresUserPrivacyConsent; // tells your application if privacy consent is still needed from the current user + @property (class) OSNotificationDisplayType inFocusDisplayType; + (NSString*)app_id; diff --git a/iOS_SDK/OneSignalSDK/Source/OneSignal.m b/iOS_SDK/OneSignalSDK/Source/OneSignal.m index ceb676a92..b265a3570 100755 --- a/iOS_SDK/OneSignalSDK/Source/OneSignal.m +++ b/iOS_SDK/OneSignalSDK/Source/OneSignal.m @@ -67,6 +67,7 @@ #import "OneSignalSetEmailParameters.h" #import "OneSignalCommonDefines.h" +#import "DelayedInitializationParameters.h" #define NOTIFICATION_TYPE_NONE 0 #define NOTIFICATION_TYPE_BADGE 1 @@ -144,7 +145,7 @@ @implementation OneSignal this property stores the parameters so that once registration is complete we can finish setEmail: */ -static OneSignalSetEmailParameters *delayedParameters; +static OneSignalSetEmailParameters *delayedEmailParameters; static NSMutableArray* pendingSendTagCallbacks; static OSResultSuccessBlock pendingGetTagsSuccessBlock; @@ -165,6 +166,9 @@ @implementation OneSignal static BOOL promptBeforeOpeningPushURLs = false; +static BOOL delayedInitializationForPrivacyConsent = false; +DelayedInitializationParameters *delayedInitParameters; + static OneSignalTrackIAP* trackIAPPurchase; static NSString* app_id; NSString* emailToSet; @@ -388,6 +392,13 @@ + (id)initWithLaunchOptions:(NSDictionary*)launchOptions appId:(NSString*)appId // Ensure a 2nd call can be made later with the appId from the developer's code. + (id)initWithLaunchOptions:(NSDictionary*)launchOptions appId:(NSString*)appId handleNotificationReceived:(OSHandleNotificationReceivedBlock)receivedCallback handleNotificationAction:(OSHandleNotificationActionBlock)actionCallback settings:(NSDictionary*)settings { + if ([self requiresUserPrivacyConsent]) { + delayedInitializationForPrivacyConsent = true; + delayedInitParameters = [[DelayedInitializationParameters alloc] initWithLaunchOptions:launchOptions withAppId:appId withHandleNotificationReceivedBlock:receivedCallback withHandleNotificationActionBlock:actionCallback withSettings:settings]; + [self onesignal_Log:ONE_S_LL_VERBOSE message:@"Delayed initialization of the OneSignal SDK until the user provides privacy consent using the consentGranted() method"]; + return self; + } + let userDefaults = [NSUserDefaults standardUserDefaults]; let success = [self initAppId:appId @@ -540,6 +551,53 @@ +(bool)initAppId:(NSString*)appId withUserDefaults:(NSUserDefaults*)userDefaults return true; } ++ (BOOL)shouldLogMissingPrivacyConsentErrorWithMethodName:(NSString *)methodName { + if (delayedInitializationForPrivacyConsent) { + [self onesignal_Log:ONE_S_LL_WARN message:[NSString stringWithFormat:@"Your application has called %@ before the user granted privacy permission. Please call `consentGranted(bool)` in order to provide user privacy consent", methodName]]; + return false; + } + + return false; +} + ++ (BOOL)requiresUserPrivacyConsent { + + // if the plist key does not exist default to true + // the plist value specifies whether GDPR privacy consent is required for this app + // if required and consent has not been previously granted, return false + + let requiresConsent = [[[NSBundle mainBundle] objectForInfoDictionaryKey:ONESIGNAL_REQUIRE_PRIVACY_CONSENT] boolValue] ?: false; + + if (requiresConsent) { + let userDefaults = [NSUserDefaults standardUserDefaults]; + + let consentGranted = (NSNumber *)[userDefaults objectForKey:GDPR_CONSENT_GRANTED]; + + if (consentGranted == nil) { + [userDefaults setObject:@false forKey:GDPR_CONSENT_GRANTED]; + [userDefaults synchronize]; + } + + return ![[userDefaults objectForKey:GDPR_CONSENT_GRANTED] boolValue]; + } + + return false; +} + ++ (void)consentGranted:(BOOL)granted { + let userDefaults = [NSUserDefaults standardUserDefaults]; + + [userDefaults setObject:@(granted) forKey:GDPR_CONSENT_GRANTED]; + [userDefaults synchronize]; + + if (!granted || !delayedInitializationForPrivacyConsent || delayedInitParameters == nil) + return; + + [self initWithLaunchOptions:delayedInitParameters.launchOptions appId:delayedInitParameters.appId handleNotificationReceived:delayedInitParameters.receivedBlock handleNotificationAction:delayedInitParameters.actionBlock settings:delayedInitParameters.settings]; + delayedInitializationForPrivacyConsent = false; + delayedInitParameters = nil; +} + // the iOS SDK used to call these selectors as a convenience but has stopped due to concerns about private API usage // the SDK will now print warnings when a developer's app implements these selectors + (void)checkIfApplicationImplementsDeprecatedMethods { @@ -557,9 +615,9 @@ +(void)downloadIOSParams { self.currentEmailSubscriptionState.requiresEmailAuth = [result[@"require_email_auth"] boolValue]; // checks if a cell to setEmail: was delayed due to missing 'requiresEmailAuth' parameter - if (delayedParameters && self.currentSubscriptionState.userId) { - [self setEmail:delayedParameters.email withEmailAuthHashToken:delayedParameters.authToken withSuccess:delayedParameters.successBlock withFailure:delayedParameters.failureBlock]; - delayedParameters = nil; + if (delayedEmailParameters && self.currentSubscriptionState.userId) { + [self setEmail:delayedEmailParameters.email withEmailAuthHashToken:delayedEmailParameters.authToken withSuccess:delayedEmailParameters.successBlock withFailure:delayedEmailParameters.failureBlock]; + delayedEmailParameters = nil; } } @@ -649,6 +707,11 @@ + (BOOL)registerForAPNsToken { } + (void)promptForPushNotificationsWithUserResponse:(void(^)(BOOL accepted))completionHandler { + + // return if the user has not granted privacy permissions + if ([self shouldLogMissingPrivacyConsentErrorWithMethodName:@"promptForPushNotificationsWithUserResponse:"]) + return; + [OneSignal onesignal_Log:ONE_S_LL_VERBOSE message:[NSString stringWithFormat:@"registerForPushNotifications Called:waitingForApnsResponse: %d", waitingForApnsResponse]]; self.currentPermissionState.hasPrompted = true; @@ -659,6 +722,11 @@ + (void)promptForPushNotificationsWithUserResponse:(void(^)(BOOL accepted))compl // This registers for a push token and prompts the user for notifiations permisions // Will trigger didRegisterForRemoteNotificationsWithDeviceToken on the AppDelegate when APNs responses. + (void)registerForPushNotifications { + + // return if the user has not granted privacy permissions + if ([self shouldLogMissingPrivacyConsentErrorWithMethodName:@"registerForPushNotifications:"]) + return; + [self promptForPushNotificationsWithUserResponse:nil]; } @@ -711,6 +779,11 @@ + (void)removeEmailSubscriptionObserver:(NSObject*) // Block not assigned if userID nil and there is a device token + (void)IdsAvailable:(OSIdsAvailableBlock)idsAvailableBlock { + + // return if the user has not granted privacy permissions + if ([self shouldLogMissingPrivacyConsentErrorWithMethodName:@"IdsAvailable:"]) + return; + idsAvailableBlockWhenReady = idsAvailableBlock; [self fireIdsAvailableCallback]; } @@ -733,6 +806,11 @@ + (void) fireIdsAvailableCallback { } + (void)sendTagsWithJsonString:(NSString*)jsonString { + + // return if the user has not granted privacy permissions + if ([self shouldLogMissingPrivacyConsentErrorWithMethodName:@"sendTagsWithJsonString:"]) + return; + NSError* jsonError; NSData* data = [jsonString dataUsingEncoding:NSUTF8StringEncoding]; @@ -746,10 +824,19 @@ + (void)sendTagsWithJsonString:(NSString*)jsonString { } + (void)sendTags:(NSDictionary*)keyValuePair { + + // return if the user has not granted privacy permissions + if ([self shouldLogMissingPrivacyConsentErrorWithMethodName:@"sendTags:"]) + return; + [self sendTags:keyValuePair onSuccess:nil onFailure:nil]; } + (void)sendTags:(NSDictionary*)keyValuePair onSuccess:(OSResultSuccessBlock)successBlock onFailure:(OSFailureBlock)failureBlock { + + // return if the user has not granted privacy permissions + if ([self shouldLogMissingPrivacyConsentErrorWithMethodName:@"sendTags:onSuccess:onFailure:"]) + return; if (![NSJSONSerialization isValidJSONObject:keyValuePair]) { onesignal_Log(ONE_S_LL_WARN, [NSString stringWithFormat:@"sendTags JSON Invalid: The following key/value pairs you attempted to send as tags are not valid JSON: %@", keyValuePair]); @@ -828,14 +915,29 @@ + (void) sendTagsToServer { } + (void)sendTag:(NSString*)key value:(NSString*)value { + + // return if the user has not granted privacy permissions + if ([self shouldLogMissingPrivacyConsentErrorWithMethodName:@"sendTag:value:"]) + return; + [self sendTag:key value:value onSuccess:nil onFailure:nil]; } + (void)sendTag:(NSString*)key value:(NSString*)value onSuccess:(OSResultSuccessBlock)successBlock onFailure:(OSFailureBlock)failureBlock { + + // return if the user has not granted privacy permissions + if ([self shouldLogMissingPrivacyConsentErrorWithMethodName:@"sendTag:value:onSuccess:onFailure:"]) + return; + [self sendTags:[NSDictionary dictionaryWithObjectsAndKeys: value, key, nil] onSuccess:successBlock onFailure:failureBlock]; } + (void)getTags:(OSResultSuccessBlock)successBlock onFailure:(OSFailureBlock)failureBlock { + + // return if the user has not granted privacy permissions + if ([self shouldLogMissingPrivacyConsentErrorWithMethodName:@"getTags:onFailure:"]) + return; + if (!self.currentSubscriptionState.userId) { pendingGetTagsSuccessBlock = successBlock; pendingGetTagsFailureBlock = failureBlock; @@ -849,23 +951,48 @@ + (void)getTags:(OSResultSuccessBlock)successBlock onFailure:(OSFailureBlock)fai } + (void)getTags:(OSResultSuccessBlock)successBlock { + + // return if the user has not granted privacy permissions + if ([self shouldLogMissingPrivacyConsentErrorWithMethodName:@"getTags:"]) + return; + [self getTags:successBlock onFailure:nil]; } + (void)deleteTag:(NSString*)key onSuccess:(OSResultSuccessBlock)successBlock onFailure:(OSFailureBlock)failureBlock { + + // return if the user has not granted privacy permissions + if ([self shouldLogMissingPrivacyConsentErrorWithMethodName:@"deleteTag:onSuccess:onFailure:"]) + return; + [self deleteTags:@[key] onSuccess:successBlock onFailure:failureBlock]; } + (void)deleteTag:(NSString*)key { + + // return if the user has not granted privacy permissions + if ([self shouldLogMissingPrivacyConsentErrorWithMethodName:@"deleteTag:"]) + return; + [self deleteTags:@[key] onSuccess:nil onFailure:nil]; } + (void)deleteTags:(NSArray*)keys { + + // return if the user has not granted privacy permissions + if ([self shouldLogMissingPrivacyConsentErrorWithMethodName:@"deleteTags:"]) + return; + [self deleteTags:keys onSuccess:nil onFailure:nil]; } + (void)deleteTagsWithJsonString:(NSString*)jsonString { + + // return if the user has not granted privacy permissions + if ([self shouldLogMissingPrivacyConsentErrorWithMethodName:@"deleteTagsWithJsonString:"]) + return; + NSError* jsonError; NSData* data = [jsonString dataUsingEncoding:NSUTF8StringEncoding]; @@ -895,10 +1022,20 @@ + (void)deleteTags:(NSArray*)keys onSuccess:(OSResultSuccessBlock)successBlock o + (void)postNotification:(NSDictionary*)jsonData { + + // return if the user has not granted privacy permissions + if ([self shouldLogMissingPrivacyConsentErrorWithMethodName:@"postNotification:"]) + return; + [self postNotification:jsonData onSuccess:nil onFailure:nil]; } + (void)postNotification:(NSDictionary*)jsonData onSuccess:(OSResultSuccessBlock)successBlock onFailure:(OSFailureBlock)failureBlock { + + // return if the user has not granted privacy permissions + if ([self shouldLogMissingPrivacyConsentErrorWithMethodName:@"postNotification:onSuccess:onFailure:"]) + return; + NSMutableDictionary *json = [jsonData mutableCopy]; [OneSignal convertDatesToISO8061Strings:json]; //convert any dates to NSString's @@ -923,6 +1060,11 @@ + (void)postNotification:(NSDictionary*)jsonData onSuccess:(OSResultSuccessBlock } + (void)postNotificationWithJsonString:(NSString*)jsonString onSuccess:(OSResultSuccessBlock)successBlock onFailure:(OSFailureBlock)failureBlock { + + // return if the user has not granted privacy permissions + if ([self shouldLogMissingPrivacyConsentErrorWithMethodName:@"postNotificationWithJsonString:onSuccess:onFailure:"]) + return; + NSError* jsonError; NSData* data = [jsonString dataUsingEncoding:NSUTF8StringEncoding]; @@ -974,6 +1116,11 @@ + (void)enableInAppLaunchURL:(NSNumber*)enable { } + (void)setSubscription:(BOOL)enable { + + // return if the user has not granted privacy permissions + if ([self shouldLogMissingPrivacyConsentErrorWithMethodName:@"setSubscription:"]) + return; + NSString* value = nil; if (!enable) value = @"no"; @@ -995,6 +1142,11 @@ + (void)setLocationShared:(BOOL)enable { } + (void) promptLocation { + + // return if the user has not granted privacy permissions + if ([self shouldLogMissingPrivacyConsentErrorWithMethodName:@"promptLocation"]) + return; + [OneSignalLocation getLocation:true]; } @@ -1273,10 +1425,10 @@ + (void)registerUserInternal { if (results.count > 0 && results[@"push"][@"id"]) { self.currentSubscriptionState.userId = results[@"push"][@"id"]; - if (delayedParameters) { + if (delayedEmailParameters) { //a call to setEmail: was delayed because the push player_id did not exist yet - [self setEmail:delayedParameters.email withEmailAuthHashToken:delayedParameters.authToken withSuccess:delayedParameters.successBlock withFailure:delayedParameters.failureBlock]; - delayedParameters = nil; + [self setEmail:delayedEmailParameters.email withEmailAuthHashToken:delayedEmailParameters.authToken withSuccess:delayedEmailParameters.successBlock withFailure:delayedEmailParameters.failureBlock]; + delayedEmailParameters = nil; } [[NSUserDefaults standardUserDefaults] setObject:self.currentSubscriptionState.userId forKey:USERID]; @@ -1713,6 +1865,11 @@ + (void)processLocalActionBasedNotification:(UILocalNotification*) notification } + (void)syncHashedEmail:(NSString *)email { + + // return if the user has not granted privacy permissions + if ([self shouldLogMissingPrivacyConsentErrorWithMethodName:@"syncHashedEmail:"]) + return; + if (!email) { [self onesignal_Log:ONE_S_LL_WARN message:@"OneSignal syncHashedEmail: The provided email is nil"]; return; @@ -1769,6 +1926,10 @@ + (void)callSuccessBlockOnMainThread:(OSEmailSuccessBlock)successBlock { + (void)setEmail:(NSString * _Nonnull)email withEmailAuthHashToken:(NSString * _Nullable)hashToken withSuccess:(OSEmailSuccessBlock _Nullable)successBlock withFailure:(OSEmailFailureBlock _Nullable)failureBlock { + // return if the user has not granted privacy permissions + if ([self shouldLogMissingPrivacyConsentErrorWithMethodName:@"setEmail:withEmailAuthHashToken:withSuccess:withFailure:"]) + return; + //some clients/wrappers may send NSNull instead of nil as the auth token NSString *emailAuthToken = hashToken; if (hashToken == (id)[NSNull null]) @@ -1804,7 +1965,7 @@ + (void)setEmail:(NSString * _Nonnull)email withEmailAuthHashToken:(NSString * _ if (!self.currentSubscriptionState.userId || (downloadedParameters == false && emailAuthToken != nil)) { [self onesignal_Log:ONE_S_LL_VERBOSE message:@"iOS Parameters for this application has not yet been downloaded. Delaying call to setEmail: until the parameters have been downloaded."]; - delayedParameters = [OneSignalSetEmailParameters withEmail:email withAuthToken:emailAuthToken withSuccess:successBlock withFailure:failureBlock]; + delayedEmailParameters = [OneSignalSetEmailParameters withEmail:email withAuthToken:emailAuthToken withSuccess:successBlock withFailure:failureBlock]; return; } @@ -1846,10 +2007,20 @@ + (void)setEmail:(NSString * _Nonnull)email withEmailAuthHashToken:(NSString * _ } + (void)setEmail:(NSString * _Nonnull)email withSuccess:(OSEmailSuccessBlock _Nullable)successBlock withFailure:(OSEmailFailureBlock _Nullable)failureBlock { + + // return if the user has not granted privacy permissions + if ([self shouldLogMissingPrivacyConsentErrorWithMethodName:@"setEmail:withSuccess:withFailure:"]) + return; + [self setEmail:email withEmailAuthHashToken:nil withSuccess:successBlock withFailure:failureBlock]; } + (void)logoutEmailWithSuccess:(OSEmailSuccessBlock _Nullable)successBlock withFailure:(OSEmailFailureBlock _Nullable)failureBlock { + + // return if the user has not granted privacy permissions + if ([self shouldLogMissingPrivacyConsentErrorWithMethodName:@"logoutEmailWithSuccess:withFailure:"]) + return; + if (!self.currentEmailSubscriptionState.emailUserId) { [OneSignal onesignal_Log:ONE_S_LL_ERROR message:@"Email Player ID does not exist, cannot logout"]; @@ -1877,14 +2048,29 @@ + (void)logoutEmailWithSuccess:(OSEmailSuccessBlock _Nullable)successBlock withF } + (void)setEmail:(NSString * _Nonnull)email { + + // return if the user has not granted privacy permissions + if ([self shouldLogMissingPrivacyConsentErrorWithMethodName:@"setEmail:"]) + return; + [self setEmail:email withSuccess:nil withFailure:nil]; } + (void)logoutEmail { + + // return if the user has not granted privacy permissions + if ([self shouldLogMissingPrivacyConsentErrorWithMethodName:@"logoutEmail"]) + return; + [self logoutEmailWithSuccess:nil withFailure:nil]; } + (void)setEmail:(NSString * _Nonnull)email withEmailAuthHashToken:(NSString * _Nullable)hashToken { + + // return if the user has not granted privacy permissions + if ([self shouldLogMissingPrivacyConsentErrorWithMethodName:@"setEmail:withEmailAuthHashToken:"]) + return; + [self setEmail:email withEmailAuthHashToken:hashToken withSuccess:nil withFailure:nil]; } diff --git a/iOS_SDK/OneSignalSDK/Source/OneSignalCommonDefines.h b/iOS_SDK/OneSignalSDK/Source/OneSignalCommonDefines.h index bbc73c3cd..c2bed18fb 100644 --- a/iOS_SDK/OneSignalSDK/Source/OneSignalCommonDefines.h +++ b/iOS_SDK/OneSignalSDK/Source/OneSignalCommonDefines.h @@ -47,6 +47,10 @@ #define PROMPT_BEFORE_OPENING_PUSH_URL @"PROMPT_BEFORE_OPENING_PUSH_URL" #define DEPRECATED_SELECTORS @[@"application:didReceiveLocalNotification:", @"application:handleActionWithIdentifier:forLocalNotification:completionHandler:", @"application:handleActionWithIdentifier:forLocalNotification:withResponseInfo:completionHandler:"] +// GDPR Privacy Consent +#define GDPR_CONSENT_GRANTED @"GDPR_CONSENT_GRANTED" +#define ONESIGNAL_REQUIRE_PRIVACY_CONSENT @"Onesignal_require_privacy_consent" + // Badge handling #define ONESIGNAL_DISABLE_BADGE_CLEARING @"OneSignal_disable_badge_clearing" #define ONESIGNAL_APP_GROUP_NAME_KEY @"OneSignal_app_groups_key" From c7f7f299552aafc0e68973ab0fe82d4d558178d6 Mon Sep 17 00:00:00 2001 From: Brad Hesse Date: Mon, 16 Apr 2018 14:21:21 -0700 Subject: [PATCH 2/7] Revoke Consent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Made it so that when the user revokes consent after granting it, the SDK will immediately stop sending any information to the server • Added checks in some internal methods to make sure personal data is never sent when consent is revoked or not provided --- iOS_SDK/OneSignalSDK/Source/OneSignal.m | 51 ++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/Source/OneSignal.m b/iOS_SDK/OneSignalSDK/Source/OneSignal.m index b265a3570..117f9ad01 100755 --- a/iOS_SDK/OneSignalSDK/Source/OneSignal.m +++ b/iOS_SDK/OneSignalSDK/Source/OneSignal.m @@ -552,8 +552,10 @@ +(bool)initAppId:(NSString*)appId withUserDefaults:(NSUserDefaults*)userDefaults } + (BOOL)shouldLogMissingPrivacyConsentErrorWithMethodName:(NSString *)methodName { - if (delayedInitializationForPrivacyConsent) { - [self onesignal_Log:ONE_S_LL_WARN message:[NSString stringWithFormat:@"Your application has called %@ before the user granted privacy permission. Please call `consentGranted(bool)` in order to provide user privacy consent", methodName]]; + if ([self requiresUserPrivacyConsent]) { + if (methodName) { + [self onesignal_Log:ONE_S_LL_WARN message:[NSString stringWithFormat:@"Your application has called %@ before the user granted privacy permission. Please call `consentGranted(bool)` in order to provide user privacy consent", methodName]]; + } return false; } @@ -789,6 +791,11 @@ + (void)IdsAvailable:(OSIdsAvailableBlock)idsAvailableBlock { } + (void) fireIdsAvailableCallback { + + // return if the user has not granted privacy permissions + if ([self shouldLogMissingPrivacyConsentErrorWithMethodName:nil]) + return; + if (!idsAvailableBlockWhenReady) return; if (!self.currentSubscriptionState.userId) @@ -874,6 +881,11 @@ + (void)sendTags:(NSDictionary*)keyValuePair onSuccess:(OSResultSuccessBlock)suc // Called only with a delay to batch network calls. + (void) sendTagsToServer { + + // return if the user has not granted privacy permissions + if ([self shouldLogMissingPrivacyConsentErrorWithMethodName:nil]) + return; + if (!tagsToSend) return; @@ -1179,6 +1191,11 @@ + (void) handleDidFailRegisterForRemoteNotification:(NSError*)err { } + (void)updateDeviceToken:(NSString*)deviceToken onSuccess:(OSResultSuccessBlock)successBlock onFailure:(OSFailureBlock)failureBlock { + + // return if the user has not granted privacy permissions + if ([self shouldLogMissingPrivacyConsentErrorWithMethodName:@"updateDeviceToken:onSuccess:onFailure:"]) + return; + onesignal_Log(ONE_S_LL_VERBOSE, @"updateDeviceToken:onSuccess:onFailure:"); // iOS 7 @@ -1240,6 +1257,11 @@ + (void)updateLastSessionDateTime { } +(BOOL)shouldRegisterNow { + + // return if the user has not granted privacy permissions + if ([self shouldLogMissingPrivacyConsentErrorWithMethodName:nil]) + return; + if (waitingForOneSReg) return false; @@ -1273,6 +1295,11 @@ + (dispatch_queue_t) getRegisterQueue { } + (void)registerUser { + + // return if the user has not granted privacy permissions + if ([self shouldLogMissingPrivacyConsentErrorWithMethodName:nil]) + return; + if (waitingForApnsResponse) { [self registerUserAfterDelay]; return; @@ -1287,6 +1314,11 @@ + (void)registerUser { } + (void)registerUserInternal { + + // return if the user has not granted privacy permissions + if ([self shouldLogMissingPrivacyConsentErrorWithMethodName:nil]) + return; + // Make sure we only call create or on_session once per open of the app. if (![self shouldRegisterNow]) return; @@ -1500,6 +1532,11 @@ +(NSString*)getUsableDeviceToken { // Updates the server with the new user's notification setting or subscription status changes + (BOOL) sendNotificationTypesUpdate { + + // return if the user has not granted privacy permissions + if ([self shouldLogMissingPrivacyConsentErrorWithMethodName:nil]) + return false; + // User changed notification settings for the app. if ([self getNotificationTypes] != -1 && self.currentSubscriptionState.userId && mLastNotificationTypes != [self getNotificationTypes]) { if (!self.currentSubscriptionState.pushToken) { @@ -1531,6 +1568,11 @@ + (BOOL) sendNotificationTypesUpdate { } + (void)sendPurchases:(NSArray*)purchases { + + // return if the user has not granted privacy permissions + if ([self shouldLogMissingPrivacyConsentErrorWithMethodName:nil]) + return; + if (!self.currentSubscriptionState.userId) return; @@ -1678,6 +1720,11 @@ + (void)launchWebURL:(NSString*)openUrl { } + (void)submitNotificationOpened:(NSString*)messageId { + + // return if the user has not granted privacy permissions + if ([self shouldLogMissingPrivacyConsentErrorWithMethodName:nil]) + return; + //(DUPLICATE Fix): Make sure we do not upload a notification opened twice for the same messageId //Keep track of the Id for the last message sent NSString* lastMessageId = [[NSUserDefaults standardUserDefaults] objectForKey:@"GT_LAST_MESSAGE_OPENED_"]; From 9be784e3a7b8882979db606bf8b6ad516a2c0cae Mon Sep 17 00:00:00 2001 From: Brad Hesse Date: Mon, 16 Apr 2018 14:29:26 -0700 Subject: [PATCH 3/7] Improve Consent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Adds consent checks to location data so that the SDK will not send location data if consent is revoked • Adds nullability specifiers to the new DelayedInitializationParameters class --- .../Source/DelayedInitializationParameters.h | 12 ++++++------ iOS_SDK/OneSignalSDK/Source/OneSignal.m | 2 +- iOS_SDK/OneSignalSDK/Source/OneSignalLocation.m | 13 +++++++++++++ 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/Source/DelayedInitializationParameters.h b/iOS_SDK/OneSignalSDK/Source/DelayedInitializationParameters.h index 17db8b43f..f1a28fb28 100644 --- a/iOS_SDK/OneSignalSDK/Source/DelayedInitializationParameters.h +++ b/iOS_SDK/OneSignalSDK/Source/DelayedInitializationParameters.h @@ -30,12 +30,12 @@ @interface DelayedInitializationParameters : NSObject --(instancetype)initWithLaunchOptions:(NSDictionary *)launchOptions withAppId:(NSString *)appId withHandleNotificationReceivedBlock:(OSHandleNotificationReceivedBlock)received withHandleNotificationActionBlock:(OSHandleNotificationActionBlock)action withSettings:(NSDictionary *)settings; +-(instancetype _Nonnull)initWithLaunchOptions:(NSDictionary * _Nullable)launchOptions withAppId:(NSString * _Nullable)appId withHandleNotificationReceivedBlock:(OSHandleNotificationReceivedBlock _Nullable)received withHandleNotificationActionBlock:(OSHandleNotificationActionBlock _Nullable)action withSettings:(NSDictionary * _Nullable)settings; -@property (strong, nonatomic) NSDictionary *launchOptions; -@property (strong, nonatomic) NSString *appId; -@property (strong, nonatomic) NSDictionary *settings; -@property (nonatomic) OSHandleNotificationReceivedBlock receivedBlock; -@property (nonatomic) OSHandleNotificationActionBlock actionBlock; +@property (strong, nonatomic, nullable) NSDictionary *launchOptions; +@property (strong, nonatomic, nullable) NSString *appId; +@property (strong, nonatomic, nullable) NSDictionary *settings; +@property (nonatomic, nullable) OSHandleNotificationReceivedBlock receivedBlock; +@property (nonatomic, nullable) OSHandleNotificationActionBlock actionBlock; @end diff --git a/iOS_SDK/OneSignalSDK/Source/OneSignal.m b/iOS_SDK/OneSignalSDK/Source/OneSignal.m index 117f9ad01..78f86d168 100755 --- a/iOS_SDK/OneSignalSDK/Source/OneSignal.m +++ b/iOS_SDK/OneSignalSDK/Source/OneSignal.m @@ -1260,7 +1260,7 @@ +(BOOL)shouldRegisterNow { // return if the user has not granted privacy permissions if ([self shouldLogMissingPrivacyConsentErrorWithMethodName:nil]) - return; + return false; if (waitingForOneSReg) return false; diff --git a/iOS_SDK/OneSignalSDK/Source/OneSignalLocation.m b/iOS_SDK/OneSignalSDK/Source/OneSignalLocation.m index 830a7edbb..1d8f51351 100644 --- a/iOS_SDK/OneSignalSDK/Source/OneSignalLocation.m +++ b/iOS_SDK/OneSignalSDK/Source/OneSignalLocation.m @@ -109,6 +109,10 @@ + (void) getLocation:(bool)prompt { + (void)onfocus:(BOOL)isActive { + // return if the user has not granted privacy permissions + if ([OneSignal requiresUserPrivacyConsent]) + return; + if(!locationManager || !started) return; /** @@ -207,6 +211,10 @@ + (void) internalGetLocation:(bool)prompt { - (void)locationManager:(id)manager didUpdateLocations:(NSArray *)locations { + // return if the user has not granted privacy permissions + if ([OneSignal requiresUserPrivacyConsent]) + return; + [manager performSelector:@selector(stopUpdatingLocation)]; id location = locations.lastObject; @@ -247,6 +255,11 @@ + (void)resetSendTimer { } + (void)sendLocation { + + // return if the user has not granted privacy permissions + if ([OneSignal requiresUserPrivacyConsent]) + return; + @synchronized(OneSignalLocation.mutexObjectForLastLocation) { if (!lastLocation || ![OneSignal mUserId]) return; From c3083459d813cf18b7c587c4f3cfda7e98afc324 Mon Sep 17 00:00:00 2001 From: Brad Hesse Date: Thu, 19 Apr 2018 19:15:30 -0700 Subject: [PATCH 4/7] Add Test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Adds a test to make sure the privacy consent functionality works correctly, delays initialization, and resumes correctly. --- .../UnitTests/Shadows/NSBundleOverrider.h | 1 + .../UnitTests/Shadows/NSBundleOverrider.m | 13 +++++++++- iOS_SDK/OneSignalSDK/UnitTests/UnitTests.m | 24 +++++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/iOS_SDK/OneSignalSDK/UnitTests/Shadows/NSBundleOverrider.h b/iOS_SDK/OneSignalSDK/UnitTests/Shadows/NSBundleOverrider.h index f97746069..f98349f5a 100644 --- a/iOS_SDK/OneSignalSDK/UnitTests/Shadows/NSBundleOverrider.h +++ b/iOS_SDK/OneSignalSDK/UnitTests/Shadows/NSBundleOverrider.h @@ -30,4 +30,5 @@ @interface NSBundleOverrider : NSObject +(void) setNsbundleDictionary:(NSDictionary*)value; +(NSDictionary*) nsbundleDictionary; ++ (void)setPrivacyState:(BOOL)state; @end diff --git a/iOS_SDK/OneSignalSDK/UnitTests/Shadows/NSBundleOverrider.m b/iOS_SDK/OneSignalSDK/UnitTests/Shadows/NSBundleOverrider.m index 13a3a771c..5b31347a5 100644 --- a/iOS_SDK/OneSignalSDK/UnitTests/Shadows/NSBundleOverrider.m +++ b/iOS_SDK/OneSignalSDK/UnitTests/Shadows/NSBundleOverrider.m @@ -28,11 +28,19 @@ #import "NSBundleOverrider.h" #import "OneSignalSelectorHelpers.h" +#import "OneSignalCommonDefines.h" @implementation NSBundleOverrider static NSDictionary* nsbundleDictionary; +//mimics the Onesignal_require_privacy_consent plist key +static BOOL privacyState = false; + ++ (void)setPrivacyState:(BOOL)state { + privacyState = state; +} + + (void)load { injectToProperClass(@selector(overrideBundleIdentifier), @selector(bundleIdentifier), @[], [NSBundleOverrider class], [NSBundle class]); @@ -56,7 +64,10 @@ - (NSString*)overrideBundleIdentifier { } - (nullable id)overrideObjectForInfoDictionaryKey:(NSString*)key { - return nsbundleDictionary[key]; + if (privacyState && [key isEqualToString:ONESIGNAL_REQUIRE_PRIVACY_CONSENT]) + return @true; + else + return nsbundleDictionary[key]; } - (NSURL*)overrideURLForResource:(NSString*)name withExtension:(NSString*)ext { diff --git a/iOS_SDK/OneSignalSDK/UnitTests/UnitTests.m b/iOS_SDK/OneSignalSDK/UnitTests/UnitTests.m index 4ec7b9c31..0ce2188a4 100644 --- a/iOS_SDK/OneSignalSDK/UnitTests/UnitTests.m +++ b/iOS_SDK/OneSignalSDK/UnitTests/UnitTests.m @@ -1850,4 +1850,28 @@ - (void)testHandlingMediaUrlExtensions { XCTAssertNotNil(cacheName); } +/* + Tests the privacy functionality to comply with the GDPR +*/ +- (void)testPrivacyState { + [NSBundleOverrider setPrivacyState:true]; + + [OneSignal initWithLaunchOptions:nil appId:@"b2f7f966-d8cc-11e4-bed1-df8f05be55ba" + handleNotificationAction:nil + settings:@{kOSSettingsKeyAutoPrompt: @false}]; + + //indicates initialization was delayed + XCTAssertNil(OneSignal.app_id); + + XCTAssertTrue([OneSignal requiresUserPrivacyConsent]); + + [OneSignal consentGranted:true]; + + XCTAssertTrue([@"b2f7f966-d8cc-11e4-bed1-df8f05be55ba" isEqualToString:OneSignal.app_id]); + + XCTAssertFalse([OneSignal requiresUserPrivacyConsent]); + + [NSBundleOverrider setPrivacyState:false]; +} + @end From 80997c395c706561a5977fa0c7094e786a7ee5c1 Mon Sep 17 00:00:00 2001 From: Brad Hesse Date: Mon, 23 Apr 2018 14:55:28 -0700 Subject: [PATCH 5/7] Add Consent Setting Override MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Adds a method to override the plist consent setting, intended for wrapper SDK's that cannot use a plist setting --- iOS_SDK/OneSignalSDK/Source/OneSignal.h | 1 + iOS_SDK/OneSignalSDK/Source/OneSignal.m | 19 +++++++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/Source/OneSignal.h b/iOS_SDK/OneSignalSDK/Source/OneSignal.h index 7dc81698e..c750025a0 100755 --- a/iOS_SDK/OneSignalSDK/Source/OneSignal.h +++ b/iOS_SDK/OneSignalSDK/Source/OneSignal.h @@ -343,6 +343,7 @@ typedef NS_ENUM(NSUInteger, ONE_S_LOG_LEVEL) { // - Privacy + (void)consentGranted:(BOOL)granted; + (BOOL)requiresUserPrivacyConsent; // tells your application if privacy consent is still needed from the current user ++ (void)setRequiresUserPrivacyConsent:(BOOL)required; //used by wrapper SDK's to require user privacy consent @property (class) OSNotificationDisplayType inFocusDisplayType; diff --git a/iOS_SDK/OneSignalSDK/Source/OneSignal.m b/iOS_SDK/OneSignalSDK/Source/OneSignal.m index 78f86d168..86e8e7e45 100755 --- a/iOS_SDK/OneSignalSDK/Source/OneSignal.m +++ b/iOS_SDK/OneSignalSDK/Source/OneSignal.m @@ -169,6 +169,11 @@ @implementation OneSignal static BOOL delayedInitializationForPrivacyConsent = false; DelayedInitializationParameters *delayedInitParameters; +//the iOS Native SDK will use the plist flag to enable privacy consent +//however wrapper SDK's will use a method call before initialization +//this boolean flag is switched on to enable this behavior +static BOOL shouldRequireUserConsent = false; + static OneSignalTrackIAP* trackIAPPurchase; static NSString* app_id; NSString* emailToSet; @@ -562,6 +567,10 @@ + (BOOL)shouldLogMissingPrivacyConsentErrorWithMethodName:(NSString *)methodName return false; } ++ (void)setRequiresUserPrivacyConsent:(BOOL)required { + shouldRequireUserConsent = required; +} + + (BOOL)requiresUserPrivacyConsent { // if the plist key does not exist default to true @@ -570,7 +579,7 @@ + (BOOL)requiresUserPrivacyConsent { let requiresConsent = [[[NSBundle mainBundle] objectForInfoDictionaryKey:ONESIGNAL_REQUIRE_PRIVACY_CONSENT] boolValue] ?: false; - if (requiresConsent) { + if (requiresConsent || shouldRequireUserConsent) { let userDefaults = [NSUserDefaults standardUserDefaults]; let consentGranted = (NSNumber *)[userDefaults objectForKey:GDPR_CONSENT_GRANTED]; @@ -603,9 +612,11 @@ + (void)consentGranted:(BOOL)granted { // the iOS SDK used to call these selectors as a convenience but has stopped due to concerns about private API usage // the SDK will now print warnings when a developer's app implements these selectors + (void)checkIfApplicationImplementsDeprecatedMethods { - for (NSString *selectorName in DEPRECATED_SELECTORS) - if ([[[UIApplication sharedApplication] delegate] respondsToSelector:NSSelectorFromString(selectorName)]) - [OneSignal onesignal_Log:ONE_S_LL_WARN message:[NSString stringWithFormat:@"OneSignal has detected that your application delegate implements a deprecated method (%@). Please note that this method has been officially deprecated and the OneSignal SDK will no longer call it. You should use UNUserNotificationCenter instead", selectorName]]; + dispatch_async(dispatch_get_main_queue(), ^{ + for (NSString *selectorName in DEPRECATED_SELECTORS) + if ([[[UIApplication sharedApplication] delegate] respondsToSelector:NSSelectorFromString(selectorName)]) + [OneSignal onesignal_Log:ONE_S_LL_WARN message:[NSString stringWithFormat:@"OneSignal has detected that your application delegate implements a deprecated method (%@). Please note that this method has been officially deprecated and the OneSignal SDK will no longer call it. You should use UNUserNotificationCenter instead", selectorName]]; + }); } +(void)downloadIOSParams { From 913d6533c765b60f1ad8509c434583ca00bd52fc Mon Sep 17 00:00:00 2001 From: Brad Hesse Date: Tue, 24 Apr 2018 13:43:17 -0700 Subject: [PATCH 6/7] Download Params MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Changes the SDK so that iOS params are still downloaded even if the user has not provided consent yet (to make things faster if they do eventually provide consent) • Added additional checks to ensure that the SDK will _never_ initiate an HTTP request (asides from GET requests) if the user has not provided consent • Adds test to make sure that swizzled methods (ie. didRegisterForRemoteNotifications) do not initiate HTTP requests or change state (ie. push token) if consent has not been granted • Adds a test to make sure the OneSignal setRequiresUserPrivacyConsent: override method works correctly • Adds a check to make sure handleNotificationOpened: does not execute if the user has not provided consent --- iOS_SDK/OneSignalSDK/Source/OneSignal.m | 30 +++++++--- iOS_SDK/OneSignalSDK/Source/OneSignalClient.m | 11 ++++ .../UNUserNotificationCenter+OneSignal.m | 7 ++- iOS_SDK/OneSignalSDK/UnitTests/UnitTests.m | 55 +++++++++++++++++++ 4 files changed, 93 insertions(+), 10 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/Source/OneSignal.m b/iOS_SDK/OneSignalSDK/Source/OneSignal.m index 86e8e7e45..a887fb04f 100755 --- a/iOS_SDK/OneSignalSDK/Source/OneSignal.m +++ b/iOS_SDK/OneSignalSDK/Source/OneSignal.m @@ -396,6 +396,12 @@ + (id)initWithLaunchOptions:(NSDictionary*)launchOptions appId:(NSString*)appId // NOTE: Wrapper SDKs such as Unity3D will call this method with appId set to nil so open events are not lost. // Ensure a 2nd call can be made later with the appId from the developer's code. + (id)initWithLaunchOptions:(NSDictionary*)launchOptions appId:(NSString*)appId handleNotificationReceived:(OSHandleNotificationReceivedBlock)receivedCallback handleNotificationAction:(OSHandleNotificationActionBlock)actionCallback settings:(NSDictionary*)settings { + NSLog(@"Called init with app ID: %@", appId); + + //Some wrapper SDK's call init multiple times and pass nil/NSNull as the appId on the first call + //the app ID is required to download parameters, so do not download params until the appID is provided + if (!didCallDownloadParameters && appId != nil && appId != (id)[NSNull null]) + [self downloadIOSParamsWithAppId:appId]; if ([self requiresUserPrivacyConsent]) { delayedInitializationForPrivacyConsent = true; @@ -508,11 +514,6 @@ + (id)initWithLaunchOptions:(NSDictionary*)launchOptions appId:(NSString*)appId (B) if this app requires email authentication */ - //Some wrapper SDK's call init multiple times and pass nil/NSNull as the appId on the first call - //the app ID is required to download parameters, so do not download params until the appID is provided - if (!didCallDownloadParameters && appId != nil && appId != (id)[NSNull null]) - [self downloadIOSParams]; - if ([OneSignalTrackFirebaseAnalytics needsRemoteParams]) { [OneSignalTrackFirebaseAnalytics init]; } @@ -561,7 +562,7 @@ + (BOOL)shouldLogMissingPrivacyConsentErrorWithMethodName:(NSString *)methodName if (methodName) { [self onesignal_Log:ONE_S_LL_WARN message:[NSString stringWithFormat:@"Your application has called %@ before the user granted privacy permission. Please call `consentGranted(bool)` in order to provide user privacy consent", methodName]]; } - return false; + return true; } return false; @@ -619,11 +620,11 @@ + (void)checkIfApplicationImplementsDeprecatedMethods { }); } -+(void)downloadIOSParams { ++(void)downloadIOSParamsWithAppId:(NSString *)appId { [self onesignal_Log:ONE_S_LL_DEBUG message:@"Downloading iOS parameters for this application"]; didCallDownloadParameters = true; - [OneSignalClient.sharedClient executeRequest:[OSRequestGetIosParams withUserId:self.currentSubscriptionState.userId appId:self.app_id] onSuccess:^(NSDictionary *result) { + [OneSignalClient.sharedClient executeRequest:[OSRequestGetIosParams withUserId:self.currentSubscriptionState.userId appId:appId] onSuccess:^(NSDictionary *result) { if (result[@"require_email_auth"]) { self.currentEmailSubscriptionState.requiresEmailAuth = [result[@"require_email_auth"] boolValue]; @@ -1610,6 +1611,8 @@ + (void)setLastnonActiveMessageId:(NSString*)value { _lastnonActiveMessageId = v // - 2A. iOS 9 - Notification received while app is in focus. // - 2B. iOS 10 - Notification received/displayed while app is in focus. + (void)notificationReceived:(NSDictionary*)messageDict isActive:(BOOL)isActive wasOpened:(BOOL)opened { + if ([OneSignal shouldLogMissingPrivacyConsentErrorWithMethodName:nil]) + return; if (!app_id) return; @@ -1686,6 +1689,10 @@ + (void)handleNotificationOpened:(NSDictionary*)messageDict actionType:(OSNotificationActionType)actionType displayType:(OSNotificationDisplayType)displayType { + // return if the user has not granted privacy permissions + if ([self shouldLogMissingPrivacyConsentErrorWithMethodName:@"handleNotificationOpened:isActive:actionType:displayType:"]) + return; + NSDictionary* customDict = [messageDict objectForKey:@"custom"] ?: [messageDict objectForKey:@"os_data"]; // Notify backend that user opened the notification @@ -1837,9 +1844,13 @@ + (void)updateNotificationTypes:(int)notificationTypes { } + (void)didRegisterForRemoteNotifications:(UIApplication*)app deviceToken:(NSData*)inDeviceToken { + if ([OneSignal shouldLogMissingPrivacyConsentErrorWithMethodName:nil]) + return; + let trimmedDeviceToken = [[inDeviceToken description] stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"<>"]]; let parsedDeviceToken = [[trimmedDeviceToken componentsSeparatedByString:@" "] componentsJoinedByString:@""]; + [OneSignal onesignal_Log:ONE_S_LL_INFO message: [NSString stringWithFormat:@"Device Registered with Apple: %@", parsedDeviceToken]]; waitingForApnsResponse = false; @@ -1903,6 +1914,9 @@ + (BOOL)remoteSilentNotification:(UIApplication*)application UserInfo:(NSDiction // iOS 8-9 - Entry point when OneSignal action button notification is displayed or opened. + (void)processLocalActionBasedNotification:(UILocalNotification*) notification identifier:(NSString*)identifier { + if ([OneSignal shouldLogMissingPrivacyConsentErrorWithMethodName:nil]) + return; + if (!notification.userInfo) return; diff --git a/iOS_SDK/OneSignalSDK/Source/OneSignalClient.m b/iOS_SDK/OneSignalSDK/Source/OneSignalClient.m index e6cbed2da..2910eadec 100644 --- a/iOS_SDK/OneSignalSDK/Source/OneSignalClient.m +++ b/iOS_SDK/OneSignalSDK/Source/OneSignalClient.m @@ -28,12 +28,17 @@ #import "OneSignalClient.h" #import "UIApplicationDelegate+OneSignal.h" #import "ReattemptRequest.h" +#import "OneSignal.h" #define REATTEMPT_DELAY 30.0 #define REQUEST_TIMEOUT_REQUEST 60.0 //for most HTTP requests #define REQUEST_TIMEOUT_RESOURCE 100.0 //for loading a resource like an image #define MAX_ATTEMPT_COUNT 3 +@interface OneSignal (OneSignalClientExtra) ++ (BOOL)shouldLogMissingPrivacyConsentErrorWithMethodName:(NSString *)methodName; +@end + @interface OneSignalClient () @property (strong, nonatomic) NSURLSession *sharedSession; @end @@ -100,6 +105,12 @@ - (void)executeSimultaneousRequests:(NSDictionarylast.from.userId); + XCTAssertNil(observer->last.to.userId); + XCTAssertFalse(observer->last.to.subscribed); + + [OneSignal setSubscription:true]; + [UnitTestCommonMethods runBackgroundThreads]; + + XCTAssertFalse(observer->last.from.userSubscriptionSetting); + XCTAssertFalse(observer->last.to.userSubscriptionSetting); + // Device registered with OneSignal so now make pushToken available. + XCTAssertNil(observer->last.to.pushToken); [NSBundleOverrider setPrivacyState:false]; } From 869a466f920f08dd31224c72fa8d9b3c3700a59d Mon Sep 17 00:00:00 2001 From: Brad Hesse Date: Tue, 24 Apr 2018 15:14:34 -0700 Subject: [PATCH 7/7] Plist Capitalization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Changes the privacy consent parameter name capitalization from Onesignal_require_privacy_consent to OneSignal_require_privacy_consent to be more consistent --- iOS_SDK/OneSignalDevApp/OneSignalDevApp/Info.plist | 2 +- iOS_SDK/OneSignalSDK/Source/OneSignalCommonDefines.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/Info.plist b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/Info.plist index 03a5960c3..aa7551392 100644 --- a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/Info.plist +++ b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/Info.plist @@ -2,7 +2,7 @@ - Onesignal_require_privacy_consent + OneSignal_require_privacy_consent CFBundleDevelopmentRegion en diff --git a/iOS_SDK/OneSignalSDK/Source/OneSignalCommonDefines.h b/iOS_SDK/OneSignalSDK/Source/OneSignalCommonDefines.h index c2bed18fb..9e0348246 100644 --- a/iOS_SDK/OneSignalSDK/Source/OneSignalCommonDefines.h +++ b/iOS_SDK/OneSignalSDK/Source/OneSignalCommonDefines.h @@ -49,7 +49,7 @@ // GDPR Privacy Consent #define GDPR_CONSENT_GRANTED @"GDPR_CONSENT_GRANTED" -#define ONESIGNAL_REQUIRE_PRIVACY_CONSENT @"Onesignal_require_privacy_consent" +#define ONESIGNAL_REQUIRE_PRIVACY_CONSENT @"OneSignal_require_privacy_consent" // Badge handling #define ONESIGNAL_DISABLE_BADGE_CLEARING @"OneSignal_disable_badge_clearing"