Skip to content

Upgrade from v0.2 to v0.3

SDK v0.3 (version code 030) standardizes on the Storefront API Cart shape everywhere: the built-in adapter, custom adapters, and the onCartChange callback all consume and return the same data model. Because v0.2 (version code 024) was built around the legacy Ajax Cart format, you must complete this migration before switching the store’s active SDK version.

v0.3 keeps no compatibility aliases for the old method names. Replace them using the tables below. There is no compatibility layer.

Scope your migration first: if your storefront uses the default adapter and your onCartChange callback only triggers a read-only UI refresh without inspecting its cart argument, you can retain those two choices. Also make init a single call per page load, migrate PP/SP calls using the tables below, and replace post-write bindCart calls with syncCart. If you maintain a custom adapter or read the callback’s cart argument directly, complete all four steps below.

Areav0.2 (024)v0.3 (030)
Cart data modelLegacy Ajax CartStorefront API Cart
Cart IDcart.tokencart.id
Cart lines collectioncart.itemscart.lines.nodes
Line identifieritem.keyline.id (CartLine.id)
Variant identifier on addmerchandiseId string (internal callers could pass a numeric variantId, which the SDK normalized)merchandiseId (full ProductVariant GID, validated before the request)
Line attributesitem.propertiesline.attributes ({ key, value }[])
Monetary amountsInteger centsMoneyV2 (decimal string amount + currencyCode)
Line update methodchangeLineupdateLines(cartId, lines)
Write method return valueAjaxCart | null (updateAttributes may return void)The complete updated Cart. A missing or partially paged Cart is rejected
Cart sync entry pointbindCart(id) plus updateWidget() / updateSP() / updatePP()syncCart(cartId?) for first bind, cart switch, and same-cart refresh
PP product pricingRequired a separate productIdloadPlans(variantId, variantDetail?) resolving { context, options }.loadPlans(null) resolves null
PP product-page add to cartgetSelectedPlan() → write the main product → bindCart() → addPP({ variantId, quantity })pp.addToCart({ context?, addMainProduct }). The callback is (attributes, context) => { cartId, mainLineId }
Primary PP primitivesMultiple fragmented methodscanBePopup / getSelectedPlan / setSelectedPlan / showModal, and line-scoped addProtection({ mainLineId, selection })
PP cart quotesgetCartOffers(): Promise<PPCartOffer[]>getCartQuotes(): { enabled, quotes }. No candidates resolves { enabled: true, quotes: [] }
PP cart add-protectionaddFromCart({ lineKey, variantId }) writes directlyofferProtection({ mainLineId }) opens the modal before writing.addProtection takes an explicit selection from a storefront-rendered list
Removing PP coverageremovePP({ mainLineKey }) or removal by protection lineremoveProtection({ mainLineId }), resolving { status: 'removed' | 'already_unprotected' }
PP line identitieslineKey / ppLineKeylineId / mainLineId / protectionLineId (pass mainLineId to cancel protection)
SP details modalshowModal()showModal() (same name, and opening it does not change the switch, while the coverage button inside expresses intent for the active mode and is hidden in checkout+)
SP fee line writesaddSpFee() / removeSpFee() / declineSpFee()Same names: addSpFee() / removeSpFee() / declineSpFee() (call directly for a storefront-drawn toggle)
SP pre-checkout confirmationresolveSP() plus storefront button branchesresolveSP(). The two checkout+ buttons call addSpFee() / declineSpFee()
Initial cart bindingSeparate bindCart call requiredCan be passed directly to init({ cartId })
After mutating the same cartCall bindCart(cart.id) againCall syncCart(cart.id)
Repeated init callsOverwrote session optionsEvery repeated init is rejected: SDK_INIT_IN_PROGRESS while in progress, SDK_ALREADY_INITIALIZED after success, environment failure, a failed first sync, or a first sync held by an in-flight operation (phase=ready / environment_failed / sync_failed / busy). Use syncCart(cartId) for the first bind, refresh, cart switches, and a late-arriving cartId
Currency validationNo shared guardThe cart total currency must equal the configured currency. The built-in and custom adapters share the rule
SDK write completion signalonCartChange(cart?: AjaxCart), with a possibly empty argumentonCartChange(cart) reports confirmed cart facts only. It does not signal business success or a completed sync. The operation’s Promise decides the business result

