Skip to content

[Checkout] Support for shareable checkout URLs which pre-populate the cart/coupons - #58140

Merged
senadir merged 41 commits into
trunkfrom
experiment/cart-link-with-token-based-session
Jun 12, 2025
Merged

[Checkout] Support for shareable checkout URLs which pre-populate the cart/coupons#58140
senadir merged 41 commits into
trunkfrom
experiment/cart-link-with-token-based-session

Conversation

@mikejolley

@mikejolley mikejolley commented May 20, 2025

Copy link
Copy Markdown
Member

Submission Review Guidelines:

Changes proposed in this Pull Request

This PR introduces the ability to generate a cart from a link which redirects to checkout with a unique session ID, or load a cart via a token in the query string.

An example link:

https://store.local/checkout-link/?products=18%3A2%2C19%3A1&coupon=TEST

or without encoding:

https://store.local/checkout-link/?products=18:2,19:1&coupon=TEST

The main changes include:

  • New /checkout-link endpoint that allows users to pre-populate their cart by visiting a URL with product IDs and product Qty, and optional coupon code.
  • Support for loading a cart session via a token in the query string, enabling persistent and shareable carts.
  • Improved error handling and user notices for invalid products and coupons, with more user-friendly messages (e.g., “Product with ID '999999' was not found and cannot be added to the cart.” and “Coupon 'INVALID_COUPON' cannot be applied because it does not exist.”).
  • Refactored session and cart token utilities to support the new flows.
  • E2E Playwright tests for:
    • Valid cart and coupon via checkout link
    • Invalid coupon via checkout link
    • Invalid product(s) via checkout link (both mixed and only invalid)
  • Updated error messages in class-wc-coupon.php and class-wc-discounts.php for consistency and clarity.

Benefits

  • Enables external sites, emails, or marketing campaigns to link directly to a pre-filled WooCommerce cart or checkout.
  • Supports persistent, shareable carts via tokens.
  • Improves UX with clear, actionable error messages for invalid cart/coupon scenarios.

Testing instructions

  1. Valid Cart Link

    • Visit /checkout-link?products=<valid_id_1>,<valid_id_2>&coupon=<valid_coupon>
    • You should be redirected to checkout with both products in the cart and the coupon applied.
  2. Valid Cart Link w/ product quantities

    • Visit /checkout-link?products=<valid_id_1>:2,<valid_id_2>&coupon=<valid_coupon>
    • You should be redirected to checkout with both products in the cart and the coupon applied.
    • <valid_id_1> should have a quantity of 2
  3. Invalid Coupon

    • Visit /checkout-link?products=<valid_id_1>,<valid_id_2>&coupon=INVALID_COUPON
    • You should see a notice: Coupon "INVALID_COUPON" cannot be applied because it does not exist.
  4. Invalid Product (mixed)

    • Visit /checkout-link?products=<valid_id_1>,999999
    • You should see a notice: Product with ID "999999" was not found and cannot be added to the cart.
    • The valid product should still be in the cart.
  5. Only Invalid Product

    • Visit /checkout-link?products=999999
    • You should be redirected to the cart page.
    • You should see a notice: The provided checkout link was out of date or invalid. No products were added to the cart.
  6. Cart Token (logged out)

    • Repeat step 1. You'll be on the checkout.
    • Copy the page address. Notice session= is part of the URL.
    • Open a new incognito window or different browser.
    • Visit the link. You should see the same cart contains and can proceed with ordering.
    • Check the application panel in console and look at the session cookie. Compare to the original window. The cookies will be difference because the session was cloned when launching incognito session.
  7. E2E Tests

    • Run Playwright E2E tests in plugins/woocommerce/tests/e2e-pw/tests/checkout/checkout-link.spec.js to verify all scenarios.

Checklist

Changelog entry

  • Automatically create a changelog entry from the details below.
  • This Pull Request does not require a changelog entry. (Comment required below)
Changelog Entry Details

Significance

  • Patch
  • Minor
  • Major

Type

  • Fix - Fixes an existing bug
  • Add - Adds functionality
  • Update - Update existing functionality
  • Dev - Development related task
  • Tweak - A minor adjustment to the codebase
  • Performance - Address performance issues
  • Enhancement - Improvement to existing functionality

Message

Changelog Entry Comment

Comment

