From 9fd42f63019e66c803a740e1ac5647190cdc7e12 Mon Sep 17 00:00:00 2001 From: Lee Burrows Date: Tue, 28 Dec 2021 18:30:47 +0000 Subject: [PATCH] adds preselected shipping to pay session --- .../contents.xcworkspacedata | 7 ++++ Pay/PayAuthorization.swift | 4 +- Pay/PayCheckout.swift | 23 ++++++++-- Pay/PaySession.swift | 27 ++++++++++-- PayTests/Models/Models.swift | 11 ++++- PayTests/PayAuthorizationTests.swift | 6 ++- PayTests/PayCheckoutTests.swift | 38 ++++++++++++++++- PayTests/PaySessionTests.swift | 42 ++++++++++++++----- 8 files changed, 135 insertions(+), 23 deletions(-) create mode 100644 .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata diff --git a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Pay/PayAuthorization.swift b/Pay/PayAuthorization.swift index 7f79c112..a9fc3742 100644 --- a/Pay/PayAuthorization.swift +++ b/Pay/PayAuthorization.swift @@ -39,7 +39,7 @@ public struct PayAuthorization { public let billingAddress: PayAddress /// Shipping address that was selected by the user - public let shippingAddress: PayAddress + public let shippingAddress: PayAddress? /// Shipping rate that was selected by the user public let shippingRate: PayShippingRate? @@ -47,7 +47,7 @@ public struct PayAuthorization { // ---------------------------------- // MARK: - Init - // - internal init(paymentData: Data, billingAddress: PayAddress, shippingAddress: PayAddress, shippingRate: PayShippingRate?) { + internal init(paymentData: Data, billingAddress: PayAddress, shippingAddress: PayAddress?, shippingRate: PayShippingRate?) { self.token = String(data: paymentData, encoding: .utf8)! self.billingAddress = billingAddress self.shippingAddress = shippingAddress diff --git a/Pay/PayCheckout.swift b/Pay/PayCheckout.swift index 76f06a58..75f312e4 100644 --- a/Pay/PayCheckout.swift +++ b/Pay/PayCheckout.swift @@ -25,7 +25,6 @@ // import Foundation -import PassKit /// Encapsulates all fields required for invoking the Apple Pay /// dialog. It also creates summary items for cart total, @@ -43,16 +42,19 @@ public struct PayCheckout { public let lineItems: [PayLineItem] public let shippingAddress: PayAddress? public let shippingRate: PayShippingRate? - + public let availableShippingRates: [PayShippingRate]? + public let currencyCode: String + public let totalDuties: Decimal? public let subtotalPrice: Decimal public let totalTax: Decimal public let paymentDue: Decimal + public let total: Decimal // ---------------------------------- // MARK: - Init - // - public init(id: String, lineItems: [PayLineItem], giftCards: [PayGiftCard]?, discount: PayDiscount?, shippingDiscount: PayDiscount?, shippingAddress: PayAddress?, shippingRate: PayShippingRate?, currencyCode: String, subtotalPrice: Decimal, needsShipping: Bool, totalTax: Decimal, paymentDue: Decimal) { + public init(id: String, lineItems: [PayLineItem], giftCards: [PayGiftCard]?, discount: PayDiscount?, shippingDiscount: PayDiscount?, shippingAddress: PayAddress?, shippingRate: PayShippingRate?, availableShippingRates: [PayShippingRate]?, currencyCode: String, totalDuties: Decimal?, subtotalPrice: Decimal, needsShipping: Bool, totalTax: Decimal, paymentDue: Decimal, total: Decimal) { self.id = id self.lineItems = lineItems @@ -62,17 +64,24 @@ public struct PayCheckout { self.giftCards = giftCards self.discount = discount self.shippingDiscount = shippingDiscount + self.availableShippingRates = availableShippingRates self.currencyCode = currencyCode + self.totalDuties = totalDuties self.subtotalPrice = subtotalPrice self.totalTax = totalTax self.paymentDue = paymentDue self.hasLineItems = !lineItems.isEmpty self.needsShipping = needsShipping + self.total = total } } +#if canImport(PassKit) + +import PassKit + // ---------------------------------- // MARK: - PassKits - // @@ -111,6 +120,12 @@ internal extension PayCheckout { summaryItems.append(discount.amount.negative.summaryItemNamed(title)) } + // Duties + + if let duties = self.totalDuties { + summaryItems.append(duties.summaryItemNamed("DUTIES")) + } + // Taxes if self.totalTax > 0.0 { @@ -140,3 +155,5 @@ internal extension PayCheckout { return summaryItems } } + +#endif diff --git a/Pay/PaySession.swift b/Pay/PaySession.swift index 96e6fdcb..3180ca33 100644 --- a/Pay/PaySession.swift +++ b/Pay/PaySession.swift @@ -24,6 +24,8 @@ // THE SOFTWARE. // +#if canImport(PassKit) + import Foundation import PassKit @@ -129,6 +131,9 @@ public class PaySession: NSObject { /// Idempotency identifier of this session. public let identifier: String + + /// Shipping Contact can be set on the Pay Session, in order to pass in a pre-populated shipping address. + public var shippingContact: PKContact? internal var checkout: PayCheckout internal var shippingRates: [PayShippingRate] = [] @@ -185,8 +190,22 @@ public class PaySession: NSObject { request.countryCode = currency.countryCode request.currencyCode = currency.currencyCode request.merchantIdentifier = merchantID - request.requiredBillingAddressFields = .all - request.requiredShippingAddressFields = .all + request.shippingContact = shippingContact + request.requiredBillingContactFields = [.phoneNumber, .name, .postalAddress] + request.requiredShippingContactFields = checkout.needsShipping ? [.phoneNumber, .name, .postalAddress, .phoneNumber, .emailAddress] : [] + if let shippingRates = checkout.availableShippingRates { + self.shippingRates = shippingRates + } + + if checkout.needsShipping { + request.shippingMethods = checkout.availableShippingRates?.compactMap { + let method = PKShippingMethod(label: $0.title, amount: $0.price) + method.identifier = $0.handle + method.detail = "" + return method + } + } + request.supportedNetworks = self.acceptedCardBrands.paymentNetworks request.merchantCapabilities = [.capability3DS] request.paymentSummaryItems = checkout.summaryItems(for: self.shopName) @@ -229,7 +248,7 @@ extension PaySession: PKPaymentAuthorizationControllerDelegate { let authorization = PayAuthorization( paymentData: payment.token.paymentData, billingAddress: PayAddress(with: payment.billingContact!), - shippingAddress: PayAddress(with: payment.shippingContact!), + shippingAddress: payment.shippingContact != nil ? PayAddress(with: payment.shippingContact!) : nil, shippingRate: shippingRate ) @@ -369,3 +388,5 @@ extension PaySession: PKPaymentAuthorizationControllerDelegate { self.delegate?.paySessionDidFinish(self) } } + +#endif diff --git a/PayTests/Models/Models.swift b/PayTests/Models/Models.swift index b598cce5..bd7b45f7 100644 --- a/PayTests/Models/Models.swift +++ b/PayTests/Models/Models.swift @@ -24,6 +24,8 @@ // THE SOFTWARE. // +#if canImport(PassKit) + import Foundation import PassKit import Pay @@ -108,7 +110,7 @@ struct Models { return PayCurrency(currencyCode: "USD", countryCode: "US") } - static func createCheckout(requiresShipping: Bool = true, giftCards: [PayGiftCard]? = nil, discount: PayDiscount? = nil, shippingDiscount: PayDiscount? = nil, shippingAddress: PayAddress? = nil, shippingRate: PayShippingRate? = nil, empty: Bool = false, hasTax: Bool = true) -> PayCheckout { + static func createCheckout(requiresShipping: Bool = true, giftCards: [PayGiftCard]? = nil, discount: PayDiscount? = nil, shippingDiscount: PayDiscount? = nil, shippingAddress: PayAddress? = nil, shippingRate: PayShippingRate? = nil, emailAddress: String? = nil, duties: Decimal? = nil, empty: Bool = false, hasTax: Bool = true) -> PayCheckout { let lineItems = [ self.createLineItem1(), @@ -123,11 +125,14 @@ struct Models { shippingDiscount: shippingDiscount, shippingAddress: shippingAddress, shippingRate: shippingRate, + availableShippingRates: [shippingRate].compactMap { $0 }, currencyCode: "CAD", + totalDuties: duties, subtotalPrice: 44.0, needsShipping: requiresShipping, totalTax: hasTax ? 6.0 : 0.0, - paymentDue: 50.0 + paymentDue: 50.0, + total: 50 ) } @@ -188,3 +193,5 @@ struct Models { return (order, PayShippingRate.DeliveryRange(from: from, to: to)) } } + +#endif diff --git a/PayTests/PayAuthorizationTests.swift b/PayTests/PayAuthorizationTests.swift index 021c7aa5..c495825f 100644 --- a/PayTests/PayAuthorizationTests.swift +++ b/PayTests/PayAuthorizationTests.swift @@ -24,6 +24,8 @@ // THE SOFTWARE. // +#if canImport(PassKit) + import XCTest @testable import Pay @@ -46,7 +48,9 @@ class PayAuthorizationTests: XCTestCase { XCTAssertEqual(authorization.token, "123") XCTAssertEqual(authorization.billingAddress.firstName, address.firstName) - XCTAssertEqual(authorization.shippingAddress.firstName, address.firstName) + XCTAssertEqual(authorization.shippingAddress?.firstName, address.firstName) XCTAssertEqual(authorization.shippingRate!.handle, rate.handle) } } + +#endif diff --git a/PayTests/PayCheckoutTests.swift b/PayTests/PayCheckoutTests.swift index e56deea1..0c375a6a 100644 --- a/PayTests/PayCheckoutTests.swift +++ b/PayTests/PayCheckoutTests.swift @@ -24,6 +24,8 @@ // THE SOFTWARE. // +#if canImport(PassKit) + import XCTest @testable import Pay @@ -52,25 +54,31 @@ class PayCheckoutTests: XCTestCase { shippingDiscount: shipping, shippingAddress: address, shippingRate: rate, + availableShippingRates: [rate].compactMap { $0 }, currencyCode: "CAD", + totalDuties: 9.95, subtotalPrice: 30.0, needsShipping: true, totalTax: 15.0, - paymentDue: 35.0 + paymentDue: 35.0, + total: 35.0 ) XCTAssertEqual(checkout.id, "123") XCTAssertEqual(checkout.lineItems.count, 2) XCTAssertEqual(checkout.shippingAddress!.city, address.city) XCTAssertEqual(checkout.shippingRate!.handle, rate.handle) + XCTAssertEqual(checkout.availableShippingRates?.count, 1) XCTAssertEqual(checkout.giftCards!.first!.amount, 5.00) XCTAssertEqual(checkout.discount!.amount, 20.0) XCTAssertEqual(checkout.shippingDiscount!.amount, 10.0) XCTAssertEqual(checkout.currencyCode, "CAD") XCTAssertEqual(checkout.subtotalPrice, 30.0) + XCTAssertEqual(checkout.totalDuties, 9.95) XCTAssertEqual(checkout.needsShipping, true) XCTAssertEqual(checkout.totalTax, 15.0) XCTAssertEqual(checkout.paymentDue, 35.0) + XCTAssertEqual(checkout.total, 35.0) } // ---------------------------------- @@ -87,6 +95,32 @@ class PayCheckoutTests: XCTestCase { XCTAssertEqual(summaryItems[3].label, "SHOPIFY") } + func testSummaryItemsWithDuties() { + let checkout = Models.createCheckout(requiresShipping: false, duties: 24.99) + let summaryItems = checkout.summaryItems(for: self.shopName) + + XCTAssertEqual(summaryItems.count, 5) + XCTAssertEqual(summaryItems[0].label, "CART TOTAL") + XCTAssertEqual(summaryItems[1].label, "SUBTOTAL") + XCTAssertEqual(summaryItems[2].label, "DUTIES") + XCTAssertEqual(summaryItems[2].amount as Decimal, 24.99) + XCTAssertEqual(summaryItems[3].label, "TAXES") + XCTAssertEqual(summaryItems[4].label, "SHOPIFY") + } + + func testSummaryItemsWithDutiesAmountZero() { + let checkout = Models.createCheckout(requiresShipping: false, duties: 0) + let summaryItems = checkout.summaryItems(for: self.shopName) + + XCTAssertEqual(summaryItems.count, 5) + XCTAssertEqual(summaryItems[0].label, "CART TOTAL") + XCTAssertEqual(summaryItems[1].label, "SUBTOTAL") + XCTAssertEqual(summaryItems[2].label, "DUTIES") + XCTAssertEqual(summaryItems[2].amount as Decimal, 0) + XCTAssertEqual(summaryItems[3].label, "TAXES") + XCTAssertEqual(summaryItems[4].label, "SHOPIFY") + } + func testSummaryItemsWithShipping() { let address = Models.createAddress() let rate = Models.createShippingRate() @@ -220,3 +254,5 @@ class PayCheckoutTests: XCTestCase { XCTAssertEqual(summaryItems[6].amount as Decimal, giftCards[0].amount.negative) } } + +#endif diff --git a/PayTests/PaySessionTests.swift b/PayTests/PaySessionTests.swift index 75c16eaa..f6dc2f75 100644 --- a/PayTests/PaySessionTests.swift +++ b/PayTests/PaySessionTests.swift @@ -24,6 +24,8 @@ // THE SOFTWARE. // +#if canImport(PassKit) + import XCTest import PassKit @testable import Pay @@ -81,8 +83,8 @@ class PaySessionTests: XCTestCase { XCTAssertEqual(digitalRequest.countryCode, currency.countryCode) XCTAssertEqual(digitalRequest.currencyCode, currency.currencyCode) XCTAssertEqual(digitalRequest.merchantIdentifier, session.merchantID) - XCTAssertEqual(digitalRequest.requiredBillingAddressFields, [.all]) - XCTAssertEqual(digitalRequest.requiredShippingAddressFields, [.all]) + XCTAssertEqual(digitalRequest.requiredBillingContactFields, [.phoneNumber,.name, .postalAddress]) + XCTAssertEqual(digitalRequest.requiredShippingContactFields, []) XCTAssertEqual(digitalRequest.supportedNetworks, [.visa, .masterCard, .amex]) XCTAssertEqual(digitalRequest.merchantCapabilities, [.capability3DS]) XCTAssertFalse(digitalRequest.paymentSummaryItems.isEmpty) @@ -93,6 +95,20 @@ class PaySessionTests: XCTestCase { XCTAssertEqual(shippingRequest.requiredShippingAddressFields, [.all]) } + func testPaymentRequest_withShipping_fields() { + let checkout = Models.createCheckout() + let currency = Models.createCurrency() + let session = Models.createSession(checkout: checkout, currency: currency) + + let digitalCheckout = Models.createCheckout(requiresShipping: true) + let digitalRequest = session.paymentRequestUsing(digitalCheckout, currency: currency, merchantID: session.merchantID) + + + XCTAssertEqual(digitalRequest.requiredBillingContactFields, [.phoneNumber,.name, .postalAddress]) + XCTAssertEqual(digitalRequest.requiredShippingContactFields, [.phoneNumber, .name, .postalAddress, .emailAddress]) + + } + // ---------------------------------- // MARK: - Payment Authorization - // @@ -105,7 +121,7 @@ class PaySessionTests: XCTestCase { let token = MockPaymentToken(paymentMethod: payMethod) let payment = MockPayment(token: token, billingContact: contact, shippingContact: contact, shippingMethod: shippingRate.summaryItem) - let checkout = Models.createCheckout(requiresShipping: true) + let checkout = Models.createCheckout(requiresShipping: true, shippingRate: shippingRate) let delegate = self.setupDelegateForMockSessionWith(checkout) { session in session.shippingRates = [ shippingRate @@ -120,7 +136,7 @@ class PaySessionTests: XCTestCase { let tokenString = String(data: token.paymentData, encoding: .utf8) XCTAssertEqual(authorization.token, tokenString) XCTAssertEqual(authorization.billingAddress.city, contact.postalAddress!.city) - XCTAssertEqual(authorization.shippingAddress.city, contact.postalAddress!.city) + XCTAssertEqual(authorization.shippingAddress?.city, contact.postalAddress!.city) XCTAssertNotNil(authorization.shippingRate) XCTAssertEqual(authorization.shippingRate!.handle, shippingRate.handle) @@ -384,7 +400,7 @@ class PaySessionTests: XCTestCase { let shippingRate = Models.createShippingRate() let shippingMethod = shippingRate.summaryItem - let checkout = Models.createCheckout(requiresShipping: true) + let checkout = Models.createCheckout(requiresShipping: true, shippingRate: shippingRate) let delegate = self.setupDelegateForMockSessionWith(checkout) { session in session.checkout = checkout @@ -411,28 +427,30 @@ class PaySessionTests: XCTestCase { func testSelectShippingMethod() { - let shippingRate = Models.createShippingRate() - let shippingMethod = shippingRate.summaryItem + let preExistingShippingRate = Models.createShippingRate() + let updatedShippingRate = Models.createShippingRates().last! - let checkout = Models.createCheckout(requiresShipping: true) + let shippingMethod = preExistingShippingRate.summaryItem + + let checkout = Models.createCheckout(requiresShipping: true, shippingRate: preExistingShippingRate) let delegate = self.setupDelegateForMockSessionWith(checkout) { session in session.checkout = checkout session.shippingRates = [ - shippingRate + preExistingShippingRate ] } let updatedCheckout = Models.createCheckout( requiresShipping: true, - shippingRate: shippingRate + shippingRate: updatedShippingRate ) delegate.didSelectShippingRate = { session, selectedShippingRate, checkoutToUpdate, provide in provide(updatedCheckout) } - XCTAssertNil(checkout.shippingRate) + XCTAssertNotNil(checkout.shippingRate) let expectation = self.expectation(description: "") @@ -588,3 +606,5 @@ class PaySessionTests: XCTestCase { return delegate } } + +#endif