Skip to content
This repository has been archived by the owner on Feb 23, 2024. It is now read-only.

Need a working example for a simple checkbox along with server side hooks with customer and cart data #5179

Closed
ryanhungate opened this issue Nov 17, 2021 · 31 comments
Labels
type: enhancement The issue is a request for an enhancement. type: question The issue is a question about how code works.

Comments

@ryanhungate
Copy link

Is your feature request related to a problem? Please describe.

The Mailchimp for Woocommerce plugin needs the ability to inject a checkbox for the "subscriber status" and "GDPR" related fields into the checkout form and save the data on the customer and order records.

Describe the solution you'd like

  1. A working example which shows how to not only add a field to the editor, but also inject current state into the components.
  2. Show how to get the customer data, along with the cart details from the form submission, or even validation functions.
  3. How to use the values being set from the react component on the user / order / checkout in context.

Describe alternatives you've considered

We have looked at this sample repo but still not sure how to use the example on the server side - getting errors as of an update today (11/17/2021):

CRITICAL Uncaught Error: Call to undefined function Automattic\WooCommerce\Blocks\gutenberg_supports_block_templates() in /var/www/html/wp-content/plugins/woo-gutenberg-products-block/src/BlockTemplatesController.php:135

Additional context

This is a blocking issue for the Mailchimp customers because we cannot capture the newsletter status of each customer. We're very well versed in React and VUE and PHP - but at this point just a bit confused on how to implement a new feature in this ecosystem and need some help :)

@ryanhungate ryanhungate added the type: enhancement The issue is a request for an enhancement. label Nov 17, 2021
@nerrad nerrad added the type: question The issue is a question about how code works. label Nov 17, 2021
@senadir
Copy link
Member

senadir commented Nov 18, 2021

Hey there @ryanhungate

This PR provides a working example for how MailPoet newsletter opt in integration works
https://github.com/mailpoet/mailpoet/pull/3778/files

The gist of it is that you will create a new JS block, that loads both on the editor and frontend (two versions of the same block).
In the editor you register a block with registerBlockType from @wordpress/blocks, and set its parent to [ "woocommerce/checkout-contact-information-block" ] to have it below the email field, feel free to change that if you want a different position, here are possible positions.

In the frontend script, you register your component using registerCheckoutBlock by passing it your component and your block.json file.

This guide should help you register that block.

This tutorial should you create a dynamic JS block.

The sample repo is slightly outdated and so needs an update.

So to summarize:

A working example which shows how to not only add a field to the editor, but also inject current state into the components.

Mailpoet example should guide you through the process.
mailpoet/mailpoet#3778

Show how to get the customer data, along with the cart details from the form submission, or even validation functions.

Those things are passed to your frontend component as props, cart would be the updated cart data including customer data like shipping and billing fields. You will append data to the form using checkoutExtensionData, and validation functions are in validation.

How to use the values being set from the react component on the user / order / checkout in context.

The value you register in the frontend, for example using setExtensionData('mailchimp', 'optin', checked);, you can access your value on the server request object by hooking into woocommerce_blocks_checkout_update_order_from_request

add_action(
      'woocommerce_blocks_checkout_update_order_from_request',
      [$this, 'process_checkout'],
      10,
      2
    );

function process_checkout( $order, $request) {
    $request['extensions']['mailchimp']['optin'];
    // your logic.
}

But for it to be on the request, you need to register it on schema, using extend_rest_api

use Automattic\WooCommerce\Blocks\Domain\Services\ExtendRestApi;
use Automattic\WooCommerce\Blocks\Package;
use Automattic\WooCommerce\Blocks\StoreApi\Schemas\CheckoutSchema;

public function extend_rest_api() {
    $extend = Package::container()->get(ExtendRestApi::class);
    $extend->register_endpoint_data(
      [
        'endpoint'        => CheckoutSchema::IDENTIFIER,
        'namespace'       => 'mailchimp',
        'schema_callback' => function () {
          return [
            'optin' => [
              'description' => __('Subscribe to marketing opt-in.', 'mailchimp'),
              'type'        => 'boolean',
            ],
          ];
        },
      ]
    );
  }