@github-actions github-actions Bot added the plugin: woocommerce Issues related to the WooCommerce Core plugin. label May 20, 2025
Comment thread plugins/woocommerce/src/Blocks/Domain/Services/CheckoutLink.php Outdated
Comment thread plugins/woocommerce/src/Blocks/Domain/Services/CheckoutLink.php Outdated
@mikejolley mikejolley added Checkout Issues related to checkout page. API labels May 21, 2025
@mikejolley mikejolley changed the title [Experiment] Add ability to generate a cart from a link or load up via token in querystring [Checkout] Support for shareable checkout URLs which pre-populate the cart/coupons May 21, 2025
@alewolf

alewolf commented Jun 13, 2025

Copy link
Copy Markdown

@mikejolley May I ask what page type this will be? I couldn't find anything in the PR. Will is_checkout() return true? Or will this be another page type entirely?

I'm asking as I want to implement this with my Pixel Manager for WooCommerce and trigger an add-to-cart event when that specific URL with those parameters is loaded.

shaybanshee pushed a commit that referenced this pull request Jun 13, 2025
… cart/coupons (#58140)

* Shared session utils

* Add token handling middleware to client

* Use SessionUtils

* Checkout Link handler

* Linting error

* Linting

* Revert "Linting error"

This reverts commit 1d08d92.

* Revert "Add token handling middleware to client"

This reverts commit 592df3a.

* Update error notices to make more friendly

* Update wc_rand_hash function to make use of random_bytes

We'll use this for customer ID generation

* Session handling

* Refactor session handler to restore session from query string

* remove debugging

* No need to set cookie manually after refactor

* Remove example

* update wc_rand_hash tests

* Remove set_customer_id as its unused

* Rename to CartTokenUtils and handle payload

* Tidy up classes

* Add CartTokenUtilsTests

* Basic test to ensure products are added to cart when using checkout link

* Lint

* e2e test and empty cart handling

* Handle qty

* Changelog

* Update wc_rand_hash and test to handle 0 length provided

* Check if wp_fast_hash exists before usage

* Put back wc_empty_cart to prevent test breakage

* Load data when init session

* Update test text

* Update plugins/woocommerce/changelog/integra-28-add-ability-to-generate-a-cart-from-link-or-load-up-via

Co-authored-by: Seghir Nadir <nadir.seghir@gmail.com>

* Validate before clear cart. Add helper for getting products from query string

* Preserve the query string

* Apply coupon after checking if products were added

* Improve error handling

* When restoring token, exclude customer address

* Add test for invalid link

* Tidy up logic

* Clone carts

* Handle older cookies with double pipes

* Cover logged in and guest for checkout links

---------

Co-authored-by: Seghir Nadir <nadir.seghir@gmail.com>
@mikejolley

Copy link
Copy Markdown
Member Author

@alewolf There is no page type; its a redirect early during template_redirect. is_checkout() won't work here. Any add-to-cart actions would still fire, since this uses standard add to cart methods to populate the cart.

@alewolf

alewolf commented Jun 17, 2025

Copy link
Copy Markdown

@mikejolley

Any add-to-cart actions would still fire, since this uses standard add to cart methods to populate the cart.

Does that include front-end JavaScript/jQuery add-to-cart events?

@mikejolley

Copy link
Copy Markdown
Member Author

@alewolf Server side woocommerce_add_to_cart. The redirect happens too early for JavaScript.

@dabesa

dabesa commented Jun 23, 2025

Copy link
Copy Markdown

How does this differ from the existing add-to-cart URL parameter?
es:
https://store.local/checkout/?add-to-cart=233

@helgatheviking

helgatheviking commented Jun 23, 2025

Copy link
Copy Markdown
Contributor

Any add-to-cart actions would still fire, since this uses standard add to cart methods to populate the cart

How would you support customizable products, like product addons, bundles, name your price, etc
https://store.local/checkout-link/?products=18%3A2%2C19%3A1&coupon=TEST

For example if product 18 is name your price how would I support passing an additional parameter for price?

@sverleis

sverleis commented Jun 24, 2025

Copy link
Copy Markdown

How does this differ from the existing add-to-cart URL parameter? es: https://store.local/checkout/?add-to-cart=233

From my understanding, one element that this has is the ability to add coupons, which the add-to-cart feature lacks. This can also add multiple different products, rather than just 1.

@pacotole

Copy link
Copy Markdown

Please, allow multiple coupons to be added.

@LuigiPulcini

LuigiPulcini commented Jul 16, 2025

Copy link
Copy Markdown
Contributor

How would you support customizable products, like product addons, bundles, name your price, etc

@helgatheviking About your question – which I am interested in as well –, a viable approach would be to add to the cart page a "Share this cart" custom button that generates the cart token for the current cart. This method also simplifies the creation of the link for complex cart contents, so you don't have to compile a URL manually.

The process would be this:

  • you visit your website as a guest (this is necessary because of this check);
  • you add any products to the cart, configuring each product with the appropriate options/metadata;
  • once you are satisfied with the cart content, you click the "Share this cart" button and use that URL.

I created a proof of concept with the code snippet below and it works on the Classic Cart.

add_action( 'woocommerce_proceed_to_checkout', 'dev_test_woocommerce_proceed_to_checkout', 21 );
function dev_test_woocommerce_proceed_to_checkout() {
	if ( ! class_exists( 'Automattic\WooCommerce\StoreApi\Utilities\CartTokenUtils' ) ) {
		return;
	}
	
	$session_token     = Automattic\WooCommerce\StoreApi\Utilities\CartTokenUtils::get_cart_token( (string) wc()->session->get_customer_id() );
	$checkout_link_url = add_query_arg( 'session', $session_token, wc_get_checkout_url() );
	?>
		<a href="<?php echo esc_url( $checkout_link_url ); ?>" class="button alt<?php echo esc_attr( wc_wp_theme_get_element_class_name( 'button' ) ? ' ' . wc_wp_theme_get_element_class_name( 'button' ) : '' ); ?>">
			<?php esc_html_e( 'Share this cart', 'woocommerce' ); ?>
		</a>
	<?php
}

@helgatheviking

helgatheviking commented Jul 16, 2025

Copy link
Copy Markdown
Contributor

@LuigiPulcini that's clever, I like it! Is the session token limited to the user who made the link? Or you can send it to anybody?

I also took a pass at adding support for additional params to the checkout link in this PR. I was able to get it working with my name your price plugin (so you can add a product with a custom price to the cart via url), but another of my plugins is a bit more complex and would require more integration.

@LuigiPulcini

Copy link
Copy Markdown
Contributor

Is the session token limited to the user who made the link? Or you can send it to anybody?

@helgatheviking As long as the user creating the cart is a guest (i.e. you are not logged in), the cart token can be shared to anyone. You can easily verify that by creating a link and use it in a different browser or computer. The check I mentioned in my previous comment bails out if the user who created the cart is a registered user of the website.

@pacotole The technique I described here also allows the use of multiple coupons because the cart is captured as a whole via its cart token and that includes any coupons added to it, no matter how many.

@LuigiPulcini

Copy link
Copy Markdown
Contributor

@helgatheviking About your other PR, I think that manually adding fully-configured products to a cart and generating a cart token for it is much less laborious than any attempt at compiling the URL, even when that is done programmatically (which would require additional code anyway).

Leveraging WooCommerce's built-in capability of storing guest carts in the database makes even more sense now that we have the CartTokenUtils class and the updated session handler.

@helgatheviking

Copy link
Copy Markdown
Contributor

I'm inclined to agree with you, esp if there's a way to support generating a guest token even while logged in- since I'm guessing store owners would want to generate links for customers.

but I think we could do both. If the checkout url feature exists then it ought to be extensible and support plugins. I'm pretty much always on that soapbox.

@KPYahoo

KPYahoo commented Jul 16, 2025

Copy link
Copy Markdown

@LuigiPulcini I am not a developer. May I request you to explore some option that this could be seen / created ONLY by Admin and not been seen by all customers in the front-end? Do you think this can be done?

Edit: I am a store owner and we have been craving this since years.

@LuigiPulcini

Copy link
Copy Markdown
Contributor

@helgatheviking I agree that the perfect method should simplify how site owners can create links for customers. The reason behind the use of guest users is that the wp_woocommerce_sessions table in the database can only have a single row per registered user because the session_key field contains the user ID. With guest users, you have a virtually unlimited number of cart sessions that you can create and share. Other approaches could be evaluated, but the key point would be trying to leverage as much as possible this new feature.

@KPYahoo A simple way to hide the button from regular visitors is adding a query argument (e.g. ?share) and checking if that argument is set with isset( $_GET['share'] ) before printing the button on the page. This way, the added button will be visible only if you visit the cart page with the URL www.example.com/cart/?share.

@helgatheviking

helgatheviking commented Jul 16, 2025

Copy link
Copy Markdown
Contributor

@LuigiPulcini what happens in your snippet if you do not pass a customer ID when generating the token?

And instead you generate the random hash yourself?
wc_rand_hash( 't_', 30 );

Which is how the customer ID is generated for non -logged in users.
if I'm reading it right

Which would also allow for restricting display to admins. I have an idea but I'm away from my keyboard. 😅

@LuigiPulcini

Copy link
Copy Markdown
Contributor

@helgatheviking As long as you pass a customer ID that:

  1. is present in the session_key column of the wp_woocommerce_sessions table, AND
  2. corresponds to a guest user

then, the new WC_Session_Handler::init_session_from_request() method will be able to load its session data and clone it into the current session.

The problem, though, is how to make it usable from the currently logged-in admin user which doesn't store its session data as a guest user in the wp_woocommerce_sessions table. So, my idea for a more advanced snippet – something I am currently experimenting with – is this:

  1. Load the session data of the current user (even logged-in ones)
  2. Clone the session data of the current user to a new session after generating a random guest customer ID with wc_rand_hash( 't_', 30 )
  3. Create a cart token for the cloned session (which is now assigned programmatically to a guest customer)
  4. Print the button with the redirection URL for that cart token

This should allow us to effortlessly create checkout links while being logged in as admins and limit the presence of the button with a check to current_user_can().

@LuigiPulcini

Copy link
Copy Markdown
Contributor

@helgatheviking It works! The following code snippet adds a button that is visible to admins or shop managers only and allows them to generate checkout links from the cart page. The link can be shared with any customers and is valid for 48 hours.

add_action( 'woocommerce_proceed_to_checkout', 'dev_test_woocommerce_proceed_to_checkout', 21 );
function dev_test_woocommerce_proceed_to_checkout() {
	if ( ! class_exists( 'Automattic\WooCommerce\StoreApi\Utilities\CartTokenUtils' ) || ! current_user_can( 'manage_woocommerce' ) ) {
		return;
	}

	global $wpdb;
	$customer_id   = wc()->session->get_customer_id();
	$session_data  = wc()->session->get_session( $customer_id, array() );
	$session_data  = array_diff_key( $session_data, array( 'customer' => true ) );
	$guest_user_id = wc_rand_hash( 't_', 30 );
	$expiration    = time() + intval( apply_filters( 'wc_session_expiration', 2 * DAY_IN_SECONDS ) );

	$wpdb->query(
		$wpdb->prepare(
			'INSERT INTO %i (`session_key`, `session_value`, `session_expiry`) VALUES (%s, %s, %d)
						ON DUPLICATE KEY UPDATE `session_value` = VALUES(`session_value`), `session_expiry` = VALUES(`session_expiry`)',
			$wpdb->prefix . 'woocommerce_sessions',
			$guest_user_id,
			maybe_serialize( $session_data ),
			$expiration
		)
	);
	wp_cache_set( WC_Cache_Helper::get_cache_prefix( WC_SESSION_CACHE_GROUP ) . $guest_user_id, $session_data, WC_SESSION_CACHE_GROUP, $expiration - time() );

	$session_token     = Automattic\WooCommerce\StoreApi\Utilities\CartTokenUtils::get_cart_token( (string) $guest_user_id );
	$checkout_link_url = add_query_arg( 'session', $session_token, wc_get_checkout_url() );
	?>
		<a href="<?php echo esc_url( $checkout_link_url ); ?>" class="button alt<?php echo esc_attr( wc_wp_theme_get_element_class_name( 'button' ) ? ' ' . wc_wp_theme_get_element_class_name( 'button' ) : '' ); ?>">
			<?php esc_html_e( 'Share this cart', 'woocommerce' ); ?>
		</a>
	<?php
}

Please note that a new guest session data row is created every time the cart page gets reloaded. Nevertheless, all those sessions will eventually disappear from the table when their expiration time comes up.

@helgatheviking

Copy link
Copy Markdown
Contributor

Awesome! I totally forgot you would need to save the session token to the DB

@sverleis

sverleis commented Sep 5, 2025

Copy link
Copy Markdown

@KPYahoo : May I request you to explore some option that this could be seen / created ONLY by Admin and not been seen by all customers in the front-end? Do you think this can be done?
Edit: I am a store owner and we have been craving this since years.

Created a plugin that for now can do simple and variable product. See if it works for you, and leave a review :)

https://wordpress.org/plugins/link-wizard-for-woocommerce/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

API Checkout Issues related to checkout page. focus: e2e tests Issues related to e2e tests plugin: woocommerce Issues related to the WooCommerce Core plugin.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

10 participants