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 );