diff --git a/Apptentive/Apptentive.xcodeproj/project.pbxproj b/Apptentive/Apptentive.xcodeproj/project.pbxproj index 7fd1a994c..a02aa575e 100644 --- a/Apptentive/Apptentive.xcodeproj/project.pbxproj +++ b/Apptentive/Apptentive.xcodeproj/project.pbxproj @@ -7,11 +7,21 @@ objects = { /* Begin PBXBuildFile section */ + 010FE33B203E2D900021C246 /* CriteriaDescriptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 010FE33A203E2D900021C246 /* CriteriaDescriptionTests.swift */; }; + 010FE346203E4C810021C246 /* ApptentiveIndentPrinter.h in Headers */ = {isa = PBXBuildFile; fileRef = 010FE344203E4C810021C246 /* ApptentiveIndentPrinter.h */; }; + 010FE347203E4C810021C246 /* ApptentiveIndentPrinter.m in Sources */ = {isa = PBXBuildFile; fileRef = 010FE345203E4C810021C246 /* ApptentiveIndentPrinter.m */; }; + 010FE34D2040C8810021C246 /* testOperatorStringEquals.json in Resources */ = {isa = PBXBuildFile; fileRef = 010FE34B2040C8800021C246 /* testOperatorStringEquals.json */; }; + 010FE34E2040C8810021C246 /* testOperatorStringNotEquals.json in Resources */ = {isa = PBXBuildFile; fileRef = 010FE34C2040C8800021C246 /* testOperatorStringNotEquals.json */; }; 0116D5271E92F516001DA5CF /* ApptentiveMessageStore.h in Headers */ = {isa = PBXBuildFile; fileRef = 0116D5251E92F516001DA5CF /* ApptentiveMessageStore.h */; }; 0116D5281E92F516001DA5CF /* ApptentiveMessageStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 0116D5261E92F516001DA5CF /* ApptentiveMessageStore.m */; }; + 01201AD31FC637BE00EB3593 /* CodePointAndInteractionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 01201AD21FC637BD00EB3593 /* CodePointAndInteractionTests.m */; }; 01216C501EBBB53E0062BD0D /* RequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01216C4F1EBBB53E0062BD0D /* RequestTests.swift */; }; 012285481E4A81AD00DD2F58 /* Apptentive_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 01A2CFAB1E490A9600C2103A /* Apptentive_Private.h */; settings = {ATTRIBUTES = (Public, ); }; }; 012285491E4A831500DD2F58 /* Apptentive.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 01A2CF911E49062700C2103A /* Apptentive.framework */; }; + 0123005F20531698000EC3C3 /* ClauseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0123005E20531698000EC3C3 /* ClauseTests.m */; }; + 012ED9292072F33F003D87F3 /* ApptentiveRetryPolicy.h in Headers */ = {isa = PBXBuildFile; fileRef = 012ED9272072F33F003D87F3 /* ApptentiveRetryPolicy.h */; }; + 012ED92A2072F33F003D87F3 /* ApptentiveRetryPolicy.m in Sources */ = {isa = PBXBuildFile; fileRef = 012ED9282072F33F003D87F3 /* ApptentiveRetryPolicy.m */; }; + 012ED92C2072FABE003D87F3 /* RetryPolicyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012ED92B2072FABE003D87F3 /* RetryPolicyTests.swift */; }; 0134EB121EA991AE00DA4925 /* ApptentiveLogoutPayload.h in Headers */ = {isa = PBXBuildFile; fileRef = 0134EB101EA991AE00DA4925 /* ApptentiveLogoutPayload.h */; }; 0134EB131EA991AE00DA4925 /* ApptentiveLogoutPayload.m in Sources */ = {isa = PBXBuildFile; fileRef = 0134EB111EA991AE00DA4925 /* ApptentiveLogoutPayload.m */; }; 0134EB161EA992F900DA4925 /* ApptentiveSDKAppReleasePayload.h in Headers */ = {isa = PBXBuildFile; fileRef = 0134EB141EA992F900DA4925 /* ApptentiveSDKAppReleasePayload.h */; }; @@ -61,9 +71,25 @@ 0174772C1EA92BED00A0A949 /* ApptentiveSurveyResponsePayload.h in Headers */ = {isa = PBXBuildFile; fileRef = 017477241EA92BED00A0A949 /* ApptentiveSurveyResponsePayload.h */; }; 0174772D1EA92BED00A0A949 /* ApptentiveSurveyResponsePayload.m in Sources */ = {isa = PBXBuildFile; fileRef = 017477251EA92BED00A0A949 /* ApptentiveSurveyResponsePayload.m */; }; 0174772F1EA92D7D00A0A949 /* PayloadTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0174772E1EA92D7C00A0A949 /* PayloadTests.swift */; }; + 0178C55E1FC49E0600DABF39 /* ApptentiveTargets.h in Headers */ = {isa = PBXBuildFile; fileRef = 0178C55C1FC49E0600DABF39 /* ApptentiveTargets.h */; }; + 0178C55F1FC49E0600DABF39 /* ApptentiveTargets.m in Sources */ = {isa = PBXBuildFile; fileRef = 0178C55D1FC49E0600DABF39 /* ApptentiveTargets.m */; }; + 0178C5621FC49F1000DABF39 /* ApptentiveTarget.h in Headers */ = {isa = PBXBuildFile; fileRef = 0178C5601FC49F1000DABF39 /* ApptentiveTarget.h */; }; + 0178C5631FC49F1000DABF39 /* ApptentiveTarget.m in Sources */ = {isa = PBXBuildFile; fileRef = 0178C5611FC49F1000DABF39 /* ApptentiveTarget.m */; }; + 0178C56B1FC4A5A000DABF39 /* ApptentiveClause.h in Headers */ = {isa = PBXBuildFile; fileRef = 0178C5691FC4A5A000DABF39 /* ApptentiveClause.h */; }; + 0178C56C1FC4A5A000DABF39 /* ApptentiveClause.m in Sources */ = {isa = PBXBuildFile; fileRef = 0178C56A1FC4A5A000DABF39 /* ApptentiveClause.m */; }; 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 */; }; + 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 */; }; + 018FAFE41FC4AC41007C52FE /* ApptentiveOrClause.m in Sources */ = {isa = PBXBuildFile; fileRef = 018FAFE21FC4AC41007C52FE /* ApptentiveOrClause.m */; }; + 018FAFE71FC4AD0D007C52FE /* ApptentiveFalseClause.h in Headers */ = {isa = PBXBuildFile; fileRef = 018FAFE51FC4AD0D007C52FE /* ApptentiveFalseClause.h */; }; + 018FAFE81FC4AD0D007C52FE /* ApptentiveFalseClause.m in Sources */ = {isa = PBXBuildFile; fileRef = 018FAFE61FC4AD0D007C52FE /* ApptentiveFalseClause.m */; }; + 018FAFEB1FC4AF8F007C52FE /* ApptentiveNotClause.h in Headers */ = {isa = PBXBuildFile; fileRef = 018FAFE91FC4AF8F007C52FE /* ApptentiveNotClause.h */; }; + 018FAFEC1FC4AF8F007C52FE /* ApptentiveNotClause.m in Sources */ = {isa = PBXBuildFile; fileRef = 018FAFEA1FC4AF8F007C52FE /* ApptentiveNotClause.m */; }; + 018FAFEF1FC4B21A007C52FE /* ApptentiveComparisonClause.h in Headers */ = {isa = PBXBuildFile; fileRef = 018FAFED1FC4B21A007C52FE /* ApptentiveComparisonClause.h */; }; + 018FAFF01FC4B21A007C52FE /* ApptentiveComparisonClause.m in Sources */ = {isa = PBXBuildFile; fileRef = 018FAFEE1FC4B21A007C52FE /* ApptentiveComparisonClause.m */; }; 01917D451E5E0B7400B37D82 /* ConversationManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01917D441E5E0B7400B37D82 /* ConversationManagerTests.swift */; }; 01A2CF9B1E49062800C2103A /* Apptentive.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 01A2CF911E49062700C2103A /* Apptentive.framework */; }; 01A2CFA21E49062800C2103A /* Apptentive.h in Headers */ = {isa = PBXBuildFile; fileRef = 01A2CF941E49062700C2103A /* Apptentive.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -116,10 +142,6 @@ 01A2D12A1E490A9700C2103A /* ApptentiveEngagementManifest.m in Sources */ = {isa = PBXBuildFile; fileRef = 01A2CFE61E490A9700C2103A /* ApptentiveEngagementManifest.m */; }; 01A2D12B1E490A9700C2103A /* ApptentiveInteraction.h in Headers */ = {isa = PBXBuildFile; fileRef = 01A2CFE71E490A9700C2103A /* ApptentiveInteraction.h */; }; 01A2D12C1E490A9700C2103A /* ApptentiveInteraction.m in Sources */ = {isa = PBXBuildFile; fileRef = 01A2CFE81E490A9700C2103A /* ApptentiveInteraction.m */; }; - 01A2D12D1E490A9700C2103A /* ApptentiveInteractionInvocation.h in Headers */ = {isa = PBXBuildFile; fileRef = 01A2CFE91E490A9700C2103A /* ApptentiveInteractionInvocation.h */; }; - 01A2D12E1E490A9700C2103A /* ApptentiveInteractionInvocation.m in Sources */ = {isa = PBXBuildFile; fileRef = 01A2CFEA1E490A9700C2103A /* ApptentiveInteractionInvocation.m */; }; - 01A2D12F1E490A9700C2103A /* ApptentiveInteractionUsageData.h in Headers */ = {isa = PBXBuildFile; fileRef = 01A2CFEB1E490A9700C2103A /* ApptentiveInteractionUsageData.h */; }; - 01A2D1301E490A9700C2103A /* ApptentiveInteractionUsageData.m in Sources */ = {isa = PBXBuildFile; fileRef = 01A2CFEC1E490A9700C2103A /* ApptentiveInteractionUsageData.m */; }; 01A2D1391E490A9700C2103A /* ApptentivePerson.h in Headers */ = {isa = PBXBuildFile; fileRef = 01A2CFF51E490A9700C2103A /* ApptentivePerson.h */; }; 01A2D13A1E490A9700C2103A /* ApptentivePerson.m in Sources */ = {isa = PBXBuildFile; fileRef = 01A2CFF61E490A9700C2103A /* ApptentivePerson.m */; }; 01A2D13B1E490A9700C2103A /* ApptentiveSDK.h in Headers */ = {isa = PBXBuildFile; fileRef = 01A2CFF71E490A9700C2103A /* ApptentiveSDK.h */; }; @@ -289,16 +311,12 @@ 01A2D1FE1E490A9700C2103A /* ApptentiveSurveySubmitButton.h in Headers */ = {isa = PBXBuildFile; fileRef = 01A2D0F61E490A9700C2103A /* ApptentiveSurveySubmitButton.h */; }; 01A2D1FF1E490A9700C2103A /* ApptentiveSurveySubmitButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 01A2D0F71E490A9700C2103A /* ApptentiveSurveySubmitButton.m */; }; 01A2D2401E4946D600C2103A /* ApptentiveConnectTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 01A2D2021E4946D500C2103A /* ApptentiveConnectTests.m */; }; - 01A2D2411E4946D600C2103A /* ApptentiveEngagementTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 01A2D2031E4946D500C2103A /* ApptentiveEngagementTests.m */; }; - 01A2D2421E4946D600C2103A /* ApptentiveInteractionInvocationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 01A2D2041E4946D500C2103A /* ApptentiveInteractionInvocationTests.m */; }; - 01A2D2431E4946D600C2103A /* ApptentiveInteractionUsageDataTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 01A2D2051E4946D500C2103A /* ApptentiveInteractionUsageDataTests.m */; }; 01A2D2441E4946D600C2103A /* ApptentiveMetricsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 01A2D2061E4946D500C2103A /* ApptentiveMetricsTests.m */; }; 01A2D2451E4946D600C2103A /* ApptentiveMigrationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 01A2D2071E4946D500C2103A /* ApptentiveMigrationTests.m */; }; 01A2D2461E4946D600C2103A /* ApptentiveConversationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 01A2D2081E4946D500C2103A /* ApptentiveConversationTests.m */; }; 01A2D2471E4946D600C2103A /* ApptentiveStyleSheetTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 01A2D2091E4946D500C2103A /* ApptentiveStyleSheetTests.m */; }; 01A2D2481E4946D600C2103A /* ApptentiveSurveyTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 01A2D20A1E4946D500C2103A /* ApptentiveSurveyTests.m */; }; 01A2D2491E4946D600C2103A /* ApptentiveUtilitiesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 01A2D20B1E4946D500C2103A /* ApptentiveUtilitiesTests.m */; }; - 01A2D24A1E4946D600C2103A /* CodePointAndInteractionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 01A2D20C1E4946D500C2103A /* CodePointAndInteractionTests.m */; }; 01A2D24B1E4946D600C2103A /* CriteriaTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 01A2D20E1E4946D500C2103A /* CriteriaTests.m */; }; 01A2D24C1E4946D600C2103A /* ATDataModelv1.sqlite in Resources */ = {isa = PBXBuildFile; fileRef = 01A2D2101E4946D500C2103A /* ATDataModelv1.sqlite */; }; 01A2D24D1E4946D600C2103A /* ATDataModelv2.sqlite in Resources */ = {isa = PBXBuildFile; fileRef = 01A2D2111E4946D500C2103A /* ATDataModelv2.sqlite */; }; @@ -346,6 +364,8 @@ 01A2D2A11E4963F700C2103A /* v2CorruptDatabase in Resources */ = {isa = PBXBuildFile; fileRef = 01A2D29E1E4963F700C2103A /* v2CorruptDatabase */; }; 01A2D2A21E4963F700C2103A /* v2WALDatabase in Resources */ = {isa = PBXBuildFile; fileRef = 01A2D29F1E4963F700C2103A /* v2WALDatabase */; }; 01A2D2A31E4963F700C2103A /* v3WALDatabase in Resources */ = {isa = PBXBuildFile; fileRef = 01A2D2A01E4963F700C2103A /* v3WALDatabase */; }; + 01A50EC41FC5FFB00058C06C /* ApptentiveInvocations.h in Headers */ = {isa = PBXBuildFile; fileRef = 01A50EC21FC5FFB00058C06C /* ApptentiveInvocations.h */; }; + 01A50EC51FC5FFB00058C06C /* ApptentiveInvocations.m in Sources */ = {isa = PBXBuildFile; fileRef = 01A50EC31FC5FFB00058C06C /* ApptentiveInvocations.m */; }; 01AA89B81F18177E00FB59AB /* ApptentiveAppInstall.h in Headers */ = {isa = PBXBuildFile; fileRef = 01AA89B61F18177E00FB59AB /* ApptentiveAppInstall.h */; }; 01AA89B91F18177E00FB59AB /* ApptentiveAppInstall.m in Sources */ = {isa = PBXBuildFile; fileRef = 01AA89B71F18177E00FB59AB /* ApptentiveAppInstall.m */; }; 01E04F9E1E819CD300D7E849 /* ApptentiveMessageManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 01E04F9C1E819CD300D7E849 /* ApptentiveMessageManager.h */; }; @@ -373,6 +393,18 @@ EF43D8AB1FD5FE6D00C0FF9F /* ApptentiveGCDDispatchQueue.h in Headers */ = {isa = PBXBuildFile; fileRef = EF43D8A91FD5FE6D00C0FF9F /* ApptentiveGCDDispatchQueue.h */; }; EF49AC941EF2C50B00E2187F /* NSMutableData+Types.h in Headers */ = {isa = PBXBuildFile; fileRef = EF49AC921EF2C50B00E2187F /* NSMutableData+Types.h */; }; EF49AC951EF2C50B00E2187F /* NSMutableData+Types.m in Sources */ = {isa = PBXBuildFile; fileRef = EF49AC931EF2C50B00E2187F /* NSMutableData+Types.m */; }; + EF4EAB8D203F876A003318C9 /* ApptentiveDispatchTask.h in Headers */ = {isa = PBXBuildFile; fileRef = EF4EAB8B203F876A003318C9 /* ApptentiveDispatchTask.h */; }; + EF4EAB8E203F876A003318C9 /* ApptentiveDispatchTask.m in Sources */ = {isa = PBXBuildFile; fileRef = EF4EAB8C203F876A003318C9 /* ApptentiveDispatchTask.m */; }; + EF4EAB91203F8A57003318C9 /* ApptentiveDispatchTask+Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = EF4EAB8F203F8A57003318C9 /* ApptentiveDispatchTask+Internal.h */; }; + EF4EAB95203F8B99003318C9 /* ApptentiveLogFileWriteTask.h in Headers */ = {isa = PBXBuildFile; fileRef = EF4EAB93203F8B99003318C9 /* ApptentiveLogFileWriteTask.h */; }; + EF4EAB96203F8B99003318C9 /* ApptentiveLogFileWriteTask.m in Sources */ = {isa = PBXBuildFile; fileRef = EF4EAB94203F8B99003318C9 /* ApptentiveLogFileWriteTask.m */; }; + EF4EAB9A203F9821003318C9 /* AppptentiveAsyncLogWriterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF4EAB99203F9821003318C9 /* AppptentiveAsyncLogWriterTests.swift */; }; + EF4EAB9D20409199003318C9 /* ApptentiveMockDispatchQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = EF4EAB9C20409199003318C9 /* ApptentiveMockDispatchQueue.m */; }; + EF4EABA02040A194003318C9 /* ApptentiveFileUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = EF4EAB9E2040A194003318C9 /* ApptentiveFileUtilities.h */; }; + EF4EABA12040A194003318C9 /* ApptentiveFileUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = EF4EAB9F2040A194003318C9 /* ApptentiveFileUtilities.m */; }; + EF4EABA42040A8D6003318C9 /* TestUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF4EABA32040A8D6003318C9 /* TestUtils.swift */; }; + EF4EABAB2040C0CF003318C9 /* ApptentiveLogMonitorSession.h in Headers */ = {isa = PBXBuildFile; fileRef = EF4EABA92040C0CF003318C9 /* ApptentiveLogMonitorSession.h */; }; + EF4EABAC2040C0CF003318C9 /* ApptentiveLogMonitorSession.m in Sources */ = {isa = PBXBuildFile; fileRef = EF4EABAA2040C0CF003318C9 /* ApptentiveLogMonitorSession.m */; }; EF8EE3981EB3F37B0033E7A1 /* ApptentiveConversationBaseRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = EF8EE3961EB3F37B0033E7A1 /* ApptentiveConversationBaseRequest.h */; }; EF8EE3991EB3F37B0033E7A1 /* ApptentiveConversationBaseRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = EF8EE3971EB3F37B0033E7A1 /* ApptentiveConversationBaseRequest.m */; }; EF8EE39B1EB3FB120033E7A1 /* ApptentiveAssert.m in Sources */ = {isa = PBXBuildFile; fileRef = EF8EE39A1EB3FB120033E7A1 /* ApptentiveAssert.m */; }; @@ -382,8 +414,6 @@ EF8EE3AC1EBBA5D00033E7A1 /* ApptentiveJWT.m in Sources */ = {isa = PBXBuildFile; fileRef = EF8EE3AA1EBBA5D00033E7A1 /* ApptentiveJWT.m */; }; EF9009901F8D370400DC5B56 /* ApptentiveLogMonitor.h in Headers */ = {isa = PBXBuildFile; fileRef = EF90098E1F8D370400DC5B56 /* ApptentiveLogMonitor.h */; }; EF9009911F8D370400DC5B56 /* ApptentiveLogMonitor.m in Sources */ = {isa = PBXBuildFile; fileRef = EF90098F1F8D370400DC5B56 /* ApptentiveLogMonitor.m */; }; - EF9009941F8D64B800DC5B56 /* ApptentiveLogWriter.h in Headers */ = {isa = PBXBuildFile; fileRef = EF9009921F8D64B800DC5B56 /* ApptentiveLogWriter.h */; }; - EF9009951F8D64B800DC5B56 /* ApptentiveLogWriter.m in Sources */ = {isa = PBXBuildFile; fileRef = EF9009931F8D64B800DC5B56 /* ApptentiveLogWriter.m */; }; EFC308D21EC52915006B6D36 /* ApptentiveStopWatch.h in Headers */ = {isa = PBXBuildFile; fileRef = EFC308D01EC52915006B6D36 /* ApptentiveStopWatch.h */; }; EFC308D31EC52915006B6D36 /* ApptentiveStopWatch.m in Sources */ = {isa = PBXBuildFile; fileRef = EFC308D11EC52915006B6D36 /* ApptentiveStopWatch.m */; }; EFC308D71ECA6692006B6D36 /* ApptentiveLegacyConversationRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = EFC308D51ECA6692006B6D36 /* ApptentiveLegacyConversationRequest.h */; }; @@ -392,6 +422,8 @@ EFC97A9F1F56303300BCC461 /* UIAlertController+Apptentive.m in Sources */ = {isa = PBXBuildFile; fileRef = EFC97A9D1F56303300BCC461 /* UIAlertController+Apptentive.m */; }; EFC97AAE1F58818900BCC461 /* ApptentiveStoreProductViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = EFC97AAC1F58818900BCC461 /* ApptentiveStoreProductViewController.h */; }; EFC97AAF1F58818900BCC461 /* ApptentiveStoreProductViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = EFC97AAD1F58818900BCC461 /* ApptentiveStoreProductViewController.m */; }; + EFEF922E203F7DAA000B3C1B /* ApptentiveAsyncLogWriter.h in Headers */ = {isa = PBXBuildFile; fileRef = EFEF922C203F7DAA000B3C1B /* ApptentiveAsyncLogWriter.h */; }; + EFEF922F203F7DAA000B3C1B /* ApptentiveAsyncLogWriter.m in Sources */ = {isa = PBXBuildFile; fileRef = EFEF922D203F7DAA000B3C1B /* ApptentiveAsyncLogWriter.m */; }; EFF3FE3B1E8C2F05006AD2B4 /* ApptentiveLog.m in Sources */ = {isa = PBXBuildFile; fileRef = EFF3FE3A1E8C2F05006AD2B4 /* ApptentiveLog.m */; }; EFF3FE421E8C386E006AD2B4 /* ApptentiveLogTag.h in Headers */ = {isa = PBXBuildFile; fileRef = EFF3FE401E8C386E006AD2B4 /* ApptentiveLogTag.h */; }; EFF3FE431E8C386E006AD2B4 /* ApptentiveLogTag.m in Sources */ = {isa = PBXBuildFile; fileRef = EFF3FE411E8C386E006AD2B4 /* ApptentiveLogTag.m */; }; @@ -418,9 +450,19 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 010FE33A203E2D900021C246 /* CriteriaDescriptionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CriteriaDescriptionTests.swift; sourceTree = ""; }; + 010FE344203E4C810021C246 /* ApptentiveIndentPrinter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ApptentiveIndentPrinter.h; sourceTree = ""; }; + 010FE345203E4C810021C246 /* ApptentiveIndentPrinter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ApptentiveIndentPrinter.m; sourceTree = ""; }; + 010FE34B2040C8800021C246 /* testOperatorStringEquals.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = testOperatorStringEquals.json; sourceTree = ""; }; + 010FE34C2040C8800021C246 /* testOperatorStringNotEquals.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = testOperatorStringNotEquals.json; sourceTree = ""; }; 0116D5251E92F516001DA5CF /* ApptentiveMessageStore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ApptentiveMessageStore.h; sourceTree = ""; }; 0116D5261E92F516001DA5CF /* ApptentiveMessageStore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ApptentiveMessageStore.m; sourceTree = ""; }; + 01201AD21FC637BD00EB3593 /* CodePointAndInteractionTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CodePointAndInteractionTests.m; sourceTree = ""; }; 01216C4F1EBBB53E0062BD0D /* RequestTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestTests.swift; sourceTree = ""; }; + 0123005E20531698000EC3C3 /* ClauseTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ClauseTests.m; sourceTree = ""; }; + 012ED9272072F33F003D87F3 /* ApptentiveRetryPolicy.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ApptentiveRetryPolicy.h; sourceTree = ""; }; + 012ED9282072F33F003D87F3 /* ApptentiveRetryPolicy.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ApptentiveRetryPolicy.m; sourceTree = ""; }; + 012ED92B2072FABE003D87F3 /* RetryPolicyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetryPolicyTests.swift; sourceTree = ""; }; 0134EB101EA991AE00DA4925 /* ApptentiveLogoutPayload.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ApptentiveLogoutPayload.h; sourceTree = ""; }; 0134EB111EA991AE00DA4925 /* ApptentiveLogoutPayload.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ApptentiveLogoutPayload.m; sourceTree = ""; }; 0134EB141EA992F900DA4925 /* ApptentiveSDKAppReleasePayload.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ApptentiveSDKAppReleasePayload.h; sourceTree = ""; }; @@ -470,9 +512,25 @@ 017477241EA92BED00A0A949 /* ApptentiveSurveyResponsePayload.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ApptentiveSurveyResponsePayload.h; sourceTree = ""; }; 017477251EA92BED00A0A949 /* ApptentiveSurveyResponsePayload.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ApptentiveSurveyResponsePayload.m; sourceTree = ""; }; 0174772E1EA92D7C00A0A949 /* PayloadTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PayloadTests.swift; sourceTree = ""; }; + 0178C55C1FC49E0600DABF39 /* ApptentiveTargets.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ApptentiveTargets.h; sourceTree = ""; }; + 0178C55D1FC49E0600DABF39 /* ApptentiveTargets.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ApptentiveTargets.m; sourceTree = ""; }; + 0178C5601FC49F1000DABF39 /* ApptentiveTarget.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ApptentiveTarget.h; sourceTree = ""; }; + 0178C5611FC49F1000DABF39 /* ApptentiveTarget.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ApptentiveTarget.m; sourceTree = ""; }; + 0178C5691FC4A5A000DABF39 /* ApptentiveClause.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ApptentiveClause.h; sourceTree = ""; }; + 0178C56A1FC4A5A000DABF39 /* ApptentiveClause.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ApptentiveClause.m; sourceTree = ""; }; 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 = ""; }; + 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 = ""; }; + 018FAFE21FC4AC41007C52FE /* ApptentiveOrClause.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ApptentiveOrClause.m; sourceTree = ""; }; + 018FAFE51FC4AD0D007C52FE /* ApptentiveFalseClause.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ApptentiveFalseClause.h; sourceTree = ""; }; + 018FAFE61FC4AD0D007C52FE /* ApptentiveFalseClause.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ApptentiveFalseClause.m; sourceTree = ""; }; + 018FAFE91FC4AF8F007C52FE /* ApptentiveNotClause.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ApptentiveNotClause.h; sourceTree = ""; }; + 018FAFEA1FC4AF8F007C52FE /* ApptentiveNotClause.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ApptentiveNotClause.m; sourceTree = ""; }; + 018FAFED1FC4B21A007C52FE /* ApptentiveComparisonClause.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ApptentiveComparisonClause.h; sourceTree = ""; }; + 018FAFEE1FC4B21A007C52FE /* ApptentiveComparisonClause.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ApptentiveComparisonClause.m; sourceTree = ""; }; 01917D431E5E0B7400B37D82 /* ApptentiveTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ApptentiveTests-Bridging-Header.h"; sourceTree = ""; }; 01917D441E5E0B7400B37D82 /* ConversationManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConversationManagerTests.swift; sourceTree = ""; }; 01A2CF911E49062700C2103A /* Apptentive.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Apptentive.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -529,10 +587,6 @@ 01A2CFE61E490A9700C2103A /* ApptentiveEngagementManifest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ApptentiveEngagementManifest.m; sourceTree = ""; }; 01A2CFE71E490A9700C2103A /* ApptentiveInteraction.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ApptentiveInteraction.h; sourceTree = ""; }; 01A2CFE81E490A9700C2103A /* ApptentiveInteraction.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ApptentiveInteraction.m; sourceTree = ""; }; - 01A2CFE91E490A9700C2103A /* ApptentiveInteractionInvocation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ApptentiveInteractionInvocation.h; sourceTree = ""; }; - 01A2CFEA1E490A9700C2103A /* ApptentiveInteractionInvocation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ApptentiveInteractionInvocation.m; sourceTree = ""; }; - 01A2CFEB1E490A9700C2103A /* ApptentiveInteractionUsageData.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ApptentiveInteractionUsageData.h; sourceTree = ""; }; - 01A2CFEC1E490A9700C2103A /* ApptentiveInteractionUsageData.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ApptentiveInteractionUsageData.m; sourceTree = ""; }; 01A2CFF51E490A9700C2103A /* ApptentivePerson.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ApptentivePerson.h; sourceTree = ""; }; 01A2CFF61E490A9700C2103A /* ApptentivePerson.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ApptentivePerson.m; sourceTree = ""; }; 01A2CFF71E490A9700C2103A /* ApptentiveSDK.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ApptentiveSDK.h; sourceTree = ""; }; @@ -727,16 +781,12 @@ 01A2D0F71E490A9700C2103A /* ApptentiveSurveySubmitButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ApptentiveSurveySubmitButton.m; sourceTree = ""; }; 01A2D2011E4946D500C2103A /* ApptentiveConnectTests-Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ApptentiveConnectTests-Prefix.pch"; sourceTree = ""; }; 01A2D2021E4946D500C2103A /* ApptentiveConnectTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ApptentiveConnectTests.m; sourceTree = ""; }; - 01A2D2031E4946D500C2103A /* ApptentiveEngagementTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ApptentiveEngagementTests.m; sourceTree = ""; }; - 01A2D2041E4946D500C2103A /* ApptentiveInteractionInvocationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ApptentiveInteractionInvocationTests.m; sourceTree = ""; }; - 01A2D2051E4946D500C2103A /* ApptentiveInteractionUsageDataTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ApptentiveInteractionUsageDataTests.m; sourceTree = ""; }; 01A2D2061E4946D500C2103A /* ApptentiveMetricsTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ApptentiveMetricsTests.m; sourceTree = ""; }; 01A2D2071E4946D500C2103A /* ApptentiveMigrationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ApptentiveMigrationTests.m; sourceTree = ""; }; 01A2D2081E4946D500C2103A /* ApptentiveConversationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ApptentiveConversationTests.m; sourceTree = ""; }; 01A2D2091E4946D500C2103A /* ApptentiveStyleSheetTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ApptentiveStyleSheetTests.m; sourceTree = ""; }; 01A2D20A1E4946D500C2103A /* ApptentiveSurveyTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ApptentiveSurveyTests.m; sourceTree = ""; }; 01A2D20B1E4946D500C2103A /* ApptentiveUtilitiesTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ApptentiveUtilitiesTests.m; sourceTree = ""; }; - 01A2D20C1E4946D500C2103A /* CodePointAndInteractionTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CodePointAndInteractionTests.m; sourceTree = ""; }; 01A2D20D1E4946D500C2103A /* CriteriaTests.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CriteriaTests.h; sourceTree = ""; }; 01A2D20E1E4946D500C2103A /* CriteriaTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CriteriaTests.m; sourceTree = ""; }; 01A2D2101E4946D500C2103A /* ATDataModelv1.sqlite */ = {isa = PBXFileReference; lastKnownFileType = file; path = ATDataModelv1.sqlite; sourceTree = ""; }; @@ -781,6 +831,8 @@ 01A2D29E1E4963F700C2103A /* v2CorruptDatabase */ = {isa = PBXFileReference; lastKnownFileType = folder; path = v2CorruptDatabase; sourceTree = ""; }; 01A2D29F1E4963F700C2103A /* v2WALDatabase */ = {isa = PBXFileReference; lastKnownFileType = folder; path = v2WALDatabase; sourceTree = ""; }; 01A2D2A01E4963F700C2103A /* v3WALDatabase */ = {isa = PBXFileReference; lastKnownFileType = folder; path = v3WALDatabase; sourceTree = ""; }; + 01A50EC21FC5FFB00058C06C /* ApptentiveInvocations.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ApptentiveInvocations.h; sourceTree = ""; }; + 01A50EC31FC5FFB00058C06C /* ApptentiveInvocations.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ApptentiveInvocations.m; sourceTree = ""; }; 01AA89B61F18177E00FB59AB /* ApptentiveAppInstall.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ApptentiveAppInstall.h; sourceTree = ""; }; 01AA89B71F18177E00FB59AB /* ApptentiveAppInstall.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ApptentiveAppInstall.m; sourceTree = ""; }; 01E04F9C1E819CD300D7E849 /* ApptentiveMessageManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ApptentiveMessageManager.h; sourceTree = ""; }; @@ -809,6 +861,19 @@ EF43D8A91FD5FE6D00C0FF9F /* ApptentiveGCDDispatchQueue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ApptentiveGCDDispatchQueue.h; sourceTree = ""; }; EF49AC921EF2C50B00E2187F /* NSMutableData+Types.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSMutableData+Types.h"; sourceTree = ""; }; EF49AC931EF2C50B00E2187F /* NSMutableData+Types.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSMutableData+Types.m"; sourceTree = ""; }; + EF4EAB8B203F876A003318C9 /* ApptentiveDispatchTask.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ApptentiveDispatchTask.h; sourceTree = ""; }; + EF4EAB8C203F876A003318C9 /* ApptentiveDispatchTask.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ApptentiveDispatchTask.m; sourceTree = ""; }; + EF4EAB8F203F8A57003318C9 /* ApptentiveDispatchTask+Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ApptentiveDispatchTask+Internal.h"; sourceTree = ""; }; + EF4EAB93203F8B99003318C9 /* ApptentiveLogFileWriteTask.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ApptentiveLogFileWriteTask.h; sourceTree = ""; }; + EF4EAB94203F8B99003318C9 /* ApptentiveLogFileWriteTask.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ApptentiveLogFileWriteTask.m; sourceTree = ""; }; + EF4EAB99203F9821003318C9 /* AppptentiveAsyncLogWriterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppptentiveAsyncLogWriterTests.swift; sourceTree = ""; }; + EF4EAB9B20409199003318C9 /* ApptentiveMockDispatchQueue.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ApptentiveMockDispatchQueue.h; sourceTree = ""; }; + EF4EAB9C20409199003318C9 /* ApptentiveMockDispatchQueue.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ApptentiveMockDispatchQueue.m; sourceTree = ""; }; + EF4EAB9E2040A194003318C9 /* ApptentiveFileUtilities.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ApptentiveFileUtilities.h; sourceTree = ""; }; + EF4EAB9F2040A194003318C9 /* ApptentiveFileUtilities.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ApptentiveFileUtilities.m; sourceTree = ""; }; + EF4EABA32040A8D6003318C9 /* TestUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestUtils.swift; sourceTree = ""; }; + EF4EABA92040C0CF003318C9 /* ApptentiveLogMonitorSession.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ApptentiveLogMonitorSession.h; sourceTree = ""; }; + EF4EABAA2040C0CF003318C9 /* ApptentiveLogMonitorSession.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ApptentiveLogMonitorSession.m; sourceTree = ""; }; EF8EE3961EB3F37B0033E7A1 /* ApptentiveConversationBaseRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ApptentiveConversationBaseRequest.h; sourceTree = ""; }; EF8EE3971EB3F37B0033E7A1 /* ApptentiveConversationBaseRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ApptentiveConversationBaseRequest.m; sourceTree = ""; }; EF8EE39A1EB3FB120033E7A1 /* ApptentiveAssert.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ApptentiveAssert.m; sourceTree = ""; }; @@ -817,8 +882,6 @@ EF8EE3AA1EBBA5D00033E7A1 /* ApptentiveJWT.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ApptentiveJWT.m; sourceTree = ""; }; EF90098E1F8D370400DC5B56 /* ApptentiveLogMonitor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ApptentiveLogMonitor.h; sourceTree = ""; }; EF90098F1F8D370400DC5B56 /* ApptentiveLogMonitor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ApptentiveLogMonitor.m; sourceTree = ""; }; - EF9009921F8D64B800DC5B56 /* ApptentiveLogWriter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ApptentiveLogWriter.h; sourceTree = ""; }; - EF9009931F8D64B800DC5B56 /* ApptentiveLogWriter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ApptentiveLogWriter.m; sourceTree = ""; }; EFC308D01EC52915006B6D36 /* ApptentiveStopWatch.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ApptentiveStopWatch.h; sourceTree = ""; }; EFC308D11EC52915006B6D36 /* ApptentiveStopWatch.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ApptentiveStopWatch.m; sourceTree = ""; }; EFC308D51ECA6692006B6D36 /* ApptentiveLegacyConversationRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ApptentiveLegacyConversationRequest.h; sourceTree = ""; }; @@ -827,6 +890,8 @@ EFC97A9D1F56303300BCC461 /* UIAlertController+Apptentive.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIAlertController+Apptentive.m"; sourceTree = ""; }; EFC97AAC1F58818900BCC461 /* ApptentiveStoreProductViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ApptentiveStoreProductViewController.h; sourceTree = ""; }; EFC97AAD1F58818900BCC461 /* ApptentiveStoreProductViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ApptentiveStoreProductViewController.m; sourceTree = ""; }; + EFEF922C203F7DAA000B3C1B /* ApptentiveAsyncLogWriter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ApptentiveAsyncLogWriter.h; sourceTree = ""; }; + EFEF922D203F7DAA000B3C1B /* ApptentiveAsyncLogWriter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ApptentiveAsyncLogWriter.m; sourceTree = ""; }; EFF3FE3A1E8C2F05006AD2B4 /* ApptentiveLog.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ApptentiveLog.m; sourceTree = ""; }; EFF3FE401E8C386E006AD2B4 /* ApptentiveLogTag.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ApptentiveLogTag.h; sourceTree = ""; }; EFF3FE411E8C386E006AD2B4 /* ApptentiveLogTag.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ApptentiveLogTag.m; sourceTree = ""; }; @@ -905,6 +970,31 @@ path = "Requests & Payloads"; sourceTree = ""; }; + 0178C5681FC4A57900DABF39 /* Targeting */ = { + isa = PBXGroup; + children = ( + 0178C55C1FC49E0600DABF39 /* ApptentiveTargets.h */, + 0178C55D1FC49E0600DABF39 /* ApptentiveTargets.m */, + 01A50EC21FC5FFB00058C06C /* ApptentiveInvocations.h */, + 01A50EC31FC5FFB00058C06C /* ApptentiveInvocations.m */, + 0178C5601FC49F1000DABF39 /* ApptentiveTarget.h */, + 0178C5611FC49F1000DABF39 /* ApptentiveTarget.m */, + 0178C5691FC4A5A000DABF39 /* ApptentiveClause.h */, + 0178C56A1FC4A5A000DABF39 /* ApptentiveClause.m */, + 018FAFE51FC4AD0D007C52FE /* ApptentiveFalseClause.h */, + 018FAFE61FC4AD0D007C52FE /* ApptentiveFalseClause.m */, + 018FAFDD1FC4A9C6007C52FE /* ApptentiveAndClause.h */, + 018FAFDE1FC4A9C6007C52FE /* ApptentiveAndClause.m */, + 018FAFE11FC4AC41007C52FE /* ApptentiveOrClause.h */, + 018FAFE21FC4AC41007C52FE /* ApptentiveOrClause.m */, + 018FAFE91FC4AF8F007C52FE /* ApptentiveNotClause.h */, + 018FAFEA1FC4AF8F007C52FE /* ApptentiveNotClause.m */, + 018FAFED1FC4B21A007C52FE /* ApptentiveComparisonClause.h */, + 018FAFEE1FC4B21A007C52FE /* ApptentiveComparisonClause.m */, + ); + path = Targeting; + sourceTree = ""; + }; 01A2CF871E49062700C2103A = { isa = PBXGroup; children = ( @@ -953,7 +1043,11 @@ 01A2CF9E1E49062800C2103A /* ApptentiveTests */ = { isa = PBXGroup; children = ( + 01201AD21FC637BD00EB3593 /* CodePointAndInteractionTests.m */, + EF4EABA22040A8C5003318C9 /* utils */, 01A2D20F1E4946D500C2103A /* data */, + EF4EAB9B20409199003318C9 /* ApptentiveMockDispatchQueue.h */, + EF4EAB9C20409199003318C9 /* ApptentiveMockDispatchQueue.m */, EFF4D2AA1EC39EC000FD4EFE /* ApptentiveAppDataContainer.h */, EFF4D2AB1EC39EC000FD4EFE /* ApptentiveAppDataContainer.m */, 01A2D2011E4946D500C2103A /* ApptentiveConnectTests-Prefix.pch */, @@ -961,9 +1055,6 @@ EFF4D2A81EC3806300FD4EFE /* ApptentiveConversationMigrationTests.m */, 01A2D2081E4946D500C2103A /* ApptentiveConversationTests.m */, 01626EBB1EB7E5B90064E73F /* ApptentiveEncryptionTests.m */, - 01A2D2031E4946D500C2103A /* ApptentiveEngagementTests.m */, - 01A2D2041E4946D500C2103A /* ApptentiveInteractionInvocationTests.m */, - 01A2D2051E4946D500C2103A /* ApptentiveInteractionUsageDataTests.m */, 017E54EC1F3B860E00EA9F81 /* ApptentiveJSONSerializationTests.m */, 01A2D2061E4946D500C2103A /* ApptentiveMetricsTests.m */, 01A2D2071E4946D500C2103A /* ApptentiveMigrationTests.m */, @@ -971,13 +1062,16 @@ 01A2D20A1E4946D500C2103A /* ApptentiveSurveyTests.m */, 01917D431E5E0B7400B37D82 /* ApptentiveTests-Bridging-Header.h */, 01A2D20B1E4946D500C2103A /* ApptentiveUtilitiesTests.m */, - 01A2D20C1E4946D500C2103A /* CodePointAndInteractionTests.m */, 01917D441E5E0B7400B37D82 /* ConversationManagerTests.swift */, 01A2D20D1E4946D500C2103A /* CriteriaTests.h */, 01A2D20E1E4946D500C2103A /* CriteriaTests.m */, 01A2CFA11E49062800C2103A /* Info.plist */, 0174772E1EA92D7C00A0A949 /* PayloadTests.swift */, 01216C4F1EBBB53E0062BD0D /* RequestTests.swift */, + 010FE33A203E2D900021C246 /* CriteriaDescriptionTests.swift */, + 0123005E20531698000EC3C3 /* ClauseTests.m */, + EF4EAB99203F9821003318C9 /* AppptentiveAsyncLogWriterTests.swift */, + 012ED92B2072FABE003D87F3 /* RetryPolicyTests.swift */, ); path = ApptentiveTests; sourceTree = ""; @@ -1072,18 +1166,10 @@ 01A2CFDA1E490A9700C2103A /* Model */ = { isa = PBXGroup; children = ( - 01AA89B61F18177E00FB59AB /* ApptentiveAppInstall.h */, - 01AA89B71F18177E00FB59AB /* ApptentiveAppInstall.m */, 01A2CFDB1E490A9700C2103A /* ApptentiveAppRelease.h */, 01A2CFDC1E490A9700C2103A /* ApptentiveAppRelease.m */, 01A2CFF91E490A9700C2103A /* ApptentiveConversation.h */, 01A2CFFA1E490A9700C2103A /* ApptentiveConversation.m */, - 01F1DFF21E5BBFB3009AB3D2 /* ApptentiveConversationManager.h */, - 01F1DFF31E5BBFB3009AB3D2 /* ApptentiveConversationManager.m */, - 01F1DFF61E5BBFBD009AB3D2 /* ApptentiveConversationMetadata.h */, - 01F1DFF71E5BBFBD009AB3D2 /* ApptentiveConversationMetadata.m */, - 01F1DFFA1E5BBFCB009AB3D2 /* ApptentiveConversationMetadataItem.h */, - 01F1DFFB1E5BBFCB009AB3D2 /* ApptentiveConversationMetadataItem.m */, 01A2CFDD1E490A9700C2103A /* ApptentiveCount.h */, 01A2CFDE1E490A9700C2103A /* ApptentiveCount.m */, 01A2CFDF1E490A9700C2103A /* ApptentiveCustomData.h */, @@ -1096,10 +1182,6 @@ 01A2CFE61E490A9700C2103A /* ApptentiveEngagementManifest.m */, 01A2CFE71E490A9700C2103A /* ApptentiveInteraction.h */, 01A2CFE81E490A9700C2103A /* ApptentiveInteraction.m */, - 01A2CFE91E490A9700C2103A /* ApptentiveInteractionInvocation.h */, - 01A2CFEA1E490A9700C2103A /* ApptentiveInteractionInvocation.m */, - 01A2CFEB1E490A9700C2103A /* ApptentiveInteractionUsageData.h */, - 01A2CFEC1E490A9700C2103A /* ApptentiveInteractionUsageData.m */, 01A2CFF51E490A9700C2103A /* ApptentivePerson.h */, 01A2CFF61E490A9700C2103A /* ApptentivePerson.m */, 01A2CFF71E490A9700C2103A /* ApptentiveSDK.h */, @@ -1108,6 +1190,15 @@ 01A2CFFC1E490A9700C2103A /* ApptentiveState.m */, 01A2CFFD1E490A9700C2103A /* ApptentiveVersion.h */, 01A2CFFE1E490A9700C2103A /* ApptentiveVersion.m */, + 01F1DFF21E5BBFB3009AB3D2 /* ApptentiveConversationManager.h */, + 01F1DFF31E5BBFB3009AB3D2 /* ApptentiveConversationManager.m */, + 01F1DFF61E5BBFBD009AB3D2 /* ApptentiveConversationMetadata.h */, + 01F1DFF71E5BBFBD009AB3D2 /* ApptentiveConversationMetadata.m */, + 01F1DFFA1E5BBFCB009AB3D2 /* ApptentiveConversationMetadataItem.h */, + 01F1DFFB1E5BBFCB009AB3D2 /* ApptentiveConversationMetadataItem.m */, + 01AA89B61F18177E00FB59AB /* ApptentiveAppInstall.h */, + 01AA89B71F18177E00FB59AB /* ApptentiveAppInstall.m */, + 0178C5681FC4A57900DABF39 /* Targeting */, ); path = Model; sourceTree = ""; @@ -1386,6 +1477,9 @@ EF37099C1EDE19F300CFC5E5 /* ApptentiveDefines.h */, EF43D89C1FD5EBBA00C0FF9F /* ApptentiveDispatchQueue.h */, EF43D89D1FD5EBBB00C0FF9F /* ApptentiveDispatchQueue.m */, + EF4EAB8B203F876A003318C9 /* ApptentiveDispatchTask.h */, + EF4EAB8F203F8A57003318C9 /* ApptentiveDispatchTask+Internal.h */, + EF4EAB8C203F876A003318C9 /* ApptentiveDispatchTask.m */, EF43D8A91FD5FE6D00C0FF9F /* ApptentiveGCDDispatchQueue.h */, EF43D8A81FD5FE6D00C0FF9F /* ApptentiveGCDDispatchQueue.m */, 01A2D08B1E490A9700C2103A /* ApptentiveJSONSerialization.h */, @@ -1396,10 +1490,10 @@ EFF3FE3A1E8C2F05006AD2B4 /* ApptentiveLog.m */, EF90098E1F8D370400DC5B56 /* ApptentiveLogMonitor.h */, EF90098F1F8D370400DC5B56 /* ApptentiveLogMonitor.m */, + EF4EABA92040C0CF003318C9 /* ApptentiveLogMonitorSession.h */, + EF4EABAA2040C0CF003318C9 /* ApptentiveLogMonitorSession.m */, EFF3FE401E8C386E006AD2B4 /* ApptentiveLogTag.h */, EFF3FE411E8C386E006AD2B4 /* ApptentiveLogTag.m */, - EF9009921F8D64B800DC5B56 /* ApptentiveLogWriter.h */, - EF9009931F8D64B800DC5B56 /* ApptentiveLogWriter.m */, EF1812491E8084C80053BD68 /* ApptentiveSafeCollections.h */, EF18124A1E8084C80053BD68 /* ApptentiveSafeCollections.m */, EFC308D01EC52915006B6D36 /* ApptentiveStopWatch.h */, @@ -1416,6 +1510,14 @@ EF49AC931EF2C50B00E2187F /* NSMutableData+Types.m */, EFC97A9C1F56303300BCC461 /* UIAlertController+Apptentive.h */, EFC97A9D1F56303300BCC461 /* UIAlertController+Apptentive.m */, + 010FE344203E4C810021C246 /* ApptentiveIndentPrinter.h */, + 010FE345203E4C810021C246 /* ApptentiveIndentPrinter.m */, + EFEF922C203F7DAA000B3C1B /* ApptentiveAsyncLogWriter.h */, + EFEF922D203F7DAA000B3C1B /* ApptentiveAsyncLogWriter.m */, + EF4EAB93203F8B99003318C9 /* ApptentiveLogFileWriteTask.h */, + EF4EAB94203F8B99003318C9 /* ApptentiveLogFileWriteTask.m */, + EF4EAB9E2040A194003318C9 /* ApptentiveFileUtilities.h */, + EF4EAB9F2040A194003318C9 /* ApptentiveFileUtilities.m */, ); path = Misc; sourceTree = ""; @@ -1464,6 +1566,8 @@ 01A2D0C31E490A9700C2103A /* ApptentiveSerialRequest.m */, 01A2D0C41E490A9700C2103A /* ApptentiveSerialRequestAttachment.h */, 01A2D0C51E490A9700C2103A /* ApptentiveSerialRequestAttachment.m */, + 012ED9272072F33F003D87F3 /* ApptentiveRetryPolicy.h */, + 012ED9282072F33F003D87F3 /* ApptentiveRetryPolicy.m */, ); path = Networking; sourceTree = ""; @@ -1583,6 +1687,8 @@ 01A2D2161E4946D500C2103A /* criteria */ = { isa = PBXGroup; children = ( + 010FE34B2040C8800021C246 /* testOperatorStringEquals.json */, + 010FE34C2040C8800021C246 /* testOperatorStringNotEquals.json */, 01A2D2171E4946D500C2103A /* testCodePointInvokesTotal.json */, 01A2D2181E4946D500C2103A /* testCodePointInvokesVersion.json */, 01A2D2191E4946D500C2103A /* testCodePointLastInvokedAt.json */, @@ -1645,6 +1751,14 @@ path = Model; sourceTree = ""; }; + EF4EABA22040A8C5003318C9 /* utils */ = { + isa = PBXGroup; + children = ( + EF4EABA32040A8D6003318C9 /* TestUtils.swift */, + ); + path = utils; + sourceTree = ""; + }; EFF4D2A51EC37F0A00FD4EFE /* containers */ = { isa = PBXGroup; children = ( @@ -1664,26 +1778,30 @@ 01A2D1391E490A9700C2103A /* ApptentivePerson.h in Headers */, 01A2D13F1E490A9700C2103A /* ApptentiveState.h in Headers */, EFC308D71ECA6692006B6D36 /* ApptentiveLegacyConversationRequest.h in Headers */, + EF4EAB91203F8A57003318C9 /* ApptentiveDispatchTask+Internal.h in Headers */, 01A2D1781E490A9700C2103A /* ApptentiveAboutViewController.h in Headers */, 01A2D17E1E490A9700C2103A /* ApptentiveMessageCenterErrorViewController.h in Headers */, EFF3FE421E8C386E006AD2B4 /* ApptentiveLogTag.h in Headers */, 01A2D1981E490A9700C2103A /* ApptentiveMessageCenterReplyCell.h in Headers */, EFC97AAE1F58818900BCC461 /* ApptentiveStoreProductViewController.h in Headers */, + 018FAFE71FC4AD0D007C52FE /* ApptentiveFalseClause.h in Headers */, + 01A50EC41FC5FFB00058C06C /* ApptentiveInvocations.h in Headers */, 014508941EAAA7A1003326E7 /* ApptentiveRequest.h in Headers */, 01A2D1431E490A9700C2103A /* ApptentiveBackend+Engagement.h in Headers */, 0145089C1EAAB0F1003326E7 /* ApptentiveConfigurationRequest.h in Headers */, 0116D5271E92F516001DA5CF /* ApptentiveMessageStore.h in Headers */, 017477261EA92BED00A0A949 /* ApptentiveEventPayload.h in Headers */, 01A2D18D1E490A9700C2103A /* ApptentiveMessageCenterCellProtocols.h in Headers */, + 0178C5621FC49F1000DABF39 /* ApptentiveTarget.h in Headers */, 01A2D0F81E490A9700C2103A /* Apptentive_Private.h in Headers */, 01A2D1DE1E490A9700C2103A /* ApptentiveSurveyAnswer.h in Headers */, 01E8E2361E6E096D00786738 /* ApptentiveInteractionAppleRatingDialogController.h in Headers */, - 01A2D12F1E490A9700C2103A /* ApptentiveInteractionUsageData.h in Headers */, 01A2D1DA1E490A9700C2103A /* ApptentiveReachability.h in Headers */, 01A2D1851E490A9700C2103A /* ApptentiveAttachmentCell.h in Headers */, 01A2D1191E490A9700C2103A /* ApptentiveInteractionSurveyController.h in Headers */, 01A2D1901E490A9700C2103A /* ApptentiveMessageCenterGreetingView.h in Headers */, 01A2D10D1E490A9700C2103A /* ApptentiveInteractionNavigateToLink.h in Headers */, + 0178C55E1FC49E0600DABF39 /* ApptentiveTargets.h in Headers */, 01A2D1761E490A9700C2103A /* ApptentiveMessageCenterViewModel.h in Headers */, EF3063A91F2A8353004B4EB9 /* ApptentivePayloadDebug.h in Headers */, 01F1DFFC1E5BBFCB009AB3D2 /* ApptentiveConversationMetadataItem.h in Headers */, @@ -1692,6 +1810,7 @@ 01A2D18E1E490A9700C2103A /* ApptentiveMessageCenterContextMessageCell.h in Headers */, 01A2D1B41E490A9700C2103A /* ApptentiveLegacySurveyResponse.h in Headers */, 4D4B36241F01892A005C7EC0 /* ApptentiveEngagementBackend.h in Headers */, + EF4EAB95203F8B99003318C9 /* ApptentiveLogFileWriteTask.h in Headers */, 0140D5FE1E835420007B5130 /* ApptentiveAttachment.h in Headers */, 0140D60C1E83553C007B5130 /* ApptentiveLegacyMessage.h in Headers */, 015408A41E54D8810027E196 /* ApptentiveTableView.h in Headers */, @@ -1700,6 +1819,9 @@ EF8EE3981EB3F37B0033E7A1 /* ApptentiveConversationBaseRequest.h in Headers */, 01F1DFF81E5BBFBD009AB3D2 /* ApptentiveConversationMetadata.h in Headers */, 01A2D1AA1E490A9700C2103A /* ApptentiveAppConfiguration.h in Headers */, + 0178C56B1FC4A5A000DABF39 /* ApptentiveClause.h in Headers */, + 012ED9292072F33F003D87F3 /* ApptentiveRetryPolicy.h in Headers */, + EF4EABAB2040C0CF003318C9 /* ApptentiveLogMonitorSession.h in Headers */, EF9009901F8D370400DC5B56 /* ApptentiveLogMonitor.h in Headers */, EF8EE3AB1EBBA5D00033E7A1 /* ApptentiveJWT.h in Headers */, 01A2D1D81E490A9700C2103A /* ApptentiveDataManager.h in Headers */, @@ -1741,7 +1863,9 @@ EFC97A9E1F56303300BCC461 /* UIAlertController+Apptentive.h in Headers */, 01F1DFF41E5BBFB3009AB3D2 /* ApptentiveConversationManager.h in Headers */, 0174772A1EA92BED00A0A949 /* ApptentivePayload.h in Headers */, + EF4EABA02040A194003318C9 /* ApptentiveFileUtilities.h in Headers */, 01A2D11B1E490A9700C2103A /* ApptentiveInteractionUpgradeMessageController.h in Headers */, + 018FAFEF1FC4B21A007C52FE /* ApptentiveComparisonClause.h in Headers */, 01A2D0FB1E490A9700C2103A /* ApptentiveStyleSheet.h in Headers */, 0134EB121EA991AE00DA4925 /* ApptentiveLogoutPayload.h in Headers */, 01A2D18B1E490A9700C2103A /* ApptentiveIndexedCollectionView.h in Headers */, @@ -1751,8 +1875,11 @@ 01A2D1A31E490A9700C2103A /* ApptentiveLog.h in Headers */, EF43D89E1FD5EBBB00C0FF9F /* ApptentiveDispatchQueue.h in Headers */, 01A2D1891E490A9700C2103A /* ApptentiveCompoundReplyCell.h in Headers */, + 018FAFEB1FC4AF8F007C52FE /* ApptentiveNotClause.h in Headers */, 01A2D1E01E490A9700C2103A /* ApptentiveSurveyQuestion.h in Headers */, + 010FE346203E4C810021C246 /* ApptentiveIndentPrinter.h in Headers */, 01A2D1801E490A9700C2103A /* ApptentiveMessageCenterViewController.h in Headers */, + EFEF922E203F7DAA000B3C1B /* ApptentiveAsyncLogWriter.h in Headers */, 01A2D1291E490A9700C2103A /* ApptentiveEngagementManifest.h in Headers */, 01A2D19A1E490A9700C2103A /* ApptentiveMessageCenterStatusView.h in Headers */, EF18124B1E8084C80053BD68 /* ApptentiveSafeCollections.h in Headers */, @@ -1763,6 +1890,7 @@ 01A2D1071E490A9700C2103A /* ApptentiveInteractionController.h in Headers */, 01A2D1E41E490A9700C2103A /* ApptentiveSurveyLayoutAttributes.h in Headers */, EF43D8AB1FD5FE6D00C0FF9F /* ApptentiveGCDDispatchQueue.h in Headers */, + EF4EAB8D203F876A003318C9 /* ApptentiveDispatchTask.h in Headers */, 01A2D1111E490A9700C2103A /* ApptentiveInteractionAppStoreController.h in Headers */, 01A2D1211E490A9700C2103A /* ApptentiveCount.h in Headers */, 017477281EA92BED00A0A949 /* ApptentiveMessagePayload.h in Headers */, @@ -1770,6 +1898,7 @@ 01A2D1DC1E490A9700C2103A /* ApptentiveSurvey.h in Headers */, 01A2D1011E490A9700C2103A /* ApptentiveNetworkImageView.h in Headers */, 01A2D1EE1E490A9700C2103A /* ApptentiveSurveyCollectionView.h in Headers */, + 018FAFE31FC4AC41007C52FE /* ApptentiveOrClause.h in Headers */, 01A2D19C1E490A9700C2103A /* ApptentiveProgressNavigationBar.h in Headers */, 01A2D1F61E490A9700C2103A /* ApptentiveSurveyQuestionBackgroundView.h in Headers */, 01A2D10F1E490A9700C2103A /* ApptentiveInteractionTextModalController.h in Headers */, @@ -1781,6 +1910,7 @@ 01A2D1051E490A9700C2103A /* ApptentiveUnreadMessagesBadgeView.h in Headers */, 01A2D11F1E490A9700C2103A /* ApptentiveAppRelease.h in Headers */, 0134EB1E1EA9930E00DA4925 /* ApptentivePersonPayload.h in Headers */, + 018FAFDF1FC4A9C6007C52FE /* ApptentiveAndClause.h in Headers */, 01A2D11D1E490A9700C2103A /* ApptentiveInteractionUpgradeMessageViewController.h in Headers */, 01A2D1871E490A9700C2103A /* ApptentiveCompoundMessageCell.h in Headers */, 01626EB91EB7D60C0064E73F /* NSData+Encryption.h in Headers */, @@ -1788,12 +1918,10 @@ 0140D6021E8354A9007B5130 /* ApptentiveMessageSender.h in Headers */, 014508A81EAABEDE003326E7 /* ApptentiveInteractionsRequest.h in Headers */, 014508A01EAAB26D003326E7 /* ApptentiveConversationRequest.h in Headers */, - EF9009941F8D64B800DC5B56 /* ApptentiveLogWriter.h in Headers */, 0140D60E1E83553C007B5130 /* ApptentiveLegacyMessageSender.h in Headers */, 01A2D1F21E490A9700C2103A /* ApptentiveSurveyMultilineCell.h in Headers */, 01A2D0FD1E490A9700C2103A /* ApptentiveHUDViewController.h in Headers */, 014508981EAAAF84003326E7 /* ApptentiveMessageGetRequest.h in Headers */, - 01A2D12D1E490A9700C2103A /* ApptentiveInteractionInvocation.h in Headers */, 014508911EAA959E003326E7 /* ApptentiveRequestProtocol.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1872,7 +2000,7 @@ 01A2CF881E49062700C2103A /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 0900; + LastUpgradeCheck = 0930; ORGANIZATIONNAME = "Apptentive, Inc."; TargetAttributes = { 01A2CF901E49062700C2103A = { @@ -1998,6 +2126,7 @@ 01A2D2591E4946D600C2103A /* testOperatorAfter.json in Resources */, 01A2D2541E4946D600C2103A /* testCodePointLastInvokedAt.json in Resources */, 01A2D2691E4946D600C2103A /* testJsonDiffing.1.expected.json in Resources */, + 010FE34D2040C8810021C246 /* testOperatorStringEquals.json in Resources */, 01A2D24F1E4946D600C2103A /* ATDataModelv4.sqlite in Resources */, 01A2D2A21E4963F700C2103A /* v2WALDatabase in Resources */, 01A2D2A11E4963F700C2103A /* v2CorruptDatabase in Resources */, @@ -2014,6 +2143,7 @@ 01A2D25D1E4946D600C2103A /* testOperatorExists.json in Resources */, 01A2D2561E4946D600C2103A /* testCornerCasesThatShouldBeTrue.json in Resources */, 01A2D2661E4946D600C2103A /* testWhitespaceTrimming.json in Resources */, + 010FE34E2040C8810021C246 /* testOperatorStringNotEquals.json in Resources */, 01A2D2651E4946D600C2103A /* testV7Criteria.json in Resources */, 01A2D2611E4946D600C2103A /* testOperatorLessThanOrEqual.json in Resources */, 01A2D2571E4946D600C2103A /* testDefaultValues.json in Resources */, @@ -2054,6 +2184,7 @@ 01A2D1DD1E490A9700C2103A /* ApptentiveSurvey.m in Sources */, 01A2D1441E490A9700C2103A /* ApptentiveBackend+Engagement.m in Sources */, 01A2D1F51E490A9700C2103A /* ApptentiveSurveyOtherCell.m in Sources */, + 0178C5631FC49F1000DABF39 /* ApptentiveTarget.m in Sources */, 014508B21EAE5F71003326E7 /* ApptentiveClient.m in Sources */, EF9009911F8D370400DC5B56 /* ApptentiveLogMonitor.m in Sources */, 0140D60D1E83553C007B5130 /* ApptentiveLegacyMessage.m in Sources */, @@ -2062,6 +2193,7 @@ 014508A51EAAB382003326E7 /* ApptentiveExistingLoginRequest.m in Sources */, 01A2D1DF1E490A9700C2103A /* ApptentiveSurveyAnswer.m in Sources */, 01A2D19B1E490A9700C2103A /* ApptentiveMessageCenterStatusView.m in Sources */, + 018FAFF01FC4B21A007C52FE /* ApptentiveComparisonClause.m in Sources */, 0134EB1F1EA9930E00DA4925 /* ApptentivePersonPayload.m in Sources */, 01A2D12A1E490A9700C2103A /* ApptentiveEngagementManifest.m in Sources */, 01A2D19D1E490A9700C2103A /* ApptentiveProgressNavigationBar.m in Sources */, @@ -2076,12 +2208,15 @@ 01626EBA1EB7D60C0064E73F /* NSData+Encryption.m in Sources */, 01A2D1771E490A9700C2103A /* ApptentiveMessageCenterViewModel.m in Sources */, 01A2D1001E490A9700C2103A /* ApptentiveNetworkImageIconView.m in Sources */, + EF4EABA12040A194003318C9 /* ApptentiveFileUtilities.m in Sources */, 01A2D1201E490A9700C2103A /* ApptentiveAppRelease.m in Sources */, 01A2D1281E490A9700C2103A /* ApptentiveEngagement.m in Sources */, 015408A51E54D8810027E196 /* ApptentiveTableView.m in Sources */, 4D4B36251F01892A005C7EC0 /* ApptentiveEngagementBackend.m in Sources */, 01A2D12C1E490A9700C2103A /* ApptentiveInteraction.m in Sources */, + 01A50EC51FC5FFB00058C06C /* ApptentiveInvocations.m in Sources */, EF43D89F1FD5EBBB00C0FF9F /* ApptentiveDispatchQueue.m in Sources */, + 012ED92A2072F33F003D87F3 /* ApptentiveRetryPolicy.m in Sources */, EF8EE3AC1EBBA5D00033E7A1 /* ApptentiveJWT.m in Sources */, EFF3FE431E8C386E006AD2B4 /* ApptentiveLogTag.m in Sources */, 01A2D1C11E490A9700C2103A /* ATDataModel v2 to v3.xcmappingmodel in Sources */, @@ -2101,7 +2236,7 @@ 017477291EA92BED00A0A949 /* ApptentiveMessagePayload.m in Sources */, 01A2D1121E490A9700C2103A /* ApptentiveInteractionAppStoreController.m in Sources */, EFC308D81ECA6692006B6D36 /* ApptentiveLegacyConversationRequest.m in Sources */, - 01A2D12E1E490A9700C2103A /* ApptentiveInteractionInvocation.m in Sources */, + EFEF922F203F7DAA000B3C1B /* ApptentiveAsyncLogWriter.m in Sources */, 01A2D1041E490A9700C2103A /* ApptentivePassThroughWindow.m in Sources */, 0134EB171EA992F900DA4925 /* ApptentiveSDKAppReleasePayload.m in Sources */, 01A2D1EB1E490A9700C2103A /* ApptentiveSurveyAnswerCell.m in Sources */, @@ -2119,6 +2254,7 @@ EF8EE39B1EB3FB120033E7A1 /* ApptentiveAssert.m in Sources */, 01A2D1FB1E490A9700C2103A /* ApptentiveSurveyQuestionView.m in Sources */, EF3063AA1F2A8353004B4EB9 /* ApptentivePayloadDebug.m in Sources */, + 018FAFEC1FC4AF8F007C52FE /* ApptentiveNotClause.m in Sources */, 01A2D1421E490A9700C2103A /* ApptentiveVersion.m in Sources */, 0140D5FF1E835420007B5130 /* ApptentiveAttachment.m in Sources */, 01A2D1E71E490A9700C2103A /* ApptentiveSurveyViewController.m in Sources */, @@ -2126,7 +2262,6 @@ EF49AC951EF2C50B00E2187F /* NSMutableData+Types.m in Sources */, 0174772B1EA92BED00A0A949 /* ApptentivePayload.m in Sources */, 01A2D1141E490A9700C2103A /* ApptentiveInteractionEnjoymentDialogController.m in Sources */, - 01A2D1301E490A9700C2103A /* ApptentiveInteractionUsageData.m in Sources */, 01A2D1C21E490A9700C2103A /* ATDataModel v3 to v4.xcmappingmodel in Sources */, 01798C031EAF94FE00633164 /* ApptentivePayloadSender.m in Sources */, 01A2D1D31E490A9700C2103A /* ApptentiveSerialRequestAttachment.m in Sources */, @@ -2139,6 +2274,7 @@ EF8EE3991EB3F37B0033E7A1 /* ApptentiveConversationBaseRequest.m in Sources */, 01A2D1F31E490A9700C2103A /* ApptentiveSurveyMultilineCell.m in Sources */, 01A2D1881E490A9700C2103A /* ApptentiveCompoundMessageCell.m in Sources */, + EF4EAB8E203F876A003318C9 /* ApptentiveDispatchTask.m in Sources */, 01F1DFFD1E5BBFCB009AB3D2 /* ApptentiveConversationMetadataItem.m in Sources */, 01AA89B91F18177E00FB59AB /* ApptentiveAppInstall.m in Sources */, 01A2D0F91E490A9700C2103A /* Apptentive.m in Sources */, @@ -2147,14 +2283,15 @@ 01A2D1911E490A9700C2103A /* ApptentiveMessageCenterGreetingView.m in Sources */, 01A2D13A1E490A9700C2103A /* ApptentivePerson.m in Sources */, 01A2D13E1E490A9700C2103A /* ApptentiveConversation.m in Sources */, + 018FAFE81FC4AD0D007C52FE /* ApptentiveFalseClause.m in Sources */, 01F1DFF51E5BBFB3009AB3D2 /* ApptentiveConversationManager.m in Sources */, 01A2D1CB1E490A9700C2103A /* ApptentiveRequestOperation.m in Sources */, 01A2D1C01E490A9700C2103A /* ATDataModel v1 to v2.xcmappingmodel in Sources */, 01A2D1061E490A9700C2103A /* ApptentiveUnreadMessagesBadgeView.m in Sources */, 01A2D11A1E490A9700C2103A /* ApptentiveInteractionSurveyController.m in Sources */, 0134EB1B1EA9930200DA4925 /* ApptentiveDevicePayload.m in Sources */, - EF9009951F8D64B800DC5B56 /* ApptentiveLogWriter.m in Sources */, 01A2D1241E490A9700C2103A /* ApptentiveCustomData.m in Sources */, + 0178C56C1FC4A5A000DABF39 /* ApptentiveClause.m in Sources */, 01A2D1E31E490A9700C2103A /* ApptentiveSurveyCollectionViewLayout.m in Sources */, 01A2D1BF1E490A9700C2103A /* ApptentiveRecord.m in Sources */, 01A2D0FC1E490A9700C2103A /* ApptentiveStyleSheet.m in Sources */, @@ -2166,17 +2303,23 @@ 017477271EA92BED00A0A949 /* ApptentiveEventPayload.m in Sources */, EFC308D31EC52915006B6D36 /* ApptentiveStopWatch.m in Sources */, 01A2D1261E490A9700C2103A /* ApptentiveDevice.m in Sources */, + 010FE347203E4C810021C246 /* ApptentiveIndentPrinter.m in Sources */, 01A2D1AB1E490A9700C2103A /* ApptentiveAppConfiguration.m in Sources */, 01A2D1D11E490A9700C2103A /* ApptentiveSerialRequest.m in Sources */, 0140D5FB1E833103007B5130 /* ApptentiveMessage.m in Sources */, 01A2D1ED1E490A9700C2103A /* ApptentiveSurveyChoiceCell.m in Sources */, + 018FAFE01FC4A9C6007C52FE /* ApptentiveAndClause.m in Sources */, 01A2D1FF1E490A9700C2103A /* ApptentiveSurveySubmitButton.m in Sources */, 01A2D1E91E490A9700C2103A /* ApptentiveSurveyViewModel.m in Sources */, 014508A11EAAB26D003326E7 /* ApptentiveConversationRequest.m in Sources */, + 018FAFE41FC4AC41007C52FE /* ApptentiveOrClause.m in Sources */, 01A2D1861E490A9700C2103A /* ApptentiveAttachmentCell.m in Sources */, 01A2D1FD1E490A9700C2103A /* ApptentiveSurveySingleLineCell.m in Sources */, + EF4EAB96203F8B99003318C9 /* ApptentiveLogFileWriteTask.m in Sources */, 01A2D1EF1E490A9700C2103A /* ApptentiveSurveyCollectionView.m in Sources */, + EF4EABAC2040C0CF003318C9 /* ApptentiveLogMonitorSession.m in Sources */, EFC97AAF1F58818900BCC461 /* ApptentiveStoreProductViewController.m in Sources */, + 0178C55F1FC49E0600DABF39 /* ApptentiveTargets.m in Sources */, 0116D5281E92F516001DA5CF /* ApptentiveMessageStore.m in Sources */, 01A2D1101E490A9700C2103A /* ApptentiveInteractionTextModalController.m in Sources */, 01A2D1401E490A9700C2103A /* ApptentiveState.m in Sources */, @@ -2191,32 +2334,35 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 01A2D2411E4946D600C2103A /* ApptentiveEngagementTests.m in Sources */, - 01A2D2421E4946D600C2103A /* ApptentiveInteractionInvocationTests.m in Sources */, EFF4D2AC1EC39EC000FD4EFE /* ApptentiveAppDataContainer.m in Sources */, - 01A2D24A1E4946D600C2103A /* CodePointAndInteractionTests.m in Sources */, 01A2D2481E4946D600C2103A /* ApptentiveSurveyTests.m in Sources */, 01A2D29B1E4963A500C2103A /* ATDataModel v3 to v4.xcmappingmodel in Sources */, 0174772F1EA92D7D00A0A949 /* PayloadTests.swift in Sources */, + 01201AD31FC637BE00EB3593 /* CodePointAndInteractionTests.m in Sources */, 01A2D2451E4946D600C2103A /* ApptentiveMigrationTests.m in Sources */, 017E54ED1F3B860E00EA9F81 /* ApptentiveJSONSerializationTests.m in Sources */, 01626EBC1EB7E5B90064E73F /* ApptentiveEncryptionTests.m in Sources */, 01A2D29A1E4963A500C2103A /* ATDataModel v2 to v3.xcmappingmodel in Sources */, 01A2D2491E4946D600C2103A /* ApptentiveUtilitiesTests.m in Sources */, + 0123005F20531698000EC3C3 /* ClauseTests.m in Sources */, 01A2D2471E4946D600C2103A /* ApptentiveStyleSheetTests.m in Sources */, 01A2D29C1E4963A500C2103A /* ATDataModel v4 to v5.xcmappingmodel in Sources */, - 01A2D2431E4946D600C2103A /* ApptentiveInteractionUsageDataTests.m in Sources */, + EF4EAB9A203F9821003318C9 /* AppptentiveAsyncLogWriterTests.swift in Sources */, + EF4EAB9D20409199003318C9 /* ApptentiveMockDispatchQueue.m in Sources */, 01A2D24B1E4946D600C2103A /* CriteriaTests.m in Sources */, 01A2D2461E4946D600C2103A /* ApptentiveConversationTests.m in Sources */, EF8EE3A61EBA81880033E7A1 /* ATDataModel v5 to v6.xcmappingmodel in Sources */, + EF4EABA42040A8D6003318C9 /* TestUtils.swift in Sources */, 01216C501EBBB53E0062BD0D /* RequestTests.swift in Sources */, EFF4D2A91EC3806300FD4EFE /* ApptentiveConversationMigrationTests.m in Sources */, 01A2D2981E49638A00C2103A /* ATDataModel.xcdatamodeld in Sources */, 01A2D2971E49614A00C2103A /* Apptentive+Debugging.m in Sources */, + 010FE33B203E2D900021C246 /* CriteriaDescriptionTests.swift in Sources */, 01A2D2401E4946D600C2103A /* ApptentiveConnectTests.m in Sources */, 01A2D2991E4963A500C2103A /* ATDataModel v1 to v2.xcmappingmodel in Sources */, 01917D451E5E0B7400B37D82 /* ConversationManagerTests.swift in Sources */, 01A2D2441E4946D600C2103A /* ApptentiveMetricsTests.m in Sources */, + 012ED92C2072FABE003D87F3 /* RetryPolicyTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2289,6 +2435,7 @@ CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; @@ -2306,7 +2453,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 11; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -2346,6 +2493,7 @@ CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; @@ -2363,7 +2511,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 11; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -2395,7 +2543,7 @@ DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 86WML2UN43; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 9; + DYLIB_CURRENT_VERSION = 11; DYLIB_INSTALL_NAME_BASE = "@rpath"; GCC_PREFIX_HEADER = "Apptentive/Misc/ApptentiveConnect-Prefix.pch"; GCC_PREPROCESSOR_DEFINITIONS = "APPTENTIVE_DEBUG=1"; @@ -2417,7 +2565,7 @@ DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 86WML2UN43; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 9; + DYLIB_CURRENT_VERSION = 11; DYLIB_INSTALL_NAME_BASE = "@rpath"; GCC_PREFIX_HEADER = "Apptentive/Misc/ApptentiveConnect-Prefix.pch"; GCC_TREAT_WARNINGS_AS_ERRORS = YES; @@ -2468,7 +2616,7 @@ DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 86WML2UN43; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 9; + DYLIB_CURRENT_VERSION = 11; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = ApptentiveDebugging/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -2487,7 +2635,7 @@ DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 86WML2UN43; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 9; + DYLIB_CURRENT_VERSION = 11; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = ApptentiveDebugging/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; diff --git a/Apptentive/Apptentive.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Apptentive/Apptentive.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/Apptentive/Apptentive.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Apptentive/Apptentive.xcodeproj/xcshareddata/xcschemes/Apptentive.xcscheme b/Apptentive/Apptentive.xcodeproj/xcshareddata/xcschemes/Apptentive.xcscheme index ad4717386..2bc868f07 100644 --- a/Apptentive/Apptentive.xcodeproj/xcshareddata/xcschemes/Apptentive.xcscheme +++ b/Apptentive/Apptentive.xcodeproj/xcshareddata/xcschemes/Apptentive.xcscheme @@ -1,6 +1,6 @@ @@ -37,7 +36,6 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - language = "" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" diff --git a/Apptentive/Apptentive/Apptentive.h b/Apptentive/Apptentive/Apptentive.h index 1ead0723b..30b54ad63 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.0.4" +#define kApptentiveVersionString @"5.1.0" /** The version number of the Apptentive API platform. */ #define kApptentiveAPIVersionString @"9" @@ -76,6 +76,13 @@ extern NSNotificationName const ApptentiveSurveyShownNotification; /** Notification sent when a survey is submitted by the user. */ extern NSNotificationName const ApptentiveSurveySentNotification; +/** Notification sent when a message is sent, either by the user or using a sendAttachment method. + You can use this notification to ask the user to enable push notifications. */ +extern NSNotificationName const ApptentiveMessageSentNotification; + +/** Notification user info key whose value indicates whether the message was sent by the user or using a sendAttachment method. */ +extern NSString * const ApptentiveSentByUserKey; + /** Error domain for the Apptentive SDK */ extern NSString *const ApptentiveErrorDomain; @@ -136,6 +143,9 @@ typedef NS_ENUM(NSUInteger, ApptentiveLogLevel) { /** The granularity of log messages emitted from the SDK (defaults to `ApptentiveLogLevelInfo`). */ @property (assign, nonatomic) ApptentiveLogLevel logLevel; +/** If set, redacts potentially-sensitive information such as user data and credentials from logging. */ +@property (assign, nonatomic) BOOL shouldSanitizeLogMessages; + /** The server URL to use for API calls. Should only be used for testing. */ @property (copy, nonatomic) NSURL *baseURL; @@ -739,7 +749,7 @@ typedef NS_ENUM(NSUInteger, ApptentiveLogLevel) { /** The style sheet used for styling Apptentive UI. - @note See the [Apptentive Styling Guide for iOS](https://docs.apptentive.com/ios/customization/) for information on configuring this property. + @note See the [Apptentive Styling Guide for iOS](https://learn.apptentive.com/knowledge-base/interface-customization-ios/) for information on configuring this property. */ @property (strong, nonatomic) id styleSheet; @@ -778,6 +788,8 @@ typedef NS_ENUM(NSUInteger, ApptentiveLogLevel) { @property (assign, nonatomic) ApptentiveLogLevel logLevel; +@property (assign, nonatomic) BOOL redactSensitiveInformation; + @end @protocol ApptentiveDelegate diff --git a/Apptentive/Apptentive/Apptentive.m b/Apptentive/Apptentive/Apptentive.m index b0b6dd213..7bb142166 100644 --- a/Apptentive/Apptentive/Apptentive.m +++ b/Apptentive/Apptentive/Apptentive.m @@ -65,12 +65,12 @@ - (nullable instancetype)initWithApptentiveKey:(NSString *)apptentiveKey apptent self = [super init]; if (self) { if (apptentiveKey.length == 0) { - ApptentiveLogError(@"Can't create Apptentive configuration: key is nil or empty"); + ApptentiveLogError(@"Can't create Apptentive configuration: key is nil or empty."); return nil; } if (apptentiveSignature.length == 0) { - ApptentiveLogError(@"Can't create Apptentive configuration: signature is nil or empty"); + ApptentiveLogError(@"Can't create Apptentive configuration: signature is nil or empty."); return nil; } @@ -78,6 +78,7 @@ - (nullable instancetype)initWithApptentiveKey:(NSString *)apptentiveKey apptent _apptentiveSignature = [apptentiveSignature copy]; _baseURL = [NSURL URLWithString:@"https://api.apptentive.com/"]; _logLevel = ApptentiveLogLevelInfo; + _shouldSanitizeLogMessages = YES; } return self; } @@ -98,7 +99,7 @@ @implementation Apptentive + (instancetype)sharedConnection { if (_sharedInstance == nil) { - ApptentiveLogWarning(@"Apptentive instance is not initialized. Make sure you've registered it with your app key and signature"); + ApptentiveLogWarning(@"Apptentive instance is not initialized. Make sure you've registered it with your app key and signature."); } return _sharedInstance; } @@ -115,16 +116,24 @@ - (id)initWithConfiguration:(ApptentiveConfiguration *)configuration { // otherwise the log monitor configuration would be overwritten by // the SDK configuration ApptentiveLogSetLevel(configuration.logLevel); - - [ApptentiveLogMonitor tryInitializeWithBaseURL:configuration.baseURL appKey:configuration.apptentiveKey signature:configuration.apptentiveSignature]; - + + // we need to initialize dispatch queue before we start log monitor _operationQueue = [ApptentiveDispatchQueue createQueueWithName:@"Apptentive Main Queue" concurrencyType:ApptentiveDispatchQueueConcurrencyTypeSerial]; + + // start log writer + ApptentiveStartLogMonitor([ApptentiveUtilities cacheDirectoryPath:@"com.apptentive.logs"]); + + // start log monitor + [ApptentiveLogMonitor startSessionWithBaseURL:configuration.baseURL appKey:configuration.apptentiveKey signature:configuration.apptentiveSignature queue:_operationQueue]; _style = [[ApptentiveStyleSheet alloc] init]; _apptentiveKey = configuration.apptentiveKey; _apptentiveSignature = configuration.apptentiveSignature; _baseURL = configuration.baseURL; _appID = configuration.appID; + + setShouldSanitizeApptentiveLogMessages(configuration.shouldSanitizeLogMessages); + _backend = [[ApptentiveBackend alloc] initWithApptentiveKey:_apptentiveKey signature:_apptentiveSignature baseURL:_baseURL @@ -145,13 +154,13 @@ - (id)initWithConfiguration:(ApptentiveConfiguration *)configuration { + (void)registerWithConfiguration:(ApptentiveConfiguration *)configuration { if (_sharedInstance != nil) { - ApptentiveLogWarning(@"Apptentive instance is already initialized"); + ApptentiveLogWarning(@"Apptentive instance is already initialized."); return; } @try { _sharedInstance = [[Apptentive alloc] initWithConfiguration:configuration]; } @catch (NSException *e) { - ApptentiveLogCrit(@"Exception while initializing Apptentive instance: %@", e); + ApptentiveLogCrit(@"Exception while initializing Apptentive instance (%@).", e); } } @@ -198,7 +207,7 @@ - (void)setPersonEmailAddress:(nullable NSString *)personEmailAddress { - (void)sendAttachmentText:(NSString *)text { [self.operationQueue dispatchAsync:^{ if (self.backend.conversationManager.activeConversation == nil) { - ApptentiveLogError(@"Attempting to send message with no active conversation."); + ApptentiveLogError(ApptentiveLogTagMessages, @"Attempting to send message with no active conversation."); return; } @@ -209,7 +218,7 @@ - (void)sendAttachmentText:(NSString *)text { if (self.messageManager) { [self.messageManager enqueueMessageForSending:message]; } else { - ApptentiveLogError(@"Unable to send attachment text: message manager is not initialized"); + ApptentiveLogError(ApptentiveLogTagMessages, @"Unable to send attachment text: message manager is not initialized."); } } }]; @@ -218,23 +227,23 @@ - (void)sendAttachmentText:(NSString *)text { - (void)sendAttachmentImage:(UIImage *)image { [self.operationQueue dispatchAsync:^{ if (self.backend.conversationManager.activeConversation == nil) { - ApptentiveLogError(@"Attempting to send message with no active conversation."); + ApptentiveLogError(ApptentiveLogTagMessages, @"Attempting to send message with no active conversation."); return; } if (image == nil) { - ApptentiveLogError(@"Unable to send image attachment: image is nil"); + ApptentiveLogError(ApptentiveLogTagMessages, @"Unable to send image attachment: image is nil."); return; } NSData *imageData = UIImageJPEGRepresentation(image, 0.95); if (imageData == nil) { - ApptentiveLogError(@"Unable to send image attachment: image data is invalid"); + ApptentiveLogError(ApptentiveLogTagMessages, @"Unable to send image attachment: image data is invalid."); return; } if (self.backend.conversationManager.messageManager == nil) { - ApptentiveLogError(@"Unable to send attachment file: message manager is not initialized"); + ApptentiveLogError(ApptentiveLogTagMessages, @"Unable to send attachment file: message manager is not initialized."); return; } @@ -248,7 +257,7 @@ - (void)sendAttachmentImage:(UIImage *)image { if (self.messageManager) { [self.messageManager enqueueMessageForSending:message]; } else { - ApptentiveLogError(@"Unable to send attachment image: message manager is not initialized"); + ApptentiveLogError(ApptentiveLogTagMessages, @"Unable to send attachment image: message manager is not initialized."); } } } @@ -258,22 +267,22 @@ - (void)sendAttachmentImage:(UIImage *)image { - (void)sendAttachmentFile:(NSData *)fileData withMimeType:(NSString *)mimeType { [self.operationQueue dispatchAsync:^{ if (self.backend.conversationManager.activeConversation == nil) { - ApptentiveLogError(@"Attempting to send message with no active conversation."); + ApptentiveLogError(ApptentiveLogTagMessages, @"Attempting to send message with no active conversation."); return; } if (fileData == nil) { - ApptentiveLogError(@"Unable to send attachment file: file data is nil"); + ApptentiveLogError(ApptentiveLogTagMessages, @"Unable to send attachment file: file data is nil."); return; } if (mimeType.length == 0) { - ApptentiveLogError(@"Unable to send attachment file: mime-type is nil or empty"); + ApptentiveLogError(ApptentiveLogTagMessages, @"Unable to send attachment file: mime-type is nil or empty."); return; } if (self.backend.conversationManager.messageManager == nil) { - ApptentiveLogError(@"Unable to send attachment file: message manager is not initialized"); + ApptentiveLogError(ApptentiveLogTagMessages, @"Unable to send attachment file: message manager is not initialized."); return; } @@ -288,7 +297,7 @@ - (void)sendAttachmentFile:(NSData *)fileData withMimeType:(NSString *)mimeType if (self.messageManager) { [self.messageManager enqueueMessageForSending:message]; } else { - ApptentiveLogError(@"Unable to send attachment file: message manager is not initialized"); + ApptentiveLogError(ApptentiveLogTagMessages, @"Unable to send attachment file: message manager is not initialized."); } } } @@ -369,7 +378,7 @@ - (void)addCustomData:(NSObject *)object withKey:(NSString *)key toCustomDataDic if (simpleType || complexType) { ApptentiveDictionarySetKeyValue(customData, key, object); } else { - ApptentiveLogError(@"Apptentive custom data must be of type NSString, NSNumber, or NSNull, or a 'complex type' NSDictionary created by one of the constructors in Apptentive.h"); + ApptentiveLogError(@"Apptentive custom data must be of type NSString, NSNumber, or NSNull, or a 'complex type' NSDictionary created by one of the constructors in Apptentive.h."); } } @@ -689,7 +698,7 @@ - (BOOL)didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHan // Make sure the push is for the currently logged-in conversation if ([apptentivePayload[@"conversation_id"] isEqualToString:self.backend.conversationManager.activeConversation.identifier]) { - ApptentiveLogInfo(@"Push notification received for active conversation. userInfo: %@", userInfo); + ApptentiveLogInfo(ApptentiveLogTagPush, @"Push notification received for active conversation. userInfo: %@", ApptentiveHideKeysIfSanitized(userInfo, @[@"alert"])); NSNumber *contentAvailable = userInfo[@"aps"][@"content-available"]; // The content available flag should be set, which indicates that we want to download new messages @@ -699,7 +708,7 @@ - (BOOL)didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHan if (self.messageManager) { [self.messageManager checkForMessagesInBackground:completionHandler]; } else { - ApptentiveLogError(@"Can't check for incoming messages: message manager is not initialized"); + ApptentiveLogError(ApptentiveLogTagPush, @"Can't check for incoming messages: message manager is not initialized."); } } @@ -707,14 +716,14 @@ - (BOOL)didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHan // We also want to fire a local notification if the app is in the foreground (in which banners aren't shown normally), // which will trigger the "open message center" logic (or if using the UserNotifications framework, show a banner in-app) if (userInfo[@"aps"][@"alert"] == nil || applicationState == UIApplicationStateActive) { - ApptentiveLogInfo(@"Silent push notification received or app in foreground. Posting local notification"); + ApptentiveLogInfo(ApptentiveLogTagPush, @"Silent push notification received or app in foreground. Posting local notification."); dispatch_async(dispatch_get_main_queue(), ^{ [self fireLocalNotificationWithUserInfo:userInfo]; }); } } else { - ApptentiveLogInfo(@"Push notification received for conversation that is not active. Active conversation ID is %@, push conversation ID is %@", self.backend.conversationManager.activeConversation.identifier, apptentivePayload[@"conversation_id"]); + ApptentiveLogInfo(ApptentiveLogTagPush, @"Push notification received for conversation that is not active. Active conversation ID is %@, push conversation ID is %@", self.backend.conversationManager.activeConversation.identifier, apptentivePayload[@"conversation_id"]); } if (shouldCallCompletionHandler && completionHandler) { @@ -724,7 +733,7 @@ - (BOOL)didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHan } }]; } else { - ApptentiveLogInfo(@"Non-apptentive push notification received."); + ApptentiveLogInfo(ApptentiveLogTagPush, @"Non-apptentive push notification received."); } return (apptentivePayload != nil); @@ -732,11 +741,11 @@ - (BOOL)didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHan - (BOOL)didReceiveLocalNotification:(UILocalNotification *)notification fromViewController:(UIViewController *)viewController { if ([self presentMessageCenterIfNeededForUserInfo:notification.userInfo fromViewController:viewController]) { - ApptentiveLogInfo(@"Apptentive local notification received."); + ApptentiveLogInfo(ApptentiveLogTagPush, @"Apptentive local notification received."); return YES; } else { - ApptentiveLogInfo(@"Non-apptentive local notification received."); + ApptentiveLogInfo(ApptentiveLogTagPush, @"Non-apptentive local notification received."); return NO; } @@ -749,7 +758,7 @@ - (BOOL)didReceveUserNotificationResponse:(UNNotificationResponse *)response wit // We allow passing a view controller to present Message Center from when the user has tapped a notification banner - (BOOL)didReceveUserNotificationResponse:(UNNotificationResponse *)response fromViewController:(nullable UIViewController *)viewController withCompletionHandler:(void (^)(void))completionHandler { if ([self presentMessageCenterIfNeededForUserInfo:response.notification.request.content.userInfo fromViewController:viewController]) { - ApptentiveLogInfo(@"Apptentive user notification received."); + ApptentiveLogInfo(ApptentiveLogTagPush, @"Apptentive user notification received."); if (completionHandler != nil) { completionHandler(); @@ -757,7 +766,7 @@ - (BOOL)didReceveUserNotificationResponse:(UNNotificationResponse *)response fro return YES; } else { - ApptentiveLogInfo(@"Non-apptentive user notification received."); + ApptentiveLogInfo(ApptentiveLogTagPush, @"Non-apptentive user notification received."); return NO; } @@ -791,7 +800,7 @@ - (BOOL)presentMessageCenterIfNeededForUserInfo:(NSDictionary *)userInfo fromVie if (self.messageManager) { [self.messageManager checkForMessages]; } else { - ApptentiveLogError(@"Can't check for incoming messages: message manager is not initialized"); + ApptentiveLogError(ApptentiveLogTagPush, @"Can't check for incoming messages: message manager is not initialized."); } } @@ -799,7 +808,7 @@ - (BOOL)presentMessageCenterIfNeededForUserInfo:(NSDictionary *)userInfo fromVie } - (void)fireLocalNotificationWithUserInfo:(NSDictionary *)userInfo { - ApptentiveLogInfo(@"Silent push notification received. Posting local notification"); + ApptentiveLogInfo(ApptentiveLogTagPush, @"Silent push notification received. Posting local notification."); NSString *title = [ApptentiveUtilities appName]; NSString *body = userInfo[@"apptentive"][@"alert"] ?: userInfo[@"aps"][@"alert"] ?: NSLocalizedString(@"A new message awaits you in Message Center", @"Default push alert body"); @@ -820,7 +829,7 @@ - (void)fireLocalNotificationWithUserInfo:(NSDictionary *)userInfo { [UNUserNotificationCenter.currentNotificationCenter addNotificationRequest:request withCompletionHandler:^(NSError *_Nullable error) { if (error) { - ApptentiveLogError(@"Error posting local notification: %@", error); + ApptentiveLogError(@"Error posting local notification (%@).", error); } }]; @@ -837,8 +846,8 @@ - (void)fireLocalNotificationWithUserInfo:(NSDictionary *)userInfo { [[UIApplication sharedApplication] presentLocalNotificationNow:localNotification]; } else { - ApptentiveLogError(@"Your app is not properly configured to accept Apptentive Message Center push notifications."); - ApptentiveLogError(@"Please see the push notification section of the integration guide for assistance: https://learn.apptentive.com/knowledge-base/ios-integration-reference/#push-notifications"); + ApptentiveLogError(ApptentiveLogTagPush, @"Your app is not properly configured to accept Apptentive Message Center push notifications."); + ApptentiveLogError(ApptentiveLogTagPush, @"Please see the push notification section of the integration guide for assistance: https://learn.apptentive.com/knowledge-base/ios-integration-reference/#push-notifications."); } } @@ -921,7 +930,7 @@ - (void)logOut { [self.operationQueue dispatchAsync:^{ if (self.backend.conversationManager.activeConversation.state != ApptentiveConversationStateLoggedIn) { - ApptentiveLogError(@"Attempting to log out of a conversation that is not logged in."); + ApptentiveLogError(ApptentiveLogTagConversation, @"Attempting to log out of a conversation that is not logged in."); return; } @@ -951,15 +960,14 @@ - (void)registerNotifications { } - (void)applicationWillEnterForegroundNotification:(NSNotification *)notification { - ApptentiveLogMonitor *logMonitor = [ApptentiveLogMonitor sharedInstance]; - if (logMonitor) { + if ([ApptentiveLogMonitor resumeSession]) { ApptentiveLogDebug(ApptentiveLogTagMonitor, @"Resuming log monitor..."); - [logMonitor resume]; } else { ApptentiveLogDebug(ApptentiveLogTagMonitor, @"Trying to initialize log monitor..."); - [ApptentiveLogMonitor tryInitializeWithBaseURL:self.baseURL + [ApptentiveLogMonitor startSessionWithBaseURL:self.baseURL appKey:self.apptentiveKey - signature:self.apptentiveSignature]; + signature:self.apptentiveSignature + queue:self.operationQueue]; } } diff --git a/Apptentive/Apptentive/ApptentiveStyleSheet.m b/Apptentive/Apptentive/ApptentiveStyleSheet.m index 996175239..91b36d0b6 100644 --- a/Apptentive/Apptentive/ApptentiveStyleSheet.m +++ b/Apptentive/Apptentive/ApptentiveStyleSheet.m @@ -353,7 +353,7 @@ - (nullable instancetype)initWithContentsOfURL:(NSURL *)stylePropertyListURL { NSDictionary *propertyList = [NSDictionary dictionaryWithContentsOfURL:stylePropertyListURL]; if (propertyList == nil) { - ApptentiveLogError(@"Style property list at URL %@ was unable to be loaded.", stylePropertyListURL); + ApptentiveLogWarning(@"Style property list at URL %@ was unable to be loaded.", stylePropertyListURL); return nil; } @@ -381,11 +381,11 @@ - (nullable instancetype)initWithContentsOfURL:(NSURL *)stylePropertyListURL { if (color) { [self setColor:color forStyle:style]; } else { - ApptentiveLogError(@"Property list color override for style %@ is not a valid hex string (e.g. #A1B2C3).", style); + ApptentiveLogWarning(@"Property list color override for style %@ is not a valid hex string (e.g. #A1B2C3).", style); } } } else { - ApptentiveLogError(@"Property list color overrides is not a valid dictionary."); + ApptentiveLogWarning(@"Property list value for color overrides key is not a valid dictionary."); } NSDictionary *fontOverrides = propertyList[FontOverridesKey]; @@ -396,7 +396,7 @@ - (nullable instancetype)initWithContentsOfURL:(NSURL *)stylePropertyListURL { NSNumber *fontSize = fontOverrides[style][@"size"]; if (![fontName isKindOfClass:[NSString class]] || ![fontSize isKindOfClass:[NSNumber class]]) { - ApptentiveLogError(@"Property list font override for style %@ has missing or invalid `font` (string) or `size` (number) value."); + ApptentiveLogWarning(@"Property list font override for style %@ has missing or invalid `font` (string) or `size` (number) value."); continue; } @@ -405,11 +405,11 @@ - (nullable instancetype)initWithContentsOfURL:(NSURL *)stylePropertyListURL { if (fontDescriptor != nil) { [self setFontDescriptor:fontDescriptor forStyle:style]; } else { - ApptentiveLogError(@"Unable to create font descriptor with name %@ and size %f", fontName, fontSize.doubleValue); + ApptentiveLogWarning(@"Unable to create font descriptor with name %@ and size %f.", fontName, fontSize.doubleValue); } } } else { - ApptentiveLogError(@"Property list font overrides is not a valid dictionary."); + ApptentiveLogWarning(@"Property list value for font overrides key is not a valid dictionary."); } } diff --git a/Apptentive/Apptentive/Custom Views/ApptentiveNetworkImageView.m b/Apptentive/Apptentive/Custom Views/ApptentiveNetworkImageView.m index f03c716ce..1429d7a27 100644 --- a/Apptentive/Apptentive/Custom Views/ApptentiveNetworkImageView.m +++ b/Apptentive/Apptentive/Custom Views/ApptentiveNetworkImageView.m @@ -36,7 +36,7 @@ - (void)restartDownload { self.task = [[NSURLSession sharedSession] dataTaskWithURL:self.imageURL completionHandler:^(NSData *_Nullable data, NSURLResponse *_Nullable response, NSError *_Nullable error) { if (data == nil) { - ApptentiveLogError(@"Unable to download image at %@: %@", self.imageURL, error); + ApptentiveLogWarning(ApptentiveLogTagNetwork, @"Unable to download image from %@ (%@).", self.imageURL, error); self.task = nil; if ([self.delegate respondsToSelector:@selector(networkImageView:didFailWithError:)]) { diff --git a/Apptentive/Apptentive/Engagement/Interactions/Notes/ApptentiveInteractionNavigateToLink.m b/Apptentive/Apptentive/Engagement/Interactions/Notes/ApptentiveInteractionNavigateToLink.m index f35907181..a68f198f3 100644 --- a/Apptentive/Apptentive/Engagement/Interactions/Notes/ApptentiveInteractionNavigateToLink.m +++ b/Apptentive/Apptentive/Engagement/Interactions/Notes/ApptentiveInteractionNavigateToLink.m @@ -39,13 +39,13 @@ - (void)presentInteractionFromViewController:(nullable UIViewController *)viewCo if (attemptToOpenURL) { openedURL = [[UIApplication sharedApplication] openURL:url]; if (!openedURL) { - ApptentiveLogError(@"Could not open URL: %@", url); + ApptentiveLogWarning(ApptentiveLogTagInteractions, @"Could not open URL %@.", url); } } else { - ApptentiveLogError(@"No application can open the Interaction's URL: %@", url); + ApptentiveLogWarning(ApptentiveLogTagInteractions, @"No application can open the Interaction's URL (%@), or the %@ scheme is missing from Info.plist's LSApplicationQueriesSchemes value.", url, url.scheme); } } else { - ApptentiveLogError(@"No URL was included in the NavigateToLink Interaction's configuration."); + ApptentiveLogError(ApptentiveLogTagInteractions, @"No URL was included in the NavigateToLink Interaction's configuration."); } NSDictionary *userInfo = @{ @"url": (urlString ?: [NSNull null]), diff --git a/Apptentive/Apptentive/Engagement/Interactions/Notes/ApptentiveInteractionTextModalController.m b/Apptentive/Apptentive/Engagement/Interactions/Notes/ApptentiveInteractionTextModalController.m index 6129f4e35..3835ce207 100644 --- a/Apptentive/Apptentive/Engagement/Interactions/Notes/ApptentiveInteractionTextModalController.m +++ b/Apptentive/Apptentive/Engagement/Interactions/Notes/ApptentiveInteractionTextModalController.m @@ -7,9 +7,9 @@ // #import "ApptentiveInteractionTextModalController.h" +#import "ApptentiveUtilities.h" #import "ApptentiveBackend+Engagement.h" #import "ApptentiveInteraction.h" -#import "ApptentiveInteractionInvocation.h" #import "ApptentiveUtilities.h" #import "Apptentive_Private.h" #import "UIAlertController+Apptentive.h" @@ -59,7 +59,7 @@ - (nullable UIAlertController *)alertControllerWithInteraction:(ApptentiveIntera NSString *message = config[@"body"]; if (!title && !message) { - ApptentiveLogError(@"Skipping display of Apptentive Note that does not have a title and body."); + ApptentiveLogError(ApptentiveLogTagInteractions, @"Skipping display of Apptentive Note that does not have a title and body."); return nil; } @@ -85,7 +85,7 @@ - (nullable UIAlertController *)alertControllerWithInteraction:(ApptentiveIntera cancelActionAdded = YES; } else { // Additional cancel buttons are ignored. - ApptentiveLogError(@"Apptentive Notes cannot have more than one cancel button."); + ApptentiveLogError(ApptentiveLogTagInteractions, @"Apptentive Notes cannot have more than one cancel button."); continue; } } @@ -106,7 +106,7 @@ - (UIAlertAction *)alertActionWithConfiguration:(NSDictionary *)actionConfig { // Better to use default button text than to potentially create an un-cancelable alert with no buttons. // Exception: 'Actions added to UIAlertController must have a title' if (!title) { - ApptentiveLogError(@"Apptentive Note button action does not have a title!"); + ApptentiveLogWarning(ApptentiveLogTagInteractions, @"Apptentive Note button action does not have a title."); title = ApptentiveLocalizedString(@"OK", @"OK"); } @@ -133,7 +133,7 @@ - (UIAlertAction *)alertActionWithConfiguration:(NSDictionary *)actionConfig { } else if ([actionType isEqualToString:@"interaction"]) { actionHandler = [self createButtonHandlerBlockInteractionAction:actionConfig]; } else { - ApptentiveLogError(@"Apptentive note contains an unknown action."); + ApptentiveLogError(ApptentiveLogTagInteractions, @"Apptentive note contains an unknown action."); } UIAlertAction *alertAction = [UIAlertAction actionWithTitle:title style:style handler:actionHandler]; diff --git a/Apptentive/Apptentive/Engagement/Interactions/Rating Flow/ApptentiveInteractionAppStoreController.m b/Apptentive/Apptentive/Engagement/Interactions/Rating Flow/ApptentiveInteractionAppStoreController.m index c76e64b5e..db6582d03 100644 --- a/Apptentive/Apptentive/Engagement/Interactions/Rating Flow/ApptentiveInteractionAppStoreController.m +++ b/Apptentive/Apptentive/Engagement/Interactions/Rating Flow/ApptentiveInteractionAppStoreController.m @@ -132,14 +132,14 @@ - (void)openAppStoreViaURL { BOOL openedURL = [[UIApplication sharedApplication] openURL:url]; if (!openedURL) { - ApptentiveLogError(@"Could not open App Store URL: %@", url); + ApptentiveLogWarning(ApptentiveLogTagInteractions, @"Could not open App Store URL: %@", url); } } else { - ApptentiveLogError(@"No application can open the URL: %@", url); + ApptentiveLogWarning(ApptentiveLogTagInteractions, @"No application can open the URL: %@", url); [self showUnableToOpenAppStoreDialog]; } } else { - ApptentiveLogError(@"Could not open App Store because App ID is not set. Set the `appID` property locally, or configure it remotely via the Apptentive dashboard."); + ApptentiveLogError(ApptentiveLogTagInteractions, @"Could not open App Store because App ID is not set. Set the `appID` property locally, or configure it remotely via the Apptentive dashboard."); [self showUnableToOpenAppStoreDialog]; } @@ -152,7 +152,7 @@ - (void)openAppStoreViaStoreKit { [vc loadProductWithParameters:@{ SKStoreProductParameterITunesItemIdentifier: self.appID } completionBlock:^(BOOL result, NSError *error) { if (error) { - ApptentiveLogError(@"Error loading product view: %@", error); + ApptentiveLogWarning(ApptentiveLogTagInteractions, @"Unable to load product view (%@).", error); [self showUnableToOpenAppStoreDialog]; } else { [Apptentive.shared.backend engage:ATInteractionAppStoreRatingEventLabelOpenStoreKit fromInteraction:self.interaction fromViewController:self.presentingViewController]; diff --git a/Apptentive/Apptentive/Engagement/Interactions/Rating Flow/ApptentiveInteractionAppleRatingDialogController.m b/Apptentive/Apptentive/Engagement/Interactions/Rating Flow/ApptentiveInteractionAppleRatingDialogController.m index c0ed4f3a7..d2421e8ea 100644 --- a/Apptentive/Apptentive/Engagement/Interactions/Rating Flow/ApptentiveInteractionAppleRatingDialogController.m +++ b/Apptentive/Apptentive/Engagement/Interactions/Rating Flow/ApptentiveInteractionAppleRatingDialogController.m @@ -49,7 +49,7 @@ - (void)presentInteractionFromViewController:(nullable UIViewController *)viewCo // Give the window a sec to appear before (possibly) invoking fallback [self performSelector:@selector(checkIfAppleRatingDialogShowed) withObject:nil afterDelay:REVIEW_WINDOW_TIMEOUT]; } else { - [self invokeNotShownInteractionFromViewController:viewController withReason:@"os too old"]; + [self invokeNotShownInteractionFromViewController:viewController withReason:@"It is not available in this iOS version."]; } } @@ -57,7 +57,7 @@ - (void)windowDidBecomeVisible:(NSNotification *)notification { if ([NSStringFromClass([notification.object class]) hasPrefix:@"SKStoreReview"]) { // Review window was shown self.didShowReviewController = YES; - ApptentiveLogInfo(@"Apple Rating Dialog did appear."); + ApptentiveLogInfo(ApptentiveLogTagInteractions, @"Apple Rating Dialog did appear."); } } @@ -78,10 +78,10 @@ - (void)invokeNotShownInteractionFromViewController:(UIViewController *)viewCont userInfo = @{ @"cause": notShownReason }; } else { // Don't include nil notShownReason in userinfo, but explain in log message - notShownReason = @"reached limit or user disabled"; + notShownReason = @"It could be that:\n\t1. The user disabled rating dialogs\n\t2. The limit for number of times to show has been reached\n\t3. The user logged into the store has already rated this version, or\n\t4. The app is being tested with TestFlight."; } - ApptentiveLogInfo(@"Apple Rating Dialog did not appear (reason: %@)", notShownReason); + ApptentiveLogInfo(ApptentiveLogTagInteractions, @"Apple Rating Dialog was requested but did not appear. %@", notShownReason); [Apptentive.shared.backend engage:ApptentiveInteractionAppleRatingDialogEventLabelNotShown fromInteraction:self.interaction fromViewController:viewController userInfo:userInfo]; @@ -95,10 +95,10 @@ - (void)invokeNotShownInteractionFromViewController:(UIViewController *)viewCont [[Apptentive sharedConnection].backend presentInteraction:interaction fromViewController:viewController]; } else { - ApptentiveLogError(@"Apple rating dialog fallback interaction has invalid id: %@", notShownInteractionIdentifier); + ApptentiveLogError(ApptentiveLogTagInteractions, @"Apple rating dialog fallback interaction has an invalid identifier: %@", notShownInteractionIdentifier); } } else { - ApptentiveLogInfo(@"Apple Rating Dialog fallback interaction not configured."); + ApptentiveLogInfo(ApptentiveLogTagInteractions, @"No fallback interaction is configured for this OS version."); } } diff --git a/Apptentive/Apptentive/Engagement/Interactions/Rating Flow/ApptentiveInteractionEnjoymentDialogController.m b/Apptentive/Apptentive/Engagement/Interactions/Rating Flow/ApptentiveInteractionEnjoymentDialogController.m index 528f4b794..ba85fee07 100644 --- a/Apptentive/Apptentive/Engagement/Interactions/Rating Flow/ApptentiveInteractionEnjoymentDialogController.m +++ b/Apptentive/Apptentive/Engagement/Interactions/Rating Flow/ApptentiveInteractionEnjoymentDialogController.m @@ -7,10 +7,10 @@ // #import "ApptentiveInteractionEnjoymentDialogController.h" +#import "ApptentiveUtilities.h" #import "ApptentiveBackend+Engagement.h" #import "ApptentiveBackend.h" #import "ApptentiveInteraction.h" -#import "ApptentiveInteractionInvocation.h" #import "ApptentiveUtilities.h" #import "Apptentive_Private.h" #import "UIAlertController+Apptentive.h" @@ -83,7 +83,7 @@ - (NSString *)noText { - (nullable UIAlertController *)alertControllerWithInteraction:(ApptentiveInteraction *)interaction { if (!self.title && !self.body) { - ApptentiveLogError(@"Skipping display of Enjoyment Dialog that does not have a title or body."); + ApptentiveLogError(ApptentiveLogTagInteractions, @"Skipping display of Enjoyment Dialog that does not have a title or body."); return nil; } diff --git a/Apptentive/Apptentive/Engagement/Interactions/Rating Flow/ApptentiveInteractionRatingDialogController.m b/Apptentive/Apptentive/Engagement/Interactions/Rating Flow/ApptentiveInteractionRatingDialogController.m index 24eaf4929..707758336 100644 --- a/Apptentive/Apptentive/Engagement/Interactions/Rating Flow/ApptentiveInteractionRatingDialogController.m +++ b/Apptentive/Apptentive/Engagement/Interactions/Rating Flow/ApptentiveInteractionRatingDialogController.m @@ -7,10 +7,10 @@ // #import "ApptentiveInteractionRatingDialogController.h" +#import "ApptentiveUtilities.h" #import "ApptentiveBackend+Engagement.h" #import "ApptentiveBackend.h" #import "ApptentiveInteraction.h" -#import "ApptentiveInteractionInvocation.h" #import "ApptentiveUtilities.h" #import "Apptentive_Private.h" #import "UIAlertController+Apptentive.h" @@ -94,7 +94,7 @@ - (NSString *)remindText { - (nullable UIAlertController *)alertControllerWithInteraction:(ApptentiveInteraction *)interaction { if (!self.title && !self.body) { - ApptentiveLogError(@"Skipping display of Rating Dialog that does not have a title or body."); + ApptentiveLogError(ApptentiveLogTagInteractions, @"Skipping display of Rating Dialog that does not have a title or body."); return nil; } diff --git a/Apptentive/Apptentive/Engagement/Model/ApptentiveAppRelease.m b/Apptentive/Apptentive/Engagement/Model/ApptentiveAppRelease.m index 5878fdee8..727613a90 100644 --- a/Apptentive/Apptentive/Engagement/Model/ApptentiveAppRelease.m +++ b/Apptentive/Apptentive/Engagement/Model/ApptentiveAppRelease.m @@ -58,7 +58,7 @@ - (instancetype)init { } - (instancetype)initWithCurrentAppRelease { - self = [super init]; + self = [self init]; if (self) { _type = @"ios"; @@ -253,4 +253,111 @@ + (NSDictionary *)JSONKeyPathMapping { @end +@implementation ApptentiveAppRelease (Criteria) + +- (nullable NSObject *)valueForFieldWithPath:(NSString *)path { + if ([path isEqualToString:@"cf_bundle_version"]) { + return self.build; + } else if ([path isEqualToString:@"cf_bundle_short_version_string"]) { + return self.version; + } else if ([path isEqualToString:@"debug"]) { + return @(self.debugBuild); + } else if ([path isEqualToString:@"dt_compiler"]) { + return self.compiler; + } else if ([path isEqualToString:@"dt_platform_build"]) { + return self.platformBuild; + } else if ([path isEqualToString:@"dt_platform_name"]) { + return self.platformName; + } else if ([path isEqualToString:@"dt_platform_version"]) { + return [[ApptentiveVersion alloc] initWithString:self.platformVersion]; + } else if ([path isEqualToString:@"dt_sdk_build"]) { + return self.SDKBuild; + } else if ([path isEqualToString:@"dt_sdk_name"]) { + return self.SDKName; + } else if ([path isEqualToString:@"dt_xcode"]) { + return self.Xcode; + } else if ([path isEqualToString:@"dt_xcode_build"]) { + return self.XcodeBuild; + } else { + NSArray *parts = [path componentsSeparatedByString:@"/"]; + NSString *first = parts.firstObject; + NSString *last = parts.lastObject; + + if ([first isEqualToString:@"is_update"]) { + if ([last isEqualToString:@"cf_bundle_short_version_string"]) { + return @(self.isUpdateVersion); + } else if ([last isEqualToString:@"cf_bundle_version"]) { + return @(self.isUpdateBuild); + } else { + ApptentiveLogError(@"Unrecognized field name “%@”", path); + return nil; + } + } else if ([first isEqualToString:@"time_at_install"]) { + if ([last isEqualToString:@"total"]) { + return self.timeAtInstallTotal; + } else if ([last isEqualToString:@"cf_bundle_short_version_string"]) { + return self.timeAtInstallVersion; + } else if ([last isEqualToString:@"cf_bundle_version"]) { + return self.timeAtInstallBuild; + } else { + ApptentiveLogError(@"Unrecognized field name “%@”", path); + return nil; + } + } + } + + ApptentiveLogError(@"Unrecognized field name “%@”", path); + return nil; +} + +- (NSString *)descriptionForFieldWithPath:(NSString *)path { + if ([path isEqualToString:@"cf_bundle_version"]) { + return @"app build (CFBundleVersion)"; + } else if ([path isEqualToString:@"cf_bundle_short_version_string"]) { + return @"app version (CFBundleShortVersionString)"; + } else if ([path isEqualToString:@"debug"]) { + return @"app built using the debug configuration"; + } else if ([path isEqualToString:@"dt_compiler"]) { + return @"developer tools compiler"; + } else if ([path isEqualToString:@"dt_platform_build"]) { + return @"developer tools platform build"; + } else if ([path isEqualToString:@"dt_platform_name"]) { + return @"developer tools platform name"; + } else if ([path isEqualToString:@"dt_platform_version"]) { + return @"developer tools platform version"; + } else if ([path isEqualToString:@"dt_sdk_build"]) { + return @"developer tools SDK build"; + } else if ([path isEqualToString:@"dt_sdk_name"]) { + return @"developer tools SDK name"; + } else if ([path isEqualToString:@"dt_xcode"]) { + return @"developer tools xcode"; + } else if ([path isEqualToString:@"dt_xcode_build"]) { + return @"developer tools xcode build"; + } else { + NSArray *parts = [path componentsSeparatedByString:@"/"]; + NSString *first = parts.firstObject; + NSString *last = parts.lastObject; + + if ([first isEqualToString:@"is_update"]) { + if ([last isEqualToString:@"cf_bundle_short_version_string"]) { + return @"app version (CFBundleShortVersionString) changed"; + } else if ([last isEqualToString:@"cf_bundle_version"]) { + return @"app build (CFBundleVersion) changed"; + } + } else if ([first isEqualToString:@"time_at_install"]) { + if ([last isEqualToString:@"total"]) { + return @"time at install"; + } else if ([last isEqualToString:@"cf_bundle_short_version_string"]) { + return @"time at install for version"; + } else if ([last isEqualToString:@"cf_bundle_version"]) { + return @"time at install for build"; + } + } + } + + return [NSString stringWithFormat:@"Unrecognized app release field %@", path]; +} + +@end + NS_ASSUME_NONNULL_END diff --git a/Apptentive/Apptentive/Engagement/Model/ApptentiveConversation.h b/Apptentive/Apptentive/Engagement/Model/ApptentiveConversation.h index d374486fe..f1f1f6642 100644 --- a/Apptentive/Apptentive/Engagement/Model/ApptentiveConversation.h +++ b/Apptentive/Apptentive/Engagement/Model/ApptentiveConversation.h @@ -226,6 +226,13 @@ extern NSString *NSStringFromApptentiveConversationState(ApptentiveConversationS - (void)updateWithCurrentValues; + +/** + Indicates that a network request to update the device has been + enqueued, so any current device diffs should be cleared. + */ +- (void)updateLastSentDevice; + /** Checks if conversation is in active state */ diff --git a/Apptentive/Apptentive/Engagement/Model/ApptentiveConversation.m b/Apptentive/Apptentive/Engagement/Model/ApptentiveConversation.m index 908cb27f3..b60acb719 100644 --- a/Apptentive/Apptentive/Engagement/Model/ApptentiveConversation.m +++ b/Apptentive/Apptentive/Engagement/Model/ApptentiveConversation.m @@ -157,7 +157,7 @@ - (void)checkForDiffs { NSDictionary *appReleaseDiffs = [ApptentiveUtilities diffDictionary:currentAppRelease.JSONDictionary againstDictionary:self.appRelease.JSONDictionary]; if (appReleaseDiffs.count > 0) { - ApptentiveLogDebug(ApptentiveLogTagConversation, @"App release did change."); + ApptentiveLogDebug(ApptentiveLogTagConversation, @"One or more App release fields have changed: %@", ApptentiveHideKeysIfSanitized(appReleaseDiffs, [ApptentiveAppRelease sensitiveKeys])); conversationNeedsUpdate = YES; if (![currentAppRelease.version isEqualToVersion:self.appRelease.version]) { @@ -176,7 +176,7 @@ - (void)checkForDiffs { NSDictionary *SDKDiffs = [ApptentiveUtilities diffDictionary:currentSDK.JSONDictionary againstDictionary:self.SDK.JSONDictionary]; if (SDKDiffs.count > 0) { - ApptentiveLogDebug(ApptentiveLogTagConversation, @"SDK did change."); + ApptentiveLogDebug(ApptentiveLogTagConversation, @"One or more Apptentive SDK fields have changed: %@", ApptentiveHideKeysIfSanitized(SDKDiffs, [ApptentiveSDK sensitiveKeys])); conversationNeedsUpdate = YES; @@ -196,27 +196,31 @@ - (void)checkForDiffs { } - (void)checkForDeviceDiffs { - ApptentiveLogVerbose(ApptentiveLogTagConversation, @"Diffing device"); + ApptentiveLogDebug(ApptentiveLogTagConversation, @"Checking for changes to the device fields..."); [self.device updateWithCurrentDeviceValues]; NSDictionary *deviceDiffs = [ApptentiveUtilities diffDictionary:self.device.JSONDictionary againstDictionary:self.lastSentDevice]; if (deviceDiffs.count > 0) { - ApptentiveLogVerbose(ApptentiveLogTagConversation, @"Device diffs found: %@", deviceDiffs); + ApptentiveLogVerbose(ApptentiveLogTagConversation, @"One or more device fields have changed: %@", ApptentiveHideKeysIfSanitized(deviceDiffs, [ApptentiveDevice sensitiveKeys])); [self.delegate conversation:self deviceDidChange:deviceDiffs]; - self.lastSentDevice = self.device.JSONDictionary; + [self updateLastSentDevice]; } } +- (void)updateLastSentDevice { + self.lastSentDevice = self.device.JSONDictionary; +} + - (void)checkForPersonDiffs { - ApptentiveLogVerbose(ApptentiveLogTagConversation, @"Diffing person"); + ApptentiveLogDebug(ApptentiveLogTagConversation, @"Checking for changes to the person fields..."); NSDictionary *personDiffs = [ApptentiveUtilities diffDictionary:self.person.JSONDictionary againstDictionary:self.lastSentPerson]; if (personDiffs.count > 0) { - ApptentiveLogVerbose(ApptentiveLogTagConversation, @"Person diffs found: %@", personDiffs); + ApptentiveLogVerbose(ApptentiveLogTagConversation, @"One or more person fields have changed: %@", ApptentiveHideKeysIfSanitized(personDiffs, [ApptentivePerson sensitiveKeys])); [self.delegate conversation:self personDidChange:personDiffs]; self.lastSentPerson = self.person.JSONDictionary; @@ -390,6 +394,18 @@ - (BOOL)isConsistent { return NO; } } + + if (self.state == ApptentiveConversationStateAnonymousPending || self.state == ApptentiveConversationStateLegacyPending) { + if (self.identifier.length > 0) { + ApptentiveLogError(ApptentiveLogTagConversation, @"Pending conversation has identifier."); + return NO; + } + + if (self.token.length > 0) { + ApptentiveLogError(ApptentiveLogTagConversation, @"Pending conversation has token."); + return NO; + } + } return YES; } @@ -418,7 +434,7 @@ - (void)setUserInfo:(NSObject *)object forKey:(NSString *)key { [_delegate conversationUserInfoDidChange:self]; } } else { - ApptentiveLogError(ApptentiveLogTagConversation, @"Attempting to set user info with nil key and/or value"); + ApptentiveLogError(ApptentiveLogTagConversation, @"Attempting to set user info with nil key and/or value."); } } @@ -426,7 +442,7 @@ - (void)removeUserInfoForKey:(NSString *)key { if (key != nil) { [self.mutableUserInfo removeObjectForKey:key]; } else { - ApptentiveLogError(ApptentiveLogTagConversation, @"Attempting to set user info with nil key and/or value"); + ApptentiveLogError(ApptentiveLogTagConversation, @"Attempting to set user info with nil key and/or value."); } } @@ -529,4 +545,75 @@ - (void)setConversationIdentifier:(NSString *)identifier JWT:(NSString *)JWT { @end +@implementation ApptentiveConversation (Criteria) + +- (nullable NSObject *)valueForFieldWithPath:(NSString *)path { + if ([path hasPrefix:@"code_point/"] || [path hasPrefix:@"interactions/"]) { + return [self.engagement valueForFieldWithPath:path]; + } else if ([path hasPrefix:@"is_update/"] || [path hasPrefix:@"time_at_install/"]) { + return [self.appRelease valueForFieldWithPath:path]; + } else if ([path isEqualToString:@"current_time"]) { + return self.currentTime; + } else { + // Fan out to properties + NSArray *parts = [path componentsSeparatedByString:@"/"]; + NSMutableArray *trimmedParts = [NSMutableArray arrayWithCapacity:parts.count]; + + for (NSString *part in parts) { + [trimmedParts addObject:[part stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]]; + } + + NSString *first = trimmedParts.firstObject; + NSString *rest = [[trimmedParts subarrayWithRange:NSMakeRange(1, parts.count - 1)] componentsJoinedByString:@"/"]; + + if ([first isEqualToString:@"application"]) { + return [self.appRelease valueForFieldWithPath:rest]; + } else if ([first isEqualToString:@"sdk"]) { + return [self.SDK valueForFieldWithPath:rest]; + } else if ([first isEqualToString:@"person"]) { + return [self.person valueForFieldWithPath:rest]; + } else if ([first isEqualToString:@"device"]) { + return [self.device valueForFieldWithPath:rest]; + } + } + + ApptentiveLogError(@"Unrecognized field name “%@”", path); + return nil; +} + +- (NSString *)descriptionForFieldWithPath:(id)path { + if ([path hasPrefix:@"code_point/"] || [path hasPrefix:@"interactions/"]) { + return [self.engagement descriptionForFieldWithPath:path]; + } else if ([path hasPrefix:@"is_update/"] || [path hasPrefix:@"time_at_install/"]) { + return [self.appRelease descriptionForFieldWithPath:path]; + } else if ([path isEqualToString:@"current_time"]) { + return @"current time"; + } else { + // Fan out to properties + NSArray *parts = [path componentsSeparatedByString:@"/"]; + NSMutableArray *trimmedParts = [NSMutableArray arrayWithCapacity:parts.count]; + + for (NSString *part in parts) { + [trimmedParts addObject:[part stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]]; + } + + NSString *first = trimmedParts.firstObject; + NSString *rest = [[trimmedParts subarrayWithRange:NSMakeRange(1, parts.count - 1)] componentsJoinedByString:@"/"]; + + if ([first isEqualToString:@"application"]) { + return [self.appRelease descriptionForFieldWithPath:rest]; + } else if ([first isEqualToString:@"sdk"]) { + return [self.SDK descriptionForFieldWithPath:rest]; + } else if ([first isEqualToString:@"person"]) { + return [self.person descriptionForFieldWithPath:rest]; + } else if ([first isEqualToString:@"device"]) { + return [self.device descriptionForFieldWithPath:rest]; + } + } + + return [NSString stringWithFormat:@"Unrecognized field %@", path]; +} + +@end + NS_ASSUME_NONNULL_END diff --git a/Apptentive/Apptentive/Engagement/Model/ApptentiveConversationManager.m b/Apptentive/Apptentive/Engagement/Model/ApptentiveConversationManager.m index f971594ac..5b0ab1299 100644 --- a/Apptentive/Apptentive/Engagement/Model/ApptentiveConversationManager.m +++ b/Apptentive/Apptentive/Engagement/Model/ApptentiveConversationManager.m @@ -34,6 +34,7 @@ #import "ApptentiveSerialRequest.h" #import "ApptentiveStopWatch.h" #import "ApptentiveUtilities.h" +#import "ApptentiveFileUtilities.h" #import "Apptentive_Private.h" #import "NSData+Encryption.h" #import "ApptentiveDispatchQueue.h" @@ -43,7 +44,7 @@ static NSString *const ConversationMetadataFilename = @"conversation-v1.meta"; static NSString *const ConversationFilename = @"conversation-v1.archive"; -static NSString *const ManifestFilename = @"manifest-v1.archive"; +static NSString *const ManifestFilename = @"manifest-v2.archive"; static NSInteger ApptentiveInternalInconsistency = -201; static NSInteger ApptentiveAlreadyLoggedInErrorCode = -202; @@ -109,6 +110,8 @@ - (BOOL)loadActiveConversation { self.activeConversation.delegate = self; + [self createMessageManagerForConversation:self.activeConversation]; + [self handleConversationStateChange:self.activeConversation]; return true; } @@ -130,7 +133,6 @@ - (nullable ApptentiveConversation *)loadConversation { if (loggedInConversation != nil) { [self loadEngagementManfiest]; - [self createMessageManagerForConversation:loggedInConversation]; return loggedInConversation; } @@ -147,7 +149,6 @@ - (nullable ApptentiveConversation *)loadConversation { if (anonymousConversation != nil) { [self loadEngagementManfiest]; - [self createMessageManagerForConversation:anonymousConversation]; return anonymousConversation; } @@ -188,7 +189,7 @@ - (nullable ApptentiveConversation *)loadConversation { return item.state == ApptentiveConversationStateLoggedOut; }]; if (item != nil) { - ApptentiveLogDebug(ApptentiveLogTagConversation, @"Can't load conversation: only 'logged-out' conversations available"); + ApptentiveLogDebug(ApptentiveLogTagConversation, @"Can't load conversation: only 'logged-out' conversations available."); return nil; } @@ -222,7 +223,7 @@ - (nullable ApptentiveConversation *)loadConversation { } - (nullable ApptentiveConversation *)loadConversationFromMetadataItem:(ApptentiveConversationMetadataItem *)item { - ApptentiveAssertNotNil(item, @"Conversation metadata item is nil"); + ApptentiveAssertNotNil(item, @"Conversation metadata item is nil."); if (item == nil) { return nil; } @@ -244,18 +245,18 @@ - (nullable ApptentiveConversation *)loadConversationFromMetadataItem:(Apptentiv if (item.state == ApptentiveConversationStateLoggedIn || item.state == ApptentiveConversationStateLoggedOut) { ApptentiveStopWatch *decryptionStopWatch = [ApptentiveStopWatch new]; - ApptentiveAssertNotNil(item.encryptionKey, @"Missing encryption key"); + ApptentiveAssertNotNil(item.encryptionKey, @"Missing encryption key."); if (item.encryptionKey == nil) { return nil; } conversationData = [conversationData apptentive_dataDecryptedWithKey:item.encryptionKey]; - ApptentiveAssertNotNil(item.encryptionKey, @"Can't decrypt conversation data"); + ApptentiveAssertNotNil(item.encryptionKey, @"Can't decrypt conversation data."); if (conversationData == nil) { return nil; } - ApptentiveLogVerbose(ApptentiveLogTagConversation, @"Conversation decrypted (took %g ms)", decryptionStopWatch.elapsedMilliseconds); + ApptentiveLogVerbose(ApptentiveLogTagConversation, @"Conversation decrypted (took %g ms).", decryptionStopWatch.elapsedMilliseconds); } ApptentiveConversation *conversation = [NSKeyedUnarchiver unarchiveObjectWithData:conversationData]; @@ -281,15 +282,12 @@ - (nullable ApptentiveConversation *)loadConversationFromMetadataItem:(Apptentiv } - (void)createMessageManagerForConversation:(ApptentiveConversation *)conversation { - ApptentiveAssertNotNil(conversation.token, @"Attempted to create message manager without conversation token"); - ApptentiveAssertNotNil(conversation.identifier, @"Attempted to create message manager without conversation identifier"); - NSString *directoryPath = [self conversationContainerPathForDirectoryName:conversation.directoryName]; - ApptentiveAssertNil(self.messageManager, @"Message manager already exists"); + ApptentiveAssertNil(self.messageManager, @"Message manager already exists."); _messageManager = [[ApptentiveMessageManager alloc] initWithStoragePath:directoryPath client:self.client pollingInterval:Apptentive.shared.backend.configuration.messageCenter.backgroundPollingInterval conversation:conversation operationQueue:self.operationQueue]; - ApptentiveAssertNotNil(self.messageManager, @"Unable to create message manager"); + ApptentiveAssertNotNil(self.messageManager, @"Unable to create message manager."); Apptentive.shared.backend.payloadSender.messageDelegate = self.messageManager; } @@ -313,7 +311,7 @@ - (void)endActiveConversation { conversation.state = ApptentiveConversationStateLoggedOut; - ApptentiveAssertNotNil(self.messageManager, @"Attempted to end active conversation without message manager"); + ApptentiveAssertNotNil(self.messageManager, @"Attempted to end active conversation without message manager."); [self.messageManager saveMessageStore]; [self.messageManager stop]; _messageManager = nil; @@ -323,14 +321,14 @@ - (void)endActiveConversation { self.activeConversation = nil; } else { - ApptentiveLogInfo(@"Attempting to log out, but no conversation is active."); + ApptentiveLogError(ApptentiveLogTagConversation, @"Attempting to log out, but no conversation is active."); } } #pragma mark - Conversation Token Fetching - (void)fetchConversationToken:(ApptentiveConversation *)conversation { - ApptentiveAssertNil(self.conversationOperation, @"Another request fetch request is running"); + ApptentiveAssertNil(self.conversationOperation, @"Another request fetch request is running."); self.conversationOperation.delegate = nil; [self.conversationOperation cancel]; @@ -347,12 +345,14 @@ - (void)fetchConversationToken:(ApptentiveConversation *)conversation { self.conversationOperation = [self.client requestOperationWithRequest:[[ApptentiveConversationRequest alloc] initWithAppInstall:conversation] token:nil delegate:delegate]; [self.client.networkQueue addOperation:self.conversationOperation]; + + [conversation updateLastSentDevice]; } - (BOOL)fetchLegacyConversation:(ApptentiveConversation *)conversation { - ApptentiveAssertNotNil(conversation, @"Conversation is nil"); - ApptentiveAssertNil(conversation.token, @"Conversation token already exists"); - ApptentiveAssertTrue(conversation.legacyToken.length > 0, @"Conversation legacy token is nil or empty"); + ApptentiveAssertNotNil(conversation, @"Conversation is nil."); + ApptentiveAssertNil(conversation.token, @"Conversation token already exists."); + ApptentiveAssertTrue(conversation.legacyToken.length > 0, @"Conversation legacy token is nil or empty."); ApptentiveRequestOperationCallback *delegate = [ApptentiveRequestOperationCallback new]; delegate.operationFinishCallback = ^(ApptentiveRequestOperation *operation) { @@ -379,7 +379,7 @@ - (BOOL)fetchLegacyConversation:(ApptentiveConversation *)conversation { - (void)handleConversationStateChange:(ApptentiveConversation *)conversation { ApptentiveAssertOperationQueue(self.operationQueue); - ApptentiveAssertNotNil(conversation, @"Conversation is nil"); + ApptentiveAssertNotNil(conversation, @"Conversation is nil."); if (conversation != nil) { NSDictionary *userInfo = @{ApptentiveConversationStateDidChangeNotificationKeyConversation: conversation}; [[NSNotificationCenter defaultCenter] postNotificationName:ApptentiveConversationStateDidChangeNotification @@ -399,7 +399,7 @@ - (void)handleConversationStateChange:(ApptentiveConversation *)conversation { } - (void)updateMetadataItems:(ApptentiveConversation *)conversation { - ApptentiveLogVerbose(ApptentiveLogTagConversation, @"Updating metadata: state=%@ localId=%@ conversationId=%@ token=%@", NSStringFromApptentiveConversationState(conversation.state), conversation.localIdentifier, conversation.identifier, conversation.token); + ApptentiveLogVerbose(ApptentiveLogTagConversation, @"Updating metadata:\nstate: %@\nlocalIdentifier: %@\nconversationIdentifier: %@\ntoken: %@", NSStringFromApptentiveConversationState(conversation.state), conversation.localIdentifier, conversation.identifier, ApptentiveHideIfSanitized(conversation.token)); // if the conversation is 'logged-in' we should not have any other 'logged-in' items in metadata if (conversation.state == ApptentiveConversationStateLoggedIn) { @@ -435,14 +435,14 @@ - (void)updateMetadataItems:(ApptentiveConversation *)conversation { item.state = conversation.state; if ([conversation hasActiveState]) { - ApptentiveAssertNotNil(conversation.token, @"Conversation token is nil"); + ApptentiveAssertNotNil(conversation.token, @"Conversation token is nil."); item.JWT = conversation.token; } if (item.state == ApptentiveConversationStateLoggedIn) { - ApptentiveAssertNotNil(conversation.encryptionKey, @"Encryption key is nil"); + ApptentiveAssertNotNil(conversation.encryptionKey, @"Encryption key is nil."); item.encryptionKey = conversation.encryptionKey; - ApptentiveAssertNotNil(conversation.userId, @"User id is nil"); + ApptentiveAssertNotNil(conversation.userId, @"userId is nil."); item.userId = conversation.userId; } @@ -455,14 +455,14 @@ - (ApptentiveConversationMetadata *)resolveMetadata { NSString *metadataPath = self.metadataPath; ApptentiveConversationMetadata *metadata = nil; - if ([ApptentiveUtilities fileExistsAtPath:metadataPath]) { + if ([ApptentiveFileUtilities fileExistsAtPath:metadataPath]) { metadata = [NSKeyedUnarchiver unarchiveObjectWithFile:metadataPath]; if (metadata) { // TODO: dispatch debug event return metadata; } - ApptentiveLogWarning(@"Unable to deserialize metadata from file: %@", metadataPath); + ApptentiveLogError(ApptentiveLogTagConversation, @"Unable to unarchive metadata from file: %@", metadataPath); } return [[ApptentiveConversationMetadata alloc] init]; @@ -490,7 +490,7 @@ - (void)requestLoggedInConversationWithToken:(NSString *)token completion:(void } if (!Apptentive.shared.backend.foreground) { - [self failLoginWithErrorCode:ApptentiveInBackgroundErrorCode failureReason:@"App is in background state"]; + [self failLoginWithErrorCode:ApptentiveInBackgroundErrorCode failureReason:@"App is in background state."]; return; } @@ -499,13 +499,13 @@ - (void)requestLoggedInConversationWithToken:(NSString *)token completion:(void NSError *jwtError; ApptentiveJWT *jwt = [ApptentiveJWT JWTWithContentOfString:token error:&jwtError]; if (jwtError != nil) { - [self failLoginWithErrorCode:ApptentiveInternalInconsistency failureReason:@"JWT parsing error: %@", jwtError]; + [self failLoginWithErrorCode:ApptentiveInternalInconsistency failureReason:@"JWT parsing error (%@).", jwtError]; return; } NSString *userId = jwt.payload[@"sub"]; if (userId.length == 0) { - [self failLoginWithErrorCode:ApptentiveInternalInconsistency failureReason:@"MISSING_SUB_CLAIM"]; + [self failLoginWithErrorCode:ApptentiveInternalInconsistency failureReason:@"Missing sub claim."]; return; } @@ -519,14 +519,14 @@ - (void)requestLoggedInConversationWithToken:(NSString *)token completion:(void }]; if (conversationItem == nil) { - ApptentiveLogVerbose(ApptentiveLogTagConversation, @"Logging in a new user..."); + ApptentiveLogDebug(ApptentiveLogTagConversation, @"Logging in a new user..."); [self sendLoginRequestWithToken:token conversationIdentifier:nil userId:userId]; return; } - ApptentiveAssertNotNil(conversationItem.conversationIdentifier, @"Missing conversation identifier"); + ApptentiveAssertNotNil(conversationItem.conversationIdentifier, @"Missing conversation identifier."); - ApptentiveLogVerbose(ApptentiveLogTagConversation, @"Logging in an existing user (%@)...", userId); + ApptentiveLogDebug(ApptentiveLogTagConversation, @"Logging in an existing user (%@)...", userId); [self sendLoginRequestWithToken:token conversationIdentifier:conversationItem.conversationIdentifier userId:userId]; return; } @@ -534,7 +534,7 @@ - (void)requestLoggedInConversationWithToken:(NSString *)token completion:(void switch (self.activeConversation.state) { case ApptentiveConversationStateAnonymousPending: case ApptentiveConversationStateLegacyPending: - ApptentiveAssertTrue(NO, @"Login operation should not kick off until conversation fetch complete"); + ApptentiveAssertTrue(NO, @"Login operation should not kick off until conversation fetch is complete."); [self failLoginWithErrorCode:ApptentiveInternalInconsistency failureReason:@"Login cannot proceed with Anonymous Pending conversation."]; break; @@ -556,8 +556,8 @@ - (void)requestLoggedInConversationWithToken:(NSString *)token completion:(void - (void)sendLoginRequestWithToken:(NSString *)token conversationIdentifier:(nullable NSString *)conversationIdentifier userId:(NSString *)userId { ApptentiveAssertOperationQueue(self.operationQueue); - ApptentiveAssertNotEmpty(token, @"Attempted to send login request with nil or empty conversation token"); - ApptentiveAssertNotEmpty(userId, @"Attempted to send login request with nil or empty user id"); + ApptentiveAssertNotEmpty(token, @"Attempted to send login request with nil or empty conversation token."); + ApptentiveAssertNotEmpty(userId, @"Attempted to send login request with nil or empty user id."); ApptentiveRequestOperationCallback *delegate = [ApptentiveRequestOperationCallback new]; delegate.operationFinishCallback = ^(ApptentiveRequestOperation *operation) { @@ -705,7 +705,7 @@ - (void)processManifestResponse:(NSDictionary *)manifestResponse cacheLifetime:( - (void)conversation:(ApptentiveConversation *)conversation processLoginResponse:(NSDictionary *)loginResponse userId:(NSString *)userId token:(NSString *)token { ApptentiveAssertOperationQueue(self.operationQueue); - ApptentiveAssertNotEmpty(token, @"Empty token in login request"); + ApptentiveAssertNotEmpty(token, @"Empty token in login request."); NSString *encryptionKey = ApptentiveDictionaryGetString(loginResponse, @"encryption_key"); NSString *deviceIdentifier = ApptentiveDictionaryGetString(loginResponse, @"device_id"); @@ -739,7 +739,7 @@ - (void)conversation:(ApptentiveConversation *)conversation processLoginResponse ApptentiveConversation *existingConversation = [self loadConversationFromMetadataItem:conversationItem]; mutableConversation = [existingConversation mutableCopy]; } else { - [self failLoginWithErrorCode:ApptentiveInternalInconsistency failureReason:@"Mismatching conversation identifiers for user '%@'. Expected '%@' but was '%@'", userId, conversationItem.conversationIdentifier, conversationIdentifier]; + [self failLoginWithErrorCode:ApptentiveInternalInconsistency failureReason:@"Mismatching conversation identifiers for user '%@'. Expected '%@' but was '%@'.", userId, conversationItem.conversationIdentifier, conversationIdentifier]; return; } } else { @@ -750,12 +750,12 @@ - (void)conversation:(ApptentiveConversation *)conversation processLoginResponse mutableConversation.state = ApptentiveConversationStateLoggedIn; mutableConversation.userId = userId; mutableConversation.encryptionKey = [NSData apptentive_dataWithHexString:encryptionKey]; - ApptentiveAssertNotNil(mutableConversation.encryptionKey, @"Apptentive encryption key should be not nil"); + ApptentiveAssertNotNil(mutableConversation.encryptionKey, @"Apptentive encryption key is nil."); - [self.messageManager stopPolling]; - self.messageManager = nil; - - [self createMessageManagerForConversation:mutableConversation]; + // If a user has logged out during since we've launched, we have to recreate the message manager. + if (self.messageManager == nil) { + [self createMessageManagerForConversation:mutableConversation]; + } self.activeConversation = mutableConversation; self.activeConversation.delegate = self; @@ -785,7 +785,6 @@ - (BOOL)updateActiveConversation:(ApptentiveConversation *)conversation withResp } [self.messageManager stop]; - [self createMessageManagerForConversation:mutableConversation]; self.activeConversation = mutableConversation; self.activeConversation.delegate = self; @@ -804,7 +803,7 @@ - (BOOL)updateActiveConversation:(ApptentiveConversation *)conversation withResp } - (BOOL)updateLegacyConversation:(ApptentiveConversation *)conversation withResponse:(NSDictionary *)conversationResponse { - ApptentiveAssertNotNil(conversation, @"Active conversation is nil"); + ApptentiveAssertNotNil(conversation, @"Active conversation is nil."); if (conversation == nil) { return NO; } @@ -825,8 +824,6 @@ - (BOOL)updateLegacyConversation:(ApptentiveConversation *)conversation withResp mutableConversation.state = ApptentiveConversationStateAnonymous; } - [self createMessageManagerForConversation:mutableConversation]; - self.activeConversation = mutableConversation; self.activeConversation.delegate = self; @@ -858,7 +855,7 @@ - (BOOL)saveConversation:(ApptentiveConversation *)conversation { NSError *error; if (![[NSFileManager defaultManager] createDirectoryAtPath:conversationDirectoryPath withIntermediateDirectories:YES attributes:nil error:&error]) { - ApptentiveAssertTrue(NO, @"Unable to create conversation directory “%@” (%@)", conversationDirectoryPath, error); + ApptentiveAssertTrue(NO, @"Unable to create conversation directory %@ (%@)", conversationDirectoryPath, error); return NO; } } @@ -881,9 +878,11 @@ - (BOOL)saveConversation:(ApptentiveConversation *)conversation { // All non-anonymous conversations should be encrypted // We may have just logged out, so also encrypt logged-out conversations if (conversation.state == ApptentiveConversationStateLoggedIn || conversation.state == ApptentiveConversationStateLoggedOut) { + ApptentiveLogDebug(ApptentiveLogTagConversation, @"Saving encrypted conversation data."); + ApptentiveStopWatch *encryptionStopWatch = [[ApptentiveStopWatch alloc] init]; - ApptentiveAssertNotNil(conversation.encryptionKey, @"Missing encryption key"); + ApptentiveAssertNotNil(conversation.encryptionKey, @"Missing encryption key."); if (conversation.encryptionKey == nil) { return NO; } @@ -898,17 +897,17 @@ - (BOOL)saveConversation:(ApptentiveConversation *)conversation { conversationData = [conversationData apptentive_dataEncryptedWithKey:conversation.encryptionKey initializationVector:initializationVector]; if (conversationData == nil) { - ApptentiveLogError(@"Unable to save conversation data: encryption failed"); + ApptentiveLogError(ApptentiveLogTagConversation, @"Unable to save conversation data: encryption failed."); return NO; } - ApptentiveLogVerbose(ApptentiveLogTagConversation, @"Conversation data encrypted (took %g ms)", encryptionStopWatch.elapsedMilliseconds); + ApptentiveLogVerbose(ApptentiveLogTagConversation, @"Conversation data encrypted (took %g ms).", encryptionStopWatch.elapsedMilliseconds); } else { - ApptentiveLogVerbose(ApptentiveLogTagConversation, @"Saving unencrypted conversation data"); + ApptentiveLogDebug(ApptentiveLogTagConversation, @"Saving unencrypted conversation data."); } BOOL succeed = [conversationData writeToFile:file atomically:YES]; - ApptentiveLogDebug(ApptentiveLogTagConversation, @"Conversation data %@saved (took %g ms): location=%@", succeed ? @"" : @"NOT ", saveStopWatch.elapsedMilliseconds, file); + ApptentiveLogDebug(ApptentiveLogTagConversation, @"Conversation data %@saved (took %g ms): location=%@.", succeed ? @"" : @"NOT ", saveStopWatch.elapsedMilliseconds, file); return succeed; } @@ -917,16 +916,16 @@ - (BOOL)saveConversation:(ApptentiveConversation *)conversation { - (void)loadEngagementManfiest { if ([[NSFileManager defaultManager] fileExistsAtPath:self.manifestPath]) { - ApptentiveLogDebug(@"Loading cached engagment manifest from %@", self.manifestPath); + ApptentiveLogDebug(ApptentiveLogTagConversation, @"Loading cached engagment manifest from %@.", self.manifestPath); @try { _manifest = [NSKeyedUnarchiver unarchiveObjectWithFile:self.manifestPath]; [self notifyEngagementManifestUpdate]; } @catch (NSException *exc) { - ApptentiveAssertFail(@"Exception when loading engagement manifest: %@", exc); + ApptentiveAssertFail(@"Exception when loading engagement manifest (%@).", exc); } } else { - ApptentiveLogDebug(@"No cached engagement manifest available at %@", self.manifestPath); + ApptentiveLogDebug(ApptentiveLogTagConversation, @"No cached engagement manifest available at %@.", self.manifestPath); } } @@ -981,7 +980,7 @@ - (void)scheduleSaveConversation:(ApptentiveConversation *)conversation { ApptentiveAssertOperationQueue(self.operationQueue); if (![self saveConversation:conversation]) { - ApptentiveLogError(@"Error saving active conversation."); + ApptentiveLogError(ApptentiveLogTagConversation, @"Error saving active conversation."); } } @@ -1046,14 +1045,14 @@ - (void)updateMissingTimeAtInstall { // Get time at install from the creation date of the `com.apptentive.feedback` directory NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:Apptentive.shared.backend.supportDirectoryPath error:&error]; if (attributes == nil) { - ApptentiveLogError(@"Error retrieving support directory attributes (%@)", error); + ApptentiveLogError(ApptentiveLogTagConversation, @"Error retrieving support directory attributes (%@)", error); return; } NSDate *timeAtInstall = attributes[NSFileCreationDate]; if (timeAtInstall == nil) { - ApptentiveLogError(@"Error retrieving support directory creation date"); + ApptentiveLogError(ApptentiveLogTagConversation, @"Error retrieving support directory creation date."); } [self.activeConversation.appRelease updateMissingTimeAtInstallTo:timeAtInstall]; @@ -1080,7 +1079,7 @@ - (void)setLocalEngagementManifestURL:(NSURL *)localEngagementManifestURL { NSDictionary *manifestDictionary = [ApptentiveJSONSerialization JSONObjectWithData:localData error:&error]; if (!manifestDictionary) { - ApptentiveLogError(@"Unable to parse local manifest %@: %@", localEngagementManifestURL.absoluteString, error); + ApptentiveLogError(ApptentiveLogTagConversation, @"Unable to parse local manifest %@ (%@).", localEngagementManifestURL.absoluteString, error); } _manifest = [[ApptentiveEngagementManifest alloc] initWithJSONDictionary:manifestDictionary cacheLifetime:MAXFLOAT]; diff --git a/Apptentive/Apptentive/Engagement/Model/ApptentiveConversationMetadata.m b/Apptentive/Apptentive/Engagement/Model/ApptentiveConversationMetadata.m index edb42c1a6..2382d6c53 100644 --- a/Apptentive/Apptentive/Engagement/Model/ApptentiveConversationMetadata.m +++ b/Apptentive/Apptentive/Engagement/Model/ApptentiveConversationMetadata.m @@ -111,13 +111,13 @@ - (void)printAsTableWithTitle:(NSString *)title { if (moreInfo.length > 0) { [moreInfo appendString:@"\n"]; } - [moreInfo appendFormat:@"JWT-%ld: %@", (unsigned long)row, item.JWT]; + [moreInfo appendFormat:@"JWT-%ld: %@", (unsigned long)row, ApptentiveHideIfSanitized(item.JWT)]; } if (item.encryptionKey) { if (moreInfo.length > 0) { [moreInfo appendString:@"\n"]; } - [moreInfo appendFormat:@"KEY-%ld: %@", (unsigned long)row, item.encryptionKey]; + [moreInfo appendFormat:@"KEY-%ld: %@", (unsigned long)row, ApptentiveHideIfSanitized(item.encryptionKey)]; } [rows addObject:@[ @@ -143,8 +143,8 @@ - (void)checkConsistency { NSMutableArray *invalidItems = [NSMutableArray array]; for (ApptentiveConversationMetadataItem *item in self.items) { - if (!item.consistent) { - ApptentiveLogError(@"Conversation metadata item %@ is inconsistent. Deleting it...", item); + if (!item.isConsistent) { + ApptentiveLogError(ApptentiveLogTagConversation, @"Conversation metadata item %@ is inconsistent. Deleting it...", item); [invalidItems addObject:item]; } diff --git a/Apptentive/Apptentive/Engagement/Model/ApptentiveConversationMetadataItem.m b/Apptentive/Apptentive/Engagement/Model/ApptentiveConversationMetadataItem.m index cbbb028a4..6056deb16 100644 --- a/Apptentive/Apptentive/Engagement/Model/ApptentiveConversationMetadataItem.m +++ b/Apptentive/Apptentive/Engagement/Model/ApptentiveConversationMetadataItem.m @@ -73,18 +73,18 @@ - (void)encodeWithCoder:(NSCoder *)coder { - (BOOL)isConsistent { if (self.state == ApptentiveConversationStateUndefined) { - ApptentiveLogError(@"Conversation metadata item state is undefined"); + ApptentiveLogError(ApptentiveLogTagConversation, @"Conversation metadata item state is undefined."); return NO; } if (self.directoryName.length == 0) { - ApptentiveLogError(@"Conversation metadata directory name is empty"); + ApptentiveLogError(ApptentiveLogTagConversation, @"Conversation metadata directory name is empty."); return NO; } if (self.state == ApptentiveConversationStateAnonymous || self.state == ApptentiveConversationStateLoggedIn) { if (self.conversationIdentifier.length == 0) { - ApptentiveLogError(@"Conversation metadata conversation identifier is empty for anonymous for state %@", NSStringFromApptentiveConversationState(self.state)); + ApptentiveLogError(ApptentiveLogTagConversation, @"Conversation metadata conversation identifier is empty for state %@.", NSStringFromApptentiveConversationState(self.state)); return NO; } @@ -98,17 +98,12 @@ - (BOOL)isConsistent { if (self.state == ApptentiveConversationStateLoggedIn) { if (self.userId.length == 0) { - ApptentiveLogError(@"Conversation metadata userId is empty for logged-in conversation."); - return NO; - } - - if (self.JWT.length == 0) { - ApptentiveLogError(@"Conversation metadata JWT is empty for logged-in conversation."); + ApptentiveLogError(ApptentiveLogTagConversation, @"Conversation metadata userId is empty for logged-in conversation."); return NO; } if (self.encryptionKey.length == 0) { - ApptentiveLogError(@"Conversation metadata encryption key is empty for logged-in conversation."); + ApptentiveLogError(ApptentiveLogTagConversation, @"Conversation metadata encryption key is empty for logged-in conversation."); return NO; } } diff --git a/Apptentive/Apptentive/Engagement/Model/ApptentiveCount.m b/Apptentive/Apptentive/Engagement/Model/ApptentiveCount.m index 14f337e39..60c7baa29 100644 --- a/Apptentive/Apptentive/Engagement/Model/ApptentiveCount.m +++ b/Apptentive/Apptentive/Engagement/Model/ApptentiveCount.m @@ -110,4 +110,32 @@ + (NSDictionary *)JSONKeyPathMapping { @end +@implementation ApptentiveCount (Criteria) + +- (nullable NSObject *)valueForFieldWithPath:(NSString *)path { + NSArray *parts = [path componentsSeparatedByString:@"/"]; + NSString *invokesOrTime = parts[0]; + NSString *scope = parts[1]; + + if ([invokesOrTime isEqualToString:@"invokes"]) { + if ([scope isEqualToString:@"total"]) { + return @(self.totalCount); + } else if ([scope isEqualToString:@"cf_bundle_short_version_string"]) { + return @(self.versionCount); + } else if ([scope isEqualToString:@"cf_bundle_version"]) { + return @(self.buildCount); + } + + ApptentiveLogError(@"Unrecognized field name “%@”", path); + return nil; + } else if ([invokesOrTime isEqualToString:@"last_invoked_at"] && [scope isEqualToString:@"total"]) { + return self.lastInvoked; + } + + ApptentiveLogError(@"Unrecognized field name “%@”", path); + return nil; +} + +@end + NS_ASSUME_NONNULL_END diff --git a/Apptentive/Apptentive/Engagement/Model/ApptentiveCustomData.m b/Apptentive/Apptentive/Engagement/Model/ApptentiveCustomData.m index f58760361..97ac22682 100644 --- a/Apptentive/Apptentive/Engagement/Model/ApptentiveCustomData.m +++ b/Apptentive/Apptentive/Engagement/Model/ApptentiveCustomData.m @@ -67,7 +67,7 @@ - (void)addCustomString:(NSString *)string withKey:(NSString *)key { if (string != nil && key != nil) { [self.mutableCustomData setObject:string forKey:key]; } else { - ApptentiveLogError(@"Attempting to add custom data string with nil key and/or value"); + ApptentiveLogError(ApptentiveLogTagConversation, @"Attempting to add custom data string with nil key and/or value."); } } @@ -75,7 +75,7 @@ - (void)addCustomNumber:(NSNumber *)number withKey:(NSString *)key { if (number != nil && key != nil) { [self.mutableCustomData setObject:number forKey:key]; } else { - ApptentiveLogError(@"Attempting to add custom data number with nil key and/or value"); + ApptentiveLogError(ApptentiveLogTagConversation, @"Attempting to add custom data number with nil key and/or value."); } } @@ -83,7 +83,7 @@ - (void)addCustomBool:(BOOL)boolValue withKey:(NSString *)key { if (key != nil) { [self.mutableCustomData setObject:@(boolValue) forKey:key]; } else { - ApptentiveLogError(@"Attempting to add custom data boolean with nil key"); + ApptentiveLogError(ApptentiveLogTagConversation, @"Attempting to add custom data boolean with nil key."); } } @@ -91,10 +91,14 @@ - (void)removeCustomValueWithKey:(NSString *)key { if (key != nil) { [self.mutableCustomData removeObjectForKey:key]; } else { - ApptentiveLogError(@"Attempting to remove custom data with nil key"); + ApptentiveLogError(ApptentiveLogTagConversation, @"Attempting to remove custom data with nil key."); } } ++ (NSArray *)sensitiveKeys { + return @[@"custom_data"]; +} + @end @@ -106,4 +110,18 @@ + (NSDictionary *)JSONKeyPathMapping { @end +@implementation ApptentiveCustomData (Criteria) + +- (nullable NSObject *)valueForFieldWithPath:(NSString *)path { + if ([path hasPrefix:@"custom_data/"]) { + NSString *customDataKey = [path substringFromIndex:@"custom_data/".length]; + return self.customData[customDataKey]; + } + + ApptentiveLogError(@"Unrecognized field name “%@”", path); + return nil; +} + +@end + NS_ASSUME_NONNULL_END diff --git a/Apptentive/Apptentive/Engagement/Model/ApptentiveDevice.h b/Apptentive/Apptentive/Engagement/Model/ApptentiveDevice.h index 326781587..d9fd25820 100644 --- a/Apptentive/Apptentive/Engagement/Model/ApptentiveDevice.h +++ b/Apptentive/Apptentive/Engagement/Model/ApptentiveDevice.h @@ -90,6 +90,11 @@ extern NSString *const ATDeviceLastUpdateValuePreferenceKey; */ @property (copy, nonatomic) NSDictionary *integrationConfiguration; +/** + The advertising identifier, if the AdSupport framework is linked, and the user has enabled it + */ +@property (readonly, nullable, strong, nonatomic) NSUUID *advertisingIdentifier; + /** Initializes a device object with values obtained from the current device. @@ -107,6 +112,11 @@ extern NSString *const ATDeviceLastUpdateValuePreferenceKey; */ + (void)getPermanentDeviceValues; +/** + Sets static variable for advertising identifier. + */ ++ (void)getAdvertisingIdentifier; + /** The push integration to be set globally for all devices */ @@ -124,6 +134,11 @@ extern NSString *const ATDeviceLastUpdateValuePreferenceKey; */ @property (class, strong, nonatomic) UIContentSizeCategory contentSizeCategory; +/** + Exposes the static variable _currentAdvertisingIdentifier for reading when debugging. + */ +@property (class, readonly, strong, nonatomic) NSUUID *advertisingIdentifier; + @end NS_ASSUME_NONNULL_END diff --git a/Apptentive/Apptentive/Engagement/Model/ApptentiveDevice.m b/Apptentive/Apptentive/Engagement/Model/ApptentiveDevice.m index 02a8a4c18..05eead3fc 100644 --- a/Apptentive/Apptentive/Engagement/Model/ApptentiveDevice.m +++ b/Apptentive/Apptentive/Engagement/Model/ApptentiveDevice.m @@ -30,6 +30,7 @@ static NSString *const LocaleLanguageCodeKey = @"localeLanguageCode"; static NSString *const UTCOffsetKey = @"UTCOffset"; static NSString *const IntegrationConfigurationKey = @"integrationConfiguration"; +static NSString *const AdvertisingIdentifierKey = @"advertisingIdentifier"; // Legacy keys NSString *const ATDeviceLastUpdateValuePreferenceKey = @"ATDeviceLastUpdateValuePreferenceKey"; @@ -45,6 +46,7 @@ static NSDictionary *_currentIntegrationConfiguration; static NSString *_currentCarrierName; static UIContentSizeCategory _currentContentSizeCategory; +static NSUUID * _Nullable _currentAdvertisingIdentifier; @implementation ApptentiveDevice @@ -73,6 +75,50 @@ + (UIContentSizeCategory)contentSizeCategory { return _currentContentSizeCategory; } ++ (void)getAdvertisingIdentifier { + NSUUID *oldAdvertisingIdentifier = _currentAdvertisingIdentifier; + _currentAdvertisingIdentifier = nil; + @try { + Class IdentifierManager = NSClassFromString(@"ASIdentifierManager"); + if (IdentifierManager) { + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Warc-performSelector-leaks" + id sharedManager = [IdentifierManager performSelector:NSSelectorFromString(@"sharedManager")]; + SEL advertisingIdentifierSelector = NSSelectorFromString(@"advertisingIdentifier"); + SEL advertisingTrackingEnabledSelector = NSSelectorFromString(@"isAdvertisingTrackingEnabled"); + + if (![sharedManager respondsToSelector:advertisingIdentifierSelector] || + ![sharedManager respondsToSelector:advertisingTrackingEnabledSelector]) { + ApptentiveLogDebug(ApptentiveLogTagConversation, @"Unable to get advertising id: required method on ASIdentifierManager not found"); + return; + } + + if (![sharedManager performSelector:advertisingTrackingEnabledSelector]) { + ApptentiveLogDebug(ApptentiveLogTagConversation, @"Unable to get advertising id: advertising tracking disabled"); + return; + } + + NSUUID *advertisingIdentifier = [sharedManager performSelector:advertisingIdentifierSelector]; + if ([advertisingIdentifier.UUIDString isEqualToString:@"00000000-0000-0000-0000-000000000000"]) { + ApptentiveLogDebug(ApptentiveLogTagConversation, @"Unable to get advertising id: invalid value"); + return; + } + + if (![advertisingIdentifier isEqual:oldAdvertisingIdentifier]) { + ApptentiveLogVerbose(ApptentiveLogTagConversation, @"Updated advertising id: %@", advertisingIdentifier); + } + _currentAdvertisingIdentifier = advertisingIdentifier; + #pragma clang diagnostic pop + } + } @catch (NSException *e) { + ApptentiveLogError(ApptentiveLogTagConversation, @"Exception while trying to resolve advertising id.\n%@", e); + } +} + ++ (NSUUID *)advertisingIdentifier { + return _currentAdvertisingIdentifier; +} + + (void)getPermanentDeviceValues { _currentUUID = [UIDevice currentDevice].identifierForVendor; _currentOSName = [UIDevice currentDevice].systemName; @@ -129,6 +175,7 @@ - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder { _localeLanguageCode = [aDecoder decodeObjectOfClass:[NSString class] forKey:LocaleLanguageCodeKey]; _UTCOffset = [aDecoder decodeIntegerForKey:UTCOffsetKey]; _integrationConfiguration = [aDecoder decodeObjectOfClass:[NSDictionary class] forKey:IntegrationConfigurationKey]; + _advertisingIdentifier = [aDecoder decodeObjectOfClass:[NSUUID class] forKey:AdvertisingIdentifierKey]; } return self; @@ -149,6 +196,7 @@ - (void)encodeWithCoder:(NSCoder *)aCoder { [aCoder encodeObject:self.localeLanguageCode forKey:LocaleLanguageCodeKey]; [aCoder encodeInteger:self.UTCOffset forKey:UTCOffsetKey]; [aCoder encodeObject:self.integrationConfiguration forKey:IntegrationConfigurationKey]; + [aCoder encodeObject:self.advertisingIdentifier forKey:AdvertisingIdentifierKey]; } - (instancetype)initAndMigrate { @@ -175,6 +223,10 @@ + (void)deleteMigratedData { [[NSUserDefaults standardUserDefaults] removeObjectForKey:ApptentiveCustomDeviceDataPreferenceKey]; } ++ (NSArray *)sensitiveKeys { + return [super.sensitiveKeys arrayByAddingObject:@"uuid"]; +} + #pragma mark - Private - (void)updateWithCurrentDeviceValues { @@ -194,6 +246,8 @@ - (void)updateWithCurrentDeviceValues { _UTCOffset = [NSTimeZone systemTimeZone].secondsFromGMT; _integrationConfiguration = ApptentiveDevice.integrationConfiguration; + + _advertisingIdentifier = _currentAdvertisingIdentifier; } @end @@ -213,6 +267,10 @@ - (NSString *)OSVersionString { return self.OSVersion.versionString; } +- (NSString *)advertisingIdentifierString { + return self.advertisingIdentifier.UUIDString; +} + + (NSDictionary *)JSONKeyPathMapping { return @{ @"custom_data": NSStringFromSelector(@selector(customData)), @@ -227,10 +285,80 @@ + (NSDictionary *)JSONKeyPathMapping { @"locale_country_code": NSStringFromSelector(@selector(localeCountryCode)), @"locale_language_code": NSStringFromSelector(@selector(localeLanguageCode)), @"utc_offset": NSStringFromSelector(@selector(boxedUTCOffset)), - @"integration_config": NSStringFromSelector(@selector(integrationConfiguration)) + @"integration_config": NSStringFromSelector(@selector(integrationConfiguration)), + @"advertiser_id": NSStringFromSelector(@selector(advertisingIdentifierString)) }; } @end +@implementation ApptentiveDevice (Criteria) + +- (nullable NSObject *)valueForFieldWithPath:(NSString *)path { + if ([path isEqualToString:@"uuid"]) { + return self.UUIDString; + } else if ([path isEqualToString:@"os_name"]) { + return self.OSName; + } else if ([path isEqualToString:@"os_version"]) { + return [[ApptentiveVersion alloc] initWithString:self.OSVersionString]; + } else if ([path isEqualToString:@"os_build"]) { + return self.OSBuild; + } else if ([path isEqualToString:@"hardware"]) { + return self.hardware; + } else if ([path isEqualToString:@"carrier"]) { + return self.carrier; + } else if ([path isEqualToString:@"content_size_category"]) { + return self.contentSizeCategory; + } else if ([path isEqualToString:@"locale_raw"]) { + return self.localeRaw; + } else if ([path isEqualToString:@"locale_country_code"]) { + return self.localeCountryCode; + } else if ([path isEqualToString:@"locale_language_code"]) { + return self.localeLanguageCode; + } else if ([path isEqualToString:@"utc_offset"]) { + return self.boxedUTCOffset; + } else if ([path isEqualToString:@"integration_config"]) { + return self.integrationConfiguration; + } else { + return [super valueForFieldWithPath:path]; + } +} + +- (NSString *)descriptionForFieldWithPath:(NSString *)path { + if ([path isEqualToString:@"uuid"]) { + return @"device identifier (identifierForVendor)"; + } else if ([path isEqualToString:@"os_name"]) { + return @"device OS name"; + } else if ([path isEqualToString:@"os_version"]) { + return @"device OS version"; + } else if ([path isEqualToString:@"os_build"]) { + return @"device OS build"; + } else if ([path isEqualToString:@"hardware"]) { + return @"device hardware"; + } else if ([path isEqualToString:@"carrier"]) { + return @"device carrier"; + } else if ([path isEqualToString:@"content_size_category"]) { + return @"device content size category"; + } else if ([path isEqualToString:@"locale_raw"]) { + return @"device raw locale"; + } else if ([path isEqualToString:@"locale_country_code"]) { + return @"device locale country code"; + } else if ([path isEqualToString:@"locale_language_code"]) { + return @"device locale language code"; + } else if ([path isEqualToString:@"utc_offset"]) { + return @"device UTC offset"; + } else if ([path isEqualToString:@"integration_config"]) { + return @"device integration configuration"; + } else { + NSArray *parts = [path componentsSeparatedByString:@"/"]; + if (parts.count != 2 || ![parts[0] isEqualToString:@"custom_data"]) { + return [NSString stringWithFormat:@"Unrecognized device field %@", path]; + } + + return [NSString stringWithFormat:@"device_data[%@]", parts[1]]; + } +} + +@end + NS_ASSUME_NONNULL_END diff --git a/Apptentive/Apptentive/Engagement/Model/ApptentiveEngagement.m b/Apptentive/Apptentive/Engagement/Model/ApptentiveEngagement.m index 3449ffc65..565459908 100644 --- a/Apptentive/Apptentive/Engagement/Model/ApptentiveEngagement.m +++ b/Apptentive/Apptentive/Engagement/Model/ApptentiveEngagement.m @@ -158,6 +158,70 @@ + (NSDictionary *)JSONKeyPathMapping { }; } +@end + +@implementation ApptentiveEngagement (Criteria) + +- (nullable NSObject *)valueForFieldWithPath:(NSString *)path { + NSArray *parts = [path componentsSeparatedByString:@"/"]; + + if (parts.count != 4) { + ApptentiveLogError(@"Invalid field name “%@”", path); + return nil; + } + + NSString *key = parts[1]; + + if ([parts.firstObject isEqualToString:@"code_point"]) { + [self warmCodePoint:key]; + } else if ([parts.firstObject isEqualToString:@"interactions"]) { + [self warmInteraction:key]; + } + + NSDictionary *values = @{ @"code_point": self.codePoints, @"interactions": self.interactions }; + + ApptentiveCount *count = [values[parts.firstObject] objectForKey:key]; + + if (count == nil) { + ApptentiveLogError(@"%@ “%@” not found", parts.firstObject, key); + return nil; + } + + return [count valueForFieldWithPath:[[parts subarrayWithRange:NSMakeRange(2, 2)] componentsJoinedByString:@"/"]]; +} + +- (NSString *)descriptionForFieldWithPath:(NSString *)path { + NSArray *parts = [path componentsSeparatedByString:@"/"]; + + if (parts.count != 4) { + ApptentiveLogError(@"Invalid field name “%@”", path); + return [NSString stringWithFormat:@"Unrecognized engagement field %@", path]; + } + + NSString *type = [parts[0] isEqualToString:@"code_point"] ? @"event" : @"interaction"; + NSString *target = parts[1]; + NSString *invokesOrTime = parts[2]; + NSString *scope = parts[3]; + + if ([invokesOrTime isEqualToString:@"invokes"]) { + if ([scope isEqualToString:@"total"]) { + return [NSString stringWithFormat:@"number of invokes for %@ '%@'", type, target]; + } else if ([scope isEqualToString:@"cf_bundle_short_version_string"]) { + // TODO: Could print out version here to match Android + return [NSString stringWithFormat:@"number of invokes for %@ '%@' for current version", type, target]; + } else if ([scope isEqualToString:@"cf_bundle_version"]) { + // TODO: Could print out build here to match Android + return [NSString stringWithFormat:@"number of invokes for %@ '%@' for current build", type, target]; + } + } else if ([invokesOrTime isEqualToString:@"last_invoked_at"] && [scope isEqualToString:@"total"]) { + return [NSString stringWithFormat:@"last time %@ '%@' was invoked", type, target]; + } + + return [NSString stringWithFormat:@"Unrecognized engagement field %@", path]; +} + + + @end NS_ASSUME_NONNULL_END diff --git a/Apptentive/Apptentive/Engagement/Model/ApptentiveEngagementManifest.h b/Apptentive/Apptentive/Engagement/Model/ApptentiveEngagementManifest.h index f85fb53bc..418fd4b14 100644 --- a/Apptentive/Apptentive/Engagement/Model/ApptentiveEngagementManifest.h +++ b/Apptentive/Apptentive/Engagement/Model/ApptentiveEngagementManifest.h @@ -10,7 +10,7 @@ NS_ASSUME_NONNULL_BEGIN -@class ApptentiveInteraction; +@class ApptentiveInteraction, ApptentiveTargets; /** @@ -25,7 +25,7 @@ NS_ASSUME_NONNULL_BEGIN of interaction invocations (a combination of an interaction identfier and criteria that must be met for the interaction to be engage). */ -@property (readonly, strong, nonatomic) NSDictionary *targets; +@property (readonly, strong, nonatomic) ApptentiveTargets *targets; /** A dictionary whose keys are interaction identifiers, and whose values are diff --git a/Apptentive/Apptentive/Engagement/Model/ApptentiveEngagementManifest.m b/Apptentive/Apptentive/Engagement/Model/ApptentiveEngagementManifest.m index 01ff23043..d98a46837 100644 --- a/Apptentive/Apptentive/Engagement/Model/ApptentiveEngagementManifest.m +++ b/Apptentive/Apptentive/Engagement/Model/ApptentiveEngagementManifest.m @@ -8,7 +8,7 @@ #import "ApptentiveEngagementManifest.h" #import "ApptentiveInteraction.h" -#import "ApptentiveInteractionInvocation.h" +#import "ApptentiveTargets.h" NS_ASSUME_NONNULL_BEGIN @@ -32,7 +32,7 @@ - (instancetype)init { self = [super init]; if (self) { - _targets = @{}; + _targets = [[ApptentiveTargets alloc] initWithTargetsDictionary:@{}]; _interactions = @{}; _expiry = [NSDate distantPast]; @@ -51,15 +51,7 @@ - (instancetype)initWithJSONDictionary:(NSDictionary *)JSONDictionary cacheLifet // Targets NSDictionary *targetsDictionary = JSONDictionary[@"targets"]; if ([targetsDictionary isKindOfClass:[NSDictionary class]]) { - NSMutableDictionary *targets = [NSMutableDictionary dictionary]; - - for (NSString *event in [targetsDictionary allKeys]) { - NSArray *invocationsJSONArray = targetsDictionary[event]; - NSArray *invocationsArray = [ApptentiveInteractionInvocation invocationsWithJSONArray:invocationsJSONArray]; - ApptentiveDictionarySetKeyValue(targets, event, invocationsArray); - } - - _targets = [NSDictionary dictionaryWithDictionary:targets]; + _targets = [[ApptentiveTargets alloc] initWithTargetsDictionary:targetsDictionary]; } // Interactions @@ -91,7 +83,7 @@ - (instancetype)initWithCachePath:(NSString *)cachePath userDefaults:(NSUserDefa @try { _targets = [NSKeyedUnarchiver unarchiveObjectWithFile:cachedTargetsPath]; } @catch (NSException *exception) { - ApptentiveLogError(@"Unable to unarchive cached targets at path %@ (%@)", cachedTargetsPath, exception); + ApptentiveLogWarning(ApptentiveLogTagConversation, @"Unable to unarchive cached targets at path %@ (%@)", cachedTargetsPath, exception); } } @@ -100,7 +92,7 @@ - (instancetype)initWithCachePath:(NSString *)cachePath userDefaults:(NSUserDefa @try { _interactions = [NSKeyedUnarchiver unarchiveObjectWithFile:cachedInteractionsPath]; } @catch (NSException *exception) { - ApptentiveLogError(@"Unable to unarchive cached interactions at path %@ (%@)", cachedInteractionsPath, exception); + ApptentiveLogWarning(ApptentiveLogTagConversation, @"Unable to unarchive cached interactions at path %@ (%@)", cachedInteractionsPath, exception); } } } @@ -116,12 +108,12 @@ + (void)deleteMigratedDataFromCachePath:(NSString *)cachePath { NSError *error; NSString *targetsCachePath = [cachePath stringByAppendingPathComponent:@"cachedtargets.objects"]; if (![[NSFileManager defaultManager] removeItemAtPath:targetsCachePath error:&error]) { - ApptentiveLogError(@"Unable to remove migrated target data: %@", error); + ApptentiveLogWarning(ApptentiveLogTagConversation, @"Unable to remove migrated target data: %@", error); } NSString *cachedInteractionsPath = [cachePath stringByAppendingPathComponent:@"cachedinteractionsV2.objects"]; if (![[NSFileManager defaultManager] removeItemAtPath:cachedInteractionsPath error:&error]) { - ApptentiveLogError(@"Unable to remove migrated interactions data: %@", error); + ApptentiveLogWarning(ApptentiveLogTagConversation, @"Unable to remove migrated interactions data: %@", error); } } @@ -129,7 +121,7 @@ - (nullable instancetype)initWithCoder:(NSCoder *)coder { self = [super init]; if (self) { - _targets = [coder decodeObjectOfClass:[NSDictionary class] forKey:TargetsKey]; + _targets = [coder decodeObjectOfClass:[ApptentiveTargets class] forKey:TargetsKey]; _interactions = [coder decodeObjectOfClass:[NSDictionary class] forKey:InteractionsKey]; _expiry = [coder decodeObjectOfClass:[NSDate class] forKey:ExpiryKey]; } diff --git a/Apptentive/Apptentive/Engagement/Model/ApptentiveInteraction.m b/Apptentive/Apptentive/Engagement/Model/ApptentiveInteraction.m index ba2c09008..cf75360dd 100644 --- a/Apptentive/Apptentive/Engagement/Model/ApptentiveInteraction.m +++ b/Apptentive/Apptentive/Engagement/Model/ApptentiveInteraction.m @@ -9,7 +9,6 @@ #import "ApptentiveInteraction.h" #import "ApptentiveBackend+Engagement.h" #import "ApptentiveInteractionController.h" -#import "ApptentiveInteractionUsageData.h" #import "ApptentiveUtilities.h" #import "Apptentive_Private.h" diff --git a/Apptentive/Apptentive/Engagement/Model/ApptentiveInteractionInvocation.h b/Apptentive/Apptentive/Engagement/Model/ApptentiveInteractionInvocation.h deleted file mode 100644 index f2a04d651..000000000 --- a/Apptentive/Apptentive/Engagement/Model/ApptentiveInteractionInvocation.h +++ /dev/null @@ -1,31 +0,0 @@ -// -// ApptentiveInteractionInvocation.h -// Apptentive -// -// Created by Peter Kamb on 12/10/14. -// Copyright (c) 2014 Apptentive, Inc. All rights reserved. -// - -#import - -NS_ASSUME_NONNULL_BEGIN - -@class ApptentiveInteractionUsageData, ApptentiveConversation; - - -@interface ApptentiveInteractionInvocation : NSObject - -@property (copy, nonatomic) NSString *interactionID; -@property (assign, nonatomic) NSInteger priority; -@property (nullable, copy, nonatomic) NSDictionary *criteria; - -+ (ApptentiveInteractionInvocation *)invocationWithJSONDictionary:(NSDictionary *)jsonDictionary; -+ (NSArray *)invocationsWithJSONArray:(NSArray *)jsonArray; - -- (BOOL)criteriaAreMetForConversation:(ApptentiveConversation *)data; - -- (NSPredicate *)criteriaPredicate; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Apptentive/Apptentive/Engagement/Model/ApptentiveInteractionInvocation.m b/Apptentive/Apptentive/Engagement/Model/ApptentiveInteractionInvocation.m deleted file mode 100644 index 990fba2f6..000000000 --- a/Apptentive/Apptentive/Engagement/Model/ApptentiveInteractionInvocation.m +++ /dev/null @@ -1,458 +0,0 @@ -// -// ApptentiveInteractionInvocation.m -// Apptentive -// -// Created by Peter Kamb on 12/10/14. -// Copyright (c) 2014 Apptentive, Inc. All rights reserved. -// - -#import "ApptentiveInteractionInvocation.h" - -#import "ApptentiveBackend+Engagement.h" -#import "ApptentiveInteractionUsageData.h" -#import "ApptentiveUtilities.h" -#import "Apptentive_Private.h" - -NS_ASSUME_NONNULL_BEGIN - - -@implementation ApptentiveInteractionInvocation - -+ (void)load { - [NSKeyedUnarchiver setClass:self forClassName:@"ATInteractionInvocation"]; -} - -+ (ApptentiveInteractionInvocation *)invocationWithJSONDictionary:(NSDictionary *)jsonDictionary { - ApptentiveInteractionInvocation *invocation = [[ApptentiveInteractionInvocation alloc] init]; - invocation.interactionID = jsonDictionary[@"interaction_id"]; - invocation.priority = [jsonDictionary[@"priority"] integerValue]; - invocation.criteria = jsonDictionary[@"criteria"]; - - return invocation; -} - -+ (NSArray *)invocationsWithJSONArray:(NSArray *)jsonArray { - NSMutableArray *invocations = [NSMutableArray array]; - - for (NSObject *invocationObject in jsonArray) { - ApptentiveInteractionInvocation *invocation = nil; - - // Handle arrays of both Invocation and NSDictionary - if ([invocationObject isKindOfClass:[ApptentiveInteractionInvocation class]]) { - invocation = (ApptentiveInteractionInvocation *)invocationObject; - } else if ([invocationObject isKindOfClass:[NSDictionary class]]) { - invocation = [ApptentiveInteractionInvocation invocationWithJSONDictionary:(NSDictionary *)invocationObject]; - } - - ApptentiveArrayAddObject(invocations, invocation); - } - - return invocations; -} - -- (NSString *)description { - NSDictionary *description = @{ @"interaction_id": self.interactionID ?: [NSNull null], - @"priority": [NSNumber numberWithInteger:self.priority] ?: [NSNull null], - @"criteria": self.criteria ?: [NSNull null] }; - - return [description description]; -} - -- (nullable instancetype)initWithCoder:(NSCoder *)coder { - if ((self = [super init])) { - self.interactionID = [coder decodeObjectForKey:@"interactionID"]; - self.priority = [coder decodeIntegerForKey:@"priority"]; - self.criteria = [coder decodeObjectForKey:@"criteria"]; - } - - return self; -} - -- (void)encodeWithCoder:(NSCoder *)coder { - [coder encodeObject:self.interactionID forKey:@"interactionID"]; - [coder encodeInteger:self.priority forKey:@"priority"]; - [coder encodeObject:self.criteria forKey:@"criteria"]; -} - -- (id)copyWithZone:(nullable NSZone *)zone { - ApptentiveInteractionInvocation *copy = [[ApptentiveInteractionInvocation alloc] init]; - - if (copy) { - copy.interactionID = self.interactionID; - copy.priority = self.priority; - copy.criteria = self.criteria; - } - - return copy; -} - -- (BOOL)criteriaAreMetForUsageData:(ApptentiveInteractionUsageData *)usageData { - BOOL criteriaAreMet = NO; - - if (!self.criteria) { - // Interactions without a criteria object should evaluate to FALSE. - criteriaAreMet = NO; - } else if (self.criteria && self.criteria.count == 0) { - // Interactions with no keys in the criteria dictionary should evaluate to TRUE. - criteriaAreMet = YES; - } else { - @try { - NSPredicate *predicate = [self criteriaPredicate]; - if (predicate) { - NSDictionary *predicateEvaluationDictionary = [usageData predicateEvaluationDictionary]; - if (predicateEvaluationDictionary) { - criteriaAreMet = [predicate evaluateWithObject:predicateEvaluationDictionary]; - if (!criteriaAreMet) { - ApptentiveLogInfo(@"Criteria for showing interaction %@ not met.", self.interactionID); - } - } else { - ApptentiveLogError(@"Could not create predicate evaluation data."); - criteriaAreMet = NO; - } - } else { - ApptentiveLogError(@"Could not create a valid criteria predicate for the Interaction criteria: %@", self.criteria); - criteriaAreMet = NO; - } - } - @catch (NSException *exception) { - ApptentiveLogError(@"Exception while processing criteria."); - criteriaAreMet = NO; - } - } - - return criteriaAreMet; -} - -- (BOOL)criteriaAreMetForConversation:(ApptentiveConversation *)conversation { - return [self criteriaAreMetForUsageData:[ApptentiveInteractionUsageData usageDataWithConversation:conversation]]; -} - -- (NSPredicate *)criteriaPredicate { - NSPredicate *criteriaPredicate = [ApptentiveInteractionInvocation compoundPredicateWithCriteria:self.criteria]; - - return criteriaPredicate; -} - -+ (nullable NSCompoundPredicate *)compoundPredicateWithCriteria:(NSDictionary *)criteria { - NSMutableArray *subPredicates = [NSMutableArray array]; - - for (NSString *key in criteria) { - NSObject *parameter = [criteria objectForKey:key]; - if ([parameter isKindOfClass:[NSString class]]) { - parameter = [(NSString *)parameter stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; - } - - NSMutableArray *trimmedKeyParts = [NSMutableArray array]; - for (NSString *part in [key componentsSeparatedByString:@"/"]) { - NSString *trimmedPart = [part stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; - [trimmedKeyParts addObject:[ApptentiveUtilities stringByEscapingForPredicate:trimmedPart]]; - } - NSString *trimmedKey = [trimmedKeyParts componentsJoinedByString:@"/"]; - - NSPredicate *predicate = nil; - - BOOL parameterIsArray = [parameter isKindOfClass:[NSArray class]]; - BOOL parameterIsDictionary = [parameter isKindOfClass:[NSDictionary class]]; - BOOL parameterIsComplexType = parameterIsDictionary && [((NSDictionary *)parameter).allKeys containsObject:@"_type"]; - BOOL parameterIsPrimitiveType = [parameter isKindOfClass:[NSString class]] || [parameter isKindOfClass:[NSNumber class]] || [parameter isKindOfClass:[NSNull class]]; - - if (parameterIsPrimitiveType || parameterIsComplexType) { - predicate = [self compoundPredicateForKeyPath:trimmedKey operatorsAndValues:@{ @"==": parameter }]; - } else if (parameterIsArray) { - BOOL hasError = NO; - NSCompoundPredicateType predicateType = [self compoundPredicateTypeFromString:trimmedKey hasError:&hasError]; - if (!hasError) { - predicate = [self compoundPredicateWithType:predicateType criteriaArray:(NSArray *)parameter]; - } - } else if (parameterIsDictionary) { - NSDictionary *dictionaryValue = (NSDictionary *)parameter; - if ([dictionaryValue.allKeys.firstObject isEqualToString:@"$not"]) { - NSString *notKey = dictionaryValue.allKeys.firstObject; - BOOL hasError; - NSCompoundPredicateType predicateType = [self compoundPredicateTypeFromString:notKey hasError:&hasError]; - if (!hasError) { - predicate = [self compoundPredicateWithType:predicateType criteriaArray:@[@{trimmedKey: dictionaryValue[notKey]}]]; - } - } else if ([trimmedKey isEqualToString:@"$not"]) { - // Work around "Common Law Feature" where $not expressions are incorrect - BOOL hasError = NO; - NSCompoundPredicateType predicateType = [self compoundPredicateTypeFromString:trimmedKey hasError:&hasError]; - if (!hasError) { - predicate = [self compoundPredicateWithType:predicateType criteriaArray:@[parameter]]; - } - } else { - predicate = [self compoundPredicateForKeyPath:trimmedKey operatorsAndValues:(NSDictionary *)parameter]; - } - } - - ApptentiveArrayAddObject(subPredicates, predicate); - - if (!predicate) { - return nil; - } - } - - NSCompoundPredicate *compoundPredicate = [NSCompoundPredicate andPredicateWithSubpredicates:subPredicates]; - - return compoundPredicate; -} - -+ (nullable NSCompoundPredicate *)compoundPredicateWithType:(NSCompoundPredicateType)type criteriaArray:(NSArray *)criteriaArray { - NSMutableArray *subPredicates = [NSMutableArray array]; - - for (NSDictionary *criteria in criteriaArray) { - NSPredicate *predicate = [self compoundPredicateWithCriteria:criteria]; - ApptentiveArrayAddObject(subPredicates, predicate); - - if (!predicate) { - return nil; - } - } - - NSCompoundPredicate *compoundPredicate = [[NSCompoundPredicate alloc] initWithType:type subpredicates:subPredicates]; - - return compoundPredicate; -} - -+ (nullable NSCompoundPredicate *)compoundPredicateForKeyPath:(NSString *)keyPath operatorsAndValues:(NSDictionary *)operatorsAndValues { - NSMutableArray *subPredicates = [NSMutableArray array]; - - for (NSString *operatorString in operatorsAndValues) { - NSObject *parameter = operatorsAndValues[operatorString]; - if ([parameter isKindOfClass:[NSString class]]) { - parameter = [(NSString *)parameter stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; - } - - NSPredicate *predicate = nil; - - BOOL parameterIsDictionary = [parameter isKindOfClass:[NSDictionary class]]; - BOOL parameterIsPrimitiveType = [parameter isKindOfClass:[NSString class]] || [parameter isKindOfClass:[NSNumber class]] || [parameter isKindOfClass:[NSNull class]]; - - if ([operatorString isEqualToString:@"$exists"]) { - if ([parameter isEqual:@YES] || [parameter isEqual:@NO]) { - NSString *predicateFormatString = [[@"(%K " stringByAppendingString:([(NSNumber *)parameter boolValue] ? @"!=" : @"==")] stringByAppendingString:@" nil)"]; - predicate = [NSPredicate predicateWithFormat:predicateFormatString, keyPath]; - } else { - predicate = [NSPredicate predicateWithValue:NO]; - } - } else if ([operatorString isEqualToString:@"$before"] || [operatorString isEqualToString:@"$after"]) { - predicate = [NSPredicate predicateWithBlock:^BOOL(id _Nonnull evaluatedObject, NSDictionary *_Nullable bindings) { - NSDictionary *complexValue = [evaluatedObject valueForKeyPath:keyPath]; - - // Time comparison with "never" always returns false. - if (complexValue == nil || [complexValue isKindOfClass:[NSNull class]]) { - return NO; - } - - // $before and $after work with datetimes only. - if ([complexValue[@"_type"] isEqualToString:@"datetime"]) { - NSNumber *fieldValue = (NSNumber *)[complexValue valueForKey:@"sec"]; - NSNumber *parameterNumber = (NSNumber *)parameter; - - if (fieldValue && parameterNumber) { - NSTimeInterval fieldSeconds = fieldValue.doubleValue; - NSTimeInterval parameterSeconds = parameterNumber.doubleValue + [[NSDate date] timeIntervalSince1970]; - if ([operatorString isEqualToString:@"$before"]) { - return fieldSeconds < parameterSeconds; - } else { - return fieldSeconds > parameterSeconds; - } - } - } - - return NO; - }]; - } else if (parameterIsPrimitiveType) { - BOOL hasError; - NSPredicateOperatorType operatorType = [self predicateOperatorTypeFromString:operatorString hasError:&hasError]; - if (!hasError && [self operator:operatorType isValidForParameter:parameter]) { - predicate = [self predicateWithLeftKeyPath:keyPath rightValue:parameter operatorType:operatorType]; - } else { - predicate = [NSPredicate predicateWithValue:NO]; - } - } else if (parameterIsDictionary) { - predicate = [NSCompoundPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) { - BOOL hasError; - NSPredicateOperatorType operatorType = [self predicateOperatorTypeFromString:operatorString hasError:&hasError]; - if (!hasError && [self operator:operatorType isValidForParameter:parameter]) { - NSPredicate *localPredicate = [self predicateWithLeftKeyPath:keyPath forObject:evaluatedObject rightComplexObject:(NSDictionary *)parameter operatorType:operatorType]; - - return [localPredicate evaluateWithObject:nil]; - } else { - return NO; - } - }]; - } - - ApptentiveArrayAddObject(subPredicates, predicate); - if (!predicate) { - return nil; - } - } - - NSCompoundPredicate *compoundPredicate = [[NSCompoundPredicate alloc] initWithType:NSAndPredicateType subpredicates:subPredicates]; - - return compoundPredicate; -} - -+ (NSPredicate *)predicateWithLeftKeyPath:(NSString *)leftKeyPath rightValue:(nullable id)rightValue operatorType:(NSPredicateOperatorType)operatorType { - [ApptentiveInteractionUsageData keyPathWasSeen:leftKeyPath]; - - NSExpression *leftExpression = [NSExpression expressionForKeyPath:leftKeyPath]; - NSExpression *rightExpression = [NSExpression expressionForConstantValue:rightValue]; - - return [self predicateWithLeftExpression:leftExpression rightExpression:rightExpression operatorType:operatorType]; -} - -+ (nullable NSPredicate *)predicateWithLeftKeyPath:(NSString *)keyPath forObject:(NSDictionary *)context rightComplexObject:(NSDictionary *)rightComplexObject operatorType:(NSPredicateOperatorType)operatorType { - NSDictionary *leftComplexObject = [context valueForKeyPath:keyPath]; - NSString *type = leftComplexObject[@"_type"]; - NSString *rightType = rightComplexObject[@"_type"]; - if (![type isEqualToString:rightType]) { - ApptentiveLogError(@"Criteria Complex Type objects must be of the same type!"); - return nil; - } - - NSObject *leftValue; - NSObject *rightValue; - - if ([type isEqualToString:@"version"]) { - NSString *leftVersion = leftComplexObject[@"version"]; - NSString *rightVersion = rightComplexObject[@"version"]; - - NSComparisonResult result = [ApptentiveUtilities compareVersionString:leftVersion toVersionString:rightVersion]; - - switch (result) { - case NSOrderedAscending: - leftValue = @0; - rightValue = @1; - break; - case NSOrderedDescending: - leftValue = @1; - rightValue = @0; - break; - case NSOrderedSame: - leftValue = @1; - rightValue = @1; - break; - } - } else if ([type isEqualToString:@"datetime"]) { - leftValue = leftComplexObject[@"sec"]; - rightValue = rightComplexObject[@"sec"]; - } - - NSPredicate *predicate = [self predicateWithLeftValue:leftValue rightValue:rightValue operatorType:operatorType]; - - return predicate; -} - -+ (NSPredicate *)predicateWithLeftValue:(nullable id)leftValue rightValue:(nullable id)rightValue operatorType:(NSPredicateOperatorType)operatorType { - NSExpression *leftExpression = [NSExpression expressionForConstantValue:leftValue]; - NSExpression *rightExpression = [NSExpression expressionForConstantValue:rightValue]; - - return [self predicateWithLeftExpression:leftExpression rightExpression:rightExpression operatorType:operatorType]; -} - -+ (NSPredicate *)predicateWithLeftExpression:(NSExpression *)leftExpression rightExpression:(NSExpression *)rightExpression operatorType:(NSPredicateOperatorType)operatorType { - NSComparisonPredicateOptions options; - Class comparisonClass; - switch (operatorType) { - case NSContainsPredicateOperatorType: - case NSBeginsWithPredicateOperatorType: - case NSEndsWithPredicateOperatorType: - options = NSCaseInsensitivePredicateOption; - comparisonClass = [NSString class]; - break; - case NSGreaterThanOrEqualToPredicateOperatorType: - case NSGreaterThanPredicateOperatorType: - case NSLessThanPredicateOperatorType: - case NSLessThanOrEqualToPredicateOperatorType: - comparisonClass = [NSNumber class]; - // fall through - default: - options = 0; - break; - } - - NSComparisonPredicate *predicate = [NSComparisonPredicate predicateWithLeftExpression:leftExpression - rightExpression:rightExpression - modifier:NSDirectPredicateModifier - type:operatorType - options:options]; - if (comparisonClass) { - NSExpression *nilExpression = [NSExpression expressionForConstantValue:nil]; - NSPredicate *notNilPredicate = [NSComparisonPredicate predicateWithLeftExpression:leftExpression rightExpression:nilExpression modifier:NSDirectPredicateModifier type:NSNotEqualToPredicateOperatorType options:0]; - NSExpression *classExpression = [NSExpression expressionForConstantValue:comparisonClass]; - NSComparisonPredicate *typePredicate = [NSComparisonPredicate predicateWithLeftExpression:leftExpression rightExpression:classExpression customSelector:@selector(isKindOfClass:)]; - return [[NSCompoundPredicate alloc] initWithType:NSAndPredicateType subpredicates:@[notNilPredicate, typePredicate, predicate]]; - } else { - return predicate; - } -} - -+ (NSCompoundPredicateType)compoundPredicateTypeFromString:(NSString *)predicateTypeString hasError:(nonnull BOOL *)hasError { - *hasError = NO; - if ([predicateTypeString isEqualToString:@"$and"]) { - return NSAndPredicateType; - } else if ([predicateTypeString isEqualToString:@"$or"]) { - return NSOrPredicateType; - } else if ([predicateTypeString isEqualToString:@"$not"]) { - return NSNotPredicateType; - } else { - ApptentiveLogError(@"Expected `$and`, `$or`, or `$not` skey; instead saw key: %@", predicateTypeString); - *hasError = YES; - return NSAndPredicateType; - } -} - -+ (NSPredicateOperatorType)predicateOperatorTypeFromString:(NSString *)operatorString hasError:(nonnull BOOL *)hasError { - *hasError = NO; - if ([operatorString isEqualToString:@"$eq"] || [operatorString isEqualToString:@"=="]) { - return NSEqualToPredicateOperatorType; - } else if ([operatorString isEqualToString:@"$gt"] || [operatorString isEqualToString:@">"]) { - return NSGreaterThanPredicateOperatorType; - } else if ([operatorString isEqualToString:@"$gte"] || [operatorString isEqualToString:@">="]) { - return NSGreaterThanOrEqualToPredicateOperatorType; - } else if ([operatorString isEqualToString:@"$lt"] || [operatorString isEqualToString:@"<"]) { - return NSLessThanPredicateOperatorType; - } else if ([operatorString isEqualToString:@"$lte"] || [operatorString isEqualToString:@"<="]) { - return NSLessThanOrEqualToPredicateOperatorType; - } else if ([operatorString isEqualToString:@"$ne"] || [operatorString isEqualToString:@"!="]) { - return NSNotEqualToPredicateOperatorType; - } else if ([operatorString isEqualToString:@"$contains"] || [operatorString isEqualToString:@"CONTAINS[c]"]) { - return NSContainsPredicateOperatorType; - } else if ([operatorString isEqualToString:@"$starts_with"] || [operatorString isEqualToString:@"BEGINSWITH[c]"]) { - return NSBeginsWithPredicateOperatorType; - } else if ([operatorString isEqualToString:@"$ends_with"] || [operatorString isEqualToString:@"ENDSWITH[c]"]) { - return NSEndsWithPredicateOperatorType; - } else { - ApptentiveLogError(@"Unrecognized comparison operator symbol: %@", operatorString); - *hasError = YES; - return NSCustomSelectorPredicateOperatorType; - } -} - -+ (BOOL) operator:(NSPredicateOperatorType) operator isValidForParameter:(NSObject *)parameter { - BOOL isString = [parameter isKindOfClass:[NSString class]]; - - switch (operator) { - case NSEqualToPredicateOperatorType: - case NSNotEqualToPredicateOperatorType: - case NSLessThanOrEqualToPredicateOperatorType: - case NSGreaterThanOrEqualToPredicateOperatorType: - case NSGreaterThanPredicateOperatorType: - case NSLessThanPredicateOperatorType: - return YES; - case NSBeginsWithPredicateOperatorType: - case NSEndsWithPredicateOperatorType: - case NSContainsPredicateOperatorType: - return isString; - default: - ApptentiveLogError(@"Unrecognized predicate operator type: %uld", (unsigned long)operator); - return NO; - } -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Apptentive/Apptentive/Engagement/Model/ApptentiveInteractionUsageData.h b/Apptentive/Apptentive/Engagement/Model/ApptentiveInteractionUsageData.h deleted file mode 100644 index 535af9d13..000000000 --- a/Apptentive/Apptentive/Engagement/Model/ApptentiveInteractionUsageData.h +++ /dev/null @@ -1,31 +0,0 @@ -// -// ApptentiveInteractionUsageData.h -// Apptentive -// -// Created by Peter Kamb on 10/14/13. -// Copyright (c) 2013 Apptentive, Inc. All rights reserved. -// - -#import "ApptentiveInteraction.h" -#import - -NS_ASSUME_NONNULL_BEGIN - -@class ApptentiveConversation; - - -@interface ApptentiveInteractionUsageData : NSObject - -@property (readonly, strong, nonatomic) ApptentiveConversation *conversation; - -+ (instancetype)usageDataWithConversation:(ApptentiveConversation *)conversation; - -- (instancetype)initWithConversation:(ApptentiveConversation *)conversation; - -- (NSDictionary *)predicateEvaluationDictionary; - -+ (void)keyPathWasSeen:(NSString *)keyPath; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Apptentive/Apptentive/Engagement/Model/ApptentiveInteractionUsageData.m b/Apptentive/Apptentive/Engagement/Model/ApptentiveInteractionUsageData.m deleted file mode 100644 index 6be2ae728..000000000 --- a/Apptentive/Apptentive/Engagement/Model/ApptentiveInteractionUsageData.m +++ /dev/null @@ -1,179 +0,0 @@ -// -// ApptentiveInteractionUsageData.m -// Apptentive -// -// Created by Peter Kamb on 10/14/13. -// Copyright (c) 2013 Apptentive, Inc. All rights reserved. -// - -#import "ApptentiveInteractionUsageData.h" -#import "Apptentive.h" -#import "ApptentiveAppRelease.h" -#import "ApptentiveBackend+Engagement.h" -#import "ApptentiveBackend.h" -#import "ApptentiveCount.h" -#import "ApptentiveDevice.h" -#import "ApptentiveEngagement.h" -#import "ApptentivePerson.h" -#import "ApptentiveSDK.h" -#import "ApptentiveUtilities.h" -#import "ApptentiveVersion.h" -#import "Apptentive_Private.h" - -NS_ASSUME_NONNULL_BEGIN - - -@implementation ApptentiveInteractionUsageData - -+ (ApptentiveInteractionUsageData *)usageDataWithConversation:(ApptentiveConversation *)conversation { - ApptentiveInteractionUsageData *usageData = [[ApptentiveInteractionUsageData alloc] initWithConversation:conversation]; - - return usageData; -} - -- (instancetype)initWithConversation:(ApptentiveConversation *)conversation { - self = [super init]; - - if (self) { - _conversation = conversation; - } - - return self; -} - -+ (void)keyPathWasSeen:(NSString *)keyPath { - /* - Record the keyPath if needed, to later be used in predicate evaluation. - */ - - if ([keyPath hasPrefix:@"code_point/"]) { - NSArray *components = [keyPath componentsSeparatedByString:@"/"]; - if (components.count > 1) { - NSString *codePoint = [components objectAtIndex:1]; - [Apptentive.shared.backend codePointWasSeen:[codePoint stringByRemovingPercentEncoding]]; - } - } else if ([keyPath hasPrefix:@"interactions/"]) { - NSArray *components = [keyPath componentsSeparatedByString:@"/"]; - if (components.count > 1) { - NSString *interactionID = [components objectAtIndex:1]; - [Apptentive.shared.backend interactionWasSeen:interactionID]; - } - } -} - -- (NSDictionary *)versionObjectWithVersion:(ApptentiveVersion *)version { - return @{ @"_type": @"version", - @"version": version.versionString ?: @"0.0.0" }; -} - -- (NSDictionary *)countDictionaryForCount:(ApptentiveCount *)count withPrefix:(NSString *)prefix { - NSMutableDictionary *result = [NSMutableDictionary dictionaryWithCapacity:4]; - - result[[prefix stringByAppendingString:@"/invokes/total"]] = @(count.totalCount); - result[[prefix stringByAppendingString:@"/invokes/cf_bundle_short_version_string"]] = @(count.versionCount); - result[[prefix stringByAppendingString:@"/invokes/cf_bundle_version"]] = @(count.buildCount); - result[[prefix stringByAppendingString:@"/last_invoked_at/total"]] = count.lastInvoked ? [Apptentive timestampObjectWithDate:count.lastInvoked] : [NSNull null]; - - return result; -} - -- (NSDictionary *)predicateEvaluationDictionary { - NSMutableDictionary *result = [NSMutableDictionary dictionary]; - - result[@"is_update/cf_bundle_short_version_string"] = @(self.conversation.appRelease.isUpdateVersion); - result[@"is_update/cf_bundle_version"] = @(self.conversation.appRelease.isUpdateBuild); - - result[@"time_at_install/total"] = [Apptentive timestampObjectWithDate:self.conversation.appRelease.timeAtInstallTotal]; - result[@"time_at_install/cf_bundle_short_version_string"] = [Apptentive timestampObjectWithDate:self.conversation.appRelease.timeAtInstallVersion]; - result[@"time_at_install/cf_bundle_version"] = [Apptentive timestampObjectWithDate:self.conversation.appRelease.timeAtInstallBuild]; - - result[@"application/cf_bundle_short_version_string"] = [self versionObjectWithVersion:self.conversation.appRelease.version]; - result[@"application/cf_bundle_version"] = [self versionObjectWithVersion:self.conversation.appRelease.build]; - result[@"application/debug"] = @(self.conversation.appRelease.debugBuild); - result[@"application/dt_compiler"] = self.conversation.appRelease.compiler; - result[@"application/dt_platform_build"] = self.conversation.appRelease.platformBuild; - result[@"application/dt_platform_name"] = self.conversation.appRelease.platformName; - result[@"application/dt_platform_version"] = [[ApptentiveVersion alloc] initWithString:self.conversation.appRelease.platformVersion]; - result[@"application/dt_sdk_build"] = self.conversation.appRelease.SDKBuild; - result[@"application/dt_sdk_name"] = self.conversation.appRelease.SDKName; - result[@"application/dt_xcode"] = self.conversation.appRelease.Xcode; - result[@"application/dt_xcode_build"] = self.conversation.appRelease.XcodeBuild; - - result[@"sdk/version"] = [self versionObjectWithVersion:self.conversation.SDK.version]; - result[@"sdk/distribution"] = self.conversation.SDK.distributionName; - result[@"sdk/distribution_version"] = [self versionObjectWithVersion:self.conversation.SDK.distributionVersion]; - - result[@"current_time"] = [Apptentive timestampObjectWithDate:self.conversation.currentTime]; - - for (NSString *key in self.conversation.engagement.codePoints) { - [result addEntriesFromDictionary:[self countDictionaryForCount:self.conversation.engagement.codePoints[key] withPrefix:[@"code_point/" stringByAppendingString:[ApptentiveUtilities stringByEscapingForPredicate:key]]]]; - } - - for (NSString *key in self.conversation.engagement.interactions) { - [result addEntriesFromDictionary:[self countDictionaryForCount:self.conversation.engagement.interactions[key] withPrefix:[@"interactions/" stringByAppendingString:[ApptentiveUtilities stringByEscapingForPredicate:key]]]]; - } - - // Device - NSDictionary *deviceData = self.conversation.device.JSONDictionary; - - // Device information - for (NSString *key in deviceData) { - if ([key isEqualToString:@"custom_data"] || [key isEqualToString:@"integration_config"]) { - continue; - } - - NSObject *value = deviceData[key]; - if (value) { - NSString *criteriaKey = [NSString stringWithFormat:@"device/%@", [ApptentiveUtilities stringByEscapingForPredicate:key]]; - - if ([key isEqualToString:@"os_version"]) { - value = [Apptentive versionObjectWithVersion:(NSString *)value]; - } - - result[criteriaKey] = value; - } - } - - // Device custom data - NSDictionary *customDeviceData = deviceData[@"custom_data"]; - for (NSString *key in customDeviceData) { - NSObject *value = customDeviceData[key]; - if (value) { - NSString *criteriaKey = [NSString stringWithFormat:@"device/custom_data/%@", [ApptentiveUtilities stringByEscapingForPredicate:key]]; - result[criteriaKey] = value; - } - } - - // Person - NSDictionary *personData = self.conversation.person.JSONDictionary; - - // Person information - for (NSString *key in [personData allKeys]) { - if ([key isEqualToString:@"custom_data"]) { - // Custom data is added below. - continue; - } - - NSObject *value = personData[key]; - if (value) { - NSString *criteriaKey = [NSString stringWithFormat:@"person/%@", [ApptentiveUtilities stringByEscapingForPredicate:key]]; - result[criteriaKey] = value; - } - } - - // Person custom data - NSDictionary *customPersonData = personData[@"custom_data"]; - for (NSString *key in customPersonData) { - NSObject *value = customPersonData[key]; - if (value) { - NSString *criteriaKey = [NSString stringWithFormat:@"person/custom_data/%@", [ApptentiveUtilities stringByEscapingForPredicate:key]]; - result[criteriaKey] = value; - } - } - - return result; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Apptentive/Apptentive/Engagement/Model/ApptentivePerson.m b/Apptentive/Apptentive/Engagement/Model/ApptentivePerson.m index fc5fa4ec4..251d81dae 100644 --- a/Apptentive/Apptentive/Engagement/Model/ApptentivePerson.m +++ b/Apptentive/Apptentive/Engagement/Model/ApptentivePerson.m @@ -85,6 +85,10 @@ + (void)deleteMigratedData { [[NSUserDefaults standardUserDefaults] removeObjectForKey:ApptentiveCustomPersonDataPreferenceKey]; } ++ (NSArray *)sensitiveKeys { + return [super.sensitiveKeys arrayByAddingObjectsFromArray:@[@"name", @"email"]]; +} + @end @@ -116,4 +120,34 @@ - (nullable instancetype)initWithCoder:(NSCoder *)coder { @end +@implementation ApptentivePerson (Criteria) + +- (nullable NSObject *)valueForFieldWithPath:(NSString *)path { + if ([path isEqualToString:@"name"]) { + return self.name; + } else if ([path isEqualToString:@"email"]) { + return self.emailAddress; + } else { + return [super valueForFieldWithPath:path]; + } +} + +- (NSString *)descriptionForFieldWithPath:(NSString *)path { + if ([path isEqualToString:@"name"]) { + return @"person name"; + } else if ([path isEqualToString:@"email"]) { + return @"person email"; + } else { + NSArray *parts = [path componentsSeparatedByString:@"/"]; + if (parts.count != 2 || ![parts[0] isEqualToString:@"custom_data"]) { + return [NSString stringWithFormat:@"Unrecognized person field %@", path]; + } + + return [NSString stringWithFormat:@"person_data[%@]", parts[1]]; + } +} + +@end + + NS_ASSUME_NONNULL_END diff --git a/Apptentive/Apptentive/Engagement/Model/ApptentiveSDK.m b/Apptentive/Apptentive/Engagement/Model/ApptentiveSDK.m index 07b984786..5e4ba3fa8 100644 --- a/Apptentive/Apptentive/Engagement/Model/ApptentiveSDK.m +++ b/Apptentive/Apptentive/Engagement/Model/ApptentiveSDK.m @@ -144,6 +144,10 @@ + (void)deleteMigratedData { [[NSUserDefaults standardUserDefaults] removeObjectForKey:ATCurrentConversationPreferenceKey]; } ++ (NSArray *)sensitiveKeys { + return @[@"author_name"]; +} + @end @@ -170,4 +174,33 @@ + (NSDictionary *)JSONKeyPathMapping { @end +@implementation ApptentiveSDK (Criteria) + +- (nullable NSObject *)valueForFieldWithPath:(NSString *)path { + if ([path isEqualToString:@"version"]) { + return self.version; + } else if ([path isEqualToString:@"distribution"]) { + return self.distributionName; + } else if ([path isEqualToString:@"distribution_version"]) { + return self.distributionVersion; + } + + ApptentiveLogError(@"Unrecognized field name “%@”", path); + return nil; +} + +- (NSString *)descriptionForFieldWithPath:(NSString *)path { + if ([path isEqualToString:@"version"]) { + return @"SDK version"; + } else if ([path isEqualToString:@"distribution"]) { + return @"SDK distribution method"; + } else if ([path isEqualToString:@"distribution_version"]) { + return @"SDK distribution package version"; + } + + return [NSString stringWithFormat:@"Unrecognized SDK field %@", path]; +} + +@end + NS_ASSUME_NONNULL_END diff --git a/Apptentive/Apptentive/Engagement/Model/ApptentiveState.h b/Apptentive/Apptentive/Engagement/Model/ApptentiveState.h index 79eee2b1e..4cc95b55f 100644 --- a/Apptentive/Apptentive/Engagement/Model/ApptentiveState.h +++ b/Apptentive/Apptentive/Engagement/Model/ApptentiveState.h @@ -16,6 +16,12 @@ NS_ASSUME_NONNULL_BEGIN */ @interface ApptentiveState : NSObject +/** + A list of keys in the JSON representation whose values may include sensitive + data that should be hidden if log santization is turned on. + */ +@property (class, readonly, nonatomic) NSArray *sensitiveKeys; + @end @@ -73,4 +79,11 @@ NS_ASSUME_NONNULL_BEGIN @end +@interface ApptentiveState (Criteria) + +- (nullable NSObject *)valueForFieldWithPath:(NSString *)path; +- (NSString *)descriptionForFieldWithPath:(NSString *)path; + +@end + NS_ASSUME_NONNULL_END diff --git a/Apptentive/Apptentive/Engagement/Model/ApptentiveState.m b/Apptentive/Apptentive/Engagement/Model/ApptentiveState.m index 8f409532d..c2908cf1c 100644 --- a/Apptentive/Apptentive/Engagement/Model/ApptentiveState.m +++ b/Apptentive/Apptentive/Engagement/Model/ApptentiveState.m @@ -23,6 +23,10 @@ - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder { - (void)encodeWithCoder:(NSCoder *)aCoder { } ++ (NSArray *)sensitiveKeys { + return @[]; +} + @end @@ -54,4 +58,18 @@ - (NSDictionary *)JSONDictionary { @end +@implementation ApptentiveState (Criteria) + +- (nullable NSObject *)valueForFieldWithPath:(NSString *)path { + ApptentiveAssertFail(@"Abstract method called"); + return nil; +} + +- (NSString *)descriptionForFieldWithPath:(NSString *)path { + ApptentiveAssertFail(@"Abstract method called"); + return [NSString stringWithFormat:@"Unrecognized field %@", path]; +} + +@end + NS_ASSUME_NONNULL_END diff --git a/Apptentive/Apptentive/Engagement/Model/ApptentiveVersion.h b/Apptentive/Apptentive/Engagement/Model/ApptentiveVersion.h index 62d216c26..7deffb9d1 100644 --- a/Apptentive/Apptentive/Engagement/Model/ApptentiveVersion.h +++ b/Apptentive/Apptentive/Engagement/Model/ApptentiveVersion.h @@ -71,6 +71,8 @@ NS_ASSUME_NONNULL_BEGIN */ - (BOOL)isEqualToVersion:(ApptentiveVersion *)version; +- (NSComparisonResult)compare:(ApptentiveVersion *)otherVersion; + @end NS_ASSUME_NONNULL_END diff --git a/Apptentive/Apptentive/Engagement/Model/ApptentiveVersion.m b/Apptentive/Apptentive/Engagement/Model/ApptentiveVersion.m index 8376e510a..01d95b3ff 100644 --- a/Apptentive/Apptentive/Engagement/Model/ApptentiveVersion.m +++ b/Apptentive/Apptentive/Engagement/Model/ApptentiveVersion.m @@ -91,6 +91,26 @@ - (BOOL)isEqualToVersion:(ApptentiveVersion *)version { } } +- (uint64_t)integerValueForComparison { + return (self.major << 20) + (self.minor << 10) + self.patch; +} + +- (NSComparisonResult)compare:(ApptentiveVersion *)otherVersion { + if (self.major == -1 || otherVersion.major == -1) { + return [self.versionString compare:otherVersion.versionString]; + } else if ([otherVersion integerValueForComparison] > [self integerValueForComparison]) { + return NSOrderedAscending; + } else if ([otherVersion integerValueForComparison] < [self integerValueForComparison]) { + return NSOrderedDescending; + } else { + return NSOrderedSame; + } +} + +- (NSString *)description { + return [NSString stringWithFormat:@"%@: %@", super.description, self.versionString]; +} + @end diff --git a/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveAndClause.h b/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveAndClause.h new file mode 100644 index 000000000..9b6428f08 --- /dev/null +++ b/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveAndClause.h @@ -0,0 +1,21 @@ +// +// ApptentiveAndClause.h +// Apptentive +// +// Created by Frank Schmitt on 11/21/17. +// Copyright © 2017 Apptentive, Inc. All rights reserved. +// + +#import "ApptentiveClause.h" + +NS_ASSUME_NONNULL_BEGIN + + +@interface ApptentiveAndClause : ApptentiveClause + ++ (ApptentiveClause *)andClauseWithDictionary:(NSDictionary *)dictionary; ++ (ApptentiveClause *)andClauseWithArray:(NSArray *)array; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveAndClause.m b/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveAndClause.m new file mode 100644 index 000000000..271379874 --- /dev/null +++ b/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveAndClause.m @@ -0,0 +1,139 @@ +// +// ApptentiveAndClause.m +// Apptentive +// +// Created by Frank Schmitt on 11/21/17. +// Copyright © 2017 Apptentive, Inc. All rights reserved. +// + +#import "ApptentiveAndClause.h" +#import "ApptentiveFalseClause.h" +#import "ApptentiveComparisonClause.h" +#import "ApptentiveOrClause.h" +#import "ApptentiveNotClause.h" +#import "ApptentiveIndentPrinter.h" + +NS_ASSUME_NONNULL_BEGIN + +static NSString * const SubClausesKey = @"subClauses"; + + +@interface ApptentiveAndClause () + +@property (strong, nonatomic) NSMutableArray *subClauses; + +@end + + +@implementation ApptentiveAndClause + ++ (ApptentiveClause *)andClauseWithDictionary:(NSDictionary *)dictionary { + return [[self alloc] initWithDictionary:dictionary]; +} + ++ (ApptentiveClause *)andClauseWithArray:(NSArray *)array { + return [[self alloc] initWithArray:array]; +} + +- (instancetype)initWithDictionary:(NSDictionary *)dictionary { + self = [super init]; + + if (self) { + _subClauses = [NSMutableArray arrayWithCapacity:dictionary.count]; + + if ([dictionary isKindOfClass:[NSDictionary class]]) { + for (NSString *key in dictionary) { + if ([key isEqualToString:@"$and"]) { + [_subClauses addObject:[ApptentiveAndClause andClauseWithArray:dictionary[key]]]; + } else if ([key isEqualToString:@"$or"]) { + [_subClauses addObject:[ApptentiveOrClause orClauseWithArray:dictionary[key]]]; + } else if ([key isEqualToString:@"$not"]) { + [_subClauses addObject:[ApptentiveNotClause notClauseWithDictionary:dictionary[key]]]; + } else if ([key hasPrefix:@"$"]) { + ApptentiveLogWarning(ApptentiveLogTagCriteria, @"Unrecognized logical operator “%@”. Evaluates to false.", key); + [_subClauses addObject:[ApptentiveFalseClause falseClauseWithObject:dictionary[key]]]; + } else { + [_subClauses addObject:[ApptentiveComparisonClause comparisonClauseWithField:key comparisons:dictionary[key]]]; + } + } + } else { + ApptentiveLogWarning(ApptentiveLogTagCriteria, @"Attempting to initialize implicit $and clause with non-dictionary parameter"); + [_subClauses addObject:[ApptentiveFalseClause falseClauseWithObject:dictionary]]; + } + } + + return self; +} + +- (instancetype)initWithArray:(NSArray *)array { + self = [super init]; + + if (self) { + _subClauses = [NSMutableArray arrayWithCapacity:array.count]; + + if ([array isKindOfClass:[NSArray class]]) { + for (NSDictionary *clauseDictionary in array) { + ApptentiveClause *subClause = [ApptentiveAndClause andClauseWithDictionary:clauseDictionary]; + + [_subClauses addObject:subClause]; + } + } else { + ApptentiveLogWarning(ApptentiveLogTagCriteria, @"Attempting to initialize $and clause with non-array parameter"); + [_subClauses addObject:[ApptentiveFalseClause falseClauseWithObject:array]]; + } + } + + return self; +} + +- (instancetype)init{ + self = [super init]; + if (self) { + _subClauses = [NSMutableArray array]; + } + return self; +} + +- (BOOL)criteriaMetForConversation:(ApptentiveConversation *)conversation indentPrinter:(nonnull ApptentiveIndentPrinter *)indentPrinter { + BOOL shouldNest = self.subClauses.count > 1; + if (shouldNest) { + [indentPrinter appendString:@"- $and"]; + [indentPrinter indent]; + } + + for (ApptentiveClause *subClause in self.subClauses) { + if (![subClause criteriaMetForConversation:conversation indentPrinter:indentPrinter]) { + if (shouldNest) { + [indentPrinter outdent]; + } + return NO; + } + } + + if (shouldNest) { + [indentPrinter outdent]; + } + return YES; +} + ++ (BOOL)supportsSecureCoding { + return YES; +} + +- (nullable instancetype)initWithCoder:(NSCoder *)coder +{ + self = [super initWithCoder:coder]; + if (self) { + _subClauses = [coder decodeObjectForKey:SubClausesKey]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder +{ + [coder encodeObject:self.subClauses forKey:SubClausesKey]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveClause.h b/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveClause.h new file mode 100644 index 000000000..c7060f004 --- /dev/null +++ b/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveClause.h @@ -0,0 +1,23 @@ +// +// ApptentiveClause.h +// Apptentive +// +// Created by Frank Schmitt on 11/21/17. +// Copyright © 2017 Apptentive, Inc. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class ApptentiveConversation, ApptentiveIndentPrinter; + + +@interface ApptentiveClause : NSObject + +- (BOOL)criteriaMetForConversation:(ApptentiveConversation *)conversation; +- (BOOL)criteriaMetForConversation:(ApptentiveConversation *)conversation indentPrinter:(ApptentiveIndentPrinter *)indentPrinter; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveClause.m b/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveClause.m new file mode 100644 index 000000000..5b7fef6e2 --- /dev/null +++ b/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveClause.m @@ -0,0 +1,50 @@ +// +// ApptentiveClause.m +// Apptentive +// +// Created by Frank Schmitt on 11/21/17. +// Copyright © 2017 Apptentive, Inc. All rights reserved. +// + +#import "ApptentiveClause.h" +#import "ApptentiveFalseClause.h" +#import "ApptentiveAndClause.h" +#import "ApptentiveOrClause.h" +#import "ApptentiveNotClause.h" +#import "ApptentiveIndentPrinter.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation ApptentiveClause + +- (BOOL)criteriaMetForConversation:(ApptentiveConversation *)conversation indentPrinter:(ApptentiveIndentPrinter *)indentPrinter { + ApptentiveLogError(ApptentiveLogTagCriteria, @"Abstract method called. Returning NO."); + return NO; +} + +- (BOOL)criteriaMetForConversation:(ApptentiveConversation *)conversation { + ApptentiveIndentPrinter *indentPrinter = [[ApptentiveIndentPrinter alloc] init]; + + BOOL result = [self criteriaMetForConversation:conversation indentPrinter:indentPrinter]; + + ApptentiveLogDebug(@"Criteria Evaluation Details:\n%@", indentPrinter.output); + + return result; +} + ++ (BOOL)supportsSecureCoding { + return YES; +} + +- (nullable instancetype)initWithCoder:(NSCoder *)coder +{ + return [super init]; +} + +- (void)encodeWithCoder:(NSCoder *)coder +{ +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveComparisonClause.h b/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveComparisonClause.h new file mode 100644 index 000000000..e67b3d864 --- /dev/null +++ b/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveComparisonClause.h @@ -0,0 +1,33 @@ +// +// ApptentiveComparisonClause.h +// Apptentive +// +// Created by Frank Schmitt on 11/21/17. +// Copyright © 2017 Apptentive, Inc. All rights reserved. +// + +#import "ApptentiveClause.h" + +NS_ASSUME_NONNULL_BEGIN + +@class ApptentiveConversation; + +typedef BOOL (^ComparisonBlock)(NSObject *value, NSObject * _Nullable parameter); + + +@interface ApptentiveComparisonClause : ApptentiveClause + ++ (ApptentiveClause *)comparisonClauseWithField:(NSString *)key comparisons:(NSDictionary *)comparisons; + ++ (NSDictionary *)operators; + +- (instancetype)initWithField:(NSString *)field comparisons:(NSDictionary *)comparisons; + +@property (strong, nonatomic) NSString *field; +@property (strong, nonatomic) NSDictionary *comparisons; + +- (NSObject *)valueInConversation:(ApptentiveConversation *)conversation; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveComparisonClause.m b/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveComparisonClause.m new file mode 100644 index 000000000..6d2e66a49 --- /dev/null +++ b/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveComparisonClause.m @@ -0,0 +1,244 @@ +// +// ApptentiveComparisonClause.m +// Apptentive +// +// Created by Frank Schmitt on 11/21/17. +// Copyright © 2017 Apptentive, Inc. All rights reserved. +// + +#import "ApptentiveComparisonClause.h" +#import "ApptentiveFalseClause.h" +#import "ApptentiveVersion.h" +#import "ApptentiveConversation.h" +#import "ApptentiveIndentPrinter.h" + +NS_ASSUME_NONNULL_BEGIN + +static NSString * const FieldKey = @"field"; +static NSString * const ComparisonsKey = @"comparisons"; + +static NSString * trimmedAndLowercased(NSObject *string) { + return [[(NSString *)string lowercaseString] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; +} + +static BOOL canCompare(NSObject *value, NSObject *parameter, NSComparisonResult *comparisonResult) { + BOOL bothNull = (value == nil) && [parameter isKindOfClass:[NSNull class]]; + if (bothNull) { + if (comparisonResult != nil) { + *comparisonResult = NSOrderedSame; + } + + return YES; + } + + BOOL bothStrings = ([value isKindOfClass:[NSString class]] && [parameter isKindOfClass:[NSString class]]); + BOOL bothNumbers = ([value isKindOfClass:[NSNumber class]] && [parameter isKindOfClass:[NSNumber class]]); + BOOL bothDates = ([value isKindOfClass:[NSDate class]] && [parameter isKindOfClass:[NSDate class]]); + BOOL bothVersions = ([value isKindOfClass:[ApptentiveVersion class]] && [parameter isKindOfClass:[ApptentiveVersion class]]); + + BOOL canCompare = bothStrings || bothNumbers || bothDates || bothVersions; + + if (bothStrings) { + value = trimmedAndLowercased(value); + parameter = trimmedAndLowercased(parameter); + } + + if (canCompare && comparisonResult != nil) { + *comparisonResult = (NSComparisonResult)[value performSelector:@selector(compare:) withObject:parameter]; + } + + return canCompare; +} + +@implementation ApptentiveComparisonClause + ++ (NSDictionary *)operators { + static NSDictionary *_operators; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + _operators = @{ + @"$exists": ^(NSObject * _Nonnull value, NSObject * parameter){ + if (![parameter isKindOfClass:[NSNumber class]]) { + return NO; + } + + BOOL shouldExist = ((NSNumber *)parameter).integerValue == 1; + return (BOOL)((value != nil) == shouldExist); + }, + @"$eq":^(NSObject * _Nonnull value, NSObject * parameter){ + NSComparisonResult result; + return canCompare(value, parameter, &result) && result == NSOrderedSame; + }, + @"$ne":^(NSObject * _Nonnull value, NSObject * parameter){ + NSComparisonResult result; + return canCompare(value, parameter, &result) && result != NSOrderedSame; + }, + @"$lt":^(NSObject * _Nonnull value, NSObject * parameter){ + NSComparisonResult result; + return canCompare(value, parameter, &result) && result == NSOrderedAscending; + }, + @"$lte":^(NSObject * _Nonnull value, NSObject * parameter){ + NSComparisonResult result; + return canCompare(value, parameter, &result) && (result == NSOrderedAscending || result == NSOrderedSame); + }, + @"$gt":^(NSObject * _Nonnull value, NSObject * parameter){ + NSComparisonResult result; + return canCompare(value, parameter, &result) && result == NSOrderedDescending; + }, + @"$gte":^(NSObject * _Nonnull value, NSObject * parameter){ + NSComparisonResult result; + return canCompare(value, parameter, &result) && (result == NSOrderedDescending || result == NSOrderedSame); + }, + @"$before":^(NSObject * _Nonnull value, NSObject * parameter){ + NSComparisonResult result; + return [value isKindOfClass:[NSDate class]] && canCompare(value, parameter, &result) && result == NSOrderedAscending; + }, + @"$after":^(NSObject * _Nonnull value, NSObject * parameter){ + NSComparisonResult result; + return [value isKindOfClass:[NSDate class]] && canCompare(value, parameter, &result) && result == NSOrderedDescending; + }, + @"$contains":^(NSObject * _Nonnull value, NSObject * parameter){ + return [value isKindOfClass:[NSString class]] && [parameter isKindOfClass:[NSString class]] && [trimmedAndLowercased(value) containsString:trimmedAndLowercased(parameter)]; + }, + @"$starts_with":^(NSObject * _Nonnull value, NSObject * parameter){ + return [value isKindOfClass:[NSString class]] && [parameter isKindOfClass:[NSString class]] && [trimmedAndLowercased(value) hasPrefix:trimmedAndLowercased(parameter)]; + }, + @"$ends_with":^(NSObject * _Nonnull value, NSObject * parameter){ + return [value isKindOfClass:[NSString class]] && [parameter isKindOfClass:[NSString class]] && [trimmedAndLowercased(value) hasSuffix:trimmedAndLowercased(parameter)]; + } + }; + }); + + return _operators; +} + ++ (ApptentiveClause *)comparisonClauseWithField:(NSString *)field comparisons:(NSDictionary *)comparisons { + BOOL fieldIsString = [field isKindOfClass:[NSString class]]; + BOOL valueIsImplicitEquals = ![comparisons isKindOfClass:[NSDictionary class]] || [(NSDictionary *)comparisons objectForKey:@"_type"] != nil; + BOOL valueIsComparison = [comparisons isKindOfClass:[NSDictionary class]] && [(NSDictionary *)comparisons objectForKey:@"_type"] == nil; + if (!fieldIsString || !(valueIsImplicitEquals || valueIsComparison)) { + ApptentiveLogWarning(ApptentiveLogTagCriteria, @"Attempting to initialize comparison clause with invalid key or value (“%@” = ”%@”).", field, comparisons); + return [ApptentiveFalseClause falseClauseWithObject:[NSString stringWithFormat:@"(“%@” = ”%@”).", field, comparisons]]; + } + + else if (valueIsImplicitEquals) { + comparisons = @{ @"$eq": comparisons }; + } + + return [[self alloc] initWithField:field comparisons:comparisons]; +} + +- (instancetype)initWithField:(NSString *)field comparisons:(NSDictionary *)comparisons { + self = [super init]; + + if (self) { + _field = [field stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + _comparisons = comparisons; + } + + return self; +} + ++ (BOOL)supportsSecureCoding { + return YES; +} + +- (nullable instancetype)initWithCoder:(NSCoder *)coder +{ + self = [super init]; + if (self) { + _field = [coder decodeObjectOfClass:[NSString class] forKey:FieldKey]; + _comparisons = [coder decodeObjectOfClass:[NSDictionary class] forKey:ComparisonsKey]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder +{ + [coder encodeObject:self.field forKey:FieldKey]; + [coder encodeObject:self.comparisons forKey:ComparisonsKey]; +} + +- (NSObject *)valueInConversation:(ApptentiveConversation *)conversation { + return [conversation valueForFieldWithPath:self.field]; +} + +- (BOOL)criteriaMetForConversation:(ApptentiveConversation *)conversation indentPrinter:(nonnull ApptentiveIndentPrinter *)indentPrinter { + NSObject *value = [self valueInConversation:conversation]; + + for (NSString *operator in self.comparisons) { + ComparisonBlock comparisonBlock = [[self class] operators][operator]; + if (comparisonBlock == nil) { + ApptentiveLogWarning(ApptentiveLogTagCriteria, @"Unrecognized operator “%@”. Evaluates to false", operator); + return NO; + } + + NSObject *parameter = self.comparisons[operator]; + + if ([parameter isKindOfClass:[NSDictionary class]]) { + NSDictionary *parameterDictionary = (NSDictionary *)parameter; + NSString *type = parameterDictionary[@"_type"]; + + if ([type isEqualToString:@"datetime"] && [parameterDictionary[@"sec"] isKindOfClass:[NSNumber class]]) { + parameter = [NSDate dateWithTimeIntervalSince1970:((NSNumber *)parameterDictionary[@"sec"]).doubleValue]; + } else if ([type isEqualToString:@"version"] && [parameterDictionary[@"version"] isKindOfClass:[NSString class]]) { + parameter = [[ApptentiveVersion alloc] initWithString:(NSString *)parameterDictionary[@"version"]]; + } else if (type == nil) { + ApptentiveLogWarning(ApptentiveLogTagCriteria, @"Complex type with no “_type” key. Evaluates to false."); + return NO; + } else { + ApptentiveLogWarning(ApptentiveLogTagCriteria, @"Unrecognized or malformed complex type “%@”. Evaluates to false.", type); + return NO; + } + } + + if ([operator isEqualToString:@"$before"] || [operator isEqualToString:@"$after"]) { + if ([parameter isKindOfClass:[NSNumber class]]) { + parameter = [conversation.currentTime dateByAddingTimeInterval:((NSNumber *)parameter).doubleValue]; + } else { + ApptentiveLogWarning(ApptentiveLogTagCriteria, @"“%@” operator with non-numeric parameter (“%@”). Evaluates to false", operator, parameter); + } + } + + BOOL result = comparisonBlock(value, parameter); + [indentPrinter appendFormat:@"- %@ ('%@') %@ '%@' => %@", [conversation descriptionForFieldWithPath:self.field], value, [self friendlyNameForOperator:operator], parameter, result ? @"true" : @"false"]; + if (!result) { + return NO; + } + } + + return YES; +} + +- (NSString *)friendlyNameForOperator:(NSString *)operator { + static NSDictionary *friendlyTrueOperators; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + friendlyTrueOperators = @{ + @"$eq": @"is equal to", + @"$ne": @"is not equal to", + @"$lt": @"is less than", + @"$lte": @"is less than or equal to", + @"$gt": @"is greater than", + @"$gte": @"is greater than or equal to", + @"$starts_with": @"starts with", + @"$ends_with": @"ends with", + @"$contains": @"contains", + @"$after": @"is after", + @"$before": @"is before", + @"$exists": @"exists" + }; + }); + + NSString *result = friendlyTrueOperators[operator]; + + if (result) { + return result; + } else { + return @"Unrecognized Operator"; + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveFalseClause.h b/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveFalseClause.h new file mode 100644 index 000000000..7714262d5 --- /dev/null +++ b/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveFalseClause.h @@ -0,0 +1,20 @@ +// +// ApptentiveFalseClause.h +// Apptentive +// +// Created by Frank Schmitt on 11/21/17. +// Copyright © 2017 Apptentive, Inc. All rights reserved. +// + +#import "ApptentiveClause.h" + +NS_ASSUME_NONNULL_BEGIN + + +@interface ApptentiveFalseClause : ApptentiveClause + ++ (instancetype)falseClauseWithObject:(NSObject *)object; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveFalseClause.m b/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveFalseClause.m new file mode 100644 index 000000000..ef0b0783f --- /dev/null +++ b/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveFalseClause.m @@ -0,0 +1,64 @@ +// +// ApptentiveFalseClause.m +// Apptentive +// +// Created by Frank Schmitt on 11/21/17. +// Copyright © 2017 Apptentive, Inc. All rights reserved. +// + +#import "ApptentiveFalseClause.h" +#import "ApptentiveIndentPrinter.h" + +NS_ASSUME_NONNULL_BEGIN + +static NSString * const InvalidCriteriaDescriptionKey = @"invalidCriteriaDescription"; + +@interface ApptentiveFalseClause () + +@property (readonly, nonatomic) NSString *invalidCriteriaDescription; + +@end + +@implementation ApptentiveFalseClause + ++ (instancetype)falseClauseWithObject:(NSObject *)object { + return [[self alloc] initWithDescriptionObject:object]; +} + +- (instancetype)initWithDescriptionObject:(NSObject *)object { + self = [super init]; + + if (self) { + _invalidCriteriaDescription = object.debugDescription; + ApptentiveLogWarning(ApptentiveLogTagCriteria, @"Criteria parser found invalid or unrecognized criteria “%@”", _invalidCriteriaDescription); + } + + return self; +} + +- (BOOL)criteriaMetForConversation:(ApptentiveConversation *)conversation indentPrinter:(ApptentiveIndentPrinter *)indentPrinter { + [indentPrinter appendFormat:@"- Invalid or unrecognized criteria (“%@”). Evaluates to false.", self.invalidCriteriaDescription]; + return NO; +} + ++ (BOOL)supportsSecureCoding { + return YES; +} + +- (nullable instancetype)initWithCoder:(NSCoder *)coder +{ + self = [super init]; + if (self) { + _invalidCriteriaDescription = [coder decodeObjectOfClass:[NSString class] forKey:InvalidCriteriaDescriptionKey]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder +{ + [coder encodeObject:self.invalidCriteriaDescription forKey:InvalidCriteriaDescriptionKey]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveInvocations.h b/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveInvocations.h new file mode 100644 index 000000000..d247ce457 --- /dev/null +++ b/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveInvocations.h @@ -0,0 +1,26 @@ +// +// ApptentiveInvocations.h +// Apptentive +// +// Created by Frank Schmitt on 11/22/17. +// Copyright © 2017 Apptentive, Inc. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class ApptentiveConversation; + + +@interface ApptentiveInvocations : NSObject + +@property (readonly, nonatomic) NSArray *targets; + +- (instancetype)initWithArray:(NSArray *)targetsArray; + +- (nullable NSString *)interactionIdentifierForConversation:(ApptentiveConversation *)conversation; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveInvocations.m b/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveInvocations.m new file mode 100644 index 000000000..590e410c8 --- /dev/null +++ b/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveInvocations.m @@ -0,0 +1,88 @@ +// +// ApptentiveInvocations.m +// Apptentive +// +// Created by Frank Schmitt on 11/22/17. +// Copyright © 2017 Apptentive, Inc. All rights reserved. +// + +#import "ApptentiveInvocations.h" +#import "ApptentiveTarget.h" +#import "ApptentiveClause.h" +#import "ApptentiveIndentPrinter.h" + +NS_ASSUME_NONNULL_BEGIN + +static NSString * const TargetsKey = @"targets"; + +@implementation ApptentiveInvocations + +- (instancetype)initWithArray:(NSArray *)targetsArray { + self = [super init]; + + if (self) { + NSMutableArray *targets = [NSMutableArray arrayWithCapacity:targetsArray.count]; + + for (NSDictionary *rawTarget in targetsArray) { + ApptentiveTarget *target = [[ApptentiveTarget alloc] initWithDictionary:rawTarget]; + + if (target) { + [targets addObject:target]; + } + } + + _targets = targets; + } + + return self; +} + ++ (BOOL)supportsSecureCoding { + return YES; +} + +- (nullable instancetype)initWithCoder:(NSCoder *)coder +{ + self = [super init]; + if (self) { + _targets = [coder decodeObjectOfClass:[NSArray class] forKey:TargetsKey]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder +{ + [coder encodeObject:self.targets forKey:TargetsKey]; +} + +- (nullable NSString *)interactionIdentifierForConversation:(ApptentiveConversation *)conversation { + if (self.targets.count == 0) { + ApptentiveLogDebug(ApptentiveLogTagCriteria, @"No interactions configured with this Where event"); + return nil; + } + + for (ApptentiveTarget *target in self.targets) { + @autoreleasepool { + ApptentiveIndentPrinter *indentPrinter = [[ApptentiveIndentPrinter alloc] init]; + if ([target.criteria criteriaMetForConversation:conversation indentPrinter:indentPrinter]) { + ApptentiveLogInfo(ApptentiveLogTagCriteria, @"Criteria for interaction '%@' evaluated => true", target.interactionIdentifier); + if (indentPrinter.output.length) { + ApptentiveLogDebug(ApptentiveLogTagCriteria, @"Criteria Evaluation Details:\n%@", indentPrinter.output); + } + return target.interactionIdentifier; + } + + ApptentiveLogInfo(ApptentiveLogTagCriteria, @"Criteria for interaction '%@' evaluated => false", target.interactionIdentifier); + if (indentPrinter.output.length) { + ApptentiveLogDebug(ApptentiveLogTagCriteria, @"Criteria Evaluation Details:\n%@", indentPrinter.output); + } + } + } + + ApptentiveLogDebug(ApptentiveLogTagCriteria, @"No interactions configured with this Where event had matching Who/When criteria"); + return nil; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveNotClause.h b/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveNotClause.h new file mode 100644 index 000000000..c360c52b9 --- /dev/null +++ b/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveNotClause.h @@ -0,0 +1,20 @@ +// +// ApptentiveNotClause.h +// Apptentive +// +// Created by Frank Schmitt on 11/21/17. +// Copyright © 2017 Apptentive, Inc. All rights reserved. +// + +#import "ApptentiveClause.h" + +NS_ASSUME_NONNULL_BEGIN + + +@interface ApptentiveNotClause : ApptentiveClause + ++ (instancetype)notClauseWithDictionary:(NSDictionary *)dictionary; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveNotClause.m b/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveNotClause.m new file mode 100644 index 000000000..c8d9de97c --- /dev/null +++ b/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveNotClause.m @@ -0,0 +1,78 @@ +// +// ApptentiveNotClause.m +// Apptentive +// +// Created by Frank Schmitt on 11/21/17. +// Copyright © 2017 Apptentive, Inc. All rights reserved. +// + +#import "ApptentiveNotClause.h" +#import "ApptentiveFalseClause.h" +#import "ApptentiveAndClause.h" +#import "ApptentiveIndentPrinter.h" + +NS_ASSUME_NONNULL_BEGIN + +static NSString * const SubClauseKey = @"subClause"; + + +@interface ApptentiveNotClause () + +@property (strong, nonatomic) ApptentiveClause *subClause; + +@end + + +@implementation ApptentiveNotClause + ++ (instancetype)notClauseWithDictionary:(NSDictionary *)dictionary { + return [[self alloc] initWithDictionary:dictionary]; +} + +- (instancetype)initWithDictionary:(NSDictionary *)dictionary { + self = [super init]; + + if (self) { + if ([dictionary isKindOfClass:[NSDictionary class]]) { + _subClause = [ApptentiveAndClause andClauseWithDictionary:dictionary]; + } else { + ApptentiveLogWarning(ApptentiveLogTagCriteria, @"Attempting to initialize $not clause with non-dictionary parameter"); + _subClause = [ApptentiveFalseClause falseClauseWithObject:dictionary]; + } + } + + return self; +} + +- (BOOL)criteriaMetForConversation:(ApptentiveConversation *)conversation indentPrinter:(nonnull ApptentiveIndentPrinter *)indentPrinter { + [indentPrinter appendString:@"- $not"]; + [indentPrinter indent]; + + BOOL result = ![self.subClause criteriaMetForConversation:conversation indentPrinter:indentPrinter]; + + [indentPrinter outdent]; + + return result; +} + ++ (BOOL)supportsSecureCoding { + return YES; +} + +- (nullable instancetype)initWithCoder:(NSCoder *)coder +{ + self = [super initWithCoder:coder]; + if (self) { + _subClause = [coder decodeObjectForKey:SubClauseKey]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder +{ + [coder encodeObject:self.subClause forKey:SubClauseKey]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveOrClause.h b/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveOrClause.h new file mode 100644 index 000000000..e8a5a37c7 --- /dev/null +++ b/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveOrClause.h @@ -0,0 +1,20 @@ +// +// ApptentiveOrClause.h +// Apptentive +// +// Created by Frank Schmitt on 11/21/17. +// Copyright © 2017 Apptentive, Inc. All rights reserved. +// + +#import "ApptentiveClause.h" + +NS_ASSUME_NONNULL_BEGIN + + +@interface ApptentiveOrClause : ApptentiveClause + ++ (ApptentiveClause *)orClauseWithArray:(NSArray *)array; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveOrClause.m b/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveOrClause.m new file mode 100644 index 000000000..fee857c8c --- /dev/null +++ b/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveOrClause.m @@ -0,0 +1,100 @@ +// +// ApptentiveOrClause.m +// Apptentive +// +// Created by Frank Schmitt on 11/21/17. +// Copyright © 2017 Apptentive, Inc. All rights reserved. +// + +#import "ApptentiveOrClause.h" +#import "ApptentiveFalseClause.h" +#import "ApptentiveAndClause.h" +#import "ApptentiveIndentPrinter.h" + +NS_ASSUME_NONNULL_BEGIN + +static NSString * const SubClausesKey = @"subClauses"; + + +@interface ApptentiveOrClause () + +@property (strong, nonatomic) NSMutableArray *subClauses; + +@end + + +@implementation ApptentiveOrClause + ++ (ApptentiveClause *)orClauseWithArray:(NSArray *)array { + if ([array isKindOfClass:[NSArray class]] && array.count == 1 && [array.firstObject isKindOfClass:[NSDictionary class]]) { + return [ApptentiveAndClause andClauseWithDictionary:array.firstObject]; + } + + return [[self alloc] initWithArray:array]; +} + +- (instancetype)initWithArray:(NSArray *)array { + self = [super init]; + + if (self) { + _subClauses = [NSMutableArray arrayWithCapacity:array.count]; + + if ([array isKindOfClass:[NSArray class]]) { + for (NSDictionary *clauseDictionary in array) { + ApptentiveClause *subClause = [ApptentiveAndClause andClauseWithDictionary:clauseDictionary]; + + [_subClauses addObject:subClause]; + } + } else { + ApptentiveLogWarning(ApptentiveLogTagCriteria, @"Attempting to initialize $or clause with non-array parameter"); + [_subClauses addObject:[ApptentiveFalseClause falseClauseWithObject:array]]; + } + } + + return self; +} + +- (instancetype)init{ + self = [super init]; + if (self) { + _subClauses = [NSMutableArray array]; + } + return self; +} + +- (BOOL)criteriaMetForConversation:(ApptentiveConversation *)conversation indentPrinter:(nonnull ApptentiveIndentPrinter *)indentPrinter { + [indentPrinter appendString:@"- $or"]; + [indentPrinter indent]; + + for (ApptentiveClause *subClause in self.subClauses) { + if ([subClause criteriaMetForConversation:conversation indentPrinter:indentPrinter]) { + [indentPrinter outdent]; + return YES; + } + } + + [indentPrinter outdent]; + return NO; +} + ++ (BOOL)supportsSecureCoding { + return YES; +} + +- (nullable instancetype)initWithCoder:(NSCoder *)coder +{ + self = [super initWithCoder:coder]; + if (self) { + _subClauses = [coder decodeObjectForKey:SubClausesKey]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder +{ + [coder encodeObject:self.subClauses forKey:SubClausesKey]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveTarget.h b/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveTarget.h new file mode 100644 index 000000000..37267472a --- /dev/null +++ b/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveTarget.h @@ -0,0 +1,25 @@ +// +// ApptentiveTarget.h +// Apptentive +// +// Created by Frank Schmitt on 11/21/17. +// Copyright © 2017 Apptentive, Inc. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class ApptentiveClause; + + +@interface ApptentiveTarget : NSObject + +@property (readonly, nonatomic) NSString *interactionIdentifier; +@property (readonly, nonatomic) ApptentiveClause *criteria; + +- (nullable instancetype)initWithDictionary:(NSDictionary *)dictionary; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveTarget.m b/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveTarget.m new file mode 100644 index 000000000..3ebb242af --- /dev/null +++ b/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveTarget.m @@ -0,0 +1,66 @@ +// +// ApptentiveTarget.m +// Apptentive +// +// Created by Frank Schmitt on 11/21/17. +// Copyright © 2017 Apptentive, Inc. All rights reserved. +// + +#import "ApptentiveTarget.h" +#import "ApptentiveAndClause.h" + +NS_ASSUME_NONNULL_BEGIN + +static NSString * const InteractionIdentifierKey = @"interactionIdentifier"; +static NSString * const CriteriaKey = @"criteria"; + +@implementation ApptentiveTarget + +- (nullable instancetype)initWithDictionary:(NSDictionary *)dictionary { + self = [super init]; + + if (self) { + if (![dictionary isKindOfClass:[NSDictionary class]]) { + ApptentiveLogError(ApptentiveLogTagCriteria, @"Attempting to initialize target with non-dictionary parameter"); + return nil; + } + + if (![dictionary[@"interaction_id"] isKindOfClass:[NSString class]] || [dictionary[@"interaction_id"] length] == 0) { + ApptentiveLogError(ApptentiveLogTagCriteria, @"Apptenting to initialize target with invalid interaction identifier"); + return nil; + } + + _interactionIdentifier = dictionary[@"interaction_id"]; + _criteria = [ApptentiveAndClause andClauseWithDictionary:dictionary[@"criteria"]]; + + if (_criteria == nil) { + ApptentiveLogError(ApptentiveLogTagCriteria, @"Attempting to initialize target with invalid criteria"); + } + } + + return self; +} + ++ (BOOL)supportsSecureCoding { + return YES; +} + +- (nullable instancetype)initWithCoder:(NSCoder *)coder +{ + self = [super init]; + if (self) { + _interactionIdentifier = [coder decodeObjectOfClass:[NSString class] forKey:InteractionIdentifierKey]; + _criteria = [coder decodeObjectForKey:CriteriaKey]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder +{ + [coder encodeObject:self.interactionIdentifier forKey:InteractionIdentifierKey]; + [coder encodeObject:self.criteria forKey:CriteriaKey]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveTargets.h b/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveTargets.h new file mode 100644 index 000000000..c4d3c8053 --- /dev/null +++ b/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveTargets.h @@ -0,0 +1,26 @@ +// +// ApptentiveTargets.h +// Apptentive +// +// Created by Frank Schmitt on 11/21/17. +// Copyright © 2017 Apptentive, Inc. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class ApptentiveConversation; + +@interface ApptentiveTargets : NSObject + +@property (readonly, nonatomic, strong) NSDictionary *invocations; + +- (nullable instancetype)initWithTargetsDictionary:(NSDictionary *)targetsDictionary; + +- (nullable NSString *)interactionIdentifierForEvent:(NSString *)event conversation:(ApptentiveConversation *)conversation; + +@end + +NS_ASSUME_NONNULL_END + diff --git a/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveTargets.m b/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveTargets.m new file mode 100644 index 000000000..5978c16d2 --- /dev/null +++ b/Apptentive/Apptentive/Engagement/Model/Targeting/ApptentiveTargets.m @@ -0,0 +1,71 @@ +// +// ApptentiveTargets.m +// Apptentive +// +// Created by Frank Schmitt on 11/21/17. +// Copyright © 2017 Apptentive, Inc. All rights reserved. +// + +#import "ApptentiveTargets.h" +#import "ApptentiveInvocations.h" + +NS_ASSUME_NONNULL_BEGIN + +static NSString * const InvocationsKey = @"invocations"; + +@implementation ApptentiveTargets + +- (nullable instancetype)initWithTargetsDictionary:(NSDictionary *)targetsDictionary { + self = [super init]; + + if (self) { + if (![targetsDictionary isKindOfClass:[NSDictionary class]]) { + ApptentiveLogError(@"targets is not a dictionary"); + return nil; + } + + NSMutableDictionary *invocations = [NSMutableDictionary dictionaryWithCapacity:targetsDictionary.count]; + + for (NSString *event in targetsDictionary) { + if (![targetsDictionary[event] isKindOfClass:[NSArray class]]) { + ApptentiveLogError(@"target value is not an array"); + continue; + } + + invocations[event] = [[ApptentiveInvocations alloc] initWithArray:targetsDictionary[event]]; + } + + _invocations = [invocations copy]; + } + + return self; +} + ++ (BOOL)supportsSecureCoding { + return YES; +} + +- (nullable instancetype)initWithCoder:(NSCoder *)coder +{ + self = [super init]; + if (self) { + _invocations = [coder decodeObjectOfClass:[NSDictionary class] forKey:InvocationsKey]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder +{ + [coder encodeObject:self.invocations forKey:InvocationsKey]; +} + +- (nullable NSString *)interactionIdentifierForEvent:(NSString *)event conversation:(ApptentiveConversation *)conversation { + ApptentiveInvocations *invocationsForEvent = self.invocations[event]; + + return [invocationsForEvent interactionIdentifierForConversation:conversation]; +} + +@end + +NS_ASSUME_NONNULL_END + diff --git a/Apptentive/Apptentive/Engagement/Persistence/ApptentiveBackend+Engagement.h b/Apptentive/Apptentive/Engagement/Persistence/ApptentiveBackend+Engagement.h index b0a92f61b..c318667dd 100644 --- a/Apptentive/Apptentive/Engagement/Persistence/ApptentiveBackend+Engagement.h +++ b/Apptentive/Apptentive/Engagement/Persistence/ApptentiveBackend+Engagement.h @@ -44,7 +44,6 @@ extern NSString *const ApptentiveEngagementMessageCenterEvent; - (BOOL)canShowInteractionForLocalEvent:(NSString *)event; - (BOOL)canShowInteractionForCodePoint:(NSString *)codePoint; -+ (NSString *)stringByEscapingCodePointSeparatorCharactersInString:(NSString *)string; + (NSString *)codePointForVendor:(NSString *)vendor interactionType:(NSString *)interactionType event:(NSString *)event; - (void)engageApptentiveAppEvent:(NSString *)event; @@ -55,6 +54,8 @@ extern NSString *const ApptentiveEngagementMessageCenterEvent; - (void)engageCodePoint:(NSString *)codePoint fromInteraction:(nullable ApptentiveInteraction *)fromInteraction userInfo:(nullable NSDictionary *)userInfo customData:(nullable NSDictionary *)customData extendedData:(nullable NSArray *)extendedData fromViewController:(nullable UIViewController *)viewController completion:(void (^_Nullable)(BOOL engaged))completion; +- (void)presentInteraction:(ApptentiveInteraction *)interaction fromViewController:(nullable UIViewController *)viewController; + - (void)codePointWasSeen:(NSString *)codePoint; - (void)engage:(NSString *)event fromInteraction:(ApptentiveInteraction *)interaction fromViewController:(nullable UIViewController *)viewController; @@ -63,8 +64,6 @@ extern NSString *const ApptentiveEngagementMessageCenterEvent; - (void)engage:(NSString *)event fromInteraction:(ApptentiveInteraction *)interaction fromViewController:(nullable UIViewController *)viewController userInfo:(nullable NSDictionary *)userInfo customData:(nullable NSDictionary *)customData extendedData:(nullable NSArray *)extendedData completion:(void (^ _Nullable)(BOOL))completion; - (void)interactionWasSeen:(NSString *)interactionID; -- (void)presentInteraction:(ApptentiveInteraction *)interaction fromViewController:(nullable UIViewController *)viewController; - - (void)invokeAction:(NSDictionary *)actionConfig withInteraction:(ApptentiveInteraction *)sourceInteraction fromViewController:(UIViewController *)fromViewController; @end diff --git a/Apptentive/Apptentive/Engagement/Persistence/ApptentiveBackend+Engagement.m b/Apptentive/Apptentive/Engagement/Persistence/ApptentiveBackend+Engagement.m index d17a7dc3f..11953a02a 100644 --- a/Apptentive/Apptentive/Engagement/Persistence/ApptentiveBackend+Engagement.m +++ b/Apptentive/Apptentive/Engagement/Persistence/ApptentiveBackend+Engagement.m @@ -9,14 +9,19 @@ #import "ApptentiveBackend+Engagement.h" #import "ApptentiveAppConfiguration.h" #import "ApptentiveBackend.h" +#import "ApptentiveInteraction.h" +#import "Apptentive_Private.h" +#import "ApptentiveInteractionController.h" #import "ApptentiveEngagement.h" #import "ApptentiveEngagementBackend.h" #import "ApptentiveEngagementManifest.h" #import "ApptentiveEventPayload.h" #import "ApptentiveInteraction.h" #import "ApptentiveInteractionController.h" -#import "ApptentiveInteractionInvocation.h" #import "ApptentiveSerialRequest.h" +#import "ApptentiveAppConfiguration.h" +#import "ApptentiveTargets.h" +#import "ApptentiveInvocations.h" #import "Apptentive_Private.h" #import "ApptentiveDispatchQueue.h" @@ -47,9 +52,12 @@ - (BOOL)canShowInteractionForCodePoint:(NSString *)codePoint { return (interaction != nil); } -- (ApptentiveInteraction *)interactionForInvocations:(NSArray *)invocations { - ApptentiveEngagementBackend *engagementBackend = [[ApptentiveEngagementBackend alloc] initWithConversation:self.conversationManager.activeConversation manifest:self.conversationManager.manifest]; - return [engagementBackend interactionForInvocations:invocations]; +- (ApptentiveInteraction *)interactionForInvocations:(NSArray *)invocationsArray { + ApptentiveInvocations *invocations = [[ApptentiveInvocations alloc] initWithArray:invocationsArray]; + + NSString *interactionIdentifier = [invocations interactionIdentifierForConversation:self.conversationManager.activeConversation]; + + return self.conversationManager.manifest.interactions[interactionIdentifier]; } - (ApptentiveInteraction *)interactionForIdentifier:(NSString *)identifier { @@ -57,31 +65,13 @@ - (ApptentiveInteraction *)interactionForIdentifier:(NSString *)identifier { } - (ApptentiveInteraction *)interactionForEvent:(NSString *)event { - NSArray *invocations = self.conversationManager.manifest.targets[event]; - ApptentiveInteraction *interaction = [self interactionForInvocations:invocations]; + NSString *interactionIdentifier = [self.conversationManager.manifest.targets interactionIdentifierForEvent:event conversation:self.conversationManager.activeConversation]; - return interaction; -} - -+ (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; + return self.conversationManager.manifest.interactions[interactionIdentifier]; } + (NSString *)codePointForVendor:(NSString *)vendor interactionType:(NSString *)interactionType event:(NSString *)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; + return [NSString stringWithFormat:@"%@#%@#%@", vendor, interactionType, event];; } - (void)engageApptentiveAppEvent:(NSString *)event { @@ -101,14 +91,14 @@ - (void)engageCodePoint:(NSString *)codePoint fromInteraction:(nullable Apptenti } - (void)engageCodePoint:(NSString *)codePoint fromInteraction:(nullable ApptentiveInteraction *)fromInteraction userInfo:(nullable NSDictionary *)userInfo customData:(nullable NSDictionary *)customData extendedData:(nullable NSArray *)extendedData fromViewController:(nullable UIViewController *)viewController completion:(void (^_Nullable)(BOOL engaged))completion { - ApptentiveLogInfo(@"Engage Apptentive event: %@", codePoint); + ApptentiveLogInfo(ApptentiveLogTagInteractions, @"Engage Apptentive event: %@", codePoint); ApptentiveAssertOperationQueue(self.operationQueue); // TODO: Do this on the background queue? ApptentiveConversation *conversation = self.conversationManager.activeConversation; if (conversation == nil) { - ApptentiveLogWarning(@"Attempting to engage event with no active conversation."); + ApptentiveLogWarning(ApptentiveLogTagInteractions, @"Attempting to engage event with no active conversation."); if (completion) { completion(NO); } @@ -126,12 +116,12 @@ - (void)engageCodePoint:(NSString *)codePoint fromInteraction:(nullable Apptenti ApptentiveInteraction *interaction = [engagementBackend interactionForEvent:codePoint]; if (interaction) { - ApptentiveLogInfo(@"--Running valid %@ interaction.", interaction.type); + ApptentiveLogInfo(ApptentiveLogTagInteractions, @"--Running valid %@ interaction.", interaction.type); dispatch_sync(dispatch_get_main_queue(), ^{ UIViewController *presentingController = viewController; if (viewController != nil && (!viewController.isViewLoaded || viewController.view.window == nil)) { - ApptentiveLogError(@"Attempting to present interaction on a view controller whose view is not visible in a window. Using a separate window instead."); + ApptentiveLogWarning(ApptentiveLogTagInteractions, @"Attempting to present interaction on a view controller whose view is not visible in a window. Using a separate window instead."); presentingController = nil; } @@ -186,7 +176,7 @@ - (void)interactionWasSeen:(NSString *)interactionID { - (void)presentInteraction:(ApptentiveInteraction *)interaction fromViewController:(nullable UIViewController *)viewController { if (!interaction) { - ApptentiveLogError(@"Attempting to present an interaction that does not exist!"); + ApptentiveLogError(ApptentiveLogTagInteractions, @"Attempting to present an interaction that does not exist."); return; } diff --git a/Apptentive/Apptentive/Engagement/Persistence/ApptentiveEngagementBackend.h b/Apptentive/Apptentive/Engagement/Persistence/ApptentiveEngagementBackend.h index 9d410d24a..c4230755d 100644 --- a/Apptentive/Apptentive/Engagement/Persistence/ApptentiveEngagementBackend.h +++ b/Apptentive/Apptentive/Engagement/Persistence/ApptentiveEngagementBackend.h @@ -21,9 +21,7 @@ NS_ASSUME_NONNULL_BEGIN @property (readonly, nonatomic) ApptentiveEngagementManifest *manifest; - (instancetype)initWithConversation:(ApptentiveConversation *)conversation manifest:(ApptentiveEngagementManifest *)manifest; - - (nullable ApptentiveInteraction *)interactionForEvent:(NSString *)event; -- (nullable ApptentiveInteraction *)interactionForInvocations:(NSArray *)invocations; @end diff --git a/Apptentive/Apptentive/Engagement/Persistence/ApptentiveEngagementBackend.m b/Apptentive/Apptentive/Engagement/Persistence/ApptentiveEngagementBackend.m index b024b45d1..825da713c 100644 --- a/Apptentive/Apptentive/Engagement/Persistence/ApptentiveEngagementBackend.m +++ b/Apptentive/Apptentive/Engagement/Persistence/ApptentiveEngagementBackend.m @@ -12,8 +12,13 @@ #import "ApptentiveEngagementBackend.h" #import "ApptentiveEngagementManifest.h" #import "ApptentiveInteraction.h" +#import "Apptentive_Private.h" +#import "ApptentiveInteractionController.h" +#import "ApptentiveEngagement.h" +#import "ApptentiveEngagementManifest.h" +#import "ApptentiveEngagementBackend.h" +#import "ApptentiveTargets.h" #import "ApptentiveInteractionController.h" -#import "ApptentiveInteractionInvocation.h" #import "Apptentive_Private.h" NS_ASSUME_NONNULL_BEGIN @@ -32,45 +37,9 @@ - (instancetype)initWithConversation:(ApptentiveConversation *)conversation mani } - (nullable ApptentiveInteraction *)interactionForEvent:(NSString *)event { - NSArray *invocations = self.manifest.targets[event]; - ApptentiveInteraction *interaction = [self interactionForInvocations:invocations]; - - return interaction; -} - -- (nullable ApptentiveInteraction *)interactionForInvocations:(NSArray *)invocations { - NSString *interactionID = nil; - - for (NSObject *invocationOrDictionary in invocations) { - ApptentiveInteractionInvocation *invocation = nil; - - // Allow parsing of ATInteractionInvocation and NSDictionary invocation objects - if ([invocationOrDictionary isKindOfClass:[ApptentiveInteractionInvocation class]]) { - invocation = (ApptentiveInteractionInvocation *)invocationOrDictionary; - } else if ([invocationOrDictionary isKindOfClass:[NSDictionary class]]) { - invocation = [ApptentiveInteractionInvocation invocationWithJSONDictionary:((NSDictionary *)invocationOrDictionary)]; - } else { - ApptentiveLogError(@"Attempting to parse an invocation that is neither an ATInteractionInvocation or NSDictionary."); - } - - if (invocation && [invocation isKindOfClass:[ApptentiveInteractionInvocation class]]) { - if ([invocation criteriaAreMetForConversation:self.conversation]) { - interactionID = invocation.interactionID; - break; - } - } - } - - ApptentiveInteraction *interaction = nil; - if (interactionID) { - interaction = [self interactionForIdentifier:interactionID]; - } - - return interaction; -} + NSString *interactionIdentifier = [self.manifest.targets interactionIdentifierForEvent:event conversation:self.conversation]; -- (ApptentiveInteraction *)interactionForIdentifier:(NSString *)identifier { - return self.manifest.interactions[identifier]; + return self.manifest.interactions[interactionIdentifier]; } @end diff --git a/Apptentive/Apptentive/Info.plist b/Apptentive/Apptentive/Info.plist index 05c0461c4..c3933b3c2 100644 --- a/Apptentive/Apptentive/Info.plist +++ b/Apptentive/Apptentive/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 5.0.4 + 5.1.0 CFBundleVersion $(CURRENT_PROJECT_VERSION) NSPrincipalClass diff --git a/Apptentive/Apptentive/Message Center/ApptentiveMessageCenterViewModel.m b/Apptentive/Apptentive/Message Center/ApptentiveMessageCenterViewModel.m index c72d60bfa..b3b882f1e 100644 --- a/Apptentive/Apptentive/Message Center/ApptentiveMessageCenterViewModel.m +++ b/Apptentive/Apptentive/Message Center/ApptentiveMessageCenterViewModel.m @@ -420,7 +420,7 @@ - (void)downloadAttachmentAtIndexPath:(NSIndexPath *)indexPath { ApptentiveAttachment *attachment = [self fileAttachmentAtIndexPath:indexPath]; attachment.attachmentDirectoryPath = self.messageManager.attachmentDirectoryPath; if (attachment.filename != nil || !attachment.remoteURL) { - ApptentiveLogError(@"Attempting to download attachment with missing or invalid remote URL"); + ApptentiveLogError(ApptentiveLogTagMessages, @"Attempting to download attachment with missing or invalid remote URL."); return; } @@ -448,7 +448,7 @@ - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTas // -beginMoveToStorageFrom: must be called on this (background) thread. NSError *error; if (![[NSFileManager defaultManager] moveItemAtURL:location toURL:finalLocation error:&error]) { - ApptentiveLogError(@"Unable to move attachment to final location: %@", error); + ApptentiveLogError(ApptentiveLogTagMessages, @"Unable to move attachment to final location (%@).", error); } dispatch_async(dispatch_get_main_queue(), ^{ diff --git a/Apptentive/Apptentive/Message Center/Controllers/ApptentiveAttachmentController.m b/Apptentive/Apptentive/Message Center/Controllers/ApptentiveAttachmentController.m index b29b9a321..12b3dc988 100644 --- a/Apptentive/Apptentive/Message Center/Controllers/ApptentiveAttachmentController.m +++ b/Apptentive/Apptentive/Message Center/Controllers/ApptentiveAttachmentController.m @@ -221,7 +221,7 @@ - (void)imagePickerController:(UIImagePickerController *)picker didFinishPicking if (photo) { [self insertImage:photo]; } else { - ApptentiveLogError(@"Unable to get photo"); + ApptentiveLogWarning(ApptentiveLogTagMessages, @"Unable to get photo."); } [self dismissImagePicker:picker]; diff --git a/Apptentive/Apptentive/Message Center/Controllers/ApptentiveMessageCenterViewController.m b/Apptentive/Apptentive/Message Center/Controllers/ApptentiveMessageCenterViewController.m index 4f5ffe311..22cb7fb01 100644 --- a/Apptentive/Apptentive/Message Center/Controllers/ApptentiveMessageCenterViewController.m +++ b/Apptentive/Apptentive/Message Center/Controllers/ApptentiveMessageCenterViewController.m @@ -1028,7 +1028,7 @@ - (void)setState:(ATMessageCenterState)state { break; default: - ApptentiveLogError(@"Invalid Message Center State: %d", state); + ApptentiveLogError(ApptentiveLogTagMessages, @"Invalid Message Center State: %d", state); break; } diff --git a/Apptentive/Apptentive/Message Center/Model/ApptentiveAttachment.m b/Apptentive/Apptentive/Message Center/Model/ApptentiveAttachment.m index 82ce9ed39..1cb13faa2 100644 --- a/Apptentive/Apptentive/Message Center/Model/ApptentiveAttachment.m +++ b/Apptentive/Apptentive/Message Center/Model/ApptentiveAttachment.m @@ -45,7 +45,7 @@ - (nullable instancetype)initWithJSON:(NSDictionary *)JSON { _contentType = ApptentiveDictionaryGetString(JSON, @"content_type"); if (_contentType.length == 0) { - ApptentiveLogError(@"Can't init %@: content type is nil or empty", NSStringFromClass([self class])); + ApptentiveLogError(ApptentiveLogTagMessages, @"Can't init %@: content type is nil or empty", NSStringFromClass([self class])); return nil; } @@ -72,7 +72,7 @@ - (nullable instancetype)initWithPath:(NSString *)path contentType:(NSString *)c BOOL isDirectory; if (![[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDirectory] || isDirectory) { - ApptentiveLogError(@"Can't init %@: file does not exist: %@", NSStringFromClass([self class]), path); + ApptentiveLogError(ApptentiveLogTagMessages, @"Can't init %@: file does not exist: %@", NSStringFromClass([self class]), path); return nil; } @@ -243,7 +243,7 @@ - (nullable NSString *)fullLocalPathForFilename:(NSString *)filename { ApptentiveAssertNotEmpty(self.attachmentDirectoryPath, @"Attachment directory path must be set"); if (filename.length == 0 || self.attachmentDirectoryPath.length == 0) { - ApptentiveLogError(@"Attempting to access attachment without attachment directory path set"); + ApptentiveLogError(ApptentiveLogTagMessages, @"Attempting to access attachment without attachment directory path set."); return nil; // TODO: assertion? } @@ -281,29 +281,29 @@ - (void)deleteSidecarIfNecessary { NSError *error = nil; BOOL isDir = NO; if (![fm fileExistsAtPath:fullPath isDirectory:&isDir] || isDir) { - ApptentiveLogError(@"File attachment sidecar doesn't exist at path or is directory: %@, %d", fullPath, isDir); + ApptentiveLogWarning(ApptentiveLogTagMessages, @"File attachment sidecar doesn't exist at path or is directory: %@, %d", fullPath, isDir); return; } if (![fm removeItemAtPath:fullPath error:&error]) { - ApptentiveLogError(@"Error removing attachment at path: %@. %@", fullPath, error); + ApptentiveLogWarning(ApptentiveLogTagMessages, @"Error removing attachment at path: %@. %@", fullPath, error); return; } // Delete any thumbnails. ApptentiveMessageManager *messageManager = Apptentive.shared.backend.conversationManager.messageManager; ApptentiveAssertNotNil(messageManager, @"Message manager is nil"); if (!messageManager) { - ApptentiveLogError(@"Error listing attachments directory: message manager is not initialized"); + ApptentiveLogWarning(ApptentiveLogTagMessages, @"Error listing attachments directory: message manager is not initialized."); } else { NSArray *filenames = [fm contentsOfDirectoryAtPath:messageManager.attachmentDirectoryPath error:&error]; if (!filenames) { - ApptentiveLogError(@"Error listing attachments directory: %@", error); + ApptentiveLogWarning(ApptentiveLogTagMessages, @"Error listing attachments directory: %@", error); } else { for (NSString *filename in filenames) { if ([filename hasPrefix:self.filename.stringByDeletingPathExtension]) { NSString *thumbnailPath = [self fullLocalPathForFilename:filename]; if (![fm removeItemAtPath:thumbnailPath error:&error]) { - ApptentiveLogError(@"Error removing attachment thumbnail at path: %@. %@", thumbnailPath, error); + ApptentiveLogWarning(ApptentiveLogTagMessages, @"Error removing attachment thumbnail at path: %@. %@", thumbnailPath, error); continue; } } diff --git a/Apptentive/Apptentive/Message Center/Model/ApptentiveMessage.m b/Apptentive/Apptentive/Message Center/Model/ApptentiveMessage.m index 10b5ee755..0ebc6321d 100644 --- a/Apptentive/Apptentive/Message Center/Model/ApptentiveMessage.m +++ b/Apptentive/Apptentive/Message Center/Model/ApptentiveMessage.m @@ -44,7 +44,7 @@ - (nullable instancetype)initWithJSON:(NSDictionary *)JSON { if (self) { if (![JSON isKindOfClass:[NSDictionary class]]) { - ApptentiveLogError(@"Can't init %@: invalid json: %@", NSStringFromClass([self class]), JSON); + ApptentiveLogError(ApptentiveLogTagMessages, @"Can't init %@: invalid json: %@", NSStringFromClass([self class]), ApptentiveHideKeysIfSanitized(JSON, @[@"body", @"custom_data", @"title"])); return nil; } @@ -68,7 +68,7 @@ - (nullable instancetype)initWithJSON:(NSDictionary *)JSON { _sender = [[ApptentiveMessageSender alloc] initWithJSON:JSON[@"sender"]]; if (_sender == nil) { - ApptentiveLogError(@"Can't init %@: sender can't be created", NSStringFromClass([self class])); + ApptentiveLogError(ApptentiveLogTagMessages, @"Can't init %@: sender can't be created", NSStringFromClass([self class])); return nil; } @@ -186,7 +186,7 @@ - (ApptentiveMessage *)mergedWith:(ApptentiveMessage *)messageFromServer { _attachments = updatedAttachments; } else { - ApptentiveLogError(@"Mismatch in number of attachments between client and server."); + ApptentiveLogError(ApptentiveLogTagMessages, @"Mismatch in number of attachments between client and server."); } return self; diff --git a/Apptentive/Apptentive/Message Center/Model/ApptentiveMessageManager.m b/Apptentive/Apptentive/Message Center/Model/ApptentiveMessageManager.m index 55a06606d..87c99d866 100644 --- a/Apptentive/Apptentive/Message Center/Model/ApptentiveMessageManager.m +++ b/Apptentive/Apptentive/Message Center/Model/ApptentiveMessageManager.m @@ -27,7 +27,10 @@ NSString *const ATMessageCenterDidSkipProfileKey = @"ATMessageCenterDidSkipProfileKey"; NSString *const ATMessageCenterDraftMessageKey = @"ATMessageCenterDraftMessageKey"; +NSString *const ApptentiveHasSentMessageKey = @"ApptentiveHasSentMessageKey"; +NSNotificationName const ApptentiveMessageSentNotification = @"ApptentiveMessageSentNotification"; +NSString *const ApptentiveSentByUserKey = @"com.apptentive.sentByUser"; @interface ApptentiveMessageManager () @@ -37,6 +40,7 @@ @interface ApptentiveMessageManager () @property (readonly, nonatomic) NSMutableDictionary *messageIdentifierIndex; @property (readonly, nonatomic) ApptentiveMessageStore *messageStore; @property (readonly, nonatomic) NSInteger unreadCount; +@property (readonly, nonatomic) BOOL hasSentMessage; @property (readonly, nonatomic) NSString *messageStorePath; @property (nullable, copy, nonatomic) void (^backgroundFetchBlock)(UIBackgroundFetchResult); @@ -65,6 +69,7 @@ - (instancetype)initWithStoragePath:(NSString *)storagePath client:(ApptentiveCl _didSkipProfile = [conversation.userInfo[ATMessageCenterDidSkipProfileKey] boolValue]; _draftMessage = conversation.userInfo[ATMessageCenterDraftMessageKey]; + _hasSentMessage = [conversation.userInfo[ApptentiveHasSentMessageKey] boolValue]; for (ApptentiveMessage *message in _messageStore.messages) { for (ApptentiveAttachment *attachment in message.attachments) { @@ -76,7 +81,7 @@ - (instancetype)initWithStoragePath:(NSString *)storagePath client:(ApptentiveCl NSError *error; if (![[NSFileManager defaultManager] createDirectoryAtPath:self.attachmentDirectoryPath withIntermediateDirectories:YES attributes:nil error:&error]) { - ApptentiveAssertTrue(NO, @"Unable to create attachments directory “%@” (%@)", self.attachmentDirectoryPath, error); + ApptentiveAssertTrue(NO, @"Unable to create attachments directory %@ (%@)", self.attachmentDirectoryPath, error); return nil; } @@ -102,7 +107,7 @@ - (void)stop { } - (void)checkForMessages { - if (self.messageOperation != nil || self.conversationIdentifier == nil) { + if (!self.hasSentMessage || self.messageOperation != nil || self.conversationIdentifier == nil) { return; } @@ -156,7 +161,7 @@ + (NSString *)attachmentDirectoryPathForConversationDirectory:(NSString *)conver NSError *error; if (![[NSFileManager defaultManager] createDirectoryAtPath:result withIntermediateDirectories:YES attributes:nil error:&error]) { - ApptentiveLogError(@"Unable to create attachments directory (%@): %@", result, error); + ApptentiveLogError(ApptentiveLogTagMessages, @"Unable to create attachments directory (%@): %@", result, error); return nil; } } @@ -190,7 +195,7 @@ - (void)processMessageOperationResponse:(ApptentiveRequestOperation *)operation NSArray *messageListJSON = [operation.responseObject valueForKey:@"messages"]; if (messageListJSON == nil) { - ApptentiveLogError(@"Unexpected response from /messages endpoint"); + ApptentiveLogError(ApptentiveLogTagMessages, @"Unexpected response from /messages endpoint."); return; } @@ -225,7 +230,7 @@ - (void)processMessageOperationResponse:(ApptentiveRequestOperation *)operation lastDownloadedMessageIdentifier = message.identifier; } else { - ApptentiveLogError(@"Unable to create message from JSON: %@", messageJSON); + ApptentiveLogError(ApptentiveLogTagMessages, @"Unable to create message from JSON: %@", ApptentiveHideKeysIfSanitized(messageJSON, @[@"body", @"custom_data", @"title"])); } } @@ -351,6 +356,14 @@ - (void)enqueueMessageForSending:(ApptentiveMessage *)message { } message.state = ApptentiveMessageStateWaiting; + + dispatch_async(dispatch_get_main_queue(), ^{ + BOOL sentByUser = [message.sender.identifier isEqualToString:self.localUserIdentifier]; + [[NSNotificationCenter defaultCenter] postNotificationName:ApptentiveMessageSentNotification object:Apptentive.shared userInfo:@{ ApptentiveSentByUserKey: @(sentByUser)}]; + }); + + _hasSentMessage = YES; + [self.conversation setUserInfo:@(self.hasSentMessage) forKey:ApptentiveHasSentMessageKey]; } #pragma mark - Client message delelgate diff --git a/Apptentive/Apptentive/Message Center/Model/ApptentiveMessageSender.m b/Apptentive/Apptentive/Message Center/Model/ApptentiveMessageSender.m index 8ee41d3e3..9f774397e 100644 --- a/Apptentive/Apptentive/Message Center/Model/ApptentiveMessageSender.m +++ b/Apptentive/Apptentive/Message Center/Model/ApptentiveMessageSender.m @@ -7,6 +7,7 @@ // #import "ApptentiveMessageSender.h" +#import "ApptentiveDefines.h" NS_ASSUME_NONNULL_BEGIN @@ -26,7 +27,7 @@ - (nullable instancetype)initWithJSON:(NSDictionary *)JSON { if (self) { if (![JSON isKindOfClass:[NSDictionary class]]) { - ApptentiveLogError(@"Can't init %@: invalid json object class: %@", NSStringFromClass([self class]), NSStringFromClass([JSON class])); + ApptentiveLogError(ApptentiveLogTagMessages, @"Can't init %@: invalid json object class: %@", NSStringFromClass([self class]), NSStringFromClass([JSON class])); return nil; } @@ -34,7 +35,7 @@ - (nullable instancetype)initWithJSON:(NSDictionary *)JSON { _identifier = ApptentiveDictionaryGetString(JSON, @"id"); if (_identifier.length == 0) { - ApptentiveLogError(@"Can't init %@: identifier is nil or empty", NSStringFromClass([self class])); + ApptentiveLogError(ApptentiveLogTagMessages, @"Can't init %@: identifier is nil or empty", NSStringFromClass([self class])); return nil; } @@ -51,10 +52,7 @@ - (nullable instancetype)initWithName:(nullable NSString *)name identifier:(NSSt self = [super init]; if (self) { - if (identifier.length == 0) { - ApptentiveLogError(@"Can't init %@: identifier is nil or empty", NSStringFromClass([self class])); - return nil; - } + APPTENTIVE_CHECK_INIT_NOT_EMPTY_ARG(identifier); _name = name; _identifier = identifier; diff --git a/Apptentive/Apptentive/Misc/ApptentiveAssert.h b/Apptentive/Apptentive/Misc/ApptentiveAssert.h index d5b37cb29..3aa297d93 100644 --- a/Apptentive/Apptentive/Misc/ApptentiveAssert.h +++ b/Apptentive/Apptentive/Misc/ApptentiveAssert.h @@ -58,6 +58,15 @@ extern NSString * _Nullable ApptentiveGetCurrentThreadName(void); #define ApptentiveAssertTrue(expression, ...) \ if (!(expression)) __ApptentiveAssertHelper(#expression, __FILE__, __LINE__, __PRETTY_FUNCTION__, __VA_ARGS__) +/*! + * @define ApptentiveAssertFalse(expression, ...) + * Generates a failure when ((\a expression) == true). + * @param expression An expression of boolean type. + * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted. + */ +#define ApptentiveAssertFalse(expression, ...) \ +if (expression) __ApptentiveAssertHelper(#expression, __FILE__, __LINE__, __PRETTY_FUNCTION__, __VA_ARGS__) + /*! * @define ApptentiveAssertDispatchQueue(expression, ...) * Generates a failure when ((\a expression1) does not match the current dispatch queue. diff --git a/Apptentive/Apptentive/Misc/ApptentiveAsyncLogWriter.h b/Apptentive/Apptentive/Misc/ApptentiveAsyncLogWriter.h new file mode 100644 index 000000000..310888922 --- /dev/null +++ b/Apptentive/Apptentive/Misc/ApptentiveAsyncLogWriter.h @@ -0,0 +1,27 @@ +// +// ApptentiveAsyncLogWriter.h +// Apptentive +// +// Created by Alex Lementuev on 2/22/18. +// Copyright © 2018 Apptentive, Inc. All rights reserved. +// + +#import + +@class ApptentiveDispatchQueue; + +NS_ASSUME_NONNULL_BEGIN + +@interface ApptentiveAsyncLogWriter : NSObject + +- (instancetype)initWithDestDir:(NSString *)destDir historySize:(NSUInteger)historySize; +- (instancetype)initWithDestDir:(NSString *)destDir historySize:(NSUInteger)historySize queue:(ApptentiveDispatchQueue *)queue; + +- (void)logMessage:(NSString *)message; +- (NSString *)createLogFilename; + +- (nullable NSArray *)listLogFiles; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Apptentive/Apptentive/Misc/ApptentiveAsyncLogWriter.m b/Apptentive/Apptentive/Misc/ApptentiveAsyncLogWriter.m new file mode 100644 index 000000000..17818b88b --- /dev/null +++ b/Apptentive/Apptentive/Misc/ApptentiveAsyncLogWriter.m @@ -0,0 +1,114 @@ +// +// ApptentiveAsyncLogWriter.m +// Apptentive +// +// Created by Alex Lementuev on 2/22/18. +// Copyright © 2018 Apptentive, Inc. All rights reserved. +// + +#import "ApptentiveAsyncLogWriter.h" +#import "ApptentiveDispatchQueue.h" +#import "ApptentiveDefines.h" +#import "ApptentiveLogFileWriteTask.h" +#import "ApptentiveFileUtilities.h" + +static const NSUInteger kMessageQueueSize = 256; + +@interface ApptentiveAsyncLogWriter () + +@property (nonatomic, strong) NSString *destDir; +@property (nonatomic, strong) NSMutableArray *pendingMessages; +@property (nonatomic, strong) ApptentiveDispatchQueue *writeQueue; +@property (nonatomic, assign) NSUInteger logHistorySize; +@property (nonatomic, strong) ApptentiveDispatchTask *writeQueueTask; + +@end + +@implementation ApptentiveAsyncLogWriter + +- (instancetype)initWithDestDir:(NSString *)destDir historySize:(NSUInteger)historySize { + return [self initWithDestDir:destDir historySize:historySize queue:[ApptentiveDispatchQueue createQueueWithName:@"Log Queue" concurrencyType:ApptentiveDispatchQueueConcurrencyTypeSerial]]; +} +- (instancetype)initWithDestDir:(NSString *)destDir historySize:(NSUInteger)historySize queue:(ApptentiveDispatchQueue *)queue { + APPTENTIVE_CHECK_INIT_NOT_EMPTY_ARG(destDir) + APPTENTIVE_CHECK_INIT_NOT_NIL_ARG(queue) + self = [super init]; + if (self) { + _destDir = destDir; + _logHistorySize = historySize; + _writeQueue = queue; + _pendingMessages = [NSMutableArray arrayWithCapacity:kMessageQueueSize]; + + NSString *logFile = [destDir stringByAppendingPathComponent:[self createLogFilename]]; + ApptentiveLogVerbose(ApptentiveLogTagUtility, @"Log file: %@", logFile); + + _writeQueueTask = [[ApptentiveLogFileWriteTask alloc] initWithFile:logFile buffer:_pendingMessages]; + + // run initialization as the first task on the write queue + [_writeQueue dispatchAsync:^{ + [self prepareLogsDirectory:destDir]; + }]; + } + return self; +} + +- (void)prepareLogsDirectory:(NSString *)logsDir { + if (![ApptentiveFileUtilities directoryExistsAtPath:logsDir]) { + return; + } + + // list existing log files + NSError *error; + NSArray *files = [ApptentiveFileUtilities listFilesAtPath:logsDir error:&error]; + if (files == nil) { + ApptentiveLogError(ApptentiveLogTagUtility, @"Error getting contents of directory %@ (%@).", logsDir, error); + return; + } + + // anything to clear? + if (files.count <= self.logHistorySize) { + return; + } + + // sort existing log files by modification date (oldest come first) + files = [files sortedArrayUsingSelector:@selector(compare:)]; + + // don't delete latest files which fit into the history size + NSArray *filesToDelete = [files subarrayWithRange:NSMakeRange(0, files.count - self.logHistorySize)]; + + // delete oldest files if the total count exceed the log history size + for (NSString *file in filesToDelete) { + if (![ApptentiveFileUtilities deleteFileAtPath:file error:&error]) { + ApptentiveLogError(ApptentiveLogTagUtility, @"Error while deleteing file %@ (%@).", file, error); + } + } +} + +- (NSArray *)listLogFiles { + return [ApptentiveFileUtilities listFilesAtPath:self.destDir error:NULL]; +} + +#pragma mark - +#pragma mark Messages + +- (void)logMessage:(NSString *)message { + ApptentiveAssertNotNil(message, @"Attempted to add a nil message"); + if (message != nil) { + @synchronized (self.pendingMessages) { + [self.pendingMessages addObject:message]; + [_writeQueue dispatchTaskOnce:_writeQueueTask]; + } + } +} + +#pragma mark - +#pragma Unit tests + +- (NSString *)createLogFilename { + NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; + [formatter setLocale:[NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]]; + [formatter setDateFormat:@"yyyy-MM-dd_hh-mm-ss"]; + return [NSString stringWithFormat:@"apptentive-%@.log", [formatter stringFromDate:[NSDate date]]]; +} + +@end diff --git a/Apptentive/Apptentive/Misc/ApptentiveDispatchQueue.h b/Apptentive/Apptentive/Misc/ApptentiveDispatchQueue.h index fec932193..f9d467fda 100644 --- a/Apptentive/Apptentive/Misc/ApptentiveDispatchQueue.h +++ b/Apptentive/Apptentive/Misc/ApptentiveDispatchQueue.h @@ -17,6 +17,8 @@ typedef enum : NSUInteger { extern NSString * _Nullable ApptentiveGetCurrentThreadName(void); +@class ApptentiveDispatchTask; + @interface ApptentiveDispatchQueue : NSObject @property (nonatomic, assign, getter=isSuspended) BOOL suspended; @@ -41,6 +43,10 @@ extern NSString * _Nullable ApptentiveGetCurrentThreadName(void); - (void)dispatchAsync:(void (^)(void))task withDependency:(NSOperation *)dependency; +- (void)dispatchTask:(ApptentiveDispatchTask *)task; + +- (void)dispatchTaskOnce:(ApptentiveDispatchTask *)task; + @end NS_ASSUME_NONNULL_END diff --git a/Apptentive/Apptentive/Misc/ApptentiveDispatchQueue.m b/Apptentive/Apptentive/Misc/ApptentiveDispatchQueue.m index 40ae805f1..be0d134b1 100644 --- a/Apptentive/Apptentive/Misc/ApptentiveDispatchQueue.m +++ b/Apptentive/Apptentive/Misc/ApptentiveDispatchQueue.m @@ -10,6 +10,7 @@ #import "ApptentiveAssert.h" #import "ApptentiveDefines.h" +#import "ApptentiveDispatchTask+Internal.h" #import "ApptentiveGCDDispatchQueue.h" static ApptentiveDispatchQueue * _mainQueue; @@ -101,6 +102,28 @@ - (void)dispatchAsync:(void (^)(void))task withDependency:(nonnull NSOperation * APPTENTIVE_ABSTRACT_METHOD_CALLED } +- (void)dispatchTask:(ApptentiveDispatchTask *)task { + ApptentiveAssertNotNil(task, @"Attempted to dispatch a nil task"); + if (task) { + [self dispatchAsync:^{ + task.scheduled = YES; + [task executeTask]; + }]; + } +} + +- (void)dispatchTaskOnce:(ApptentiveDispatchTask *)task { + ApptentiveAssertNotNil(task, @"Attempted to dispatch a nil task"); + if (task) { + [self dispatchAsync:^{ + if (!task.scheduled) { + task.scheduled = YES; + [task executeTask]; + } + }]; + } +} + #pragma mark - #pragma mark Properties diff --git a/Apptentive/Apptentive/Misc/ApptentiveDispatchTask+Internal.h b/Apptentive/Apptentive/Misc/ApptentiveDispatchTask+Internal.h new file mode 100644 index 000000000..d9b125b21 --- /dev/null +++ b/Apptentive/Apptentive/Misc/ApptentiveDispatchTask+Internal.h @@ -0,0 +1,17 @@ +// +// ApptentiveDispatchTask+Internal.h +// Apptentive +// +// Created by Alex Lementuev on 2/22/18. +// Copyright © 2018 Apptentive, Inc. All rights reserved. +// + +#import "ApptentiveDispatchTask.h" + +@interface ApptentiveDispatchTask (Internal) + +- (void)executeTask; +- (void)setScheduled:(BOOL)scheduled; +- (void)setCancelled:(BOOL)cancelled; + +@end diff --git a/Apptentive/Apptentive/Misc/ApptentiveDispatchTask.h b/Apptentive/Apptentive/Misc/ApptentiveDispatchTask.h new file mode 100644 index 000000000..00c3a3947 --- /dev/null +++ b/Apptentive/Apptentive/Misc/ApptentiveDispatchTask.h @@ -0,0 +1,18 @@ +// +// ApptentiveDispatchTask.h +// Apptentive +// +// Created by Alex Lementuev on 2/22/18. +// Copyright © 2018 Apptentive, Inc. All rights reserved. +// + +#import + +@interface ApptentiveDispatchTask : NSObject + +@property (atomic, readonly) BOOL scheduled; +@property (atomic, readonly) BOOL cancelled; + +- (void)execute; + +@end diff --git a/Apptentive/Apptentive/Misc/ApptentiveDispatchTask.m b/Apptentive/Apptentive/Misc/ApptentiveDispatchTask.m new file mode 100644 index 000000000..b83020a9e --- /dev/null +++ b/Apptentive/Apptentive/Misc/ApptentiveDispatchTask.m @@ -0,0 +1,39 @@ +// +// ApptentiveDispatchTask.m +// Apptentive +// +// Created by Alex Lementuev on 2/22/18. +// Copyright © 2018 Apptentive, Inc. All rights reserved. +// + +#import "ApptentiveDispatchTask+Internal.h" +#import "ApptentiveDefines.h" + +@interface ApptentiveDispatchTask () + +@property (atomic, assign) BOOL scheduled; +@property (atomic, assign) BOOL cancelled; + +@end + +@implementation ApptentiveDispatchTask + +- (void)executeTask { + @try { + self.scheduled = NO; + + if (!self.cancelled) { + [self execute]; + } + } @catch (NSException *e) { + ApptentiveLogError(@"Exception while executing task"); + } @finally { + self.cancelled = NO; + } +} + +- (void)execute { + APPTENTIVE_ABSTRACT_METHOD_CALLED +} + +@end diff --git a/Apptentive/Apptentive/Misc/ApptentiveFileUtilities.h b/Apptentive/Apptentive/Misc/ApptentiveFileUtilities.h new file mode 100644 index 000000000..7ba540b64 --- /dev/null +++ b/Apptentive/Apptentive/Misc/ApptentiveFileUtilities.h @@ -0,0 +1,24 @@ +// +// ApptentiveFileUtilities.h +// Apptentive +// +// Created by Alex Lementuev on 2/23/18. +// Copyright © 2018 Apptentive, Inc. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface ApptentiveFileUtilities : NSObject + ++ (BOOL)fileExistsAtPath:(NSString *)path; ++ (BOOL)directoryExistsAtPath:(NSString *)path; ++ (BOOL)deleteFileAtPath:(NSString *)path; ++ (BOOL)deleteFileAtPath:(NSString *)path error:(NSError **)error; ++ (BOOL)deleteDirectoryAtPath:(NSString *)path error:(NSError **)error; ++ (nullable NSArray *)listFilesAtPath:(NSString *)path error:(NSError **)error; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Apptentive/Apptentive/Misc/ApptentiveFileUtilities.m b/Apptentive/Apptentive/Misc/ApptentiveFileUtilities.m new file mode 100644 index 000000000..0d5d3432d --- /dev/null +++ b/Apptentive/Apptentive/Misc/ApptentiveFileUtilities.m @@ -0,0 +1,55 @@ +// +// ApptentiveFileUtilities.m +// Apptentive +// +// Created by Alex Lementuev on 2/23/18. +// Copyright © 2018 Apptentive, Inc. All rights reserved. +// + +#import "ApptentiveFileUtilities.h" +#import "ApptentiveUtilities.h" + +@implementation ApptentiveFileUtilities + ++ (BOOL)fileExistsAtPath:(NSString *)path { + return path != nil && [[NSFileManager defaultManager] fileExistsAtPath:path]; +} + ++ (BOOL)directoryExistsAtPath:(NSString *)path { + BOOL directory; + return path != nil && [[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&directory] && directory; +} + ++ (BOOL)deleteFileAtPath:(NSString *)path { + return [self deleteFileAtPath:path error:NULL]; +} + ++ (BOOL)deleteFileAtPath:(NSString *)path error:(NSError **)error { + return path != nil && [[NSFileManager defaultManager] removeItemAtPath:path error:error]; +} + ++ (BOOL)deleteDirectoryAtPath:(NSString *)path error:(NSError **)error { + return path != nil && [[NSFileManager defaultManager] removeItemAtPath:path error:error]; +} + ++ (nullable NSArray *)listFilesAtPath:(NSString *)path error:(NSError **)error { + if (path.length == 0) { + if (error) { + *error = [ApptentiveUtilities errorWithCode:100 failureReason:@"Path is nil or empty"]; + } + return nil; + } + + NSArray *names = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:path error:error]; + if (names == nil) { + return nil; + } + + NSMutableArray *files = [NSMutableArray arrayWithCapacity:names.count]; + for (NSString *name in names) { + [files addObject:[path stringByAppendingPathComponent:name]]; + } + return files; +} + +@end diff --git a/Apptentive/Apptentive/Misc/ApptentiveGCDDispatchQueue.m b/Apptentive/Apptentive/Misc/ApptentiveGCDDispatchQueue.m index 7a1dffd89..9e60405fd 100644 --- a/Apptentive/Apptentive/Misc/ApptentiveGCDDispatchQueue.m +++ b/Apptentive/Apptentive/Misc/ApptentiveGCDDispatchQueue.m @@ -50,7 +50,7 @@ - (void)dispatchTaskGuarded:(void (^)(void))task { @try { task(); } @catch (NSException *exception) { - ApptentiveLogCrit(@"Exception while dispatching task: %@", exception); + ApptentiveLogCrit(ApptentiveLogTagUtility, @"Exception while dispatching task (%@).", exception); } } diff --git a/Apptentive/Apptentive/Misc/ApptentiveIndentPrinter.h b/Apptentive/Apptentive/Misc/ApptentiveIndentPrinter.h new file mode 100644 index 000000000..610daf3dc --- /dev/null +++ b/Apptentive/Apptentive/Misc/ApptentiveIndentPrinter.h @@ -0,0 +1,27 @@ +// +// ApptentiveIndentPrinter.h +// Apptentive +// +// Created by Frank Schmitt on 2/21/18. +// Copyright © 2018 Apptentive, Inc. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface ApptentiveIndentPrinter : NSObject + +@property (readonly, nonatomic) NSInteger indentLevel; +@property (assign, nonatomic) NSInteger indentWidth; +@property (readonly, nonatomic) NSString *output; + +- (void)indent; +- (void)outdent; + +- (void)appendString:(NSString *)string; +- (void)appendFormat:(NSString *)format, ... NS_FORMAT_FUNCTION(1, 2); + +@end + +NS_ASSUME_NONNULL_END diff --git a/Apptentive/Apptentive/Misc/ApptentiveIndentPrinter.m b/Apptentive/Apptentive/Misc/ApptentiveIndentPrinter.m new file mode 100644 index 000000000..43f85b281 --- /dev/null +++ b/Apptentive/Apptentive/Misc/ApptentiveIndentPrinter.m @@ -0,0 +1,64 @@ +// +// ApptentiveIndentPrinter.m +// Apptentive +// +// Created by Frank Schmitt on 2/21/18. +// Copyright © 2018 Apptentive, Inc. All rights reserved. +// + +#import "ApptentiveIndentPrinter.h" + +@interface ApptentiveIndentPrinter () + +@property (readwrite, nonatomic) NSInteger indentLevel; +//@property (readwrite, nonatomic) NSString *output; +@property (strong, nonatomic) NSMutableArray *lines; + +@end + +@implementation ApptentiveIndentPrinter + +- (instancetype)init +{ + self = [super init]; + if (self) { + _indentWidth = 2; + _lines = [NSMutableArray array]; + } + return self; +} + +- (void)indent { + self.indentLevel ++; +} + +- (void)outdent { + if (self.indentLevel >= 1) { + self.indentLevel --; + } else { + ApptentiveAssertFail(@"Attempting to outdent past zero"); + } +} + +- (NSString *)indentationString { + return [@"" stringByPaddingToLength:self.indentLevel * self.indentWidth withString:@" " startingAtIndex:0]; +} + +- (void)appendString:(NSString *)string { + [self.lines addObject:[NSString stringWithFormat:@"%@%@", [self indentationString], string]]; +} + +- (void)appendFormat:(NSString *)format, ... { + va_list args; + va_start(args, format); + NSString *string = [[NSString alloc] initWithFormat:format arguments:args]; + va_end(args); + + [self appendString:string]; +} + +- (NSString *)output { + return [self.lines componentsJoinedByString:@"\n"]; +} + +@end diff --git a/Apptentive/Apptentive/Misc/ApptentiveJSONSerialization.m b/Apptentive/Apptentive/Misc/ApptentiveJSONSerialization.m index 9bf30829f..2ff8185bc 100644 --- a/Apptentive/Apptentive/Misc/ApptentiveJSONSerialization.m +++ b/Apptentive/Apptentive/Misc/ApptentiveJSONSerialization.m @@ -26,8 +26,8 @@ + (NSData *)dataWithJSONObject:(id)obj options:(NSJSONWritingOptions)opt error:( *error = [NSError errorWithDomain:ApptentiveErrorDomain code:ApptentiveJSONSerializationErrorCode userInfo:@{ NSLocalizedFailureReasonErrorKey: @"JSON object is malformed." }]; } - ApptentiveLogError(@"Exception when encoding JSON: %@.", exception.reason); - ApptentiveLogError(@"Attempted to encode %@.", obj); + ApptentiveLogError(ApptentiveLogTagUtility, @"Exception when encoding JSON (%@).", exception.reason); + ApptentiveLogError(ApptentiveLogTagUtility, @"Attempted to encode %@.", obj); } return jsonData; @@ -36,7 +36,7 @@ + (NSData *)dataWithJSONObject:(id)obj options:(NSJSONWritingOptions)opt error:( *error = [NSError errorWithDomain:ApptentiveErrorDomain code:ApptentiveJSONDeserializationErrorCode userInfo:@{ NSLocalizedFailureReasonErrorKey: @"Object is not valid JSON object." }]; } - ApptentiveLogError(@"Attempting to create JSON data from an invalid JSON object (%@).", obj); + ApptentiveLogError(ApptentiveLogTagUtility, @"Attempting to create JSON data from an invalid JSON object (%@).", obj); return nil; } @@ -50,7 +50,7 @@ + (id)JSONObjectWithData:(NSData *)data error:(NSError *__autoreleasing *)error *error = [NSError errorWithDomain:ApptentiveErrorDomain code:ApptentiveJSONDeserializationErrorCode userInfo:@{ NSLocalizedFailureReasonErrorKey: @"JSON data is nil." }]; } - ApptentiveLogError(@"Attempting to decode nil JSON data."); + ApptentiveLogError(ApptentiveLogTagUtility, @"Attempting to decode nil JSON data."); return nil; } @@ -64,8 +64,8 @@ + (id)JSONObjectWithData:(NSData *)data error:(NSError *__autoreleasing *)error *error = [NSError errorWithDomain:ApptentiveErrorDomain code:ApptentiveJSONSerializationErrorCode userInfo:@{ NSLocalizedFailureReasonErrorKey: @"JSON data is malformed." }]; } - ApptentiveLogError(@"Exception when decoding JSON: %@", exception.reason); - ApptentiveLogError(@"Attempted to decode “%@”", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]); + ApptentiveLogError(ApptentiveLogTagUtility, @"Exception when decoding JSON (%@).", exception.reason); + ApptentiveLogError(ApptentiveLogTagUtility, @"Attempted to decode: %@", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]); return nil; } diff --git a/Apptentive/Apptentive/Misc/ApptentiveJWT.m b/Apptentive/Apptentive/Misc/ApptentiveJWT.m index 157141d29..7c3b14f09 100644 --- a/Apptentive/Apptentive/Misc/ApptentiveJWT.m +++ b/Apptentive/Apptentive/Misc/ApptentiveJWT.m @@ -38,7 +38,7 @@ NSError *jsonError = nil; id dictionary = [ApptentiveJSONSerialization JSONObjectWithData:data error:&jsonError]; if (jsonError != nil) { - ApptentiveLogError(@"Unable to parse json string: '%@'", error); + ApptentiveLogError(ApptentiveLogTagUtility, @"Unable to parse json string: '%@'", jsonError); if (error) { *error = _createError([jsonError localizedDescription]); } @@ -62,16 +62,16 @@ - (nullable instancetype)initWithAlg:(NSString *)alg type:(NSString *)type paylo self = [super init]; if (self) { if (alg.length == 0) { - ApptentiveLogError(@"Unable to create JWT: 'alg' is nil or empty"); + ApptentiveLogError(ApptentiveLogTagUtility, @"Unable to create JWT: 'alg' is nil or empty."); return nil; } if (type.length == 0) { - ApptentiveLogError(@"Unable to create JWT: 'type' is nil or empty"); + ApptentiveLogError(ApptentiveLogTagUtility, @"Unable to create JWT: 'type' is nil or empty."); return nil; } if (payload == nil) { - ApptentiveLogError(@"Unable to create JWT: 'payload' is nil"); + ApptentiveLogError(ApptentiveLogTagUtility, @"Unable to create JWT: 'payload' is nil."); return nil; } diff --git a/Apptentive/Apptentive/Misc/ApptentiveLog.h b/Apptentive/Apptentive/Misc/ApptentiveLog.h index 7d1d2a469..ea7d0499b 100644 --- a/Apptentive/Apptentive/Misc/ApptentiveLog.h +++ b/Apptentive/Apptentive/Misc/ApptentiveLog.h @@ -18,14 +18,19 @@ NS_ASSUME_NONNULL_BEGIN #define ApptentiveLogTagUtility [ApptentiveLogTag utilityTag] #define ApptentiveLogTagStorage [ApptentiveLogTag storageTag] #define ApptentiveLogTagMonitor [ApptentiveLogTag logMonitorTag] +#define ApptentiveLogTagCriteria [ApptentiveLogTag criteriaTag] +#define ApptentiveLogTagInteractions [ApptentiveLogTag interactionsTag] +#define ApptentiveLogTagPush [ApptentiveLogTag pushTag] +#define ApptentiveLogTagMessages [ApptentiveLogTag messagesTag] extern ApptentiveLogLevel ApptentiveLogGetLevel(void); extern void ApptentiveLogSetLevel(ApptentiveLogLevel level); extern BOOL ApptentiveCanLogLevel(ApptentiveLogLevel level); extern NSString *NSStringFromApptentiveLogLevel(ApptentiveLogLevel level); extern ApptentiveLogLevel ApptentiveLogLevelFromString(NSString *level); - -typedef void (^ApptentiveLoggerCallback)(ApptentiveLogLevel level, NSString *message); +extern NSObject * _Nullable ApptentiveHideIfSanitized(NSObject * _Nullable value); +NSDictionary *ApptentiveHideKeysIfSanitized(NSDictionary *dictionary, NSArray *sensitiveKeys); +extern void setShouldSanitizeApptentiveLogMessages(BOOL shouldSanitize); void ApptentiveLogCrit(id arg, ...); void ApptentiveLogError(id arg, ...); @@ -34,6 +39,7 @@ void ApptentiveLogInfo(id arg, ...); void ApptentiveLogDebug(id arg, ...); void ApptentiveLogVerbose(id arg, ...); -void ApptentiveSetLoggerCallback(_Nullable ApptentiveLoggerCallback callback); +void ApptentiveStartLogMonitor(NSString *logDir); +NSArray * _Nullable ApptentiveListLogFiles(void); NS_ASSUME_NONNULL_END diff --git a/Apptentive/Apptentive/Misc/ApptentiveLog.m b/Apptentive/Apptentive/Misc/ApptentiveLog.m index 69a9a463c..8dfdae9f6 100644 --- a/Apptentive/Apptentive/Misc/ApptentiveLog.m +++ b/Apptentive/Apptentive/Misc/ApptentiveLog.m @@ -8,11 +8,15 @@ #import "ApptentiveLog.h" #import "ApptentiveDispatchQueue.h" +#import "ApptentiveAsyncLogWriter.h" NS_ASSUME_NONNULL_BEGIN +static const NSUInteger kLogHistorySize = 2; + static ApptentiveLogLevel _logLevel = ApptentiveLogLevelInfo; -static ApptentiveLoggerCallback _logCallback; +static ApptentiveAsyncLogWriter * _logWriter; +static BOOL _shouldSanitizeLogMessages; static const char *_Nonnull _logLevelNameLookup[] = { "?", // ApptentiveLogLevelUndefined @@ -36,29 +40,29 @@ inline static BOOL shouldLogLevel(ApptentiveLogLevel logLevel) { static void _ApptentiveLogHelper(ApptentiveLogLevel level, id arg, va_list ap) { ApptentiveLogTag *tag = [arg isKindOfClass:[ApptentiveLogTag class]] ? arg : nil; - if (shouldLogLevel(level) && (tag == nil || tag.enabled)) { - if (tag != nil) { - arg = va_arg(ap, ApptentiveLogTag *); - } + if (tag != nil) { + arg = va_arg(ap, ApptentiveLogTag *); + } - NSString *format = arg; - NSString *threadName = ApptentiveGetCurrentThreadName(); - NSString *message = [[NSString alloc] initWithFormat:format arguments:ap]; + NSString *format = arg; + NSString *threadName = ApptentiveGetCurrentThreadName(); + NSString *message = [[NSString alloc] initWithFormat:format arguments:ap]; - NSMutableString *fullMessage = [[NSMutableString alloc] initWithFormat:@"%s/Apptentive: ", _logLevelNameLookup[level]]; - if (threadName != nil) { - [fullMessage appendFormat:@"[%@] ", threadName]; - } - if (tag != nil) { - [fullMessage appendFormat:@"[%@] ", tag.name]; - } - [fullMessage appendString:message]; + NSMutableString *fullMessage = [[NSMutableString alloc] initWithFormat:@"%s/Apptentive: ", _logLevelNameLookup[level]]; + if (threadName != nil) { + [fullMessage appendFormat:@"[%@] ", threadName]; + } + if (tag != nil) { + [fullMessage appendFormat:@"[%@] ", tag.name]; + } + [fullMessage appendString:message]; + if (shouldLogLevel(level)) { NSLog(@"%@", fullMessage); + } - if (_logCallback) { - _logCallback(level, fullMessage); - } + if (_logWriter) { + [_logWriter logMessage:fullMessage]; } } @@ -104,10 +108,6 @@ void ApptentiveLogVerbose(id arg, ...) { va_end(ap); } -void ApptentiveSetLoggerCallback(_Nullable ApptentiveLoggerCallback callback) { - _logCallback = callback; -} - ApptentiveLogLevel ApptentiveLogGetLevel(void) { return _logLevel; } @@ -164,4 +164,42 @@ ApptentiveLogLevel ApptentiveLogLevelFromString(NSString *level) { return ApptentiveLogLevelUndefined; } +NSObject * _Nullable ApptentiveHideIfSanitized(NSObject * _Nullable value) { + return value != nil && _shouldSanitizeLogMessages ? @"" : value; +} + +NSDictionary *ApptentiveHideKeysIfSanitized(NSDictionary *dictionary, NSArray *sensitiveKeys) { + if (dictionary == nil || !_shouldSanitizeLogMessages || ![dictionary isKindOfClass:[NSDictionary class]]) { + return dictionary; + } + + NSMutableDictionary *result = [NSMutableDictionary dictionaryWithCapacity:dictionary.count]; + for (NSString *key in dictionary) { + NSObject *value = dictionary[key]; + + if ([value isKindOfClass:[NSDictionary class]]) { + value = ApptentiveHideKeysIfSanitized((NSDictionary *)value, ((NSDictionary *)value).allKeys); + } else if ([sensitiveKeys containsObject:key]) { + value = @""; + } + + [result setObject:value forKey:key]; + } + + return result; +} + + +void setShouldSanitizeApptentiveLogMessages(BOOL shouldSanitize) { + _shouldSanitizeLogMessages = shouldSanitize; +} + +void ApptentiveStartLogMonitor(NSString *logDir) { + _logWriter = [[ApptentiveAsyncLogWriter alloc] initWithDestDir:logDir historySize:kLogHistorySize]; +} + +NSArray *ApptentiveListLogFiles(void) { + return [_logWriter listLogFiles]; +} + NS_ASSUME_NONNULL_END diff --git a/Apptentive/Apptentive/Misc/ApptentiveLogFileWriteTask.h b/Apptentive/Apptentive/Misc/ApptentiveLogFileWriteTask.h new file mode 100644 index 000000000..165c861c5 --- /dev/null +++ b/Apptentive/Apptentive/Misc/ApptentiveLogFileWriteTask.h @@ -0,0 +1,21 @@ +// +// ApptentiveLogFileWriteTask.h +// Apptentive +// +// Created by Alex Lementuev on 2/22/18. +// Copyright © 2018 Apptentive, Inc. All rights reserved. +// + +#import + +#import "ApptentiveDispatchTask.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface ApptentiveLogFileWriteTask : ApptentiveDispatchTask + +- (instancetype)initWithFile:(NSString *)file buffer:(NSMutableArray *)buffer; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Apptentive/Apptentive/Misc/ApptentiveLogFileWriteTask.m b/Apptentive/Apptentive/Misc/ApptentiveLogFileWriteTask.m new file mode 100644 index 000000000..85ce02702 --- /dev/null +++ b/Apptentive/Apptentive/Misc/ApptentiveLogFileWriteTask.m @@ -0,0 +1,98 @@ +// +// ApptentiveLogFileWriteTask.m +// Apptentive +// +// Created by Alex Lementuev on 2/22/18. +// Copyright © 2018 Apptentive, Inc. All rights reserved. +// + +#import "ApptentiveLogFileWriteTask.h" +#import "ApptentiveDefines.h" +#import "ApptentiveFileUtilities.h" + +@interface ApptentiveLogFileWriteTask () + +@property (nonatomic, strong) NSString *file; +@property (nonatomic, strong) NSMutableArray *buffer; +@property (nonatomic, strong) NSMutableArray *queuedMessagesTemp; +@property (nonatomic, strong) NSFileHandle *fileHandle; + +@end + +@implementation ApptentiveLogFileWriteTask + +- (instancetype)initWithFile:(NSString *)file buffer:(NSMutableArray *)buffer { + APPTENTIVE_CHECK_INIT_NOT_EMPTY_ARG(file) + APPTENTIVE_CHECK_INIT_NOT_NIL_ARG(buffer) + self = [super init]; + if (self) { + _file = file; + _fileHandle = [[self class] openFileWrite:file]; + if (_fileHandle == nil) { + ApptentiveLogCrit(ApptentiveLogTagUtility, @"Unable to start log writing task"); + return nil; + } + + _buffer = buffer; + _queuedMessagesTemp = [NSMutableArray new]; + } + return self; +} + +- (void)dealloc { + [self.fileHandle synchronizeFile]; + [self.fileHandle closeFile]; +} + +- (void)execute { + // we don't want to acquire the mutex for too long so just copy pending messages + // to the temp list which would be used in a blocking IO + @synchronized (_buffer) { + [_queuedMessagesTemp addObjectsFromArray:_buffer]; + [_buffer removeAllObjects]; + } + + // write to a file + @autoreleasepool { + NSMutableString *text = [NSMutableString new]; + for (NSString *line in _queuedMessagesTemp) { + [text appendString:line]; + [text appendString:@"\n"]; + } + NSData *data = [text dataUsingEncoding:NSUTF8StringEncoding]; + [self.fileHandle writeData:data]; + [self.fileHandle synchronizeFile]; + } + + [_queuedMessagesTemp removeAllObjects]; +} + +#pragma mark - +#pragma mark File Handler + ++ (NSFileHandle *)openFileWrite:(NSString *)path { + // create output directory if it doesn't exist + NSString *dirName = [path stringByDeletingLastPathComponent]; + NSError *error; + BOOL directoryCreated = [[NSFileManager defaultManager] createDirectoryAtPath:dirName withIntermediateDirectories:YES attributes:nil error:&error]; + if (!directoryCreated) { + ApptentiveLogCrit(ApptentiveLogTagUtility, @"Unable to create log output directory '%@' (%@)", dirName, error); + return nil; + } + + // delete an old file if any + BOOL oldFileDeleted = [ApptentiveFileUtilities deleteFileAtPath:path]; + ApptentiveAssertFalse(oldFileDeleted, @"Duplicate log file existed: %@", path); + + // create a new log file + BOOL fileCreated = [[NSFileManager defaultManager] createFileAtPath:path contents:nil attributes:nil]; + if (!fileCreated) { + ApptentiveLogCrit(ApptentiveLogTagUtility, @"Unable to create log file '%@'", path); + return nil; + } + + // open a file handle for writing + return [NSFileHandle fileHandleForWritingAtPath:path]; +} + +@end diff --git a/Apptentive/Apptentive/Misc/ApptentiveLogMonitor.h b/Apptentive/Apptentive/Misc/ApptentiveLogMonitor.h index 8fff22f13..fd7fa5950 100644 --- a/Apptentive/Apptentive/Misc/ApptentiveLogMonitor.h +++ b/Apptentive/Apptentive/Misc/ApptentiveLogMonitor.h @@ -10,28 +10,12 @@ NS_ASSUME_NONNULL_BEGIN - -@interface ApptentiveLogMonitorConfigration : NSObject - -/** Email recipients for the log email */ -@property (nonatomic, strong) NSArray *emailRecipients; - -/** New log level */ -@property (nonatomic, assign) ApptentiveLogLevel logLevel; - -/** True if configuration was restored from the persistent storage */ -@property (nonatomic, readonly, getter=isRestored) BOOL restored; - -@end - +@class ApptentiveDispatchQueue; @interface ApptentiveLogMonitor : NSObject -+ (BOOL)tryInitializeWithBaseURL:(NSURL *)baseURL appKey:(NSString *)appKey signature:(NSString *)appSignature; - -+ (instancetype)sharedInstance; - -- (void)resume; ++ (void)startSessionWithBaseURL:(NSURL *)baseURL appKey:(NSString *)appKey signature:(NSString *)appSignature queue:(ApptentiveDispatchQueue *)queue; ++ (BOOL)resumeSession; @end diff --git a/Apptentive/Apptentive/Misc/ApptentiveLogMonitor.m b/Apptentive/Apptentive/Misc/ApptentiveLogMonitor.m index 48cdca622..fd22e3b3b 100644 --- a/Apptentive/Apptentive/Misc/ApptentiveLogMonitor.m +++ b/Apptentive/Apptentive/Misc/ApptentiveLogMonitor.m @@ -6,255 +6,148 @@ // Copyright © 2017 Apptentive, Inc. All rights reserved. // -#import "Apptentive.h" #import "ApptentiveJSONSerialization.h" #import "ApptentiveJSONSerialization.h" #import "ApptentiveJWT.h" #import "ApptentiveLogMonitor.h" -#import "ApptentiveLogWriter.h" +#import "ApptentiveLogMonitorSession.h" #import "ApptentiveSafeCollections.h" #import "ApptentiveUtilities.h" +#import "ApptentiveFileUtilities.h" +#import "ApptentiveDispatchQueue.h" +#import "ApptentiveUtilities.h" -#import // These constants are defined in Apptentive-Private.h but the compiler is "unhappy" if // this file is imported here (TODO: figure it out) +extern NSString *ApptentiveLocalizedString(NSString *key, NSString *comment); extern NSNotificationName _Nonnull const ApptentiveManifestRawDataDidReceiveNotification; extern NSString *_Nonnull const ApptentiveManifestRawDataKey; -extern NSString *ApptentiveLocalizedString(NSString *key, NSString *comment); - -static NSString *const KeyEmailRecipients = @"emailRecipients"; -static NSString *const KeyLogLevel = @"logLevel"; - -static NSString *const ConfigurationStorageFile = @"apptentive-log-monitor.cfg"; static NSString *const LogFileName = @"apptentive-log.txt"; -static NSString *const ManifestFileName = @"apptentive-manifest.txt"; - static NSString *const DebugTextHeader = @"com.apptentive.debug:"; -static ApptentiveLogMonitor *_sharedInstance; - - -@interface ApptentiveLogMonitorConfigration () - -@end +static ApptentiveLogMonitorSession * _currentSession; +@implementation ApptentiveLogMonitor -@implementation ApptentiveLogMonitorConfigration +#pragma mark - +#pragma mark Session -- (instancetype)init { - self = [super init]; - if (self) { - _emailRecipients = @[@"support@apptentive.com"]; - _logLevel = ApptentiveLogLevelVerbose; ++ (void)startSessionWithBaseURL:(NSURL *)baseURL appKey:(NSString *)appKey signature:(NSString *)appSignature queue:(nonnull ApptentiveDispatchQueue *)queue { + if (baseURL == nil) { + ApptentiveLogError(ApptentiveLogTagMonitor, @"Unable to initialize log monitor: base URL is nil."); + return; } - return self; -} -- (void)encodeWithCoder:(NSCoder *)coder { - [coder encodeObject:[_emailRecipients componentsJoinedByString:@","] forKey:KeyEmailRecipients]; - [coder encodeInt:(int)_logLevel forKey:KeyLogLevel]; -} - -- (nullable instancetype)initWithCoder:(NSCoder *)decoder { - self = [super init]; - if (self) { - _emailRecipients = [[decoder decodeObjectForKey:KeyEmailRecipients] componentsSeparatedByString:@","]; - _logLevel = (ApptentiveLogLevel)[decoder decodeIntForKey:KeyLogLevel]; - _restored = YES; + if (appKey.length == 0) { + ApptentiveLogError(ApptentiveLogTagMonitor, @"Unable to initialize log monitor: app key is nil or empty."); + return; } - return self; -} -- (NSString *)description { - return [NSString stringWithFormat:@"logLevel=%@ recipients=%@ restored=%@", NSStringFromApptentiveLogLevel(_logLevel), [_emailRecipients componentsJoinedByString:@","], _restored ? @"YES" : @"NO"]; -} - -@end - - -@interface ApptentiveLogMonitor () - -@property (nonatomic, readonly) NSURL *baseURL; -@property (nonatomic, readonly) NSString *accessToken; -@property (nonatomic, readonly) ApptentiveLogLevel logLevel; -@property (nonatomic, readonly) ApptentiveLogLevel originalLogLevel; -@property (nonatomic, readonly) NSArray *emailRecipients; -@property (nonatomic, readonly, getter=isSessionRestored) BOOL sessionRestored; -@property (nonatomic, readonly) ApptentiveLogWriter *logWriter; -@property (nonatomic, strong) UIWindow *mailComposeControllerWindow; - -@end - - -@implementation ApptentiveLogMonitor - -- (instancetype)initWithBaseURL:(NSURL *)baseURL configuration:(ApptentiveLogMonitorConfigration *)configuration { - self = [super init]; - if (self) { - _baseURL = baseURL; - _originalLogLevel = ApptentiveLogGetLevel(); - _logLevel = configuration.logLevel; - _emailRecipients = configuration.emailRecipients; - _sessionRestored = configuration.isRestored; + if (appSignature.length == 0) { + ApptentiveLogError(ApptentiveLogTagMonitor, @"Unable to initialize log monitor: app signature is nil or empty."); + return; } - return self; -} - -#pragma mark - -#pragma mark Life cycle - -- (void)start { - ApptentiveLogSetLevel(_logLevel); - ApptentiveLogInfo(ApptentiveLogTagMonitor, @"Override log level %@ -> %@", NSStringFromApptentiveLogLevel(_originalLogLevel), NSStringFromApptentiveLogLevel(_logLevel)); - - NSString *logFilePath = [ApptentiveLogMonitor logFilePath]; - if (!_sessionRestored) { - [ApptentiveUtilities deleteFileAtPath:logFilePath]; + + if (queue == NULL) { + ApptentiveLogError(ApptentiveLogTagMonitor, @"Unable to initialize log monitor: no dispatch queue."); + return; } - ApptentiveLogWriter *logWriter = [[ApptentiveLogWriter alloc] initWithPath:logFilePath]; - ApptentiveSetLoggerCallback(^(ApptentiveLogLevel level, NSString *message) { - [logWriter appendMessage:message]; - }); - [logWriter start]; - - _logWriter = logWriter; - - // dispatch on the main thread to avoid UI-issues - dispatch_async(dispatch_get_main_queue(), ^{ - [self showReportUI]; - }); - - ApptentiveLogInfo(ApptentiveLogTagMonitor, @"Troubleshooting mode enabled"); -} - -- (void)resume { - [self showReportUI]; + // all the initialization should happen on the dedicated queue + [queue dispatchAsync:^{ + @try { + // register observers + [self registerObservers]; + + // attempt to start a session + [self startSessionWithBaseURL:baseURL appKey:appKey signature:appSignature]; + } @catch (NSException *e) { + ApptentiveLogCrit(ApptentiveLogTagMonitor, @"Exception while starting log monitor session (%@)", e); + } + }]; +} + ++ (void)startSessionWithBaseURL:(NSURL *)baseURL appKey:(NSString *)appKey signature:(NSString *)appSignature { + ApptentiveLogMonitorSession *session = [ApptentiveLogMonitorSessionIO readSessionFromPersistentStorage]; + if (session != nil) { + ApptentiveLogInfo(ApptentiveLogTagMonitor, @"Previous Apptentive Log Monitor session loaded from persistent storage: %@", session); + [self startSession:session]; + } else { + // attempt to read access token from a clipboard + NSString *accessToken = [self readAccessTokenFromClipboard]; + if (accessToken == nil) { + ApptentiveLogVerbose(ApptentiveLogTagMonitor, @"No access token found in clipboard"); + return; + } + + // clear pastboard text + [[UIPasteboard generalPasteboard] setString:@""]; + + // send token verification request + [self verifyAccessToken:accessToken baseURL:baseURL appKey:appKey signature:appSignature completionHandler:^(BOOL sessionValid, NSError * _Nullable error) { + if (!sessionValid) { + ApptentiveLogVerbose(ApptentiveLogTagMonitor, @"Unable to start Apptentive Log Monitor: the access token was rejected on the server (%@)", accessToken); + return; + } + + ApptentiveLogMonitorSession *session = [ApptentiveLogMonitorSessionIO readSessionFromJWT:accessToken]; + if (session == nil) { + ApptentiveLogVerbose(ApptentiveLogTagMonitor, @"Unable to start Apptentive Log Monitor: failed to parse the access token (%s)", accessToken); + return; + } + + ApptentiveLogInfo(ApptentiveLogTagMonitor, @"Read log monitor configuration from clipboard: %@", session); + + // save session + [ApptentiveLogMonitorSessionIO writeSessionToPersistentStorage:session]; + + // start session + [self startSession:session]; + }]; + } } -- (void)stop { - ApptentiveLogInfo(ApptentiveLogTagMonitor, @"Troubleshooting mode disabled"); - - // restore the original log level - ApptentiveLogSetLevel(_originalLogLevel); - - // remove log callbacks - ApptentiveSetLoggerCallback(nil); - - // stop writting logs - [_logWriter stop]; - - // delete store configuration - [ApptentiveLogMonitor clearConfiguration]; ++ (void)startSession:(nonnull ApptentiveLogMonitorSession *)session { + ApptentiveAssertNil(_currentSession, @"Attempted to start a session while previous session is still active"); + _currentSession = session; + [_currentSession start]; } -#pragma mark - -#pragma mark User Interactions - -- (void)showReportUI { - // create a custom window to show UI on top of everything - UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; - - // create alert controller with "Send", "Continue" and "Discard" actions - UIAlertController *alertController = [UIAlertController alertControllerWithTitle:ApptentiveLocalizedString(@"Apptentive", @"Apptentive") message:ApptentiveLocalizedString(@"Troubleshooting mode", @"Troubleshooting mode") preferredStyle:UIAlertControllerStyleActionSheet]; - [alertController addAction:[UIAlertAction actionWithTitle:ApptentiveLocalizedString(@"Send Report", @"Send Report") - style:UIAlertActionStyleDefault - handler:^(UIAlertAction *_Nonnull action) { - window.hidden = YES; - __weak id weakSelf = self; - self->_logWriter.finishCallback = ^(ApptentiveLogWriter *writer) { - dispatch_async(dispatch_get_main_queue(), ^{ - [weakSelf sendReportWithAttachedFiles:@[writer.path, [ApptentiveLogMonitor manifestFilePath]]]; - }); - }; - [self stop]; - }]]; - [alertController addAction:[UIAlertAction actionWithTitle:ApptentiveLocalizedString(@"Continue", @"Continue") - style:UIAlertActionStyleCancel - handler:^(UIAlertAction *_Nonnull action) { - window.hidden = YES; - }]]; - [alertController addAction:[UIAlertAction actionWithTitle:ApptentiveLocalizedString(@"Discard Report", @"Discard Report") - style:UIAlertActionStyleDestructive - handler:^(UIAlertAction *_Nonnull action) { - window.hidden = YES; - [self stop]; - }]]; - - window.rootViewController = [[UIViewController alloc] init]; - window.windowLevel = UIWindowLevelAlert + 1; - window.hidden = NO; // don't use makeKeyAndVisible since we don't have any knowledge about the host app's UI - [window.rootViewController presentViewController:alertController animated:YES completion:nil]; ++ (BOOL)resumeSession { + if (_currentSession != nil) { + [_currentSession resume]; + return YES; + } + + return NO; } #pragma mark - -#pragma mark Static initialization - -+ (BOOL)tryInitializeWithBaseURL:(NSURL *)baseURL appKey:(NSString *)appKey signature:(NSString *)appSignature { - if (baseURL == nil) { - ApptentiveLogError(ApptentiveLogTagMonitor, @"Unable to initialize log monitor: base URL is nil"); - return NO; - } - - if (appKey.length == 0) { - ApptentiveLogError(ApptentiveLogTagMonitor, @"Unable to initialize log monitor: app key is nil or empty"); - return NO; - } - - if (appSignature.length == 0) { - ApptentiveLogError(ApptentiveLogTagMonitor, @"Unable to initialize log monitor: app signature is nil or empty"); - return NO; - } +#pragma mark Observers ++ (void)registerObservers { // Store raw manifest data each time the update is received - NSString *manifestPath = [ApptentiveLogMonitor manifestFilePath]; + NSString *manifestPath = [ApptentiveLogMonitorSession manifestFilePath]; [[NSNotificationCenter defaultCenter] addObserverForName:ApptentiveManifestRawDataDidReceiveNotification object:nil - queue:nil + queue:NSOperationQueue.currentQueue // dispatch on the same queue usingBlock:^(NSNotification *_Nonnull note) { - NSData *data = note.userInfo[ApptentiveManifestRawDataKey]; - ApptentiveAssertNotNil(data, @"Missing manifest data"); - [data writeToFile:manifestPath atomically:YES]; + NSData *data = note.userInfo[ApptentiveManifestRawDataKey]; + ApptentiveAssertNotNil(data, @"Missing manifest data"); + [data writeToFile:manifestPath atomically:YES]; + }]; + + // clean stored session when it's over + [[NSNotificationCenter defaultCenter] addObserverForName:ApptentiveLogMonitorSessionDidStop + object:nil + queue:NSOperationQueue.currentQueue // dispatch on the same queue + usingBlock:^(NSNotification * _Nonnull note) { + ApptentiveAssertNotNil(_currentSession, @"Current session already stopped"); + _currentSession = nil; + [ApptentiveLogMonitorSessionIO clearCurrentSession]; }]; - - @try { - NSString *storagePath = [self configurationStoragePath]; - ApptentiveLogMonitorConfigration *configuration = [self readConfigurationFromStoragePath:storagePath]; - if (configuration != nil) { - ApptentiveLogInfo(ApptentiveLogTagMonitor, @"Read log monitor configuration from persistent storage: %@", configuration); - } else { - NSString *accessToken = [self readAccessTokenFromClipboard]; - if (![self syncVerifyAccessToken:accessToken baseURL:baseURL appKey:appKey signature:appSignature]) { - return NO; - } - - configuration = [self readConfigurationFromToken:accessToken]; - if (configuration != nil) { - ApptentiveLogInfo(ApptentiveLogTagMonitor, @"Read log monitor configuration from clipboard: %@", configuration); - // save configuration - [self writeConfiguration:configuration toStoragePath:storagePath]; - - // clear pastboard text - [[UIPasteboard generalPasteboard] setString:@""]; - } - } - - if (configuration != nil) { - _sharedInstance = [[ApptentiveLogMonitor alloc] initWithBaseURL:baseURL configuration:configuration]; - [_sharedInstance start]; - return YES; - } - } @catch (NSException *e) { - ApptentiveLogError(ApptentiveLogTagMonitor, @"Exception while initializing log monitor: %@", e); - } - - return NO; -} - -+ (instancetype)sharedInstance { - return _sharedInstance; } #pragma mark - @@ -276,14 +169,9 @@ + (nullable NSString *)readAccessTokenFromClipboard { return [text substringFromIndex:DebugTextHeader.length]; } -+ (BOOL)syncVerifyAccessToken:(NSString *)accessToken baseURL:(NSURL *)baseURL appKey:(NSString *)appKey signature:(NSString *)appSignature { - if (accessToken.length == 0) { - return NO; - } - ++ (void)verifyAccessToken:(NSString *)accessToken baseURL:(NSURL *)baseURL appKey:(NSString *)appKey signature:(NSString *)appSignature completionHandler:(void(^)(BOOL success, NSError *error))completionHandler { ApptentiveLogInfo(ApptentiveLogTagMonitor, @"Starting access token verification: %@", accessToken); - NSDate *startDate = [NSDate new]; NSData *body = [ApptentiveJSONSerialization dataWithJSONObject:@{ @"debug_token": accessToken } options:0 error:nil]; NSDictionary *headers = @{ @@ -296,22 +184,6 @@ + (BOOL)syncVerifyAccessToken:(NSString *)accessToken baseURL:(NSURL *)baseURL a }; NSURL *URL = [NSURL URLWithString:@"/debug_token/verify" relativeToURL:baseURL]; - NSDictionary *json = [self loadJsonFromURL:URL body:body headers:headers]; - NSTimeInterval duration = -[startDate timeIntervalSinceNow]; - - if (json == nil) { - ApptentiveLogError(ApptentiveLogTagMonitor, @"Access token verification failed: invalid server response (took %g sec)", duration); - - return NO; - } - - BOOL valid = ApptentiveDictionaryGetBool(json, @"valid"); - ApptentiveLogInfo(ApptentiveLogTagMonitor, @"Access token is %@ (took %g sec)", valid ? @"valid" : @"invalid", duration); - - return valid; -} - -+ (NSDictionary *)loadJsonFromURL:(NSURL *)URL body:(NSData *)body headers:(NSDictionary *)headers { NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL]; for (NSString *key in headers) { [request setValue:headers[key] forHTTPHeaderField:key]; @@ -319,162 +191,35 @@ + (NSDictionary *)loadJsonFromURL:(NSURL *)URL body:(NSData *)body headers:(NSDi request.HTTPBody = body; request.HTTPMethod = @"POST"; - NSURLResponse *response; - NSError *requestError; - -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&requestError]; -#pragma clang diagnostic pop - - if (requestError != nil) { - ApptentiveLogError(@"Unable to load json from URL: %@", requestError); - return nil; - } - - NSError *jsonError; - id object = [ApptentiveJSONSerialization JSONObjectWithData:data error:&jsonError]; - if (jsonError != nil) { - ApptentiveLogError(@"Unable to parse json from URL: %@", requestError); - return nil; - } - - if (![object isKindOfClass:[NSDictionary class]]) { - ApptentiveLogError(@"Unexpected json object: %@", object); - return nil; - } - - return object; -} - -#pragma mark - -#pragma mark Configuration - -+ (nullable ApptentiveLogMonitorConfigration *)readConfigurationFromStoragePath:(NSString *)path { - return [NSKeyedUnarchiver unarchiveObjectWithFile:path]; -} - -+ (void)clearConfiguration { - NSString *filepath = [self configurationStoragePath]; - [ApptentiveUtilities deleteFileAtPath:filepath]; -} - -+ (void)writeConfiguration:(ApptentiveLogMonitorConfigration *)configuration toStoragePath:(NSString *)path { - [NSKeyedArchiver archiveRootObject:configuration toFile:path]; -} - -+ (nullable ApptentiveLogMonitorConfigration *)readConfigurationFromToken:(NSString *)token { - NSError *jwtError; - ApptentiveJWT *jwt = [ApptentiveJWT JWTWithContentOfString:token error:&jwtError]; - if (jwtError != nil) { - ApptentiveLogError(ApptentiveLogTagMonitor, @"JWT parsing error: %@", jwtError); - return nil; - } - - ApptentiveLogMonitorConfigration *configuration = [[ApptentiveLogMonitorConfigration alloc] init]; - - NSString *logLevelStr = ApptentiveDictionaryGetString(jwt.payload, @"level"); - ApptentiveLogLevel logLevel = ApptentiveLogLevelFromString(logLevelStr); - if (logLevel != ApptentiveLogLevelUndefined) { - configuration.logLevel = logLevel; - } - - NSArray *emailRecepients = ApptentiveDictionaryGetArray(jwt.payload, @"recipients"); - if (emailRecepients != nil) { - configuration.emailRecipients = emailRecepients; - } - - return configuration; -} - -#pragma mark - -#pragma mark Report - -- (void)sendReportWithAttachedFiles:(NSArray *)files { - if (![MFMailComposeViewController canSendMail]) { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:ApptentiveLocalizedString(@"Apptentive Log Monitor", @"Apptentive Log Monitor") message:ApptentiveLocalizedString(@"Unable to send email", @"Unable to send email") delegate:nil cancelButtonTitle:ApptentiveLocalizedString(@"OK", @"OK") otherButtonTitles:nil]; - [alertView show]; -#pragma clang diagnostic pop - - return; - } - - NSDictionary *bundleInfo = [[NSBundle mainBundle] infoDictionary]; - - // collecting system info - NSMutableString *messageBody = [NSMutableString new]; - [messageBody appendString:@"This email may contain sensitive content.\n Please review before sending.\n\n"]; - [messageBody appendFormat:@"App Bundle Identifier: %@\n", [NSBundle mainBundle].bundleIdentifier]; - [messageBody appendFormat:@"App Version: %@\n", [bundleInfo objectForKey:@"CFBundleShortVersionString"]]; - [messageBody appendFormat:@"App Build: %@\n", [bundleInfo objectForKey:@"CFBundleVersion"]]; - [messageBody appendFormat:@"Apptentive SDK: %@\n", kApptentiveVersionString]; - [messageBody appendFormat:@"Device Model: %@\n", [ApptentiveUtilities deviceMachine]]; - [messageBody appendFormat:@"iOS Version: %@\n", [UIDevice currentDevice].systemVersion]; - [messageBody appendFormat:@"Locale: %@", [NSLocale currentLocale].localeIdentifier]; - - NSString *emailTitle = [NSString stringWithFormat:@"%@ (iOS)", [NSBundle mainBundle].infoDictionary[@"CFBundleName"]]; - - MFMailComposeViewController *mc = [[MFMailComposeViewController alloc] init]; - mc.mailComposeDelegate = self; - [mc setSubject:emailTitle]; - [mc setMessageBody:messageBody isHTML:NO]; - [mc setToRecipients:_emailRecipients]; - - // Get the resource path and read the file using NSData - for (NSString *path in files) { - NSString *filename = [path lastPathComponent]; - NSData *fileData = [NSData dataWithContentsOfFile:path]; - if (fileData.length == 0) { - ApptentiveLogError(ApptentiveLogTagMonitor, @"Attachment file does not exist or empty: %@", path); - continue; + NSOperationQueue *delegateQueue = NSOperationQueue.currentQueue; // this is a hack: we dispatch the enclosing call on ApptentiveGCDDispatchQueue which is based on NSOperation queue + ApptentiveAssertNotNil(delegateQueue, @"Delegate queue is nil"); + NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:nil delegateQueue:delegateQueue]; + NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { + if (!data) { + completionHandler(false, error); + return; } - - // Add attachment - [mc addAttachmentData:fileData mimeType:@"text/plain" fileName:filename]; - } - - // Present mail view controller on screen in a separate window - UIViewController *rootController = [UIViewController new]; - - self.mailComposeControllerWindow = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; - self.mailComposeControllerWindow.windowLevel = UIWindowLevelAlert + 1; - self.mailComposeControllerWindow.rootViewController = rootController; - self.mailComposeControllerWindow.hidden = NO; - - [rootController presentViewController:mc animated:YES completion:nil]; -} - -#pragma mark - -#pragma mark MFMailComposeViewControllerDelegate - -- (void)mailComposeController:(MFMailComposeViewController *)controller didFinishWithResult:(MFMailComposeResult)result error:(nullable NSError *)error { - [controller dismissViewControllerAnimated:YES - completion:^{ - self.mailComposeControllerWindow.hidden = YES; - self.mailComposeControllerWindow = nil; - }]; -} - -#pragma mark - -#pragma mark Helpers - -+ (NSString *)configurationStoragePath { - return [self cacheDirectoryPath:ConfigurationStorageFile]; -} - -+ (NSString *)logFilePath { - return [self cacheDirectoryPath:LogFileName]; -} - -+ (NSString *)manifestFilePath { - return [self cacheDirectoryPath:ManifestFileName]; -} - -+ (NSString *)cacheDirectoryPath:(NSString *)path { - NSString *cacheDirectory = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject]; - return [cacheDirectory stringByAppendingPathComponent:path]; + + NSError *jsonError; + id object = [ApptentiveJSONSerialization JSONObjectWithData:data error:&jsonError]; + if (jsonError != nil) { + ApptentiveLogError(ApptentiveLogTagMonitor, @"Unable to verify access token: returned json object is invalid (%@)", jsonError); + completionHandler(false, jsonError); + return; + } + + if (![object isKindOfClass:[NSDictionary class]]) { + ApptentiveLogError(ApptentiveLogTagMonitor, @"Unable to verify access token: unexpected JSON object (%@)", object); + completionHandler(false, [ApptentiveUtilities errorWithCode:101 failureReason:@"Unexpected JSON object"]); + return; + } + + NSDictionary *json = (NSDictionary *)object; + BOOL valid = [[json objectForKey:@"valid"] boolValue]; + + completionHandler(valid, nil); + }]; + [task resume]; } @end diff --git a/Apptentive/Apptentive/Misc/ApptentiveLogMonitorSession.h b/Apptentive/Apptentive/Misc/ApptentiveLogMonitorSession.h new file mode 100644 index 000000000..d5bb7f332 --- /dev/null +++ b/Apptentive/Apptentive/Misc/ApptentiveLogMonitorSession.h @@ -0,0 +1,38 @@ +// +// ApptentiveLogMonitorSession.h +// Apptentive +// +// Created by Alex Lementuev on 2/23/18. +// Copyright © 2018 Apptentive, Inc. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +extern NSNotificationName const ApptentiveLogMonitorSessionDidStart; +extern NSNotificationName const ApptentiveLogMonitorSessionDidStop; + +@interface ApptentiveLogMonitorSession : NSObject + +/** Email recipients for the log email */ +@property (nonatomic, strong) NSArray *emailRecipients; + +- (void)start; +- (void)resume; +- (void)stop; + ++ (NSString *)manifestFilePath; + +@end + +@interface ApptentiveLogMonitorSessionIO : NSObject + ++ (nullable ApptentiveLogMonitorSession *)readSessionFromPersistentStorage; ++ (void)clearCurrentSession; ++ (void)writeSessionToPersistentStorage:(ApptentiveLogMonitorSession *)session; ++ (nullable ApptentiveLogMonitorSession *)readSessionFromJWT:(NSString *)token; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Apptentive/Apptentive/Misc/ApptentiveLogMonitorSession.m b/Apptentive/Apptentive/Misc/ApptentiveLogMonitorSession.m new file mode 100644 index 000000000..e8fdc3511 --- /dev/null +++ b/Apptentive/Apptentive/Misc/ApptentiveLogMonitorSession.m @@ -0,0 +1,250 @@ +// +// ApptentiveLogMonitorSession.m +// Apptentive +// +// Created by Alex Lementuev on 2/23/18. +// Copyright © 2018 Apptentive, Inc. All rights reserved. +// + +#import + +#import "ApptentiveLogMonitorSession.h" +#import "ApptentiveUtilities.h" +#import "ApptentiveFileUtilities.h" +#import "ApptentiveJWT.h" +#import "ApptentiveSafeCollections.h" + +NSNotificationName const ApptentiveLogMonitorSessionDidStart = @"ApptentiveLogMonitorSessionDidStart"; +NSNotificationName const ApptentiveLogMonitorSessionDidStop = @"ApptentiveLogMonitorSessionDidStop"; + +static NSString *const kSessionStorageFile = @"apptentive-log-monitor.cfg"; +static NSString *const kManifestFileName = @"apptentive-manifest.txt"; +static NSString *const kKeyEmailRecipients = @"emailRecipients"; + +extern NSString *ApptentiveLocalizedString(NSString *key, NSString *_Nullable comment); + +@interface ApptentiveLogMonitorSession () + +@property (nonatomic, strong) UIWindow *mailComposeControllerWindow; +@property (nonatomic, assign) ApptentiveLogLevel oldLogLevel; + +@end + +@implementation ApptentiveLogMonitorSession + +- (instancetype)init { + self = [super init]; + if (self) { + _emailRecipients = @[@"support@apptentive.com"]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [coder encodeObject:[_emailRecipients componentsJoinedByString:@","] forKey:kKeyEmailRecipients]; +} + +- (nullable instancetype)initWithCoder:(NSCoder *)decoder { + self = [super init]; + if (self) { + _emailRecipients = [[decoder decodeObjectForKey:kKeyEmailRecipients] componentsSeparatedByString:@","]; + } + return self; +} + +- (void)start { + self.oldLogLevel = ApptentiveLogGetLevel(); + ApptentiveLogInfo(ApptentiveLogTagMonitor, @"Overriding log level: %@", NSStringFromApptentiveLogLevel(ApptentiveLogLevelVerbose)); + ApptentiveLogSetLevel(ApptentiveLogLevelVerbose); + + dispatch_async(dispatch_get_main_queue(), ^{ + [self showReportUI]; + }); + + [[NSNotificationCenter defaultCenter] postNotificationName:ApptentiveLogMonitorSessionDidStart object:nil]; +} + +- (void)resume { + dispatch_async(dispatch_get_main_queue(), ^{ + [self showReportUI]; + }); +} + +- (void)stop { + ApptentiveLogSetLevel(self.oldLogLevel); + [[NSNotificationCenter defaultCenter] postNotificationName:ApptentiveLogMonitorSessionDidStop object:nil]; +} + +#pragma mark - +#pragma mark User Interactions + +- (void)showReportUI { + // create a custom window to show UI on top of everything + UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + + // create alert controller with "Send", "Continue" and "Discard" actions + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:ApptentiveLocalizedString(@"Apptentive", @"Apptentive") message:ApptentiveLocalizedString(@"Troubleshooting mode", @"Troubleshooting mode") preferredStyle:UIAlertControllerStyleActionSheet]; + [alertController addAction:[UIAlertAction actionWithTitle:ApptentiveLocalizedString(@"Send Report", @"Send Report") + style:UIAlertActionStyleDefault + handler:^(UIAlertAction *_Nonnull action) { + window.hidden = YES; + [self sendReportWithAttachedFiles:[ApptentiveLogMonitorSession listAttachments]]; + }]]; + [alertController addAction:[UIAlertAction actionWithTitle:ApptentiveLocalizedString(@"Continue", @"Continue") + style:UIAlertActionStyleCancel + handler:^(UIAlertAction *_Nonnull action) { + window.hidden = YES; + }]]; + [alertController addAction:[UIAlertAction actionWithTitle:ApptentiveLocalizedString(@"Discard Report", @"Discard Report") + style:UIAlertActionStyleDestructive + handler:^(UIAlertAction *_Nonnull action) { + window.hidden = YES; + [self stop]; + }]]; + + window.rootViewController = [[UIViewController alloc] init]; + window.windowLevel = UIWindowLevelAlert + 1; + window.hidden = NO; // don't use makeKeyAndVisible since we don't have any knowledge about the host app's UI + [window.rootViewController presentViewController:alertController animated:YES completion:nil]; +} + +#pragma mark - +#pragma mark Report + +- (void)sendReportWithAttachedFiles:(NSArray *)files { + if (![MFMailComposeViewController canSendMail]) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:ApptentiveLocalizedString(@"Apptentive Log Monitor", @"Apptentive Log Monitor") message:ApptentiveLocalizedString(@"Unable to send email", @"Unable to send email") delegate:nil cancelButtonTitle:ApptentiveLocalizedString(@"OK", @"OK") otherButtonTitles:nil]; + [alertView show]; +#pragma clang diagnostic pop + + return; + } + + NSDictionary *bundleInfo = [[NSBundle mainBundle] infoDictionary]; + + // collecting system info + NSMutableString *messageBody = [NSMutableString new]; + [messageBody appendString:@"This email may contain sensitive content.\n Please review before sending.\n\n"]; + [messageBody appendFormat:@"App Bundle Identifier: %@\n", [NSBundle mainBundle].bundleIdentifier]; + [messageBody appendFormat:@"App Version: %@\n", [bundleInfo objectForKey:@"CFBundleShortVersionString"]]; + [messageBody appendFormat:@"App Build: %@\n", [bundleInfo objectForKey:@"CFBundleVersion"]]; + [messageBody appendFormat:@"Apptentive SDK: %@\n", kApptentiveVersionString]; + [messageBody appendFormat:@"Device Model: %@\n", [ApptentiveUtilities deviceMachine]]; + [messageBody appendFormat:@"iOS Version: %@\n", [UIDevice currentDevice].systemVersion]; + [messageBody appendFormat:@"Locale: %@", [NSLocale currentLocale].localeIdentifier]; + + NSString *emailTitle = [NSString stringWithFormat:@"%@ (iOS)", [NSBundle mainBundle].infoDictionary[@"CFBundleName"]]; + + MFMailComposeViewController *mc = [[MFMailComposeViewController alloc] init]; + mc.mailComposeDelegate = self; + [mc setSubject:emailTitle]; + [mc setMessageBody:messageBody isHTML:NO]; + [mc setToRecipients:_emailRecipients]; + + // Get the resource path and read the file using NSData + for (NSString *path in files) { + NSString *filename = [path lastPathComponent]; + NSData *fileData = [NSData dataWithContentsOfFile:path]; + if (fileData.length == 0) { + ApptentiveLogError(ApptentiveLogTagMonitor, @"Attachment file does not exist or empty: %@", path); + continue; + } + + // Add attachment + [mc addAttachmentData:fileData mimeType:@"text/plain" fileName:filename]; + } + + // Present mail view controller on screen in a separate window + UIViewController *rootController = [UIViewController new]; + + self.mailComposeControllerWindow = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + self.mailComposeControllerWindow.windowLevel = UIWindowLevelAlert + 1; + self.mailComposeControllerWindow.rootViewController = rootController; + self.mailComposeControllerWindow.hidden = NO; + + [rootController presentViewController:mc animated:YES completion:nil]; +} + +#pragma mark - +#pragma mark MFMailComposeViewControllerDelegate + +- (void)mailComposeController:(MFMailComposeViewController *)controller didFinishWithResult:(MFMailComposeResult)result error:(nullable NSError *)error { + [controller dismissViewControllerAnimated:YES + completion:^{ + [self stop]; + self.mailComposeControllerWindow.hidden = YES; + self.mailComposeControllerWindow = nil; + }]; +} + +#pragma mark - +#pragma mark Helpers + ++ (NSArray *)listAttachments { + NSMutableArray *attachments = [NSMutableArray new]; + ApptentiveArrayAddObject(attachments, [self manifestFilePath]); + NSArray *logFiles = ApptentiveListLogFiles(); + for (NSString *logFile in logFiles) { + ApptentiveArrayAddObject(attachments, logFile); + } + return attachments; +} + ++ (NSString *)manifestFilePath { + return [ApptentiveUtilities cacheDirectoryPath:kManifestFileName]; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"recipients=%@", [_emailRecipients componentsJoinedByString:@","]]; +} + +@end + +@implementation ApptentiveLogMonitorSessionIO + ++ (nullable ApptentiveLogMonitorSession *)readSessionFromPersistentStorage { + NSString *filepath = [self sessionStoragePath]; + ApptentiveAssertNotNil(filepath, @"Session path is nil"); + return filepath != nil ? [NSKeyedUnarchiver unarchiveObjectWithFile:filepath] : nil; +} + ++ (void)clearCurrentSession { + NSString *filepath = [self sessionStoragePath]; + ApptentiveAssertNotNil(filepath, @"Session path is nil"); + [ApptentiveFileUtilities deleteFileAtPath:filepath]; +} + ++ (void)writeSessionToPersistentStorage:(ApptentiveLogMonitorSession *)session { + ApptentiveAssertNotNil(session, @"Session is nil"); + NSString *filepath = [self sessionStoragePath]; + ApptentiveAssertNotNil(filepath, @"Session path is nil"); + if (filepath) { + [NSKeyedArchiver archiveRootObject:session toFile:filepath]; + } +} + ++ (nullable ApptentiveLogMonitorSession *)readSessionFromJWT:(NSString *)token { + NSError *jwtError; + ApptentiveJWT *jwt = [ApptentiveJWT JWTWithContentOfString:token error:&jwtError]; + if (jwtError != nil) { + ApptentiveLogError(ApptentiveLogTagMonitor, @"JWT parsing error (%@)", jwtError); + return nil; + } + + ApptentiveLogMonitorSession *configuration = [[ApptentiveLogMonitorSession alloc] init]; + + NSArray *emailRecepients = ApptentiveDictionaryGetArray(jwt.payload, @"recipients"); + if (emailRecepients != nil) { + configuration.emailRecipients = emailRecepients; + } + + return configuration; +} + ++ (NSString *)sessionStoragePath { + return [ApptentiveUtilities cacheDirectoryPath:kSessionStorageFile]; +} + +@end diff --git a/Apptentive/Apptentive/Misc/ApptentiveLogTag.h b/Apptentive/Apptentive/Misc/ApptentiveLogTag.h index b482dbb9b..be0370db2 100644 --- a/Apptentive/Apptentive/Misc/ApptentiveLogTag.h +++ b/Apptentive/Apptentive/Misc/ApptentiveLogTag.h @@ -14,10 +14,9 @@ NS_ASSUME_NONNULL_BEGIN @interface ApptentiveLogTag : NSObject @property (nonatomic, readonly) NSString *name; -@property (nonatomic, assign) BOOL enabled; -+ (instancetype)logTagWithName:(NSString *)name enabled:(BOOL)enabled; -- (instancetype)initWithName:(NSString *)name enabled:(BOOL)enabled; ++ (instancetype)logTagWithName:(NSString *)name; +- (instancetype)initWithName:(NSString *)name; + (ApptentiveLogTag *)conversationTag; + (ApptentiveLogTag *)networkTag; @@ -25,6 +24,10 @@ NS_ASSUME_NONNULL_BEGIN + (ApptentiveLogTag *)utilityTag; + (ApptentiveLogTag *)storageTag; + (ApptentiveLogTag *)logMonitorTag; ++ (ApptentiveLogTag *)criteriaTag; ++ (ApptentiveLogTag *)interactionsTag; ++ (ApptentiveLogTag *)pushTag; ++ (ApptentiveLogTag *)messagesTag; @end diff --git a/Apptentive/Apptentive/Misc/ApptentiveLogTag.m b/Apptentive/Apptentive/Misc/ApptentiveLogTag.m index 392b32780..bd35cf962 100644 --- a/Apptentive/Apptentive/Misc/ApptentiveLogTag.m +++ b/Apptentive/Apptentive/Misc/ApptentiveLogTag.m @@ -16,6 +16,10 @@ static ApptentiveLogTag *_utilityTag; static ApptentiveLogTag *_storageTag; static ApptentiveLogTag *_logMonitorTag; +static ApptentiveLogTag *_criteriaTag; +static ApptentiveLogTag *_interactionsTag; +static ApptentiveLogTag *_pushTag; +static ApptentiveLogTag *_messagesTag; @implementation ApptentiveLogTag @@ -23,24 +27,27 @@ @implementation ApptentiveLogTag + (void)initialize { if ([self class] == [ApptentiveLogTag class]) { - _conversationTag = [ApptentiveLogTag logTagWithName:@"CONVERSATION" enabled:YES]; - _networkTag = [ApptentiveLogTag logTagWithName:@"NETWORKING" enabled:YES]; - _payloadTag = [ApptentiveLogTag logTagWithName:@"PAYLOAD" enabled:YES]; - _utilityTag = [ApptentiveLogTag logTagWithName:@"UTILITY" enabled:YES]; - _storageTag = [ApptentiveLogTag logTagWithName:@"STORAGE" enabled:YES]; - _logMonitorTag = [ApptentiveLogTag logTagWithName:@"LOG_MONITOR" enabled:YES]; + _conversationTag = [ApptentiveLogTag logTagWithName:@"CONVERSATION"]; + _networkTag = [ApptentiveLogTag logTagWithName:@"NETWORK"]; + _payloadTag = [ApptentiveLogTag logTagWithName:@"PAYLOADS"]; + _utilityTag = [ApptentiveLogTag logTagWithName:@"UTIL"]; + _storageTag = [ApptentiveLogTag logTagWithName:@"STORAGE"]; + _logMonitorTag = [ApptentiveLogTag logTagWithName:@"LOG_MONITOR"]; + _interactionsTag = [ApptentiveLogTag logTagWithName:@"INTERACTIONS"]; + _pushTag = [ApptentiveLogTag logTagWithName:@"PUSH"]; + _messagesTag = [ApptentiveLogTag logTagWithName:@"MESSAGES"]; + _criteriaTag = [ApptentiveLogTag logTagWithName:@"CRITERIA"]; } } -+ (instancetype)logTagWithName:(NSString *)name enabled:(BOOL)enabled { - return [[self alloc] initWithName:name enabled:enabled]; ++ (instancetype)logTagWithName:(NSString *)name { + return [[self alloc] initWithName:name]; } -- (instancetype)initWithName:(NSString *)name enabled:(BOOL)enabled { +- (instancetype)initWithName:(NSString *)name { self = [super init]; if (self) { _name = name; - _enabled = enabled; } return self; } @@ -72,6 +79,22 @@ + (ApptentiveLogTag *)logMonitorTag { return _logMonitorTag; } ++ (ApptentiveLogTag *)criteriaTag { + return _criteriaTag; +} + ++ (ApptentiveLogTag *)interactionsTag { + return _logMonitorTag; +} + ++ (ApptentiveLogTag *)pushTag { + return _logMonitorTag; +} + ++ (ApptentiveLogTag *)messagesTag { + return _logMonitorTag; +} + @end NS_ASSUME_NONNULL_END diff --git a/Apptentive/Apptentive/Misc/ApptentiveLogWriter.h b/Apptentive/Apptentive/Misc/ApptentiveLogWriter.h deleted file mode 100644 index 7bae6d89b..000000000 --- a/Apptentive/Apptentive/Misc/ApptentiveLogWriter.h +++ /dev/null @@ -1,23 +0,0 @@ -// -// ApptentiveLogWriter.h -// Apptentive -// -// Created by Alex Lementuev on 10/10/17. -// Copyright © 2017 Apptentive, Inc. All rights reserved. -// - -#import - - -@interface ApptentiveLogWriter : NSObject - -@property (nonatomic, readonly) NSString *path; -@property (nonatomic, copy) void (^finishCallback)(ApptentiveLogWriter *writer); - -- (instancetype)initWithPath:(NSString *)path; -- (void)start; -- (void)stop; - -- (void)appendMessage:(NSString *)message; - -@end diff --git a/Apptentive/Apptentive/Misc/ApptentiveLogWriter.m b/Apptentive/Apptentive/Misc/ApptentiveLogWriter.m deleted file mode 100644 index 30d3b9814..000000000 --- a/Apptentive/Apptentive/Misc/ApptentiveLogWriter.m +++ /dev/null @@ -1,64 +0,0 @@ -// -// ApptentiveLogWriter.h -// ApptentiveLogWriter -// -// Created by Alex Lementuev on 10/12/17. -// Copyright 2017 Apptentive, Inc. All rights reserved. -// - -#import "ApptentiveLogWriter.h" - - -@interface ApptentiveLogWriter () - -@property (nonatomic, readonly) dispatch_queue_t writerQueue; -@property (nonatomic, assign) BOOL running; - -@end - - -@implementation ApptentiveLogWriter - -- (instancetype)initWithPath:(NSString *)path { - self = [super init]; - if (self) { - _path = path; - } - return self; -} - -- (void)start { - self.running = YES; - _writerQueue = dispatch_queue_create("Apptentive Log Writer", DISPATCH_QUEUE_SERIAL); -} - -- (void)stop { - dispatch_async(_writerQueue, ^{ - self.running = NO; - if (self.finishCallback) { - self.finishCallback(self); - } - }); -} - -- (void)appendMessage:(NSString *)message { - NSDate *timeStamp = [NSDate new]; - dispatch_async(_writerQueue, ^{ - if (self.running) { - [self writeMessage:[[NSString alloc] initWithFormat:@"%@ %@\n", timeStamp, message]]; - } - }); -} - -- (void)writeMessage:(NSString *)message { - NSFileHandle *fileHandle = [NSFileHandle fileHandleForWritingAtPath:_path]; - if (fileHandle == nil) { - [[NSFileManager defaultManager] createFileAtPath:_path contents:nil attributes:nil]; - fileHandle = [NSFileHandle fileHandleForWritingAtPath:_path]; - } - [fileHandle seekToEndOfFile]; - [fileHandle writeData:[message dataUsingEncoding:NSUTF8StringEncoding]]; - [fileHandle closeFile]; -} - -@end diff --git a/Apptentive/Apptentive/Misc/ApptentiveUtilities.h b/Apptentive/Apptentive/Misc/ApptentiveUtilities.h index 5631b2459..727ca5f90 100644 --- a/Apptentive/Apptentive/Misc/ApptentiveUtilities.h +++ b/Apptentive/Apptentive/Misc/ApptentiveUtilities.h @@ -12,13 +12,8 @@ NS_ASSUME_NONNULL_BEGIN @interface ApptentiveUtilities : NSObject - -+ (BOOL)fileExistsAtPath:(NSString *)path; -+ (BOOL)deleteFileAtPath:(NSString *)path; -+ (BOOL)deleteFileAtPath:(NSString *)path error:(NSError **)error; -+ (BOOL)deleteDirectoryAtPath:(NSString *)path error:(NSError **)error; - + (NSString *)applicationSupportPath; ++ (NSString * _Nullable)cacheDirectoryPath:(NSString *)path; + (NSBundle *)resourceBundle; + (UIStoryboard *)storyboard; + (UIImage *)imageNamed:(NSString *)name; @@ -50,6 +45,8 @@ NS_ASSUME_NONNULL_BEGIN + (NSString *)deviceMachine; ++ (NSError *)errorWithCode:(NSInteger)code failureReason:(NSString *)failureReason; + @end NS_ASSUME_NONNULL_END diff --git a/Apptentive/Apptentive/Misc/ApptentiveUtilities.m b/Apptentive/Apptentive/Misc/ApptentiveUtilities.m index b953ebd44..ab3bc8539 100644 --- a/Apptentive/Apptentive/Misc/ApptentiveUtilities.m +++ b/Apptentive/Apptentive/Misc/ApptentiveUtilities.m @@ -31,22 +31,6 @@ @implementation ApptentiveUtilities -+ (BOOL)fileExistsAtPath:(NSString *)path { - return path != nil && [[NSFileManager defaultManager] fileExistsAtPath:path]; -} - -+ (BOOL)deleteFileAtPath:(NSString *)path { - return [self deleteFileAtPath:path error:NULL]; -} - -+ (BOOL)deleteFileAtPath:(NSString *)path error:(NSError **)error { - return path != nil && [[NSFileManager defaultManager] removeItemAtPath:path error:error]; -} - -+ (BOOL)deleteDirectoryAtPath:(NSString *)path error:(NSError **)error { - return path != nil && [[NSFileManager defaultManager] removeItemAtPath:path error:error]; -} - + (NSString *)applicationSupportPath { static NSString *_applicationSupportPath; static dispatch_once_t onceToken; @@ -56,21 +40,48 @@ + (NSString *)applicationSupportPath { NSError *error; if (![[NSFileManager defaultManager] createDirectoryAtPath:_applicationSupportPath withIntermediateDirectories:YES attributes:nil error:&error]) { - ApptentiveLogError(@"Failed to create Application Support directory: %@", _applicationSupportPath); - ApptentiveLogError(@"Error was: %@", error); + ApptentiveLogError(ApptentiveLogTagUtility, @"Failed to create Application Support directory: %@", _applicationSupportPath); + ApptentiveLogError(ApptentiveLogTagUtility, @"Error was: %@", error); _applicationSupportPath = nil; } if (![[NSFileManager defaultManager] setAttributes:@{ NSFileProtectionKey: NSFileProtectionCompleteUntilFirstUserAuthentication } ofItemAtPath:_applicationSupportPath error:&error]) { - ApptentiveLogError(@"Failed to set file protection level: %@", _applicationSupportPath); - ApptentiveLogError(@"Error was: %@", error); + ApptentiveLogError(ApptentiveLogTagUtility, @"Failed to set file protection level: %@", _applicationSupportPath); + ApptentiveLogError(ApptentiveLogTagUtility, @"Error was: %@", error); } }); return _applicationSupportPath; } ++ (NSString * _Nullable)cacheDirectoryPath:(NSString *)path { + ApptentiveAssertNotNil(path, @"Attempted to get nil cache directory subpath"); + if (path.length == 0) { + return nil; + } + + static NSString *_cacheDirectoryPath; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + _cacheDirectoryPath = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject; + + NSError *error; + + if (![[NSFileManager defaultManager] createDirectoryAtPath:_cacheDirectoryPath withIntermediateDirectories:YES attributes:nil error:&error]) { + ApptentiveLogError(ApptentiveLogTagUtility, @"Failed to create Cache directory: %@", _cacheDirectoryPath); + ApptentiveLogError(ApptentiveLogTagUtility, @"Error was: %@", error); + _cacheDirectoryPath = nil; + } + + if (![[NSFileManager defaultManager] setAttributes:@{ NSFileProtectionKey: NSFileProtectionCompleteUntilFirstUserAuthentication } ofItemAtPath:_cacheDirectoryPath error:&error]) { + ApptentiveLogError(ApptentiveLogTagUtility, @"Failed to set file protection level: %@", _cacheDirectoryPath); + ApptentiveLogError(ApptentiveLogTagUtility, @"Error was: %@", error); + } + }); + + return [_cacheDirectoryPath stringByAppendingPathComponent:path]; +} + (NSBundle *)resourceBundle { NSBundle *bundleForClass = [NSBundle bundleForClass:[self class]]; @@ -348,7 +359,7 @@ + (BOOL)emailAddressIsValid:(NSString *)emailAddress { NSError *error = nil; NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"^\\s*[^\\s@]+@[^\\s@]+\\s*$" options:NSRegularExpressionCaseInsensitive error:&error]; if (!regex) { - ApptentiveLogError(@"Unable to build email regular expression: %@", error); + ApptentiveLogError(ApptentiveLogTagUtility, @"Unable to build email regular expression: %@", error); return NO; } NSUInteger count = [regex numberOfMatchesInString:emailAddress options:NSMatchingAnchored range:NSMakeRange(0, [emailAddress length])]; @@ -419,6 +430,11 @@ + (NSString *)deviceMachine { return [NSString stringWithCString:systemInfo.machine encoding:NSUTF8StringEncoding]; } ++ (NSError *)errorWithCode:(NSInteger)code failureReason:(NSString *)failureReason { + NSDictionary *userInfo = failureReason != nil ? @{NSLocalizedFailureReasonErrorKey: failureReason} : @{}; + return [NSError errorWithDomain:ApptentiveErrorDomain code:code userInfo:userInfo]; +} + @end NS_ASSUME_NONNULL_END diff --git a/Apptentive/Apptentive/Misc/NSData+Encryption.m b/Apptentive/Apptentive/Misc/NSData+Encryption.m index 62630290a..37fa8a029 100644 --- a/Apptentive/Apptentive/Misc/NSData+Encryption.m +++ b/Apptentive/Apptentive/Misc/NSData+Encryption.m @@ -29,12 +29,12 @@ - (nullable NSData *)apptentive_dataEncryptedWithKey:(NSData *)key { - (nullable NSData *)apptentive_dataEncryptedWithKey:(NSData *)key initializationVector:(NSData *)initializationVector { if (key == nil) { - ApptentiveLogError(@"Unable to encrypt data: encryption key is nil"); + ApptentiveLogError(ApptentiveLogTagUtility, @"Unable to encrypt data: encryption key is nil."); return nil; } if (initializationVector.length == 0) { - ApptentiveLogError(@"Unable to encrypt data: initialization vector is nil or empty"); + ApptentiveLogError(ApptentiveLogTagUtility, @"Unable to encrypt data: initialization vector is nil or empty."); return nil; } @@ -51,7 +51,7 @@ - (nullable NSData *)apptentive_dataEncryptedWithKey:(NSData *)key initializatio return ciphertextData; } else { - ApptentiveLogError(@"Failed to encrypt data (error code: %ld)", err); + ApptentiveLogError(ApptentiveLogTagUtility, @"Failed to encrypt data (error code: %ld).", err); return nil; } } @@ -71,14 +71,14 @@ - (nullable NSData *)apptentive_dataDecryptedWithKey:(NSData *)key { return result; } else { - ApptentiveLogError(@"Failed to decrypt data (error code: %ld)", err); + ApptentiveLogError(ApptentiveLogTagUtility, @"Failed to decrypt data (error code: %ld)", err); return nil; } } + (nullable instancetype)apptentive_dataWithHexString:(NSString *)string { if (string.length % 2 != 0) { - ApptentiveLogError(@"Key length must be an even number of characters: '%@'", string); + ApptentiveLogError(ApptentiveLogTagUtility, @"Key length must be an even number of characters: '%@'", string); return nil; } diff --git a/Apptentive/Apptentive/Model/ApptentiveAppConfiguration.h b/Apptentive/Apptentive/Model/ApptentiveAppConfiguration.h index f050d5805..5f06a5e0f 100644 --- a/Apptentive/Apptentive/Model/ApptentiveAppConfiguration.h +++ b/Apptentive/Apptentive/Model/ApptentiveAppConfiguration.h @@ -113,6 +113,11 @@ NS_ASSUME_NONNULL_BEGIN */ @property (readonly, assign, nonatomic) BOOL metricsEnabled; +/** + Whether to collect the advertisingIdentifier from the AdSupport framework. + */ +@property (readonly, assign, nonatomic) BOOL collectAdvertisingIdentifier; + /** The configuration for Message Center (see `ApptentiveMessageCenterConfiguration`). diff --git a/Apptentive/Apptentive/Model/ApptentiveAppConfiguration.m b/Apptentive/Apptentive/Model/ApptentiveAppConfiguration.m index 6e19e3fd5..996ff8c76 100644 --- a/Apptentive/Apptentive/Model/ApptentiveAppConfiguration.m +++ b/Apptentive/Apptentive/Model/ApptentiveAppConfiguration.m @@ -17,6 +17,7 @@ static NSString *const HideBrandingKey = @"hideBranding"; static NSString *const MessageCenterEnabledKey = @"messageCenterEnabled"; static NSString *const MetricsEnabledKey = @"metricsEnabled"; +static NSString *const CollectAdvertisingIdentifierKey = @"collectAdvertisingIdentifier"; static NSString *const MessageCenterKey = @"messageCenter"; static NSString *const ExpiryKey = @"expiry"; @@ -71,6 +72,7 @@ - (instancetype)initWithJSONDictionary:(NSDictionary *)JSONDictionary cacheLifet _hideBranding = [JSONDictionary[@"hide_branding"] boolValue]; _messageCenterEnabled = [JSONDictionary[@"message_center_enabled"] boolValue]; _metricsEnabled = [JSONDictionary[@"metrics_enabled"] boolValue]; + _collectAdvertisingIdentifier = [JSONDictionary[@"collect_ad_id"] boolValue]; _messageCenter = [[ApptentiveMessageCenterConfiguration alloc] initWithJSONDictionary:JSONDictionary[@"message_center"]]; @@ -115,6 +117,7 @@ - (nullable instancetype)initWithCoder:(NSCoder *)coder { _hideBranding = [coder decodeBoolForKey:HideBrandingKey]; _messageCenterEnabled = [coder decodeBoolForKey:MessageCenterEnabledKey]; _metricsEnabled = [coder decodeBoolForKey:MetricsEnabledKey]; + _collectAdvertisingIdentifier = [coder decodeBoolForKey:CollectAdvertisingIdentifierKey]; _messageCenter = [coder decodeObjectOfClass:[ApptentiveMessageCenterConfiguration class] forKey:MessageCenterKey]; _expiry = [coder decodeObjectOfClass:[NSDate class] forKey:ExpiryKey]; } @@ -129,6 +132,7 @@ - (void)encodeWithCoder:(NSCoder *)coder { [coder encodeBool:self.hideBranding forKey:HideBrandingKey]; [coder encodeBool:self.messageCenterEnabled forKey:MessageCenterEnabledKey]; [coder encodeBool:self.metricsEnabled forKey:MetricsEnabledKey]; + [coder encodeBool:self.collectAdvertisingIdentifier forKey:CollectAdvertisingIdentifierKey]; [coder encodeObject:self.messageCenter forKey:MessageCenterKey]; [coder encodeObject:self.expiry forKey:ExpiryKey]; } diff --git a/Apptentive/Apptentive/Model/ApptentiveLegacyEvent.m b/Apptentive/Apptentive/Model/ApptentiveLegacyEvent.m index bbafc38fa..eb8239a13 100644 --- a/Apptentive/Apptentive/Model/ApptentiveLegacyEvent.m +++ b/Apptentive/Apptentive/Model/ApptentiveLegacyEvent.m @@ -32,7 +32,7 @@ + (void)enqueueUnsentEventsInContext:(NSManagedObjectContext *)context forConver NSArray *unsentEvents = [context executeFetchRequest:request error:&error]; if (unsentEvents == nil) { - ApptentiveLogError(@"Unable to retrieve unsent events: %@", error); + ApptentiveLogError(ApptentiveLogTagInteractions, @"Unable to retrieve unsent events: %@", error); return; } @@ -89,17 +89,17 @@ - (nullable NSDictionary *)apiJSON { // Monitor that the Event payload has not been dropped on retry if (!result) { - ApptentiveLogError(@"Event json should not be nil."); + ApptentiveLogError(ApptentiveLogTagInteractions, @"Event json should not be nil."); } if (result.count == 0) { - ApptentiveLogError(@"Event json should return a result."); + ApptentiveLogError(ApptentiveLogTagInteractions, @"Event json should return a result."); } if (!result[@"label"]) { - ApptentiveLogError(@"Event json should include a `label`."); + ApptentiveLogError(ApptentiveLogTagInteractions, @"Event json should include a `label`."); return nil; } if (!result[@"nonce"]) { - ApptentiveLogError(@"Event json should include a `nonce`."); + ApptentiveLogError(ApptentiveLogTagInteractions, @"Event json should include a `nonce`."); return nil; } @@ -131,7 +131,7 @@ - (NSDictionary *)dictionaryForCurrentData { @try { result = [NSKeyedUnarchiver unarchiveObjectWithData:self.dictionaryData]; } @catch (NSException *exception) { - ApptentiveLogError(@"Unable to unarchive event: %@", exception); + ApptentiveLogError(ApptentiveLogTagInteractions, @"Unable to unarchive event: %@", exception); } return result; } diff --git a/Apptentive/Apptentive/Model/ApptentiveLegacyMessage.m b/Apptentive/Apptentive/Model/ApptentiveLegacyMessage.m index 6030da836..cabd6e6f0 100644 --- a/Apptentive/Apptentive/Model/ApptentiveLegacyMessage.m +++ b/Apptentive/Apptentive/Model/ApptentiveLegacyMessage.m @@ -52,7 +52,7 @@ + (void)enqueueUnsentMessagesInContext:(NSManagedObjectContext *)context forConv NSArray *unsentMessages = [context executeFetchRequest:request error:&error]; if (unsentMessages == nil) { - ApptentiveLogError(@"Unable to retrieve unsent messages: %@", error); + ApptentiveLogError(ApptentiveLogTagMessages, @"Unable to retrieve unsent messages: %@", error); return; } @@ -75,7 +75,7 @@ + (void)enqueueUnsentMessagesInContext:(NSManagedObjectContext *)context forConv NSString *newPath = [newAttachmentPath stringByAppendingPathComponent:filename]; if (![[NSFileManager defaultManager] moveItemAtPath:oldPath toPath:newPath error:&error]) { - ApptentiveLogError(@"Unable to move attachment file to %@: %@", newPath, error); + ApptentiveLogError(ApptentiveLogTagMessages, @"Unable to move attachment file to %@: %@", newPath, error); continue; } @@ -106,7 +106,7 @@ + (void)enqueueUnsentMessagesInContext:(NSManagedObjectContext *)context forConv } if ([[NSFileManager defaultManager] fileExistsAtPath:oldAttachmentPath] && ![[NSFileManager defaultManager] removeItemAtPath:oldAttachmentPath error:&error]) { - ApptentiveLogError(@"Unable to remove legacy attachments directory (%@): %@", oldAttachmentPath, error); + ApptentiveLogWarning(ApptentiveLogTagMessages, @"Unable to remove legacy attachments directory (%@): %@", oldAttachmentPath, error); } } diff --git a/Apptentive/Apptentive/Model/ApptentiveLegacySurveyResponse.m b/Apptentive/Apptentive/Model/ApptentiveLegacySurveyResponse.m index 7feb8eab7..43c99fc2c 100644 --- a/Apptentive/Apptentive/Model/ApptentiveLegacySurveyResponse.m +++ b/Apptentive/Apptentive/Model/ApptentiveLegacySurveyResponse.m @@ -32,7 +32,7 @@ + (void)enqueueUnsentSurveyResponsesInContext:(NSManagedObjectContext *)context NSArray *unsentSurveyResponses = [context executeFetchRequest:request error:&error]; if (unsentSurveyResponses == nil) { - ApptentiveLogError(@"Unable to retrieve unsent events: %@", error); + ApptentiveLogError(ApptentiveLogTagInteractions, @"Unable to retrieve unsent events: %@", error); return; } @@ -83,7 +83,7 @@ - (NSDictionary *)dictionaryForAnswers { @try { result = [NSKeyedUnarchiver unarchiveObjectWithData:self.answersData]; } @catch (NSException *exception) { - ApptentiveLogError(@"Unable to unarchive answers data: %@", exception); + ApptentiveLogError(ApptentiveLogTagInteractions, @"Unable to unarchive answers data: %@", exception); } return result; } diff --git a/Apptentive/Apptentive/Networking/ApptentiveClient.h b/Apptentive/Apptentive/Networking/ApptentiveClient.h index 710c995b6..93bc534b0 100644 --- a/Apptentive/Apptentive/Networking/ApptentiveClient.h +++ b/Apptentive/Apptentive/Networking/ApptentiveClient.h @@ -17,6 +17,10 @@ NS_ASSUME_NONNULL_BEGIN @interface ApptentiveClient : NSObject ++ (NSIndexSet *)okStatusCodes; ++ (NSIndexSet *)clientErrorStatusCodes; ++ (NSIndexSet *)serverErrorStatusCodes; + @property (readonly, nonatomic) NSOperationQueue *networkQueue; @property (readonly, nonatomic) NSURL *baseURL; @property (readonly, nonatomic) NSString *apptentiveKey; diff --git a/Apptentive/Apptentive/Networking/ApptentiveClient.m b/Apptentive/Apptentive/Networking/ApptentiveClient.m index 74ba37756..bb6dcdec5 100644 --- a/Apptentive/Apptentive/Networking/ApptentiveClient.m +++ b/Apptentive/Apptentive/Networking/ApptentiveClient.m @@ -13,17 +13,56 @@ #import "ApptentiveSerialRequest.h" #import "ApptentiveGCDDispatchQueue.h" +#import "ApptentiveRetryPolicy.h" -#define APPTENTIVE_MIN_BACKOFF_DELAY 1.0 +#define APPTENTIVE_MIN_BACKOFF_DELAY 5.0 #define APPTENTIVE_BACKOFF_MULTIPLIER 2.0 +#define APPTENTIVE_BACKOFF_CAP 10.0 * 60.0 NS_ASSUME_NONNULL_BEGIN +@interface ApptentiveClient () + +@property (strong, nonatomic) ApptentiveRetryPolicy *retryPolicy; + +@end + @implementation ApptentiveClient @synthesize URLSession = _URLSession; -@synthesize backoffDelay = _backoffDelay; + ++ (NSIndexSet *)okStatusCodes { + static NSIndexSet *_okStatusCodes; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + _okStatusCodes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(200, 100)]; // 2xx status codes + }); + + return _okStatusCodes; +} + ++ (NSIndexSet *)clientErrorStatusCodes { + static NSIndexSet *_clientErrorStatusCodes; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + _clientErrorStatusCodes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(400, 100)]; // 4xx status codes + + }); + + return _clientErrorStatusCodes; +} + ++ (NSIndexSet *)serverErrorStatusCodes { + static NSIndexSet *_serverErrorStatusCodes; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + _serverErrorStatusCodes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(500, 100)]; // 5xx status codes + + }); + + return _serverErrorStatusCodes; +} - (instancetype)initWithBaseURL:(NSURL *)baseURL apptentiveKey:(nonnull NSString *)apptentiveKey apptentiveSignature:(nonnull NSString *)apptentiveSignature delegateQueue:(ApptentiveDispatchQueue *)delegateQueue { self = [super init]; @@ -34,6 +73,11 @@ - (instancetype)initWithBaseURL:(NSURL *)baseURL apptentiveKey:(nonnull NSString _apptentiveSignature = apptentiveSignature; _networkQueue = [NSOperationQueue new]; + _retryPolicy = [[ApptentiveRetryPolicy alloc] initWithInitialBackoff:APPTENTIVE_MIN_BACKOFF_DELAY base:APPTENTIVE_BACKOFF_MULTIPLIER]; + _retryPolicy.shouldAddJitter = YES; + _retryPolicy.cap = APPTENTIVE_BACKOFF_CAP; + _retryPolicy.retryStatusCodes = [[self class] serverErrorStatusCodes]; + NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; configuration.HTTPAdditionalHeaders = @{ @"Accept": @"application/json", @@ -46,23 +90,11 @@ - (instancetype)initWithBaseURL:(NSURL *)baseURL apptentiveKey:(nonnull NSString configuration.URLCache = nil; _URLSession = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:((ApptentiveGCDDispatchQueue *) delegateQueue).queue]; - - [self resetBackoffDelay]; } return self; } -#pragma mark - Request operation data source - -- (void)increaseBackoffDelay { - _backoffDelay *= APPTENTIVE_BACKOFF_MULTIPLIER; -} - -- (void)resetBackoffDelay { - _backoffDelay = APPTENTIVE_MIN_BACKOFF_DELAY; -} - #pragma mark - Creating request operations - (ApptentiveRequestOperation *)requestOperationWithRequest:(id)request token:(nullable NSString *)token delegate:(ApptentiveRequestOperationCallback *)delegate { diff --git a/Apptentive/Apptentive/Networking/ApptentivePayloadDebug.m b/Apptentive/Apptentive/Networking/ApptentivePayloadDebug.m index 2aa1f96d4..6dacc5ec3 100644 --- a/Apptentive/Apptentive/Networking/ApptentivePayloadDebug.m +++ b/Apptentive/Apptentive/Networking/ApptentivePayloadDebug.m @@ -55,7 +55,7 @@ + (void)printPayloadSendingQueueWithContext:(NSManagedObjectContext *)context ti if (moreInfo.length > 0) { [moreInfo appendString:@"\n"]; } - [moreInfo appendFormat:@"JWT-%ld: %@", (unsigned long)row, request.authToken]; + [moreInfo appendFormat:@"JWT-%ld: %@", (unsigned long)row, ApptentiveHideIfSanitized(request.authToken)]; } [rows addObject:@[ request.type ?: @"nil", diff --git a/Apptentive/Apptentive/Networking/ApptentivePayloadSender.m b/Apptentive/Apptentive/Networking/ApptentivePayloadSender.m index d933e3f80..5e2dcfb83 100644 --- a/Apptentive/Apptentive/Networking/ApptentivePayloadSender.m +++ b/Apptentive/Apptentive/Networking/ApptentivePayloadSender.m @@ -56,15 +56,15 @@ - (instancetype)initWithBaseURL:(NSURL *)baseURL apptentiveKey:(NSString *)appte - (void)cancelNetworkOperations { for (NSOperation *operation in self.networkQueue.operations) { if ([operation isKindOfClass:[ApptentiveRequestOperation class]]) { - ApptentiveLogVerbose(@"Cancelling request operation %@.", operation.name); + ApptentiveLogVerbose(ApptentiveLogTagPayload, @"Cancelling request operation %@.", operation.name); [operation cancel]; } else if ([operation.name isEqualToString:ApptentiveBuildPayloadRequestsName]) { - ApptentiveLogVerbose(@"Cancelling build payload requets operation."); + ApptentiveLogVerbose(ApptentiveLogTagPayload, @"Cancelling build payload requets operation."); [operation cancel]; } } - ApptentiveLogVerbose(ApptentiveLogTagPayload, @"Clearing isResuming Flag"); + ApptentiveLogVerbose(ApptentiveLogTagPayload, @"Clearing isResuming Flag."); self.isResuming = NO; } @@ -81,7 +81,7 @@ - (void)createOperationsForQueuedRequestsInContext:(NSManagedObjectContext *)con return; } - ApptentiveLogVerbose(ApptentiveLogTagPayload, @"Setting isResuming Flag"); + ApptentiveLogVerbose(ApptentiveLogTagPayload, @"Setting isResuming Flag."); self.isResuming = YES; NSBlockOperation *buildPayloadRequestsOperation = [NSBlockOperation blockOperationWithBlock:^{ @@ -103,12 +103,12 @@ - (void)createOperationsForQueuedRequestsInContext:(NSManagedObjectContext *)con ApptentiveLogError(ApptentiveLogTagPayload, @"Unable to fetch waiting network payloads."); } - ApptentiveLogDebug(ApptentiveLogTagPayload, @"Adding %d record operations for queued payloads", queuedRequests.count); + ApptentiveLogDebug(ApptentiveLogTagPayload, @"Adding %d record operations for queued payloads.", queuedRequests.count); // Add an operation for every record in the queue // When the operation succeeds (or fails permanently), it deletes the associated record for (ApptentiveSerialRequest *request in queuedRequests) { - ApptentiveAssertNotNil(request.authToken, @"Attempted to send a request without a token: %@", request); + ApptentiveAssertNotNil(request.authToken, @"Attempted to send a request without a token: %@", ApptentiveHideIfSanitized(request)); ApptentiveRequestOperationCallback *callback = [ApptentiveRequestOperationCallback new]; callback.operationStartCallback = ^(ApptentiveRequestOperation *operation) { [self requestOperationDidStart:operation]; @@ -124,7 +124,7 @@ - (void)createOperationsForQueuedRequestsInContext:(NSManagedObjectContext *)con }; ApptentiveRequestOperation *operation = [self requestOperationWithRequest:request token:request.authToken delegate:callback]; - ApptentiveLogVerbose(ApptentiveLogTagPayload, @"Adding operation for %@ %@", operation.URLRequest.HTTPMethod, operation.URLRequest.URL.absoluteString); + ApptentiveLogVerbose(ApptentiveLogTagPayload, @"Adding operation for %@ %@.", operation.URLRequest.HTTPMethod, operation.URLRequest.URL.absoluteString); operation.request = request; @@ -135,19 +135,19 @@ - (void)createOperationsForQueuedRequestsInContext:(NSManagedObjectContext *)con if (queuedRequests.count) { // Save the context after all enqueued records have been sent NSBlockOperation *saveBlockOperation = [NSBlockOperation blockOperationWithBlock:^{ - ApptentiveLogVerbose(ApptentiveLogTagPayload, @"Saving Private Managed Object Context (with completed payloads deleted)"); + ApptentiveLogVerbose(ApptentiveLogTagPayload, @"Saving Private Managed Object Context (with completed payloads deleted)."); [childContext performBlockAndWait:^{ NSError *saveError; if (![childContext save:&saveError]) { - ApptentiveLogError(@"Unable to save temporary managed object context: %@", saveError); + ApptentiveLogError(ApptentiveLogTagPayload, @"Unable to save temporary managed object context (%@).", saveError); return; } - ApptentiveLogVerbose(ApptentiveLogTagPayload, @"Saving Parent Managed Object Context (with completed payloads deleted)"); + ApptentiveLogVerbose(ApptentiveLogTagPayload, @"Saving Parent Managed Object Context (with completed payloads deleted)."); [context performBlockAndWait:^{ NSError *parentSaveError; if (![context save:&parentSaveError]) { - ApptentiveLogError(@"Unable to save parent managed object context: %@", parentSaveError); + ApptentiveLogError(ApptentiveLogTagPayload, @"Unable to save parent managed object context (%@).", parentSaveError); } }]; }]; @@ -160,7 +160,7 @@ - (void)createOperationsForQueuedRequestsInContext:(NSManagedObjectContext *)con [self.networkQueue addOperation:saveBlockOperation]; } - ApptentiveLogVerbose(ApptentiveLogTagPayload, @"Clearing isResuming Flag"); + ApptentiveLogVerbose(ApptentiveLogTagPayload, @"Clearing isResuming Flag."); self.isResuming = NO; }]; @@ -309,11 +309,11 @@ - (void)updateQueuedRequestsInContext:(NSManagedObjectContext *)context withConv NSError *fetchError; NSArray *queuedRequests = [childContext executeFetchRequest:fetchRequest error:&fetchError]; if (fetchError != nil) { - ApptentiveLogError(@"Error while fetching requests without a conversation id: %@", fetchError); + ApptentiveLogError(ApptentiveLogTagPayload, @"Error while fetching requests without a conversation identifier (%@).", fetchError); return; } - ApptentiveLogDebug(@"Fetched %d requests without a conversation id", queuedRequests.count); + ApptentiveLogDebug(ApptentiveLogTagPayload, @"Fetched %d requests without a conversation identifier.", queuedRequests.count); if (queuedRequests.count > 0) { // Set a new conversation identifier @@ -327,13 +327,13 @@ - (void)updateQueuedRequestsInContext:(NSManagedObjectContext *)context withConv // save child context NSError *saveError; if (![childContext save:&saveError]) { - ApptentiveLogError(@"Unable to save temporary managed object context: %@", saveError); + ApptentiveLogError(ApptentiveLogTagPayload, @"Unable to save temporary managed object context (%@).", saveError); } [context performBlockAndWait:^{ NSError *parentSaveError; if (![context save:&parentSaveError]) { - ApptentiveLogError(@"Unable to save parent managed object context: %@", parentSaveError); + ApptentiveLogError(ApptentiveLogTagPayload, @"Unable to save parent managed object context (%@).", parentSaveError); } // we call -createOperationsForQueuedRequestsInContext: to send everything diff --git a/Apptentive/Apptentive/Networking/ApptentiveRequestOperation.h b/Apptentive/Apptentive/Networking/ApptentiveRequestOperation.h index 58d69e614..5b6e055f1 100644 --- a/Apptentive/Apptentive/Networking/ApptentiveRequestOperation.h +++ b/Apptentive/Apptentive/Networking/ApptentiveRequestOperation.h @@ -12,7 +12,7 @@ NS_ASSUME_NONNULL_BEGIN @protocol ApptentiveRequestOperationDataSource; -@class ApptentiveRequestOperationCallback; +@class ApptentiveRequestOperationCallback, ApptentiveRetryPolicy; extern NSErrorDomain const ApptentiveHTTPErrorDomain; @@ -114,19 +114,7 @@ extern NSErrorDomain const ApptentiveHTTPErrorDomain; /** The number of seconds the operation should wait before retrying a request. */ -@property (readonly, nonatomic) NSTimeInterval backoffDelay; - -/** - Indicates that the data source should increase the backoff delay because the - previous request encountered a retry-able error. - */ -- (void)increaseBackoffDelay; - -/** - Indicates taht the data source should reset its backoff delay because a request - succeeded. - */ -- (void)resetBackoffDelay; +@property (readonly, nonatomic) ApptentiveRetryPolicy *retryPolicy; @end diff --git a/Apptentive/Apptentive/Networking/ApptentiveRequestOperation.m b/Apptentive/Apptentive/Networking/ApptentiveRequestOperation.m index d0fd11454..9339498a6 100644 --- a/Apptentive/Apptentive/Networking/ApptentiveRequestOperation.m +++ b/Apptentive/Apptentive/Networking/ApptentiveRequestOperation.m @@ -13,6 +13,7 @@ #import "ApptentiveRequestProtocol.h" #import "ApptentiveSafeCollections.h" #import "ApptentiveSerialRequest.h" +#import "ApptentiveRetryPolicy.h" NS_ASSUME_NONNULL_BEGIN @@ -31,38 +32,6 @@ @interface ApptentiveRequestOperation () { @implementation ApptentiveRequestOperation -+ (NSIndexSet *)okStatusCodes { - static NSIndexSet *_okStatusCodes; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - _okStatusCodes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(200, 100)]; // 2xx status codes - }); - - return _okStatusCodes; -} - -+ (NSIndexSet *)clientErrorStatusCodes { - static NSIndexSet *_clientErrorStatusCodes; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - _clientErrorStatusCodes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(400, 100)]; // 4xx status codes - - }); - - return _clientErrorStatusCodes; -} - -+ (NSIndexSet *)serverErrorStatusCodes { - static NSIndexSet *_serverErrorStatusCodes; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - _serverErrorStatusCodes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(500, 100)]; // 5xx status codes - - }); - - return _serverErrorStatusCodes; -} - - (instancetype)initWithURLRequest:(NSURLRequest *)URLRequest delegate:(ApptentiveRequestOperationCallback *)delegate dataSource:(id)dataSource { self = [super init]; @@ -111,7 +80,7 @@ - (void)startTask { NSHTTPURLResponse *URLResponse = (NSHTTPURLResponse *)response; self->_responseData = data; // Store "raw" response data to access from the callback - if ([[[self class] okStatusCodes] containsIndex:URLResponse.statusCode]) { + if ([[ApptentiveClient okStatusCodes] containsIndex:URLResponse.statusCode]) { NSObject *responseObject = nil; if (URLResponse.statusCode != 204) { // "No Content" @@ -136,9 +105,8 @@ - (void)startTask { [self.task resume]; [self didChangeValueForKey:@"isExecuting"]; - ApptentiveLogDebug(ApptentiveLogTagNetwork, @"%@ %@ started.", self.URLRequest.HTTPMethod, self.URLRequest.URL.absoluteString); - ApptentiveLogVerbose(ApptentiveLogTagNetwork, @"Headers: %@%@", self.URLRequest.allHTTPHeaderFields, self.URLRequest.HTTPBody.length > 0 ? [NSString stringWithFormat:@"\n-----------PAYLOAD BEGIN-----------\n%@\n-----------PAYLOAD END-----------", [[NSString alloc] initWithData:self.URLRequest.HTTPBody encoding:NSUTF8StringEncoding]] : @""); + ApptentiveLogVerbose(ApptentiveLogTagNetwork, @"Headers: %@%@", ApptentiveHideKeysIfSanitized(self.URLRequest.allHTTPHeaderFields, @[@"Authorization"]), self.URLRequest.HTTPBody.length > 0 ? [NSString stringWithFormat:@"\n-----------PAYLOAD BEGIN-----------\n%@\n-----------PAYLOAD END-----------", ApptentiveHideIfSanitized([[NSString alloc] initWithData:self.URLRequest.HTTPBody encoding:NSUTF8StringEncoding])] : @""); [self.dataSource.URLSession.delegateQueue addOperationWithBlock:^{ [self.delegate requestOperationDidStart:self]; @@ -158,11 +126,12 @@ - (void)processResponse:(NSHTTPURLResponse *)response withObject:(nullable NSObj _responseObject = responseObject; ApptentiveLogDebug(ApptentiveLogTagNetwork, @"%@ %@ finished successfully (took %g sec).", self.URLRequest.HTTPMethod, self.URLRequest.URL.absoluteString, self.duration); - ApptentiveLogVerbose(ApptentiveLogTagNetwork, @"Response object:\n%@.", responseObject); + + ApptentiveLogVerbose(ApptentiveLogTagNetwork, @"Response object:\n%@.", ApptentiveHideIfSanitized(responseObject ?: @"")); [self.delegate requestOperationDidFinish:self]; - [self.dataSource resetBackoffDelay]; + [self.dataSource.retryPolicy resetRetryDelay]; [self completeOperation]; } @@ -172,15 +141,14 @@ - (void)processNetworkError:(NSError *)error { } - (void)processHTTPError:(NSError *)error withResponse:(NSHTTPURLResponse *)response responseData:(NSData *)responseData { - BOOL shouldRetry = YES; + BOOL shouldRetry = [self.dataSource.retryPolicy shouldRetryRequestWithStatusCode:response.statusCode]; NSString *HTTPErrorTitle; NSString *HTTPErrorMessage = responseData == nil ? @"" : [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding]; - if ([[[self class] serverErrorStatusCodes] containsIndex:response.statusCode]) { + if ([[ApptentiveClient serverErrorStatusCodes] containsIndex:response.statusCode]) { HTTPErrorTitle = @"Server error"; - } else if ([[[self class] clientErrorStatusCodes] containsIndex:response.statusCode]) { + } else if ([[ApptentiveClient clientErrorStatusCodes] containsIndex:response.statusCode]) { HTTPErrorTitle = @"Client error"; - shouldRetry = NO; } if (error == nil && HTTPErrorTitle != nil) { @@ -201,16 +169,17 @@ - (void)processHTTPError:(NSError *)error withResponse:(NSHTTPURLResponse *)resp - (void)retryTaskWithError:(nullable NSError *)error { if (error != nil) { - ApptentiveLogError(ApptentiveLogTagNetwork, @"%@ %@ failed with error: %@", self.URLRequest.HTTPMethod, self.URLRequest.URL.absoluteString, error); + ApptentiveLogWarning(ApptentiveLogTagNetwork, @"%@ %@ failed with error (%@).", self.URLRequest.HTTPMethod, self.URLRequest.URL.absoluteString, error); } - ApptentiveLogInfo(@"%@ %@ will retry in %f seconds.", self.URLRequest.HTTPMethod, self.URLRequest.URL.absoluteString, self.dataSource.backoffDelay); + [self.dataSource.retryPolicy increaseRetryDelay]; + NSTimeInterval retryDelay = self.dataSource.retryPolicy.retryDelay; - [self.delegate requestOperationWillRetry:self withError:error]; + ApptentiveLogInfo(@"%@ %@ will retry in %f seconds.", self.URLRequest.HTTPMethod, self.URLRequest.URL.absoluteString, retryDelay); - [self.dataSource increaseBackoffDelay]; + [self.delegate requestOperationWillRetry:self withError:error]; - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(self.dataSource.backoffDelay * NSEC_PER_SEC)), dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{ + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(retryDelay * NSEC_PER_SEC)), dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{ [self startTask]; }); } @@ -219,12 +188,12 @@ - (void)processAuthenticationFailureResponseData:(NSData *)data { NSError *error; id jsonObject = [ApptentiveJSONSerialization JSONObjectWithData:data error:&error]; if (error) { - ApptentiveLogError(@"Error while parsing JSON: %@", error); + ApptentiveLogError(ApptentiveLogTagNetwork, @"Error while parsing JSON (%@).", error); return; } if (![jsonObject isKindOfClass:[NSDictionary class]]) { - ApptentiveLogError(@"Unexpected JSON object: %@", jsonObject); + ApptentiveLogError(ApptentiveLogTagNetwork, @"Unexpected JSON object: %@", ApptentiveHideIfSanitized(jsonObject)); return; } @@ -253,7 +222,7 @@ - (void)completeOperation { } - (void)finishWithError:(NSError *)error { - ApptentiveLogError(ApptentiveLogTagNetwork, @"%@ %@ failed with error (took %g sec): %@. Not retrying.", self.URLRequest.HTTPMethod, self.URLRequest.URL.absoluteString, self.duration, error); + ApptentiveLogError(ApptentiveLogTagNetwork, @"%@ %@ failed with error after %g sec (%@). Not retrying.", self.URLRequest.HTTPMethod, self.URLRequest.URL.absoluteString, self.duration, error); [self.delegate requestOperation:self didFailWithError:error]; diff --git a/Apptentive/Apptentive/Networking/ApptentiveRetryPolicy.h b/Apptentive/Apptentive/Networking/ApptentiveRetryPolicy.h new file mode 100644 index 000000000..302ff8647 --- /dev/null +++ b/Apptentive/Apptentive/Networking/ApptentiveRetryPolicy.h @@ -0,0 +1,34 @@ +// +// ApptentiveRetryPolicy.h +// Apptentive +// +// Created by Frank Schmitt on 4/2/18. +// Copyright © 2018 Apptentive, Inc. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface ApptentiveRetryPolicy : NSObject + +@property (readonly, nonatomic) NSTimeInterval initialBackoff; +@property (readonly, nonatomic) float base; + +- (instancetype)initWithInitialBackoff:(NSTimeInterval)initialBackoff base:(float)base; + +@property (assign, nonatomic) BOOL shouldAddJitter; +@property (assign, nonatomic) NSTimeInterval cap; + +@property (strong, nonatomic) NSIndexSet *retryStatusCodes; +@property (strong, nonatomic) NSIndexSet *failStatusCodes; + +@property (readonly, nonatomic) NSTimeInterval retryDelay; + +- (BOOL)shouldRetryRequestWithStatusCode:(NSInteger)statusCode; +- (void)increaseRetryDelay; +- (void)resetRetryDelay; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Apptentive/Apptentive/Networking/ApptentiveRetryPolicy.m b/Apptentive/Apptentive/Networking/ApptentiveRetryPolicy.m new file mode 100644 index 000000000..c74cd6c0a --- /dev/null +++ b/Apptentive/Apptentive/Networking/ApptentiveRetryPolicy.m @@ -0,0 +1,49 @@ +// +// ApptentiveRetryPolicy.m +// Apptentive +// +// Created by Frank Schmitt on 4/2/18. +// Copyright © 2018 Apptentive, Inc. All rights reserved. +// + +#import "ApptentiveRetryPolicy.h" + +@implementation ApptentiveRetryPolicy + +@synthesize retryDelay = _retryDelay; + +- (instancetype)initWithInitialBackoff:(NSTimeInterval)initialBackoff base:(float)base { + self = [super init]; + + if (self) { + _initialBackoff = initialBackoff; + _base = base; + _shouldAddJitter = YES; + _cap = DBL_MAX; + + [self resetRetryDelay]; + } + + return self; +} + +- (BOOL)shouldRetryRequestWithStatusCode:(NSInteger)statusCode { + return [self.retryStatusCodes containsIndex:statusCode]; +} + +- (void)increaseRetryDelay { + _retryDelay = _retryDelay * self.base; +} + +- (void)resetRetryDelay { + _retryDelay = self.initialBackoff; +} + +- (NSTimeInterval)retryDelay { + double jitter = self.shouldAddJitter ? ((double)rand() / RAND_MAX) : 1.0; + double cappedRetryDelay = fmin(self.cap, _retryDelay); + + return cappedRetryDelay * jitter; +} + +@end diff --git a/Apptentive/Apptentive/Networking/ApptentiveSerialRequest.m b/Apptentive/Apptentive/Networking/ApptentiveSerialRequest.m index 86a319df5..ec9054a67 100644 --- a/Apptentive/Apptentive/Networking/ApptentiveSerialRequest.m +++ b/Apptentive/Apptentive/Networking/ApptentiveSerialRequest.m @@ -64,7 +64,7 @@ + (BOOL)enqueuePayload:(ApptentivePayload *)payload forConversation:(ApptentiveC ApptentiveAssertNotNil(context, @"Managed object context is nil"); if (context == nil) { - ApptentiveLogError(@"Unable encode enqueue request: managed object context is nil"); + ApptentiveLogError(ApptentiveLogTagPayload, @"Unable encode enqueue request: managed object context is nil."); return NO; } @@ -100,9 +100,9 @@ + (BOOL)enqueuePayload:(ApptentivePayload *)payload forConversation:(ApptentiveC ApptentiveSerialRequest *request = (ApptentiveSerialRequest *)[[NSManagedObject alloc] initWithEntity:[NSEntityDescription entityForName:@"QueuedRequest" inManagedObjectContext:childContext] insertIntoManagedObjectContext:childContext]; - ApptentiveAssertNotNil(request, @"Can't load managed request object"); + ApptentiveAssertNotNil(request, @"Can't load managed request object."); if (request == nil) { - ApptentiveLogError(@"Unable encode enqueue request '%@': can't load managed request object", payloadPath); + ApptentiveLogError(ApptentiveLogTagPayload, @"Unable encode enqueue request '%@': can't load managed request object.", payloadPath); return; } @@ -127,14 +127,14 @@ + (BOOL)enqueuePayload:(ApptentivePayload *)payload forConversation:(ApptentiveC // save child context NSError *saveError; if (![childContext save:&saveError]) { - ApptentiveLogError(@"Unable to save temporary managed object context: %@", saveError); + ApptentiveLogError(ApptentiveLogTagPayload, @"Unable to save temporary managed object context (%@).", saveError); } // save parent context [context performBlockAndWait:^{ NSError *parentSaveError; if (![context save:&parentSaveError]) { - ApptentiveLogError(@"Unable to save parent managed object context: %@", parentSaveError); + ApptentiveLogError(ApptentiveLogTagPayload, @"Unable to save parent managed object context (%@).", parentSaveError); } }]; diff --git a/Apptentive/Apptentive/Networking/ApptentiveSerialRequestAttachment.m b/Apptentive/Apptentive/Networking/ApptentiveSerialRequestAttachment.m index ea29d40ac..dd7169055 100644 --- a/Apptentive/Apptentive/Networking/ApptentiveSerialRequestAttachment.m +++ b/Apptentive/Apptentive/Networking/ApptentiveSerialRequestAttachment.m @@ -34,13 +34,13 @@ - (nullable NSData *)fileData { NSError *error = nil; fileData = [NSData dataWithContentsOfFile:self.path options:NSDataReadingMappedIfSafe error:&error]; if (!fileData) { - ApptentiveLogError(@"Unable to get contents of file path for uploading: %@", error); + ApptentiveLogError(ApptentiveLogTagPayload, @"Unable to get contents of file path for uploading: %@", error); } else { return fileData; } } - ApptentiveLogError(@"Missing sidecar file for %@", self); + ApptentiveLogError(ApptentiveLogTagPayload, @"Missing sidecar file for: %@", self); return nil; } diff --git a/Apptentive/Apptentive/Networking/Requests & Payloads/ApptentiveEventPayload.m b/Apptentive/Apptentive/Networking/Requests & Payloads/ApptentiveEventPayload.m index 5a76f23d8..e7c91f6c5 100644 --- a/Apptentive/Apptentive/Networking/Requests & Payloads/ApptentiveEventPayload.m +++ b/Apptentive/Apptentive/Networking/Requests & Payloads/ApptentiveEventPayload.m @@ -18,7 +18,7 @@ - (nullable instancetype)initWithLabel:(NSString *)label creationDate:(NSDate *) if (self) { if (label.length == 0) { - ApptentiveLogError(@"Event label is nil"); + ApptentiveLogError(ApptentiveLogTagPayload, @"Event label is nil."); return nil; } @@ -58,8 +58,8 @@ - (NSDictionary *)contents { if ([NSJSONSerialization isValidJSONObject:customDataDictionary]) { [contents addEntriesFromDictionary:customDataDictionary]; } else { - ApptentiveLogError(@"Event `customData` cannot be transformed into valid JSON and will be ignored."); - ApptentiveLogError(@"Please see NSJSONSerialization's `+isValidJSONObject:` for allowed types."); + ApptentiveLogError(ApptentiveLogTagPayload, @"Event `customData` cannot be transformed into valid JSON and will be ignored."); + ApptentiveLogError(ApptentiveLogTagPayload, @"Please see NSJSONSerialization's `+isValidJSONObject:` for allowed types."); } } @@ -69,8 +69,8 @@ - (NSDictionary *)contents { // Extended data items are not added for key "extended_data", but rather for key of extended data type: "time", "location", etc. [contents addEntriesFromDictionary:data]; } else { - ApptentiveLogError(@"Event `extendedData` cannot be transformed into valid JSON and will be ignored."); - ApptentiveLogError(@"Please see NSJSONSerialization's `+isValidJSONObject:` for allowed types."); + ApptentiveLogError(ApptentiveLogTagPayload, @"Event `extendedData` cannot be transformed into valid JSON and will be ignored."); + ApptentiveLogError(ApptentiveLogTagPayload, @"Please see NSJSONSerialization's `+isValidJSONObject:` for allowed types."); } } } diff --git a/Apptentive/Apptentive/Networking/Requests & Payloads/ApptentiveLegacyConversationRequest.m b/Apptentive/Apptentive/Networking/Requests & Payloads/ApptentiveLegacyConversationRequest.m index 9ff30a3d2..ebf745d06 100644 --- a/Apptentive/Apptentive/Networking/Requests & Payloads/ApptentiveLegacyConversationRequest.m +++ b/Apptentive/Apptentive/Networking/Requests & Payloads/ApptentiveLegacyConversationRequest.m @@ -8,6 +8,7 @@ #import "ApptentiveLegacyConversationRequest.h" #import "ApptentiveConversation.h" +#import "ApptentiveDefines.h" NS_ASSUME_NONNULL_BEGIN @@ -18,10 +19,7 @@ - (nullable instancetype)initWithConversation:(ApptentiveConversation *)conversa self = [super init]; if (self) { - if (conversation == nil) { - ApptentiveLogError(@"Can't init %@: conversation is nil"); - return nil; - } + APPTENTIVE_CHECK_INIT_NOT_NIL_ARG(conversation); _conversation = conversation; } diff --git a/Apptentive/Apptentive/Networking/Requests & Payloads/ApptentiveMessagePayload.m b/Apptentive/Apptentive/Networking/Requests & Payloads/ApptentiveMessagePayload.m index 0702b61bb..0568a2cb3 100644 --- a/Apptentive/Apptentive/Networking/Requests & Payloads/ApptentiveMessagePayload.m +++ b/Apptentive/Apptentive/Networking/Requests & Payloads/ApptentiveMessagePayload.m @@ -12,6 +12,7 @@ #import "ApptentiveUtilities.h" #import "NSData+Encryption.h" #import "NSMutableData+Types.h" +#import "ApptentiveDefines.h" NS_ASSUME_NONNULL_BEGIN @@ -29,10 +30,7 @@ - (nullable instancetype)initWithMessage:(ApptentiveMessage *)message { self = [super init]; if (self) { - if (message == nil) { - ApptentiveLogError(@"Can't init %@: message is nil", NSStringFromClass([self class])); - return nil; - } + APPTENTIVE_CHECK_INIT_NOT_NIL_ARG(message); _message = message; _superContents = super.contents; @@ -136,10 +134,10 @@ - (nullable NSData *)payload { ApptentiveLogVerbose(ApptentiveLogTagPayload, @"Encrypting attachment bytes: %ld", attachmentBytes.length); NSData *encryptedAttachment = [attachmentBytes apptentive_dataEncryptedWithKey:self.encryptionKey]; - ApptentiveLogVerbose(ApptentiveLogTagPayload, @"Writing encrypted attachment bytes: %ld", encryptedAttachment.length); + ApptentiveLogVerbose(ApptentiveLogTagPayload, @"Writing encrypted attachment bytes: %ld.", encryptedAttachment.length); [data appendData:encryptedAttachment]; } else { - ApptentiveLogVerbose(ApptentiveLogTagPayload, @"Writing attachment bytes: %ld", attachmentBytes.length); + ApptentiveLogVerbose(ApptentiveLogTagPayload, @"Writing attachment bytes: %ld.", attachmentBytes.length); [data appendData:attachmentBytes]; } [data apptentive_appendString:@"\r\n"]; @@ -147,7 +145,7 @@ - (nullable NSData *)payload { } [data apptentive_appendFormat:@"--%@--", boundary]; - ApptentiveLogVerbose(ApptentiveLogTagPayload, @"Total payload body bytes: %ld", data.length); + ApptentiveLogVerbose(ApptentiveLogTagPayload, @"Total payload body bytes: %ld.", data.length); return data; } diff --git a/Apptentive/Apptentive/Networking/Requests & Payloads/ApptentiveSurveyResponsePayload.m b/Apptentive/Apptentive/Networking/Requests & Payloads/ApptentiveSurveyResponsePayload.m index 71b8f26bb..27ec1d42c 100644 --- a/Apptentive/Apptentive/Networking/Requests & Payloads/ApptentiveSurveyResponsePayload.m +++ b/Apptentive/Apptentive/Networking/Requests & Payloads/ApptentiveSurveyResponsePayload.m @@ -15,12 +15,12 @@ @implementation ApptentiveSurveyResponsePayload - (nullable instancetype)initWithAnswers:(NSDictionary *)answers identifier:(NSString *)identifier creationDate:(nonnull NSDate *)creationDate { if (answers == nil) { - ApptentiveLogError(@"Attempting to create survey response without answers"); + ApptentiveLogError(ApptentiveLogTagPayload, @"Attempting to create survey response without answers."); return nil; } if (identifier.length == 0) { - ApptentiveLogError(@"Attempting to create survey response without identifier"); + ApptentiveLogError(ApptentiveLogTagPayload, @"Attempting to create survey response without identifier."); return nil; } diff --git a/Apptentive/Apptentive/Persistence/ApptentiveBackend.m b/Apptentive/Apptentive/Persistence/ApptentiveBackend.m index acf4cbb96..90da87a08 100644 --- a/Apptentive/Apptentive/Persistence/ApptentiveBackend.m +++ b/Apptentive/Apptentive/Persistence/ApptentiveBackend.m @@ -28,6 +28,7 @@ #import "ApptentiveVersion.h" #import "Apptentive_Private.h" #import "ApptentiveDispatchQueue.h" +#import "ApptentiveRetryPolicy.h" #import "ApptentiveLegacyEvent.h" #import "ApptentiveLegacyFileAttachment.h" @@ -119,6 +120,8 @@ - (instancetype)initWithApptentiveKey:(NSString *)apptentiveKey signature:(NSStr }); [self loadConfiguration]; + + [self maybeGetAdvertisingIdentifier]; [self startUp]; }]; @@ -213,6 +216,7 @@ - (void)applicationWillEnterForegroundNotification:(NSNotification *)notificatio self->_foreground = YES; [self resume]; [self addLaunchMetric]; + [self maybeGetAdvertisingIdentifier]; }]; } @@ -261,7 +265,7 @@ - (void)createSupportDirectoryIfNeeded { if (![[NSFileManager defaultManager] fileExistsAtPath:self.supportDirectoryPath]) { NSError *error; if (![[NSFileManager defaultManager] createDirectoryAtPath:self.supportDirectoryPath withIntermediateDirectories:YES attributes:nil error:&error]) { - ApptentiveLogError(@"Unable to create storage path “%@”: %@", self.supportDirectoryPath, error); + ApptentiveLogError(ApptentiveLogTagStorage, @"Unable to create storage path %@ (%@).", self.supportDirectoryPath, error); } } } @@ -291,13 +295,13 @@ - (void)startUp { // This is called when we're about to enter the background - (void)shutDown { - ApptentiveLogVerbose(@"Shutting down backend"); + ApptentiveLogVerbose(@"Shutting down backend."); // Asynchronous tasks off the main thread will not be given a chance to complete automatically. // We create a background task to clear out our operation queue and the payload sender queue. self.backgroundTaskIdentifier = [[UIApplication sharedApplication] beginBackgroundTaskWithName:@"Wind Down Backend" expirationHandler:^{ - ApptentiveLogError(@"Background task (%ld) did not complete in time.", (unsigned long)self.backgroundTaskIdentifier); + ApptentiveLogWarning(@"Background task (%ld) did not complete in time.", (unsigned long)self.backgroundTaskIdentifier); [[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskIdentifier]; }]; @@ -328,12 +332,12 @@ - (void)resume { // Note: must be called on main thread - (void)setUpCoreData { ApptentiveAssertMainQueue - ApptentiveLogVerbose(ApptentiveLogTagStorage, @"Setting up data manager"); + ApptentiveLogVerbose(ApptentiveLogTagStorage, @"Setting up data manager."); self.dataManager = [[ApptentiveDataManager alloc] initWithModelName:@"ATDataModel" inBundle:[ApptentiveUtilities resourceBundle] storagePath:[self supportDirectoryPath]]; if (![self.dataManager setupAndVerify]) { ApptentiveLogError(ApptentiveLogTagStorage, @"Unable to setup and verify data manager."); } else if (![self.dataManager persistentStoreCoordinator]) { - ApptentiveLogError(ApptentiveLogTagStorage, @"There was a problem setting up the persistent store coordinator!"); + ApptentiveLogError(ApptentiveLogTagStorage, @"There was a problem setting up the persistent store coordinator."); } } @@ -409,7 +413,7 @@ - (void)migrateLegacyCoreDataAndTaskQueueForConversation:(ApptentiveConversation NSError *coreDataError; if (![migrationContext save:&coreDataError]) { - ApptentiveLogError(@"Unable to save migration context: %@", coreDataError); + ApptentiveLogError(@"Unable to save core data migration context (%@).", coreDataError); } }]; @@ -430,6 +434,8 @@ - (void)processConfigurationResponse:(NSDictionary *)configurationResponse cache _configuration = [[ApptentiveAppConfiguration alloc] initWithJSONDictionary:configurationResponse cacheLifetime:cacheLifetime]; [self saveConfiguration]; + + [self maybeGetAdvertisingIdentifier]; } - (BOOL)saveConfiguration { @@ -446,8 +452,8 @@ - (void)updateNetworkingForCurrentNetworkStatus { if (self.networkAvailable != networkWasAvailable) { if (self.networkAvailable) { - [self.client resetBackoffDelay]; - [self.payloadSender resetBackoffDelay]; + [self.client.retryPolicy resetRetryDelay]; + [self.payloadSender.retryPolicy resetRetryDelay]; [self completeHousekeepingTasks]; } else { @@ -492,7 +498,7 @@ - (void)presentMessageCenterFromViewController:(nullable UIViewController *)view self.currentCustomData = customData; if (viewController.presentedViewController) { - ApptentiveLogError(@"Attempting to present Apptentive Message Center from View Controller that is already presenting a modal view controller"); + ApptentiveLogError(ApptentiveLogTagInteractions, @"Attempting to present Apptentive Message Center from View Controller that is already presenting a modal view controller."); if (completion) { completion(NO); } @@ -500,7 +506,7 @@ - (void)presentMessageCenterFromViewController:(nullable UIViewController *)view } if (self.presentedMessageCenterViewController != nil) { - ApptentiveLogInfo(@"Apptentive message center controller already shown."); + ApptentiveLogInfo(ApptentiveLogTagInteractions, @"Apptentive message center controller already shown."); if (completion) { completion(NO); } @@ -653,7 +659,7 @@ - (void)authenticationDidFailNotification:(NSNotification *)notification { NSString *conversationIdentifier = ApptentiveDictionaryGetString(notification.userInfo, ApptentiveAuthenticationDidFailNotificationKeyConversationIdentifier); if (![conversationIdentifier isEqualToString:self.conversationManager.activeConversation.identifier]) { - ApptentiveLogDebug(@"Conversation identifier mismatch"); + ApptentiveLogError(ApptentiveLogTagConversation, @"The identifier for the newly logged-in conversation did not match the active conversation."); return; } @@ -677,8 +683,7 @@ - (NSString *)cacheDirectoryPath { NSError *error = nil; BOOL result = [fm createDirectoryAtPath:newPath withIntermediateDirectories:YES attributes:nil error:&error]; if (!result) { - ApptentiveLogError(@"Failed to create support directory: %@", newPath); - ApptentiveLogError(@"Error was: %@", error); + ApptentiveLogError(ApptentiveLogTagStorage, @"Failed to create support directory %@ (%@).", newPath, error); return nil; } return newPath; @@ -696,6 +701,18 @@ - (ApptentiveMessageManager *)messageManager { return self.conversationManager.messageManager; } +#pragma mark - +#pragma mark Advertising Identifier + +- (void)maybeGetAdvertisingIdentifier { + ApptentiveAssertOperationQueue(self.operationQueue); + + if (self.configuration.collectAdvertisingIdentifier) { + [ApptentiveDevice getAdvertisingIdentifier]; + [self scheduleDeviceUpdate]; + } +} + @end NS_ASSUME_NONNULL_END diff --git a/Apptentive/Apptentive/Persistence/ApptentiveDataManager.m b/Apptentive/Apptentive/Persistence/ApptentiveDataManager.m index d31399cb0..41798515e 100644 --- a/Apptentive/Apptentive/Persistence/ApptentiveDataManager.m +++ b/Apptentive/Apptentive/Persistence/ApptentiveDataManager.m @@ -117,10 +117,10 @@ - (BOOL)setupAndVerify { } } @catch (NSException *exception) { - ApptentiveLogError(@"Caught exception attempting to test classes: %@", exception); + ApptentiveLogError(ApptentiveLogTagStorage, @"Caught exception attempting to test classes (%@).", exception); self.managedObjectContext = nil; self.persistentStoreCoordinator = nil; - ApptentiveLogError(@"Removing persistent store and starting over."); + ApptentiveLogError(ApptentiveLogTagStorage, @"Removing persistent store and starting over."); [self removePersistentStore]; } @finally { @@ -163,14 +163,14 @@ - (NSPersistentStoreCoordinator *)persistentStoreCoordinator { _persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]]; } @catch (NSException *exception) { - ApptentiveLogError(@"Unable to setup persistent store: %@", exception); + ApptentiveLogError(ApptentiveLogTagStorage, @"Unable to setup persistent store (%@).", exception); return nil; } BOOL storeExists = [[NSFileManager defaultManager] fileExistsAtPath:[storeURL path]]; if (storeExists && [self isMigrationNecessary:_persistentStoreCoordinator]) { if (![self migrateStoreError:&error]) { - ApptentiveLogError(@"Failed to migrate store. Need to start over from scratch: %@", error); + ApptentiveLogError(ApptentiveLogTagStorage, @"Failed to migrate persistent store. Need to start over from scratch (%@).", error); self.didFailToMigrateStore = YES; [self removePersistentStore]; } else { @@ -184,7 +184,7 @@ - (NSPersistentStoreCoordinator *)persistentStoreCoordinator { // So, there's no need to set these explicitly for our purposes. NSDictionary *options = @{ NSSQLitePragmasOption: @{@"journal_mode": @"DELETE"} }; if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:options error:&error]) { - ApptentiveLogError(@"Unable to create new persistent store: %@", error); + ApptentiveLogError(ApptentiveLogTagStorage, @"Unable to create new persistent store (%@).", error); _persistentStoreCoordinator = nil; return nil; } @@ -206,7 +206,7 @@ - (void)removePersistentStore { if ([fileManager fileExistsAtPath:sourcePath]) { NSError *error = nil; if (![[NSFileManager defaultManager] removeItemAtURL:storeURL error:&error]) { - ApptentiveLogError(@"Failed to delete the store: %@", error); + ApptentiveLogError(ApptentiveLogTagStorage, @"Failed to delete the persistent store (%@).", error); } } [self removeSQLiteSidecarsForPath:sourcePath]; @@ -236,7 +236,7 @@ - (BOOL)removeCanaryFile { if ([[NSFileManager defaultManager] removeItemAtPath:[self canaryFilePath] error:&error]) { return YES; } - ApptentiveLogError(@"Error removing upgrade canary: %@", error); + ApptentiveLogError(ApptentiveLogTagStorage, @"Error removing core data upgrade canary (%@).", error); return NO; } @end @@ -281,7 +281,7 @@ - (BOOL)progressivelyMigrateURL:(NSURL *)sourceStoreURL ofType:(NSString *)type NSArray *bundlesForSourceModel = @[self.bundle]; NSManagedObjectModel *sourceModel = [NSManagedObjectModel mergedModelFromBundles:bundlesForSourceModel forStoreMetadata:sourceMetadata]; if (sourceModel == nil) { - ApptentiveLogError(@"Failed to find source model."); + ApptentiveLogError(ApptentiveLogTagStorage, @"Failed to find core data source model."); if (error) { *error = [NSError errorWithDomain:@"ATErrorDomain" code:ATMigrationMergedModelErrorCode userInfo:@{ NSLocalizedDescriptionKey: @"Failed to find source model for migration" }]; } @@ -353,18 +353,18 @@ - (BOOL)progressivelyMigrateURL:(NSURL *)sourceStoreURL ofType:(NSString *)type NSFileManager *fileManager = [NSFileManager defaultManager]; if (![fileManager createDirectoryAtPath:[backupPath stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:error]) { - ApptentiveLogError(@"Unable to create backup source store path."); + ApptentiveLogError(ApptentiveLogTagStorage, @"Unable to create backup source persistent store path."); return NO; } if (![fileManager moveItemAtPath:[sourceStoreURL path] toPath:backupPath error:error]) { - ApptentiveLogError(@"Unable to backup source store path."); + ApptentiveLogError(ApptentiveLogTagStorage, @"Unable to back up source persistent store path."); return NO; } if (![fileManager moveItemAtPath:storePath toPath:[sourceStoreURL path] error:error]) { [fileManager moveItemAtPath:backupPath toPath:[sourceStoreURL path] error:nil]; - ApptentiveLogError(@"Unable to move new store into place."); + ApptentiveLogError(ApptentiveLogTagStorage, @"Unable to move new persistent store into place."); return NO; } else { // Kill any remaining -wal or -shm files. Kill them with fire. @@ -387,7 +387,7 @@ - (BOOL)removeSQLiteSidecarsForPath:(NSString *)sourcePath { NSError *localError = nil; if ([fileManager fileExistsAtPath:obsoletePath isDirectory:&isDir] && !isDir) { if (![fileManager removeItemAtPath:obsoletePath error:&localError]) { - ApptentiveLogError(@"Unable to remove obsolete WAL file %@ with error: %@", obsoletePath, localError); + ApptentiveLogError(ApptentiveLogTagStorage, @"Unable to remove obsolete WAL file %@ (%@).", obsoletePath, localError); success = NO; } } diff --git a/Apptentive/Apptentive/Surveys/View Controllers/ApptentiveSurveyViewModel.m b/Apptentive/Apptentive/Surveys/View Controllers/ApptentiveSurveyViewModel.m index f9f8c4a91..fcda4f0e3 100644 --- a/Apptentive/Apptentive/Surveys/View Controllers/ApptentiveSurveyViewModel.m +++ b/Apptentive/Apptentive/Surveys/View Controllers/ApptentiveSurveyViewModel.m @@ -46,7 +46,7 @@ - (instancetype)initWithInteraction:(ApptentiveInteraction *)interaction { @try { _survey = [[ApptentiveSurvey alloc] initWithJSON:interaction.configuration]; } @catch (NSException *exception) { - ApptentiveLogError(@"Unable to parse survey."); + ApptentiveLogError(ApptentiveLogTagInteractions, @"Unable to parse survey."); return nil; } diff --git a/Apptentive/ApptentiveDebugging/Apptentive+Debugging.m b/Apptentive/ApptentiveDebugging/Apptentive+Debugging.m index 7e428d22e..573d48b97 100644 --- a/Apptentive/ApptentiveDebugging/Apptentive+Debugging.m +++ b/Apptentive/ApptentiveDebugging/Apptentive+Debugging.m @@ -22,6 +22,7 @@ #import "ApptentiveConversationMetadata.h" #import "ApptentiveConversationMetadataItem.h" #import "ApptentiveSafeCollections.h" +#import "ApptentiveTargets.h" #import "ApptentiveJWT.h" #import "ApptentiveDispatchQueue.h" @@ -74,8 +75,8 @@ - (NSDictionary *)deviceInfo { } - (NSArray *)engagementEvents { - NSDictionary *targets = Apptentive.shared.backend.conversationManager.manifest.targets; - NSArray *localCodePoints = [targets.allKeys filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"SELF BEGINSWITH[c] %@", @"local#app#"]]; + ApptentiveTargets *targets = Apptentive.shared.backend.conversationManager.manifest.targets; + NSArray *localCodePoints = [[targets.invocations.allKeys filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"SELF BEGINSWITH[c] %@", @"local#app#"]] sortedArrayUsingSelector:@selector(compare:)]; NSMutableArray *eventNames = [NSMutableArray array]; for (NSString *codePoint in localCodePoints) { ApptentiveArrayAddObject(eventNames, [codePoint substringFromIndex:10]); diff --git a/Apptentive/ApptentiveDebugging/Info.plist b/Apptentive/ApptentiveDebugging/Info.plist index 05c0461c4..c3933b3c2 100644 --- a/Apptentive/ApptentiveDebugging/Info.plist +++ b/Apptentive/ApptentiveDebugging/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 5.0.4 + 5.1.0 CFBundleVersion $(CURRENT_PROJECT_VERSION) NSPrincipalClass diff --git a/Apptentive/ApptentiveTests/AppptentiveAsyncLogWriterTests.swift b/Apptentive/ApptentiveTests/AppptentiveAsyncLogWriterTests.swift new file mode 100644 index 000000000..a4c0e561f --- /dev/null +++ b/Apptentive/ApptentiveTests/AppptentiveAsyncLogWriterTests.swift @@ -0,0 +1,108 @@ +// +// AppptentiveAsyncLogWriterTests.swift +// ApptentiveTests +// +// Created by Alex Lementuev on 2/22/18. +// Copyright © 2018 Apptentive, Inc. All rights reserved. +// + +import XCTest + +class AppptentiveAsyncLogWriterTests: XCTestCase { + + let destDir = (NSTemporaryDirectory() as NSString).appendingPathComponent("logs") + + override func setUp() { + super.setUp() + do { + if FileManager.default.fileExists(atPath: destDir) { + try FileManager.default.removeItem(atPath: destDir) + } + } catch { + XCTFail("Unable to delete dir: \(destDir)") + } + } + + func testExample() { + var writer: MockAsyncLogWriter! + + // create the first writer and output some Unicode text (Hiragana characters) + writer = MockAsyncLogWriter(destDir: destDir, historySize: 3); + writer.logMessage("あ"); + writer.logMessage("い"); + writer.logMessage("う"); + + assertFiles(listLogFiles(destDir), ["あ\nい\nう\n"]); + + // create the second writer and output more text + writer = MockAsyncLogWriter(destDir: destDir, historySize: 3); + writer.logMessage("1"); + writer.logMessage("2"); + writer.logMessage("3"); + + assertFiles(listLogFiles(destDir), ["あ\nい\nう\n", "1\n2\n3\n"]); + + // create the third writer and output more text + writer = MockAsyncLogWriter(destDir: destDir, historySize: 3); + writer.logMessage("4"); + writer.logMessage("5"); + writer.logMessage("6"); + + assertFiles(listLogFiles(destDir), ["あ\nい\nう\n", "1\n2\n3\n", "4\n5\n6\n"]); + + // create the fourth writer and output more text + writer = MockAsyncLogWriter(destDir: destDir, historySize: 3); + writer.logMessage("7"); + writer.logMessage("8"); + writer.logMessage("9"); + + // truncation should appear + assertFiles(listLogFiles(destDir), ["1\n2\n3\n", "4\n5\n6\n", "7\n8\n9\n"]); + + // create the fifth writer and output more text + writer = MockAsyncLogWriter(destDir: destDir, historySize: 3); + writer.logMessage("10"); + writer.logMessage("11"); + writer.logMessage("12"); + + // truncation should appear + assertFiles(listLogFiles(destDir), ["4\n5\n6\n", "7\n8\n9\n", "10\n11\n12\n"]); + + // create the sixth writer and output more text + writer = MockAsyncLogWriter(destDir: destDir, historySize: 3); + writer.logMessage("13"); + writer.logMessage("14"); + writer.logMessage("15"); + + // truncation should appear + assertFiles(listLogFiles(destDir), ["7\n8\n9\n", "10\n11\n12\n", "13\n14\n15\n"]); + } + + func assertFiles(_ files: [String], _ actual: [String]) { + let expected = files.map() { contentsOfFile(atPath: $0)! } + XCTAssertEqual(expected, actual) + } + + func listLogFiles(_ destDir: String) -> [String] { + do { + let files = try FileManager.default.contentsOfDirectory(atPath: destDir) + return files.sorted().map() {(destDir as NSString).appendingPathComponent($0)} + } catch { + XCTFail("Unable to read files from: \(destDir)") + } + return [] + } + + class MockAsyncLogWriter : ApptentiveAsyncLogWriter { + static var nextId = 0 + + override init(destDir: String, historySize: UInt) { + super.init(destDir: destDir, historySize: historySize, queue: ApptentiveMockDispatchQueue()) + } + + override func createLogFilename() -> String { + MockAsyncLogWriter.nextId += 1 + return "\(MockAsyncLogWriter.nextId)-\(super.createLogFilename())" + } + } +} diff --git a/Apptentive/ApptentiveTests/ApptentiveConversationTests.m b/Apptentive/ApptentiveTests/ApptentiveConversationTests.m index c4e5c1527..32e9eeffe 100644 --- a/Apptentive/ApptentiveTests/ApptentiveConversationTests.m +++ b/Apptentive/ApptentiveTests/ApptentiveConversationTests.m @@ -308,6 +308,25 @@ - (void)testNSCoding { XCTAssertEqualWithAccuracy(testInteraction.lastInvoked.timeIntervalSince1970, [engagementTime timeIntervalSince1970], 0.01); } +- (void)testResetDeviceDiffs { + [self.conversation updateLastSentDevice]; + [self.conversation checkForDeviceDiffs]; + + XCTAssertNil(self.deviceDiffs); + + [self.conversation.device addCustomString:@"foo" withKey:@"bar"]; + [self.conversation checkForDeviceDiffs]; + + XCTAssertEqualObjects(self.deviceDiffs, @{ @"custom_data": @{ @"bar": @"foo"}}); + + self.deviceDiffs = nil; + [self.conversation.device addCustomString:@"bar" withKey:@"foo"]; + [self.conversation updateLastSentDevice]; + [self.conversation checkForDeviceDiffs]; + + XCTAssertNil(self.deviceDiffs); +} + #pragma mark - Conversation delegate - (void)conversation:(ApptentiveConversation *)conversation deviceDidChange:(NSDictionary *)diffs { diff --git a/Apptentive/ApptentiveTests/ApptentiveEngagementTests.m b/Apptentive/ApptentiveTests/ApptentiveEngagementTests.m deleted file mode 100644 index 0dfe6e232..000000000 --- a/Apptentive/ApptentiveTests/ApptentiveEngagementTests.m +++ /dev/null @@ -1,575 +0,0 @@ -// -// ApptentiveEngagementTests.m -// Apptentive -// -// Created by Peter Kamb on 9/5/13. -// Copyright (c) 2013 Apptentive, Inc. All rights reserved. -// - -#import "Apptentive+Debugging.h" -#import "Apptentive.h" -#import "ApptentiveAppRelease.h" -#import "ApptentiveBackend+Engagement.h" -#import "ApptentiveBackend+Engagement.h" -#import "ApptentiveConversation.h" -#import "ApptentiveEngagement.h" -#import "ApptentiveInteraction.h" -#import "ApptentiveInteractionInvocation.h" -#import "ApptentiveInteractionUsageData.h" -#import "ApptentiveMessageManager.h" -#import "ApptentiveSDK.h" -#import "ApptentiveUtilities.h" -#import "ApptentiveVersion.h" -#import -#import "ApptentiveDispatchQueue.h" - - -@interface ApptentiveEngagementTests : XCTestCase -@end - - -@implementation ApptentiveEngagementTests - -/* - time_at_install/total - When the app was installed (NSDate, using $before or $after for comparison) - time_at_install/version - When the app was upgraded (NSDate, using $before or $after for comparison) - time_at_install/build - When the app was upgraded (NSDate, using $before or $after for comparison) - - application_version - The currently running application version (string). - application_build - The currently running application build "number" (string). - current_time - The current time as a numeric Unix timestamp in seconds. - - app_release/version - The currently running application version (string). - app_release/build - The currently running application build "number" (string). - app_release/debug - Whether the currently running application is a debug build (boolean). - - sdk/version - The currently running SDK version (string). - sdk/distribution - The current SDK distribution, if available (string). - sdk/distribution_version - The current version of the SDK distribution, if available (string). - - is_update/cf_bundle_short_version_string - Returns true if we have seen a version prior to the current one. - is_update/cf_bundle_version - Returns true if we have seen a build prior to the current one. - - code_point.code_point_name.invokes.total - The total number of times code_point_name has been invoked across all versions of the app (regardless if an Interaction was shown at that point) (integer) - code_point.code_point_name.invokes.version - The number of times code_point_name has been invoked in the current version of the app (regardless if an Interaction was shown at that point) (integer) - interactions.interaction_instance_id.invokes.total - The number of times the Interaction Instance with id interaction_instance_id has been invoked (irrespective of app version) (integer) - interactions.interaction_instance_id.invokes.version - The number of times the Interaction Instance with id interaction_instance_id has been invoked within the current version of the app (integer) - */ - -- (void)setUp { - [super setUp]; - - NSString *path = [[[ApptentiveUtilities applicationSupportPath] stringByAppendingPathComponent:@"com.apptentive.feedback"] stringByAppendingPathComponent:@"conversation-v1.meta"]; - - [[NSFileManager defaultManager] removeItemAtPath:path error:NULL]; -} - -- (void)testEventLabelsContainingCodePointSeparatorCharacters { - //Escape "%", "/", and "#". - - NSString *i, *o; - i = @"testEventLabelSeparators"; - o = @"testEventLabelSeparators"; - XCTAssertTrue([[ApptentiveBackend stringByEscapingCodePointSeparatorCharactersInString:i] isEqualToString:o], @"Test escaping code point separator characters from event labels."); - - i = @"test#Event#Label#Separators"; - o = @"test%23Event%23Label%23Separators"; - XCTAssertTrue([[ApptentiveBackend stringByEscapingCodePointSeparatorCharactersInString:i] isEqualToString:o], @"Test escaping code point separator characters from event labels."); - - i = @"test/Event/Label/Separators"; - o = @"test%2FEvent%2FLabel%2FSeparators"; - XCTAssertTrue([[ApptentiveBackend stringByEscapingCodePointSeparatorCharactersInString:i] isEqualToString:o], @"Test escaping code point separator characters from event labels."); - - i = @"test%Event/Label#Separators"; - o = @"test%25Event%2FLabel%23Separators"; - XCTAssertTrue([[ApptentiveBackend stringByEscapingCodePointSeparatorCharactersInString:i] isEqualToString:o], @"Test escaping code point separator characters from event labels."); - - i = @"test#Event/Label%Separators"; - o = @"test%23Event%2FLabel%25Separators"; - XCTAssertTrue([[ApptentiveBackend stringByEscapingCodePointSeparatorCharactersInString:i] isEqualToString: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 stringByEscapingCodePointSeparatorCharactersInString:i] isEqualToString: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 stringByEscapingCodePointSeparatorCharactersInString:i] isEqualToString:o], @"Test escaping code point separator characters from event labels."); - - i = @"test%/#"; - o = @"test%25%2F%23"; - XCTAssertTrue([[ApptentiveBackend stringByEscapingCodePointSeparatorCharactersInString:i] isEqualToString:o], @"Test escaping code point separator characters from event labels."); -} - -- (void)testInteractionCriteria { - ApptentiveInteractionInvocation *invocation = [[ApptentiveInteractionInvocation alloc] init]; - invocation.criteria = @{ @"time_at_install/total": @{@"$before": @(-5 * 60 * 60 * 24), @"$after": @(-7 * 60 * 60 * 24)} }; - - ApptentiveConversation *conversation = [[ApptentiveConversation alloc] initWithState:ApptentiveConversationStateAnonymous]; - - [conversation.appRelease setValue:[NSDate dateWithTimeIntervalSinceNow:-6 * 60 * 60 * 24] forKey:@"timeAtInstallTotal"]; - [conversation.appRelease setValue:[NSDate dateWithTimeIntervalSinceNow:-6 * 60 * 60 * 24] forKey:@"timeAtInstallVersion"]; - [conversation.appRelease setValue:@NO forKey:@"updateVersion"]; - [conversation.appRelease setValue:@NO forKey:@"updateBuild"]; - - [conversation.appRelease setValue:[[ApptentiveVersion alloc] initWithString:@"1.8.9"] forKey:@"version"]; - [conversation.appRelease setValue:[[ApptentiveVersion alloc] initWithString:@"39"] forKey:@"build"]; - - XCTAssertTrue([invocation criteriaAreMetForConversation:conversation], @"Install date"); -} - -- (void)testUnknownKeyInCriteria { - ApptentiveInteractionInvocation *invocation = [[ApptentiveInteractionInvocation alloc] init]; - invocation.criteria = @{ @"time_at_install/total": @{@"$before": @(6 * 60 * 60 * 24)}, - @"time_at_install/cf_bundle_short_version_string": @{@"$before": @(6 * 60 * 60 * 24)} }; - - ApptentiveConversation *conversation = [[ApptentiveConversation alloc] initWithState:ApptentiveConversationStateAnonymous]; - - [conversation.appRelease setValue:[NSDate dateWithTimeIntervalSinceNow:-6 * 60 * 60 * 24] forKey:@"timeAtInstallTotal"]; - [conversation.appRelease setValue:[NSDate dateWithTimeIntervalSinceNow:-6 * 60 * 60 * 24] forKey:@"timeAtInstallVersion"]; - [conversation.appRelease setValue:@NO forKey:@"updateVersion"]; - [conversation.appRelease setValue:@NO forKey:@"updateBuild"]; - - [conversation.appRelease setValue:[[ApptentiveVersion alloc] initWithString:@"1.8.9"] forKey:@"version"]; - [conversation.appRelease setValue:[[ApptentiveVersion alloc] initWithString:@"39"] forKey:@"build"]; - - XCTAssertTrue([invocation criteriaAreMetForConversation:conversation], @"All keys are known, thus the criteria is met."); - - invocation.criteria = @{ @"time_since_install/total": @6, - @"unknown_key": @"criteria_should_not_be_met" }; - XCTAssertFalse([invocation criteriaAreMetForConversation:conversation], @"Criteria should not be met if the criteria includes a key that the client does not recognize."); - - invocation.criteria = @{ @6: @"this is weird" }; - XCTAssertFalse([invocation criteriaAreMetForConversation:conversation], @"Criteria should not be met if the criteria includes a key that the client does not recognize."); -} - -- (void)testEmptyCriteria { - ApptentiveInteractionInvocation *invocation = [[ApptentiveInteractionInvocation alloc] init]; - ApptentiveInteractionUsageData *usageData = [[ApptentiveInteractionUsageData alloc] init]; - - invocation.criteria = nil; - XCTAssertFalse([invocation criteriaAreMetForConversation:usageData.conversation], @"Dictionary with nil criteria should evaluate to False."); - - invocation.criteria = @{[NSNull null]: [NSNull null]}; - XCTAssertFalse([invocation criteriaAreMetForConversation:usageData.conversation], @"Dictionary with Null criteria should evaluate to False."); - - invocation.criteria = @{}; - XCTAssertTrue([invocation criteriaAreMetForConversation:usageData.conversation], @"Empty criteria dictionary with no keys should evaluate to True."); - - invocation.criteria = @{ @"": @6 }; - XCTAssertFalse([invocation criteriaAreMetForConversation:usageData.conversation], @"Criteria with a key that is an empty string should fail (if usage data does not match)."); -} - -- (void)testInteractionCriteriaDaysSnceInstall { - ApptentiveInteractionInvocation *invocation = [[ApptentiveInteractionInvocation alloc] init]; - - ApptentiveConversation *conversation = [[ApptentiveConversation alloc] initWithState:ApptentiveConversationStateAnonymous]; - - NSTimeInterval dayTimeInterval = 60 * 60 * 24; - - invocation.criteria = @{ @"time_at_install/total": @{@"$before": @(-6 * dayTimeInterval)} }; - [conversation.appRelease setValue:[NSDate dateWithTimeIntervalSinceNow:-7 * dayTimeInterval] forKey:@"timeAtInstallTotal"]; - XCTAssertTrue([invocation criteriaAreMetForConversation:conversation], @"Install date"); - [conversation.appRelease setValue:[NSDate dateWithTimeIntervalSinceNow:-5 * dayTimeInterval] forKey:@"timeAtInstallTotal"]; - XCTAssertFalse([invocation criteriaAreMetForConversation:conversation], @"Install date"); - - invocation.criteria = @{ @"time_at_install/total": @{@"$before": @(-5 * dayTimeInterval), @"$after": @(-7 * dayTimeInterval)} }; - [conversation.appRelease setValue:[NSDate dateWithTimeIntervalSinceNow:-6 * dayTimeInterval] forKey:@"timeAtInstallTotal"]; - XCTAssertTrue([invocation criteriaAreMetForConversation:conversation], @"Install date"); - [conversation.appRelease setValue:[NSDate dateWithTimeIntervalSinceNow:-4.999 * dayTimeInterval] forKey:@"timeAtInstallTotal"]; - XCTAssertFalse([invocation criteriaAreMetForConversation:conversation], @"Install date"); - [conversation.appRelease setValue:[NSDate dateWithTimeIntervalSinceNow:-7 * dayTimeInterval] forKey:@"timeAtInstallTotal"]; - XCTAssertFalse([invocation criteriaAreMetForConversation:conversation], @"Install date"); -} - -- (void)testInteractionCriteriaDebug { - ApptentiveInteractionInvocation *invocation = [[ApptentiveInteractionInvocation alloc] init]; - ApptentiveConversation *conversation = [[ApptentiveConversation alloc] initWithState:ApptentiveConversationStateAnonymous]; - -// Debug default to false -#if APPTENTIVE_DEBUG - invocation.criteria = @{ @"application/debug": @YES }; -#else - invocation.criteria = @{ @"application/debug": @NO }; -#endif - - XCTAssertTrue([invocation criteriaAreMetForConversation:conversation], @"Debug boolean"); - -#if APPTENTIVE_DEBUG - invocation.criteria = @{ @"application/debug": @NO }; -#else - invocation.criteria = @{ @"application/debug": @YES }; -#endif - - XCTAssertFalse([invocation criteriaAreMetForConversation:conversation], @"Debug boolean"); -} - -- (void)testInteractionCriteriaVersion { - ApptentiveInteractionInvocation *invocation = [[ApptentiveInteractionInvocation alloc] init]; - ApptentiveInteractionUsageData *usageData = [[ApptentiveInteractionUsageData alloc] initWithConversation:[[ApptentiveConversation alloc] initWithState:ApptentiveConversationStateAnonymous]]; - - invocation.criteria = @{ @"application/cf_bundle_short_version_string": [Apptentive versionObjectWithVersion:@"1.2.8"] }; - [usageData.conversation.appRelease setValue:[[ApptentiveVersion alloc] initWithString:@"1.2.8"] forKey:@"version"]; - XCTAssertTrue([invocation criteriaAreMetForConversation:usageData.conversation], @"Version number"); - [usageData.conversation.appRelease setValue:[[ApptentiveVersion alloc] initWithString:@"v1.2.8"] forKey:@"version"]; - XCTAssertFalse([invocation criteriaAreMetForConversation:usageData.conversation], @"Version number must not have a 'v' in front!"); - - invocation.criteria = @{ @"application/version": [Apptentive versionObjectWithVersion:@"1.2.8"] }; - XCTAssertFalse([invocation criteriaAreMetForConversation:usageData.conversation], @"application/version not a valid key"); - - invocation.criteria = @{ @"application/version_code": [Apptentive versionObjectWithVersion:@"1.2.8"] }; - XCTAssertFalse([invocation criteriaAreMetForConversation:usageData.conversation], @"application/version not a valid key"); -} - -- (void)testInteractionCriteriaBuild { - ApptentiveInteractionInvocation *invocation = [[ApptentiveInteractionInvocation alloc] init]; - ApptentiveInteractionUsageData *usageData = [[ApptentiveInteractionUsageData alloc] initWithConversation:[[ApptentiveConversation alloc] initWithState:ApptentiveConversationStateAnonymous]]; - - invocation.criteria = @{ @"application/cf_bundle_version": [Apptentive versionObjectWithVersion:@"39"] }; - [usageData.conversation.appRelease setValue:[[ApptentiveVersion alloc] initWithString:@"39"] forKey:@"build"]; - - XCTAssertTrue([invocation criteriaAreMetForConversation:usageData.conversation], @"Build number"); - - [usageData.conversation.appRelease setValue:[[ApptentiveVersion alloc] initWithString:@"v39"] forKey:@"build"]; - XCTAssertFalse([invocation criteriaAreMetForConversation:usageData.conversation], @"Build number must not have a 'v' in front!"); - - [usageData.conversation.appRelease setValue:[[ApptentiveVersion alloc] initWithString:@"3.0"] forKey:@"build"]; - XCTAssertFalse([invocation criteriaAreMetForConversation:usageData.conversation], @"Build number must not have a 'v' in front!"); - - invocation.criteria = @{ @"application/cf_bundle_version": [Apptentive versionObjectWithVersion:@"3.0"] }; - [usageData.conversation.appRelease setValue:[[ApptentiveVersion alloc] initWithString:@"3.0"] forKey:@"build"]; - XCTAssertTrue([invocation criteriaAreMetForConversation:usageData.conversation], @"Build number"); - - [usageData.conversation.appRelease setValue:[[ApptentiveVersion alloc] initWithString:@"v3.0"] forKey:@"build"]; - XCTAssertFalse([invocation criteriaAreMetForConversation:usageData.conversation], @"Build number must not have a 'v' in front!"); - - invocation.criteria = @{ @"application/cf_bundle_version": @{@"$contains": @3.0} }; - XCTAssertFalse([invocation criteriaAreMetForConversation:usageData.conversation], @"Should fail with invalid types."); - - [usageData.conversation.appRelease setValue:[[ApptentiveVersion alloc] initWithString:@"3.0"] forKey:@"build"]; - invocation.criteria = @{ @"application/build": [Apptentive versionObjectWithVersion:@"3.0.0"] }; - XCTAssertFalse([invocation criteriaAreMetForConversation:usageData.conversation], @"application/build not a valid key"); -} - -- (void)testInteractionCriteriaSDK { - ApptentiveInteractionInvocation *invocation = [[ApptentiveInteractionInvocation alloc] init]; - ApptentiveInteractionUsageData *usageData = [[ApptentiveInteractionUsageData alloc] initWithConversation:[[ApptentiveConversation alloc] initWithState:ApptentiveConversationStateAnonymous]]; - - invocation.criteria = @{ @"sdk/version": [Apptentive versionObjectWithVersion:@"1.4.2"] }; - [usageData.conversation.SDK setValue:[[ApptentiveVersion alloc] initWithString:@"1.4.2"] forKey:@"version"]; - XCTAssertTrue([invocation criteriaAreMetForConversation:usageData.conversation], @"SDK Version should be 1.4.2"); - - [usageData.conversation.SDK setValue:[[ApptentiveVersion alloc] initWithString:@"1.4"] forKey:@"version"]; - XCTAssertFalse([invocation criteriaAreMetForConversation:usageData.conversation], @"SDK Version isn't 1.4"); - - [usageData.conversation.SDK setValue:[[ApptentiveVersion alloc] initWithString:@"1.5.0"] forKey:@"version"]; - XCTAssertFalse([invocation criteriaAreMetForConversation:usageData.conversation], @"SDK Version isn't 1.5.0"); - - invocation.criteria = @{ @"sdk/version": @{@"$contains": @3.0} }; - XCTAssertFalse([invocation criteriaAreMetForConversation:usageData.conversation], @"Should fail with invalid types."); -} - -- (void)testInteractionCriteriaCurrentTime { - ApptentiveInteractionInvocation *invocation = [[ApptentiveInteractionInvocation alloc] init]; - ApptentiveInteractionUsageData *usageData = [[ApptentiveInteractionUsageData alloc] initWithConversation:[[ApptentiveConversation alloc] initWithState:ApptentiveConversationStateAnonymous]]; - - invocation.criteria = @{ @"current_time": @{@"$exists": @YES} }; - XCTAssertTrue([invocation criteriaAreMetForConversation:usageData.conversation], @"Must have default current time."); - // Make sure it's actually a reasonable value… - NSTimeInterval currentTimestamp = [[NSDate date] timeIntervalSince1970]; - NSTimeInterval timestamp = [usageData.conversation.currentTime timeIntervalSince1970]; - XCTAssertTrue(timestamp < (currentTimestamp + 0.5) && timestamp > (currentTimestamp - 0.5), @"Current time not a believable value."); - - invocation.criteria = @{ @"current_time": @{@"$gt": [Apptentive timestampObjectWithDate:[NSDate dateWithTimeIntervalSinceNow:-5]]} }; - XCTAssertTrue([invocation criteriaAreMetForConversation:usageData.conversation], @"Current time criteria not met."); - - invocation.criteria = @{ @"current_time": @{@"$lt": [Apptentive timestampObjectWithDate:[NSDate dateWithTimeIntervalSinceNow:0.5]], @"$gt": [Apptentive timestampObjectWithDate:[NSDate dateWithTimeIntervalSinceNow:-0.5]]} }; - XCTAssertTrue([invocation criteriaAreMetForConversation:usageData.conversation], @"Current time criteria not met."); - - invocation.criteria = @{ @"current_time": @{@"$gt": @"1183135260"} }; - XCTAssertFalse([invocation criteriaAreMetForConversation:usageData.conversation], @"Should fail because of type but not crash."); - - invocation.criteria = @{ @"current_time": @"1397598109" }; - XCTAssertFalse([invocation criteriaAreMetForConversation:usageData.conversation], @"Should fail with invalid types."); -} - -- (void)testCodePointInvokesVersion { - ApptentiveInteractionInvocation *invocation = [[ApptentiveInteractionInvocation alloc] init]; - ApptentiveInteractionUsageData *usageData = [[ApptentiveInteractionUsageData alloc] initWithConversation:[[ApptentiveConversation alloc] initWithState:ApptentiveConversationStateAnonymous]]; - - [usageData.conversation warmCodePoint:@"app.launch"]; - invocation.criteria = @{ @"code_point/app.launch/invokes/cf_bundle_short_version_string": @1 }; - [usageData.conversation engageCodePoint:@"app.launch"]; - XCTAssertTrue([invocation criteriaAreMetForConversation:usageData.conversation], @"This version has been invoked 1 time."); - [usageData.conversation.engagement resetBuild]; - XCTAssertTrue([invocation criteriaAreMetForConversation:usageData.conversation], @"Reset build should not affect version"); - - [usageData.conversation.engagement resetVersion]; - XCTAssertFalse([invocation criteriaAreMetForConversation:usageData.conversation], @"Codepoint version invokes."); - [usageData.conversation engageCodePoint:@"app.launch"]; - [usageData.conversation engageCodePoint:@"app.launch"]; - XCTAssertFalse([invocation criteriaAreMetForConversation:usageData.conversation], @"Codepoint version invokes."); - - // "version" has been replaced with "cf_bundle_short_version_string" - invocation.criteria = @{ @"interactions/big.win/invokes/version": @1 }; - XCTAssertFalse([invocation criteriaAreMetForConversation:usageData.conversation], @"Should fail with invalid key."); -} - -- (void)testCodePointInvokesBuild { - ApptentiveInteractionInvocation *invocation = [[ApptentiveInteractionInvocation alloc] init]; - ApptentiveInteractionUsageData *usageData = [[ApptentiveInteractionUsageData alloc] initWithConversation:[[ApptentiveConversation alloc] initWithState:ApptentiveConversationStateAnonymous]]; - - [usageData.conversation warmCodePoint:@"app.launch"]; - invocation.criteria = @{ @"code_point/app.launch/invokes/cf_bundle_version": @1 }; - [usageData.conversation engageCodePoint:@"app.launch"]; - XCTAssertTrue([invocation criteriaAreMetForConversation:usageData.conversation], @"This build has been invoked 1 time."); - [usageData.conversation.engagement resetVersion]; - XCTAssertTrue([invocation criteriaAreMetForConversation:usageData.conversation], @"Reset version should not affect version"); - - [usageData.conversation.engagement resetBuild]; - XCTAssertFalse([invocation criteriaAreMetForConversation:usageData.conversation], @"Codepoint build invokes."); - [usageData.conversation engageCodePoint:@"app.launch"]; - [usageData.conversation engageCodePoint:@"app.launch"]; - XCTAssertFalse([invocation criteriaAreMetForConversation:usageData.conversation], @"Codepoint build invokes."); - - // "build" has been replaced with "cf_bundle_version" - invocation.criteria = @{ @"interactions/big.win/invokes/build": @1 }; - XCTAssertFalse([invocation criteriaAreMetForConversation:usageData.conversation], @"Should fail with invalid key."); -} - -- (void)testInteractionInvokesVersion { - ApptentiveInteractionInvocation *invocation = [[ApptentiveInteractionInvocation alloc] init]; - ApptentiveInteractionUsageData *usageData = [[ApptentiveInteractionUsageData alloc] initWithConversation:[[ApptentiveConversation alloc] initWithState:ApptentiveConversationStateAnonymous]]; - - [usageData.conversation warmInteraction:@"526fe2836dd8bf546a00000b"]; - invocation.criteria = @{ @"interactions/526fe2836dd8bf546a00000b/invokes/cf_bundle_short_version_string": @(1) }; - [usageData.conversation engageInteraction:@"526fe2836dd8bf546a00000b"]; - XCTAssertTrue([invocation criteriaAreMetForConversation:usageData.conversation], @"This version has been invoked 1 time."); - [usageData.conversation.engagement resetBuild]; - XCTAssertTrue([invocation criteriaAreMetForConversation:usageData.conversation], @"Reset build should not affect version"); - - [usageData.conversation.engagement resetVersion]; - XCTAssertFalse([invocation criteriaAreMetForConversation:usageData.conversation], @"Interaction version invokes."); - [usageData.conversation engageInteraction:@"526fe2836dd8bf546a00000b"]; - [usageData.conversation engageInteraction:@"526fe2836dd8bf546a00000b"]; - XCTAssertFalse([invocation criteriaAreMetForConversation:usageData.conversation], @"Interaction version invokes."); - - // "version" has been replaced with "cf_bundle_short_version_string" - invocation.criteria = @{ @"interactions/526fe2836dd8bf546a00000b/invokes/version": @1 }; - XCTAssertFalse([invocation criteriaAreMetForConversation:usageData.conversation], @"Should fail with invalid key."); -} - -- (void)testInteractionInvokesBuild { - ApptentiveInteractionInvocation *invocation = [[ApptentiveInteractionInvocation alloc] init]; - ApptentiveInteractionUsageData *usageData = [[ApptentiveInteractionUsageData alloc] initWithConversation:[[ApptentiveConversation alloc] initWithState:ApptentiveConversationStateAnonymous]]; - - [usageData.conversation warmInteraction:@"526fe2836dd8bf546a00000b"]; - invocation.criteria = @{ @"interactions/526fe2836dd8bf546a00000b/invokes/cf_bundle_version": @(1) }; - [usageData.conversation engageInteraction:@"526fe2836dd8bf546a00000b"]; - XCTAssertTrue([invocation criteriaAreMetForConversation:usageData.conversation], @"This version has been invoked 1 time."); - [usageData.conversation.engagement resetVersion]; - XCTAssertTrue([invocation criteriaAreMetForConversation:usageData.conversation], @"Reset build should not affect version"); - - [usageData.conversation.engagement resetBuild]; - XCTAssertFalse([invocation criteriaAreMetForConversation:usageData.conversation], @"Interaction build invokes."); - [usageData.conversation engageInteraction:@"526fe2836dd8bf546a00000b"]; - [usageData.conversation engageInteraction:@"526fe2836dd8bf546a00000b"]; - XCTAssertFalse([invocation criteriaAreMetForConversation:usageData.conversation], @"Interaction build invokes."); - - // "build" has been replaced with "cf_bundle_version" - invocation.criteria = @{ @"interactions/526fe2836dd8bf546a00000b/invokes/build": @1 }; - XCTAssertFalse([invocation criteriaAreMetForConversation:usageData.conversation], @"Should fail with invalid key."); -} - -- (void)testUpgradeMessageCriteria { - ApptentiveInteractionInvocation *invocation = [[ApptentiveInteractionInvocation alloc] init]; - ApptentiveInteractionUsageData *usageData = [[ApptentiveInteractionUsageData alloc] initWithConversation:[[ApptentiveConversation alloc] initWithState:ApptentiveConversationStateAnonymous]]; - - invocation.criteria = @{ @"code_point/app.launch/invokes/cf_bundle_short_version_string": @1, - @"application/cf_bundle_short_version_string": [Apptentive versionObjectWithVersion:@"1.3.0"], - @"application/cf_bundle_version": [Apptentive versionObjectWithVersion:@"39"] }; - [usageData.conversation warmCodePoint:@"app.launch"]; - [usageData.conversation engageCodePoint:@"app.launch"]; - - [usageData.conversation.appRelease setValue:[[ApptentiveVersion alloc] initWithString:@"1.3.0"] forKey:@"version"]; - XCTAssertFalse([invocation criteriaAreMetForConversation:usageData.conversation], @"Test Upgrade Message without build number."); - [usageData.conversation.appRelease setValue:[[ApptentiveVersion alloc] initWithString:@"39"] forKey:@"build"]; - XCTAssertTrue([invocation criteriaAreMetForConversation:usageData.conversation], @"Test Upgrade Message."); - - [usageData.conversation.appRelease setValue:[[ApptentiveVersion alloc] initWithString:@"1.3.1"] forKey:@"version"]; - XCTAssertFalse([invocation criteriaAreMetForConversation:usageData.conversation], @"Test Upgrade Message."); - - invocation.criteria = @{ @"application/cf_bundle_short_version_string": [Apptentive versionObjectWithVersion:@"1.3.1"], - @"code_point/app.launch/invokes/cf_bundle_short_version_string": @{@"$gte": @1} }; - - XCTAssertTrue([invocation criteriaAreMetForConversation:usageData.conversation], @"Test Upgrade Message."); - [usageData.conversation engageCodePoint:@"app.launch"]; - XCTAssertTrue([invocation criteriaAreMetForConversation:usageData.conversation], @"Test Upgrade Message."); - - invocation.criteria = @{ @"application/cf_bundle_short_version_string": [Apptentive versionObjectWithVersion:@"1.3.1"], - @"code_point/app.launch/invokes/cf_bundle_short_version_string": @{@"$lte": @3} }; - [usageData.conversation engageCodePoint:@"app.launch"]; - XCTAssertTrue([invocation criteriaAreMetForConversation:usageData.conversation], @"Test Upgrade Message."); - [usageData.conversation engageCodePoint:@"app.launch"]; - XCTAssertFalse([invocation criteriaAreMetForConversation:usageData.conversation], @"Test Upgrade Message."); - - invocation.criteria = @{ @"code_point/app.launch/invokes/cf_bundle_short_version_string": @[@1], - @"application_version": @"1.3.1", - @"application_build": @"39" }; - - [Apptentive.shared.backend.conversationManager.activeConversation.engagement resetVersion]; - XCTAssertFalse([invocation criteriaAreMetForConversation:usageData.conversation], @"Should fail with invalid types."); -} - -- (void)testIsUpdateVersionsAndBuilds { - ApptentiveInteractionInvocation *invocation = [[ApptentiveInteractionInvocation alloc] init]; - ApptentiveInteractionUsageData *usageData = [[ApptentiveInteractionUsageData alloc] initWithConversation:[[ApptentiveConversation alloc] initWithState:ApptentiveConversationStateAnonymous]]; - - //Version - invocation.criteria = @{ @"is_update/cf_bundle_short_version_string": @YES }; - [usageData.conversation.appRelease setValue:@YES forKey:@"updateVersion"]; - XCTAssertTrue([invocation criteriaAreMetForConversation:usageData.conversation], @"Test isUpdate"); - - invocation.criteria = @{ @"is_update/cf_bundle_short_version_string": @NO }; - [usageData.conversation.appRelease setValue:@NO forKey:@"updateVersion"]; - XCTAssertTrue([invocation criteriaAreMetForConversation:usageData.conversation], @"Test isUpdate"); - - invocation.criteria = @{ @"is_update/cf_bundle_short_version_string": @YES }; - XCTAssertFalse([invocation criteriaAreMetForConversation:usageData.conversation], @"Test isUpdate"); - - invocation.criteria = @{ @"is_update/cf_bundle_short_version_string": @NO }; - [usageData.conversation.appRelease setValue:@YES forKey:@"updateVersion"]; - XCTAssertFalse([invocation criteriaAreMetForConversation:usageData.conversation], @"Test isUpdate"); - - //Build - invocation.criteria = @{ @"is_update/cf_bundle_version": @YES }; - [usageData.conversation.appRelease setValue:@YES forKey:@"updateBuild"]; - XCTAssertTrue([invocation criteriaAreMetForConversation:usageData.conversation], @"Test isUpdate"); - - invocation.criteria = @{ @"is_update/cf_bundle_version": @NO }; - [usageData.conversation.appRelease setValue:@NO forKey:@"updateBuild"]; - XCTAssertTrue([invocation criteriaAreMetForConversation:usageData.conversation], @"Test isUpdate"); - - invocation.criteria = @{ @"is_update/cf_bundle_version": @YES }; - XCTAssertFalse([invocation criteriaAreMetForConversation:usageData.conversation], @"Test isUpdate"); - - invocation.criteria = @{ @"is_update/cf_bundle_version": @NO }; - [usageData.conversation.appRelease setValue:@YES forKey:@"updateBuild"]; - XCTAssertFalse([invocation criteriaAreMetForConversation:usageData.conversation], @"Test isUpdate"); - - - invocation.criteria = @{ @"is_update/cf_bundle_version": @[[NSNull null]] }; - [usageData.conversation.appRelease setValue:@NO forKey:@"updateBuild"]; - XCTAssertFalse([invocation criteriaAreMetForConversation:usageData.conversation], @"Should fail with invalid types."); - invocation.criteria = @{ @"is_update/cf_bundle_version": @{@"$gt": @"lajd;fl ajsd;flj"} }; - [usageData.conversation.appRelease setValue:@NO forKey:@"updateBuild"]; - XCTAssertFalse([invocation criteriaAreMetForConversation:usageData.conversation], @"Should fail with invalid types."); - - [usageData.conversation.appRelease setValue:@NO forKey:@"updateVersion"]; - [usageData.conversation.appRelease setValue:@NO forKey:@"updateBuild"]; - invocation.criteria = @{ @"is_update/version_code": @NO }; - XCTAssertFalse([invocation criteriaAreMetForConversation:usageData.conversation], @"Should fail with invalid key."); - - invocation.criteria = @{ @"is_update/version": @NO }; - XCTAssertFalse([invocation criteriaAreMetForConversation:usageData.conversation], @"Should fail with invalid key."); - - invocation.criteria = @{ @"is_update/build": @NO }; - XCTAssertFalse([invocation criteriaAreMetForConversation:usageData.conversation], @"Should fail with invalid key."); -} - -- (void)testEnjoymentDialogCriteria { - ApptentiveConfiguration *configuration = [ApptentiveConfiguration configurationWithApptentiveKey:@"app-key" apptentiveSignature:@"app-signature"]; - [Apptentive registerWithConfiguration:configuration]; - - Apptentive.shared.logLevel = ApptentiveLogLevelVerbose; - - XCTestExpectation *expectation = [self expectationWithDescription:@"Backend stood up"]; - - [Apptentive.shared.backend.operationQueue dispatchAsync:^{ - Apptentive.shared.localInteractionsURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"testInteractions" withExtension:@"json"]; - - [Apptentive.shared.backend.conversationManager.activeConversation warmCodePoint:@"local#app#init"]; - [Apptentive.shared.backend.conversationManager.activeConversation engageCodePoint:@"local#app#init"]; - [Apptentive.shared.backend.conversationManager.activeConversation engageCodePoint:@"local#app#init"]; - [Apptentive.shared.backend.conversationManager.activeConversation engageCodePoint:@"local#app#init"]; - [Apptentive.shared.backend.conversationManager.activeConversation engageCodePoint:@"local#app#init"]; - [Apptentive.shared.backend.conversationManager.activeConversation engageCodePoint:@"local#app#init"]; - [Apptentive.shared.backend.conversationManager.activeConversation engageCodePoint:@"local#app#init"]; - [Apptentive.shared.backend.conversationManager.activeConversation engageCodePoint:@"local#app#init"]; - [Apptentive.shared.backend.conversationManager.activeConversation engageCodePoint:@"local#app#init"]; - [Apptentive.shared.backend.conversationManager.activeConversation engageCodePoint:@"local#app#init"]; - - [Apptentive.shared.backend.conversationManager.activeConversation.appRelease setValue:[NSDate dateWithTimeIntervalSinceNow:-863999] forKey:@"timeAtInstallTotal"]; - - [Apptentive.shared.backend.conversationManager.activeConversation warmCodePoint:@"local#app#testRatingFlow"]; - [Apptentive.shared.backend.conversationManager.activeConversation engageCodePoint:@"local#app#testRatingFlow"]; - [Apptentive.shared.backend.conversationManager.activeConversation engageCodePoint:@"local#app#testRatingFlow"]; - [Apptentive.shared.backend.conversationManager.activeConversation engageCodePoint:@"local#app#testRatingFlow"]; - [Apptentive.shared.backend.conversationManager.activeConversation engageCodePoint:@"local#app#testRatingFlow"]; - [Apptentive.shared.backend.conversationManager.activeConversation engageCodePoint:@"local#app#testRatingFlow"]; - [Apptentive.shared.backend.conversationManager.activeConversation engageCodePoint:@"local#app#testRatingFlow"]; - [Apptentive.shared.backend.conversationManager.activeConversation engageCodePoint:@"local#app#testRatingFlow"]; - [Apptentive.shared.backend.conversationManager.activeConversation engageCodePoint:@"local#app#testRatingFlow"]; - [Apptentive.shared.backend.conversationManager.activeConversation engageCodePoint:@"local#app#testRatingFlow"]; - - [Apptentive.shared.backend.conversationManager.activeConversation warmInteraction:@"533ed97a7724c5457e00003f"]; - - [Apptentive.shared queryCanShowInteractionForEvent:@"testRatingFlow" completion:^(BOOL canShowInteraction) { - XCTAssertFalse(canShowInteraction, @"The OR clauses are failing."); - - [Apptentive.shared.backend.conversationManager.activeConversation engageCodePoint:@"local#app#init"]; - [Apptentive.shared.backend.conversationManager.activeConversation engageCodePoint:@"local#app#init"]; - - [Apptentive.shared.backend.conversationManager.activeConversation engageCodePoint:@"local#app#testRatingFlow"]; - [Apptentive.shared.backend.conversationManager.activeConversation engageCodePoint:@"local#app#testRatingFlow"]; - - [Apptentive.shared queryCanShowInteractionForEvent:@"testRatingFlow" completion:^(BOOL canShowInteraction) { - XCTAssertTrue(canShowInteraction, @"One of the OR clauses is true. The other ANDed clause is also true. Should work."); - - [Apptentive.shared.backend.conversationManager.activeConversation.appRelease setValue:[NSDate dateWithTimeIntervalSinceNow:-864001] forKey:@"timeAtInstallTotal"]; - [Apptentive.shared queryCanShowInteractionForEvent:@"testRatingFlow" completion:^(BOOL canShowInteraction) { - XCTAssertTrue(canShowInteraction, @"All of the OR clauses are true. The other ANDed clause is also true. Should work."); - - [Apptentive.shared.backend.conversationManager.activeConversation engageInteraction:@"533ed97a7724c5457e00003f"]; - [Apptentive.shared queryCanShowInteractionForEvent:@"testRatingFlow" completion:^(BOOL canShowInteraction) { - XCTAssertFalse(canShowInteraction, @"All the OR clauses are true. The other ANDed clause is not true. Should fail."); - - [expectation fulfill]; - }]; - }]; - }]; - }]; - }]; - - [self waitForExpectationsWithTimeout:5 handler:nil]; -} - -- (void)testCanShowInteractionForEvent { - // TODO: create synchronous backend initializer - ApptentiveConfiguration *configuration = [ApptentiveConfiguration configurationWithApptentiveKey:@"app-key" apptentiveSignature:@"app-signature"]; - [Apptentive registerWithConfiguration:configuration]; // trigger creation of engagement backend - - XCTestExpectation *expectation = [self expectationWithDescription:@"Backend stood up"]; - - [Apptentive.shared.backend.operationQueue dispatchAsync:^{ - [Apptentive.shared.backend.conversationManager.activeConversation setValue:@"abc123" forKey:@"token"]; - [Apptentive.shared.backend.conversationManager.activeConversation setValue:@"abc123" forKey:@"identifier"]; - - [Apptentive.shared.backend.conversationManager createMessageManagerForConversation:Apptentive.shared.backend.conversationManager.activeConversation]; - Apptentive.shared.localInteractionsURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"testInteractions" withExtension:@"json"]; - - [Apptentive.shared queryCanShowInteractionForEvent:@"canShow" completion:^(BOOL canShowInteraction) { - XCTAssertTrue(canShowInteraction, @"If invocation is valid, it will be shown for the next targeted event."); - - [Apptentive.shared queryCanShowInteractionForEvent:@"cannotShow" completion:^(BOOL canShowInteraction) { - XCTAssertFalse(canShowInteraction, @"If invocation is not valid, it will not be shown for the next targeted event."); - - [expectation fulfill]; - }]; - }]; - }]; - - [self waitForExpectationsWithTimeout:5 handler:nil]; -} - -@end diff --git a/Apptentive/ApptentiveTests/ApptentiveInteractionInvocationTests.m b/Apptentive/ApptentiveTests/ApptentiveInteractionInvocationTests.m deleted file mode 100644 index e5c1e007d..000000000 --- a/Apptentive/ApptentiveTests/ApptentiveInteractionInvocationTests.m +++ /dev/null @@ -1,131 +0,0 @@ -// -// ApptentiveInteractionInvocationTests.m -// Apptentive -// -// Created by Andrew Wooster on 11/11/15. -// Copyright (c) 2015 Apptentive, Inc. All rights reserved. -// - -#import -#import - -#import "ApptentiveInteractionInvocation.h" -#import "ApptentiveInteractionUsageData.h" - - -@interface ApptentiveInteractionInvocationTests : XCTestCase - -@end - - -@interface ApptentiveInteractionInvocation () -+ (NSCompoundPredicateType)compoundPredicateTypeFromString:(NSString *)predicateTypeString hasError:(nonnull BOOL *)hasError; -+ (NSPredicateOperatorType)predicateOperatorTypeFromString:(NSString *)operatorString hasError:(nonnull BOOL *)hasError; -+ (BOOL) operator:(NSPredicateOperatorType) operator isValidForParameter:(NSObject *)parameter; -+ (NSPredicate *)predicateWithLeftKeyPath:(NSString *)keyPath forObject:(NSDictionary *)context rightComplexObject:(NSDictionary *)rightComplexObject operatorType:(NSPredicateOperatorType)operatorType; -+ (NSCompoundPredicate *)compoundPredicateWithType:(NSCompoundPredicateType)type criteriaArray:(NSArray *)criteriaArray; -+ (NSCompoundPredicate *)compoundPredicateForKeyPath:(NSString *)keyPath operatorsAndValues:(NSDictionary *)operatorsAndValues; - -@end - - -@interface ATFailingUsageData : ApptentiveInteractionUsageData -@end - - -@implementation ATFailingUsageData - -- (NSDictionary *)predicateEvaluationDictionary { - return nil; -} - -@end - - -@implementation ApptentiveInteractionInvocationTests - -- (void)setUp { - [super setUp]; -} - -- (void)tearDown { - [super tearDown]; -} - -- (void)testCompoundPredicateTypeFromString { - BOOL hasError; - XCTAssertEqual(NSAndPredicateType, [ApptentiveInteractionInvocation compoundPredicateTypeFromString:@"$and" hasError:&hasError]); - XCTAssertFalse(hasError); - XCTAssertEqual(NSOrPredicateType, [ApptentiveInteractionInvocation compoundPredicateTypeFromString:@"$or" hasError:&hasError]); - XCTAssertFalse(hasError); - XCTAssertEqual(NSNotPredicateType, [ApptentiveInteractionInvocation compoundPredicateTypeFromString:@"$not" hasError:&hasError]); - XCTAssertFalse(hasError); - - [ApptentiveInteractionInvocation compoundPredicateTypeFromString:@"" hasError:&hasError]; - XCTAssertTrue(hasError); -} - -- (void)testPredicateOperatorTypeFromString { - BOOL hasError; - XCTAssertEqual(NSEqualToPredicateOperatorType, [ApptentiveInteractionInvocation predicateOperatorTypeFromString:@"==" hasError:&hasError]); - XCTAssertFalse(hasError); - XCTAssertEqual(NSEqualToPredicateOperatorType, [ApptentiveInteractionInvocation predicateOperatorTypeFromString:@"$eq" hasError:&hasError]); - XCTAssertFalse(hasError); - - XCTAssertEqual(NSGreaterThanPredicateOperatorType, [ApptentiveInteractionInvocation predicateOperatorTypeFromString:@"$gt" hasError:&hasError]); - XCTAssertFalse(hasError); - XCTAssertEqual(NSGreaterThanPredicateOperatorType, [ApptentiveInteractionInvocation predicateOperatorTypeFromString:@">" hasError:&hasError]); - XCTAssertFalse(hasError); - - XCTAssertEqual(NSGreaterThanOrEqualToPredicateOperatorType, [ApptentiveInteractionInvocation predicateOperatorTypeFromString:@"$gte" hasError:&hasError]); - XCTAssertFalse(hasError); - XCTAssertEqual(NSGreaterThanOrEqualToPredicateOperatorType, [ApptentiveInteractionInvocation predicateOperatorTypeFromString:@">=" hasError:&hasError]); - XCTAssertFalse(hasError); - - XCTAssertEqual(NSLessThanPredicateOperatorType, [ApptentiveInteractionInvocation predicateOperatorTypeFromString:@"$lt" hasError:&hasError]); - XCTAssertFalse(hasError); - XCTAssertEqual(NSLessThanPredicateOperatorType, [ApptentiveInteractionInvocation predicateOperatorTypeFromString:@"<" hasError:&hasError]); - XCTAssertFalse(hasError); - - XCTAssertEqual(NSLessThanOrEqualToPredicateOperatorType, [ApptentiveInteractionInvocation predicateOperatorTypeFromString:@"$lte" hasError:&hasError]); - XCTAssertFalse(hasError); - XCTAssertEqual(NSLessThanOrEqualToPredicateOperatorType, [ApptentiveInteractionInvocation predicateOperatorTypeFromString:@"<=" hasError:&hasError]); - XCTAssertFalse(hasError); - - XCTAssertEqual(NSNotEqualToPredicateOperatorType, [ApptentiveInteractionInvocation predicateOperatorTypeFromString:@"$ne" hasError:&hasError]); - XCTAssertFalse(hasError); - XCTAssertEqual(NSNotEqualToPredicateOperatorType, [ApptentiveInteractionInvocation predicateOperatorTypeFromString:@"!=" hasError:&hasError]); - XCTAssertFalse(hasError); - - XCTAssertEqual(NSContainsPredicateOperatorType, [ApptentiveInteractionInvocation predicateOperatorTypeFromString:@"$contains" hasError:&hasError]); - XCTAssertFalse(hasError); - XCTAssertEqual(NSContainsPredicateOperatorType, [ApptentiveInteractionInvocation predicateOperatorTypeFromString:@"CONTAINS[c]" hasError:&hasError]); - XCTAssertFalse(hasError); - - XCTAssertEqual(NSBeginsWithPredicateOperatorType, [ApptentiveInteractionInvocation predicateOperatorTypeFromString:@"$starts_with" hasError:&hasError]); - XCTAssertFalse(hasError); - XCTAssertEqual(NSBeginsWithPredicateOperatorType, [ApptentiveInteractionInvocation predicateOperatorTypeFromString:@"BEGINSWITH[c]" hasError:&hasError]); - XCTAssertFalse(hasError); - - XCTAssertEqual(NSEndsWithPredicateOperatorType, [ApptentiveInteractionInvocation predicateOperatorTypeFromString:@"$ends_with" hasError:&hasError]); - XCTAssertFalse(hasError); - XCTAssertEqual(NSEndsWithPredicateOperatorType, [ApptentiveInteractionInvocation predicateOperatorTypeFromString:@"ENDSWITH[c]" hasError:&hasError]); - XCTAssertFalse(hasError); - - [ApptentiveInteractionInvocation predicateOperatorTypeFromString:@"" hasError:&hasError]; - XCTAssertTrue(hasError); -} - -- (void)testOperatorIsValidForParameterFail { - XCTAssertFalse([ApptentiveInteractionInvocation operator:-999 isValidForParameter:@"Hey"]); -} - -- (void)testPredicateWithLeftKeyPathForObjectRightComplexObjectOperatorTypeFail { - XCTAssertNil([ApptentiveInteractionInvocation predicateWithLeftKeyPath:@"datetime" forObject:@{ @"datetime": @{@"_type": @"datetime"} } rightComplexObject:@{ @"_type": @"foo" } operatorType:NSEqualToPredicateOperatorType]); -} - -- (void)testCompoundPredicateWithTypeCriteriaArray { - XCTAssertNil([ApptentiveInteractionInvocation compoundPredicateWithType:NSAndPredicateType criteriaArray:@[@{ @"foo": [NSDate date] }]]); -} - -@end diff --git a/Apptentive/ApptentiveTests/ApptentiveInteractionUsageDataTests.m b/Apptentive/ApptentiveTests/ApptentiveInteractionUsageDataTests.m deleted file mode 100644 index 2efc9b8fc..000000000 --- a/Apptentive/ApptentiveTests/ApptentiveInteractionUsageDataTests.m +++ /dev/null @@ -1,116 +0,0 @@ -// -// ApptentiveInteractionUsageDataTests.m -// Apptentive -// -// Created by Andrew Wooster on 11/15/15. -// Copyright © 2015 Apptentive, Inc. All rights reserved. -// - -#import - -#import "Apptentive+Debugging.h" -#import "ApptentiveAppRelease.h" -#import "ApptentiveBackend+Engagement.h" -#import "ApptentiveBackend.h" -#import "ApptentiveConversation.h" -#import "ApptentiveInteractionInvocation.h" -#import "ApptentiveInteractionUsageData.h" -#import "ApptentivePerson.h" -#import "ApptentiveVersion.h" -#import "Apptentive_Private.h" - - -@interface ApptentiveInteractionUsageDataTests : XCTestCase - -@property (strong, nonatomic) ApptentiveInteractionUsageData *usage; - -@end - - -@implementation ApptentiveInteractionUsageDataTests - -- (void)setUp { - [super setUp]; - - self.usage = [[ApptentiveInteractionUsageData alloc] initWithConversation:[[ApptentiveConversation alloc] initWithState:ApptentiveConversationStateAnonymous]]; -} - -- (void)testApplicationVersion { - ApptentiveInteractionInvocation *invocation = [[ApptentiveInteractionInvocation alloc] init]; - invocation.criteria = @{ @"application/cf_bundle_short_version_string": @{@"$eq": @{@"_type": @"version", @"version": @"4.0.0"}} }; - - [self.usage.conversation.appRelease setValue:[[ApptentiveVersion alloc] initWithString:@"2"] forKey:@"version"]; - - NSDictionary *evaluationDictionary = [self.usage predicateEvaluationDictionary]; - NSDictionary *versionValue = evaluationDictionary[@"application/cf_bundle_short_version_string"]; - XCTAssertNotNil(versionValue, @"No application/cf_bundle_short_version_string key found."); - XCTAssertEqualObjects(versionValue[@"_type"], @"version"); - XCTAssertEqualObjects(versionValue[@"version"], @"2"); - - XCTAssertFalse([invocation criteriaAreMetForConversation:self.usage.conversation], @"4.0.0 is not 2"); - [self.usage.conversation.appRelease setValue:[[ApptentiveVersion alloc] initWithString:@"4.0"] forKey:@"version"]; - XCTAssertTrue([invocation criteriaAreMetForConversation:self.usage.conversation], @"4.0 is like 4.0.0"); -} - -- (void)testDefaultApplicationVersion { - NSDictionary *evaluationDictionary = [self.usage predicateEvaluationDictionary]; - NSDictionary *versionValue = evaluationDictionary[@"application/cf_bundle_short_version_string"]; - XCTAssertNotNil(versionValue, @"No application/cf_bundle_short_version_string key found."); - XCTAssertEqualObjects(versionValue[@"_type"], @"version"); - XCTAssertEqualObjects(versionValue[@"version"], @"0.0.0"); -} - -- (void)testSDKVersion { - ApptentiveConfiguration *configuration = [ApptentiveConfiguration configurationWithApptentiveKey:@"app-key" apptentiveSignature:@"app-signature"]; - [Apptentive registerWithConfiguration:configuration]; - sleep(1); - ApptentiveInteractionUsageData *usage = [[ApptentiveInteractionUsageData alloc] initWithConversation:[[ApptentiveConversation alloc] initWithState:ApptentiveConversationStateAnonymous]]; - - NSDictionary *evaluationDictionary = [usage predicateEvaluationDictionary]; - NSDictionary *versionValue = evaluationDictionary[@"sdk/version"]; - XCTAssertNotNil(versionValue, @"No sdk/version key found."); - XCTAssertEqualObjects(versionValue[@"_type"], @"version"); - XCTAssertEqualObjects(versionValue[@"version"], Apptentive.shared.SDKVersion); -} - -- (void)testCurrentTime { - NSDictionary *evaluationDictionary = [self.usage predicateEvaluationDictionary]; - NSDictionary *currentTimeValue = evaluationDictionary[@"current_time"]; - XCTAssertNotNil(currentTimeValue, @"No current_time key found."); - XCTAssertEqualObjects(currentTimeValue[@"_type"], @"datetime"); - XCTAssertEqualWithAccuracy([currentTimeValue[@"sec"] doubleValue], self.usage.conversation.currentTime.timeIntervalSince1970, 0.01); -} - -- (void)testTimeAtInstall { - NSDictionary *evaluationDictionary = [self.usage predicateEvaluationDictionary]; - NSDictionary *timeAtInstallValue = evaluationDictionary[@"time_at_install/total"]; - XCTAssertNotNil(timeAtInstallValue, @"No time_at_install/total key found."); - XCTAssertEqualObjects(timeAtInstallValue[@"_type"], @"datetime"); - XCTAssertEqualWithAccuracy([timeAtInstallValue[@"sec"] doubleValue], self.usage.conversation.appRelease.timeAtInstallTotal.timeIntervalSince1970, 0.01); - - NSDictionary *timeAtInstallVersionValue = evaluationDictionary[@"time_at_install/cf_bundle_short_version_string"]; - XCTAssertNotNil(timeAtInstallVersionValue, @"No time_at_install/version key found."); - XCTAssertEqualObjects(timeAtInstallVersionValue[@"_type"], @"datetime"); - XCTAssertEqualWithAccuracy([timeAtInstallVersionValue[@"sec"] doubleValue], self.usage.conversation.appRelease.timeAtInstallVersion.timeIntervalSince1970, 0.01); -} - -//TODO: Test for code point last_invoked_at/total -//TODO: Test for interaction last_invoked_at/total - -- (void)testPerson { - self.usage.conversation.person.name = nil; - self.usage.conversation.person.emailAddress = nil; - - NSDictionary *evaluationDictionary = [self.usage predicateEvaluationDictionary]; - XCTAssertNil(evaluationDictionary[@"person/name"]); - XCTAssertNil(evaluationDictionary[@"person/email"]); - - self.usage.conversation.person.name = @"Andrew"; - self.usage.conversation.person.emailAddress = @"example@example.com"; - - NSDictionary *validEvaluationDictionary = [self.usage predicateEvaluationDictionary]; - XCTAssertEqualObjects(validEvaluationDictionary[@"person/name"], @"Andrew"); - XCTAssertEqualObjects(validEvaluationDictionary[@"person/email"], @"example@example.com"); -} - -@end diff --git a/Apptentive/ApptentiveTests/ApptentiveMockDispatchQueue.h b/Apptentive/ApptentiveTests/ApptentiveMockDispatchQueue.h new file mode 100644 index 000000000..5a9df6505 --- /dev/null +++ b/Apptentive/ApptentiveTests/ApptentiveMockDispatchQueue.h @@ -0,0 +1,18 @@ +// +// ApptentiveMockDispatchQueue.h +// ApptentiveTests +// +// Created by Alex Lementuev on 2/23/18. +// Copyright © 2018 Apptentive, Inc. All rights reserved. +// + +#import +#import "ApptentiveDispatchQueue.h" + +@interface ApptentiveMockDispatchQueue : ApptentiveDispatchQueue + +- (instancetype)initWithRunImmediately:(BOOL)runImmediately; + +- (void)dispatchTasks; + +@end diff --git a/Apptentive/ApptentiveTests/ApptentiveMockDispatchQueue.m b/Apptentive/ApptentiveTests/ApptentiveMockDispatchQueue.m new file mode 100644 index 000000000..0b82b6e25 --- /dev/null +++ b/Apptentive/ApptentiveTests/ApptentiveMockDispatchQueue.m @@ -0,0 +1,50 @@ +// +// ApptentiveMockDispatchQueue.m +// ApptentiveTests +// +// Created by Alex Lementuev on 2/23/18. +// Copyright © 2018 Apptentive, Inc. All rights reserved. +// + +#import "ApptentiveMockDispatchQueue.h" + +@interface ApptentiveMockDispatchQueue () + +@property (nonatomic, strong) NSMutableArray * tasks; +@property (nonatomic, assign) BOOL runImmediately; + +@end + +typedef void (^ApptentiveMockDispatchTask)(void); + +@implementation ApptentiveMockDispatchQueue + +- (instancetype)init { + return [self initWithRunImmediately:YES]; +} + +- (instancetype)initWithRunImmediately:(BOOL)runImmediately { + self = [super init]; + if (self) { + _tasks = [NSMutableArray new]; + _runImmediately = runImmediately; + } + return self; +} + +- (void)dispatchAsync:(void (^)(void))task { + if (self.runImmediately) { + task(); + } else { + [self.tasks addObject:task]; + } +} + +- (void)dispatchTasks { + for (ApptentiveMockDispatchTask task in self.tasks) { + task(); + } + [self.tasks removeAllObjects]; +} + +@end diff --git a/Apptentive/ApptentiveTests/ApptentiveSurveyTests.m b/Apptentive/ApptentiveTests/ApptentiveSurveyTests.m index 588c38e80..1232959ce 100644 --- a/Apptentive/ApptentiveTests/ApptentiveSurveyTests.m +++ b/Apptentive/ApptentiveTests/ApptentiveSurveyTests.m @@ -11,7 +11,8 @@ #import "ApptentiveCount.h" #import "ApptentiveEngagement.h" #import "ApptentiveInteraction.h" -#import "ApptentiveInteractionUsageData.h" +#import "ApptentiveStyleSheet.h" +#import "ApptentiveConversation.h" #import "ApptentiveStyleSheet.h" #import "ApptentiveSurveyViewModel.h" #import "Apptentive_Private.h" @@ -38,6 +39,9 @@ - (void)setUp { NSError *error; NSDictionary *JSONDictionary = [NSJSONSerialization JSONObjectWithData:JSONData options:0 error:&error]; + // We use a stylesheet object internally. This should get injected as a dependency at some point + [Apptentive registerWithConfiguration:[ApptentiveConfiguration configurationWithApptentiveKey:@"abc123" apptentiveSignature:@"abc123"]]; + if (!JSONDictionary) { NSLog(@"Error reading JSON: %@", error); } else { diff --git a/Apptentive/ApptentiveTests/ApptentiveTests-Bridging-Header.h b/Apptentive/ApptentiveTests/ApptentiveTests-Bridging-Header.h index abdfb29f0..02003d140 100644 --- a/Apptentive/ApptentiveTests/ApptentiveTests-Bridging-Header.h +++ b/Apptentive/ApptentiveTests/ApptentiveTests-Bridging-Header.h @@ -3,6 +3,8 @@ // #import "Apptentive.h" +#import "ApptentiveLog.h" + #import "ApptentiveConversationManager.h" #import "ApptentiveAppRelease.h" @@ -19,7 +21,18 @@ #import "ApptentiveConversationRequest.h" #import "ApptentiveDevice.h" #import "ApptentivePerson.h" +#import "ApptentiveEngagement.h" +#import "ApptentiveConversation.h" #import "ApptentiveAttachment.h" #import "ApptentiveMessage.h" #import "ApptentiveMessageSender.h" + +#import "ApptentiveTarget.h" +#import "ApptentiveClause.h" +#import "ApptentiveIndentPrinter.h" + +#import "ApptentiveAsyncLogWriter.h" +#import "ApptentiveMockDispatchQueue.h" + +#import "ApptentiveRetryPolicy.h" diff --git a/Apptentive/ApptentiveTests/ClauseTests.m b/Apptentive/ApptentiveTests/ClauseTests.m new file mode 100644 index 000000000..553ea830d --- /dev/null +++ b/Apptentive/ApptentiveTests/ClauseTests.m @@ -0,0 +1,130 @@ +// +// ClauseTests.m +// ApptentiveTests +// +// Created by Frank Schmitt on 3/9/18. +// Copyright © 2018 Apptentive, Inc. All rights reserved. +// + +#import +#import "ApptentiveConversation.h" +#import "ApptentiveDevice.h" +#import "ApptentiveIndentPrinter.h" +#import "ApptentiveFalseClause.h" +#import "ApptentiveAndClause.h" +#import "ApptentiveOrClause.h" +#import "ApptentiveNotClause.h" + +@interface ClauseTests : XCTestCase + +@property (nonatomic, strong) ApptentiveConversation *conversation; +@property (nonatomic, strong) ApptentiveIndentPrinter *indentPrinter; + +@end + + +@interface ApptentiveTrueClause : ApptentiveClause +@end + + +@implementation ApptentiveTrueClause + +- (BOOL)criteriaMetForConversation:(ApptentiveConversation *)conversation indentPrinter:(ApptentiveIndentPrinter *)indentPrinter { + [indentPrinter appendFormat:@"- Mock ”always true” clause -> true"]; + return YES; +} + +@end + + +@interface ApptentiveAndClause () + +@property (readonly, nonatomic) NSMutableArray *subClauses; + +@end + + +@interface ApptentiveOrClause () + +@property (readonly, nonatomic) NSMutableArray *subClauses; + +@end + + +@interface ApptentiveNotClause () + +@property (readwrite, nonatomic) ApptentiveClause *subClause; + +@end + + +@implementation ClauseTests + +- (void)setUp { + [super setUp]; + + [ApptentiveDevice getPermanentDeviceValues]; + + self.conversation = [[ApptentiveConversation alloc] initWithState:ApptentiveConversationStateAnonymous]; + + self.indentPrinter = [[ApptentiveIndentPrinter alloc] init]; +} + +- (void)tearDown { + NSLog(@"%@", self.indentPrinter.output); + + [super tearDown]; +} + +- (void)testFalse { + ApptentiveFalseClause *clause = [[ApptentiveFalseClause alloc] init]; + + XCTAssertFalse([clause criteriaMetForConversation:self.conversation indentPrinter:self.indentPrinter]); +} + +- (void)testTrue { + ApptentiveTrueClause *clause = [[ApptentiveTrueClause alloc] init]; + + XCTAssertTrue([clause criteriaMetForConversation:self.conversation indentPrinter:self.indentPrinter]); +} + +- (void)testAnd { + ApptentiveAndClause *clause = [[ApptentiveAndClause alloc] init]; + + XCTAssertTrue([clause criteriaMetForConversation:self.conversation indentPrinter:self.indentPrinter]); + + [clause.subClauses addObject:[[ApptentiveTrueClause alloc] init]]; + + XCTAssertTrue([clause criteriaMetForConversation:self.conversation indentPrinter:self.indentPrinter]); + + [clause.subClauses addObject:[[ApptentiveFalseClause alloc] init]]; + + XCTAssertFalse([clause criteriaMetForConversation:self.conversation indentPrinter:self.indentPrinter]); +} + +- (void)testOr { + ApptentiveOrClause *clause = [[ApptentiveOrClause alloc] init]; + + XCTAssertFalse([clause criteriaMetForConversation:self.conversation indentPrinter:self.indentPrinter]); + + [clause.subClauses addObject:[[ApptentiveFalseClause alloc] init]]; + + XCTAssertFalse([clause criteriaMetForConversation:self.conversation indentPrinter:self.indentPrinter]); + + [clause.subClauses addObject:[[ApptentiveTrueClause alloc] init]]; + + XCTAssertTrue([clause criteriaMetForConversation:self.conversation indentPrinter:self.indentPrinter]); +} + +- (void)testNot { + ApptentiveNotClause *clause = [[ApptentiveNotClause alloc] init]; + clause.subClause = [[ApptentiveFalseClause alloc] init]; + + XCTAssertTrue([clause criteriaMetForConversation:self.conversation indentPrinter:self.indentPrinter]); + + clause.subClause = [[ApptentiveTrueClause alloc] init]; + + XCTAssertFalse([clause criteriaMetForConversation:self.conversation indentPrinter:self.indentPrinter]); +} + +@end diff --git a/Apptentive/ApptentiveTests/CodePointAndInteractionTests.m b/Apptentive/ApptentiveTests/CodePointAndInteractionTests.m index 885154bf8..11c9caa01 100644 --- a/Apptentive/ApptentiveTests/CodePointAndInteractionTests.m +++ b/Apptentive/ApptentiveTests/CodePointAndInteractionTests.m @@ -7,17 +7,21 @@ // #import "CriteriaTests.h" +#import "Apptentive_Private.h" +#import "ApptentiveConversation.h" +#import "ApptentiveEngagement.h" +#import "ApptentiveCount.h" +#import "ApptentiveClause.h" #import "ApptentiveConversation.h" #import "ApptentiveCount.h" #import "ApptentiveEngagement.h" -#import "ApptentiveInteractionInvocation.h" -#import "ApptentiveInteractionUsageData.h" #import "Apptentive_Private.h" @interface CodePointTest : CriteriaTest -@property (strong, nonatomic) ApptentiveInteractionUsageData *usageData; +@property (strong, nonatomic) ApptentiveConversation *conversation; + @end @@ -26,15 +30,15 @@ @implementation CodePointTest - (void)setUp { [super setUp]; - self.usageData = [ApptentiveInteractionUsageData usageDataWithConversation:[[ApptentiveConversation alloc] initWithState:ApptentiveConversationStateAnonymous]]; + self.conversation = [[ApptentiveConversation alloc] initWithState:ApptentiveConversationStateAnonymous]; } - (void)incrementCodePoint:(NSString *)codePoint { - [self.usageData.conversation.engagement engageCodePoint:codePoint]; + [self.conversation.engagement engageCodePoint:codePoint]; } - (void)incrementInteraction:(NSString *)interactionID { - [self.usageData.conversation.engagement engageInteraction:interactionID]; + [self.conversation.engagement engageInteraction:interactionID]; } @end @@ -49,54 +53,54 @@ @implementation CodePointInvokesTotal - (void)setUp { [super setUp]; - [self.usageData.conversation.engagement warmCodePoint:@"test.code.point"]; - [self.usageData.conversation.engagement warmCodePoint:@"switch.code.point"]; + [self.conversation.engagement warmCodePoint:@"test.code.point"]; + [self.conversation.engagement warmCodePoint:@"switch.code.point"]; } - (void)testGt { - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); [self incrementCodePoint:@"test.code.point"]; - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); [self incrementCodePoint:@"test.code.point"]; - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); [self incrementCodePoint:@"test.code.point"]; - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.conversation]); } - (void)testGte { [self incrementCodePoint:@"switch.code.point"]; - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); [self incrementCodePoint:@"test.code.point"]; - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); [self incrementCodePoint:@"test.code.point"]; - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.conversation]); [self incrementCodePoint:@"test.code.point"]; - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.conversation]); } - (void)testNe { [self incrementCodePoint:@"switch.code.point"]; [self incrementCodePoint:@"switch.code.point"]; - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.conversation]); [self incrementCodePoint:@"test.code.point"]; - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.conversation]); [self incrementCodePoint:@"test.code.point"]; - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); [self incrementCodePoint:@"test.code.point"]; - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.conversation]); } - (void)testEq { [self incrementCodePoint:@"switch.code.point"]; [self incrementCodePoint:@"switch.code.point"]; [self incrementCodePoint:@"switch.code.point"]; - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); [self incrementCodePoint:@"test.code.point"]; - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); [self incrementCodePoint:@"test.code.point"]; - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.conversation]); [self incrementCodePoint:@"test.code.point"]; - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); } - (void)testColon { @@ -104,13 +108,13 @@ - (void)testColon { [self incrementCodePoint:@"switch.code.point"]; [self incrementCodePoint:@"switch.code.point"]; [self incrementCodePoint:@"switch.code.point"]; - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); [self incrementCodePoint:@"test.code.point"]; - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); [self incrementCodePoint:@"test.code.point"]; - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.conversation]); [self incrementCodePoint:@"test.code.point"]; - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); } - (void)testLte { @@ -119,13 +123,13 @@ - (void)testLte { [self incrementCodePoint:@"switch.code.point"]; [self incrementCodePoint:@"switch.code.point"]; [self incrementCodePoint:@"switch.code.point"]; - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.conversation]); [self incrementCodePoint:@"test.code.point"]; - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.conversation]); [self incrementCodePoint:@"test.code.point"]; - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.conversation]); [self incrementCodePoint:@"test.code.point"]; - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); } - (void)testLt { @@ -135,13 +139,13 @@ - (void)testLt { [self incrementCodePoint:@"switch.code.point"]; [self incrementCodePoint:@"switch.code.point"]; [self incrementCodePoint:@"switch.code.point"]; - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.conversation]); [self incrementCodePoint:@"test.code.point"]; - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.conversation]); [self incrementCodePoint:@"test.code.point"]; - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); [self incrementCodePoint:@"test.code.point"]; - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); } @end @@ -156,8 +160,8 @@ @implementation CodePointInvokesVersion - (void)setUp { [super setUp]; - [self.usageData.conversation.engagement warmCodePoint:@"test.code.point"]; - [self.usageData.conversation.engagement warmCodePoint:@"switch.code.point"]; + [self.conversation.engagement warmCodePoint:@"test.code.point"]; + [self.conversation.engagement warmCodePoint:@"switch.code.point"]; } - (NSString *)codePointFormatString { @@ -165,49 +169,49 @@ - (NSString *)codePointFormatString { } - (void)testGt { - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); [self incrementCodePoint:@"test.code.point"]; - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); [self incrementCodePoint:@"test.code.point"]; - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); [self incrementCodePoint:@"test.code.point"]; - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.conversation]); } - (void)testGte { [self incrementCodePoint:@"switch.code.point"]; - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); [self incrementCodePoint:@"test.code.point"]; - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); [self incrementCodePoint:@"test.code.point"]; - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.conversation]); [self incrementCodePoint:@"test.code.point"]; - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.conversation]); } - (void)testNe { [self incrementCodePoint:@"switch.code.point"]; [self incrementCodePoint:@"switch.code.point"]; - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.conversation]); [self incrementCodePoint:@"test.code.point"]; - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.conversation]); [self incrementCodePoint:@"test.code.point"]; - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); [self incrementCodePoint:@"test.code.point"]; - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.conversation]); } - (void)testEq { [self incrementCodePoint:@"switch.code.point"]; [self incrementCodePoint:@"switch.code.point"]; [self incrementCodePoint:@"switch.code.point"]; - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); [self incrementCodePoint:@"test.code.point"]; - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); [self incrementCodePoint:@"test.code.point"]; - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.conversation]); [self incrementCodePoint:@"test.code.point"]; - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); } - (void)testColon { @@ -215,13 +219,13 @@ - (void)testColon { [self incrementCodePoint:@"switch.code.point"]; [self incrementCodePoint:@"switch.code.point"]; [self incrementCodePoint:@"switch.code.point"]; - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); [self incrementCodePoint:@"test.code.point"]; - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); [self incrementCodePoint:@"test.code.point"]; - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.conversation]); [self incrementCodePoint:@"test.code.point"]; - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); } - (void)testLte { @@ -230,13 +234,13 @@ - (void)testLte { [self incrementCodePoint:@"switch.code.point"]; [self incrementCodePoint:@"switch.code.point"]; [self incrementCodePoint:@"switch.code.point"]; - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.conversation]); [self incrementCodePoint:@"test.code.point"]; - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.conversation]); [self incrementCodePoint:@"test.code.point"]; - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.conversation]); [self incrementCodePoint:@"test.code.point"]; - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); } - (void)testLt { @@ -246,13 +250,13 @@ - (void)testLt { [self incrementCodePoint:@"switch.code.point"]; [self incrementCodePoint:@"switch.code.point"]; [self incrementCodePoint:@"switch.code.point"]; - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.conversation]); [self incrementCodePoint:@"test.code.point"]; - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.conversation]); [self incrementCodePoint:@"test.code.point"]; - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); [self incrementCodePoint:@"test.code.point"]; - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); } @end @@ -269,54 +273,50 @@ @implementation CodePointLastInvokedAt - (void)setUp { [super setUp]; - [self.usageData.conversation.engagement warmCodePoint:@"test.code.point"]; - [self.usageData.conversation.engagement warmCodePoint:@"switch.code.point"]; + [self.conversation.engagement warmCodePoint:@"test.code.point"]; + [self.conversation.engagement warmCodePoint:@"switch.code.point"]; } - (void)incrementTimeAgoCodePoint:(NSString *)codePoint { - [self.usageData.conversation.engagement engageCodePoint:codePoint]; + [self.conversation.engagement engageCodePoint:codePoint]; } - (void)testAfter { - [self.usageData.conversation.engagement.codePoints[@"test.code.point"] setValue:[NSDate distantPast] forKey:@"lastInvoked"]; + [self.conversation.engagement.codePoints[@"test.code.point"] setValue:[NSDate distantPast] forKey:@"lastInvoked"]; - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); - NSLog(@"%@", [self.usageData predicateEvaluationDictionary][@"code_point/test.code.point/last_invoked_at/total"]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); [self incrementTimeAgoCodePoint:@"test.code.point"]; - NSLog(@"%@", [self.usageData predicateEvaluationDictionary][@"code_point/test.code.point/last_invoked_at/total"]); - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.conversation]); [self incrementTimeAgoCodePoint:@"test.code.point"]; usleep(300000); - NSLog(@"%@", [self.usageData predicateEvaluationDictionary][@"code_point/test.code.point/last_invoked_at/total"]); - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.conversation]); usleep(300000); - NSLog(@"%@", [self.usageData predicateEvaluationDictionary][@"code_point/test.code.point/last_invoked_at/total"]); - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); } - (void)testNe { - [self.usageData.conversation.engagement engageCodePoint:@"test.code.point"]; + [self.conversation.engagement engageCodePoint:@"test.code.point"]; [self incrementCodePoint:@"switch.code.point"]; // There is always going to be a few microseconds of time offset here, so I can't really run this test. - //XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + //XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); [self incrementTimeAgoCodePoint:@"test.code.point"]; usleep(300000); - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.conversation]); usleep(300000); - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.conversation]); } - (void)testEq { // 2 - $eq // There's no easy way to test this unless we contrive the times. [self incrementCodePoint:@"switch.code.point"]; [self incrementCodePoint:@"switch.code.point"]; - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); [self incrementTimeAgoCodePoint:@"test.code.point"]; usleep(300000); - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); usleep(300000); - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); } - (void)testColon { @@ -324,12 +324,12 @@ - (void)testColon { [self incrementCodePoint:@"switch.code.point"]; [self incrementCodePoint:@"switch.code.point"]; [self incrementCodePoint:@"switch.code.point"]; - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); [self incrementTimeAgoCodePoint:@"test.code.point"]; usleep(300000); - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); usleep(300000); - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); } - (void)testBefore { @@ -337,12 +337,12 @@ - (void)testBefore { [self incrementCodePoint:@"switch.code.point"]; [self incrementCodePoint:@"switch.code.point"]; [self incrementCodePoint:@"switch.code.point"]; - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); [self incrementTimeAgoCodePoint:@"test.code.point"]; usleep(300000); - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); usleep(300000); - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.conversation]); } @end @@ -357,54 +357,54 @@ @implementation InteractionInvokesTotal - (void)setUp { [super setUp]; - [self.usageData.conversation.engagement warmInteraction:@"test.interaction"]; - [self.usageData.conversation.engagement warmCodePoint:@"switch.code.point"]; + [self.conversation.engagement warmInteraction:@"test.interaction"]; + [self.conversation.engagement warmCodePoint:@"switch.code.point"]; } - (void)testInteractionInvokesTotalGt { - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); [self incrementInteraction:@"test.interaction"]; - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); [self incrementInteraction:@"test.interaction"]; - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); [self incrementInteraction:@"test.interaction"]; - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.conversation]); } - (void)testInteractionInvokesTotalGte { [self incrementCodePoint:@"switch.code.point"]; - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); [self incrementInteraction:@"test.interaction"]; - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); [self incrementInteraction:@"test.interaction"]; - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.conversation]); [self incrementInteraction:@"test.interaction"]; - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.conversation]); } - (void)testInteractionInvokesTotalNe { [self incrementCodePoint:@"switch.code.point"]; [self incrementCodePoint:@"switch.code.point"]; - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.conversation]); [self incrementInteraction:@"test.interaction"]; - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.conversation]); [self incrementInteraction:@"test.interaction"]; - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); [self incrementInteraction:@"test.interaction"]; - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.conversation]); } - (void)testInteractionInvokesTotalEq { [self incrementCodePoint:@"switch.code.point"]; [self incrementCodePoint:@"switch.code.point"]; [self incrementCodePoint:@"switch.code.point"]; - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); [self incrementInteraction:@"test.interaction"]; - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); [self incrementInteraction:@"test.interaction"]; - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.conversation]); [self incrementInteraction:@"test.interaction"]; - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); } - (void)testInteractionInvokesTotalColon { @@ -412,13 +412,13 @@ - (void)testInteractionInvokesTotalColon { [self incrementCodePoint:@"switch.code.point"]; [self incrementCodePoint:@"switch.code.point"]; [self incrementCodePoint:@"switch.code.point"]; - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); [self incrementInteraction:@"test.interaction"]; - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); [self incrementInteraction:@"test.interaction"]; - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.conversation]); [self incrementInteraction:@"test.interaction"]; - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); } - (void)testInteractionInvokesTotalLte { @@ -427,13 +427,13 @@ - (void)testInteractionInvokesTotalLte { [self incrementCodePoint:@"switch.code.point"]; [self incrementCodePoint:@"switch.code.point"]; [self incrementCodePoint:@"switch.code.point"]; - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.conversation]); [self incrementInteraction:@"test.interaction"]; - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.conversation]); [self incrementInteraction:@"test.interaction"]; - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.conversation]); [self incrementInteraction:@"test.interaction"]; - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); } - (void)testInteractionInvokesTotalLt { @@ -443,13 +443,13 @@ - (void)testInteractionInvokesTotalLt { [self incrementCodePoint:@"switch.code.point"]; [self incrementCodePoint:@"switch.code.point"]; [self incrementCodePoint:@"switch.code.point"]; - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.conversation]); [self incrementInteraction:@"test.interaction"]; - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.conversation]); [self incrementInteraction:@"test.interaction"]; - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); [self incrementInteraction:@"test.interaction"]; - XCTAssertFalse([self.interaction criteriaAreMetForConversation:self.usageData.conversation]); + XCTAssertFalse([self.clause criteriaMetForConversation:self.conversation]); } @end diff --git a/Apptentive/ApptentiveTests/CriteriaDescriptionTests.swift b/Apptentive/ApptentiveTests/CriteriaDescriptionTests.swift new file mode 100644 index 000000000..e85829524 --- /dev/null +++ b/Apptentive/ApptentiveTests/CriteriaDescriptionTests.swift @@ -0,0 +1,94 @@ +// +// CriteriaDescriptionTests.swift +// ApptentiveTests +// +// Created by Frank Schmitt on 2/21/18. +// Copyright © 2018 Apptentive, Inc. All rights reserved. +// + +import XCTest + +class CriteriaDescriptionTests: XCTestCase { + var conversation: ApptentiveConversation! + + override func setUp() { + ApptentiveDevice.getPermanentDeviceValues() + + conversation = ApptentiveConversation(state: .anonymous) + + super.setUp() + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + func testConversationFieldDescription() { + XCTAssertEqual(conversation.descriptionForField(withPath: "current_time"), "current time") + } + + func testEngagementFieldDescriptions() { + XCTAssertEqual(conversation.descriptionForField(withPath: "interactions/foo/invokes/total"), "number of invokes for interaction 'foo'") + XCTAssertEqual(conversation.descriptionForField(withPath: "interactions/foo/invokes/cf_bundle_short_version_string"), "number of invokes for interaction 'foo' for current version") + XCTAssertEqual(conversation.descriptionForField(withPath: "interactions/foo/invokes/cf_bundle_version"), "number of invokes for interaction 'foo' for current build") + XCTAssertEqual(conversation.descriptionForField(withPath: "interactions/foo/last_invoked_at/total"), "last time interaction 'foo' was invoked") + + XCTAssertEqual(conversation.descriptionForField(withPath: "code_point/foo/invokes/total"), "number of invokes for event 'foo'") + XCTAssertEqual(conversation.descriptionForField(withPath: "code_point/foo/invokes/cf_bundle_short_version_string"), "number of invokes for event 'foo' for current version") + XCTAssertEqual(conversation.descriptionForField(withPath: "code_point/foo/invokes/cf_bundle_version"), "number of invokes for event 'foo' for current build") + XCTAssertEqual(conversation.descriptionForField(withPath: "code_point/foo/last_invoked_at/total"), "last time event 'foo' was invoked") + } + + func testAppReleaseFieldDescriptions() { + XCTAssertEqual(conversation.descriptionForField(withPath: "application/cf_bundle_short_version_string"), "app version (CFBundleShortVersionString)") + XCTAssertEqual(conversation.descriptionForField(withPath: "application/cf_bundle_version"), "app build (CFBundleVersion)") + XCTAssertEqual(conversation.descriptionForField(withPath: "time_at_install/total"), "time at install") + XCTAssertEqual(conversation.descriptionForField(withPath: "time_at_install/cf_bundle_short_version_string"), "time at install for version") + XCTAssertEqual(conversation.descriptionForField(withPath: "time_at_install/cf_bundle_version"), "time at install for build") + } + + func testSDKFieldDescriptions() { + XCTAssertEqual(conversation.descriptionForField(withPath: "sdk/version"), "SDK version") + XCTAssertEqual(conversation.descriptionForField(withPath: "sdk/distribution"), "SDK distribution method") + XCTAssertEqual(conversation.descriptionForField(withPath: "sdk/distribution_version"), "SDK distribution package version") + } + + func testPersonFieldDescription() { + XCTAssertEqual(conversation.descriptionForField(withPath: "person/name"), "person name") + XCTAssertEqual(conversation.descriptionForField(withPath: "person/email"), "person email") + XCTAssertEqual(conversation.descriptionForField(withPath: "person/custom_data/foo"), "person_data[foo]") + } + + func testDeviceFieldDescription() { + XCTAssertEqual(conversation.descriptionForField(withPath: "device/uuid"), "device identifier (identifierForVendor)") + XCTAssertEqual(conversation.descriptionForField(withPath: "device/os_name"), "device OS name") + XCTAssertEqual(conversation.descriptionForField(withPath: "device/os_version"), "device OS version") + XCTAssertEqual(conversation.descriptionForField(withPath: "device/os_build"), "device OS build") + XCTAssertEqual(conversation.descriptionForField(withPath: "device/hardware"), "device hardware") + XCTAssertEqual(conversation.descriptionForField(withPath: "device/carrier"), "device carrier") + XCTAssertEqual(conversation.descriptionForField(withPath: "device/content_size_category"), "device content size category") + XCTAssertEqual(conversation.descriptionForField(withPath: "device/locale_raw"), "device raw locale") + XCTAssertEqual(conversation.descriptionForField(withPath: "device/locale_country_code"), "device locale country code") + XCTAssertEqual(conversation.descriptionForField(withPath: "device/locale_language_code"), "device locale language code") + XCTAssertEqual(conversation.descriptionForField(withPath: "device/utc_offset"), "device UTC offset") + XCTAssertEqual(conversation.descriptionForField(withPath: "device/integration_config"), "device integration configuration") + } + + func testIndentPrinter() { + let indentPrinter = ApptentiveIndentPrinter() + + XCTAssertEqual(indentPrinter.output, "") + + indentPrinter.append("foo") + + XCTAssertEqual(indentPrinter.output, "foo") + + indentPrinter.indent() + indentPrinter.append("bar") + + XCTAssertEqual(indentPrinter.output, "foo\n bar") + + indentPrinter.outdent() + + indentPrinter.append("foo2") + + XCTAssertEqual(indentPrinter.output, "foo\n bar\nfoo2") + } +} diff --git a/Apptentive/ApptentiveTests/CriteriaTests.h b/Apptentive/ApptentiveTests/CriteriaTests.h index b6e552b88..8a8a100bb 100644 --- a/Apptentive/ApptentiveTests/CriteriaTests.h +++ b/Apptentive/ApptentiveTests/CriteriaTests.h @@ -8,11 +8,11 @@ #import -@class ApptentiveInteractionInvocation; +@class ApptentiveClause; @interface CriteriaTest : XCTestCase -@property (strong, nonatomic) ApptentiveInteractionInvocation *interaction; +@property (strong, nonatomic) ApptentiveClause *clause; @end diff --git a/Apptentive/ApptentiveTests/CriteriaTests.m b/Apptentive/ApptentiveTests/CriteriaTests.m index 139b2eb80..b3d671d25 100644 --- a/Apptentive/ApptentiveTests/CriteriaTests.m +++ b/Apptentive/ApptentiveTests/CriteriaTests.m @@ -11,8 +11,10 @@ #import "ApptentiveConversation.h" #import "ApptentiveDevice.h" #import "ApptentiveEngagement.h" -#import "ApptentiveInteractionInvocation.h" #import "ApptentivePerson.h" +#import "ApptentiveAndClause.h" +#import "ApptentiveLog.h" +#import "ApptentiveVersion.h" @interface CriteriaTest () @@ -33,6 +35,8 @@ - (NSString *)JSONFilename { - (void)setUp { [super setUp]; + ApptentiveLogSetLevel(ApptentiveLogLevelDebug); + NSURL *JSONURL = [[NSBundle bundleForClass:[self class]] URLForResource:self.JSONFilename withExtension:@"json"]; NSData *JSONData = [NSData dataWithContentsOfURL:JSONURL]; NSError *error; @@ -41,9 +45,7 @@ - (void)setUp { if (!JSONDictionary) { NSLog(@"Error reading JSON: %@", error); } else { - NSDictionary *invocationDictionary = @{ @"criteria": JSONDictionary }; - - self.interaction = [ApptentiveInteractionInvocation invocationWithJSONDictionary:invocationDictionary]; + self.clause = [ApptentiveAndClause andClauseWithDictionary:JSONDictionary]; } self.data = [[ApptentiveConversation alloc] initWithState:ApptentiveConversationStateAnonymous]; @@ -64,7 +66,7 @@ @interface CornerCasesThatShouldBeFalse : CriteriaTest @implementation CornerCasesThatShouldBeFalse - (void)testCornerCasesThatShouldBeFalse { - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.data]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.data]); } @end @@ -77,7 +79,7 @@ @interface CornerCasesThatShouldBeTrue : CriteriaTest @implementation CornerCasesThatShouldBeTrue - (void)testCornerCasesThatShouldBeTrue { - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.data]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.data]); } @end @@ -96,7 +98,7 @@ - (void)testDefaultValues { [Apptentive sharedConnection].personName = nil; [Apptentive sharedConnection].personEmailAddress = nil; - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.data]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.data]); } @end @@ -109,7 +111,19 @@ @interface PredicateParsing : CriteriaTest @implementation PredicateParsing - (void)testPredicateParsing { - XCTAssertNotNil([self.interaction valueForKey:@"criteriaPredicate"]); + // On Android this just checks that the parser can parse the criteria. + XCTAssertNotNil(self.clause); + +// There are some problems with the criteria that make it not actually work: +// 1. There is no field called, e.g. "booleanQuery". This could be custom data ("person/custom_data/booleanQuery"), but that won't work for dates or versions. +// 2. Even if we add methods to add date and version custom data (and edit the field names in the criteria), the version is set to be both equal and not equal to 1.0.0. +// [self.data.person addCustomBool:YES withKey:@"booleanQuery"]; +// [self.data.person addCustomNumber:@(0) withKey:@"numberQuery"]; +// [self.data.person addCustomString:@"foo" withKey:@"stringQuery"]; +// [self.data.person addCustomDate:[NSDate dateWithTimeIntervalSince1970:123456789] withKey:@"dateTimeQuery"]; +// [self.data.person addCustomVersion:[[ApptentiveVersion alloc] initWithString:@"1.0.0"] withKey:@"versionQuery"]; +// +// XCTAssertTrue([self.clause criteriaMetForConversation:self.data]); } @end @@ -124,7 +138,7 @@ @implementation OperatorContains - (void)testOperatorContains { self.data.person.emailAddress = @"test@example.com"; - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.data]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.data]); } @end @@ -137,7 +151,7 @@ @interface OperatorStartsWith : CriteriaTest @implementation OperatorStartsWith - (void)testOperatorStartsWith { - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.data]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.data]); } @end @@ -150,7 +164,7 @@ @interface OperatorEndsWith : CriteriaTest @implementation OperatorEndsWith - (void)testOperatorEndsWith { - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.data]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.data]); } @end @@ -163,7 +177,7 @@ @interface OperatorNot : CriteriaTest @implementation OperatorNot - (void)testOperatorNot { - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.data]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.data]); } @end @@ -176,7 +190,7 @@ @interface OperatorExists : CriteriaTest @implementation OperatorExists - (void)testOperatorExists { - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.data]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.data]); } @end @@ -189,7 +203,29 @@ @interface WhitespaceTrimming : CriteriaTest @implementation WhitespaceTrimming - (void)testWhitespaceTrimming { - XCTAssertTrue([self.interaction criteriaAreMetForConversation:self.data]); + XCTAssertTrue([self.clause criteriaMetForConversation:self.data]); +} + +@end + +@interface OperatorStringEquals : CriteriaTest +@end + +@implementation OperatorStringEquals + +- (void)testWhitespaceTrimming { + XCTAssertTrue([self.clause criteriaMetForConversation:self.data]); +} + +@end + +@interface OperatorStringNotEquals : CriteriaTest +@end + +@implementation OperatorStringNotEquals + +- (void)testWhitespaceTrimming { + XCTAssertTrue([self.clause criteriaMetForConversation:self.data]); } @end diff --git a/Apptentive/ApptentiveTests/Info.plist b/Apptentive/ApptentiveTests/Info.plist index f1f7aa683..33c610906 100644 --- a/Apptentive/ApptentiveTests/Info.plist +++ b/Apptentive/ApptentiveTests/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 5.0.4 + 5.1.0 CFBundleVersion 1 diff --git a/Apptentive/ApptentiveTests/RetryPolicyTests.swift b/Apptentive/ApptentiveTests/RetryPolicyTests.swift new file mode 100644 index 000000000..31c759ac6 --- /dev/null +++ b/Apptentive/ApptentiveTests/RetryPolicyTests.swift @@ -0,0 +1,50 @@ +// +// RetryPolicyTests.swift +// ApptentiveTests +// +// Created by Frank Schmitt on 4/2/18. +// Copyright © 2018 Apptentive, Inc. All rights reserved. +// + +import XCTest + +class RetryPolicyTests: XCTestCase { + let retryPolicy = ApptentiveRetryPolicy(initialBackoff: 1.0, base: 2.0); + + override func setUp() { + super.setUp() + + // Need this to be predictable for testing + retryPolicy.shouldAddJitter = false; + + retryPolicy.cap = 4; + + retryPolicy.retryStatusCodes = IndexSet(integer: 69); + } + + func testBackoff() { + XCTAssertEqual(retryPolicy.retryDelay, 1.0); + + retryPolicy.increaseRetryDelay(); + + XCTAssertEqual(retryPolicy.retryDelay, 2.0); + + retryPolicy.increaseRetryDelay(); + + XCTAssertEqual(retryPolicy.retryDelay, 4.0); + + retryPolicy.increaseRetryDelay(); + + XCTAssertEqual(retryPolicy.retryDelay, 4.0); + + retryPolicy.resetRetryDelay(); + + XCTAssertEqual(retryPolicy.retryDelay, 1.0); + } + + func testShouldRetry() { + XCTAssertFalse(retryPolicy.shouldRetryRequest(withStatusCode: 0)); + XCTAssertTrue(retryPolicy.shouldRetryRequest(withStatusCode: 69)); + XCTAssertFalse(retryPolicy.shouldRetryRequest(withStatusCode: 100)); + } +} diff --git a/Apptentive/ApptentiveTests/TargetingTests.m b/Apptentive/ApptentiveTests/TargetingTests.m new file mode 100644 index 000000000..774e487ce --- /dev/null +++ b/Apptentive/ApptentiveTests/TargetingTests.m @@ -0,0 +1,65 @@ +// +// TargetingTests.m +// ApptentiveTests +// +// Created by Frank Schmitt on 3/6/18. +// Copyright © 2018 Apptentive, Inc. All rights reserved. +// + +#import +#import "ApptentiveTargets.h" +#import "ApptentiveConversation.h" + +@interface TargetingTests : XCTestCase + +@end + + +@implementation TargetingTests + +- (void)testInvalidFormats { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" +#pragma clang diagnostic ignored "-Wincompatible-pointer-types" +#pragma clang diagnostic ignored "-Wobjc-literal-conversion" + @try { + XCTAssertNil(([[ApptentiveTargets alloc] initWithTargetsDictionary:@[@"foo", @"bar", @"baz"]])); + XCTAssertNil([[ApptentiveTargets alloc] initWithTargetsDictionary:@"foo"]); + XCTAssertNil([[ApptentiveTargets alloc] initWithTargetsDictionary:nil]); + + ApptentiveTargets *targets = [[ApptentiveTargets alloc] initWithTargetsDictionary:@{@"foo": @{@"bar": @"baz"}}]; + XCTAssertNotNil(targets); + XCTAssertEqual(targets.invocations.count, 0); + + } @catch (NSException *e) { + XCTFail(@"Caught exception"); + } +#pragma clang diagnostic pop +} + +- (void)testValidFormat { + @try { + ApptentiveTargets *targets = [[ApptentiveTargets alloc] initWithTargetsDictionary:@{@"event_1": @[@{ @"interaction_id": @"abc123", @"criteria": @{}}]}]; + + XCTAssertNotNil(targets); + XCTAssertEqual(targets.invocations.count, 1); + XCTAssertEqualObjects([targets interactionIdentifierForEvent:@"event_1" conversation:[[ApptentiveConversation alloc] initWithState:ApptentiveConversationStateAnonymous]], @"abc123"); + } @catch (NSException *e) { + XCTFail(@"Caught exception"); + } +} + +- (void)testArchiving { + @try { + NSData *data = [NSKeyedArchiver archivedDataWithRootObject:[[ApptentiveTargets alloc] initWithTargetsDictionary:@{@"event_1": @[@{ @"interaction_id": @"abc123", @"criteria": @{}}]}]]; + ApptentiveTargets *targets = [NSKeyedUnarchiver unarchiveObjectWithData:data]; + + XCTAssertNotNil(targets); + XCTAssertEqual(targets.invocations.count, 1); + XCTAssertEqualObjects([targets interactionIdentifierForEvent:@"event_1" conversation:[[ApptentiveConversation alloc] initWithState:ApptentiveConversationStateAnonymous]], @"abc123"); + } @catch (NSException *e) { + XCTFail(@"Caught exception"); + } +} + +@end diff --git a/Apptentive/ApptentiveTests/data/criteria/testCodePointLastInvokedAt.json b/Apptentive/ApptentiveTests/data/criteria/testCodePointLastInvokedAt.json index b70eb0b7f..19ac01654 100644 --- a/Apptentive/ApptentiveTests/data/criteria/testCodePointLastInvokedAt.json +++ b/Apptentive/ApptentiveTests/data/criteria/testCodePointLastInvokedAt.json @@ -38,4 +38,4 @@ } } ] -} +} \ No newline at end of file diff --git a/Apptentive/ApptentiveTests/data/criteria/testCornerCasesThatShouldBeTrue.json b/Apptentive/ApptentiveTests/data/criteria/testCornerCasesThatShouldBeTrue.json index 9d4ebbcec..30fb5f4dd 100644 --- a/Apptentive/ApptentiveTests/data/criteria/testCornerCasesThatShouldBeTrue.json +++ b/Apptentive/ApptentiveTests/data/criteria/testCornerCasesThatShouldBeTrue.json @@ -10,4 +10,4 @@ } } ] -} +} \ No newline at end of file diff --git a/Apptentive/ApptentiveTests/data/criteria/testOperatorGreaterThan.json b/Apptentive/ApptentiveTests/data/criteria/testOperatorGreaterThan.json index 596710c5d..dbc5c7937 100644 --- a/Apptentive/ApptentiveTests/data/criteria/testOperatorGreaterThan.json +++ b/Apptentive/ApptentiveTests/data/criteria/testOperatorGreaterThan.json @@ -75,6 +75,74 @@ "$gt": 0 } } + }, + { + "$not": { + "device/custom_data/boolean_true": { + "$gt": true + } + } + }, + { + "device/custom_data/boolean_true": { + "$gt": false + } + }, + { + "device/custom_data/datetime_1000": { + "$gt": { + "_type": "datetime", + "sec": "999" + } + } + }, + { + "$not": { + "device/custom_data/datetime_1000": { + "$gt": { + "_type": "datetime", + "sec": "1000" + } + } + } + }, + { + "$not": { + "device/custom_data/datetime_1000": { + "$gt": { + "_type": "datetime", + "sec": "1001" + } + } + } + }, + { + "device/custom_data/version_1.2.3": { + "$gt": { + "_type": "version", + "version": "1.2.2" + } + } + }, + { + "$not": { + "device/custom_data/version_1.2.3": { + "$gt": { + "_type": "version", + "version": "1.2.3" + } + } + } + }, + { + "$not": { + "device/custom_data/version_1.2.3": { + "$gt": { + "_type": "version", + "version": "1.2.4" + } + } + } } ] } diff --git a/Apptentive/ApptentiveTests/data/criteria/testOperatorGreaterThanOrEqual.json b/Apptentive/ApptentiveTests/data/criteria/testOperatorGreaterThanOrEqual.json index ea29c988e..98d3d2289 100644 --- a/Apptentive/ApptentiveTests/data/criteria/testOperatorGreaterThanOrEqual.json +++ b/Apptentive/ApptentiveTests/data/criteria/testOperatorGreaterThanOrEqual.json @@ -73,6 +73,68 @@ "$gte": 0 } } + }, + { + "device/custom_data/boolean_true": { + "$gte": true + } + }, + { + "device/custom_data/boolean_true": { + "$gte": false + } + }, + { + "device/custom_data/datetime_1000": { + "$gte": { + "_type": "datetime", + "sec": "999" + } + } + }, + { + "device/custom_data/datetime_1000": { + "$gte": { + "_type": "datetime", + "sec": "1000" + } + } + }, + { + "$not": { + "device/custom_data/datetime_1000": { + "$gte": { + "_type": "datetime", + "sec": "1001" + } + } + } + }, + { + "device/custom_data/version_1.2.3": { + "$gte": { + "_type": "version", + "version": "1.2.2" + } + } + }, + { + "device/custom_data/version_1.2.3": { + "$gte": { + "_type": "version", + "version": "1.2.3" + } + } + }, + { + "$not": { + "device/custom_data/version_1.2.3": { + "$gte": { + "_type": "version", + "version": "1.2.4" + } + } + } } ] } diff --git a/Apptentive/ApptentiveTests/data/criteria/testOperatorLessThan.json b/Apptentive/ApptentiveTests/data/criteria/testOperatorLessThan.json index 806f190a5..cfb329a38 100644 --- a/Apptentive/ApptentiveTests/data/criteria/testOperatorLessThan.json +++ b/Apptentive/ApptentiveTests/data/criteria/testOperatorLessThan.json @@ -70,6 +70,76 @@ "$lt": 0 } } + }, + { + "$not": { + "device/custom_data/boolean_true": { + "$lt": true + } + } + }, + { + "$not": { + "device/custom_data/boolean_true": { + "$lt": false + } + } + }, + { + "$not": { + "device/custom_data/datetime_1000": { + "$lt": { + "_type": "datetime", + "sec": "999" + } + } + } + }, + { + "$not": { + "device/custom_data/datetime_1000": { + "$lt": { + "_type": "datetime", + "sec": "1000" + } + } + } + }, + { + "device/custom_data/datetime_1000": { + "$lt": { + "_type": "datetime", + "sec": "1001" + } + } + }, + { + "$not": { + "device/custom_data/version_1.2.3": { + "$lt": { + "_type": "version", + "version": "1.2.2" + } + } + } + }, + { + "$not": { + "device/custom_data/version_1.2.3": { + "$lt": { + "_type": "version", + "version": "1.2.3" + } + } + } + }, + { + "device/custom_data/version_1.2.3": { + "$lt": { + "_type": "version", + "version": "1.2.4" + } + } } ] } diff --git a/Apptentive/ApptentiveTests/data/criteria/testOperatorLessThanOrEqual.json b/Apptentive/ApptentiveTests/data/criteria/testOperatorLessThanOrEqual.json index 8b031c0a2..287068e31 100644 --- a/Apptentive/ApptentiveTests/data/criteria/testOperatorLessThanOrEqual.json +++ b/Apptentive/ApptentiveTests/data/criteria/testOperatorLessThanOrEqual.json @@ -75,6 +75,70 @@ "$lte": 0 } } + }, + { + "device/custom_data/boolean_true": { + "$lte": true + } + }, + { + "$not": { + "device/custom_data/boolean_true": { + "$lte": false + } + } + }, + { + "$not": { + "device/custom_data/datetime_1000": { + "$lte": { + "_type": "datetime", + "sec": "999" + } + } + } + }, + { + "device/custom_data/datetime_1000": { + "$lte": { + "_type": "datetime", + "sec": "1000" + } + } + }, + { + "device/custom_data/datetime_1000": { + "$lte": { + "_type": "datetime", + "sec": "1001" + } + } + }, + { + "$not": { + "device/custom_data/version_1.2.3": { + "$lte": { + "_type": "version", + "version": "1.2.2" + } + } + } + }, + { + "device/custom_data/version_1.2.3": { + "$lte": { + "_type": "version", + "version": "1.2.3" + } + } + }, + { + "device/custom_data/version_1.2.3": { + "$lte": { + "_type": "version", + "version": "1.2.4" + } + } } ] } diff --git a/Apptentive/ApptentiveTests/data/criteria/testOperatorStringEquals.json b/Apptentive/ApptentiveTests/data/criteria/testOperatorStringEquals.json new file mode 100644 index 000000000..ef91d50ec --- /dev/null +++ b/Apptentive/ApptentiveTests/data/criteria/testOperatorStringEquals.json @@ -0,0 +1,44 @@ +{ + "$and": [ + { + "device/custom_data/string_qwerty": { + "$eq": "qwerty" + } + }, + { + "device/custom_data/string_qwerty": "qwerty" + }, + { + "device/custom_data/string_qwerty": { + "$eq": "QWERTY" + } + }, + { + "device/custom_data/string_qwerty": "QWERTY" + }, + { + "$not": { + "device/custom_data/string_qwerty": { + "$eq": "werty" + } + } + }, + { + "$not": { + "device/custom_data/string_qwerty": "werty" + } + }, + { + "$not": { + "device/custom_data/string_qwerty": { + "$eq": null + } + } + }, + { + "$not": { + "device/custom_data/string_qwerty": null + } + } + ] +} diff --git a/Apptentive/ApptentiveTests/data/criteria/testOperatorStringNotEquals.json b/Apptentive/ApptentiveTests/data/criteria/testOperatorStringNotEquals.json new file mode 100644 index 000000000..e86c5005a --- /dev/null +++ b/Apptentive/ApptentiveTests/data/criteria/testOperatorStringNotEquals.json @@ -0,0 +1,23 @@ +{ + "$and": [ + { + "$not": { + "device/custom_data/string_qwerty": { + "$ne": "qwerty" + } + } + }, + { + "$not": { + "device/custom_data/string_qwerty": { + "$ne": "QWERTY" + } + } + }, + { + "device/custom_data/string_qwerty": { + "$ne": "werty" + } + } + ] +} diff --git a/Apptentive/ApptentiveTests/utils/TestUtils.swift b/Apptentive/ApptentiveTests/utils/TestUtils.swift new file mode 100644 index 000000000..6e9f3f593 --- /dev/null +++ b/Apptentive/ApptentiveTests/utils/TestUtils.swift @@ -0,0 +1,17 @@ +// +// TestUtils.swift +// ApptentiveTests +// +// Created by Alex Lementuev on 2/23/18. +// Copyright © 2018 Apptentive, Inc. All rights reserved. +// + +import Foundation + +func contentsOfFile(atPath path: String) -> String? { + do { + return try String(contentsOfFile: path) + } catch { + return nil; + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md index eeec0eac3..85e3772ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +# 2018-04-18 - v5.1.0 + +#### Improvements + +- Refine exponential backoff of network requests +- Improve logging of targeting criteria evaluation + +#### Bugs Fixed + +- Eliminate a redundant network request on initialization + # 2018-05-04 - v5.0.4 #### Bugs Fixed diff --git a/Example/Example.xcodeproj/project.pbxproj b/Example/Example.xcodeproj/project.pbxproj index cac3a1650..1547d69c4 100644 --- a/Example/Example.xcodeproj/project.pbxproj +++ b/Example/Example.xcodeproj/project.pbxproj @@ -162,7 +162,7 @@ attributes = { LastSwiftMigration = 0700; LastSwiftUpdateCheck = 0700; - LastUpgradeCheck = 0920; + LastUpgradeCheck = 0930; ORGANIZATIONNAME = "Apptentive, Inc."; TargetAttributes = { 014E03391B7401A50059A3C6 = { @@ -258,12 +258,14 @@ CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; @@ -311,12 +313,14 @@ CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; diff --git a/Example/Example.xcodeproj/xcshareddata/xcschemes/iOSExample.xcscheme b/Example/Example.xcodeproj/xcshareddata/xcschemes/iOSExample.xcscheme index b5d3c9713..b3f71d45d 100644 --- a/Example/Example.xcodeproj/xcshareddata/xcschemes/iOSExample.xcscheme +++ b/Example/Example.xcodeproj/xcshareddata/xcschemes/iOSExample.xcscheme @@ -1,6 +1,6 @@ @@ -46,7 +45,6 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - language = "" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" diff --git a/Example/Example.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Example/Example.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/Example/Example.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Example/Podfile.lock b/Example/Podfile.lock index 7d15e8b06..95ae0bd46 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -1,5 +1,5 @@ PODS: - - apptentive-ios (5.0.4) + - apptentive-ios (5.1.0) DEPENDENCIES: - apptentive-ios (from `..`) @@ -9,8 +9,8 @@ EXTERNAL SOURCES: :path: .. SPEC CHECKSUMS: - apptentive-ios: a309254660710639ca9110b1f8d3c072b4cf6e7f + apptentive-ios: 4f62a5776060814cac8cc140b9ae2fbe0c657c37 -PODFILE CHECKSUM: fb7822acbd17e9b6c60d2db75808647cc370b6a0 +PODFILE CHECKSUM: 89d2b5f4683b04482e89df6d46b268cc9ed1ef79 COCOAPODS: 1.4.0 diff --git a/Example/podfile b/Example/podfile index b0ccc4663..edd1d7c48 100644 --- a/Example/podfile +++ b/Example/podfile @@ -6,5 +6,5 @@ target 'iOSExample' do pod 'apptentive-ios', :path => '..' # In your own apps, you will want to use the latest version from CocoaPods, like so: - # pod 'apptentive-ios', '~> 4' + # pod 'apptentive-ios', '~> 5' end diff --git a/README.md b/README.md index b36019063..7ba4734b6 100644 --- a/README.md +++ b/README.md @@ -4,22 +4,21 @@ The Apptentive iOS SDK provides a simple and powerful channel to communicate in- Use Apptentive features to improve your app's App Store ratings, collect and respond to customer feedback, show surveys at specific points within your app, and more. -## Install Guide +See our [Quick Start Guide](https://learn.apptentive.com/knowledge-base/ios-quick-start/) to get up and running as quickly as possible. -Apptentive can be installed manually as an Xcode subproject or via the dependency manager CocoaPods. +For complete information on installing and using Apptentive, please see our [iOS integration reference](https://learn.apptentive.com/knowledge-base/ios-integration-reference/). -The following guides explain the integration process: +## Installation - - [Xcode project setup guide](http://www.apptentive.com/docs/ios/setup/xcode/) - - [CocoaPods installation guide](http://www.apptentive.com/docs/ios/setup/cocoapods) - - As of version 3.3.1, we also support Carthage. +Apptentive can be installed using CocoaPods or Carthage, or manually as an Xcode subproject. -## Using Apptentive in your App + - [CocoaPods installation guide](https://learn.apptentive.com/knowledge-base/ios-integration-reference/#cocoapods) + - [Carthage installation guide](https://learn.apptentive.com/knowledge-base/ios-integration-reference/#carthage) + - [Xcode project setup guide](https://learn.apptentive.com/knowledge-base/ios-integration-reference/#subproject) -After integrating the Apptentive SDK into your project, you can [begin using Apptentive features in your app](http://www.apptentive.com/docs/ios/integration/). +## Using Apptentive in your App -To begin using the SDK, import the SDK and create a configuration object with your Apptentive App Key and Apptentive App Signature (found in the [API section of your Apptentive dashboard](https://be.apptentive.com/apps/current/settings/api)). +To begin, you will have to [initialize the Apptentive SDK](https://learn.apptentive.com/knowledge-base/ios-integration-reference/#initialize-apptentive): ``` objective-c @import Apptentive; @@ -44,17 +43,15 @@ Apptentive.shared.engage(event: "event_name", from: viewController) Later, on your Apptentive dashboard, you will target these events with Apptentive features such as Message Center, Ratings Prompts, and Surveys. -Please see our [iOS integration guide](http://www.apptentive.com/docs/ios/integration/) for more on this subject. - ## API Documentation -Please see our docs site for the Apptentive iOS SDK's [API documentation](http://www.apptentive.com/docs/ios/api/Classes/Apptentive.html). +Please see our Customer Learning Center for the Apptentive iOS SDK's [API documentation](https://learn.apptentive.com/knowledge-base/ios-sdk-api/). Apptentive's [API changelog](docs/APIChanges.md) is also updated with each release of the SDK. ## Testing Apptentive Features -Please see the [Apptentive testing guide](http://www.apptentive.com/docs/ios/testing/) for directions on how to test that the Rating Prompt, Surveys, and other Apptentive features have been configured correctly. +Please see the [Apptentive testing guide](https://learn.apptentive.com/knowledge-base/testing-your-apptentive-integration-ios/) for directions on how to test that the Rating Prompt, Surveys, and other Apptentive features have been configured correctly. # Apptentive Example App diff --git a/apptentive-ios.podspec b/apptentive-ios.podspec index 51f47dbc0..f0ba74da2 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.0.4' + s.version = '5.1.0' s.license = 'BSD' s.summary = 'Apptentive Customer Communications SDK.' s.homepage = 'https://www.apptentive.com/' diff --git a/docs/APIChanges.md b/docs/APIChanges.md index b22ee8c09..7a672ebc1 100644 --- a/docs/APIChanges.md +++ b/docs/APIChanges.md @@ -36,7 +36,7 @@ This document tracks changes to the API between versions. * The `ATNavigationController` class has been renamed to `ApptentiveNavigationController`. A compatibility alias is provided for legacy code. * The `apiKey` property on the Apptentive instance has been renamed to `APIKey` to match Apple's naming guidelines. The previous spelling is included for compatibility, but is deprecated. * The UI for surveys has undergone a complete visual redesign, but still retains the same functionality. -* A new style sheet object allows extensive customization of the Apptentive UI. See our [Customization Guide](https://docs.apptentive.com/ios/customization/) for details. +* A new style sheet object allows extensive customization of the Apptentive UI. See our [Customization Guide](https://learn.apptentive.com/knowledge-base/interface-customization-ios/) for details. # 2.1.0 diff --git a/docs/MigratingTo_3.0.0.md b/docs/MigratingTo_3.0.0.md index 52e6d8bf1..64e9d6ab0 100644 --- a/docs/MigratingTo_3.0.0.md +++ b/docs/MigratingTo_3.0.0.md @@ -1,6 +1,6 @@ # Migration to Apptentive v3.0.0 -If you have integrated a previous version of the Apptentive SDK, you will need to keep in mind the following changes in our version 3.0.0 release. For more information, please see our [Integration Guide](https://docs.apptentive.com/ios/integration/). +If you have integrated a previous version of the Apptentive SDK, you will need to keep in mind the following changes in our version 3.0.0 release. For more information, please see our [Integration Guide](https://learn.apptentive.com/knowledge-base/ios-integration-reference/). ## Renamed Classes and Constants @@ -20,7 +20,7 @@ Currently the style sheet is respected by the Survey and Message Center interact You will need to import the `ApptentiveStyleSheet.h` file if you would like to use the built-in styles and you are integrating via source or using the static library. -You can find more information in our [Customization Guide](https://docs.apptentive.com/ios/customization/). +You can find more information in our [Customization Guide](https://learn.apptentive.com/knowledge-base/interface-customization-ios/). ## Survey Redesign