Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions Networking/Networking.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,15 @@
DEC51AFB2769C66B009F3DF4 /* SystemStatusMapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC51AFA2769C66B009F3DF4 /* SystemStatusMapperTests.swift */; };
DEC51B02276AFB35009F3DF4 /* SystemStatus+DropinMustUsePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC51B01276AFB34009F3DF4 /* SystemStatus+DropinMustUsePlugin.swift */; };
E12552C526385B05001CEE70 /* ShippingLabelAddressValidationSuccess.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12552C426385B05001CEE70 /* ShippingLabelAddressValidationSuccess.swift */; };
E13BAD5328F8625600217769 /* InAppPurchasesRemoteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13BAD5228F8625600217769 /* InAppPurchasesRemoteTests.swift */; };
E16C59B528F8640B007D55BB /* InAppPurchaseOrderResultMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = E16C59B428F8640B007D55BB /* InAppPurchaseOrderResultMapper.swift */; };
E16C59B728F92782007D55BB /* iap-sample-receipt.json in Resources */ = {isa = PBXBuildFile; fileRef = E16C59B628F92782007D55BB /* iap-sample-receipt.json */; };
E16C59B928F927CA007D55BB /* iap-order-create.json in Resources */ = {isa = PBXBuildFile; fileRef = E16C59B828F927CA007D55BB /* iap-order-create.json */; };
E18152BE28F85B5B0011A0EC /* InAppPurchasesRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18152BD28F85B5B0011A0EC /* InAppPurchasesRemote.swift */; };
E18152C028F85D4A0011A0EC /* InAppPurchasesProductMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18152BF28F85D4A0011A0EC /* InAppPurchasesProductMapper.swift */; };
E18152C228F85E0A0011A0EC /* iap-products.json in Resources */ = {isa = PBXBuildFile; fileRef = E18152C128F85E0A0011A0EC /* iap-products.json */; };
E18152C428F85E5C0011A0EC /* InAppPurchasesProductsMapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18152C328F85E5C0011A0EC /* InAppPurchasesProductsMapperTests.swift */; };
E1A5C27228F93ED900081046 /* InAppPurchaseOrderResultMapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A5C27128F93ED900081046 /* InAppPurchaseOrderResultMapperTests.swift */; };
EE8A86F1286C5226003E8AA4 /* media-update-product-id-in-wordpress-site.json in Resources */ = {isa = PBXBuildFile; fileRef = EE8A86F0286C5226003E8AA4 /* media-update-product-id-in-wordpress-site.json */; };
EECB7EE8286555180028C888 /* media-update-product-id.json in Resources */ = {isa = PBXBuildFile; fileRef = EECB7EE7286555180028C888 /* media-update-product-id.json */; };
FE28F6E226840DED004465C7 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE28F6E126840DED004465C7 /* User.swift */; };
Expand Down Expand Up @@ -1384,6 +1393,15 @@
DEC51AFA2769C66B009F3DF4 /* SystemStatusMapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemStatusMapperTests.swift; sourceTree = "<group>"; };
DEC51B01276AFB34009F3DF4 /* SystemStatus+DropinMustUsePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SystemStatus+DropinMustUsePlugin.swift"; sourceTree = "<group>"; };
E12552C426385B05001CEE70 /* ShippingLabelAddressValidationSuccess.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShippingLabelAddressValidationSuccess.swift; sourceTree = "<group>"; };
E13BAD5228F8625600217769 /* InAppPurchasesRemoteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppPurchasesRemoteTests.swift; sourceTree = "<group>"; };
E16C59B428F8640B007D55BB /* InAppPurchaseOrderResultMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppPurchaseOrderResultMapper.swift; sourceTree = "<group>"; };
E16C59B628F92782007D55BB /* iap-sample-receipt.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "iap-sample-receipt.json"; sourceTree = "<group>"; };
E16C59B828F927CA007D55BB /* iap-order-create.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "iap-order-create.json"; sourceTree = "<group>"; };
E18152BD28F85B5B0011A0EC /* InAppPurchasesRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppPurchasesRemote.swift; sourceTree = "<group>"; };
E18152BF28F85D4A0011A0EC /* InAppPurchasesProductMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppPurchasesProductMapper.swift; sourceTree = "<group>"; };
E18152C128F85E0A0011A0EC /* iap-products.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "iap-products.json"; sourceTree = "<group>"; };
E18152C328F85E5C0011A0EC /* InAppPurchasesProductsMapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppPurchasesProductsMapperTests.swift; sourceTree = "<group>"; };
E1A5C27128F93ED900081046 /* InAppPurchaseOrderResultMapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppPurchaseOrderResultMapperTests.swift; sourceTree = "<group>"; };
EE8A86F0286C5226003E8AA4 /* media-update-product-id-in-wordpress-site.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "media-update-product-id-in-wordpress-site.json"; sourceTree = "<group>"; };
EECB7EE7286555180028C888 /* media-update-product-id.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "media-update-product-id.json"; sourceTree = "<group>"; };
F3F25DC15EC1D7C631169CB5 /* Pods_Networking.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Networking.framework; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -1612,6 +1630,7 @@
B524194821AC659500D6FC0A /* DevicesRemoteTests.swift */,
24F98C5F2502EF8200F49B68 /* FeatureFlagRemoteTests.swift */,
4513382127A8409000AE5E78 /* InboxNotesRemoteTests.swift */,
E13BAD5228F8625600217769 /* InAppPurchasesRemoteTests.swift */,
26B2F74824C55ACE0065CCC8 /* LeaderboardsRemoteTests.swift */,
020D07BF23D8587700FD9580 /* MediaRemoteTests.swift */,
B554FA8A2180B1D500C54DFF /* NotificationsRemoteTests.swift */,
Expand Down Expand Up @@ -1745,6 +1764,7 @@
45E461BB26837CC500011BF2 /* DataRemote.swift */,
B572F69921AC475C003EEFF0 /* DevicesRemote.swift */,
24F98C512502E79800F49B68 /* FeatureFlagsRemote.swift */,
E18152BD28F85B5B0011A0EC /* InAppPurchasesRemote.swift */,
4513381F27A8227F00AE5E78 /* InboxNotesRemote.swift */,
26B2F74024C1F2C10065CCC8 /* LeaderboardsRemote.swift */,
B5DAEFEF2180DD5A0002356A /* NotificationsRemote.swift */,
Expand Down Expand Up @@ -1954,6 +1974,9 @@
B524194621AC643900D6FC0A /* device-settings.json */,
24F98C612502EFF600F49B68 /* feature-flags-load-all.json */,
268B68FA24C87384007EBF1D /* leaderboards-products.json */,
E18152C128F85E0A0011A0EC /* iap-products.json */,
E16C59B628F92782007D55BB /* iap-sample-receipt.json */,
E16C59B828F927CA007D55BB /* iap-order-create.json */,
26B2F74624C55A6E0065CCC8 /* leaderboards-year.json */,
268B68FC24C87E37007EBF1D /* leaderboards-year-alt.json */,
B505F6D420BEE4E600BB1B69 /* me.json */,
Expand Down Expand Up @@ -2144,6 +2167,8 @@
24F98C572502EA8800F49B68 /* FeatureFlagMapper.swift */,
DE50295C28C6068B00551736 /* JetpackUserMapper.swift */,
AEF9458A27297FF6001DCCFB /* IgnoringResponseMapper.swift */,
E18152BF28F85D4A0011A0EC /* InAppPurchasesProductMapper.swift */,
E16C59B428F8640B007D55BB /* InAppPurchaseOrderResultMapper.swift */,
4513382327A951B300AE5E78 /* InboxNoteMapper.swift */,
45CCFCE527A2E3710012E8CB /* InboxNoteListMapper.swift */,
26B2F74424C5573F0065CCC8 /* LeaderboardListMapper.swift */,
Expand Down Expand Up @@ -2293,6 +2318,8 @@
45150A9F26837357006922EA /* CountryListMapperTests.swift */,
B524194221AC622500D6FC0A /* DotcomDeviceMapperTests.swift */,
AED8AEBB272A997500663FCC /* IgnoringResponseMapperTests.swift */,
E18152C328F85E5C0011A0EC /* InAppPurchasesProductsMapperTests.swift */,
E1A5C27128F93ED900081046 /* InAppPurchaseOrderResultMapperTests.swift */,
4513382527A96DB700AE5E78 /* InboxNoteMapperTests.swift */,
45CCFCE727A2E5020012E8CB /* InboxNoteListMapperTests.swift */,
B554FA922180C17200C54DFF /* NoteHashListMapperTests.swift */,
Expand Down Expand Up @@ -2583,6 +2610,7 @@
740211DF2193985A002248DA /* comment-moderate-spam.json in Resources */,
B5147876211B9227007562E5 /* broken-orders-mark-2.json in Resources */,
3158FE6C26129D2E00E566B9 /* wcpay-account-rejected-terms-of-service.json in Resources */,
E16C59B928F927CA007D55BB /* iap-order-create.json in Resources */,
31054728262E2FEE00C5C02B /* wcpay-payment-intent-canceled.json in Resources */,
31A451CC27863A2E00FE81AA /* stripe-account-rejected-fraud.json in Resources */,
31A451D827863A2E00FE81AA /* stripe-account-restricted-overdue.json in Resources */,
Expand Down Expand Up @@ -2710,6 +2738,7 @@
31054714262E2F3B00C5C02B /* wcpay-payment-intent-requires-payment-method.json in Resources */,
2683D71024456EE4002A1589 /* categories-extra.json in Resources */,
A69FE19D2588D70E0059A96B /* order-with-deleted-refunds.json in Resources */,
E18152C228F85E0A0011A0EC /* iap-products.json in Resources */,
DE2095C127966EC800171F1C /* coupon-reports.json in Resources */,
453305F52459ED2700264E50 /* site-post-update.json in Resources */,
CECC759E23D6231A00486676 /* order-560-all-refunds.json in Resources */,
Expand Down Expand Up @@ -2739,6 +2768,7 @@
CC9A254626442CA7005DE56E /* shipping-label-eligibility-failure.json in Resources */,
4524CD9C242CEFAB00B2F20A /* product-on-sale-with-empty-sale-price.json in Resources */,
020220E223966CD900290165 /* product-shipping-classes-load-one.json in Resources */,
E16C59B728F92782007D55BB /* iap-sample-receipt.json in Resources */,
3158FE7826129DF300E566B9 /* wcpay-account-restricted.json in Resources */,
45A4B85625D2E75300776FB4 /* shipping-label-address-validation-success.json in Resources */,
457FC68C2382B2FD00B41B02 /* product-update.json in Resources */,
Expand Down Expand Up @@ -3055,10 +3085,12 @@
2685C0FE263B5D8900D9EE97 /* AddOnGroupRemote.swift in Sources */,
450106912399B2C800E24722 /* TaxClassListMapper.swift in Sources */,
B963A5CC2853870000EFADA0 /* OrderItemRefundMetaData.swift in Sources */,
E18152BE28F85B5B0011A0EC /* InAppPurchasesRemote.swift in Sources */,
0329CF9B27A82E19008AFF91 /* WCPayCharge.swift in Sources */,
74749B97224134FF005C4CF2 /* ProductMapper.swift in Sources */,
26455E2A25F669F0008A1D32 /* ProductAttributeTermMapper.swift in Sources */,
0359EA1127AAC6740048DE2D /* WCPayPaymentMethodType.swift in Sources */,
E16C59B528F8640B007D55BB /* InAppPurchaseOrderResultMapper.swift in Sources */,
026CF61A237D607A009563D4 /* ProductVariationAttribute.swift in Sources */,
D8FBFF1A22D4DF7A006E3336 /* OrderStatsV4.swift in Sources */,
74A1D26B21189B8100931DFA /* SiteVisitStatsItem.swift in Sources */,
Expand Down Expand Up @@ -3141,6 +3173,7 @@
CE0A0F1B223989670075ED8D /* ProductsRemote.swift in Sources */,
0359EA1527AAC7460048DE2D /* WCPayCardBrand.swift in Sources */,
2665032E261F4FBF0079A159 /* ProductAddOnOption.swift in Sources */,
E18152C028F85D4A0011A0EC /* InAppPurchasesProductMapper.swift in Sources */,
028296F7237D588700E84012 /* ProductVariation.swift in Sources */,
DEC51AEF2768A628009F3DF4 /* SystemStatus+Database.swift in Sources */,
);
Expand All @@ -3158,6 +3191,7 @@
45B204BA24890A8C00FE6526 /* ProductCategoryMapperTests.swift in Sources */,
74749B9522413119005C4CF2 /* ProductsRemoteTests.swift in Sources */,
B524194321AC622500D6FC0A /* DotcomDeviceMapperTests.swift in Sources */,
E1A5C27228F93ED900081046 /* InAppPurchaseOrderResultMapperTests.swift in Sources */,
45150AA026837357006922EA /* CountryListMapperTests.swift in Sources */,
74D5BECE217E0F98007B0348 /* SiteSettingsRemoteTests.swift in Sources */,
D8FBFF1C22D51C34006E3336 /* OrderStatsRemoteV4Tests.swift in Sources */,
Expand Down Expand Up @@ -3221,6 +3255,7 @@
2670C3FC270F4E06002FE931 /* SiteListMapperTests.swift in Sources */,
025CA2C6238F4F3500B05C81 /* ProductShippingClassRemoteTests.swift in Sources */,
D800DA0A25EFEB9C001E13CE /* WCPayRemoteTests.swift in Sources */,
E13BAD5328F8625600217769 /* InAppPurchasesRemoteTests.swift in Sources */,
CC851D1425E52AB500249E9C /* Decimal+ExtensionsTests.swift in Sources */,
B554FA8B2180B1D500C54DFF /* NotificationsRemoteTests.swift in Sources */,
B518662A20A09C6F00037A38 /* OrdersRemoteTests.swift in Sources */,
Expand Down Expand Up @@ -3267,6 +3302,7 @@
740211E121939908002248DA /* CommentRemoteTests.swift in Sources */,
B57B1E6A21C925280046E764 /* DotcomValidatorTests.swift in Sources */,
B567AF3020A0FB8F00AB6C62 /* DotcomRequestTests.swift in Sources */,
E18152C428F85E5C0011A0EC /* InAppPurchasesProductsMapperTests.swift in Sources */,
D8A2849A25FBB2E70019A84B /* ProductAttributeTermListMapperTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
28 changes: 28 additions & 0 deletions Networking/Networking/Mapper/InAppPurchaseOrderResultMapper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import Foundation


/// Mapper: IAP Order Creation Result
///
struct InAppPurchaseOrderResultMapper: Mapper {

/// (Attempts) to extract the order ID from a given JSON Encoded response.
///
func map(response: Data) throws -> Int {

let dictionary = try JSONDecoder().decode([String: AnyDecodable].self, from: response)
guard let orderId = (dictionary[Constants.orderIdKey]?.value as? Int) else {
throw Error.parseError
}
return orderId
}
}

private extension InAppPurchaseOrderResultMapper {
enum Constants {
static let orderIdKey: String = "order_id"
}

enum Error: Swift.Error {
case parseError
}
}
13 changes: 13 additions & 0 deletions Networking/Networking/Mapper/InAppPurchasesProductMapper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import Foundation

/// Mapper: IAP Product
///
struct InAppPurchasesProductMapper: Mapper {
/// (Attempts) to convert a dictionary into a list of product identifiers.
///
func map(response: Data) throws -> [String] {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(DateFormatter.Defaults.dateTimeFormatter)
return try decoder.decode([String].self, from: response)
}
}
119 changes: 119 additions & 0 deletions Networking/Networking/Remote/InAppPurchasesRemote.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import Alamofire
import Foundation

/// In-app Purchases Endpoints
///
public class InAppPurchasesRemote: Remote {
/// Retrieves a list of product identifiers available for purchase
///
/// - Parameters:
/// - completion: Closure to be executed upon completion
///
public func loadProducts(completion: @escaping (Swift.Result<[String], Error>) -> Void) {
let dotComRequest = DotcomRequest(wordpressApiVersion: .wpcomMark2, method: .get, path: Constants.productsPath)
let request = augmentedRequestWithAppId(dotComRequest)
let mapper = InAppPurchasesProductMapper()
enqueue(request, mapper: mapper, completion: completion)
}

/// Creates a new order for a new In-app Purchase
/// - Parameters:
/// - siteID: Site the purchase is for
/// - price: An integer representation of the price in cents (5.99 -> 599)
/// - productIdentifier: The IAP sku for the product
/// - appStoreCountryCode: The country of the user's App Store
/// - receiptData: The transaction receipt from Apple
/// - completion: Closure to be executed upon completion
///
public func createOrder(
for siteID: Int64,
price: Int,
productIdentifier: String,
appStoreCountryCode: String,
receiptData: Data,
completion: @escaping (Swift.Result<Int, Error>) -> Void) {
let parameters: [String: Any] = [
Constants.siteIDKey: siteID,
Constants.priceKey: price,
Constants.productIDKey: productIdentifier,
Constants.appStoreCountryCodeKey: appStoreCountryCode,
Constants.receiptDataKey: receiptData.base64EncodedString()
]
let dotComRequest = DotcomRequest(wordpressApiVersion: .wpcomMark2, method: .post, path: Constants.ordersPath, parameters: parameters)
let request = augmentedRequestWithAppId(dotComRequest)
let mapper = InAppPurchaseOrderResultMapper()
enqueue(request, mapper: mapper, completion: completion)
}
}

// MARK: - Async methods

public extension InAppPurchasesRemote {
/// Retrieves a list of product identifiers available for purchase
///
/// - Returns: a list of product identifiers.
///
func loadProducts() async throws -> [String] {
try await withCheckedThrowingContinuation { continuation in
loadProducts { result in
continuation.resume(with: result)
}
}
}

/// Creates a new order for a new In-app Purchase
/// - Parameters:
/// - siteID: Site the purchase is for
/// - price: An integer representation of the price in cents (5.99 -> 599)
/// - productIdentifier: The IAP sku for the product
/// - appStoreCountryCode: The country of the user's App Store
/// - receiptData: The transaction receipt from Apple
///
/// - Returns: The ID of the created order.
///
func createOrder(
for siteID: Int64,
price: Int,
productIdentifier: String,
appStoreCountryCode: String,
receiptData: Data
) async throws -> Int {
try await withCheckedThrowingContinuation { continuation in
createOrder(
for: siteID,
price: price,
productIdentifier: productIdentifier,
appStoreCountryCode: appStoreCountryCode,
receiptData: receiptData
) { result in
continuation.resume(with: result)
}
}
}
}

private extension InAppPurchasesRemote {
func augmentedRequestWithAppId(_ request: URLRequestConvertible) -> URLRequestConvertible {
guard let bundleIdentifier = Bundle.main.bundleIdentifier,
var augmented = try? request.asURLRequest() else {
return request
}

augmented.setValue(bundleIdentifier, forHTTPHeaderField: "X-APP-ID")

return augmented
}
}

private extension InAppPurchasesRemote {
enum Constants {
static let productsPath = "iap/products"
static let ordersPath = "iap/orders"

static let siteIDKey = "site_id"
static let priceKey = "price"
static let productIDKey = "product_id"
static let appStoreCountryCodeKey = "appstore_country"
static let receiptDataKey = "apple_receipt"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import XCTest
@testable import Networking

final class InAppPurchasesOrderResultMapperTests: XCTestCase {
func test_iap_order_creation_is_decoded_from_json_response() throws {
// Given
let jsonData = try XCTUnwrap(Loader.contentsOf("iap-order-create"))
let expectedOrderId = 12345

// When
let orderId = try InAppPurchaseOrderResultMapper().map(response: jsonData)

// Then
assertEqual(expectedOrderId, orderId)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import XCTest
@testable import Networking

final class InAppPurchasesProductsMapperTests: XCTestCase {
func test_iap_products_list_is_decoded_from_json_response() throws {
// Given
let jsonData = try XCTUnwrap(Loader.contentsOf("iap-products"))
let expectedProductIdentifiers = [
"woocommerce_entry_monthly",
"woocommerce_entry_yearly"]

// When
let products = try InAppPurchasesProductMapper().map(response: jsonData)

// Then
assertEqual(expectedProductIdentifiers, products)
}
}
Loading