{"id":365248,"date":"2026-09-27T07:18:18","date_gmt":"2026-09-27T07:18:18","guid":{"rendered":"https:\/\/wordpress.org\/plugins\/product-add-ons\/"},"modified":"2026-09-27T08:34:04","modified_gmt":"2026-09-27T08:34:04","slug":"tnt-extras-for-woocommerce","status":"publish","type":"plugin","link":"https:\/\/wordpress.org\/plugins\/tnt-extras-for-woocommerce\/","author":16133903,"comment_status":"closed","ping_status":"closed","template":"","meta":{"version":"1.0.10","stable_tag":"1.0.10","tested":"7.1.2","requires":"5.8","requires_php":"7.4","requires_plugins":null,"header_name":"TNT \u2014 Simple Product Add-Ons","header_author":"Keshab Adhikari","header_description":"Add simple, optional paid add-ons to individual WooCommerce products. Add-ons follow the product from the single product page through Cart, Checkout, Order, and Invoice.","assets_banners_color":"c7dffe","last_updated":"2026-09-27 08:34:04","external_support_url":"","external_repository_url":"","donate_link":"","header_plugin_uri":"https:\/\/topnewthings.com","header_author_uri":"","rating":0,"author_block_rating":0,"active_installs":0,"downloads":54,"num_ratings":0,"support_threads":0,"support_threads_resolved":0,"author_block_count":0,"sections":["description","installation","faq","changelog"],"tags":{"1.0.10":{"tag":"1.0.10","author":"kesadh","date":"2026-09-27 08:34:04","revision":3715161}},"upgrade_notice":[],"ratings":[],"assets_icons":{"icon-128x128.png":{"filename":"icon-128x128.png","revision":3715161,"resolution":"128x128","location":"assets","locale":"","width":128,"height":128},"icon-256x256.png":{"filename":"icon-256x256.png","revision":3715161,"resolution":"256x256","location":"assets","locale":"","width":256,"height":256}},"assets_banners":{"banner-1544x500.png":{"filename":"banner-1544x500.png","revision":3715161,"resolution":"1544x500","location":"assets","locale":"","width":1544,"height":500},"banner-772x250.png":{"filename":"banner-772x250.png","revision":3715161,"resolution":"772x250","location":"assets","locale":"","width":772,"height":250}},"assets_blueprints":{},"all_blocks":[],"tagged_versions":["1.0.10"],"block_files":[],"assets_screenshots":[],"screenshots":[]},"plugin_section":[],"plugin_tags":[21138,2218,155785,1311,286],"plugin_category":[45],"plugin_contributors":[282896],"plugin_business_model":[],"class_list":["post-365248","plugin","type-plugin","status-publish","hentry","plugin_tags-add-ons","plugin_tags-options","plugin_tags-product-options","plugin_tags-upsell","plugin_tags-woocommerce","plugin_category-ecommerce","plugin_contributors-kesadh","plugin_committers-kesadh"],"banners":{"banner":"https:\/\/ps.w.org\/tnt-extras-for-woocommerce\/assets\/banner-772x250.png?rev=3715161","banner_2x":"https:\/\/ps.w.org\/tnt-extras-for-woocommerce\/assets\/banner-1544x500.png?rev=3715161","banner_rtl":false,"banner_2x_rtl":false},"icons":{"svg":false,"icon":"https:\/\/ps.w.org\/tnt-extras-for-woocommerce\/assets\/icon-128x128.png?rev=3715161","icon_2x":"https:\/\/ps.w.org\/tnt-extras-for-woocommerce\/assets\/icon-256x256.png?rev=3715161","generated":false},"screenshots":[],"raw_content":"<!--section=description-->\n<p><strong>No bloat, no upsells, no page builder \u2014 just optional add-ons that follow your product through checkout.<\/strong><\/p>\n\n<p>Product Add-Ons lets a store admin attach optional extras to any simple or variable product \u2014 for example an Office Chair at $200 with an Extended Warranty (+$30), a Headrest (+$45), a Footrest (+$25) and Premium Assembly (+$50). Shoppers tick the add-ons they want on the product page; the total updates instantly with no page reload, and the selected add-ons and their prices ride along with that product through the cart, checkout, the resulting order, order emails and any invoice built from standard WooCommerce order data.<\/p>\n\n<p>This plugin is intentionally simple: no page builder, no complex conditional logic, no separate add-on inventory. Just add-ons, priced, optionally imaged, shown in one of five ready-made designs.<\/p>\n\n<h4>Features<\/h4>\n\n<ul>\n<li>Up to 5 add-ons per product, managed from a repeater on the product's own \"Add-Ons\" tab<\/li>\n<li>Drag-and-drop reordering, per-add-on enable\/disable, name\/description\/price\/image fields<\/li>\n<li>Global default position and design, with an optional per-product override<\/li>\n<li>Five frontend designs: Classic List, Grid Cards, Compact List, Toggle Switches, and Image Cards<\/li>\n<li>Live price preview (Product Price \/ Add-Ons \/ Total) with zero page reloads<\/li>\n<li>Add-on price is only ever added when selected \u2014 the product's own base price is never modified<\/li>\n<li>Selections are re-validated and re-priced on the server at Add to Cart \u2014 the browser is never trusted for the actual charge<\/li>\n<li>Different add-on selections never merge into the same cart line<\/li>\n<li>Add-ons appear on the cart and checkout review tables, and are saved to the order as line item meta \u2014 visible in wp-admin (classic and HPOS), order emails, the customer's account order view, and most invoice\/PDF plugins that read standard order item meta<\/li>\n<li>Works with WooCommerce's High-Performance Order Storage (HPOS)<\/li>\n<li>Graceful when WooCommerce is inactive \u2014 the plugin stays dormant instead of causing a fatal error<\/li>\n<li>Optional data cleanup on uninstall (off by default \u2014 your data is safe unless you opt in)<\/li>\n<\/ul>\n\n<h4>Known limitations (by design, to keep this simple)<\/h4>\n\n<ul>\n<li>The Cart\/Checkout <strong>blocks<\/strong> (as opposed to the classic shortcode-based Cart\/Checkout pages) are not yet officially declared compatible; the underlying data hooks used here are widely supported by WooCommerce Blocks' backward-compatibility layer, but this hasn't been exhaustively tested against every block-based theme.<\/li>\n<li>Add-ons are configured per simple\/variable product (variable add-ons apply to the whole product, not per-variation).<\/li>\n<li>Add-ons don't carry their own separate stock\/inventory tracking.<\/li>\n<li>The Product CSV importer\/exporter and the REST API do not currently include add-on fields.<\/li>\n<li>\"Order Again\" (re-ordering a past order from My Account) re-adds the base product but does not currently re-select its add-ons.<\/li>\n<\/ul>\n\n<!--section=installation-->\n<ol>\n<li>Upload the plugin folder to <code>\/wp-content\/plugins\/<\/code>, or install the zip via Plugins \u2192 Add New \u2192 Upload Plugin.<\/li>\n<li>Activate the plugin. WooCommerce must already be active \u2014 if it isn't, the plugin stays installed but inactive and shows a notice until WooCommerce is available.<\/li>\n<li>Go to WooCommerce \u2192 Settings \u2192 Product Add-Ons to set the default position, design, and what's shown (image\/price\/description).<\/li>\n<li>Edit a product, open its <strong>Add-Ons<\/strong> tab, tick \"Enable Add-Ons\", and add up to 5 add-ons.<\/li>\n<\/ol>\n\n<!--section=faq-->\n<dl>\n<dt id=\"does%20this%20change%20my%20product%27s%20actual%20price%3F\"><h3>Does this change my product's actual price?<\/h3><\/dt>\n<dd><p>No. The product's stored price is never modified. Add-on prices are added only to the line item shown to the customer, and only for the add-ons they actually select.<\/p><\/dd>\n<dt id=\"what%20happens%20to%20an%20order%20if%20i%20later%20edit%20or%20delete%20an%20add-on%3F\"><h3>What happens to an order if I later edit or delete an add-on?<\/h3><\/dt>\n<dd><p>Orders already placed keep the name and price that was charged at the time \u2014 editing an add-on afterwards never changes past orders. If you edit an add-on's price while it's still sitting unpurchased in someone's cart, their cart total will reflect the corrected price the next time it recalculates (the same way a normal product price correction would). If you delete an add-on entirely, any cart that already had it selected keeps the price it had captured at the time it was added.<\/p><\/dd>\n<dt id=\"how%20many%20add-ons%20can%20a%20product%20have%3F\"><h3>How many add-ons can a product have?<\/h3><\/dt>\n<dd><p>Up to 5. This is a fixed limit in the current version.<\/p><\/dd>\n<dt id=\"why%20don%27t%20i%20see%20the%20google%20pay%20%2F%20apple%20pay%20%2F%20link%20button%20on%20a%20product%20with%20add-ons%3F\"><h3>Why don't I see the Google Pay \/ Apple Pay \/ Link button on a product with add-ons?<\/h3><\/dt>\n<dd><p>That's intentional. Those \"express checkout\" buttons charge whatever price they're shown before the customer ever sees the add-on choices, so letting one through would silently check out without any add-ons the customer might have wanted. Whenever a product has active add-ons, the plugin hides that button so customers go through the normal Add to Cart flow instead, where the price is always correct. The button is unaffected on products with no add-ons, and it still appears normally on the Cart and Checkout pages (by then the price is already correct).<\/p><\/dd>\n<dt id=\"does%20the%20add-on%20price%20include%20or%20exclude%20tax%3F\"><h3>Does the add-on price include or exclude tax?<\/h3><\/dt>\n<dd><p>Add-on prices are added directly into the product's line item, so they automatically follow that product's tax class and your store's normal tax display settings \u2014 you don't need to configure tax separately for add-ons.<\/p><\/dd>\n\n<\/dl>\n\n<!--section=changelog-->\n<h4>1.0.10<\/h4>\n\n<ul>\n<li>Change: the WordPress.org review team has confirmed and reserved the new slug <code>tnt-extras-for-woocommerce<\/code>. Text Domain header and every translation string now updated to match, for real this time \u2014 this is the final value going forward.<\/li>\n<li>Add: <code>Requires Plugins: woocommerce<\/code> header, flagged as useful by the review team's automated dependency check since the plugin extends WooCommerce; declares the dependency to WordPress core (6.5+) so it can prompt\/require WooCommerce to be active before this plugin activates.<\/li>\n<\/ul>\n\n<h4>1.0.9<\/h4>\n\n<ul>\n<li>Fix: reverted the Text Domain header and every translation string from \"tnt-extras-for-woocommerce\" back to \"product-add-ons\". WordPress.org's Plugin Check derives its \"expected\" text domain from the slug currently reserved for this submission, which is still <code>product-add-ons<\/code> \u2014 the review team has not yet processed the <code>tnt-extras-for-woocommerce<\/code> rename requested in 1.0.8, so shipping that value now fails every text-domain check. The plugin's <em>display<\/em> name stays \"TNT Extras for WooCommerce\" (that satisfies the reviewer's naming objection); only the internal Text Domain reverts, and only until the new slug is confirmed, at which point it will be updated to match in a follow-up release.<\/li>\n<li>Fix: removed a stray <code>.DS_Store<\/code> file from the plugin folder (\"Hidden files are not permitted\").<\/li>\n<\/ul>\n\n<h4>1.0.8<\/h4>\n\n<ul>\n<li>Change: renamed the plugin from \"Product Add-Ons\" to \"TNT Extras for WooCommerce\" (slug: <code>tnt-extras-for-woocommerce<\/code>) at the request of the WordPress.org review team \u2014 the previous name was judged too close to WooCommerce's own \"Product Add-Ons\" extension. Text Domain and every translation string updated to match; user-facing labels (\"Product Add-Ons\" settings tab, admin notices) renamed to \"TNT Extras\".<\/li>\n<\/ul>\n\n<h4>1.0.7<\/h4>\n\n<ul>\n<li>Fix: <code>WordPress.DB.SlowDBQuery.slow_db_query_meta_key<\/code> warnings (4x) on the <code>$wpdb-&gt;delete()<\/code> cleanup calls in the uninstall routine \u2014 added to the existing <code>phpcs:disable<\/code>\/<code>phpcs:enable<\/code> justification block alongside the direct-query codes already suppressed there.<\/li>\n<\/ul>\n\n<h4>1.0.6<\/h4>\n\n<ul>\n<li>Fix: Text Domain header and every translation string now consistently use \"product-add-ons\", matching the plugin's WordPress.org slug (the 1.0.5 changelog entry below describing the opposite move was itself a regression \u2014 this corrects it for good).<\/li>\n<li>Fix: all internal class names (previously the generic <code>AP_<\/code> prefix, e.g. <code>AP_Settings<\/code>) renamed to a unique, plugin-specific prefix (<code>Product_Add_Ons_*<\/code>) together with the plugin's global helper functions and constants, resolving \"non-prefixed\" naming warnings from the WordPress.org Plugin Check.<\/li>\n<li>Fix: added the missing nonce-verification justification comments on two <code>$_POST<\/code> reads in the add-to-cart handler (the add-to-cart form has no dedicated nonce in core WooCommerce; selections are always re-validated server-side against the product's own configuration regardless).<\/li>\n<li>Fix: <code>$product<\/code> global reassignment in the frontend renderer now carries the same justified <code>phpcs:ignore<\/code> as its declaration (WooCommerce's own global, not one this plugin defines).<\/li>\n<\/ul>\n\n<h4>1.0.5<\/h4>\n\n<ul>\n<li>Fix: resolved a Plugin Check text-domain mismatch \u2014 the \"Text Domain\" header and every translation string now consistently use \"addon-products\", matching the plugin's actual folder\/slug (this reverses the 1.0.4 change, which had moved the wrong direction).<\/li>\n<li>Fix: a translatable string with two \"%d\" placeholders is now correctly numbered (\"%1$d\"\/\"%2$d\") so translators can reorder them.<\/li>\n<li>Fix: corrected inline <code>phpcs:ignore<\/code> placement for two <code>$_POST<\/code> reads in the cart add-to-cart handling so the documented justification sits on the exact flagged line.<\/li>\n<li>Fix: added justified <code>phpcs:ignore<\/code> annotations for WooCommerce's own <code>$product<\/code> global in the frontend renderer, and renamed a local <code>$settings<\/code> variable in the uninstall routine to a prefixed name \u2014 both were flagged as unprefixed globals.<\/li>\n<li>Fix: shortened the plugin's short description so it's no longer truncated on WordPress.org.<\/li>\n<\/ul>\n\n<h4>1.0.4<\/h4>\n\n<ul>\n<li>Fix: WordPress.org's automated Plugin Check failed the submission on three points, now all resolved \u2014 \"Tested up to\" bumped to the current WordPress version; the Text Domain header (and every translation string in the code) changed from \"addon-products\" to \"product-add-ons\" to match the plugin's assigned slug; the \"Domain Path\" header was removed since no bundled translation files are shipped (WordPress.org loads translations for hosted plugins automatically, without needing this).<\/li>\n<li>Housekeeping: removed the load_plugin_textdomain() call, which is unnecessary for WordPress.org-hosted plugins and can trigger a \"translation loading was triggered too early\" notice on WordPress 6.7+; tidied the uninstall routine's database cleanup to use $wpdb-&gt;delete() with clear justification comments instead of raw SQL.<\/li>\n<\/ul>\n\n<h4>1.0.3<\/h4>\n\n<ul>\n<li>Fix: plugin upload was rejected with \"Your plugin and author URIs are the same\" \u2014 the header had identical Plugin URI and Author URI values. Removed the Author URI line (Plugin URI stays topnewthings.com).<\/li>\n<\/ul>\n\n<h4>1.0.2<\/h4>\n\n<ul>\n<li>Fix: Google Pay \/ Apple Pay \/ Link \"express checkout\" buttons on the product page were charging only the base product price, skipping any selected add-ons, because those buttons bypass the normal Add to Cart flow entirely. These buttons are now hidden on any product page where add-ons are active, so customers are guided through the normal flow where the price is always correct; Add to Cart, Cart and Checkout are unaffected.<\/li>\n<\/ul>\n\n<h4>1.0.1<\/h4>\n\n<ul>\n<li>Fix: activation could trigger a fatal error (\"Class AP_Settings not found\") because the activation hook loaded the install class without first loading the settings class it depends on. Activation now loads both, in the right order.<\/li>\n<\/ul>\n\n<h4>1.0.0<\/h4>\n\n<ul>\n<li>Initial release: repeater-based per-product add-ons (max 5), 5 frontend designs, live price preview, full Cart \u2192 Checkout \u2192 Order \u2192 Invoice persistence, HPOS compatibility.<\/li>\n<\/ul>","raw_excerpt":"No bloat, no upsells, no page builder \u2014 just optional add-ons that follow your product through checkout.","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin\/365248","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin"}],"about":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/types\/plugin"}],"replies":[{"embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/comments?post=365248"}],"author":[{"embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wporg\/v1\/users\/kesadh"}],"wp:attachment":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/media?parent=365248"}],"wp:term":[{"taxonomy":"plugin_section","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_section?post=365248"},{"taxonomy":"plugin_tags","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_tags?post=365248"},{"taxonomy":"plugin_category","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_category?post=365248"},{"taxonomy":"plugin_contributors","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_contributors?post=365248"},{"taxonomy":"plugin_business_model","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_business_model?post=365248"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}