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.
| Storage | What it holds |
|---|---|
u2code_pa_group post type | One 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 type | Reusable option sets; the options live in _u2code_pa_options meta. |
| Product meta | Product-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 item | Selections travel under the u2code_pa_addons key as a list of normalized entries (shape below). |
| Order item meta | One 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 aWP_Errorto 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 insideviews/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 customprice_type.u2code_pa/skip_cart_price_adjustment( false, $cartKey, $cartItem ) — returntrueto 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 (insidewp-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; returntruefor custom product templates.u2code_pa/script_dependencies( $deps ) — dependencies of the storefront script (defaults:jquery,wp-i18n).u2code_pa/frontend_data( $data ) — theu2codePaAddonsDataobject 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.
| Method | Route | Purpose |
|---|---|---|
| GET / POST | /addons | List addon groups / create one. |
| GET / PUT / DELETE | /addons/{id} | Read, update or delete a group. |
| POST | /addons/{id}/duplicate | Duplicate a group (copy arrives as a draft). |
| PUT | /addons/reorder | Persist a new group order. |
| POST | /addons/import | Import exported group JSON (one group or a list). |
| POST | /addons/bulk-delete | Delete several groups at once. |
| GET / PUT | /product-fields/{id} | Read / update a product’s own addon fields. |
| GET | /product-addons | List 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-sets | List option sets / create one. |
| PUT / DELETE | /option-sets/{id} | Update or delete an option set. |
| GET | /products, /variations/{id}, /terms, /customers, /attributes, /attribute-options | Search 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.