Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
4d9d99a
fix: remove tax fields from cart queries unless its prepare for compl…
kieran-osgood-shopify Feb 24, 2025
cfffb45
refactor: inline guard statement
kieran-osgood-shopify Feb 24, 2025
086d7d3
fix: lint issue with var names
kieran-osgood-shopify Feb 24, 2025
c6822f8
chore: add doc comments on why we are using deprecated field
kieran-osgood-shopify Feb 24, 2025
bd0dd6f
refactor: ensure summary items include tax from correct fields
kieran-osgood-shopify Feb 24, 2025
f9281af
chore: add comments on why tax is separated from cart state
kieran-osgood-shopify Feb 24, 2025
332608a
refactor: undo cartResult rename
kieran-osgood-shopify Feb 24, 2025
25543dc
debug: ensure tax is pulled and improve taxc name
kieran-osgood-shopify Feb 27, 2025
3f8b629
add code signing identitys
kieran-osgood-shopify Feb 27, 2025
04b6f8e
fix: non-exempt encryption prompt on testflight
kieran-osgood-shopify Feb 27, 2025
55c0b98
rename buyerIdentityUpdate to cartBuyerIdentityUpdate
kieran-osgood-shopify Feb 27, 2025
cea7874
Merge branch 'main' into kieran-osgood/fix/deprecated-tax-fields
kieran-osgood-shopify Apr 7, 2025
5bf3035
Merge branch 'main' into kieran-osgood/fix/deprecated-tax-fields
kieran-osgood-shopify Jun 18, 2025
b82f649
Update Samples/MobileBuyIntegration/MobileBuyIntegration/CartManager.…
kieran-osgood-shopify Jun 20, 2025
4023855
fix: remove unnecessary info plist value
kieran-osgood-shopify Jun 20, 2025
4a22f56
refactor: mark discardableResult on fields we dont expect to be handled
kieran-osgood-shopify Jun 20, 2025
6efdb8b
refactor: improving doc comment and naming on cart with pending terms
kieran-osgood-shopify Jun 20, 2025
91ad2c3
Amend comment
kieran-osgood-shopify Jun 20, 2025
0f64d27
fix: missed discardableResult
kieran-osgood-shopify Jun 20, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = MobileBuyIntegration/MobileBuyIntegration.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = A7XGC83MZE;
Expand All @@ -520,6 +521,7 @@
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.shopify.example.MobileBuyIntegration;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
Expand All @@ -532,6 +534,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = MobileBuyIntegration/MobileBuyIntegration.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = A7XGC83MZE;
Expand All @@ -551,6 +554,7 @@
ONLY_ACTIVE_ARCH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.shopify.example.MobileBuyIntegration;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,26 +103,16 @@ extension ApplePayHandler: PKPaymentAuthorizationControllerDelegate {
print(CartManager.Errors.invariant(message: "Shipping method identifier is nil"))
return PKPaymentRequestShippingMethodUpdate(
paymentSummaryItems: PassKitFactory.shared.createPaymentSummaryItems(
cart: CartManager.shared.cart,
shippingMethod: nil
)
)
}

