Developer Documentation

Hooks, filters, REST endpoints and data structures for extending U2Code Product Addons for WooCommerce.

Overview

U2Code Product Addons for WooCommerce is built to be extended. Every PHP hook lives under the u2code_pa/ prefix, database keys use u2code_pa_, and the REST namespace is u2code-product-addons-for-woocommerce/v1. Nothing here requires touching plugin files — put the snippets in a small companion plugin or your theme’s functions.php.

Data Model

Everything is stored in standard WordPress primitives — no custom tables. That means groups survive exports, search-replace migrations and the usual backup tooling.

StorageWhat it holds
u2code_pa_group post typeOne post per addon group. The post title is the group name; post status is the Active toggle (publish = active, draft = paused).
Group post meta_u2code_pa_fields, _u2code_pa_assignment, _u2code_pa_visibility, _u2code_pa_appearance, _u2code_pa_cart — the arrays you see as builder panels.
u2code_pa_option_set post typeReusable option sets; the options live in _u2code_pa_options meta.
Product metaProduct-specific addons: _u2code_pa_product_fields, _u2code_pa_product_status, _u2code_pa_product_appearance, _u2code_pa_product_cart. Duplicated and deleted with the product.
Cart itemSelections travel under the u2code_pa_addons key as a list of normalized entries (shape below).
Order item metaOne visible meta row per entry (label → display value), plus a machine-readable copy of all entries in _u2code_pa_addons.

A single cart / order entry is a plain array:

array(
	'field_id'   => 'f_1a2b3c',
	'type'       => 'input',
	'name'       => 'Engraving',
	'value'      => 'EMMA',
	'display'    => 'EMMA',
	'price'      => 0.50,
	'price_type' => 'char', // flat | percent | qty | char | area | per_day
)

Global settings are regular options: u2code_pa_theme (storefront style), u2code_pa_max_file_size, u2code_pa_allowed_file_types, u2code_pa_upload_retention_days, u2code_pa_delete_data_on_uninstall, and one u2code_pa_integration_{id} toggle per integration.

Custom Field Types

Register a type through the u2code_pa/field_types filter and it appears in the builder’s Type dropdown automatically — label, description, pricing toggle and all. The icon key names a @wordpress/icons export in kebab-case and may be omitted; the builder falls back to a generic glyph.

add_filter( 'u2code_pa/field_types', function ( $types ) {
	$types['engraving_font'] = array(
		'type'           => 'engraving_font',
		'label'          => 'Engraving Font',
		'description'    => 'Font picker for engraved text.',
		'hasOptions'     => false,
		'hasPlaceholder' => false,
		'hasPricing'     => true,  // one field-level price
		'isContent'      => false, // collects a value
	);
	return $types;
} );

The storefront asks you to render unknown types via a dynamic action. Name your input u2code_pa_addon[<field id>] and it is parsed, validated and stored like any built-in field (file inputs use u2code_pa_addon_file_<field id>):

add_action( 'u2code_pa/render_field_engraving_font', function ( $field, $renderer ) {
	printf(
		'<select name="u2code_pa_addon[%s]"><option>Serif</option><option>Script</option></select>',
		esc_attr( $field['id'] )
	);
}, 10, 2 );

Two further filters complete a custom type’s lifecycle:

  • u2code_pa/validate_field ( true, $field, $value, $label ) — return a WP_Error to reject a submitted value; the message is shown to the customer.
  • u2code_pa/build_field_entries ( null, $field, $value, $label, $calculator ) — return an array of cart entries for field types the core parser does not understand.

hasPricing and hasOptions are mutually exclusive: option-based fields carry a price per option, everything else carries one field-level price.

Render Positions

The position setting maps a slug to the WooCommerce action the group renders on. Five positions ship by default (before / after the Add to Cart button, before / after the quantity input, after the variations table). Add your own for themes with non-standard product pages:

add_filter( 'u2code_pa/positions', function ( $positions ) {
	$positions['my_theme_extras'] = 'mytheme_product_extras_hook';
	return $positions;
} );

The action must fire inside the add-to-cart <form> — outside it, submitted values never reach the cart. On variable products, positions WooCommerce hides until a variation is chosen are relocated above the variations table automatically.

Custom Storefront Styles

The six bundled styles come from the u2code_pa/themes filter, and yours can join them — the Settings picker renders every registered style as a live preview. The plugin only ships stylesheets for its own slugs, so enqueue your own, scoped to .u2code-pa-theme-<slug>:

add_filter( 'u2code_pa/themes', function ( $themes ) {
	$themes['midnight'] = array(
		'label'       => 'Midnight',
		'description' => 'Dark panel that matches our theme.',
	);
	return $themes;
} );

add_action( 'wp_enqueue_scripts', function () {
	wp_enqueue_style( 'my-pa-midnight', get_theme_file_uri( 'css/pa-midnight.css' ) );
} );

If a stored slug disappears — say the add-on registering it is deactivated — the plugin falls back to the Default style rather than rendering unstyled fields.

Markup & Template Hooks

  • u2code_pa/field_template ( $template, $field ) — swap the view file used for a field; the value is a file name inside views/frontend/fields/.
  • u2code_pa/before_group / u2code_pa/after_group ( $addon, $product ) — actions that fire around every rendered group, inside its wrapper.

Pricing & Cart Filters

