Skip to content

Commit 9e4663e

Browse files
authored
Merge branch 'trunk' into issue/14004-better-application-password-disabled-note-take-2
2 parents 9fafc53 + ceb70bd commit 9e4663e

File tree

49 files changed

+1168
-479
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+1168
-479
lines changed

Networking/Networking.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,8 @@
423423
450106852399A7CB00E24722 /* TaxClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 450106842399A7CB00E24722 /* TaxClass.swift */; };
424424
4501068F2399B19500E24722 /* TaxRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4501068E2399B19500E24722 /* TaxRemote.swift */; };
425425
450106912399B2C800E24722 /* TaxClassListMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 450106902399B2C800E24722 /* TaxClassListMapper.swift */; };
426+
450BFA6B2D52777500C2D1D7 /* WordPressMediaMapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 450BFA6A2D52777500C2D1D7 /* WordPressMediaMapperTests.swift */; };
427+
450BFA6D2D52790E00C2D1D7 /* media-library-with-empty-sizes.json in Resources */ = {isa = PBXBuildFile; fileRef = 450BFA6C2D52790200C2D1D7 /* media-library-with-empty-sizes.json */; };
426428
451274A625276C82009911FF /* product-variation.json in Resources */ = {isa = PBXBuildFile; fileRef = 451274A525276C82009911FF /* product-variation.json */; };
427429
4513382027A8227F00AE5E78 /* InboxNotesRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4513381F27A8227F00AE5E78 /* InboxNotesRemote.swift */; };
428430
4513382227A8409000AE5E78 /* InboxNotesRemoteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4513382127A8409000AE5E78 /* InboxNotesRemoteTests.swift */; };
@@ -1621,6 +1623,8 @@
16211623
450106842399A7CB00E24722 /* TaxClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaxClass.swift; sourceTree = "<group>"; };
16221624
4501068E2399B19500E24722 /* TaxRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaxRemote.swift; sourceTree = "<group>"; };
16231625
450106902399B2C800E24722 /* TaxClassListMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaxClassListMapper.swift; sourceTree = "<group>"; };
1626+
450BFA6A2D52777500C2D1D7 /* WordPressMediaMapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WordPressMediaMapperTests.swift; sourceTree = "<group>"; };
1627+
450BFA6C2D52790200C2D1D7 /* media-library-with-empty-sizes.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "media-library-with-empty-sizes.json"; sourceTree = "<group>"; };
16241628
451274A525276C82009911FF /* product-variation.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "product-variation.json"; sourceTree = "<group>"; };
16251629
4513381F27A8227F00AE5E78 /* InboxNotesRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InboxNotesRemote.swift; sourceTree = "<group>"; };
16261630
4513382127A8409000AE5E78 /* InboxNotesRemoteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InboxNotesRemoteTests.swift; sourceTree = "<group>"; };
@@ -3319,6 +3323,7 @@
33193323
93D8BBFE226BC1DA00AD2EB3 /* me-settings.json */,
33203324
EEDADD272B7C6A5E00F7302B /* media.json */,
33213325
02AF07EB27492FDD00B2D81E /* media-library.json */,
3326+
450BFA6C2D52790200C2D1D7 /* media-library-with-empty-sizes.json */,
33223327
EECB7EE7286555180028C888 /* media-update-product-id.json */,
33233328
B58D10C92114D22E00107ED4 /* new-order-note.json */,
33243329
022902D322E2436400059692 /* no_stats_permission_error.json */,
@@ -3908,6 +3913,7 @@
39083913
EEA1E2012AC18CD700A37ADD /* AIProductMapperTests.swift */,
39093914
EE078D922AD2F1E500C1199E /* JWTokenMapperTests.swift */,
39103915
EE065ACF2B8E56C2009848CB /* BlazeCampaignListItemsMapperTests.swift */,
3916+
450BFA6A2D52777500C2D1D7 /* WordPressMediaMapperTests.swift */,
39113917
DEDA8DA62B18399D0076BF0F /* WordPressThemeListMapperTests.swift */,
39123918
DEDA8DB82B187EC90076BF0F /* WordPressThemeMapperTests.swift */,
39133919
DEA493762B39987B00EED015 /* BlazeTargetOptionMapperTests.swift */,
@@ -4752,6 +4758,7 @@
47524758
EE065ACE2B8E51AD009848CB /* blaze-campaigns-list-success.json in Resources */,
47534759
45A4B85625D2E75300776FB4 /* shipping-label-address-validation-success.json in Resources */,
47544760
DEB3878F2C2D71A10025256E /* gla-campaign-list-without-data-envelope.json in Resources */,
4761+
450BFA6D2D52790E00C2D1D7 /* media-library-with-empty-sizes.json in Resources */,
47554762
457FC68C2382B2FD00B41B02 /* product-update.json in Resources */,
47564763
CE71E2292A4C35C900DB5376 /* reports-products.json in Resources */,
47574764
CE19CB11222486A600E8AF7A /* order-statuses.json in Resources */,
@@ -5629,6 +5636,7 @@
56295636
B5C6FCCD20A34B8300A4F8E4 /* OrderListMapperTests.swift in Sources */,
56305637
B518663520A0A2E800037A38 /* Constants.swift in Sources */,
56315638
EEA658422966C41A00112DF0 /* ProductIDMapperTests.swift in Sources */,
5639+
450BFA6B2D52777500C2D1D7 /* WordPressMediaMapperTests.swift in Sources */,
56325640
CE12AE9D29F2AD1C0056DD17 /* SubscriptionMapperTests.swift in Sources */,
56335641
D8FBFF1E22D51F39006E3336 /* OrderStatsMapperV4Tests.swift in Sources */,
56345642
02E7FFCB256218F600C53030 /* ShippingLabelRemoteTests.swift in Sources */,

