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
1 change: 1 addition & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ included:
excluded:
- platforms/swift/Samples
- platforms/swift/Sources/ShopifyCheckoutKit/Models.swift
- platforms/swift/Sources/ShopifyCheckoutProtocol/Generated

opt_in_rules:
- array_init
Expand Down
16 changes: 15 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ let package = Package(
.library(
name: "ShopifyAcceleratedCheckouts",
targets: ["ShopifyAcceleratedCheckouts"]
),
.library(
name: "ShopifyCheckoutProtocol",
targets: ["ShopifyCheckoutProtocol"]
)
],
dependencies: [
Expand All @@ -27,9 +31,13 @@ let package = Package(
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "ShopifyCheckoutProtocol",
path: "platforms/swift/Sources/ShopifyCheckoutProtocol"
),
.target(
name: "ShopifyCheckoutKit",
dependencies: [],
dependencies: ["ShopifyCheckoutProtocol"],
path: "platforms/swift/Sources/ShopifyCheckoutKit",
resources: [.process("Assets.xcassets")]
),
Expand All @@ -39,6 +47,12 @@ let package = Package(
path: "platforms/swift/Sources/ShopifyAcceleratedCheckouts",
resources: [.process("Localizable.xcstrings"), .process("Media.xcassets")]
),
.testTarget(
name: "ShopifyCheckoutProtocolTests",
dependencies: ["ShopifyCheckoutProtocol"],
path: "platforms/swift/Tests/ShopifyCheckoutProtocolTests",
resources: [.copy("Fixtures")]
),
.testTarget(
name: "ShopifyCheckoutKitTests",
dependencies: ["ShopifyCheckoutKit"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,9 @@
4EBBA7742A5F0CE200193E19 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4EBBA7732A5F0CE200193E19 /* Assets.xcassets */; };
4EBBA7772A5F0CE200193E19 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4EBBA7752A5F0CE200193E19 /* LaunchScreen.storyboard */; };
CB0000001234567A /* Apollo in Frameworks */ = {isa = PBXBuildFile; productRef = CB00000012345679 /* Apollo */; };
CB0000001234567B /* ApolloAPI in Frameworks */ = {isa = PBXBuildFile; productRef = CB00000012345680 /* ApolloAPI */; };
CB001E312F3CDA0300286F69 /* ShopifyCheckoutProtocol in Frameworks */ = {isa = PBXBuildFile; productRef = CB001E302F3CDA0300286F69 /* ShopifyCheckoutProtocol */; };
CB001E372F3CDBA600286F69 /* ShopifyCheckoutProtocol in Frameworks */ = {isa = PBXBuildFile; productRef = CB001E362F3CDBA600286F69 /* ShopifyCheckoutProtocol */; };
CB001E3A2F3CDBD600286F69 /* ShopifyCheckoutKit in Frameworks */ = {isa = PBXBuildFile; productRef = CB001E392F3CDBD600286F69 /* ShopifyCheckoutKit */; };
CB1B10B52E4CDDB0001713F8 /* ShopifyCheckoutKit in Frameworks */ = {isa = PBXBuildFile; productRef = CB1B10B42E4CDDB0001713F8 /* ShopifyCheckoutKit */; };
CB236DEA2FB2186C00F0D914 /* ShopifyCheckoutKit in Frameworks */ = {isa = PBXBuildFile; productRef = CBED2D502F3F5D1B00EC866A /* ShopifyCheckoutKit */; };
CB2370022FB21BF100F0D914 /* ShopifyAcceleratedCheckouts in Frameworks */ = {isa = PBXBuildFile; productRef = CB2370012FB21BF100F0D914 /* ShopifyAcceleratedCheckouts */; };
CB2370042FB21BF100F0D914 /* ShopifyCheckoutKit in Frameworks */ = {isa = PBXBuildFile; productRef = CB2370032FB21BF100F0D914 /* ShopifyCheckoutKit */; };
CB2370072FB21D1800F0D914 /* ShopifyCheckoutProtocol in Frameworks */ = {isa = PBXBuildFile; productRef = CB2370062FB21D1800F0D914 /* ShopifyCheckoutProtocol */; };
CBE9B3332F3DF82500E266EB /* ShopifyCheckoutProtocol in Frameworks */ = {isa = PBXBuildFile; productRef = CBE9B3322F3DF82500E266EB /* ShopifyCheckoutProtocol */; };
CBED2D4F2F3F5D1B00EC866A /* ShopifyAcceleratedCheckouts in Frameworks */ = {isa = PBXBuildFile; productRef = CBED2D4E2F3F5D1B00EC866A /* ShopifyAcceleratedCheckouts */; };
/* End PBXBuildFile section */

Expand All @@ -41,17 +35,11 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
CB236DEA2FB2186C00F0D914 /* ShopifyCheckoutKit in Frameworks */,
CB0000001234567A /* Apollo in Frameworks */,
CB2370042FB21BF100F0D914 /* ShopifyCheckoutKit in Frameworks */,
CBED2D4F2F3F5D1B00EC866A /* ShopifyAcceleratedCheckouts in Frameworks */,
CB0000001234567B /* ApolloAPI in Frameworks */,
CB001E312F3CDA0300286F69 /* ShopifyCheckoutProtocol in Frameworks */,
CB1B10B52E4CDDB0001713F8 /* ShopifyCheckoutKit in Frameworks */,
CB2370072FB21D1800F0D914 /* ShopifyCheckoutProtocol in Frameworks */,
CB001E3A2F3CDBD600286F69 /* ShopifyCheckoutKit in Frameworks */,
CB001E372F3CDBA600286F69 /* ShopifyCheckoutProtocol in Frameworks */,
CBE9B3332F3DF82500E266EB /* ShopifyCheckoutProtocol in Frameworks */,
CB2370022FB21BF100F0D914 /* ShopifyAcceleratedCheckouts in Frameworks */,
CBED2D4F2F3F5D1B00EC866A /* ShopifyAcceleratedCheckouts in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -134,16 +122,10 @@
name = MobileBuyIntegration;
packageProductDependencies = (
CB00000012345679 /* Apollo */,
CB1B10B42E4CDDB0001713F8 /* ShopifyCheckoutKit */,
CB00000012345680 /* ApolloAPI */,
CB001E302F3CDA0300286F69 /* ShopifyCheckoutProtocol */,
CB001E362F3CDBA600286F69 /* ShopifyCheckoutProtocol */,
CB001E392F3CDBD600286F69 /* ShopifyCheckoutKit */,
CBE9B3322F3DF82500E266EB /* ShopifyCheckoutProtocol */,
CB1B10B42E4CDDB0001713F8 /* ShopifyCheckoutKit */,
CBED2D4E2F3F5D1B00EC866A /* ShopifyAcceleratedCheckouts */,
CBED2D502F3F5D1B00EC866A /* ShopifyCheckoutKit */,
CB2370012FB21BF100F0D914 /* ShopifyAcceleratedCheckouts */,
CB2370032FB21BF100F0D914 /* ShopifyCheckoutKit */,
CB2370062FB21D1800F0D914 /* ShopifyCheckoutProtocol */,
);
productName = MobileBuyIntegration;
productReference = 4EBBA7672A5F0CE200193E19 /* MobileBuyIntegration.app */;
Expand Down Expand Up @@ -178,7 +160,6 @@
packageReferences = (
CB00000012345678 /* XCRemoteSwiftPackageReference "apollo-ios" */,
CB2370002FB21BF100F0D914 /* XCLocalSwiftPackageReference "../../../../../checkout-kit" */,
CB2370052FB21D1800F0D914 /* XCLocalSwiftPackageReference "../../../../protocol/languages/swift" */,
);
productRefGroup = 4EBBA7682A5F0CE200193E19 /* Products */;
projectDirPath = "";
Expand Down Expand Up @@ -466,10 +447,6 @@
isa = XCLocalSwiftPackageReference;
relativePath = "../../../../../checkout-kit";
};
CB2370052FB21D1800F0D914 /* XCLocalSwiftPackageReference "../../../../protocol/languages/swift" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = ../../../../protocol/languages/swift;
};
/* End XCLocalSwiftPackageReference section */

