Skip to content

Types

For AI assistants: what to know before writing SparkLayer SDK code
  • What it is: window.spark, added to the storefront by the SparkLayer Core Script (version 4.x). On Shopify the SparkLayer app embed loads it; elsewhere it's <script async src="https://sparkcdn.io/sparkjs/<SITE_ID>/<live|test>"></script> in the theme's <head>. The old cdn.sparklayer.io/spark.latest.js and spark.1.x.js files are legacy: never use them.
  • Options: Configure it with window.sparkOptions, set before the Core Script loads. Extend the object rather than replacing it: window.sparkOptions = { ...window.sparkOptions, … }, or the Site ID and other snippets' hooks are lost. Headless only: window.initSpark(options) is defined when window.sparkOptions isn't set.
  • When to call it: Only after SparkLayer is ready: inside the onReady(spark) hook, or after a promise resolved from it. Most methods are async; prices and the cart are only available to signed-in B2B customers (check await spark.isLoggedIn()).
  • Products and prices: Products are looked up by the platform's parent product ID (on Shopify, product.id) and variants by SKU: getPricingForVariant(parentProductId, sku), calculatePricingForVariant(parentProductId, sku, qty), getPackSizeForVariant(parentProductId, sku). For instant recalculation, fetch once with getVariant and call calculatePricingForVariantData(variant, qty).
  • Cart: updateCart({ products: [{ sku, adjustQuantity }] }) adds or removes relative to what's in the cart (also customAttributes: [{ key, value }], clearCart: true). It resolves to the results, or null after showing its own error message; it doesn't throw. getCart() reads the cart; openDrawer('cart') opens it.
  • Hooks: onCartUpdate(cart, results), onCartLoad(cart), onLogout(), preCartUpdateListener(input) (async: return the possibly changed input) and onCheckoutValidation(cart, cartUiState) (return [{ message }] to block checkout, or null). When adding a hook, call any existing one first.
  • Customising: Build around SparkLayer or customise its own components, including their HTML. Prefer documented options, hooks, CSS variables and custom slots where they fit; keep direct HTML changes targeted and re-check them after SparkLayer updates.
  • Docs: The reference: methods docs.sparklayer.io/developers/javascript-sdk/reference/methods.md, hooks docs.sparklayer.io/developers/javascript-sdk/reference/hooks.md, options docs.sparklayer.io/developers/javascript-sdk/reference/options.md, types docs.sparklayer.io/developers/javascript-sdk/reference/types.md (or the same types as a file: docs.sparklayer.io/developers/javascript-sdk/spark.d.ts). Setup and waiting for SparkLayer: docs.sparklayer.io/developers/javascript-sdk/setup.md. Use only what the reference documents.

The objects you pass to the SDK and get back from it, written in TypeScript and listed A to Z. In JavaScript, they describe the shape of those objects. Every block on this page goes into spark.d.ts, which you can download for autocomplete and type checking in your editor.

The cart, product and pricing types come from SparkLayer's GraphQL API. Their field types and nullability are inferred from how the SDK uses them, so treat a field as possibly null unless you're sure it's set.

AccountSectionId

The sections of the My Account panel, used in display.sectionConfiguration to reorder or hide them.

type AccountSectionId =
  | "spark-slot-my-account"
  | "recent-activity"
  | "shopping-lists"
  | "favourites"
  | "financials"
  | "my-details"
  | "reports"
  | "address-book"
  | "company-users";

Used by: DisplayOptions

AnalyticsEventName

The B2B events SparkLayer can send to your analytics. See Event tracking for analytics for what each event means.

type AnalyticsEventName =
  | "addToCart"
  | "cartUpdate"
  | "shoppingListSave"
  | "shoppingListLoad"
  | "shoppingListDelete"
  | "csvUpload"
  | "quickAdd"
  | "finalStageCheckout"
  | "shippingUpdate"
  | "beginCheckout"
  | "purchase"
  | "viewCart";

Used by: AnalyticsOptions

AnalyticsOptions

The type of the analytics option: the providers to send events to, and which events each one gets. See Event tracking for analytics.

type AnalyticsOptions = {
  providers: {
    // "ga" for Google Analytics (GA4), "gtm" for Google Tag Manager, or your own function
    handler: "ga" | "gtm" | ((eventName: AnalyticsEventName, eventParams: unknown) => void);
    // The events to send to this provider
    events: Partial<Record<AnalyticsEventName, boolean>>;
  }[];
};

