Conversation
A payment made through a hosted button, link or QR code never touches the site, so the site registers a webhook on the merchant's PayPal account for the PAYMENT.CAPTURE.* events, verifies each delivery with PayPal, and records it as a jetpack_paypal_capture_* Tracks event attributed to the connection owner. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Are you an Automattician? Please test your changes on all WordPress.com environments to help mitigate accidental explosions.
Interested in more tips and information?
|
|
Thank you for your PR! When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:
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 🤖 Follow this PR Review Process:
If you have questions about anything, reach out in #jetpack-developers for guidance! |
Code Coverage SummaryCoverage changed in 3 files.
2 files are newly checked for coverage.
Full summary · PHP report · JS report If appropriate, add one of these labels to override the failing coverage check:
Covered by non-unit tests
|
Proposed changes
The V1 block's payments were logged by WordPress.com because every purchase went through its
simple-payments/paypal/paymentand/executeendpoints. V2 buttons are PayPal-hosted (HostedButtons, pay links, QR codes): the buyer pays inside PayPal's iframe or page, the merchant's credentials live on the site, and nothing on our side sees a payment. The hosted-buttons SDK component takes only a button id and exposes no callback, so there is no client-side signal either. This PR has PayPal tell the site about payments through a webhook, and records each one as a Tracks event.PayPal_Webhooksregisters a webhook on the connected account (POST /v1/notifications/webhooks, with the merchant's credentials) forPAYMENT.CAPTURE.COMPLETED,DECLINED,PENDING,REFUNDEDandREVERSED, pointing atrest_url( 'wpcom/v2/paypal/webhook' ). The id, environment and URL are stored in thejetpack_paypal_payment_buttons_webhookoption. A URL PayPal already knows (WEBHOOK_URL_ALREADY_EXISTS) is adopted from the account's webhook list, so a site that lost the option or reconnected the same account ends up registered. PayPal only accepts HTTPS listeners; a site it cannot reach stays connected, and registration is retried at most hourly.handle_connectand ofPayPal_Partner_Onboarding::complete_onboarding(); lazily fromhandle_connection_status, which the editor calls whenever a block opens, so sites connected before this ships pick it up; and again when the environment is switched, since the webhook lives on the other environment's API. Disconnect deletes the webhook before the credentials go.POST wpcom/v2/paypal/webhookis open, as PayPal has no account here; the signature check stands in for the permission check. The handler passes thePAYPAL-*headers, the stored webhook id and the decoded body toPOST /v1/notifications/verify-webhook-signature, and answers 400 to anything PayPal does not vouch for.PayPal_Webhooks::handle()turns a verified notification into one event,jetpack_paypal_capture_{completed,declined,pending,refunded,reversed}, withevent_id,capture_id,status,amount,currency,order_id,custom_id,invoice_id,final_captureandenvironment, plus the blog id and site URL the Tracking class adds. A notification PayPal delivers twice is recorded once. Nothing about the payer is sent.PayPal_Tracksrecords events through the connection package'sTrackingclass, attributed to the connection owner, the way the V1 WordPress.com events went to the product's author. A site with no connection owner records nothing.PayPal_API_Clientgainscreate_webhook(),list_webhooks(),delete_webhook()(a 404 counts as deleted) andverify_webhook_signature(), on the existing request layer.Not covered here, on purpose: whether
custom_idcarries the payment link id for hosted-button payments (PayPal's docs do not say; the property is recorded as PayPal sends it), and merchants on WordPress.com Simple sites, where the listener URL and the route's reachability need checking on that side. Both are called out in the testing instructions.Related product discussion/links
Does this pull request change what data or activity we track or use?
Yes. Five new Tracks events, listed above, attributed to the site's Jetpack connection owner. They carry PayPal's capture id, order id, amount, currency, status and the
custom_idandinvoice_idPayPal attaches to the capture, and the environment. They do not carry the buyer's email, name or address. The site also registers a webhook on the merchant's PayPal account, which the merchant can see in their PayPal developer dashboard.Testing instructions
Automated:
jp test php packages/paypal-payments(775 tests) passes and PHPCS is clean. The new tests mock PayPal's webhook, verification and Tracks pixel endpoints and assert on the requests. Phan did not run locally (noastextension); CI will.The site must be reachable by PayPal over HTTPS: a Jurassic Ninja site works, a local site does not (registration is refused with
paypal_webhook_url_not_httpsand nothing else changes).add_filter( 'jetpack_feature_flag_enabled_paypal-payments-api-managed-buttons', '__return_true' );wp option get jetpack_paypal_payment_buttons_webhookholds an id,sandbox, and the site'swp-json/wpcom/v2/paypal/webhookURL;PAYMENT.CAPTURE.*events.jetpack_paypal_capture_completedfor the site's blog id with the capture id and amount from the sandbox merchant dashboard,environment=sandbox, and note whethercustom_idholds thePLB-payment link id. That last point is the open question this PR cannot answer from the docs.recorded: falseand no second event appears.paypal_webhook_invalid_signature(orpaypal_webhook_missing_headers) and records nothing.🤖 Generated with Claude Code