jetpack icon indicating copy to clipboard operation
jetpack copied to clipboard

Add PayPal NCPS block to PayPal Payments package

Open allie500 opened this issue 7 months ago • 3 comments

Fixes PAYPAL-14

Proposed changes:

  • This PR adds the new PayPal NCPS block to the PayPal Payments package.
  • It also deprecates the Simple Payments legacy widget.

Other information:

  • [x] Have you written new tests for your changes, if applicable?
  • [x] Have you checked the E2E test CI results, and verified that your changes do not break them?
  • [ ] Have you tested your changes on WordPress.com, if applicable (if so, you'll see a generated comment below with a script to run)?

Jetpack product discussion

pcqRLn-2xw-p2

Does this pull request change what data or activity we track or use?

No

Testing instructions:

Block Testing:

  • Check out this branch
  • Use the button codes and test customer found in this Google doc: 1gfODwe3r_k4gdLng9oYreljOEQypQgPj4L2053FAums-gdoc
  • In your local Jetpack development environment, add a new page or post.
  • Add the PayPal NCPS block and perform the following checks:
  1. Verify that the block looks like this when added (the Stacked Buttons tooltip may also be displayed when first added)

Screenshot on 2025-06-19 at 11-58-15 (1)

  1. Hover over the Stacked Buttons option and verify that you see the following tooltip text displayed above option: Stacked Buttons are the recommended option for better conversion rates.

  2. Add the stacked buttons codes found in the Google doc. Save and preview the page. The frontend should look like the screenshot below.

Screenshot on 2025-06-17 at 14-12-25

  1. Open your browser inspector to the elements tab and find the PayPal SDK script (stacked buttons head code). Then verify that you see the data-paypal-partner-attribution-id attribute with the value set as: WooNCPS_Ecom_Wordpress.

  2. Click the PayPal button on the frontend rendered block and perform a test payment using the sandbox customer in the Google Doc.

  3. Remove the head code and deselect the block. Verify that you see a warning notice like the screenshot below.

Screenshot on 2025-06-20 at 11-45-31

  1. Replace the body code with gibberish and deselect the block. Verify that you see the error notice like the screenshot below.

Screenshot on 2025-06-20 at 11-46-28

  1. Delete all code from both text boxes and select the single button option. It should look like the screenshot below with no code added.

Screenshot on 2025-06-20 at 11-47-16

  1. Add some gibberish to the single button text area and deselect the block. Verify that you see the error notice like the screenshot below.

Screenshot on 2025-06-20 at 11-47-59

  1. Add the single button code found in the Google doc. Save and preview the page. The button should render on the frontend like the screenshot below.

Screenshot on 2025-06-17 at 14-22-25

  1. Open your browser inspector to the elements tab and verify that the form's action attribute URL value includes the following query parameter: ?at_code=WooNCPS_Ecom_Wordpress".

  2. Click the Buy Now button on the frontend rendered block and perform a test payment using the sandbox customer in the Google Doc.

Legacy Widget Deprecation Testing:

  • Install and activate the TwentyTen theme.
  • Install and activate the Classic Widget plugin.
  • Navigate to Appearance > Widgets (/wp-admin/widgets.php).
  • Verify that the Pay with PayPal widget is not included in the Available Widgets.
  • Apply the following patch:
diff --git a/projects/packages/paypal-payments/src/legacy/class-simple-payments.php b/projects/packages/paypal-payments/src/legacy/class-simple-payments.php
index 5b9ba2a5c7..00b71120bf 100644
--- a/projects/packages/paypal-payments/src/legacy/class-simple-payments.php
+++ b/projects/packages/paypal-payments/src/legacy/class-simple-payments.php
@@ -758,15 +758,15 @@ class Simple_Payments {
 		if ( ! self::is_enabled_jetpack_simple_payments() ) {
 			return;
 		}
-		$transient  = 'jetpack_simple_payments_widget::is_active';
-		$has_widget = get_transient( $transient );
-
-		if ( ! $has_widget ) {
-			$is_active_widget = is_active_widget( false, false, 'jetpack_simple_payments_widget' );
-			$has_widget       = (int) ! empty( $is_active_widget );
-			set_transient( $transient, $has_widget, HOUR_IN_SECONDS );
-		}
-
+		// $transient  = 'jetpack_simple_payments_widget::is_active';
+		// $has_widget = get_transient( $transient );
+
+		// if ( ! $has_widget ) {
+		// $is_active_widget = is_active_widget( false, false, 'jetpack_simple_payments_widget' );
+		// $has_widget       = (int) ! empty( $is_active_widget );
+		// set_transient( $transient, $has_widget, HOUR_IN_SECONDS );
+		// }
+		$has_widget = true;
 		// [DEPRECATION]: Only register widget if active widget exists already
 		if ( $has_widget ) {
 			register_widget( 'Automattic\Jetpack\Paypal_Payments\Widgets\Simple_Payments_Widget' );
  • Refresh the Widgets page and verify that you see the Pay with PayPal widget.
  • Add the widget to the primary sidebar and save.
  • Drop the patch (git restore .).
  • Refresh the page. Verify that you can still see the widget in the Available Widgets section and that the widget is still in the primary sidebar. Check the front end to ensure that the widget is rendering correctly.
  • Login to the Jetpack local dev env's phpMyAdmin and run the following in the console:
SELECT * FROM `wp_options` WHERE `option_name` LIKE '_transient_%jetpack_simple_payments_widget%';
  • Verify that you see the transient is set and that the transient's timeout is set like the screenshot below.

Screenshot on 2025-06-17 at 14-45-16

  • Remove the widget from the primary sidebar, refresh the widgets page, verify that the Pay with PayPal widget is still in the Available Widgets section.
  • Delete the two transients from the options table and refresh the widgets page.
  • Verify that the Pay with PayPal widget is no longer in the Available Widgets section.

allie500 avatar Jun 12 '25 15:06 allie500

Are you an Automattician? Please test your changes on all WordPress.com environments to help mitigate accidental explosions.

  • To test on WoA, go to the Plugins menu on a WoA dev site. Click on the "Upload" button and follow the upgrade flow to be able to upload, install, and activate the Jetpack Beta plugin. Once the plugin is active, go to Jetpack > Jetpack Beta, select your plugin (Jetpack), and enable the add/paypal-ncps-block-to-paypal-payments-pkg branch.
  • To test on Simple, run the following command on your sandbox:
bin/jetpack-downloader test jetpack add/paypal-ncps-block-to-paypal-payments-pkg

Interested in more tips and information?

  • In your local development environment, use the jetpack rsync command to sync your changes to a WoA dev blog.
  • Read more about our development workflow here: PCYsg-eg0-p2
  • Figure out when your changes will be shipped to customers here: PCYsg-eg5-p2

github-actions[bot] avatar Jun 12 '25 15:06 github-actions[bot]

Thank you for your PR!

When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:

  • :white_check_mark: Include a description of your PR changes.
  • :white_check_mark: Add a "[Status]" label (In Progress, Needs Review, ...).
  • :white_check_mark: Add a "[Type]" label (Bug, Enhancement, Janitorial, Task).
  • :white_check_mark: Add testing instructions.
  • :white_check_mark: Specify whether this PR includes any changes to data or privacy.
  • :white_check_mark: Add changelog entries to affected projects

This comment will be updated as you work on your PR and make changes. If you think that some of those checks are not needed for your PR, please explain why you think so. Thanks for cooperation :robot:


Follow this PR Review Process:

  1. Ensure all required checks appearing at the bottom of this PR are passing.
  2. Make sure to test your changes on all platforms that it applies to. You're responsible for the quality of the code you ship.
  3. You can use GitHub's Reviewers functionality to request a review.
  4. When it's reviewed and merged, you will be pinged in Slack to deploy the changes to WordPress.com simple once the build is done.

If you have questions about anything, reach out in #jetpack-developers for guidance!


Jetpack plugin:

The Jetpack plugin has different release cadences depending on the platform:

  • WordPress.com Simple releases happen as soon as you deploy your changes after merging this PR (PCYsg-Jjm-p2).
  • WoA releases happen weekly.
  • Releases to self-hosted sites happen monthly:
    • Scheduled release: August 5, 2025
    • Code freeze: August 4, 2025

If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack.


Boost plugin:

No scheduled milestone found for this plugin.

If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack.

github-actions[bot] avatar Jun 12 '25 15:06 github-actions[bot]

Code Coverage Summary

2 files are newly checked for coverage.

File Coverage
projects/packages/paypal-payments/src/paypal-payment-buttons/class-paypal-payment-buttons.php 0/76 (0.00%) 💔
projects/plugins/jetpack/extensions/blocks/paypal-payment-buttons/paypal-payment-buttons.php 0/4 (0.00%) 💔

Full summary · PHP report · JS report

If appropriate, add one of these labels to override the failing coverage check: https://github.com/Automattic/jetpack/labels/Covered%20by%20non-unit%20tests https://github.com/Automattic/jetpack/labels/Coverage%20tests%20to%20be%20added%20later https://github.com/Automattic/jetpack/labels/I%20don%27t%20care%20about%20code%20coverage%20for%20this%20PR

jp-launch-control[bot] avatar Jun 12 '25 15:06 jp-launch-control[bot]

Note: I'm running through the no code payment button flow and the button looks pretty dynamic (changing text, adding images, etc). So, maybe a bit more complex. Even after adding images and other items though, the patterns below still hold.

I don't think that encoding/decoding the block attributes is the way to move forward here. Instead, I think that we need to parse the input, pull out the relevant information, and then store that as block attributes.

<script src="...">
</script>

For example, for the stacked buttons head, we probably only need the src. For the stacked buttons body, we probably only need the button ID.

<div id="paypal-container-..."></div>
<script>
  paypal.HostedButtons({
    hostedButtonId: "...",
  }).render("#paypal-container-...")
</script>

Given just those two attributes, we can rehydrate the HTML on render in the editor or frontend.

The single button code is a bit more of a mess. But, I'm sure that we can find a way to address this. Realistically, what we need to figure out is how many variations of the code NCPS code are there going to be?

Stashing my Codex output for later reference codex Below is a sketch of how you could stop lumping the raw “paste‑in your HTML” snippets into your block attributes and instead pull out just the bits you care about (the SDK src URL in the head, and the hosted‑button/container ID in the body), persist those as real block attributes, and then on save (or in your PHP render‑callback) reconstruct the minimal HTML you need.

1. Declare new attributes in block.json

Instead of only storing a codeHead and codeBody string, you’d add two typed attributes—say scriptSrc and containerId—and keep or remove your legacy code‐snippets as you see fit. For example:

--- a/projects/packages/paypal-payments/src/paypal-payment-buttons/block.json
+++ b/projects/packages/paypal-payments/src/paypal-payment-buttons/block.json
@@ "attributes": {
-   "codeHead": {
-       "type": "string"
-   },
-   "codeBody": {
-       "type": "string"
-   }
+   "scriptSrc": {
+     "type": "string"
+   },
+   "containerId": {
+     "type": "string"
+   }
 }

projects/packages/paypal-payments/src/paypal-payment-buttons/block.json


2. Extract and persist those bits in your Edit component

In your edit.js you already have the two <PlainText> inputs (one for head, one for body). Hook into their onChange handlers, run a tiny parser/regex against the pasted HTML, and stash out the real values (scriptSrc and containerId). For instance, you could add helper functions up top:

// Minimal helpers to pull src from a script tag and ID from the container div:
function extractScriptSrc( html ) {
  const m = html.match( /<script\s+[^>]*src=(['"])([^'"]+)\1/ );
  return m ? m[2] : '';
}

function extractContainerId( html ) {
  const m = html.match( /<div\s+[^>]*id=(['"])(paypal-container-([^'"]+))\1/ );
  // returns the ID _suffix_, e.g. "WEX69XEX4KD6Q"
  return m ? m[3] : '';
}

projects/packages/paypal-payments/src/paypal-payment-buttons/edit.js

Then wire them into your two <PlainText> inputs. Roughly around here:

--- a/projects/packages/paypal-payments/src/paypal-payment-buttons/edit.js
+++ b/projects/packages/paypal-payments/src/paypal-payment-buttons/edit.js
@@ { /* … existing imports and validCodeHead/validCodeBody … */ }

-export default function Edit( { attributes, setAttributes, isSelected } ) {
- const { buttonType, codeHead, codeBody } = attributes;
+export default function Edit( { attributes, setAttributes, isSelected } ) {
+ const { buttonType, scriptSrc, containerId } = attributes;
   const [ notice, setNotice ] = useState( null );

@@
- { 'stacked' === buttonType && (
-   <PlainText
-     value={ codeHead }
-     onChange={ code => setAttributes( { codeHead: code } ) }
-     placeholder={ __( 'Paste the head code here…', 'jetpack-paypal-payments' ) }
-     aria-label={ __( 'PayPal button head code', 'jetpack-paypal-payments' ) }
-     name="paypal-payment-buttons-code-head"
-   />
- ) }
-<PlainText
-   value={ codeBody }
-   onChange={ codeBody => setAttributes( { codeBody } ) }
-   placeholder={ __( 'Paste the code here…', 'jetpack-paypal-payments' ) }
-   aria-label={ __( 'PayPal button code', 'jetpack-paypal-payments' ) }
-   name="paypal-payment-buttons-code-body"
/>
+{ 'stacked' === buttonType && (
+  <PlainText
+    value={ scriptSrc }
+    onChange={ html => {
+      const src = extractScriptSrc( html );
+      setAttributes( { scriptSrc: src } );
+    } }
+    placeholder={ __( 'Paste the head script tag here…', 'jetpack-paypal-payments' ) }
+    aria-label={ __( 'PayPal SDK script URL', 'jetpack-paypal-payments' ) }
+    name="paypal-payment-buttons-script-src"
+  />
+)}
+<PlainText
+  value={ containerId }
+  onChange={ html => {
+    const id = extractContainerId( html );
+    setAttributes( { containerId: id } );
+  } }
+  placeholder={ __( 'Paste the button container and script here…', 'jetpack-paypal-payments' ) }
+  aria-label={ __( 'PayPal button container ID', 'jetpack-paypal-payments' ) }
+  name="paypal-payment-buttons-container-id"
/>

projects/packages/paypal-payments/src/paypal-payment-buttons/edit.js


3. Update your save (or PHP render callback) to re‑emit safe HTML

Because you now have the raw scriptSrc and containerId, you can emit just the minimal tags you need—no unfiltered input HTML required.

If you’re doing a JS save()

export default function save( { attributes } ) {
  const { buttonType, scriptSrc, containerId } = attributes;

  if ( buttonType === 'stacked' ) {
    return (
      <>
        { scriptSrc && <script src={ scriptSrc }></script> }
        <div id={ `paypal-container-${ containerId }` }></div>
        <script
          dangerouslySetInnerHTML={ {
            __html: `
paypal.HostedButtons({
  hostedButtonId: "${ containerId }"
}).render("#paypal-container-${ containerId }")
          `,
          } }
        />
      </>
    );
  }

  // … single‑button case …
}

Or, in PHP (your existing render_callback)

You’d drop the $code_head / $code_body regex in favor of:

$scriptSrc   = $attributes['scriptSrc'] ?? '';
$containerId = $attributes['containerId'] ?? '';

if ( 'stacked' === $button_type && $scriptSrc ) {
  wp_enqueue_script( 'paypal-sdk', esc_url( $scriptSrc ), [], null, false );
}

if ( 'stacked' === $button_type && $containerId ) {
  return sprintf(
    '<div id="paypal-container-%1$s"></div>
     <script>paypal.HostedButtons({hostedButtonId:"%1$s"}).render("#paypal-container-%1$s")</script>',
    esc_attr( $containerId )
  );
}

projects/packages/paypal-payments/src/paypal-payment-buttons/class-paypal-payment-buttons.php


Why this helps

* **No more “raw HTML”** in your block’s saved attributes → you aren’t trusting authors or hackers to paste valid `<script>` or `<div>` tags.
* All the heavy lifting of validating or parsing lives in your block’s code, not in user input.
* You keep your UI as‑is (two paste‑areas), but under the hood you only stash the tiny bits you need.
* Saves you from wrestling with `wp_kses()` or `dangerouslySetInnerHTML` on unpredictable user‑supplied markup.

TL;DR

1. **Block.json:** add `scriptSrc` + `containerId` attributes.
2. **Edit.js:** parse the pasted snippets with a regex or DOMParser in your `onChange` to pull out `src="…"` and `id="paypal-container-…"`.
3. **Save/PHP:** rebuild the minimal `<script>` + `<div>` + inline initializer from those attributes.

This approach fully isolates the unsafe HTML parsing to your controlled JS/PHP, and your block attributes remain simple strings.

ebinnion avatar Jun 28 '25 00:06 ebinnion

Let me add some context here. It was a while ago, but if I remember correctly, Allie had already considered that option, but PayPal had advised against it due to the potential future changes to their code. Validation is quite simple for that reason, too.

However, they may have a different point of view now.

ismaeldcom avatar Jun 30 '25 06:06 ismaeldcom

PayPal had advised against it due to the potential future changes to their code. Validation is quite simple for that reason, too.

Wouldn't that be an argument for storing the options as attributes? It would make updating the block's markup easier.

jeherve avatar Jun 30 '25 16:06 jeherve

Wouldn't that be an argument for storing the options as attributes? It would make updating the block's markup easier.

Yes, but that also means we need to work closely with PayPal in case they change anything, whether it's the design or the logic behind the buttons. In any case, they are probably the best people to ask about this proposal. 🙂

ismaeldcom avatar Jul 01 '25 06:07 ismaeldcom

After merging #43413 into trunk, I've just cleaned up this PR so that it should only be additional to what is in trunk.

ebinnion avatar Jul 11 '25 08:07 ebinnion

In a follow-up PR, we'll probably want to remove the old block from the inserter to avoid this:

Agreed. Though, I think that we'll also need to work out documentation updates across Dotcom, Jetpack, etc. For now, I think the beta block approach works as an interim since I don't think that we know exactly how we're going to deploy this just now.

We have a meeting in ~45 minutes where I expect that we'll work out some details.

This is looking good, and tests well. I'm thinking we could merge this as is, with maybe a single change: move paypal-payment-buttons to beta in projects/plugins/jetpack/extensions/index.json. This way we can iterate in follow-up PRs until we're ready to release the block for everyone.

Sounds good. I'd much prefer this so that the PRs are scoped and easy to describe and test. Large PRs like this are much more difficult.

I've just pushed a commit for the beta block registration. Assuming all is well after that, I can sketch out the rest of the issues in Linear and get them knocked out.

--

As always, I appreciate the review sir.

ebinnion avatar Jul 15 '25 19:07 ebinnion

I'm going ahead and marking the PR as needs review with the understanding that the block is in beta mode and I'll be following up with several smaller PRs to fix issues.

ebinnion avatar Jul 16 '25 02:07 ebinnion