diff --git a/Samples/MobileBuyIntegration/MobileBuyIntegration/CartViewController.swift b/Samples/MobileBuyIntegration/MobileBuyIntegration/CartViewController.swift index 8de49a4d..dfd71f1e 100644 --- a/Samples/MobileBuyIntegration/MobileBuyIntegration/CartViewController.swift +++ b/Samples/MobileBuyIntegration/MobileBuyIntegration/CartViewController.swift @@ -155,9 +155,7 @@ extension CartViewController: CheckoutDelegate { func checkoutDidComplete(event: ShopifyCheckoutSheetKit.CheckoutCompletedEvent) { resetCart() - if let orderId = event.orderDetails?.id { - ShopifyCheckoutSheetKit.configuration.logger.log("Order created: \(orderId)") - } + ShopifyCheckoutSheetKit.configuration.logger.log("Order created: \(event.orderDetails.id)") } func checkoutDidCancel() { diff --git a/Samples/MobileBuyIntegration/MobileBuyIntegration/Localizable.xcstrings b/Samples/MobileBuyIntegration/MobileBuyIntegration/Localizable.xcstrings index 10e9ab42..d8658f3a 100644 --- a/Samples/MobileBuyIntegration/MobileBuyIntegration/Localizable.xcstrings +++ b/Samples/MobileBuyIntegration/MobileBuyIntegration/Localizable.xcstrings @@ -1,6 +1,45 @@ { "sourceLanguage" : "en", "strings" : { + "✓" : { + + }, + "App version" : { + + }, + "Clear" : { + + }, + "Clear logs" : { + + }, + "Events" : { + + }, + "Features" : { + + }, + "Logs" : { + + }, + "No logs available" : { + + }, + "OK" : { + "comment" : "Default action" + }, + "Prefill buyer information" : { + + }, + "Preload checkout" : { + + }, + "SDK version" : { + + }, + "Settings" : { + + }, "shopify_checkout_sheet_title" : { "comment" : "The title of the checkout sheet.", "extractionState" : "manual", @@ -24,7 +63,16 @@ } } } + }, + "Theme" : { + + }, + "Version" : { + + }, + "Web pixel events" : { + } }, "version" : "1.0" -} +} \ No newline at end of file diff --git a/Samples/SwiftUIExample/SwiftUIExample/CartView.swift b/Samples/SwiftUIExample/SwiftUIExample/CartView.swift index 85934b7a..21149391 100644 --- a/Samples/SwiftUIExample/SwiftUIExample/CartView.swift +++ b/Samples/SwiftUIExample/SwiftUIExample/CartView.swift @@ -72,7 +72,7 @@ struct CartView: View { } } .onComplete { checkout in - print("Checkout completed - Order id: \(String(describing: checkout.orderDetails?.id))") + print("Checkout completed - Order id: \(String(describing: checkout.orderDetails.id))") } .onFail { error in print(error) diff --git a/Sources/ShopifyCheckoutSheetKit/CheckoutBridge.swift b/Sources/ShopifyCheckoutSheetKit/CheckoutBridge.swift index 28682ecf..58e9308b 100644 --- a/Sources/ShopifyCheckoutSheetKit/CheckoutBridge.swift +++ b/Sources/ShopifyCheckoutSheetKit/CheckoutBridge.swift @@ -107,7 +107,7 @@ extension CheckoutBridge { self = .checkoutComplete(event: checkoutCompletedEvent) } catch { logger.logError(error, "Error decoding CheckoutCompletedEvent") - self = .checkoutComplete(event: CheckoutCompletedEvent()) + self = .checkoutComplete(event: emptyCheckoutCompletedEvent) } case "error": // needs to support .checkoutUnavailable by parsing error payload on body diff --git a/Sources/ShopifyCheckoutSheetKit/CheckoutCompletedEvent.swift b/Sources/ShopifyCheckoutSheetKit/CheckoutCompletedEvent.swift index 6d035921..0b40b095 100644 --- a/Sources/ShopifyCheckoutSheetKit/CheckoutCompletedEvent.swift +++ b/Sources/ShopifyCheckoutSheetKit/CheckoutCompletedEvent.swift @@ -26,175 +26,127 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SO import Foundation public struct CheckoutCompletedEvent: Decodable { - public let orderDetails: OrderDetails? - - enum CodingKeys: String, CodingKey { - case orderDetails - } - - init() { - orderDetails = nil - } + public let orderDetails: OrderDetails } extension CheckoutCompletedEvent { - public struct OrderDetails: Decodable { - public let id: String? - public let email: String? - public let phone: String? - public let cart: CartInfo? - public let paymentMethods: [OrderPaymentMethod]? - public let billingAddress: Address? - public let deliveries: [DeliveryInfo]? - - enum CodingKeys: String, CodingKey { - case id - case email - case phone - case cart - case paymentMethods - case billingAddress - case deliveries - } - } - - public struct CartInfo: Decodable { - public let lines: [CartLine]? - public let price: PriceSet? - - enum CodingKeys: String, CodingKey { - case lines - case price - } - } - - public struct OrderPaymentMethod: Decodable { - public let type: String? - public let details: [String: String?] - - enum CodingKeys: String, CodingKey { - case type - case details - } - } - public struct Address: Decodable { - public let referenceId: String? - public let name: String? - public let firstName: String? - public let lastName: String? public let address1: String? public let address2: String? public let city: String? public let countryCode: String? - public let zoneCode: String? - public let postalCode: String? + public let firstName: String? + public let lastName: String? + public let name: String? public let phone: String? + public let postalCode: String? + public let referenceId: String? + public let zoneCode: String? + } - enum CodingKeys: String, CodingKey { - case referenceId - case name - case firstName - case lastName - case address1 - case address2 - case city - case countryCode - case zoneCode - case postalCode - case phone - } + public struct CartInfo: Decodable { + public let lines: [CartLine] + public let price: Price + public let token: String } - public struct DeliveryInfo: Decodable { - public let method: String? - public let details: DeliveryDetails? + public struct CartLineImage: Decodable { + public let altText: String? + public let lg: String + public let md: String + public let sm: String + } - enum CodingKeys: String, CodingKey { - case method - case details - } + public struct CartLine: Decodable { + public let discounts: [Discount]? + public let image: CartLineImage? + public let merchandiseId: String? + public let price: Money + public let productId: String? + public let quantity: Int + public let title: String } public struct DeliveryDetails: Decodable { - public let name: String? - public let location: Address? public let additionalInfo: String? - - enum CodingKeys: String, CodingKey { - case name - case location - case additionalInfo - } + public let location: Address? + public let name: String? } - public struct PriceSet: Decodable { - public let subtotal: Money? - public let total: Money? - public let taxes: Money? - public let discounts: [Discount]? - public let shipping: Money? - - enum CodingKeys: String, CodingKey { - case subtotal - case total - case taxes - case discounts - case shipping - } + public struct DeliveryInfo: Decodable { + public let details: DeliveryDetails + public let method: String } public struct Discount: Decodable { - public let title: String? public let amount: Money? public let applicationType: String? - public let valueType: String? + public let title: String? public let value: Double? + public let valueType: String? } - public struct CartLineImage: Decodable { - public let sm: String? - public let md: String? - public let lg: String? - public let altText: String? + public struct OrderDetails: Decodable { + public let billingAddress: Address? + public let cart: CartInfo + public let deliveries: [DeliveryInfo]? + public let email: String? + public let id: String + public let paymentMethods: [PaymentMethod]? + public let phone: String? + } - enum CodingKeys: String, CodingKey { - case sm - case md - case lg - case altText - } + public struct PaymentMethod: Decodable { + public let details: [String: String?] + public let type: String } - public struct CartLine: Decodable { - public let title: String? - public let quantity: Int? - public let price: Money? - public let image: CartLineImage? - public let merchandiseId: String? - public let productId: String? + public struct Price: Decodable { public let discounts: [Discount]? - - enum CodingKeys: String, CodingKey { - case title - case quantity - case price - case image - case merchandiseId - case productId - case discounts - } + public let shipping: Money? + public let subtotal: Money? + public let taxes: Money? + public let total: Money? } public struct Money: Decodable { public let amount: Double? public let currencyCode: String? - - enum CodingKeys: String, CodingKey { - case amount - case currencyCode - } } } // swiftlint:enable identifier_name + +internal let emptyCheckoutCompletedEvent = CheckoutCompletedEvent( + orderDetails: CheckoutCompletedEvent.OrderDetails( + billingAddress: CheckoutCompletedEvent.Address( + address1: nil, + address2: nil, + city: nil, + countryCode: nil, + firstName: nil, + lastName: nil, + name: nil, + phone: nil, + postalCode: nil, + referenceId: nil, + zoneCode: nil + ), + cart: CheckoutCompletedEvent.CartInfo( + lines: [], + price: CheckoutCompletedEvent.Price( + discounts: nil, + shipping: CheckoutCompletedEvent.Money(amount: nil, currencyCode: nil), + subtotal: CheckoutCompletedEvent.Money(amount: nil, currencyCode: nil), + taxes: CheckoutCompletedEvent.Money(amount: nil, currencyCode: nil), + total: CheckoutCompletedEvent.Money(amount: nil, currencyCode: nil) + ), + token: "" + ), + deliveries: nil, + email: nil, + id: "", + paymentMethods: nil, + phone: nil + ) +) diff --git a/Sources/ShopifyCheckoutSheetKit/CheckoutCompletedEventDecoder.swift b/Sources/ShopifyCheckoutSheetKit/CheckoutCompletedEventDecoder.swift index 17629efe..9d0efc94 100644 --- a/Sources/ShopifyCheckoutSheetKit/CheckoutCompletedEventDecoder.swift +++ b/Sources/ShopifyCheckoutSheetKit/CheckoutCompletedEventDecoder.swift @@ -28,7 +28,7 @@ class CheckoutCompletedEventDecoder { let messageBody = try container.decode(String.self, forKey: .body) guard let data = messageBody.data(using: .utf8) else { - return CheckoutCompletedEvent() + return emptyCheckoutCompletedEvent } return try JSONDecoder().decode(CheckoutCompletedEvent.self, from: data) diff --git a/Tests/ShopifyCheckoutSheetKitTests/CheckoutBridgeTests.swift b/Tests/ShopifyCheckoutSheetKitTests/CheckoutBridgeTests.swift index e11f865c..01baa4f3 100644 --- a/Tests/ShopifyCheckoutSheetKitTests/CheckoutBridgeTests.swift +++ b/Tests/ShopifyCheckoutSheetKitTests/CheckoutBridgeTests.swift @@ -71,7 +71,7 @@ class CheckoutBridgeTests: XCTestCase { } func testDecodeSupportsCheckoutCompletedEvent() throws { - let body = "{\"orderDetails\":{\"id\":\"gid://shopify/OrderIdentity/8\",\"cart\":{\"lines\":[{\"quantity\":1,\"title\":\"Awesome Plastic Shoes\",\"price\":{\"amount\":87.99,\"currencyCode\":\"CAD\"},\"merchandiseId\":\"gid://shopify/ProductVariant/1\",\"productId\":\"gid://shopify/Product/1\"}],\"price\":{\"total\":{\"amount\":109.89,\"currencyCode\":\"CAD\"},\"subtotal\":{\"amount\":87.99,\"currencyCode\":\"CAD\"},\"taxes\":{\"amount\":0,\"currencyCode\":\"CAD\"},\"shipping\":{\"amount\":21.9,\"currencyCode\":\"CAD\"}}},\"billingAddress\":{\"city\":\"Cagalry\",\"countryCode\":\"CA\",\"postalCode\":\"T1X 0L3\",\"address1\":\"The Cloak & Dagger\",\"address2\":\"1st Street Southeast\",\"firstName\":\"Test\",\"lastName\":\"McTest\",\"name\":\"Test\",\"zoneCode\":\"AB\",\"coordinates\":{\"latitude\":45.416311,\"longitude\":-75.68683}},\"paymentMethods\":[{\"type\":\"direct\",\"details\":{\"amount\":\"109.89\",\"currency\":\"CAD\",\"brand\":\"BOGUS\",\"lastFourDigits\":\"1\"}}],\"deliveries\":[{\"method\":\"SHIPPING\",\"details\":{\"location\":{\"city\":\"Cagalry\",\"countryCode\":\"CA\",\"postalCode\":\"T1X 0L3\",\"address1\":\"The Cloak & Dagger\",\"address2\":\"1st Street Southeast\",\"firstName\":\"Test\",\"lastName\":\"McTest\",\"name\":\"Test\",\"zoneCode\":\"AB\",\"coordinates\":{\"latitude\":45.416311,\"longitude\":-75.68683}}}}]},\"orderId\":\"gid://shopify/OrderIdentity/19\",\"cart\":{\"lines\":[{\"quantity\":1,\"title\":\"Awesome Plastic Shoes\",\"price\":{\"amount\":87.99,\"currencyCode\":\"CAD\"},\"merchandiseId\":\"gid://shopify/ProductVariant/1\",\"productId\":\"gid://shopify/Product/1\"}],\"price\":{\"total\":{\"amount\":109.89,\"currencyCode\":\"CAD\"},\"subtotal\":{\"amount\":87.99,\"currencyCode\":\"CAD\"},\"taxes\":{\"amount\":0,\"currencyCode\":\"CAD\"},\"shipping\":{\"amount\":21.9,\"currencyCode\":\"CAD\"}}}}" + let body = "{\"orderDetails\":{\"id\":\"gid://shopify/OrderIdentity/8\",\"cart\":{\"lines\":[{\"quantity\":1,\"title\":\"Awesome Plastic Shoes\",\"price\":{\"amount\":87.99,\"currencyCode\":\"CAD\"},\"merchandiseId\":\"gid://shopify/ProductVariant/1\",\"productId\":\"gid://shopify/Product/1\"}],\"price\":{\"total\":{\"amount\":109.89,\"currencyCode\":\"CAD\"},\"subtotal\":{\"amount\":87.99,\"currencyCode\":\"CAD\"},\"taxes\":{\"amount\":0,\"currencyCode\":\"CAD\"},\"shipping\":{\"amount\":21.9,\"currencyCode\":\"CAD\"}},\"token\": \"fake-token\"},\"billingAddress\":{\"city\":\"Cagalry\",\"countryCode\":\"CA\",\"postalCode\":\"T1X 0L3\",\"address1\":\"The Cloak & Dagger\",\"address2\":\"1st Street Southeast\",\"firstName\":\"Test\",\"lastName\":\"McTest\",\"name\":\"Test\",\"zoneCode\":\"AB\",\"coordinates\":{\"latitude\":45.416311,\"longitude\":-75.68683}},\"paymentMethods\":[{\"type\":\"direct\",\"details\":{\"amount\":\"109.89\",\"currency\":\"CAD\",\"brand\":\"BOGUS\",\"lastFourDigits\":\"1\"}}],\"deliveries\":[{\"method\":\"SHIPPING\",\"details\":{\"location\":{\"city\":\"Cagalry\",\"countryCode\":\"CA\",\"postalCode\":\"T1X 0L3\",\"address1\":\"The Cloak & Dagger\",\"address2\":\"1st Street Southeast\",\"firstName\":\"Test\",\"lastName\":\"McTest\",\"name\":\"Test\",\"zoneCode\":\"AB\",\"coordinates\":{\"latitude\":45.416311,\"longitude\":-75.68683}}}}]},\"orderId\":\"gid://shopify/OrderIdentity/19\"}" .replacingOccurrences(of: "\"", with: "\\\"") .replacingOccurrences(of: "\n", with: "") @@ -89,16 +89,16 @@ class CheckoutBridgeTests: XCTestCase { return } - XCTAssertEqual("gid://shopify/OrderIdentity/8", event.orderDetails?.id) - XCTAssertEqual(1, event.orderDetails?.cart?.lines?.count) - XCTAssertEqual("gid://shopify/Product/1", event.orderDetails?.cart?.lines?[0].productId) - XCTAssertEqual(1, event.orderDetails?.paymentMethods?.count) - XCTAssertEqual("direct", event.orderDetails?.paymentMethods?[0].type) + XCTAssertEqual("gid://shopify/OrderIdentity/8", event.orderDetails.id) + XCTAssertEqual(1, event.orderDetails.cart.lines.count) + XCTAssertEqual("gid://shopify/Product/1", event.orderDetails.cart.lines[0].productId) + XCTAssertEqual(1, event.orderDetails.paymentMethods?.count) + XCTAssertEqual("direct", event.orderDetails.paymentMethods?[0].type) } - func testDecodeSupportsPartialCheckoutCompletedEvent() throws { + func testFailedDecodeReturnsEmptyEvent() throws { /// Missing orderId, taxes, billingAddress - let body = "{\"orderDetails\":{\"cart\":{\"lines\":[{\"quantity\":1,\"title\":\"Awesome Plastic Shoes\",\"price\":{\"amount\":87.99,\"currencyCode\":\"CAD\"},\"merchandiseId\":\"gid://shopify/ProductVariant/1\",\"productId\":\"gid://shopify/Product/1\"}],\"price\":{\"total\":{\"amount\":109.89,\"currencyCode\":\"CAD\"},\"subtotal\":{\"amount\":87.99,\"currencyCode\":\"CAD\"},\"shipping\":{\"amount\":21.9,\"currencyCode\":\"CAD\"}}},\"paymentMethods\":[{\"type\":\"direct\",\"details\":{\"amount\":\"109.89\",\"currency\":\"CAD\",\"brand\":\"BOGUS\",\"lastFourDigits\":\"1\"}}],\"deliveries\":[{\"method\":\"SHIPPING\",\"details\":{\"location\":{\"city\":\"Cagalry\",\"countryCode\":\"CA\",\"postalCode\":\"T1X 0L3\",\"address1\":\"The Cloak & Dagger\",\"address2\":\"1st Street Southeast\",\"firstName\":\"Test\",\"lastName\":\"McTest\",\"name\":\"Test\",\"zoneCode\":\"AB\",\"coordinates\":{\"latitude\":45.416311,\"longitude\":-75.68683}}}}]},\"orderId\":\"gid://shopify/OrderIdentity/19\",\"cart\":{\"lines\":[{\"quantity\":1,\"title\":\"Awesome Plastic Shoes\",\"price\":{\"amount\":87.99,\"currencyCode\":\"CAD\"},\"merchandiseId\":\"gid://shopify/ProductVariant/1\",\"productId\":\"gid://shopify/Product/1\"}],\"price\":{\"total\":{\"amount\":109.89,\"currencyCode\":\"CAD\"},\"subtotal\":{\"amount\":87.99,\"currencyCode\":\"CAD\"},\"taxes\":{\"amount\":0,\"currencyCode\":\"CAD\"},\"shipping\":{\"amount\":21.9,\"currencyCode\":\"CAD\"}}}}" + let body = "{\"orderDetails\":{\"cart\":{\"lines\":[{\"quantity\":1,\"title\":\"Awesome Plastic Shoes\",\"price\":{\"amount\":87.99,\"currencyCode\":\"CAD\"},\"merchandiseId\":\"gid://shopify/ProductVariant/1\",\"productId\":\"gid://shopify/Product/1\"}],\"price\":{\"total\":{\"amount\":109.89,\"currencyCode\":\"CAD\"},\"subtotal\":{\"amount\":87.99,\"currencyCode\":\"CAD\"},\"shipping\":{\"amount\":21.9,\"currencyCode\":\"CAD\"}},\"token\":\"fake-token\"},\"paymentMethods\":[{\"type\":\"direct\",\"details\":{\"amount\":\"109.89\",\"currency\":\"CAD\",\"brand\":\"BOGUS\",\"lastFourDigits\":\"1\"}}],\"deliveries\":[{\"method\":\"SHIPPING\",\"details\":{\"location\":{\"city\":\"Cagalry\",\"countryCode\":\"CA\",\"postalCode\":\"T1X 0L3\",\"address1\":\"The Cloak & Dagger\",\"address2\":\"1st Street Southeast\",\"firstName\":\"Test\",\"lastName\":\"McTest\",\"name\":\"Test\",\"zoneCode\":\"AB\",\"coordinates\":{\"latitude\":45.416311,\"longitude\":-75.68683}}}}]},\"orderId\":\"gid://shopify/OrderIdentity/19\",\"cart\":{\"lines\":[{\"quantity\":1,\"title\":\"Awesome Plastic Shoes\",\"price\":{\"amount\":87.99,\"currencyCode\":\"CAD\"},\"merchandiseId\":\"gid://shopify/ProductVariant/1\",\"productId\":\"gid://shopify/Product/1\"}],\"price\":{\"total\":{\"amount\":109.89,\"currencyCode\":\"CAD\"},\"subtotal\":{\"amount\":87.99,\"currencyCode\":\"CAD\"},\"taxes\":{\"amount\":0,\"currencyCode\":\"CAD\"},\"shipping\":{\"amount\":21.9,\"currencyCode\":\"CAD\"}}}}" .replacingOccurrences(of: "\"", with: "\\\"") .replacingOccurrences(of: "\n", with: "") @@ -111,25 +111,7 @@ class CheckoutBridgeTests: XCTestCase { return } - XCTAssertEqual(1, event.orderDetails?.cart?.lines?.count) - XCTAssertEqual("gid://shopify/Product/1", event.orderDetails?.cart?.lines?[0].productId) - XCTAssertEqual(1, event.orderDetails?.paymentMethods?.count) - XCTAssertEqual("direct", event.orderDetails?.paymentMethods?[0].type) - } - - func testDecodeGracefullyReturnsEmptyEvent() throws { - let mock = WKScriptMessageMock(body: "{\"name\":\"completed\",\"body\": \"INVALID JSON\"}") - let logger = MockLogger() - CheckoutBridge.logger = logger - let result = try CheckoutBridge.decode(mock) - XCTAssertEqual(logger.loggedMessage, "Error decoding CheckoutCompletedEvent") - - guard case .checkoutComplete(let event) = result else { - XCTFail("Expected empty completed event, got \(result)") - return - } - - XCTAssertNil(event.orderDetails) + XCTAssertEqual(event.orderDetails.id, "") } func testDecodeSupportsCheckoutUnavailableEvent() throws { diff --git a/Tests/ShopifyCheckoutSheetKitTests/CheckoutViewControllerTests.swift b/Tests/ShopifyCheckoutSheetKitTests/CheckoutViewControllerTests.swift index 084a144d..585acbf5 100644 --- a/Tests/ShopifyCheckoutSheetKitTests/CheckoutViewControllerTests.swift +++ b/Tests/ShopifyCheckoutSheetKitTests/CheckoutViewControllerTests.swift @@ -63,7 +63,7 @@ class CheckoutViewDelegateTests: XCTestCase { let two = CheckoutWebView.for(checkout: checkoutURL) XCTAssertEqual(one, two) - viewController.checkoutViewDidCompleteCheckout(event: ShopifyCheckoutSheetKit.CheckoutCompletedEvent()) + viewController.checkoutViewDidCompleteCheckout(event: emptyCheckoutCompletedEvent) let three = CheckoutWebView.for(checkout: checkoutURL) XCTAssertNotEqual(two, three) diff --git a/Tests/ShopifyCheckoutSheetKitTests/SwiftUITests.swift b/Tests/ShopifyCheckoutSheetKitTests/SwiftUITests.swift index 23b3af00..d920a18a 100644 --- a/Tests/ShopifyCheckoutSheetKitTests/SwiftUITests.swift +++ b/Tests/ShopifyCheckoutSheetKitTests/SwiftUITests.swift @@ -66,7 +66,7 @@ class CheckoutSheetTests: XCTestCase { func testOnComplete() { var actionCalled = false var actionData: CheckoutCompletedEvent? - let event = CheckoutCompletedEvent() + let event = emptyCheckoutCompletedEvent checkoutSheet.onComplete { event in actionCalled = true