---
title: "Upgrading to V2: for developers"
description: Every V1 hook, JavaScript method, shortcode, template, endpoint and CSS hook, what happened to it in V2, and how to migrate your code.
sidebar:
  label: For developers
  order: 2
  icon: code
---

This page is for developers and agencies who wrote code against CartPops V1 (1.5.x). It lists what is kept, changed or removed in V2, and what to use instead. Store owners should start with [Upgrading to V2](/upgrading-to-v2).

V2 references: [PHP actions](/developers/php-actions), [PHP filters](/developers/php-filters), [JavaScript](/developers/javascript), [Store API and REST](/developers/store-api-and-rest) and [CSS customization](/developers/css-customization).

## What changed in the architecture

V1 rendered the drawer from PHP templates, loaded cart changes through `admin-ajax.php` actions, swapped HTML fragments into the page by CSS selector, and exposed a jQuery-based `window.CartPops` object. V2 renders the drawer as a server-rendered block (`cartpops/cart-drawer`, plus `cartpops/cart-launcher`), updates it on the client with the WordPress Interactivity API (a store named `cartpops`), and reads and changes the cart through the WooCommerce Store API and the `cartpops/v1` REST namespace. There are no V1 PHP templates to override, no HTML fragments to replace, and no `window.CartPops`. PHP hooks that fired while V1 built markup now fire once per server render of the drawer block. They do not run again when the browser updates items, coupons, recommendations, shipping or totals.

:::warning[Breaking changes, by design]
V2 does not try to keep V1 hook or JavaScript compatibility where it would cause stale or unsafe output. Several V1 hooks are removed with no replacement. This page says so for each one. Free and Pro expose the same hooks, except the three marked **Pro** below.
:::

## Start here: what is most likely to break

1. **`window.CartPops` does not exist.** Any code that touches it throws a `ReferenceError`.
2. **`cpops-toggle-drawer` no longer opens the drawer.** It is a styling class on CartPops' own launcher only.
3. **Custom JavaScript from V1 is never executed.** It is quarantined in the database.
4. **Per-item, totals and class filters are not applied.** `woocommerce_after_cart_item_name`, the `cartpops_cart_totals_*_html` filters and the `cartpops_*_classes` filters do nothing.
5. **Theme template overrides are ignored.** `{theme}/cartpops/...` is no longer read.
6. **CartPops sends the single product page `form.cart` in the background only when its setting is on.** **Add to cart on product pages without reloading** (Advanced > General) is on by default. A submit handler of your own that calls `event.preventDefault()` keeps the form; CartPops then does nothing.
7. **`admin-ajax.php` actions and HTML fragments are gone.** Use the Store API and `cartpops/v1`.
8. **`fs_cartpops()` can return `null`.** Check before you call methods on it.

## PHP actions

Drawer actions fire with no arguments, once per server render of the drawer block. A callback that prints markup owns its escaping, accessibility and balanced HTML. Native V2 sections are not callbacks on these actions. Priority cannot interleave your output with them, and `remove_action()` against a V1 native callback is now a no-op.

| Action | V2 status | What to know |
| --- | --- | --- |
| `cartpops_loaded` | Kept | Fires on `plugins_loaded` after the container is built. It does not fire when WooCommerce is missing or when Free and Pro are both active. |
| `fs_cartpops_loaded` | Kept | Fires after the V2 Freemius bootstrap is ready. |
| `cartpops_drawer_wrapper_start`, `cartpops_drawer_wrapper_end` | Kept | First and last child of the interactive drawer root. The V1 ID-based wrapper is gone, but the slot is the same. |
| `cartpops_drawer_panel_wrapper_end` | Kept | Last child of the drawer dialog, after the footer. |
| `cartpops_drawer_panel_wrapper_start` | Changed | First child of the dialog, before the header. V2 no longer prints WooCommerce notices through this action, so removing the V1 notices callback does nothing. |
| `cartpops_drawer_header_before` | Kept | Inside the header, before the title and close button. |
| `cartpops_drawer_header_after` | Changed | Inside the header, after the title and close button. The Pro shipping meter is a native section, not a callback, so a V1 `remove_action()` of the meter does nothing. |
| `cartpops_drawer_content` | Changed | Fires once at the start of the scrollable content. Native sections (notices, meter, items, empty state, recommendations, totals) are not callbacks. |
| `cartpops_drawer_footer_before`, `cartpops_drawer_footer_after` | Kept | Start and end of the native footer. They fire even when the cart starts empty. The footer shows and hides itself, but your callback does not run again. |
| `cartpops_drawer_footer_content` | Changed | Fires after `footer_before` and before the native add-on, coupon, totals and checkout markup. |
| `cartpops_drawer_coupon_wrapper_start`, `cartpops_drawer_coupon_wrapper_end`, `cartpops_drawer_coupon_form_before` | Kept | Fire only when the coupon setting is on. |
| `cartpops_drawer_coupon_form_after` | Changed | Applied-coupon tags are native and follow this position. Removing the V1 tag callback does nothing. |
| `cartpops_drawer_before_checkout_button`, `cartpops_drawer_after_checkout_button` | Kept | Before and after the primary checkout link. |
| `cartpops_drawer_before_secondary_checkout_button`, `cartpops_drawer_after_secondary_checkout_button` | Changed | They fire below the checkout button in V2. V1 printed them above it. They fire in both editions, even when no secondary action exists. The secondary action itself is **Pro**. |

