diff --git a/UnitTests/MPIdentityCachingTests.m b/UnitTests/MPIdentityCachingTests.m new file mode 100644 index 00000000..36035c09 --- /dev/null +++ b/UnitTests/MPIdentityCachingTests.m @@ -0,0 +1,318 @@ +#ifndef MPARTICLE_LOCATION_DISABLE +@import mParticle_Apple_SDK; +#else +@import mParticle_Apple_SDK_NoLocation; +#endif + +#import +#import "MPBaseTestCase.h" +#import "MPIdentityCaching.h" +#import "MPIdentityDTO.h" + +// Dictionary keys +static NSString *const kMPIdentityCachingBodyData = @"kMPIdentityCachingBodyData"; +static NSString *const kMPIdentityCachingStatusCode = @"kMPIdentityCachingStatusCode"; +static NSString *const kMPIdentityCachingExpires = @"kMPIdentityCachingExpires"; + +@interface MPIdentityCachedResponse() +- (nullable instancetype)initWithDictionary:(NSDictionary *)dictionary; +@end + +@interface MPIdentityCaching() ++ (void)cacheIdentityResponse:(nonnull MPIdentityCachedResponse *)cachedResponse endpoint:(MPEndpoint)endpoint identities:(nonnull NSDictionary *)identities; ++ (nullable MPIdentityCachedResponse *)getCachedIdentityResponseForEndpoint:(MPEndpoint)endpoint identities:(nonnull NSDictionary *)identities; ++ (nullable NSDictionary *)getCache; ++ (void)setCache:(nullable NSDictionary *)cache; ++ (nonnull NSString *)keyWithEndpoint:(MPEndpoint)endpoint identities:(nonnull NSDictionary *)identities; ++ (nullable NSDictionary *)identitiesFromIdentityRequest:(nonnull MPIdentityHTTPBaseRequest *)identityRequest; ++ (nullable NSString *)hashIdentities:(NSDictionary *)identities; ++ (nullable NSString *)serializeIdentities:(NSDictionary *)identities; ++ (nullable NSString *)sha256Hash:(NSString *)string; +@end + +@interface MPIdentityCachingTests : MPBaseTestCase +@end + +@implementation MPIdentityCachingTests + +- (void)setUp { + [super setUp]; + [MPIdentityCaching setCache:nil]; +} + +- (void)tearDown { + [super tearDown]; + [MPIdentityCaching setCache:nil]; +} + +- (void)testGetCachedResponse { + NSDictionary *identities = @{ + @"ios_idfv": @"abcdefg", + @"email": @"test1@test2.com", + @"customerid": @"12345", + @"google": [NSNull null] + }; + + NSDictionary *cache = @{ + @"0::6aeb076bd3732431628b4d88c6019274b3d4444393ec041f8975f4e69773e4f1": @{ + kMPIdentityCachingBodyData: [@"0" dataUsingEncoding:NSUTF8StringEncoding], + kMPIdentityCachingStatusCode: @200, + kMPIdentityCachingExpires: [NSDate date] // Expired + }, + @"1::6aeb076bd3732431628b4d88c6019274b3d4444393ec041f8975f4e69773e4f1": @{ + kMPIdentityCachingBodyData: [@"1" dataUsingEncoding:NSUTF8StringEncoding], + kMPIdentityCachingStatusCode: @200, + kMPIdentityCachingExpires: [[NSDate date] dateByAddingTimeInterval:50] // Valid + }, + @"2::6aeb076bd3732431628b4d88c6019274b3d4444393ec041f8975f4e69773e4f1": @{ + kMPIdentityCachingBodyData: [@"2" dataUsingEncoding:NSUTF8StringEncoding], + kMPIdentityCachingStatusCode: @200, + kMPIdentityCachingExpires: [[NSDate date] dateByAddingTimeInterval:-100] // Expired + }, + @"3::6aeb076bd3732431628b4d88c6019274b3d4444393ec041f8975f4e69773e4f1": @{ + kMPIdentityCachingBodyData: [@"3" dataUsingEncoding:NSUTF8StringEncoding], + kMPIdentityCachingStatusCode: @200, + kMPIdentityCachingExpires: [[NSDate date] dateByAddingTimeInterval:8000] // Valid + } + }; + [MPIdentityCaching setCache:cache]; + + // Test valid match + MPIdentityCachedResponse *cached1 = [MPIdentityCaching getCachedIdentityResponseForEndpoint:MPEndpointIdentityLogout identities:identities]; + NSDictionary *dict1 = [cache objectForKey:@"1::6aeb076bd3732431628b4d88c6019274b3d4444393ec041f8975f4e69773e4f1"]; + MPIdentityCachedResponse *original1 = [[MPIdentityCachedResponse alloc] initWithDictionary:dict1]; + XCTAssertEqualObjects(cached1, original1); + + // Test expired match + MPIdentityCachedResponse *cached2 = [MPIdentityCaching getCachedIdentityResponseForEndpoint:MPEndpointIdentityLogin identities:identities]; + XCTAssertNil(cached2); + + // Test partial match + NSDictionary *partialIdentities = @{ + @"email": @"test1@test2.com", + @"customerid": @"12345" + }; + MPIdentityCachedResponse *cached3 = [MPIdentityCaching getCachedIdentityResponseForEndpoint:MPEndpointIdentityModify identities:partialIdentities]; + XCTAssertNil(cached3); + + // Test no match + NSDictionary *noMatchIdentities = @{ + @"MPIdentityEmail": @"test5@test10.com", + @"MPIdentityCustomerId": @"67890" + }; + MPIdentityCachedResponse *cached4 = [MPIdentityCaching getCachedIdentityResponseForEndpoint:MPEndpointIdentityModify identities:noMatchIdentities]; + XCTAssertNil(cached4); +} + +- (void)testSetGetCache { + NSDictionary *cache = @{ + @"0::fd54ce706ef89564f0d25321dc38cefce06fc0b886aae7c71f38e293fb67789d": @{ + kMPIdentityCachingBodyData: [@"0" dataUsingEncoding:NSUTF8StringEncoding], + kMPIdentityCachingStatusCode: @200, + kMPIdentityCachingExpires: [NSDate date] // Expired + }, + @"1::fd54ce706ef89564f0d25321dc38cefce06fc0b886aae7c71f38e293fb67789d": @{ + kMPIdentityCachingBodyData: [@"1" dataUsingEncoding:NSUTF8StringEncoding], + kMPIdentityCachingStatusCode: @200, + kMPIdentityCachingExpires: [[NSDate date] dateByAddingTimeInterval:50] // Valid + }, + @"2::fd54ce706ef89564f0d25321dc38cefce06fc0b886aae7c71f38e293fb67789d": @{ + kMPIdentityCachingBodyData: [@"2" dataUsingEncoding:NSUTF8StringEncoding], + kMPIdentityCachingStatusCode: @200, + kMPIdentityCachingExpires: [[NSDate date] dateByAddingTimeInterval:-100] // Expired + }, + @"3::fd54ce706ef89564f0d25321dc38cefce06fc0b886aae7c71f38e293fb67789d": @{ + kMPIdentityCachingBodyData: [@"3" dataUsingEncoding:NSUTF8StringEncoding], + kMPIdentityCachingStatusCode: @200, + kMPIdentityCachingExpires: [[NSDate date] dateByAddingTimeInterval:8000] // Valid + } + }; + + XCTAssertNil([MPIdentityCaching getCache]); + + [MPIdentityCaching setCache:cache]; + XCTAssertEqualObjects(cache, [MPIdentityCaching getCache]); +} + +- (void)testClearAllCache { + NSDictionary *cache = @{ + @"0::fd54ce706ef89564f0d25321dc38cefce06fc0b886aae7c71f38e293fb67789d": @{ + kMPIdentityCachingBodyData: [@"0" dataUsingEncoding:NSUTF8StringEncoding], + kMPIdentityCachingStatusCode: @200, + kMPIdentityCachingExpires: [NSDate date] // Expired + }, + @"1::fd54ce706ef89564f0d25321dc38cefce06fc0b886aae7c71f38e293fb67789d": @{ + kMPIdentityCachingBodyData: [@"1" dataUsingEncoding:NSUTF8StringEncoding], + kMPIdentityCachingStatusCode: @200, + kMPIdentityCachingExpires: [[NSDate date] dateByAddingTimeInterval:50] // Valid + }, + @"2::fd54ce706ef89564f0d25321dc38cefce06fc0b886aae7c71f38e293fb67789d": @{ + kMPIdentityCachingBodyData: [@"2" dataUsingEncoding:NSUTF8StringEncoding], + kMPIdentityCachingStatusCode: @200, + kMPIdentityCachingExpires: [[NSDate date] dateByAddingTimeInterval:-100] // Expired + }, + @"3::fd54ce706ef89564f0d25321dc38cefce06fc0b886aae7c71f38e293fb67789d": @{ + kMPIdentityCachingBodyData: [@"3" dataUsingEncoding:NSUTF8StringEncoding], + kMPIdentityCachingStatusCode: @200, + kMPIdentityCachingExpires: [[NSDate date] dateByAddingTimeInterval:8000] // Valid + } + }; + [MPIdentityCaching setCache:cache]; + XCTAssertEqualObjects(cache, [MPIdentityCaching getCache]); + + [MPIdentityCaching clearAllCache]; + XCTAssertNil([MPIdentityCaching getCache]); +} + +- (void)testClearExpiredCache { + NSDictionary *cache = @{ + @"0::fd54ce706ef89564f0d25321dc38cefce06fc0b886aae7c71f38e293fb67789d": @{ + kMPIdentityCachingBodyData: [@"0" dataUsingEncoding:NSUTF8StringEncoding], + kMPIdentityCachingStatusCode: @200, + kMPIdentityCachingExpires: [NSDate date] // Expired + }, + @"1::fd54ce706ef89564f0d25321dc38cefce06fc0b886aae7c71f38e293fb67789d": @{ + kMPIdentityCachingBodyData: [@"1" dataUsingEncoding:NSUTF8StringEncoding], + kMPIdentityCachingStatusCode: @200, + kMPIdentityCachingExpires: [[NSDate date] dateByAddingTimeInterval:50] // Valid + }, + @"2::fd54ce706ef89564f0d25321dc38cefce06fc0b886aae7c71f38e293fb67789d": @{ + kMPIdentityCachingBodyData: [@"2" dataUsingEncoding:NSUTF8StringEncoding], + kMPIdentityCachingStatusCode: @200, + kMPIdentityCachingExpires: [[NSDate date] dateByAddingTimeInterval:-100] // Expired + }, + @"3::fd54ce706ef89564f0d25321dc38cefce06fc0b886aae7c71f38e293fb67789d": @{ + kMPIdentityCachingBodyData: [@"3" dataUsingEncoding:NSUTF8StringEncoding], + kMPIdentityCachingStatusCode: @200, + kMPIdentityCachingExpires: [[NSDate date] dateByAddingTimeInterval:8000] // Valid + } + }; + + NSMutableDictionary *onlyValidCache = [cache mutableCopy]; + [onlyValidCache removeObjectForKey:@"0::fd54ce706ef89564f0d25321dc38cefce06fc0b886aae7c71f38e293fb67789d"]; + [onlyValidCache removeObjectForKey:@"2::fd54ce706ef89564f0d25321dc38cefce06fc0b886aae7c71f38e293fb67789d"]; + + [MPIdentityCaching setCache:cache]; + XCTAssertEqualObjects(cache, [MPIdentityCaching getCache]); + + [MPIdentityCaching clearExpiredCache]; + XCTAssertEqualObjects(onlyValidCache, [MPIdentityCaching getCache]); +} + +- (void)testKeyWithEndpointAndIdentities { + NSDictionary *identities = @{ + @"ios_idfv": @"abcdefg", + @"email": @"test1@test2.com", + @"customerid": @"12345", + @"google": [NSNull null] + }; + + NSString *key1 = [MPIdentityCaching keyWithEndpoint:MPEndpointIdentityLogin identities:identities]; + XCTAssertEqualObjects(key1, @"0::6aeb076bd3732431628b4d88c6019274b3d4444393ec041f8975f4e69773e4f1"); + + NSString *key2 = [MPIdentityCaching keyWithEndpoint:MPEndpointIdentityLogout identities:identities]; + XCTAssertEqualObjects(key2, @"1::6aeb076bd3732431628b4d88c6019274b3d4444393ec041f8975f4e69773e4f1"); + + NSString *key3 = [MPIdentityCaching keyWithEndpoint:MPEndpointIdentityIdentify identities:identities]; + XCTAssertEqualObjects(key3, @"2::6aeb076bd3732431628b4d88c6019274b3d4444393ec041f8975f4e69773e4f1"); + + NSString *key4 = [MPIdentityCaching keyWithEndpoint:MPEndpointIdentityModify identities:identities]; + XCTAssertEqualObjects(key4, @"3::6aeb076bd3732431628b4d88c6019274b3d4444393ec041f8975f4e69773e4f1"); +} + +- (void)testIdentitiesFromIdentifyHTTPRequest { + MPIdentifyHTTPRequest *identifyRequest = [[MPIdentifyHTTPRequest alloc] init]; + identifyRequest.knownIdentities = [[MPIdentityHTTPIdentities alloc] initWithIdentities:@{ + @(MPIdentityIOSVendorId): @"abcdefg", + @(MPIdentityEmail): @"test1@test2.com", + @(MPIdentityCustomerId): @"12345", + @(MPIdentityGoogle): [NSNull null] + }]; + + NSDictionary *identities = [MPIdentityCaching identitiesFromIdentityRequest:identifyRequest]; + NSDictionary *expected = @{ + @"ios_idfv": @"abcdefg", + @"email": @"test1@test2.com", + @"customerid": @"12345", + @"google": [NSNull null] + }; + XCTAssertEqualObjects(identities, expected); +} + +- (void)testIdentitiesFromModifyHTTPRequest { + MPIdentityHTTPModifyRequest *modifyRequest = [[MPIdentityHTTPModifyRequest alloc] initWithIdentityChanges:@[ + [[MPIdentityHTTPIdentityChange alloc] initWithOldValue:@"test1@test1.com" value:@"test2@test2.com" identityType:@"email"], + [[MPIdentityHTTPIdentityChange alloc] initWithOldValue:nil value:@"12345" identityType:@"customerid"], + [[MPIdentityHTTPIdentityChange alloc] initWithOldValue:@"1234" value:@"5678" identityType:@"other2"] + ]]; + + NSDictionary *identities = [MPIdentityCaching identitiesFromIdentityRequest:modifyRequest]; + NSDictionary *expected = @{ + @"email": @{@"identity_type": @"email", @"old_value": @"test1@test1.com", @"new_value": @"test2@test2.com"}, + @"customerid": @{@"identity_type": @"customerid", @"old_value": [NSNull null], @"new_value": @"12345"}, + @"other2": @{@"identity_type": @"other2", @"old_value": @"1234", @"new_value": @"5678"} + }; + XCTAssertEqualObjects(identities, expected); +} + +- (void)testHashIdentities { + NSString *hash1 = [MPIdentityCaching hashIdentities:nil]; + XCTAssertNil(hash1); + + NSString *hash2 = [MPIdentityCaching hashIdentities:@{}]; + XCTAssertNil(hash2); + + NSDictionary *dict3 = @{ + @(MPIdentityEmail): @"test@test.com" + }; + NSString *hash3 = [MPIdentityCaching hashIdentities:dict3]; + XCTAssertEqualObjects(hash3, @"c610ba538a9a66cd34b4eb0bc7937ce944bbcf48ca292d500ed85b805aca3e02"); + + NSDictionary *dict4 = @{ + @"ios_idfv": @"abcdefg", + @"email": @"test1@test2.com", + @"customerid": @"12345", + @"google": [NSNull null] + }; + NSString *hash4 = [MPIdentityCaching hashIdentities:dict4]; + XCTAssertEqualObjects(hash4, @"6aeb076bd3732431628b4d88c6019274b3d4444393ec041f8975f4e69773e4f1"); +} + +- (void)testSerializeIdentities { + NSString *string1 = [MPIdentityCaching serializeIdentities:nil]; + XCTAssertEqualObjects(string1, @""); + + NSString *string2 = [MPIdentityCaching serializeIdentities:@{}]; + XCTAssertEqualObjects(string2, @""); + + NSDictionary *dict3 = @{ + @(MPIdentityEmail): @"test@test.com" + }; + NSString *string3 = [MPIdentityCaching serializeIdentities:dict3]; + XCTAssertEqualObjects(string3, @"::7:test@test.com"); + + NSDictionary *dict4 = @{ + @"ios_idfv": @"abcdefg", + @"email": @"test1@test2.com", + @"customerid": @"12345", + @"google": [NSNull null] + }; + NSString *string4 = [MPIdentityCaching serializeIdentities:dict4]; + XCTAssertEqualObjects(string4, @"::customerid:12345::email:test1@test2.com::google:null::ios_idfv:abcdefg"); +} + +- (void)testSha256Hash { + NSString *hash1 = [MPIdentityCaching sha256Hash:nil]; + XCTAssertNil(hash1); + + NSString *hash2 = [MPIdentityCaching sha256Hash:@""]; + XCTAssertNil(hash2); + + NSString *hash3 = [MPIdentityCaching sha256Hash:@"::email:test@test.com::customerid:12435"]; + XCTAssertEqualObjects(hash3, @"aa58bbc1adccecb75fbb00cf9f424ca2098b8b7273a235a07c473b1b129810b5"); + + NSString *hash4 = [MPIdentityCaching sha256Hash:@"::email:null"]; + XCTAssertEqualObjects(hash4, @"46bdfb15bd51f77b7955516d3ac92ec1a90856cac70e9343c510cf39532d2007"); +} + +@end diff --git a/mParticle-Apple-SDK.xcodeproj/project.pbxproj b/mParticle-Apple-SDK.xcodeproj/project.pbxproj index 61eccecc..5f92a6fd 100644 --- a/mParticle-Apple-SDK.xcodeproj/project.pbxproj +++ b/mParticle-Apple-SDK.xcodeproj/project.pbxproj @@ -7,6 +7,12 @@ objects = { /* Begin PBXBuildFile section */ + 531BCF342B28A23400F5C573 /* MPIdentityCaching.h in Headers */ = {isa = PBXBuildFile; fileRef = 531BCF322B28A23400F5C573 /* MPIdentityCaching.h */; }; + 531BCF352B28A23400F5C573 /* MPIdentityCaching.h in Headers */ = {isa = PBXBuildFile; fileRef = 531BCF322B28A23400F5C573 /* MPIdentityCaching.h */; }; + 531BCF362B28A23400F5C573 /* MPIdentityCaching.m in Sources */ = {isa = PBXBuildFile; fileRef = 531BCF332B28A23400F5C573 /* MPIdentityCaching.m */; }; + 531BCF372B28A23400F5C573 /* MPIdentityCaching.m in Sources */ = {isa = PBXBuildFile; fileRef = 531BCF332B28A23400F5C573 /* MPIdentityCaching.m */; }; + 531BCF3A2B28A83E00F5C573 /* MPIdentityCachingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 531BCF392B28A83E00F5C573 /* MPIdentityCachingTests.m */; }; + 531BCF3B2B28A83E00F5C573 /* MPIdentityCachingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 531BCF392B28A83E00F5C573 /* MPIdentityCachingTests.m */; }; 534CD25C29CE2877008452B3 /* NSNumber+MPFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53A79B2629CDFB1F00E7489F /* NSNumber+MPFormatter.swift */; }; 534CD25E29CE2BF1008452B3 /* Swift.h in Headers */ = {isa = PBXBuildFile; fileRef = 534CD25D29CE2BF1008452B3 /* Swift.h */; }; 534CD25F29CE2BF1008452B3 /* Swift.h in Headers */ = {isa = PBXBuildFile; fileRef = 534CD25D29CE2BF1008452B3 /* Swift.h */; }; @@ -127,7 +133,7 @@ 53A79B9629CDFB2000E7489F /* MPSession.m in Sources */ = {isa = PBXBuildFile; fileRef = 53A79ACC29CDFB1E00E7489F /* MPSession.m */; }; 53A79B9729CDFB2000E7489F /* MPIntegrationAttributes.m in Sources */ = {isa = PBXBuildFile; fileRef = 53A79ACD29CDFB1E00E7489F /* MPIntegrationAttributes.m */; }; 53A79B9829CDFB2000E7489F /* MPConsumerInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 53A79ACE29CDFB1F00E7489F /* MPConsumerInfo.h */; }; - 53A79B9929CDFB2000E7489F /* MPIConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = 53A79ACF29CDFB1F00E7489F /* MPIConstants.h */; }; + 53A79B9929CDFB2000E7489F /* MPIConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = 53A79ACF29CDFB1F00E7489F /* MPIConstants.h */; settings = {ATTRIBUTES = (Private, ); }; }; 53A79B9A29CDFB2000E7489F /* MPEnums.m in Sources */ = {isa = PBXBuildFile; fileRef = 53A79AD029CDFB1F00E7489F /* MPEnums.m */; }; 53A79BBD29CDFB2000E7489F /* MPBackendController.m in Sources */ = {isa = PBXBuildFile; fileRef = 53A79AF429CDFB1F00E7489F /* MPBackendController.m */; }; 53A79BBE29CDFB2000E7489F /* MPCCPAConsent.m in Sources */ = {isa = PBXBuildFile; fileRef = 53A79AF629CDFB1F00E7489F /* MPCCPAConsent.m */; }; @@ -531,6 +537,9 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 531BCF322B28A23400F5C573 /* MPIdentityCaching.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MPIdentityCaching.h; sourceTree = ""; }; + 531BCF332B28A23400F5C573 /* MPIdentityCaching.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MPIdentityCaching.m; sourceTree = ""; }; + 531BCF392B28A83E00F5C573 /* MPIdentityCachingTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MPIdentityCachingTests.m; sourceTree = ""; }; 534CD25D29CE2BF1008452B3 /* Swift.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Swift.h; sourceTree = ""; }; 534CD2AA29CE2CE1008452B3 /* mParticle-Apple-SDK-NoLocationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "mParticle-Apple-SDK-NoLocationTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 53A79A7929CCCD6400E7489F /* mParticle_Apple_SDK.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = mParticle_Apple_SDK.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -950,6 +959,8 @@ 53A79AB929CDFB1E00E7489F /* MPDatabaseMigrationController.m */, 53A79ABA29CDFB1E00E7489F /* MPDatabaseMigrationController.h */, 53A79ABB29CDFB1E00E7489F /* MPPersistenceController.h */, + 531BCF322B28A23400F5C573 /* MPIdentityCaching.h */, + 531BCF332B28A23400F5C573 /* MPIdentityCaching.m */, ); path = Persistence; sourceTree = ""; @@ -1279,6 +1290,7 @@ 53A79CB529CE019F00E7489F /* MPAppNotificationHandlerTests.m */, 53A79CB629CE019F00E7489F /* MPKitRegisterTests.m */, 53A79CB729CE019F00E7489F /* HasherTests.m */, + 531BCF392B28A83E00F5C573 /* MPIdentityCachingTests.m */, ); path = UnitTests; sourceTree = ""; @@ -1388,6 +1400,7 @@ 53A79BFE29CDFB2100E7489F /* MPAppNotificationHandler.h in Headers */, 53A79C1E29CDFB2100E7489F /* MPKitFilter.h in Headers */, 53A79C1529CDFB2100E7489F /* MPDataPlanFilter.h in Headers */, + 531BCF342B28A23400F5C573 /* MPIdentityCaching.h in Headers */, 53A79C0029CDFB2100E7489F /* MPPromotion+Dictionary.h in Headers */, 53A79B9229CDFB2000E7489F /* MPBreadcrumb.h in Headers */, 53A79B7B29CDFB2000E7489F /* MPConnectorProtocol.h in Headers */, @@ -1494,6 +1507,7 @@ 53A79D4A29CE23F700E7489F /* MPAppNotificationHandler.h in Headers */, 53A79D4B29CE23F700E7489F /* MPKitFilter.h in Headers */, 53A79D4C29CE23F700E7489F /* MPDataPlanFilter.h in Headers */, + 531BCF352B28A23400F5C573 /* MPIdentityCaching.h in Headers */, 53A79D4D29CE23F700E7489F /* MPPromotion+Dictionary.h in Headers */, 53A79D4E29CE23F700E7489F /* MPBreadcrumb.h in Headers */, 53A79D4F29CE23F700E7489F /* MPConnectorProtocol.h in Headers */, @@ -1734,6 +1748,7 @@ 534CD29D29CE2CE1008452B3 /* MPLaunchInfoTests.m in Sources */, 534CD29E29CE2CE1008452B3 /* MPConvertJSTests.m in Sources */, 534CD29F29CE2CE1008452B3 /* MPAppNotificationHandlerTests.m in Sources */, + 531BCF3B2B28A83E00F5C573 /* MPIdentityCachingTests.m in Sources */, 534CD2A029CE2CE1008452B3 /* MParticleTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1761,6 +1776,7 @@ 53A79C0D29CDFB2100E7489F /* MPKitConfiguration.mm in Sources */, 53A79BC129CDFB2000E7489F /* MPConsentState.m in Sources */, 53A79BE329CDFB2000E7489F /* MPBracket.cpp in Sources */, + 531BCF362B28A23400F5C573 /* MPIdentityCaching.m in Sources */, 53A79B9129CDFB2000E7489F /* MParticleUserNotification.m in Sources */, 53A79C2129CDFB2100E7489F /* MPIConstants.m in Sources */, 53A79BEE29CDFB2000E7489F /* MPLaunchInfo.m in Sources */, @@ -1897,6 +1913,7 @@ 53A79CE229CE019F00E7489F /* MPLaunchInfoTests.m in Sources */, 53A79CD429CE019F00E7489F /* MPConvertJSTests.m in Sources */, 53A79CF729CE019F00E7489F /* MPAppNotificationHandlerTests.m in Sources */, + 531BCF3A2B28A83E00F5C573 /* MPIdentityCachingTests.m in Sources */, 53A79CD829CE019F00E7489F /* MParticleTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1924,6 +1941,7 @@ 53A79D7829CE23F700E7489F /* MPConsentState.m in Sources */, 53A79D7929CE23F700E7489F /* MPBracket.cpp in Sources */, 53A79D7A29CE23F700E7489F /* MParticleUserNotification.m in Sources */, + 531BCF372B28A23400F5C573 /* MPIdentityCaching.m in Sources */, 53A79D7B29CE23F700E7489F /* MPIConstants.m in Sources */, 53A79D7C29CE23F700E7489F /* MPLaunchInfo.m in Sources */, 53A79D7D29CE23F700E7489F /* MPPromotion.m in Sources */, diff --git a/mParticle-Apple-SDK/MPBackendController.m b/mParticle-Apple-SDK/MPBackendController.m index 3a0b4c0b..dc50185f 100644 --- a/mParticle-Apple-SDK/MPBackendController.m +++ b/mParticle-Apple-SDK/MPBackendController.m @@ -31,6 +31,7 @@ #import "MPListenerController.h" #import "MParticleWebView.h" #import "MPDevice.h" +#import "MPIdentityCaching.h" #import "Swift.h" #if TARGET_OS_IOS == 1 @@ -2089,6 +2090,7 @@ - (void)cleanUp { nextCleanUpTime = currentTime + TWENTY_FOUR_HOURS; } [persistence purgeMemory]; + [MPIdentityCaching clearExpiredCache]; } - (void)handleApplicationDidEnterBackground:(NSNotification *)notification { diff --git a/mParticle-Apple-SDK/Network/MPNetworkCommunication.m b/mParticle-Apple-SDK/Network/MPNetworkCommunication.m index 987c1e09..46448b3c 100644 --- a/mParticle-Apple-SDK/Network/MPNetworkCommunication.m +++ b/mParticle-Apple-SDK/Network/MPNetworkCommunication.m @@ -27,6 +27,7 @@ #import "MPResponseConfig.h" #import "MPURL.h" #import "MPConnectorFactoryProtocol.h" +#import "MPIdentityCaching.h" NSString *const urlFormat = @"%@://%@/%@/%@%@"; // Scheme, URL Host, API Version, API key, path NSString *const urlFormatOverride = @"%@://%@/%@%@"; // Scheme, URL Host, API key, path @@ -53,6 +54,8 @@ NSString *const kMPURLHostEventSubdomain = @"nativesdks"; NSString *const kMPURLHostIdentitySubdomain = @"identity"; +NSString *const kMPIdentityCachingMaxAgeHeader = @"X-MP-Max-Age"; + static NSObject *factory = nil; @interface MParticle () @@ -415,9 +418,7 @@ - (void)requestConfig:(nullable NSObject *)connector withCo completionHandler(YES); return; } - - __weak MPNetworkCommunication *weakSelf = self; - + MPILogVerbose(@"Starting config request"); NSTimeInterval start = [[NSDate date] timeIntervalSince1970]; @@ -441,14 +442,7 @@ - (void)requestConfig:(nullable NSObject *)connector withCo NSString *cacheControl = httpResponse.allHeaderFields[kMPHTTPCacheControlHeaderKey]; NSString *ageString = httpResponse.allHeaderFields[kMPHTTPAgeHeaderKey]; - NSNumber *maxAge = [self maxAgeForCache:cacheControl]; - - __strong MPNetworkCommunication *strongSelf = weakSelf; - if (!strongSelf) { - completionHandler(NO); - return; - } if (![MPStateMachine isAppExtension]) { if (backgroundTaskIdentifier != UIBackgroundTaskInvalid) { @@ -769,32 +763,6 @@ - (void)identityApiRequestWithURL:(NSURL*)url identityRequest:(MPIdentityHTTPBas if (blockOtherRequests) { self.identifying = YES; } - __weak MPNetworkCommunication *weakSelf = self; - - __block UIBackgroundTaskIdentifier backgroundTaskIdentifier = UIBackgroundTaskInvalid; - - if (![MPStateMachine isAppExtension]) { - backgroundTaskIdentifier = [[MPApplication sharedUIApplication] beginBackgroundTaskWithExpirationHandler:^{ - if (backgroundTaskIdentifier != UIBackgroundTaskInvalid) { - __strong MPNetworkCommunication *strongSelf = weakSelf; - if (strongSelf) { - strongSelf.identifying = NO; - } - - [[MPApplication sharedUIApplication] endBackgroundTask:backgroundTaskIdentifier]; - backgroundTaskIdentifier = UIBackgroundTaskInvalid; - } - }]; - } - - NSTimeInterval start = [[NSDate date] timeIntervalSince1970]; - - NSDictionary *dictionary = [identityRequest dictionaryRepresentation]; - NSData *data = [NSJSONSerialization dataWithJSONObject:dictionary options:0 error:nil]; - NSString *jsonRequest = [[NSString alloc] initWithData:data - encoding:NSUTF8StringEncoding]; - - MPILogVerbose(@"Identity request:\nURL: %@ \nBody:%@", url, jsonRequest); MPEndpoint endpointType; MPURL *mpURL; @@ -811,64 +779,113 @@ - (void)identityApiRequestWithURL:(NSURL*)url identityRequest:(MPIdentityHTTPBas endpointType = MPEndpointIdentityModify; mpURL = self.modifyURL; } - [MPListenerController.sharedInstance onNetworkRequestStarted:endpointType url:url.absoluteString body:data]; - - NSObject *connector = [self makeConnector]; - NSObject *response = [connector responseFromPostRequestToURL:mpURL - message:nil - serializedParams:data]; - NSData *responseData = response.data; - NSError *error = response.error; - NSHTTPURLResponse *httpResponse = response.httpResponse; - - __strong MPNetworkCommunication *strongSelf = weakSelf; - - if (!strongSelf) { - if (completion) { - MPIdentityHTTPErrorResponse *errorResponse = [[MPIdentityHTTPErrorResponse alloc] initWithJsonObject:nil httpCode:0]; - completion(nil, [NSError errorWithDomain:mParticleIdentityErrorDomain code:MPIdentityErrorResponseCodeUnknown userInfo:@{mParticleIdentityErrorKey:errorResponse}]); - } - - return; - } - if (![MPStateMachine isAppExtension]) { - if (backgroundTaskIdentifier != UIBackgroundTaskInvalid) { - [[MPApplication sharedUIApplication] endBackgroundTask:backgroundTaskIdentifier]; - backgroundTaskIdentifier = UIBackgroundTaskInvalid; - } - } + NSTimeInterval start = [[NSDate date] timeIntervalSince1970]; - NSDictionary *responseDictionary = nil; - NSString *responseString = nil; - NSInteger responseCode = [httpResponse statusCode]; - BOOL success = responseCode == HTTPStatusCodeSuccess || responseCode == HTTPStatusCodeAccepted; + NSDictionary *dictionary = [identityRequest dictionaryRepresentation]; + NSData *data = [NSJSONSerialization dataWithJSONObject:dictionary options:0 error:nil]; + NSString *jsonRequest = [[NSString alloc] initWithData:data + encoding:NSUTF8StringEncoding]; - success = success && [responseData length] > 0; + MPILogVerbose(@"Identity request:\nURL: %@ \nBody:%@", url, jsonRequest); - NSError *serializationError = nil; - MPILogVerbose(@"Identity response code: %ld", (long)responseCode); + [MPListenerController.sharedInstance onNetworkRequestStarted:endpointType url:url.absoluteString body:data]; - [self checkResponseCodeToDisableEventLogging:[httpResponse statusCode]]; + BOOL success = NO; + NSError *error = nil; + NSDictionary *responseDictionary = nil; + NSString *responseString = nil; + NSInteger responseCode = 0; - if (success) { + MPIdentityCachedResponse *cachedResponse = [MPIdentityCaching getCachedIdentityResponseForEndpoint:endpointType identityRequest:identityRequest]; + if (cachedResponse) { @try { - responseString = [[NSString alloc] initWithData:responseData - encoding:NSUTF8StringEncoding]; - responseDictionary = [NSJSONSerialization JSONObjectWithData:responseData - options:0 - error:&serializationError]; + NSError *serializationError = nil; + responseString = [[NSString alloc] initWithData:cachedResponse.bodyData encoding:NSUTF8StringEncoding]; + responseDictionary = [NSJSONSerialization JSONObjectWithData:cachedResponse.bodyData options:0 error:&serializationError]; + + if (serializationError) { + responseDictionary = nil; + success = NO; + MPILogError(@"Identity response serialization error: %@", [serializationError localizedDescription]); + } } @catch (NSException *exception) { responseDictionary = nil; success = NO; MPILogError(@"Identity response serialization error: %@", [exception reason]); } + } else { + __block UIBackgroundTaskIdentifier backgroundTaskIdentifier = UIBackgroundTaskInvalid; + + if (![MPStateMachine isAppExtension]) { + backgroundTaskIdentifier = [[MPApplication sharedUIApplication] beginBackgroundTaskWithExpirationHandler:^{ + if (backgroundTaskIdentifier != UIBackgroundTaskInvalid) { + self.identifying = NO; + + [[MPApplication sharedUIApplication] endBackgroundTask:backgroundTaskIdentifier]; + backgroundTaskIdentifier = UIBackgroundTaskInvalid; + } + }]; + } + + NSObject *connector = [self makeConnector]; + NSObject *response = [connector responseFromPostRequestToURL:mpURL + message:nil + serializedParams:data]; + + NSData *responseData = response.data; + error = response.error; + NSHTTPURLResponse *httpResponse = response.httpResponse; + + if (![MPStateMachine isAppExtension]) { + if (backgroundTaskIdentifier != UIBackgroundTaskInvalid) { + [[MPApplication sharedUIApplication] endBackgroundTask:backgroundTaskIdentifier]; + backgroundTaskIdentifier = UIBackgroundTaskInvalid; + } + } + + responseCode = [httpResponse statusCode]; + success = responseCode == HTTPStatusCodeSuccess || responseCode == HTTPStatusCodeAccepted; + success = success && [responseData length] > 0; + + + MPILogVerbose(@"Identity response code: %ld", (long)responseCode); + + [self checkResponseCodeToDisableEventLogging:[httpResponse statusCode]]; + + if (success) { + @try { + NSError *serializationError = nil; + responseString = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding]; + responseDictionary = [NSJSONSerialization JSONObjectWithData:responseData options:0 error:&serializationError]; + + if (responseDictionary && !serializationError) { + // Cache response if it contains a the custom max age header + NSInteger maxAgeSeconds = [response.httpResponse.allHeaderFields[kMPIdentityCachingMaxAgeHeader] integerValue]; + if (maxAgeSeconds > 0) { + NSDate *expires = [[NSDate date] dateByAddingTimeInterval:(NSTimeInterval)maxAgeSeconds]; + MPIdentityCachedResponse *cachedResponse = [[MPIdentityCachedResponse alloc] initWithBodyData:responseData + statusCode:responseCode + expires:expires]; + [MPIdentityCaching cacheIdentityResponse:cachedResponse endpoint:endpointType identityRequest:identityRequest]; + } + } else { + responseDictionary = nil; + success = NO; + MPILogError(@"Identity response serialization error: %@", [serializationError localizedDescription]); + } + } @catch (NSException *exception) { + responseDictionary = nil; + success = NO; + MPILogError(@"Identity response serialization error: %@", [exception reason]); + } + } } MPILogVerbose(@"Identity execution time: %.2fms", ([[NSDate date] timeIntervalSince1970] - start) * 1000.0); - strongSelf.identifying = NO; + self.identifying = NO; [MPListenerController.sharedInstance onNetworkRequestFinished:endpointType url:url.absoluteString body:responseDictionary responseCode:responseCode]; if (success) { diff --git a/mParticle-Apple-SDK/Persistence/MPIdentityCaching.h b/mParticle-Apple-SDK/Persistence/MPIdentityCaching.h new file mode 100644 index 00000000..61946129 --- /dev/null +++ b/mParticle-Apple-SDK/Persistence/MPIdentityCaching.h @@ -0,0 +1,26 @@ +// +// MPIdentityCaching.h +// mParticle-Apple-SDK +// +// Created by Ben Baron on 12/12/23. +// + +#import +#import "MPListenerProtocol.h" +#import "MPIdentityDTO.h" + +@interface MPIdentityCachedResponse : NSObject +@property (nonnull, nonatomic, readonly) NSData *bodyData; +@property (nonatomic, readonly) NSInteger statusCode; +@property (nonnull, nonatomic, readonly) NSDate *expires; +- (nonnull instancetype)initWithBodyData:(nonnull NSData *)bodyData statusCode:(NSInteger)statusCode expires:(nonnull NSDate *)expires; +@end + +@interface MPIdentityCaching : NSObject + ++ (void)cacheIdentityResponse:(nonnull MPIdentityCachedResponse *)cachedResponse endpoint:(MPEndpoint)endpoint identityRequest:(nonnull MPIdentityHTTPBaseRequest *)identityRequest; ++ (nullable MPIdentityCachedResponse *)getCachedIdentityResponseForEndpoint:(MPEndpoint)endpoint identityRequest:(nonnull MPIdentityHTTPBaseRequest *)identityRequest; ++ (void)clearAllCache; ++ (void)clearExpiredCache; + +@end diff --git a/mParticle-Apple-SDK/Persistence/MPIdentityCaching.m b/mParticle-Apple-SDK/Persistence/MPIdentityCaching.m new file mode 100644 index 00000000..d091cb9c --- /dev/null +++ b/mParticle-Apple-SDK/Persistence/MPIdentityCaching.m @@ -0,0 +1,242 @@ +// +// MPIdentityCaching.m +// mParticle-Apple-SDK +// +// Created by Ben Baron on 12/12/23. +// + +#import "MPIdentityCaching.h" +#import "MPIUserDefaults.h" +#import + +// User defaults key +static NSString *const kMPIdentityCachingCachedIdentityCallsKey = @"kMPIdentityCachingCachedIdentityCallsKey"; + +// Dictionary keys +static NSString *const kMPIdentityCachingBodyData = @"kMPIdentityCachingBodyData"; +static NSString *const kMPIdentityCachingStatusCode = @"kMPIdentityCachingStatusCode"; +static NSString *const kMPIdentityCachingExpires = @"kMPIdentityCachingExpires"; + +@interface MPIdentityCachedResponse() +@property (nonnull, nonatomic, readwrite) NSData *bodyData; +@property (nonatomic, readwrite) NSInteger statusCode; +@property (nonnull, nonatomic, readwrite) NSDate *expires; +- (nullable instancetype)initWithDictionary:(NSDictionary *)dictionary; +- (NSDictionary *)dictionaryRepresentation; +@end + +@implementation MPIdentityCachedResponse + +- (nonnull instancetype)initWithBodyData:(nonnull NSData *)bodyData statusCode:(NSInteger)statusCode expires:(nonnull NSDate *)expires { + if (self = [super init]) { + _bodyData = bodyData; + _statusCode = statusCode; + _expires = expires; + } + return self; +} + +- (nullable instancetype)initWithDictionary:(NSDictionary *)dictionary { + if ([dictionary count] != 3) { + return nil; + } + + NSData *bodyData = dictionary[kMPIdentityCachingBodyData]; + NSNumber *statusCode = dictionary[kMPIdentityCachingStatusCode]; + NSDate *expires = dictionary[kMPIdentityCachingExpires]; + if (!bodyData || !statusCode || !expires) { + return nil; + } + + if (self = [super init]) { + _bodyData = bodyData; + _statusCode = [statusCode integerValue]; + _expires = expires; + } + return self; +} + +- (nonnull NSDictionary *)dictionaryRepresentation { + return @{ + kMPIdentityCachingBodyData: _bodyData, + kMPIdentityCachingStatusCode: @(_statusCode), + kMPIdentityCachingExpires: _expires + }; +} + +- (NSUInteger)hash { + return _bodyData.hash ^ _statusCode ^ _expires.hash; +} + +- (BOOL)isEqual:(id)object { + if (![object isKindOfClass:self.class]) { + return NO; + } + + MPIdentityCachedResponse *rhs = object; + return [_bodyData isEqualToData:rhs.bodyData] && _statusCode == rhs.statusCode && [_expires isEqualToDate:rhs.expires]; +} + +@end + +@implementation MPIdentityCaching + +#pragma mark - Public + ++ (void)cacheIdentityResponse:(nonnull MPIdentityCachedResponse *)cachedResponse endpoint:(MPEndpoint)endpoint identityRequest:(nonnull MPIdentityHTTPBaseRequest *)identityRequest { + NSDictionary *identities = [self identitiesFromIdentityRequest:identityRequest]; + return [self cacheIdentityResponse:cachedResponse endpoint:endpoint identities:identities]; +} + ++ (nullable MPIdentityCachedResponse *)getCachedIdentityResponseForEndpoint:(MPEndpoint)endpoint identityRequest:(nonnull MPIdentityHTTPBaseRequest *)identityRequest { + NSDictionary *identities = [self identitiesFromIdentityRequest:identityRequest]; + return [self getCachedIdentityResponseForEndpoint:endpoint identities:identities]; +} + ++ (void)cacheIdentityResponse:(nonnull MPIdentityCachedResponse *)cachedResponse endpoint:(MPEndpoint)endpoint identities:(nonnull NSDictionary *)identities { + NSString *key = [self keyWithEndpoint:endpoint identities:identities]; + if (key.length == 0) { + return; + } + + NSDictionary *cache = [self getCache] ?: @{}; + NSMutableDictionary *mutableCache = [cache mutableCopy]; + [mutableCache setObject:cachedResponse.dictionaryRepresentation forKey:key]; + [self setCache:mutableCache]; +} + ++ (nullable MPIdentityCachedResponse *)getCachedIdentityResponseForEndpoint:(MPEndpoint)endpoint identities:(nonnull NSDictionary *)identities { + NSDictionary *cache = [self getCache]; + NSString *key = [self keyWithEndpoint:endpoint identities:identities]; + if (key.length == 0) { + return nil; + } + + NSDictionary *dictionary = [cache objectForKey:key]; + if (!dictionary) { + return nil; + } + + MPIdentityCachedResponse *cachedResponse = [[MPIdentityCachedResponse alloc] initWithDictionary:dictionary]; + if (!cachedResponse || [[NSDate date] timeIntervalSinceDate:cachedResponse.expires] > 0) { + return nil; + } + return cachedResponse; +} + ++ (void)clearAllCache { + [self setCache:nil]; +} + ++ (void)clearExpiredCache { + NSDictionary *cache = [self getCache]; + NSMutableDictionary *mutableCache = [cache mutableCopy]; + [cache enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + if ([obj isKindOfClass:[NSDictionary class]]) { + NSDate *expires = [obj objectForKey:kMPIdentityCachingExpires]; + if (!expires || [expires timeIntervalSinceDate:[NSDate date]] < 0) { + [mutableCache removeObjectForKey:key]; + } + } else { + // Invalid cache data, remove from cache + [mutableCache removeObjectForKey:key]; + } + }]; + + if ([cache count] != [mutableCache count]) { + [self setCache:mutableCache]; + } +} + +#pragma mark - Private + ++ (nullable NSDictionary *)getCache { + return [[MPIUserDefaults standardUserDefaults] mpObjectForKey:kMPIdentityCachingCachedIdentityCallsKey userId:@0]; +} + ++ (void)setCache:(nullable NSDictionary *)cache { + [[MPIUserDefaults standardUserDefaults] setMPObject:cache forKey:kMPIdentityCachingCachedIdentityCallsKey userId:@0]; +} + ++ (nonnull NSString *)keyWithEndpoint:(MPEndpoint)endpoint identities:(nonnull NSDictionary *)identities { + return [NSString stringWithFormat:@"%ld::%@", (long)endpoint, [self hashIdentities:identities]]; +} + ++ (nullable NSDictionary *)identitiesFromIdentityRequest:(nonnull MPIdentityHTTPBaseRequest *)identityRequest { + NSDictionary *dict = identityRequest.dictionaryRepresentation; + + // Identify and Login requests include a known identities dictionary which can be used for the cache key + NSDictionary *knownIdentities = dict[@"known_identities"]; + if (knownIdentities) { + return knownIdentities; + } + + // Modify requests include an array of identity changes which need to be converted to a dictionary first + NSArray *identityChanges = dict[@"identity_changes"]; + if (identityChanges) { + // The data format should be an array of dictionaries, each of which should always have an identity_type key + // We can use this key as the dictionary key as there can only be one of each type + // If for some reason it doesn't exist, bail and don't cache + NSMutableDictionary *identities = [[NSMutableDictionary alloc] initWithCapacity:identityChanges.count]; + for (NSDictionary *change in identityChanges) { + if (![change isKindOfClass:[NSDictionary class]]) { + return nil; + } + + NSString *identityType = change[@"identity_type"]; + if (![identityType isKindOfClass:[NSString class]]) { + return nil; + } + identities[identityType] = change; + } + return identities; + } + + return nil; +} + ++ (nullable NSString *)hashIdentities:(NSDictionary *)identities { + NSString *serializedIdentities = [self serializeIdentities:identities]; + NSString *hashedString = [self sha256Hash:serializedIdentities]; + return hashedString; +} + ++ (nullable NSString *)serializeIdentities:(NSDictionary *)identities { + NSArray *sortedKeys = [identities.allKeys sortedArrayUsingSelector: @selector(compare:)]; + NSMutableString *serializedString = [[NSMutableString alloc] init]; + for (NSString *key in sortedKeys) { + [serializedString appendFormat:@"::%@", key]; + + // Can be either a string or NSNull + NSObject *value = identities[key]; + if ([value isKindOfClass:[NSString class]]) { + [serializedString appendFormat:@":%@", (NSString *)value]; + } else if ([value isKindOfClass:[NSNull class]]) { + [serializedString appendString:@":null"]; + } else { + // This should never happen, so return nil + return nil; + } + } + + return serializedString; +} + ++ (nullable NSString *)sha256Hash:(NSString *)string { + NSData *dataIn = [string dataUsingEncoding:NSUTF8StringEncoding]; + if (!dataIn || dataIn.length == 0) { + return nil; + } + + NSMutableData *dataOut = [NSMutableData dataWithLength:CC_SHA256_DIGEST_LENGTH]; + CC_SHA256(dataIn.bytes, (CC_LONG)dataIn.length, dataOut.mutableBytes); + + const uint8_t *dataOutBytes = dataOut.bytes; + NSMutableString *hexString = [NSMutableString stringWithCapacity:CC_SHA256_DIGEST_LENGTH * 2]; + for (int i = 0; i < CC_SHA256_DIGEST_LENGTH; i++) { + [hexString appendFormat:@"%02x", dataOutBytes[i]]; + } + return hexString; +} + +@end