Used by: the analytics option

Cart

The B2B cart.

type Cart = {
  cartUuid: string;
  intent: string; // For example "ORDER"
  canCreateOrder: boolean;
  canCreateQuote: boolean;
  hasLaunched: boolean;
  lockedByUserId: string | null;
  lockedByUserLastAccessedAt: string | null;
  loadedFromPurchaseAt: string | null;
  loadedFromPurchaseType: string | null;
  items: CartItem[];
  validationErrors: CartValidationError[];
  taxInclusiveDisplay: boolean;
  totals: CartTotals;
  currencyCode: string;
  totalWeight: number | null;
  customerAddresses: { id: string; description: string }[];
  deliveryAddressId: string | null;
  customerReference: string | null;
  poNumber: string | null;
  shippingRequestedDate: string | null;
  customFields: { name: string; value: string }[];
  approvingFor: { name: string } | null;
  discounts: {
    id: string;
    slug: string;
    name: string;
    appliedCouponCodes: string[];
    amount: { net: number; gross: number; currencyCode: string };
  }[];
  discountsAreAvailable: boolean;
  couponCodes: string[];
  removedCouponCodes: string[];
  quote: {
    statusSlug: string;
    statusUpdated: string;
    expiry: string | null;
    assignedAgent: { id: string } | null;
    message: string | null;
    note: string | null;
  } | null;
};

Used by: getCart(), getCartCache(), addCustomLineItem(), onCartLoad, onCartUpdate, onCheckoutValidation

CartItem

One line in the cart. It has every ProductVariant field (sku, name, price and so on) plus these:

type CartItem = ProductVariant & {
  type: string;
  itemKey: string; // Identifies the line in updateCart
  quantity: number;
  customAttributes: { key: string; value: string; type?: string | null }[];
  lineTotal: CartLineAmount;
  lineTotalPreDiscounts: CartLineAmount;
  lineTotalDiscountAmount: CartLineAmount;
  unitTotal: CartLineAmount;
  unitTotalPreDiscounts: CartLineAmount;
  unitTotalDiscountAmount: CartLineAmount;
  quotedPrice: string | null;
  preorderQuantityDisplay: number | null;
};

Used by: Cart (items), calculatePricingForVariantData()

CartItemInput

One product in an UpdateCart. Identify the line by sku or itemKey, and set the quantity with adjustQuantity (added to the current quantity; negative to remove) or quantity (the exact quantity; 0 removes the line).

type CartItemInput = {
  sku?: string;
  itemKey?: string; // From a CartItem
  adjustQuantity?: number;
  quantity?: number;
  customAttributes?: { key: string; value: string; type?: string }[];
};

For a file, value is a JSON string of an array of file GIDs from uploadFile().

Used by: UpdateCart (products)

CartLineAmount

An amount on a cart line, such as its total or unit price.

type CartLineAmount = {
  net: number;
  gross: number;
  currencyCode: string;
  taxInclusiveDisplay: boolean;
};

Used by: CartItem

CartMoney

An amount in the cart's totals.

type CartMoney = { number: string };

Used by: CartTotals

CartResults

The result for one product line in an updateCart call. See What updateCart returns for the messages types.

type CartResults = {
  messages: { type: string }[];
  requestedQuantity: number;
  resultantQuantity: number;
  itemKey: string;
  sku: string;
  name: string;
};

Used by: updateCart(), onCartUpdate

CartTotals

The cart's totals. Each amount is a CartMoney object with a number.

type CartTotals = {
  subTotal: {
    preDiscount: { net: CartMoney; gross: CartMoney };
    discount: { net: CartMoney; gross: CartMoney };
    charge: { net: CartMoney; gross: CartMoney };
  };
  shipping: {
    preDiscount: { net: CartMoney };
    discount: { net: CartMoney };
    charge: { net: CartMoney };
  };
  total: {
    charge: { gross: CartMoney; tax: CartMoney };
    discount: { net: CartMoney };
    preDiscount: { gross: CartMoney };
  };
};

Used by: Cart (totals)

CartUIState

The current checkout step.

type CartUIState = "cart" | "shipping" | "quote-details" | "payment" | "thanks";

Used by: onCheckoutValidation

CartValidationError

A reason the cart can't be checked out yet, such as an order minimum that hasn't been met. Each error has a type code (for example quantity-unavailable or pricing-unavailable) and the fields for its kind.

The type is one of the cart validation codes the API uses: maximum-order-item-quantity, maximum-order-totals, maximum-parent-quantity, maximum-variant-quantity, minimum-order-item-quantity, minimum-order-totals, minimum-parent-quantity, minimum-variant-quantity, pack-size, pricing-unavailable, pricing-unavailable-for-selected-qty or quantity-unavailable.

type CartValidationError = {
  type: string;
  amount?: number;
  amountGross?: number;
  minimum?: number;
  maximum?: number;
  productName?: string;
  parentSKU?: string;
  variantSKU?: string;
  packSize?: number;
  availableQuantity?: number;
  overridden?: boolean;
};
Kind of error (GraphQL type)Fields
CartValidationErrorMinimumOrderTotals, CartValidationErrorMaximumOrderTotalsamount, amountGross
CartValidationErrorMinimumOrderItemQuantityminimum
CartValidationErrorMaximumOrderItemQuantitymaximum
CartValidationErrorMinimumParentQuantity, CartValidationErrorMaximumParentQuantityminimum or maximum, productName, parentSKU, overridden
CartValidationErrorMinimumVariantQuantity, CartValidationErrorMaximumVariantQuantityminimum or maximum, variantSKU, overridden
CartValidationErrorPackSizepackSize, variantSKU, overridden
CartValidationErrorQuantityUnavailablevariantSKU, availableQuantity
CartValidationErrorPricingUnavailable, CartValidationErrorPricingUnavailableForSelectedQtyvariantSKU

Used by: Cart (validationErrors)

CheckoutElementConfig

One entry in checkoutCustomElements: a standard field by id, or a custom field by name. See Custom field settings for every key and Standard checkout fields for the IDs.

type CheckoutElementConfig = {
  // A standard field
  id?: "customer-reference" | "po-number" | "shipping-requested-date";
  // A custom field (required unless id is set)
  name?: string;
  // Customer-facing text, keyed by language code
  translations?: Record<string, { title: string; detail?: string; [messageKey: string]: string | undefined }>;
  // HTML attributes such as type, required, min, max, maxlength and pattern
  attributes?: Record<string, string | number | boolean>;
  // For select fields: the drop-down options, optionally in groups
  options?: {
    translations?: Record<string, { groupName: string }>;
    items: { value: string; translations: Record<string, { label: string }> }[];
  }[];
  // Returns a message key from translations if the value is invalid
  onChange?(value: string, element: HTMLElement): string | null | undefined;
};

Used by: the checkoutCustomElements option

CustomAccountDetailLinkType

Formats a customAccountDetails value as a link of this kind.

type CustomAccountDetailLinkType = "email" | "phone" | "sms" | "website";

Used by: the customAccountDetails option

CustomLineItemInput

A custom line item: a free-text name, price and quantity not tied to a SKU.

type CustomLineItemInput = {
  // The name shown on the custom line item
  name: string;
  // The price of a single unit, as a decimal string
  quotedPrice: string;
  quantity: number;
};

Used by: addCustomLineItem()

DisplayOptions

The type of the display option.

type DisplayOptions = {
  orderListShowShippingAddress: boolean;
  customerReferenceHidden: boolean; // deprecated
  customerReferenceRequired: boolean; // deprecated
  customerReferenceMaxLength?: number; // deprecated
  darkTheme: boolean;
  savingsUseRrp: boolean;
  roundPriceBreakPercentages: boolean;
  showPriceBreakSavings: boolean;
  sectionConfiguration: {
    "my-account"?: {
      order?: AccountSectionId[];
      hide?: AccountSectionId[];
    };
  };
  showSlots: string[];
  showRrpToggle: boolean;
  showFavouritesLists: boolean;
  showShoppingLists: boolean;
  showSkus: boolean;
  hideFinancials?: boolean;
};
PropertyDefaultWhat it does
sectionConfiguration{}Reorder (order) or hide (hide) My Account sections by AccountSectionId. See My account.
showSlots[]Custom slots to show. See Custom slots.
showFavouritesListstrueWhether to show the favourites lists feature.
showShoppingListstrueWhether to show the shopping lists feature.
showSkustrueWhether to show product SKUs on cart line items.
darkThemefalseUse the dark theme. See Customising design.
savingsUseRrpfalseCalculate savings against the RRP.
roundPriceBreakPercentagestrueRound price break percentages.
showPriceBreakSavingstrueShow savings on price breaks.
showRrpTogglefalseShow the RRP toggle.
orderListShowShippingAddressfalseShow the shipping address in the purchases list.
hideFinancials?–Hide the Financials section of My Account.
customerReferenceHidden, customerReferenceRequired, customerReferenceMaxLength?false, false, –Deprecated. Use a { id: 'customer-reference' } entry in checkoutCustomElements instead.

