From 3076db8f9123c37549fe6173f7a15e68027a3cbc Mon Sep 17 00:00:00 2001 From: Beemo Lee Date: Wed, 15 Mar 2023 17:10:11 +0800 Subject: [PATCH 01/10] Add new feature flag for lock screen widgets in Jetpack (#20309) * Add new feature flag for widget in lock screen * Add new widget kind to widget configuration * Add empty lock screen widget * Add JETPACK_STATS_WIDGETS compilation condition To prevent lock screen widgets from displaying on WordPress target * Move LockScreenStatsWidgetsView to views folder group * Revert compilation condition, change to isJetpack of AppCondiguration --- .../WidgetConfiguration.swift | 1 + .../BuildInformation/FeatureFlag.swift | 7 ++++ WordPress/Jetpack/WidgetConfiguration.swift | 1 + WordPress/WordPress.xcodeproj/project.pbxproj | 28 ++++++++++++++++ .../LockScreenStatsWidget.swift | 33 +++++++++++++++++++ .../Views/LockScreenStatsWidgetsView.swift | 11 +++++++ .../WordPressStatsWidgets/StatsWidgets.swift | 3 ++ 7 files changed, 84 insertions(+) create mode 100644 WordPress/WordPressStatsWidgets/LockScreenWidgets/LockScreenStatsWidget.swift create mode 100644 WordPress/WordPressStatsWidgets/LockScreenWidgets/Views/LockScreenStatsWidgetsView.swift diff --git a/WordPress/Classes/Utility/App Configuration/WidgetConfiguration.swift b/WordPress/Classes/Utility/App Configuration/WidgetConfiguration.swift index 486199aaa2fd..b6c143f6c56a 100644 --- a/WordPress/Classes/Utility/App Configuration/WidgetConfiguration.swift +++ b/WordPress/Classes/Utility/App Configuration/WidgetConfiguration.swift @@ -16,6 +16,7 @@ import Foundation @objc static let userDefaultsSiteIdKey = "WordPressHomeWidgetsSiteId" @objc static let userDefaultsLoggedInKey = "WordPressHomeWidgetsLoggedIn" @objc static let userDefaultsJetpackFeaturesDisabledKey = "WordPressJPFeaturesDisabledKey" + @objc static let lockScreenTodayViewsKind = "WordPressLockScreenWidgetTodayViews" @objc static let todayKind = "WordPressHomeWidgetToday" @objc static let allTimeKind = "WordPressHomeWidgetAllTime" @objc static let thisWeekKind = "WordPressHomeWidgetThisWeek" diff --git a/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift b/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift index 31a98cefb402..038d80d24b04 100644 --- a/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift +++ b/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift @@ -11,6 +11,7 @@ enum FeatureFlag: Int, CaseIterable, OverrideableFlag { case homepageSettings case unifiedPrologueCarousel case todayWidget + case lockScreenWidget case milestoneNotifications case bloggingReminders case siteIconCreator @@ -74,6 +75,8 @@ enum FeatureFlag: Int, CaseIterable, OverrideableFlag { return true case .todayWidget: return true + case .lockScreenWidget: + return true case .milestoneNotifications: return true case .bloggingReminders: @@ -218,6 +221,8 @@ extension FeatureFlag { return "Unified Prologue Carousel" case .todayWidget: return "iOS 14 Today Widget" + case .lockScreenWidget: + return "iOS 16 Widget in Lock Screen" case .milestoneNotifications: return "Milestone notifications" case .bloggingReminders: @@ -301,6 +306,8 @@ extension FeatureFlag { return false case .todayWidget: return false + case .lockScreenWidget: + return false case .weeklyRoundup: return false case .weeklyRoundupStaticNotification: diff --git a/WordPress/Jetpack/WidgetConfiguration.swift b/WordPress/Jetpack/WidgetConfiguration.swift index d3ec31711525..d562e881b66e 100644 --- a/WordPress/Jetpack/WidgetConfiguration.swift +++ b/WordPress/Jetpack/WidgetConfiguration.swift @@ -16,6 +16,7 @@ import Foundation @objc static let userDefaultsSiteIdKey = "JetpackHomeWidgetsSiteId" @objc static let userDefaultsLoggedInKey = "JetpackHomeWidgetsLoggedIn" @objc static let userDefaultsJetpackFeaturesDisabledKey = "JetpackJPFeaturesDisabledKey" + @objc static let lockScreenTodayViewsKind = "JetpackLockScreenWidgetTodayViews" @objc static let todayKind = "JetpackHomeWidgetToday" @objc static let allTimeKind = "JetpackHomeWidgetAllTime" @objc static let thisWeekKind = "JetpackHomeWidgetThisWeek" diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index 92d576442939..8db955a484eb 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -2858,6 +2858,10 @@ C957C20626DCC1770037628F /* LandInTheEditorHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C957C20526DCC1770037628F /* LandInTheEditorHelper.swift */; }; C957C20726DCC1770037628F /* LandInTheEditorHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C957C20526DCC1770037628F /* LandInTheEditorHelper.swift */; }; C99B08FC26081AD600CA71EB /* TemplatePreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C99B08FB26081AD600CA71EB /* TemplatePreviewViewController.swift */; }; + C9C21D7729BECFC1009F68E5 /* LockScreenStatsWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9C21D7629BECFC1009F68E5 /* LockScreenStatsWidget.swift */; }; + C9C21D7829BECFC7009F68E5 /* LockScreenStatsWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9C21D7629BECFC1009F68E5 /* LockScreenStatsWidget.swift */; }; + C9C21D7B29BED18C009F68E5 /* LockScreenStatsWidgetsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9C21D7A29BED18C009F68E5 /* LockScreenStatsWidgetsView.swift */; }; + C9C21D7C29BED18C009F68E5 /* LockScreenStatsWidgetsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9C21D7A29BED18C009F68E5 /* LockScreenStatsWidgetsView.swift */; }; C9F1D4B72706ED7C00BDF917 /* EditHomepageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9F1D4B62706ED7C00BDF917 /* EditHomepageViewController.swift */; }; C9F1D4B82706ED7C00BDF917 /* EditHomepageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9F1D4B62706ED7C00BDF917 /* EditHomepageViewController.swift */; }; C9F1D4BA2706EEEB00BDF917 /* HomepageEditorNavigationBarManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9F1D4B92706EEEB00BDF917 /* HomepageEditorNavigationBarManager.swift */; }; @@ -8090,6 +8094,8 @@ C957C20526DCC1770037628F /* LandInTheEditorHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LandInTheEditorHelper.swift; sourceTree = ""; }; C99B039B2602F3CB00CA71EB /* WordPress 117.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 117.xcdatamodel"; sourceTree = ""; }; C99B08FB26081AD600CA71EB /* TemplatePreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplatePreviewViewController.swift; sourceTree = ""; }; + C9C21D7629BECFC1009F68E5 /* LockScreenStatsWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockScreenStatsWidget.swift; sourceTree = ""; }; + C9C21D7A29BED18C009F68E5 /* LockScreenStatsWidgetsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockScreenStatsWidgetsView.swift; sourceTree = ""; }; C9D7DDBF2613B84500104E95 /* WordPress 119.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 119.xcdatamodel"; sourceTree = ""; }; C9F1D4B62706ED7C00BDF917 /* EditHomepageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditHomepageViewController.swift; sourceTree = ""; }; C9F1D4B92706EEEB00BDF917 /* HomepageEditorNavigationBarManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomepageEditorNavigationBarManager.swift; sourceTree = ""; }; @@ -11028,6 +11034,7 @@ children = ( 3F8EEC4D25B4817000EC9DAE /* StatsWidgets.swift */, 3F8EEC6F25B4849A00EC9DAE /* SiteListProvider.swift */, + C9C21D7529BECFAE009F68E5 /* LockScreenWidgets */, 3FFDDCB925B8A65F008D5BDD /* Widgets */, 3FB34ABB25672A59001A74A6 /* Model */, 3FFDDC0325B89F0C008D5BDD /* Cache */, @@ -15630,6 +15637,23 @@ path = Tenor; sourceTree = ""; }; + C9C21D7529BECFAE009F68E5 /* LockScreenWidgets */ = { + isa = PBXGroup; + children = ( + C9C21D7629BECFC1009F68E5 /* LockScreenStatsWidget.swift */, + C9C21D8829BF4998009F68E5 /* Views */, + ); + path = LockScreenWidgets; + sourceTree = ""; + }; + C9C21D8829BF4998009F68E5 /* Views */ = { + isa = PBXGroup; + children = ( + C9C21D7A29BED18C009F68E5 /* LockScreenStatsWidgetsView.swift */, + ); + path = Views; + sourceTree = ""; + }; CC098B8116A9EB0400450976 /* HTML */ = { isa = PBXGroup; children = ( @@ -20386,6 +20410,7 @@ files = ( 0107E0B428F97D5000DE87DB /* Constants.m in Sources */, 01CE5012290A890B00A9C2E0 /* TracksConfiguration.swift in Sources */, + C9C21D7C29BED18C009F68E5 /* LockScreenStatsWidgetsView.swift in Sources */, 0107E0B528F97D5000DE87DB /* StatsWidgetEntry.swift in Sources */, 0107E0B628F97D5000DE87DB /* HomeWidgetCache.swift in Sources */, 0107E18C29000E2A00DE87DB /* AppStyleGuide.swift in Sources */, @@ -20406,6 +20431,7 @@ 0107E0C328F97D5000DE87DB /* ThisWeekWidgetStats.swift in Sources */, 0107E0C428F97D5000DE87DB /* HomeWidgetAllTimeData.swift in Sources */, 0107E0C528F97D5000DE87DB /* GroupedViewData.swift in Sources */, + C9C21D7829BECFC7009F68E5 /* LockScreenStatsWidget.swift in Sources */, 0107E0C628F97D5000DE87DB /* FeatureFlag.swift in Sources */, 0107E0C728F97D5000DE87DB /* StatsWidgets.swift in Sources */, 0107E18F29000EA200DE87DB /* UIColor+MurielColors.swift in Sources */, @@ -22241,6 +22267,7 @@ files = ( 3F1FD30D2548B0A80060C53A /* Constants.m in Sources */, 01CE500C290A88BF00A9C2E0 /* TracksConfiguration.swift in Sources */, + C9C21D7B29BED18C009F68E5 /* LockScreenStatsWidgetsView.swift in Sources */, 3F63B93C258179D100F581BE /* StatsWidgetEntry.swift in Sources */, 3FA53E9D256571D800F4D9A2 /* HomeWidgetCache.swift in Sources */, 0107E18D29000E3300DE87DB /* AppStyleGuide.swift in Sources */, @@ -22261,6 +22288,7 @@ 3F5C861A25C9EA2500BABE64 /* HomeWidgetAllTimeData.swift in Sources */, 3FE20C1525CF165700A15525 /* GroupedViewData.swift in Sources */, 3F6BC04B25B2474C007369D3 /* FeatureFlag.swift in Sources */, + C9C21D7729BECFC1009F68E5 /* LockScreenStatsWidget.swift in Sources */, 3F8EEC4E25B4817000EC9DAE /* StatsWidgets.swift in Sources */, 3FA59B9A258289E30073772F /* StatsValueView.swift in Sources */, 0107E18E29000EA100DE87DB /* UIColor+MurielColors.swift in Sources */, diff --git a/WordPress/WordPressStatsWidgets/LockScreenWidgets/LockScreenStatsWidget.swift b/WordPress/WordPressStatsWidgets/LockScreenWidgets/LockScreenStatsWidget.swift new file mode 100644 index 000000000000..dae513fe66cd --- /dev/null +++ b/WordPress/WordPressStatsWidgets/LockScreenWidgets/LockScreenStatsWidget.swift @@ -0,0 +1,33 @@ +import WidgetKit +import SwiftUI + +@available(iOS 16.0, *) +struct LockScreenStatsWidget: Widget { + private let placeholderContent = HomeWidgetTodayData( + siteID: 0, + siteName: "My WordPress Site", + url: "", + timeZone: TimeZone.current, + date: Date(), + stats: TodayWidgetStats(views: 649, + visitors: 572, + likes: 16, + comments: 8)) + + var body: some WidgetConfiguration { + IntentConfiguration( + kind: AppConfiguration.Widget.Stats.lockScreenTodayViewsKind, + intent: SelectSiteIntent.self, + provider: SiteListProvider( + service: StatsWidgetsService(), + placeholderContent: placeholderContent, + widgetKind: .today + ) + ) { (entry: StatsWidgetEntry) -> LockScreenStatsWidgetsView in + return LockScreenStatsWidgetsView(timelineEntry: entry) + } + .configurationDisplayName(LocalizableStrings.todayWidgetTitle) + .description(LocalizableStrings.todayPreviewDescription) + .supportedFamilies(FeatureFlag.lockScreenWidget.enabled ? [.accessoryRectangular] : []) + } +} diff --git a/WordPress/WordPressStatsWidgets/LockScreenWidgets/Views/LockScreenStatsWidgetsView.swift b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Views/LockScreenStatsWidgetsView.swift new file mode 100644 index 000000000000..a6d960b76b03 --- /dev/null +++ b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Views/LockScreenStatsWidgetsView.swift @@ -0,0 +1,11 @@ +import SwiftUI +import WidgetKit + +struct LockScreenStatsWidgetsView: View { + let timelineEntry: StatsWidgetEntry + + @ViewBuilder + var body: some View { + Text("Build Later") + } +} diff --git a/WordPress/WordPressStatsWidgets/StatsWidgets.swift b/WordPress/WordPressStatsWidgets/StatsWidgets.swift index 748b8aad201d..e8fb41bb6f07 100644 --- a/WordPress/WordPressStatsWidgets/StatsWidgets.swift +++ b/WordPress/WordPressStatsWidgets/StatsWidgets.swift @@ -7,5 +7,8 @@ struct WordPressStatsWidgets: WidgetBundle { WordPressHomeWidgetToday() WordPressHomeWidgetThisWeek() WordPressHomeWidgetAllTime() + if AppConfiguration.isJetpack, #available(iOS 16.0, *) { + LockScreenStatsWidget() + } } } From 8bb8533eafee75f8204b43a2cb475416958dd2c7 Mon Sep 17 00:00:00 2001 From: Beemo Lee Date: Thu, 16 Mar 2023 16:39:34 +0800 Subject: [PATCH 02/10] Implement lock screen widgets UI (#20312) * Extract statsUrl from private to extension for reusable with lock screen widget * Implement single stat view for today views widget Add view provider for handling building view for site selected status Add ViewModelMapper for converting viewModel from home widget data * Update LockScreenStatsWidgetsView body to get the view from view provider * Add mapper test to verify the viewModel converting from widget data correctly Add mapper and viewModel to WordPress target because app extension is not able to run unitTest * Fix getStatsURL typo --- WordPress/WordPress.xcodeproj/project.pbxproj | 72 +++++++++++++++++++ .../Extensions/HomeWidgetData+StatsURL.swift | 27 +++++++ .../LockScreenStatsWidget.swift | 5 +- .../LockScreenSingleStatViewModel.swift | 9 +++ .../LockScreenWidgetViewModelMapper.swift | 45 ++++++++++++ ...ckScreenSingleStatWidgetViewProvider.swift | 20 ++++++ .../Views/LockScreenSingleStatView.swift | 57 +++++++++++++++ .../Views/LockScreenStatsWidgetsView.swift | 22 +++++- .../Views/StatsWidgetsView.swift | 27 ------- .../Widgets/WidgetsViewModelMapperTests.swift | 57 +++++++++++++++ 10 files changed, 311 insertions(+), 30 deletions(-) create mode 100644 WordPress/WordPressStatsWidgets/Extensions/HomeWidgetData+StatsURL.swift create mode 100644 WordPress/WordPressStatsWidgets/LockScreenWidgets/Models/LockScreenSingleStatViewModel.swift create mode 100644 WordPress/WordPressStatsWidgets/LockScreenWidgets/Models/LockScreenWidgetViewModelMapper.swift create mode 100644 WordPress/WordPressStatsWidgets/LockScreenWidgets/ViewProvider/LockScreenSingleStatWidgetViewProvider.swift create mode 100644 WordPress/WordPressStatsWidgets/LockScreenWidgets/Views/LockScreenSingleStatView.swift create mode 100644 WordPress/WordPressTest/Widgets/WidgetsViewModelMapperTests.swift diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index 8db955a484eb..27e8ef43b1d9 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -2866,6 +2866,20 @@ C9F1D4B82706ED7C00BDF917 /* EditHomepageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9F1D4B62706ED7C00BDF917 /* EditHomepageViewController.swift */; }; C9F1D4BA2706EEEB00BDF917 /* HomepageEditorNavigationBarManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9F1D4B92706EEEB00BDF917 /* HomepageEditorNavigationBarManager.swift */; }; C9F1D4BB2706EEEB00BDF917 /* HomepageEditorNavigationBarManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9F1D4B92706EEEB00BDF917 /* HomepageEditorNavigationBarManager.swift */; }; + C9FE382229C2040600D39841 /* HomeWidgetData+StatsURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9FE382129C2040600D39841 /* HomeWidgetData+StatsURL.swift */; }; + C9FE382329C2040D00D39841 /* HomeWidgetData+StatsURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9FE382129C2040600D39841 /* HomeWidgetData+StatsURL.swift */; }; + C9FE382729C204C100D39841 /* LockScreenSingleStatWidgetViewProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9FE382629C204C100D39841 /* LockScreenSingleStatWidgetViewProvider.swift */; }; + C9FE382829C204C100D39841 /* LockScreenSingleStatWidgetViewProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9FE382629C204C100D39841 /* LockScreenSingleStatWidgetViewProvider.swift */; }; + C9FE382C29C204E700D39841 /* LockScreenWidgetViewModelMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9FE382A29C204E700D39841 /* LockScreenWidgetViewModelMapper.swift */; }; + C9FE382D29C204E700D39841 /* LockScreenWidgetViewModelMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9FE382A29C204E700D39841 /* LockScreenWidgetViewModelMapper.swift */; }; + C9FE382E29C204E700D39841 /* LockScreenSingleStatViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9FE382B29C204E700D39841 /* LockScreenSingleStatViewModel.swift */; }; + C9FE382F29C204E700D39841 /* LockScreenSingleStatViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9FE382B29C204E700D39841 /* LockScreenSingleStatViewModel.swift */; }; + C9FE383129C2053300D39841 /* LockScreenSingleStatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9FE383029C2053300D39841 /* LockScreenSingleStatView.swift */; }; + C9FE383229C2053300D39841 /* LockScreenSingleStatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9FE383029C2053300D39841 /* LockScreenSingleStatView.swift */; }; + C9FE383729C2067E00D39841 /* WidgetsViewModelMapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9FE383629C2067E00D39841 /* WidgetsViewModelMapperTests.swift */; }; + C9FE383829C206AE00D39841 /* LockScreenWidgetViewModelMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9FE382A29C204E700D39841 /* LockScreenWidgetViewModelMapper.swift */; }; + C9FE383929C2077700D39841 /* LockScreenSingleStatViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9FE382B29C204E700D39841 /* LockScreenSingleStatViewModel.swift */; }; + C9FE383A29C207F600D39841 /* HomeWidgetData+StatsURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9FE382129C2040600D39841 /* HomeWidgetData+StatsURL.swift */; }; CB1FD8D826E4BBAA00EDAF06 /* SharePostTypePickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB48172926E0D93D008C2D9B /* SharePostTypePickerViewController.swift */; }; CB48172A26E0D93D008C2D9B /* SharePostTypePickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB48172926E0D93D008C2D9B /* SharePostTypePickerViewController.swift */; }; CBF6201326E8FB520061A1F8 /* RemotePost+ShareData.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBF6201226E8FB520061A1F8 /* RemotePost+ShareData.swift */; }; @@ -8099,6 +8113,12 @@ C9D7DDBF2613B84500104E95 /* WordPress 119.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 119.xcdatamodel"; sourceTree = ""; }; C9F1D4B62706ED7C00BDF917 /* EditHomepageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditHomepageViewController.swift; sourceTree = ""; }; C9F1D4B92706EEEB00BDF917 /* HomepageEditorNavigationBarManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomepageEditorNavigationBarManager.swift; sourceTree = ""; }; + C9FE382129C2040600D39841 /* HomeWidgetData+StatsURL.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "HomeWidgetData+StatsURL.swift"; sourceTree = ""; }; + C9FE382629C204C100D39841 /* LockScreenSingleStatWidgetViewProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockScreenSingleStatWidgetViewProvider.swift; sourceTree = ""; }; + C9FE382A29C204E700D39841 /* LockScreenWidgetViewModelMapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockScreenWidgetViewModelMapper.swift; sourceTree = ""; }; + C9FE382B29C204E700D39841 /* LockScreenSingleStatViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockScreenSingleStatViewModel.swift; sourceTree = ""; }; + C9FE383029C2053300D39841 /* LockScreenSingleStatView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockScreenSingleStatView.swift; sourceTree = ""; }; + C9FE383629C2067E00D39841 /* WidgetsViewModelMapperTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WidgetsViewModelMapperTests.swift; sourceTree = ""; }; CB1DAFB7DE085F2FF0314622 /* Pods-WordPressShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WordPressShareExtension.debug.xcconfig"; path = "../Pods/Target Support Files/Pods-WordPressShareExtension/Pods-WordPressShareExtension.debug.xcconfig"; sourceTree = ""; }; CB1FD8D926E605CF00EDAF06 /* Extensions 4.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Extensions 4.xcdatamodel"; sourceTree = ""; }; CB48172926E0D93D008C2D9B /* SharePostTypePickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharePostTypePickerViewController.swift; sourceTree = ""; }; @@ -11035,6 +11055,7 @@ 3F8EEC4D25B4817000EC9DAE /* StatsWidgets.swift */, 3F8EEC6F25B4849A00EC9DAE /* SiteListProvider.swift */, C9C21D7529BECFAE009F68E5 /* LockScreenWidgets */, + C9FE382029C203EE00D39841 /* Extensions */, 3FFDDCB925B8A65F008D5BDD /* Widgets */, 3FB34ABB25672A59001A74A6 /* Model */, 3FFDDC0325B89F0C008D5BDD /* Cache */, @@ -15641,7 +15662,9 @@ isa = PBXGroup; children = ( C9C21D7629BECFC1009F68E5 /* LockScreenStatsWidget.swift */, + C9FE382929C204D700D39841 /* Models */, C9C21D8829BF4998009F68E5 /* Views */, + C9FE382529C204A500D39841 /* ViewProvider */, ); path = LockScreenWidgets; sourceTree = ""; @@ -15650,10 +15673,44 @@ isa = PBXGroup; children = ( C9C21D7A29BED18C009F68E5 /* LockScreenStatsWidgetsView.swift */, + C9FE383029C2053300D39841 /* LockScreenSingleStatView.swift */, ); path = Views; sourceTree = ""; }; + C9FE382029C203EE00D39841 /* Extensions */ = { + isa = PBXGroup; + children = ( + C9FE382129C2040600D39841 /* HomeWidgetData+StatsURL.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + C9FE382529C204A500D39841 /* ViewProvider */ = { + isa = PBXGroup; + children = ( + C9FE382629C204C100D39841 /* LockScreenSingleStatWidgetViewProvider.swift */, + ); + path = ViewProvider; + sourceTree = ""; + }; + C9FE382929C204D700D39841 /* Models */ = { + isa = PBXGroup; + children = ( + C9FE382B29C204E700D39841 /* LockScreenSingleStatViewModel.swift */, + C9FE382A29C204E700D39841 /* LockScreenWidgetViewModelMapper.swift */, + ); + path = Models; + sourceTree = ""; + }; + C9FE383329C2063900D39841 /* Widgets */ = { + isa = PBXGroup; + children = ( + C9FE383629C2067E00D39841 /* WidgetsViewModelMapperTests.swift */, + ); + path = Widgets; + sourceTree = ""; + }; CC098B8116A9EB0400450976 /* HTML */ = { isa = PBXGroup; children = ( @@ -16039,6 +16096,7 @@ 852416D01A12ED2D0030700C /* Utility */, BE20F5E11B2F738E0020694C /* ViewRelated */, 3F3D8548251E63DF001CA4D2 /* What's New */, + C9FE383329C2063900D39841 /* Widgets */, FF9839A71CD3960600E85258 /* WordPressAPI */, ); name = Tests; @@ -20411,6 +20469,7 @@ 0107E0B428F97D5000DE87DB /* Constants.m in Sources */, 01CE5012290A890B00A9C2E0 /* TracksConfiguration.swift in Sources */, C9C21D7C29BED18C009F68E5 /* LockScreenStatsWidgetsView.swift in Sources */, + C9FE382F29C204E700D39841 /* LockScreenSingleStatViewModel.swift in Sources */, 0107E0B528F97D5000DE87DB /* StatsWidgetEntry.swift in Sources */, 0107E0B628F97D5000DE87DB /* HomeWidgetCache.swift in Sources */, 0107E18C29000E2A00DE87DB /* AppStyleGuide.swift in Sources */, @@ -20440,10 +20499,12 @@ 0107E0CA28F97D5000DE87DB /* HomeWidgetThisWeekData.swift in Sources */, 0107E0CB28F97D5000DE87DB /* WordPressHomeWidgetAllTime.swift in Sources */, 0107E0CC28F97D5000DE87DB /* KeyValueDatabase.swift in Sources */, + C9FE382829C204C100D39841 /* LockScreenSingleStatWidgetViewProvider.swift in Sources */, 0107E0CD28F97D5000DE87DB /* CocoaLumberjack.swift in Sources */, 0107E0CE28F97D5000DE87DB /* ListRow.swift in Sources */, 0107E18A29000E1500DE87DB /* MurielColor.swift in Sources */, 0107E0D028F97D5000DE87DB /* WordPressHomeWidgetThisWeek.swift in Sources */, + C9FE382D29C204E700D39841 /* LockScreenWidgetViewModelMapper.swift in Sources */, 0107E0D128F97D5000DE87DB /* SingleStatView.swift in Sources */, 0107E0D228F97D5000DE87DB /* UnconfiguredView.swift in Sources */, 0107E1852900059300DE87DB /* LocalizationConfiguration.swift in Sources */, @@ -20453,6 +20514,8 @@ 0107E0D628F97D5000DE87DB /* AllTimeWidgetStats.swift in Sources */, 0107E0D728F97D5000DE87DB /* Sites.intentdefinition in Sources */, 0107E0D828F97D5000DE87DB /* LocalizableStrings.swift in Sources */, + C9FE383229C2053300D39841 /* LockScreenSingleStatView.swift in Sources */, + C9FE382329C2040D00D39841 /* HomeWidgetData+StatsURL.swift in Sources */, 0107E0D928F97D5000DE87DB /* SFHFKeychainUtils.m in Sources */, 0107E0DA28F97D5000DE87DB /* ListViewData.swift in Sources */, 0107E0DB28F97D5000DE87DB /* Double+Stats.swift in Sources */, @@ -20818,6 +20881,7 @@ 4A072CD229093704006235BE /* AsyncBlockOperation.swift in Sources */, E185042F1EE6ABD9005C234C /* Restorer.swift in Sources */, 02761EC02270072F009BAF0F /* BlogDetailsViewController+SectionHelpers.swift in Sources */, + C9FE383829C206AE00D39841 /* LockScreenWidgetViewModelMapper.swift in Sources */, 984B138E21F65F870004B6A2 /* SiteStatsPeriodTableViewController.swift in Sources */, 433ADC1D223B2A7F00ED9DE1 /* TextBundleWrapper.m in Sources */, 2F605FAA25145F7200F99544 /* WPCategoryTree.swift in Sources */, @@ -20997,6 +21061,7 @@ 40232A9E230A6A740036B0B6 /* AbstractPost+HashHelpers.m in Sources */, FAE4201A1C5AEFE100C1D036 /* StartOverViewController.swift in Sources */, 0A3FCA1D28B71CBD00499A15 /* FullScreenCommentReplyViewModel.swift in Sources */, + C9FE383A29C207F600D39841 /* HomeWidgetData+StatsURL.swift in Sources */, 1790A4531E28F0ED00AE54C2 /* UINavigationController+Helpers.swift in Sources */, FEA088052696F7AA00193358 /* WPStyleGuide+List.swift in Sources */, 17C2FF0925D4852400CDB712 /* UnifiedProloguePages.swift in Sources */, @@ -22132,6 +22197,7 @@ B50EED791C0E5B2400D278CA /* SettingsPickerViewController.swift in Sources */, E1F5A1BC1771C90A00E0495F /* WPTableImageSource.m in Sources */, 17C1D6912670E4A2006C8970 /* UIFont+Fitting.swift in Sources */, + C9FE383929C2077700D39841 /* LockScreenSingleStatViewModel.swift in Sources */, 329F8E5824DDBD11002A5311 /* ReaderTopicCollectionViewCoordinator.swift in Sources */, 5948AD0E1AB734F2006E8882 /* WPAppAnalytics.m in Sources */, B53AD9BF1BE9584B009AB87E /* SettingsSelectionViewController.m in Sources */, @@ -22268,6 +22334,7 @@ 3F1FD30D2548B0A80060C53A /* Constants.m in Sources */, 01CE500C290A88BF00A9C2E0 /* TracksConfiguration.swift in Sources */, C9C21D7B29BED18C009F68E5 /* LockScreenStatsWidgetsView.swift in Sources */, + C9FE382E29C204E700D39841 /* LockScreenSingleStatViewModel.swift in Sources */, 3F63B93C258179D100F581BE /* StatsWidgetEntry.swift in Sources */, 3FA53E9D256571D800F4D9A2 /* HomeWidgetCache.swift in Sources */, 0107E18D29000E3300DE87DB /* AppStyleGuide.swift in Sources */, @@ -22297,10 +22364,12 @@ 3F5C86C025CA197500BABE64 /* WordPressHomeWidgetAllTime.swift in Sources */, 3F6BC07E25B247A4007369D3 /* KeyValueDatabase.swift in Sources */, 3F1FD27B2548AE900060C53A /* CocoaLumberjack.swift in Sources */, + C9FE382729C204C100D39841 /* LockScreenSingleStatWidgetViewProvider.swift in Sources */, 3FCF66FB25CAF8E00047F337 /* ListRow.swift in Sources */, 8323789828526E6D003F4443 /* AppConfiguration.swift in Sources */, 0107E18B29000E1700DE87DB /* MurielColor.swift in Sources */, 3F8B138F25D09AA5004FAC0A /* WordPressHomeWidgetThisWeek.swift in Sources */, + C9FE382C29C204E700D39841 /* LockScreenWidgetViewModelMapper.swift in Sources */, 3F5689F0254209790048A9E4 /* SingleStatView.swift in Sources */, 3FAA18CC25797B85002B1911 /* UnconfiguredView.swift in Sources */, 0107E1872900065500DE87DB /* LocalizationConfiguration.swift in Sources */, @@ -22310,6 +22379,8 @@ 3F5C863B25C9EA8200BABE64 /* AllTimeWidgetStats.swift in Sources */, 3FD675D925C87A15009AB3C1 /* Sites.intentdefinition in Sources */, 3FE77C8325B0CA89007DE9E5 /* LocalizableStrings.swift in Sources */, + C9FE383129C2053300D39841 /* LockScreenSingleStatView.swift in Sources */, + C9FE382229C2040600D39841 /* HomeWidgetData+StatsURL.swift in Sources */, 3FCC8FD9256C911300810295 /* SFHFKeychainUtils.m in Sources */, 3FE20C3725CF211F00A15525 /* ListViewData.swift in Sources */, 3F71D5302548C2B200A4BA93 /* Double+Stats.swift in Sources */, @@ -23048,6 +23119,7 @@ 0879FC161E9301DD00E1EFC8 /* MediaTests.swift in Sources */, B556EFCB1DCA374200728F93 /* DictionaryHelpersTests.swift in Sources */, DC06DFF927BD52BE00969974 /* WeeklyRoundupBackgroundTaskTests.swift in Sources */, + C9FE383729C2067E00D39841 /* WidgetsViewModelMapperTests.swift in Sources */, 24C69A8B2612421900312D9A /* UserSettingsTests.swift in Sources */, 8B6BD55024293FBE00DB8F28 /* PrepublishingNudgesViewControllerTests.swift in Sources */, DC13DB7E293FD09F00E33561 /* StatsInsightsStoreTests.swift in Sources */, diff --git a/WordPress/WordPressStatsWidgets/Extensions/HomeWidgetData+StatsURL.swift b/WordPress/WordPressStatsWidgets/Extensions/HomeWidgetData+StatsURL.swift new file mode 100644 index 000000000000..e08968880354 --- /dev/null +++ b/WordPress/WordPressStatsWidgets/Extensions/HomeWidgetData+StatsURL.swift @@ -0,0 +1,27 @@ +import Foundation + +extension HomeWidgetTodayData { + static let statsUrl = "https://wordpress.com/stats/day/" + + var statsURL: URL? { + URL(string: Self.statsUrl + "\(siteID)?source=widget") + } +} + + +extension HomeWidgetAllTimeData { + static let statsUrl = "https://wordpress.com/stats/insights/" + + var statsURL: URL? { + URL(string: Self.statsUrl + "\(siteID)?source=widget") + } +} + + +extension HomeWidgetThisWeekData { + static let statsUrl = "https://wordpress.com/stats/week/" + + var statsURL: URL? { + URL(string: Self.statsUrl + "\(siteID)?source=widget") + } +} diff --git a/WordPress/WordPressStatsWidgets/LockScreenWidgets/LockScreenStatsWidget.swift b/WordPress/WordPressStatsWidgets/LockScreenWidgets/LockScreenStatsWidget.swift index dae513fe66cd..304c3055c9d0 100644 --- a/WordPress/WordPressStatsWidgets/LockScreenWidgets/LockScreenStatsWidget.swift +++ b/WordPress/WordPressStatsWidgets/LockScreenWidgets/LockScreenStatsWidget.swift @@ -24,7 +24,10 @@ struct LockScreenStatsWidget: Widget { widgetKind: .today ) ) { (entry: StatsWidgetEntry) -> LockScreenStatsWidgetsView in - return LockScreenStatsWidgetsView(timelineEntry: entry) + return LockScreenStatsWidgetsView( + timelineEntry: entry, + viewProvider: LockScreenSingleStatWidgetViewProvider() + ) } .configurationDisplayName(LocalizableStrings.todayWidgetTitle) .description(LocalizableStrings.todayPreviewDescription) diff --git a/WordPress/WordPressStatsWidgets/LockScreenWidgets/Models/LockScreenSingleStatViewModel.swift b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Models/LockScreenSingleStatViewModel.swift new file mode 100644 index 000000000000..00c784bb60a9 --- /dev/null +++ b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Models/LockScreenSingleStatViewModel.swift @@ -0,0 +1,9 @@ +import Foundation + +struct LockScreenSingleStatViewModel { + let siteName: String + let title: String + let value: String + let dateRange: String + let updatedTime: Date +} diff --git a/WordPress/WordPressStatsWidgets/LockScreenWidgets/Models/LockScreenWidgetViewModelMapper.swift b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Models/LockScreenWidgetViewModelMapper.swift new file mode 100644 index 000000000000..7e250fa60112 --- /dev/null +++ b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Models/LockScreenWidgetViewModelMapper.swift @@ -0,0 +1,45 @@ +import Foundation + +struct LockScreenWidgetViewModelMapper { + let data: HomeWidgetData + + func getLockScreenSingleStatViewModel(title: String, dateRange: String) -> LockScreenSingleStatViewModel { + LockScreenSingleStatViewModel( + siteName: getSiteName(), + title: title, + value: getViews(), + dateRange: dateRange, + updatedTime: data.date + ) + } + + // TODO: Add `LockScreenStatsWidgetData` in creating lock screen widget provider and entry PR + // define statsURL, views, date, siteName + // HomeWidgetTodayData, HomeWidgetAllTimeData, HomeWidgetThisWeekData conform to it + // to reduce the type converting + func getStatsURL() -> URL? { + if let todayData = data as? HomeWidgetTodayData { + return todayData.statsURL + } else if let allTimeData = data as? HomeWidgetAllTimeData { + return allTimeData.statsURL + } else if let thisWeekData = data as? HomeWidgetThisWeekData { + return thisWeekData.statsURL + } else { + return nil + } + } + + func getSiteName() -> String { + data.siteName + } + + func getViews() -> String { + if let todayData = data as? HomeWidgetTodayData { + return todayData.stats.views.abbreviatedString() + } else if let allTimeData = data as? HomeWidgetAllTimeData { + return allTimeData.stats.views.abbreviatedString() + } else { + return "" + } + } +} diff --git a/WordPress/WordPressStatsWidgets/LockScreenWidgets/ViewProvider/LockScreenSingleStatWidgetViewProvider.swift b/WordPress/WordPressStatsWidgets/LockScreenWidgets/ViewProvider/LockScreenSingleStatWidgetViewProvider.swift new file mode 100644 index 000000000000..49e6fe058a7f --- /dev/null +++ b/WordPress/WordPressStatsWidgets/LockScreenWidgets/ViewProvider/LockScreenSingleStatWidgetViewProvider.swift @@ -0,0 +1,20 @@ +import SwiftUI + +@available(iOS 16.0, *) +struct LockScreenSingleStatWidgetViewProvider: LockScreenStatsWidgetsViewProvider { + typealias SiteSelectedView = LockScreenSingleStatView + + func buildSiteSelectedView(_ data: HomeWidgetData) -> LockScreenSingleStatView { + let mapper = LockScreenWidgetViewModelMapper(data: data) + let viewModel = mapper.getLockScreenSingleStatViewModel( + title: LocalizableStrings.viewsTitle, + dateRange: LocalizableStrings.todayWidgetTitle + ) + return LockScreenSingleStatView(viewModel: viewModel) + } + + func statsURL(_ data: HomeWidgetData) -> URL? { + let mapper = LockScreenWidgetViewModelMapper(data: data) + return mapper.getStatsURL() + } +} diff --git a/WordPress/WordPressStatsWidgets/LockScreenWidgets/Views/LockScreenSingleStatView.swift b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Views/LockScreenSingleStatView.swift new file mode 100644 index 000000000000..c2b5444858b0 --- /dev/null +++ b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Views/LockScreenSingleStatView.swift @@ -0,0 +1,57 @@ +import SwiftUI +import WidgetKit + +@available(iOS 16.0, *) +struct LockScreenSingleStatView: View { + @Environment(\.widgetFamily) var family: WidgetFamily + let viewModel: LockScreenSingleStatViewModel + + var body: some View { + if family == .accessoryRectangular { + ZStack { + AccessoryWidgetBackground().cornerRadius(8) + VStack(alignment: .leading) { + Text(viewModel.siteName) + .frame(maxWidth: .infinity, alignment: .leading) + .font(.system(size: 11)) + .minimumScaleFactor(0.8) + .lineLimit(1) + Text(viewModel.value) + .frame(maxWidth: .infinity, alignment: .leading) + .font(.system(size: 20, weight: .bold)) + .minimumScaleFactor(0.5) + .foregroundColor(.white) + Text("\(viewModel.title) \(viewModel.dateRange)") + .frame(maxWidth: .infinity, alignment: .leading) + .font(.system(size: 11)) + .minimumScaleFactor(0.8) + } + .padding( + EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8) + ) + } + } else { + Text("Not implemented for widget family \(family.debugDescription)") + } + } +} + +@available(iOS 16.0, *) +struct LockScreenSingleStatView_Previews: PreviewProvider { + static let viewModel = LockScreenSingleStatViewModel( + siteName: "My WordPress Site", + title: "Views", + value: "649", + dateRange: "Today", + updatedTime: Date() + ) + + static var previews: some View { + LockScreenSingleStatView( + viewModel: LockScreenSingleStatView_Previews.viewModel + ) + .previewContext( + WidgetPreviewContext(family: .accessoryRectangular) + ) + } +} diff --git a/WordPress/WordPressStatsWidgets/LockScreenWidgets/Views/LockScreenStatsWidgetsView.swift b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Views/LockScreenStatsWidgetsView.swift index a6d960b76b03..12fdc0d8bb19 100644 --- a/WordPress/WordPressStatsWidgets/LockScreenWidgets/Views/LockScreenStatsWidgetsView.swift +++ b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Views/LockScreenStatsWidgetsView.swift @@ -1,11 +1,29 @@ import SwiftUI import WidgetKit -struct LockScreenStatsWidgetsView: View { +protocol LockScreenStatsWidgetsViewProvider { + associatedtype SiteSelectedView: View + + @ViewBuilder + func buildSiteSelectedView(_ data: HomeWidgetData) -> SiteSelectedView + + func statsURL(_ data: HomeWidgetData) -> URL? +} + +struct LockScreenStatsWidgetsView: View { let timelineEntry: StatsWidgetEntry + let viewProvider: T @ViewBuilder var body: some View { - Text("Build Later") + switch timelineEntry { + case let .siteSelected(data, _): + viewProvider + .buildSiteSelectedView(data) + .widgetURL(viewProvider.statsURL(data)) + default: + // TODO: Build view for loggedOut, noSite, noData status + Text("Build Later") + } } } diff --git a/WordPress/WordPressStatsWidgets/Views/StatsWidgetsView.swift b/WordPress/WordPressStatsWidgets/Views/StatsWidgetsView.swift index 7ff90e846eae..8a66d364f71b 100644 --- a/WordPress/WordPressStatsWidgets/Views/StatsWidgetsView.swift +++ b/WordPress/WordPressStatsWidgets/Views/StatsWidgetsView.swift @@ -96,33 +96,6 @@ private extension StatsWidgetsView { } } - -private extension HomeWidgetTodayData { - static let statsUrl = "https://wordpress.com/stats/day/" - - var statsURL: URL? { - URL(string: Self.statsUrl + "\(siteID)?source=widget") - } -} - - -private extension HomeWidgetAllTimeData { - static let statsUrl = "https://wordpress.com/stats/insights/" - - var statsURL: URL? { - URL(string: Self.statsUrl + "\(siteID)?source=widget") - } -} - - -private extension HomeWidgetThisWeekData { - static let statsUrl = "https://wordpress.com/stats/week/" - - var statsURL: URL? { - URL(string: Self.statsUrl + "\(siteID)?source=widget") - } -} - private extension StatsWidgetKind { var statsURL: URL? { switch self { diff --git a/WordPress/WordPressTest/Widgets/WidgetsViewModelMapperTests.swift b/WordPress/WordPressTest/Widgets/WidgetsViewModelMapperTests.swift new file mode 100644 index 000000000000..78ccef087670 --- /dev/null +++ b/WordPress/WordPressTest/Widgets/WidgetsViewModelMapperTests.swift @@ -0,0 +1,57 @@ +import XCTest +@testable import WordPress + +final class WidgetsViewModelMapperTests: XCTestCase { + func testSingleStatViewModel() { + let views = 649875 + let date = Date() + let title = "Views" + let dateRange = "Today" + let todayStats = makeTodayWidgetStats(views: views) + let data = makeTodayData(stats: todayStats, date: date) + + let sut = makeSUT(data) + let viewModel = sut.getLockScreenSingleStatViewModel( + title: title, + dateRange: dateRange + ) + + XCTAssertEqual(viewModel.siteName, data.siteName) + XCTAssertEqual(viewModel.title, title) + XCTAssertEqual(viewModel.value, views.abbreviatedString()) + XCTAssertEqual(viewModel.dateRange, dateRange) + XCTAssertEqual(viewModel.updatedTime, date) + } + + func testTodayViewsStatsURL() { + let todayStats = makeTodayWidgetStats(views: 649) + let data = makeTodayData(stats: todayStats, date: Date()) + + let sut = makeSUT(data) + let statsURL = sut.getStatsURL() + + XCTAssertEqual(statsURL?.absoluteString, "https://wordpress.com/stats/day/0?source=widget") + } +} + +extension WidgetsViewModelMapperTests { + func makeSUT(_ data: HomeWidgetData) -> LockScreenWidgetViewModelMapper { + LockScreenWidgetViewModelMapper(data: data) + } + + func makeTodayData(stats: TodayWidgetStats, date: Date) -> HomeWidgetTodayData { + HomeWidgetTodayData(siteID: 0, + siteName: "My WordPress Site", + url: "", + timeZone: TimeZone.current, + date: date, + stats: stats) + } + + func makeTodayWidgetStats(views: Int) -> TodayWidgetStats { + TodayWidgetStats(views: views, + visitors: 572, + likes: 16, + comments: 8) + } +} From 6b55e85ba16a33a1402f40f7794997a6e5733d32 Mon Sep 17 00:00:00 2001 From: Beemo Lee Date: Mon, 20 Mar 2023 20:49:29 +0800 Subject: [PATCH 03/10] Add localizable string for "Views Today" title (#20353) * Add views in today localizable string for footer label * Remove dateRange and footer, only title is enough for existing feature --- .../Models/LockScreenSingleStatViewModel.swift | 1 - .../Models/LockScreenWidgetViewModelMapper.swift | 5 +++-- .../LockScreenSingleStatWidgetViewProvider.swift | 3 +-- .../LockScreenWidgets/Views/LockScreenSingleStatView.swift | 5 ++--- .../Views/Localization/LocalizableStrings.swift | 5 +++++ .../Widgets/WidgetsViewModelMapperTests.swift | 7 ++----- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/WordPress/WordPressStatsWidgets/LockScreenWidgets/Models/LockScreenSingleStatViewModel.swift b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Models/LockScreenSingleStatViewModel.swift index 00c784bb60a9..b5295e421e01 100644 --- a/WordPress/WordPressStatsWidgets/LockScreenWidgets/Models/LockScreenSingleStatViewModel.swift +++ b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Models/LockScreenSingleStatViewModel.swift @@ -4,6 +4,5 @@ struct LockScreenSingleStatViewModel { let siteName: String let title: String let value: String - let dateRange: String let updatedTime: Date } diff --git a/WordPress/WordPressStatsWidgets/LockScreenWidgets/Models/LockScreenWidgetViewModelMapper.swift b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Models/LockScreenWidgetViewModelMapper.swift index 7e250fa60112..7c124701cd25 100644 --- a/WordPress/WordPressStatsWidgets/LockScreenWidgets/Models/LockScreenWidgetViewModelMapper.swift +++ b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Models/LockScreenWidgetViewModelMapper.swift @@ -3,12 +3,13 @@ import Foundation struct LockScreenWidgetViewModelMapper { let data: HomeWidgetData - func getLockScreenSingleStatViewModel(title: String, dateRange: String) -> LockScreenSingleStatViewModel { + func getLockScreenSingleStatViewModel( + title: String + ) -> LockScreenSingleStatViewModel { LockScreenSingleStatViewModel( siteName: getSiteName(), title: title, value: getViews(), - dateRange: dateRange, updatedTime: data.date ) } diff --git a/WordPress/WordPressStatsWidgets/LockScreenWidgets/ViewProvider/LockScreenSingleStatWidgetViewProvider.swift b/WordPress/WordPressStatsWidgets/LockScreenWidgets/ViewProvider/LockScreenSingleStatWidgetViewProvider.swift index 49e6fe058a7f..590b347d25b0 100644 --- a/WordPress/WordPressStatsWidgets/LockScreenWidgets/ViewProvider/LockScreenSingleStatWidgetViewProvider.swift +++ b/WordPress/WordPressStatsWidgets/LockScreenWidgets/ViewProvider/LockScreenSingleStatWidgetViewProvider.swift @@ -7,8 +7,7 @@ struct LockScreenSingleStatWidgetViewProvider: LockScreenStatsWidgetsViewProvide func buildSiteSelectedView(_ data: HomeWidgetData) -> LockScreenSingleStatView { let mapper = LockScreenWidgetViewModelMapper(data: data) let viewModel = mapper.getLockScreenSingleStatViewModel( - title: LocalizableStrings.viewsTitle, - dateRange: LocalizableStrings.todayWidgetTitle + title: LocalizableStrings.viewsInTodayTitle ) return LockScreenSingleStatView(viewModel: viewModel) } diff --git a/WordPress/WordPressStatsWidgets/LockScreenWidgets/Views/LockScreenSingleStatView.swift b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Views/LockScreenSingleStatView.swift index c2b5444858b0..d29f3020278c 100644 --- a/WordPress/WordPressStatsWidgets/LockScreenWidgets/Views/LockScreenSingleStatView.swift +++ b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Views/LockScreenSingleStatView.swift @@ -21,7 +21,7 @@ struct LockScreenSingleStatView: View { .font(.system(size: 20, weight: .bold)) .minimumScaleFactor(0.5) .foregroundColor(.white) - Text("\(viewModel.title) \(viewModel.dateRange)") + Text(viewModel.title) .frame(maxWidth: .infinity, alignment: .leading) .font(.system(size: 11)) .minimumScaleFactor(0.8) @@ -40,9 +40,8 @@ struct LockScreenSingleStatView: View { struct LockScreenSingleStatView_Previews: PreviewProvider { static let viewModel = LockScreenSingleStatViewModel( siteName: "My WordPress Site", - title: "Views", + title: "Views Today", value: "649", - dateRange: "Today", updatedTime: Date() ) diff --git a/WordPress/WordPressStatsWidgets/Views/Localization/LocalizableStrings.swift b/WordPress/WordPressStatsWidgets/Views/Localization/LocalizableStrings.swift index 4784056083a7..def04a1540ce 100644 --- a/WordPress/WordPressStatsWidgets/Views/Localization/LocalizableStrings.swift +++ b/WordPress/WordPressStatsWidgets/Views/Localization/LocalizableStrings.swift @@ -16,6 +16,11 @@ enum LocalizableStrings { value: "This Week", comment: "Title of this week widget") + // Lock Screen Widgets content + static let viewsInTodayTitle = AppLocalizedString("widget.lockscreen.todayview.label", + value: "Views Today", + comment: "Title of the one-liner information consist of views field and today date range in lock screen today views widget") + // Widgets content static let viewsTitle = AppLocalizedString("widget.today.views.label", value: "Views", diff --git a/WordPress/WordPressTest/Widgets/WidgetsViewModelMapperTests.swift b/WordPress/WordPressTest/Widgets/WidgetsViewModelMapperTests.swift index 78ccef087670..6ab12381953b 100644 --- a/WordPress/WordPressTest/Widgets/WidgetsViewModelMapperTests.swift +++ b/WordPress/WordPressTest/Widgets/WidgetsViewModelMapperTests.swift @@ -5,21 +5,18 @@ final class WidgetsViewModelMapperTests: XCTestCase { func testSingleStatViewModel() { let views = 649875 let date = Date() - let title = "Views" - let dateRange = "Today" + let title = "Views Today" let todayStats = makeTodayWidgetStats(views: views) let data = makeTodayData(stats: todayStats, date: date) let sut = makeSUT(data) let viewModel = sut.getLockScreenSingleStatViewModel( - title: title, - dateRange: dateRange + title: title ) XCTAssertEqual(viewModel.siteName, data.siteName) XCTAssertEqual(viewModel.title, title) XCTAssertEqual(viewModel.value, views.abbreviatedString()) - XCTAssertEqual(viewModel.dateRange, dateRange) XCTAssertEqual(viewModel.updatedTime, date) } From fc1d6d639c6bcbc445d122713a6c482f21325879 Mon Sep 17 00:00:00 2001 From: Beemo Lee Date: Tue, 21 Mar 2023 18:13:48 +0800 Subject: [PATCH 04/10] Fix home screen widget for WordPress target disappeared. (#20371) The root cause is the WidgetBundleBuilder body doesn't allow control flow statements, move it to supportedFamilies with feature flag to keep all control logic in the same place --- .../LockScreenWidgets/LockScreenStatsWidget.swift | 13 ++++++++++++- WordPress/WordPressStatsWidgets/StatsWidgets.swift | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/WordPress/WordPressStatsWidgets/LockScreenWidgets/LockScreenStatsWidget.swift b/WordPress/WordPressStatsWidgets/LockScreenWidgets/LockScreenStatsWidget.swift index 304c3055c9d0..2dbf80c2313d 100644 --- a/WordPress/WordPressStatsWidgets/LockScreenWidgets/LockScreenStatsWidget.swift +++ b/WordPress/WordPressStatsWidgets/LockScreenWidgets/LockScreenStatsWidget.swift @@ -31,6 +31,17 @@ struct LockScreenStatsWidget: Widget { } .configurationDisplayName(LocalizableStrings.todayWidgetTitle) .description(LocalizableStrings.todayPreviewDescription) - .supportedFamilies(FeatureFlag.lockScreenWidget.enabled ? [.accessoryRectangular] : []) + .supportedFamilies(supportedFamilies()) + } +} + +@available(iOS 16.0, *) +extension LockScreenStatsWidget { + // TODO: Move to widget config after PR #20317 merged + func supportedFamilies() -> [WidgetFamily] { + guard AppConfiguration.isJetpack, FeatureFlag.lockScreenWidget.enabled else { + return [] + } + return [.accessoryRectangular] } } diff --git a/WordPress/WordPressStatsWidgets/StatsWidgets.swift b/WordPress/WordPressStatsWidgets/StatsWidgets.swift index e8fb41bb6f07..b5c01a91571f 100644 --- a/WordPress/WordPressStatsWidgets/StatsWidgets.swift +++ b/WordPress/WordPressStatsWidgets/StatsWidgets.swift @@ -7,7 +7,7 @@ struct WordPressStatsWidgets: WidgetBundle { WordPressHomeWidgetToday() WordPressHomeWidgetThisWeek() WordPressHomeWidgetAllTime() - if AppConfiguration.isJetpack, #available(iOS 16.0, *) { + if #available(iOS 16.0, *) { LockScreenStatsWidget() } } From b6493018687013c4b692d29d20ccf0c92ea12e23 Mon Sep 17 00:00:00 2001 From: Beemo Lee Date: Wed, 22 Mar 2023 20:57:15 +0800 Subject: [PATCH 05/10] Extract content logic to config from lock screen widget (#20317) * Update title of view provider from hardcoded to pass from config * Add lock screen stats widget config and implementation to include required information * Pass config to LockScreenStatsWidget Replaced the hardcoded data with config properties * Add associated type view for other status and TODO comments * Extract app configuration and feature flag check into config --- WordPress/WordPress.xcodeproj/project.pbxproj | 20 ++++++++ .../Configs/LockScreenStatsWidgetConfig.swift | 13 +++++ ...LockScreenTodayViewsStatWidgetConfig.swift | 48 +++++++++++++++++++ .../LockScreenStatsWidget.swift | 48 ++++++++----------- ...ckScreenSingleStatWidgetViewProvider.swift | 22 ++++++++- .../Views/LockScreenStatsWidgetsView.swift | 31 ++++++++++-- .../WordPressStatsWidgets/StatsWidgets.swift | 2 +- .../Localization/LocalizableStrings.swift | 2 +- 8 files changed, 151 insertions(+), 35 deletions(-) create mode 100644 WordPress/WordPressStatsWidgets/LockScreenWidgets/Configs/LockScreenStatsWidgetConfig.swift create mode 100644 WordPress/WordPressStatsWidgets/LockScreenWidgets/Configs/LockScreenTodayViewsStatWidgetConfig.swift diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index 27e8ef43b1d9..d0be3d123124 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -2880,6 +2880,10 @@ C9FE383829C206AE00D39841 /* LockScreenWidgetViewModelMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9FE382A29C204E700D39841 /* LockScreenWidgetViewModelMapper.swift */; }; C9FE383929C2077700D39841 /* LockScreenSingleStatViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9FE382B29C204E700D39841 /* LockScreenSingleStatViewModel.swift */; }; C9FE383A29C207F600D39841 /* HomeWidgetData+StatsURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9FE382129C2040600D39841 /* HomeWidgetData+StatsURL.swift */; }; + C9FE384029C2A3D200D39841 /* LockScreenTodayViewsStatWidgetConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9FE383C29C2A3D100D39841 /* LockScreenTodayViewsStatWidgetConfig.swift */; }; + C9FE384129C2A3D200D39841 /* LockScreenTodayViewsStatWidgetConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9FE383C29C2A3D100D39841 /* LockScreenTodayViewsStatWidgetConfig.swift */; }; + C9FE384629C2A3D200D39841 /* LockScreenStatsWidgetConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9FE383F29C2A3D200D39841 /* LockScreenStatsWidgetConfig.swift */; }; + C9FE384729C2A3D200D39841 /* LockScreenStatsWidgetConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9FE383F29C2A3D200D39841 /* LockScreenStatsWidgetConfig.swift */; }; CB1FD8D826E4BBAA00EDAF06 /* SharePostTypePickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB48172926E0D93D008C2D9B /* SharePostTypePickerViewController.swift */; }; CB48172A26E0D93D008C2D9B /* SharePostTypePickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB48172926E0D93D008C2D9B /* SharePostTypePickerViewController.swift */; }; CBF6201326E8FB520061A1F8 /* RemotePost+ShareData.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBF6201226E8FB520061A1F8 /* RemotePost+ShareData.swift */; }; @@ -8119,6 +8123,8 @@ C9FE382B29C204E700D39841 /* LockScreenSingleStatViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockScreenSingleStatViewModel.swift; sourceTree = ""; }; C9FE383029C2053300D39841 /* LockScreenSingleStatView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockScreenSingleStatView.swift; sourceTree = ""; }; C9FE383629C2067E00D39841 /* WidgetsViewModelMapperTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WidgetsViewModelMapperTests.swift; sourceTree = ""; }; + C9FE383C29C2A3D100D39841 /* LockScreenTodayViewsStatWidgetConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockScreenTodayViewsStatWidgetConfig.swift; sourceTree = ""; }; + C9FE383F29C2A3D200D39841 /* LockScreenStatsWidgetConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockScreenStatsWidgetConfig.swift; sourceTree = ""; }; CB1DAFB7DE085F2FF0314622 /* Pods-WordPressShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WordPressShareExtension.debug.xcconfig"; path = "../Pods/Target Support Files/Pods-WordPressShareExtension/Pods-WordPressShareExtension.debug.xcconfig"; sourceTree = ""; }; CB1FD8D926E605CF00EDAF06 /* Extensions 4.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Extensions 4.xcdatamodel"; sourceTree = ""; }; CB48172926E0D93D008C2D9B /* SharePostTypePickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharePostTypePickerViewController.swift; sourceTree = ""; }; @@ -15662,6 +15668,7 @@ isa = PBXGroup; children = ( C9C21D7629BECFC1009F68E5 /* LockScreenStatsWidget.swift */, + C9FE383B29C2A3A300D39841 /* Configs */, C9FE382929C204D700D39841 /* Models */, C9C21D8829BF4998009F68E5 /* Views */, C9FE382529C204A500D39841 /* ViewProvider */, @@ -15711,6 +15718,15 @@ path = Widgets; sourceTree = ""; }; + C9FE383B29C2A3A300D39841 /* Configs */ = { + isa = PBXGroup; + children = ( + C9FE383F29C2A3D200D39841 /* LockScreenStatsWidgetConfig.swift */, + C9FE383C29C2A3D100D39841 /* LockScreenTodayViewsStatWidgetConfig.swift */, + ); + path = Configs; + sourceTree = ""; + }; CC098B8116A9EB0400450976 /* HTML */ = { isa = PBXGroup; children = ( @@ -20477,6 +20493,7 @@ 0107E0B828F97D5000DE87DB /* KeychainUtils.swift in Sources */, 0107E0B928F97D5000DE87DB /* BuildConfiguration.swift in Sources */, 0107E0BA28F97D5000DE87DB /* TodayWidgetStats.swift in Sources */, + C9FE384129C2A3D200D39841 /* LockScreenTodayViewsStatWidgetConfig.swift in Sources */, 0107E16128FFE99300DE87DB /* WidgetConfiguration.swift in Sources */, 0107E0BB28F97D5000DE87DB /* StatsWidgetsService.swift in Sources */, 0107E0BC28F97D5000DE87DB /* StatsWidgetsView.swift in Sources */, @@ -20505,6 +20522,7 @@ 0107E18A29000E1500DE87DB /* MurielColor.swift in Sources */, 0107E0D028F97D5000DE87DB /* WordPressHomeWidgetThisWeek.swift in Sources */, C9FE382D29C204E700D39841 /* LockScreenWidgetViewModelMapper.swift in Sources */, + C9FE384729C2A3D200D39841 /* LockScreenStatsWidgetConfig.swift in Sources */, 0107E0D128F97D5000DE87DB /* SingleStatView.swift in Sources */, 0107E0D228F97D5000DE87DB /* UnconfiguredView.swift in Sources */, 0107E1852900059300DE87DB /* LocalizationConfiguration.swift in Sources */, @@ -22342,6 +22360,7 @@ 83A1B19A28AFE47C00E737AC /* KeychainUtils.swift in Sources */, 3F6BC06D25B24787007369D3 /* BuildConfiguration.swift in Sources */, 3F1FD2502548AD8B0060C53A /* TodayWidgetStats.swift in Sources */, + C9FE384029C2A3D200D39841 /* LockScreenTodayViewsStatWidgetConfig.swift in Sources */, 0107E16A28FFED1800DE87DB /* WidgetConfiguration.swift in Sources */, 3F2F0C16256C6B2C003351C7 /* StatsWidgetsService.swift in Sources */, 3F526D572539FAC60069706C /* StatsWidgetsView.swift in Sources */, @@ -22370,6 +22389,7 @@ 0107E18B29000E1700DE87DB /* MurielColor.swift in Sources */, 3F8B138F25D09AA5004FAC0A /* WordPressHomeWidgetThisWeek.swift in Sources */, C9FE382C29C204E700D39841 /* LockScreenWidgetViewModelMapper.swift in Sources */, + C9FE384629C2A3D200D39841 /* LockScreenStatsWidgetConfig.swift in Sources */, 3F5689F0254209790048A9E4 /* SingleStatView.swift in Sources */, 3FAA18CC25797B85002B1911 /* UnconfiguredView.swift in Sources */, 0107E1872900065500DE87DB /* LocalizationConfiguration.swift in Sources */, diff --git a/WordPress/WordPressStatsWidgets/LockScreenWidgets/Configs/LockScreenStatsWidgetConfig.swift b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Configs/LockScreenStatsWidgetConfig.swift new file mode 100644 index 000000000000..2f6144f51f2e --- /dev/null +++ b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Configs/LockScreenStatsWidgetConfig.swift @@ -0,0 +1,13 @@ +import WidgetKit + +protocol LockScreenStatsWidgetConfig { + associatedtype WidgetData: HomeWidgetData + associatedtype ViewProvider: LockScreenStatsWidgetsViewProvider + + var supportFamilies: [WidgetFamily] { get } + var displayName: String { get } + var description: String { get } + var kind: String { get } + var placeholderContent: WidgetData { get } + var viewProvider: ViewProvider { get } +} diff --git a/WordPress/WordPressStatsWidgets/LockScreenWidgets/Configs/LockScreenTodayViewsStatWidgetConfig.swift b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Configs/LockScreenTodayViewsStatWidgetConfig.swift new file mode 100644 index 000000000000..1a257bcb1e18 --- /dev/null +++ b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Configs/LockScreenTodayViewsStatWidgetConfig.swift @@ -0,0 +1,48 @@ +import WidgetKit + +@available(iOS 16.0, *) +struct LockScreenTodayViewsStatWidgetConfig: LockScreenStatsWidgetConfig { + typealias WidgetData = HomeWidgetTodayData + typealias ViewProvider = LockScreenSingleStatWidgetViewProvider + + var supportFamilies: [WidgetFamily] { + guard AppConfiguration.isJetpack, FeatureFlag.lockScreenWidget.enabled else { + return [] + } + return [.accessoryRectangular] + } + + var displayName: String { + LocalizableStrings.viewsInTodayTitle + } + + var description: String { + LocalizableStrings.todayPreviewDescription + } + + var kind: String { + AppConfiguration.Widget.Stats.lockScreenTodayViewsKind + } + + var placeholderContent: HomeWidgetTodayData { + HomeWidgetTodayData( + siteID: 0, + siteName: "My WordPress Site", + url: "", + timeZone: TimeZone.current, + date: Date(), + stats: TodayWidgetStats( + views: 649, + visitors: 572, + likes: 16, + comments: 8 + ) + ) + } + + var viewProvider: ViewProvider { + LockScreenSingleStatWidgetViewProvider( + title: LocalizableStrings.viewsInTodayTitle + ) + } +} diff --git a/WordPress/WordPressStatsWidgets/LockScreenWidgets/LockScreenStatsWidget.swift b/WordPress/WordPressStatsWidgets/LockScreenWidgets/LockScreenStatsWidget.swift index 2dbf80c2313d..b4b7f288038c 100644 --- a/WordPress/WordPressStatsWidgets/LockScreenWidgets/LockScreenStatsWidget.swift +++ b/WordPress/WordPressStatsWidgets/LockScreenWidgets/LockScreenStatsWidget.swift @@ -2,46 +2,36 @@ import WidgetKit import SwiftUI @available(iOS 16.0, *) -struct LockScreenStatsWidget: Widget { - private let placeholderContent = HomeWidgetTodayData( - siteID: 0, - siteName: "My WordPress Site", - url: "", - timeZone: TimeZone.current, - date: Date(), - stats: TodayWidgetStats(views: 649, - visitors: 572, - likes: 16, - comments: 8)) +struct LockScreenStatsWidget: Widget { + private let config: T + + init(config: T) { + self.config = config + } + + @available(*, deprecated, renamed: "init(config:)") + init() { + fatalError("Please use init(config: SingleStatWidgetConfig) to provide the config") + } var body: some WidgetConfiguration { IntentConfiguration( - kind: AppConfiguration.Widget.Stats.lockScreenTodayViewsKind, + kind: config.kind, intent: SelectSiteIntent.self, - provider: SiteListProvider( + provider: SiteListProvider( service: StatsWidgetsService(), - placeholderContent: placeholderContent, + placeholderContent: config.placeholderContent, + // TODO: remove widgetKind in creating lock screen widget provider and entry PR widgetKind: .today ) ) { (entry: StatsWidgetEntry) -> LockScreenStatsWidgetsView in return LockScreenStatsWidgetsView( timelineEntry: entry, - viewProvider: LockScreenSingleStatWidgetViewProvider() + viewProvider: config.viewProvider ) } - .configurationDisplayName(LocalizableStrings.todayWidgetTitle) - .description(LocalizableStrings.todayPreviewDescription) - .supportedFamilies(supportedFamilies()) - } -} - -@available(iOS 16.0, *) -extension LockScreenStatsWidget { - // TODO: Move to widget config after PR #20317 merged - func supportedFamilies() -> [WidgetFamily] { - guard AppConfiguration.isJetpack, FeatureFlag.lockScreenWidget.enabled else { - return [] - } - return [.accessoryRectangular] + .configurationDisplayName(config.displayName) + .description(config.description) + .supportedFamilies(config.supportFamilies) } } diff --git a/WordPress/WordPressStatsWidgets/LockScreenWidgets/ViewProvider/LockScreenSingleStatWidgetViewProvider.swift b/WordPress/WordPressStatsWidgets/LockScreenWidgets/ViewProvider/LockScreenSingleStatWidgetViewProvider.swift index 590b347d25b0..b71361f0cb91 100644 --- a/WordPress/WordPressStatsWidgets/LockScreenWidgets/ViewProvider/LockScreenSingleStatWidgetViewProvider.swift +++ b/WordPress/WordPressStatsWidgets/LockScreenWidgets/ViewProvider/LockScreenSingleStatWidgetViewProvider.swift @@ -3,11 +3,16 @@ import SwiftUI @available(iOS 16.0, *) struct LockScreenSingleStatWidgetViewProvider: LockScreenStatsWidgetsViewProvider { typealias SiteSelectedView = LockScreenSingleStatView + typealias LoggedOutView = Text + typealias NoSiteView = Text + typealias NoDataView = Text + + let title: String func buildSiteSelectedView(_ data: HomeWidgetData) -> LockScreenSingleStatView { let mapper = LockScreenWidgetViewModelMapper(data: data) let viewModel = mapper.getLockScreenSingleStatViewModel( - title: LocalizableStrings.viewsInTodayTitle + title: title ) return LockScreenSingleStatView(viewModel: viewModel) } @@ -16,4 +21,19 @@ struct LockScreenSingleStatWidgetViewProvider: LockScreenStatsWidgetsViewProvide let mapper = LockScreenWidgetViewModelMapper(data: data) return mapper.getStatsURL() } + + // TODO: Build view for loggedOut status + func buildLoggedOutView() -> Text { + Text("Build Later") + } + + // TODO: Build view for noSite status + func buildNoSiteView() -> Text { + Text("Build Later") + } + + // TODO: Build view for noData status + func buildNoDataView() -> Text { + Text("Build Later") + } } diff --git a/WordPress/WordPressStatsWidgets/LockScreenWidgets/Views/LockScreenStatsWidgetsView.swift b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Views/LockScreenStatsWidgetsView.swift index 12fdc0d8bb19..6b2cd60437fe 100644 --- a/WordPress/WordPressStatsWidgets/LockScreenWidgets/Views/LockScreenStatsWidgetsView.swift +++ b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Views/LockScreenStatsWidgetsView.swift @@ -3,10 +3,22 @@ import WidgetKit protocol LockScreenStatsWidgetsViewProvider { associatedtype SiteSelectedView: View + associatedtype LoggedOutView: View + associatedtype NoSiteView: View + associatedtype NoDataView: View @ViewBuilder func buildSiteSelectedView(_ data: HomeWidgetData) -> SiteSelectedView + @ViewBuilder + func buildLoggedOutView() -> LoggedOutView + + @ViewBuilder + func buildNoSiteView() -> NoSiteView + + @ViewBuilder + func buildNoDataView() -> NoDataView + func statsURL(_ data: HomeWidgetData) -> URL? } @@ -21,9 +33,22 @@ struct LockScreenStatsWidgetsView: View { viewProvider .buildSiteSelectedView(data) .widgetURL(viewProvider.statsURL(data)) - default: - // TODO: Build view for loggedOut, noSite, noData status - Text("Build Later") + case .loggedOut: + viewProvider + .buildLoggedOutView() + .widgetURL(nil) + case .noSite: + viewProvider + .buildNoSiteView() + .widgetURL(nil) + case .noData: + viewProvider + .buildNoDataView() + .widgetURL(nil) + case .disabled: + // TODO: Remove disabled case when adding lock screen TimeLineProvider and Entry + // Lock Screen widget should not have disable status + EmptyView() } } } diff --git a/WordPress/WordPressStatsWidgets/StatsWidgets.swift b/WordPress/WordPressStatsWidgets/StatsWidgets.swift index b5c01a91571f..d415471dd090 100644 --- a/WordPress/WordPressStatsWidgets/StatsWidgets.swift +++ b/WordPress/WordPressStatsWidgets/StatsWidgets.swift @@ -8,7 +8,7 @@ struct WordPressStatsWidgets: WidgetBundle { WordPressHomeWidgetThisWeek() WordPressHomeWidgetAllTime() if #available(iOS 16.0, *) { - LockScreenStatsWidget() + LockScreenStatsWidget(config: LockScreenTodayViewsStatWidgetConfig()) } } } diff --git a/WordPress/WordPressStatsWidgets/Views/Localization/LocalizableStrings.swift b/WordPress/WordPressStatsWidgets/Views/Localization/LocalizableStrings.swift index def04a1540ce..719250f13ab5 100644 --- a/WordPress/WordPressStatsWidgets/Views/Localization/LocalizableStrings.swift +++ b/WordPress/WordPressStatsWidgets/Views/Localization/LocalizableStrings.swift @@ -16,7 +16,7 @@ enum LocalizableStrings { value: "This Week", comment: "Title of this week widget") - // Lock Screen Widgets content + // Lock Screen Widgets title static let viewsInTodayTitle = AppLocalizedString("widget.lockscreen.todayview.label", value: "Views Today", comment: "Title of the one-liner information consist of views field and today date range in lock screen today views widget") From 0e87db23bbbad72871a9c74caca01017b8b38afb Mon Sep 17 00:00:00 2001 From: Beemo Lee Date: Fri, 24 Mar 2023 14:59:58 +0800 Subject: [PATCH 06/10] Create a standalone TimeLineProvider and TimelineEntry for the lock screen (#20368) * Add widget data and timeline entry for lock screen to separate concerns * Add lock screen time line provider for lock screen stats widget entry Update stats widgets service result type to consist of home widget and lock screen widget for reusable * Update to use LockScreenSiteListProvider in LockScreenStatsWidget Update associated type widget data to conform `LockScreenStatsWidgetData` more Update model mapper and view provider to based on lock screen widget data Remove unnecessary `disabled` status from `LockScreenStatsWidgetsView` Remove unnecessary `widgetKind` parameter from `LockScreenStatsWidget` Remove unused statsURL converter from the view model mapper Add home widget data conform lock screen widget data extension to WordPress target for unitTest * Extract widget load data to loader for sharing with lock screen time line provider * Update widget reader to extract the cache read from getSnapshot * Extract noData, noSite, loggedOut, disabled status checking logic to data reader with status callback * Add unitTest for widget data reader Extract cache reading to `HomeWidgetDataFileReader` for mocking the testing object Extract app configuration is jetpack to parameter for mocking the config * Remove multiple space lines * Update multiple callback to Result and enum Error --- WordPress/WordPress.xcodeproj/project.pbxproj | 56 ++++++ ...WidgetData+LockScreenStatsWidgetData.swift | 19 ++ .../Extensions/HomeWidgetData+StatsURL.swift | 2 - .../Helpers/HomeWidgetDataFileReader.swift | 12 ++ .../Helpers/WidgetDataReader.swift | 73 +++++++ .../Configs/LockScreenStatsWidgetConfig.swift | 2 +- .../LockScreenSiteListProvider.swift | 72 +++++++ .../LockScreenStatsWidget.swift | 8 +- .../Models/LockScreenStatsWidgetData.swift | 8 + .../Models/LockScreenStatsWidgetEntry.swift | 17 ++ .../LockScreenWidgetViewModelMapper.swift | 26 +-- ...ckScreenSingleStatWidgetViewProvider.swift | 7 +- .../Views/LockScreenStatsWidgetsView.swift | 12 +- .../Remote service/StatsWidgetsService.swift | 9 +- .../SiteListProvider.swift | 129 ++++--------- .../Widgets/WidgetDataReaderTests.swift | 178 ++++++++++++++++++ .../Widgets/WidgetsViewModelMapperTests.swift | 6 +- 17 files changed, 493 insertions(+), 143 deletions(-) create mode 100644 WordPress/WordPressStatsWidgets/Extensions/HomeWidgetData+LockScreenStatsWidgetData.swift create mode 100644 WordPress/WordPressStatsWidgets/Helpers/HomeWidgetDataFileReader.swift create mode 100644 WordPress/WordPressStatsWidgets/Helpers/WidgetDataReader.swift create mode 100644 WordPress/WordPressStatsWidgets/LockScreenWidgets/LockScreenSiteListProvider.swift create mode 100644 WordPress/WordPressStatsWidgets/LockScreenWidgets/Models/LockScreenStatsWidgetData.swift create mode 100644 WordPress/WordPressStatsWidgets/LockScreenWidgets/Models/LockScreenStatsWidgetEntry.swift create mode 100644 WordPress/WordPressTest/Widgets/WidgetDataReaderTests.swift diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index d0be3d123124..69c241842248 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -2858,6 +2858,23 @@ C957C20626DCC1770037628F /* LandInTheEditorHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C957C20526DCC1770037628F /* LandInTheEditorHelper.swift */; }; C957C20726DCC1770037628F /* LandInTheEditorHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C957C20526DCC1770037628F /* LandInTheEditorHelper.swift */; }; C99B08FC26081AD600CA71EB /* TemplatePreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C99B08FB26081AD600CA71EB /* TemplatePreviewViewController.swift */; }; + C9B4778429C85949008CBF49 /* LockScreenStatsWidgetData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9B4778229C85948008CBF49 /* LockScreenStatsWidgetData.swift */; }; + C9B4778529C85949008CBF49 /* LockScreenStatsWidgetData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9B4778229C85948008CBF49 /* LockScreenStatsWidgetData.swift */; }; + C9B4778629C85949008CBF49 /* LockScreenStatsWidgetEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9B4778329C85949008CBF49 /* LockScreenStatsWidgetEntry.swift */; }; + C9B4778729C85949008CBF49 /* LockScreenStatsWidgetEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9B4778329C85949008CBF49 /* LockScreenStatsWidgetEntry.swift */; }; + C9B4778929C85956008CBF49 /* HomeWidgetData+LockScreenStatsWidgetData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9B4778829C85956008CBF49 /* HomeWidgetData+LockScreenStatsWidgetData.swift */; }; + C9B4778A29C85956008CBF49 /* HomeWidgetData+LockScreenStatsWidgetData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9B4778829C85956008CBF49 /* HomeWidgetData+LockScreenStatsWidgetData.swift */; }; + C9B4778E29C85BC5008CBF49 /* LockScreenStatsWidgetData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9B4778229C85948008CBF49 /* LockScreenStatsWidgetData.swift */; }; + C9B4779F29C88323008CBF49 /* HomeWidgetData+LockScreenStatsWidgetData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9B4778829C85956008CBF49 /* HomeWidgetData+LockScreenStatsWidgetData.swift */; }; + C9B477A829CC13C6008CBF49 /* LockScreenSiteListProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9B477A729CC13C6008CBF49 /* LockScreenSiteListProvider.swift */; }; + C9B477A929CC13CB008CBF49 /* LockScreenSiteListProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9B477A729CC13C6008CBF49 /* LockScreenSiteListProvider.swift */; }; + C9B477AC29CC15D9008CBF49 /* WidgetDataReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9B477AB29CC15D9008CBF49 /* WidgetDataReader.swift */; }; + C9B477AD29CC15D9008CBF49 /* WidgetDataReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9B477AB29CC15D9008CBF49 /* WidgetDataReader.swift */; }; + C9B477AE29CC35A0008CBF49 /* WidgetDataReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9B477AB29CC15D9008CBF49 /* WidgetDataReader.swift */; }; + C9B477B029CC35C5008CBF49 /* WidgetDataReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9B477AF29CC35C5008CBF49 /* WidgetDataReaderTests.swift */; }; + C9B477B229CC4949008CBF49 /* HomeWidgetDataFileReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9B477B129CC4949008CBF49 /* HomeWidgetDataFileReader.swift */; }; + C9B477B329CC4949008CBF49 /* HomeWidgetDataFileReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9B477B129CC4949008CBF49 /* HomeWidgetDataFileReader.swift */; }; + C9B477B429CC4949008CBF49 /* HomeWidgetDataFileReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9B477B129CC4949008CBF49 /* HomeWidgetDataFileReader.swift */; }; C9C21D7729BECFC1009F68E5 /* LockScreenStatsWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9C21D7629BECFC1009F68E5 /* LockScreenStatsWidget.swift */; }; C9C21D7829BECFC7009F68E5 /* LockScreenStatsWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9C21D7629BECFC1009F68E5 /* LockScreenStatsWidget.swift */; }; C9C21D7B29BED18C009F68E5 /* LockScreenStatsWidgetsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9C21D7A29BED18C009F68E5 /* LockScreenStatsWidgetsView.swift */; }; @@ -8112,6 +8129,13 @@ C957C20526DCC1770037628F /* LandInTheEditorHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LandInTheEditorHelper.swift; sourceTree = ""; }; C99B039B2602F3CB00CA71EB /* WordPress 117.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 117.xcdatamodel"; sourceTree = ""; }; C99B08FB26081AD600CA71EB /* TemplatePreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplatePreviewViewController.swift; sourceTree = ""; }; + C9B4778229C85948008CBF49 /* LockScreenStatsWidgetData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockScreenStatsWidgetData.swift; sourceTree = ""; }; + C9B4778329C85949008CBF49 /* LockScreenStatsWidgetEntry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockScreenStatsWidgetEntry.swift; sourceTree = ""; }; + C9B4778829C85956008CBF49 /* HomeWidgetData+LockScreenStatsWidgetData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "HomeWidgetData+LockScreenStatsWidgetData.swift"; sourceTree = ""; }; + C9B477A729CC13C6008CBF49 /* LockScreenSiteListProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockScreenSiteListProvider.swift; sourceTree = ""; }; + C9B477AB29CC15D9008CBF49 /* WidgetDataReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WidgetDataReader.swift; sourceTree = ""; }; + C9B477AF29CC35C5008CBF49 /* WidgetDataReaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetDataReaderTests.swift; sourceTree = ""; }; + C9B477B129CC4949008CBF49 /* HomeWidgetDataFileReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeWidgetDataFileReader.swift; sourceTree = ""; }; C9C21D7629BECFC1009F68E5 /* LockScreenStatsWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockScreenStatsWidget.swift; sourceTree = ""; }; C9C21D7A29BED18C009F68E5 /* LockScreenStatsWidgetsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockScreenStatsWidgetsView.swift; sourceTree = ""; }; C9D7DDBF2613B84500104E95 /* WordPress 119.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 119.xcdatamodel"; sourceTree = ""; }; @@ -11062,6 +11086,7 @@ 3F8EEC6F25B4849A00EC9DAE /* SiteListProvider.swift */, C9C21D7529BECFAE009F68E5 /* LockScreenWidgets */, C9FE382029C203EE00D39841 /* Extensions */, + C9B477AA29CC15C2008CBF49 /* Helpers */, 3FFDDCB925B8A65F008D5BDD /* Widgets */, 3FB34ABB25672A59001A74A6 /* Model */, 3FFDDC0325B89F0C008D5BDD /* Cache */, @@ -15664,10 +15689,20 @@ path = Tenor; sourceTree = ""; }; + C9B477AA29CC15C2008CBF49 /* Helpers */ = { + isa = PBXGroup; + children = ( + C9B477AB29CC15D9008CBF49 /* WidgetDataReader.swift */, + C9B477B129CC4949008CBF49 /* HomeWidgetDataFileReader.swift */, + ); + path = Helpers; + sourceTree = ""; + }; C9C21D7529BECFAE009F68E5 /* LockScreenWidgets */ = { isa = PBXGroup; children = ( C9C21D7629BECFC1009F68E5 /* LockScreenStatsWidget.swift */, + C9B477A729CC13C6008CBF49 /* LockScreenSiteListProvider.swift */, C9FE383B29C2A3A300D39841 /* Configs */, C9FE382929C204D700D39841 /* Models */, C9C21D8829BF4998009F68E5 /* Views */, @@ -15689,6 +15724,7 @@ isa = PBXGroup; children = ( C9FE382129C2040600D39841 /* HomeWidgetData+StatsURL.swift */, + C9B4778829C85956008CBF49 /* HomeWidgetData+LockScreenStatsWidgetData.swift */, ); path = Extensions; sourceTree = ""; @@ -15704,6 +15740,8 @@ C9FE382929C204D700D39841 /* Models */ = { isa = PBXGroup; children = ( + C9B4778229C85948008CBF49 /* LockScreenStatsWidgetData.swift */, + C9B4778329C85949008CBF49 /* LockScreenStatsWidgetEntry.swift */, C9FE382B29C204E700D39841 /* LockScreenSingleStatViewModel.swift */, C9FE382A29C204E700D39841 /* LockScreenWidgetViewModelMapper.swift */, ); @@ -15714,6 +15752,7 @@ isa = PBXGroup; children = ( C9FE383629C2067E00D39841 /* WidgetsViewModelMapperTests.swift */, + C9B477AF29CC35C5008CBF49 /* WidgetDataReaderTests.swift */, ); path = Widgets; sourceTree = ""; @@ -20482,6 +20521,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + C9B4778729C85949008CBF49 /* LockScreenStatsWidgetEntry.swift in Sources */, 0107E0B428F97D5000DE87DB /* Constants.m in Sources */, 01CE5012290A890B00A9C2E0 /* TracksConfiguration.swift in Sources */, C9C21D7C29BED18C009F68E5 /* LockScreenStatsWidgetsView.swift in Sources */, @@ -20493,6 +20533,7 @@ 0107E0B828F97D5000DE87DB /* KeychainUtils.swift in Sources */, 0107E0B928F97D5000DE87DB /* BuildConfiguration.swift in Sources */, 0107E0BA28F97D5000DE87DB /* TodayWidgetStats.swift in Sources */, + C9B477AD29CC15D9008CBF49 /* WidgetDataReader.swift in Sources */, C9FE384129C2A3D200D39841 /* LockScreenTodayViewsStatWidgetConfig.swift in Sources */, 0107E16128FFE99300DE87DB /* WidgetConfiguration.swift in Sources */, 0107E0BB28F97D5000DE87DB /* StatsWidgetsService.swift in Sources */, @@ -20504,8 +20545,10 @@ 0107E0C028F97D5000DE87DB /* WordPressHomeWidgetToday.swift in Sources */, 0107E0C128F97D5000DE87DB /* FlexibleCard.swift in Sources */, 0107E0C228F97D5000DE87DB /* VerticalCard.swift in Sources */, + C9B477A929CC13CB008CBF49 /* LockScreenSiteListProvider.swift in Sources */, 0107E0C328F97D5000DE87DB /* ThisWeekWidgetStats.swift in Sources */, 0107E0C428F97D5000DE87DB /* HomeWidgetAllTimeData.swift in Sources */, + C9B4778529C85949008CBF49 /* LockScreenStatsWidgetData.swift in Sources */, 0107E0C528F97D5000DE87DB /* GroupedViewData.swift in Sources */, C9C21D7829BECFC7009F68E5 /* LockScreenStatsWidget.swift in Sources */, 0107E0C628F97D5000DE87DB /* FeatureFlag.swift in Sources */, @@ -20513,6 +20556,7 @@ 0107E18F29000EA200DE87DB /* UIColor+MurielColors.swift in Sources */, 0107E0C828F97D5000DE87DB /* StatsValueView.swift in Sources */, 0107E0C928F97D5000DE87DB /* SiteListProvider.swift in Sources */, + C9B477B429CC4949008CBF49 /* HomeWidgetDataFileReader.swift in Sources */, 0107E0CA28F97D5000DE87DB /* HomeWidgetThisWeekData.swift in Sources */, 0107E0CB28F97D5000DE87DB /* WordPressHomeWidgetAllTime.swift in Sources */, 0107E0CC28F97D5000DE87DB /* KeyValueDatabase.swift in Sources */, @@ -20529,6 +20573,7 @@ 0107E0D328F97D5000DE87DB /* Tracks+StatsWidgets.swift in Sources */, 0107E0D428F97D5000DE87DB /* HomeWidgetData.swift in Sources */, 0107E0D528F97D5000DE87DB /* HomeWidgetTodayData.swift in Sources */, + C9B4778A29C85956008CBF49 /* HomeWidgetData+LockScreenStatsWidgetData.swift in Sources */, 0107E0D628F97D5000DE87DB /* AllTimeWidgetStats.swift in Sources */, 0107E0D728F97D5000DE87DB /* Sites.intentdefinition in Sources */, 0107E0D828F97D5000DE87DB /* LocalizableStrings.swift in Sources */, @@ -20939,6 +20984,7 @@ 98AA9F2127EA890800B3A98C /* FeatureIntroductionViewController.swift in Sources */, BE1071FC1BC75E7400906AFF /* WPStyleGuide+Blog.swift in Sources */, B56695B01D411EEB007E342F /* KeyboardDismissHelper.swift in Sources */, + C9B477B229CC4949008CBF49 /* HomeWidgetDataFileReader.swift in Sources */, F5B9D7F0245BA938002BB2C7 /* FancyAlertViewController+CreateButtonAnnouncement.swift in Sources */, FF54D4641D6F3FA900A0DC4D /* GutenbergSettings.swift in Sources */, 3FAF9CC526D03C7400268EA2 /* DomainSuggestionViewControllerWrapper.swift in Sources */, @@ -21209,7 +21255,9 @@ B59D994F1C0790CC0003D795 /* SettingsListEditorViewController.swift in Sources */, 9A4E215C21F75BBE00EFF212 /* QuickStartChecklistManager.swift in Sources */, FAC1B81E29B0C2AC00E0C542 /* BlazeOverlayViewModel.swift in Sources */, + C9B4778E29C85BC5008CBF49 /* LockScreenStatsWidgetData.swift in Sources */, C81CCD84243BF7A600A83E27 /* NoResultsTenorConfiguration.swift in Sources */, + C9B477AE29CC35A0008CBF49 /* WidgetDataReader.swift in Sources */, 9A8ECE0F2254A3260043C8DA /* JetpackRemoteInstallViewController.swift in Sources */, 1724DDC81C60F1200099D273 /* PlanDetailViewController.swift in Sources */, 9A2CD5372146B8C700AE5055 /* Array+Page.swift in Sources */, @@ -22267,6 +22315,7 @@ C39ABBAE294BE84000F6F278 /* BackupListViewController+JetpackBannerViewController.swift in Sources */, 1714F8D020E6DA8900226DCB /* RouteMatcher.swift in Sources */, 591A428F1A6DC6F2003807A6 /* WPGUIConstants.m in Sources */, + C9B4779F29C88323008CBF49 /* HomeWidgetData+LockScreenStatsWidgetData.swift in Sources */, 3FD272E024CF8F270021F0C8 /* UIColor+Notice.swift in Sources */, 98830A922747043B0061A87C /* BorderedButtonTableViewCell.swift in Sources */, FAD9457E25B5647B00F011B5 /* JetpackBackupOptionsCoordinator.swift in Sources */, @@ -22349,6 +22398,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + C9B4778629C85949008CBF49 /* LockScreenStatsWidgetEntry.swift in Sources */, 3F1FD30D2548B0A80060C53A /* Constants.m in Sources */, 01CE500C290A88BF00A9C2E0 /* TracksConfiguration.swift in Sources */, C9C21D7B29BED18C009F68E5 /* LockScreenStatsWidgetsView.swift in Sources */, @@ -22360,6 +22410,7 @@ 83A1B19A28AFE47C00E737AC /* KeychainUtils.swift in Sources */, 3F6BC06D25B24787007369D3 /* BuildConfiguration.swift in Sources */, 3F1FD2502548AD8B0060C53A /* TodayWidgetStats.swift in Sources */, + C9B477AC29CC15D9008CBF49 /* WidgetDataReader.swift in Sources */, C9FE384029C2A3D200D39841 /* LockScreenTodayViewsStatWidgetConfig.swift in Sources */, 0107E16A28FFED1800DE87DB /* WidgetConfiguration.swift in Sources */, 3F2F0C16256C6B2C003351C7 /* StatsWidgetsService.swift in Sources */, @@ -22371,8 +22422,10 @@ 3F568A2F254216550048A9E4 /* FlexibleCard.swift in Sources */, 3F568A1F254213B60048A9E4 /* VerticalCard.swift in Sources */, 3F8B306825D1D4B8005A2903 /* ThisWeekWidgetStats.swift in Sources */, + C9B477A829CC13C6008CBF49 /* LockScreenSiteListProvider.swift in Sources */, 3F5C861A25C9EA2500BABE64 /* HomeWidgetAllTimeData.swift in Sources */, 3FE20C1525CF165700A15525 /* GroupedViewData.swift in Sources */, + C9B4778429C85949008CBF49 /* LockScreenStatsWidgetData.swift in Sources */, 3F6BC04B25B2474C007369D3 /* FeatureFlag.swift in Sources */, C9C21D7729BECFC1009F68E5 /* LockScreenStatsWidget.swift in Sources */, 3F8EEC4E25B4817000EC9DAE /* StatsWidgets.swift in Sources */, @@ -22380,6 +22433,7 @@ 0107E18E29000EA100DE87DB /* UIColor+MurielColors.swift in Sources */, 3F8EEC7025B4849A00EC9DAE /* SiteListProvider.swift in Sources */, 3F8B136D25D08F34004FAC0A /* HomeWidgetThisWeekData.swift in Sources */, + C9B477B329CC4949008CBF49 /* HomeWidgetDataFileReader.swift in Sources */, 3F5C86C025CA197500BABE64 /* WordPressHomeWidgetAllTime.swift in Sources */, 3F6BC07E25B247A4007369D3 /* KeyValueDatabase.swift in Sources */, 3F1FD27B2548AE900060C53A /* CocoaLumberjack.swift in Sources */, @@ -22396,6 +22450,7 @@ 98390AC3254C984700868F0A /* Tracks+StatsWidgets.swift in Sources */, 3FA53ED62565860900F4D9A2 /* HomeWidgetData.swift in Sources */, 3FB34ACB25672A90001A74A6 /* HomeWidgetTodayData.swift in Sources */, + C9B4778929C85956008CBF49 /* HomeWidgetData+LockScreenStatsWidgetData.swift in Sources */, 3F5C863B25C9EA8200BABE64 /* AllTimeWidgetStats.swift in Sources */, 3FD675D925C87A15009AB3C1 /* Sites.intentdefinition in Sources */, 3FE77C8325B0CA89007DE9E5 /* LocalizableStrings.swift in Sources */, @@ -23070,6 +23125,7 @@ D848CC1920FF3A2400A9038F /* FormattableNotIconTests.swift in Sources */, 32110547250BFC3E0048446F /* ImageDimensionParserTests.swift in Sources */, E1AB5A3A1E0C464700574B4E /* DelayTests.swift in Sources */, + C9B477B029CC35C5008CBF49 /* WidgetDataReaderTests.swift in Sources */, 8B7F51CB24EED8A8008CF5B5 /* ReaderTrackerTests.swift in Sources */, D848CC0320FF04FA00A9038F /* FormattableUserContentTests.swift in Sources */, 5948AD111AB73D19006E8882 /* WPAppAnalyticsTests.m in Sources */, diff --git a/WordPress/WordPressStatsWidgets/Extensions/HomeWidgetData+LockScreenStatsWidgetData.swift b/WordPress/WordPressStatsWidgets/Extensions/HomeWidgetData+LockScreenStatsWidgetData.swift new file mode 100644 index 000000000000..e213c27f7ce2 --- /dev/null +++ b/WordPress/WordPressStatsWidgets/Extensions/HomeWidgetData+LockScreenStatsWidgetData.swift @@ -0,0 +1,19 @@ +import Foundation + +extension HomeWidgetTodayData: LockScreenStatsWidgetData { + var views: Int? { + stats.views + } +} + +extension HomeWidgetAllTimeData: LockScreenStatsWidgetData { + var views: Int? { + stats.views + } +} + +extension HomeWidgetThisWeekData: LockScreenStatsWidgetData { + var views: Int? { + nil + } +} diff --git a/WordPress/WordPressStatsWidgets/Extensions/HomeWidgetData+StatsURL.swift b/WordPress/WordPressStatsWidgets/Extensions/HomeWidgetData+StatsURL.swift index e08968880354..58ac96e28fcf 100644 --- a/WordPress/WordPressStatsWidgets/Extensions/HomeWidgetData+StatsURL.swift +++ b/WordPress/WordPressStatsWidgets/Extensions/HomeWidgetData+StatsURL.swift @@ -8,7 +8,6 @@ extension HomeWidgetTodayData { } } - extension HomeWidgetAllTimeData { static let statsUrl = "https://wordpress.com/stats/insights/" @@ -17,7 +16,6 @@ extension HomeWidgetAllTimeData { } } - extension HomeWidgetThisWeekData { static let statsUrl = "https://wordpress.com/stats/week/" diff --git a/WordPress/WordPressStatsWidgets/Helpers/HomeWidgetDataFileReader.swift b/WordPress/WordPressStatsWidgets/Helpers/HomeWidgetDataFileReader.swift new file mode 100644 index 000000000000..0cd91b1652f3 --- /dev/null +++ b/WordPress/WordPressStatsWidgets/Helpers/HomeWidgetDataFileReader.swift @@ -0,0 +1,12 @@ +import Foundation + +final class HomeWidgetDataFileReader: WidgetDataCacheReader { + func widgetData(for siteID: String) -> T? { + /// - TODO: we should not really be needing to do this conversion. Maybe we can evaluate a better mechanism for site identification. + guard let siteID = Int(siteID) else { + return nil + } + + return T.read()?[siteID] + } +} diff --git a/WordPress/WordPressStatsWidgets/Helpers/WidgetDataReader.swift b/WordPress/WordPressStatsWidgets/Helpers/WidgetDataReader.swift new file mode 100644 index 000000000000..fd0527e830d6 --- /dev/null +++ b/WordPress/WordPressStatsWidgets/Helpers/WidgetDataReader.swift @@ -0,0 +1,73 @@ +import Foundation + +protocol WidgetDataCacheReader { + func widgetData(for siteID: String) -> T? +} + +enum WidgetDataReadError: Error { + case jetpackFeatureDisabled + case noData + case noSite + case loggedOut +} + +final class WidgetDataReader { + let userDefaults: UserDefaults? + let cacheReader: WidgetDataCacheReader + + init(_ userDefaults: UserDefaults? = UserDefaults(suiteName: WPAppGroupName), + _ cacheReader: any WidgetDataCacheReader = HomeWidgetDataFileReader() + ) { + self.userDefaults = userDefaults + self.cacheReader = cacheReader + } + + /// Returns cached widget data based on the selected site when editing widget and the default site. + /// Configuration.site is nil until IntentHandler is initialized. + /// Configuration.site can have old value after logging in with a different account. No way to reset configuration when the user logs out. + /// Using defaultSiteID if both of these cases. + /// - Parameters: + /// - configuration: Configuration of the Widget Site Selection Intent + /// - defaultSiteID: ID of the default site in the account + /// - Returns: Widget data + func widgetData(for configuration: SelectSiteIntent, defaultSiteID: Int?) -> T? { + + /// If configuration.site.identifier has value but there's no widgetData, it means that this identifier comes from previously logged in account + if let selectedSite = configuration.site?.identifier, + let widgetData: T = cacheReader.widgetData(for: selectedSite) { + return widgetData + } else if let defaultSiteID = defaultSiteID { + return cacheReader.widgetData(for: String(defaultSiteID)) + } else { + return nil + } + } + + func widgetData( + for configuration: SelectSiteIntent, + defaultSiteID: Int?, + isJetpack: Bool + ) -> Result { + guard let defaults = userDefaults else { + return .failure(.noData) + } + // Jetpack won't have disable status, only WordPress need to check is Jetpack feature disabled + guard isJetpack || !defaults.bool(forKey: AppConfiguration.Widget.Stats.userDefaultsJetpackFeaturesDisabledKey) else { + return .failure(.jetpackFeatureDisabled) + } + guard let defaultSiteID = defaultSiteID else { + let loggedIn = defaults.bool(forKey: AppConfiguration.Widget.Stats.userDefaultsLoggedInKey) + + if loggedIn { + return .failure(.noSite) + } else { + return .failure(.loggedOut) + } + } + guard let widgetData = widgetData(for: configuration, defaultSiteID: defaultSiteID) else { + return .failure(.noData) + } + + return .success(widgetData) + } +} diff --git a/WordPress/WordPressStatsWidgets/LockScreenWidgets/Configs/LockScreenStatsWidgetConfig.swift b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Configs/LockScreenStatsWidgetConfig.swift index 2f6144f51f2e..8bd2bd84760f 100644 --- a/WordPress/WordPressStatsWidgets/LockScreenWidgets/Configs/LockScreenStatsWidgetConfig.swift +++ b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Configs/LockScreenStatsWidgetConfig.swift @@ -1,7 +1,7 @@ import WidgetKit protocol LockScreenStatsWidgetConfig { - associatedtype WidgetData: HomeWidgetData + associatedtype WidgetData: HomeWidgetData & LockScreenStatsWidgetData associatedtype ViewProvider: LockScreenStatsWidgetsViewProvider var supportFamilies: [WidgetFamily] { get } diff --git a/WordPress/WordPressStatsWidgets/LockScreenWidgets/LockScreenSiteListProvider.swift b/WordPress/WordPressStatsWidgets/LockScreenWidgets/LockScreenSiteListProvider.swift new file mode 100644 index 000000000000..b756a452f828 --- /dev/null +++ b/WordPress/WordPressStatsWidgets/LockScreenWidgets/LockScreenSiteListProvider.swift @@ -0,0 +1,72 @@ +import WidgetKit +import SwiftUI + +struct LockScreenSiteListProvider: IntentTimelineProvider { + let service: StatsWidgetsService + let placeholderContent: T + + // refresh interval of the widget, in minutes + let refreshInterval = 30 + // minimum elapsed time, in minutes, before new data are fetched from the backend. + let minElapsedTimeToRefresh = 1 + + private var defaultSiteID: Int? { + UserDefaults(suiteName: WPAppGroupName)?.object(forKey: AppConfiguration.Widget.Stats.userDefaultsSiteIdKey) as? Int + } + + private let widgetDataLoader = WidgetDataReader() + + func placeholder(in context: Context) -> LockScreenStatsWidgetEntry { + LockScreenStatsWidgetEntry.siteSelected(placeholderContent, context) + } + + func getSnapshot(for configuration: SelectSiteIntent, in context: Context, completion: @escaping (LockScreenStatsWidgetEntry) -> Void) { + let content = widgetDataLoader.widgetData(for: configuration, defaultSiteID: defaultSiteID) ?? placeholderContent + completion(.siteSelected(content, context)) + } + + func getTimeline(for configuration: SelectSiteIntent, in context: Context, completion: @escaping (Timeline) -> Void) { + switch widgetDataLoader.widgetData( + for: configuration, + defaultSiteID: defaultSiteID, + isJetpack: AppConfiguration.isJetpack + ) { + case .success(let widgetData): + let date = Date() + let nextRefreshDate = Calendar.current.date(byAdding: .minute, value: refreshInterval, to: date) ?? date + let elapsedTime = abs(Calendar.current.dateComponents([.minute], from: widgetData.date, to: date).minute ?? 0) + + let privateCompletion = { (timelineEntry: LockScreenStatsWidgetEntry) in + let timeline = Timeline(entries: [timelineEntry], policy: .after(nextRefreshDate)) + completion(timeline) + } + // if cached data are "too old", refresh them from the backend, otherwise keep them + guard elapsedTime > minElapsedTimeToRefresh else { + privateCompletion(.siteSelected(widgetData, context)) + return + } + + service.fetchStats(for: widgetData) { result in + switch result { + case .failure(let error): + DDLogError("LockScreen StatsWidgets: failed to fetch remote stats. Returned error: \(error.localizedDescription)") + privateCompletion(.siteSelected(widgetData, context)) + case .success(let newWidgetData): + privateCompletion(.siteSelected(newWidgetData, context)) + } + } + case .failure(let error): + switch error { + case .noData: + completion(Timeline(entries: [.noData], policy: .never)) + case .noSite: + completion(Timeline(entries: [.noSite], policy: .never)) + case .loggedOut: + completion(Timeline(entries: [.loggedOut], policy: .never)) + case .jetpackFeatureDisabled: + DDLogError("LockScreen StatsWidgets: lock screen widget should have Jetpack feature disable error") + completion(Timeline(entries: [.noData], policy: .never)) + } + } + } +} diff --git a/WordPress/WordPressStatsWidgets/LockScreenWidgets/LockScreenStatsWidget.swift b/WordPress/WordPressStatsWidgets/LockScreenWidgets/LockScreenStatsWidget.swift index b4b7f288038c..8229eadacd77 100644 --- a/WordPress/WordPressStatsWidgets/LockScreenWidgets/LockScreenStatsWidget.swift +++ b/WordPress/WordPressStatsWidgets/LockScreenWidgets/LockScreenStatsWidget.swift @@ -18,13 +18,11 @@ struct LockScreenStatsWidget: Widget { IntentConfiguration( kind: config.kind, intent: SelectSiteIntent.self, - provider: SiteListProvider( + provider: LockScreenSiteListProvider( service: StatsWidgetsService(), - placeholderContent: config.placeholderContent, - // TODO: remove widgetKind in creating lock screen widget provider and entry PR - widgetKind: .today + placeholderContent: config.placeholderContent ) - ) { (entry: StatsWidgetEntry) -> LockScreenStatsWidgetsView in + ) { (entry: LockScreenStatsWidgetEntry) -> LockScreenStatsWidgetsView in return LockScreenStatsWidgetsView( timelineEntry: entry, viewProvider: config.viewProvider diff --git a/WordPress/WordPressStatsWidgets/LockScreenWidgets/Models/LockScreenStatsWidgetData.swift b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Models/LockScreenStatsWidgetData.swift new file mode 100644 index 000000000000..cf687ed3a07c --- /dev/null +++ b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Models/LockScreenStatsWidgetData.swift @@ -0,0 +1,8 @@ +import Foundation + +protocol LockScreenStatsWidgetData { + var siteName: String { get } + var statsURL: URL? { get } + var views: Int? { get } + var date: Date { get } +} diff --git a/WordPress/WordPressStatsWidgets/LockScreenWidgets/Models/LockScreenStatsWidgetEntry.swift b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Models/LockScreenStatsWidgetEntry.swift new file mode 100644 index 000000000000..3bc703a6623b --- /dev/null +++ b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Models/LockScreenStatsWidgetEntry.swift @@ -0,0 +1,17 @@ +import WidgetKit + +enum LockScreenStatsWidgetEntry: TimelineEntry { + case siteSelected(LockScreenStatsWidgetData, TimelineProviderContext) + case loggedOut + case noSite + case noData + + var date: Date { + switch self { + case .siteSelected(let widgetData, _): + return widgetData.date + case .loggedOut, .noSite, .noData: + return Date() + } + } +} diff --git a/WordPress/WordPressStatsWidgets/LockScreenWidgets/Models/LockScreenWidgetViewModelMapper.swift b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Models/LockScreenWidgetViewModelMapper.swift index 7c124701cd25..4004b9bcbcf9 100644 --- a/WordPress/WordPressStatsWidgets/LockScreenWidgets/Models/LockScreenWidgetViewModelMapper.swift +++ b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Models/LockScreenWidgetViewModelMapper.swift @@ -1,7 +1,7 @@ import Foundation struct LockScreenWidgetViewModelMapper { - let data: HomeWidgetData + let data: LockScreenStatsWidgetData func getLockScreenSingleStatViewModel( title: String @@ -14,33 +14,11 @@ struct LockScreenWidgetViewModelMapper { ) } - // TODO: Add `LockScreenStatsWidgetData` in creating lock screen widget provider and entry PR - // define statsURL, views, date, siteName - // HomeWidgetTodayData, HomeWidgetAllTimeData, HomeWidgetThisWeekData conform to it - // to reduce the type converting - func getStatsURL() -> URL? { - if let todayData = data as? HomeWidgetTodayData { - return todayData.statsURL - } else if let allTimeData = data as? HomeWidgetAllTimeData { - return allTimeData.statsURL - } else if let thisWeekData = data as? HomeWidgetThisWeekData { - return thisWeekData.statsURL - } else { - return nil - } - } - func getSiteName() -> String { data.siteName } func getViews() -> String { - if let todayData = data as? HomeWidgetTodayData { - return todayData.stats.views.abbreviatedString() - } else if let allTimeData = data as? HomeWidgetAllTimeData { - return allTimeData.stats.views.abbreviatedString() - } else { - return "" - } + data.views?.abbreviatedString() ?? "" } } diff --git a/WordPress/WordPressStatsWidgets/LockScreenWidgets/ViewProvider/LockScreenSingleStatWidgetViewProvider.swift b/WordPress/WordPressStatsWidgets/LockScreenWidgets/ViewProvider/LockScreenSingleStatWidgetViewProvider.swift index b71361f0cb91..cf632d803d57 100644 --- a/WordPress/WordPressStatsWidgets/LockScreenWidgets/ViewProvider/LockScreenSingleStatWidgetViewProvider.swift +++ b/WordPress/WordPressStatsWidgets/LockScreenWidgets/ViewProvider/LockScreenSingleStatWidgetViewProvider.swift @@ -9,7 +9,7 @@ struct LockScreenSingleStatWidgetViewProvider: LockScreenStatsWidgetsViewProvide let title: String - func buildSiteSelectedView(_ data: HomeWidgetData) -> LockScreenSingleStatView { + func buildSiteSelectedView(_ data: LockScreenStatsWidgetData) -> LockScreenSingleStatView { let mapper = LockScreenWidgetViewModelMapper(data: data) let viewModel = mapper.getLockScreenSingleStatViewModel( title: title @@ -17,11 +17,6 @@ struct LockScreenSingleStatWidgetViewProvider: LockScreenStatsWidgetsViewProvide return LockScreenSingleStatView(viewModel: viewModel) } - func statsURL(_ data: HomeWidgetData) -> URL? { - let mapper = LockScreenWidgetViewModelMapper(data: data) - return mapper.getStatsURL() - } - // TODO: Build view for loggedOut status func buildLoggedOutView() -> Text { Text("Build Later") diff --git a/WordPress/WordPressStatsWidgets/LockScreenWidgets/Views/LockScreenStatsWidgetsView.swift b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Views/LockScreenStatsWidgetsView.swift index 6b2cd60437fe..04c17426be0d 100644 --- a/WordPress/WordPressStatsWidgets/LockScreenWidgets/Views/LockScreenStatsWidgetsView.swift +++ b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Views/LockScreenStatsWidgetsView.swift @@ -8,7 +8,7 @@ protocol LockScreenStatsWidgetsViewProvider { associatedtype NoDataView: View @ViewBuilder - func buildSiteSelectedView(_ data: HomeWidgetData) -> SiteSelectedView + func buildSiteSelectedView(_ data: LockScreenStatsWidgetData) -> SiteSelectedView @ViewBuilder func buildLoggedOutView() -> LoggedOutView @@ -18,12 +18,10 @@ protocol LockScreenStatsWidgetsViewProvider { @ViewBuilder func buildNoDataView() -> NoDataView - - func statsURL(_ data: HomeWidgetData) -> URL? } struct LockScreenStatsWidgetsView: View { - let timelineEntry: StatsWidgetEntry + let timelineEntry: LockScreenStatsWidgetEntry let viewProvider: T @ViewBuilder @@ -32,7 +30,7 @@ struct LockScreenStatsWidgetsView: View { case let .siteSelected(data, _): viewProvider .buildSiteSelectedView(data) - .widgetURL(viewProvider.statsURL(data)) + .widgetURL(data.statsURL) case .loggedOut: viewProvider .buildLoggedOutView() @@ -45,10 +43,6 @@ struct LockScreenStatsWidgetsView: View { viewProvider .buildNoDataView() .widgetURL(nil) - case .disabled: - // TODO: Remove disabled case when adding lock screen TimeLineProvider and Entry - // Lock Screen widget should not have disable status - EmptyView() } } } diff --git a/WordPress/WordPressStatsWidgets/Remote service/StatsWidgetsService.swift b/WordPress/WordPressStatsWidgets/Remote service/StatsWidgetsService.swift index 3ce038e032e3..3c09f4799ef3 100644 --- a/WordPress/WordPressStatsWidgets/Remote service/StatsWidgetsService.swift +++ b/WordPress/WordPressStatsWidgets/Remote service/StatsWidgetsService.swift @@ -2,6 +2,7 @@ import WordPressKit /// Type that wraps the backend request for new stats class StatsWidgetsService { + typealias ResultType = HomeWidgetData & LockScreenStatsWidgetData private enum State { case loading @@ -22,7 +23,7 @@ class StatsWidgetsService { func fetchStats(for widgetData: HomeWidgetData, - completion: @escaping (Result) -> Void) { + completion: @escaping (Result) -> Void) { guard !state.isLoading else { return @@ -53,7 +54,7 @@ class StatsWidgetsService { private func fetchTodayStats(service: StatsServiceRemoteV2, widgetData: HomeWidgetTodayData, - completion: @escaping (Result) -> Void) { + completion: @escaping (Result) -> Void) { service.getInsight { [weak self] (insight: StatsTodayInsight?, error) in guard let self = self else { @@ -92,7 +93,7 @@ class StatsWidgetsService { private func fetchAllTimeStats(service: StatsServiceRemoteV2, widgetData: HomeWidgetAllTimeData, - completion: @escaping (Result) -> Void) { + completion: @escaping (Result) -> Void) { service.getInsight { [weak self] (insight: StatsAllTimesInsight?, error) in @@ -127,7 +128,7 @@ class StatsWidgetsService { private func fetchThisWeekStats(service: StatsServiceRemoteV2, widgetData: HomeWidgetThisWeekData, - completion: @escaping (Result) -> Void) { + completion: @escaping (Result) -> Void) { // Get the current date in the site's time zone. let siteTimeZone = widgetData.timeZone diff --git a/WordPress/WordPressStatsWidgets/SiteListProvider.swift b/WordPress/WordPressStatsWidgets/SiteListProvider.swift index 86e9cc16a2e3..b9cf4680e269 100644 --- a/WordPress/WordPressStatsWidgets/SiteListProvider.swift +++ b/WordPress/WordPressStatsWidgets/SiteListProvider.swift @@ -17,109 +17,60 @@ struct SiteListProvider: IntentTimelineProvider { UserDefaults(suiteName: WPAppGroupName)?.object(forKey: AppConfiguration.Widget.Stats.userDefaultsSiteIdKey) as? Int } + private let widgetDataLoader = WidgetDataReader() + func placeholder(in context: Context) -> StatsWidgetEntry { StatsWidgetEntry.siteSelected(placeholderContent, context) } func getSnapshot(for configuration: SelectSiteIntent, in context: Context, completion: @escaping (StatsWidgetEntry) -> Void) { - guard let site = configuration.site, - let siteIdentifier = site.identifier, - let widgetData = widgetData(for: siteIdentifier) else { - - if let siteID = defaultSiteID, let content = T.read()?[siteID] { - completion(.siteSelected(content, context)) - } else { - completion(.siteSelected(placeholderContent, context)) - } - return - } - - completion(.siteSelected(widgetData, context)) + let content = widgetDataLoader.widgetData(for: configuration, defaultSiteID: defaultSiteID) ?? placeholderContent + completion(.siteSelected(content, context)) } func getTimeline(for configuration: SelectSiteIntent, in context: Context, completion: @escaping (Timeline) -> Void) { - guard let defaults = UserDefaults(suiteName: WPAppGroupName) else { - completion(Timeline(entries: [.noData(widgetKind)], policy: .never)) - return - } - guard !defaults.bool(forKey: AppConfiguration.Widget.Stats.userDefaultsJetpackFeaturesDisabledKey) else { - completion(Timeline(entries: [.disabled(widgetKind)], policy: .never)) - return - } - guard let defaultSiteID = defaultSiteID else { - let loggedIn = defaults.bool(forKey: AppConfiguration.Widget.Stats.userDefaultsLoggedInKey) - - if loggedIn { - completion(Timeline(entries: [.noSite(widgetKind)], policy: .never)) - } else { - completion(Timeline(entries: [.loggedOut(widgetKind)], policy: .never)) + switch widgetDataLoader.widgetData( + for: configuration, + defaultSiteID: defaultSiteID, + isJetpack: AppConfiguration.isJetpack + ) { + case .success(let widgetData): + let date = Date() + let nextRefreshDate = Calendar.current.date(byAdding: .minute, value: refreshInterval, to: date) ?? date + let elapsedTime = abs(Calendar.current.dateComponents([.minute], from: widgetData.date, to: date).minute ?? 0) + + let privateCompletion = { (timelineEntry: StatsWidgetEntry) in + let timeline = Timeline(entries: [timelineEntry], policy: .after(nextRefreshDate)) + completion(timeline) } - return - } - - guard let widgetData = widgetData(for: configuration, defaultSiteID: defaultSiteID) else { - completion(Timeline(entries: [.noData(widgetKind)], policy: .never)) - return - } - - let date = Date() - let nextRefreshDate = Calendar.current.date(byAdding: .minute, value: refreshInterval, to: date) ?? date - let elapsedTime = abs(Calendar.current.dateComponents([.minute], from: widgetData.date, to: date).minute ?? 0) - - let privateCompletion = { (timelineEntry: StatsWidgetEntry) in - let timeline = Timeline(entries: [timelineEntry], policy: .after(nextRefreshDate)) - completion(timeline) - } - - // if cached data are "too old", refresh them from the backend, otherwise keep them - guard elapsedTime > minElapsedTimeToRefresh else { - - privateCompletion(.siteSelected(widgetData, context)) - return - } - - service.fetchStats(for: widgetData) { result in - - switch result { - case .failure(let error): - DDLogError("StatsWidgets: failed to fetch remote stats. Returned error: \(error.localizedDescription)") - + // if cached data are "too old", refresh them from the backend, otherwise keep them + guard elapsedTime > minElapsedTimeToRefresh else { privateCompletion(.siteSelected(widgetData, context)) - - case .success(let newWidgetData): - - privateCompletion(.siteSelected(newWidgetData, context)) + return } - } - } -} -// MARK: - Widget Data - -private extension SiteListProvider { - /// Returns cached widget data based on the selected site when editing widget and the default site. - /// Configuration.site is nil until IntentHandler is initialized. - /// Configuration.site can have old value after logging in with a different account. No way to reset configuration when the user logs out. - /// Using defaultSiteID if both of these cases. - /// - Parameters: - /// - configuration: Configuration of the Widget Site Selection Intent - /// - defaultSiteID: ID of the default site in the account - /// - Returns: Widget data - func widgetData(for configuration: SelectSiteIntent, defaultSiteID: Int) -> T? { - - /// If configuration.site.identifier has value but there's no widgetData, it means that this identifier comes from previously logged in account - return widgetData(for: configuration.site?.identifier ?? String(defaultSiteID)) - ?? widgetData(for: String(defaultSiteID)) - } - - func widgetData(for siteID: String) -> T? { - /// - TODO: we should not really be needing to do this conversion. Maybe we can evaluate a better mechanism for site identification. - guard let siteID = Int(siteID) else { - return nil + service.fetchStats(for: widgetData) { result in + switch result { + case .failure(let error): + DDLogError("StatsWidgets: failed to fetch remote stats. Returned error: \(error.localizedDescription)") + privateCompletion(.siteSelected(widgetData, context)) + case .success(let newWidgetData): + privateCompletion(.siteSelected(newWidgetData, context)) + } + } + case .failure(let error): + switch error { + case .noData: + completion(Timeline(entries: [.noData(widgetKind)], policy: .never)) + case .noSite: + completion(Timeline(entries: [.noSite(widgetKind)], policy: .never)) + case .loggedOut: + completion(Timeline(entries: [.loggedOut(widgetKind)], policy: .never)) + case .jetpackFeatureDisabled: + completion(Timeline(entries: [.disabled(widgetKind)], policy: .never)) + } } - - return T.read()?[siteID] } } diff --git a/WordPress/WordPressTest/Widgets/WidgetDataReaderTests.swift b/WordPress/WordPressTest/Widgets/WidgetDataReaderTests.swift new file mode 100644 index 000000000000..a6fd0703830b --- /dev/null +++ b/WordPress/WordPressTest/Widgets/WidgetDataReaderTests.swift @@ -0,0 +1,178 @@ +import XCTest +@testable import WordPress + +final class WidgetDataReaderTests: XCTestCase { + func testDisabled() { + let intent = SelectSiteIntent() + intent.site = Site(identifier: nil, display: "") + let sut = makeSUT( + makeUserDefaults(suiteName: #function), + makeCacheReader(isCacheExisted: true), + isLoggedIn: true, + isJetpackDisabled: true + ) + + verifyWidgetStatus(sut, configuration: intent, defaultSiteID: nil, isJetpack: false, expectDisabled: true) + } + + func testNoSite() { + let intent = SelectSiteIntent() + intent.site = Site(identifier: nil, display: "") + let sut = makeSUT( + makeUserDefaults(suiteName: #function), + makeCacheReader(isCacheExisted: true), + isLoggedIn: true, + isJetpackDisabled: false + ) + + verifyWidgetStatus(sut, configuration: intent, defaultSiteID: nil, isJetpack: true, expectNoSite: true) + } + + func testLoggedOut() { + let intent = SelectSiteIntent() + intent.site = Site(identifier: nil, display: "") + let sut = makeSUT( + makeUserDefaults(suiteName: #function), + makeCacheReader(isCacheExisted: true), + isLoggedIn: false, + isJetpackDisabled: false + ) + + verifyWidgetStatus(sut, configuration: intent, defaultSiteID: nil, isJetpack: true, expectLoggedOut: true) + } + + func testNoDataWhenNoUserDefaults() { + let intent = SelectSiteIntent() + intent.site = Site(identifier: nil, display: "") + let sut = makeSUT( + nil, + makeCacheReader(isCacheExisted: true), + isLoggedIn: false, + isJetpackDisabled: false + ) + + verifyWidgetStatus(sut, configuration: intent, defaultSiteID: 123, isJetpack: true, expectNoData: true) + } + + func testNoDataWhenWidgetDataNotFound() { + let intent = SelectSiteIntent() + intent.site = Site(identifier: "test", display: "") + let sut = makeSUT( + makeUserDefaults(suiteName: #function), + makeCacheReader(isCacheExisted: false), + isLoggedIn: true, + isJetpackDisabled: false + ) + + verifyWidgetStatus(sut, configuration: intent, defaultSiteID: 123, isJetpack: true, expectNoData: true) + } + + func testSiteSelected() { + let intent = SelectSiteIntent() + intent.site = Site(identifier: "test", display: "") + let sut = makeSUT( + makeUserDefaults(suiteName: #function), + makeCacheReader(isCacheExisted: true), + isLoggedIn: true, + isJetpackDisabled: false + ) + + verifyWidgetStatus(sut, configuration: intent, defaultSiteID: 123, isJetpack: true, expectSiteSelected: true) + } +} + +extension WidgetDataReaderTests { + func makeSUT( + _ userDefaults: UserDefaults?, + _ cacheReader: WidgetDataCacheReader, + isLoggedIn: Bool, + isJetpackDisabled: Bool + ) -> WidgetDataReader { + userDefaults?.set(isLoggedIn, forKey: AppConfiguration.Widget.Stats.userDefaultsLoggedInKey) + userDefaults?.set(isJetpackDisabled, forKey: AppConfiguration.Widget.Stats.userDefaultsJetpackFeaturesDisabledKey) + return WidgetDataReader(userDefaults, cacheReader) + } + + func makeUserDefaults(suiteName: String) -> UserDefaults? { + let userDefaults = UserDefaults(suiteName: suiteName) + userDefaults?.removePersistentDomain(forName: suiteName) + return userDefaults + } + + func makeCacheReader(isCacheExisted: Bool) -> MockHomeWidgetDataFileReader { + MockHomeWidgetDataFileReader(isMockDataReturned: isCacheExisted) + } + + func verifyWidgetStatus( + _ sut: WidgetDataReader, + configuration: SelectSiteIntent, + defaultSiteID: Int?, + isJetpack: Bool, + expectDisabled: Bool = false, + expectNoData: Bool = false, + expectNoSite: Bool = false, + expectLoggedOut: Bool = false, + expectSiteSelected: Bool = false + ) { + let disabledExpectation = XCTestExpectation(description: "Disabled Expectation") + disabledExpectation.isInverted = !expectDisabled + let noDataExpectation = XCTestExpectation(description: "NoData Expectation") + noDataExpectation.isInverted = !expectNoData + let noSiteExpectation = XCTestExpectation(description: "NoSite Expectation") + noSiteExpectation.isInverted = !expectNoSite + let loggedOutExpectation = XCTestExpectation(description: "LoggedOut Expectation") + loggedOutExpectation.isInverted = !expectLoggedOut + let siteSelectedExpectation = XCTestExpectation(description: "NoSiteSelected Expectation") + siteSelectedExpectation.isInverted = !expectSiteSelected + + switch sut.widgetData( + for: configuration, + defaultSiteID: defaultSiteID, + isJetpack: isJetpack + ) { + case .success: + siteSelectedExpectation.fulfill() + case .failure(let error): + switch error { + case .noData: + noDataExpectation.fulfill() + case .noSite: + noSiteExpectation.fulfill() + case .loggedOut: + loggedOutExpectation.fulfill() + case .jetpackFeatureDisabled: + disabledExpectation.fulfill() + } + } + wait(for: [ + disabledExpectation, + noDataExpectation, + noSiteExpectation, + loggedOutExpectation, + siteSelectedExpectation + ], timeout: 0.1) + } +} + +struct MockHomeWidgetDataFileReader: WidgetDataCacheReader { + let mockData = HomeWidgetTodayData(siteID: 0, + siteName: "My WordPress Site", + url: "", + timeZone: TimeZone.current, + date: Date(), + stats: TodayWidgetStats( + views: 649, + visitors: 572, + likes: 16, + comments: 8 + )) + let isMockDataReturned: Bool + + func widgetData(for siteID: String) -> T? { + if isMockDataReturned { + return mockData as? T + } else { + return nil + } + } +} diff --git a/WordPress/WordPressTest/Widgets/WidgetsViewModelMapperTests.swift b/WordPress/WordPressTest/Widgets/WidgetsViewModelMapperTests.swift index 6ab12381953b..1fd74f18334f 100644 --- a/WordPress/WordPressTest/Widgets/WidgetsViewModelMapperTests.swift +++ b/WordPress/WordPressTest/Widgets/WidgetsViewModelMapperTests.swift @@ -25,18 +25,18 @@ final class WidgetsViewModelMapperTests: XCTestCase { let data = makeTodayData(stats: todayStats, date: Date()) let sut = makeSUT(data) - let statsURL = sut.getStatsURL() + let statsURL = data.statsURL XCTAssertEqual(statsURL?.absoluteString, "https://wordpress.com/stats/day/0?source=widget") } } extension WidgetsViewModelMapperTests { - func makeSUT(_ data: HomeWidgetData) -> LockScreenWidgetViewModelMapper { + func makeSUT(_ data: LockScreenStatsWidgetData) -> LockScreenWidgetViewModelMapper { LockScreenWidgetViewModelMapper(data: data) } - func makeTodayData(stats: TodayWidgetStats, date: Date) -> HomeWidgetTodayData { + func makeTodayData(stats: TodayWidgetStats, date: Date) -> LockScreenStatsWidgetData { HomeWidgetTodayData(siteID: 0, siteName: "My WordPress Site", url: "", From dd7a162b6e62f41697b54e0eae7f848fd6db25a2 Mon Sep 17 00:00:00 2001 From: Beemo Lee Date: Fri, 24 Mar 2023 21:35:32 +0800 Subject: [PATCH 07/10] Implement view for logged-out, no-site, and no-data status (#20399) * Add unconfigured view for lockscreen widget * Add unconfigured view model to view provider and model mapper * Add unconfigured view model unit test * Add background to unconfigured view align with success status widget appearance * Remove todo comments --- WordPress/WordPress.xcodeproj/project.pbxproj | 14 +++++ ...LockScreenTodayViewsStatWidgetConfig.swift | 3 +- .../LockScreenUnconfiguredViewModel.swift | 5 ++ .../LockScreenWidgetViewModelMapper.swift | 15 +++-- ...ckScreenSingleStatWidgetViewProvider.swift | 58 ++++++++++++++----- .../Views/LockScreenUnconfiguredView.swift | 41 +++++++++++++ .../Widgets/WidgetsViewModelMapperTests.swift | 17 ++++-- 7 files changed, 128 insertions(+), 25 deletions(-) create mode 100644 WordPress/WordPressStatsWidgets/LockScreenWidgets/Models/LockScreenUnconfiguredViewModel.swift create mode 100644 WordPress/WordPressStatsWidgets/LockScreenWidgets/Views/LockScreenUnconfiguredView.swift diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index 69c241842248..3c20091b3631 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -2875,6 +2875,11 @@ C9B477B229CC4949008CBF49 /* HomeWidgetDataFileReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9B477B129CC4949008CBF49 /* HomeWidgetDataFileReader.swift */; }; C9B477B329CC4949008CBF49 /* HomeWidgetDataFileReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9B477B129CC4949008CBF49 /* HomeWidgetDataFileReader.swift */; }; C9B477B429CC4949008CBF49 /* HomeWidgetDataFileReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9B477B129CC4949008CBF49 /* HomeWidgetDataFileReader.swift */; }; + C9B477B629CD2EF7008CBF49 /* LockScreenUnconfiguredView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9B477B529CD2EF7008CBF49 /* LockScreenUnconfiguredView.swift */; }; + C9B477B729CD2EF7008CBF49 /* LockScreenUnconfiguredView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9B477B529CD2EF7008CBF49 /* LockScreenUnconfiguredView.swift */; }; + C9B477B929CD2FEF008CBF49 /* LockScreenUnconfiguredViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9B477B829CD2FEE008CBF49 /* LockScreenUnconfiguredViewModel.swift */; }; + C9B477BA29CD2FEF008CBF49 /* LockScreenUnconfiguredViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9B477B829CD2FEE008CBF49 /* LockScreenUnconfiguredViewModel.swift */; }; + C9B477BB29CD576F008CBF49 /* LockScreenUnconfiguredViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9B477B829CD2FEE008CBF49 /* LockScreenUnconfiguredViewModel.swift */; }; C9C21D7729BECFC1009F68E5 /* LockScreenStatsWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9C21D7629BECFC1009F68E5 /* LockScreenStatsWidget.swift */; }; C9C21D7829BECFC7009F68E5 /* LockScreenStatsWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9C21D7629BECFC1009F68E5 /* LockScreenStatsWidget.swift */; }; C9C21D7B29BED18C009F68E5 /* LockScreenStatsWidgetsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9C21D7A29BED18C009F68E5 /* LockScreenStatsWidgetsView.swift */; }; @@ -8136,6 +8141,8 @@ C9B477AB29CC15D9008CBF49 /* WidgetDataReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WidgetDataReader.swift; sourceTree = ""; }; C9B477AF29CC35C5008CBF49 /* WidgetDataReaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetDataReaderTests.swift; sourceTree = ""; }; C9B477B129CC4949008CBF49 /* HomeWidgetDataFileReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeWidgetDataFileReader.swift; sourceTree = ""; }; + C9B477B529CD2EF7008CBF49 /* LockScreenUnconfiguredView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockScreenUnconfiguredView.swift; sourceTree = ""; }; + C9B477B829CD2FEE008CBF49 /* LockScreenUnconfiguredViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockScreenUnconfiguredViewModel.swift; sourceTree = ""; }; C9C21D7629BECFC1009F68E5 /* LockScreenStatsWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockScreenStatsWidget.swift; sourceTree = ""; }; C9C21D7A29BED18C009F68E5 /* LockScreenStatsWidgetsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockScreenStatsWidgetsView.swift; sourceTree = ""; }; C9D7DDBF2613B84500104E95 /* WordPress 119.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 119.xcdatamodel"; sourceTree = ""; }; @@ -15716,6 +15723,7 @@ children = ( C9C21D7A29BED18C009F68E5 /* LockScreenStatsWidgetsView.swift */, C9FE383029C2053300D39841 /* LockScreenSingleStatView.swift */, + C9B477B529CD2EF7008CBF49 /* LockScreenUnconfiguredView.swift */, ); path = Views; sourceTree = ""; @@ -15743,6 +15751,7 @@ C9B4778229C85948008CBF49 /* LockScreenStatsWidgetData.swift */, C9B4778329C85949008CBF49 /* LockScreenStatsWidgetEntry.swift */, C9FE382B29C204E700D39841 /* LockScreenSingleStatViewModel.swift */, + C9B477B829CD2FEE008CBF49 /* LockScreenUnconfiguredViewModel.swift */, C9FE382A29C204E700D39841 /* LockScreenWidgetViewModelMapper.swift */, ); path = Models; @@ -20537,6 +20546,7 @@ C9FE384129C2A3D200D39841 /* LockScreenTodayViewsStatWidgetConfig.swift in Sources */, 0107E16128FFE99300DE87DB /* WidgetConfiguration.swift in Sources */, 0107E0BB28F97D5000DE87DB /* StatsWidgetsService.swift in Sources */, + C9B477B729CD2EF7008CBF49 /* LockScreenUnconfiguredView.swift in Sources */, 0107E0BC28F97D5000DE87DB /* StatsWidgetsView.swift in Sources */, 0107E0BD28F97D5000DE87DB /* AppLocalizedString.swift in Sources */, 0107E0BE28F97D5000DE87DB /* MultiStatsView.swift in Sources */, @@ -20567,6 +20577,7 @@ 0107E0D028F97D5000DE87DB /* WordPressHomeWidgetThisWeek.swift in Sources */, C9FE382D29C204E700D39841 /* LockScreenWidgetViewModelMapper.swift in Sources */, C9FE384729C2A3D200D39841 /* LockScreenStatsWidgetConfig.swift in Sources */, + C9B477BA29CD2FEF008CBF49 /* LockScreenUnconfiguredViewModel.swift in Sources */, 0107E0D128F97D5000DE87DB /* SingleStatView.swift in Sources */, 0107E0D228F97D5000DE87DB /* UnconfiguredView.swift in Sources */, 0107E1852900059300DE87DB /* LocalizationConfiguration.swift in Sources */, @@ -21610,6 +21621,7 @@ D8212CB520AA68D5008E8AE8 /* ReaderSubscribingNotificationAction.swift in Sources */, FF8C54AD21F677260003ABCF /* GutenbergMediaInserterHelper.swift in Sources */, 176BA53B268266E70025E4A3 /* BlogService+Reminders.swift in Sources */, + C9B477BB29CD576F008CBF49 /* LockScreenUnconfiguredViewModel.swift in Sources */, 40C403F32215D66A00E8C894 /* TopViewedAuthorStatsRecordValue+CoreDataClass.swift in Sources */, 08A7343F298AB68000F925C7 /* JetpackPluginOverlayViewModel.swift in Sources */, E11C4B72201096EF00A6619C /* JetpackState.swift in Sources */, @@ -22414,6 +22426,7 @@ C9FE384029C2A3D200D39841 /* LockScreenTodayViewsStatWidgetConfig.swift in Sources */, 0107E16A28FFED1800DE87DB /* WidgetConfiguration.swift in Sources */, 3F2F0C16256C6B2C003351C7 /* StatsWidgetsService.swift in Sources */, + C9B477B629CD2EF7008CBF49 /* LockScreenUnconfiguredView.swift in Sources */, 3F526D572539FAC60069706C /* StatsWidgetsView.swift in Sources */, 3F2656A125AF4DFA0073A832 /* AppLocalizedString.swift in Sources */, 3F568A0025420DE80048A9E4 /* MultiStatsView.swift in Sources */, @@ -22444,6 +22457,7 @@ 3F8B138F25D09AA5004FAC0A /* WordPressHomeWidgetThisWeek.swift in Sources */, C9FE382C29C204E700D39841 /* LockScreenWidgetViewModelMapper.swift in Sources */, C9FE384629C2A3D200D39841 /* LockScreenStatsWidgetConfig.swift in Sources */, + C9B477B929CD2FEF008CBF49 /* LockScreenUnconfiguredViewModel.swift in Sources */, 3F5689F0254209790048A9E4 /* SingleStatView.swift in Sources */, 3FAA18CC25797B85002B1911 /* UnconfiguredView.swift in Sources */, 0107E1872900065500DE87DB /* LocalizationConfiguration.swift in Sources */, diff --git a/WordPress/WordPressStatsWidgets/LockScreenWidgets/Configs/LockScreenTodayViewsStatWidgetConfig.swift b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Configs/LockScreenTodayViewsStatWidgetConfig.swift index 1a257bcb1e18..c172c3bca759 100644 --- a/WordPress/WordPressStatsWidgets/LockScreenWidgets/Configs/LockScreenTodayViewsStatWidgetConfig.swift +++ b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Configs/LockScreenTodayViewsStatWidgetConfig.swift @@ -42,7 +42,8 @@ struct LockScreenTodayViewsStatWidgetConfig: LockScreenStatsWidgetConfig { var viewProvider: ViewProvider { LockScreenSingleStatWidgetViewProvider( - title: LocalizableStrings.viewsInTodayTitle + title: LocalizableStrings.viewsInTodayTitle, + widgetKind: .today ) } } diff --git a/WordPress/WordPressStatsWidgets/LockScreenWidgets/Models/LockScreenUnconfiguredViewModel.swift b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Models/LockScreenUnconfiguredViewModel.swift new file mode 100644 index 000000000000..a4368ce1ad76 --- /dev/null +++ b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Models/LockScreenUnconfiguredViewModel.swift @@ -0,0 +1,5 @@ +import Foundation + +struct LockScreenUnconfiguredViewModel { + let message: String +} diff --git a/WordPress/WordPressStatsWidgets/LockScreenWidgets/Models/LockScreenWidgetViewModelMapper.swift b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Models/LockScreenWidgetViewModelMapper.swift index 4004b9bcbcf9..ad4bd838e7b2 100644 --- a/WordPress/WordPressStatsWidgets/LockScreenWidgets/Models/LockScreenWidgetViewModelMapper.swift +++ b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Models/LockScreenWidgetViewModelMapper.swift @@ -1,24 +1,27 @@ import Foundation struct LockScreenWidgetViewModelMapper { - let data: LockScreenStatsWidgetData - func getLockScreenSingleStatViewModel( + data: LockScreenStatsWidgetData, title: String ) -> LockScreenSingleStatViewModel { LockScreenSingleStatViewModel( - siteName: getSiteName(), + siteName: getSiteName(data), title: title, - value: getViews(), + value: getViews(data), updatedTime: data.date ) } - func getSiteName() -> String { + func getLockScreenUnconfiguredViewModel(_ message: String) -> LockScreenUnconfiguredViewModel { + LockScreenUnconfiguredViewModel(message: message) + } + + private func getSiteName(_ data: LockScreenStatsWidgetData) -> String { data.siteName } - func getViews() -> String { + private func getViews(_ data: LockScreenStatsWidgetData) -> String { data.views?.abbreviatedString() ?? "" } } diff --git a/WordPress/WordPressStatsWidgets/LockScreenWidgets/ViewProvider/LockScreenSingleStatWidgetViewProvider.swift b/WordPress/WordPressStatsWidgets/LockScreenWidgets/ViewProvider/LockScreenSingleStatWidgetViewProvider.swift index cf632d803d57..3c3a2c8017a9 100644 --- a/WordPress/WordPressStatsWidgets/LockScreenWidgets/ViewProvider/LockScreenSingleStatWidgetViewProvider.swift +++ b/WordPress/WordPressStatsWidgets/LockScreenWidgets/ViewProvider/LockScreenSingleStatWidgetViewProvider.swift @@ -3,32 +3,64 @@ import SwiftUI @available(iOS 16.0, *) struct LockScreenSingleStatWidgetViewProvider: LockScreenStatsWidgetsViewProvider { typealias SiteSelectedView = LockScreenSingleStatView - typealias LoggedOutView = Text - typealias NoSiteView = Text - typealias NoDataView = Text + typealias LoggedOutView = LockScreenUnconfiguredView + typealias NoSiteView = LockScreenUnconfiguredView + typealias NoDataView = LockScreenUnconfiguredView let title: String + let widgetKind: StatsWidgetKind + let mapper = LockScreenWidgetViewModelMapper() func buildSiteSelectedView(_ data: LockScreenStatsWidgetData) -> LockScreenSingleStatView { - let mapper = LockScreenWidgetViewModelMapper(data: data) let viewModel = mapper.getLockScreenSingleStatViewModel( + data: data, title: title ) return LockScreenSingleStatView(viewModel: viewModel) } - // TODO: Build view for loggedOut status - func buildLoggedOutView() -> Text { - Text("Build Later") + func buildLoggedOutView() -> LockScreenUnconfiguredView { + var message: String { + switch widgetKind { + case .today: + return AppConfiguration.Widget.Localization.unconfiguredViewTodayTitle + case .allTime: + return AppConfiguration.Widget.Localization.unconfiguredViewAllTimeTitle + case .thisWeek: + return AppConfiguration.Widget.Localization.unconfiguredViewThisWeekTitle + } + } + let viewModel = mapper.getLockScreenUnconfiguredViewModel(message) + return LockScreenUnconfiguredView(viewModel: viewModel) } - // TODO: Build view for noSite status - func buildNoSiteView() -> Text { - Text("Build Later") + func buildNoSiteView() -> LockScreenUnconfiguredView { + var message: String { + switch widgetKind { + case .today: + return LocalizableStrings.noSiteViewTodayTitle + case .allTime: + return LocalizableStrings.noSiteViewAllTimeTitle + case .thisWeek: + return LocalizableStrings.noSiteViewThisWeekTitle + } + } + let viewModel = mapper.getLockScreenUnconfiguredViewModel(message) + return LockScreenUnconfiguredView(viewModel: viewModel) } - // TODO: Build view for noData status - func buildNoDataView() -> Text { - Text("Build Later") + func buildNoDataView() -> LockScreenUnconfiguredView { + var message: String { + switch widgetKind { + case .today: + return LocalizableStrings.noDataViewTodayTitle + case .allTime: + return LocalizableStrings.noDataViewAllTimeTitle + case .thisWeek: + return LocalizableStrings.noDataViewThisWeekTitle + } + } + let viewModel = mapper.getLockScreenUnconfiguredViewModel(message) + return LockScreenUnconfiguredView(viewModel: viewModel) } } diff --git a/WordPress/WordPressStatsWidgets/LockScreenWidgets/Views/LockScreenUnconfiguredView.swift b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Views/LockScreenUnconfiguredView.swift new file mode 100644 index 000000000000..480aeb488869 --- /dev/null +++ b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Views/LockScreenUnconfiguredView.swift @@ -0,0 +1,41 @@ +import SwiftUI +import WidgetKit + +@available(iOS 16.0, *) +struct LockScreenUnconfiguredView: View { + @Environment(\.widgetFamily) var family: WidgetFamily + let viewModel: LockScreenUnconfiguredViewModel + + var body: some View { + if family == .accessoryRectangular { + ZStack { + AccessoryWidgetBackground().cornerRadius(8) + Text(viewModel.message) + .font(.system(size: 11)) + .minimumScaleFactor(0.8) + .multilineTextAlignment(.center) + .padding( + EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8) + ) + } + } else { + Text("Not implemented for widget family \(family.debugDescription)") + } + } +} + +@available(iOS 16.0, *) +struct LockScreenUnconfiguredView_Previews: PreviewProvider { + static let viewModel = LockScreenUnconfiguredViewModel( + message: "Log in to Jetpack to see today's stats." + ) + + static var previews: some View { + LockScreenUnconfiguredView( + viewModel: LockScreenUnconfiguredView_Previews.viewModel + ) + .previewContext( + WidgetPreviewContext(family: .accessoryRectangular) + ) + } +} diff --git a/WordPress/WordPressTest/Widgets/WidgetsViewModelMapperTests.swift b/WordPress/WordPressTest/Widgets/WidgetsViewModelMapperTests.swift index 1fd74f18334f..a0395133275a 100644 --- a/WordPress/WordPressTest/Widgets/WidgetsViewModelMapperTests.swift +++ b/WordPress/WordPressTest/Widgets/WidgetsViewModelMapperTests.swift @@ -9,8 +9,9 @@ final class WidgetsViewModelMapperTests: XCTestCase { let todayStats = makeTodayWidgetStats(views: views) let data = makeTodayData(stats: todayStats, date: date) - let sut = makeSUT(data) + let sut = makeSUT() let viewModel = sut.getLockScreenSingleStatViewModel( + data: data, title: title ) @@ -23,17 +24,23 @@ final class WidgetsViewModelMapperTests: XCTestCase { func testTodayViewsStatsURL() { let todayStats = makeTodayWidgetStats(views: 649) let data = makeTodayData(stats: todayStats, date: Date()) - - let sut = makeSUT(data) let statsURL = data.statsURL XCTAssertEqual(statsURL?.absoluteString, "https://wordpress.com/stats/day/0?source=widget") } + + func testUnconfiguredViewModel() { + let sut = makeSUT() + let message = "Test" + let viewModel = sut.getLockScreenUnconfiguredViewModel(message) + + XCTAssertEqual(viewModel.message, message) + } } extension WidgetsViewModelMapperTests { - func makeSUT(_ data: LockScreenStatsWidgetData) -> LockScreenWidgetViewModelMapper { - LockScreenWidgetViewModelMapper(data: data) + func makeSUT() -> LockScreenWidgetViewModelMapper { + LockScreenWidgetViewModelMapper() } func makeTodayData(stats: TodayWidgetStats, date: Date) -> LockScreenStatsWidgetData { From 688f921285dc82e6650783dea4b6fe27c3270350 Mon Sep 17 00:00:00 2001 From: Beemo Lee Date: Mon, 27 Mar 2023 21:33:18 +0800 Subject: [PATCH 08/10] Add reload timelines for lock screen widget (#20405) --- WordPress/Classes/Stores/StatsWidgetsStore.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/WordPress/Classes/Stores/StatsWidgetsStore.swift b/WordPress/Classes/Stores/StatsWidgetsStore.swift index 2647d0de02a8..d3e7f0ae51f7 100644 --- a/WordPress/Classes/Stores/StatsWidgetsStore.swift +++ b/WordPress/Classes/Stores/StatsWidgetsStore.swift @@ -22,6 +22,7 @@ class StatsWidgetsStore { if let newTodayData = refreshStats(type: HomeWidgetTodayData.self) { HomeWidgetTodayData.write(items: newTodayData) WidgetCenter.shared.reloadTimelines(ofKind: AppConfiguration.Widget.Stats.todayKind) + WidgetCenter.shared.reloadTimelines(ofKind: AppConfiguration.Widget.Stats.lockScreenTodayViewsKind) } if let newAllTimeData = refreshStats(type: HomeWidgetAllTimeData.self) { @@ -43,6 +44,7 @@ class StatsWidgetsStore { DDLogInfo("StatsWidgets: Writing initialization data into HomeWidgetTodayData.plist") HomeWidgetTodayData.write(items: initializeHomeWidgetData(type: HomeWidgetTodayData.self)) WidgetCenter.shared.reloadTimelines(ofKind: AppConfiguration.Widget.Stats.todayKind) + WidgetCenter.shared.reloadTimelines(ofKind: AppConfiguration.Widget.Stats.lockScreenTodayViewsKind) } if !HomeWidgetThisWeekData.cacheDataExists() { @@ -115,6 +117,9 @@ class StatsWidgetsStore { T.write(items: homeWidgetCache) WidgetCenter.shared.reloadTimelines(ofKind: widgetKind) + if widgetKind == AppConfiguration.Widget.Stats.todayKind { + WidgetCenter.shared.reloadTimelines(ofKind: AppConfiguration.Widget.Stats.lockScreenTodayViewsKind) + } } } @@ -273,6 +278,7 @@ private extension StatsWidgetsStore { WidgetCenter.shared.reloadTimelines(ofKind: AppConfiguration.Widget.Stats.todayKind) WidgetCenter.shared.reloadTimelines(ofKind: AppConfiguration.Widget.Stats.thisWeekKind) WidgetCenter.shared.reloadTimelines(ofKind: AppConfiguration.Widget.Stats.allTimeKind) + WidgetCenter.shared.reloadTimelines(ofKind: AppConfiguration.Widget.Stats.lockScreenTodayViewsKind) } } } From ed4ab983fbfa94fce3dcf1ce3151da678e622899 Mon Sep 17 00:00:00 2001 From: Beemo Lee Date: Wed, 29 Mar 2023 18:32:19 +0800 Subject: [PATCH 09/10] Implement event tracking for the new lock screen widget (#20422) * Add event key to configuration and pass to trackers in lockscreen widget Expand tracks for lockscreen stats entry Add rectangular widgets count and widgets update of the new widget * Add lockscreen_widget source to deeplink tracked Update statsUrl to widgetUrl for separating the url from homescreen widgets * Update unitTests * Remove separate widget url, move widget source from widget data extension Add widget source type, assign the type in widgetURL in the view * Add widget url source test to verify the converting with DeepLinkSource --- .../WidgetConfiguration.swift | 1 + .../Utility/Universal Links/Route.swift | 3 ++ .../Universal Links/UniversalLinkRouter.swift | 2 ++ WordPress/Jetpack/WidgetConfiguration.swift | 1 + WordPress/WordPress.xcodeproj/project.pbxproj | 12 ++++++++ .../Extensions/HomeWidgetData+StatsURL.swift | 6 ++-- .../Extensions/URL+WidgetSource.swift | 17 +++++++++++ .../Configs/LockScreenStatsWidgetConfig.swift | 1 + ...LockScreenTodayViewsStatWidgetConfig.swift | 4 +++ .../LockScreenStatsWidget.swift | 6 ++++ .../Views/LockScreenStatsWidgetsView.swift | 2 +- .../Tracks/Tracks+StatsWidgets.swift | 23 ++++++++++++++- .../Views/StatsWidgetsView.swift | 6 ++-- .../Widgets/WidgetUrlSourceTests.swift | 28 +++++++++++++++++++ .../Widgets/WidgetsViewModelMapperTests.swift | 2 +- 15 files changed, 105 insertions(+), 9 deletions(-) create mode 100644 WordPress/WordPressStatsWidgets/Extensions/URL+WidgetSource.swift create mode 100644 WordPress/WordPressTest/Widgets/WidgetUrlSourceTests.swift diff --git a/WordPress/Classes/Utility/App Configuration/WidgetConfiguration.swift b/WordPress/Classes/Utility/App Configuration/WidgetConfiguration.swift index b6c143f6c56a..e3efd5194abf 100644 --- a/WordPress/Classes/Utility/App Configuration/WidgetConfiguration.swift +++ b/WordPress/Classes/Utility/App Configuration/WidgetConfiguration.swift @@ -20,6 +20,7 @@ import Foundation @objc static let todayKind = "WordPressHomeWidgetToday" @objc static let allTimeKind = "WordPressHomeWidgetAllTime" @objc static let thisWeekKind = "WordPressHomeWidgetThisWeek" + @objc static let lockScreenTodayViewsProperties = "WordPressLockScreenWidgetTodayViewsProperties" @objc static let todayProperties = "WordPressHomeWidgetTodayProperties" @objc static let allTimeProperties = "WordPressHomeWidgetAllTimeProperties" @objc static let thisWeekProperties = "WordPressHomeWidgetThisWeekProperties" diff --git a/WordPress/Classes/Utility/Universal Links/Route.swift b/WordPress/Classes/Utility/Universal Links/Route.swift index adde5cb10e06..a17391cca58e 100644 --- a/WordPress/Classes/Utility/Universal Links/Route.swift +++ b/WordPress/Classes/Utility/Universal Links/Route.swift @@ -94,6 +94,7 @@ enum DeepLinkSource: Equatable { case banner case email(campaign: String) case widget + case lockScreenWidget case inApp(presenter: UIViewController?) init?(sourceName: String) { @@ -102,6 +103,8 @@ enum DeepLinkSource: Equatable { // add others in the future if necessary. case "widget": self = .widget + case "lockscreen_widget": + self = .lockScreenWidget default: return nil } diff --git a/WordPress/Classes/Utility/Universal Links/UniversalLinkRouter.swift b/WordPress/Classes/Utility/Universal Links/UniversalLinkRouter.swift index 08a6e35002f1..594863496027 100644 --- a/WordPress/Classes/Utility/Universal Links/UniversalLinkRouter.swift +++ b/WordPress/Classes/Utility/Universal Links/UniversalLinkRouter.swift @@ -243,6 +243,8 @@ extension DeepLinkSource { return "email" case .widget: return "widget" + case .lockScreenWidget: + return "lockscreen_widget" case .inApp: return "internal" } diff --git a/WordPress/Jetpack/WidgetConfiguration.swift b/WordPress/Jetpack/WidgetConfiguration.swift index d562e881b66e..a159f8c7fd9e 100644 --- a/WordPress/Jetpack/WidgetConfiguration.swift +++ b/WordPress/Jetpack/WidgetConfiguration.swift @@ -20,6 +20,7 @@ import Foundation @objc static let todayKind = "JetpackHomeWidgetToday" @objc static let allTimeKind = "JetpackHomeWidgetAllTime" @objc static let thisWeekKind = "JetpackHomeWidgetThisWeek" + @objc static let lockScreenTodayViewsProperties = "JetpackLockScreenWidgetTodayViewsProperties" @objc static let todayProperties = "JetpackHomeWidgetTodayProperties" @objc static let allTimeProperties = "JetpackHomeWidgetAllTimeProperties" @objc static let thisWeekProperties = "JetpackHomeWidgetThisWeekProperties" diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index 3c20091b3631..024efdb2748b 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -2857,6 +2857,10 @@ C94C0B1B25DCFA0100F2F69B /* FilterableCategoriesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C94C0B1A25DCFA0100F2F69B /* FilterableCategoriesViewController.swift */; }; C957C20626DCC1770037628F /* LandInTheEditorHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C957C20526DCC1770037628F /* LandInTheEditorHelper.swift */; }; C957C20726DCC1770037628F /* LandInTheEditorHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C957C20526DCC1770037628F /* LandInTheEditorHelper.swift */; }; + C995C22229D306DD00ACEF43 /* URL+WidgetSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = C995C22129D306DD00ACEF43 /* URL+WidgetSource.swift */; }; + C995C22329D306E100ACEF43 /* URL+WidgetSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = C995C22129D306DD00ACEF43 /* URL+WidgetSource.swift */; }; + C995C22429D30A9900ACEF43 /* URL+WidgetSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = C995C22129D306DD00ACEF43 /* URL+WidgetSource.swift */; }; + C995C22629D30AB000ACEF43 /* WidgetUrlSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C995C22529D30AB000ACEF43 /* WidgetUrlSourceTests.swift */; }; C99B08FC26081AD600CA71EB /* TemplatePreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C99B08FB26081AD600CA71EB /* TemplatePreviewViewController.swift */; }; C9B4778429C85949008CBF49 /* LockScreenStatsWidgetData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9B4778229C85948008CBF49 /* LockScreenStatsWidgetData.swift */; }; C9B4778529C85949008CBF49 /* LockScreenStatsWidgetData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9B4778229C85948008CBF49 /* LockScreenStatsWidgetData.swift */; }; @@ -8132,6 +8136,8 @@ C9264D275F6288F66C33D2CE /* Pods-WordPressTest.release-internal.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WordPressTest.release-internal.xcconfig"; path = "../Pods/Target Support Files/Pods-WordPressTest/Pods-WordPressTest.release-internal.xcconfig"; sourceTree = ""; }; C94C0B1A25DCFA0100F2F69B /* FilterableCategoriesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterableCategoriesViewController.swift; sourceTree = ""; }; C957C20526DCC1770037628F /* LandInTheEditorHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LandInTheEditorHelper.swift; sourceTree = ""; }; + C995C22129D306DD00ACEF43 /* URL+WidgetSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+WidgetSource.swift"; sourceTree = ""; }; + C995C22529D30AB000ACEF43 /* WidgetUrlSourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetUrlSourceTests.swift; sourceTree = ""; }; C99B039B2602F3CB00CA71EB /* WordPress 117.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 117.xcdatamodel"; sourceTree = ""; }; C99B08FB26081AD600CA71EB /* TemplatePreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplatePreviewViewController.swift; sourceTree = ""; }; C9B4778229C85948008CBF49 /* LockScreenStatsWidgetData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockScreenStatsWidgetData.swift; sourceTree = ""; }; @@ -15733,6 +15739,7 @@ children = ( C9FE382129C2040600D39841 /* HomeWidgetData+StatsURL.swift */, C9B4778829C85956008CBF49 /* HomeWidgetData+LockScreenStatsWidgetData.swift */, + C995C22129D306DD00ACEF43 /* URL+WidgetSource.swift */, ); path = Extensions; sourceTree = ""; @@ -15762,6 +15769,7 @@ children = ( C9FE383629C2067E00D39841 /* WidgetsViewModelMapperTests.swift */, C9B477AF29CC35C5008CBF49 /* WidgetDataReaderTests.swift */, + C995C22529D30AB000ACEF43 /* WidgetUrlSourceTests.swift */, ); path = Widgets; sourceTree = ""; @@ -20532,6 +20540,7 @@ files = ( C9B4778729C85949008CBF49 /* LockScreenStatsWidgetEntry.swift in Sources */, 0107E0B428F97D5000DE87DB /* Constants.m in Sources */, + C995C22329D306E100ACEF43 /* URL+WidgetSource.swift in Sources */, 01CE5012290A890B00A9C2E0 /* TracksConfiguration.swift in Sources */, C9C21D7C29BED18C009F68E5 /* LockScreenStatsWidgetsView.swift in Sources */, C9FE382F29C204E700D39841 /* LockScreenSingleStatViewModel.swift in Sources */, @@ -22010,6 +22019,7 @@ 010459E629153FFF000C7778 /* JetpackNotificationMigrationService.swift in Sources */, FAC1B82729B1F1EE00E0C542 /* BlazePostPreviewView.swift in Sources */, E61084C21B9B47BA008050C5 /* ReaderTagTopic.swift in Sources */, + C995C22429D30A9900ACEF43 /* URL+WidgetSource.swift in Sources */, F110239B2318479000C4E84A /* Media.swift in Sources */, 176CE91627FB44C100F1E32B /* StatsBaseCell.swift in Sources */, E1C2260723901AAD0021D03C /* WordPressOrgRestApi+WordPress.swift in Sources */, @@ -22412,6 +22422,7 @@ files = ( C9B4778629C85949008CBF49 /* LockScreenStatsWidgetEntry.swift in Sources */, 3F1FD30D2548B0A80060C53A /* Constants.m in Sources */, + C995C22229D306DD00ACEF43 /* URL+WidgetSource.swift in Sources */, 01CE500C290A88BF00A9C2E0 /* TracksConfiguration.swift in Sources */, C9C21D7B29BED18C009F68E5 /* LockScreenStatsWidgetsView.swift in Sources */, C9FE382E29C204E700D39841 /* LockScreenSingleStatViewModel.swift in Sources */, @@ -23215,6 +23226,7 @@ DC13DB7E293FD09F00E33561 /* StatsInsightsStoreTests.swift in Sources */, ACACE3AE28D729FA000992F9 /* NoResultsViewControllerTests.swift in Sources */, 8BFE36FF230F1C850061EBA8 /* AbstractPost+fixLocalMediaURLsTests.swift in Sources */, + C995C22629D30AB000ACEF43 /* WidgetUrlSourceTests.swift in Sources */, 08A2AD791CCED2A800E84454 /* PostTagServiceTests.m in Sources */, F543AF5723A84E4D0022F595 /* PublishSettingsControllerTests.swift in Sources */, 027AC5212278983F0033E56E /* DomainCreditEligibilityTests.swift in Sources */, diff --git a/WordPress/WordPressStatsWidgets/Extensions/HomeWidgetData+StatsURL.swift b/WordPress/WordPressStatsWidgets/Extensions/HomeWidgetData+StatsURL.swift index 58ac96e28fcf..f15993413451 100644 --- a/WordPress/WordPressStatsWidgets/Extensions/HomeWidgetData+StatsURL.swift +++ b/WordPress/WordPressStatsWidgets/Extensions/HomeWidgetData+StatsURL.swift @@ -4,7 +4,7 @@ extension HomeWidgetTodayData { static let statsUrl = "https://wordpress.com/stats/day/" var statsURL: URL? { - URL(string: Self.statsUrl + "\(siteID)?source=widget") + URL(string: Self.statsUrl + "\(siteID)") } } @@ -12,7 +12,7 @@ extension HomeWidgetAllTimeData { static let statsUrl = "https://wordpress.com/stats/insights/" var statsURL: URL? { - URL(string: Self.statsUrl + "\(siteID)?source=widget") + URL(string: Self.statsUrl + "\(siteID)") } } @@ -20,6 +20,6 @@ extension HomeWidgetThisWeekData { static let statsUrl = "https://wordpress.com/stats/week/" var statsURL: URL? { - URL(string: Self.statsUrl + "\(siteID)?source=widget") + URL(string: Self.statsUrl + "\(siteID)") } } diff --git a/WordPress/WordPressStatsWidgets/Extensions/URL+WidgetSource.swift b/WordPress/WordPressStatsWidgets/Extensions/URL+WidgetSource.swift new file mode 100644 index 000000000000..071ce5966836 --- /dev/null +++ b/WordPress/WordPressStatsWidgets/Extensions/URL+WidgetSource.swift @@ -0,0 +1,17 @@ +import Foundation + +enum WidgetUrlSource: String { + case homeScreenWidget = "widget" + case lockScreenWidget = "lockscreen_widget" +} + +extension URL { + func appendingSource(_ source: WidgetUrlSource) -> URL { + var components = URLComponents(url: self, resolvingAgainstBaseURL: false) + var queryItems = components?.queryItems ?? [] + let sourceQuery = URLQueryItem(name: "source", value: source.rawValue) + queryItems.append(sourceQuery) + components?.queryItems = queryItems + return components?.url ?? self + } +} diff --git a/WordPress/WordPressStatsWidgets/LockScreenWidgets/Configs/LockScreenStatsWidgetConfig.swift b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Configs/LockScreenStatsWidgetConfig.swift index 8bd2bd84760f..97c63e4f18e1 100644 --- a/WordPress/WordPressStatsWidgets/LockScreenWidgets/Configs/LockScreenStatsWidgetConfig.swift +++ b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Configs/LockScreenStatsWidgetConfig.swift @@ -8,6 +8,7 @@ protocol LockScreenStatsWidgetConfig { var displayName: String { get } var description: String { get } var kind: String { get } + var countKey: String { get } var placeholderContent: WidgetData { get } var viewProvider: ViewProvider { get } } diff --git a/WordPress/WordPressStatsWidgets/LockScreenWidgets/Configs/LockScreenTodayViewsStatWidgetConfig.swift b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Configs/LockScreenTodayViewsStatWidgetConfig.swift index c172c3bca759..09eb74f1fc7b 100644 --- a/WordPress/WordPressStatsWidgets/LockScreenWidgets/Configs/LockScreenTodayViewsStatWidgetConfig.swift +++ b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Configs/LockScreenTodayViewsStatWidgetConfig.swift @@ -24,6 +24,10 @@ struct LockScreenTodayViewsStatWidgetConfig: LockScreenStatsWidgetConfig { AppConfiguration.Widget.Stats.lockScreenTodayViewsKind } + var countKey: String { + AppConfiguration.Widget.Stats.lockScreenTodayViewsProperties + } + var placeholderContent: HomeWidgetTodayData { HomeWidgetTodayData( siteID: 0, diff --git a/WordPress/WordPressStatsWidgets/LockScreenWidgets/LockScreenStatsWidget.swift b/WordPress/WordPressStatsWidgets/LockScreenWidgets/LockScreenStatsWidget.swift index 8229eadacd77..bb17dc109898 100644 --- a/WordPress/WordPressStatsWidgets/LockScreenWidgets/LockScreenStatsWidget.swift +++ b/WordPress/WordPressStatsWidgets/LockScreenWidgets/LockScreenStatsWidget.swift @@ -3,6 +3,7 @@ import SwiftUI @available(iOS 16.0, *) struct LockScreenStatsWidget: Widget { + private let tracks = Tracks(appGroupName: WPAppGroupName) private let config: T init(config: T) { @@ -23,6 +24,11 @@ struct LockScreenStatsWidget: Widget { placeholderContent: config.placeholderContent ) ) { (entry: LockScreenStatsWidgetEntry) -> LockScreenStatsWidgetsView in + defer { + tracks.trackWidgetUpdatedIfNeeded(entry: entry, + widgetKind: config.kind, + widgetCountKey: config.countKey) + } return LockScreenStatsWidgetsView( timelineEntry: entry, viewProvider: config.viewProvider diff --git a/WordPress/WordPressStatsWidgets/LockScreenWidgets/Views/LockScreenStatsWidgetsView.swift b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Views/LockScreenStatsWidgetsView.swift index 04c17426be0d..4d3b21e45b4d 100644 --- a/WordPress/WordPressStatsWidgets/LockScreenWidgets/Views/LockScreenStatsWidgetsView.swift +++ b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Views/LockScreenStatsWidgetsView.swift @@ -30,7 +30,7 @@ struct LockScreenStatsWidgetsView: View { case let .siteSelected(data, _): viewProvider .buildSiteSelectedView(data) - .widgetURL(data.statsURL) + .widgetURL(data.statsURL?.appendingSource(.lockScreenWidget)) case .loggedOut: viewProvider .buildLoggedOutView() diff --git a/WordPress/WordPressStatsWidgets/Tracks/Tracks+StatsWidgets.swift b/WordPress/WordPressStatsWidgets/Tracks/Tracks+StatsWidgets.swift index 1545b2597209..59f7c50d2efa 100644 --- a/WordPress/WordPressStatsWidgets/Tracks/Tracks+StatsWidgets.swift +++ b/WordPress/WordPressStatsWidgets/Tracks/Tracks+StatsWidgets.swift @@ -5,6 +5,20 @@ import WidgetKit /// extension Tracks { + func trackWidgetUpdatedIfNeeded(entry: LockScreenStatsWidgetEntry, widgetKind: String, widgetCountKey: String) { + switch entry { + case .siteSelected(_, let context): + if !context.isPreview { + trackWidgetUpdated(widgetKind: widgetKind, + widgetCountKey: widgetCountKey) + } + + case .loggedOut, .noSite, .noData: + trackWidgetUpdated(widgetKind: widgetKind, + widgetCountKey: widgetCountKey) + } + } + func trackWidgetUpdatedIfNeeded(entry: StatsWidgetEntry, widgetKind: String, widgetCountKey: String) { switch entry { case .siteSelected(_, let context): @@ -39,10 +53,13 @@ extension Tracks { private func trackUpdatedWidgetInfo(widgetInfo: [WidgetInfo], widgetPropertiesKey: String) { - let properties = ["total_widgets": widgetInfo.count, + var properties = ["total_widgets": widgetInfo.count, "small_widgets": widgetInfo.filter { $0.family == .systemSmall }.count, "medium_widgets": widgetInfo.filter { $0.family == .systemMedium }.count, "large_widgets": widgetInfo.filter { $0.family == .systemLarge }.count] + if #available(iOS 16.0, *) { + properties["rectangular_widgets"] = widgetInfo.filter { $0.family == .accessoryRectangular }.count + } let previousProperties = UserDefaults(suiteName: WPAppGroupName)?.object(forKey: widgetPropertiesKey) as? [String: Int] @@ -71,6 +88,8 @@ extension Tracks { case allTimeWidgetUpdated = "alltime_home_extension_widget_updated" // Users installs an instance of the this week widget case thisWeekWidgetUpdated = "thisweek_home_extension_widget_updated" + // Users installs an instance of the lockscreen today views widget + case todayViewsLockScreenWidgetUpdated = "today_views_lockscreen_extension_widget_updated" case noEvent @@ -82,6 +101,8 @@ extension Tracks { return .allTimeWidgetUpdated case AppConfiguration.Widget.Stats.thisWeekProperties: return .thisWeekWidgetUpdated + case AppConfiguration.Widget.Stats.lockScreenTodayViewsProperties: + return .todayViewsLockScreenWidgetUpdated default: return .noEvent } diff --git a/WordPress/WordPressStatsWidgets/Views/StatsWidgetsView.swift b/WordPress/WordPressStatsWidgets/Views/StatsWidgetsView.swift index 8a66d364f71b..d7f16d5a46ed 100644 --- a/WordPress/WordPressStatsWidgets/Views/StatsWidgetsView.swift +++ b/WordPress/WordPressStatsWidgets/Views/StatsWidgetsView.swift @@ -13,7 +13,7 @@ struct StatsWidgetsView: View { case .disabled(let kind): UnconfiguredView(timelineEntry: timelineEntry) - .widgetURL(kind.statsURL) + .widgetURL(kind.statsURL?.appendingSource(.homeScreenWidget)) case .loggedOut, .noSite, .noData: UnconfiguredView(timelineEntry: timelineEntry) .widgetURL(nil) @@ -25,12 +25,12 @@ struct StatsWidgetsView: View { case .systemSmall: SingleStatView(viewData: viewData) - .widgetURL(viewData.statsURL) + .widgetURL(viewData.statsURL?.appendingSource(.homeScreenWidget)) .padding() case .systemMedium: MultiStatsView(viewData: viewData) - .widgetURL(viewData.statsURL) + .widgetURL(viewData.statsURL?.appendingSource(.homeScreenWidget)) .padding() default: diff --git a/WordPress/WordPressTest/Widgets/WidgetUrlSourceTests.swift b/WordPress/WordPressTest/Widgets/WidgetUrlSourceTests.swift new file mode 100644 index 000000000000..c416acfa2868 --- /dev/null +++ b/WordPress/WordPressTest/Widgets/WidgetUrlSourceTests.swift @@ -0,0 +1,28 @@ +import XCTest +@testable import WordPress + +final class WidgetUrlSourceTests: XCTestCase { + func testHomeScreenWidgetSource() throws { + let url = try XCTUnwrap(URL(string: "https://test")) + let widgetUrl = url.appendingSource(.homeScreenWidget) + XCTAssertEqual(widgetUrl.absoluteString, "https://test?source=widget") + } + + func testLockScreenWidgetSource() throws { + let url = try XCTUnwrap(URL(string: "https://test")) + let widgetUrl = url.appendingSource(.lockScreenWidget) + XCTAssertEqual(widgetUrl.absoluteString, "https://test?source=lockscreen_widget") + } + + func testHomeScreenWidgetSourceType() { + let source = WidgetUrlSource.homeScreenWidget.rawValue + let deepLinkSource = DeepLinkSource(sourceName: source) + XCTAssertEqual(deepLinkSource, .widget) + } + + func testLockScreenWidgetSourceType() { + let source = WidgetUrlSource.lockScreenWidget.rawValue + let deepLinkSource = DeepLinkSource(sourceName: source) + XCTAssertEqual(deepLinkSource, .lockScreenWidget) + } +} diff --git a/WordPress/WordPressTest/Widgets/WidgetsViewModelMapperTests.swift b/WordPress/WordPressTest/Widgets/WidgetsViewModelMapperTests.swift index a0395133275a..8a8b9a2da6b3 100644 --- a/WordPress/WordPressTest/Widgets/WidgetsViewModelMapperTests.swift +++ b/WordPress/WordPressTest/Widgets/WidgetsViewModelMapperTests.swift @@ -26,7 +26,7 @@ final class WidgetsViewModelMapperTests: XCTestCase { let data = makeTodayData(stats: todayStats, date: Date()) let statsURL = data.statsURL - XCTAssertEqual(statsURL?.absoluteString, "https://wordpress.com/stats/day/0?source=widget") + XCTAssertEqual(statsURL?.absoluteString, "https://wordpress.com/stats/day/0") } func testUnconfiguredViewModel() { From 0fce1387783b7b188e4e7f89c84c9957956b4fc8 Mon Sep 17 00:00:00 2001 From: Beemo Lee Date: Thu, 30 Mar 2023 22:14:11 +0800 Subject: [PATCH 10/10] Disable the feature flag (#20427) --- WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift b/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift index 038d80d24b04..53bbe0848b28 100644 --- a/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift +++ b/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift @@ -76,7 +76,7 @@ enum FeatureFlag: Int, CaseIterable, OverrideableFlag { case .todayWidget: return true case .lockScreenWidget: - return true + return false case .milestoneNotifications: return true case .bloggingReminders: