Product protection modal
When a customer clicks “Add to cart” without selecting a plan in the PP panel, the SDK can present a modal prompting them to choose a plan. If they dismiss the modal, the main product is still added to the cart normally. The modal does not require a cart. It opens on the product page whenever plans are available.
You can trigger the modal in two ways:
| Trigger | Method | Controlled by your account manager (AM)‘s setting |
|---|---|---|
| Automatically on add to cart | pp.addToCart() | Yes. When disabled in the back office, no plan means protection: 'skipped' |
| Explicitly from a custom button | pp.showModal() | No. Not controlled by the back-office modal setting |
A standard add-to-cart needs no modal branch at all: pp.addToCart() already resolves the selection and opens the modal when configured, resolving protection: 'skipped' if the customer cancels. Use pp.showModal() only when you want to invite the customer to pick a plan earlier.
Before you start
Section titled “Before you start”- You have implemented the add-to-cart flow in Add product protection. This page explains how the modal fits into that flow. The add-to-cart call itself does not change.
- The product page has called
loadPlans(variantId)for the currently selected variant. - The custom button needs a storefront function such as
showSelectedPlan(plan)to display the chosen plan.
- Standard add to cart: call
pp.addToCart({ addMainProduct })directly. The callback is(attributes, context)and novariantIdis passed. The SDK awaits the quote for this variant: a plan already selected in the panel is used immediately without a modal. Otherwise the back-office setting decides whether to open the modal. - Handle the result: if the customer dismisses the modal, the result still resolves with
protection: 'skipped', and the main product is added without a protection line. All other results match Add product protection. - Custom trigger button: to open the modal from a dedicated “Choose protection plan” button, call
pp.showModal()(orpp.showModal(context)). Confirming resolves withPPSelection. Cancelling, closing, or pressing Escape resolves withnulland clears the PDP selection. No available plan rejects withPP_NO_OPTIONS. Switching variants or leaving the page while waiting or while the modal is open rejects withPP_CONTEXT_CHANGEDand is not treated as a cancellation. - Reuse the chosen plan: a plan confirmed in the modal stays in the panel state, so the next
pp.addToCart()resolves the same selection. Your storefront neither stores the choice nor rewrites the main product attributes.
Both entry points capture the product context when called. Switching variants or leaving the page while waiting for a quote (including A→B→A) rejects with PP_CONTEXT_CHANGED. Any re-quote after the modal opens closes it and rejects the pending call with PP_CONTEXT_CHANGED, so the customer can never confirm a stale price. Cancellation and technical failure stay separate: only a customer cancellation resolves null and clears the PDP selection.
Selection exclusivity starts with waiting for the quote and ends on confirm, cancel, or failure. Calling showModal() or setSelectedPlan() while it runs rejects immediately with PP_SELECTION_IN_PROGRESS (no queueing, no stacked modals). After a cancellation, the next add to cart still follows the back-office automatic modal setting. The returned selection is an independent snapshot, and mainLineAttributes is already a Storefront AttributeInput[], so no format conversion is needed.
Full example
Section titled “Full example”The example below uses pp.showModal() to open the modal early from a custom button. If you only need “open the modal on add to cart when configured”, skip the custom button and use pp.addToCart() from the integration page:
'use client'
import { xcottonInit } from '@/lib/xcotton'import { showSelectedPlan } from '@/lib/cart'async function onChoosePlanClick() { await xcottonInit() const pp = window.xcotton?.pp if (!pp) throw new Error('[XC] PP is not enabled for this store') // Show the chosen plan; the next pp.addToCart resolves the same selection. const selection = await pp.showModal() if (selection) showSelectedPlan(selection.plan)}
export function ChoosePlanButton() { return ( <button type="button" onClick={() => void onChoosePlanClick().catch(console.error)} > Choose protection plan </button> )}import { xcottonInit } from '../lib/xcotton'import { showSelectedPlan } from '../lib/cart'async function onChoosePlanClick() { await xcottonInit() const pp = window.xcotton?.pp if (!pp) throw new Error('[XC] PP is not enabled for this store') // Show the chosen plan; the next pp.addToCart resolves the same selection. const selection = await pp.showModal() if (selection) showSelectedPlan(selection.plan)}
export function ChoosePlanButton() { return ( <button type="button" onClick={() => void onChoosePlanClick().catch(console.error)} > Choose protection plan </button> )}<script setup lang="ts">import { xcottonInit } from '~/lib/xcotton'import { showSelectedPlan } from '~/lib/cart'async function onChoosePlanClick() { await xcottonInit() const pp = window.xcotton?.pp if (!pp) throw new Error('[XC] PP is not enabled for this store') // Show the chosen plan; the next pp.addToCart resolves the same selection. const selection = await pp.showModal() if (selection) showSelectedPlan(selection.plan)}
function onClick() { void onChoosePlanClick().catch(console.error)}</script>
<template> <button type="button" @click="onClick">Choose protection plan</button></template>import { xcottonInit } from '../lib/xcotton'import { showSelectedPlan } from '../lib/cart'async function onChoosePlanClick() { await xcottonInit() const pp = window.xcotton?.pp if (!pp) throw new Error('[XC] PP is not enabled for this store') // Show the chosen plan; the next pp.addToCart resolves the same selection. const selection = await pp.showModal() if (selection) showSelectedPlan(selection.plan)}
export function ChoosePlanButton() { return ( <button type="button" onClick={() => void onChoosePlanClick().catch(console.error)} > Choose protection plan </button> )}<script setup lang="ts">import { xcottonInit } from '../lib/xcotton'import { showSelectedPlan } from '../lib/cart'async function onChoosePlanClick() { await xcottonInit() const pp = window.xcotton?.pp if (!pp) throw new Error('[XC] PP is not enabled for this store') // Show the chosen plan; the next pp.addToCart resolves the same selection. const selection = await pp.showModal() if (selection) showSelectedPlan(selection.plan)}
function onClick() { void onChoosePlanClick().catch(console.error)}</script>
<template> <button type="button" @click="onClick">Choose protection plan</button></template><script lang="ts"> import { xcottonInit } from '$lib/xcotton' import { showSelectedPlan } from '$lib/cart' async function onChoosePlanClick() { await xcottonInit() const pp = window.xcotton?.pp if (!pp) throw new Error('[XC] PP is not enabled for this store') // Show the chosen plan; the next pp.addToCart resolves the same selection. const selection = await pp.showModal() if (selection) showSelectedPlan(selection.plan) }
function onClick() { void onChoosePlanClick().catch(console.error) }</script>
<button type="button" onclick={onClick}>Choose protection plan</button>import { xcottonInit } from '../lib/xcotton'import { showSelectedPlan } from '../lib/cart'async function onChoosePlanClick() { await xcottonInit() const pp = window.xcotton?.pp if (!pp) throw new Error('[XC] PP is not enabled for this store') // Show the chosen plan; the next pp.addToCart resolves the same selection. const selection = await pp.showModal() if (selection) showSelectedPlan(selection.plan)}
export function ChoosePlanButton() { return ( <button type="button" onClick={() => void onChoosePlanClick().catch(console.error)} > Choose protection plan </button> )}Verify and troubleshoot
Section titled “Verify and troubleshoot”| Action | Expected result |
|---|---|
| Select a plan in the panel, then click Add to cart | No modal opens. The product and protection line are added directly |
| Click Add to cart without a plan (modal setting enabled) | The plan selection modal opens |
| Dismiss, cancel, or press Escape on the add-to-cart modal | pp.addToCart() still adds the main product with protection: 'skipped'. The PDP selection is cleared |
Dismiss, cancel, or press Escape on a custom showModal() | Resolves null, clears the PDP selection, and writes nothing |
| Add to cart again after cancelling (still no PDP plan) | The back-office setting decides again. The cancellation is not recorded as a refusal |
Confirm a plan from a custom showModal() button | A selection is returned and the next pp.addToCart uses that same plan |
| Switch variants or leave the page while the modal is open | The modal closes and the pending call rejects PP_CONTEXT_CHANGED. The newer context keeps its selection |
Call showModal() again while the modal is open | Rejects PP_SELECTION_IN_PROGRESS. No second modal is stacked |
| Symptom | Cause and fix |
|---|---|
| Modal does not open when adding to cart without a plan | Your account manager (AM) disabled the automatic modal on add to cart. Ask them to enable it, or call showModal() from a custom button |
The pending showModal() rejected with PP_CONTEXT_CHANGED | The variant changed or loadPlans(null) ran while the modal was open, voiding the previous quote. Reopen the modal for the active variant |
| The custom button does nothing | window.xcotton.pp is undefined: PP is disabled for the store or init() has not resolved. Neither depends on a cart |