Used by: the display option

DrawerUIState

A tab of the SparkLayer drawer.

type DrawerUIState = "dashboard" | "browse-catalog" | "cart" | "sales-agent" | "account";

Used by: openDrawer(), the view-update event

ErrorOf

The error type of a GraphQL query or mutation passed to fetch. For a plain query string, it's GraphqlError.

type ErrorOf<Q> = Q extends { readonly __graphqlTypes?: { error: infer TError } }
  ? TError
  : GraphqlError;

Used by: fetch()

FetchResponse

The response returned by fetch: a standard Response whose json() resolves to the GraphQL response.

interface FetchResponse<TData = unknown, TError = GraphqlError> extends Response {
  json(): Promise<{ data: TData; errors: TError[] }>;
}

Used by: fetch()

GraphqlError

An entry in the errors array of a GraphQL response.

type GraphqlError = {
  message: string;
  path?: (string | number)[];
  extensions?: { code?: string; [key: string]: unknown };
};

Used by: FetchResponse, fetch()

GraphqlExtensionError

A GraphqlError with a code in extensions that identifies the error.

type GraphqlExtensionError = GraphqlError & {
  extensions: { code: string; [key: string]: unknown };
};

Used by: fetch()

GraphqlQueryOrMutation

The query you pass to fetch. In JavaScript, pass it as a string, and the variables as a plain object. In TypeScript, a typed query can also carry its response, variables and error types, which ResponseOf, VariablesOf and ErrorOf read back.

type GraphqlQueryOrMutation<
  TResponse = unknown,
  TVariables = Record<string, unknown>,
  TError = GraphqlError,
> = string & {
  // Type-only: never set at runtime
  readonly __graphqlTypes?: { response: TResponse; variables: TVariables; error: TError };
};

Used by: fetch()

PaymentMethodName

The payment methods you can list in paymentMethodsOrder.

type PaymentMethodName = "paymentOnAccount" | "paymentByInvoice" | "upfrontPayment" | "quote";

Used by: the paymentMethodsOrder option

Product

A product, with its variants.

type Product = {
  id: string;
  externalId: string; // Your platform's product ID
  slug: string;
  variants: ProductVariant[];
};

Used by: getProduct(), getProductBySlug(), the spark-variant-change event

ProductVariant

One variant of a product, and the base of each CartItem.

type ProductVariant = {
  id: string;
  externalId: string; // Your platform's variant ID
  parentExternalId: string; // Your platform's product ID
  stockDisplay: {
    status: string; // For example "out_of_stock" or "backorder"
    units: number | null;
    available: unknown;
  } | null;
  restockDate: string | null;
  cartImageUrl: string | null;
  position: number;
  sku: string;
  barcode: string | null;
  slug: string;
  name: string;
  price: VariantPrice | null;
  rrp: unknown; // Legacy: use rrpV2
  rrpV2: { number: string; currency: string } | null;
  priceBreaks: VariantPriceBreak[];
  // Product settings, such as packSize, minOrderQuantity and maxOrderQuantity
  settings: { key: string; value: unknown }[];
  // The variant's options, such as { group: "Size", value: "M" }
  options: { group: string; value: string }[];
};

Used by: getVariant(), Product (variants), CartItem, calculatePricingForVariantData(), the spark-variant-change event

ProductVariantDataPricing

The variant data calculatePricingForVariantData needs. A ProductVariant or CartItem has these fields.

type ProductVariantDataPricing = {
  priceBreaks: VariantPriceBreak[] | null;
  rrpV2?: { number: string; currency: string } | null;
  unitTotal?: { currencyCode: string } | null; // Cart items only
};