/* Begin XCRemoteSwiftPackageReference section */
Expand All @@ -489,46 +466,26 @@
package = CB00000012345678 /* XCRemoteSwiftPackageReference "apollo-ios" */;
productName = Apollo;
};
CB001E302F3CDA0300286F69 /* ShopifyCheckoutProtocol */ = {
CB00000012345680 /* ApolloAPI */ = {
isa = XCSwiftPackageProductDependency;
productName = ShopifyCheckoutProtocol;
package = CB00000012345678 /* XCRemoteSwiftPackageReference "apollo-ios" */;
productName = ApolloAPI;
};
CB001E362F3CDBA600286F69 /* ShopifyCheckoutProtocol */ = {
CB001E302F3CDA0300286F69 /* ShopifyCheckoutProtocol */ = {
isa = XCSwiftPackageProductDependency;
package = CB2370002FB21BF100F0D914 /* XCLocalSwiftPackageReference "../../../../../checkout-kit" */;
productName = ShopifyCheckoutProtocol;
};
CB001E392F3CDBD600286F69 /* ShopifyCheckoutKit */ = {
isa = XCSwiftPackageProductDependency;
productName = ShopifyCheckoutKit;
};
CB1B10B42E4CDDB0001713F8 /* ShopifyCheckoutKit */ = {
isa = XCSwiftPackageProductDependency;
package = CB2370002FB21BF100F0D914 /* XCLocalSwiftPackageReference "../../../../../checkout-kit" */;
productName = ShopifyCheckoutKit;
};
CB2370012FB21BF100F0D914 /* ShopifyAcceleratedCheckouts */ = {
isa = XCSwiftPackageProductDependency;
productName = ShopifyAcceleratedCheckouts;
};
CB2370032FB21BF100F0D914 /* ShopifyCheckoutKit */ = {
isa = XCSwiftPackageProductDependency;
productName = ShopifyCheckoutKit;
};
CB2370062FB21D1800F0D914 /* ShopifyCheckoutProtocol */ = {
isa = XCSwiftPackageProductDependency;
productName = ShopifyCheckoutProtocol;
};
CBE9B3322F3DF82500E266EB /* ShopifyCheckoutProtocol */ = {
isa = XCSwiftPackageProductDependency;
productName = ShopifyCheckoutProtocol;
};
CBED2D4E2F3F5D1B00EC866A /* ShopifyAcceleratedCheckouts */ = {
isa = XCSwiftPackageProductDependency;
package = CB2370002FB21BF100F0D914 /* XCLocalSwiftPackageReference "../../../../../checkout-kit" */;
productName = ShopifyAcceleratedCheckouts;
};
CBED2D502F3F5D1B00EC866A /* ShopifyCheckoutKit */ = {
isa = XCSwiftPackageProductDependency;
productName = ShopifyCheckoutKit;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 4EBBA75F2A5F0CE200193E19 /* Project object */;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,6 @@
<string>$(EMAIL)</string>
<key>Phone</key>
<string>$(PHONE)</string>
<key>EcAuthToken</key>
<string>$(EC_AUTH_TOKEN)</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Your location may be required to locate pickup points near you when you request this shipping option.</string>
<key>ITSAppUsesNonExemptEncryption</key>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class CartManager: ObservableObject {

public func preloadCheckout() {
if let url = cart?.checkoutURL, isDirty {
ShopifyCheckoutKit.preload(checkout: url.appendingEcParams())
ShopifyCheckoutKit.preload(checkout: url)
markCartAsReady()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class CheckoutCoordinator: UIViewController {

public func present(checkout url: URL) {
if let rootViewController = window?.topMostViewController() {
ShopifyCheckoutKit.present(checkout: url.appendingEcParams(), from: rootViewController, client: client)
ShopifyCheckoutKit.present(checkout: url, from: rootViewController, client: client)
root = rootViewController
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
MIT License

Copyright 2023 - Present, Shopify Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

import ShopifyCheckoutProtocol

enum CheckoutProtocolClient {
static let shared = CheckoutProtocol.Client()
.on(CheckoutProtocol.start) { checkout in
print("[UCP] ec.start: \(checkout.id)")
}
.on(CheckoutProtocol.complete) { checkout in
print("[UCP] ec.complete: \(checkout.order?.id ?? "unknown")")
CartManager.shared.resetCart()
}
.on(CheckoutProtocol.buyerChange) { checkout in
print("[UCP] ec.buyer.change: \(checkout.id)")
}
.on(CheckoutProtocol.lineItemsChange) { checkout in
print("[UCP] ec.line_items.change: \(checkout.id)")
}
.on(CheckoutProtocol.messagesChange) { checkout in
print("[UCP] ec.messages.change: \(checkout.id)")
}
.on(CheckoutProtocol.totalsChange) { checkout in
print("[UCP] ec.totals.change: \(checkout.id)")
}
.on(CheckoutProtocol.error) { error in
print("[UCP] ec.error: \(error.messages.first?.content ?? "(no message)")")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,6 @@ final class InfoDictionary: Sendable {
let customerAccountApiClientId: String?
let customerAccountApiShopId: String?

/// Embedded Checkout Protocol (optional)
let ecAuthToken: String?

var customerAccountApiRedirectUri: String? {
guard let shopId = customerAccountApiShopId, !shopId.isEmpty else {
return nil
Expand Down Expand Up @@ -90,51 +87,5 @@ final class InfoDictionary: Sendable {
// Customer Account API configuration (optional)
customerAccountApiClientId = infoPlist["CustomerAccountApiClientId"] as? String
customerAccountApiShopId = infoPlist["CustomerAccountApiShopId"] as? String

// Embedded Checkout Protocol configuration (optional)
ecAuthToken = infoPlist["EcAuthToken"] as? String
}
}

extension URL {
func appendingEcParams() -> URL {
var components = URLComponents(url: self, resolvingAgainstBaseURL: false)
var queryItems = components?.queryItems ?? []
queryItems.append(URLQueryItem(name: "ec_version", value: "2026-01-23"))
queryItems.append(
URLQueryItem(
name: "ec_delegate",
value: "fulfillment.address_change,payment.instruments_change,payment.credential"
)
)
if let token = InfoDictionary.shared.ecAuthToken, !token.isEmpty {
Self.validateJWTExpiration(token)
queryItems.append(URLQueryItem(name: "ec_auth", value: token))
}
components?.queryItems = queryItems
return components?.url ?? self
}

private static func validateJWTExpiration(_ token: String) {
let segments = token.split(separator: ".")
guard segments.count == 3 else { return }

var base64 = String(segments[1])
.replacingOccurrences(of: "-", with: "+")
.replacingOccurrences(of: "_", with: "/")
while base64.count % 4 != 0 {
base64.append("=")
}

guard let data = Data(base64Encoded: base64),
let payload = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let exp = payload["exp"] as? TimeInterval
else { return }

if Date().timeIntervalSince1970 >= exp {
fatalError(
"EC_AUTH_TOKEN expired at \(Date(timeIntervalSince1970: exp)). Renew it in Storefront.xcconfig."
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,7 @@ struct CartView: View {

@ObservedObject var cartManager: CartManager = .shared

private let client = CheckoutProtocol.Client()
.on(CheckoutProtocol.start) { checkout in
print("[UCP] Checkout started: \(checkout.id)")
}
.on(CheckoutProtocol.complete) { checkout in
print("[UCP] Checkout completed: \(checkout.order?.id ?? "unknown")")
CartManager.shared.resetCart()
}
private let client = CheckoutProtocolClient.shared

@AppStorage(AppStorageKeys.applePayStyle.rawValue)
var applePayStyle: ApplePayStyleOption = .automatic
Expand Down Expand Up @@ -115,7 +108,7 @@ struct CartView: View {
}
.sheet(isPresented: $showCheckoutSheet) {
if let url = cartManager.cart?.checkoutURL {
CheckoutSheet(checkout: url.appendingEcParams())
CheckoutSheet(checkout: url)
.connect(client)
.colorScheme(.automatic)
.onCancel {
Expand Down Expand Up @@ -283,7 +276,7 @@ struct CartLines: View {
updating = nil

if let checkoutUrl = cart.checkoutURL {
ShopifyCheckoutKit.preload(checkout: checkoutUrl.appendingEcParams())
ShopifyCheckoutKit.preload(checkout: checkoutUrl)
}
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ class CartViewController: UIViewController, UITableViewDelegate, UITableViewData
tableView.reloadData()

if let url = CartManager.shared.cart?.checkoutURL {
ShopifyCheckoutKit.preload(checkout: url.appendingEcParams())
ShopifyCheckoutKit.preload(checkout: url)
}
}

Expand Down Expand Up @@ -376,7 +376,7 @@ class CartViewController: UIViewController, UITableViewDelegate, UITableViewData
cell.quantityLabel.text = "\(cart.lines.nodes[indexPath.item].quantity)"

if let checkoutUrl = cart.checkoutURL {
ShopifyCheckoutKit.preload(checkout: checkoutUrl.appendingEcParams())
ShopifyCheckoutKit.preload(checkout: checkoutUrl)
}
}
}
Expand Down Expand Up @@ -404,7 +404,7 @@ class CartViewController: UIViewController, UITableViewDelegate, UITableViewData
@objc private func presentCheckout() {
guard let url = CartManager.shared.cart?.checkoutURL else { return }

ShopifyCheckoutKit.present(checkout: url.appendingEcParams(), from: self, client: client)
ShopifyCheckoutKit.present(checkout: url, from: self, client: client)
}

@objc private func resetCart() {
Expand Down
5 changes: 4 additions & 1 deletion platforms/swift/ShopifyCheckoutKit.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ Pod::Spec.new do |s|
s.default_subspecs = 'Core'

s.subspec 'Core' do |core|
core.source_files = 'Sources/ShopifyCheckoutKit/**/*.swift'
core.source_files = [
'Sources/ShopifyCheckoutKit/**/*.swift',
'Sources/ShopifyCheckoutProtocol/**/*.swift',
]
core.resource_bundles = {
'ShopifyCheckoutKit' => ['Sources/ShopifyCheckoutKit/Assets.xcassets']
}
Expand Down
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Ref Line 103) Worth checking: sendResponse now interpolates the encoded JSON-RPC payload directly into JavaScript, so window.EmbeddedCheckoutProtocol.postMessage(...) receives an object literal instead of the response string. Could this break things? Is ite better to escape the encoded JSON and call window.EmbeddedCheckoutProtocol.postMessage('...') so checkout receives a JSON-RPC string, not a JS object.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Going to look into this in a follow up 👍

Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ enum BridgeError: Swift.Error {
protocol CheckoutBridgeProtocol {
static func instrument(_ webView: WKWebView, _ instrumentation: InstrumentationPayload)
static func sendMessage(_ webView: WKWebView, messageName: String, messageBody: String?)
static func sendResponse(_ webView: WKWebView, messageBody: String)
}

enum CheckoutBridge: CheckoutBridgeProtocol {
Expand Down
Loading
Loading