From 4c10d84d247b379c873be05e2c12c91014c25ddc Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 26 Feb 2025 16:05:00 +0100 Subject: [PATCH 01/35] =?UTF-8?q?=F0=9F=91=94=20Remove=20the=20PAYMENT=5FA?= =?UTF-8?q?UTHORIZATION=20intent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This changes the layout of the GooglePay sheet to unlock more test-cards in sandbox mode. It’s the first step in enabling 3DS testing --- .../ppcp-googlepay/resources/js/GooglepayButton.js | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js index 82fe1ff12..f983febb4 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js @@ -44,7 +44,6 @@ import moduleStorage from './Helper/GooglePayStorage'; * @property {Function} createButton - The convenience method is used to generate a Google Pay payment button styled with the latest Google Pay branding for insertion into a webpage. * @property {Function} isReadyToPay - Use the isReadyToPay(isReadyToPayRequest) method to determine a user's ability to return a form of payment from the Google Pay API. * @property {(Object) => Promise} loadPaymentData - This method presents a Google Pay payment sheet that allows selection of a payment method and optionally configured parameters - * @property {Function} onPaymentAuthorized - This method is called when a payment is authorized in the payment sheet. * @property {Function} onPaymentDataChanged - This method handles payment data changes in the payment sheet such as shipping address and shipping options. */ @@ -190,7 +189,6 @@ class GooglepayButton extends PaymentButton { ); this.init = this.init.bind( this ); - this.onPaymentAuthorized = this.onPaymentAuthorized.bind( this ); this.onPaymentDataChanged = this.onPaymentDataChanged.bind( this ); this.onButtonClick = this.onButtonClick.bind( this ); @@ -411,8 +409,6 @@ class GooglepayButton extends PaymentButton { return callbacks; } - callbacks.onPaymentAuthorized = this.onPaymentAuthorized; - if ( this.requiresShipping ) { callbacks.onPaymentDataChanged = this.onPaymentDataChanged; } @@ -591,7 +587,7 @@ class GooglepayButton extends PaymentButton { }; const useShippingCallback = this.requiresShipping; - const callbackIntents = [ 'PAYMENT_AUTHORIZATION' ]; + const callbackIntents = []; if ( useShippingCallback ) { callbackIntents.push( 'SHIPPING_ADDRESS', 'SHIPPING_OPTION' ); @@ -791,12 +787,6 @@ class GooglepayButton extends PaymentButton { // Payment process //------------------------ - onPaymentAuthorized( paymentData ) { - this.log( 'onPaymentAuthorized', paymentData ); - - return this.processPayment( paymentData ); - } - async processPayment( paymentData ) { this.logGroup( 'processPayment' ); From 7035e1edbc3718019533ada8b0963706cca94ea6 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 26 Feb 2025 16:34:28 +0100 Subject: [PATCH 02/35] =?UTF-8?q?=F0=9F=8E=A8=20Correctly=20annotate=20pro?= =?UTF-8?q?mise=20return=20values?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-googlepay/resources/js/GooglepayButton.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js index f983febb4..51ea2a450 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js @@ -535,7 +535,7 @@ class GooglepayButton extends PaymentButton { onButtonClick() { this.log( 'onButtonClick' ); - const initiatePaymentRequest = () => { + const initiatePaymentRequest = async () => { window.ppcpFundingSource = 'googlepay'; const paymentDataRequest = this.paymentDataRequest(); @@ -548,7 +548,7 @@ class GooglepayButton extends PaymentButton { return this.paymentsClient.loadPaymentData( paymentDataRequest ); }; - const validateForm = () => { + const validateForm = async () => { if ( 'function' !== typeof this.contextHandler.validateForm ) { return Promise.resolve(); } @@ -559,7 +559,7 @@ class GooglepayButton extends PaymentButton { } ); }; - const getTransactionInfo = () => { + const getTransactionInfo = async () => { if ( 'function' !== typeof this.contextHandler.transactionInfo ) { return Promise.resolve(); } From 83d6998cd50f3b8b7e059d39c2c1843c8b7bb854 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 26 Feb 2025 16:36:35 +0100 Subject: [PATCH 03/35] =?UTF-8?q?=F0=9F=94=8A=20Improve=20logging=20for=20?= =?UTF-8?q?=E2=80=9CinitiatePaymentRequest=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/GooglepayButton.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js index 51ea2a450..1780dd6bc 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js @@ -533,7 +533,7 @@ class GooglepayButton extends PaymentButton { * Show Google Pay payment sheet when Google Pay payment button is clicked */ onButtonClick() { - this.log( 'onButtonClick' ); + this.logGroup( 'onButtonClick' ); const initiatePaymentRequest = async () => { window.ppcpFundingSource = 'googlepay'; @@ -545,7 +545,16 @@ class GooglepayButton extends PaymentButton { this.context ); - return this.paymentsClient.loadPaymentData( paymentDataRequest ); + return this.paymentsClient + .loadPaymentData( paymentDataRequest ) + .then( ( paymentData ) => { + this.log( 'loadPaymentData response:', paymentData ); + return paymentData; + } ) + .catch( ( error ) => { + this.error( 'loadPaymentData failed:', error ); + throw error; + } ); }; const validateForm = async () => { @@ -577,7 +586,8 @@ class GooglepayButton extends PaymentButton { validateForm() .then( getTransactionInfo ) - .then( initiatePaymentRequest ); + .then( initiatePaymentRequest ) + .finally( () => this.logGroup() ) } paymentDataRequest() { From 57e91432c7407d2cb5e7957113f7a9736d000e42 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 26 Feb 2025 16:53:07 +0100 Subject: [PATCH 04/35] =?UTF-8?q?=F0=9F=8E=A8=20Improve=20code=20readabili?= =?UTF-8?q?ty?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-googlepay/resources/js/GooglepayButton.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js index 1780dd6bc..fea959813 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js @@ -532,7 +532,7 @@ class GooglepayButton extends PaymentButton { /** * Show Google Pay payment sheet when Google Pay payment button is clicked */ - onButtonClick() { + async onButtonClick() { this.logGroup( 'onButtonClick' ); const initiatePaymentRequest = async () => { @@ -584,10 +584,11 @@ class GooglepayButton extends PaymentButton { } ); }; - validateForm() + const paymentData = await validateForm() .then( getTransactionInfo ) - .then( initiatePaymentRequest ) - .finally( () => this.logGroup() ) + .then( initiatePaymentRequest ); + + this.logGroup(); } paymentDataRequest() { From 63dfb167fb7aa0c7db2be06f779135fd031062d2 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 26 Feb 2025 17:23:16 +0100 Subject: [PATCH 05/35] =?UTF-8?q?=F0=9F=90=9B=20Wire=20up=20the=20broken?= =?UTF-8?q?=20processPayment=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit While removing the PAYMENT_AUTHORIZATION intent, we also removed the processPayment call from the Google Pay button handler. This change restores the correct payment processing flow --- modules/ppcp-googlepay/resources/js/GooglepayButton.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js index fea959813..9e58fbca5 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js @@ -589,6 +589,13 @@ class GooglepayButton extends PaymentButton { .then( initiatePaymentRequest ); this.logGroup(); + + // If something failed above, stop here. Only continue if we have the paymentData. + if ( ! paymentData ) { + return; + } + + return this.processPayment( paymentData ); } paymentDataRequest() { From d3ddc625d8af0c6e1bd423a5c21557b29845bf42 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 26 Feb 2025 17:52:44 +0100 Subject: [PATCH 06/35] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Start=20to=20simplif?= =?UTF-8?q?y=20processPayment=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ppcp-googlepay/resources/js/GooglepayButton.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js index 9e58fbca5..6df3320af 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js @@ -808,6 +808,8 @@ class GooglepayButton extends PaymentButton { async processPayment( paymentData ) { this.logGroup( 'processPayment' ); + let result; + const payer = payerDataFromPaymentResponse( paymentData ); const paymentError = ( reason ) => { @@ -891,14 +893,13 @@ class GooglepayButton extends PaymentButton { } }; - const addBillingDataToSession = () => { - moduleStorage.setPayer( payer ); - setPayerData( payer ); - }; return new Promise( async ( resolve ) => { try { - addBillingDataToSession(); + // Add billing data to session. + moduleStorage.setPayer( payer ); + setPayerData( payer ); + await processPaymentPromise( resolve ); } catch ( err ) { resolve( paymentError( err.message ) ); From c4a269780fbc410a3186acc56c618d43a2e43f5d Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 26 Feb 2025 17:53:25 +0100 Subject: [PATCH 07/35] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Rearrange=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-googlepay/resources/js/GooglepayButton.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js index 6df3320af..83a74b69d 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js @@ -895,11 +895,11 @@ class GooglepayButton extends PaymentButton { return new Promise( async ( resolve ) => { - try { - // Add billing data to session. - moduleStorage.setPayer( payer ); - setPayerData( payer ); + // Add billing data to session. + moduleStorage.setPayer( payer ); + setPayerData( payer ); + try { await processPaymentPromise( resolve ); } catch ( err ) { resolve( paymentError( err.message ) ); From d6e66fd55c886ff93e9674ca6ebcc60f4bb4149f Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 26 Feb 2025 17:55:57 +0100 Subject: [PATCH 08/35] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Extract=20createOrde?= =?UTF-8?q?r=20into=20the=20main=20function=20body?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ppcp-googlepay/resources/js/GooglepayButton.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js index 83a74b69d..fe339a690 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js @@ -870,11 +870,7 @@ class GooglepayButton extends PaymentButton { return isApproved; }; - const processPaymentPromise = async ( resolve ) => { - const id = await this.contextHandler.createOrder(); - - this.log( 'createOrder', id ); - + const processPaymentPromise = async ( resolve, id ) => { const isApprovedByPayPal = await checkPayPalApproval( id ); if ( ! isApprovedByPayPal ) { @@ -900,7 +896,10 @@ class GooglepayButton extends PaymentButton { setPayerData( payer ); try { - await processPaymentPromise( resolve ); + const orderId = await this.contextHandler.createOrder(); + this.log( 'createOrder', orderId ); + + await processPaymentPromise( resolve, orderId ); } catch ( err ) { resolve( paymentError( err.message ) ); } From 672641ba827dddfb1496e032ef9a1e77e4db73df Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 26 Feb 2025 17:59:35 +0100 Subject: [PATCH 09/35] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Simplify=20control?= =?UTF-8?q?=20flow=20by=20removing=20a=20Promise?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/GooglepayButton.js | 36 +++++++++---------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js index fe339a690..3c4124a64 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js @@ -870,42 +870,38 @@ class GooglepayButton extends PaymentButton { return isApproved; }; - const processPaymentPromise = async ( resolve, id ) => { + const processPaymentPromise = async ( id ) => { const isApprovedByPayPal = await checkPayPalApproval( id ); if ( ! isApprovedByPayPal ) { - resolve( paymentError( 'TRANSACTION FAILED' ) ); - - return; + return paymentError( 'TRANSACTION FAILED' ); } // This must be the last step in the process, as it initiates a redirect. const success = await approveOrderServerSide( id ); if ( success ) { - resolve( this.processPaymentResponse( 'SUCCESS' ) ); - } else { - resolve( paymentError( 'FAILED TO APPROVE' ) ); + return this.processPaymentResponse( 'SUCCESS' ); } + return paymentError( 'FAILED TO APPROVE' ); }; + // Add billing data to session. + moduleStorage.setPayer( payer ); + setPayerData( payer ); - return new Promise( async ( resolve ) => { - // Add billing data to session. - moduleStorage.setPayer( payer ); - setPayerData( payer ); + try { + const orderId = await this.contextHandler.createOrder(); + this.log( 'createOrder', orderId ); - try { - const orderId = await this.contextHandler.createOrder(); - this.log( 'createOrder', orderId ); + result = await processPaymentPromise( orderId ); + } catch ( err ) { + result = paymentError( err.message ); + } - await processPaymentPromise( resolve, orderId ); - } catch ( err ) { - resolve( paymentError( err.message ) ); - } + this.logGroup(); - this.logGroup(); - } ); + return result; } processPaymentResponse( state, intent = null, message = null ) { From 5a5074df8c75741896d240d276a67df62118c82c Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 26 Feb 2025 18:01:13 +0100 Subject: [PATCH 10/35] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Flatten=20processPay?= =?UTF-8?q?ment=20to=20functon=20body?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This makes the flow easier to understand and debug --- .../resources/js/GooglepayButton.js | 30 ++++++++----------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js index 3c4124a64..9ae747ddd 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js @@ -870,22 +870,6 @@ class GooglepayButton extends PaymentButton { return isApproved; }; - const processPaymentPromise = async ( id ) => { - const isApprovedByPayPal = await checkPayPalApproval( id ); - - if ( ! isApprovedByPayPal ) { - return paymentError( 'TRANSACTION FAILED' ); - } - - // This must be the last step in the process, as it initiates a redirect. - const success = await approveOrderServerSide( id ); - - if ( success ) { - return this.processPaymentResponse( 'SUCCESS' ); - } - return paymentError( 'FAILED TO APPROVE' ); - }; - // Add billing data to session. moduleStorage.setPayer( payer ); setPayerData( payer ); @@ -894,7 +878,19 @@ class GooglepayButton extends PaymentButton { const orderId = await this.contextHandler.createOrder(); this.log( 'createOrder', orderId ); - result = await processPaymentPromise( orderId ); + const isApprovedByPayPal = await checkPayPalApproval( orderId ); + + if ( ! isApprovedByPayPal ) { + result = paymentError( 'TRANSACTION FAILED' ); + } else { + const success = await approveOrderServerSide( orderId ); + + if ( success ) { + result = this.processPaymentResponse( 'SUCCESS' ); + } else { + result = paymentError( 'FAILED TO APPROVE' ); + } + } } catch ( err ) { result = paymentError( err.message ); } From 12b5fe808284dbc1a555d6f7c3fd1029a28b2d1c Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 26 Feb 2025 18:09:20 +0100 Subject: [PATCH 11/35] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Embed=20a=20function?= =?UTF-8?q?=20that=E2=80=99s=20only=20locally=20used?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/GooglepayButton.js | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js index 9ae747ddd..c82a3a068 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js @@ -812,14 +812,27 @@ class GooglepayButton extends PaymentButton { const payer = payerDataFromPaymentResponse( paymentData ); + const paymentResponse = ( state, intent = null, message = null ) => { + const response = { + transactionState: state, + }; + + if ( intent || message ) { + response.error = { + intent, + message, + }; + } + + this.log( 'processPaymentResponse', response ); + + return response; + }; + const paymentError = ( reason ) => { this.error( reason ); - return this.processPaymentResponse( - 'ERROR', - 'PAYMENT_AUTHORIZATION', - reason - ); + return paymentResponse( 'ERROR', 'PAYMENT_AUTHORIZATION', reason ); }; const checkPayPalApproval = async ( orderId ) => { @@ -840,9 +853,13 @@ class GooglepayButton extends PaymentButton { /** * This approval mainly confirms that the orderID is valid. * - * It's still needed because this handler redirects to the checkout page if the server-side + * It's still needed because this handler REDIRECTS to the checkout page if the server-side * approval was successful. * + * I.e. on success, the approveOrder handler initiates a browser navigation; this means + * it should be the last action that happens in the payment process. + * + * @see onApproveForContinue.js * @param {string} orderID */ const approveOrderServerSide = async ( orderID ) => { @@ -886,7 +903,7 @@ class GooglepayButton extends PaymentButton { const success = await approveOrderServerSide( orderId ); if ( success ) { - result = this.processPaymentResponse( 'SUCCESS' ); + result = paymentResponse( 'SUCCESS' ); } else { result = paymentError( 'FAILED TO APPROVE' ); } @@ -900,23 +917,6 @@ class GooglepayButton extends PaymentButton { return result; } - processPaymentResponse( state, intent = null, message = null ) { - const response = { - transactionState: state, - }; - - if ( intent || message ) { - response.error = { - intent, - message, - }; - } - - this.log( 'processPaymentResponse', response ); - - return response; - } - /** * Updates the shipping option in the checkout form, if a form with shipping options is * detected. From dc90a73f81b3bf0176c6a30c8d59f8f748744bef Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 26 Feb 2025 18:23:49 +0100 Subject: [PATCH 12/35] =?UTF-8?q?=F0=9F=91=94=20Add=20short=20delay=20befo?= =?UTF-8?q?re=20redirecting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OnApproveHandler/onApproveForContinue.js | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue.js b/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue.js index d492802f1..13b914335 100644 --- a/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue.js +++ b/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue.js @@ -1,3 +1,18 @@ +const initiateRedirect = ( successUrl ) => { + /** + * Notice how this step initiates a redirect to a new page using a plain + * URL as new location. This process does not send any details about the + * approved order or billed customer. + * + * The redirect will start after a short delay, giving the calling method + * time to process the return value of the `await onApprove()` call. + */ + + setTimeout( () => { + window.location.href = successUrl; + }, 200 ); +}; + const onApprove = ( context, errorHandler ) => { return ( data, actions ) => { const canCreateOrder = @@ -28,24 +43,13 @@ const onApprove = ( context, errorHandler ) => { .then( ( approveData ) => { if ( ! approveData.success ) { errorHandler.genericError(); - return actions.restart().catch( ( err ) => { + return actions.restart().catch( () => { errorHandler.genericError(); } ); } const orderReceivedUrl = approveData.data?.order_received_url; - - /** - * Notice how this step initiates a redirect to a new page using a plain - * URL as new location. This process does not send any details about the - * approved order or billed customer. - * Also, due to the redirect starting _instantly_ there should be no other - * logic scheduled after calling `await onApprove()`; - */ - - window.location.href = orderReceivedUrl - ? orderReceivedUrl - : context.config.redirect; + initiateRedirect( orderReceivedUrl || context.config.redirect ); } ); }; }; From 021a4b3de7fb0f6eeecc12cce017bd731a44a3c7 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 26 Feb 2025 18:58:27 +0100 Subject: [PATCH 13/35] =?UTF-8?q?=E2=9C=A8=20Enable=203DS=20check=20for=20?= =?UTF-8?q?Google=20Pay?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change finally triggers a `PAYER_ACTION_REQUIRED` status response during checkout. --- .../ppcp-googlepay/src/GooglepayModule.php | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/modules/ppcp-googlepay/src/GooglepayModule.php b/modules/ppcp-googlepay/src/GooglepayModule.php index a50408bd3..4c09705ba 100644 --- a/modules/ppcp-googlepay/src/GooglepayModule.php +++ b/modules/ppcp-googlepay/src/GooglepayModule.php @@ -22,6 +22,7 @@ use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameI use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; +use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; /** * Class GooglepayModule @@ -248,6 +249,42 @@ class GooglepayModule implements ServiceModule, ExtendingModule, ExecutableModul } ); + add_filter( + 'ppcp_create_order_request_body_data', + static function ( array $data, string $payment_method ) use ( $c ) : array { + // TODO (bug): This condition only works when using Google Pay as separate gateway! + // When GooglePay is part of the smart buttons block, this condition fails and will not enable 3DS. + if ( $payment_method !== GooglePayGateway::ID ) { + return $data; + } + + $settings = $c->get( 'wcgateway.settings' ); + assert( $settings instanceof Settings ); + + $three_d_secure_contingency = + $settings->has( '3d_secure_contingency' ) + ? apply_filters( 'woocommerce_paypal_payments_three_d_secure_contingency', $settings->get( '3d_secure_contingency' ) ) + : ''; + + if ( + $three_d_secure_contingency === 'SCA_ALWAYS' + || $three_d_secure_contingency === 'SCA_WHEN_REQUIRED' + ) { + $data['payment_source']['google_pay'] = array( + 'attributes' => array( + 'verification' => array( + 'method' => $three_d_secure_contingency, + ), + ), + ); + } + + return $data; + }, + 10, + 2 + ); + return true; } } From ca76d33aa390966de74a5a73eed4759d062a953e Mon Sep 17 00:00:00 2001 From: carmenmaymo Date: Mon, 3 Mar 2025 15:34:45 +0100 Subject: [PATCH 14/35] Use capture for 3ds in google --- .../OnApproveHandler/onApproveForContinue.js | 2 +- modules/ppcp-button/services.php | 15 +++ .../ppcp-button/src/Assets/SmartButton.php | 10 ++ modules/ppcp-button/src/ButtonModule.php | 22 +++++ .../src/Endpoint/CaptureOrderEndpoint.php | 98 +++++++++++++++++++ .../src/Endpoint/GetOrderEndpoint.php | 84 ++++++++++++++++ .../resources/js/Context/BaseHandler.js | 47 +++++++++ .../resources/js/GooglepayButton.js | 57 ++++++++++- 8 files changed, 333 insertions(+), 2 deletions(-) create mode 100644 modules/ppcp-button/src/Endpoint/CaptureOrderEndpoint.php create mode 100644 modules/ppcp-button/src/Endpoint/GetOrderEndpoint.php diff --git a/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue.js b/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue.js index 13b914335..8f8edd3c9 100644 --- a/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue.js +++ b/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue.js @@ -14,6 +14,7 @@ const initiateRedirect = ( successUrl ) => { }; const onApprove = ( context, errorHandler ) => { + console.log( 'onApprove' ); return ( data, actions ) => { const canCreateOrder = ! context.config.vaultingEnabled || data.paymentSource !== 'venmo'; @@ -48,7 +49,6 @@ const onApprove = ( context, errorHandler ) => { } ); } - const orderReceivedUrl = approveData.data?.order_received_url; initiateRedirect( orderReceivedUrl || context.config.redirect ); } ); }; diff --git a/modules/ppcp-button/services.php b/modules/ppcp-button/services.php index 44e9a52ac..952e308c3 100644 --- a/modules/ppcp-button/services.php +++ b/modules/ppcp-button/services.php @@ -10,7 +10,9 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\Button; use WooCommerce\PayPalCommerce\Button\Endpoint\ApproveSubscriptionEndpoint; +use WooCommerce\PayPalCommerce\Button\Endpoint\CaptureOrderEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\CartScriptParamsEndpoint; +use WooCommerce\PayPalCommerce\Button\Endpoint\GetOrderEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\SimulateCartEndpoint; use WooCommerce\PayPalCommerce\Button\Helper\CartProductsHelper; use WooCommerce\PayPalCommerce\Button\Helper\CheckoutFormSaver; @@ -282,6 +284,19 @@ return array( $container->get( 'wcgateway.paypal-gateway' ) ); }, + 'button.endpoint.get-order' => static function( ContainerInterface $container ): GetOrderEndpoint { + return new GetOrderEndpoint( + $container->get( 'button.request-data' ), + $container->get( 'api.endpoint.order' ) + ); + }, + 'button.endpoint.capture-order' => static function( ContainerInterface $container ): CaptureOrderEndpoint { + return new CaptureOrderEndpoint( + $container->get( 'button.request-data' ), + $container->get( 'api.endpoint.order' ), + $container->get( 'button.helper.wc-order-creator' ), + ); + }, 'button.checkout-form-saver' => static function ( ContainerInterface $container ): CheckoutFormSaver { return new CheckoutFormSaver( $container->get( 'session.handler' ) diff --git a/modules/ppcp-button/src/Assets/SmartButton.php b/modules/ppcp-button/src/Assets/SmartButton.php index 81ced3a68..ff33158ad 100644 --- a/modules/ppcp-button/src/Assets/SmartButton.php +++ b/modules/ppcp-button/src/Assets/SmartButton.php @@ -24,10 +24,12 @@ use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies; use WooCommerce\PayPalCommerce\Blocks\Endpoint\UpdateShippingEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\ApproveOrderEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\ApproveSubscriptionEndpoint; +use WooCommerce\PayPalCommerce\Button\Endpoint\CaptureOrderEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\CartScriptParamsEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\ChangeCartEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\CreateOrderEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\DataClientIdEndpoint; +use WooCommerce\PayPalCommerce\Button\Endpoint\GetOrderEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData; use WooCommerce\PayPalCommerce\Button\Endpoint\SaveCheckoutFormEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\SimulateCartEndpoint; @@ -1143,10 +1145,18 @@ document.querySelector("#payment").before(document.querySelector(".ppcp-messages 'endpoint' => \WC_AJAX::get_endpoint( ChangeCartEndpoint::ENDPOINT ), 'nonce' => wp_create_nonce( ChangeCartEndpoint::nonce() ), ), + 'get_order' => array( + 'endpoint' => \WC_AJAX::get_endpoint( GetOrderEndpoint::ENDPOINT ), + 'nonce' => wp_create_nonce( GetOrderEndpoint::nonce() ), + ), 'create_order' => array( 'endpoint' => \WC_AJAX::get_endpoint( CreateOrderEndpoint::ENDPOINT ), 'nonce' => wp_create_nonce( CreateOrderEndpoint::nonce() ), ), + 'capture_order' => array( + 'endpoint' => \WC_AJAX::get_endpoint( CaptureOrderEndpoint::ENDPOINT ), + 'nonce' => wp_create_nonce( CaptureOrderEndpoint::nonce() ), + ), 'approve_order' => array( 'endpoint' => \WC_AJAX::get_endpoint( ApproveOrderEndpoint::ENDPOINT ), 'nonce' => wp_create_nonce( ApproveOrderEndpoint::nonce() ), diff --git a/modules/ppcp-button/src/ButtonModule.php b/modules/ppcp-button/src/ButtonModule.php index cd2c8d381..160943f28 100644 --- a/modules/ppcp-button/src/ButtonModule.php +++ b/modules/ppcp-button/src/ButtonModule.php @@ -10,7 +10,9 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\Button; use WooCommerce\PayPalCommerce\Button\Endpoint\ApproveSubscriptionEndpoint; +use WooCommerce\PayPalCommerce\Button\Endpoint\CaptureOrderEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\CartScriptParamsEndpoint; +use WooCommerce\PayPalCommerce\Button\Endpoint\GetOrderEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\SaveCheckoutFormEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\SimulateCartEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\ValidateCheckoutEndpoint; @@ -177,6 +179,26 @@ class ButtonModule implements ServiceModule, ExtendingModule, ExecutableModule { } ); + add_action( + 'wc_ajax_' . GetOrderEndpoint::ENDPOINT, + static function () use ( $container ) { + $endpoint = $container->get( 'button.endpoint.get-order' ); + assert( $endpoint instanceof GetOrderEndpoint ); + + $endpoint->handle_request(); + } + ); + + add_action( + 'wc_ajax_' . CaptureOrderEndpoint::ENDPOINT, + static function () use ( $container ) { + $endpoint = $container->get( 'button.endpoint.capture-order' ); + assert( $endpoint instanceof GetOrderEndpoint ); + + $endpoint->handle_request(); + } + ); + add_action( 'wc_ajax_' . CreateOrderEndpoint::ENDPOINT, static function () use ( $container ) { diff --git a/modules/ppcp-button/src/Endpoint/CaptureOrderEndpoint.php b/modules/ppcp-button/src/Endpoint/CaptureOrderEndpoint.php new file mode 100644 index 000000000..9379f9521 --- /dev/null +++ b/modules/ppcp-button/src/Endpoint/CaptureOrderEndpoint.php @@ -0,0 +1,98 @@ +request_data = $request_data; + $this->order_endpoint = $order_endpoint; + $this->wc_order_creator = $wc_order_creator; + } + + /** + * The nonce. + * + * @return string + */ + public static function nonce(): string { + return self::ENDPOINT; + } + + /** + * Handles the request. + * + * @return bool + * @throws RuntimeException When order not found or handling failed. + */ + public function handle_request(): bool { + $data = $this->request_data->read_request( $this->nonce() ); + if ( ! isset( $data['order_id'] ) ) { + throw new RuntimeException( + __( 'No order id given', 'woocommerce-paypal-payments' ) + ); + } + $order = $this->order_endpoint->order( $data['order_id'] ); + $order = $this->order_endpoint->capture( $order ); + if ($order->status()->is('COMPLETED')) { + $wc_order = $this->wc_order_creator->create_from_paypal_order( $order, WC()->cart ); + $order_received_url = $wc_order->get_checkout_order_received_url(); + + wp_send_json_success( array( 'order_received_url' => $order_received_url ) ); + } + + wp_send_json_success(); + return true; + } +} diff --git a/modules/ppcp-button/src/Endpoint/GetOrderEndpoint.php b/modules/ppcp-button/src/Endpoint/GetOrderEndpoint.php new file mode 100644 index 000000000..6ce974e3e --- /dev/null +++ b/modules/ppcp-button/src/Endpoint/GetOrderEndpoint.php @@ -0,0 +1,84 @@ +request_data = $request_data; + $this->order_endpoint = $order_endpoint; + } + + /** + * The nonce. + * + * @return string + */ + public static function nonce(): string { + return self::ENDPOINT; + } + + /** + * Handles the request. + * + * @return bool + * @throws RuntimeException When order not found or handling failed. + */ + public function handle_request(): bool { + $data = $this->request_data->read_request( $this->nonce() ); + if ( ! isset( $data['order_id'] ) ) { + throw new RuntimeException( + __( 'No order id given', 'woocommerce-paypal-payments' ) + ); + } + + $order = $this->order_endpoint->order( $data['order_id'] ); + + wp_send_json_success(array( + 'order' => $order + )); + return true; + } +} diff --git a/modules/ppcp-googlepay/resources/js/Context/BaseHandler.js b/modules/ppcp-googlepay/resources/js/Context/BaseHandler.js index d49bee615..262b6f098 100644 --- a/modules/ppcp-googlepay/resources/js/Context/BaseHandler.js +++ b/modules/ppcp-googlepay/resources/js/Context/BaseHandler.js @@ -55,6 +55,53 @@ class BaseHandler { return this.actionHandler().configuration().onApprove( data, actions ); } + captureOrder( data, actions ) { + return fetch( this.ppcpConfig.ajax.get_order.endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'same-origin', + body: JSON.stringify( { + nonce: this.ppcpConfig.ajax.get_order.nonce, + order_id: data.orderID, + } ), + } ) + .then( ( order ) => { + console.log( 'order', order ); + const orderResponse = order.json(); + console.log( + orderResponse?.payment_source?.google_pay?.card + ?.authentication_result + ); + + return fetch( this.ppcpConfig.ajax.capture_order.endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'same-origin', + body: JSON.stringify( { + nonce: this.ppcpConfig.ajax.capture_order.nonce, + order_id: data.orderID, + } ), + } ); + } ) + .then( ( response ) => response.json() ) + .then( ( captureResponse ) => { + console.log( 'Capture response:', captureResponse ); + const orderReceivedUrl = + captureResponse.data?.order_received_url; + console.log( 'orderReceivedUrl', orderReceivedUrl ); + setTimeout( () => { + window.location.href = orderReceivedUrl; + }, 200 ); + } ) + .catch( ( error ) => { + console.error( 'Error:', error ); + } ); + } + actionHandler() { return new CartActionHandler( this.ppcpConfig, this.errorHandler() ); } diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js index c82a3a068..f24274f0a 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js @@ -193,6 +193,7 @@ class GooglepayButton extends PaymentButton { this.onButtonClick = this.onButtonClick.bind( this ); this.log( 'Create instance' ); + this.log( this.ppcpConfig ); } /** @@ -847,7 +848,31 @@ class GooglepayButton extends PaymentButton { this.log( 'confirmOrder', confirmOrderResponse ); - return 'APPROVED' === confirmOrderResponse?.status; + switch ( confirmOrderResponse?.status ) { + case 'APPROVED': + return true; + case 'PAYER_ACTION_REQUIRED': + return 'action_required'; + default: + return false; + } + }; + /** + * Initiates payer action and handles the 3DS contingency. + * + * @param {string} orderID + */ + const initiatePayerAction = async ( orderID ) => { + this.log( 'initiatePayerAction', orderID ); + + this.log( + '==== Confirm Payment Completed Payer Action Required =====' + ); + await widgetBuilder.paypal + .Googlepay() + .initiatePayerAction( { orderId: orderID } ); + + this.log( '===== Payer Action Completed =====' ); }; /** @@ -887,6 +912,28 @@ class GooglepayButton extends PaymentButton { return isApproved; }; + const captureOrderServerSide = async ( orderID ) => { + let isCaptured = true; + this.log( 'context', this.contextHandler ); + await this.contextHandler.captureOrder( + { orderID, payer }, + { + restart: () => + new Promise( ( resolve ) => { + isCaptured = false; + resolve(); + } ), + order: { + get: () => + new Promise( ( resolve ) => { + resolve( null ); + } ), + }, + } + ); + return isCaptured; + }; + // Add billing data to session. moduleStorage.setPayer( payer ); setPayerData( payer ); @@ -899,6 +946,14 @@ class GooglepayButton extends PaymentButton { if ( ! isApprovedByPayPal ) { result = paymentError( 'TRANSACTION FAILED' ); + } else if ( isApprovedByPayPal === 'action_required' ) { + await initiatePayerAction( orderId ); + const success = await captureOrderServerSide( orderId ); + if ( success ) { + result = paymentResponse( 'SUCCESS' ); + } else { + result = paymentError( 'FAILED TO APPROVE' ); + } } else { const success = await approveOrderServerSide( orderId ); From 7b579bfc9b170dc96188bad98a9f94ade18bc361 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 3 Mar 2025 16:51:26 +0100 Subject: [PATCH 15/35] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Simplify=20the=20pay?= =?UTF-8?q?ment=20flow=20before=20order=20approval?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/GooglepayButton.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js index f24274f0a..432071843 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js @@ -946,15 +946,14 @@ class GooglepayButton extends PaymentButton { if ( ! isApprovedByPayPal ) { result = paymentError( 'TRANSACTION FAILED' ); - } else if ( isApprovedByPayPal === 'action_required' ) { - await initiatePayerAction( orderId ); - const success = await captureOrderServerSide( orderId ); - if ( success ) { - result = paymentResponse( 'SUCCESS' ); - } else { - result = paymentError( 'FAILED TO APPROVE' ); - } } else { + /** + * This payment requires a 3DS verification before we can process the order. + */ + if ( isApprovedByPayPal === 'action_required' ) { + await initiatePayerAction( orderId ); + } + const success = await approveOrderServerSide( orderId ); if ( success ) { From 3e2a1f347f849e8294c5c1cac34db659fbc42225 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 3 Mar 2025 17:25:55 +0100 Subject: [PATCH 16/35] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Make=20the=20final?= =?UTF-8?q?=20server-side=20approval=20conditional?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With 3DS the final approval should only fire when the 3DS process was successful. Note that we still need to implement a check to confirm the verification via API --- .../resources/js/GooglepayButton.js | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js index 432071843..8fd9db12c 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js @@ -857,6 +857,7 @@ class GooglepayButton extends PaymentButton { return false; } }; + /** * Initiates payer action and handles the 3DS contingency. * @@ -865,14 +866,13 @@ class GooglepayButton extends PaymentButton { const initiatePayerAction = async ( orderID ) => { this.log( 'initiatePayerAction', orderID ); - this.log( - '==== Confirm Payment Completed Payer Action Required =====' - ); await widgetBuilder.paypal .Googlepay() .initiatePayerAction( { orderId: orderID } ); - this.log( '===== Payer Action Completed =====' ); + // TODO: We need to make a server-side request to check if the 3DS process was successful. + + return true; }; /** @@ -947,14 +947,19 @@ class GooglepayButton extends PaymentButton { if ( ! isApprovedByPayPal ) { result = paymentError( 'TRANSACTION FAILED' ); } else { - /** - * This payment requires a 3DS verification before we can process the order. - */ - if ( isApprovedByPayPal === 'action_required' ) { - await initiatePayerAction( orderId ); - } + let success = false; - const success = await approveOrderServerSide( orderId ); + // This payment requires a 3DS verification before we can process the order. + if ( isApprovedByPayPal === 'action_required' ) { + const approved = await initiatePayerAction( orderId ); + + // Only approve on server-side when 3DS was successful. + if ( approved ) { + success = await approveOrderServerSide( orderId ); + } + } else { + success = await approveOrderServerSide( orderId ); + } if ( success ) { result = paymentResponse( 'SUCCESS' ); From 4f4905af96b7ee2737d2fb0408874f27892d0da1 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 3 Mar 2025 18:09:30 +0100 Subject: [PATCH 17/35] =?UTF-8?q?=E2=9C=A8=20Add=20new=20=E2=80=9COrderEnd?= =?UTF-8?q?point::raw=5Forder()=E2=80=9D=20getter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Allows developers to decide if they want an `Order` class for internal usage, or an untyped `stdClass` for ajax responses. --- .../src/Endpoint/OrderEndpoint.php | 25 ++++- .../src/Endpoint/CaptureOrderEndpoint.php | 98 ------------------- 2 files changed, 21 insertions(+), 102 deletions(-) delete mode 100644 modules/ppcp-button/src/Endpoint/CaptureOrderEndpoint.php diff --git a/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php b/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php index 91b45d550..fcda542de 100644 --- a/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php +++ b/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php @@ -449,15 +449,16 @@ class OrderEndpoint { return $order; } + /** - * Fetches an order for a given ID. + * Fetches an order for a given ID and returns the raw, unparsed JSON response. * - * @param string $id The ID. + * @param string $id The PayPal order-ID. * - * @return Order + * @return stdClass * @throws RuntimeException If the request fails. */ - public function order( string $id ): Order { + public function raw_order( string $id ): stdClass { $bearer = $this->bearer->bearer(); $url = trailingslashit( $this->host ) . 'v2/checkout/orders/' . $id; $args = array( @@ -480,6 +481,7 @@ class OrderEndpoint { } $json = json_decode( $response['body'] ); $status_code = (int) wp_remote_retrieve_response_code( $response ); + if ( 404 === $status_code || empty( $response['body'] ) ) { $error = new RuntimeException( __( 'Could not retrieve order.', 'woocommerce-paypal-payments' ), @@ -495,6 +497,7 @@ class OrderEndpoint { ); throw $error; } + if ( 200 !== $status_code ) { $error = new PayPalApiException( $json, @@ -511,6 +514,20 @@ class OrderEndpoint { throw $error; } + return $json; + } + + /** + * Fetches an order for a given ID. + * + * @param string $id The ID. + * + * @return Order + * @throws RuntimeException If the request fails. + */ + public function order( string $id ) : Order { + $json = $this->raw_order( $id ); + return $this->order_factory->from_paypal_response( $json ); } diff --git a/modules/ppcp-button/src/Endpoint/CaptureOrderEndpoint.php b/modules/ppcp-button/src/Endpoint/CaptureOrderEndpoint.php deleted file mode 100644 index 9379f9521..000000000 --- a/modules/ppcp-button/src/Endpoint/CaptureOrderEndpoint.php +++ /dev/null @@ -1,98 +0,0 @@ -request_data = $request_data; - $this->order_endpoint = $order_endpoint; - $this->wc_order_creator = $wc_order_creator; - } - - /** - * The nonce. - * - * @return string - */ - public static function nonce(): string { - return self::ENDPOINT; - } - - /** - * Handles the request. - * - * @return bool - * @throws RuntimeException When order not found or handling failed. - */ - public function handle_request(): bool { - $data = $this->request_data->read_request( $this->nonce() ); - if ( ! isset( $data['order_id'] ) ) { - throw new RuntimeException( - __( 'No order id given', 'woocommerce-paypal-payments' ) - ); - } - $order = $this->order_endpoint->order( $data['order_id'] ); - $order = $this->order_endpoint->capture( $order ); - if ($order->status()->is('COMPLETED')) { - $wc_order = $this->wc_order_creator->create_from_paypal_order( $order, WC()->cart ); - $order_received_url = $wc_order->get_checkout_order_received_url(); - - wp_send_json_success( array( 'order_received_url' => $order_received_url ) ); - } - - wp_send_json_success(); - return true; - } -} From 2591cf444e17ac58179d1e38f0b1a281396d172a Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 3 Mar 2025 18:21:39 +0100 Subject: [PATCH 18/35] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Fix=20the=20GetOrder?= =?UTF-8?q?Endpoint=20handler?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We use the new `::raw_order()` method to return the plain object. The `Order` object could not be serialized in a meaningful way. --- modules/ppcp-button/services.php | 2 +- .../src/Endpoint/GetOrderEndpoint.php | 39 +++++++++---------- 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/modules/ppcp-button/services.php b/modules/ppcp-button/services.php index 952e308c3..9432ab216 100644 --- a/modules/ppcp-button/services.php +++ b/modules/ppcp-button/services.php @@ -284,7 +284,7 @@ return array( $container->get( 'wcgateway.paypal-gateway' ) ); }, - 'button.endpoint.get-order' => static function( ContainerInterface $container ): GetOrderEndpoint { + 'button.endpoint.get-order' => static function( ContainerInterface $container ): GetOrderEndpoint { return new GetOrderEndpoint( $container->get( 'button.request-data' ), $container->get( 'api.endpoint.order' ) diff --git a/modules/ppcp-button/src/Endpoint/GetOrderEndpoint.php b/modules/ppcp-button/src/Endpoint/GetOrderEndpoint.php index 6ce974e3e..a412d705a 100644 --- a/modules/ppcp-button/src/Endpoint/GetOrderEndpoint.php +++ b/modules/ppcp-button/src/Endpoint/GetOrderEndpoint.php @@ -5,50 +5,47 @@ * @package WooCommerce\PayPalCommerce\Button\Endpoint */ -declare(strict_types=1); +declare( strict_types = 1 ); namespace WooCommerce\PayPalCommerce\Button\Endpoint; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; use WooCommerce\PayPalCommerce\Button\Exception\RuntimeException; -use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait; /** * Class ApproveSubscriptionEndpoint */ class GetOrderEndpoint implements EndpointInterface { - use ContextTrait; - - const ENDPOINT = 'ppc-get-order'; + public const ENDPOINT = 'ppc-get-order'; /** * The request data helper. * * @var RequestData */ - private $request_data; + private RequestData $request_data; /** * The order endpoint. * * @var OrderEndpoint */ - private $order_endpoint; + private OrderEndpoint $order_endpoint; /** - * ApproveSubscriptionEndpoint constructor. + * Constructor. * - * @param RequestData $request_data The request data helper. - * @param OrderEndpoint $order_endpoint The order endpoint. + * @param RequestData $request_data The request data helper. + * @param OrderEndpoint $order_endpoint The order endpoint. */ public function __construct( RequestData $request_data, OrderEndpoint $order_endpoint ) { - $this->request_data = $request_data; - $this->order_endpoint = $order_endpoint; + $this->request_data = $request_data; + $this->order_endpoint = $order_endpoint; } /** @@ -56,29 +53,29 @@ class GetOrderEndpoint implements EndpointInterface { * * @return string */ - public static function nonce(): string { + public static function nonce() : string { return self::ENDPOINT; } /** - * Handles the request. + * Handles the request responds with the PayPal order details. * - * @return bool + * @return bool This method never returns a value, but we must implement the interface. * @throws RuntimeException When order not found or handling failed. */ - public function handle_request(): bool { - $data = $this->request_data->read_request( $this->nonce() ); + public function handle_request() : bool { + $data = $this->request_data->read_request( self::nonce() ); + if ( ! isset( $data['order_id'] ) ) { throw new RuntimeException( __( 'No order id given', 'woocommerce-paypal-payments' ) ); } - $order = $this->order_endpoint->order( $data['order_id'] ); + $order = $this->order_endpoint->raw_order( $data['order_id'] ); + + wp_send_json_success( $order ); - wp_send_json_success(array( - 'order' => $order - )); return true; } } From b85d1a5806bff211cdc0a5e7015007542168dc3b Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 3 Mar 2025 18:59:50 +0100 Subject: [PATCH 19/35] =?UTF-8?q?=F0=9F=91=94=20Add=20initial=203DS=20stat?= =?UTF-8?q?us=20parsing=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/Context/BaseHandler.js | 21 +++++ .../resources/js/GooglepayButton.js | 89 ++++++++++++++++++- 2 files changed, 106 insertions(+), 4 deletions(-) diff --git a/modules/ppcp-googlepay/resources/js/Context/BaseHandler.js b/modules/ppcp-googlepay/resources/js/Context/BaseHandler.js index 262b6f098..096367438 100644 --- a/modules/ppcp-googlepay/resources/js/Context/BaseHandler.js +++ b/modules/ppcp-googlepay/resources/js/Context/BaseHandler.js @@ -55,6 +55,27 @@ class BaseHandler { return this.actionHandler().configuration().onApprove( data, actions ); } + getOrder( orderId ) { + return new Promise( ( resolve, reject ) => { + fetch( this.ppcpConfig.ajax.get_order.endpoint, { + method: 'POST', + credentials: 'same-origin', + body: JSON.stringify( { + nonce: this.ppcpConfig.ajax.get_order.nonce, + order_id: orderId, + } ), + } ) + .then( ( result ) => result.json() ) + .then( ( result ) => { + if ( ! result.success || ! result.data ) { + reject(); + } + + resolve( result.data ); + } ); + } ); + } + captureOrder( data, actions ) { return fetch( this.ppcpConfig.ajax.get_order.endpoint, { method: 'POST', diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js index 8fd9db12c..e1a1c7232 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js @@ -193,7 +193,6 @@ class GooglepayButton extends PaymentButton { this.onButtonClick = this.onButtonClick.bind( this ); this.log( 'Create instance' ); - this.log( this.ppcpConfig ); } /** @@ -858,6 +857,81 @@ class GooglepayButton extends PaymentButton { } }; + const verifyThreeDSResponse = ( status ) => { + const { + liability_shift: liabilityShift = null, + three_d_secure: threeDS = null, + } = status || {}; + + if ( ! threeDS ) { + console.error( '3DS: No 3DS data available' ); + return false; + } + + // Check enrollment status first + switch ( threeDS.enrollment_status ) { + case 'Y': + // All clear; the following 3DS checks are meaningful. + break; + + case 'N': + case 'U': + // Risky. The 3DS verification is not available, or the bank does not participate. + // Bail, as the checks below are not reliable/relevant. + console.warn( '3DS: Not available', threeDS ); + return true; + } + + // 3DS is active, so a missing liabilityShift indicates a failure. + if ( ! liabilityShift ) { + console.error( '3DS: Missing liability shift data' ); + return false; + } + + // Check liability shift; is a risk-mitigation indicator, no failure possible. + switch ( liabilityShift ) { + case 'POSSIBLE': + console.log( '3DS: Liability shift possible' ); + break; + case 'NO': + console.warn( '3DS: Liability with the merchant' ); + break; + case 'UNKNOWN': + console.warn( '3DS: Liability shift unknown' ); + break; + } + + // Check 3DS authentication status. + switch ( threeDS.authentication_status ) { + case 'Y': + // Highest security clearance. Great customer! + console.log( '3DS: Authentication successful' ); + return true; + case 'A': + // Indicates a basic risk mitigation, but not full 3DS clearance. + console.log( '3DS: Verification started, but incomplete' ); + return true; + case 'N': + console.error( '3DS: Authentication failed or denied' ); + return false; + case 'U': + console.error( '3DS: Unable to complete authentication' ); + return false; + case 'R': + console.error( '3DS: Authentication rejected' ); + return false; + case 'C': + case 'D': + // Risky, but not failed. + console.warn( '3DS: Challenge required' ); + return true; + } + + // If we've made it this far, consider it a success, since no red-flags were present. + console.warn( '3DS: Cleared, but unknown status', threeDS ); + return true; + }; + /** * Initiates payer action and handles the 3DS contingency. * @@ -865,14 +939,21 @@ class GooglepayButton extends PaymentButton { */ const initiatePayerAction = async ( orderID ) => { this.log( 'initiatePayerAction', orderID ); + let wasApproved = false; await widgetBuilder.paypal .Googlepay() - .initiatePayerAction( { orderId: orderID } ); + .initiatePayerAction( { orderId: orderID } ) + .then( async () => { + const order = await this.contextHandler.getOrder( orderID ); + const status = + order?.payment_source?.google_pay?.card + ?.authentication_result; - // TODO: We need to make a server-side request to check if the 3DS process was successful. + wasApproved = verifyThreeDSResponse( status ); + } ); - return true; + return wasApproved; }; /** From ea23cf743a9f5d586f0c94ac92823a6918d7e372 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 3 Mar 2025 19:00:41 +0100 Subject: [PATCH 20/35] =?UTF-8?q?=E2=8F=AA=EF=B8=8F=20Remove=20unnecessary?= =?UTF-8?q?=20endpoint,=20restore=20URL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Those changes are not relevant for 3DS integration --- .../modules/OnApproveHandler/onApproveForContinue.js | 2 +- modules/ppcp-button/services.php | 8 -------- modules/ppcp-button/src/Assets/SmartButton.php | 6 +----- modules/ppcp-button/src/ButtonModule.php | 11 ----------- 4 files changed, 2 insertions(+), 25 deletions(-) diff --git a/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue.js b/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue.js index 8f8edd3c9..13b914335 100644 --- a/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue.js +++ b/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue.js @@ -14,7 +14,6 @@ const initiateRedirect = ( successUrl ) => { }; const onApprove = ( context, errorHandler ) => { - console.log( 'onApprove' ); return ( data, actions ) => { const canCreateOrder = ! context.config.vaultingEnabled || data.paymentSource !== 'venmo'; @@ -49,6 +48,7 @@ const onApprove = ( context, errorHandler ) => { } ); } + const orderReceivedUrl = approveData.data?.order_received_url; initiateRedirect( orderReceivedUrl || context.config.redirect ); } ); }; diff --git a/modules/ppcp-button/services.php b/modules/ppcp-button/services.php index 9432ab216..93f1454bc 100644 --- a/modules/ppcp-button/services.php +++ b/modules/ppcp-button/services.php @@ -10,7 +10,6 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\Button; use WooCommerce\PayPalCommerce\Button\Endpoint\ApproveSubscriptionEndpoint; -use WooCommerce\PayPalCommerce\Button\Endpoint\CaptureOrderEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\CartScriptParamsEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\GetOrderEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\SimulateCartEndpoint; @@ -290,13 +289,6 @@ return array( $container->get( 'api.endpoint.order' ) ); }, - 'button.endpoint.capture-order' => static function( ContainerInterface $container ): CaptureOrderEndpoint { - return new CaptureOrderEndpoint( - $container->get( 'button.request-data' ), - $container->get( 'api.endpoint.order' ), - $container->get( 'button.helper.wc-order-creator' ), - ); - }, 'button.checkout-form-saver' => static function ( ContainerInterface $container ): CheckoutFormSaver { return new CheckoutFormSaver( $container->get( 'session.handler' ) diff --git a/modules/ppcp-button/src/Assets/SmartButton.php b/modules/ppcp-button/src/Assets/SmartButton.php index ff33158ad..b1c1c7e87 100644 --- a/modules/ppcp-button/src/Assets/SmartButton.php +++ b/modules/ppcp-button/src/Assets/SmartButton.php @@ -1145,7 +1145,7 @@ document.querySelector("#payment").before(document.querySelector(".ppcp-messages 'endpoint' => \WC_AJAX::get_endpoint( ChangeCartEndpoint::ENDPOINT ), 'nonce' => wp_create_nonce( ChangeCartEndpoint::nonce() ), ), - 'get_order' => array( + 'get_order' => array( 'endpoint' => \WC_AJAX::get_endpoint( GetOrderEndpoint::ENDPOINT ), 'nonce' => wp_create_nonce( GetOrderEndpoint::nonce() ), ), @@ -1153,10 +1153,6 @@ document.querySelector("#payment").before(document.querySelector(".ppcp-messages 'endpoint' => \WC_AJAX::get_endpoint( CreateOrderEndpoint::ENDPOINT ), 'nonce' => wp_create_nonce( CreateOrderEndpoint::nonce() ), ), - 'capture_order' => array( - 'endpoint' => \WC_AJAX::get_endpoint( CaptureOrderEndpoint::ENDPOINT ), - 'nonce' => wp_create_nonce( CaptureOrderEndpoint::nonce() ), - ), 'approve_order' => array( 'endpoint' => \WC_AJAX::get_endpoint( ApproveOrderEndpoint::ENDPOINT ), 'nonce' => wp_create_nonce( ApproveOrderEndpoint::nonce() ), diff --git a/modules/ppcp-button/src/ButtonModule.php b/modules/ppcp-button/src/ButtonModule.php index 160943f28..2ae0456d6 100644 --- a/modules/ppcp-button/src/ButtonModule.php +++ b/modules/ppcp-button/src/ButtonModule.php @@ -10,7 +10,6 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\Button; use WooCommerce\PayPalCommerce\Button\Endpoint\ApproveSubscriptionEndpoint; -use WooCommerce\PayPalCommerce\Button\Endpoint\CaptureOrderEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\CartScriptParamsEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\GetOrderEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\SaveCheckoutFormEndpoint; @@ -189,16 +188,6 @@ class ButtonModule implements ServiceModule, ExtendingModule, ExecutableModule { } ); - add_action( - 'wc_ajax_' . CaptureOrderEndpoint::ENDPOINT, - static function () use ( $container ) { - $endpoint = $container->get( 'button.endpoint.capture-order' ); - assert( $endpoint instanceof GetOrderEndpoint ); - - $endpoint->handle_request(); - } - ); - add_action( 'wc_ajax_' . CreateOrderEndpoint::ENDPOINT, static function () use ( $container ) { From c1a3ff4814370abf80ed3f160d5940d638c0159b Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 4 Mar 2025 14:38:37 +0100 Subject: [PATCH 21/35] =?UTF-8?q?=F0=9F=94=A5=20Clean=20up=20GooglepoayBut?= =?UTF-8?q?ton?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/GooglepayButton.js | 121 +----------------- 1 file changed, 3 insertions(+), 118 deletions(-) diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js index e1a1c7232..6660d4c76 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js @@ -857,81 +857,6 @@ class GooglepayButton extends PaymentButton { } }; - const verifyThreeDSResponse = ( status ) => { - const { - liability_shift: liabilityShift = null, - three_d_secure: threeDS = null, - } = status || {}; - - if ( ! threeDS ) { - console.error( '3DS: No 3DS data available' ); - return false; - } - - // Check enrollment status first - switch ( threeDS.enrollment_status ) { - case 'Y': - // All clear; the following 3DS checks are meaningful. - break; - - case 'N': - case 'U': - // Risky. The 3DS verification is not available, or the bank does not participate. - // Bail, as the checks below are not reliable/relevant. - console.warn( '3DS: Not available', threeDS ); - return true; - } - - // 3DS is active, so a missing liabilityShift indicates a failure. - if ( ! liabilityShift ) { - console.error( '3DS: Missing liability shift data' ); - return false; - } - - // Check liability shift; is a risk-mitigation indicator, no failure possible. - switch ( liabilityShift ) { - case 'POSSIBLE': - console.log( '3DS: Liability shift possible' ); - break; - case 'NO': - console.warn( '3DS: Liability with the merchant' ); - break; - case 'UNKNOWN': - console.warn( '3DS: Liability shift unknown' ); - break; - } - - // Check 3DS authentication status. - switch ( threeDS.authentication_status ) { - case 'Y': - // Highest security clearance. Great customer! - console.log( '3DS: Authentication successful' ); - return true; - case 'A': - // Indicates a basic risk mitigation, but not full 3DS clearance. - console.log( '3DS: Verification started, but incomplete' ); - return true; - case 'N': - console.error( '3DS: Authentication failed or denied' ); - return false; - case 'U': - console.error( '3DS: Unable to complete authentication' ); - return false; - case 'R': - console.error( '3DS: Authentication rejected' ); - return false; - case 'C': - case 'D': - // Risky, but not failed. - console.warn( '3DS: Challenge required' ); - return true; - } - - // If we've made it this far, consider it a success, since no red-flags were present. - console.warn( '3DS: Cleared, but unknown status', threeDS ); - return true; - }; - /** * Initiates payer action and handles the 3DS contingency. * @@ -939,21 +864,10 @@ class GooglepayButton extends PaymentButton { */ const initiatePayerAction = async ( orderID ) => { this.log( 'initiatePayerAction', orderID ); - let wasApproved = false; await widgetBuilder.paypal .Googlepay() .initiatePayerAction( { orderId: orderID } ) - .then( async () => { - const order = await this.contextHandler.getOrder( orderID ); - const status = - order?.payment_source?.google_pay?.card - ?.authentication_result; - - wasApproved = verifyThreeDSResponse( status ); - } ); - - return wasApproved; }; /** @@ -993,28 +907,6 @@ class GooglepayButton extends PaymentButton { return isApproved; }; - const captureOrderServerSide = async ( orderID ) => { - let isCaptured = true; - this.log( 'context', this.contextHandler ); - await this.contextHandler.captureOrder( - { orderID, payer }, - { - restart: () => - new Promise( ( resolve ) => { - isCaptured = false; - resolve(); - } ), - order: { - get: () => - new Promise( ( resolve ) => { - resolve( null ); - } ), - }, - } - ); - return isCaptured; - }; - // Add billing data to session. moduleStorage.setPayer( payer ); setPayerData( payer ); @@ -1028,20 +920,13 @@ class GooglepayButton extends PaymentButton { if ( ! isApprovedByPayPal ) { result = paymentError( 'TRANSACTION FAILED' ); } else { - let success = false; - // This payment requires a 3DS verification before we can process the order. if ( isApprovedByPayPal === 'action_required' ) { - const approved = await initiatePayerAction( orderId ); - - // Only approve on server-side when 3DS was successful. - if ( approved ) { - success = await approveOrderServerSide( orderId ); - } - } else { - success = await approveOrderServerSide( orderId ); + await initiatePayerAction( orderId ); } + const success = await approveOrderServerSide( orderId ); + if ( success ) { result = paymentResponse( 'SUCCESS' ); } else { From ff40717c698edebb75bc2466f285524c96c95b45 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 4 Mar 2025 14:38:50 +0100 Subject: [PATCH 22/35] =?UTF-8?q?=F0=9F=8E=A8=20Add=20missing=20semicolon?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-googlepay/resources/js/GooglepayButton.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js index 6660d4c76..b62b2d5ea 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js @@ -867,7 +867,7 @@ class GooglepayButton extends PaymentButton { await widgetBuilder.paypal .Googlepay() - .initiatePayerAction( { orderId: orderID } ) + .initiatePayerAction( { orderId: orderID } ); }; /** From c403f4becab31573084c80a382cb1683e8e8c5d3 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 4 Mar 2025 14:39:06 +0100 Subject: [PATCH 23/35] =?UTF-8?q?=F0=9F=94=A5=20Remove=20unused=20BaseHand?= =?UTF-8?q?ler=20methods?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/Context/BaseHandler.js | 68 ------------------- 1 file changed, 68 deletions(-) diff --git a/modules/ppcp-googlepay/resources/js/Context/BaseHandler.js b/modules/ppcp-googlepay/resources/js/Context/BaseHandler.js index 096367438..d49bee615 100644 --- a/modules/ppcp-googlepay/resources/js/Context/BaseHandler.js +++ b/modules/ppcp-googlepay/resources/js/Context/BaseHandler.js @@ -55,74 +55,6 @@ class BaseHandler { return this.actionHandler().configuration().onApprove( data, actions ); } - getOrder( orderId ) { - return new Promise( ( resolve, reject ) => { - fetch( this.ppcpConfig.ajax.get_order.endpoint, { - method: 'POST', - credentials: 'same-origin', - body: JSON.stringify( { - nonce: this.ppcpConfig.ajax.get_order.nonce, - order_id: orderId, - } ), - } ) - .then( ( result ) => result.json() ) - .then( ( result ) => { - if ( ! result.success || ! result.data ) { - reject(); - } - - resolve( result.data ); - } ); - } ); - } - - captureOrder( data, actions ) { - return fetch( this.ppcpConfig.ajax.get_order.endpoint, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - credentials: 'same-origin', - body: JSON.stringify( { - nonce: this.ppcpConfig.ajax.get_order.nonce, - order_id: data.orderID, - } ), - } ) - .then( ( order ) => { - console.log( 'order', order ); - const orderResponse = order.json(); - console.log( - orderResponse?.payment_source?.google_pay?.card - ?.authentication_result - ); - - return fetch( this.ppcpConfig.ajax.capture_order.endpoint, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - credentials: 'same-origin', - body: JSON.stringify( { - nonce: this.ppcpConfig.ajax.capture_order.nonce, - order_id: data.orderID, - } ), - } ); - } ) - .then( ( response ) => response.json() ) - .then( ( captureResponse ) => { - console.log( 'Capture response:', captureResponse ); - const orderReceivedUrl = - captureResponse.data?.order_received_url; - console.log( 'orderReceivedUrl', orderReceivedUrl ); - setTimeout( () => { - window.location.href = orderReceivedUrl; - }, 200 ); - } ) - .catch( ( error ) => { - console.error( 'Error:', error ); - } ); - } - actionHandler() { return new CartActionHandler( this.ppcpConfig, this.errorHandler() ); } From 3083d891730190fc8e20d5ea2e7be789b2f32fd1 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 4 Mar 2025 14:41:47 +0100 Subject: [PATCH 24/35] =?UTF-8?q?=F0=9F=94=A5=20Remove=20the=20GetOrderEnd?= =?UTF-8?q?point=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We don’t need this endpoint, as orders are processed on server side --- modules/ppcp-button/services.php | 7 -- .../ppcp-button/src/Assets/SmartButton.php | 6 -- modules/ppcp-button/src/ButtonModule.php | 11 --- .../src/Endpoint/GetOrderEndpoint.php | 81 ------------------- 4 files changed, 105 deletions(-) delete mode 100644 modules/ppcp-button/src/Endpoint/GetOrderEndpoint.php diff --git a/modules/ppcp-button/services.php b/modules/ppcp-button/services.php index 93f1454bc..44e9a52ac 100644 --- a/modules/ppcp-button/services.php +++ b/modules/ppcp-button/services.php @@ -11,7 +11,6 @@ namespace WooCommerce\PayPalCommerce\Button; use WooCommerce\PayPalCommerce\Button\Endpoint\ApproveSubscriptionEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\CartScriptParamsEndpoint; -use WooCommerce\PayPalCommerce\Button\Endpoint\GetOrderEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\SimulateCartEndpoint; use WooCommerce\PayPalCommerce\Button\Helper\CartProductsHelper; use WooCommerce\PayPalCommerce\Button\Helper\CheckoutFormSaver; @@ -283,12 +282,6 @@ return array( $container->get( 'wcgateway.paypal-gateway' ) ); }, - 'button.endpoint.get-order' => static function( ContainerInterface $container ): GetOrderEndpoint { - return new GetOrderEndpoint( - $container->get( 'button.request-data' ), - $container->get( 'api.endpoint.order' ) - ); - }, 'button.checkout-form-saver' => static function ( ContainerInterface $container ): CheckoutFormSaver { return new CheckoutFormSaver( $container->get( 'session.handler' ) diff --git a/modules/ppcp-button/src/Assets/SmartButton.php b/modules/ppcp-button/src/Assets/SmartButton.php index b1c1c7e87..81ced3a68 100644 --- a/modules/ppcp-button/src/Assets/SmartButton.php +++ b/modules/ppcp-button/src/Assets/SmartButton.php @@ -24,12 +24,10 @@ use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies; use WooCommerce\PayPalCommerce\Blocks\Endpoint\UpdateShippingEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\ApproveOrderEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\ApproveSubscriptionEndpoint; -use WooCommerce\PayPalCommerce\Button\Endpoint\CaptureOrderEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\CartScriptParamsEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\ChangeCartEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\CreateOrderEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\DataClientIdEndpoint; -use WooCommerce\PayPalCommerce\Button\Endpoint\GetOrderEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData; use WooCommerce\PayPalCommerce\Button\Endpoint\SaveCheckoutFormEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\SimulateCartEndpoint; @@ -1145,10 +1143,6 @@ document.querySelector("#payment").before(document.querySelector(".ppcp-messages 'endpoint' => \WC_AJAX::get_endpoint( ChangeCartEndpoint::ENDPOINT ), 'nonce' => wp_create_nonce( ChangeCartEndpoint::nonce() ), ), - 'get_order' => array( - 'endpoint' => \WC_AJAX::get_endpoint( GetOrderEndpoint::ENDPOINT ), - 'nonce' => wp_create_nonce( GetOrderEndpoint::nonce() ), - ), 'create_order' => array( 'endpoint' => \WC_AJAX::get_endpoint( CreateOrderEndpoint::ENDPOINT ), 'nonce' => wp_create_nonce( CreateOrderEndpoint::nonce() ), diff --git a/modules/ppcp-button/src/ButtonModule.php b/modules/ppcp-button/src/ButtonModule.php index 2ae0456d6..cd2c8d381 100644 --- a/modules/ppcp-button/src/ButtonModule.php +++ b/modules/ppcp-button/src/ButtonModule.php @@ -11,7 +11,6 @@ namespace WooCommerce\PayPalCommerce\Button; use WooCommerce\PayPalCommerce\Button\Endpoint\ApproveSubscriptionEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\CartScriptParamsEndpoint; -use WooCommerce\PayPalCommerce\Button\Endpoint\GetOrderEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\SaveCheckoutFormEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\SimulateCartEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\ValidateCheckoutEndpoint; @@ -178,16 +177,6 @@ class ButtonModule implements ServiceModule, ExtendingModule, ExecutableModule { } ); - add_action( - 'wc_ajax_' . GetOrderEndpoint::ENDPOINT, - static function () use ( $container ) { - $endpoint = $container->get( 'button.endpoint.get-order' ); - assert( $endpoint instanceof GetOrderEndpoint ); - - $endpoint->handle_request(); - } - ); - add_action( 'wc_ajax_' . CreateOrderEndpoint::ENDPOINT, static function () use ( $container ) { diff --git a/modules/ppcp-button/src/Endpoint/GetOrderEndpoint.php b/modules/ppcp-button/src/Endpoint/GetOrderEndpoint.php deleted file mode 100644 index a412d705a..000000000 --- a/modules/ppcp-button/src/Endpoint/GetOrderEndpoint.php +++ /dev/null @@ -1,81 +0,0 @@ -request_data = $request_data; - $this->order_endpoint = $order_endpoint; - } - - /** - * The nonce. - * - * @return string - */ - public static function nonce() : string { - return self::ENDPOINT; - } - - /** - * Handles the request responds with the PayPal order details. - * - * @return bool This method never returns a value, but we must implement the interface. - * @throws RuntimeException When order not found or handling failed. - */ - public function handle_request() : bool { - $data = $this->request_data->read_request( self::nonce() ); - - if ( ! isset( $data['order_id'] ) ) { - throw new RuntimeException( - __( 'No order id given', 'woocommerce-paypal-payments' ) - ); - } - - $order = $this->order_endpoint->raw_order( $data['order_id'] ); - - wp_send_json_success( $order ); - - return true; - } -} From 4abdf880e677722870637c9e894fc3cf071963ad Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 4 Mar 2025 15:21:41 +0100 Subject: [PATCH 25/35] =?UTF-8?q?=E2=9C=A8=20Extend=20ThreeDSecure=20class?= =?UTF-8?q?=20for=20use=20by=20GooglePay?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ppcp-button/src/Helper/ThreeDSecure.php | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/modules/ppcp-button/src/Helper/ThreeDSecure.php b/modules/ppcp-button/src/Helper/ThreeDSecure.php index 1991112b5..d4419edc1 100644 --- a/modules/ppcp-button/src/Helper/ThreeDSecure.php +++ b/modules/ppcp-button/src/Helper/ThreeDSecure.php @@ -70,14 +70,29 @@ class ThreeDSecure { return $this->return_decision( self::NO_DECISION, $order ); } - if ( ! ( $payment_source->properties()->brand ?? '' ) ) { + if ( isset( $payment_source->properties()->card ) ) { + /** + * GooglePay provides the credit-card details and authentication-result + * via the "cards" attribute. We assume, that this structure is also + * used for other payment methods that support 3DS. + */ + $card_properties = $payment_source->properties()->card; + } else { + /** + * For regular credit card payments (via PayPal) we get all details + * directly in the payment_source properties. + */ + $card_properties = $payment_source->properties(); + } + + if ( ! ( $card_properties->brand ?? '' ) ) { return $this->return_decision( self::NO_DECISION, $order ); } - if ( ! ( $payment_source->properties()->authentication_result ?? '' ) ) { + if ( ! ( $card_properties->authentication_result ?? '' ) ) { return $this->return_decision( self::NO_DECISION, $order ); } - $authentication_result = $payment_source->properties()->authentication_result ?? null; + $authentication_result = $card_properties->authentication_result ?? null; if ( $authentication_result ) { $result = $this->card_authentication_result_factory->from_paypal_response( $authentication_result ); From 2e298cee3e653f265a0347447a44de079a84c486 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 4 Mar 2025 15:24:28 +0100 Subject: [PATCH 26/35] =?UTF-8?q?=F0=9F=8E=A8=20Major=20code=20style=20cle?= =?UTF-8?q?anup,=20no=20functional=20changes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make code more readable and maintainable --- .../ppcp-button/src/Helper/ThreeDSecure.php | 109 +++++++++--------- 1 file changed, 54 insertions(+), 55 deletions(-) diff --git a/modules/ppcp-button/src/Helper/ThreeDSecure.php b/modules/ppcp-button/src/Helper/ThreeDSecure.php index d4419edc1..0702afd41 100644 --- a/modules/ppcp-button/src/Helper/ThreeDSecure.php +++ b/modules/ppcp-button/src/Helper/ThreeDSecure.php @@ -5,7 +5,7 @@ * @package WooCommerce\PayPalCommerce\Button\Helper */ -declare(strict_types=1); +declare( strict_types = 1 ); namespace WooCommerce\PayPalCommerce\Button\Helper; @@ -19,49 +19,49 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\CardAuthenticationResultFactory */ class ThreeDSecure { - const NO_DECISION = 0; - const PROCCEED = 1; - const REJECT = 2; - const RETRY = 3; + public const NO_DECISION = 0; + public const PROCCEED = 1; + public const REJECT = 2; + public const RETRY = 3; /** * Card authentication result factory. * * @var CardAuthenticationResultFactory */ - private $card_authentication_result_factory; + private CardAuthenticationResultFactory $authentication_result; /** * The logger. * * @var LoggerInterface */ - protected $logger; + protected LoggerInterface $logger; /** * ThreeDSecure constructor. * - * @param CardAuthenticationResultFactory $card_authentication_result_factory Card authentication result factory. - * @param LoggerInterface $logger The logger. + * @param CardAuthenticationResultFactory $authentication_factory Card authentication result factory. + * @param LoggerInterface $logger The logger. */ public function __construct( - CardAuthenticationResultFactory $card_authentication_result_factory, + CardAuthenticationResultFactory $authentication_factory, LoggerInterface $logger ) { - $this->logger = $logger; - $this->card_authentication_result_factory = $card_authentication_result_factory; + $this->logger = $logger; + $this->authentication_result = $authentication_factory; } /** * Determine, how we proceed with a given order. * - * @link https://developer.paypal.com/docs/business/checkout/add-capabilities/3d-secure/#authenticationresult + * @link https://developer.paypal.com/docs/checkout/advanced/customize/3d-secure/response-parameters/ * * @param Order $order The order for which the decision is needed. * * @return int */ - public function proceed_with_order( Order $order ): int { + public function proceed_with_order( Order $order ) : int { do_action( 'woocommerce_paypal_payments_three_d_secure_before_check', $order ); @@ -85,29 +85,29 @@ class ThreeDSecure { $card_properties = $payment_source->properties(); } - if ( ! ( $card_properties->brand ?? '' ) ) { - return $this->return_decision( self::NO_DECISION, $order ); - } - if ( ! ( $card_properties->authentication_result ?? '' ) ) { + if ( empty( $card_properties->brand ) ) { return $this->return_decision( self::NO_DECISION, $order ); } - $authentication_result = $card_properties->authentication_result ?? null; - if ( $authentication_result ) { - $result = $this->card_authentication_result_factory->from_paypal_response( $authentication_result ); + if ( empty( $card_properties->authentication_result ) ) { + return $this->return_decision( self::NO_DECISION, $order ); + } - $this->logger->info( '3DS Authentication Result: ' . wc_print_r( $result->to_array(), true ) ); + $result = $this->authentication_result->from_paypal_response( $card_properties->authentication_result ); + $liability = $result->liability_shift(); - if ( $result->liability_shift() === AuthResult::LIABILITY_SHIFT_POSSIBLE ) { - return $this->return_decision( self::PROCCEED, $order ); - } + $this->logger->info( '3DS Authentication Result: ' . wc_print_r( $result->to_array(), true ) ); - if ( $result->liability_shift() === AuthResult::LIABILITY_SHIFT_UNKNOWN ) { - return $this->return_decision( self::RETRY, $order ); - } - if ( $result->liability_shift() === AuthResult::LIABILITY_SHIFT_NO ) { - return $this->return_decision( $this->no_liability_shift( $result ), $order ); - } + if ( $liability === AuthResult::LIABILITY_SHIFT_POSSIBLE ) { + return $this->return_decision( self::PROCCEED, $order ); + } + + if ( $liability === AuthResult::LIABILITY_SHIFT_UNKNOWN ) { + return $this->return_decision( self::RETRY, $order ); + } + + if ( $liability === AuthResult::LIABILITY_SHIFT_NO ) { + return $this->return_decision( $this->no_liability_shift( $result ), $order ); } return $this->return_decision( self::NO_DECISION, $order ); @@ -117,12 +117,13 @@ class ThreeDSecure { * Processes and returns a ThreeD secure decision. * * @param int $decision The ThreeD secure decision. - * @param Order $order The PayPal Order object. + * @param Order $order The PayPal Order object. * @return int */ - public function return_decision( int $decision, Order $order ) { + public function return_decision( int $decision, Order $order ) : int { $decision = apply_filters( 'woocommerce_paypal_payments_three_d_secure_decision', $decision, $order ); do_action( 'woocommerce_paypal_payments_three_d_secure_after_check', $order, $decision ); + return $decision; } @@ -133,42 +134,40 @@ class ThreeDSecure { * * @return int */ - private function no_liability_shift( AuthResult $result ): int { + private function no_liability_shift( AuthResult $result ) : int { + $enrollment = $result->enrollment_status(); + $authentication = $result->authentication_result(); - if ( - $result->enrollment_status() === AuthResult::ENROLLMENT_STATUS_BYPASS - && ! $result->authentication_result() - ) { - return self::PROCCEED; - } - if ( - $result->enrollment_status() === AuthResult::ENROLLMENT_STATUS_UNAVAILABLE - && ! $result->authentication_result() - ) { - return self::PROCCEED; - } - if ( - $result->enrollment_status() === AuthResult::ENROLLMENT_STATUS_NO - && ! $result->authentication_result() - ) { - return self::PROCCEED; + if ( ! $authentication ) { + if ( $enrollment === AuthResult::ENROLLMENT_STATUS_BYPASS ) { + return self::PROCCEED; + } + + if ( $enrollment === AuthResult::ENROLLMENT_STATUS_UNAVAILABLE ) { + return self::PROCCEED; + } + + if ( $enrollment === AuthResult::ENROLLMENT_STATUS_NO ) { + return self::PROCCEED; + } } - if ( $result->authentication_result() === AuthResult::AUTHENTICATION_RESULT_REJECTED ) { + if ( $authentication === AuthResult::AUTHENTICATION_RESULT_REJECTED ) { return self::REJECT; } - if ( $result->authentication_result() === AuthResult::AUTHENTICATION_RESULT_NO ) { + if ( $authentication === AuthResult::AUTHENTICATION_RESULT_NO ) { return self::REJECT; } - if ( $result->authentication_result() === AuthResult::AUTHENTICATION_RESULT_UNABLE ) { + if ( $authentication === AuthResult::AUTHENTICATION_RESULT_UNABLE ) { return self::RETRY; } - if ( ! $result->authentication_result() ) { + if ( ! $authentication ) { return self::RETRY; } + return self::NO_DECISION; } } From 60ab51a2f8dbdc2050fb7b1f80b7bc8579efe29f Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 4 Mar 2025 17:14:07 +0100 Subject: [PATCH 27/35] =?UTF-8?q?=F0=9F=8E=A8=20Slightly=20simplify=20JS?= =?UTF-8?q?=20return=20value?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-googlepay/resources/js/GooglepayButton.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js index b62b2d5ea..7ad3bca40 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js @@ -862,10 +862,10 @@ class GooglepayButton extends PaymentButton { * * @param {string} orderID */ - const initiatePayerAction = async ( orderID ) => { + const initiatePayerAction = ( orderID ) => { this.log( 'initiatePayerAction', orderID ); - await widgetBuilder.paypal + return widgetBuilder.paypal .Googlepay() .initiatePayerAction( { orderId: orderID } ); }; From 8153026e22a33c79e1521f2d12f8245f06a0f50e Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 4 Mar 2025 17:29:09 +0100 Subject: [PATCH 28/35] =?UTF-8?q?=F0=9F=8E=A8=20Code-style,=20apply=20phpc?= =?UTF-8?q?s=20rules?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Endpoint/ApproveOrderEndpoint.php | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/modules/ppcp-button/src/Endpoint/ApproveOrderEndpoint.php b/modules/ppcp-button/src/Endpoint/ApproveOrderEndpoint.php index 2e8cc4c0b..57e394539 100644 --- a/modules/ppcp-button/src/Endpoint/ApproveOrderEndpoint.php +++ b/modules/ppcp-button/src/Endpoint/ApproveOrderEndpoint.php @@ -6,7 +6,7 @@ * @package WooCommerce\PayPalCommerce\Button\Endpoint */ -declare(strict_types=1); +declare( strict_types = 1 ); namespace WooCommerce\PayPalCommerce\Button\Endpoint; @@ -24,7 +24,6 @@ use WooCommerce\PayPalCommerce\Button\Helper\WooCommerceOrderCreator; use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; -use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper; /** * Class ApproveOrderEndpoint @@ -115,17 +114,17 @@ class ApproveOrderEndpoint implements EndpointInterface { /** * ApproveOrderEndpoint constructor. * - * @param RequestData $request_data The request data helper. - * @param OrderEndpoint $order_endpoint The order endpoint. - * @param SessionHandler $session_handler The session handler. - * @param ThreeDSecure $three_d_secure The 3d secure helper object. - * @param Settings $settings The settings. - * @param DccApplies $dcc_applies The DCC applies object. - * @param OrderHelper $order_helper The order helper. + * @param RequestData $request_data The request data helper. + * @param OrderEndpoint $order_endpoint The order endpoint. + * @param SessionHandler $session_handler The session handler. + * @param ThreeDSecure $three_d_secure The 3d secure helper object. + * @param Settings $settings The settings. + * @param DccApplies $dcc_applies The DCC applies object. + * @param OrderHelper $order_helper The order helper. * @param bool $final_review_enabled Whether the final review is enabled. - * @param PayPalGateway $gateway The WC gateway. - * @param WooCommerceOrderCreator $wc_order_creator The WooCommerce order creator. - * @param LoggerInterface $logger The logger. + * @param PayPalGateway $gateway The WC gateway. + * @param WooCommerceOrderCreator $wc_order_creator The WooCommerce order creator. + * @param LoggerInterface $logger The logger. */ public function __construct( RequestData $request_data, @@ -159,7 +158,7 @@ class ApproveOrderEndpoint implements EndpointInterface { * * @return string */ - public static function nonce(): string { + public static function nonce() : string { return self::ENDPOINT; } @@ -169,9 +168,9 @@ class ApproveOrderEndpoint implements EndpointInterface { * @return bool * @throws RuntimeException When order not found or handling failed. */ - public function handle_request(): bool { + public function handle_request() : bool { try { - $data = $this->request_data->read_request( $this->nonce() ); + $data = $this->request_data->read_request( self::nonce() ); if ( ! isset( $data['order_id'] ) ) { throw new RuntimeException( __( 'No order id given', 'woocommerce-paypal-payments' ) @@ -181,6 +180,7 @@ class ApproveOrderEndpoint implements EndpointInterface { $order = $this->api_endpoint->order( $data['order_id'] ); $payment_source = $order->payment_source(); + if ( $payment_source && $payment_source->name() === 'card' ) { if ( $this->settings->has( 'disable_cards' ) ) { $disabled_cards = (array) $this->settings->get( 'disable_cards' ); @@ -250,6 +250,7 @@ class ApproveOrderEndpoint implements EndpointInterface { wp_send_json_success( array( 'order_received_url' => $order_received_url ) ); } wp_send_json_success(); + return true; } catch ( Exception $error ) { $this->logger->error( 'Order approve failed: ' . $error->getMessage() ); @@ -262,6 +263,7 @@ class ApproveOrderEndpoint implements EndpointInterface { 'details' => is_a( $error, PayPalApiException::class ) ? $error->details() : array(), ) ); + return false; } } @@ -271,7 +273,7 @@ class ApproveOrderEndpoint implements EndpointInterface { * * @return void */ - protected function toggle_final_review_enabled_setting(): void { + protected function toggle_final_review_enabled_setting() : void { // TODO new-ux: This flag must also be updated in the new settings. $final_review_enabled_setting = $this->settings->has( 'blocks_final_review_enabled' ) && $this->settings->get( 'blocks_final_review_enabled' ); $this->settings->set( 'blocks_final_review_enabled', ! $final_review_enabled_setting ); From 4f420a2f8ab24ec05b7e86adf814ca2f4ef2a941 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 4 Mar 2025 17:29:41 +0100 Subject: [PATCH 29/35] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Extract=20the=203DS?= =?UTF-8?q?=20check=20into=20a=20new=20method?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Endpoint/ApproveOrderEndpoint.php | 100 ++++++++++++++---- 1 file changed, 82 insertions(+), 18 deletions(-) diff --git a/modules/ppcp-button/src/Endpoint/ApproveOrderEndpoint.php b/modules/ppcp-button/src/Endpoint/ApproveOrderEndpoint.php index 57e394539..7d586a6c8 100644 --- a/modules/ppcp-button/src/Endpoint/ApproveOrderEndpoint.php +++ b/modules/ppcp-button/src/Endpoint/ApproveOrderEndpoint.php @@ -13,6 +13,7 @@ namespace WooCommerce\PayPalCommerce\Button\Endpoint; use Exception; use Psr\Log\LoggerInterface; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; +use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies; @@ -199,29 +200,23 @@ class ApproveOrderEndpoint implements EndpointInterface { ); } } - $proceed = $this->threed_secure->proceed_with_order( $order ); - if ( ThreeDSecure::RETRY === $proceed ) { - throw new RuntimeException( - __( - 'Something went wrong. Please try again.', - 'woocommerce-paypal-payments' - ) - ); - } - if ( ThreeDSecure::REJECT === $proceed ) { - throw new RuntimeException( - __( - 'Unfortunately, we can\'t accept your card. Please choose a different payment method.', - 'woocommerce-paypal-payments' - ) - ); - } + + // This check will either pass, or throw an exception. + $this->verify_three_d_secure( $order ); + $this->session_handler->replace_order( $order ); + // Exit the request early. wp_send_json_success(); } - if ( $this->order_helper->contains_physical_goods( $order ) && ! $order->status()->is( OrderStatus::APPROVED ) && ! $order->status()->is( OrderStatus::CREATED ) ) { + // Verify 3DS details. Throws an error when security check fails. + $this->verify_three_d_secure( $order ); + + $is_ready = $order->status()->is( OrderStatus::APPROVED ) + || $order->status()->is( OrderStatus::CREATED ); + + if ( ! $is_ready && $this->order_helper->contains_physical_goods( $order ) ) { $message = sprintf( // translators: %s is the id of the order. __( 'Order %s is not ready for processing yet.', 'woocommerce-paypal-payments' ), @@ -279,4 +274,73 @@ class ApproveOrderEndpoint implements EndpointInterface { $this->settings->set( 'blocks_final_review_enabled', ! $final_review_enabled_setting ); $this->settings->persist(); } + + /** + * Performs a 3DS check to verify the payment is not rejected from PayPal side. + * + * This method only checks, if the payment was rejected: + * + * - No 3DS details are present: The payment can proceed. + * - 3DS details present but no rejected: Payment can proceed. + * - 3DS details with a clear rejected: Payment fails. + * + * @param Order $order The PayPal order to inspect. + * @throws RuntimeException When the 3DS check was rejected. + */ + protected function verify_three_d_secure( Order $order ) : void { + $payment_source = $order->payment_source(); + + if ( ! $payment_source ) { + // Missing 3DS details. + return; + } + + $proceed = ThreeDSecure::NO_DECISION; + $order_status = $order->status(); + $source_name = $payment_source->name(); + + /** + * For GooglePay (and possibly other payment sources) we check the order + * status, as it will clearly indicate if verification is needed. + * + * Note: PayPal is currently investigating this case. + * Maybe the order status is wrong and should be ACCEPTED, in that case, + * we could drop the condition and always run proceed_with_order(). + */ + if ( $order_status->is( OrderStatus::PAYER_ACTION_REQUIRED ) ) { + $proceed = $this->threed_secure->proceed_with_order( $order ); + } elseif ( 'card' === $source_name ) { + // For credit cards, we also check the 3DS response. + $proceed = $this->threed_secure->proceed_with_order( $order ); + } + + // Handle the verification result based on the proceed value. + switch ( $proceed ) { + case ThreeDSecure::PROCCEED: + // Check was successful. + return; + + case ThreeDSecure::NO_DECISION: + // No rejection. Let's proceed with the payment. + return; + + case ThreeDSecure::RETRY: + // Rejection case 1, verification can be retried. + throw new RuntimeException( + __( + 'Something went wrong. Please try again.', + 'woocommerce-paypal-payments' + ) + ); + + case ThreeDSecure::REJECT: + // Rejection case 2, payment was rejected. + throw new RuntimeException( + __( + 'Unfortunately, we can\'t accept your card. Please choose a different payment method.', + 'woocommerce-paypal-payments' + ) + ); + } + } } From c25d1768e4769a36d91f2776fe2119ab220ba450 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 4 Mar 2025 17:44:08 +0100 Subject: [PATCH 30/35] =?UTF-8?q?=E2=8F=AA=EF=B8=8F=20Undo=20irrelevant=20?= =?UTF-8?q?changes=20in=20OrderEndpoint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Endpoint/OrderEndpoint.php | 23 ++++--------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php b/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php index fcda542de..93bf7245c 100644 --- a/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php +++ b/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php @@ -449,16 +449,15 @@ class OrderEndpoint { return $order; } - /** - * Fetches an order for a given ID and returns the raw, unparsed JSON response. + * Fetches an order for a given ID. * - * @param string $id The PayPal order-ID. + * @param string $id The ID. * - * @return stdClass + * @return Order * @throws RuntimeException If the request fails. */ - public function raw_order( string $id ): stdClass { + public function order( string $id ): Order { $bearer = $this->bearer->bearer(); $url = trailingslashit( $this->host ) . 'v2/checkout/orders/' . $id; $args = array( @@ -514,20 +513,6 @@ class OrderEndpoint { throw $error; } - return $json; - } - - /** - * Fetches an order for a given ID. - * - * @param string $id The ID. - * - * @return Order - * @throws RuntimeException If the request fails. - */ - public function order( string $id ) : Order { - $json = $this->raw_order( $id ); - return $this->order_factory->from_paypal_response( $json ); } From 581607223c103b79ffb7a90a45659e6b3d5aa962 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 4 Mar 2025 17:55:57 +0100 Subject: [PATCH 31/35] =?UTF-8?q?=F0=9F=8E=A8=20Make=20the=20JS=20code=20m?= =?UTF-8?q?ore=20readable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/GooglepayButton.js | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js index 7ad3bca40..6a897a125 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js @@ -62,6 +62,21 @@ import moduleStorage from './Helper/GooglePayStorage'; * @property {string} checkoutOption - Optional. Affects the submit button text displayed in the Google Pay payment sheet. */ +/** + * Indicates that the payment was approved by PayPal and can be processed. + */ +const ORDER_APPROVED = 'approved'; + +/** + * We should not process this order, as it failed for some reason. + */ +const ORDER_FAILED = 'failed'; + +/** + * The order is still pending, and we need to request 3DS details from the customer. + */ +const ORDER_INCOMPLETE = 'payerAction'; + function payerDataFromPaymentResponse( response ) { const raw = response?.paymentMethodData?.info?.billingAddress; @@ -818,10 +833,7 @@ class GooglepayButton extends PaymentButton { }; if ( intent || message ) { - response.error = { - intent, - message, - }; + response.error = { intent, message }; } this.log( 'processPaymentResponse', response ); @@ -849,11 +861,13 @@ class GooglepayButton extends PaymentButton { switch ( confirmOrderResponse?.status ) { case 'APPROVED': - return true; + return ORDER_APPROVED; + case 'PAYER_ACTION_REQUIRED': - return 'action_required'; + return ORDER_INCOMPLETE; + default: - return false; + return ORDER_FAILED; } }; @@ -915,13 +929,13 @@ class GooglepayButton extends PaymentButton { const orderId = await this.contextHandler.createOrder(); this.log( 'createOrder', orderId ); - const isApprovedByPayPal = await checkPayPalApproval( orderId ); + const orderState = await checkPayPalApproval( orderId ); - if ( ! isApprovedByPayPal ) { + if ( ORDER_FAILED === orderState ) { result = paymentError( 'TRANSACTION FAILED' ); } else { // This payment requires a 3DS verification before we can process the order. - if ( isApprovedByPayPal === 'action_required' ) { + if ( ORDER_INCOMPLETE === orderState ) { await initiatePayerAction( orderId ); } From 377bdc1178dec8c3bde0b9a8931a76237b5b50f2 Mon Sep 17 00:00:00 2001 From: carmenmaymo Date: Fri, 21 Mar 2025 09:54:38 +0100 Subject: [PATCH 32/35] Expect funding source for googlepay --- modules/ppcp-googlepay/src/GooglepayModule.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/ppcp-googlepay/src/GooglepayModule.php b/modules/ppcp-googlepay/src/GooglepayModule.php index 4c09705ba..9d23bf200 100644 --- a/modules/ppcp-googlepay/src/GooglepayModule.php +++ b/modules/ppcp-googlepay/src/GooglepayModule.php @@ -251,10 +251,10 @@ class GooglepayModule implements ServiceModule, ExtendingModule, ExecutableModul add_filter( 'ppcp_create_order_request_body_data', - static function ( array $data, string $payment_method ) use ( $c ) : array { - // TODO (bug): This condition only works when using Google Pay as separate gateway! - // When GooglePay is part of the smart buttons block, this condition fails and will not enable 3DS. - if ( $payment_method !== GooglePayGateway::ID ) { + static function ( array $data, string $payment_method, array $request ) use ( $c ) : array { + + $funding_source = $request['funding_source']; + if ( $payment_method !== GooglePayGateway::ID && $funding_source !== 'googlepay' ) { return $data; } @@ -282,7 +282,7 @@ class GooglepayModule implements ServiceModule, ExtendingModule, ExecutableModul return $data; }, 10, - 2 + 3 ); return true; From 01adac61015d6ea70b432097e4daad61de7fada0 Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Thu, 27 Mar 2025 09:57:35 +0100 Subject: [PATCH 33/35] Fix typo --- .../ppcp-button/src/Endpoint/ApproveOrderEndpoint.php | 2 +- modules/ppcp-button/src/Helper/ThreeDSecure.php | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/ppcp-button/src/Endpoint/ApproveOrderEndpoint.php b/modules/ppcp-button/src/Endpoint/ApproveOrderEndpoint.php index 7d586a6c8..8749fb4ba 100644 --- a/modules/ppcp-button/src/Endpoint/ApproveOrderEndpoint.php +++ b/modules/ppcp-button/src/Endpoint/ApproveOrderEndpoint.php @@ -316,7 +316,7 @@ class ApproveOrderEndpoint implements EndpointInterface { // Handle the verification result based on the proceed value. switch ( $proceed ) { - case ThreeDSecure::PROCCEED: + case ThreeDSecure::PROCEED: // Check was successful. return; diff --git a/modules/ppcp-button/src/Helper/ThreeDSecure.php b/modules/ppcp-button/src/Helper/ThreeDSecure.php index 0702afd41..dee633fe2 100644 --- a/modules/ppcp-button/src/Helper/ThreeDSecure.php +++ b/modules/ppcp-button/src/Helper/ThreeDSecure.php @@ -20,7 +20,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\CardAuthenticationResultFactory class ThreeDSecure { public const NO_DECISION = 0; - public const PROCCEED = 1; + public const PROCEED = 1; public const REJECT = 2; public const RETRY = 3; @@ -99,7 +99,7 @@ class ThreeDSecure { $this->logger->info( '3DS Authentication Result: ' . wc_print_r( $result->to_array(), true ) ); if ( $liability === AuthResult::LIABILITY_SHIFT_POSSIBLE ) { - return $this->return_decision( self::PROCCEED, $order ); + return $this->return_decision( self::PROCEED, $order ); } if ( $liability === AuthResult::LIABILITY_SHIFT_UNKNOWN ) { @@ -140,15 +140,15 @@ class ThreeDSecure { if ( ! $authentication ) { if ( $enrollment === AuthResult::ENROLLMENT_STATUS_BYPASS ) { - return self::PROCCEED; + return self::PROCEED; } if ( $enrollment === AuthResult::ENROLLMENT_STATUS_UNAVAILABLE ) { - return self::PROCCEED; + return self::PROCEED; } if ( $enrollment === AuthResult::ENROLLMENT_STATUS_NO ) { - return self::PROCCEED; + return self::PROCEED; } } From c798172fd2e5e6188a4545f3d88db0dcd2ba44a1 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 23 Apr 2025 14:57:27 +0200 Subject: [PATCH 34/35] =?UTF-8?q?=F0=9F=94=8A=20Add=20one=20additional=20l?= =?UTF-8?q?og=20to=20confirm=203DS=20process?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-googlepay/resources/js/GooglepayButton.js | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js index 6a897a125..46b6459e7 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js @@ -937,6 +937,7 @@ class GooglepayButton extends PaymentButton { // This payment requires a 3DS verification before we can process the order. if ( ORDER_INCOMPLETE === orderState ) { await initiatePayerAction( orderId ); + this.log( '3DS verification completed' ); } const success = await approveOrderServerSide( orderId ); From 3ace72fdf252c75c47ea2a2ae7f20e50c7fd29f8 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 23 Apr 2025 18:25:27 +0200 Subject: [PATCH 35/35] =?UTF-8?q?=F0=9F=94=8A=20Improve=203DS=20logging?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-googlepay/resources/js/GooglepayButton.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js index 46b6459e7..627038c83 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js @@ -936,8 +936,8 @@ class GooglepayButton extends PaymentButton { } else { // This payment requires a 3DS verification before we can process the order. if ( ORDER_INCOMPLETE === orderState ) { - await initiatePayerAction( orderId ); - this.log( '3DS verification completed' ); + const response = await initiatePayerAction( orderId ); + this.log( '3DS verification completed', response ); } const success = await approveOrderServerSide( orderId );