Skip to content

Commit 887104e

Browse files
authored
Merge branch 'trunk' into woomob-1339-schema-many-to-many-images
2 parents 08ba3a6 + 892e677 commit 887104e

File tree

20 files changed

+292
-30
lines changed

20 files changed

+292
-30
lines changed

Modules/Tests/YosemiteTests/Model/Extensions/OrderStatsV4Interval+DateTests.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ final class OrderStatsV4Interval_DateTests: XCTestCase {
1111
dateEnd: dateStringInSiteTimeZone,
1212
subtotals: mockIntervalSubtotals)
1313
// As long as the dates are parsed and formatted in the same time zone, they should be consistent.
14-
let timeZone = TimeZone(secondsFromGMT: 29302)!
14+
// Note that iOS 26 has changed behaviour here, and excess seconds in the timezone break the calculation
15+
// This is 488 minutes, or 8 hours and 8 minutes
16+
let timeZone = TimeZone(secondsFromGMT: 29280)!
1517
[interval.dateStart(timeZone: timeZone), interval.dateEnd(timeZone: timeZone)].forEach { date in
1618
let dateComponents = Calendar(identifier: .iso8601).dateComponents(in: timeZone, from: date)
1719
XCTAssertEqual(dateComponents.year, 2019)

Modules/Tests/YosemiteTests/Tools/Media/MediaTypeTests.swift

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,12 @@ final class MediaTypeTests: XCTestCase {
2424

2525
func testInitWithAudioFileExtensions() throws {
2626
try XCTSkipIf(testingOnRosetta())
27-
// Note: "ogg" isn't supported on iOS.
28-
let audioFileExtensions = ["mp3", "m4a", "wav"]
27+
var audioFileExtensions = ["mp3", "m4a", "wav"]
28+
// Note: "ogg" is only supported on iOS 26 and above.
29+
if #available(iOS 26.0, *) {
30+
audioFileExtensions.append("ogg")
31+
}
32+
2933
audioFileExtensions.forEach { fileExtension in
3034
XCTAssertEqual(MediaType(fileExtension: fileExtension), .audio, "Unexpected media type for file extension: \(fileExtension)")
3135
}
@@ -41,11 +45,16 @@ final class MediaTypeTests: XCTestCase {
4145

4246
func testInitWithOtherFileExtensions() throws {
4347
try XCTSkipIf(testingOnRosetta())
44-
let presentationFileExtensions = [
48+
var presentationFileExtensions = [
4549
"pdf", "doc", "odt", "xls", "xlsx",
46-
// Audio/video formats that are not supported on iOS
47-
"ogg", "ogv"
50+
// Video formats that are not supported on iOS
51+
"ogv"
4852
]
53+
54+
// Note: "ogg" is only supported on iOS 26 and above.
55+
if #unavailable(iOS 26.0) {
56+
presentationFileExtensions.append("ogg")
57+
}
4958
presentationFileExtensions.forEach { fileExtension in
5059
XCTAssertEqual(MediaType(fileExtension: fileExtension), .other, "Unexpected media type for file extension: \(fileExtension)")
5160
}
@@ -80,7 +89,11 @@ final class MediaTypeTests: XCTestCase {
8089

8190
func testInitWithAudioMimeType() throws {
8291
try XCTSkipIf(testingOnRosetta())
83-
let audioMimeTypes = ["audio/midi", "audio/x-midi", "audio/mpeg", "audio/wav"]
92+
var audioMimeTypes = ["audio/midi", "audio/x-midi", "audio/mpeg", "audio/wav"]
93+
// Note: "ogg" is only supported on iOS 26 and above.
94+
if #available(iOS 26.0, *) {
95+
audioMimeTypes.append("audio/ogg")
96+
}
8497
audioMimeTypes.forEach { mimeType in
8598
XCTAssertEqual(MediaType(mimeType: mimeType), .audio, "Unexpected media type for file extension: \(mimeType)")
8699
}
@@ -96,14 +109,19 @@ final class MediaTypeTests: XCTestCase {
96109

97110
func testInitWithOtherMimeType() throws {
98111
try XCTSkipIf(testingOnRosetta())
99-
let otherMimeTypes = [
112+
var otherMimeTypes = [
100113
"application/pdf", "application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
101114
"application/vnd.oasis.opendocument.text", "application/vnd.ms-excel", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
102115
// Video formats that are not supported on iOS
103116
"video/ogg", "video/mp2t", "video/webm",
104117
// Audio formats that are not supported on iOS
105-
"audio/ogg", "audio/opus", "audio/webm"
118+
"audio/opus", "audio/webm"
106119
]
120+
121+
// Note: "ogg" is only supported on iOS 26 and above.
122+
if #unavailable(iOS 26.0) {
123+
otherMimeTypes.append("audio/ogg")
124+
}
107125
otherMimeTypes.forEach { mimeType in
108126
XCTAssertEqual(MediaType(mimeType: mimeType), .other, "Unexpected media type for file extension: \(mimeType)")
109127
}

RELEASE-NOTES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
23.5
55
-----
66
- [internal] POS code was moved to its own module, including POS specific color and image assets. [https://github.com/woocommerce/woocommerce-ios/pull/16176]
7+
- [*] Fix Create Coupon screen scrolling issue. [https://github.com/woocommerce/woocommerce-ios/pull/16218]
78

89

910
23.4

WooCommerce.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

WooCommerce/Classes/Bookings/BookingList/BookingListContainerView.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import struct Yosemite.Booking
33

44
struct BookingListContainerView: View {
55
@ObservedObject private var viewModel: BookingListContainerViewModel
6+
@ScaledMetric private var scale: CGFloat = 1.0
67
@Binding var selectedBooking: Booking?
78

89
init(viewModel: BookingListContainerViewModel, selectedBooking: Binding<Booking?>) {
@@ -82,18 +83,19 @@ private extension BookingListContainerView {
8283
.padding(.vertical, 12)
8384
}
8485
}
86+
.frame(height: Layout.topTabBarHeight * scale)
8587
.overlay(alignment: .bottom) {
8688
Color.accentColor
8789
.frame(width: geometry.size.width / CGFloat(BookingListTab.allCases.count),
88-
height: Layout.selectedTabIndicatorHeight)
90+
height: Layout.selectedTabIndicatorHeight * scale)
8991
.offset(x: tabIndicatorOffset(containerWidth: geometry.size.width,
9092
tabCount: BookingListTab.allCases.count,
9193
selectedIndex: viewModel.selectedTab.rawValue),
92-
y: -Layout.selectedTabIndicatorHeight / 2)
94+
y: Layout.selectedTabIndicatorHeight * scale / 2)
9395
.animation(.easeInOut(duration: 0.3), value: viewModel.selectedTab.rawValue)
9496
}
9597
}
96-
.frame(height: Layout.topTabBarHeight)
98+
.frame(height: Layout.topTabBarHeight * scale)
9799
.background(Color(.listForeground(modal: false)))
98100
}
99101

WooCommerce/Classes/Bookings/BookingList/BookingListContainerViewModel.swift

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,34 @@ enum BookingListTab: Int, CaseIterable {
6161
}
6262
}
6363

64+
func emptyStateTitle(hasFilters: Bool) -> String {
65+
guard !hasFilters else {
66+
return Localization.EmptyState.filterTitle
67+
}
68+
switch self {
69+
case .today:
70+
return Localization.EmptyState.todayTitle
71+
case .upcoming:
72+
return Localization.EmptyState.upcomingTitle
73+
case .all:
74+
return Localization.EmptyState.filterTitle
75+
}
76+
}
77+
78+
func emptyStateDescription(hasFilters: Bool) -> String {
79+
guard !hasFilters else {
80+
return Localization.EmptyState.filterDescription
81+
}
82+
switch self {
83+
case .today:
84+
return Localization.EmptyState.todayDescription
85+
case .upcoming:
86+
return Localization.EmptyState.upcomingDescription
87+
case .all:
88+
return ""
89+
}
90+
}
91+
6492
private enum Localization {
6593
static let today = NSLocalizedString(
6694
"bookingListView.today",
@@ -77,5 +105,37 @@ enum BookingListTab: Int, CaseIterable {
77105
value: "All",
78106
comment: "Tab title for all bookings"
79107
)
108+
enum EmptyState {
109+
static let todayTitle = NSLocalizedString(
110+
"bookingListView.emptyState.today.title",
111+
value: "No bookings today",
112+
comment: "Title for the empty state when no bookings for today is found"
113+
)
114+
static let todayDescription = NSLocalizedString(
115+
"bookingListView.emptyState.today.description",
116+
value: "You don't have any appointments or events scheduled for today.",
117+
comment: "Description for the empty state when no bookings for today is found"
118+
)
119+
static let upcomingTitle = NSLocalizedString(
120+
"bookingListView.emptyState.upcoming.title",
121+
value: "No upcoming bookings",
122+
comment: "Title for the empty state when there's no bookings for today"
123+
)
124+
static let upcomingDescription = NSLocalizedString(
125+
"bookingListView.emptyState.upcoming.description",
126+
value: "You don't have any future appointments or events scheduled yet.",
127+
comment: "Description for the empty state when there's no upcoming bookings"
128+
)
129+
static let filterTitle = NSLocalizedString(
130+
"bookingListView.emptyState.filter.title",
131+
value: "No bookings found",
132+
comment: "Title for the empty state when there's no bookings for the given filter"
133+
)
134+
static let filterDescription = NSLocalizedString(
135+
"bookingListView.emptyState.filter.description",
136+
value: "No bookings match your filters. Try adjusting them to see more results.",
137+
comment: "Description for the empty state when there's no bookings for the given filter"
138+
)
139+
}
80140
}
81141
}

WooCommerce/Classes/Bookings/BookingList/BookingListView.swift

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import struct Yosemite.Booking
33

44
struct BookingListView: View {
55
@ObservedObject private var viewModel: BookingListViewModel
6+
@StateObject private var connectivityMonitor = ConnectivityMonitor()
7+
@ScaledMetric private var scale: CGFloat = 1.0
68
@Binding var selectedBooking: Booking?
79

810
init(viewModel: BookingListViewModel, selectedBooking: Binding<Booking?>) {
@@ -14,9 +16,7 @@ struct BookingListView: View {
1416
VStack {
1517
switch viewModel.syncState {
1618
case .empty:
17-
Spacer()
18-
Text("No bookings found") // TODO: update this in WOOMOB-1394
19-
Spacer()
19+
emptyStateView
2020
case .syncingFirstPage:
2121
Spacer()
2222
ProgressView().progressViewStyle(.circular)
@@ -28,6 +28,12 @@ struct BookingListView: View {
2828
.task {
2929
viewModel.loadBookings()
3030
}
31+
.overlay(alignment: .bottom) {
32+
if viewModel.errorFetching {
33+
errorSnackBar
34+
.transition(.move(edge: .bottom))
35+
}
36+
}
3137
}
3238
}
3339

@@ -46,6 +52,7 @@ private extension BookingListView {
4652
}
4753
}
4854
.listStyle(.plain)
55+
.background(Color(.listBackground))
4956
.accentColor(Color(.listSelectedBackground))
5057
.refreshable {
5158
await viewModel.onRefreshAction()
@@ -88,11 +95,85 @@ private extension BookingListView {
8895
.padding(.vertical, 4)
8996
.background(color.clipShape(RoundedRectangle(cornerRadius: 4)))
9097
}
98+
99+
var emptyStateView: some View {
100+
GeometryReader { proxy in
101+
ScrollView {
102+
VStack(spacing: Layout.emptyStatePadding) {
103+
Spacer()
104+
Image(uiImage: .noBookings)
105+
.resizable()
106+
.aspectRatio(contentMode: .fit)
107+
.frame(width: Layout.emptyStateImageWidth * scale)
108+
.padding(.bottom, Layout.viewPadding)
109+
VStack(spacing: Layout.textVerticalPadding) {
110+
Text(viewModel.emptyStateTitle)
111+
.font(.title2)
112+
.fontWeight(.semibold)
113+
.foregroundStyle(.primary)
114+
Text(viewModel.emptyStateDescription)
115+
.font(.title3)
116+
.foregroundStyle(.secondary)
117+
}
118+
if viewModel.hasFilters {
119+
VStack(spacing: Layout.textVerticalPadding) {
120+
Button("Change filters") {
121+
// TODO
122+
}
123+
.buttonStyle(PrimaryButtonStyle())
124+
Button("Clear filters") {
125+
// TODO
126+
}
127+
.buttonStyle(SecondaryButtonStyle())
128+
}
129+
}
130+
Spacer()
131+
}
132+
.multilineTextAlignment(.center)
133+
.padding(.horizontal, Layout.emptyStatePadding)
134+
.frame(minWidth: proxy.size.width, minHeight: proxy.size.height)
135+
}
136+
.refreshable {
137+
await viewModel.onRefreshAction()
138+
}
139+
}
140+
}
141+
142+
var errorSnackBar: some View {
143+
Text(Localization.errorMessage)
144+
.foregroundStyle(Color(.listForeground(modal: false)))
145+
.frame(maxWidth: .infinity, alignment: .leading)
146+
.padding(Layout.viewPadding)
147+
.background {
148+
RoundedRectangle(cornerRadius: Layout.cornerRadius)
149+
.fill(Color(.text))
150+
}
151+
.padding(Layout.viewPadding)
152+
.padding(.bottom, connectivityMonitor.isOffline ? OfflineBannerView.height : 0)
153+
.contentShape(Rectangle())
154+
.onTapGesture {
155+
withAnimation {
156+
viewModel.errorFetching = false
157+
}
158+
}
159+
}
91160
}
92161

93162
private extension BookingListView {
94163
enum Layout {
164+
static let textVerticalPadding: CGFloat = 8
95165
static let viewPadding: CGFloat = 16
166+
static let emptyStatePadding: CGFloat = 24
167+
static let emptyStateImageWidth: CGFloat = 67
96168
static let defaultBadgeColor = Color(uiColor: .init(light: .systemGray6, dark: .systemGray5))
169+
static let cornerRadius: CGFloat = 8
170+
}
171+
172+
enum Localization {
173+
static let errorMessage = NSLocalizedString(
174+
"bookingList.errorMessage",
175+
value: "Error fetching bookings",
176+
comment: "Error message when fetching bookings fails"
177+
)
97178
}
98179
}

WooCommerce/Classes/Bookings/BookingList/BookingListViewModel.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Foundation
2+
import SwiftUI
23
import Yosemite
34
import protocol Storage.StorageManagerType
45

@@ -7,6 +8,21 @@ final class BookingListViewModel: ObservableObject {
78

89
@Published private(set) var bookings: [Booking] = []
910

11+
@Published var errorFetching = false
12+
13+
var hasFilters: Bool {
14+
// TODO: Update when adding filters
15+
return false
16+
}
17+
18+
var emptyStateTitle: String {
19+
type.emptyStateTitle(hasFilters: hasFilters)
20+
}
21+
22+
var emptyStateDescription: String {
23+
type.emptyStateDescription(hasFilters: hasFilters)
24+
}
25+
1026
private let siteID: Int64
1127
private let type: BookingListTab
1228
private let stores: StoresManager
@@ -112,6 +128,9 @@ private extension BookingListViewModel {
112128
extension BookingListViewModel: PaginationTrackerDelegate {
113129
func sync(pageNumber: Int, pageSize: Int, reason: String?, onCompletion: SyncCompletion?) {
114130
transitionToSyncingState()
131+
withAnimation {
132+
errorFetching = false
133+
}
115134
let shouldClearCache = reason == Self.refreshCacheReason
116135
let action = BookingAction.synchronizeBookings(
117136
siteID: siteID,
@@ -127,6 +146,9 @@ extension BookingListViewModel: PaginationTrackerDelegate {
127146

128147
case .failure(let error):
129148
DDLogError("⛔️ Error synchronizing bookings: \(error)")
149+
withAnimation {
150+
self?.errorFetching = true
151+
}
130152
onCompletion?(.failure(error))
131153
}
132154

0 commit comments

Comments
 (0)