Skip to content

Commit

Permalink
Merge branch 'trunk' into issue/13779-get-origin-addresses-networking…
Browse files Browse the repository at this point in the history
…-support
  • Loading branch information
bozidarsevo authored Dec 20, 2024
2 parents 55c1bb5 + 4bdd158 commit 4236f99
Show file tree
Hide file tree
Showing 86 changed files with 2,958 additions and 493 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
<!--
Contains editorialized release notes. Raw release notes should go into `RELEASE-NOTES.txt`.
-->
## 21.3

This update brings enhanced reliability and clarity to your WooCommerce experience! Enjoy improved Jetpack setup, smoother media handling, and better product and payment workflows. We’ve also optimized storage and addressed key UI issues to elevate performance. Plus, Tap to Pay onboarding now guides you with ease!

## 21.2
In just two weeks, we've jam-packed this release. There's GTIN global product identifier support, and you can edit the call-to-action in Blaze campaigns.

Expand Down
2 changes: 2 additions & 0 deletions Experiments/Experiments/DefaultFeatureFlagService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ public struct DefaultFeatureFlagService: FeatureFlagService {
return true
case .variableProductsInPointOfSale:
return buildConfig == .localDeveloper || buildConfig == .alpha
case .hideSitesInStorePicker:
return buildConfig == .localDeveloper || buildConfig == .alpha
default:
return true
}
Expand Down
4 changes: 4 additions & 0 deletions Experiments/Experiments/FeatureFlag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -215,4 +215,8 @@ public enum FeatureFlag: Int {
/// Supports variable products in POS.
///
case variableProductsInPointOfSale

/// Supports hiding sites from the store picker
///
case hideSitesInStorePicker
}
7 changes: 7 additions & 0 deletions Fakes/Fakes/Fake.swift
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,10 @@ extension NSRange {
.init()
}
}

extension UUID {
/// Returns a default UUID
static func fake() -> Self {
.init()
}
}
16 changes: 16 additions & 0 deletions Fakes/Fakes/Yosemite.generated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,22 @@ extension Yosemite.JustInTimeMessageTemplate {
.banner
}
}
extension Yosemite.POSSimpleProduct {
/// Returns a "ready to use" type filled with fake values.
///
public static func fake() -> Yosemite.POSSimpleProduct {
.init(
id: .fake(),
name: .fake(),
formattedPrice: .fake(),
productImageSource: .fake(),
productID: .fake(),
price: .fake(),
productType: .fake(),
bundledItems: .fake()
)
}
}
extension Yosemite.ProductReviewFromNoteParcel {
/// Returns a "ready to use" type filled with fake values.
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,21 @@ extension WooShippingCustomPackage: Codable {

var boxWeight: Double = 0.0
// Looks like some endpoints have boxWeight as String and some as Double
// and some endpoints have it as box_weight and some as boxWeight
if let boxWeightDouble = try? container.decodeIfPresent(Double.self, forKey: .boxWeight) {
boxWeight = boxWeightDouble
}
else if let boxWeightString = try? container.decodeIfPresent(String.self, forKey: .boxWeight),
let boxWeightDouble = Double(boxWeightString) {
boxWeight = boxWeightDouble
}
else if let boxWeightDouble = try? container.decodeIfPresent(Double.self, forKey: .boxWeightAlternate) {
boxWeight = boxWeightDouble
}
else if let boxWeightString = try? container.decodeIfPresent(String.self, forKey: .boxWeightAlternate),
let boxWeightDouble = Double(boxWeightString) {
boxWeight = boxWeightDouble
}

self.init(id: id, name: name, rawType: type, dimensions: dimensions, boxWeight: boxWeight)
}
Expand All @@ -92,6 +100,7 @@ extension WooShippingCustomPackage: Codable {
case name
case type
case dimensions
case boxWeight = "box_weight"
case boxWeight
case boxWeightAlternate = "box_weight"
}
}
2 changes: 1 addition & 1 deletion Networking/Networking/Model/Site.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Codegen