Server-side pricing is filterable at every step. Signatures below list the filtered value first.

  • u2code_pa/price_types ( $types ) — the accepted pricing modes.
  • u2code_pa/cart_entries ( $entries, $addons, $postData ) — the normalized entries parsed from a submitted form, before they are stored on the cart item.
  • u2code_pa/unit_price_adjustment ( $adjustment, $entries, $basePrice, $quantity ) — the per-unit amount added to the product price.
  • u2code_pa/entry_line_total ( $total, $entry ) — the line total of a single entry; the place to price a custom price_type.
  • u2code_pa/skip_cart_price_adjustment ( false, $cartKey, $cartItem ) — return true to leave a specific cart item’s price untouched.
  • u2code_pa/entry_display_value ( $display, $entry ) — the human-readable value shown in cart, checkout and order screens.
  • u2code_pa/reorder_entries ( $visible, $storedEntries, $addons ) — the entries restored when a customer re-orders.
  • u2code_pa/edit_link ( $url, $cartItem, $cartItemKey ) — the “Edit options” URL on the cart line.

Targeting & Admin Filters

  • u2code_pa/addons_for_product ( $matched, $productId ) — the final list of groups that apply to a product, after targeting rules run (cached per request).
  • u2code_pa/supports_addons ( $supported, $product ) — which product types render addons at all.
  • u2code_pa/supports_order_editing ( $supported, $product ) — which order line types get the Configure addons button.
  • u2code_pa/duplicate_data ( $data, $addon ) — the payload used when a group is duplicated.

Currency Integration

Six currency switchers are supported out of the box. Anything else can plug into the same filter — convert a base-currency amount into the active currency and every addon price, subtotal and stored cart value follows:

add_filter( 'u2code_pa/convert_price', function ( $price, $priceType ) {
	return $price * my_switcher_get_rate();
}, 10, 2 );

Ratio-based amounts (percentage pricing) are never passed through the filter — a ratio is currency-neutral. The effective rate is probed with a large sample amount, so switchers that round or charm-price individual values still produce consistent totals.

File Uploads

  • u2code_pa/upload_subdir ( $subdir, $context ) — the protected subdirectory (inside wp-content/uploads) customer files are stored in.
  • u2code_pa/upload_retention ( $seconds ) — how long files never attached to an order are kept; derived from the retention setting.

Cleanup runs on a daily u2code_pa_cleanup_uploads task — scheduled through Action Scheduler when WooCommerce provides it, WP-Cron otherwise. Files attached to an order are always kept.

Assets & Frontend Data

  • u2code_pa/enqueue_assets ( $needed ) — whether storefront assets load on the current page. Product, cart and checkout pages (plus [product_page] shortcodes) are detected automatically; return true for custom product templates.
  • u2code_pa/script_dependencies ( $deps ) — dependencies of the storefront script (defaults: jquery, wp-i18n).
  • u2code_pa/frontend_data ( $data ) — the u2codePaAddonsData object localized to the storefront script; add your own keys for a companion script.

JavaScript API

The storefront script exposes jQuery events on the add-to-cart form:

jQuery( 'form.cart' )
	// Fires BEFORE totals are summed - the calculation object is mutable.
	.on( 'u2code_pa:calculate', function ( e, calc ) {
		calc.items.push( { label: 'Rush fee', total: 5 } );
	} )
	// Fires after every render with the final calculation object.
	.on( 'u2code_pa:updated', function ( e, calc ) {
		console.log( calc.total );
	} );

// Force re-evaluation after changing the form programmatically:
jQuery( 'form.cart' ).trigger( 'u2code_pa:update' );

A custom field type can also join the storefront logic itself by registering an extension object on window.u2codePaAddonsExt before the form initializes. Extensions may implement refresh( form ), validateField( $field, type, required, form ) returning an error string, and collectItem( $field, type, priceType, item, form ) returning the totals-box item (or null to skip it).

Live instances are available at document.u2codePaAddons.instances for debugging.

u2code_pa:calculate changes the displayed totals only. Anything that must survive to the cart belongs in the server-side pricing filters above — the cart never trusts browser math.

REST API

The admin app is an ordinary consumer of the plugin’s REST API — everything the builder does, your tooling can do. All routes live under u2code-product-addons-for-woocommerce/v1 and use standard WordPress authentication (cookie + X-WP-Nonce, or an application password). Every route requires the manage_woocommerce capability; the product-fields routes additionally require edit rights on the product itself.

MethodRoutePurpose
GET / POST/addonsList addon groups / create one.
GET / PUT / DELETE/addons/{id}Read, update or delete a group.
POST/addons/{id}/duplicateDuplicate a group (copy arrives as a draft).
PUT/addons/reorderPersist a new group order.
POST/addons/importImport exported group JSON (one group or a list).
POST/addons/bulk-deleteDelete several groups at once.
GET / PUT/product-fields/{id}Read / update a product’s own addon fields.
GET/product-addonsList products carrying product-specific addons.
POST/product-addons/export
/product-addons/import
/product-addons/bulk-delete
Export, import or bulk-clear product-specific addons.
GET / POST/option-setsList option sets / create one.
PUT / DELETE/option-sets/{id}Update or delete an option set.
GET/products, /variations/{id}, /terms, /customers, /attributes, /attribute-optionsSearch endpoints powering the builder’s pickers.

Import & Export Format

An exported group is plain JSON carrying the same arrays the plugin stores as post meta: title, fields, assignment, visibility, appearance and cart. The payload is portable across sites; imported groups always arrive as drafts so nothing goes live unreviewed, and field IDs are re-mapped on import to avoid collisions.