diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..732ed293 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at support@leanplum.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..72bdfb93 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,18 @@ +# How to Contribute + +Here are some important resources: + + * [Leanplum Docs](http://leanplum.com/docs) The official Leanplum docs. + * Support: Get help at support@leanplum.com. + +## Submitting changes + +Please consider following guidelines before creating a PR: + +- Confirm the intention and design of your change by reaching out to your Customer Success Manager or to Leanplum Dev Team through a new Github Issue. +- Start creating your pull request from `develop` branch +- Please send a GitHub Pull Request with a clear list of what you've done (read more about [pull requests](https://help.github.com/articles/about-pull-requests/)). +- Add unit tests for the code that you have touched. +- Please follow the existing coding conventions (which are mostly standard of the default IDE). +- Make sure all of your commits are atomic (one feature per commit). +- Please adhere to the conventional changelog commit style: https://github.com/commitizen/cz-cli That allows us to easily create change logs. diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..dce3f1c9 --- /dev/null +++ b/ISSUE_TEMPLATE.md @@ -0,0 +1,18 @@ +## Expected Behavior + + +## Actual Behavior + + +## Steps to Reproduce the Problem + + 1. + 1. + 1. + +## Specifications + + - Version: + - Platform: + - Subsystem: + diff --git a/Leanplum-SDK/Classes/Constants.h b/Leanplum-SDK/Classes/Constants.h index 01c928f9..a7a025b1 100644 --- a/Leanplum-SDK/Classes/Constants.h +++ b/Leanplum-SDK/Classes/Constants.h @@ -45,7 +45,7 @@ #define IS_NOOP ((!IS_SUPPORTED_IOS_VERSION) || IS_JAILBROKEN || [LPConstantsState sharedState].isTestMode || [LPConstantsState sharedState].isInPermanentFailureState) #define RETURN_IF_NOOP if (IS_NOOP) return -#define LEANPLUM_SDK_VERSION @"2.0.4" +#define LEANPLUM_SDK_VERSION @"2.0.5" #define LEANPLUM_CLIENT @"ios" // Can upload up to 100 files or 50 MB per request. diff --git a/Leanplum-SDK/Classes/Constants.m b/Leanplum-SDK/Classes/Constants.m index fb77e8a5..69a3b5db 100644 --- a/Leanplum-SDK/Classes/Constants.m +++ b/Leanplum-SDK/Classes/Constants.m @@ -315,13 +315,17 @@ void leanplumInternalError(NSException *e) int userCodeBlocks = [[[[NSThread currentThread] threadDictionary] objectForKey:LP_USER_CODE_BLOCKS] intValue]; if (userCodeBlocks <= 0) { - [[LeanplumRequest post:LP_METHOD_LOG - params:@{ - LP_PARAM_TYPE: LP_VALUE_SDK_ERROR, - LP_PARAM_MESSAGE: [e description], - @"stackTrace": [[e callStackSymbols] description] ?: @"", - LP_PARAM_VERSION_NAME: versionName - }] send]; + @try { + [[LeanplumRequest post:LP_METHOD_LOG + params:@{ + LP_PARAM_TYPE: LP_VALUE_SDK_ERROR, + LP_PARAM_MESSAGE: [e description], + @"stackTrace": [[e callStackSymbols] description] ?: @"", + LP_PARAM_VERSION_NAME: versionName + }] send]; + } @catch (NSException *e) { + // This empty try/catch is needed to prevent crash <-> loop. + } NSLog(@"Leanplum: INTERNAL ERROR: %@\n%@", e, [e callStackSymbols]); } else { NSLog(@"Leanplum: Caught exception in callback code: %@\n%@", e, [e callStackSymbols]); diff --git a/Leanplum-SDK/Classes/LPDatabase.m b/Leanplum-SDK/Classes/LPDatabase.m index 413bedd5..a639483c 100644 --- a/Leanplum-SDK/Classes/LPDatabase.m +++ b/Leanplum-SDK/Classes/LPDatabase.m @@ -47,13 +47,13 @@ - (id)init /** * Create/Open SQLite database. */ -- (void)initSQLite +- (sqlite3 *)initSQLite { const char *sqliteFilePath = [[LPDatabase sqliteFilePath] UTF8String]; int result = sqlite3_open(sqliteFilePath, &sqlite); if (result != SQLITE_OK) { [self handleSQLiteError:@"SQLite fail to open" errorResult:result query:nil]; - return; + return nil; } retryOnCorrupt = NO; @@ -61,6 +61,7 @@ - (void)initSQLite [self runQuery:@"CREATE TABLE IF NOT EXISTS event (" "data TEXT NOT NULL" "); PRAGMA user_version = 1;"]; + return sqlite; } - (void)dealloc @@ -97,18 +98,9 @@ - (void)handleSQLiteError:(NSString *)errorName errorResult:(int)result query:(N } LPLog(LPError, @"%@: %@", errorName, reason); - // Send error log. Using willSendErrorLog to prevent infinte loop. - if (!willSendErrorLog) { - willSendErrorLog = YES; - NSException *exception = [NSException exceptionWithName:errorName - reason:reason - userInfo:nil]; - leanplumInternalError(exception); - } - - // If SQLite is corrupted create a new one. + // If SQLite is corrupted, create a new one. // Using retryOnCorrupt to prevent infinite loop. - if (result == SQLITE_CORRUPT || !retryOnCorrupt) { + if (result == SQLITE_CORRUPT && !retryOnCorrupt) { [[NSFileManager defaultManager] removeItemAtPath:[LPDatabase sqliteFilePath] error:nil]; retryOnCorrupt = YES; [self initSQLite]; @@ -122,15 +114,15 @@ - (void)handleSQLiteError:(NSString *)errorName errorResult:(int)result query:(N - (sqlite3_stmt *)sqliteStatementFromQuery:(NSString *)query bindObjects:(NSArray *)objectsToBind { - if (!query) { + // Retry creating SQLite. + if (!query || (!sqlite && [self initSQLite])) { return nil; } sqlite3_stmt *statement; int __block result = sqlite3_prepare_v2(sqlite, [query UTF8String], -1, &statement, NULL); if (result != SQLITE_OK) { - LPLog(LPError, @"Preparing '%@': %s (%d)", query, sqlite3_errmsg(sqlite), - result); + [self handleSQLiteError:@"SQLite fail to prepare" errorResult:result query:query]; return nil; } @@ -147,8 +139,8 @@ - (sqlite3_stmt *)sqliteStatementFromQuery:(NSString *)query SQLITE_TRANSIENT); if (result != SQLITE_OK) { - LPLog(LPError, @"Binding %@ to %ld: %s (%d)", obj, idx+1, sqlite3_errmsg(sqlite), - result); + NSString *message = [NSString stringWithFormat:@"SQLite fail to bind %@ to %ld", obj, idx+1]; + [self handleSQLiteError:message errorResult:result query:query]; } }]; @@ -162,18 +154,26 @@ - (void)runQuery:(NSString *)query - (void)runQuery:(NSString *)query bindObjects:(NSArray *)objectsToBind { + // Retry creating SQLite. + if (!sqlite && [self initSQLite]) { + return; + } + @synchronized (self) { - sqlite3_stmt *statement = [self sqliteStatementFromQuery:query bindObjects:objectsToBind]; - if (!statement) { - return; - } - - int result = sqlite3_step(statement); - if (result != SQLITE_DONE) { - [self handleSQLiteError:@"SQLite fail to run query" errorResult:result query:query]; + @try { + sqlite3_stmt *statement = [self sqliteStatementFromQuery:query bindObjects:objectsToBind]; + if (!statement) { + return; + } + int result = sqlite3_step(statement); + if (result != SQLITE_DONE) { + LPLog(LPError, @"SQLite fail to run query."); + } + sqlite3_finalize(statement); + } @catch (NSException *e) { + LPLog(LPError, @"SQLite operation failed."); + // TODO: Make sure to catch this when new logging is in place, } - willSendErrorLog = NO; - sqlite3_finalize(statement); } } @@ -184,6 +184,11 @@ - (NSArray *)rowsFromQuery:(NSString *)query - (NSArray *)rowsFromQuery:(NSString *)query bindObjects:(NSArray *)objectsToBind { + // Retry creating SQLite. + if (!sqlite && [self initSQLite]) { + return @[]; + } + @synchronized (self) { NSMutableArray *rows = [NSMutableArray new]; sqlite3_stmt *statement = [self sqliteStatementFromQuery:query diff --git a/Leanplum-SDK/Classes/LPMessageTemplates.m b/Leanplum-SDK/Classes/LPMessageTemplates.m index fb46eabc..8a0fba52 100644 --- a/Leanplum-SDK/Classes/LPMessageTemplates.m +++ b/Leanplum-SDK/Classes/LPMessageTemplates.m @@ -95,11 +95,11 @@ #define LPMT_DEFAULT_NO_BUTTON_TEXT @"No" #define LPMT_DEFAULT_LATER_BUTTON_TEXT @"Maybe Later" #define LPMT_DEFAULT_URL @"http://www.example.com" -#define LPMT_DEFAULT_CLOSE_URL @"http://leanplum:close" -#define LPMT_DEFAULT_OPEN_URL @"http://leanplum:loadFinished" -#define LPMT_DEFAULT_TRACK_URL @"http://leanplum:track" -#define LPMT_DEFAULT_ACTION_URL @"http://leanplum:runAction" -#define LPMT_DEFAULT_TRACK_ACTION_URL @"http://leanplum:runTrackedAction" +#define LPMT_DEFAULT_CLOSE_URL @"http://leanplum/close" +#define LPMT_DEFAULT_OPEN_URL @"http://leanplum/loadFinished" +#define LPMT_DEFAULT_TRACK_URL @"http://leanplum/track" +#define LPMT_DEFAULT_ACTION_URL @"http://leanplum/runAction" +#define LPMT_DEFAULT_TRACK_ACTION_URL @"http://leanplum/runTrackedAction" #define LPMT_DEFAULT_HAS_DISMISS_BUTTON YES #define LPMT_DEFAULT_APP_ICON @"__iOSAppIcon-PrimaryIcon.png" diff --git a/Leanplum-SDK/Classes/LPVarCache.m b/Leanplum-SDK/Classes/LPVarCache.m index 9f3daf73..0c9f83f7 100644 --- a/Leanplum-SDK/Classes/LPVarCache.m +++ b/Leanplum-SDK/Classes/LPVarCache.m @@ -511,60 +511,60 @@ + (void)applyVariableDiffs:(NSDictionary *)diffs_ maybeDownloadFiles]; } } - } - // If LeanplumLocation is linked in, setup region monitoring. - if (messages_ || regions_) { - if (!regionInitBlock) { - if ([LPConstantsState sharedState].isDevelopmentModeEnabled) { - if ([regions_ count] > 0) { - NSLog(@"Leanplum: Regions have been defined in dashboard, but the app is not built to handle them."); - NSLog(@"Leanplum: Add LeanplumLocation.framework or LeanplumBeacon.framework to Build Settings -> Link Binary With Libraries."); - NSLog(@"Leanplum: Disregard warning if there are no plans to utilize iBeacon or Geofencing within the app"); + // If LeanplumLocation is linked in, setup region monitoring. + if (messages_ || regions_) { + if (!regionInitBlock) { + if ([LPConstantsState sharedState].isDevelopmentModeEnabled) { + if ([regions_ count] > 0) { + NSLog(@"Leanplum: Regions have been defined in dashboard, but the app is not built to handle them."); + NSLog(@"Leanplum: Add LeanplumLocation.framework or LeanplumBeacon.framework to Build Settings -> Link Binary With Libraries."); + NSLog(@"Leanplum: Disregard warning if there are no plans to utilize iBeacon or Geofencing within the app"); + } } - } - } else { - NSSet *foregroundRegionNames; - NSSet *backgroundRegionNames; - [LPActionManager getForegroundRegionNames:&foregroundRegionNames - andBackgroundRegionNames:&backgroundRegionNames]; - regionInitBlock([LPVarCache regions], foregroundRegionNames, backgroundRegionNames); + } else { + NSSet *foregroundRegionNames; + NSSet *backgroundRegionNames; + [LPActionManager getForegroundRegionNames:&foregroundRegionNames + andBackgroundRegionNames:&backgroundRegionNames]; + regionInitBlock([LPVarCache regions], foregroundRegionNames, backgroundRegionNames); + } } - } - - BOOL interfaceUpdated = NO; - if (updateRules_) { - interfaceUpdated = ![updateRules_ isEqual:updateRulesDiffs]; - updateRulesDiffs = [updateRules_ mutableCopy]; - [self downloadUpdateRulesImages]; - } - - BOOL eventsUpdated = NO; - if (eventRules_ && ![eventRules_ isKindOfClass:NSNull.class]) { - eventsUpdated = ![eventRules_ isEqual:eventRulesDiffs]; - eventRulesDiffs = eventRules_; - } - - if (variants_) { - variants = variants_; - } - - contentVersion++; - if (!silent) { - [self saveDiffs]; - - hasReceivedDiffs = YES; - if (updateBlock) { - updateBlock(); + BOOL interfaceUpdated = NO; + if (updateRules_) { + interfaceUpdated = ![updateRules_ isEqual:updateRulesDiffs]; + updateRulesDiffs = [updateRules_ mutableCopy]; + [self downloadUpdateRulesImages]; } - if (interfaceUpdated) { - interfaceUpdateBlock(); + BOOL eventsUpdated = NO; + if (eventRules_ && ![eventRules_ isKindOfClass:NSNull.class]) { + eventsUpdated = ![eventRules_ isEqual:eventRulesDiffs]; + eventRulesDiffs = eventRules_; + } + + if (variants_) { + variants = variants_; } - if (eventsUpdated) { - eventsUpdateBlock(); + contentVersion++; + + if (!silent) { + [self saveDiffs]; + + hasReceivedDiffs = YES; + if (updateBlock) { + updateBlock(); + } + + if (interfaceUpdated) { + interfaceUpdateBlock(); + } + + if (eventsUpdated) { + eventsUpdateBlock(); + } } } } diff --git a/Leanplum-SDK/Classes/Leanplum.h b/Leanplum-SDK/Classes/Leanplum.h index 322df6dc..a1a5b5a1 100644 --- a/Leanplum-SDK/Classes/Leanplum.h +++ b/Leanplum-SDK/Classes/Leanplum.h @@ -1,6 +1,6 @@ // // Leanplum.h -// Leanplum iOS SDK Version 2.0.4 +// Leanplum iOS SDK Version 2.0.5 // // Copyright (c) 2012 Leanplum, Inc. All rights reserved. // diff --git a/Leanplum-SDK/Classes/Leanplum.m b/Leanplum-SDK/Classes/Leanplum.m index c40c9e1c..2e8a8496 100644 --- a/Leanplum-SDK/Classes/Leanplum.m +++ b/Leanplum-SDK/Classes/Leanplum.m @@ -692,7 +692,9 @@ + (void)reset [state.noDownloadsBlocks removeAllObjects]; [state.onceNoDownloadsBlocks removeAllObjects]; [state.noDownloadsResponders removeAllObjects]; - [state.userAttributeChanges removeAllObjects]; + @synchronized([LPInternalState sharedState].userAttributeChanges) { + [state.userAttributeChanges removeAllObjects]; + } state.calledHandleNotification = NO; [[LPInbox sharedState] reset]; @@ -775,7 +777,9 @@ + (void)startWithUserId:(NSString *)userId [LPMessageTemplatesClass sharedTemplates]; attributes = [self validateAttributes:attributes named:@"userAttributes" allowLists:YES]; if (attributes != nil) { - [state.userAttributeChanges addObject:attributes]; + @synchronized([LPInternalState sharedState].userAttributeChanges) { + [state.userAttributeChanges addObject:attributes]; + } } state.calledStart = YES; dispatch_async(dispatch_get_main_queue(), ^{ @@ -2017,12 +2021,14 @@ + (void)setUserId:(NSString *)userId withUserAttributes:(NSDictionary *)attribut [self throwError:@"You cannot call setUserId before calling start"]; return; } + LP_END_USER_CODE // Catch when setUser is called in start response. LP_TRY attributes = [self validateAttributes:attributes named:@"userAttributes" allowLists:YES]; [self onStartIssued:^{ [self setUserIdInternal:userId withAttributes:attributes]; }]; LP_END_TRY + LP_BEGIN_USER_CODE } + (void)setUserIdInternal:(NSString *)userId withAttributes:(NSDictionary *)attributes @@ -2052,7 +2058,9 @@ + (void)setUserIdInternal:(NSString *)userId withAttributes:(NSDictionary *)attr } if (attributes != nil) { - [[LPInternalState sharedState].userAttributeChanges addObject:attributes]; + @synchronized([LPInternalState sharedState].userAttributeChanges) { + [[LPInternalState sharedState].userAttributeChanges addObject:attributes]; + } } [Leanplum onStartResponse:^(BOOL success) { @@ -2066,35 +2074,30 @@ + (void)setUserIdInternal:(NSString *)userId withAttributes:(NSDictionary *)attr // Returns if attributes have changed. + (void)recordAttributeChanges { - BOOL madeChanges = NO; - // Making a copy. Other threads can add attributes while iterating. - NSMutableArray *attributeChanges = [[LPInternalState sharedState].userAttributeChanges copy]; - // Keep track of processed changes to be removed at the end. - NSMutableArray *processedChanges = [NSMutableArray new]; - for (NSDictionary *attributes in attributeChanges) { + @synchronized([LPInternalState sharedState].userAttributeChanges){ + BOOL __block madeChanges = NO; NSMutableDictionary *existingAttributes = [LPVarCache userAttributes]; - [processedChanges addObject:attributes]; - for (NSString *attributeName in [attributes allKeys]) { - id existingValue = existingAttributes[attributeName]; - id value = attributes[attributeName]; - if (![value isEqual:existingValue]) { - LPContextualValues *contextualValues = [[LPContextualValues alloc] init]; - contextualValues.previousAttributeValue = existingValue; - contextualValues.attributeValue = value; - existingAttributes[attributeName] = value; - [Leanplum maybePerformActions:@[@"userAttribute"] - withEventName:attributeName - withFilter:kLeanplumActionFilterAll - fromMessageId:nil - withContextualValues:contextualValues]; - madeChanges = YES; - } + for (NSDictionary *attributes in [LPInternalState sharedState].userAttributeChanges) { + [attributes enumerateKeysAndObjectsUsingBlock:^(id attributeName, id value, BOOL *stop) { + id existingValue = existingAttributes[attributeName]; + if (![value isEqual:existingValue]) { + LPContextualValues *contextualValues = [LPContextualValues new]; + contextualValues.previousAttributeValue = existingValue; + contextualValues.attributeValue = value; + existingAttributes[attributeName] = value; + [Leanplum maybePerformActions:@[@"userAttribute"] + withEventName:attributeName + withFilter:kLeanplumActionFilterAll + fromMessageId:nil + withContextualValues:contextualValues]; + madeChanges = YES; + } + }]; + } + [[LPInternalState sharedState].userAttributeChanges removeAllObjects]; + if (madeChanges) { + [LPVarCache saveUserAttributes]; } - } - // Remove only processed changes. - [[LPInternalState sharedState].userAttributeChanges removeObjectsInArray:processedChanges]; - if (madeChanges) { - [LPVarCache saveUserAttributes]; } } diff --git a/Leanplum-SDK/Classes/LeanplumRequest.m b/Leanplum-SDK/Classes/LeanplumRequest.m index c5003882..cf0488ef 100644 --- a/Leanplum-SDK/Classes/LeanplumRequest.m +++ b/Leanplum-SDK/Classes/LeanplumRequest.m @@ -360,7 +360,6 @@ - (void)sendRequests:(BOOL)async } LP_END_TRY - [LPEventCallbackManager invokeSuccessCallbacksOnResponses:json requests:requestsToSend operation:operation]; @@ -425,10 +424,9 @@ - (void)sendRequests:(BOOL)async LP_TRY NSLog(@"Leanplum: Request %@ timed out", _apiMethod); [op cancel]; - if (_error != nil) { - _error([NSError errorWithDomain:@"Leanplum" code:1 - userInfo:@{NSLocalizedDescriptionKey: @"Request timed out"}]); - } + NSError *error = [NSError errorWithDomain:@"Leanplum" code:1 + userInfo:@{NSLocalizedDescriptionKey: @"Request timed out"}]; + [LPEventCallbackManager invokeErrorCallbacksWithError:error]; [[LeanplumRequest sendNowQueue] cancelAllOperations]; LP_END_TRY } diff --git a/Leanplum-iOS-SDK-source.podspec b/Leanplum-iOS-SDK-source.podspec index f8c2f19a..fdb7154c 100644 --- a/Leanplum-iOS-SDK-source.podspec +++ b/Leanplum-iOS-SDK-source.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.name = 'Leanplum-iOS-SDK-source' - s.version = '2.0.4' + s.version = '2.0.5' s.summary = 'Mobile Marketing Platform. Integrated. ROI Engine.' s.description = <<-DESC Leanplum’s integrated solution delivers meaningful engagement across messaging and the in-app diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..6f778769 --- /dev/null +++ b/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,8 @@ +Fixes # + +## Proposed Changes + + - + - + - +