do {
_ = try await CartManager.shared.performCartSelectedDeliveryOptionsUpdate(
try await CartManager.shared.performCartSelectedDeliveryOptionsUpdate(
deliveryOptionHandle: identifier
)

let cart = try await CartManager.shared.performCartPrepareForCompletion()

let paymentRequestShippingContactUpdate = PKPaymentRequestShippingMethodUpdate(
paymentSummaryItems: PassKitFactory.shared.createPaymentSummaryItems(
cart: cart,
shippingMethod: shippingMethod
)
)
return paymentRequestShippingContactUpdate
} catch {
print(
CartManager.Errors.apiErrors(
Expand All @@ -131,10 +121,19 @@ extension ApplePayHandler: PKPaymentAuthorizationControllerDelegate {
"Check response from cartSelectedDeliveryOptionsUpdate or cartPrepareForCompletion \(error)"
)
)
}

do {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this ensures prepare is called even if updating the shipping options fails, which happens regularly at the moment due to a 500 server error atm

try await CartManager.shared.performCartPrepareForCompletion()

return PKPaymentRequestShippingMethodUpdate(
paymentSummaryItems: PassKitFactory.shared.createPaymentSummaryItems(
shippingMethod: shippingMethod
)
)
} catch {
return PKPaymentRequestShippingMethodUpdate(
paymentSummaryItems: PassKitFactory.shared.createPaymentSummaryItems(
cart: CartManager.shared.cart,
shippingMethod: nil
)
)
Expand All @@ -146,7 +145,7 @@ extension ApplePayHandler: PKPaymentAuthorizationControllerDelegate {
didSelectShippingContact contact: PKContact
) async -> PKPaymentRequestShippingContactUpdate {
do {
_ = try await CartManager.shared.performBuyerIdentityUpdate(
try await CartManager.shared.performCartBuyerIdentityUpdate(
contact: contact,
partial: true
)
Expand All @@ -155,12 +154,11 @@ extension ApplePayHandler: PKPaymentAuthorizationControllerDelegate {
firstDeliveryGroup: CartManager.shared.cart?.deliveryGroups.nodes.first
)

_ = try await CartManager.shared.performCartPrepareForCompletion()
try await CartManager.shared.performCartPrepareForCompletion()

return PKPaymentRequestShippingContactUpdate(
errors: [],
paymentSummaryItems: PassKitFactory.shared.createPaymentSummaryItems(
cart: CartManager.shared.cart,
shippingMethod: nil
),
shippingMethods: shippingMethods
Expand All @@ -170,7 +168,6 @@ extension ApplePayHandler: PKPaymentAuthorizationControllerDelegate {
return PKPaymentRequestShippingContactUpdate(
errors: [error],
paymentSummaryItems: PassKitFactory.shared.createPaymentSummaryItems(
cart: CartManager.shared.cart,
shippingMethod: nil
),
shippingMethods: []
Expand All @@ -185,29 +182,26 @@ extension ApplePayHandler: PKPaymentAuthorizationControllerDelegate {
/**
* Apply validations that make sense for your business requirements
*/
guard
let shippingContact = payment.shippingContact,
payment.shippingContact?.postalAddress?.isoCountryCode == "US"
else {
guard let shippingContact = payment.shippingContact else {
paymentStatus = .failure
return PassKitFactory.shared.createPKPaymentUSAdressError()
}

guard
let emailAddress = shippingContact.emailAddress,
emailAddress.isEmpty == false
else {
paymentStatus = .failure
return PassKitFactory.shared.createPKPaymentEmailError()
}

/**
* (Optional) If the user is a guest and you haven't set an email on buyerIdentity
* update the buyerIdentity with the shippingContact.email
*/
if appConfiguration.useVaultedState == false {
/**
* (Optional) If the user is a guest and you haven't set an email on buyerIdentity
* update the buyerIdentity with the shippingContact.email
*/
guard
let emailAddress = shippingContact.emailAddress,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

email guard moved in to vaulted state check as we require it via apple payment so its guaranteed to be set outside of this flow

emailAddress.isEmpty == false
else {
paymentStatus = .failure
return PassKitFactory.shared.createPKPaymentEmailError()
}

do {
_ = try await CartManager.shared.performBuyerIdentityUpdate(
try await CartManager.shared.performCartBuyerIdentityUpdate(
contact: shippingContact,
partial: true
)
Expand All @@ -219,14 +213,17 @@ extension ApplePayHandler: PKPaymentAuthorizationControllerDelegate {
}

do {
_ = try await CartManager.shared.performCartPaymentUpdate(payment: payment)
try await CartManager.shared.performCartPrepareForCompletion()
sleep(1)
try await CartManager.shared.performCartPaymentUpdate(payment: payment)
} catch {
print("[didAuthorizePayment][performCartPaymentUpdate][failure] \(error)")
paymentStatus = .failure
return PKPaymentAuthorizationResult(status: .failure, errors: [error])
}

do {
sleep(1)
let response = try await CartManager.shared.performCartSubmitForCompletion()
CartManager.shared.redirectUrl = response.redirectUrl
paymentStatus = .success
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,15 @@ class PassKitFactory {
)
}

paymentSummaryItems.append(
.init(
label: "Tax",
amount: NSDecimalNumber(decimal: cart.cost.totalTaxAmount?.amount ?? 0),
type: .final
if let tax = CartManager.shared.totalTaxAmount {
paymentSummaryItems.append(
.init(
label: "Tax",
amount: NSDecimalNumber(decimal: tax),
type: .final
)
)
)
}

paymentSummaryItems.append(
.init(
Expand Down Expand Up @@ -134,9 +136,9 @@ class PassKitFactory {
}

public func createPaymentSummaryItems(
cart: Storefront.Cart?, shippingMethod: PKShippingMethod?
shippingMethod: PKShippingMethod?
) -> [PKPaymentSummaryItem] {
guard let cart, !cart.lines.nodes.isEmpty else {
guard let cart = CartManager.shared.cart, !cart.lines.nodes.isEmpty else {
return []
}

Expand Down Expand Up @@ -165,11 +167,11 @@ class PassKitFactory {
}

// Null and 0 mean different things
if let amount = cart.cost.totalTaxAmount?.amount {
if let tax = CartManager.shared.totalTaxAmount {
paymentSummaryItems.append(
.init(
label: "Tax",
amount: NSDecimalNumber(decimal: amount),
amount: NSDecimalNumber(decimal: tax),
type: .final
)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,16 @@ class CartManager: ObservableObject {
public var redirectUrl: URL?

@Published var cart: Storefront.Cart?
/**
* Represents the cart's total tax amount (`cart.totalTaxAmount.amount`).
*
* This property is handled separately from the main cart object because:
* 1. The BuySDK throws errors when accessing unrequested properties
* 2. `totalTaxAmount` is deprecated in most cart operations (only available in `prepareForCompletion`)
*
* By isolating this property, we avoid SDK errors while maintaining access to tax data when needed.
*/
@Published var totalTaxAmount: Decimal?
@Published var isDirty: Bool = false

// MARK: Initializers
Expand Down Expand Up @@ -68,7 +78,7 @@ class CartManager: ObservableObject {
* Creates cart if no cart.id present, or adds line items to pre-existing cart
* Non-idempotent - subsequent calls for existing cartLine items will increase quantity by 1
*/
func performCartLinesAdd(variant: GraphQL.ID) async throws -> Storefront.Cart {
@discardableResult func performCartLinesAdd(variant: GraphQL.ID) async throws -> Storefront.Cart {
guard let cartId = cart?.id else {
return try await performCartCreate(items: [variant])
}
Expand All @@ -80,7 +90,7 @@ class CartManager: ObservableObject {
) {
$0.cartLinesAdd(cartId: cartId, lines: lines) {
$0.cart { $0.cartManagerFragment() }
.userErrors { $0.code().message() }
.userErrors { $0.code().message() }
}
}

Expand Down Expand Up @@ -151,14 +161,13 @@ class CartManager: ObservableObject {
}
}

func performBuyerIdentityUpdate(
@discardableResult func performCartBuyerIdentityUpdate(
contact: PKContact,
partial _: Bool
) async throws -> Storefront.Cart {
guard let cartId = cart?.id else {
throw Errors.invariant(message: "cart.id should be defined")
}

guard let address = contact.postalAddress else {
throw Errors.invariant(message: "contact.postalAddress is nil")
}
Expand Down Expand Up @@ -252,7 +261,7 @@ class CartManager: ObservableObject {
}
}

func performCartSelectedDeliveryOptionsUpdate(
@discardableResult func performCartSelectedDeliveryOptionsUpdate(
deliveryOptionHandle: String
) async throws -> Storefront.Cart {
guard let cartId = cart?.id else {
Expand Down Expand Up @@ -312,7 +321,7 @@ class CartManager: ObservableObject {
}
}

func performCartPaymentUpdate(
@discardableResult func performCartPaymentUpdate(
payment: PKPayment // REFACTOR: this method should just receive the decoded payment token
) async throws -> Storefront.Cart {
guard let cartId = cart?.id else {
Expand Down Expand Up @@ -344,7 +353,7 @@ class CartManager: ObservableObject {

let mutation = Storefront.buildMutation(inContext: CartManager.ContextDirective) {
$0.cartPaymentUpdate(cartId: cartId, payment: paymentInput) {
$0.cart { $0.cartManagerFragment() }
$0.cart { $0.cartPrepareForCompletionFragment() }
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this change adds tax pulling for the payment update which is still not deprecated

.userErrors {
$0.code().message()
}
Expand Down Expand Up @@ -376,7 +385,7 @@ class CartManager: ObservableObject {
}
}

func performCartPrepareForCompletion() async throws -> Storefront.Cart {
@discardableResult func performCartPrepareForCompletion() async throws -> Storefront.Cart {
guard let cartId = cart?.id else {
throw Errors.invariant(message: "cart.id should be defined")
}
Expand All @@ -386,10 +395,10 @@ class CartManager: ObservableObject {
) {
$0.cartPrepareForCompletion(cartId: cartId) {
$0.result {
$0.onCartStatusReady { $0.cart { $0.cartManagerFragment() } }
$0.onCartStatusReady { $0.cart { $0.cartPrepareForCompletionFragment() } }
$0.onCartThrottled { $0.pollAfter() }
$0.onCartStatusNotReady {
$0.cart { $0.cartManagerFragment() }
$0.cart { $0.cartPrepareForCompletionFragment() }
.errors { $0.code().message() }
}
}.userErrors { $0.code().message() }
Expand All @@ -410,17 +419,27 @@ class CartManager: ObservableObject {

guard
let result = payload.result as? Storefront.CartStatusReady,
let cart = result.cart
let cartWithResolvedPendingTerms = result.cart
else {
throw Errors.invariant(
message: "CartPrepareForCompletionResult is not CartStatusReady")
}

DispatchQueue.main.async {
self.cart = cart
self.cart = cartWithResolvedPendingTerms
/**
* IMPORTANT: Special handling for `totalTaxAmount`:
* - Deprecated field on cart, except from `cartPrepareForCompletion` mutation
* - Not included in standard cart fragments used by other mutations
* - Accessing `self.cart.cost.totalTaxAmount` will throw if cart was set by other mutations
* - Store separately to preserve the value when cart is updated by subsequent mutations
*/
if let tax = cartWithResolvedPendingTerms.cost.totalTaxAmount?.amount {
self.totalTaxAmount = tax
}
}

return cart
return cartWithResolvedPendingTerms
} catch {
throw Errors.apiErrors(requestName: "cartSubmitForCompletion", message: "\(error)")
}
Expand All @@ -435,7 +454,11 @@ class CartManager: ObservableObject {
$0.cartSubmitForCompletion(cartId: cartId, attemptToken: UUID().uuidString) {
$0.result {
$0.onSubmitSuccess { $0.redirectUrl() }
$0.onSubmitFailed { $0.checkoutUrl() }
$0.onSubmitFailed {
$0.checkoutUrl().errors {
$0.code().message()
}
}
$0.onSubmitAlreadyAccepted { $0.attemptId() }
$0.onSubmitThrottled { $0.pollAfter() }
}
Expand Down
Loading
Loading