For the full position table, see [PHP actions](/developers/php-actions).

### Removed actions

The actions below have no V2 equivalent. Popup, bar and assistant were Pro-only in V1, so Free sites never had them.

**Popup, bar and assistant actions**

The beta popup and bar and the V1 assistant are retired. Their render lifecycle does not exist in V2.

```text
cartpops_popup_wrapper_start, cartpops_popup_wrapper_end
cartpops_recommendation_popup
cartpops_cart_trigger_contents, cartpops_last_cart_item
cartpops_free_shipping_meter_horizontal, cartpops_powered_by
cartpops_assistant_wrapper_start, cartpops_assistant_wrapper_end
cartpops_assistant_panel, cartpops_assistant_panel_top, cartpops_assistant_panel_bottom
cartpops_assistant_panel_wrapper_start, cartpops_assistant_panel_wrapper_end
```

For static additions, use the drawer positions in the table above. The V1 assistant is replaced by the inline shipping calculator in the Pro drawer, which has no action API.

**Rules engine and rule admin screens**

V1 automation rules (the plural `cartpops_rules` post type) are retired and not converted. Their admin and lifecycle hooks are gone.

```text
cartpops_after_created_new_rule, cartpops_after_updated_rule
cartpops_before_rule_general_settings, cartpops_after_rule_general_settings
cartpops_before_rule_criteria_settings, cartpops_after_rule_criteria_settings
cartpops_before_rule_filters_settings, cartpops_after_rule_filters_settings
cartpops_before_rule_restrictions_settings, cartpops_after_rule_restrictions_settings
cartpops_before_rule_notices_settings, cartpops_after_rule_notices_settings
cartpops_rule_data_panels, cartpops_update_rule_order_count, cartpops_update_rule_user_usage_count
```

The rules list table filters (`cartpops_rules_get_columns` and similar) are gone with it. V2 has a different, separate `cartpops_rule` model, and the upgrade does not convert V1 rows into it.

**Admin settings framework**

V1 built its admin screens with a PHP settings framework. V2 uses a React admin and a REST settings endpoint. These hooks are gone.

```text
cartpops_admin_settings_menu_title, cartpops_admin_settings_page_title
cartpops_admin_settings_sanitize_option, cartpops_settings_fields
cartpops_settings_menu_items, cartpops_settings_tabs_array
cartpops_default_settings, cartpops_page_screen_ids, cartpops_plugin_issues_list
cartpops_before_shortcode_contents, cartpops_after_shortcodes_content
```

Also gone: every tab, section, save, reset and field-render hook that was built from the `cartpops_settings_*` and `cartpops_admin_field_*` name patterns.

## PHP filters

### Kept or changed

| Filter | V2 status | What to know |
| --- | --- | --- |
| `cartpops_checkout_button_url` | Changed | Same single-value call. The result must be an absolute HTTP or HTTPS URL with a host, or a root-relative path such as `/express`. Anything else falls back to the WooCommerce checkout URL. Return a raw URL, not HTML. |
| `cartpops_empty_cart_button_url` | Changed | Same rules as above, applied to the Continue shopping link. |
| `cartpops_add_to_cart_trigger` | Changed | Canonical values are `add_to_cart` and `launcher`. V1 `both`, `drawer`, `popup` and `bar` become `add_to_cart`. `manual` and `none` become `launcher`. A retired `popup` or `bar` callback therefore now opens the drawer. |
| `cartpops_powered_by_link` | Changed | Runs only when Powered by CartPops is on and a partner code is stored, and receives the Freemius partner URL. V1 ran it unconditionally with a CartPops URL as the default, so callbacks that rewrote that default no longer run. |
| `cartpops_secondary_btn_classes` | Changed, **Pro** | Additive only. The callback receives the CartPops classes and the mode (`continue_shopping`, `view_cart` or `custom_url`). You cannot remove the owned classes. The result is at most 16 valid CSS identifiers, and any invalid result restores the full CartPops list. |
| `cartpops_elementor_widget_cart_is_hidden` | Kept, **Pro** | The result is cast to a boolean. |

### Replaced

| V1 filter or hook | Use in V2 |
| --- | --- |
| `cartpops_after_cart_item_name_hook_collapsible` | `cartpops_cart_item_data`. V2 has no collapsible details block. A callback can empty or rewrite `extraLines`. |
| `woocommerce_after_cart_item_name` (action) | `cartpops_cart_item_data` (`extraLines`), or WooCommerce's `woocommerce_get_item_data`, which CartPops already reads into the same row. See [Add a line under the product name](#add-a-line-under-the-product-name). |
| `cartpops_is_valid_product` | `woocommerce_widget_cart_item_visible`, which V2 still applies. |