@ryanhungate
Copy link
Author

@senadir thank you so much for the help on this... I will get into this today and hopefully you gave me all the clues I needed to finish out our part! :) 👍

I will report back after a few hours and will close this out if we're good.

@senadir
Copy link
Member

senadir commented Nov 18, 2021

@ryanhungate this is a working example of a newsletter plugin

https://github.com/opr/newsletter-test/tree/update/to-latest-working-api

It doesn't contain any frontend validation or form blocking, if you're looking for that, take a look at the terms and conditions block

https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/fc0eebe2a0701ea6c2706dba5e4e783ac689b13e/assets/js/blocks/cart-checkout/checkout/inner-blocks/checkout-terms-block/frontend.tsx

@senadir
Copy link
Member

senadir commented Nov 18, 2021

From what I understood, you're looking at adding more than just a checkbox right? Are you trying to add text fields as well?

@ryanhungate
Copy link
Author

@senadir our plugin renders out dynamic checkboxes based on the store owner's GDPR fields on the Mailchimp audience... so basically it is not hard coded. We make an API call to the Mailchimp API - and then dynamically render the checkboxes along with the labels on the older checkout screen.

So ideally if there was a way to pass in an array of objects to the component that could add additional checkboxes at runtime, that's really what we're after yes.

Thanks so much for your help. Very much appreciated here. Also I'll take a look at that working repo you showed me because I think I was using the master version which wasn't.

@senadir
Copy link
Member

senadir commented Nov 18, 2021

our plugin renders out dynamic checkboxes based on the store owner's GDPR fields on the Mailchimp audience... so basically it is not hard coded. We make an API call to the Mailchimp API - and then dynamically render the checkboxes along with the labels on the older checkout screen.

Can this condition be evaluated on the server, and then just pass the data with get_script_data or ExtendRestAPI

@ryanhungate
Copy link
Author

@senadir yeah for sure. That's what we're currently doing in the standard checkout screens, so if there was a way for us to pass in these options to the component registration somehow that's really all we're after...

@ryanhungate
Copy link
Author

@senadir i wanted to hold off to say something till I had a chance to get this thing implemented in our staging environment - and here's the current findings. Code looks great, very well organized and easy to understand - so thank you very much for this - but I'm seeing some issues that I don't understand yet.

  1. This line isn't working unless I add this inside a plugins_loaded hook.
if ( class_exists( '\Automattic\WooCommerce\Blocks\Package' ) ) { ... }
  1. The front end seems broken with an error:
(index):573 Scripts that have a dependency on [wc-settings, wc-blocks-checkout] must be loaded in the footer, woocommerce-mailchimp-newsletter-subscription-editor-script was registered to load in the header, but has been switched to load in the footer instead. See https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5059

This is as far as I've gotten for the day - but i'm sure tomorrow I'll be able to get deep into this and resolve all the issues. I think overall this is close to a perfect starting point so I did want to thank you again for that.

@senadir
Copy link
Member

senadir commented Nov 19, 2021

Hey, it seems your editor script is loading on the frontend for some reason? only the frontend script should load there. Can you show me the code you have that enqueues/registers your editor scripts? Preferably, you shouldn't enqueue anything yourself for the editor, register_block_type and block.json should handle both.

@ryanhungate
Copy link
Author

@senadir i used the exact code in your example repository... :) Changed up a couple names to avoid conflicts but it's the same otherwise.

@senadir
Copy link
Member

senadir commented Nov 19, 2021

Yeah I can see "Scripts that have a dependency" warning, however, it shouldn't break your block? It's working fine for me
image
I will the first issue a look, I couldn't reproduce it in my code but it seems like a valid issue.

@ryanhungate
Copy link
Author

@senadir it's broken on the front end - not the back end editor if that matters.

@senadir
Copy link
Member

senadir commented Nov 19, 2021

Yes, I know. It's weird that this is breaking your frontend instead of just a warning.
Try this diff to get rid of that warning
woocommerce/newsletter-test#5

@ryanhungate
Copy link
Author

@senadir yeah the console error went away - but this is what i'm seeing on the checkout screen. Almost like it's still in a loading state. No errors at all, and I also see the newsletter additions in the source - so it looks as if it's loading correctly... this is really odd. Have you seen this before?