Used by: calculatePricingForVariantData()

ProductVariantPricing

The price for a quantity of a variant: the total, the unit price at that quantity and the price breaks.

type ProductVariantPricing = {
  totalPrice: number | null; // unitPrice × quantity
  totalRrpPrice: number | null;
  hasPriceBreaks: boolean;
  unitPrice: number | null; // The unit price at this quantity
  basePrice: number | null; // The unit price before price breaks
  priceBreaks: VariantPriceBreak[];
  priceBreakSavingsPercentage: number | null;
  rrpV2: { number: string; currency: string } | null;
  currencyCode: string;
  unitPriceBreakdown: {
    items: {
      unitOfMeasure: string | null;
      price: number;
      quantity: number;
      uomQuantity: number;
    }[];
  };
};

Used by: calculatePricingForVariant(), calculatePricingForVariantData()

ResponseOf

The data type of a GraphQL query or mutation passed to fetch. For a plain query string, it's unknown.

type ResponseOf<Q> = Q extends { readonly __graphqlTypes?: { response: infer TResponse } }
  ? TResponse
  : unknown;

Used by: fetch()

SimplifiedProductPrice

The "from" price across a product's variants, and how many different prices there are.

type SimplifiedProductPrice = {
  numberOfPrices: number;
  numberOfPricesExcludingBreaks: number;
  numberOfRrpPrices: number;
  fromPrice: number | null;
  fromPriceExcludingBreaks: number | null;
  currencyCode: string;
  hasPriceBreaks: boolean;
  rrpPrice: number | null;
  rrpCurrencyCode: string | null;
  taxInclusiveDisplay: boolean;
};

If numberOfPrices is more than 1, show "from" before fromPrice.

Used by: getPriceForProduct()

Spark

The window.spark object. See Methods for each one.

type Spark = {
  options: SparkLayerOptions;
  isLoggedIn(): Promise<boolean>;
  switchImpersonatingCustomer(customerId: string | null): Promise<UserData | null>;
  refreshGlobalData(): Promise<UserData | null>;
  getActiveCustomerVerificationToken(): Promise<string | null>;
  setActiveCompanySection(sectionId: string): Promise<boolean>;
  getProduct(parentProductId: string): Promise<Product>;
  getProductBySlug(slug: string): Promise<Product>;
  getVariant(parentProductId: string, variantSku: string): Promise<ProductVariant>;
  getPriceForProduct(parentProductId: string): Promise<SimplifiedProductPrice>;
  getPricingForVariant(
    parentProductId: string,
    variantSku: string,
  ): Promise<{
    price: number | null;
    rrp: number | null;
    rrpCurrencyCode: string | null;
    priceBreaks: VariantPriceBreak[];
    currencyCode: string;
  }>;
  getRrpPriceForVariant(
    parentProductId: string,
    variantSku: string,
  ): Promise<{ rrp: number | null; currencyCode: string | null }>;
  getPackSizeForVariant(parentProductId: string, variantSku: string): Promise<number>;
  calculatePricingForVariant(
    parentProductId: string,
    variantSku: string,
    qty: number,
    qtyAcrossVariants?: number | null,
  ): Promise<ProductVariantPricing>;
  calculatePricingForVariantData(
    variant: ProductVariantDataPricing,
    qty: number,
    qtyAcrossVariants?: number | null,
  ): ProductVariantPricing;
  getCart(): Promise<Cart | null>;
  getCartCache(): Cart | null;
  updateCart(
    input: Partial<UpdateCart>,
    cartSuccessHandledLocally?: boolean,
    cartErrorsHandledLocally?: boolean,
  ): Promise<CartResults[] | null>;
  addCustomLineItem(customItem: CustomLineItemInput): Promise<Cart | null>;
  externalClearBasket(): Promise<void>;
  openDrawer(initialTab?: DrawerUIState): void;
  closeDrawer(): void;
  uploadFile(fileName: string, fileData: Blob): Promise<string>;
  fetch<Q extends GraphqlQueryOrMutation<unknown, Record<string, unknown>, GraphqlError | GraphqlExtensionError>>(
    query: Q,
    variables?: VariablesOf<Q>,
    useImpersonatingCustomer?: boolean,
    bypassAppProxy?: boolean,
    signal?: AbortSignal,
  ): Promise<FetchResponse<ResponseOf<Q>, ErrorOf<Q>>>;
};

