Skip to content

Commit

Permalink
Subscription attribution (#2845)
Browse files Browse the repository at this point in the history
Add a new pixel to track Privacy Pro subscription attribution
  • Loading branch information
alessandroboron authored May 16, 2024
1 parent 7903952 commit e25b296
Show file tree
Hide file tree
Showing 19 changed files with 283 additions and 39 deletions.
2 changes: 2 additions & 0 deletions Core/PixelEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,7 @@ extension Pixel {
case privacyProVPNAccessRevokedDialogShown
case privacyProVPNBetaStoppedWhenPrivacyProEnabled
case privacyProTransactionProgressNotHiddenAfter60s
case privacyProSuccessfulSubscriptionAttribution

// MARK: Pixel Experiment
case pixelExperimentEnrollment
Expand Down Expand Up @@ -1308,6 +1309,7 @@ extension Pixel.Event {
case .privacyProSubscriptionManagementPlanBilling: return "m_privacy-pro_settings_change-plan-or-billing_click"
case .privacyProSubscriptionManagementRemoval: return "m_privacy-pro_settings_remove-from-device_click"
case .privacyProTransactionProgressNotHiddenAfter60s: return "m_privacy-pro_progress_not_hidden_after_60s"
case .privacyProSuccessfulSubscriptionAttribution: return "m_subscribe"

// MARK: Pixel Experiment
case .pixelExperimentEnrollment: return "pixel_experiment_enrollment"
Expand Down
22 changes: 21 additions & 1 deletion DuckDuckGo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -578,7 +578,10 @@
98F3A1D8217B37010011A0D4 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98F3A1D7217B37010011A0D4 /* Theme.swift */; };
98F6EA472863124100720957 /* ContentBlockerRulesLists.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98F6EA462863124100720957 /* ContentBlockerRulesLists.swift */; };
98F78B8E22419093007CACF4 /* ThemableNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98F78B8D22419093007CACF4 /* ThemableNavigationController.swift */; };
9F2510142BF5809E0096DB16 /* SubscriptionFlowViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F2510132BF5809E0096DB16 /* SubscriptionFlowViewModelTests.swift */; };
9F8FE9492BAE50E50071E372 /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 9F8FE9482BAE50E50071E372 /* Lottie */; };
9FA5E44B2BF1AF3400BDEF02 /* SubscriptionContainerViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA5E44A2BF1AF3400BDEF02 /* SubscriptionContainerViewFactory.swift */; };
9FA5E44E2BF1B16400BDEF02 /* SubscriptionContainerViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA5E44D2BF1B16400BDEF02 /* SubscriptionContainerViewModelTests.swift */; };
AA3D854523D9942200788410 /* AppIconSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3D854423D9942200788410 /* AppIconSettingsViewController.swift */; };
AA3D854723D9E88E00788410 /* AppIconSettingsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3D854623D9E88E00788410 /* AppIconSettingsCell.swift */; };
AA3D854923DA1DFB00788410 /* AppIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3D854823DA1DFB00788410 /* AppIcon.swift */; };
Expand Down Expand Up @@ -2212,6 +2215,9 @@
98F3A1D7217B37010011A0D4 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = "<group>"; };
98F6EA462863124100720957 /* ContentBlockerRulesLists.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentBlockerRulesLists.swift; sourceTree = "<group>"; };
98F78B8D22419093007CACF4 /* ThemableNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemableNavigationController.swift; sourceTree = "<group>"; };
9F2510132BF5809E0096DB16 /* SubscriptionFlowViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionFlowViewModelTests.swift; sourceTree = "<group>"; };
9FA5E44A2BF1AF3400BDEF02 /* SubscriptionContainerViewFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionContainerViewFactory.swift; sourceTree = "<group>"; };
9FA5E44D2BF1B16400BDEF02 /* SubscriptionContainerViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionContainerViewModelTests.swift; sourceTree = "<group>"; };
AA3D854423D9942200788410 /* AppIconSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconSettingsViewController.swift; sourceTree = "<group>"; };
AA3D854623D9E88E00788410 /* AppIconSettingsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconSettingsCell.swift; sourceTree = "<group>"; };
AA3D854823DA1DFB00788410 /* AppIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIcon.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -4087,6 +4093,15 @@
name = Themes;
sourceTree = "<group>";
};
9FA5E44C2BF1B14100BDEF02 /* Subscription */ = {
isa = PBXGroup;
children = (
9FA5E44D2BF1B16400BDEF02 /* SubscriptionContainerViewModelTests.swift */,
9F2510132BF5809E0096DB16 /* SubscriptionFlowViewModelTests.swift */,
);
name = Subscription;
sourceTree = "<group>";
};
AA4D6A8023DE4973007E8790 /* AppIcon */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -4463,6 +4478,7 @@
children = (
D664C7AD2B289AA000CBFA76 /* PurchaseInProgressView.swift */,
D66F683C2BB333C100AE93E2 /* SubscriptionContainerView.swift */,
9FA5E44A2BF1AF3400BDEF02 /* SubscriptionContainerViewFactory.swift */,
D664C7AE2B289AA000CBFA76 /* SubscriptionFlowView.swift */,
D68DF81B2B58302E0023DBEA /* SubscriptionRestoreView.swift */,
D64648AC2B59936B0033090B /* SubscriptionEmailView.swift */,
Expand Down Expand Up @@ -4824,6 +4840,7 @@
F1D477C71F2139210031ED49 /* OmniBar */,
C1B7B52E28944DDC0098FD6A /* RemoteMessaging */,
98EA2C3F218BB5140023E1DC /* Settings */,
9FA5E44C2BF1B14100BDEF02 /* Subscription */,
F13B4BF71F18C9E800814661 /* Tabs */,
98EA2C3A218B9A880023E1DC /* Themes */,
F12790DD1EBBDDF3001D3AEC /* Tutorials */,
Expand Down Expand Up @@ -6492,6 +6509,7 @@
1E8AD1D527C2E22900ABA377 /* DownloadsListSectionViewModel.swift in Sources */,
EE0798C52B179936000A4F64 /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */,
31584616281AFB46004ADB8B /* AutofillLoginDetailsViewController.swift in Sources */,
9FA5E44B2BF1AF3400BDEF02 /* SubscriptionContainerViewFactory.swift in Sources */,
C1F341C72A6924100032057B /* EmailAddressPromptViewModel.swift in Sources */,
F47E53D9250A97330037C686 /* OnboardingDefaultBroswerViewController.swift in Sources */,
F13B4BD51F183B3600814661 /* TabsModelPersistenceExtension.swift in Sources */,
Expand Down Expand Up @@ -6831,6 +6849,7 @@
83EDCC411F86B89C005CDFCD /* StatisticsLoaderTests.swift in Sources */,
C14882E327F20D9A00D59F0C /* BookmarksExporterTests.swift in Sources */,
85C29708247BDD060063A335 /* DaxDialogsBrowsingSpecTests.swift in Sources */,
9FA5E44E2BF1B16400BDEF02 /* SubscriptionContainerViewModelTests.swift in Sources */,
85BA58581F34F72F00C6E8CA /* AppUserDefaultsTests.swift in Sources */,
F1134EBC1F40D45700B73467 /* MockStatisticsStore.swift in Sources */,
31C138AC27A403CB00FFD4B2 /* DownloadManagerTests.swift in Sources */,
Expand Down Expand Up @@ -6884,6 +6903,7 @@
1E8146AD28C8ABF000D1AF63 /* TrackerAnimationLogicTests.swift in Sources */,
C1CDA31E2AFBF811006D1476 /* AutofillNeverPromptWebsitesManagerTests.swift in Sources */,
B6AD9E3A28D456820019CDE9 /* PrivacyConfigurationManagerMock.swift in Sources */,
9F2510142BF5809E0096DB16 /* SubscriptionFlowViewModelTests.swift in Sources */,
F189AED71F18F6DE001EBAE1 /* TabTests.swift in Sources */,
F13B4BFB1F18E3D900814661 /* TabsModelPersistenceExtensionTests.swift in Sources */,
C12B6E7C2BED69C100050D93 /* AutofillPixelReporterTests.swift in Sources */,
Expand Down Expand Up @@ -9834,7 +9854,7 @@
repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit";
requirement = {
kind = exactVersion;
version = 145.1.0;
version = 145.2.0;
};
};
9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/DuckDuckGo/BrowserServicesKit",
"state" : {
"revision" : "739e7a89f1ddf18d2fe83e94a83892e248f60668",
"version" : "145.1.0"
"revision" : "c69a664b58cb351ccb73aa548726854a2861c0ed",
"version" : "145.2.0"
}
},
{
Expand Down
2 changes: 1 addition & 1 deletion DuckDuckGo/MainViewController+Segues.swift
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ extension MainViewController {
os_log(#function, log: .generalLog, type: .debug)
hideAllHighlightsIfNeeded()
launchSettings {
$0.triggerDeepLinkNavigation(to: .subscriptionFlow)
$0.triggerDeepLinkNavigation(to: .subscriptionFlow())
}
}

Expand Down
8 changes: 7 additions & 1 deletion DuckDuckGo/MainViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1372,7 +1372,13 @@ class MainViewController: UIViewController {
.sink { [weak self] notification in
switch notification.name {
case .urlInterceptPrivacyPro:
self?.launchSettings(deepLinkTarget: .subscriptionFlow)
let deepLinkTarget: SettingsViewModel.SettingsDeepLinkSection
if let origin = notification.userInfo?[AttributionParameter.origin] as? String {
deepLinkTarget = .subscriptionFlow(origin: origin)
} else {
deepLinkTarget = .subscriptionFlow()
}
self?.launchSettings(deepLinkTarget: deepLinkTarget)
default:
return
}
Expand Down
6 changes: 3 additions & 3 deletions DuckDuckGo/SettingsRootView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,10 @@ struct SettingsRootView: View {
SubscriptionPIRView()
case .itr:
SubscriptionITPView()
case .subscriptionFlow:
SubscriptionContainerView(currentView: .subscribe).environmentObject(subscriptionNavigationCoordinator)
case let .subscriptionFlow(origin):
SubscriptionContainerViewFactory.makeSubscribeFlow(origin: origin, navigationCoordinator: subscriptionNavigationCoordinator)
case .subscriptionRestoreFlow:
SubscriptionContainerView(currentView: .restore).environmentObject(subscriptionNavigationCoordinator)
SubscriptionContainerViewFactory.makeRestoreFlow(navigationCoordinator: subscriptionNavigationCoordinator)
default:
EmptyView()
}
Expand Down
17 changes: 9 additions & 8 deletions DuckDuckGo/SettingsSubscriptionView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,12 @@ struct SettingsSubscriptionView: View {
Group {
SettingsCustomCell(content: { subscriptionDescriptionView })

let subscribeView = SubscriptionContainerView(currentView: .subscribe)
let subscribeView = SubscriptionContainerViewFactory.makeSubscribeFlow(
origin: nil,
navigationCoordinator: subscriptionNavigationCoordinator
).navigationViewStyle(.stack)
let restoreView = SubscriptionContainerViewFactory.makeRestoreFlow(navigationCoordinator: subscriptionNavigationCoordinator)
.navigationViewStyle(.stack)
.environmentObject(subscriptionNavigationCoordinator)
let restoreView = SubscriptionContainerView(currentView: .restore)
.navigationViewStyle(.stack)
.environmentObject(subscriptionNavigationCoordinator)
.onFirstAppear {
Pixel.fire(pixel: .privacyProRestorePurchaseClick)
}
Expand Down Expand Up @@ -122,9 +122,10 @@ struct SettingsSubscriptionView: View {
}
})

let subscribeView = SubscriptionContainerView(currentView: .subscribe)
.navigationViewStyle(.stack)
.environmentObject(subscriptionNavigationCoordinator)
let subscribeView = SubscriptionContainerViewFactory.makeSubscribeFlow(
origin: nil,
navigationCoordinator: subscriptionNavigationCoordinator
).navigationViewStyle(.stack)
NavigationLink(
destination: subscribeView,
isActive: $isShowingSubscribeFlow,
Expand Down
6 changes: 3 additions & 3 deletions DuckDuckGo/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,10 @@ struct SettingsView: View {
SubscriptionPIRView()
case .itr:
SubscriptionITPView()
case .subscriptionFlow:
SubscriptionContainerView(currentView: .subscribe).environmentObject(subscriptionNavigationCoordinator)
case let .subscriptionFlow(origin):
SubscriptionContainerViewFactory.makeSubscribeFlow(origin: origin, navigationCoordinator: subscriptionNavigationCoordinator)
case .subscriptionRestoreFlow:
SubscriptionContainerView(currentView: .restore).environmentObject(subscriptionNavigationCoordinator)
SubscriptionContainerViewFactory.makeRestoreFlow(navigationCoordinator: subscriptionNavigationCoordinator)
default:
EmptyView()
}
Expand Down
4 changes: 2 additions & 2 deletions DuckDuckGo/SettingsViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -687,11 +687,11 @@ extension SettingsViewModel: AutofillLoginSettingsListViewControllerDelegate {
// MARK: DeepLinks
extension SettingsViewModel {

enum SettingsDeepLinkSection: Identifiable {
enum SettingsDeepLinkSection: Identifiable, Equatable {
case netP
case dbp
case itr
case subscriptionFlow
case subscriptionFlow(origin: String? = nil)
case subscriptionRestoreFlow
// Add other cases as needed

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec

var originalMessage: WKScriptMessage?

private let subscriptionAttributionOrigin: String?
init(subscriptionAttributionOrigin: String?) {
self.subscriptionAttributionOrigin = subscriptionAttributionOrigin
}

func with(broker: UserScriptMessageBroker) {
self.broker = broker
}
Expand Down Expand Up @@ -252,6 +257,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec
case .success(let purchaseUpdate):
DailyPixel.fireDailyAndCount(pixel: .privacyProPurchaseSuccess)
UniquePixel.fire(pixel: .privacyProSubscriptionActivated)
Pixel.fireAttribution(pixel: .privacyProSuccessfulSubscriptionAttribution, origin: subscriptionAttributionOrigin)
setTransactionStatus(.idle)
await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: purchaseUpdate)
case .failure:
Expand Down Expand Up @@ -421,6 +427,24 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature, ObservableObjec
onActivateSubscription = nil
onBackToSettings = nil
}


}

private extension Pixel {

enum AttributionParameters {
static let origin = "origin"
static let locale = "locale"
}

static func fireAttribution(pixel: Pixel.Event, origin: String?, locale: Locale = .current) {
var parameters: [String: String] = [:]
parameters[AttributionParameters.locale] = locale.identifier
if let origin {
parameters[AttributionParameters.origin] = origin
}
Self.fire(pixel: pixel, withAdditionalParameters: parameters)
}

}
// swiftlint:enable file_length
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
//

import Foundation
import Subscription
import Combine

@available(iOS 15.0, *)
Expand All @@ -31,11 +32,14 @@ final class SubscriptionContainerViewModel: ObservableObject {
let email: SubscriptionEmailViewModel


init(userScript: SubscriptionPagesUserScript = SubscriptionPagesUserScript(),
subFeature: SubscriptionPagesUseSubscriptionFeature = SubscriptionPagesUseSubscriptionFeature()) {
init(
origin: String?,
userScript: SubscriptionPagesUserScript,
subFeature: SubscriptionPagesUseSubscriptionFeature
) {
self.userScript = userScript
self.subFeature = subFeature
self.flow = SubscriptionFlowViewModel(userScript: userScript, subFeature: subFeature)
self.flow = SubscriptionFlowViewModel(origin: origin, userScript: userScript, subFeature: subFeature)
self.restore = SubscriptionRestoreViewModel(userScript: userScript, subFeature: subFeature)
self.email = SubscriptionEmailViewModel(userScript: userScript, subFeature: subFeature)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,8 @@ final class SubscriptionFlowViewModel: ObservableObject {
let subFeature: SubscriptionPagesUseSubscriptionFeature
let purchaseManager: PurchaseManager
var webViewModel: AsyncHeadlessWebViewViewModel

var purchaseURL = URL.subscriptionPurchase

let purchaseURL: URL

private var cancellables = Set<AnyCancellable>()
private var canGoBackCancellable: AnyCancellable?
private var urlCancellable: AnyCancellable?
Expand Down Expand Up @@ -69,10 +68,16 @@ final class SubscriptionFlowViewModel: ObservableObject {
allowedDomains: allowedDomains,
contentBlocking: false)

init(userScript: SubscriptionPagesUserScript,
init(origin: String?,
userScript: SubscriptionPagesUserScript,
subFeature: SubscriptionPagesUseSubscriptionFeature,
purchaseManager: PurchaseManager = PurchaseManager.shared,
selectedFeature: SettingsViewModel.SettingsDeepLinkSection? = nil) {
if let origin {
purchaseURL = URL.subscriptionPurchase.appendingParameter(name: AttributionParameter.origin, value: origin)
} else {
purchaseURL = URL.subscriptionPurchase
}
self.userScript = userScript
self.subFeature = subFeature
self.purchaseManager = purchaseManager
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ struct SubscriptionContainerView: View {
private let emailViewModel: SubscriptionEmailViewModel

init(currentView: CurrentView,
viewModel: SubscriptionContainerViewModel = SubscriptionContainerViewModel()) {
viewModel: SubscriptionContainerViewModel) {
_currentViewState = State(initialValue: currentView)
self.viewModel = viewModel
let userScript = viewModel.userScript
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//
// SubscriptionContainerViewFactory.swift
// DuckDuckGo
//
// Copyright © 2024 DuckDuckGo. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import SwiftUI

@available(iOS 15.0, *)
enum SubscriptionContainerViewFactory {

static func makeSubscribeFlow(origin: String?, navigationCoordinator: SubscriptionNavigationCoordinator) -> some View {
let viewModel = SubscriptionContainerViewModel(
origin: origin,
userScript: SubscriptionPagesUserScript(),
subFeature: SubscriptionPagesUseSubscriptionFeature(subscriptionAttributionOrigin: origin)
)
return SubscriptionContainerView(currentView: .subscribe, viewModel: viewModel)
.environmentObject(navigationCoordinator)
}

static func makeRestoreFlow(navigationCoordinator: SubscriptionNavigationCoordinator) -> some View {
let viewModel = SubscriptionContainerViewModel(
origin: nil,
userScript: SubscriptionPagesUserScript(),
subFeature: SubscriptionPagesUseSubscriptionFeature(subscriptionAttributionOrigin: nil)
)
return SubscriptionContainerView(currentView: .restore, viewModel: viewModel)
.environmentObject(navigationCoordinator)
}

}
3 changes: 1 addition & 2 deletions DuckDuckGo/Subscription/Views/SubscriptionSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,7 @@ struct SubscriptionSettingsView: View {
private var devicesSection: some View {
Section(header: Text(UserText.subscriptionManageDevices)) {

NavigationLink(destination: SubscriptionContainerView(currentView: .restore)
.environmentObject(subscriptionNavigationCoordinator),
NavigationLink(destination: SubscriptionContainerViewFactory.makeRestoreFlow(navigationCoordinator: subscriptionNavigationCoordinator),
isActive: $isShowingRestoreView) {
SettingsCustomCell(content: {
Text(UserText.subscriptionAddDeviceButton)
Expand Down
Loading

0 comments on commit e25b296

Please sign in to comment.