Networking/Networking/Model/Media/WordPressMedia.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,20 @@ public extension WordPressMedia {
7171
case fileName = "file"
7272
case sizes
7373
}
74+
75+
public init(from decoder: Decoder) throws {
76+
let container = try decoder.container(keyedBy: CodingKeys.self)
77+
width = try container.decodeIfPresent(Double.self, forKey: .width)
78+
height = try container.decodeIfPresent(Double.self, forKey: .height)
79+
fileName = try container.decodeIfPresent(String.self, forKey: .fileName)
80+
81+
do {
82+
sizes = try container.decodeIfPresent([String: MediaSizeDetails].self, forKey: .sizes)
83+
} catch {
84+
// Handle the case where sizes is an empty array. Ref: https://github.com/woocommerce/woocommerce-ios/issues/14516
85+
sizes = nil
86+
}
87+
}
7488
}
7589

7690
/// Details about a size of WordPress site media (e.g. `thumbnail`, `medium`, `2048x2048`).
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import XCTest
2+
@testable import Networking
3+
4+
final class WordPressMediaMapperTests: XCTestCase {
5+
6+
/// Tests that WordPressMediaList and consequently WordPressMedia is correctly mapped from a response where sizes is a dictionary.
7+
///
8+
/// - Throws: An error if the mapping fails.
9+
func test_it_maps_WordPressMediaList_correctly_with_sizes_as_dictionary() throws {
10+
// Given
11+
let data = try retrieveWordPressMediaResponseWithSizesAsDictionary()
12+
let mapper = WordPressMediaListMapper()
13+
14+
// When
15+
let mediaList = try mapper.map(response: data)
16+
17+
// Then
18+
XCTAssertEqual(mediaList.count, 3)
19+
let media = mediaList.first { $0.mediaID == 22 }
20+
XCTAssertNotNil(media)
21+
XCTAssertEqual(media?.details?.sizes?["medium"]?.width, 300)
22+
XCTAssertEqual(media?.details?.sizes?["medium"]?.height, 225)
23+
XCTAssertEqual(media?.details?.sizes?["large"]?.width, 1024)
24+
XCTAssertEqual(media?.details?.sizes?["large"]?.height, 768)
25+
}
26+
27+
/// Tests that WordPressMediaList and consequently WordPressMedia is correctly mapped from a response where sizes is an empty array.
28+
///
29+
/// - Throws: An error if the mapping fails.
30+
func test_it_maps_WordPressMediaList_correctly_with_sizes_as_empty_array() throws {
31+
// Given
32+
let data = try retrieveWordPressMediaResponseWithSizesAsEmptyArray()
33+
let mapper = WordPressMediaListMapper()
34+
35+
// When
36+
let mediaList = try mapper.map(response: data)
37+
38+
// Then
39+
XCTAssertEqual(mediaList.count, 2)
40+
let media = mediaList.first { $0.mediaID == 13 }
41+
XCTAssertNotNil(media)
42+
XCTAssertNil(media?.details?.sizes)
43+
}
44+
}
45+
46+
// MARK: - Test Helpers
47+
///
48+
private extension WordPressMediaMapperTests {
49+
func retrieveWordPressMediaResponseWithSizesAsDictionary() throws -> Data {
50+
guard let response = Loader.contentsOf("media-library") else {
51+
throw FileNotFoundError()
52+
}
53+
54+
return response
55+
}
56+
57+
func retrieveWordPressMediaResponseWithSizesAsEmptyArray() throws -> Data {
58+
guard let response = Loader.contentsOf("media-library-with-empty-sizes") else {
59+
throw FileNotFoundError()
60+
}
61+
62+
return response
63+
}
64+
65+
struct FileNotFoundError: Error {}
66+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
[
2+
{
3+
"slug" : "image-copy",
4+
"date_gmt" : "2025-02-04t16:08:31",
5+
"id" : 13,
6+
"media_details" : {
7+
"filesize" : 1343,
8+
"sizes" : [
9+
10+
],
11+
"image_meta" : {
12+
"caption" : "",
13+
"orientation" : "1",
14+
"aperture" : "0",
15+
"focal_length" : "0",
16+
"title" : "",
17+
"credit" : "",
18+
"camera" : "",
19+
"keywords" : [
20+
21+
],
22+
"created_timestamp" : "0",
23+
"shutter_speed" : "0",
24+
"copyright" : "",
25+
"iso" : "0"
26+
},
27+
"width" : 4,
28+
"height" : 2,
29+
"file" : "2025/02/image-copy.jpg"
30+
},
31+
"title" : {
32+
"rendered" : "image copy"
33+
},
34+
"alt_text" : "",
35+
"mime_type" : "image/jpeg",
36+
"source_url" : "https://example.com/wp-content/uploads/image-copy.jpeg"
37+
},
38+
{
39+
"slug" : "woocommerce-placeholder",
40+
"date_gmt" : "2025-02-04t16:04:33",
41+
"id" : 4,
42+
"media_details" : {
43+
"filesize" : 48149,
44+
"sizes" : {
45+
"medium" : {
46+
"filesize" : 12321,
47+
"mime_type" : "image/png",
48+
"source_url" : "https://example.com/wp-content/uploads/woocommerce-placeholder-300x300.jpeg",
49+
"file" : "woocommerce-placeholder-300x300.png",
50+
"width" : 300,
51+
"height" : 300
52+
},
53+
"woocommerce_single" : {
54+
"filesize" : 38159,
55+
"mime_type" : "image/png",
56+
"source_url" : "https://example.com/wp-content/uploads/woocommerce-placeholder-600x600.jpeg",
57+
"file" : "woocommerce-placeholder-600x600.png",
58+
"width" : 600,
59+
"height" : 600
60+
},
61+
"large" : {
62+
"filesize" : 90808,
63+
"mime_type" : "image/png",
64+
"source_url" : "https://example.com/wp-content/uploads/woocommerce-placeholder-1024x1024.jpeg",
65+
"file" : "woocommerce-placeholder-1024x1024.png",
66+
"width" : 1024,
67+
"height" : 1024
68+
},
69+
"thumbnail" : {
70+
"filesize" : 4209,
71+
"mime_type" : "image/png",
72+
"source_url" : "https://example.com/wp-content/uploads/woocommerce-placeholder-150x150.jpeg",
73+
"file" : "woocommerce-placeholder-150x150.png",
74+
"width" : 150,
75+
"height" : 150
76+
},
77+
"medium_large" : {
78+
"filesize" : 56643,
79+
"mime_type" : "image/png",
80+
"source_url" : "https://example.com/wp-content/uploads/woocommerce-placeholder-768x768.jpeg",
81+
"file" : "woocommerce-placeholder-768x768.png",
82+
"width" : 768,
83+
"height" : 768
84+
},
85+
"woocommerce_gallery_thumbnail" : {
86+
"filesize" : 2330,
87+
"mime_type" : "image/png",
88+
"source_url" : "https://example.com/wp-content/uploads/woocommerce-placeholder-100x100.jpeg",
89+
"file" : "woocommerce-placeholder-100x100.png",
90+
"width" : 100,
91+
"height" : 100
92+
},
93+
"woocommerce_thumbnail" : {
94+
"height" : 300,
95+
"source_url" : "https://example.com/wp-content/uploads/woocommerce-placeholder-300x300.jpeg",
96+
"filesize" : 12321,
97+
"file" : "woocommerce-placeholder-300x300.png",
98+
"width" : 300,
99+
"uncropped" : false,
100+
"mime_type" : "image/png"
101+
},
102+
"full" : {
103+
"mime_type" : "image/png",
104+
"source_url" : "https://example.com/wp-content/uploads/woocommerce-placeholder.jpeg",
105+
"file" : "woocommerce-placeholder.png",
106+
"width" : 1200,
107+
"height" : 1200
108+
}
109+
},
110+
"image_meta" : {
111+
"caption" : "",
112+
"orientation" : "0",
113+
"aperture" : "0",
114+
"focal_length" : "0",
115+
"title" : "",
116+
"credit" : "",
117+
"camera" : "",
118+
"keywords" : [
119+
120+
],
121+
"created_timestamp" : "0",
122+
"shutter_speed" : "0",
123+
"copyright" : "",
124+
"iso" : "0"
125+
},
126+
"width" : 1200,
127+
"height" : 1200,
128+
"file" : "woocommerce-placeholder.png"
129+
},
130+
"title" : {
131+
"rendered" : "woocommerce-placeholder"
132+
},
133+
"alt_text" : "",
134+
"mime_type" : "image/png",
135+
"source_url" : "https://example.com/wp-content/uploads/woocommerce-placeholder.jpeg"
136+
}
137+
]

RELEASE-NOTES.txt

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

44
21.7
55
-----
6+
- [**] Fixed an issue with the WordPress Media Library not loading due to a decoding error in media details sizes. [https://github.com/woocommerce/woocommerce-ios/pull/15056]
67
- [*] Better error messages if Application Password login is disabled on user's website. [https://github.com/woocommerce/woocommerce-ios/pull/15031]
78
- [*] Now, usernames and emails in text fields across multiple login views are no longer capitalized. [https://github.com/woocommerce/woocommerce-ios/pull/15002]
89
- [*] Product Details: Display cover tag on the first product image [https://github.com/woocommerce/woocommerce-ios/pull/15041]

WooCommerce/Classes/Analytics/AppStartupWaitingTimeTracker.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class AppStartupWaitingTimeTracker: WaitingTimeTracker {
2222

2323
init(analyticsService: Analytics = ServiceLocator.analytics,
2424
currentTimeInMillis: @escaping () -> TimeInterval = { Date().timeIntervalSince1970 }) {
25-
super.init(trackScenario: .appStartup, analyticsService: analyticsService, currentTimeInMillis: currentTimeInMillis)
25+
super.init(trackScenario: .appStartup, analyticsService: analyticsService, currentTime: currentTimeInMillis)
2626
}
2727

2828
/// Ends the waiting time for the provided startup action.

WooCommerce/Classes/Analytics/TracksProvider.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@ private extension TracksProvider {
116116
let exemptedEvents: Set<String> = [
117117
"application_opened",
118118
"application_closed",
119+
"orders_add_new",
120+
"support_new_request_viewed",
119121
"dynamic_dashboard_card_data_loading_completed"
120122
]
121123
if exemptedEvents.contains(eventName) {

WooCommerce/Classes/Analytics/WaitingTimeTracker.swift

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,35 @@ import protocol WooFoundation.Analytics
77
///
88
class WaitingTimeTracker {
99
private let trackScenario: WooAnalyticsEvent.WaitingTime.Scenario
10-
private let currentTimeInMillis: () -> TimeInterval
10+
private let currentTime: () -> TimeInterval
1111
private let analyticsService: Analytics
1212
private let waitingStartedTimestamp: TimeInterval
1313

1414
init(trackScenario: WooAnalyticsEvent.WaitingTime.Scenario,
1515
analyticsService: Analytics = ServiceLocator.analytics,
16-
currentTimeInMillis: @escaping () -> TimeInterval = { Date().timeIntervalSince1970 }
16+
currentTime: @escaping () -> TimeInterval = { Date().timeIntervalSince1970 }
1717
) {
1818
self.trackScenario = trackScenario
1919
self.analyticsService = analyticsService
20-
self.currentTimeInMillis = currentTimeInMillis
21-
waitingStartedTimestamp = currentTimeInMillis()
20+
self.currentTime = currentTime
21+
waitingStartedTimestamp = currentTime()
2222
}
2323

2424
/// End the waiting time by evaluating the elapsed time from the init,
25-
/// and sending it as an analytics event.
25+
/// and sending it as an analytics event, in seconds.
2626
///
2727
func end() {
28-
let elapsedTime = currentTimeInMillis() - waitingStartedTimestamp
28+
let elapsedTime = currentTime() - waitingStartedTimestamp
2929
let analyticsEvent = WooAnalyticsEvent.WaitingTime.waitingFinished(scenario: trackScenario, elapsedTime: elapsedTime)
3030
analyticsService.track(event: analyticsEvent)
3131
}
32+
33+
/// End the waiting time by evaluating the elapsed time from the init,
34+
/// and sending it as an analytics event, in milliseconds
35+
///
36+
func endInMilliseconds() {
37+
let elapsedTimeMs = (currentTime() - waitingStartedTimestamp) * 1000
38+
let analyticsEvent = WooAnalyticsEvent.WaitingTime.waitingFinished(scenario: trackScenario, elapsedTime: elapsedTimeMs)
39+
analyticsService.track(event: analyticsEvent)
40+
}
3241
}

WooCommerce/Classes/Analytics/WooAnalyticsEvent.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2486,10 +2486,12 @@ extension WooAnalyticsEvent {
24862486
case dashboardMainStats
24872487
case analyticsHub
24882488
case appStartup
2489+
case pointOfSaleLoaded
24892490
}
24902491

24912492
private enum Keys {
24922493
static let waitingTime = "waiting_time"
2494+
static let millisecondsTimeElapsedInSplashScreen = "milliseconds_time_elapsed_in_splash_screen"
24932495
}
24942496

24952497
static func waitingFinished(scenario: Scenario, elapsedTime: TimeInterval) -> WooAnalyticsEvent {
@@ -2504,6 +2506,8 @@ extension WooAnalyticsEvent {
25042506
return WooAnalyticsEvent(statName: .analyticsHubWaitingTimeLoaded, properties: [Keys.waitingTime: elapsedTime])
25052507
case .appStartup:
25062508
return WooAnalyticsEvent(statName: .applicationOpenedWaitingTimeLoaded, properties: [Keys.waitingTime: elapsedTime])
2509+
case .pointOfSaleLoaded:
2510+
return WooAnalyticsEvent(statName: .pointOfSaleLoaded, properties: [Keys.millisecondsTimeElapsedInSplashScreen: elapsedTime])
25072511
}
25082512
}
25092513
}

WooCommerce/Classes/Analytics/WooAnalyticsStat.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1268,9 +1268,24 @@ enum WooAnalyticsStat: String {
12681268
case backgroundUpdatesDisabled = "background_updates_disabled"
12691269

12701270
// MARK: Point of Sale events
1271+
case pointOfSaleLoaded = "pos_loaded"
1272+
case pointOfSaleProductsPullToRefresh = "products_pull_to_refresh"
1273+
case pointOfSaleVariationsPullToRefresh = "variations_pull_to_refresh"
12711274
case pointOfSaleAddItemToCart = "item_added_to_cart"
1275+
case pointOfSaleItemRemovedFromCart = "item_removed_from_cart"
1276+
case pointOfSaleCheckoutTapped = "checkout_tapped"
1277+
case pointOfSaleBackToCartTapped = "back_to_cart_tapped"
1278+
case pointOfSaleClearCartTapped = "clear_cart_tapped"
1279+
case pointOfSaleExitMenuItemTapped = "exit_pos_menu_item_tapped"
1280+
case pointOfSaleExitConfirmed = "exit_pos_confirmed"
1281+
case pointOfSaleGetSupportTapped = "get_support_tapped"
1282+
case pointOfSaleSimpleProductsExplanationDialogShown = "simple_products_explanation_dialog_shown"
1283+
case pointOfSaleCreateNewOrderTapped = "create_new_order_tapped"
1284+
case pointOfSaleEmailReceiptTapped = "email_receipt_tapped"
1285+
case pointOfSaleEmailReceiptSendTapped = "email_receipt_send_tapped"
12721286
case pointOfSalePaymentsOnboardingShown = "payments_onboarding_shown"
12731287
case pointOfSalePaymentsOnboardingDismissed = "payments_onboarding_dismissed"
1288+
case pointOfSaleCardReaderConnectionTapped = "card_reader_connection_tapped"
12741289

12751290
// MARK: Custom Fields events
12761291
case productDetailCustomFieldsTapped = "product_detail_custom_fields_tapped"

0 commit comments

Comments
 (0)