Used by: window.spark, window.initSpark(), onReady, onLoad

SparkLayerOptions

The type of window.sparkOptions and the options you pass to window.initSpark(). See Options and Hooks for each one.

type SparkLayerOptions = {
  sparkDomain: "app.sparklayer.io" | "test.app.sparklayer.io";
  platform: "base" | "bigcommerce" | "shopify" | "wix" | "woocommerce" | "magento";
  siteId: string;
  rootUrl: string;
  accountRedirect: {
    urlRegex?: RegExp;
    goTo?: string;
  };
  cartRedirect: {
    urlRegex?: RegExp;
    goTo?: string;
  };
  wordpressSiteUrl?: string;
  accountButtonSelectors: string;
  logoutButtonSelectors: string;
  cartButtonSelectors: string;
  loginButtonSelectors: string;
  display: DisplayOptions;
  termsAndConditionsLink: string;
  language: string;
  locale: string | null;
  translations: Translations;
  showTranslations: boolean;
  checkoutCustomElements: CheckoutElementConfig[];
  paymentMethodsOrder: PaymentMethodName[];
  saveToAddressBookDefault: boolean;
  productLink: string;
  customAccountDetails: {
    title: string;
    value: string | number;
    displayText?: string;
    type?: CustomAccountDetailLinkType;
  }[];
  onCheckoutValidation(
    cart: Cart,
    cartUiState: CartUIState,
  ): Promise<{ message: string }[] | null>;
  onCartUpdate(cart: Cart, results: CartResults[]): Promise<void>;
  onCartLoad(cart: Cart): Promise<void>;
  preCartUpdateListener(input: Partial<UpdateCart>): Promise<Partial<UpdateCart>>;
  onReady?(spark: Spark): Promise<void>;
  onLoad?(spark: Spark): Promise<void>;
  onLogout?(): Promise<void>;
  shopify?: {
    useAppProxy?: boolean;
  };
  bigcommerce?: {
    appClientId?: string;
  };
  auth: {
    user?: string;
    token?: string;
  };
  authLogoutUri: string | null;
  analytics?: AnalyticsOptions;
  igniteCheckoutContext?: Record<string, unknown>;
};

Used by: window.sparkOptions, window.initSpark(), Spark (options)

Translations

The type of the translations option: text overrides keyed by language code, then by translation key. See Change text in the Core Script.

type Translations = Record<string, Record<string, string>>;

Used by: the translations option

UpdateCart

A change to the cart. Every field is optional: send only what you want to change. See Cart.

type UpdateCart = {
  clearCart?: boolean;
  products?: CartItemInput[];
  deliveryAddressId?: string;
  customerReference?: string;
  poNumber?: string;
  shippingRequestedDate?: string;
  customFields?: { name: string; value: string }[];
  cartIntent?: string;
};

SparkLayer adds the impersonated customer and company section for you.

Used by: updateCart(), preCartUpdateListener

UserData

The active customer. This lists the fields most integrations need; the object has more.

type UserData = {
  id: string;
  email: string;
  name: string;
  companyName: string | null;
  customerVerificationToken: string | null;
  salesAgent: boolean;
  parentAccount: { id: string; name: string; email: string } | null;
  companySections: { id: string; name: string }[];
};

Used by: switchImpersonatingCustomer(), refreshGlobalData()

VariablesOf

The variables type of a GraphQL query or mutation passed to fetch. For a plain query string, it's any plain object.

type VariablesOf<Q> = Q extends { readonly __graphqlTypes?: { variables: infer TVariables } }
  ? TVariables
  : Record<string, unknown>;

Used by: fetch()

VariantPrice

A variant's price for the customer.

type VariantPrice = {
  taxRate: number;
  net: number;
  gross: number;
  currencyCode: string;
  taxInclusiveDisplay: boolean; // If true, show gross; otherwise net
};

Used by: ProductVariant (price), VariantPriceBreak

VariantPriceBreak

One price break (tier) for a variant: the price per unit from quantity upwards.

type VariantPriceBreak = {
  quantity: number;
  price: VariantPrice;
  unitOfMeasure: string | null;
};

Used by: getPricingForVariant(), ProductVariant, ProductVariantPricing

Next steps

Was this page helpful?

Last updated