diff --git a/WordPress/Classes/Stores/StatsWidgetsStore.swift b/WordPress/Classes/Stores/StatsWidgetsStore.swift index d9532becf851..c3abea6fd42e 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) + } } } @@ -276,6 +281,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) } /// Observes WPSigninDidFinishNotification and wordpressLoginFinishedJetpackLogin notifications and initializes the widget. diff --git a/WordPress/Classes/Utility/App Configuration/WidgetConfiguration.swift b/WordPress/Classes/Utility/App Configuration/WidgetConfiguration.swift index 486199aaa2fd..e3efd5194abf 100644 --- a/WordPress/Classes/Utility/App Configuration/WidgetConfiguration.swift +++ b/WordPress/Classes/Utility/App Configuration/WidgetConfiguration.swift @@ -16,9 +16,11 @@ 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" + @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/BuildInformation/FeatureFlag.swift b/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift index 625e48db635a..f391dbacba29 100644 --- a/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift +++ b/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift @@ -10,6 +10,7 @@ enum FeatureFlag: Int, CaseIterable { case homepageSettings case unifiedPrologueCarousel case todayWidget + case lockScreenWidget case milestoneNotifications case bloggingReminders case siteIconCreator @@ -68,6 +69,8 @@ enum FeatureFlag: Int, CaseIterable { return true case .todayWidget: return true + case .lockScreenWidget: + return false case .milestoneNotifications: return true case .bloggingReminders: @@ -178,6 +181,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: @@ -262,6 +267,8 @@ extension FeatureFlag: OverridableFlag { return false case .todayWidget: return false + case .lockScreenWidget: + return false case .weeklyRoundup: return false case .weeklyRoundupStaticNotification: 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 a67ea50a7c40..a1ed4e922c46 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 d3ec31711525..a159f8c7fd9e 100644 --- a/WordPress/Jetpack/WidgetConfiguration.swift +++ b/WordPress/Jetpack/WidgetConfiguration.swift @@ -16,9 +16,11 @@ 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" + @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 20b967a5dbf7..b7f4dfae209a 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -3025,11 +3025,59 @@ 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 */; }; + 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 */; }; + 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 */; }; + 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 */; }; 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 */; }; + 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 */; }; @@ -8346,11 +8394,32 @@ 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 = ""; }; + 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 = ""; }; + 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 = ""; }; 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 = ""; }; + 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 = ""; }; @@ -11475,6 +11544,9 @@ children = ( 3F8EEC4D25B4817000EC9DAE /* StatsWidgets.swift */, 3F8EEC6F25B4849A00EC9DAE /* SiteListProvider.swift */, + C9C21D7529BECFAE009F68E5 /* LockScreenWidgets */, + C9FE382029C203EE00D39841 /* Extensions */, + C9B477AA29CC15C2008CBF49 /* Helpers */, 3FFDDCB925B8A65F008D5BDD /* Widgets */, 3FB34ABB25672A59001A74A6 /* Model */, 3FFDDC0325B89F0C008D5BDD /* Cache */, @@ -16116,6 +16188,87 @@ 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 */, + C9FE382529C204A500D39841 /* ViewProvider */, + ); + path = LockScreenWidgets; + sourceTree = ""; + }; + C9C21D8829BF4998009F68E5 /* Views */ = { + isa = PBXGroup; + children = ( + C9C21D7A29BED18C009F68E5 /* LockScreenStatsWidgetsView.swift */, + C9FE383029C2053300D39841 /* LockScreenSingleStatView.swift */, + C9B477B529CD2EF7008CBF49 /* LockScreenUnconfiguredView.swift */, + ); + path = Views; + sourceTree = ""; + }; + C9FE382029C203EE00D39841 /* Extensions */ = { + isa = PBXGroup; + children = ( + C9FE382129C2040600D39841 /* HomeWidgetData+StatsURL.swift */, + C9B4778829C85956008CBF49 /* HomeWidgetData+LockScreenStatsWidgetData.swift */, + C995C22129D306DD00ACEF43 /* URL+WidgetSource.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + C9FE382529C204A500D39841 /* ViewProvider */ = { + isa = PBXGroup; + children = ( + C9FE382629C204C100D39841 /* LockScreenSingleStatWidgetViewProvider.swift */, + ); + path = ViewProvider; + sourceTree = ""; + }; + C9FE382929C204D700D39841 /* Models */ = { + isa = PBXGroup; + children = ( + C9B4778229C85948008CBF49 /* LockScreenStatsWidgetData.swift */, + C9B4778329C85949008CBF49 /* LockScreenStatsWidgetEntry.swift */, + C9FE382B29C204E700D39841 /* LockScreenSingleStatViewModel.swift */, + C9B477B829CD2FEE008CBF49 /* LockScreenUnconfiguredViewModel.swift */, + C9FE382A29C204E700D39841 /* LockScreenWidgetViewModelMapper.swift */, + ); + path = Models; + sourceTree = ""; + }; + C9FE383329C2063900D39841 /* Widgets */ = { + isa = PBXGroup; + children = ( + C9FE383629C2067E00D39841 /* WidgetsViewModelMapperTests.swift */, + C9B477AF29CC35C5008CBF49 /* WidgetDataReaderTests.swift */, + C995C22529D30AB000ACEF43 /* WidgetUrlSourceTests.swift */, + ); + path = Widgets; + sourceTree = ""; + }; + C9FE383B29C2A3A300D39841 /* Configs */ = { + isa = PBXGroup; + children = ( + C9FE383F29C2A3D200D39841 /* LockScreenStatsWidgetConfig.swift */, + C9FE383C29C2A3D100D39841 /* LockScreenTodayViewsStatWidgetConfig.swift */, + ); + path = Configs; + sourceTree = ""; + }; CC098B8116A9EB0400450976 /* HTML */ = { isa = PBXGroup; children = ( @@ -16505,6 +16658,7 @@ 852416D01A12ED2D0030700C /* Utility */, BE20F5E11B2F738E0020694C /* ViewRelated */, 3F3D8548251E63DF001CA4D2 /* What's New */, + C9FE383329C2063900D39841 /* Widgets */, FF9839A71CD3960600E85258 /* WordPressAPI */, ); name = Tests; @@ -20888,8 +21042,12 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; 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 */, 0107E0B528F97D5000DE87DB /* StatsWidgetEntry.swift in Sources */, 0107E0B628F97D5000DE87DB /* HomeWidgetCache.swift in Sources */, 0107E18C29000E2A00DE87DB /* AppStyleGuide.swift in Sources */, @@ -20897,8 +21055,11 @@ 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 */, + C9B477B729CD2EF7008CBF49 /* LockScreenUnconfiguredView.swift in Sources */, 0107E0BC28F97D5000DE87DB /* StatsWidgetsView.swift in Sources */, 0107E0BD28F97D5000DE87DB /* AppLocalizedString.swift in Sources */, 0107E0BE28F97D5000DE87DB /* MultiStatsView.swift in Sources */, @@ -20907,30 +21068,41 @@ 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 */, 0107E0C728F97D5000DE87DB /* StatsWidgets.swift in Sources */, 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 */, + 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 */, + 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 */, 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 */, + 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 */, @@ -21308,6 +21480,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 */, @@ -21348,6 +21521,7 @@ 98AA9F2127EA890800B3A98C /* FeatureIntroductionViewController.swift in Sources */, BE1071FC1BC75E7400906AFF /* WPStyleGuide+Blog.swift in Sources */, B56695B01D411EEB007E342F /* KeyboardDismissHelper.swift in Sources */, + C9B477B229CC4949008CBF49 /* HomeWidgetDataFileReader.swift in Sources */, 4A2C73F42A95856000ACE79E /* PostRepository.swift in Sources */, F5B9D7F0245BA938002BB2C7 /* FancyAlertViewController+CreateButtonAnnouncement.swift in Sources */, FF54D4641D6F3FA900A0DC4D /* GutenbergSettings.swift in Sources */, @@ -21492,6 +21666,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 */, @@ -21627,7 +21802,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 */, @@ -21996,6 +22173,7 @@ D8212CB520AA68D5008E8AE8 /* ReaderSubscribingNotificationAction.swift in Sources */, FF8C54AD21F677260003ABCF /* GutenbergMediaInserterHelper.swift in Sources */, 176BA53B268266E70025E4A3 /* BlogService+Reminders.swift in Sources */, + C9B477BB29CD576F008CBF49 /* LockScreenUnconfiguredViewModel.swift in Sources */, 08A7343F298AB68000F925C7 /* JetpackPluginOverlayViewModel.swift in Sources */, E11C4B72201096EF00A6619C /* JetpackState.swift in Sources */, 437542E31DD4E19E00D6B727 /* EditPostViewController.swift in Sources */, @@ -22394,6 +22572,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 */, @@ -22671,6 +22850,7 @@ B50EED791C0E5B2400D278CA /* SettingsPickerViewController.swift in Sources */, 0C8FC9A72A8BFAAE0059DCE4 /* NSItemProvider+Exportable.swift 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 */, @@ -22726,6 +22906,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 */, @@ -22803,8 +22984,12 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; 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 */, 3F63B93C258179D100F581BE /* StatsWidgetEntry.swift in Sources */, 3FA53E9D256571D800F4D9A2 /* HomeWidgetCache.swift in Sources */, 0107E18D29000E3300DE87DB /* AppStyleGuide.swift in Sources */, @@ -22812,8 +22997,11 @@ 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 */, + C9B477B629CD2EF7008CBF49 /* LockScreenUnconfiguredView.swift in Sources */, 3F526D572539FAC60069706C /* StatsWidgetsView.swift in Sources */, 3F2656A125AF4DFA0073A832 /* AppLocalizedString.swift in Sources */, 3F568A0025420DE80048A9E4 /* MultiStatsView.swift in Sources */, @@ -22822,30 +23010,41 @@ 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 */, 3FA59B9A258289E30073772F /* StatsValueView.swift in Sources */, 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 */, + 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 */, + 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 */, 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 */, + 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 */, @@ -23530,6 +23729,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 */, @@ -23603,12 +23803,14 @@ 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 */, ACACE3AE28D729FA000992F9 /* NoResultsViewControllerTests.swift in Sources */, 4A2C73E42A943DEA00ACE79E /* TaggedManagedObjectIDTests.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+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 new file mode 100644 index 000000000000..f15993413451 --- /dev/null +++ b/WordPress/WordPressStatsWidgets/Extensions/HomeWidgetData+StatsURL.swift @@ -0,0 +1,25 @@ +import Foundation + +extension HomeWidgetTodayData { + static let statsUrl = "https://wordpress.com/stats/day/" + + var statsURL: URL? { + URL(string: Self.statsUrl + "\(siteID)") + } +} + +extension HomeWidgetAllTimeData { + static let statsUrl = "https://wordpress.com/stats/insights/" + + var statsURL: URL? { + URL(string: Self.statsUrl + "\(siteID)") + } +} + +extension HomeWidgetThisWeekData { + static let statsUrl = "https://wordpress.com/stats/week/" + + var statsURL: URL? { + 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/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 new file mode 100644 index 000000000000..97c63e4f18e1 --- /dev/null +++ b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Configs/LockScreenStatsWidgetConfig.swift @@ -0,0 +1,14 @@ +import WidgetKit + +protocol LockScreenStatsWidgetConfig { + associatedtype WidgetData: HomeWidgetData & LockScreenStatsWidgetData + associatedtype ViewProvider: LockScreenStatsWidgetsViewProvider + + var supportFamilies: [WidgetFamily] { get } + 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 new file mode 100644 index 000000000000..09eb74f1fc7b --- /dev/null +++ b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Configs/LockScreenTodayViewsStatWidgetConfig.swift @@ -0,0 +1,53 @@ +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 countKey: String { + AppConfiguration.Widget.Stats.lockScreenTodayViewsProperties + } + + 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, + widgetKind: .today + ) + } +} 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 new file mode 100644 index 000000000000..bb17dc109898 --- /dev/null +++ b/WordPress/WordPressStatsWidgets/LockScreenWidgets/LockScreenStatsWidget.swift @@ -0,0 +1,41 @@ +import WidgetKit +import SwiftUI + +@available(iOS 16.0, *) +struct LockScreenStatsWidget: Widget { + private let tracks = Tracks(appGroupName: WPAppGroupName) + 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: config.kind, + intent: SelectSiteIntent.self, + provider: LockScreenSiteListProvider( + service: StatsWidgetsService(), + 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 + ) + } + .configurationDisplayName(config.displayName) + .description(config.description) + .supportedFamilies(config.supportFamilies) + } +} diff --git a/WordPress/WordPressStatsWidgets/LockScreenWidgets/Models/LockScreenSingleStatViewModel.swift b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Models/LockScreenSingleStatViewModel.swift new file mode 100644 index 000000000000..b5295e421e01 --- /dev/null +++ b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Models/LockScreenSingleStatViewModel.swift @@ -0,0 +1,8 @@ +import Foundation + +struct LockScreenSingleStatViewModel { + let siteName: String + let title: String + let value: String + let updatedTime: Date +} 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/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 new file mode 100644 index 000000000000..ad4bd838e7b2 --- /dev/null +++ b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Models/LockScreenWidgetViewModelMapper.swift @@ -0,0 +1,27 @@ +import Foundation + +struct LockScreenWidgetViewModelMapper { + func getLockScreenSingleStatViewModel( + data: LockScreenStatsWidgetData, + title: String + ) -> LockScreenSingleStatViewModel { + LockScreenSingleStatViewModel( + siteName: getSiteName(data), + title: title, + value: getViews(data), + updatedTime: data.date + ) + } + + func getLockScreenUnconfiguredViewModel(_ message: String) -> LockScreenUnconfiguredViewModel { + LockScreenUnconfiguredViewModel(message: message) + } + + private func getSiteName(_ data: LockScreenStatsWidgetData) -> String { + data.siteName + } + + 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 new file mode 100644 index 000000000000..3c3a2c8017a9 --- /dev/null +++ b/WordPress/WordPressStatsWidgets/LockScreenWidgets/ViewProvider/LockScreenSingleStatWidgetViewProvider.swift @@ -0,0 +1,66 @@ +import SwiftUI + +@available(iOS 16.0, *) +struct LockScreenSingleStatWidgetViewProvider: LockScreenStatsWidgetsViewProvider { + typealias SiteSelectedView = LockScreenSingleStatView + typealias LoggedOutView = LockScreenUnconfiguredView + typealias NoSiteView = LockScreenUnconfiguredView + typealias NoDataView = LockScreenUnconfiguredView + + let title: String + let widgetKind: StatsWidgetKind + let mapper = LockScreenWidgetViewModelMapper() + + func buildSiteSelectedView(_ data: LockScreenStatsWidgetData) -> LockScreenSingleStatView { + let viewModel = mapper.getLockScreenSingleStatViewModel( + data: data, + title: title + ) + return LockScreenSingleStatView(viewModel: viewModel) + } + + 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) + } + + 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) + } + + 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/LockScreenSingleStatView.swift b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Views/LockScreenSingleStatView.swift new file mode 100644 index 000000000000..d29f3020278c --- /dev/null +++ b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Views/LockScreenSingleStatView.swift @@ -0,0 +1,56 @@ +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) + .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 Today", + value: "649", + 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 new file mode 100644 index 000000000000..4d3b21e45b4d --- /dev/null +++ b/WordPress/WordPressStatsWidgets/LockScreenWidgets/Views/LockScreenStatsWidgetsView.swift @@ -0,0 +1,48 @@ +import SwiftUI +import WidgetKit + +protocol LockScreenStatsWidgetsViewProvider { + associatedtype SiteSelectedView: View + associatedtype LoggedOutView: View + associatedtype NoSiteView: View + associatedtype NoDataView: View + + @ViewBuilder + func buildSiteSelectedView(_ data: LockScreenStatsWidgetData) -> SiteSelectedView + + @ViewBuilder + func buildLoggedOutView() -> LoggedOutView + + @ViewBuilder + func buildNoSiteView() -> NoSiteView + + @ViewBuilder + func buildNoDataView() -> NoDataView +} + +struct LockScreenStatsWidgetsView: View { + let timelineEntry: LockScreenStatsWidgetEntry + let viewProvider: T + + @ViewBuilder + var body: some View { + switch timelineEntry { + case let .siteSelected(data, _): + viewProvider + .buildSiteSelectedView(data) + .widgetURL(data.statsURL?.appendingSource(.lockScreenWidget)) + case .loggedOut: + viewProvider + .buildLoggedOutView() + .widgetURL(nil) + case .noSite: + viewProvider + .buildNoSiteView() + .widgetURL(nil) + case .noData: + viewProvider + .buildNoDataView() + .widgetURL(nil) + } + } +} 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/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/WordPressStatsWidgets/StatsWidgets.swift b/WordPress/WordPressStatsWidgets/StatsWidgets.swift index 748b8aad201d..d415471dd090 100644 --- a/WordPress/WordPressStatsWidgets/StatsWidgets.swift +++ b/WordPress/WordPressStatsWidgets/StatsWidgets.swift @@ -7,5 +7,8 @@ struct WordPressStatsWidgets: WidgetBundle { WordPressHomeWidgetToday() WordPressHomeWidgetThisWeek() WordPressHomeWidgetAllTime() + if #available(iOS 16.0, *) { + LockScreenStatsWidget(config: LockScreenTodayViewsStatWidgetConfig()) + } } } 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/Localization/LocalizableStrings.swift b/WordPress/WordPressStatsWidgets/Views/Localization/LocalizableStrings.swift index 4784056083a7..719250f13ab5 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 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") + // Widgets content static let viewsTitle = AppLocalizedString("widget.today.views.label", value: "Views", diff --git a/WordPress/WordPressStatsWidgets/Views/StatsWidgetsView.swift b/WordPress/WordPressStatsWidgets/Views/StatsWidgetsView.swift index 7ff90e846eae..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: @@ -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/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/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 new file mode 100644 index 000000000000..8a8b9a2da6b3 --- /dev/null +++ b/WordPress/WordPressTest/Widgets/WidgetsViewModelMapperTests.swift @@ -0,0 +1,61 @@ +import XCTest +@testable import WordPress + +final class WidgetsViewModelMapperTests: XCTestCase { + func testSingleStatViewModel() { + let views = 649875 + let date = Date() + let title = "Views Today" + let todayStats = makeTodayWidgetStats(views: views) + let data = makeTodayData(stats: todayStats, date: date) + + let sut = makeSUT() + let viewModel = sut.getLockScreenSingleStatViewModel( + data: data, + title: title + ) + + XCTAssertEqual(viewModel.siteName, data.siteName) + XCTAssertEqual(viewModel.title, title) + XCTAssertEqual(viewModel.value, views.abbreviatedString()) + XCTAssertEqual(viewModel.updatedTime, date) + } + + func testTodayViewsStatsURL() { + let todayStats = makeTodayWidgetStats(views: 649) + let data = makeTodayData(stats: todayStats, date: Date()) + let statsURL = data.statsURL + + XCTAssertEqual(statsURL?.absoluteString, "https://wordpress.com/stats/day/0") + } + + func testUnconfiguredViewModel() { + let sut = makeSUT() + let message = "Test" + let viewModel = sut.getLockScreenUnconfiguredViewModel(message) + + XCTAssertEqual(viewModel.message, message) + } +} + +extension WidgetsViewModelMapperTests { + func makeSUT() -> LockScreenWidgetViewModelMapper { + LockScreenWidgetViewModelMapper() + } + + func makeTodayData(stats: TodayWidgetStats, date: Date) -> LockScreenStatsWidgetData { + 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) + } +}