Use the table below to search and replace removed or renamed methods across your codebase:

v0.2 callv0.3 replacement
bindCart(cartId)syncCart(cartId).syncCart() to refresh the bound cart
updateWidget(), updateSP(), updatePP()syncCart(cart.id). Call once after storefront cart writes to refresh both SP and PP
pp.getPlans({ productId, variantId, variantDetail })pp.loadPlans(variantId, variantDetail?)
pp.canBePopup(), pp.getSelectedPlan()canBePopup() is unchanged.getSelectedPlan() keeps its name but returns PPSelection | null (no hasAddPP / planID). Commit or clear a selection with pp.setSelectedPlan(selection | null, context?)
pp.showModal() returning { hasAddPP, planID, variantItem }pp.showModal() returning PPSelection | null
pp.addPP({ variantId, quantity })pp.addToCart({ addMainProduct }) (callback (attributes, context)), or the pp.addProtection({ mainLineId, selection }) primitive
pp.addFromCart({ lineKey, variantId })pp.offerProtection({ mainLineId }). Use pp.addProtection({ mainLineId, selection }) for a storefront-rendered list
pp.getCartOffers()pp.getCartQuotes(), which returns a Promise.await it. It returns { enabled, quotes } with { mainLineId, quantity, options } per entry
pp.getPPRelations()pp.getRelations(), returning { mainLineId, protectionLineId }[] where both values are CartLine.id
pp.removePP({ mainLineKey }) or no argumentspp.removeProtection({ mainLineId }), resolving { status: 'removed' | 'already_unprotected' }
sp.showModal()sp.showModal() (same name)
sp.addSpFee() / sp.declineSpFee() / sp.removeSpFee()Same names.addSpFee() now rejects when unavailable or the write fails, and declined is the literal true. Update call sites to handle rejections
sp.resolveSP() plus reading the switch in the storefrontsp.resolveSP() for checkout. The two checkout+ buttons call sp.addSpFee() / sp.declineSpFee()
adapter.changeLineadapter.updateLines

Field-level PP type and parameter changes:

v0.2 type / fieldv0.3 type / fieldNotes
PPShowModalResult (hasAddPP, planID, variantItem)PPSelection (mainVariantId, plan, mainLineAttributes)No hasAddPP / planID. Line attributes become AttributeInput[]
PPGetPlansParams (productId + variantId)PPPlansResult (context, options)context is a handle returned by loadPlans. Never construct it by hand
addPP({ variantId, quantity })PPAddToCartOptions (context?, addMainProduct(attributes, context))variantId is gone. Write the quantity in your own callback wrapper
addPP resolving PPPlanItem | nullPPAddToCartResult (cartId, mainLineId, protection: 'added' | 'skipped')Partial failures reject.protection: 'failed' no longer exists
getCartOffers(): Promise<PPCartOffer[]> (lineKey, quantity, plans)getCartQuotes(): { enabled, quotes } (mainLineId, quantity, options)Switch state is separate from “no candidates”.lineKey → mainLineId, plans → options
removePP({ mainLineKey }) / removeProtection({ protectionLineId })removeProtection({ mainLineId }), resolving PPRemoveProtectionResultThe parameter moves from the protection line to the main line. The result is status, not { removed }
addFromCart({ lineKey, variantId }) resolving { added: boolean }offerProtection({ mainLineId }) resolving PPOfferProtectionResult ({ status: 'added' | 'cancelled' })cancelled means the customer cancelled before any write. Failures reject