/// Represents a WordPress.com Site.
///
public struct Site: Decodable, Equatable, GeneratedFakeable, GeneratedCopiable {
public struct Site: Decodable, Equatable, Hashable, GeneratedFakeable, GeneratedCopiable {

/// WordPress.com Site Identifier.
///
Expand Down
16 changes: 16 additions & 0 deletions Networking/Networking/Network/AlamofireNetwork.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,22 @@ public class AlamofireNetwork: Network {
}
}

public func responseDataAndHeaders(for request: URLRequestConvertible) async throws -> (Data, ResponseHeaders?) {
let request = requestConverter.convert(request)
let sessionRequest = alamofireSession.request(request)
.validateIfRestRequest(for: request)
let response = await sessionRequest.serializingData().response
if let error = response.networkingError {
throw error
}
switch response.result {
case .success(let data):
return (data, response.response?.headers.dictionary)
case .failure(let error):
throw error
}
}

/// Executes the specified Network Request. Upon completion, the payload or error will be emitted to the publisher.
/// Only one value will be emitted and the request cannot be retried.
///
Expand Down
17 changes: 17 additions & 0 deletions Networking/Networking/Network/MockNetwork.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ class MockNetwork: Network {
///
var requestsForResponseData = [URLRequestConvertible]()

/// Response headers to be returned with the response data.
var responseHeaders: [String: String]?

/// Number of notification objects in notifications-load-all.json file.
///
static let notificationLoadAllJSONCount = 46
Expand Down Expand Up @@ -78,6 +81,20 @@ class MockNetwork: Network {
completion(.success(data))
}

func responseDataAndHeaders(for request: any URLRequestConvertible) async throws -> (Data, ResponseHeaders?) {
requestsForResponseData.append(request)

if let error = error(for: request) {
throw error
}

guard let name = filename(for: request), let data = Loader.contentsOf(name) else {
throw NetworkError.notFound()
}

return (data, responseHeaders)
}

func responseDataPublisher(for request: URLRequestConvertible) -> AnyPublisher<Swift.Result<Data, Error>, Never> {
requestsForResponseData.append(request)

Expand Down
3 changes: 3 additions & 0 deletions Networking/Networking/Network/Network.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public protocol MultipartFormData {
/// Unit Testing target, and inject mocked up responses.
///
public protocol Network {
typealias ResponseHeaders = [String: String]

var session: URLSession { get }

Expand All @@ -39,6 +40,8 @@ public protocol Network {
func responseData(for request: URLRequestConvertible,
completion: @escaping (Swift.Result<Data, Error>) -> Void)

func responseDataAndHeaders(for request: URLRequestConvertible) async throws -> (Data, ResponseHeaders?)

/// Executes the specified Network Request. Upon completion, the payload or error will be emitted to the publisher.
///
/// - Parameters:
Expand Down
4 changes: 4 additions & 0 deletions Networking/Networking/Network/NullNetwork.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ public final class NullNetwork: Network {

}

public func responseDataAndHeaders(for request: any URLRequestConvertible) async throws -> (Data, ResponseHeaders?) {
throw NetworkError.notFound()
}

public func responseDataPublisher(for request: URLRequestConvertible) -> AnyPublisher<Swift.Result<Data, Error>, Never> {
Empty<Swift.Result<Data, Error>, Never>().eraseToAnyPublisher()
}
Expand Down
16 changes: 16 additions & 0 deletions Networking/Networking/Network/WordPressOrgNetwork.swift
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,22 @@ public final class WordPressOrgNetwork: Network {
}
}

public func responseDataAndHeaders(for request: URLRequestConvertible) async throws -> (Data, ResponseHeaders?) {
let sessionRequest = alamofireSession.request(request).validate()
let response = await sessionRequest.serializingData().response
do {
try validateResponse(response.data)
switch response.result {
case .success(let data):
return (data, response.response?.headers.dictionary)
case .failure(let error):
throw error
}
} catch {
throw error
}
}

/// Executes the specified Network Request. Upon completion, the payload or error will be emitted to the publisher.
/// Only one value will be emitted and the request cannot be retried.
///
Expand Down
15 changes: 13 additions & 2 deletions Networking/Networking/Remote/ProductsRemote.swift
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,9 @@ public final class ProductsRemote: Remote, ProductsRemoteProtocol {
/// - productTypes: A list of product types to be included in the results.
/// - pageNumber: Number of page that should be retrieved.
///
public func loadProductsForPointOfSale(for siteID: Int64, productTypes: [ProductType] = [.simple], pageNumber: Int = 1) async throws -> [Product] {
public func loadProductsForPointOfSale(for siteID: Int64,
productTypes: [ProductType] = [.simple],
pageNumber: Int = 1) async throws -> PagedItems<Product> {
let parameters = [
ParameterKey.page: String(pageNumber),
ParameterKey.perPage: POSConstants.productsPerPage,
Expand All @@ -222,7 +224,16 @@ public final class ProductsRemote: Remote, ProductsRemoteProtocol {
parameters: parameters,
availableAsRESTRequest: true)
let mapper = ProductListMapper(siteID: siteID)
return try await enqueue(request, mapper: mapper)

let (products, responseHeaders) = try await enqueueWithResponseHeaders(request, mapper: mapper)

// Extracts the total number of pages from the response headers.
// Response header names are case insensitive.
let totalPages = responseHeaders?.first(where: { $0.key.lowercased() == Remote.PaginationHeaderKey.totalPagesCount.lowercased() })
.flatMap { Int($0.value) }
let hasMorePages = totalPages.map { pageNumber < $0 } ?? true

return .init(items: products, hasMorePages: hasMorePages)
}

/// Retrieves a specific list of `Product`s by `productID`.
Expand Down
40 changes: 40 additions & 0 deletions Networking/Networking/Remote/Remote.swift
Original file line number Diff line number Diff line change
Expand Up @@ -258,8 +258,33 @@ public class Remote: NSObject {
}
}
}

func enqueueWithResponseHeaders<M: Mapper>(_ request: Request, mapper: M) async throws -> (data: M.Output, headers: [String: String]?) {
do {
let (data, headers) = try await network.responseDataAndHeaders(for: request)
let validator = request.responseDataValidator()
let parsedData = try validateAndParseData(data, request: request, validator: validator, mapper: mapper)
return (data: parsedData, headers: headers)
} catch {
handleResponseError(error: error, for: request)
throw mapNetworkError(error: error, for: request)
}
}
}

private extension Remote {
// Validation and parsing of the response data is separated so that the decoding error can be handled separately from network error.
func validateAndParseData<M: Mapper>(_ data: Data, request: Request, validator: ResponseDataValidator, mapper: M) throws -> M.Output {
do {
try validator.validate(data: data)
return try mapper.map(response: data)
} catch {
DDLogError("<> Mapping Error: \(error)")
handleDecodingError(error: error, for: request, entityName: "\(M.Output.self)")
throw error
}
}
}

// MARK: - Private Methods
//
Expand Down Expand Up @@ -341,6 +366,17 @@ private extension Remote {
}
}

/// Contains the result of a paginated request.
public struct PagedItems<T> {
public let items: [T]
public let hasMorePages: Bool

public init(items: [T], hasMorePages: Bool) {
self.items = items
self.hasMorePages = hasMorePages
}
}

// MARK: - Constants!
//
public extension Remote {
Expand All @@ -349,6 +385,10 @@ public extension Remote {
public static let firstPageNumber: Int = 1
}

enum PaginationHeaderKey {
static let totalPagesCount = "x-wp-totalpages"
}

enum JSONParsingErrorUserInfoKey {
public static let path = "path"
public static let entityName = "entity"
Expand Down
46 changes: 42 additions & 4 deletions Networking/NetworkingTests/Remote/ProductsRemoteTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -940,9 +940,10 @@ final class ProductsRemoteTests: XCTestCase {
// When
network.simulateResponse(requestUrlSuffix: "products", filename: "products-load-all-type-simple")

let products = try await remote.loadProductsForPointOfSale(for: sampleSiteID)
let pagedProducts = try await remote.loadProductsForPointOfSale(for: sampleSiteID)

// Then
let products = pagedProducts.items
XCTAssertEqual(products.count, expectedProductsFromResponse)
for product in products {
XCTAssertEqual(try XCTUnwrap(product).productType, .simple)
Expand Down Expand Up @@ -970,9 +971,10 @@ final class ProductsRemoteTests: XCTestCase {
// When
network.simulateResponse(requestUrlSuffix: "products", filename: "products-load-all-type-simple")

let products = try await remote.loadProductsForPointOfSale(for: sampleSiteID, pageNumber: initialPageNumber)
let pagedProducts = try await remote.loadProductsForPointOfSale(for: sampleSiteID, pageNumber: initialPageNumber)

// Then
let products = pagedProducts.items
XCTAssertEqual(products.count, expectedProductsFromResponse)
for product in products {
XCTAssertEqual(try XCTUnwrap(product).productType, .simple)
Expand All @@ -988,10 +990,46 @@ final class ProductsRemoteTests: XCTestCase {
// When
network.simulateResponse(requestUrlSuffix: "products", filename: "empty-data-array")

let products = try await remote.loadProductsForPointOfSale(for: sampleSiteID, pageNumber: pageNumber)
let pagedProducts = try await remote.loadProductsForPointOfSale(for: sampleSiteID, pageNumber: pageNumber)

// Then
XCTAssertEqual(products.count, expectedProductsFromResponse)
XCTAssertEqual(pagedProducts.items.count, expectedProductsFromResponse)
}

func test_loadProductsForPointOfSale_returns_hasMorePages_based_on_header_with_case_insensitive_name() async throws {
// Given
let remote = ProductsRemote(network: network)
network.responseHeaders = ["X-WP-TotalPages": "5"]
network.simulateResponse(requestUrlSuffix: "products", filename: "empty-data-array")

// When loading page 1 to 4
for pageNumber in 1...4 {
let pagedProducts = try await remote.loadProductsForPointOfSale(for: sampleSiteID, pageNumber: pageNumber)

// Then
XCTAssertTrue(pagedProducts.hasMorePages)
}

// When loading page 17
let pagedProducts = try await remote.loadProductsForPointOfSale(for: sampleSiteID, pageNumber: 5)

// Then
XCTAssertFalse(pagedProducts.hasMorePages)
}

func test_loadProductsForPointOfSale_returns_hasMorePages_true_when_header_is_not_set() async throws {
// Given
let remote = ProductsRemote(network: network)
network.responseHeaders = nil
network.simulateResponse(requestUrlSuffix: "products", filename: "empty-data-array")

// When loading the first 5 pages
for pageNumber in 1...5 {
let pagedProducts = try await remote.loadProductsForPointOfSale(for: sampleSiteID, pageNumber: pageNumber)

// Then
XCTAssertTrue(pagedProducts.hasMorePages)
}
}
}

Expand Down
6 changes: 6 additions & 0 deletions RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
*** PLEASE FOLLOW THIS FORMAT: [<priority indicator, more stars = higher priority>] <description> [<PR URL>]
*** Use [*****] to indicate smoke tests of all critical flows should be run on the final IPA before release (e.g. major library or OS update).
21.4
-----


21.3
-----
- [*] Jetpack Setup: Fixed an issue where the WordPress.com authentication fails when using a passwordless account that's already connected to Jetpack [https://github.com/woocommerce/woocommerce-ios/pull/14501]
Expand Down Expand Up @@ -41,6 +45,8 @@
- [*] Google Ads: Fix issue displaying campaign list for stores with existing campaigns from the hub menu entry point [https://github.com/woocommerce/woocommerce-ios/pull/14661]
- [*] Payments: Improved the card reader connection process by introducing a location permission pre-alert and earlier location request, ensuring greater clarity for users. [https://github.com/woocommerce/woocommerce-ios/pull/14672]
- [*] Payments: Tap to Pay is now the first payment method for eligible merchants. [https://github.com/woocommerce/woocommerce-ios/pull/14717]
- [**] Payments: Tap to Pay onboarding now includes detailed guidelines for accepting payments with Tap to Pay on iPhone and educating customers on using contactless payments. [https://github.com/woocommerce/woocommerce-ios/pull/14731]
- [**] Receipts: Email receipts can now be sent to customers after both successful and failed payments. This feature is available for merchants using WooCommerce version 9.5+ and WooPayments 8.6+. [https://github.com/woocommerce/woocommerce-ios/pull/14731].

21.2
-----
Expand Down
Loading

0 comments on commit 4236f99

Please sign in to comment.