diff --git a/Apptentive/Apptentive.xcodeproj/project.pbxproj b/Apptentive/Apptentive.xcodeproj/project.pbxproj index b3c554305..ae20d2a50 100644 --- a/Apptentive/Apptentive.xcodeproj/project.pbxproj +++ b/Apptentive/Apptentive.xcodeproj/project.pbxproj @@ -78,6 +78,9 @@ 01798C021EAF94FE00633164 /* ApptentivePayloadSender.h in Headers */ = {isa = PBXBuildFile; fileRef = 01798C001EAF94FD00633164 /* ApptentivePayloadSender.h */; }; 01798C031EAF94FE00633164 /* ApptentivePayloadSender.m in Sources */ = {isa = PBXBuildFile; fileRef = 01798C011EAF94FD00633164 /* ApptentivePayloadSender.m */; }; 017E54ED1F3B860E00EA9F81 /* ApptentiveJSONSerializationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 017E54EC1F3B860E00EA9F81 /* ApptentiveJSONSerializationTests.m */; }; + 018E1CE021936B1400E58F33 /* ApptentiveEngagementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 018E1CDF21936B1300E58F33 /* ApptentiveEngagementTests.swift */; }; + 018E1CE321936B6600E58F33 /* conversation-4.archive in Resources */ = {isa = PBXBuildFile; fileRef = 018E1CE121936B6600E58F33 /* conversation-4.archive */; }; + 018E1CE421936B6600E58F33 /* conversation-5.archive in Resources */ = {isa = PBXBuildFile; fileRef = 018E1CE221936B6600E58F33 /* conversation-5.archive */; }; 018FAFDF1FC4A9C6007C52FE /* ApptentiveAndClause.h in Headers */ = {isa = PBXBuildFile; fileRef = 018FAFDD1FC4A9C6007C52FE /* ApptentiveAndClause.h */; }; 018FAFE01FC4A9C6007C52FE /* ApptentiveAndClause.m in Sources */ = {isa = PBXBuildFile; fileRef = 018FAFDE1FC4A9C6007C52FE /* ApptentiveAndClause.m */; }; 018FAFE31FC4AC41007C52FE /* ApptentiveOrClause.h in Headers */ = {isa = PBXBuildFile; fileRef = 018FAFE11FC4AC41007C52FE /* ApptentiveOrClause.h */; }; @@ -512,6 +515,9 @@ 01798C001EAF94FD00633164 /* ApptentivePayloadSender.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ApptentivePayloadSender.h; sourceTree = ""; }; 01798C011EAF94FD00633164 /* ApptentivePayloadSender.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ApptentivePayloadSender.m; sourceTree = ""; }; 017E54EC1F3B860E00EA9F81 /* ApptentiveJSONSerializationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ApptentiveJSONSerializationTests.m; sourceTree = ""; }; + 018E1CDF21936B1300E58F33 /* ApptentiveEngagementTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApptentiveEngagementTests.swift; sourceTree = ""; }; + 018E1CE121936B6600E58F33 /* conversation-4.archive */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; path = "conversation-4.archive"; sourceTree = ""; }; + 018E1CE221936B6600E58F33 /* conversation-5.archive */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; path = "conversation-5.archive"; sourceTree = ""; }; 018FAFDD1FC4A9C6007C52FE /* ApptentiveAndClause.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ApptentiveAndClause.h; sourceTree = ""; }; 018FAFDE1FC4A9C6007C52FE /* ApptentiveAndClause.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ApptentiveAndClause.m; sourceTree = ""; }; 018FAFE11FC4AC41007C52FE /* ApptentiveOrClause.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ApptentiveOrClause.h; sourceTree = ""; }; @@ -1024,6 +1030,7 @@ 01A2CF9E1E49062800C2103A /* ApptentiveTests */ = { isa = PBXGroup; children = ( + 01917D431E5E0B7400B37D82 /* ApptentiveTests-Bridging-Header.h */, 01201AD21FC637BD00EB3593 /* CodePointAndInteractionTests.m */, EF4EABA22040A8C5003318C9 /* utils */, 01A2D20F1E4946D500C2103A /* data */, @@ -1041,7 +1048,6 @@ 01A2D2071E4946D500C2103A /* ApptentiveMigrationTests.m */, 01A2D2091E4946D500C2103A /* ApptentiveStyleSheetTests.m */, 01A2D20A1E4946D500C2103A /* ApptentiveSurveyTests.m */, - 01917D431E5E0B7400B37D82 /* ApptentiveTests-Bridging-Header.h */, 01A2D20B1E4946D500C2103A /* ApptentiveUtilitiesTests.m */, 01917D441E5E0B7400B37D82 /* ConversationManagerTests.swift */, 01A2D20D1E4946D500C2103A /* CriteriaTests.h */, @@ -1053,6 +1059,7 @@ 0123005E20531698000EC3C3 /* ClauseTests.m */, EF4EAB99203F9821003318C9 /* AppptentiveAsyncLogWriterTests.swift */, 012ED92B2072FABE003D87F3 /* RetryPolicyTests.swift */, + 018E1CDF21936B1300E58F33 /* ApptentiveEngagementTests.swift */, ); path = ApptentiveTests; sourceTree = ""; @@ -1645,6 +1652,8 @@ 01A2D20F1E4946D500C2103A /* data */ = { isa = PBXGroup; children = ( + 018E1CE121936B6600E58F33 /* conversation-4.archive */, + 018E1CE221936B6600E58F33 /* conversation-5.archive */, EFF4D2A51EC37F0A00FD4EFE /* containers */, 01A2D2161E4946D500C2103A /* criteria */, 01A2D2101E4946D500C2103A /* ATDataModelv1.sqlite */, @@ -2081,6 +2090,8 @@ 01A2D2551E4946D600C2103A /* testCornerCasesThatShouldBeFalse.json in Resources */, 01A2D2531E4946D600C2103A /* testCodePointInvokesVersion.json in Resources */, 01A2D25F1E4946D600C2103A /* testOperatorGreaterThanOrEqual.json in Resources */, + 018E1CE321936B6600E58F33 /* conversation-4.archive in Resources */, + 018E1CE421936B6600E58F33 /* conversation-5.archive in Resources */, 01A2D25B1E4946D600C2103A /* testOperatorContains.json in Resources */, 01A2D24E1E4946D600C2103A /* ATDataModelv3.sqlite in Resources */, 01A2D26A1E4946D600C2103A /* testJsonDiffing.1.new.json in Resources */, @@ -2280,6 +2291,7 @@ EFF4D2AC1EC39EC000FD4EFE /* ApptentiveAppDataContainer.m in Sources */, 01A2D2481E4946D600C2103A /* ApptentiveSurveyTests.m in Sources */, 01A2D29B1E4963A500C2103A /* ATDataModel v3 to v4.xcmappingmodel in Sources */, + 018E1CE021936B1400E58F33 /* ApptentiveEngagementTests.swift in Sources */, 0174772F1EA92D7D00A0A949 /* PayloadTests.swift in Sources */, 01201AD31FC637BE00EB3593 /* CodePointAndInteractionTests.m in Sources */, 01A2D2451E4946D600C2103A /* ApptentiveMigrationTests.m in Sources */, @@ -2382,7 +2394,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 20; + CURRENT_PROJECT_VERSION = 23; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -2440,7 +2452,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 20; + CURRENT_PROJECT_VERSION = 23; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -2472,7 +2484,7 @@ DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 86WML2UN43; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 20; + DYLIB_CURRENT_VERSION = 23; DYLIB_INSTALL_NAME_BASE = "@rpath"; GCC_PREFIX_HEADER = "Apptentive/Misc/ApptentiveConnect-Prefix.pch"; GCC_PREPROCESSOR_DEFINITIONS = "APPTENTIVE_DEBUG=1"; @@ -2494,7 +2506,7 @@ DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 86WML2UN43; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 20; + DYLIB_CURRENT_VERSION = 23; DYLIB_INSTALL_NAME_BASE = "@rpath"; GCC_PREFIX_HEADER = "Apptentive/Misc/ApptentiveConnect-Prefix.pch"; GCC_TREAT_WARNINGS_AS_ERRORS = YES; @@ -2538,7 +2550,6 @@ }; name = Release; }; - /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ diff --git a/Apptentive/Apptentive/Apptentive.h b/Apptentive/Apptentive/Apptentive.h index 24eb3390b..fe382c3ea 100644 --- a/Apptentive/Apptentive/Apptentive.h +++ b/Apptentive/Apptentive/Apptentive.h @@ -20,7 +20,7 @@ FOUNDATION_EXPORT double ApptentiveVersionNumber; FOUNDATION_EXPORT const unsigned char ApptentiveVersionString[]; /** The version number of the Apptentive SDK. */ -#define kApptentiveVersionString @"5.2.2" +#define kApptentiveVersionString @"5.2.3" /** The version number of the Apptentive API platform. */ #define kApptentiveAPIVersionString @"9" diff --git a/Apptentive/Apptentive/Engagement/Model/ApptentiveConversationManager.m b/Apptentive/Apptentive/Engagement/Model/ApptentiveConversationManager.m index a9d732e4e..f7b969d44 100644 --- a/Apptentive/Apptentive/Engagement/Model/ApptentiveConversationManager.m +++ b/Apptentive/Apptentive/Engagement/Model/ApptentiveConversationManager.m @@ -821,6 +821,8 @@ - (BOOL)updateActiveConversation:(ApptentiveConversation *)conversation withResp [self saveConversation:self.activeConversation]; + self.messageManager.conversation = self.activeConversation; + [self handleConversationStateChange:self.activeConversation]; [self updateManifestIfNeeded]; @@ -854,10 +856,15 @@ - (BOOL)updateLegacyConversation:(ApptentiveConversation *)conversation withResp mutableConversation.state = ApptentiveConversationStateAnonymous; } + [self.messageManager stop]; + self.activeConversation = mutableConversation; self.activeConversation.delegate = self; [self saveConversation:self.activeConversation]; + + self.messageManager.conversation = self.activeConversation; + [self handleConversationStateChange:self.activeConversation]; [self updateManifestIfNeeded]; diff --git a/Apptentive/Apptentive/Engagement/Model/ApptentiveCount.h b/Apptentive/Apptentive/Engagement/Model/ApptentiveCount.h index accf8de60..8503d9b10 100644 --- a/Apptentive/Apptentive/Engagement/Model/ApptentiveCount.h +++ b/Apptentive/Apptentive/Engagement/Model/ApptentiveCount.h @@ -79,6 +79,18 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)invoke; + +/** + Adds the values from `otherCount` to the current object. + + @param oldCount The older count object, presumably from a previous version. + @param newCount The newer count object, which would have been invoked last. + @return The sum of the two count objects. + + @discussion If either is nil, the other one is returned. If both are nil, nil is returned. + */ ++ (ApptentiveCount *)mergeOldCount:(nullable ApptentiveCount *)oldCount withNewCount:(nullable ApptentiveCount *)newCount; + @end NS_ASSUME_NONNULL_END diff --git a/Apptentive/Apptentive/Engagement/Model/ApptentiveCount.m b/Apptentive/Apptentive/Engagement/Model/ApptentiveCount.m index 60c7baa29..42f64cdf3 100644 --- a/Apptentive/Apptentive/Engagement/Model/ApptentiveCount.m +++ b/Apptentive/Apptentive/Engagement/Model/ApptentiveCount.m @@ -78,6 +78,16 @@ - (NSString *)description { return [NSString stringWithFormat:@"[%@] totalCount=%ld versionCount=%ld buildCount=%ld lastInvoked=%@", NSStringFromClass([self class]), (unsigned long)_totalCount, (unsigned long)_versionCount, (unsigned long)_buildCount, _lastInvoked]; } +// This is used for migrating version 5.1.0 through 5.2.2 (with unescaped code points) to 5.2.3 and later. ++ (ApptentiveCount *)mergeOldCount:(nullable ApptentiveCount *)oldCount withNewCount:(nullable ApptentiveCount *)newCount { + NSInteger totalCount = oldCount.totalCount + newCount.totalCount; + NSInteger versionCount = newCount.versionCount; // Old count is likely to be for a different version + NSInteger buildCount = newCount.buildCount; // Old count is likely to be for a different build + NSDate *lastInvoked = newCount.lastInvoked ?: oldCount.lastInvoked; // New count, if present, will have been invoked last + + return [[ApptentiveCount alloc] initWithTotalCount:totalCount versionCount:versionCount buildCount:buildCount lastInvoked:lastInvoked]; +} + @end diff --git a/Apptentive/Apptentive/Engagement/Model/ApptentiveEngagement.h b/Apptentive/Apptentive/Engagement/Model/ApptentiveEngagement.h index 8684b0221..814cc968e 100644 --- a/Apptentive/Apptentive/Engagement/Model/ApptentiveEngagement.h +++ b/Apptentive/Apptentive/Engagement/Model/ApptentiveEngagement.h @@ -72,6 +72,10 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)resetBuild; +// Test only ++ (nullable NSString *)escapedKeyForKey:(NSString *)key; +@property (readonly, nonatomic) NSInteger version; + @end NS_ASSUME_NONNULL_END diff --git a/Apptentive/Apptentive/Engagement/Model/ApptentiveEngagement.m b/Apptentive/Apptentive/Engagement/Model/ApptentiveEngagement.m index 565459908..da556b5c6 100644 --- a/Apptentive/Apptentive/Engagement/Model/ApptentiveEngagement.m +++ b/Apptentive/Apptentive/Engagement/Model/ApptentiveEngagement.m @@ -8,11 +8,13 @@ #import "ApptentiveEngagement.h" #import "ApptentiveCount.h" +#import "ApptentiveBackend+Engagement.h" NS_ASSUME_NONNULL_BEGIN static NSString *const InteractionsKey = @"interactions"; static NSString *const CodePointsKey = @"codePoints"; +static NSString *const VersionKey = @"version"; // Legacy keys static NSString *const ATEngagementCodePointsInvokesTotalKey = @"ATEngagementCodePointsInvokesTotalKey"; @@ -23,16 +25,16 @@ static NSString *const ATEngagementInteractionsInvokesVersionKey = @"ATEngagementInteractionsInvokesVersionKey"; static NSString *const ATEngagementInteractionsInvokesBuildKey = @"ATEngagementInteractionsInvokesBuildKey"; static NSString *const ATEngagementInteractionsInvokesLastDateKey = @"ATEngagementInteractionsInvokesLastDateKey"; - +static NSInteger const CurrentVersion = 2; @interface ApptentiveEngagement () @property (strong, nonatomic) NSMutableDictionary *mutableInteractions; @property (strong, nonatomic) NSMutableDictionary *mutableCodePoints; +@property (assign, nonatomic) NSInteger version; @end - @implementation ApptentiveEngagement - (instancetype)init { @@ -40,6 +42,7 @@ - (instancetype)init { if (self) { _mutableInteractions = [NSMutableDictionary dictionary]; _mutableCodePoints = [NSMutableDictionary dictionary]; + _version = CurrentVersion; } return self; } @@ -49,6 +52,20 @@ - (nullable instancetype)initWithCoder:(NSCoder *)coder { if (self) { _mutableInteractions = [coder decodeObjectOfClass:[NSMutableDictionary class] forKey:InteractionsKey]; _mutableCodePoints = [coder decodeObjectOfClass:[NSMutableDictionary class] forKey:CodePointsKey]; + if ([coder containsValueForKey:VersionKey]) { + _version = [coder decodeIntegerForKey:VersionKey]; + } else { + _version = 1; + } + + @try { + if (_version != CurrentVersion) { + [self migrateFrom:_version to:CurrentVersion]; + } + } @catch(NSException *exception) { + ApptentiveLogError(ApptentiveLogTagConversation, @"Caught exception %e when migrating engagement data. Starting over.", exception); + return [self init]; + } } return self; } @@ -57,8 +74,10 @@ - (void)encodeWithCoder:(NSCoder *)coder { [super encodeWithCoder:coder]; [coder encodeObject:self.mutableInteractions forKey:InteractionsKey]; [coder encodeObject:self.mutableCodePoints forKey:CodePointsKey]; + [coder encodeInteger:self.version forKey:VersionKey]; } +// This migrates pre-4.0 data stored in NSUserDefaults to 4.0 and later versions stored in NSCoding archive - (instancetype)initAndMigrate { self = [self init]; @@ -96,6 +115,75 @@ + (void)deleteMigratedData { [[NSUserDefaults standardUserDefaults] removeObjectForKey:ATEngagementInteractionsInvokesLastDateKey]; } +- (void)migrateFrom:(NSInteger)fromVersion to:(NSInteger)toVersion { + if (fromVersion == 1 && toVersion == 2) { + if ([self escapeUnescapedKeysInCodePoints]) { + _version = toVersion; + } + } +} + +- (BOOL)escapeUnescapedKeysInCodePoints { + NSMutableArray *codePointsToMerge = [NSMutableArray array]; + + for (NSString *key in self.codePoints) { + NSString *escapedKey = [[self class] escapedKeyForKey:key]; + + if (escapedKey == nil) { + continue; + } + + [codePointsToMerge addObject:@[key, escapedKey]]; + } + + NSMutableDictionary *escapedCodePoints = [NSMutableDictionary dictionaryWithDictionary:self.codePoints]; + for (NSArray *keys in codePointsToMerge) { + NSString *key = keys[0]; + NSString *escapedKey = keys[1]; + + ApptentiveCount *oldCount = [self.codePoints objectForKey:key]; + ApptentiveCount *newCount = [self.codePoints objectForKey:escapedKey]; + escapedCodePoints[escapedKey] = [ApptentiveCount mergeOldCount:oldCount withNewCount:newCount]; + [escapedCodePoints removeObjectForKey:key]; + } + + _mutableCodePoints = escapedCodePoints; + + return YES; +} + ++ (nullable NSString *)escapedKeyForKey:(NSString *)key { + NSArray *keyParts = [key componentsSeparatedByString:@"#"]; + + if (keyParts.count < 3) { + ApptentiveLogWarning(ApptentiveLogTagConversation, @"Unable to migrate unencoded code point %@", key); + return nil; + } + + NSString *vendor = keyParts[0]; + NSString *interaction = keyParts[1]; + // If the event name had pound signs in it, then there will be two or more parts starting at index 2. + // We join those parts with a pound sign, which conveniently no-ops in the case of a single part. + NSString *event = [[keyParts subarrayWithRange:NSMakeRange(2, keyParts.count - 2)] componentsJoinedByString:@"#"]; + + if ([[self class] eventNeedsEscaping:event]) { + return [ApptentiveBackend codePointForVendor:vendor interactionType:interaction event:event]; + } else { + return nil; + } +} + +// Use some heuristics to see if the event name needs to be escaped and hasn't already been escaped ++ (BOOL)eventNeedsEscaping:(NSString *)event { + // Slashes definitey need escaping + BOOL slashesFound = [event containsString:@"/"]; + // Pound signs definitely need escaping + BOOL poundSignsFound = [event containsString:@"#"]; + // Third-party percent signs would also need escaping, but there aren't instances of those in our events database, so we good. + + return slashesFound || poundSignsFound; +} + - (NSDictionary *)interactions { return [NSDictionary dictionaryWithDictionary:self.mutableInteractions]; } diff --git a/Apptentive/Apptentive/Engagement/Persistence/ApptentiveBackend+Engagement.h b/Apptentive/Apptentive/Engagement/Persistence/ApptentiveBackend+Engagement.h index c318667dd..69f45fefe 100644 --- a/Apptentive/Apptentive/Engagement/Persistence/ApptentiveBackend+Engagement.h +++ b/Apptentive/Apptentive/Engagement/Persistence/ApptentiveBackend+Engagement.h @@ -11,21 +11,6 @@ NS_ASSUME_NONNULL_BEGIN -extern NSString *const ATEngagementInstallDateKey; -extern NSString *const ATEngagementUpgradeDateKey; -extern NSString *const ATEngagementLastUsedVersionKey; -extern NSString *const ATEngagementIsUpdateVersionKey; -extern NSString *const ATEngagementIsUpdateBuildKey; -extern NSString *const ATEngagementCodePointsInvokesTotalKey; -extern NSString *const ATEngagementCodePointsInvokesVersionKey; -extern NSString *const ATEngagementCodePointsInvokesBuildKey; -extern NSString *const ATEngagementCodePointsInvokesLastDateKey; -extern NSString *const ATEngagementInteractionsInvokesTotalKey; -extern NSString *const ATEngagementInteractionsInvokesVersionKey; -extern NSString *const ATEngagementInteractionsInvokesBuildKey; -extern NSString *const ATEngagementInteractionsInvokesLastDateKey; -extern NSString *const ATEngagementInteractionsSDKVersionKey; - extern NSString *const ATEngagementCodePointHostAppVendorKey; extern NSString *const ATEngagementCodePointHostAppInteractionKey; extern NSString *const ATEngagementCodePointApptentiveVendorKey; @@ -45,6 +30,7 @@ extern NSString *const ApptentiveEngagementMessageCenterEvent; - (BOOL)canShowInteractionForCodePoint:(NSString *)codePoint; + (NSString *)codePointForVendor:(NSString *)vendor interactionType:(NSString *)interactionType event:(NSString *)event; ++ (NSString *)stringByEscapingCodePointSeparatorCharactersInString:(NSString *)string; - (void)engageApptentiveAppEvent:(NSString *)event; - (void)engageLocalEvent:(NSString *)event userInfo:(nullable NSDictionary *)userInfo customData:(nullable NSDictionary *)customData extendedData:(nullable NSArray *)extendedData fromViewController:(nullable UIViewController *)viewController; diff --git a/Apptentive/Apptentive/Engagement/Persistence/ApptentiveBackend+Engagement.m b/Apptentive/Apptentive/Engagement/Persistence/ApptentiveBackend+Engagement.m index 2405a30f9..70648a0d1 100644 --- a/Apptentive/Apptentive/Engagement/Persistence/ApptentiveBackend+Engagement.m +++ b/Apptentive/Apptentive/Engagement/Persistence/ApptentiveBackend+Engagement.m @@ -70,8 +70,25 @@ - (ApptentiveInteraction *)interactionForEvent:(NSString *)event { return self.conversationManager.manifest.interactions[interactionIdentifier]; } ++ (NSString *)stringByEscapingCodePointSeparatorCharactersInString:(NSString *)string { + // Only escape "%", "/", and "#". + // Do not change unless the server spec changes. + NSMutableString *escape = [string mutableCopy]; + [escape replaceOccurrencesOfString:@"%" withString:@"%25" options:NSLiteralSearch range:NSMakeRange(0, escape.length)]; + [escape replaceOccurrencesOfString:@"/" withString:@"%2F" options:NSLiteralSearch range:NSMakeRange(0, escape.length)]; + [escape replaceOccurrencesOfString:@"#" withString:@"%23" options:NSLiteralSearch range:NSMakeRange(0, escape.length)]; + + return escape; +} + + (NSString *)codePointForVendor:(NSString *)vendor interactionType:(NSString *)interactionType event:(NSString *)event { - return [NSString stringWithFormat:@"%@#%@#%@", vendor, interactionType, event];; + NSString *encodedVendor = [[self class] stringByEscapingCodePointSeparatorCharactersInString:vendor]; + NSString *encodedInteractionType = [[self class] stringByEscapingCodePointSeparatorCharactersInString:interactionType]; + NSString *encodedEvent = [[self class] stringByEscapingCodePointSeparatorCharactersInString:event]; + + NSString *codePoint = [NSString stringWithFormat:@"%@#%@#%@", encodedVendor, encodedInteractionType, encodedEvent]; + + return codePoint; } - (void)engageApptentiveAppEvent:(NSString *)event { diff --git a/Apptentive/Apptentive/Info.plist b/Apptentive/Apptentive/Info.plist index 6fe1bc01a..b9a3d061c 100644 --- a/Apptentive/Apptentive/Info.plist +++ b/Apptentive/Apptentive/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 5.2.2 + 5.2.3 CFBundleVersion $(CURRENT_PROJECT_VERSION) NSPrincipalClass diff --git a/Apptentive/Apptentive/Message Center/Model/ApptentiveMessageManager.h b/Apptentive/Apptentive/Message Center/Model/ApptentiveMessageManager.h index 1dc31d783..cf5e5966c 100644 --- a/Apptentive/Apptentive/Message Center/Model/ApptentiveMessageManager.h +++ b/Apptentive/Apptentive/Message Center/Model/ApptentiveMessageManager.h @@ -22,7 +22,7 @@ NS_ASSUME_NONNULL_BEGIN @property (readonly, nonatomic) NSString *storagePath; @property (readonly, nonatomic) NSString *conversationIdentifier; @property (readonly, nonatomic) ApptentiveDispatchQueue *operationQueue; -@property (readonly, nonatomic) ApptentiveConversation *conversation; +@property (strong, nonatomic) ApptentiveConversation *conversation; @property (readonly, nonatomic) ApptentiveClient *client; @property (assign, nonatomic) NSTimeInterval pollingInterval; diff --git a/Apptentive/Apptentive/Message Center/Model/ApptentiveMessageManager.m b/Apptentive/Apptentive/Message Center/Model/ApptentiveMessageManager.m index 2a1cb57f1..6d6485345 100644 --- a/Apptentive/Apptentive/Message Center/Model/ApptentiveMessageManager.m +++ b/Apptentive/Apptentive/Message Center/Model/ApptentiveMessageManager.m @@ -69,7 +69,7 @@ - (instancetype)initWithStoragePath:(NSString *)storagePath client:(ApptentiveCl _didSkipProfile = [conversation.userInfo[ATMessageCenterDidSkipProfileKey] boolValue]; _draftMessage = conversation.userInfo[ATMessageCenterDraftMessageKey]; - _hasSentMessage = [conversation.userInfo[ApptentiveHasSentMessageKey] boolValue]; + _hasSentMessage = [conversation.userInfo[ApptentiveHasSentMessageKey] boolValue] || _messageStore.messages.count > 0; for (ApptentiveMessage *message in _messageStore.messages) { for (ApptentiveAttachment *attachment in message.attachments) { diff --git a/Apptentive/Apptentive/Model/ApptentiveLegacyMessage.h b/Apptentive/Apptentive/Model/ApptentiveLegacyMessage.h index b8197d506..feef6729e 100644 --- a/Apptentive/Apptentive/Model/ApptentiveLegacyMessage.h +++ b/Apptentive/Apptentive/Model/ApptentiveLegacyMessage.h @@ -40,7 +40,7 @@ typedef NS_ENUM(NSInteger, ATPendingMessageState) { @property (copy, nonatomic) NSString *title; @property (strong, nonatomic) NSOrderedSet *attachments; -+ (void)enqueueUnsentMessagesInContext:(NSManagedObjectContext *)context forConversation:(ApptentiveConversation *)conversation oldAttachmentPath:(NSString *)oldAttachmentPath newAttachmentPath:(NSString *)newAttachmentPath; ++ (BOOL)enqueueUnsentMessagesInContext:(NSManagedObjectContext *)context forConversation:(ApptentiveConversation *)conversation oldAttachmentPath:(NSString *)oldAttachmentPath newAttachmentPath:(NSString *)newAttachmentPath; @end diff --git a/Apptentive/Apptentive/Model/ApptentiveLegacyMessage.m b/Apptentive/Apptentive/Model/ApptentiveLegacyMessage.m index 9a73f3bfc..ca920fc7d 100644 --- a/Apptentive/Apptentive/Model/ApptentiveLegacyMessage.m +++ b/Apptentive/Apptentive/Model/ApptentiveLegacyMessage.m @@ -39,7 +39,7 @@ @implementation ApptentiveLegacyMessage @dynamic body; @dynamic attachments; -+ (void)enqueueUnsentMessagesInContext:(NSManagedObjectContext *)context forConversation:(ApptentiveConversation *)conversation oldAttachmentPath:(NSString *)oldAttachmentPath newAttachmentPath:(NSString *)newAttachmentPath { ++ (BOOL)enqueueUnsentMessagesInContext:(NSManagedObjectContext *)context forConversation:(ApptentiveConversation *)conversation oldAttachmentPath:(NSString *)oldAttachmentPath newAttachmentPath:(NSString *)newAttachmentPath { ApptentiveAssertNotNil(context, @"Context is nil"); ApptentiveAssertNotNil(conversation, @"Conversation is nil"); ApptentiveAssertNotNil(oldAttachmentPath, @"Old attachment path is nil"); @@ -53,7 +53,7 @@ + (void)enqueueUnsentMessagesInContext:(NSManagedObjectContext *)context forConv if (unsentMessages == nil) { ApptentiveLogError(ApptentiveLogTagMessages, @"Unable to retrieve unsent messages: %@", error); - return; + return NO; } for (ApptentiveLegacyMessage *legacyMessage in unsentMessages) { @@ -108,6 +108,8 @@ + (void)enqueueUnsentMessagesInContext:(NSManagedObjectContext *)context forConv if ([[NSFileManager defaultManager] fileExistsAtPath:oldAttachmentPath] && ![[NSFileManager defaultManager] removeItemAtPath:oldAttachmentPath error:&error]) { ApptentiveLogWarning(ApptentiveLogTagMessages, @"Unable to remove legacy attachments directory (%@): %@", oldAttachmentPath, error); } + + return unsentMessages.count > 0; } @end diff --git a/Apptentive/Apptentive/Persistence/ApptentiveBackend.h b/Apptentive/Apptentive/Persistence/ApptentiveBackend.h index 0e70f5d88..f03af8030 100644 --- a/Apptentive/Apptentive/Persistence/ApptentiveBackend.h +++ b/Apptentive/Apptentive/Persistence/ApptentiveBackend.h @@ -19,6 +19,7 @@ extern NSString *const ApptentiveAuthenticationDidFailNotification; extern NSString *const ApptentiveAuthenticationDidFailNotificationKeyErrorType; extern NSString *const ApptentiveAuthenticationDidFailNotificationKeyErrorMessage; extern NSString *const ApptentiveAuthenticationDidFailNotificationKeyConversationIdentifier; +extern NSString *const ApptentiveHasSentMessageKey; @class ApptentiveConversation, ApptentiveEngagementManifest, ApptentiveAppConfiguration, ApptentiveMessageCenterViewController, ApptentiveMessageManager, ApptentivePayloadSender, ApptentiveDispatchQueue; diff --git a/Apptentive/Apptentive/Persistence/ApptentiveBackend.m b/Apptentive/Apptentive/Persistence/ApptentiveBackend.m index a3278675e..367fef891 100644 --- a/Apptentive/Apptentive/Persistence/ApptentiveBackend.m +++ b/Apptentive/Apptentive/Persistence/ApptentiveBackend.m @@ -408,7 +408,8 @@ - (void)migrateLegacyCoreDataAndTaskQueueForConversation:(ApptentiveConversation NSString *oldAttachmentPath = [self.supportDirectoryPath stringByAppendingPathComponent:@"attachments"]; [self.managedObjectContext performBlockAndWait:^{ - [ApptentiveLegacyMessage enqueueUnsentMessagesInContext:self.managedObjectContext forConversation:conversation oldAttachmentPath:oldAttachmentPath newAttachmentPath:newAttachmentPath]; + BOOL hasMessages = [ApptentiveLegacyMessage enqueueUnsentMessagesInContext:self.managedObjectContext forConversation:conversation oldAttachmentPath:oldAttachmentPath newAttachmentPath:newAttachmentPath]; + [conversation setUserInfo:@(hasMessages) forKey:ApptentiveHasSentMessageKey]; [ApptentiveLegacyEvent enqueueUnsentEventsInContext:self.managedObjectContext forConversation:conversation]; [ApptentiveLegacySurveyResponse enqueueUnsentSurveyResponsesInContext:self.managedObjectContext forConversation:conversation]; diff --git a/Apptentive/ApptentiveTests/ApptentiveEngagementTests.swift b/Apptentive/ApptentiveTests/ApptentiveEngagementTests.swift new file mode 100644 index 000000000..14ca17afe --- /dev/null +++ b/Apptentive/ApptentiveTests/ApptentiveEngagementTests.swift @@ -0,0 +1,128 @@ +// +// ApptentiveEngagementTests.swift +// ApptentiveTests +// +// Created by Frank Schmitt on 11/5/18. +// Copyright © 2018 Apptentive, Inc. All rights reserved. +// + +import XCTest + +class ApptentiveEngagementTests: XCTestCase { + + func testEventLabelsContainingCodePointSeparatorCharacters() { + //Escape "%", "/", and "#". + + var i = "testEventLabelSeparators"; + var o = "testEventLabelSeparators"; + XCTAssertTrue(ApptentiveBackend.stringByEscapingCodePointSeparatorCharacters(in: i) == o, "Test escaping code point separator characters from event labels."); + + i = "test#Event#Label#Separators"; + o = "test%23Event%23Label%23Separators"; + XCTAssertTrue(ApptentiveBackend.stringByEscapingCodePointSeparatorCharacters(in: i) == o, "Test escaping code point separator characters from event labels."); + + i = "test/Event/Label/Separators"; + o = "test%2FEvent%2FLabel%2FSeparators"; + XCTAssertTrue(ApptentiveBackend.stringByEscapingCodePointSeparatorCharacters(in: i) == o, "Test escaping code point separator characters from event labels."); + + i = "test%Event/Label#Separators"; + o = "test%25Event%2FLabel%23Separators"; + XCTAssertTrue(ApptentiveBackend.stringByEscapingCodePointSeparatorCharacters(in: i) == o, "Test escaping code point separator characters from event labels."); + + i = "test#Event/Label%Separators"; + o = "test%23Event%2FLabel%25Separators"; + XCTAssertTrue(ApptentiveBackend.stringByEscapingCodePointSeparatorCharacters(in: i) == o, "Test escaping code point separator characters from event labels."); + + i = "test###Event///Label%%%Separators"; + o = "test%23%23%23Event%2F%2F%2FLabel%25%25%25Separators"; + XCTAssertTrue(ApptentiveBackend.stringByEscapingCodePointSeparatorCharacters(in: i) == o, "Test escaping code point separator characters from event labels."); + + i = "test#%///#%//%%/#Event_!@#$%^&*(){}Label1234567890[]`~Separators"; + o = "test%23%25%2F%2F%2F%23%25%2F%2F%25%25%2F%23Event_!@%23$%25^&*(){}Label1234567890[]`~Separators"; + XCTAssertTrue(ApptentiveBackend.stringByEscapingCodePointSeparatorCharacters(in: i) == o, "Test escaping code point separator characters from event labels."); + + i = "test%/#"; + o = "test%25%2F%23"; + XCTAssertTrue(ApptentiveBackend.stringByEscapingCodePointSeparatorCharacters(in: i) == o, "Test escaping code point separator characters from event labels."); + } + + func testMergingCounts() { + let aLongTimeAgo = Date(timeIntervalSinceNow: -1000) + let aLittleWhileAgo = Date(timeIntervalSinceNow: -500) + + let oldCount = ApptentiveCount(totalCount: 5, versionCount: 10, buildCount: 20, lastInvoked: aLongTimeAgo); + let newCount = ApptentiveCount(totalCount: 1, versionCount: 2, buildCount: 4, lastInvoked: aLittleWhileAgo); + + let mergedCount = ApptentiveCount.mergeOldCount(oldCount, withNewCount: newCount); + + XCTAssertEqual(mergedCount.totalCount, 6) + XCTAssertEqual(mergedCount.versionCount, 2) + XCTAssertEqual(mergedCount.buildCount, 4) + XCTAssertEqual(mergedCount.lastInvoked, aLittleWhileAgo) + + let mergedCount2 = ApptentiveCount.mergeOldCount(nil, withNewCount: nil) + + XCTAssertEqual(mergedCount2.totalCount, 0) + XCTAssertEqual(mergedCount2.versionCount, 0) + XCTAssertEqual(mergedCount2.buildCount, 0) + XCTAssertEqual(mergedCount2.lastInvoked, nil) + + let mergedCount3 = ApptentiveCount.mergeOldCount(oldCount, withNewCount: nil) + + XCTAssertEqual(mergedCount3.totalCount, 5) + XCTAssertEqual(mergedCount3.versionCount, 0) + XCTAssertEqual(mergedCount3.buildCount, 0) + XCTAssertEqual(mergedCount3.lastInvoked, aLongTimeAgo) + + let mergedCount4 = ApptentiveCount.mergeOldCount(nil, withNewCount: newCount) + + XCTAssertEqual(mergedCount4.totalCount, 1) + XCTAssertEqual(mergedCount4.versionCount, 2) + XCTAssertEqual(mergedCount4.buildCount, 4) + XCTAssertEqual(mergedCount4.lastInvoked, aLittleWhileAgo) + } + + func testEscapedKeyForKey() { + XCTAssertEqual(ApptentiveEngagement.escapedKey(forKey: "local#app#go/daddy"), "local#app#go%2Fdaddy") + XCTAssertEqual(ApptentiveEngagement.escapedKey(forKey: "local#app#go#daddy"), "local#app#go%23daddy") + XCTAssertEqual(ApptentiveEngagement.escapedKey(forKey: "local#app#go%25daddy"), nil) + + XCTAssertEqual(ApptentiveEngagement.escapedKey(forKey: "com.apptentive#app#launch"), nil) + } + + func testMigration() { + Bundle(for: ApptentiveEngagementTests.self).url(forResource: "conversation-4", withExtension:"archive") + + // Open a 4.0.0 archive with slash/pound/percent event names + guard let fourOhUrl = Bundle(for: ApptentiveEngagementTests.self).url(forResource: "conversation-4", withExtension:"archive"), let fourOhConversation = NSKeyedUnarchiver.unarchiveObject(withFile: fourOhUrl.path) as? ApptentiveConversation else { + XCTFail("Can't open 4.0 conversation archive") + return + } + + let fourOhEngagement = fourOhConversation.engagement + XCTAssertEqual(fourOhEngagement.version, 2) + XCTAssertEqual(fourOhEngagement.codePoints["local#app#go%2Fdaddy"]?.totalCount, 1) + XCTAssertEqual(fourOhEngagement.codePoints["local#app#go%2Fdaddy"]?.buildCount, 1) + XCTAssertEqual(fourOhEngagement.codePoints["local#app#go%2Fdaddy"]?.versionCount, 1) + XCTAssertEqual(fourOhEngagement.codePoints["local#app#go%23daddy"]?.totalCount, 1) + XCTAssertEqual(fourOhEngagement.codePoints["local#app#go%25daddy"]?.totalCount, 1) + XCTAssertEqual(fourOhEngagement.codePoints["local#app#go%2520daddy"]?.totalCount, 1) + + + // Open a 5.2.2 archive with slash/pound/percent event names + guard let fiveTwoUrl = Bundle(for: ApptentiveEngagementTests.self).url(forResource: "conversation-5", withExtension:"archive"), let fiveTwoConversation = NSKeyedUnarchiver.unarchiveObject(withFile: fiveTwoUrl.path) as? ApptentiveConversation else { + XCTFail("Can't open 5.2 conversation archive") + return + } + + let fiveTwoEngagement = fiveTwoConversation.engagement + XCTAssertEqual(fiveTwoEngagement.version, 2) + XCTAssertEqual(fiveTwoEngagement.codePoints["local#app#go%2Fdaddy"]?.totalCount, 2) + XCTAssertEqual(fiveTwoEngagement.codePoints["local#app#go%2Fdaddy"]?.buildCount, 0) + XCTAssertEqual(fiveTwoEngagement.codePoints["local#app#go%2Fdaddy"]?.versionCount, 0) + XCTAssertEqual(fiveTwoEngagement.codePoints["local#app#go%23daddy"]?.totalCount, 2) + // No longer migrating these, as there aren't any in the wild. + // XCTAssertEqual(fiveTwoEngagement.codePoints["local#app#go%25daddy"]?.totalCount, 2) + // XCTAssertEqual(fiveTwoEngagement.codePoints["local#app#go%2520daddy"]?.totalCount, 2) + } +} diff --git a/Apptentive/ApptentiveTests/ApptentiveTests-Bridging-Header.h b/Apptentive/ApptentiveTests/ApptentiveTests-Bridging-Header.h index 02003d140..94062678c 100644 --- a/Apptentive/ApptentiveTests/ApptentiveTests-Bridging-Header.h +++ b/Apptentive/ApptentiveTests/ApptentiveTests-Bridging-Header.h @@ -23,6 +23,7 @@ #import "ApptentivePerson.h" #import "ApptentiveEngagement.h" #import "ApptentiveConversation.h" +#import "ApptentiveCount.h" #import "ApptentiveAttachment.h" #import "ApptentiveMessage.h" @@ -36,3 +37,4 @@ #import "ApptentiveMockDispatchQueue.h" #import "ApptentiveRetryPolicy.h" +#import "ApptentiveBackend+Engagement.h" diff --git a/Apptentive/ApptentiveTests/Info.plist b/Apptentive/ApptentiveTests/Info.plist index f1ef26f78..3a0895f8c 100644 --- a/Apptentive/ApptentiveTests/Info.plist +++ b/Apptentive/ApptentiveTests/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 5.2.2 + 5.2.3 CFBundleVersion 1 diff --git a/Apptentive/ApptentiveTests/data/conversation-4.archive b/Apptentive/ApptentiveTests/data/conversation-4.archive new file mode 100644 index 000000000..f0d07a9e6 Binary files /dev/null and b/Apptentive/ApptentiveTests/data/conversation-4.archive differ diff --git a/Apptentive/ApptentiveTests/data/conversation-5.archive b/Apptentive/ApptentiveTests/data/conversation-5.archive new file mode 100644 index 000000000..67b61d8fe Binary files /dev/null and b/Apptentive/ApptentiveTests/data/conversation-5.archive differ diff --git a/CHANGELOG.md b/CHANGELOG.md index 20f6d627c..2bfab9a41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# 2018-11-27 - v5.2.3 + +#### Bugs Fixed + +* Fix problem that required app relaunch to receive replies in Message Center. +* Correct handling of event names that include any pound signs, percent signs, or slashes. + # 2018-10-23 - v5.2.2 #### Improvements diff --git a/Example/Podfile.lock b/Example/Podfile.lock index 211e75db8..c43b09860 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -1,5 +1,5 @@ PODS: - - apptentive-ios (5.2.2) + - apptentive-ios (5.2.3) DEPENDENCIES: - apptentive-ios (from `..`) @@ -9,7 +9,7 @@ EXTERNAL SOURCES: :path: ".." SPEC CHECKSUMS: - apptentive-ios: 4ef3801eea4162cac3ab6aaa11448dcb26a5e42e + apptentive-ios: cfe3763ac1ef62af21791e57da0c30cbf28022f8 PODFILE CHECKSUM: 89d2b5f4683b04482e89df6d46b268cc9ed1ef79 diff --git a/apptentive-ios.podspec b/apptentive-ios.podspec index 2739d67ab..8b42ecebc 100644 --- a/apptentive-ios.podspec +++ b/apptentive-ios.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = 'apptentive-ios' s.module_name = 'Apptentive' - s.version = '5.2.2' + s.version = '5.2.3' s.license = 'BSD' s.summary = 'Apptentive Customer Communications SDK.' s.homepage = 'https://www.apptentive.com/'