spWidgetSelector and ppWidgetSelector remain unchanged. getMode() and getQuote() keep their names, but getQuote() no longer returns inRange. An unavailable quote rejects with a specific error. The seven SP public methods keep their v0.2 names (addSpFee / removeSpFee / declineSpFee / resolveSP / getQuote / getMode / showModal). Their behavior follows the new contract: addSpFee() rejects when the order cannot be protected or the write fails and resolves { added: true, extId }.removeSpFee() / declineSpFee() resolve { removed } / { declined: true } and reject on failure.resolveSP() returns four results: added, removed, and two skipped variants distinguished by protection. It rejects in checkout+.showModal() requires at least one mounted panel.

  • Coordinate with your account manager (AM) to confirm the store’s current SDK version and scheduled cutover window. sdk.js is a versionless loader whose active runtime is controlled by store settings. Swapping local type files or refreshing the browser does not change the version the back office serves.
  • Back up your existing integration code, the store’s version setting, and the v0.2 type definitions so you can restore all three together if a rollback is needed.
  • Download the v0.3 type definitions. Let TypeScript surface all breaking changes rather than patching the old types with optional fields.
  • Confirm that window.xcottonConfig.currency, the cart, and the Storefront market all agree. v0.3 rejects carts whose currency differs.

Update field accesses according to the comparison table above, keeping these rules in mind:

  • A single variant can exist across multiple cart lines. Always target updates and removals using the CartLine.id from the latest cart snapshot, and note that modifying line attributes may assign a new line ID.
  • PP write and cancel parameters are now mainLineId (a Storefront CartLine.id).protectionLineId is returned by getRelations() for display only and is no longer a removeProtection argument.
  • Pass monetary amounts through unchanged as Storefront MoneyV2 objects. Do not multiply by 100 in your adapter or replace zero amounts with fallback prices. A line’s payable total lives at cost.totalAmount. The SDK handles unit and ID conversions for pricing internally.
  • If your storefront passes variantDetail to loadPlans, switch the price to the Storefront price.amount decimal string plus price.currencyCode, and the product owner to product.id.

Skip this step if you use the built-in Storefront adapter.

Implement getCart, addLines, updateLines, removeLines, and updateAttributes following Use your own cart API. Watch out for the most common pitfalls when moving from v0.2:

  • The SDK passes the currently bound cartId as the first parameter to every adapter method. Do not cache a separate “current cart ID” inside your adapter.
  • Every write method must return the complete post-mutation Cart with all line pages fetched.
  • getCart may return null only when the cart is confirmed not to exist. Network errors, auth failures, and rate limits must reject.
  • The cart total currency must equal the configured currency. The built-in and custom adapters share the same currency guard, so a mismatch rejects before quoting or writing.
  • On throttling, reject an Error with at least status: 429 or code: 'THROTTLED', optionally with retryAfterMs, so the SDK can enter its cooldown. Do not swallow the error or retry on your own.
  • A client timeout does not guarantee the server write failed. Disable automatic retries in your API layer, and on failure, re-read the cart rather than blindly replaying or rolling back the request.
  • After any storefront cart mutation, await syncCart(cart.id) before calling getCartQuotes(). When switching carts, pass the new cart ID. The SDK decides whether this is a switch or a same-cart refresh.
  • The storefront no longer tracks “the last bound cart ID” or a sync queue: syncCart covers the first bind, cart switches, and same-cart refreshes, and the SDK runs the internal steps in dependency order within one sync. A concurrent external call rejects with OPERATION_IN_PROGRESS.
  • Inside onCartChange, only refresh your storefront UI. Never call syncCart.
  • Each line-mutating SDK call triggers onCartChange at most once (including when a failed write is followed by a successful recovery read). Read-only calls do not fire onCartChange. A confirmed standalone cart-attribute write does fire it once.
  • The SDK does not await the Promise returned by onCartChange. Always catch async errors inside your callback.
  • Drive both the cart page and the cart drawer from the same shared cart state so that completing a protection write updates every visible cart view at once.

