diff --git a/.gitignore b/.gitignore index bef7c62..2ecdcbf 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,5 @@ Example/Pods fastlane/* !fastlane/.env +tests-build/* +test-reports/* diff --git a/Changelogs/1.5.0 b/Changelogs/1.5.0 index f560441..3d2bb6a 100644 --- a/Changelogs/1.5.0 +++ b/Changelogs/1.5.0 @@ -9,3 +9,5 @@ Improvements to the 'RingPublishingTracking' module. * Separate method to update user data * Separate method to update SSO system name * New method for user logout +* New event parameter - Content marked as paid +* New tracking event types for paid diff --git a/Example/RingPublishingTracking.xcodeproj/project.pbxproj b/Example/RingPublishingTracking.xcodeproj/project.pbxproj index a8bc7f0..34a82af 100644 --- a/Example/RingPublishingTracking.xcodeproj/project.pbxproj +++ b/Example/RingPublishingTracking.xcodeproj/project.pbxproj @@ -110,7 +110,6 @@ 60108EB42B0515C6005656C1 /* AtomicArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60108E452B0515C6005656C1 /* AtomicArray.swift */; }; 60108EB52B0515C6005656C1 /* TrackingStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60108E472B0515C6005656C1 /* TrackingStorage.swift */; }; 60108EB62B0515C6005656C1 /* UserDefaultsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60108E482B0515C6005656C1 /* UserDefaultsStorage.swift */; }; - 60108EB72B0515C6005656C1 /* DispatchQueue+BackgroundTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60108E4A2B0515C6005656C1 /* DispatchQueue+BackgroundTimer.swift */; }; 60108EB82B0515C6005656C1 /* VideoContentCategory+ParameterName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60108E4B2B0515C6005656C1 /* VideoContentCategory+ParameterName.swift */; }; 60108EB92B0515C6005656C1 /* Dictionary+JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60108E4C2B0515C6005656C1 /* Dictionary+JSON.swift */; }; 60108EBB2B0515C6005656C1 /* VideoVisibility+ParameterName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60108E4E2B0515C6005656C1 /* VideoVisibility+ParameterName.swift */; }; @@ -156,6 +155,16 @@ 7D7D5B9626FCA3DF00B79FDD /* TraceableScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D7D5B8A26FCA3DF00B79FDD /* TraceableScreen.swift */; }; 7DE4453B26E8913900A93431 /* LoggerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DE4453A26E8913900A93431 /* LoggerTests.swift */; }; 854F1289D6E3FA4BC790018B /* Pods_RingPublishingTracking_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF2945259492AA544792B04F /* Pods_RingPublishingTracking_Example.framework */; }; + A03CD5412C88AE44002DFD48 /* Encodable+JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = A03CD5402C88AE44002DFD48 /* Encodable+JSON.swift */; }; + A03CD5452C890312002DFD48 /* PaidEventUserId.swift in Sources */ = {isa = PBXBuildFile; fileRef = A03CD5442C890312002DFD48 /* PaidEventUserId.swift */; }; + A03CD5472C890A3A002DFD48 /* RingPublishingTracking+Paid.swift in Sources */ = {isa = PBXBuildFile; fileRef = A03CD5462C890A3A002DFD48 /* RingPublishingTracking+Paid.swift */; }; + A03CD5502C891219002DFD48 /* LikelihoodData.swift in Sources */ = {isa = PBXBuildFile; fileRef = A03CD54A2C891219002DFD48 /* LikelihoodData.swift */; }; + A03CD5512C891219002DFD48 /* MetricsData.swift in Sources */ = {isa = PBXBuildFile; fileRef = A03CD54B2C891219002DFD48 /* MetricsData.swift */; }; + A03CD5522C891219002DFD48 /* OfferContextData.swift in Sources */ = {isa = PBXBuildFile; fileRef = A03CD54C2C891219002DFD48 /* OfferContextData.swift */; }; + A03CD5532C891219002DFD48 /* OfferData.swift in Sources */ = {isa = PBXBuildFile; fileRef = A03CD54D2C891219002DFD48 /* OfferData.swift */; }; + A03CD5542C891219002DFD48 /* SubscriptionPaymentData.swift in Sources */ = {isa = PBXBuildFile; fileRef = A03CD54E2C891219002DFD48 /* SubscriptionPaymentData.swift */; }; + A03CD5562C891287002DFD48 /* EventsFactory+Paid.swift in Sources */ = {isa = PBXBuildFile; fileRef = A03CD5552C891287002DFD48 /* EventsFactory+Paid.swift */; }; + A03CD55A2C8926F3002DFD48 /* PaidEventsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A03CD5592C8926F3002DFD48 /* PaidEventsTests.swift */; }; A0A52C432C873E0600748DCB /* ContentMarkAsPaid.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0A52C422C873E0600748DCB /* ContentMarkAsPaid.swift */; }; A0A52C452C88867900748DCB /* ContentMetadata+Parameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0A52C442C88867900748DCB /* ContentMetadata+Parameters.swift */; }; /* End PBXBuildFile section */ @@ -275,7 +284,6 @@ 60108E452B0515C6005656C1 /* AtomicArray.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AtomicArray.swift; sourceTree = ""; }; 60108E472B0515C6005656C1 /* TrackingStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrackingStorage.swift; sourceTree = ""; }; 60108E482B0515C6005656C1 /* UserDefaultsStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDefaultsStorage.swift; sourceTree = ""; }; - 60108E4A2B0515C6005656C1 /* DispatchQueue+BackgroundTimer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DispatchQueue+BackgroundTimer.swift"; sourceTree = ""; }; 60108E4B2B0515C6005656C1 /* VideoContentCategory+ParameterName.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "VideoContentCategory+ParameterName.swift"; sourceTree = ""; }; 60108E4C2B0515C6005656C1 /* Dictionary+JSON.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Dictionary+JSON.swift"; sourceTree = ""; }; 60108E4E2B0515C6005656C1 /* VideoVisibility+ParameterName.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "VideoVisibility+ParameterName.swift"; sourceTree = ""; }; @@ -329,6 +337,16 @@ 7D9F12BA2791A15D00F3635A /* Changelogs */ = {isa = PBXFileReference; lastKnownFileType = folder; name = Changelogs; path = ../Changelogs; sourceTree = ""; }; 7DE4453A26E8913900A93431 /* LoggerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggerTests.swift; sourceTree = ""; }; 8E2A37C5A1593C87B0EB5A44 /* Pods-RingPublishingTrackingTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RingPublishingTrackingTests.release.xcconfig"; path = "Target Support Files/Pods-RingPublishingTrackingTests/Pods-RingPublishingTrackingTests.release.xcconfig"; sourceTree = ""; }; + A03CD5402C88AE44002DFD48 /* Encodable+JSON.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Encodable+JSON.swift"; sourceTree = ""; }; + A03CD5442C890312002DFD48 /* PaidEventUserId.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaidEventUserId.swift; sourceTree = ""; }; + A03CD5462C890A3A002DFD48 /* RingPublishingTracking+Paid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RingPublishingTracking+Paid.swift"; sourceTree = ""; }; + A03CD54A2C891219002DFD48 /* LikelihoodData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LikelihoodData.swift; sourceTree = ""; }; + A03CD54B2C891219002DFD48 /* MetricsData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MetricsData.swift; sourceTree = ""; }; + A03CD54C2C891219002DFD48 /* OfferContextData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OfferContextData.swift; sourceTree = ""; }; + A03CD54D2C891219002DFD48 /* OfferData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OfferData.swift; sourceTree = ""; }; + A03CD54E2C891219002DFD48 /* SubscriptionPaymentData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionPaymentData.swift; sourceTree = ""; }; + A03CD5552C891287002DFD48 /* EventsFactory+Paid.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "EventsFactory+Paid.swift"; sourceTree = ""; }; + A03CD5592C8926F3002DFD48 /* PaidEventsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaidEventsTests.swift; sourceTree = ""; }; A0A52C422C873E0600748DCB /* ContentMarkAsPaid.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentMarkAsPaid.swift; sourceTree = ""; }; A0A52C442C88867900748DCB /* ContentMetadata+Parameters.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ContentMetadata+Parameters.swift"; sourceTree = ""; }; BF2945259492AA544792B04F /* Pods_RingPublishingTracking_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RingPublishingTracking_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -400,6 +418,7 @@ 204EAF332729469A009E2F0D /* IdentifyResponseTests.swift */, 204EAF3527294BF7009E2F0D /* EventTests.swift */, 7D34D08B27BFA9950045CCE0 /* AtomicArrayTests.swift */, + A03CD5592C8926F3002DFD48 /* PaidEventsTests.swift */, ); path = RingPublishingTrackingTests; sourceTree = ""; @@ -441,6 +460,7 @@ 60108DEE2B0515C6005656C1 /* RingPublishingTrackingKeepAliveDataSource.swift */, 60108DEF2B0515C6005656C1 /* RingPublishingTrackingDelegate.swift */, 60108DF02B0515C6005656C1 /* RingPublishingTracking+Aureus.swift */, + A03CD5462C890A3A002DFD48 /* RingPublishingTracking+Paid.swift */, 60108DF12B0515C6005656C1 /* RingPublishingTrackingConfiguration.swift */, 60108DF22B0515C6005656C1 /* Model */, 60108E042B0515C6005656C1 /* RingPublishingTracking+CurrentState.swift */, @@ -453,6 +473,7 @@ 60108DF22B0515C6005656C1 /* Model */ = { isa = PBXGroup; children = ( + A03CD54F2C891219002DFD48 /* Paid */, 60108DF32B0515C6005656C1 /* VideoStartMode.swift */, 60108DF42B0515C6005656C1 /* ContentPageViewSource.swift */, 60108DF52B0515C6005656C1 /* VideoStreamFormat.swift */, @@ -474,21 +495,22 @@ 60108E072B0515C6005656C1 /* Private */ = { isa = PBXGroup; children = ( - 60108E082B0515C6005656C1 /* Constants.swift */, - 60108E092B0515C6005656C1 /* UserManager.swift */, - 60108E0A2B0515C6005656C1 /* RingPublishingTracking+EventsServiceDelegate.swift */, - 60108E0B2B0515C6005656C1 /* RingPublishingTracking+KeepAliveManagerDelegate.swift */, - 60108E0C2B0515C6005656C1 /* OperationMode.swift */, - 60108E0D2B0515C6005656C1 /* Logger */, 60108E102B0515C6005656C1 /* Decorator */, + 60108E492B0515C6005656C1 /* Extensions */, + 60108E0D2B0515C6005656C1 /* Logger */, 60108E222B0515C6005656C1 /* Networking */, - 60108E422B0515C6005656C1 /* EventsFactory.swift */, - 60108E432B0515C6005656C1 /* Utils */, 60108E462B0515C6005656C1 /* Storage */, - 60108E492B0515C6005656C1 /* Extensions */, 60108E542B0515C6005656C1 /* Tracking */, + 60108E432B0515C6005656C1 /* Utils */, + 60108E082B0515C6005656C1 /* Constants.swift */, + 60108E422B0515C6005656C1 /* EventsFactory.swift */, + A03CD5552C891287002DFD48 /* EventsFactory+Paid.swift */, 60108E692B0515C6005656C1 /* Operationable.swift */, + 60108E0C2B0515C6005656C1 /* OperationMode.swift */, + 60108E0A2B0515C6005656C1 /* RingPublishingTracking+EventsServiceDelegate.swift */, + 60108E0B2B0515C6005656C1 /* RingPublishingTracking+KeepAliveManagerDelegate.swift */, 60108E6A2B0515C6005656C1 /* TrackingIdentifierError+ServiceError.swift */, + 60108E092B0515C6005656C1 /* UserManager.swift */, ); path = Private; sourceTree = ""; @@ -544,6 +566,7 @@ 60108E1C2B0515C6005656C1 /* ClientType.swift */, 60108E1D2B0515C6005656C1 /* UserData.swift */, A0A52C422C873E0600748DCB /* ContentMarkAsPaid.swift */, + A03CD5442C890312002DFD48 /* PaidEventUserId.swift */, ); path = Models; sourceTree = ""; @@ -650,7 +673,6 @@ isa = PBXGroup; children = ( A0A52C442C88867900748DCB /* ContentMetadata+Parameters.swift */, - 60108E4A2B0515C6005656C1 /* DispatchQueue+BackgroundTimer.swift */, 60108E4B2B0515C6005656C1 /* VideoContentCategory+ParameterName.swift */, 60108E4C2B0515C6005656C1 /* Dictionary+JSON.swift */, 60108E4E2B0515C6005656C1 /* VideoVisibility+ParameterName.swift */, @@ -659,6 +681,7 @@ 60108E512B0515C6005656C1 /* VideoStartMode+ParameterName.swift */, 60108E522B0515C6005656C1 /* VideoAdsConfiguration+ParameterName.swift */, 60108E532B0515C6005656C1 /* VideoStreamFormat+ParameterName.swift */, + A03CD5402C88AE44002DFD48 /* Encodable+JSON.swift */, ); path = Extensions; sourceTree = ""; @@ -810,6 +833,18 @@ path = ../Sources; sourceTree = ""; }; + A03CD54F2C891219002DFD48 /* Paid */ = { + isa = PBXGroup; + children = ( + A03CD54A2C891219002DFD48 /* LikelihoodData.swift */, + A03CD54B2C891219002DFD48 /* MetricsData.swift */, + A03CD54C2C891219002DFD48 /* OfferContextData.swift */, + A03CD54D2C891219002DFD48 /* OfferData.swift */, + A03CD54E2C891219002DFD48 /* SubscriptionPaymentData.swift */, + ); + path = Paid; + sourceTree = ""; + }; D8F2BCCA6D38846157E01D50 /* Pods */ = { isa = PBXGroup; children = ( @@ -1014,17 +1049,21 @@ 60108E9D2B0515C6005656C1 /* IdentifyRequest.swift in Sources */, 60108E902B0515C6005656C1 /* SizeProviding.swift in Sources */, 60108EC12B0515C6005656C1 /* VendorManager.swift in Sources */, + A03CD5512C891219002DFD48 /* MetricsData.swift in Sources */, 204EAF0B2718DA95009E2F0D /* MD5Tests.swift in Sources */, + A03CD5472C890A3A002DFD48 /* RingPublishingTracking+Paid.swift in Sources */, 60108ECE2B0515C6005656C1 /* EventsService+Decorators.swift in Sources */, 60108E742B0515C6005656C1 /* VideoState.swift in Sources */, 60108E802B0515C6005656C1 /* TrackingIdentifier.swift in Sources */, 60108EB82B0515C6005656C1 /* VideoContentCategory+ParameterName.swift in Sources */, 60108E8A2B0515C6005656C1 /* Optional+Logable.swift in Sources */, 60108E6C2B0515C6005656C1 /* RingPublishingTrackingKeepAliveDataSource.swift in Sources */, + A03CD5452C890312002DFD48 /* PaidEventUserId.swift in Sources */, 60108E7C2B0515C6005656C1 /* VideoMetadata.swift in Sources */, 60108EAD2B0515C6005656C1 /* UserAgent.swift in Sources */, 60108E9B2B0515C6005656C1 /* IdentifyResponse.swift in Sources */, 60108ED12B0515C6005656C1 /* EventsService+EventsQueueManagerDelegate.swift in Sources */, + A03CD5542C891219002DFD48 /* SubscriptionPaymentData.swift in Sources */, 60108E712B0515C6005656C1 /* ContentPageViewSource.swift in Sources */, 60108EB62B0515C6005656C1 /* UserDefaultsStorage.swift in Sources */, 60108E6E2B0515C6005656C1 /* RingPublishingTracking+Aureus.swift in Sources */, @@ -1047,6 +1086,7 @@ 60108E982B0515C6005656C1 /* Decorator.swift in Sources */, 60108EBB2B0515C6005656C1 /* VideoVisibility+ParameterName.swift in Sources */, 60108EC42B0515C6005656C1 /* UserActionParameter.swift in Sources */, + A03CD5532C891219002DFD48 /* OfferData.swift in Sources */, 60108E6F2B0515C6005656C1 /* RingPublishingTrackingConfiguration.swift in Sources */, 60108EC02B0515C6005656C1 /* VideoStreamFormat+ParameterName.swift in Sources */, 60108ED42B0515C6005656C1 /* TrackingIdentifierError+ServiceError.swift in Sources */, @@ -1060,6 +1100,7 @@ 60108E7B2B0515C6005656C1 /* ContentMetadata.swift in Sources */, 60108E932B0515C6005656C1 /* ClientType.swift in Sources */, 204EAF38272A7918009E2F0D /* RingPublishingTrackingDelegateMock.swift in Sources */, + A03CD5412C88AE44002DFD48 /* Encodable+JSON.swift in Sources */, 60108EB12B0515C6005656C1 /* Endpoint.swift in Sources */, 60108E852B0515C6005656C1 /* UserManager.swift in Sources */, 20C2394C26F22238005A734A /* NetworkSessionMock.swift in Sources */, @@ -1073,6 +1114,7 @@ 60108EAE2B0515C6005656C1 /* Service.swift in Sources */, 7D1A2B6426E796C000648882 /* StoredValueInUserDefaultsTests.swift in Sources */, 60108E922B0515C6005656C1 /* UniqueIdentifierDecorator.swift in Sources */, + A03CD5502C891219002DFD48 /* LikelihoodData.swift in Sources */, 60108EB42B0515C6005656C1 /* AtomicArray.swift in Sources */, 60108E7E2B0515C6005656C1 /* ArtemisID.swift in Sources */, 204EAEFD27143837009E2F0D /* UserManagerTests.swift in Sources */, @@ -1084,7 +1126,6 @@ 60108EA32B0515C6005656C1 /* BodableError.swift in Sources */, 60108EBC2B0515C6005656C1 /* VideoEvent+VE.swift in Sources */, 204EAF24271F18AB009E2F0D /* KeepAliveManagerDelegateMock.swift in Sources */, - 60108EB72B0515C6005656C1 /* DispatchQueue+BackgroundTimer.swift in Sources */, 60108EC82B0515C6005656C1 /* KeepAliveManagerDelegate.swift in Sources */, 60108EC52B0515C6005656C1 /* VendorIdentifiable.swift in Sources */, 2088320B2702926D00EA53FD /* XCTestCase+Helpers.swift in Sources */, @@ -1096,6 +1137,8 @@ 60108EB32B0515C6005656C1 /* StoredValueInUserDefaults.swift in Sources */, 7DE4453B26E8913900A93431 /* LoggerTests.swift in Sources */, 60108E752B0515C6005656C1 /* VideoEvent.swift in Sources */, + A03CD5562C891287002DFD48 /* EventsFactory+Paid.swift in Sources */, + A03CD55A2C8926F3002DFD48 /* PaidEventsTests.swift in Sources */, 204EAF1C271DB4C3009E2F0D /* AureusTests.swift in Sources */, 60108EB52B0515C6005656C1 /* TrackingStorage.swift in Sources */, 60108E882B0515C6005656C1 /* OperationMode.swift in Sources */, @@ -1130,6 +1173,7 @@ 60108E8C2B0515C6005656C1 /* StructureInfoDecorator.swift in Sources */, 60108E812B0515C6005656C1 /* RingPublishingTracking+CurrentState.swift in Sources */, 204EAF20271EE5A2009E2F0D /* KeepAliveIntervalsProviderTests.swift in Sources */, + A03CD5522C891219002DFD48 /* OfferContextData.swift in Sources */, 208CD61A26F9B276006A406C /* EventsQueueManagerTests.swift in Sources */, 20C2394D26F22238005A734A /* APIServiceTests.swift in Sources */, 60108E942B0515C6005656C1 /* UserData.swift in Sources */, @@ -1300,7 +1344,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Manual; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = 97W46277W4; INFOPLIST_FILE = RingPublishingTracking/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -1323,7 +1367,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Manual; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = 97W46277W4; INFOPLIST_FILE = RingPublishingTracking/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -1344,7 +1388,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; CODE_SIGN_STYLE = Manual; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = 97W46277W4; INFOPLIST_FILE = ../Tests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.5; LD_RUNPATH_SEARCH_PATHS = ( @@ -1367,7 +1411,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; CODE_SIGN_STYLE = Manual; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = 97W46277W4; INFOPLIST_FILE = ../Tests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.5; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/Sources/RingPublishingTracking/Private/Decorator/Generic/ClientDecorator.swift b/Sources/RingPublishingTracking/Private/Decorator/Generic/ClientDecorator.swift index 4c208fc..9302368 100644 --- a/Sources/RingPublishingTracking/Private/Decorator/Generic/ClientDecorator.swift +++ b/Sources/RingPublishingTracking/Private/Decorator/Generic/ClientDecorator.swift @@ -14,12 +14,7 @@ final class ClientDecorator: Decorator { var parameters: [String: AnyHashable] { var userDataParams: [String: AnyHashable] = [:] - // swiftlint:disable non_optional_string_data_conversion - if let jsonData = try? JSONEncoder().encode(client), - let jsonString = String(data: jsonData, encoding: .utf8) { - userDataParams["RDLC"] = Data(jsonString.utf8).base64EncodedString() - } - // swiftlint:enable non_optional_string_data_conversion + userDataParams["RDLC"] = client.jsonStringBase64 return userDataParams } diff --git a/Sources/RingPublishingTracking/Private/Decorator/Generic/Models/ContentMarkAsPaid.swift b/Sources/RingPublishingTracking/Private/Decorator/Generic/Models/ContentMarkAsPaid.swift index e7d3482..b7f9cd7 100644 --- a/Sources/RingPublishingTracking/Private/Decorator/Generic/Models/ContentMarkAsPaid.swift +++ b/Sources/RingPublishingTracking/Private/Decorator/Generic/Models/ContentMarkAsPaid.swift @@ -3,6 +3,7 @@ // RingPublishingTracking // // Created by Bernard Bijoch on 03/09/2024. +// Copyright © 2023 Ringier Axel Springer Tech. All rights reserved. // import Foundation @@ -30,7 +31,11 @@ struct ContentMarkAsPaid: Encodable { case source } - init(contentMetadata: ContentMetadata) { + init?(contentMetadata: ContentMetadata?) { + guard let contentMetadata = contentMetadata else { + return nil + } + publication = Publication(premium: contentMetadata.paidContent) source = Source(id: contentMetadata.contentId, system: contentMetadata.sourceSystemName) } diff --git a/Sources/RingPublishingTracking/Private/Decorator/Generic/Models/PaidEventUserId.swift b/Sources/RingPublishingTracking/Private/Decorator/Generic/Models/PaidEventUserId.swift new file mode 100644 index 0000000..5264c65 --- /dev/null +++ b/Sources/RingPublishingTracking/Private/Decorator/Generic/Models/PaidEventUserId.swift @@ -0,0 +1,20 @@ +// +// PaidEventUserId.swift +// RingPublishingTracking +// +// Created by Bernard Bijoch on 04/09/2024. +// Copyright © 2023 Ringier Axel Springer Tech. All rights reserved. +// + +import Foundation + +struct PaidEventUserId: Encodable { + + enum CodingKeys: String, CodingKey { + case fakeUserId = "fake_user_id" + case realUserId = "real_user_id" + } + + let fakeUserId: String? + let realUserId: String? +} diff --git a/Sources/RingPublishingTracking/Private/EventsFactory+Paid.swift b/Sources/RingPublishingTracking/Private/EventsFactory+Paid.swift new file mode 100644 index 0000000..c028f84 --- /dev/null +++ b/Sources/RingPublishingTracking/Private/EventsFactory+Paid.swift @@ -0,0 +1,173 @@ +// +// EventsFactory+Paid.swift +// RingPublishingTracking +// +// Created by Bernard Bijoch on 04/09/2024. +// Copyright © 2023 Ringier Axel Springer Tech. All rights reserved. +// + +import Foundation + +extension EventsFactory { + + func createPaidEvent(parameters: [String: AnyHashable]) -> Event { + return Event(analyticsSystemName: AnalyticsSystem.kropkaEvents.rawValue, + eventName: EventType.paid.rawValue, + eventParameters: parameters) + } + + func createShowOfferEvent(contentMetadata: ContentMetadata?, + offerData: OfferData, + offerContextData: OfferContextData, + targetPromotionCampaignCode: String?) -> Event { + var parameters: [String: AnyHashable] = [:] + + parameters["event_category"] = "checkout" + parameters["event_action"] = "showOffer" + parameters["supplier_app_id"] = offerData.supplierData.supplierAppId + parameters["paywall_supplier"] = offerData.supplierData.paywallSupplier + parameters["paywall_template_id"] = offerData.paywallTemplateId + parameters["display_mode"] = offerData.displayMode.rawValue + parameters["source"] = offerContextData.source + parameters["source_dx"] = contentMetadata?.dxParameter + parameters["source_publication_uuid"] = contentMetadata?.publicationId + parameters["paywall_variant_id"] = offerData.paywallVariantId + parameters["closure_percentage"] = offerContextData.closurePercentage + parameters["tpcc"] = targetPromotionCampaignCode + parameters["RDLCN"] = contentMetadata?.rdlcnParameter + + return createPaidEvent(parameters: parameters) + } + + func createShowOfferTeaserEvent(contentMetadata: ContentMetadata?, + offerData: OfferData, + offerContextData: OfferContextData, + targetPromotionCampaignCode: String?) -> Event { + var parameters: [String: AnyHashable] = [:] + + parameters["event_category"] = "checkout" + parameters["event_action"] = "showOfferTeaser" + parameters["supplier_app_id"] = offerData.supplierData.supplierAppId + parameters["paywall_supplier"] = offerData.supplierData.paywallSupplier + parameters["paywall_template_id"] = offerData.paywallTemplateId + parameters["display_mode"] = offerData.displayMode.rawValue + parameters["source"] = offerContextData.source + parameters["source_dx"] = contentMetadata?.dxParameter + parameters["source_publication_uuid"] = contentMetadata?.publicationId + parameters["paywall_variant_id"] = offerData.paywallVariantId + parameters["closure_percentage"] = offerContextData.closurePercentage + parameters["tpcc"] = targetPromotionCampaignCode + parameters["RDLCN"] = contentMetadata?.rdlcnParameter + + return createPaidEvent(parameters: parameters) + } + + func createPurchaseClickButtonEvent(contentMetadata: ContentMetadata?, + offerData: OfferData, + offerContextData: OfferContextData, + termId: String, + targetPromotionCampaignCode: String?) -> Event { + var parameters: [String: AnyHashable] = [:] + + parameters["event_category"] = "checkout" + parameters["event_action"] = "clickButton" + parameters["supplier_app_id"] = offerData.supplierData.supplierAppId + parameters["paywall_supplier"] = offerData.supplierData.paywallSupplier + parameters["paywall_template_id"] = offerData.paywallTemplateId + parameters["display_mode"] = offerData.displayMode.rawValue + parameters["source"] = offerContextData.source + parameters["term_id"] = termId + parameters["source_dx"] = contentMetadata?.dxParameter + parameters["source_publication_uuid"] = contentMetadata?.publicationId + parameters["paywall_variant_id"] = offerData.paywallVariantId + parameters["tpcc"] = targetPromotionCampaignCode + parameters["RDLCN"] = contentMetadata?.rdlcnParameter + + return createPaidEvent(parameters: parameters) + } + + // swiftlint:disable function_parameter_count + + func createPurchaseEvent(contentMetadata: ContentMetadata?, + offerData: OfferData, + offerContextData: OfferContextData, + subscriptionPaymentData: SubscriptionPaymentData, + termId: String, + termConversionId: String, + targetPromotionCampaignCode: String?, + temporaryUserId: String?) -> Event { + var parameters: [String: AnyHashable] = [:] + + parameters["event_category"] = "checkout" + parameters["event_action"] = "purchase" + parameters["supplier_app_id"] = offerData.supplierData.supplierAppId + parameters["paywall_supplier"] = offerData.supplierData.paywallSupplier + parameters["paywall_template_id"] = offerData.paywallTemplateId + parameters["display_mode"] = offerData.displayMode.rawValue + parameters["source"] = offerContextData.source + parameters["term_id"] = termId + parameters["term_conversion_id"] = termConversionId + parameters["payment_method"] = subscriptionPaymentData.paymentMethod.rawValue + parameters["subscription_base_price"] = subscriptionPaymentData.subscriptionBasePrice + parameters["subscription_price_currency"] = subscriptionPaymentData.subscriptionPriceCurrency + parameters["source_dx"] = contentMetadata?.dxParameter + parameters["source_publication_uuid"] = contentMetadata?.publicationId + parameters["paywall_variant_id"] = offerData.paywallVariantId + parameters["tpcc"] = targetPromotionCampaignCode + parameters["subscription_promo_price"] = subscriptionPaymentData.subscriptionPromoPrice + parameters["subscription_promo_price_duration"] = subscriptionPaymentData.subscriptionPromoPriceDuration + parameters["event_details"] = PaidEventUserId(fakeUserId: temporaryUserId, realUserId: nil).jsonString + parameters["RDLCN"] = contentMetadata?.rdlcnParameter + + return createPaidEvent(parameters: parameters) + } + + // swiftlint:enable function_parameter_count + + func createShowMetricLimitEvent(contentMetadata: ContentMetadata, + supplierData: SupplierData, + metricsData: MetricsData) -> Event { + var parameters: [String: AnyHashable] = [:] + + parameters["event_category"] = "metric_limit" + parameters["event_action"] = "showMetricLimit" + parameters["supplier_app_id"] = supplierData.supplierAppId + parameters["paywall_supplier"] = supplierData.paywallSupplier + parameters["metric_limit_name"] = metricsData.metricLimitName + parameters["free_pv_cnt"] = metricsData.freePageViewCount + parameters["free_pv_limit"] = metricsData.freePageViewLimit + parameters["source_dx"] = contentMetadata.dxParameter + parameters["source_publication_uuid"] = contentMetadata.publicationId + parameters["RDLCN"] = contentMetadata.rdlcnParameter + + return createPaidEvent(parameters: parameters) + } + + func createLikelihoodScoringEvent(contentMetadata: ContentMetadata?, + supplierData: SupplierData, + likelihoodData: LikelihoodData) -> Event { + var parameters: [String: AnyHashable] = [:] + + parameters["event_category"] = "likelihood_scoring" + parameters["event_action"] = "likelihoodScoring" + parameters["supplier_app_id"] = supplierData.supplierAppId + parameters["paywall_supplier"] = supplierData.paywallSupplier + parameters["source_dx"] = contentMetadata?.dxParameter + parameters["source_publication_uuid"] = contentMetadata?.publicationId + parameters["event_details"] = likelihoodData.jsonString + parameters["RDLCN"] = contentMetadata?.rdlcnParameter + + return createPaidEvent(parameters: parameters) + } + + func createMobileAppFakeUserIdReplacedEvent(temporaryUserId: String, + realUserId: String) -> Event { + var parameters: [String: AnyHashable] = [:] + + parameters["event_category"] = "mobile_app_fake_user_id_replaced" + parameters["event_action"] = "mobileAppFakeUserIdReplaced" + parameters["event_details"] = PaidEventUserId(fakeUserId: temporaryUserId, realUserId: realUserId).jsonString + + return createPaidEvent(parameters: parameters) + } +} diff --git a/Sources/RingPublishingTracking/Private/Extensions/ContentMetadata+Parameters.swift b/Sources/RingPublishingTracking/Private/Extensions/ContentMetadata+Parameters.swift index b92f5d2..5b0dd41 100644 --- a/Sources/RingPublishingTracking/Private/Extensions/ContentMetadata+Parameters.swift +++ b/Sources/RingPublishingTracking/Private/Extensions/ContentMetadata+Parameters.swift @@ -25,19 +25,7 @@ extension ContentMetadata { return "PV_4,\(sourceSystem),\(pubId),\(part),\(paid)".replacingOccurrences(of: " ", with: "_") } - // swiftlint:disable non_optional_string_data_conversion - var rdlcnParameter: String? { - let contentMarkAsPaid = ContentMarkAsPaid(contentMetadata: self) - - if let jsonData = try? Self.encoder.encode(contentMarkAsPaid), - let jsonString = String(data: jsonData, encoding: .utf8) { - return Data(jsonString.utf8).base64EncodedString() - } - - return nil + return ContentMarkAsPaid(contentMetadata: self).jsonStringBase64 } - - // swiftlint:enable non_optional_string_data_conversion - } diff --git a/Sources/RingPublishingTracking/Private/Extensions/DispatchQueue+BackgroundTimer.swift b/Sources/RingPublishingTracking/Private/Extensions/DispatchQueue+BackgroundTimer.swift deleted file mode 100644 index aee4107..0000000 --- a/Sources/RingPublishingTracking/Private/Extensions/DispatchQueue+BackgroundTimer.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// DispatchQueue+BackgroundTimer.swift -// RingPublishingTracking -// -// Created by Artur Rymarz on 17/10/2021. -// Copyright © 2021 Ringier Axel Springer Tech. All rights reserved. -// - -import Foundation - -extension DispatchQueue { - - /// Schedule timer on background thread - /// Action is called on main thread - /// - /// - Parameters: - /// - timeInterval: TimeInterval from now when timer should fire - /// - action: (() -> Void)? Action to execute when timer is fired - static func scheduledTimer(timeInterval: TimeInterval, action: (() -> Void)?) -> DispatchSourceTimer { - let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.global(qos: .background)) - timer.setEventHandler { - DispatchQueue.main.async { - action?() - } - } - timer.schedule(deadline: .now() + timeInterval, repeating: .never) - timer.activate() - - return timer - } -} diff --git a/Sources/RingPublishingTracking/Private/Extensions/Encodable+JSON.swift b/Sources/RingPublishingTracking/Private/Extensions/Encodable+JSON.swift new file mode 100644 index 0000000..277f39d --- /dev/null +++ b/Sources/RingPublishingTracking/Private/Extensions/Encodable+JSON.swift @@ -0,0 +1,32 @@ +// +// Encodable+JSON.swift +// RingPublishingTracking +// +// Created by Bernard Bijoch on 04/09/2024. +// Copyright © 2023 Ringier Axel Springer Tech. All rights reserved. +// + +import Foundation + +extension Encodable { + + // swiftlint:disable non_optional_string_data_conversion + + var jsonString: String? { + let encoder = JSONEncoder() + encoder.outputFormatting = .sortedKeys + + guard let data = try? encoder.encode(self), + let jsonString = String(data: data, encoding: .utf8) else { return nil } + + return jsonString + } + + // swiftlint:enable non_optional_string_data_conversion + + var jsonStringBase64: String? { + guard let jsonString = jsonString else { return nil } + + return Data(jsonString.utf8).base64EncodedString() + } +} diff --git a/Sources/RingPublishingTracking/Private/Extensions/VideoVisibility+ParameterName.swift b/Sources/RingPublishingTracking/Private/Extensions/VideoVisibility+ParameterName.swift index 1c4ee9f..d61dca7 100644 --- a/Sources/RingPublishingTracking/Private/Extensions/VideoVisibility+ParameterName.swift +++ b/Sources/RingPublishingTracking/Private/Extensions/VideoVisibility+ParameterName.swift @@ -25,14 +25,7 @@ extension VideoVisibility { let visibilityData = VideoVisibilityWrapper(context: VideoVisibilityContext(visible: visibilityParamName)) - // swiftlint:disable non_optional_string_data_conversion - guard let jsonData = try? JSONEncoder().encode(visibilityData), - let jsonString = String(data: jsonData, encoding: .utf8) else { - return nil - } - // swiftlint:enable non_optional_string_data_conversion - - return Data(jsonString.utf8).base64EncodedString() + return visibilityData.jsonStringBase64 } } diff --git a/Sources/RingPublishingTracking/Private/Tracking/EventType.swift b/Sources/RingPublishingTracking/Private/Tracking/EventType.swift index b089417..8e0f982 100644 --- a/Sources/RingPublishingTracking/Private/Tracking/EventType.swift +++ b/Sources/RingPublishingTracking/Private/Tracking/EventType.swift @@ -16,4 +16,5 @@ enum EventType: String { case keepAlive = "KeepAlive" case videoEvent = "VidEvent" case error = "ErrEvent" + case paid = "PaidEvent" } diff --git a/Sources/RingPublishingTracking/Private/Tracking/KeepAlive/KeepAliveManager.swift b/Sources/RingPublishingTracking/Private/Tracking/KeepAlive/KeepAliveManager.swift index f0c0d24..9bb2ed1 100644 --- a/Sources/RingPublishingTracking/Private/Tracking/KeepAlive/KeepAliveManager.swift +++ b/Sources/RingPublishingTracking/Private/Tracking/KeepAlive/KeepAliveManager.swift @@ -39,6 +39,7 @@ final class KeepAliveManager { private var pauseTimeStart = [Date]() private var pauseTimeEnd = [Date]() + private let timerQueue = DispatchQueue(label: "timerQueue", attributes: .concurrent) private var measurementTimer: DispatchSourceTimer? private var sendingTimer: DispatchSourceTimer? @@ -213,6 +214,25 @@ extension KeepAliveManager { extension KeepAliveManager { + /// Schedule timer on dedicated concurrent queue + /// Action is called on main thread + /// + /// - Parameters: + /// - timeInterval: TimeInterval from now when timer should fire + /// - action: (() -> Void)? Action to execute when timer is fired + func scheduledTimer(timeInterval: TimeInterval, action: (() -> Void)?) -> DispatchSourceTimer { + let timer = DispatchSource.makeTimerSource(queue: timerQueue) + timer.setEventHandler { + DispatchQueue.main.async { + action?() + } + } + timer.schedule(deadline: .now() + timeInterval, repeating: .never) + timer.activate() + + return timer + } + private func scheduleMeasurementTimer() { guard let timeFromStart = timeFromStart else { stop() @@ -223,7 +243,7 @@ extension KeepAliveManager { Logger.log("Keep alive manager: Scheduling measurement timer \(interval)s") - measurementTimer = DispatchQueue.scheduledTimer(timeInterval: interval, action: { [weak self] in + measurementTimer = scheduledTimer(timeInterval: interval, action: { [weak self] in self?.takeMeasurements(measureType: .activityTimer) self?.scheduleMeasurementTimer() }) @@ -239,7 +259,7 @@ extension KeepAliveManager { Logger.log("Keep alive manager: Scheduling sending timer \(interval)s") - sendingTimer = DispatchQueue.scheduledTimer(timeInterval: interval, action: { [weak self] in + sendingTimer = scheduledTimer(timeInterval: interval, action: { [weak self] in self?.takeMeasurements(measureType: .sendTimer) self?.sendMeasurements() self?.scheduleSendingTimer() diff --git a/Sources/RingPublishingTracking/Public/Model/Paid/LikelihoodData.swift b/Sources/RingPublishingTracking/Public/Model/Paid/LikelihoodData.swift new file mode 100644 index 0000000..108404e --- /dev/null +++ b/Sources/RingPublishingTracking/Public/Model/Paid/LikelihoodData.swift @@ -0,0 +1,34 @@ +// +// LikelihoodData.swift +// RingPublishingTracking +// +// Created by Bernard Bijoch on 04/09/2024. +// Copyright © 2023 Ringier Axel Springer Tech. All rights reserved. +// + +import Foundation + +/// User likelihood data +public struct LikelihoodData: Encodable { + + enum CodingKeys: String, CodingKey { + case likelihoodToSubscribe = "lts" + case likelihoodToCancel = "ltc" + } + + /// Likelihood to subscribe + let likelihoodToSubscribe: Int? + + /// Likelihood to cancel + let likelihoodToCancel: Int? + + /// LikelihoodData initializer + /// + /// Prameters: + /// - likelihoodToSubscribe: Likelihood to subscribe + /// - likelihoodToCancel: Likelihood to cancel + public init(likelihoodToSubscribe: Int?, likelihoodToCancel: Int?) { + self.likelihoodToSubscribe = likelihoodToSubscribe + self.likelihoodToCancel = likelihoodToCancel + } +} diff --git a/Sources/RingPublishingTracking/Public/Model/Paid/MetricsData.swift b/Sources/RingPublishingTracking/Public/Model/Paid/MetricsData.swift new file mode 100644 index 0000000..e7fc73f --- /dev/null +++ b/Sources/RingPublishingTracking/Public/Model/Paid/MetricsData.swift @@ -0,0 +1,34 @@ +// +// MetricsData.swift +// RingPublishingTracking +// +// Created by Bernard Bijoch on 04/09/2024. +// Copyright © 2023 Ringier Axel Springer Tech. All rights reserved. +// + +import Foundation + +/// Metric counter data +public struct MetricsData { + + /// Name of displayed metric counter + let metricLimitName: String + + /// Number of views remaining within the metric counter + let freePageViewCount: Int + + /// Number of free views within the metric counter + let freePageViewLimit: Int + + /// MetricsData initializer + /// + /// Parameters: + /// - metricLimitName: Name of displayed metric counter + /// - freePageViewCount: Number of views remaining within the metric counter + /// - freePageViewLimit: Number of free views within the metric counter + public init(metricLimitName: String, freePageViewCount: Int, freePageViewLimit: Int) { + self.metricLimitName = metricLimitName + self.freePageViewCount = freePageViewCount + self.freePageViewLimit = freePageViewLimit + } +} diff --git a/Sources/RingPublishingTracking/Public/Model/Paid/OfferContextData.swift b/Sources/RingPublishingTracking/Public/Model/Paid/OfferContextData.swift new file mode 100644 index 0000000..f5091e0 --- /dev/null +++ b/Sources/RingPublishingTracking/Public/Model/Paid/OfferContextData.swift @@ -0,0 +1,29 @@ +// +// OfferContextData.swift +// RingPublishingTracking +// +// Created by Bernard Bijoch on 04/09/2024. +// Copyright © 2023 Ringier Axel Springer Tech. All rights reserved. +// + +import Foundation + +/// Data for the offer context +public struct OfferContextData { + + /// Location where the offer was presented + let source: String + + /// Percentage of offer being hidden by the paywall + let closurePercentage: Int? + + /// OfferContextData initializer + /// + /// Parameters: + /// - source: Location where the offer was presented + /// - closurePercentage: Percentage of offer being hidden by the paywall + public init(source: String, closurePercentage: Int?) { + self.source = source + self.closurePercentage = closurePercentage + } +} diff --git a/Sources/RingPublishingTracking/Public/Model/Paid/OfferData.swift b/Sources/RingPublishingTracking/Public/Model/Paid/OfferData.swift new file mode 100644 index 0000000..3d04539 --- /dev/null +++ b/Sources/RingPublishingTracking/Public/Model/Paid/OfferData.swift @@ -0,0 +1,60 @@ +// +// OfferData.swift +// RingPublishingTracking +// +// Created by Bernard Bijoch on 04/09/2024. +// Copyright © 2023 Ringier Axel Springer Tech. All rights reserved. +// + +import Foundation + +/// Data regarding the supplier of sales offers and offers themselves +public struct OfferData { + let supplierData: SupplierData + let paywallTemplateId: String + let paywallVariantId: String? + let displayMode: OfferDisplayMode + + /// OfferData initializer + /// + /// Paramters: + /// - supplierData: Supplier data + /// - paywallTemplateId: Paywall template ID + /// - paywallVariantId: Paywall variant ID + /// - displayMode: Offer display mode + public init(supplierData: SupplierData, + paywallTemplateId: String, + paywallVariantId: String?, + displayMode: OfferDisplayMode) { + self.supplierData = supplierData + self.paywallTemplateId = paywallTemplateId + self.paywallVariantId = paywallVariantId + self.displayMode = displayMode + } +} + +/// Sales supplier data +public struct SupplierData { + + /// Supplier app ID + let supplierAppId: String + + /// Paywall supplier + let paywallSupplier: String + + /// SupplierData initializer + /// + /// Parameters: + /// - supplierAppId: Supplier app ID + /// - paywallSupplier: Paywall supplier + public init(supplierAppId: String, paywallSupplier: String) { + self.supplierAppId = supplierAppId + self.paywallSupplier = paywallSupplier + } +} + +/// Offer display mode +public enum OfferDisplayMode: String { + case inline + case modal +} diff --git a/Sources/RingPublishingTracking/Public/Model/Paid/SubscriptionPaymentData.swift b/Sources/RingPublishingTracking/Public/Model/Paid/SubscriptionPaymentData.swift new file mode 100644 index 0000000..0028c5d --- /dev/null +++ b/Sources/RingPublishingTracking/Public/Model/Paid/SubscriptionPaymentData.swift @@ -0,0 +1,54 @@ +// +// SubscriptionPaymentData.swift +// RingPublishingTracking +// +// Created by Bernard Bijoch on 04/09/2024. +// Copyright © 2023 Ringier Axel Springer Tech. All rights reserved. +// + +import Foundation + +/// Subscription payment data +public struct SubscriptionPaymentData { + + /// Subscription base price + let subscriptionBasePrice: String + + /// Subscription promotion price (optional - of someone purchases from promotion) + let subscriptionPromoPrice: String? + + /// Promotion duration (optional - of someone purchases from promotion) 1w / 1m / 1y etc. + let subscriptionPromoPriceDuration: String? + + /// Purchase price currency identifier + let subscriptionPriceCurrency: String + + /// Payment method + let paymentMethod: PaymentMethod + + /// SubscriptionPaymentData initializer + /// + /// Parameters: + /// - subscriptionBasePrice: Subscription base price + /// - subscriptionPromoPrice: Subscription promotion price + /// - subscriptionPromoPriceDuration: Promotion duration + /// - subscriptionPriceCurrency: Purchase price currency identifier + /// - paymentMethod: Payment method + public init(subscriptionBasePrice: String, + subscriptionPromoPrice: String?, + subscriptionPromoPriceDuration: String?, + subscriptionPriceCurrency: String, + paymentMethod: PaymentMethod) { + self.subscriptionBasePrice = subscriptionBasePrice + self.subscriptionPromoPrice = subscriptionPromoPrice + self.subscriptionPromoPriceDuration = subscriptionPromoPriceDuration + self.subscriptionPriceCurrency = subscriptionPriceCurrency + self.paymentMethod = paymentMethod + } +} + +/// Payment method +public enum PaymentMethod: String { + case appStore = "app_store" + case other +} diff --git a/Sources/RingPublishingTracking/Public/RingPublishingTracking+Paid.swift b/Sources/RingPublishingTracking/Public/RingPublishingTracking+Paid.swift new file mode 100644 index 0000000..8ea5b82 --- /dev/null +++ b/Sources/RingPublishingTracking/Public/RingPublishingTracking+Paid.swift @@ -0,0 +1,148 @@ +// +// RingPublishingTracking+Paid.swift +// RingPublishingTrackingTests +// +// Created by Bernard Bijoch on 04/09/2024. +// Copyright © 2024 Ringier Axel Springer Tech. All rights reserved. +// + +import Foundation + +extension RingPublishingTracking { + + /// Reports showing offer event + /// There is possibility to start purchasing process flow from this place + /// + /// - Parameters: + /// - contentMetadata: Content metadata + /// - offerData: Data regarding the supplier of sales offers and offers themselves + /// - offerContextData: Data regarding the offer context / content + /// - targetPromotionCampaignCode: Offer id of given promotion / campaign + func reportShowOfferEvent(contentMetadata: ContentMetadata?, + offerData: OfferData, + offerContextData: OfferContextData, + targetPromotionCampaignCode: String?) { + let event = eventsFactory.createShowOfferEvent(contentMetadata: contentMetadata, + offerData: offerData, + offerContextData: offerContextData, + targetPromotionCampaignCode: targetPromotionCampaignCode) + reportEvent(event) + } + + /// Reports showing offer teaser event + /// There is no possibility to start purchasing process flow from this place + /// + /// - Parameters: + /// - contentMetadata: Content metadata + /// - offerData: Data regarding the supplier of sales offers and offers themselves + /// - offerContextData: Data regarding the offer context / content + /// - targetPromotionCampaignCode: Offer id of given promotion / campaign + func reportShowOfferTeaserEvent(contentMetadata: ContentMetadata?, + offerData: OfferData, + offerContextData: OfferContextData, + targetPromotionCampaignCode: String?) { + let event = eventsFactory.createShowOfferTeaserEvent(contentMetadata: contentMetadata, + offerData: offerData, + offerContextData: offerContextData, + targetPromotionCampaignCode: targetPromotionCampaignCode) + reportEvent(event) + } + + /// Reports event of clicking button used to start purchasing process flow + /// + /// - Parameters: + /// - contentMetadata: Content metadata + /// - offerData: Data regarding the supplier of sales offers and offers themselves + /// - offerContextData: Data regarding the offer context / content + /// - termId: Id of specific purchase term / offer selected by user + /// - targetPromotionCampaignCode: Offer id of given promotion / campaign + func reportPurchaseClickButtonEvent(contentMetadata: ContentMetadata?, + offerData: OfferData, + offerContextData: OfferContextData, + termId: String, + targetPromotionCampaignCode: String?) { + let event = eventsFactory.createPurchaseClickButtonEvent(contentMetadata: contentMetadata, + offerData: offerData, + offerContextData: offerContextData, + termId: termId, + targetPromotionCampaignCode: targetPromotionCampaignCode) + reportEvent(event) + } + + // swiftlint:disable function_parameter_count + + /// Reports subscription purchase event + /// + /// - Parameters: + /// - contentMetadata: Content metadata + /// - offerData: Data regarding the supplier of sales offers and offers themselves + /// - offerContextData: Data regarding the offer context / content + /// - subscriptionPaymentData: Data regarding subscription payment + /// - termId: Id of specific purchase term / offer selected by user + /// - termConversionId: Purchase conversion id + /// - targetPromotionCampaignCode: Offer id of given promotion / campaign + /// - temporaryUserId: Temporary user id + func reportPurchaseEvent(contentMetadata: ContentMetadata?, + offerData: OfferData, + offerContextData: OfferContextData, + subscriptionPaymentData: SubscriptionPaymentData, + termId: String, + termConversionId: String, + targetPromotionCampaignCode: String?, + temporaryUserId: String?) { + let event = eventsFactory.createPurchaseEvent(contentMetadata: contentMetadata, + offerData: offerData, + offerContextData: offerContextData, + subscriptionPaymentData: subscriptionPaymentData, + termId: termId, + termConversionId: termConversionId, + targetPromotionCampaignCode: targetPromotionCampaignCode, + temporaryUserId: temporaryUserId) + reportEvent(event) + } + + // swiftlint:enable function_parameter_count + + /// Reports event of displaying paid content to the user within a metered counter. + /// + /// - Parameters: + /// - contentMetadata: Content metadata + /// - supplierData: Data regarding the supplier of sales + /// - metricsData: Metric counter data + func reportShowMetricLimitEvent(contentMetadata: ContentMetadata, + supplierData: SupplierData, + metricsData: MetricsData) { + let event = eventsFactory.createShowMetricLimitEvent(contentMetadata: contentMetadata, + supplierData: supplierData, + metricsData: metricsData) + reportEvent(event) + } + + /// Reports event of prediction of user likelihood to subscribe / cancel subscription + /// + /// - Parameters: + /// - contentMetadata: Content metadata + /// - supplierData: Data regarding the supplier of sales + /// - likelihoodData: Data regarding likelihood to subscribe / cancel subscription + func reportLikelihoodScoringEvent(contentMetadata: ContentMetadata?, + supplierData: SupplierData, + likelihoodData: LikelihoodData) { + let event = eventsFactory.createLikelihoodScoringEvent(contentMetadata: contentMetadata, + supplierData: supplierData, + likelihoodData: likelihoodData) + reportEvent(event) + } + + /// Reports event about changing user data from temporary to real + /// + /// - Parameters: + /// - temporaryUserId: Temporary user id + /// - realUserId: New user id + func reportMobileAppTemporaryUserIdReplacedEvent(temporaryUserId: String, + realUserId: String) { + let event = eventsFactory.createMobileAppFakeUserIdReplacedEvent(temporaryUserId: temporaryUserId, + realUserId: realUserId) + reportEvent(event) + } + +} diff --git a/Tests/RingPublishingTrackingTests/PaidEventsTests.swift b/Tests/RingPublishingTrackingTests/PaidEventsTests.swift new file mode 100644 index 0000000..5775258 --- /dev/null +++ b/Tests/RingPublishingTrackingTests/PaidEventsTests.swift @@ -0,0 +1,294 @@ +// +// PaidEventsTests.swift +// RingPublishingTrackingTests +// +// Created by Bernard Bijoch on 05/09/2024. +// Copyright © 2024 Ringier Axel Springer Tech. All rights reserved. +// + +import Foundation +import XCTest + +class PaidEventsFactoryTests: XCTestCase { + + // swiftlint:disable implicitly_unwrapped_optional + + private var sampleSupplierData: SupplierData! + private var sampleOfferData: OfferData! + private var sampleOfferContextData: OfferContextData! + private var sampleSubscriptionPaymentData: SubscriptionPaymentData! + private var sampleMetricsData: MetricsData! + private var sampleLikelihoodData: LikelihoodData! + private var sampleContentMetadata: ContentMetadata! + + // swiftlint:enable implicitly_unwrapped_optional + + override func setUp() { + super.setUp() + + sampleSupplierData = SupplierData( + supplierAppId: "GTccriLYpe", + paywallSupplier: "piano" + ) + + sampleOfferData = OfferData( + supplierData: sampleSupplierData, + paywallTemplateId: "OTT8ICJL3LWX", + paywallVariantId: "OTVAEW37T5NG3", + displayMode: .inline + ) + + sampleOfferContextData = OfferContextData( + source: "closedArticle", + closurePercentage: 50 + ) + + sampleSubscriptionPaymentData = SubscriptionPaymentData( + subscriptionBasePrice: "100", + subscriptionPromoPrice: "99.99", + subscriptionPromoPriceDuration: "1w", + subscriptionPriceCurrency: "usd", + paymentMethod: .appStore + ) + + sampleMetricsData = MetricsData( + metricLimitName: "OnetMeter", + freePageViewCount: 9, + freePageViewLimit: 10 + ) + + sampleLikelihoodData = LikelihoodData( + likelihoodToSubscribe: 5, + likelihoodToCancel: 4 + ) + + sampleContentMetadata = ContentMetadata( + publicationId: "publicationId", + publicationUrl: URL(string: "https://domain.com")!, // swiftlint:disable:this force_unwrapping + sourceSystemName: "source System_Name", + contentPartIndex: 1, + paidContent: true, + contentId: "my-unique-content-id-1234" + ) + } + + func testCreatePaidEvent_createShowOfferEvent_parametersInEvent() { + // Given + let eventsFactory = EventsFactory() + + // When + let event = eventsFactory.createShowOfferEvent( + contentMetadata: sampleContentMetadata, + offerData: sampleOfferData, + offerContextData: sampleOfferContextData, + targetPromotionCampaignCode: "hard_xmass_promoInline" + ) + + // Then + XCTAssertFalse(event.eventParameters.isEmpty, "Event parameters should not be empty") + } + + func testPaidEvent_createShowOfferEvent_properParametersInEvent() { + // Given + let eventsFactory = EventsFactory() + let sampleTpcc = "hard_xmass_promoInline" + + // When + let event = eventsFactory.createShowOfferEvent( + contentMetadata: sampleContentMetadata, + offerData: sampleOfferData, + offerContextData: sampleOfferContextData, + targetPromotionCampaignCode: sampleTpcc + ) + + // Then + XCTAssertEqual(event.eventParameters["supplier_app_id"], sampleOfferData.supplierData.supplierAppId) + XCTAssertEqual(event.eventParameters["paywall_supplier"], sampleOfferData.supplierData.paywallSupplier) + XCTAssertEqual(event.eventParameters["paywall_template_id"], sampleOfferData.paywallTemplateId) + XCTAssertEqual(event.eventParameters["paywall_variant_id"], sampleOfferData.paywallVariantId) + XCTAssertEqual(event.eventParameters["source"], sampleOfferContextData.source) + XCTAssertEqual(event.eventParameters["source_publication_uuid"], sampleContentMetadata.publicationId) + XCTAssertEqual(event.eventParameters["source_dx"], sampleContentMetadata.dxParameter) + XCTAssertEqual(event.eventParameters["closure_percentage"], sampleOfferContextData.closurePercentage) + XCTAssertEqual(event.eventParameters["tpcc"], sampleTpcc) + XCTAssertEqual(event.eventParameters["RDLCN"], mockRdlcnEncodingPaid()) + } + + func testPaidEvent_createShowOfferTeaserEvent_properParametersInEvent() { + // Given + let eventsFactory = EventsFactory() + let sampleTpcc = "hard_xmass_promoInline" + + // When + let event = eventsFactory.createShowOfferTeaserEvent( + contentMetadata: sampleContentMetadata, + offerData: sampleOfferData, + offerContextData: sampleOfferContextData, + targetPromotionCampaignCode: sampleTpcc + ) + + // Then + XCTAssertEqual(event.eventParameters["supplier_app_id"], sampleOfferData.supplierData.supplierAppId) + XCTAssertEqual(event.eventParameters["paywall_supplier"], sampleOfferData.supplierData.paywallSupplier) + XCTAssertEqual(event.eventParameters["paywall_template_id"], sampleOfferData.paywallTemplateId) + XCTAssertEqual(event.eventParameters["paywall_variant_id"], sampleOfferData.paywallVariantId) + XCTAssertEqual(event.eventParameters["source"], sampleOfferContextData.source) + XCTAssertEqual(event.eventParameters["source_publication_uuid"], sampleContentMetadata.publicationId) + XCTAssertEqual(event.eventParameters["source_dx"], sampleContentMetadata.dxParameter) + XCTAssertEqual(event.eventParameters["closure_percentage"], sampleOfferContextData.closurePercentage) + XCTAssertEqual(event.eventParameters["tpcc"], sampleTpcc) + XCTAssertEqual(event.eventParameters["RDLCN"], mockRdlcnEncodingPaid()) + } + + func testPaidEvent_createPurchaseClickButtonEvent_properParametersInEvent() { + // Given + let eventsFactory = EventsFactory() + let sampleTpcc = "hard_xmass_promoInline" + let sampleTermId = "TMEVT00KVHV0" + let sampleOfferContextData = OfferContextData(source: sampleOfferContextData.source, closurePercentage: nil) + + // When + let event = eventsFactory.createPurchaseClickButtonEvent( + contentMetadata: sampleContentMetadata, + offerData: sampleOfferData, + offerContextData: sampleOfferContextData, + termId: sampleTermId, + targetPromotionCampaignCode: sampleTpcc + ) + + // Then + XCTAssertEqual(event.eventParameters["supplier_app_id"], sampleOfferData.supplierData.supplierAppId) + XCTAssertEqual(event.eventParameters["paywall_supplier"], sampleOfferData.supplierData.paywallSupplier) + XCTAssertEqual(event.eventParameters["paywall_template_id"], sampleOfferData.paywallTemplateId) + XCTAssertEqual(event.eventParameters["paywall_variant_id"], sampleOfferData.paywallVariantId) + XCTAssertEqual(event.eventParameters["source"], sampleOfferContextData.source) + XCTAssertEqual(event.eventParameters["source_publication_uuid"], sampleContentMetadata.publicationId) + XCTAssertEqual(event.eventParameters["source_dx"], sampleContentMetadata.dxParameter) + XCTAssertEqual(event.eventParameters["closure_percentage"], nil) + XCTAssertEqual(event.eventParameters["tpcc"], sampleTpcc) + XCTAssertEqual(event.eventParameters["term_id"], sampleTermId) + XCTAssertEqual(event.eventParameters["RDLCN"], mockRdlcnEncodingPaid()) + } + + func testPaidEvent_createPurchaseEvent_properParametersInEvent() { + // Given + let eventsFactory = EventsFactory() + let sampleTpcc = "hard_xmass_promoInline" + let sampleTermId = "TMEVT00KVHV0" + let sampleFakeUserId = "fake_001" + let sampleTermConversionId = "TCCJTS9X87VB" + let sampleFakeUserJson = "{\"fake_user_id\":\"\(sampleFakeUserId)\"}" + + // When + let event = eventsFactory.createPurchaseEvent( + contentMetadata: sampleContentMetadata, + offerData: sampleOfferData, + offerContextData: OfferContextData(source: sampleOfferContextData.source, closurePercentage: nil), + subscriptionPaymentData: sampleSubscriptionPaymentData, + termId: sampleTermId, + termConversionId: sampleTermConversionId, + targetPromotionCampaignCode: sampleTpcc, + temporaryUserId: sampleFakeUserId + ) + + // swiftlint:disable line_length + + // Then + XCTAssertTrue(!event.eventParameters.isEmpty) + XCTAssertEqual(event.eventParameters["supplier_app_id"], sampleOfferData.supplierData.supplierAppId) + XCTAssertEqual(event.eventParameters["paywall_supplier"], sampleOfferData.supplierData.paywallSupplier) + XCTAssertEqual(event.eventParameters["paywall_template_id"], sampleOfferData.paywallTemplateId) + XCTAssertEqual(event.eventParameters["paywall_variant_id"], sampleOfferData.paywallVariantId) + XCTAssertEqual(event.eventParameters["source"], sampleOfferContextData.source) + XCTAssertEqual(event.eventParameters["source_publication_uuid"], sampleContentMetadata.publicationId) + XCTAssertEqual(event.eventParameters["source_dx"], sampleContentMetadata.dxParameter) + XCTAssertEqual(event.eventParameters["closure_percentage"], nil) + XCTAssertEqual(event.eventParameters["subscription_base_price"], sampleSubscriptionPaymentData.subscriptionBasePrice) + XCTAssertEqual(event.eventParameters["subscription_promo_price"], sampleSubscriptionPaymentData.subscriptionPromoPrice) + XCTAssertEqual(event.eventParameters["subscription_promo_price_duration"], sampleSubscriptionPaymentData.subscriptionPromoPriceDuration) + XCTAssertEqual(event.eventParameters["subscription_price_currency"], sampleSubscriptionPaymentData.subscriptionPriceCurrency) + XCTAssertEqual(event.eventParameters["payment_method"], sampleSubscriptionPaymentData.paymentMethod.rawValue) + XCTAssertEqual(event.eventParameters["tpcc"], sampleTpcc) + XCTAssertEqual(event.eventParameters["term_id"], sampleTermId) + XCTAssertEqual(event.eventParameters["term_conversion_id"], sampleTermConversionId) + XCTAssertEqual(event.eventParameters["RDLCN"], mockRdlcnEncodingPaid()) + XCTAssertEqual(event.eventParameters["event_details"], sampleFakeUserJson) + } + + // swiftlint:enable line_length + + func testPaidEvent_createShowMetricLimitEvent_properParametersInEvent() { + // Given + let eventsFactory = EventsFactory() + + // When + let event = eventsFactory.createShowMetricLimitEvent( + contentMetadata: sampleContentMetadata, + supplierData: sampleSupplierData, + metricsData: sampleMetricsData + ) + + // Then + XCTAssertTrue(!event.eventParameters.isEmpty) + XCTAssertEqual(event.eventParameters["metric_limit_name"], sampleMetricsData.metricLimitName) + XCTAssertEqual(event.eventParameters["free_pv_cnt"], sampleMetricsData.freePageViewCount) + XCTAssertEqual(event.eventParameters["free_pv_limit"], sampleMetricsData.freePageViewLimit) + XCTAssertEqual(event.eventParameters["supplier_app_id"], sampleSupplierData.supplierAppId) + XCTAssertEqual(event.eventParameters["paywall_supplier"], sampleSupplierData.paywallSupplier) + XCTAssertEqual(event.eventParameters["source_publication_uuid"], sampleContentMetadata.publicationId) + XCTAssertEqual(event.eventParameters["source_dx"], sampleContentMetadata.dxParameter) + XCTAssertEqual(event.eventParameters["RDLCN"], mockRdlcnEncodingPaid()) + } + + func testPaidEvent_createLikelihoodScoringEvent_properParametersInEvent() { + // Given + let eventsFactory = EventsFactory() + let sampleLikelihoodJson = "{\"ltc\":\(sampleLikelihoodData.likelihoodToCancel ?? 0)," + + "\"lts\":\(sampleLikelihoodData.likelihoodToSubscribe ?? 0)}" + + // When + let event = eventsFactory.createLikelihoodScoringEvent( + contentMetadata: sampleContentMetadata, + supplierData: sampleSupplierData, + likelihoodData: sampleLikelihoodData + ) + + // Then + XCTAssertTrue(!event.eventParameters.isEmpty) + XCTAssertEqual(event.eventParameters["supplier_app_id"], sampleSupplierData.supplierAppId) + XCTAssertEqual(event.eventParameters["paywall_supplier"], sampleSupplierData.paywallSupplier) + XCTAssertEqual(event.eventParameters["source_publication_uuid"], sampleContentMetadata.publicationId) + XCTAssertEqual(event.eventParameters["source_dx"], sampleContentMetadata.dxParameter) + XCTAssertEqual(event.eventParameters["event_details"], sampleLikelihoodJson) + XCTAssertEqual(event.eventParameters["RDLCN"], mockRdlcnEncodingPaid()) + } + + func testPaidEvent_createMobileAppFakeUserIdReplacedEvent_properParametersInEvent() { + // Given + let eventsFactory = EventsFactory() + let sampleFakeUserId = "fake_001" + let sampleRealUserId = "real_001" + let sampleUserJson = "{\"fake_user_id\":\"\(sampleFakeUserId)\",\"real_user_id\":\"\(sampleRealUserId)\"}" + + // When + let event = eventsFactory.createMobileAppFakeUserIdReplacedEvent( + temporaryUserId: sampleFakeUserId, + realUserId: sampleRealUserId + ) + + // Then + XCTAssertTrue(!event.eventParameters.isEmpty) + XCTAssertEqual(event.eventParameters["event_details"], sampleUserJson) + } + + // Utility function + private func mockRdlcnEncodingPaid() -> String { + let input = "{\"publication\":{\"premium\":\(sampleContentMetadata.paidContent)},\"source\":{\"id\":" + + "\"\(sampleContentMetadata.contentId)\",\"system\":\"\(sampleContentMetadata.sourceSystemName)\"}}" + return encode(input: input) + } + + private func encode(input: String) -> String { + return Data(input.utf8).base64EncodedString(options: .endLineWithLineFeed) + } +} diff --git a/Tests/RingPublishingTrackingTests/Utils/XCTestCase+Helpers.swift b/Tests/RingPublishingTrackingTests/Utils/XCTestCase+Helpers.swift index 06c0859..a6a5647 100644 --- a/Tests/RingPublishingTrackingTests/Utils/XCTestCase+Helpers.swift +++ b/Tests/RingPublishingTrackingTests/Utils/XCTestCase+Helpers.swift @@ -11,6 +11,6 @@ import XCTest extension XCTestCase { func wait(for seconds: TimeInterval) { - RunLoop.current.run(until: Date(timeIntervalSinceNow: seconds)) + _ = XCTWaiter.wait(for: [expectation(description: "Wait for \(seconds) seconds")], timeout: seconds) } }