Skip to content

Commit 4f86900

Browse files
authored
Merge release/23.3 into trunk (#16189)
2 parents 0a622d9 + 29a49f7 commit 4f86900

37 files changed

+1281
-147
lines changed

Modules/Sources/NetworkingCore/Model/OrderItem.swift

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,18 @@ public struct OrderItem: Codable, Equatable, Hashable, Sendable, GeneratedFakeab
127127
forKey: .attributes)
128128
.first(where: { $0.key == "_pao_ids" })?.value ?? []
129129

130-
// Order item product image
131-
let image = try container.decodeIfPresent(OrderItemProductImage.self, forKey: .image)
130+
// Order item product image can be either a string URL or an object with src field
131+
// Use failsafeDecodeIfPresent with alternative types to handle both formats gracefully
132+
let image: OrderItemProductImage? = {
133+
if let imageObject = container.failsafeDecodeIfPresent(OrderItemProductImage.self, forKey: .image) {
134+
return imageObject
135+
}
136+
if let urlString = container.failsafeDecodeIfPresent(stringForKey: .image),
137+
!urlString.isEmpty {
138+
return OrderItemProductImage(src: urlString)
139+
}
140+
return nil
141+
}()
132142

133143
// Product Bundle extension properties:
134144
// If the order item is part of a product bundle, `bundledBy` is the parent order item (product bundle).
@@ -240,16 +250,20 @@ private struct OrderItemProductAddOnContainer: Decodable {
240250
// MARK: - Order Item Product Image
241251
//
242252
public struct OrderItemProductImage: Codable, Equatable, Hashable, Sendable {
243-
public let src: String?
253+
public let src: String
254+
255+
public init(src: String) {
256+
self.src = src
257+
}
244258

245259
public init(from decoder: Decoder) throws {
246260
let container = try decoder.container(keyedBy: CodingKeys.self)
247-
self.src = try? container.decodeIfPresent(String.self, forKey: .src)
261+
self.src = try container.decode(String.self, forKey: .src)
248262
}
249263

250264
public func encode(to encoder: Encoder) throws {
251265
var container = encoder.container(keyedBy: CodingKeys.self)
252-
try container.encodeIfPresent(src, forKey: .src)
266+
try container.encode(src, forKey: .src)
253267
}
254268

255269
private enum CodingKeys: String, CodingKey {

Modules/Tests/NetworkingTests/Mapper/OrderListMapperTests.swift

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -166,30 +166,67 @@ class OrderListMapperTests: XCTestCase {
166166

167167
/// Verifies that OrderItem decoding is robust for various image field scenarios from WooCommerce API
168168
///
169+
/// Tests all possible image field formats:
170+
/// 1. String URL - should parse successfully with image object
171+
/// 2. Object with src field - should parse successfully with image object
172+
/// 3. Object without src field - should be nil
173+
/// 4. Invalid types should fail gracefully with nil image
174+
/// 5. Nested objects - should be nil
175+
/// 6. Empty values - should be nil
176+
/// 7. Missing field - should be nil
177+
///
169178
func test_order_item_image_decoding_robustness() {
170179
let orders = mapOrders(from: "order-item-image")
171180
XCTAssertEqual(orders.count, 1)
172181

173182
let order = orders[0]
174-
XCTAssertEqual(order.items.count, 4)
183+
XCTAssertEqual(order.items.count, 11)
175184

176-
// Item 1: Product with valid image src
185+
// Item 1: Product with valid image object with src field (standard WooCommerce format)
177186
let item1 = order.items[0]
178-
XCTAssertNotNil(item1.image)
187+
XCTAssertNotNil(item1.image, "Image object should exist for object with valid src field")
179188
XCTAssertEqual(item1.image?.src, "https://example.com/image.jpg")
180189

181-
// Item 2: Product with no image field
190+
// Item 2: Product with no image field - should be nil
182191
let item2 = order.items[1]
183-
XCTAssertNil(item2.image)
192+
XCTAssertNil(item2.image, "Image should be nil when field is missing")
184193

185-
// Item 3: Product with image but no src field
194+
// Item 3: Product with image object but no src field - should be nil
186195
let item3 = order.items[2]
187-
XCTAssertNotNil(item3.image)
188-
XCTAssertNil(item3.image?.src)
196+
XCTAssertNil(item3.image, "Image should be nil when object has no src field")
189197

190-
// Item 4: Product with wrong src type
198+
// Item 4: Product with image object with wrong src type (number instead of string) - should be nil
191199
let item4 = order.items[3]
192-
XCTAssertNil(item4.image?.src)
200+
XCTAssertNil(item4.image, "Image should be nil when src is not a string")
201+
202+
// Item 5: Product with direct string URL image (WordPress format) - should parse successfully
203+
let item5 = order.items[4]
204+
XCTAssertNotNil(item5.image, "Image object should exist for valid string URL")
205+
XCTAssertEqual(item5.image?.src, "https://wordpress.example/wp-content/uploads/product-image-150x150.jpeg")
206+
207+
// Item 6: Product with unexpected image type (array) - should fail gracefully with nil
208+
let item6 = order.items[5]
209+
XCTAssertNil(item6.image, "Image should be nil for array type")
210+
211+
// Item 7: Product with nested object in image.src - should be nil
212+
let item7 = order.items[6]
213+
XCTAssertNil(item7.image, "Image should be nil when src is a nested object instead of string")
214+
215+
// Item 8: Product with image as number - should be parsed to string if possible
216+
let item8 = order.items[7]
217+
XCTAssertNotNil(item8.image?.src, "Image src should be not nil for number type")
218+
219+
// Item 9: Product with image as boolean - should fail gracefully with nil
220+
let item9 = order.items[8]
221+
XCTAssertNil(item9.image, "Image should be nil for boolean type")
222+
223+
// Item 10: Product with empty string image - should be nil
224+
let item10 = order.items[9]
225+
XCTAssertNil(item10.image, "Image should be nil for empty string")
226+
227+
// Item 11: Product with empty object image - should be nil
228+
let item11 = order.items[10]
229+
XCTAssertNil(item11.image, "Image should be nil for empty object")
193230
}
194231
}
195232

Modules/Tests/NetworkingTests/Responses/order-item-image.json

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,129 @@
129129
"image": {
130130
"src": 12345
131131
}
132+
},
133+
{
134+
"id": 5,
135+
"name": "Product with direct string URL image",
136+
"product_id": 127,
137+
"variation_id": 0,
138+
"quantity": 1,
139+
"price": 10.0,
140+
"sku": "",
141+
"subtotal": "10.00",
142+
"subtotal_tax": "0.00",
143+
"tax_class": "",
144+
"taxes": [],
145+
"total": "10.00",
146+
"total_tax": "0.00",
147+
"meta_data": [],
148+
"image": "https://wordpress.example/wp-content/uploads/product-image-150x150.jpeg"
149+
},
150+
{
151+
"id": 6,
152+
"name": "Product with unexpected image type (array)",
153+
"product_id": 128,
154+
"variation_id": 0,
155+
"quantity": 1,
156+
"price": 10.0,
157+
"sku": "",
158+
"subtotal": "10.00",
159+
"subtotal_tax": "0.00",
160+
"tax_class": "",
161+
"taxes": [],
162+
"total": "10.00",
163+
"total_tax": "0.00",
164+
"meta_data": [],
165+
"image": ["unexpected", "array"]
166+
},
167+
{
168+
"id": 7,
169+
"name": "Product with nested object in image",
170+
"product_id": 129,
171+
"variation_id": 0,
172+
"quantity": 1,
173+
"price": 10.0,
174+
"sku": "",
175+
"subtotal": "10.00",
176+
"subtotal_tax": "0.00",
177+
"tax_class": "",
178+
"taxes": [],
179+
"total": "10.00",
180+
"total_tax": "0.00",
181+
"meta_data": [],
182+
"image": {
183+
"src": {
184+
"url": "https://nested.com/image.jpg"
185+
}
186+
}
187+
},
188+
{
189+
"id": 8,
190+
"name": "Product with image as number",
191+
"product_id": 130,
192+
"variation_id": 0,
193+
"quantity": 1,
194+
"price": 10.0,
195+
"sku": "",
196+
"subtotal": "10.00",
197+
"subtotal_tax": "0.00",
198+
"tax_class": "",
199+
"taxes": [],
200+
"total": "10.00",
201+
"total_tax": "0.00",
202+
"meta_data": [],
203+
"image": 12345
204+
},
205+
{
206+
"id": 9,
207+
"name": "Product with image as boolean",
208+
"product_id": 131,
209+
"variation_id": 0,
210+
"quantity": 1,
211+
"price": 10.0,
212+
"sku": "",
213+
"subtotal": "10.00",
214+
"subtotal_tax": "0.00",
215+
"tax_class": "",
216+
"taxes": [],
217+
"total": "10.00",
218+
"total_tax": "0.00",
219+
"meta_data": [],
220+
"image": true
221+
},
222+
{
223+
"id": 10,
224+
"name": "Product with empty string image",
225+
"product_id": 132,
226+
"variation_id": 0,
227+
"quantity": 1,
228+
"price": 10.0,
229+
"sku": "",
230+
"subtotal": "10.00",
231+
"subtotal_tax": "0.00",
232+
"tax_class": "",
233+
"taxes": [],
234+
"total": "10.00",
235+
"total_tax": "0.00",
236+
"meta_data": [],
237+
"image": ""
238+
},
239+
{
240+
"id": 11,
241+
"name": "Product with empty object image",
242+
"product_id": 133,
243+
"variation_id": 0,
244+
"quantity": 1,
245+
"price": 10.0,
246+
"sku": "",
247+
"subtotal": "10.00",
248+
"subtotal_tax": "0.00",
249+
"tax_class": "",
250+
"taxes": [],
251+
"total": "10.00",
252+
"total_tax": "0.00",
253+
"meta_data": [],
254+
"image": {}
132255
}
133256
],
134257
"coupon_lines": [],

WooCommerce/Resources/ar.lproj/Localizable.strings

Lines changed: 67 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* Translation-Revision-Date: 2025-09-17 16:36:59+0000 */
1+
/* Translation-Revision-Date: 2025-09-22 16:54:04+0000 */
22
/* Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5; */
33
/* Generator: GlotPress/2.4.0-alpha */
44
/* Language: ar */
@@ -3203,9 +3203,6 @@ which should be translated separately and considered part of this sentence. */
32033203
/* A fallback describing the contactless limit, shown on the About Tap to Pay screen. %1$@ will be replaced with the country name of the store, which is a trade off as it can't be contextually translated, however this string is only used when there's a problem decoding the limit, so it's acceptable. */
32043204
"In %1$@, cards may only be used with Tap to Pay for transactions up to the contactless limit." = "في %1$@، لا يمكن استخدام البطاقات إلا من خلال ميزة Tap to Pay لإجراء معاملات تصل إلى الحد غير التلامسي.";
32053205

3206-
/* Accessibility label for an indeterminate loading indicator */
3207-
"In progress" = "قيد التقدم";
3208-
32093206
/* Display label for the bundle item's inventory stock status */
32103207
"In stock" = "متوفِّر";
32113208

@@ -11468,6 +11465,12 @@ which should be translated separately and considered part of this sentence. */
1146811465
/* Title of the Help section within Point of Sale settings. */
1146911466
"pointOfSaleSettingsView.sidebarNavigationHelpTitle" = "المساعدة";
1147011467

11468+
/* Description of the settings to be found within the Local catalog section. */
11469+
"pointOfSaleSettingsView.sidebarNavigationLocalCatalogSubtitle" = "إدارة إعدادات الكتالوج";
11470+
11471+
/* Title of the Local catalog section within Point of Sale settings. */
11472+
"pointOfSaleSettingsView.sidebarNavigationLocalCatalogTitle" = "الكتالوج";
11473+
1147111474
/* Description of the settings to be found within the Store section. */
1147211475
"pointOfSaleSettingsView.sidebarNavigationStoreSubtitle" = "تكوين المتجر والإعدادات";
1147311476

@@ -11786,6 +11789,12 @@ which should be translated separately and considered part of this sentence. */
1178611789
/* Accessibility label for button to dismiss a notice banner */
1178711790
"pos.noticeView.dismiss.button.accessibiltyLabel" = "تجاهل";
1178811791

11792+
/* Text appearing in the order details pane when there are no orders available. */
11793+
"pos.orderDetailsEmptyView.noOrderToDisplay" = "لا يوجد طلب للعرض";
11794+
11795+
/* Title at the header for the Order Details empty view. */
11796+
"pos.orderDetailsEmptyView.ordersTitle" = "طلب";
11797+
1178911798
/* Section title for the products list */
1179011799
"pos.orderDetailsLoadingView.productsTitle" = "المنتجات";
1179111800

@@ -11798,6 +11807,9 @@ which should be translated separately and considered part of this sentence. */
1179811807
/* Label for discount total in the totals section */
1179911808
"pos.orderDetailsView.discountTotalLabel" = "إجمالي الخصم";
1180011809

11810+
/* Label for email receipt action on order details view */
11811+
"pos.orderDetailsView.emailReceiptAction.title" = "إرسال الإيصال في رسالة عبر البريد الإلكتروني";
11812+
1180111813
/* Label for net payment amount after refunds */
1180211814
"pos.orderDetailsView.netPaymentLabel" = "المبلغ الصافي للمدفوعات";
1180311815

@@ -11837,9 +11849,30 @@ which should be translated separately and considered part of this sentence. */
1183711849
/* Text appearing on the order list screen when there's an error loading orders. */
1183811850
"pos.orderList.failedToLoadOrdersTitle" = "يتعذر تحميل الطلبات";
1183911851

11852+
/* Button text for refreshing orders when list is empty. */
11853+
"pos.orderListView.emptyOrdersButtonTitle" = "تحديث";
11854+
11855+
/* Hint text suggesting to modify search terms when no orders are found. */
11856+
"pos.orderListView.emptyOrdersSearchHint" = "حاول تعديل مصطلح البحث.";
11857+
11858+
/* Subtitle appearing when order search returns no results. */
11859+
"pos.orderListView.emptyOrdersSearchSubtitle" = "لم نتمكن من العثور على أي شيء يطابق بحثك.";
11860+
11861+
/* Title appearing when order search returns no results. */
11862+
"pos.orderListView.emptyOrdersSearchTitle" = "لم يتم العثور على طلبات";
11863+
11864+
/* Subtitle appearing when there are no orders to display. */
11865+
"pos.orderListView.emptyOrdersSubtitle" = "ستظهر الطلبات هنا بمجرد بدء معالجة المبيعات على POS.";
11866+
11867+
/* Title appearing when there are no orders to display. */
11868+
"pos.orderListView.emptyOrdersTitle" = "لا توجد طلبات حتى الآن";
11869+
1184011870
/* Title at the header for the Orders view. */
1184111871
"pos.orderListView.ordersTitle" = "الطلبات";
1184211872

11873+
/* Placeholder for a search field in the Orders view. */
11874+
"pos.orderListView.searchFieldPlaceholder" = "البحث في الطلبات";
11875+
1184311876
/* Text indicating that there are options available for a parent product */
1184411877
"pos.parentProductCard.optionsAvailable" = "خيارات متوفرة";
1184511878

@@ -11930,9 +11963,6 @@ which should be translated separately and considered part of this sentence. */
1193011963
/* Title for discount total amount field */
1193111964
"pos.totalsView.discountTotal2" = "إجمالي الخصم";
1193211965

11933-
/* Text for the banner requiring specific WooCommerce version. */
11934-
"pos.totalsView.receipts.banner.updateWooCommerceVersionText" = "يرجى تحديث WooCommerce إلى الإصدار 9.5.0";
11935-
1193611966
/* Title for subtotal amount field */
1193711967
"pos.totalsView.subtotal" = "الإجمالي الفرعي";
1193811968

@@ -11948,6 +11978,36 @@ which should be translated separately and considered part of this sentence. */
1194811978
/* An error shown when the Point of Sale is used in iOS split view, but with not enough horizontal space. */
1194911979
"pos.unsupportedWidth.title" = "نقطة البيع غير مدعومة في عرض الشاشة هذا.";
1195011980

11981+
/* Label for allow full sync on cellular data toggle in Point of Sale settings. */
11982+
"posSettingsLocalCatalogDetailView.allowFullSyncOnCellular" = "السماح بالمزامنة الكاملة على بيانات شبكة الجوال";
11983+
11984+
/* Label for catalog size field in Point of Sale settings. */
11985+
"posSettingsLocalCatalogDetailView.catalogSize" = "حجم الكتالوج";
11986+
11987+
/* Section title for catalog status in Point of Sale settings. */
11988+
"posSettingsLocalCatalogDetailView.catalogStatus" = "حالة الكتالوج";
11989+
11990+
/* Label for last full sync field in Point of Sale settings. */
11991+
"posSettingsLocalCatalogDetailView.lastFullSync" = "آخر مزامنة كاملة";
11992+
11993+
/* Label for last incremental update field in Point of Sale settings. */
11994+
"posSettingsLocalCatalogDetailView.lastIncrementalUpdate" = "آخر تحديث تزايدي";
11995+
11996+
/* Section title for managing data usage in Point of Sale settings. */
11997+
"posSettingsLocalCatalogDetailView.managingDataUsage" = "إدارة استخدام البيانات";
11998+
11999+
/* Section title for manual catalog update in Point of Sale settings. */
12000+
"posSettingsLocalCatalogDetailView.manualCatalogUpdate" = "تحديث الكتالوج يدويًا";
12001+
12002+
/* Info text explaining when to use manual catalog update. */
12003+
"posSettingsLocalCatalogDetailView.manualUpdateInfo" = "لا تستخدم هذا التحديث إلا عندما يبدو أن شيئًا ما متوقفًا عن التشغيل - يحافظ POS على تحديث البيانات تلقائيًا.";
12004+
12005+
/* Button text for refreshing the catalog manually. */
12006+
"posSettingsLocalCatalogDetailView.refreshCatalog" = "تحديث الكتالوج";
12007+
12008+
/* Navigation title for the local catalog details in POS settings. */
12009+
"posSettingsLocalCatalogDetailView.title" = "إعدادات الكتالوج";
12010+
1195112011
/* Close title for the navigation bar button on the Print Shipping Label view. */
1195212012
"print.shipping.label.close.button.title" = "إغلاق";
1195312013

0 commit comments

Comments
 (0)