### New in V2

| Filter | What it does |
| --- | --- |
| `cartpops_cart_item_data` | Adjust a cart row's text, extra lines and a bounded custom presentation map. Three arguments: the presentation row, the raw cart item and the cart item key. |
| `cartpops_store_api_cart_extension_data`, `cartpops_store_api_cart_extension_schema` | Add bounded data to CartPops' Store API extension namespace. |
| `cartpops_pro_recommendation_button_presentation` (**Pro**) | Change the recommendation add button mode and text for entitled sites. |
| `cartpops_render_drawer` | Return `false` to stop the automatic drawer that CartPops renders in the footer on classic themes. |
| `cartpops_menu_launcher_attributes` | Adjust the bounded launcher attributes for a `cpops-cart-menu-item` menu item. |
| `cartpops_custom_css_style_dependencies` | Adjust the style dependencies of the stylesheet that carries your Custom CSS. |
| `cartpops_rest_allowed_origins`, `cartpops_rest_trusted_proxy_cidrs` | Adjust which origins and proxies the CartPops REST protections accept. A CDN or reverse proxy can otherwise cause rate-limit errors. |

### Removed

**Class and HTML filters (documented in V1)**

V2 does not apply any of these. The elements keep many V1 class names, so scoped CSS often does the job. See [Add classes](#add-classes).

```text
cartpops_drawer_classes, cartpops_drawer_empty_cart_classes
cartpops_drawer_header_classes, cartpops_drawer_footer_classes
cartpops_drawer_coupon_form_classes, cartpops_drawer_cart_totals_classes
cartpops_drawer_recommendations_classes, cartpops_drawer_cart_item_classes
cartpops_single_line_item_classes, cartpops_powered_by_classes
cartpops_cart_launcher_classes, cartpops_floating_cart_launcher_classes
cartpops_drawer_shipping_meter_classes, cartpops_shipping_classes
```

`cartpops_secondary_btn_attributes` is also removed. Raw attribute strings cannot be made safe with V2's reactive and accessibility contract. To add a link with attributes such as `target`, print your own element with `cartpops_drawer_before_secondary_checkout_button` or `cartpops_drawer_after_secondary_checkout_button`.

**Totals, price and notice filters**

V2 formats totals in the browser from WooCommerce's numeric values, so there is no HTML to filter. To change a total, change the underlying WooCommerce price, fee, tax or shipping calculation.

```text
cartpops_cart_totals_subtotal_html, cartpops_cart_totals_order_total_html
cartpops_cart_totals_vat_total_html, cartpops_cart_totals_discount_total_html
cartpops_cart_totals_shipping_html
cartpops_cart_item_price_display, cartpops_after_cart_item_name_price
cartpops_drawer_notice_html, cartpops_notices_html, cartpops_kses_notice_allowed_tags
```

`cartpops_after_cart_item_name_price` used to switch whether the price shows after the item name. V2 always shows the price in the row.

**Behavior, timing and fragment filters**

```text
cartpops_notification_remove_delay, cartpops_loading_state_delay
cartpops_generic_error_message
cartpops_cart_item_removed_title, cartpops_cart_item_removed_notice_type
cartpops_ajax_fragments, cartpops_available_cart_fragments
cartpops_bundled_items_data, cartpops_get_product
cartpops_template, cartpops_template_path
```

The closest V2 concept to `cartpops_notification_remove_delay` is the Undo window after removing an item. It is fixed at 5000 ms and is not filterable. Bundled and add-on cart lines are recognized from a fixed list of markers, not through `cartpops_bundled_items_data`.

**Popup, bar, assistant and upsell-table filters**

```text
cartpops_alt_trigger_classes, cartpops_last_cart_item_classes
cartpops_popup_recommendations_classes
cartpops_assistant_content, cartpops_assistant_content_classes
out_of_stock_add_to_cart_text, out_of_stock_add_to_cart_url
cartpops_pagination_classes, cartpops_pagination_number
cartpops_get_dropdown_upsell_product_name, cartpops_show_dropdown_add_to_button
cartpops_upsell_product_add_to_cart_button_label
cartpops_upsell_product_dropdown_default_value_label
cartpops_upsell_product_heading_label
```

**Rules, post types and admin framework filters**

```text
cartpops_add_custom_post_types, cartpops_add_custom_post_status
cartpops_rules_post_type_args, cartpops_master_log_post_type_args
cartpops_active_post_status_args, cartpops_inactive_post_status_args
cartpops_manual_post_status_args, cartpops_automatic_post_status_args
cartpops_rule_statuses, cartpops_master_log_statuses, cartpops_paid_order_statuses
cartpops_rule_data_tabs, cartpops_rule_translate_string
```

The `cartpops_master_log` post type is no longer registered. Its rows are left in place.

The documented V1 names `cartpops_drawer_inside` (it never fired in V1), `cartpops_cart_totals_subtotal_total_html` (a typo for the subtotal filter) and the callback names `cartpops_checkout_url` and `cartpops_empty_cart_url` in old docs were never real hooks.

## WooCommerce hooks in the drawer

V1 re-fired many WooCommerce hooks while it built its own cart rows. V2 builds rows from WooCommerce's cart data and Store API, so only these hooks still matter.

| WooCommerce hook | V2 behavior |
| --- | --- |
| `woocommerce_widget_cart_item_visible` | Kept. Same three arguments. A non-scalar result or an exception hides the row. |
| `woocommerce_get_item_data` | Kept. CartPops reads it into the row's extra lines. |
| `woocommerce_cart_item_backorder_notification`, `woocommerce_cart_item_product_id` | Still applied, with a narrower scope. The backorder text must be a string, bounded to 500 bytes, and is shown as plain text. |
| `woocommerce_cart_item_permalink` | Partial. WooCommerce's Store API applies it, so it takes effect after a refresh, but not on the first server render. |
| `woocommerce_cart_item_thumbnail` | Not applied. `woocommerce_store_api_cart_item_images` changes images after a refresh on WooCommerce 9.6 and newer, not on the first render. |
| `woocommerce_quantity_input_min`, `woocommerce_quantity_input_max`, `woocommerce_quantity_input_step` | Version dependent. Use `woocommerce_store_api_product_quantity_minimum`, `woocommerce_store_api_product_quantity_maximum` and `woocommerce_store_api_product_quantity_multiple_of`, which V2 relies on. |
| `woocommerce_cart_item_name`, `woocommerce_cart_item_subtotal`, `woocommerce_cart_item_product` | No effect on the drawer. Use `cartpops_cart_item_data` for the name and extra lines. |
| `woocommerce_quantity_input_classes`, `_pattern`, `_inputmode`, `_placeholder` | No effect. V2 has plus and minus buttons and a text value, with no `input` element. |
| `woocommerce_cart_no_shipping_available_html`, `woocommerce_shipping_estimate_html`, `woocommerce_shipping_may_be_available_html`, `woocommerce_shipping_not_enabled_on_cart_html`, `woocommerce_after_shipping_rate` | Not applied. The drawer shows a shipping string derived from WooCommerce. |
| `woocommerce_add_to_cart_fragments` | WooCommerce still applies it for its own refreshes, so your callbacks still run. CartPops adds one fragment, `.cartpops-cart-json`, and no longer adds HTML fragments. |
| `woocommerce_ajax_added_to_cart` | WooCommerce fires it for its own AJAX adds. Adds that CartPops starts (Store API, recommendation add, bundle add) do not fire it. Trackers should use WooCommerce's `woocommerce_add_to_cart` instead. |

## JavaScript API

V1 exposed `window.CartPops` with `drawer`, `popup`, `bar` and `assistant` objects. V2 has no global object. Classic scripts talk to the drawer through DOM events on `document`.

| V1 | V2 |
| --- | --- |
| `window.CartPops` | Removed. Referencing it throws. |
| `CartPops.drawer.show()` | Dispatch `cartpops:open` on `document`. |
| `CartPops.drawer.hide()` | Dispatch `cartpops:close`. |
| `CartPops.drawer.toggle()` | Dispatch `cartpops:toggle`. |
| `CartPops.drawer.on('show')` and `on('hide')` | No equivalent. V2 does not emit an open or close event. |
| `CartPops.popup`, `CartPops.bar` | Removed with the retired presentations. |
| `CartPops.assistant.show()` | Removed. The Pro shipping calculator is inline in the drawer. |
| `window.CartPopsConfig` | Removed. V2 publishes its configuration as Interactivity API state, not as a global. |
| `$(document.body).trigger('adding_to_cart')` and `'added_to_cart'` fired by CartPops | CartPops no longer triggers them. WooCommerce still fires `added_to_cart` for its own AJAX adds. |

V2 browser events:

| Event | Direction | What it does |
| --- | --- | --- |
| `cartpops:open` | You dispatch it on `document` | Opens the drawer. |
| `cartpops:close` | You dispatch it on `document` | Closes the drawer. |
| `cartpops:toggle` | You dispatch it on `document` | Toggles the drawer. |
| `cartpops:count-updated` | CartPops dispatches it on `document` | Fires after the cart count changes. The count is in `event.detail.count`. |

Things to know:

- The `cartpops:open`, `cartpops:close` and `cartpops:toggle` listeners are installed when the drawer's script module loads. An event dispatched earlier, such as at `DOMContentLoaded`, is ignored. Dispatch from a user action, such as a click.
- V2 has no `cartpops:ready`, `cartpops:opened` or `cartpops:closed` event. To react to the drawer opening, you have no supported signal.
- CartPops still listens for WooCommerce's jQuery `added_to_cart` event to open the drawer, but only if `window.jQuery` exists when its script module runs. A plugin that delays or defers jQuery can stop it from hearing classic AJAX adds.
- Adds that CartPops makes itself, such as recommendation or bundle adds, do not fire jQuery `added_to_cart`. Analytics or `dataLayer` code that listens for it will miss them.
- `data-cpops-cart-open="false"` on an add to cart button still stops that one add from opening the drawer.
- Quarantined V1 custom JavaScript is never executed. Move it into your theme or a plugin and rewrite it against the events above.

## Shortcodes and menu items

| V1 | V2 status | What to know |
| --- | --- | --- |
| `[cartpops_cart_launcher]` | Kept | Same tag. The output is new markup: a `button` with the class `cpops-launcher`, not the V1 `div.cartpops-cart__container`. |
| `[cartpops_launcher]` | New | The current tag. Same renderer. |
| `icon` attribute | Changed | Values are `cart`, `bag` and `basket`. V1 `cpops-icon-*` names map to the nearest one. A custom string, such as `icon="my-icon"` with CSS on `.cartpops-cart__container-icon i`, falls back to `cart`. That recipe no longer works. |
| `subtotal`, `indicator`, `indicator_hide_empty` | Kept | Same values. |
| `hide_empty` | New | Hides the whole launcher while the cart is empty. |
| `wrapper`, `shortcode` | Removed | Internal V1 flags, now ignored. |
| Menu class `cpops-cart-menu-item` | Kept, changed | CartPops replaces the whole item output with a launcher. The V1 `<a class="menu-link">` wrapper is gone. Classic menus only, not the Navigation block. The new `cartpops_menu_launcher_attributes` filter adjusts it. |
| Menu or element class `cpops-toggle-drawer` | Removed as a trigger | Still present on the launcher button as a style class. |
| Elementor widget `cartpops-cart` (**Pro**) | Kept | Widget identity and the control IDs sampled in review are preserved, so saved pages keep the widget. Spot-check the icon values on your pages. |

Shortcode launchers take their colors from **Launcher** > **Inline launcher appearance**, not from attributes.

## Templates and overrides

V1 let a theme override templates in `{theme}/cartpops/{slug}.php`. The assistant template loaded from `{theme}/woocommerce/premium/assistant.php`. V2 has no template loader and does not read either location. A copied V1 `cartpops` folder is dead code and does no harm.

| V1 template group | V2 |
| --- | --- |
| `drawer/drawer`, `header`, `footer`, `cart-totals`, `coupon-form`, `empty-state`, `recommendations`, `shipping` | Replaced by the `cartpops/cart-drawer` block render and a client module. `shipping` is replaced by the Pro inline calculator. |
| `drawer/cart-item` | Replaced by a client template that renders from state. Use `cartpops_cart_item_data` for text. |
| `components/launcher`, `components/powered-by` | Replaced by the launcher block and the Powered by link logic. |
| `components/product/price`, `components/product/quantity-selector` | Removed. The browser formats prices, and the quantity buttons are part of the drawer module. |
| `premium/cartpops-shipping-meter` | Replaced by the Pro Free Shipping Meter block. The circular meter is dropped. |
| `premium/assistant*`, `premium/cartpops-popup*`, `premium/cartpops-bar*`, `premium/cartpops-last-added-item` | Removed with the retired features. |

The removed loader functions are `cartpops_get_template()`, `cartpops_get_theme_template()`, `cartpops_get_plugin_template()` and `cartpops_template_path()`, plus the `cartpops_template` and `cartpops_template_path` filters.

There is no supported way to replace the drawer's markup. Use the action positions for static additions, `cartpops_cart_item_data` for row text, and CSS for appearance.

## Global functions, constants and classes

| V1 | V2 status | Use instead |
| --- | --- | --- |
| `cartpops_get_cart_item_count()`, `cartpops_get_wc_cart_subtotal()`, `cartpops_get_wc_cart_total()`, `cartpops_price()` | Removed | `WC()->cart->get_cart_contents_count()`, `get_cart_subtotal()`, `get_cart_total()` and `wc_price()`. |
| `cartpops_get_template()` and the other template loaders | Removed | See [Templates and overrides](#templates-and-overrides). |
| `fs_cartpops()`, `cartpops_fs()` | Kept, changed | Both return `?object`. They can return `null`. Check before you call a method. |
| `CARTPOPS_VERSION`, `CARTPOPS_PATH`, `CARTPOPS_URL` | Kept | |
| `CARTPOPS_FILE` | Changed | V1 stored `plugin_basename()`. V2 stores the absolute `__FILE__` path. |
| `CARTPOPS_PREFIX`, `CARTPOPS_ABSPATH`, `CARTPOPS_DOCS`, `CARTPOPS_TEMPLATE_DEBUG_MODE`, `cartpops_plugin_path()` | Removed | Use `CARTPOPS_PATH` and `CARTPOPS_URL`. |
| `CartPops_Cart::render_cart_launcher()` | Removed | `echo do_shortcode( '[cartpops_launcher]' );` |
| `CartPops\Admin\Options::get()` and the other V1 settings classes | Removed | There is no documented PHP API for reading CartPops settings. |
| V1 classes such as `CartPops_Frontend_Ajax`, `CartPops_Public`, `CartPops_Assets`, `CartPops_Settings` | Removed | V2 classes are namespaced, mostly `final`, and are not a public API. |

**Removed V1 helper functions (rules, admin and layout)**

These were internals of the rules engine, admin screens and upsell tables. None was documented, and none exists in V2.

```text
cartpops_get_wc_cart_category_subtotal, cartpops_get_product_count_in_cart
cartpops_get_buy_product_count_in_cart, cartpops_get_coupon_upsell_product_count_in_cart
cartpops_get_free_upsell_products_count_in_cart, cartpops_get_rule_products_count_in_cart
cartpops_get_upsell_products_in_cart, cartpops_get_free_upsells_per_page_column_count
cartpops_get_carousel_options, cartpops_get_rule_translated_string
cartpops_get_address_metas, cartpops_get_address, cartpops_get_product
cartpops_cart_item_price, cartpops_cart_item_thumbnail, cartpops_cart_item_product
cartpops_cart_item_product_name, cartpops_get_quantity_from_cart_item
cartpops_after_cart_item_name, cartpops_render_product_image
cartpops_implode_html_attributes, cartpops_mapped_implode, cartpops_check_is_array
cartpops_outbound_url, cartpops_get_emoji_flag, cartpops_set_cart_constant
cartpops_create_new_rule, cartpops_get_rule, cartpops_update_rule, cartpops_delete_rule
cartpops_create_new_master_log, cartpops_get_master_log, cartpops_update_master_log
cartpops_delete_master_log, cartpops_select2_html, cartpops_get_datepicker_html
```

Plus the remaining pagination, upsell-label and admin-page helpers.

## AJAX endpoints and fragments

V1 registered ten `admin-ajax.php` actions, each as `wp_ajax_` and `wp_ajax_nopriv_`. They returned HTML fragments keyed by CSS selector and did not verify a nonce. None was documented. V2 removes all of them.

| V1 `admin-ajax.php` action | V2 replacement |
| --- | --- |
| `cpops_add_to_cart` | WooCommerce's own add to cart, or the Store API `POST /wc/store/v1/cart/add-item`. |
| `cpops_refresh_cart`, `cpops_force_refresh_fragments` | `GET /wc/store/v1/cart` or `GET /cartpops/v1/cart`. The V1 force-refresh option is now **Advanced** > **General** > **Performance** > **Refresh cart data on page load**. |
| `cpops_update_cart` | Store API `update-item`. |
| `cpops_remove_product`, `cpops_restore_product` | Store API `/batch` with `remove-item`, or `POST /cartpops/v1/cart/remove-items`. Undo is a client action. |
| `cpops_apply_coupon`, `cpops_remove_coupon` | `POST /cartpops/v1/coupon` and `DELETE /cartpops/v1/coupon`, each with a `code`. |
| `cpops_calculate_shipping`, `cpops_update_shipping_method` (**Pro**) | The inline shipping calculator in the Pro drawer. There is no direct replacement endpoint. |
| Admin helpers such as `cartpops_json_search_products` | Admin REST routes under `cartpops/v1`, such as `/settings` and `/products/search`. |

V2 requests authenticate with a WooCommerce `Cart-Token` header and a nonce. CartPops also accepts requests only from your site's own origins and rate limits them per client. See [Store API and REST](/developers/store-api-and-rest). V1 registered no `wc-ajax` handlers of its own.

### Fragments

V1 filtered `woocommerce_add_to_cart_fragments` to return HTML keyed by selector, and its JavaScript called `replaceWith()` for each one. These keys are gone:

```text
.cpops-drawer-cart, div.cpops-drawer-notices-wrapper, div.cpops-cart-total
div.cpops-coupon-remove, div.cpops-drawer-recommendations-wrapper
div#cpops-floating-cart, .cpops-free-shipping-meter
.cartpops-cart__container-counter, .cartpops-cart__container-text
.cpops-popup-recommend-products, .cpops-added-product-item, div.cpops-assistant-items
```

V2 emits a single fragment, `.cartpops-cart-json`: an inert `script` element of type `application/json` that carries lightweight cart state, so a classic add can skip a second request. The visible cart updates from Interactivity API state.

:::warning[Do not recreate the V1 fragment keys]
Returning `.cartpops-cart__container-counter` from your own fragment filter makes WooCommerce replace V2's launcher badge, which is bound to state. The badge then stops updating. Use `cartpops:count-updated` instead.
:::

## Triggers, IDs and data attributes

| V1 | V2 status | What to know |
| --- | --- | --- |
| Class `cpops-toggle-drawer` on any element or menu item | Removed as a trigger | There is no click handler for it. See [Open the drawer from a custom button](#open-the-drawer-from-a-custom-button). |
| Click on a WooCommerce `a.added_to_cart` ("View cart") link opened the drawer | Removed | The link goes to the cart page. |
| Submit on `form.cart` sent through AJAX | Kept, behind a setting | With **Add to cart on product pages without reloading** on, CartPops posts the form's own fields to the form's URL, so WooCommerce's normal add to cart handling and validation run. It then fires `added_to_cart` with fresh fragments, like a shop page add. It does not fire `adding_to_cart`. File upload, external, grouped and redirect-to-cart cases, failed requests, and submits another handler already prevented are left to the browser. |
| `.cpops-toggle-assistant` with `data-assistant="shipping"` | Removed | The assistant is gone. |
| `data-cpops-cart-open="false"` on an add button | Kept | Skips the automatic open for that add. |
| `data-cpops-type="recommendation"`, `data-cpops-toggle="collapse"`, `data-cpops-target`, `data-dismiss="cpops-modal"` | Removed | |
| IDs `#cpops-drawer-modal`, `#cartpops-drawer`, `#cpops-floating-cart`, `#cartpops-cart-launcher-<n>` | Removed | V2 uses request-unique drawer IDs such as `cpops-drawer-<n>` for `aria-controls`, and does not give launchers an ID. Select on classes. |
| IDs `#cartpops-popup`, `#cartpops-bar`, `#cartpops-assistant` and their modal IDs | Removed | Retired features. |
| `html.cpops-trigger-open` scroll lock class | Removed | V2 sets an inline `overflow: hidden` on `body`. |
| `.cpops-show`, `.cpops-is-closing`, `.cpops-fade` animation classes | Removed | Open state is the class `cpops-drawer-open` on `.cpops-modal` and `.cpops-drawer`. |

V1 had no URL, hash or query-string trigger, and V2 has none either.

## CSS classes and custom properties

The class prefixes are unchanged: `cpops-` and `cartpops-`. The structure underneath changed, so only part of the V1 class set exists in V2. Your **Custom CSS** is migrated and loaded after the CartPops styles. See [CSS customization](/developers/css-customization).

**Kept as aliases on the current element:**

| V1 class | Now on |
| --- | --- |
| `.cpops-modal` | The block wrapper that holds the overlay and drawer |
| `.cpops-modal-backdrop` | `.cpops-overlay` |
| `.cpops-default-drawer`, `.cpops-modal-wrap`, `.cpops-panel` | `.cpops-drawer`, all on one element, so selectors that relied on them being nested no longer match |
| `.cpops-drawer-header` (and `__heading`, `__title`, `__close`) | `.cpops-drawer__header`, `.cpops-drawer__title`, `.cpops-drawer__close` |
| `.cpops-drawer-cart`, `.cpops-empty-cart`, `.cpops-drawer-footer` | `.cpops-drawer__content`, `.cpops-drawer__empty`, `.cpops-drawer__footer` |
| `.cartpops-cart__toggle`, `.cartpops-cart__container`, `.cpops-toggle-drawer` | The launcher button (styling only) |
| `.cpops-floating-cart__button`, `.cpops-floating-cart__count` | The floating launcher button and its count |
| `.cartpops-cart--items-indicator-*`, `.cartpops-cart--empty-indicator-hide`, `.cartpops-cart--show-subtotal-yes` | The launcher button, matching the current mode |
| `.cartpops-cart__container-counter`, `.cartpops-cart__container-text` | The launcher count and subtotal |
| `.cpops-free-shipping-meter` (and `__text`), `.cpops-shipping-progress-bar` (and `__active`, `__progress`) (**Pro**) | The horizontal meter, its message and its bar |

**Removed:** the V1 cart row internals (`.cpops-cart-item__quantity*`, `__product*`, `.cpops-cart-line-items`, `.cpops-restore-item`), totals and coupon-tag wrappers, notices, the Splide slider (`.cpops-slider*`), the V1 component kit (`.cpops-button`, `.cpops-label*`, `.cpops-collapse*`, `.cpops-tooltip*`), the circular meter, and all popup, bar and assistant classes. Selectors that depend on V1 wrapper depth, sibling order or `:nth-child` are unsupported.

**Custom properties:** V1 `--color-cpops-*` and `--cpops-width-drawer-*` variables set on `:root` keep working through a two-way bridge, and the live element exposes the effective value under the V1 name too. Setting a V1 variable on a narrower element is not supported. Use the current V2 name. `--cpops-animation-duration` and `--cpops-border-radius` kept their names. Removed: `--color-cpops-button-hover-opacity`, `--color-cpops-loader-color`, `--cpops-white-space-text` (now the **Product name display** setting), and the slider and popup colors.

| V1 variable (on `:root`) | V2 variable |
| --- | --- |
| `--color-cpops-accent-color` | `--cpops-primary` |
| `--color-cpops-background-primary` | `--cpops-background` |
| `--color-cpops-text-primary` | `--cpops-text-primary` |
| `--color-cpops-button-primary-background` | `--cpops-button-primary-bg` |
| `--cpops-width-drawer-desktop` | `--cpops-drawer-width` |
| `--cpops-width-drawer-mobile` | `--cpops-drawer-width-mobile` |

The full variable mapping lives in [CSS customization](/developers/css-customization).

## Code migrations

### Open the drawer from a custom button

In V1, any element with `cpops-toggle-drawer`, or a call to `CartPops.drawer.show()`, opened the drawer.

```js V1
jQuery( '.my-cart-button' ).on( 'click', function ( e ) {
	e.preventDefault();
	CartPops.drawer.show();
} );
```

In V2, dispatch `cartpops:open` on `document` when the user clicks.

```js V2
document.addEventListener( 'click', function ( event ) {
	if ( ! event.target.closest( '.my-cart-button' ) ) {
		return;
	}
	event.preventDefault();
	document.dispatchEvent( new CustomEvent( 'cartpops:open' ) );
} );
```

Give the link a real `href` (your cart page) so it still works if the click happens before the drawer script has loaded. Use `cartpops:close` and `cartpops:toggle` the same way.

### Keep a header counter in sync

V1 code usually listened for jQuery cart events or returned its own WooCommerce fragment. In V2, render the starting count on the server and update it from `cartpops:count-updated`.

```php header.php
<span class="my-cart-count">
	<?php echo (int) ( WC()->cart ? WC()->cart->get_cart_contents_count() : 0 ); ?>
</span>
```

```js V2
document.addEventListener( 'cartpops:count-updated', function ( event ) {
	document.querySelectorAll( '.my-cart-count' ).forEach( function ( el ) {
		el.textContent = event.detail.count;
	} );
} );
```

A fragment-based counter can keep working, because WooCommerce still applies `woocommerce_add_to_cart_fragments` and CartPops asks WooCommerce to refresh fragments after its own changes. That depends on WooCommerce's cart-fragments script being loaded, and many themes and speed plugins remove it. The event does not depend on that script.

### Add a line under the product name

In V1, you printed HTML after the item name.

```php functions.php (V1)
add_action( 'woocommerce_after_cart_item_name', function ( $cart_item, $cart_item_key ) {
	if ( ! empty( $cart_item['my_engraving'] ) ) {
		echo '<div class="my-engraving">Engraving: ' . esc_html( $cart_item['my_engraving'] ) . '</div>';
	}
}, 10, 2 );
```

In V2, add a row to `extraLines` with `cartpops_cart_item_data`.

```php functions.php (V2)
add_filter(
	'cartpops_cart_item_data',
	static function ( array $item, array $cart_item, string $cart_key ): array {
		if ( empty( $cart_item['my_engraving'] ) ) {
			return $item;
		}

		$item['extraLines'][] = array(
			'label' => __( 'Engraving', 'my-plugin' ),
			'value' => (string) $cart_item['my_engraving'],
		);
		return $item;
	},
	10,
	3
);
```

If your data is already exposed through WooCommerce's standard `woocommerce_get_item_data` filter, CartPops shows it in the drawer without extra code.

`extraLines` are plain text. Tags and control characters are stripped, and the drawer never runs row HTML. You can add at most 20 rows, with a label up to 160 bytes and a value up to 1,000 bytes. CartPops ignores overrides of price, quantity, key, link and image. The same filter also runs for Store API responses, so the lines survive quantity and remove updates. For custom client code, `customPresentation` carries a bounded map under your own plugin key. See [PHP filters](/developers/php-filters).

### Change the checkout URL

The filter name and call are the same as in V1.

```php functions.php
add_filter( 'cartpops_checkout_button_url', function ( $url ) {
	return home_url( '/express-checkout/' );
} );
```

What changed is validation. CartPops accepts an absolute HTTP or HTTPS URL with a host, or a root-relative path such as `/express-checkout/`. A bare relative value such as `express-checkout`, a protocol-relative URL, a URL with credentials or an unsupported scheme falls back to the WooCommerce checkout URL. Return a raw URL, not HTML. `cartpops_empty_cart_button_url` follows the same rules.

### Add classes

V1 filters such as `cartpops_drawer_classes` are not applied in V2. There is no filter that adds classes to CartPops-owned elements, and an action cannot do it either. Use CSS. Many elements keep their V1 class names, so you can scope your rules with a class on `body`.

```php functions.php
add_filter( 'body_class', function ( $classes ) {
	$classes[] = 'my-cartpops-skin';
	return $classes;
} );
```

```css Custom CSS
.my-cartpops-skin .cpops-drawer {
	--cpops-primary: #1d4ed8;
}
```

In **Pro**, the secondary drawer button still supports extra classes through `cartpops_secondary_btn_classes`. It is additive only.

```php functions.php
add_filter( 'cartpops_secondary_btn_classes', function ( array $classes, string $mode ): array {
	$classes[] = 'my-secondary-button';
	return $classes;
}, 10, 2 );
```

### Guard `fs_cartpops()`

`fs_cartpops()` and `cartpops_fs()` return `null` when the Freemius bootstrap did not complete. A V1 snippet that calls a method directly on the result can fail with a fatal error.

```php functions.php
$fs = function_exists( 'fs_cartpops' ) ? fs_cartpops() : null;
if ( null === $fs ) {
	return;
}
```

## Related

- [Upgrading to V2](/upgrading-to-v2)
- [PHP actions](/developers/php-actions)
- [PHP filters](/developers/php-filters)
- [JavaScript](/developers/javascript)
- [Store API and REST](/developers/store-api-and-rest)
- [CSS customization](/developers/css-customization)