See Keep the cart in sync for full details.

  • PP product pricing (loadPlans) still works before a cart is created.
  • PP product-page add to cart collapses into one pp.addToCart({ context?, addMainProduct }) call: the callback receives (attributes, context), writes only the main product, and returns { cartId, mainLineId }. The SDK handles the selection, synchronization, and protection line. Success resolves only protection: 'added' | 'skipped'. When the main product was written but synchronization or protection failed, the call rejects PP_WRITE_CONFIRMATION_FAILED, and the main product must not be added again.
  • In-cart protection uses addProtection({ mainLineId, selection }) (selection is required) or the modal orchestration offerProtection({ mainLineId }). Removal uses removeProtection({ mainLineId }). None needs a manual sync or attribute rewrite.
  • SP checkout buttons branch by mode: cart and checkout call resolveSP() to align fee lines with the switch intent before navigating. The two checkout+ buttons call addSpFee() and declineSpFee() and do not navigate when the Promise rejects.
  • The SP details button calls showModal(). A storefront-drawn toggle calls addSpFee() / removeSpFee() directly, with no separate setter entry point.
  • Migrating the adapter does not alter the behavior of cart, checkout, or checkoutPlus.

When using the default adapter with a read-only UI refresh callback, v0.3 integration code looks like this. Product-page add to cart uses pp.addToCart. Other storefront writes use syncCart. The example uses reloadCartView, showCartError, addMainProduct, and quantity from your own storefront:

await window.xcotton!.init({
cartId: 'gid://shopify/Cart/your-cart-id',
onCartChange: () => {
void reloadCartView().catch(showCartError)
},
})
let cartId: string | undefined
try {
const result = await window.xcotton!.pp!.addToCart({
addMainProduct: async (attributes, context) => {
const { cart, mainLine } = await addMainProduct(
context.mainVariantId,
quantity,
attributes,
)
return { cartId: cart.id, mainLineId: mainLine.id }
},
})
cartId = result.cartId
} catch (error) {
// The main product may already be written; report it, never add it again.
showProtectionError(error)
}
// After any other storefront cart write:
if (cartId) await window.xcotton!.syncCart(cartId)

If your onCartChange callback inspects the cart argument directly, update it to read cart.lines.nodes and MoneyV2 amounts. Because the callback’s cart contains only the SDK’s field subset, continue fetching UI fields such as images and checkoutUrl through your own storefront query (see Cart).

Run this checklist both before and after switching the store’s SDK version, saving the final cart payload and browser screenshots for verification:

  • Open a product page with no cart: PP loads quotes normally, and rapid variant switching displays only the final variant’s quote.
  • Select a plan and add to cart: the main product is written once, and both the main product line and the protection line appear with matching quantities.
  • Add the same variant as two separate lines with different attributes, then update and remove each line. Confirm that the other line is unaffected.
  • Updating a main product’s quantity updates its linked protection line quantity, and removing the main product cleans up its protection line.
  • Adding protection to a line on the cart page removes that line’s “Add protection” button in the cart drawer at the same time.
  • Verify all three SP integration modes: fee line quantities are accurate, and a failed write blocks navigation to checkout.
  • Test once in the store’s base currency and once in a secondary presentment currency: matching currencies work, and a mismatch is rejected instead of written with the wrong currency.
  • Simulate a dropped write response: verify no duplicate items are added, the failure is not reported as success, and normal operation resumes after re-reading the cart.
  • Simulate throttling (a custom adapter rejecting an Error with status: 429, code: 'THROTTLED', and retryAfterMs) and confirm the SDK rejects requests during the cooldown instead of continuing to send them.

If you need to roll back, restore all three artifacts together: the store’s v0.2 version setting in the back office, your v0.2 storefront integration code, and the v0.2 type definitions. Never allow a v0.3 adapter or callback to process v0.2 cart payloads.