Skip to content

Commit 5e34f47

Browse files
authored
Merge pull request #7779 from woocommerce/issue/71-woo-version-in-settings
[Settings] Show WooCommerce version and available updates in settings
2 parents 3c57e6d + 1c72a3a commit 5e34f47

File tree

8 files changed

+302
-17
lines changed

8 files changed

+302
-17
lines changed

RELEASE-NOTES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
- [**] Products tab: products search now has an option to search products by SKU. Stores with WC version 6.6+ support partial SKU search, otherwise the product(s) with the exact SKU match is returned. [https://github.com/woocommerce/woocommerce-ios/pull/7781]
77
- [*] Fixed a rare crash when selecting a store in the store picker. [https://github.com/woocommerce/woocommerce-ios/pull/7765]
8+
- [*] Settings: Display the WooCommerce version and available updates in Settings [https://github.com/woocommerce/woocommerce-ios/pull/7779]
89
- [*] Show suggestion for logging in to a WP.com site with a mismatched WP.com account. [https://github.com/woocommerce/woocommerce-ios/pull/7773]
910
- [*] Help center: Added help center web page with FAQs for "Not a WooCommerce site" and "Wrong WordPress.com account" error screens. [https://github.com/woocommerce/woocommerce-ios/pull/7767, https://github.com/woocommerce/woocommerce-ios/pull/7769]
1011

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import SwiftUI
2+
import Yosemite
3+
4+
struct PluginDetailsRowView: View {
5+
@ObservedObject var viewModel: PluginDetailsViewModel
6+
7+
@State var webViewPresented = false
8+
9+
var body: some View {
10+
NavigationRow(selectable: viewModel.updateURL != nil,
11+
content: {
12+
PluginDetailsRowContent(viewModel: viewModel)
13+
.sheet(isPresented: $webViewPresented,
14+
onDismiss: {
15+
viewModel.refreshPlugin()
16+
}) {
17+
if let updateURL = viewModel.updateURL {
18+
SafariView(url: updateURL)
19+
}
20+
}
21+
},
22+
action: { webViewPresented.toggle() })
23+
}
24+
}
25+
26+
struct PluginDetailsRowContent: View {
27+
@ObservedObject var viewModel: PluginDetailsViewModel
28+
29+
var body: some View {
30+
HStack {
31+
VStack {
32+
HStack {
33+
Text(viewModel.title)
34+
.bodyStyle()
35+
Spacer()
36+
Text(viewModel.version)
37+
.secondaryBodyStyle()
38+
}
39+
.padding([.bottom], 2)
40+
41+
if viewModel.updateAvailable {
42+
PluginDetailsRowUpdateAvailable(versionLatest: viewModel.versionLatest)
43+
} else {
44+
PluginDetailsRowUpToDate()
45+
}
46+
}
47+
}
48+
}
49+
50+
}
51+
52+
struct PluginDetailsRowUpdateAvailable: View {
53+
@State var versionLatest: String?
54+
55+
var body: some View {
56+
HStack {
57+
Image(systemName: Constants.softwareUpdateSymbolName)
58+
Text(Localization.updateAvailableTitle)
59+
Spacer()
60+
if let versionLatest = versionLatest {
61+
Text(versionLatest)
62+
}
63+
}
64+
.font(.footnote)
65+
.foregroundColor(Color(.warning))
66+
}
67+
}
68+
69+
struct PluginDetailsRowUpToDate: View {
70+
var body: some View {
71+
HStack {
72+
Image(systemName: Constants.upToDateSymbolName)
73+
Text(Localization.upToDateTitle)
74+
Spacer()
75+
}
76+
.font(.footnote)
77+
.foregroundColor(Color(UIColor.systemGreen))
78+
}
79+
}
80+
81+
private enum Constants {
82+
static let softwareUpdateSymbolName = "exclamationmark.arrow.triangle.2.circlepath"
83+
static let upToDateSymbolName = "checkmark.circle"
84+
}
85+
86+
private enum Localization {
87+
static let updateAvailableTitle = NSLocalizedString(
88+
"Update available",
89+
comment: "String shown to indicate the latest version of a plugin when an " +
90+
"update is available and highlighted to the user")
91+
static let upToDateTitle = NSLocalizedString(
92+
"Up to date",
93+
comment: "String shown to indicate the latest version of a plugin when an " +
94+
"update is available and highlighted to the user")
95+
}
96+
97+
98+
struct PluginDetailsRowView_Previews: PreviewProvider {
99+
private static func viewModel(
100+
version: String,
101+
versionLatest: String) -> PluginDetailsViewModel {
102+
let viewModel = PluginDetailsViewModel(
103+
siteID: 0,
104+
pluginName: "WooCommerce")
105+
viewModel.plugin = SystemPlugin(siteID: 0,
106+
plugin: "",
107+
name: "",
108+
version: version,
109+
versionLatest: versionLatest,
110+
url: "",
111+
authorName: "",
112+
authorUrl: "",
113+
networkActivated: false,
114+
active: true)
115+
viewModel.updateURL = URL(string: "https://woocommerce.com")!
116+
return viewModel
117+
}
118+
119+
static var previews: some View {
120+
Group {
121+
PluginDetailsRowView(viewModel: viewModel(version: "6.8.0", versionLatest: "6.11.0"))
122+
.previewLayout(.fixed(width: 375, height: 100))
123+
PluginDetailsRowView(viewModel: viewModel(version: "6.11.0", versionLatest: "6.11.0"))
124+
.previewLayout(.fixed(width: 375, height: 100))
125+
}
126+
}
127+
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import Foundation
2+
import Yosemite
3+
import protocol Storage.StorageManagerType
4+
5+
final class PluginDetailsViewModel: ObservableObject {
6+
/// ID of the site to load plugins for
7+
///
8+
private let siteID: Int64
9+
10+
/// Name of the plugin to show details for
11+
///
12+
private let pluginName: String
13+
14+
/// Reference to the StoresManager to dispatch Yosemite Actions.
15+
///
16+
private let storesManager: StoresManager
17+
18+
/// StorageManager to load plugins from storage
19+
///
20+
private let storageManager: StorageManagerType
21+
22+
/// Results controller for the plugin list
23+
///
24+
private lazy var resultsController: ResultsController<StorageSystemPlugin> = {
25+
let predicate = NSPredicate(format: "siteID = %ld AND name = %@", self.siteID, pluginName)
26+
let resultsController = ResultsController<StorageSystemPlugin>(
27+
storageManager: storageManager,
28+
matching: predicate,
29+
sortedBy: []
30+
)
31+
32+
do {
33+
try resultsController.performFetch()
34+
plugin = resultsController.fetchedObjects.first
35+
} catch {
36+
DDLogError("⛔️ Error fetching WooCommerce plugin details!")
37+
}
38+
return resultsController
39+
}()
40+
41+
/// Title for the plugin details row
42+
///
43+
let title: String
44+
45+
var updateAvailable: Bool {
46+
guard let plugin = plugin else {
47+
return false
48+
}
49+
return !VersionHelpers.isVersionSupported(version: plugin.version, minimumRequired: plugin.versionLatest)
50+
}
51+
52+
/// URL for the plugins page in WP-admin, used for the update webview when an update is available
53+
///
54+
@Published var updateURL: URL?
55+
56+
/// Version of the plugin installed on the current site
57+
///
58+
@Published var version: String
59+
60+
/// Latest version of the plugin installed on the current site
61+
///
62+
@Published var versionLatest: String?
63+
64+
var plugin: SystemPlugin? {
65+
didSet {
66+
version = plugin?.version ?? Localization.unknownVersionValue
67+
versionLatest = plugin?.versionLatest
68+
updateURL = updateURL(for: plugin)
69+
}
70+
}
71+
72+
init(siteID: Int64,
73+
pluginName: String,
74+
storesManager: StoresManager = ServiceLocator.stores,
75+
storageManager: StorageManagerType = ServiceLocator.storageManager) {
76+
self.siteID = siteID
77+
self.pluginName = pluginName
78+
self.storesManager = storesManager
79+
self.storageManager = storageManager
80+
self.title = String(format: Localization.pluginDetailTitle, pluginName)
81+
self.plugin = nil
82+
self.updateURL = nil
83+
self.version = ""
84+
self.versionLatest = nil
85+
observePlugin { self.plugin = self.resultsController.fetchedObjects.first }
86+
}
87+
88+
/// Start fetching and observing plugin data from local storage.
89+
///
90+
private func observePlugin(onDataChanged: @escaping () -> Void) {
91+
resultsController.onDidChangeContent = onDataChanged
92+
}
93+
94+
/// Used to refresh the store after the webview is used to perform an update
95+
///
96+
func refreshPlugin() {
97+
let action = SystemStatusAction.synchronizeSystemPlugins(siteID: siteID) { _ in }
98+
storesManager.dispatch(action)
99+
}
100+
}
101+
102+
private extension PluginDetailsViewModel {
103+
private func updateURL(for plugin: SystemPlugin?) -> URL? {
104+
guard let url = storesManager.sessionManager.defaultSite?.pluginsURL,
105+
updateAvailable(for: plugin)
106+
else {
107+
return nil
108+
}
109+
110+
return URL(string: url)
111+
}
112+
113+
private func updateAvailable(for plugin: SystemPlugin?) -> Bool {
114+
guard let plugin = plugin else {
115+
return false
116+
}
117+
return !VersionHelpers.isVersionSupported(version: plugin.version, minimumRequired: plugin.versionLatest)
118+
}
119+
120+
}
121+
122+
private enum Localization {
123+
static let pluginDetailTitle = NSLocalizedString(
124+
"%1$@ Version",
125+
comment: "Title for the plugin version detail row in settings. %1$@ is a placeholder for the plugin name. " +
126+
"This is displayed with the current version number, and whether an update is available.")
127+
128+
static let unknownVersionValue = NSLocalizedString(
129+
"unknown",
130+
comment: "Value for the WooCommerce plugin version detail row in settings, when the version is unknown. " +
131+
"This is in place of the current version number.")
132+
}

WooCommerce/Classes/ViewRelated/Dashboard/Settings/Settings/SettingsViewController.swift

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ final class SettingsViewController: UIViewController {
1717

1818
private let viewModel: ViewModel
1919

20+
private lazy var woocommercePluginViewModel: PluginDetailsViewModel = PluginDetailsViewModel(
21+
siteID: stores.sessionManager.defaultStoreID ?? 0,
22+
pluginName: "WooCommerce")
23+
2024
/// Main TableView
2125
///
2226
@IBOutlet private weak var tableView: UITableView!
@@ -128,6 +132,8 @@ private extension SettingsViewController {
128132
configureSwitchStore(cell: cell)
129133
case let cell as BasicTableViewCell where row == .plugins:
130134
configurePlugins(cell: cell)
135+
case let cell as HostingTableViewCell<PluginDetailsRowView> where row == .woocommerceDetails:
136+
configureWooCommmerceDetails(cell: cell)
131137
case let cell as HostingTableViewCell<FeatureAnnouncementCardView> where row == .upsellCardReadersFeatureAnnouncement:
132138
configureUpsellCardReadersFeatureAnnouncement(cell: cell)
133139
case let cell as BasicTableViewCell where row == .installJetpack:
@@ -173,6 +179,12 @@ private extension SettingsViewController {
173179
cell.textLabel?.text = Localization.plugins
174180
}
175181

182+
func configureWooCommmerceDetails(cell: HostingTableViewCell<PluginDetailsRowView>) {
183+
let view = PluginDetailsRowView.init(viewModel: woocommercePluginViewModel)
184+
cell.host(view, parent: self)
185+
cell.selectionStyle = .none
186+
}
187+
176188
func configureSupport(cell: BasicTableViewCell) {
177189
cell.accessoryType = .disclosureIndicator
178190
cell.selectionStyle = .default
@@ -586,6 +598,7 @@ extension SettingsViewController {
586598

587599
// Plugins
588600
case plugins
601+
case woocommerceDetails
589602

590603
// Store settings
591604
case upsellCardReadersFeatureAnnouncement
@@ -615,7 +628,7 @@ extension SettingsViewController {
615628

616629
fileprivate var registerWithNib: Bool {
617630
switch self {
618-
case .upsellCardReadersFeatureAnnouncement:
631+
case .upsellCardReadersFeatureAnnouncement, .woocommerceDetails:
619632
return false
620633
default:
621634
return true
@@ -630,6 +643,8 @@ extension SettingsViewController {
630643
return BasicTableViewCell.self
631644
case .plugins:
632645
return BasicTableViewCell.self
646+
case .woocommerceDetails:
647+
return HostingTableViewCell<PluginDetailsRowView>.self
633648
case .support:
634649
return BasicTableViewCell.self
635650
case .upsellCardReadersFeatureAnnouncement:

WooCommerce/Classes/ViewRelated/Dashboard/Settings/Settings/SettingsViewModel.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,13 @@ final class SettingsViewModel: SettingsViewModelOutput, SettingsViewModelActions
134134
} else {
135135
paymentGatewayAccountsResultsController = nil
136136
}
137+
138+
/// Synchronize system plugins for the WooCommerce plugin version row
139+
///
140+
if let siteID = stores.sessionManager.defaultSite?.siteID {
141+
let action = SystemStatusAction.synchronizeSystemPlugins(siteID: siteID, onCompletion: { _ in })
142+
stores.dispatch(action)
143+
}
137144
}
138145

139146
/// Sets up the view model and loads the settings.
@@ -204,7 +211,7 @@ private extension SettingsViewModel {
204211
}
205212

206213
return Section(title: Localization.pluginsTitle,
207-
rows: [.plugins],
214+
rows: [.plugins, .woocommerceDetails],
208215
footerHeight: UITableView.automaticDimension)
209216
}()
210217

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,27 @@
11
import SwiftUI
22

33
struct DisclosureIndicator: View {
4-
/// Keeps track of the current screen scale
5-
///
6-
@ScaledMetric private var scale = 1
7-
84
var body: some View {
9-
Image(uiImage: .chevronImage)
10-
.resizable()
11-
.flipsForRightToLeftLayoutDirection(true)
12-
.frame(width: Constants.chevronSize(scale: scale), height: Constants.chevronSize(scale: scale))
13-
.foregroundColor(Color(.systemGray))
5+
Image(systemName: Constants.chevronSymbolName)
6+
.font(.system(size: Constants.chevronSize, weight: .bold))
7+
.foregroundColor(Color(UIColor.systemGray3))
148
.accessibility(hidden: true)
159
}
1610
}
1711

1812
// MARK: Constants
1913
private extension DisclosureIndicator {
2014
enum Constants {
21-
static func chevronSize(scale: CGFloat) -> CGFloat {
22-
22 * scale
23-
}
15+
static let chevronSize: CGFloat = 14.0
16+
static let chevronSymbolName = "chevron.forward"
2417
}
2518
}
2619

2720
struct DisclosureIndicator_Previews: PreviewProvider {
2821
static var previews: some View {
29-
DisclosureIndicator()
30-
.previewLayout(.sizeThatFits)
22+
Group {
23+
DisclosureIndicator()
24+
.previewLayout(.sizeThatFits)
25+
}
3126
}
3227
}

0 commit comments

Comments
 (0)