Screen Shot 2021-11-19 at 08 56 32

@ryanhungate
Copy link
Author

@senadir i think this had something to do with an addition I had added - but didn't see any error messages so it was invisible.

I need to render out an array of GDPR fields into this component too. Do you have a snippet that shows how to pass in an array to the component so we can load dynamic checkboxes at all? Sorry for all the back and forth - but we're making a lot of headway so It's much appreciated what you've been doing.

@ryanhungate
Copy link
Author

@senadir do you happen to know the hook for grabbing the cart when it's updated? This new checkout seems to be bypassing the typical order and cart hooks we're used to getting. Looks like a really nice move forward - but yeah just need to know how to accomplish the same things as the traditional stores did if you have time. Thanks!

@senadir
Copy link
Member

senadir commented Nov 19, 2021

The cart content is passed as cart prop to your block (along side other props like text, and checkoutExtensionsData)
https://github.com/opr/newsletter-test/blob/add/script-in-footer/assets/js/checkout-newsletter-subscription-block/block.js#L7

const Block = ( { cart, text, checkoutExtensionData } ) => {
	const { billingAddress, shippingAddress } = cart;
	const [ checked, setChecked ] = useState( false );
	const { setExtensionData } = checkoutExtensionData;

To pass data from the server to your component, you use extendRestApi, see this guide

https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/trunk/docs/extensibility/extend-rest-api-add-data.md

You would add another register_endpoint_data callback here, but use CartSchema::IDENTIFIER and pass both schema_callback and data_callback.

https://github.com/opr/newsletter-test/blob/add/script-in-footer/woocommerce-blocks-integration.php#L146-L169

After doing that, you will get access to your data in your block in frontend, a new prop extensions (along side cart, text, and checkoutExtensionsData)

const Block = ( { cart, extensions, text, checkoutExtensionData } ) => {
	const { mailchimp } = extensions;
	const { gdprFields } = mailchimp;
	const { billingAddress, shippingAddress } = cart;
	const [ checked, setChecked ] = useState( false );
	const { setExtensionData } = checkoutExtensionData;

@senadir
Copy link
Member

senadir commented Nov 19, 2021

so it looks as if it's loading correctly... this is really odd. Have you seen this before?

The error you're seeing means the Checkout never finished loading, are you seeing any console errors, server errors, scripts not loading?

@ryanhungate
Copy link
Author

@senadir ok - great... this is starting to make sense to me. Thanks. So I guess I would need to just make an ajax request to the server with the cart details inside this component ( to capture the cart and push to Mailchimp as abandoned )... right? There's no pre-built hook that would allow us to use what's currently being pushed to the server for this information?

I didn't want to double up on the requests if there was already a "cart updated" hook that you're already providing.

@senadir
Copy link
Member

senadir commented Nov 19, 2021

There's no "cart updated" hook, your Block component would re-render each time cart is updated and you would get the most up to date value each time.

Preferably, if you can explain to me how you imagine the integration would work, or how does it work with Checkout shortcode, then I can point down all the various extensibility patterns we have.

setExtensionData would send data to the server when you make a POST request.
ExtendRestApi would bring data from the server to your component, and it happens each time the cart updates.
The props passed to your block update each time the cart update, you always have the most up to date version.

There are other extensibility APIs, like forcing the cart to refetch if you want.

@ryanhungate
Copy link
Author

@senadir we need to get the cart data on the server side, not the client side. Reason being is that we need to pass over the cart details to Mailchimp so the customers can use the "abandoned cart" mail feature.

Does this use the same woo session for the cart, or is it using an entirely different storage system?

Same thing for the "orders being created" it seems as if the hooks are different as well?

@senadir
Copy link
Member

senadir commented Nov 19, 2021

Does this use the same woo session for the cart, or is it using an entirely different storage system?

It uses the same, so you can just wc()->cart. We provide a CartController class, but I'm not sure if it's available, you can try it.

use Automattic\WooCommerce\Blocks\StoreApi\Utilities\CartController;

$cart_controller     = new CartController();
$cart                = $cart_controller->get_cart_instance();

For hooks, we have new hooks since the interface passed to them changed (WP_Request object instead of $_POST). Those two should do the work (all of them here).

woocommerce_blocks_checkout_order_processed
woocommerce_blocks_checkout_update_order_from_request

Checkout block uses draft orders so an order is created much earlier compared to Checkout shortcode.

In shortcode, the order is created once you place an order, in block, it's created once you visit the checkout, when you hit submit, woocommerce_blocks_checkout_order_processed is triggered.
woocommerce_blocks_checkout_update_order_from_request is triggered each time the order is updated in checkout block but before it's submitted (changing address, writing email, selecting another shipping method...)

@ryanhungate
Copy link
Author

@senadir ok I think this is where i'm confused because the hook woocommerce_blocks_checkout_update_order_from_request only seems to fire when PLACING the order. I'm not seeing any sort of activity in this function until the customer places it.

@senadir
Copy link
Member

senadir commented Nov 19, 2021

Hmm, that's true, it doesn't trigger for store/update-customer requests, I remember having a similar issue in AutomateWoo and I just hooked into woocommerce_before_order_object_save.

That's the code I used:

/**
	 * Store guest info when they submit email from Store API.
	 *
	 * The guest email, first name and last name are captured.
	 *
	 * @see \Automattic\WooCommerce\Blocks\StoreApi\Routes\CartUpdateCustomer
	 *
	 * @param \WC_Order $order
	 */
	public static function capture_from_store_api( $order ) {
		if ( ! Options::presubmit_capture_enabled() || is_user_logged_in() ) {
			return;
		}

		if ( $order->get_status() !== 'checkout-draft' || ! $order->is_created_via( 'store-api' ) || ! $order->get_billing_email() || $order->get_customer_id() ) {
			return;
		}

		$customer = self::set_session_by_captured_email( $order->get_billing_email() );

		if ( $customer ) {

			// Capture the guest's name
			$guest = $customer->get_guest();
			if ( $guest ) {
				$guest->update_meta( 'billing_first_name', $order->get_billing_first_name() );
				$guest->update_meta( 'billing_last_name', $order->get_billing_last_name() );
			}
		}
	}

@ryanhungate
Copy link
Author

@senadir ok - this last snippet is great... exactly what I needed. Sorry to bother again but is there any chance of getting the extension values in this hook as well? I just realized this could solve another problem we were trying to fix where people only wanted cart messages to go to folks that are marked as subscribed. This would be great if I could access the extension values here too?

@senadir
Copy link
Member

senadir commented Nov 20, 2021

Sorry to bother again but is there any chance of getting the extension values in this hook as well?

I'm not sure I understand this question. You can't access the $request object on woocommerce_before_order_object_save as it's not yet available, it's only available once you start submitting the order.

@ryanhungate
Copy link
Author

@senadir yeah basically the concept is, hook into the woocommerce_before_order_object_save event as you had suggested ( which is perfect for us to pass the cart data to Mailchimp )... but i'm missing the properties that could have existed in our extension at this step.

We actually have customers that wanted to limit the cart submissions to the ones that had the checkbox checked. Is there any other hook that we could use or a method on the server side which would show this extension data?

@senadir
Copy link
Member

senadir commented Nov 22, 2021

We actually have customers that wanted to limit the cart submissions to the ones that had the checkbox checked. Is there any other hook that we could use or a method on the server side which would show this extension data?

You can prevent order from being submitted by adding validation to your checkbox, I linked to it before but here's how the terms and conditions checkbox prevent order submitting if it's not checked:

https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/fc0eebe2a0701ea6c2706dba5e4e783ac689b13e/assets/js/blocks/cart-checkout/checkout/inner-blocks/checkout-terms-block/frontend.tsx#L34-L71

Alternatively, you can reject submission at woocommerce_blocks_checkout_update_order_from_request level by throwing a RouteException.

@ryanhungate
Copy link
Author

@senadir sorry I'm not sure if my question got jumbled - but what I meant is that during the checkout process, while an order is still in a "draft" state, we would consider this "abandoned"... and during this step of creating the draft order, it would be great if we could see the extensions current value as well. Does that make sense?

The newsletter checkbox being checked at this stage ( before the order is placed ) would allow us to capture the subscriber status, and allow store owners that are concerned with GDPR that they would only send cart messages to people that were subscribed as well.

So not having this value makes us have to implement an ajax request and store something in session every time the checkbox is changed - which we can do of course, but just wanted to try and avoid this if at all possible too using proper hooks already being sent to the server side.

@senadir
Copy link
Member

senadir commented Nov 22, 2021

If I understand correctly, you want to capture abandoned carts, but also if the user consented to being contacted by checking the checkbox? You can react the email field being filled, but not to the checkbox being checked, not before placing an order.

It also depends if you're going to store the merchant opt it in a persistent place (DB) or just in session until they actually submit the order?

What I would do in this case is:

1- Register opt in status in the cart response using ExtendRestApi (assuming it's saved in a persistent place), now you can access the current in extentions.

use Automattic\WooCommerce\Blocks\Domain\Services\ExtendRestApi;
use Automattic\WooCommerce\Blocks\Package;
use Automattic\WooCommerce\Blocks\StoreApi\Schemas\CartSchema;

public function extend_rest_api() {
    $extend = Package::container()->get(ExtendRestApi::class);
    $extend->register_endpoint_data(
      [
        'endpoint'        => CartSchema::IDENTIFIER,
        'namespace'       => 'mailchimp',
        'schema_callback' => function () {
          return [
            'is_opted_in' => [
              'description' => __('Has Subscribed to marketing opt-in.', 'mailchimp'),
              'type'        => 'boolean',
            ],
          ];
        },
        'data_callback' => function () {
          return [
            'is_opted_in' => is_current_customer_opted_in(),
          ];
        },
      ]
    );
  }
const Block = ( { cart, extensions, text, } ) => {
	const { mailchimp } = extensions;
	const { is_opted_in } = mailchimp;
  1. You would use ExtendRestApi::register_update_callback and extensionCartUpdate to force update cart each time that value is changed.
import { extensionCartUpdate } from '@woocommerce/blocks-checkout';

const Block = ( { cart, extensions, text, } ) => {
	const { mailchimp } = extensions;
	const { is_opted_in } = mailchimp;
	const [ checked, setChecked ] = useState( is_opted_in )l
	const buttonClickHandler = () => {
		extensionCartUpdate( {
			namespace: 'mailchimp',
			data: {
				checked: ! checked,
			},
		} );
		setChecked( ! checked );
	};
	return <Checkbox checked={ checked } onClick={ buttonClickHandler } />
use Automattic\WooCommerce\Blocks\Domain\Services\ExtendRestApi;
use Automattic\WooCommerce\Blocks\Package;
use Automattic\WooCommerce\Blocks\StoreApi\Schemas\CartSchema;

public function extend_rest_api() {
    $extend = Package::container()->get(ExtendRestApi::class);
    $extend->register_endpoint_data(
      [
        'endpoint'        => CartSchema::IDENTIFIER,
        'namespace'       => 'mailchimp',
        'schema_callback' => function () {
          return [
            'is_opted_in' => [
              'description' => __('Has Subscribed to marketing opt-in.', 'mailchimp'),
              'type'        => 'boolean',
            ],
          ];
        },
        'data_callback' => function () {
          return [
            'is_opted_in' => is_current_customer_opted_in(),
          ];
        },
      ]
    );

		$extend->register_update_callback(
      [
        'namespace'  => 'mailchimp',
        'callback'   => function ( $data ) {
          persist_customer_optin( $data['checked'] );
        }
      ]
    );
  }

Now, your optin is persisted, and is updated each that checkbox is clicked, you probably don't need woocommerce_blocks_checkout_update_order_from_request or woocommerce_before_order_object_save now.

@sunyatasattva
Copy link
Contributor

Hi @ryanhungate,

As a part of this repository’s maintenance, I am closing this issue due to inactivity. Please feel free to comment on it in case we missed something. We’d be happy to take another look.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
type: enhancement The issue is a request for an enhancement. type: question The issue is a question about how code works.
Projects
None yet
Development

No branches or pull requests

5 participants