diff --git a/Modules/Utils/Sources/PocketCastsUtils/Feature Flags/FeatureFlag.swift b/Modules/Utils/Sources/PocketCastsUtils/Feature Flags/FeatureFlag.swift index b3b718a186..9f4007e094 100644 --- a/Modules/Utils/Sources/PocketCastsUtils/Feature Flags/FeatureFlag.swift +++ b/Modules/Utils/Sources/PocketCastsUtils/Feature Flags/FeatureFlag.swift @@ -378,7 +378,7 @@ public enum FeatureFlag: String, CaseIterable { case .newOnboardingVariant: true case .playlistsRebranding: - false + true case .retryWithoutUserAgent: true case .userSatisfactionSurvey: @@ -450,7 +450,11 @@ extension FeatureFlag: OverrideableFlag { public var canOverride: Bool { switch self { case .searchPredictive, .searchImprovements: - !Self.isTestFlight +#if DEBUG + true +#else + !Self.isTestFlight +#endif default: true } diff --git a/podcasts.xcodeproj/project.pbxproj b/podcasts.xcodeproj/project.pbxproj index 2a9b0b1e2b..617ab70bf2 100644 --- a/podcasts.xcodeproj/project.pbxproj +++ b/podcasts.xcodeproj/project.pbxproj @@ -737,6 +737,7 @@ 9AB645182D5138B200A842C8 /* Pocket Casts App Clip.app in Embed App Clips */ = {isa = PBXBuildFile; fileRef = F5D5F7792CEBE769001F492D /* Pocket Casts App Clip.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 9ACB950B2E0D9199007238A7 /* DescriptiveActionAttributedTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ACB950A2E0D9199007238A7 /* DescriptiveActionAttributedTextView.swift */; }; 9ACB950C2E0D9199007238A7 /* DescriptiveActionAttributedTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ACB950A2E0D9199007238A7 /* DescriptiveActionAttributedTextView.swift */; }; + 9AD41AC22EB4E0750048FB8B /* PlaylistDetailViewController+Analytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AD41AC12EB4E0700048FB8B /* PlaylistDetailViewController+Analytics.swift */; }; 9AD809AF2EA12D940050649B /* PlaylistDetailFetchOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AD809AE2EA12D850050649B /* PlaylistDetailFetchOperation.swift */; }; 9AE5043C2DEDF2030027A4AE /* TranscriptContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AE5043B2DEDF1F80027A4AE /* TranscriptContainerViewController.swift */; }; 9AE5043E2DEDF5590027A4AE /* TranscriptPlaybackManaging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AE5043D2DEDF54F0027A4AE /* TranscriptPlaybackManaging.swift */; }; @@ -3085,6 +3086,7 @@ 9AA14FAC2E45022200464A5A /* NewPlaylistViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewPlaylistViewController.swift; sourceTree = ""; }; 9AA3B6302D27F0B600A0E30E /* CancelSubscriptionPlansViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancelSubscriptionPlansViewModel.swift; sourceTree = ""; }; 9ACB950A2E0D9199007238A7 /* DescriptiveActionAttributedTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DescriptiveActionAttributedTextView.swift; sourceTree = ""; }; + 9AD41AC12EB4E0700048FB8B /* PlaylistDetailViewController+Analytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PlaylistDetailViewController+Analytics.swift"; sourceTree = ""; }; 9AD809AE2EA12D850050649B /* PlaylistDetailFetchOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistDetailFetchOperation.swift; sourceTree = ""; }; 9AE5043B2DEDF1F80027A4AE /* TranscriptContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscriptContainerViewController.swift; sourceTree = ""; }; 9AE5043D2DEDF54F0027A4AE /* TranscriptPlaybackManaging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscriptPlaybackManaging.swift; sourceTree = ""; }; @@ -5672,6 +5674,7 @@ 9AF646872E5B56E500627836 /* Playlist Header */, 9A57780E2E969B1C0094B194 /* PlaylistDetailViewModel */, 9A1A73E02E5623E700E9248C /* PlaylistDetailViewController.swift */, + 9AD41AC12EB4E0700048FB8B /* PlaylistDetailViewController+Analytics.swift */, 9AF646942E5E081400627836 /* PlaylistDetailViewController+TableView.swift */, 9AF646962E5E0BB700627836 /* PlaylistDetailViewController+Search.swift */, 9AF646902E5B774900627836 /* PlaylistDetailViewController+EditActions.swift */, @@ -11286,6 +11289,7 @@ 8B99197729A686BA00A5C81C /* SearchResultsView.swift in Sources */, 40E1B4CA2255856C006C96CE /* PlaylistsShortcutsViewController.swift in Sources */, BDED9312251DD5EB000BF622 /* CarPlayImageHelper.swift in Sources */, + 9AD41AC22EB4E0750048FB8B /* PlaylistDetailViewController+Analytics.swift in Sources */, FF0753842D69088E00BE8B3B /* GridFoldersView.swift in Sources */, BDB206AA2191734D00C1E456 /* NowPlayingHelper.swift in Sources */, 10DFE9372C5A8D1300957D0A /* ABTestProvider.swift in Sources */, diff --git a/podcasts/Analytics/AnalyticsEvent.swift b/podcasts/Analytics/AnalyticsEvent.swift index 9650ac3bd4..2357709500 100644 --- a/podcasts/Analytics/AnalyticsEvent.swift +++ b/podcasts/Analytics/AnalyticsEvent.swift @@ -321,6 +321,24 @@ enum AnalyticsEvent: String { case filterAddEpisodesPodcastTapped case filterAddEpisodesEpisodeTapped + case filterEditRulesTapped + case filterAddEpisodesTapped + + case filterPlayAllTapped + case filterPlayAllSaveUpNextTapped + case filterPlayAllReplaceAndPlayTapped + case filterPlayAllReplaceAndPlayConfirmTapped + case filterPlayAllDismissed + + case filterOptionsTapped + case filterSelectEpisodesTapped + case filterSortByTapped + case filterDownloadAllTapped + case filterChromeCastTapped + case filterArchiveAllTapped + case filterUnarchiveAllTapped + case filterRearrangeEpisodesTapped + case episodeRecentlyPlayedSortOptionTooltipShown case episodeRecentlyPlayedSortOptionTooltipDismissed diff --git a/podcasts/New Detail/PlaylistDetailViewController+Analytics.swift b/podcasts/New Detail/PlaylistDetailViewController+Analytics.swift new file mode 100644 index 0000000000..0493b599cf --- /dev/null +++ b/podcasts/New Detail/PlaylistDetailViewController+Analytics.swift @@ -0,0 +1,18 @@ +import Foundation + +extension PlaylistDetailViewController: AnalyticsSourceProvider { + var analyticsSource: AnalyticsSource { + .filters + } + + var analyticsSourceType: String { + viewModel.isManualPlaylist ? "manual" : "smart" + } + + func track(_ event: AnalyticsEvent, properties: [AnyHashable: Any]? = nil) { + var playlistEventProperties = properties ?? [:] + playlistEventProperties["filter_type"] = analyticsSourceType + + Analytics.track(event, properties: playlistEventProperties) + } +} diff --git a/podcasts/New Detail/PlaylistDetailViewController+EditActions.swift b/podcasts/New Detail/PlaylistDetailViewController+EditActions.swift index cc5839c3a1..fca4cf1578 100644 --- a/podcasts/New Detail/PlaylistDetailViewController+EditActions.swift +++ b/podcasts/New Detail/PlaylistDetailViewController+EditActions.swift @@ -10,7 +10,7 @@ extension PlaylistDetailViewController { } @objc func moreTapped() { - Analytics.track(.filterOptionsButtonTapped) + track(.filterOptionsTapped) let optionsPicker = OptionsPicker(title: nil) @@ -50,20 +50,26 @@ extension PlaylistDetailViewController { return } + track(.filterPlayAllTapped) + Task { [weak self] in guard let self else { return } let hasDifferencesWithUpNext = await self.checkDifferencesWithUpNext() if hasDifferencesWithUpNext { await MainActor.run { - Analytics.track(.filterOptionsModalOptionTapped, properties: ["option": "play_all"]) PlaylistPlayAllHelper.playAll { [weak self] action in guard let self else { return } switch action { case .saveAndPlay: + self.track(.filterPlayAllSaveUpNextTapped) self.viewModel.saveUpNextAndPlay() + case .showSecondPicker: + self.track(.filterPlayAllReplaceAndPlayTapped) case .replaceAndPlay: + self.track(.filterPlayAllReplaceAndPlayConfirmTapped) self.viewModel.playAllEpisodes() - case .close: + case .dismiss, .close: + self.track(.filterPlayAllDismissed) break } } @@ -85,7 +91,7 @@ extension PlaylistDetailViewController { private func multiSelectAction() -> OptionAction { OptionAction(label: L10n.selectEpisodes, icon: "option-multiselect") { [weak self] in - Analytics.track(.filterOptionsModalOptionTapped, properties: ["option": "select_episodes"]) + self?.track(.filterSelectEpisodesTapped) self?.isMultiSelectEnabled = true } } @@ -93,9 +99,9 @@ extension PlaylistDetailViewController { // MARK: - Chromecast private func chromecastAction() -> OptionAction { - OptionAction(label: "Chromecast", icon: "nav_cast_off") { - Analytics.track(.filterOptionsModalOptionTapped, properties: ["option": "chromecast"]) - self.castButtonTapped() + OptionAction(label: "Chromecast", icon: "nav_cast_off") { [weak self] in + self?.track(.filterChromeCastTapped) + self?.castButtonTapped() } } @@ -103,9 +109,9 @@ extension PlaylistDetailViewController { private func sortAction() -> OptionAction { let currentSort = PlaylistSort(rawValue: viewModel.playlist.sortType)?.description ?? "" - return OptionAction(label: L10n.sortBy, secondaryLabel: currentSort, icon: "podcastlist_sort") { - Analytics.track(.filterOptionsModalOptionTapped, properties: ["option": "sort_by"]) - self.showSortByPicker() + return OptionAction(label: L10n.sortBy, secondaryLabel: currentSort, icon: "podcastlist_sort") { [weak self] in + self?.track(.filterSortByTapped) + self?.showSortByPicker() } } @@ -125,8 +131,9 @@ extension PlaylistDetailViewController { } private func addSortAction(to optionPicker: OptionsPicker, sortOrder: PlaylistSort) { - let action = OptionAction(label: sortOrder.description, selected: viewModel.playlist.sortType == sortOrder.rawValue) { - Analytics.track(.filterSortByChanged, properties: ["sort_order": sortOrder]) + let action = OptionAction(label: sortOrder.description, selected: viewModel.playlist.sortType == sortOrder.rawValue) { [weak self] in + guard let self else { return } + self.track(.filterSortByChanged, properties: ["sort_order": sortOrder]) let playlist = self.viewModel.playlist! playlist.sortType = sortOrder.rawValue self.viewModel.update(playlist: playlist) @@ -147,8 +154,8 @@ extension PlaylistDetailViewController { private func reorderEpisodesAction() -> OptionAction { OptionAction(label: L10n.playlistManualEpisodesOrderOption, icon: "filter_manual_episode_order") { [weak self] in - //TODO: Add analytics guard let self = self else { return } + self.track(.filterRearrangeEpisodesTapped) self.showCustomOrderList() } } @@ -163,7 +170,7 @@ extension PlaylistDetailViewController { private func downloadAllOption() -> OptionAction { OptionAction(label: L10n.downloadAll, icon: "filter_downloaded") { [weak self] in guard let self = self else { return } - Analytics.track(.filterOptionsModalOptionTapped, properties: ["option": "download_all"]) + self.track(.filterDownloadAllTapped) let downloadableCount = self.downloadableCount(listEpisodes: self.viewModel.episodes) let downloadLimitExceeded = downloadableCount > Constants.Limits.maxBulkDownloads @@ -250,12 +257,12 @@ extension PlaylistDetailViewController { if unarchivedCount > 0 { return OptionAction(label: L10n.podcastArchiveAll, icon: "podcast-archiveall") { [weak self] in - //TODO: Add Analytics + self?.track(.filterArchiveAllTapped) self?.archiveAllPlaylistEpisodes() } } return OptionAction(label: L10n.podcastUnarchiveAll, icon: "list_unarchive") { [weak self] in - //TODO: Add Analytics + self?.track(.filterUnarchiveAllTapped) self?.unarchiveAllPlaylistEpisodes() } } @@ -277,9 +284,9 @@ extension PlaylistDetailViewController { // MARK: - Edit private func editAction() -> OptionAction { - OptionAction(label: L10n.playlistOptions, icon: "profile-settings") { - Analytics.track(.filterOptionsModalOptionTapped, properties: ["option": "filter_options"]) - self.playlistOptionsTapped() + OptionAction(label: L10n.playlistOptions, icon: "profile-settings") { [weak self] in + self?.track(.filterOptionsButtonTapped) + self?.playlistOptionsTapped() } } diff --git a/podcasts/New Detail/PlaylistDetailViewController.swift b/podcasts/New Detail/PlaylistDetailViewController.swift index e004e59cce..d2fb9ca82c 100644 --- a/podcasts/New Detail/PlaylistDetailViewController.swift +++ b/podcasts/New Detail/PlaylistDetailViewController.swift @@ -187,6 +187,8 @@ class PlaylistDetailViewController: FakeNavViewController { if viewModel.firstTimeLoading { loadingIndicator.startAnimating() } + + track(.filterShown) } override func viewWillAppear(_ animated: Bool) { @@ -398,6 +400,8 @@ class PlaylistDetailViewController: FakeNavViewController { } func editPlaylist() { + track(.filterEditRulesTapped) + let vc = PlaylistPreviewViewController(playlist: self.viewModel.playlist) { [weak self] in self?.viewModel.reloadPlaylistAndEpisodes() } @@ -406,7 +410,11 @@ class PlaylistDetailViewController: FakeNavViewController { } func addEpisodes() { - if viewModel.isPlaylistFull { + let isPlaylistFull = viewModel.isPlaylistFull + + track(.filterAddEpisodesTapped, properties: ["is_playlist_full": isPlaylistFull]) + + if isPlaylistFull { let theme: any ToastTheme = ToastIconTheme(iconName: "option-alert", iconColor: Theme.sharedTheme.primaryIcon01) Toast.show(L10n.playlistManualAddEpisodeFullPlaylistToast, theme: theme) return @@ -434,9 +442,3 @@ class PlaylistDetailViewController: FakeNavViewController { present(navVC, animated: true, completion: nil) } } - -extension PlaylistDetailViewController: AnalyticsSourceProvider { - var analyticsSource: AnalyticsSource { - .filters - } -} diff --git a/podcasts/New Detail/PlaylistPlayAllHelper/PlaylistPlayAllHelper.swift b/podcasts/New Detail/PlaylistPlayAllHelper/PlaylistPlayAllHelper.swift index c8eb7aacc6..87f4d6f1df 100644 --- a/podcasts/New Detail/PlaylistPlayAllHelper/PlaylistPlayAllHelper.swift +++ b/podcasts/New Detail/PlaylistPlayAllHelper/PlaylistPlayAllHelper.swift @@ -4,8 +4,10 @@ import PocketCastsUtils class PlaylistPlayAllHelper { enum Action { case close + case showSecondPicker case replaceAndPlay case saveAndPlay + case dismiss } class func playAll(confirmAction: @escaping (Action) -> Void) { if PlaybackManager.shared.queue.upNextCount() == 0 { @@ -26,6 +28,7 @@ class PlaylistPlayAllHelper { label: L10n.playlistPlayAllOptionReplaceQueue, icon: nil, action: { + confirmAction(.showSecondPicker) displayOverridePicker(confirmAction: confirmAction) } ) @@ -41,6 +44,9 @@ class PlaylistPlayAllHelper { replace ] ) + picker.setNoActionCallback { + confirmAction(.dismiss) + } picker.show(statusBarStyle: .default) } @@ -73,6 +79,9 @@ class PlaylistPlayAllHelper { close ] ) + picker.setNoActionCallback { + confirmAction(.dismiss) + } picker.show(statusBarStyle: .default) } } diff --git a/podcasts/SwipeActionsHelper.swift b/podcasts/SwipeActionsHelper.swift index b24f9346f9..df79154252 100644 --- a/podcasts/SwipeActionsHelper.swift +++ b/podcasts/SwipeActionsHelper.swift @@ -161,7 +161,21 @@ enum SwipeActionsHelper { fileprivate static func performAction(_ action: SwipeActions, handler: SwipeHandler, willBeRemoved: Bool) { let source = handler.swipeSource - Analytics.track(.episodeSwipeActionPerformed, properties: ["action": action, "source": source]) + var properties = ["action": action, "source": source] as [String: Any] + if FeatureFlag.playlistsRebranding.enabled { + let playlistSourceType = switch handler.swipeSourceType { + case .manualPlaylistDetail: + "manual" + case .smartPlaylistDetail: + "smart" + default: + "" + } + if !playlistSourceType.isEmpty { + properties["filter_type"] = playlistSourceType + } + } + Analytics.track(.episodeSwipeActionPerformed, properties: properties) guard action != .delete else { return