🔀 Merge branch 'trunk'

# Conflicts:
#	modules/ppcp-settings/resources/js/data/common/action-types.js
#	modules/ppcp-settings/resources/js/data/common/controls.js
#	modules/ppcp-settings/resources/js/data/common/hooks.js
#	modules/ppcp-settings/services.php
This commit is contained in:
Philipp Stracker 2025-01-03 11:03:18 +01:00
commit 9a84c7b4a9
No known key found for this signature in database
52 changed files with 2318 additions and 1462 deletions

View file

@ -27,7 +27,7 @@ web_environment:
- ADMIN_USER=admin
- ADMIN_PASS=admin
- ADMIN_EMAIL=admin@example.com
- WC_VERSION=7.7.2
- WC_VERSION=9.5.1
# Key features of ddev's config.yaml:

View file

@ -50,7 +50,7 @@ jobs:
if: github.event.inputs.filePrefix
- name: Upload
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: ${{ env.FILENAME }}
path: dist/

View file

@ -1,22 +1,34 @@
*** Changelog ***
= 2.9.6 - XXXX-XX-XX =
* Fix - NOT_ENABLED_TO_VAULT_PAYMENT_SOURCE on PayPal transactions when using ACDC Vaulting without PayPal Vault approval #2955
* Fix - Express buttons for Free Trial Subscription products on Block Cart/Checkout trigger CANNOT_BE_ZERO_OR_NEGATIVE error #2872
* Fix - String translations not applied to Card Fields on Block Checkout #2934
* Fix - Fastlane component included in script when Fastlane is disabled #2911
* Fix - Zero amount line items may trigger CANNOT_BE_ZERO_OR_NEGATIVE error after rounding error #2906
* Fix - “Save changes” is grey and unclickable when switching from Sandbox to Live #2895
* Fix - plugin queries variations when button/messaging is disabled on single product page #2896
* Fix - Use get_id instead of get_order_number on setting custom_id (author @0verscore) #2930
* Enhancement - Improve fraud response order notes for Advanced Card Processing transactions #2905
* Tweak - Update the minimum plugin requirements to WordPress 6.5 & WooCommerce 9.2 #2920
= 2.9.5 - 2024-12-10 =
Fix - Early translation loading triggers `Function _load_textdomain_just_in_time was called incorrectly.` notice #2816
Fix - ACDC card fields not loading and payment not successful when Classic Checkout Smart Button Location disabled #2852
Fix - ACDC gateway does not appear for guests when is Fastlane enabled and a subscription product is in the cart #2745
Fix - "Voide authorization" button does not appear for Apple Pay/Google Pay orders when payment buttons are separated #2752
Fix - Additional payment tokens saved with new customer_id #2820
Fix - Vaulted payment method may not be displayed in PayPal button for return buyer #2809
Fix - Conflict with EasyShip plugin due to shipping methods loading too early #2845
Fix - Restore accidentally removed ACDC currencies #2838
Enhancement - Native gateway icon for PayPal & Pay upon Invoice gateways #2712
Enhancement - Allow disabling specific card types for Fastlane #2704
Enhancement - Fastlane Insights SDK implementation for block Checkout #2737
Enhancement - Hide split local APMs in Payments settings tab when PayPal is not enabled #2703
Enhancement - Do not load split local APMs on Checkout when PayPal is not enabled #2792
Enhancement - Add support for Button Options in the Block Checkout for Apple Pay & Google Pay buttons #2797 #2772
Enhancement - Disable “Add payment method” button while saving ACDC payment #2794
Enhancement - Sanitize soft_descriptor field #2846 #2854
* Fix - Early translation loading triggers `Function _load_textdomain_just_in_time was called incorrectly.` notice #2816
* Fix - ACDC card fields not loading and payment not successful when Classic Checkout Smart Button Location disabled #2852
* Fix - ACDC gateway does not appear for guests when is Fastlane enabled and a subscription product is in the cart #2745
* Fix - "Voide authorization" button does not appear for Apple Pay/Google Pay orders when payment buttons are separated #2752
* Fix - Additional payment tokens saved with new customer_id #2820
* Fix - Vaulted payment method may not be displayed in PayPal button for return buyer #2809
* Fix - Conflict with EasyShip plugin due to shipping methods loading too early #2845
* Fix - Restore accidentally removed ACDC currencies #2838
* Enhancement - Native gateway icon for PayPal & Pay upon Invoice gateways #2712
* Enhancement - Allow disabling specific card types for Fastlane #2704
* Enhancement - Fastlane Insights SDK implementation for block Checkout #2737
* Enhancement - Hide split local APMs in Payments settings tab when PayPal is not enabled #2703
* Enhancement - Do not load split local APMs on Checkout when PayPal is not enabled #2792
* Enhancement - Add support for Button Options in the Block Checkout for Apple Pay & Google Pay buttons #2797 #2772
* Enhancement - Disable “Add payment method” button while saving ACDC payment #2794
* Enhancement - Sanitize soft_descriptor field #2846 #2854
= 2.9.4 - 2024-11-11 =
* Fix - Apple Pay button preview missing in Standard payment and Advanced Processing tabs #2755

View file

@ -17,44 +17,44 @@ class FraudProcessorResponse {
/**
* The AVS response code.
*
* @var string|null
* @var string
*/
protected $avs_code;
protected string $avs_code;
/**
* The CVV response code.
*
* @var string|null
* @var string
*/
protected $cvv_code;
protected string $cvv2_code;
/**
* FraudProcessorResponse constructor.
*
* @param string|null $avs_code The AVS response code.
* @param string|null $cvv_code The CVV response code.
* @param string|null $cvv2_code The CVV response code.
*/
public function __construct( ?string $avs_code, ?string $cvv_code ) {
$this->avs_code = $avs_code;
$this->cvv_code = $cvv_code;
public function __construct( ?string $avs_code, ?string $cvv2_code ) {
$this->avs_code = (string) $avs_code;
$this->cvv2_code = (string) $cvv2_code;
}
/**
* Returns the AVS response code.
*
* @return string|null
* @return string
*/
public function avs_code(): ?string {
public function avs_code(): string {
return $this->avs_code;
}
/**
* Returns the CVV response code.
*
* @return string|null
* @return string
*/
public function cvv_code(): ?string {
return $this->cvv_code;
public function cvv_code(): string {
return $this->cvv2_code;
}
/**
@ -64,11 +64,99 @@ class FraudProcessorResponse {
*/
public function to_array(): array {
return array(
'avs_code' => $this->avs_code() ?: '',
'avs_code' => $this->avs_code(),
'cvv2_code' => $this->cvv_code(),
// For backwards compatibility.
'address_match' => $this->avs_code() === 'M' ? 'Y' : 'N',
'postal_match' => $this->avs_code() === 'M' ? 'Y' : 'N',
'cvv_match' => $this->cvv_code() === 'M' ? 'Y' : 'N',
);
}
/**
* Retrieves the AVS (Address Verification System) code messages based on the AVS response code.
*
* Provides human-readable descriptions for various AVS response codes
* and returns the corresponding message for the given code.
*
* @return string The AVS response code message. If the code is not found, an error message is returned.
*/
public function get_avs_code_message(): string {
if ( ! $this->avs_code() ) {
return '';
}
$messages = array(
/* Visa, Mastercard, Discover, American Express */
'A' => 'A: Address - Address only (no ZIP code)',
'B' => 'B: International "A" - Address only (no ZIP code)',
'C' => 'C: International "N" - None. The transaction is declined.',
'D' => 'D: International "X" - Address and Postal Code',
'E' => 'E: Not allowed for MOTO (Internet/Phone) transactions - Not applicable. The transaction is declined.',
'F' => 'F: UK-specific "X" - Address and Postal Code',
'G' => 'G: Global Unavailable - Not applicable',
'I' => 'I: International Unavailable - Not applicable',
'M' => 'M: Address - Address and Postal Code',
'N' => 'N: No - None. The transaction is declined.',
'P' => 'P: Postal (International "Z") - Postal Code only (no Address)',
'R' => 'R: Retry - Not applicable',
'S' => 'S: Service not Supported - Not applicable',
'U' => 'U: Unavailable / Address not checked, or acquirer had no response. Service not available.',
'W' => 'W: Whole ZIP - Nine-digit ZIP code (no Address)',
'X' => 'X: Exact match - Address and nine-digit ZIP code)',
'Y' => 'Y: Yes - Address and five-digit ZIP',
'Z' => 'Z: ZIP - Five-digit ZIP code (no Address)',
/* Maestro */
'0' => '0: All the address information matched.',
'1' => '1: None of the address information matched. The transaction is declined.',
'2' => '2: Part of the address information matched.',
'3' => '3: The merchant did not provide AVS information. Not processed.',
'4' => '4: Address not checked, or acquirer had no response. Service not available.',
);
/**
* Psalm suppress
*
* @psalm-suppress PossiblyNullArrayOffset
* @psalm-suppress PossiblyNullArgument
*/
return $messages[ $this->avs_code() ] ?? sprintf( '%s: Error', $this->avs_code() );
}
/**
* Retrieves the CVV2 code message based on the CVV code provided.
*
* This method maps CVV response codes to their corresponding descriptive messages.
*
* @return string The descriptive message corresponding to the CVV2 code, or a formatted error message if the code is unrecognized.
*/
public function get_cvv2_code_message(): string {
if ( ! $this->cvv_code() ) {
return '';
}
$messages = array(
/* Visa, Mastercard, Discover, American Express */
'E' => 'E: Error - Unrecognized or Unknown response',
'I' => 'I: Invalid or Null',
'M' => 'M: Match or CSC',
'N' => 'N: No match',
'P' => 'P: Not processed',
'S' => 'S: Service not supported',
'U' => 'U: Unknown - Issuer is not certified',
'X' => 'X: No response / Service not available',
/* Maestro */
'0' => '0: Matched CVV2',
'1' => '1: No match',
'2' => '2: The merchant has not implemented CVV2 code handling',
'3' => '3: Merchant has indicated that CVV2 is not present on card',
'4' => '4: Service not available',
);
/**
* Psalm suppress
*
* @psalm-suppress PossiblyNullArrayOffset
* @psalm-suppress PossiblyNullArgument
*/
return $messages[ $this->cvv_code() ] ?? sprintf( '%s: Error', $this->cvv_code() );
}
}

View file

@ -178,6 +178,11 @@ class PurchaseUnitSanitizer {
// Get a more intelligent adjustment mechanism.
$increment = ( new MoneyFormatter() )->minimum_increment( $item['unit_amount']['currency_code'] );
// not floor items that will be negative then.
if ( (float) $item['unit_amount']['value'] < $increment ) {
continue;
}
$this->purchase_unit['items'][ $index ]['unit_amount'] = ( new Money(
( (float) $item['unit_amount']['value'] ) - $increment,
$item['unit_amount']['currency_code']

View file

@ -92,7 +92,10 @@ class AxoBlockModule implements ServiceModule, ExtendingModule, ExecutableModule
*/
add_filter(
'woocommerce_paypal_payments_sdk_components_hook',
function( $components ) {
function( $components ) use ( $c ) {
if ( ! $c->has( 'axo.available' ) || ! $c->get( 'axo.available' ) ) {
return $components;
}
$components[] = 'fastlane';
return $components;
}

View file

@ -44,7 +44,9 @@ return array(
// If AXO is configured and onboarded.
'axo.available' => static function ( ContainerInterface $container ): bool {
return true;
$settings = $container->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
return $settings->has( 'axo_enabled' ) && $settings->get( 'axo_enabled' );
},
'axo.url' => static function ( ContainerInterface $container ): string {

View file

@ -246,7 +246,13 @@ class AxoModule implements ServiceModule, ExtendingModule, ExecutableModule {
*/
add_filter(
'woocommerce_paypal_payments_sdk_components_hook',
function( $components ) {
function( $components ) use ( $c ) {
$dcc_configuration = $c->get( 'wcgateway.configuration.dcc' );
assert( $dcc_configuration instanceof DCCGatewayConfiguration );
if ( ! $dcc_configuration->use_fastlane() ) {
return $components;
}
$components[] = 'fastlane';
return $components;
}
@ -255,14 +261,18 @@ class AxoModule implements ServiceModule, ExtendingModule, ExecutableModule {
add_action(
'wp_head',
function () use ( $c ) {
// phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript
echo '<script async src="https://www.paypalobjects.com/insights/v1/paypal-insights.sandbox.min.js"></script>';
// Add meta tag to allow feature-detection of the site's AXO payment state.
$dcc_configuration = $c->get( 'wcgateway.configuration.dcc' );
assert( $dcc_configuration instanceof DCCGatewayConfiguration );
$this->add_feature_detection_tag( $dcc_configuration->use_fastlane() );
if ( $dcc_configuration->use_fastlane() ) {
// phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript
echo '<script async src="https://www.paypalobjects.com/insights/v1/paypal-insights.sandbox.min.js"></script>';
$this->add_feature_detection_tag( true );
} else {
$this->add_feature_detection_tag( false );
}
}
);

View file

@ -0,0 +1,52 @@
import { useMemo } from '@wordpress/element';
import { normalizeStyleForFundingSource } from '../../../../ppcp-button/resources/js/modules/Helper/Style';
import { PayPalButtons, PayPalScriptProvider } from '@paypal/react-paypal-js';
export const BlockEditorPayPalComponent = ( {
config,
fundingSource,
buttonAttributes,
} ) => {
const urlParams = useMemo(
() => ( {
clientId: 'test',
...config.scriptData.url_params,
dataNamespace: 'ppcp-blocks-editor-paypal-buttons',
components: 'buttons',
} ),
[]
);
const style = useMemo( () => {
const configStyle = normalizeStyleForFundingSource(
config.scriptData.button.style,
fundingSource
);
if ( buttonAttributes ) {
return {
...configStyle,
height: buttonAttributes.height
? Number( buttonAttributes.height )
: configStyle.height,
borderRadius: buttonAttributes.borderRadius
? Number( buttonAttributes.borderRadius )
: configStyle.borderRadius,
};
}
return configStyle;
}, [ fundingSource, buttonAttributes ] );
return (
<PayPalScriptProvider options={ urlParams }>
<PayPalButtons
className={ `ppc-button-container-${ fundingSource }` }
fundingSource={ fundingSource }
style={ style }
forceReRender={ [ buttonAttributes || {} ] }
onClick={ () => false }
/>
</PayPalScriptProvider>
);
};

View file

@ -19,11 +19,7 @@ import {
import { cartHasSubscriptionProducts } from '../Helper/Subscription';
import { __ } from '@wordpress/i18n';
export function CardFields( {
config,
eventRegistration,
emitResponse,
} ) {
export function CardFields( { config, eventRegistration, emitResponse } ) {
const { onPaymentSetup } = eventRegistration;
const { responseTypes } = emitResponse;
@ -96,14 +92,34 @@ export function CardFields( {
console.error( err );
} }
>
<PayPalNameField placeholder={ __( 'Cardholder Name (optional)', 'woocommerce-paypal-payments' ) }/>
<PayPalNumberField placeholder={ __( 'Card number', 'woocommerce-paypal-payments' ) }/>
<div style={ { display: "flex", width: "100%" } }>
<div style={ { width: "100%" } }>
<PayPalExpiryField placeholder={ __( 'MM / YY', 'woocommerce-paypal-payments' ) }/>
<PayPalNameField
placeholder={ __(
'Cardholder Name (optional)',
'woocommerce-paypal-payments'
) }
/>
<PayPalNumberField
placeholder={ __(
'Card number',
'woocommerce-paypal-payments'
) }
/>
<div style={ { display: 'flex', width: '100%' } }>
<div style={ { width: '100%' } }>
<PayPalExpiryField
placeholder={ __(
'MM / YY',
'woocommerce-paypal-payments'
) }
/>
</div>
<div style={ { width: "100%" } }>
<PayPalCVVField placeholder={ __( 'CVV', 'woocommerce-paypal-payments' ) }/>
<div style={ { width: '100%' } }>
<PayPalCVVField
placeholder={ __(
'CVV',
'woocommerce-paypal-payments'
) }
/>
</div>
</div>
<CheckoutHandler

View file

@ -0,0 +1,14 @@
export const PaypalLabel = ( { components, config } ) => {
const { PaymentMethodIcons } = components;
return (
<>
<span
dangerouslySetInnerHTML={ {
__html: config.title,
} }
/>
<PaymentMethodIcons icons={ config.icon } align="right" />
</>
);
};

View file

@ -0,0 +1,493 @@
import { useEffect, useState } from '@wordpress/element';
import { loadPayPalScript } from '../../../../ppcp-button/resources/js/modules/Helper/PayPalScriptLoading';
import {
mergeWcAddress,
paypalAddressToWc,
paypalOrderToWcAddresses,
} from '../Helper/Address';
import { convertKeysToSnakeCase } from '../Helper/Helper';
import buttonModuleWatcher from '../../../../ppcp-button/resources/js/modules/ButtonModuleWatcher';
import { normalizeStyleForFundingSource } from '../../../../ppcp-button/resources/js/modules/Helper/Style';
import {
cartHasSubscriptionProducts,
isPayPalSubscription,
} from '../Helper/Subscription';
import {
createOrder,
createSubscription,
createVaultSetupToken,
handleApprove,
handleApproveSubscription,
onApproveSavePayment,
} from '../paypal-config';
const PAYPAL_GATEWAY_ID = 'ppcp-gateway';
const namespace = 'ppcpBlocksPaypalExpressButtons';
let registeredContext = false;
let paypalScriptPromise = null;
export const PayPalComponent = ( {
config,
onClick,
onClose,
onSubmit,
onError,
eventRegistration,
emitResponse,
activePaymentMethod,
shippingData,
isEditing,
fundingSource,
buttonAttributes,
} ) => {
const { onPaymentSetup, onCheckoutFail, onCheckoutValidation } =
eventRegistration;
const { responseTypes } = emitResponse;
const [ paypalOrder, setPaypalOrder ] = useState( null );
const [ continuationFilled, setContinuationFilled ] = useState( false );
const [ gotoContinuationOnError, setGotoContinuationOnError ] =
useState( false );
const [ paypalScriptLoaded, setPaypalScriptLoaded ] = useState( false );
if ( ! paypalScriptLoaded ) {
if ( ! paypalScriptPromise ) {
// for editor, since canMakePayment was not called
paypalScriptPromise = loadPayPalScript(
namespace,
config.scriptData
);
}
paypalScriptPromise.then( () => setPaypalScriptLoaded( true ) );
}
const methodId = fundingSource
? `${ config.id }-${ fundingSource }`
: config.id;
/**
* The block cart displays express checkout buttons. Those buttons are handled by the
* PAYPAL_GATEWAY_ID method on the server ("PayPal Smart Buttons").
*
* A possible bug in WooCommerce does not use the correct payment method ID for the express
* payment buttons inside the cart, but sends the ID of the _first_ active payment method.
*
* This function uses an internal WooCommerce dispatcher method to set the correct method ID.
*/
const enforcePaymentMethodForCart = () => {
// Do nothing, unless we're handling block cart express payment buttons.
if ( 'cart-block' !== config.scriptData.context ) {
return;
}
// Set the active payment method to PAYPAL_GATEWAY_ID.
wp.data
.dispatch( 'wc/store/payment' )
.__internalSetActivePaymentMethod( PAYPAL_GATEWAY_ID, {} );
};
useEffect( () => {
// fill the form if in continuation (for product or mini-cart buttons)
if ( continuationFilled || ! config.scriptData.continuation?.order ) {
return;
}
try {
const paypalAddresses = paypalOrderToWcAddresses(
config.scriptData.continuation.order
);
const wcAddresses = wp.data
.select( 'wc/store/cart' )
.getCustomerData();
const addresses = mergeWcAddress( wcAddresses, paypalAddresses );
wp.data
.dispatch( 'wc/store/cart' )
.setBillingAddress( addresses.billingAddress );
if ( shippingData.needsShipping ) {
wp.data
.dispatch( 'wc/store/cart' )
.setShippingAddress( addresses.shippingAddress );
}
} catch ( err ) {
// sometimes the PayPal address is missing, skip in this case.
console.error( err );
}
// this useEffect should run only once, but adding this in case of some kind of full re-rendering
setContinuationFilled( true );
}, [ shippingData, continuationFilled ] );
const getCheckoutRedirectUrl = () => {
const checkoutUrl = new URL( config.scriptData.redirect );
// sometimes some browsers may load some kind of cached version of the page,
// so adding a parameter to avoid that
checkoutUrl.searchParams.append(
'ppcp-continuation-redirect',
new Date().getTime().toString()
);
return checkoutUrl.toString();
};
useEffect( () => {
const unsubscribe = onCheckoutValidation( () => {
if ( config.scriptData.continuation ) {
return true;
}
if (
gotoContinuationOnError &&
wp.data.select( 'wc/store/validation' ).hasValidationErrors()
) {
location.href = getCheckoutRedirectUrl();
return { type: responseTypes.ERROR };
}
return true;
} );
return unsubscribe;
}, [ onCheckoutValidation, gotoContinuationOnError ] );
const handleClick = ( data, actions ) => {
if ( isEditing ) {
return actions.reject();
}
window.ppcpFundingSource = data.fundingSource;
onClick();
};
const shouldHandleShippingInPayPal = () => {
return shouldskipFinalConfirmation() && config.needShipping;
};
const shouldskipFinalConfirmation = () => {
if ( config.finalReviewEnabled ) {
return false;
}
return (
window.ppcpFundingSource !== 'venmo' ||
! config.scriptData.vaultingEnabled
);
};
let handleShippingOptionsChange = null;
let handleShippingAddressChange = null;
if ( shippingData.needsShipping && shouldHandleShippingInPayPal() ) {
handleShippingOptionsChange = async ( data, actions ) => {
try {
const shippingOptionId = data.selectedShippingOption?.id;
if ( shippingOptionId ) {
await wp.data
.dispatch( 'wc/store/cart' )
.selectShippingRate( shippingOptionId );
await shippingData.setSelectedRates( shippingOptionId );
}
const res = await fetch( config.ajax.update_shipping.endpoint, {
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify( {
nonce: config.ajax.update_shipping.nonce,
order_id: data.orderID,
} ),
} );
const json = await res.json();
if ( ! json.success ) {
throw new Error( json.data.message );
}
} catch ( e ) {
console.error( e );
actions.reject();
}
};
handleShippingAddressChange = async ( data, actions ) => {
try {
const address = paypalAddressToWc(
convertKeysToSnakeCase( data.shippingAddress )
);
await wp.data.dispatch( 'wc/store/cart' ).updateCustomerData( {
shipping_address: address,
} );
await shippingData.setShippingAddress( address );
const res = await fetch( config.ajax.update_shipping.endpoint, {
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify( {
nonce: config.ajax.update_shipping.nonce,
order_id: data.orderID,
} ),
} );
const json = await res.json();
if ( ! json.success ) {
throw new Error( json.data.message );
}
} catch ( e ) {
console.error( e );
actions.reject();
}
};
}
useEffect( () => {
if ( activePaymentMethod !== methodId ) {
return;
}
const unsubscribeProcessing = onPaymentSetup( () => {
if (
cartHasSubscriptionProducts( config.scriptData ) &&
config.scriptData.is_free_trial_cart
) {
return {
type: responseTypes.SUCCESS,
};
}
if ( config.scriptData.continuation ) {
return {
type: responseTypes.SUCCESS,
meta: {
paymentMethodData: {
paypal_order_id:
config.scriptData.continuation.order_id,
funding_source:
window.ppcpFundingSource ?? 'paypal',
},
},
};
}
const addresses = paypalOrderToWcAddresses( paypalOrder );
return {
type: responseTypes.SUCCESS,
meta: {
paymentMethodData: {
paypal_order_id: paypalOrder.id,
funding_source: window.ppcpFundingSource ?? 'paypal',
},
...addresses,
},
};
} );
return () => {
unsubscribeProcessing();
};
}, [ onPaymentSetup, paypalOrder, activePaymentMethod ] );
useEffect( () => {
if ( activePaymentMethod !== methodId ) {
return;
}
const unsubscribe = onCheckoutFail( ( { processingResponse } ) => {
console.error( processingResponse );
if ( onClose ) {
onClose();
}
if ( config.scriptData.continuation ) {
return true;
}
if ( shouldskipFinalConfirmation() ) {
location.href = getCheckoutRedirectUrl();
}
return true;
} );
return unsubscribe;
}, [ onCheckoutFail, onClose, activePaymentMethod ] );
if ( config.scriptData.continuation ) {
return (
<div
dangerouslySetInnerHTML={ {
__html: config.scriptData.continuation.cancel.html,
} }
></div>
);
}
if ( ! registeredContext ) {
buttonModuleWatcher.registerContextBootstrap(
config.scriptData.context,
{
createOrder: ( data ) => {
return createOrder( data, config, onError, onClose );
},
onApprove: ( data, actions ) => {
return handleApprove(
data,
actions,
config,
shouldHandleShippingInPayPal,
shippingData,
setPaypalOrder,
shouldskipFinalConfirmation,
getCheckoutRedirectUrl,
setGotoContinuationOnError,
enforcePaymentMethodForCart,
onSubmit,
onError,
onClose
);
},
}
);
registeredContext = true;
}
const style = normalizeStyleForFundingSource(
config.scriptData.button.style,
fundingSource
);
if ( typeof buttonAttributes !== 'undefined' ) {
style.height = buttonAttributes?.height
? Number( buttonAttributes.height )
: style.height;
style.borderRadius = buttonAttributes?.borderRadius
? Number( buttonAttributes.borderRadius )
: style.borderRadius;
}
if ( ! paypalScriptLoaded ) {
return null;
}
const PayPalButton = ppcpBlocksPaypalExpressButtons.Buttons.driver(
'react',
{ React, ReactDOM }
);
const getOnShippingOptionsChange = ( fundingSource ) => {
if ( fundingSource === 'venmo' ) {
return null;
}
return ( data, actions ) => {
shouldHandleShippingInPayPal()
? handleShippingOptionsChange( data, actions )
: null;
};
};
const getOnShippingAddressChange = ( fundingSource ) => {
if ( fundingSource === 'venmo' ) {
return null;
}
return ( data, actions ) => {
const shippingAddressChange = shouldHandleShippingInPayPal()
? handleShippingAddressChange( data, actions )
: null;
return shippingAddressChange;
};
};
if (
cartHasSubscriptionProducts( config.scriptData ) &&
config.scriptData.is_free_trial_cart
) {
return (
<PayPalButton
style={ style }
onClick={ handleClick }
onCancel={ onClose }
onError={ onClose }
createVaultSetupToken={ () => createVaultSetupToken( config ) }
onApprove={ ( { vaultSetupToken } ) =>
onApproveSavePayment( vaultSetupToken, config, onSubmit )
}
/>
);
}
if ( isPayPalSubscription( config.scriptData ) ) {
return (
<PayPalButton
fundingSource={ fundingSource }
style={ style }
onClick={ handleClick }
onCancel={ onClose }
onError={ onClose }
createSubscription={ ( data, actions ) =>
createSubscription( data, actions, config )
}
onApprove={ ( data, actions ) =>
handleApproveSubscription(
data,
actions,
config,
shouldHandleShippingInPayPal,
shippingData,
setPaypalOrder,
shouldskipFinalConfirmation,
getCheckoutRedirectUrl,
setGotoContinuationOnError,
enforcePaymentMethodForCart,
onSubmit,
onError,
onClose
)
}
onShippingOptionsChange={ getOnShippingOptionsChange(
fundingSource
) }
onShippingAddressChange={ getOnShippingAddressChange(
fundingSource
) }
/>
);
}
return (
<PayPalButton
fundingSource={ fundingSource }
style={ style }
onClick={ handleClick }
onCancel={ onClose }
onError={ onClose }
createOrder={ ( data ) =>
createOrder( data, config, onError, onClose )
}
onApprove={ ( data, actions ) =>
handleApprove(
data,
actions,
config,
shouldHandleShippingInPayPal,
shippingData,
setPaypalOrder,
shouldskipFinalConfirmation,
getCheckoutRedirectUrl,
setGotoContinuationOnError,
enforcePaymentMethodForCart,
onSubmit,
onError,
onClose
)
}
onShippingOptionsChange={ getOnShippingOptionsChange(
fundingSource
) }
onShippingAddressChange={ getOnShippingAddressChange(
fundingSource
) }
/>
);
};

View file

@ -1,750 +1,26 @@
import { useEffect, useState, useMemo } from '@wordpress/element';
import {
registerExpressPaymentMethod,
registerPaymentMethod,
} from '@woocommerce/blocks-registry';
import { __ } from '@wordpress/i18n';
import {
mergeWcAddress,
paypalAddressToWc,
paypalOrderToWcAddresses,
paypalSubscriptionToWcAddresses,
} from './Helper/Address';
import { convertKeysToSnakeCase } from './Helper/Helper';
import {
cartHasSubscriptionProducts,
isPayPalSubscription,
} from './Helper/Subscription';
import { loadPayPalScript } from '../../../ppcp-button/resources/js/modules/Helper/PayPalScriptLoading';
import { PayPalScriptProvider, PayPalButtons } from '@paypal/react-paypal-js';
import { normalizeStyleForFundingSource } from '../../../ppcp-button/resources/js/modules/Helper/Style';
import buttonModuleWatcher from '../../../ppcp-button/resources/js/modules/ButtonModuleWatcher';
import BlockCheckoutMessagesBootstrap from './Bootstrap/BlockCheckoutMessagesBootstrap';
import { PayPalComponent } from './Components/paypal';
import { BlockEditorPayPalComponent } from './Components/block-editor-paypal';
import { PaypalLabel } from './Components/paypal-label';
const namespace = 'ppcpBlocksPaypalExpressButtons';
const config = wc.wcSettings.getSetting( 'ppcp-gateway_data' );
window.ppcpFundingSource = config.fundingSource;
let registeredContext = false;
let paypalScriptPromise = null;
const PAYPAL_GATEWAY_ID = 'ppcp-gateway';
const PayPalComponent = ( {
onClick,
onClose,
onSubmit,
onError,
eventRegistration,
emitResponse,
activePaymentMethod,
shippingData,
isEditing,
fundingSource,
buttonAttributes,
} ) => {
const { onPaymentSetup, onCheckoutFail, onCheckoutValidation } =
eventRegistration;
const { responseTypes } = emitResponse;
const [ paypalOrder, setPaypalOrder ] = useState( null );
const [ continuationFilled, setContinuationFilled ] = useState( false );
const [ gotoContinuationOnError, setGotoContinuationOnError ] =
useState( false );
const [ paypalScriptLoaded, setPaypalScriptLoaded ] = useState( false );
if ( ! paypalScriptLoaded ) {
if ( ! paypalScriptPromise ) {
// for editor, since canMakePayment was not called
paypalScriptPromise = loadPayPalScript(
namespace,
config.scriptData
);
}
paypalScriptPromise.then( () => setPaypalScriptLoaded( true ) );
}
const methodId = fundingSource
? `${ config.id }-${ fundingSource }`
: config.id;
/**
* The block cart displays express checkout buttons. Those buttons are handled by the
* PAYPAL_GATEWAY_ID method on the server ("PayPal Smart Buttons").
*
* A possible bug in WooCommerce does not use the correct payment method ID for the express
* payment buttons inside the cart, but sends the ID of the _first_ active payment method.
*
* This function uses an internal WooCommerce dispatcher method to set the correct method ID.
*/
const enforcePaymentMethodForCart = () => {
// Do nothing, unless we're handling block cart express payment buttons.
if ( 'cart-block' !== config.scriptData.context ) {
return;
}
// Set the active payment method to PAYPAL_GATEWAY_ID.
wp.data
.dispatch( 'wc/store/payment' )
.__internalSetActivePaymentMethod( PAYPAL_GATEWAY_ID, {} );
};
useEffect( () => {
// fill the form if in continuation (for product or mini-cart buttons)
if ( continuationFilled || ! config.scriptData.continuation?.order ) {
return;
}
try {
const paypalAddresses = paypalOrderToWcAddresses(
config.scriptData.continuation.order
);
const wcAddresses = wp.data
.select( 'wc/store/cart' )
.getCustomerData();
const addresses = mergeWcAddress( wcAddresses, paypalAddresses );
wp.data
.dispatch( 'wc/store/cart' )
.setBillingAddress( addresses.billingAddress );
if ( shippingData.needsShipping ) {
wp.data
.dispatch( 'wc/store/cart' )
.setShippingAddress( addresses.shippingAddress );
}
} catch ( err ) {
// sometimes the PayPal address is missing, skip in this case.
console.log( err );
}
// this useEffect should run only once, but adding this in case of some kind of full re-rendering
setContinuationFilled( true );
}, [ shippingData, continuationFilled ] );
const createOrder = async ( data, actions ) => {
try {
const requestBody = {
nonce: config.scriptData.ajax.create_order.nonce,
bn_code: '',
context: config.scriptData.context,
payment_method: 'ppcp-gateway',
funding_source: window.ppcpFundingSource ?? 'paypal',
createaccount: false,
...( data?.paymentSource && {
payment_source: data.paymentSource,
} ),
};
const res = await fetch(
config.scriptData.ajax.create_order.endpoint,
{
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify( requestBody ),
}
);
const json = await res.json();
if ( ! json.success ) {
if ( json.data?.details?.length > 0 ) {
throw new Error(
json.data.details
.map( ( d ) => `${ d.issue } ${ d.description }` )
.join( '<br/>' )
);
} else if ( json.data?.message ) {
throw new Error( json.data.message );
}
throw new Error( config.scriptData.labels.error.generic );
}
return json.data.id;
} catch ( err ) {
console.error( err );
onError( err.message );
onClose();
throw err;
}
};
const createSubscription = async ( data, actions ) => {
let planId = config.scriptData.subscription_plan_id;
if (
config.scriptData
.variable_paypal_subscription_variation_from_cart !== ''
) {
planId =
config.scriptData
.variable_paypal_subscription_variation_from_cart;
}
return actions.subscription.create( {
plan_id: planId,
} );
};
const handleApproveSubscription = async ( data, actions ) => {
try {
const subscription = await actions.subscription.get();
if ( subscription ) {
const addresses =
paypalSubscriptionToWcAddresses( subscription );
const promises = [
// save address on server
wp.data.dispatch( 'wc/store/cart' ).updateCustomerData( {
billing_address: addresses.billingAddress,
shipping_address: addresses.shippingAddress,
} ),
];
if ( shouldHandleShippingInPayPal() ) {
// set address in UI
promises.push(
wp.data
.dispatch( 'wc/store/cart' )
.setBillingAddress( addresses.billingAddress )
);
if ( shippingData.needsShipping ) {
promises.push(
wp.data
.dispatch( 'wc/store/cart' )
.setShippingAddress( addresses.shippingAddress )
);
}
}
await Promise.all( promises );
}
setPaypalOrder( subscription );
const res = await fetch(
config.scriptData.ajax.approve_subscription.endpoint,
{
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify( {
nonce: config.scriptData.ajax.approve_subscription
.nonce,
order_id: data.orderID,
subscription_id: data.subscriptionID,
} ),
}
);
const json = await res.json();
if ( ! json.success ) {
if (
typeof actions !== 'undefined' &&
typeof actions.restart !== 'undefined'
) {
return actions.restart();
}
if ( json.data?.message ) {
throw new Error( json.data.message );
}
throw new Error( config.scriptData.labels.error.generic );
}
if ( ! shouldskipFinalConfirmation() ) {
location.href = getCheckoutRedirectUrl();
} else {
setGotoContinuationOnError( true );
enforcePaymentMethodForCart();
onSubmit();
}
} catch ( err ) {
console.error( err );
onError( err.message );
onClose();
throw err;
}
};
const getCheckoutRedirectUrl = () => {
const checkoutUrl = new URL( config.scriptData.redirect );
// sometimes some browsers may load some kind of cached version of the page,
// so adding a parameter to avoid that
checkoutUrl.searchParams.append(
'ppcp-continuation-redirect',
new Date().getTime().toString()
);
return checkoutUrl.toString();
};
const handleApprove = async ( data, actions ) => {
try {
const order = await actions.order.get();
if ( order ) {
const addresses = paypalOrderToWcAddresses( order );
const promises = [
// save address on server
wp.data.dispatch( 'wc/store/cart' ).updateCustomerData( {
billing_address: addresses.billingAddress,
shipping_address: addresses.shippingAddress,
} ),
];
if ( shouldHandleShippingInPayPal() ) {
// set address in UI
promises.push(
wp.data
.dispatch( 'wc/store/cart' )
.setBillingAddress( addresses.billingAddress )
);
if ( shippingData.needsShipping ) {
promises.push(
wp.data
.dispatch( 'wc/store/cart' )
.setShippingAddress( addresses.shippingAddress )
);
}
}
await Promise.all( promises );
}
setPaypalOrder( order );
const res = await fetch(
config.scriptData.ajax.approve_order.endpoint,
{
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify( {
nonce: config.scriptData.ajax.approve_order.nonce,
order_id: data.orderID,
funding_source: window.ppcpFundingSource ?? 'paypal',
} ),
}
);
const json = await res.json();
if ( ! json.success ) {
if (
typeof actions !== 'undefined' &&
typeof actions.restart !== 'undefined'
) {
return actions.restart();
}
if ( json.data?.message ) {
throw new Error( json.data.message );
}
throw new Error( config.scriptData.labels.error.generic );
}
if ( ! shouldskipFinalConfirmation() ) {
location.href = getCheckoutRedirectUrl();
} else {
setGotoContinuationOnError( true );
enforcePaymentMethodForCart();
onSubmit();
}
} catch ( err ) {
console.error( err );
onError( err.message );
onClose();
throw err;
}
};
useEffect( () => {
const unsubscribe = onCheckoutValidation( () => {
if ( config.scriptData.continuation ) {
return true;
}
if (
gotoContinuationOnError &&
wp.data.select( 'wc/store/validation' ).hasValidationErrors()
) {
location.href = getCheckoutRedirectUrl();
return { type: responseTypes.ERROR };
}
return true;
} );
return unsubscribe;
}, [ onCheckoutValidation, gotoContinuationOnError ] );
const handleClick = ( data, actions ) => {
if ( isEditing ) {
return actions.reject();
}
window.ppcpFundingSource = data.fundingSource;
onClick();
};
const shouldHandleShippingInPayPal = () => {
return shouldskipFinalConfirmation() && config.needShipping;
};
const shouldskipFinalConfirmation = () => {
if ( config.finalReviewEnabled ) {
return false;
}
return (
window.ppcpFundingSource !== 'venmo' ||
! config.scriptData.vaultingEnabled
);
};
let handleShippingOptionsChange = null;
let handleShippingAddressChange = null;
let handleSubscriptionShippingOptionsChange = null;
let handleSubscriptionShippingAddressChange = null;
if ( shippingData.needsShipping && shouldHandleShippingInPayPal() ) {
handleShippingOptionsChange = async ( data, actions ) => {
try {
const shippingOptionId = data.selectedShippingOption?.id;
if ( shippingOptionId ) {
await wp.data
.dispatch( 'wc/store/cart' )
.selectShippingRate( shippingOptionId );
await shippingData.setSelectedRates( shippingOptionId );
}
const res = await fetch( config.ajax.update_shipping.endpoint, {
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify( {
nonce: config.ajax.update_shipping.nonce,
order_id: data.orderID,
} ),
} );
const json = await res.json();
if ( ! json.success ) {
throw new Error( json.data.message );
}
} catch ( e ) {
console.error( e );
actions.reject();
}
};
handleShippingAddressChange = async ( data, actions ) => {
try {
const address = paypalAddressToWc(
convertKeysToSnakeCase( data.shippingAddress )
);
await wp.data.dispatch( 'wc/store/cart' ).updateCustomerData( {
shipping_address: address,
} );
await shippingData.setShippingAddress( address );
const res = await fetch( config.ajax.update_shipping.endpoint, {
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify( {
nonce: config.ajax.update_shipping.nonce,
order_id: data.orderID,
} ),
} );
const json = await res.json();
if ( ! json.success ) {
throw new Error( json.data.message );
}
} catch ( e ) {
console.error( e );
actions.reject();
}
};
handleSubscriptionShippingOptionsChange = async ( data, actions ) => {
try {
const shippingOptionId = data.selectedShippingOption?.id;
if ( shippingOptionId ) {
await wp.data
.dispatch( 'wc/store/cart' )
.selectShippingRate( shippingOptionId );
await shippingData.setSelectedRates( shippingOptionId );
}
} catch ( e ) {
console.error( e );
actions.reject();
}
};
handleSubscriptionShippingAddressChange = async ( data, actions ) => {
try {
const address = paypalAddressToWc(
convertKeysToSnakeCase( data.shippingAddress )
);
await wp.data.dispatch( 'wc/store/cart' ).updateCustomerData( {
shipping_address: address,
} );
await shippingData.setShippingAddress( address );
const res = await fetch( config.ajax.update_shipping.endpoint, {
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify( {
nonce: config.ajax.update_shipping.nonce,
order_id: data.orderID,
} ),
} );
const json = await res.json();
if ( ! json.success ) {
throw new Error( json.data.message );
}
} catch ( e ) {
console.error( e );
actions.reject();
}
};
}
useEffect( () => {
if ( activePaymentMethod !== methodId ) {
return;
}
const unsubscribeProcessing = onPaymentSetup( () => {
if ( config.scriptData.continuation ) {
return {
type: responseTypes.SUCCESS,
meta: {
paymentMethodData: {
paypal_order_id:
config.scriptData.continuation.order_id,
funding_source:
window.ppcpFundingSource ?? 'paypal',
},
},
};
}
const addresses = paypalOrderToWcAddresses( paypalOrder );
return {
type: responseTypes.SUCCESS,
meta: {
paymentMethodData: {
paypal_order_id: paypalOrder.id,
funding_source: window.ppcpFundingSource ?? 'paypal',
},
...addresses,
},
};
} );
return () => {
unsubscribeProcessing();
};
}, [ onPaymentSetup, paypalOrder, activePaymentMethod ] );
useEffect( () => {
if ( activePaymentMethod !== methodId ) {
return;
}
const unsubscribe = onCheckoutFail( ( { processingResponse } ) => {
console.error( processingResponse );
if ( onClose ) {
onClose();
}
if ( config.scriptData.continuation ) {
return true;
}
if ( shouldskipFinalConfirmation() ) {
location.href = getCheckoutRedirectUrl();
}
return true;
} );
return unsubscribe;
}, [ onCheckoutFail, onClose, activePaymentMethod ] );
if ( config.scriptData.continuation ) {
return (
<div
dangerouslySetInnerHTML={ {
__html: config.scriptData.continuation.cancel.html,
} }
></div>
);
}
if ( ! registeredContext ) {
buttonModuleWatcher.registerContextBootstrap(
config.scriptData.context,
{
createOrder: () => {
return createOrder();
},
onApprove: ( data, actions ) => {
return handleApprove( data, actions );
},
}
);
registeredContext = true;
}
const style = normalizeStyleForFundingSource(
config.scriptData.button.style,
fundingSource
);
if ( typeof buttonAttributes !== 'undefined' ) {
style.height = buttonAttributes?.height
? Number( buttonAttributes.height )
: style.height;
style.borderRadius = buttonAttributes?.borderRadius
? Number( buttonAttributes.borderRadius )
: style.borderRadius;
}
if ( ! paypalScriptLoaded ) {
return null;
}
const PayPalButton = ppcpBlocksPaypalExpressButtons.Buttons.driver(
'react',
{ React, ReactDOM }
);
const getOnShippingOptionsChange = ( fundingSource ) => {
if ( fundingSource === 'venmo' ) {
return null;
}
return ( data, actions ) => {
shouldHandleShippingInPayPal()
? handleShippingOptionsChange( data, actions )
: null;
};
};
const getOnShippingAddressChange = ( fundingSource ) => {
if ( fundingSource === 'venmo' ) {
return null;
}
return ( data, actions ) => {
const shippingAddressChange = shouldHandleShippingInPayPal()
? handleShippingAddressChange( data, actions )
: null;
return shippingAddressChange;
};
};
if ( isPayPalSubscription( config.scriptData ) ) {
return (
<PayPalButton
fundingSource={ fundingSource }
style={ style }
onClick={ handleClick }
onCancel={ onClose }
onError={ onClose }
createSubscription={ createSubscription }
onApprove={ handleApproveSubscription }
onShippingOptionsChange={ getOnShippingOptionsChange(
fundingSource
) }
onShippingAddressChange={ getOnShippingAddressChange(
fundingSource
) }
/>
);
}
return (
<PayPalButton
fundingSource={ fundingSource }
style={ style }
onClick={ handleClick }
onCancel={ onClose }
onError={ onClose }
createOrder={ createOrder }
onApprove={ handleApprove }
onShippingOptionsChange={ getOnShippingOptionsChange(
fundingSource
) }
onShippingAddressChange={ getOnShippingAddressChange(
fundingSource
) }
/>
);
};
const BlockEditorPayPalComponent = ( { fundingSource, buttonAttributes } ) => {
const urlParams = useMemo(
() => ( {
clientId: 'test',
...config.scriptData.url_params,
dataNamespace: 'ppcp-blocks-editor-paypal-buttons',
components: 'buttons',
} ),
[]
);
const style = useMemo( () => {
const configStyle = normalizeStyleForFundingSource(
config.scriptData.button.style,
fundingSource
);
if ( buttonAttributes ) {
return {
...configStyle,
height: buttonAttributes.height
? Number( buttonAttributes.height )
: configStyle.height,
borderRadius: buttonAttributes.borderRadius
? Number( buttonAttributes.borderRadius )
: configStyle.borderRadius,
};
}
return configStyle;
}, [ fundingSource, buttonAttributes ] );
return (
<PayPalScriptProvider options={ urlParams }>
<PayPalButtons
className={ `ppc-button-container-${ fundingSource }` }
fundingSource={ fundingSource }
style={ style }
forceReRender={ [ buttonAttributes || {} ] }
onClick={ () => false }
/>
</PayPalScriptProvider>
);
};
const features = [ 'products' ];
let block_enabled = true;
let blockEnabled = true;
if ( cartHasSubscriptionProducts( config.scriptData ) ) {
// Don't show buttons on block cart page if using vault v2 and user is not logged in
@ -754,7 +30,17 @@ if ( cartHasSubscriptionProducts( config.scriptData ) ) {
! isPayPalSubscription( config.scriptData ) && // using vaulting
! config.scriptData?.save_payment_methods?.id_token // not vault v3
) {
block_enabled = false;
blockEnabled = false;
}
// Don't show buttons on block cart page if user is not logged in and cart contains free trial product
if (
! config.scriptData.user.is_logged &&
config.scriptData.context === 'cart-block' &&
cartHasSubscriptionProducts( config.scriptData ) &&
config.scriptData.is_free_trial_cart
) {
blockEnabled = false;
}
// Don't render if vaulting disabled and is in vault subscription mode
@ -762,7 +48,7 @@ if ( cartHasSubscriptionProducts( config.scriptData ) ) {
! isPayPalSubscription( config.scriptData ) &&
! config.scriptData.can_save_vault_token
) {
block_enabled = false;
blockEnabled = false;
}
// Don't render buttons if in subscription mode and product not associated with a PayPal subscription
@ -770,13 +56,21 @@ if ( cartHasSubscriptionProducts( config.scriptData ) ) {
isPayPalSubscription( config.scriptData ) &&
! config.scriptData.subscription_product_allowed
) {
block_enabled = false;
blockEnabled = false;
}
// Don't show buttons if cart contains free trial product and the stroe is not eligible for saving payment methods.
if (
! config.scriptData.vault_v3_enabled &&
config.scriptData.is_free_trial_cart
) {
blockEnabled = false;
}
features.push( 'subscriptions' );
}
if ( block_enabled ) {
if ( blockEnabled ) {
if ( config.placeOrderEnabled && ! config.scriptData.continuation ) {
let descriptionElement = (
<div
@ -802,21 +96,6 @@ if ( block_enabled ) {
);
}
const PaypalLabel = ( { components, config } ) => {
const { PaymentMethodIcons } = components;
return (
<>
<span
dangerouslySetInnerHTML={ {
__html: config.title,
} }
/>
<PaymentMethodIcons icons={ config.icon } align="right" />
</>
);
};
registerPaymentMethod( {
name: config.id,
label: <PaypalLabel config={ config } />,
@ -837,8 +116,13 @@ if ( block_enabled ) {
registerPaymentMethod( {
name: config.id,
label: <div dangerouslySetInnerHTML={ { __html: config.title } } />,
content: <PayPalComponent isEditing={ false } />,
edit: <BlockEditorPayPalComponent fundingSource={ 'paypal' } />,
content: <PayPalComponent config={ config } isEditing={ false } />,
edit: (
<BlockEditorPayPalComponent
config={ config }
fundingSource={ 'paypal' }
/>
),
ariaLabel: config.title,
canMakePayment: () => {
return true;
@ -848,10 +132,11 @@ if ( block_enabled ) {
},
} );
} else if ( config.smartButtonsEnabled ) {
for ( const fundingSource of [
'paypal',
...config.enabledFundingSources,
] ) {
const fundingSources = config.scriptData.is_free_trial_cart
? [ 'paypal' ]
: [ 'paypal', ...config.enabledFundingSources ];
for ( const fundingSource of fundingSources ) {
registerExpressPaymentMethod( {
name: `${ config.id }-${ fundingSource }`,
title: 'PayPal',
@ -866,12 +151,14 @@ if ( block_enabled ) {
),
content: (
<PayPalComponent
config={ config }
isEditing={ false }
fundingSource={ fundingSource }
/>
),
edit: (
<BlockEditorPayPalComponent
config={ config }
fundingSource={ fundingSource }
/>
),

View file

@ -0,0 +1,316 @@
import {
paypalOrderToWcAddresses,
paypalSubscriptionToWcAddresses,
} from './Helper/Address';
export const createOrder = async ( data, config, onError, onClose ) => {
try {
const requestBody = {
nonce: config.scriptData.ajax.create_order.nonce,
bn_code: '',
context: config.scriptData.context,
payment_method: 'ppcp-gateway',
funding_source: window.ppcpFundingSource ?? 'paypal',
createaccount: false,
...( data?.paymentSource && {
payment_source: data.paymentSource,
} ),
};
const res = await fetch( config.scriptData.ajax.create_order.endpoint, {
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify( requestBody ),
} );
const json = await res.json();
if ( ! json.success ) {
if ( json.data?.details?.length > 0 ) {
throw new Error(
json.data.details
.map( ( d ) => `${ d.issue } ${ d.description }` )
.join( '<br/>' )
);
} else if ( json.data?.message ) {
throw new Error( json.data.message );
}
throw new Error( config.scriptData.labels.error.generic );
}
return json.data.id;
} catch ( err ) {
console.error( err );
onError( err.message );
onClose();
throw err;
}
};
export const handleApprove = async (
data,
actions,
config,
shouldHandleShippingInPayPal,
shippingData,
setPaypalOrder,
shouldskipFinalConfirmation,
getCheckoutRedirectUrl,
setGotoContinuationOnError,
enforcePaymentMethodForCart,
onSubmit,
onError,
onClose
) => {
try {
const order = await actions.order.get();
if ( order ) {
const addresses = paypalOrderToWcAddresses( order );
const promises = [
// save address on server
wp.data.dispatch( 'wc/store/cart' ).updateCustomerData( {
billing_address: addresses.billingAddress,
shipping_address: addresses.shippingAddress,
} ),
];
if ( shouldHandleShippingInPayPal() ) {
// set address in UI
promises.push(
wp.data
.dispatch( 'wc/store/cart' )
.setBillingAddress( addresses.billingAddress )
);
if ( shippingData.needsShipping ) {
promises.push(
wp.data
.dispatch( 'wc/store/cart' )
.setShippingAddress( addresses.shippingAddress )
);
}
}
await Promise.all( promises );
}
setPaypalOrder( order );
const res = await fetch(
config.scriptData.ajax.approve_order.endpoint,
{
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify( {
nonce: config.scriptData.ajax.approve_order.nonce,
order_id: data.orderID,
funding_source: window.ppcpFundingSource ?? 'paypal',
} ),
}
);
const json = await res.json();
if ( ! json.success ) {
if (
typeof actions !== 'undefined' &&
typeof actions.restart !== 'undefined'
) {
return actions.restart();
}
if ( json.data?.message ) {
throw new Error( json.data.message );
}
throw new Error( config.scriptData.labels.error.generic );
}
if ( ! shouldskipFinalConfirmation() ) {
location.href = getCheckoutRedirectUrl();
} else {
setGotoContinuationOnError( true );
enforcePaymentMethodForCart();
onSubmit();
}
} catch ( err ) {
console.error( err );
onError( err.message );
onClose();
throw err;
}
};
export const createSubscription = async ( data, actions, config ) => {
let planId = config.scriptData.subscription_plan_id;
if (
config.scriptData.variable_paypal_subscription_variation_from_cart !==
''
) {
planId =
config.scriptData.variable_paypal_subscription_variation_from_cart;
}
return actions.subscription.create( {
plan_id: planId,
} );
};
export const handleApproveSubscription = async (
data,
actions,
config,
shouldHandleShippingInPayPal,
shippingData,
setPaypalOrder,
shouldskipFinalConfirmation,
getCheckoutRedirectUrl,
setGotoContinuationOnError,
enforcePaymentMethodForCart,
onSubmit,
onError,
onClose
) => {
try {
const subscription = await actions.subscription.get();
if ( subscription ) {
const addresses = paypalSubscriptionToWcAddresses( subscription );
const promises = [
// save address on server
wp.data.dispatch( 'wc/store/cart' ).updateCustomerData( {
billing_address: addresses.billingAddress,
shipping_address: addresses.shippingAddress,
} ),
];
if ( shouldHandleShippingInPayPal() ) {
// set address in UI
promises.push(
wp.data
.dispatch( 'wc/store/cart' )
.setBillingAddress( addresses.billingAddress )
);
if ( shippingData.needsShipping ) {
promises.push(
wp.data
.dispatch( 'wc/store/cart' )
.setShippingAddress( addresses.shippingAddress )
);
}
}
await Promise.all( promises );
}
setPaypalOrder( subscription );
const res = await fetch(
config.scriptData.ajax.approve_subscription.endpoint,
{
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify( {
nonce: config.scriptData.ajax.approve_subscription.nonce,
order_id: data.orderID,
subscription_id: data.subscriptionID,
} ),
}
);
const json = await res.json();
if ( ! json.success ) {
if (
typeof actions !== 'undefined' &&
typeof actions.restart !== 'undefined'
) {
return actions.restart();
}
if ( json.data?.message ) {
throw new Error( json.data.message );
}
throw new Error( config.scriptData.labels.error.generic );
}
if ( ! shouldskipFinalConfirmation() ) {
location.href = getCheckoutRedirectUrl();
} else {
setGotoContinuationOnError( true );
enforcePaymentMethodForCart();
onSubmit();
}
} catch ( err ) {
console.error( err );
onError( err.message );
onClose();
throw err;
}
};
export const createVaultSetupToken = async ( config ) => {
return fetch( config.scriptData.ajax.create_setup_token.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify( {
nonce: config.scriptData.ajax.create_setup_token.nonce,
payment_method: 'ppcp-gateway',
} ),
} )
.then( ( response ) => response.json() )
.then( ( result ) => {
return result.data.id;
} )
.catch( ( err ) => {
console.error( err );
} );
};
export const onApproveSavePayment = async (
vaultSetupToken,
config,
onSubmit
) => {
let endpoint =
config.scriptData.ajax.create_payment_token_for_guest.endpoint;
let bodyContent = {
nonce: config.scriptData.ajax.create_payment_token_for_guest.nonce,
vault_setup_token: vaultSetupToken,
};
if ( config.scriptData.user.is_logged_in ) {
endpoint = config.scriptData.ajax.create_payment_token.endpoint;
bodyContent = {
nonce: config.scriptData.ajax.create_payment_token.nonce,
vault_setup_token: vaultSetupToken,
is_free_trial_cart: config.scriptData.is_free_trial_cart,
};
}
const response = await fetch( endpoint, {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify( bodyContent ),
} );
const result = await response.json();
if ( result.success === true ) {
onSubmit();
}
console.error( result );
};

View file

@ -97,11 +97,16 @@ class AdvancedCardPaymentMethod extends AbstractPaymentMethodType {
wp_register_script(
'ppcp-advanced-card-checkout-block',
trailingslashit( $this->module_url ) . 'assets/js/advanced-card-checkout-block.js',
array(),
array( 'wp-i18n' ),
$this->version,
true
);
wp_set_script_translations(
'ppcp-advanced-card-checkout-block',
'woocommerce-paypal-payments'
);
return array( 'ppcp-advanced-card-checkout-block' );
}

View file

@ -1910,7 +1910,7 @@ document.querySelector("#payment").before(document.querySelector(".ppcp-messages
$in_stock = $product->is_in_stock();
if ( $product->is_type( 'variable' ) ) {
if ( ! $in_stock && $product->is_type( 'variable' ) ) {
/**
* The method is defined in WC_Product_Variable class.
*

View file

@ -345,6 +345,10 @@ window.ppcp_onboarding_productionCallback = function ( ...args ) {
const sandboxSwitchElement = document.querySelector( '#ppcp-sandbox_on' );
sandboxSwitchElement?.addEventListener( 'click', () => {
document.querySelector( '.woocommerce-save-button' )?.removeAttribute( 'disabled' );
});
const validate = () => {
const selectors = sandboxSwitchElement.checked
? sandboxCredentialElementsSelectors

View file

@ -96,7 +96,7 @@ class CreatePaymentToken implements EndpointInterface {
$customer_id = get_user_meta( get_current_user_id(), '_ppcp_target_customer_id', true );
$result = $this->payment_method_tokens_endpoint->create_payment_token( $payment_source, $customer_id );
$result = $this->payment_method_tokens_endpoint->create_payment_token( $payment_source, (string) $customer_id );
if ( is_user_logged_in() && isset( $result->customer->id ) ) {
$current_user_id = get_current_user_id();

View file

@ -105,7 +105,7 @@ class CreateSetupToken implements EndpointInterface {
$customer_id = get_user_meta( get_current_user_id(), '_ppcp_target_customer_id', true );
$result = $this->payment_method_tokens_endpoint->setup_tokens( $payment_source, $customer_id );
$result = $this->payment_method_tokens_endpoint->setup_tokens( $payment_source, (string) $customer_id );
wp_send_json_success( $result );
return true;

View file

@ -66,23 +66,33 @@ class SavePaymentMethodsModule implements ServiceModule, ExtendingModule, Execut
add_action(
'woocommerce_paypal_payments_gateway_migrate_on_update',
function() use ( $c ) {
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
$billing_agreements_endpoint = $c->get( 'api.endpoint.billing-agreements' );
assert( $billing_agreements_endpoint instanceof BillingAgreementsEndpoint );
$reference_transaction_enabled = $billing_agreements_endpoint->reference_transaction_enabled();
if ( $reference_transaction_enabled !== true ) {
$c->get( 'wcgateway.settings' )->set( 'vault_enabled', false );
$c->get( 'wcgateway.settings' )->persist();
$settings->set( 'vault_enabled', false );
$settings->persist();
}
}
);
add_action(
'after_setup_theme',
function () use ( $c ) {
$settings = $c->get( 'wcgateway.settings' );
if (
( ! $settings->has( 'vault_enabled' ) || ! $settings->get( 'vault_enabled' ) )
&& ( ! $settings->has( 'vault_enabled_dcc' ) || ! $settings->get( 'vault_enabled_dcc' ) )
) {
return true;
}
add_filter(
'woocommerce_paypal_payments_localized_script_data',
function ( array $localized_script_data ) use ( $c ) {
if ( ! self::vault_enabled( $c ) ) {
return $localized_script_data;
}
$subscriptions_helper = $c->get( 'wc-subscriptions.helper' );
assert( $subscriptions_helper instanceof SubscriptionHelper );
if ( ! is_user_logged_in() && ! $subscriptions_helper->cart_contains_subscription() ) {
@ -103,10 +113,13 @@ class SavePaymentMethodsModule implements ServiceModule, ExtendingModule, Execut
add_filter(
'ppcp_create_order_request_body_data',
function ( array $data, string $payment_method, array $request_data ) use ( $c ): array {
if ( ! self::vault_enabled( $c ) ) {
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
if ( $payment_method === CreditCardGateway::ID ) {
if ( ! $settings->has( 'vault_enabled_dcc' ) || ! $settings->get( 'vault_enabled_dcc' ) ) {
return $data;
}
if ( $payment_method === CreditCardGateway::ID ) {
$save_payment_method = $request_data['save_payment_method'] ?? false;
if ( $save_payment_method ) {
$data['payment_source'] = array(
@ -133,6 +146,10 @@ class SavePaymentMethodsModule implements ServiceModule, ExtendingModule, Execut
}
if ( $payment_method === PayPalGateway::ID ) {
if ( ! $settings->has( 'vault_enabled' ) || ! $settings->get( 'vault_enabled' ) ) {
return $data;
}
$funding_source = $request_data['funding_source'] ?? null;
if ( $funding_source && $funding_source === 'venmo' ) {
@ -185,9 +202,6 @@ class SavePaymentMethodsModule implements ServiceModule, ExtendingModule, Execut
add_action(
'woocommerce_paypal_payments_after_order_processor',
function ( WC_Order $wc_order, Order $order ) use ( $c ) {
if ( ! self::vault_enabled( $c ) ) {
return;
}
$payment_source = $order->payment_source();
assert( $payment_source instanceof PaymentSource );
@ -250,30 +264,13 @@ class SavePaymentMethodsModule implements ServiceModule, ExtendingModule, Execut
2
);
add_filter(
'woocommerce_paypal_payments_disable_add_payment_method',
function ( bool $value ) use ( $c ): bool {
if ( ! self::vault_enabled( $c ) ) {
return $value;
}
return false;
}
);
add_filter(
'woocommerce_paypal_payments_should_render_card_custom_fields',
function ( bool $value ) use ( $c ): bool {
if ( ! self::vault_enabled( $c ) ) {
return $value;
}
return false;
}
);
add_filter( 'woocommerce_paypal_payments_disable_add_payment_method', '__return_false' );
add_filter( 'woocommerce_paypal_payments_should_render_card_custom_fields', '__return_false' );
add_action(
'wp_enqueue_scripts',
function () use ( $c ) {
if ( ! is_user_logged_in() || ! ( $this->is_add_payment_method_page() || $this->is_subscription_change_payment_method_page() ) || ! self::vault_enabled( $c ) ) {
if ( ! is_user_logged_in() || ! ( $this->is_add_payment_method_page() || $this->is_subscription_change_payment_method_page() ) ) {
return;
}
@ -363,8 +360,8 @@ class SavePaymentMethodsModule implements ServiceModule, ExtendingModule, Execut
add_action(
'woocommerce_add_payment_method_form_bottom',
function () use ( $c ) {
if ( ! is_user_logged_in() || ! is_add_payment_method_page() || ! self::vault_enabled( $c ) ) {
function () {
if ( ! is_user_logged_in() || ! is_add_payment_method_page() ) {
return;
}
@ -375,9 +372,6 @@ class SavePaymentMethodsModule implements ServiceModule, ExtendingModule, Execut
add_action(
'wc_ajax_' . CreateSetupToken::ENDPOINT,
static function () use ( $c ) {
if ( ! self::vault_enabled( $c ) ) {
return;
}
$endpoint = $c->get( 'save-payment-methods.endpoint.create-setup-token' );
assert( $endpoint instanceof CreateSetupToken );
@ -388,9 +382,6 @@ class SavePaymentMethodsModule implements ServiceModule, ExtendingModule, Execut
add_action(
'wc_ajax_' . CreatePaymentToken::ENDPOINT,
static function () use ( $c ) {
if ( ! self::vault_enabled( $c ) ) {
return;
}
$endpoint = $c->get( 'save-payment-methods.endpoint.create-payment-token' );
assert( $endpoint instanceof CreatePaymentToken );
@ -401,9 +392,6 @@ class SavePaymentMethodsModule implements ServiceModule, ExtendingModule, Execut
add_action(
'wc_ajax_' . CreatePaymentTokenForGuest::ENDPOINT,
static function () use ( $c ) {
if ( ! self::vault_enabled( $c ) ) {
return;
}
$endpoint = $c->get( 'save-payment-methods.endpoint.create-payment-token-for-guest' );
assert( $endpoint instanceof CreatePaymentTokenForGuest );
@ -414,9 +402,6 @@ class SavePaymentMethodsModule implements ServiceModule, ExtendingModule, Execut
add_action(
'woocommerce_paypal_payments_before_delete_payment_token',
function( string $token_id ) use ( $c ) {
if ( ! self::vault_enabled( $c ) ) {
return;
}
try {
$endpoint = $c->get( 'api.endpoint.payment-tokens' );
assert( $endpoint instanceof PaymentTokensEndpoint );
@ -439,11 +424,13 @@ class SavePaymentMethodsModule implements ServiceModule, ExtendingModule, Execut
add_filter(
'woocommerce_paypal_payments_credit_card_gateway_supports',
function( array $supports ) use ( $c ): array {
if ( ! self::vault_enabled( $c ) ) {
return $supports;
}
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof ContainerInterface );
if ( $settings->has( 'vault_enabled_dcc' ) && $settings->get( 'vault_enabled_dcc' ) ) {
$supports[] = 'tokenization';
$supports[] = 'add_payment_method';
}
return $supports;
}
@ -451,13 +438,12 @@ class SavePaymentMethodsModule implements ServiceModule, ExtendingModule, Execut
add_filter(
'woocommerce_paypal_payments_save_payment_methods_eligible',
function( bool $value ) use ( $c ): bool {
if ( ! self::vault_enabled( $c ) ) {
return $value;
}
function() {
return true;
}
);
}
);
return true;
}
@ -502,24 +488,4 @@ class SavePaymentMethodsModule implements ServiceModule, ExtendingModule, Execut
return $localized_script_data;
}
/**
* Checks whether the vault functionality is enabled based on configuration settings.
*
* @param ContainerInterface $container The dependency injection container from which settings can be retrieved.
*
* @return bool Returns true if either 'vault_enabled' or 'vault_enabled_dcc' settings are enabled; otherwise, false.
*/
private static function vault_enabled( ContainerInterface $container ): bool {
$settings = $container->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
if (
( ! $settings->has( 'vault_enabled' ) || ! $settings->get( 'vault_enabled' ) )
&& ( ! $settings->has( 'vault_enabled_dcc' ) || ! $settings->get( 'vault_enabled_dcc' ) )
) {
return false;
}
return true;
}
}

View file

@ -11,10 +11,6 @@
}
// Todo List and Feature Items
.ppcp-r-tab-overview-todo {
margin: 0 0 48px 0;
}
.ppcp-r-todo-item {
position: relative;
display: flex;
@ -101,6 +97,8 @@
span {
font-weight: 500;
}
margin-top:24px;
}
}
@ -231,6 +229,10 @@
}
// Settings Card and Block Styles
.ppcp-r-settings-card {
margin: 0 0 48px 0;
}
.ppcp-r-settings-card__content {
> .ppcp-r-settings-block {
&:not(:last-child) {

View file

@ -1 +1,3 @@
export { default as openSignup } from './Icons/open-signup';
export const NOTIFICATION_SUCCESS = '✔️';
export const NOTIFICATION_ERROR = '❌';

View file

@ -5,26 +5,29 @@ import { countryPriceInfo } from '../../utils/countryPriceInfo';
import { formatPrice } from '../../utils/formatPrice';
import TitleBadge, { TITLE_BADGE_INFO } from './TitleBadge';
const getFixedAmount = ( currency, priceList ) => {
const getFixedAmount = ( currency, priceList, itemFixedAmount ) => {
if ( priceList[ currency ] ) {
return formatPrice( priceList[ currency ], currency );
const sum = priceList[ currency ] + itemFixedAmount;
return formatPrice( sum, currency );
}
const [ defaultCurrency, defaultPrice ] = Object.entries( priceList )[ 0 ];
return formatPrice( defaultPrice, defaultCurrency );
const sum = defaultPrice + itemFixedAmount;
return formatPrice( sum, defaultCurrency );
};
const PricingTitleBadge = ( { item } ) => {
const { storeCountry } = CommonHooks.useWooSettings();
const { storeCountry, storeCurrency } = CommonHooks.useWooSettings();
const infos = countryPriceInfo[ storeCountry ];
const itemKey = item.split(' ')[0]; // Extract the first word, fastlane has more than one
if ( ! infos || ! infos[ item ] ) {
if ( ! infos || ! infos[ itemKey ] ) {
return null;
}
const percentage = infos[ item ].toFixed( 2 );
const fixedAmount = getFixedAmount( storeCountry, infos.fixedFee );
const percentage = typeof infos[itemKey] === 'number' ? infos[itemKey].toFixed(2) : infos[itemKey]['percentage'].toFixed(2);
const itemFixedAmount = infos[itemKey]['fixedFee'] ? infos[itemKey]['fixedFee'] : 0;
const fixedAmount = getFixedAmount( storeCurrency, infos.fixedFee, itemFixedAmount );
const label = sprintf(
__( 'from %1$s%% + %2$s', 'woocommerce-paypal-payments' ),

View file

@ -1,6 +1,6 @@
import { Button } from '@wordpress/components';
import SettingsBlock from './SettingsBlock';
import { Header, Title, Action, Description } from './SettingsBlockElements';
import { Action, Description, Header, Title } from './SettingsBlockElements';
const ButtonSettingsBlock = ( { title, description, ...props } ) => (
<SettingsBlock { ...props } className="ppcp-r-settings-block__button">
@ -10,6 +10,7 @@ const ButtonSettingsBlock = ( { title, description, ...props } ) => (
</Header>
<Action>
<Button
isBusy={ props.actionProps?.isBusy }
variant={ props.actionProps?.buttonType }
onClick={
props.actionProps?.callback

View file

@ -37,6 +37,7 @@ const FeatureSettingsBlock = ( { title, description, ...props } ) => {
<div className="ppcp-r-feature-item__buttons">
{ props.actionProps?.buttons.map( ( button ) => (
<Button
className={ button.class ? button.class : '' }
href={ button.url }
key={ button.text }
variant={ button.type }

View file

@ -53,6 +53,9 @@ const AcdcFlow = ( { isFastlane, isPayLater, storeCountry } ) => {
imageBadge={ [
'icon-payment-method-paypal-small.svg',
] }
textBadge={
<PricingTitleBadge item="plater" />
}
description={ sprintf(
// translators: %s: Link to PayPal business fees guide
__(

View file

@ -72,16 +72,16 @@ const TabOverview = () => {
className="ppcp-r-tab-overview-features"
title={ __( 'Features', 'woocommerce-paypal-payments' ) }
description={
<div>
<>
<p>
{ __(
'Enable additional features',
'Enable additional features and capabilities on your WooCommerce store.',
'woocommerce-paypal-payments'
) }
</p>
<p>
{ __(
'Click Refresh',
'Click Refresh to update your current features after making changes.',
'woocommerce-paypal-payments'
) }
</p>
@ -101,7 +101,7 @@ const TabOverview = () => {
'woocommerce-paypal-payments'
) }
</Button>
</div>
</>
}
contentItems={ features.map( ( feature ) => (
<FeatureSettingsBlock
@ -125,6 +125,60 @@ const TabOverview = () => {
/>
) ) }
/>
<SettingsCard
className="ppcp-r-tab-overview-help"
title={ __( 'Help Center', 'woocommerce-paypal-payments' ) }
description={ __(
'Access detailed guides and responsive support to streamline setup and enhance your experience.',
'woocommerce-paypal-payments'
) }
contentItems={ [
<FeatureSettingsBlock
key="documentation"
title={ __(
'Documentation',
'woocommerce-paypal-payments'
) }
description={ __(
'Find detailed guides and resources to help you set up, manage, and optimize your PayPal integration.',
'woocommerce-paypal-payments'
) }
actionProps={ {
buttons: [
{
type: 'tertiary',
text: __(
'View full documentation',
'woocommerce-paypal-payments'
),
url: '#',
},
],
} }
/>,
<FeatureSettingsBlock
key="support"
title={ __( 'Support', 'woocommerce-paypal-payments' ) }
description={ __(
'Need help? Access troubleshooting tips or contact our support team for personalized assistance.',
'woocommerce-paypal-payments'
) }
actionProps={ {
buttons: [
{
type: 'tertiary',
text: __(
'View support options',
'woocommerce-paypal-payments'
),
url: '#',
},
],
} }
/>,
] }
/>
</div>
);
};
@ -177,6 +231,7 @@ const featuresDefault = [
buttons: [
{
type: 'secondary',
class: 'small-button',
text: __( 'Configure', 'woocommerce-paypal-payments' ),
url: '#',
},
@ -200,6 +255,7 @@ const featuresDefault = [
buttons: [
{
type: 'secondary',
class: 'small-button',
text: __( 'Configure', 'woocommerce-paypal-payments' ),
url: '#',
},
@ -223,6 +279,7 @@ const featuresDefault = [
buttons: [
{
type: 'secondary',
class: 'small-button',
text: __( 'Apply', 'woocommerce-paypal-payments' ),
url: '#',
},
@ -243,6 +300,7 @@ const featuresDefault = [
buttons: [
{
type: 'secondary',
class: 'small-button',
text: __( 'Configure', 'woocommerce-paypal-payments' ),
url: '#',
},
@ -252,9 +310,6 @@ const featuresDefault = [
url: '#',
},
],
notes: [
__( '¹PayPal Q2 Earnings-2021.', 'woocommerce-paypal-payments' ),
],
},
{
id: 'apple_pay',
@ -266,6 +321,7 @@ const featuresDefault = [
buttons: [
{
type: 'secondary',
class: 'small-button',
text: __(
'Domain registration',
'woocommerce-paypal-payments'
@ -289,6 +345,7 @@ const featuresDefault = [
buttons: [
{
type: 'secondary',
class: 'small-button',
text: __( 'Configure', 'woocommerce-paypal-payments' ),
url: '#',
},
@ -298,6 +355,9 @@ const featuresDefault = [
url: '#',
},
],
notes: [
__( '¹PayPal Q2 Earnings-2021.', 'woocommerce-paypal-payments' ),
],
},
];

View file

@ -1,168 +0,0 @@
import { __ } from '@wordpress/i18n';
import {
Header,
Title,
Description,
AccordionSettingsBlock,
ToggleSettingsBlock,
ButtonSettingsBlock,
} from '../../../../ReusableComponents/SettingsBlocks';
import SettingsBlock from '../../../../ReusableComponents/SettingsBlocks/SettingsBlock';
const Troubleshooting = ( { updateFormValue, settings } ) => {
return (
<AccordionSettingsBlock
className="ppcp-r-settings-block--troubleshooting"
title={ __( 'Troubleshooting', 'woocommerce-paypal-payments' ) }
description={ __(
'Access tools to help debug and resolve issues.',
'woocommerce-paypal-payments'
) }
actionProps={ {
callback: updateFormValue,
key: 'payNowExperience',
value: settings.payNowExperience,
} }
>
<ToggleSettingsBlock
title={ __( 'Logging', 'woocommerce-paypal-payments' ) }
description={ __(
'Log additional debugging information in the WooCommerce logs that can assist technical staff to determine issues.',
'woocommerce-paypal-payments'
) }
actionProps={ {
callback: updateFormValue,
key: 'logging',
value: settings.logging,
} }
/>
<SettingsBlock>
<Header>
<Title>
{ __(
'Subscribed PayPal webhooks',
'woocommerce-paypal-payments'
) }
</Title>
<Description>
{ __(
'The following PayPal webhooks are subscribed. More information about the webhooks is available in the',
'woocommerce-paypal-payments'
) }{ ' ' }
<a href="https://woocommerce.com/document/woocommerce-paypal-payments/#webhook-status">
{ __(
'Webhook Status documentation',
'woocommerce-paypal-payments'
) }
</a>
.
</Description>
</Header>
<HooksTable data={ hooksExampleData() } />
</SettingsBlock>
<ButtonSettingsBlock
title={ __(
'Resubscribe webhooks',
'woocommerce-paypal-payments'
) }
description={ __(
'Click to remove the current webhook subscription and subscribe again, for example, if the website domain or URL structure changed.',
'woocommerce-paypal-payments'
) }
actionProps={ {
buttonType: 'secondary',
callback: () =>
console.log(
'Resubscribe webhooks',
'woocommerce-paypal-payments'
),
value: __(
'Resubscribe webhooks',
'woocommerce-paypal-payments'
),
} }
/>
<ButtonSettingsBlock
title={ __(
'Simulate webhooks',
'woocommerce-paypal-payments'
) }
actionProps={ {
buttonType: 'secondary',
callback: () =>
console.log(
'Simulate webhooks',
'woocommerce-paypal-payments'
),
value: __(
'Simulate webhooks',
'woocommerce-paypal-payments'
),
} }
/>
</AccordionSettingsBlock>
);
};
const hooksExampleData = () => {
return {
url: 'https://www.rt3.tech/wordpress/paypal-ux-testin/index.php?rest_route=/paypal/v1/incoming',
hooks: [
'billing plan pricing-change activated',
'billing plan updated',
'billing subscription cancelled',
'catalog product updated',
'checkout order approved',
'checkout order completed',
'checkout payment-approval reversed',
'payment authorization voided',
'payment capture completed',
'payment capture denied',
'payment capture pending',
'payment capture refunded',
'payment capture reversed',
'payment order cancelled',
'payment sale completed',
'payment sale refunded',
'vault payment-token created',
'vault payment-token deleted',
],
};
};
const HooksTable = ( { data } ) => {
return (
<table className="ppcp-r-table">
<thead>
<tr>
<th className="ppcp-r-table__hooks-url">
{ __( 'URL', 'woocommerce-paypal-payments' ) }
</th>
<th className="ppcp-r-table__hooks-events">
{ __(
'Tracked events',
'woocommerce-paypal-payments'
) }
</th>
</tr>
</thead>
<tbody>
<tr>
<td className="ppcp-r-table__hooks-url">{ data?.url }</td>
<td className="ppcp-r-table__hooks-events">
{ data.hooks.map( ( hook, index ) => (
<span key={ hook }>
{ hook }{ ' ' }
{ index !== data.hooks.length - 1 && ',' }
</span>
) ) }
</td>
</tr>
</tbody>
</table>
);
};
export default Troubleshooting;

View file

@ -0,0 +1,37 @@
import { __ } from '@wordpress/i18n';
import { CommonHooks } from '../../../../../../data';
const HooksTableBlock = () => {
const { webhooks } = CommonHooks.useWebhooks();
return (
<table className="ppcp-r-table">
<thead>
<tr>
<th className="ppcp-r-table__hooks-url">
{ __( 'URL', 'woocommerce-paypal-payments' ) }
</th>
<th className="ppcp-r-table__hooks-events">
{ __(
'Tracked events',
'woocommerce-paypal-payments'
) }
</th>
</tr>
</thead>
<tbody>
<tr>
<td className="ppcp-r-table__hooks-url">
{ webhooks?.url }
</td>
<td
className="ppcp-r-table__hooks-events"
dangerouslySetInnerHTML={ { __html: webhooks?.events } }
></td>
</tr>
</tbody>
</table>
);
};
export default HooksTableBlock;

View file

@ -0,0 +1,72 @@
import { useState } from '@wordpress/element';
import { STORE_NAME } from '../../../../../../data/common';
import { ButtonSettingsBlock } from '../../../../../ReusableComponents/SettingsBlocks';
import { __ } from '@wordpress/i18n';
import { useDispatch } from '@wordpress/data';
import { store as noticesStore } from '@wordpress/notices';
import {
NOTIFICATION_ERROR,
NOTIFICATION_SUCCESS,
} from '../../../../../ReusableComponents/Icons';
const ResubscribeBlock = () => {
const { createSuccessNotice, createErrorNotice } =
useDispatch( noticesStore );
const [ resubscribing, setResubscribing ] = useState( false );
const { resubscribeWebhooks } = useDispatch( STORE_NAME );
const startResubscribingWebhooks = async () => {
setResubscribing( true );
try {
await resubscribeWebhooks();
} catch ( error ) {
setResubscribing( false );
createErrorNotice(
__(
'Operation failed. Check WooCommerce logs for more details.',
'woocommerce-paypal-payments'
),
{
icon: NOTIFICATION_ERROR,
}
);
return;
}
setResubscribing( false );
createSuccessNotice(
__(
'Webhooks were successfully re-subscribed.',
'woocommerce-paypal-payments'
),
{
icon: NOTIFICATION_SUCCESS,
}
);
};
return (
<ButtonSettingsBlock
title={ __(
'Resubscribe webhooks',
'woocommerce-paypal-payments'
) }
description={ __(
'Click to remove the current webhook subscription and subscribe again, for example, if the website domain or URL structure changed.',
'woocommerce-paypal-payments'
) }
actionProps={ {
buttonType: 'secondary',
isBusy: resubscribing,
callback: () => startResubscribingWebhooks(),
value: __(
'Resubscribe webhooks',
'woocommerce-paypal-payments'
),
} }
/>
);
};
export default ResubscribeBlock;

View file

@ -0,0 +1,129 @@
import { useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { ButtonSettingsBlock } from '../../../../../ReusableComponents/SettingsBlocks';
import { useDispatch } from '@wordpress/data';
import { store as noticesStore } from '@wordpress/notices';
import { CommonHooks } from '../../../../../../data';
import {
NOTIFICATION_ERROR,
NOTIFICATION_SUCCESS,
} from '../../../../../ReusableComponents/Icons';
const SimulationBlock = () => {
const {
createSuccessNotice,
createInfoNotice,
createErrorNotice,
removeNotice,
} = useDispatch( noticesStore );
const { startWebhookSimulation, checkWebhookSimulationState } =
CommonHooks.useWebhooks();
const [ simulating, setSimulating ] = useState( false );
const sleep = ( ms ) => {
return new Promise( ( resolve ) => setTimeout( resolve, ms ) );
};
const startSimulation = async ( maxRetries ) => {
const webhookInfoNoticeId = 'paypal-webhook-simulation-info-notice';
const triggerWebhookInfoNotice = () => {
createInfoNotice(
__(
'Waiting for the webhook to arrive…',
'woocommerce-paypal-payments'
),
{
id: webhookInfoNoticeId,
}
);
};
const stopSimulation = () => {
removeNotice( webhookInfoNoticeId );
setSimulating( false );
};
setSimulating( true );
triggerWebhookInfoNotice();
try {
await startWebhookSimulation();
} catch ( error ) {
console.error( error );
setSimulating( false );
createErrorNotice(
__(
'Operation failed. Check WooCommerce logs for more details.',
'woocommerce-paypal-payments'
),
{
icon: NOTIFICATION_ERROR,
}
);
return;
}
for ( let i = 0; i < maxRetries; i++ ) {
await sleep( 2000 );
const simulationStateResponse = await checkWebhookSimulationState();
try {
if ( ! simulationStateResponse.success ) {
console.error(
'Simulation state query failed: ' +
simulationStateResponse?.data
);
continue;
}
if ( simulationStateResponse?.data?.state === 'received' ) {
createSuccessNotice(
__(
'The webhook was received successfully.',
'woocommerce-paypal-payments'
),
{
icon: NOTIFICATION_SUCCESS,
}
);
stopSimulation();
return;
}
removeNotice( webhookInfoNoticeId );
triggerWebhookInfoNotice();
} catch ( error ) {
console.error( error );
}
}
stopSimulation();
createErrorNotice(
__(
'Looks like the webhook cannot be received. Check that your website is accessible from the internet.',
'woocommerce-paypal-payments'
),
{
icon: NOTIFICATION_ERROR,
}
);
};
return (
<>
<ButtonSettingsBlock
title={ __(
'Simulate webhooks',
'woocommerce-paypal-payments'
) }
actionProps={ {
buttonType: 'secondary',
isBusy: simulating,
callback: () => startSimulation( 30 ),
value: __(
'Simulate webhooks',
'woocommerce-paypal-payments'
),
} }
/>
</>
);
};
export default SimulationBlock;

View file

@ -0,0 +1,72 @@
import { __ } from '@wordpress/i18n';
import {
AccordionSettingsBlock,
Description,
Header,
Title,
ToggleSettingsBlock,
} from '../../../../../ReusableComponents/SettingsBlocks';
import SettingsBlock from '../../../../../ReusableComponents/SettingsBlocks/SettingsBlock';
import SimulationBlock from './SimulationBlock';
import ResubscribeBlock from './ResubscribeBlock';
import HooksTableBlock from './HooksTableBlock';
const Troubleshooting = ( { updateFormValue, settings } ) => {
return (
<AccordionSettingsBlock
className="ppcp-r-settings-block--troubleshooting"
title={ __( 'Troubleshooting', 'woocommerce-paypal-payments' ) }
description={ __(
'Access tools to help debug and resolve issues.',
'woocommerce-paypal-payments'
) }
actionProps={ {
callback: updateFormValue,
key: 'payNowExperience',
value: settings.payNowExperience,
} }
>
<ToggleSettingsBlock
title={ __( 'Logging', 'woocommerce-paypal-payments' ) }
description={ __(
'Log additional debugging information in the WooCommerce logs that can assist technical staff to determine issues.',
'woocommerce-paypal-payments'
) }
actionProps={ {
callback: updateFormValue,
key: 'logging',
value: settings.logging,
} }
/>
<SettingsBlock>
<Header>
<Title>
{ __(
'Subscribed PayPal webhooks',
'woocommerce-paypal-payments'
) }
</Title>
<Description>
{ __(
'The following PayPal webhooks are subscribed. More information about the webhooks is available in the',
'woocommerce-paypal-payments'
) }{ ' ' }
<a href="https://woocommerce.com/document/woocommerce-paypal-payments/#webhook-status">
{ __(
'Webhook Status documentation',
'woocommerce-paypal-payments'
) }
</a>
.
</Description>
</Header>
<HooksTableBlock />
<ResubscribeBlock />
<SimulationBlock />
</SettingsBlock>
</AccordionSettingsBlock>
);
};
export default Troubleshooting;

View file

@ -5,7 +5,7 @@ import {
ContentWrapper,
} from '../../../ReusableComponents/SettingsBlocks';
import Sandbox from './Blocks/Sandbox';
import Troubleshooting from './Blocks/Troubleshooting';
import Troubleshooting from './Blocks/Troubleshooting/Troubleshooting';
import PaypalSettings from './Blocks/PaypalSettings';
import OtherSettings from './Blocks/OtherSettings';

View file

@ -24,4 +24,8 @@ export default {
DO_GENERATE_ONBOARDING_URL: 'COMMON:DO_GENERATE_ONBOARDING_URL',
DO_REFRESH_MERCHANT: 'COMMON:DO_REFRESH_MERCHANT',
DO_REFRESH_FEATURES: 'COMMON:DO_REFRESH_FEATURES',
DO_RESUBSCRIBE_WEBHOOKS: 'COMMON:DO_RESUBSCRIBE_WEBHOOKS',
DO_START_WEBHOOK_SIMULATION: 'COMMON:DO_START_WEBHOOK_SIMULATION',
DO_CHECK_WEBHOOK_SIMULATION_STATE:
'COMMON:DO_CHECK_WEBHOOK_SIMULATION_STATE',
};

View file

@ -250,3 +250,48 @@ export const refreshFeatureStatuses = function* () {
return result;
};
/**
* Persistent. Changes the "webhooks" value.
*
* @param {string} webhooks
* @return {Action} The action.
*/
export const setWebhooks = ( webhooks ) => ( {
type: ACTION_TYPES.SET_PERSISTENT,
payload: { webhooks },
} );
/**
* Side effect
* Refreshes subscribed webhooks via a REST request
*
* @return {Action} The action.
*/
export const resubscribeWebhooks = function* () {
const result = yield { type: ACTION_TYPES.DO_RESUBSCRIBE_WEBHOOKS };
if ( result && result.success ) {
yield hydrate( result );
}
return result;
};
/**
* Side effect. Starts webhook simulation.
*
* @return {Action} The action.
*/
export const startWebhookSimulation = function* () {
return yield { type: ACTION_TYPES.DO_START_WEBHOOK_SIMULATION };
};
/**
* Side effect. Checks webhook simulation.
*
* @return {Action} The action.
*/
export const checkWebhookSimulationState = function* () {
return yield { type: ACTION_TYPES.DO_CHECK_WEBHOOK_SIMULATION_STATE };
};

View file

@ -65,6 +65,26 @@ export const REST_ISU_AUTHENTICATION_PATH = '/wc/v3/wc_paypal/authenticate/isu';
*/
export const REST_CONNECTION_URL_PATH = '/wc/v3/wc_paypal/login_link';
/**
* REST path to fetch webhooks data or resubscribe webhooks,
*
* Used by: Controls
* See: WebhookSettingsEndpoint.php
*
* @type {string}
*/
export const REST_WEBHOOKS = '/wc/v3/wc_paypal/webhook_settings';
/**
* REST path to start webhook simulation and observe the state,
*
* Used by: Controls
* See: WebhookSettingsEndpoint.php
*
* @type {string}
*/
export const REST_WEBHOOKS_SIMULATE = '/wc/v3/wc_paypal/webhook_simulate';
/**
* REST path to refresh the feature status.
*

View file

@ -16,6 +16,8 @@ import {
REST_HYDRATE_MERCHANT_PATH,
REST_REFRESH_FEATURES_PATH,
REST_ISU_AUTHENTICATION_PATH,
REST_WEBHOOKS,
REST_WEBHOOKS_SIMULATE,
} from './constants';
import ACTION_TYPES from './action-types';
@ -121,4 +123,24 @@ export const controls = {
};
}
},
async [ ACTION_TYPES.DO_RESUBSCRIBE_WEBHOOKS ]() {
return await apiFetch( {
method: 'POST',
path: REST_WEBHOOKS,
} );
},
async [ ACTION_TYPES.DO_START_WEBHOOK_SIMULATION ]() {
return await apiFetch( {
method: 'POST',
path: REST_WEBHOOKS_SIMULATE,
} );
},
async [ ACTION_TYPES.DO_CHECK_WEBHOOK_SIMULATION_STATE ]() {
return await apiFetch( {
path: REST_WEBHOOKS_SIMULATE,
} );
},
};

View file

@ -9,7 +9,6 @@
import { useDispatch, useSelect } from '@wordpress/data';
import { useCallback } from '@wordpress/element';
import { STORE_NAME } from './constants';
const useTransient = ( key ) =>
@ -35,6 +34,8 @@ const useHooks = () => {
productionOnboardingUrl,
connectViaSecret,
connectViaAuthCode,
startWebhookSimulation,
checkWebhookSimulationState,
} = useDispatch( STORE_NAME );
// Transient accessors.
@ -45,7 +46,7 @@ const useHooks = () => {
const clientSecret = usePersistent( 'clientSecret' );
const isSandboxMode = usePersistent( 'useSandbox' );
const isManualConnectionMode = usePersistent( 'useManualConnection' );
const webhooks = usePersistent( 'webhooks' );
const merchant = useSelect(
( select ) => select( STORE_NAME ).merchant(),
[]
@ -84,6 +85,9 @@ const useHooks = () => {
connectViaAuthCode,
merchant,
wooSettings,
webhooks,
startWebhookSimulation,
checkWebhookSimulationState,
};
};
@ -129,6 +133,22 @@ export const useWooSettings = () => {
return wooSettings;
};
export const useWebhooks = () => {
const {
webhooks,
setWebhooks,
registerWebhooks,
startWebhookSimulation,
checkWebhookSimulationState,
} = useHooks();
return {
webhooks,
setWebhooks,
registerWebhooks,
startWebhookSimulation,
checkWebhookSimulationState,
};
};
export const useMerchantInfo = () => {
const { merchant } = useHooks();
const { refreshMerchantData } = useDispatch( STORE_NAME );

View file

@ -35,6 +35,7 @@ const defaultPersistent = Object.freeze( {
useManualConnection: false,
clientId: '',
clientSecret: '',
webhooks: [],
} );
// Reducer logic.

View file

@ -12,7 +12,7 @@ import { dispatch } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { apiFetch } from '@wordpress/data-controls';
import { STORE_NAME, REST_HYDRATE_PATH } from './constants';
import { STORE_NAME, REST_HYDRATE_PATH, REST_WEBHOOKS } from './constants';
export const resolvers = {
/**
@ -21,6 +21,9 @@ export const resolvers = {
*persistentData() {
try {
const result = yield apiFetch( { path: REST_HYDRATE_PATH } );
const webhooks = yield apiFetch( { path: REST_WEBHOOKS } );
result.data = { ...result.data, ...webhooks.data };
yield dispatch( STORE_NAME ).hydrate( result );
yield dispatch( STORE_NAME ).setIsReady( true );

View file

@ -33,3 +33,7 @@ export const merchant = ( state ) => {
export const wooSettings = ( state ) => {
return getState( state ).wooSettings || EMPTY_OBJ;
};
export const webhooks = ( state ) => {
return getState( state ).webhooks || EMPTY_OBJ;
};

View file

@ -2,81 +2,139 @@ export const countryPriceInfo = {
US: {
fixedFee: {
USD: 0.49,
GBP: 0.39,
CAD: 0.59,
AUD: 0.59,
EUR: 0.39,
},
checkout: 3.49,
ccf: 2.59,
dw: 2.59,
apm: 2.59,
fastlane: 2.59,
plater: 4.99,
ccf: {
percentage: 2.59,
fixedFee: 0.29,
},
dw: {
percentage: 2.59,
fixedFee: 0.29,
},
apm: {
percentage: 2.89,
fixedFee: 0.29,
},
fast: {
percentage: 2.59,
fixedFee: 0.29,
},
standardCardFields: 2.99,
},
UK: {
fixedFee: {
GPB: 0.3,
USD: 0.3,
CAD: 0.3,
AUD: 0.3,
EUR: 0.35,
},
checkout: 2.9,
plater: 2.9,
ccf: 1.2,
dw: 1.2,
fast: 1.2,
apm: 1.2,
standardCardFields: 1.2,
},
CA: {
fixedFee: {
CAD: 0.3,
USD: 0.3,
GBP: 0.2,
AUD: 0.3,
EUR: 0.35,
},
checkout: 2.9,
ccf: 2.7,
dw: 2.7,
fast: 2.7,
apm: 2.9,
standardCardFields: 2.9,
},
AU: {
fixedFee: {
AUD: 0.3,
USD: 0.3,
GBP: 0.2,
CAD: 0.3,
EUR: 0.35,
},
checkout: 2.6,
plater: 2.6,
ccf: 1.75,
dw: 1.75,
fast: 1.75,
apm: 2.6,
standardCardFields: 2.6,
},
FR: {
fixedFee: {
EUR: 0.35,
USD: 0.3,
GBP: 0.3,
CAD: 0.3,
AUD: 0.3,
},
checkout: 2.9,
plater: 2.9,
ccf: 1.2,
dw: 1.2,
fast: 1.2,
apm: 1.2,
standardCardFields: 1.2,
},
IT: {
fixedFee: {
EUR: 0.35,
USD: 0.3,
GBP: 0.3,
CAD: 0.3,
AUD: 0.3,
},
checkout: 3.4,
plater: 3.4,
ccf: 1.2,
dw: 1.2,
fast: 1.2,
apm: 1.2,
standardCardFields: 1.2,
},
DE: {
fixedFee: {
EUR: 0.39,
USD: 0.49,
GBP: 0.29,
CAD: 0.59,
AUD: 0.59,
},
checkout: 2.99,
plater: 2.99,
ccf: 2.99,
dw: 2.99,
fast: 2.99,
apm: 2.99,
standardCardFields: 2.99,
},
ES: {
fixedFee: {
EUR: 0.35,
USD: 0.3,
GBP: 0.3,
CAD: 0.3,
AUD: 0.3,
},
checkout: 2.9,
plater: 2.9,
ccf: 1.2,
dw: 1.2,
fast: 1.2,
apm: 1.2,
standardCardFields: 1.2,
},

View file

@ -19,12 +19,12 @@ use WooCommerce\PayPalCommerce\Settings\Endpoint\LoginLinkRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\OnboardingRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\RefreshFeatureStatusEndpoint;
use WooCommerce\PayPalCommerce\Settings\Ajax\SwitchSettingsUiEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\WebhookSettingsEndpoint;
use WooCommerce\PayPalCommerce\Settings\Service\ConnectionUrlGenerator;
use WooCommerce\PayPalCommerce\Settings\Service\OnboardingUrlManager;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\Settings\Handler\ConnectionListener;
use WooCommerce\PayPalCommerce\Settings\Service\ConnectionManager;
use WooCommerce\PayPalCommerce\Settings\Service\EnvironmentConfig;
return array(
'settings.url' => static function ( ContainerInterface $container ) : string {
@ -90,6 +90,13 @@ return array(
$container->get( 'settings.service.connection-url-generators' ),
);
},
'settings.rest.webhooks' => static function ( ContainerInterface $container ) : WebhookSettingsEndpoint {
return new WebhookSettingsEndpoint(
$container->get( 'api.endpoint.webhook' ),
$container->get( 'webhook.registrar' ),
$container->get( 'webhook.status.simulation' )
);
},
'settings.casual-selling.supported-countries' => static function ( ContainerInterface $container ) : array {
return array(
'AR',

View file

@ -58,6 +58,9 @@ class CommonRestEndpoint extends RestEndpoint {
'js_name' => 'clientSecret',
'sanitize' => 'sanitize_text_field',
),
'webhooks' => array(
'js_name' => 'webhooks',
),
);
/**

View file

@ -0,0 +1,185 @@
<?php
/**
* REST endpoint to manage the onboarding module.
*
* @package WooCommerce\PayPalCommerce\Settings\Endpoint
*/
declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Settings\Endpoint;
use stdClass;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\WebhookEndpoint;
use WooCommerce\PayPalCommerce\Webhooks\Status\WebhookSimulation;
use WooCommerce\PayPalCommerce\Webhooks\WebhookRegistrar;
use WP_REST_Response;
use WP_REST_Server;
/**
* Class WebhookSettingsEndpoint
*
* Note: Endpoint for webhook related requests
*/
class WebhookSettingsEndpoint extends RestEndpoint {
/**
* Endpoint base to fetch webhook settings and resubscribe
*
* @var string
*/
protected $rest_base = 'webhook_settings';
/**
* Endpoint base to start webhook simulation and check the state
*
* @var string
*/
protected string $rest_simulate_base = 'webhook_simulate';
/**
* Application webhook endpoint
*
* @var WebhookEndpoint
*/
private WebhookEndpoint $webhook_endpoint;
/**
* A service that allows resubscribing webhooks
*
* @var WebhookRegistrar
*/
private WebhookRegistrar $webhook_registrar;
/**
* A service that allows webhook simulations
*
* @var WebhookSimulation
*/
private WebhookSimulation $webhook_simulation;
/**
* WebhookSettingsEndpoint constructor.
*
* @param WebhookEndpoint $webhook_endpoint A list of subscribed webhooks and a webhook endpoint URL.
* @param WebhookRegistrar $webhook_registrar A service that allows resubscribing webhooks.
* @param WebhookSimulation $webhook_simulation A service that allows webhook simulations.
*/
public function __construct( WebhookEndpoint $webhook_endpoint, WebhookRegistrar $webhook_registrar, WebhookSimulation $webhook_simulation ) {
$this->webhook_endpoint = $webhook_endpoint;
$this->webhook_registrar = $webhook_registrar;
$this->webhook_simulation = $webhook_simulation;
}
/**
* Configure REST API routes.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_webhooks' ),
'permission_callback' => array( $this, 'check_permission' ),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'resubscribe_webhooks' ),
'permission_callback' => array( $this, 'check_permission' ),
),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_simulate_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'check_simulated_webhook_state' ),
'permission_callback' => array( $this, 'check_permission' ),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'simulate_webhooks_start' ),
'permission_callback' => array( $this, 'check_permission' ),
),
)
);
}
/**
* Returns a webhook endpoint URL and list of subscribed webhooks
*
* @return WP_REST_Response
*/
public function get_webhooks(): WP_REST_Response {
try {
$webhook_list = ( $this->webhook_endpoint->list() )[0];
$webhook_events = array_map(
function ( stdClass $webhook ) {
return strtolower( $webhook->name );
},
$webhook_list->event_types()
);
return $this->return_success(
array(
'webhooks' => array(
'url' => $webhook_list->url(),
'events' => implode( ', ', $webhook_events ),
),
)
);
} catch ( \Exception $error ) {
return $this->return_error( 'Problem while fetching webhooks data' );
}
}
/**
* Re-subscribes webhooks and returns webhooks
*
* @return WP_REST_Response
*/
public function resubscribe_webhooks(): WP_REST_Response {
if ( ! $this->webhook_registrar->register() ) {
return $this->return_error( 'Webhook subscription failed.' );
}
return $this->get_webhooks();
}
/**
* Starts webhook simulation
*
* @return WP_REST_Response
*/
public function simulate_webhooks_start(): WP_REST_Response {
try {
$this->webhook_simulation->start();
return $this->return_success( array() );
} catch ( \Exception $error ) {
return $this->return_error( $error->getMessage() );
}
}
/**
* Checks webhook simulation state
*
* @return WP_REST_Response
*/
public function check_simulated_webhook_state(): WP_REST_Response {
try {
$state = $this->webhook_simulation->get_state();
return $this->return_success(
array(
'state' => $state,
)
);
} catch ( \Exception $error ) {
return $this->return_error( $error->getMessage() );
}
}
}

View file

@ -181,6 +181,7 @@ class SettingsModule implements ServiceModule, ExecutableModule {
$container->get( 'settings.rest.common' ),
$container->get( 'settings.rest.connect_manual' ),
$container->get( 'settings.rest.login_link' ),
$container->get( 'settings.rest.webhooks' ),
$container->get( 'settings.rest.refresh_feature_status' ),
);

View file

@ -466,7 +466,7 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
continue;
}
$custom_id = $wc_order->get_order_number();
$custom_id = (string) $wc_order->get_id();
$invoice_id = $this->prefix . $wc_order->get_order_number();
$create_order = $this->capture_card_payment->create_order( $token->get_token(), $custom_id, $invoice_id, $wc_order );

View file

@ -96,52 +96,35 @@ trait CreditCardOrderInfoHandlingTrait {
return;
}
$fraud_responses = $fraud->to_array();
$card_brand = $payment_source->properties()->brand ?? __( 'N/A', 'woocommerce-paypal-payments' );
$card_last_digits = $payment_source->properties()->last_digits ?? __( 'N/A', 'woocommerce-paypal-payments' );
$avs_response_order_note_title = __( 'Address Verification Result', 'woocommerce-paypal-payments' );
$response_order_note_title = __( 'PayPal Advanced Card Processing Verification:', 'woocommerce-paypal-payments' );
/* translators: %1$s is AVS order note title, %2$s is AVS order note result markup */
$avs_response_order_note_format = __( '%1$s %2$s', 'woocommerce-paypal-payments' );
$avs_response_order_note_result_format = '<ul class="ppcp_avs_result">
$response_order_note_format = __( '%1$s %2$s', 'woocommerce-paypal-payments' );
$response_order_note_result_format = '<ul class="ppcp_avs_cvv_result">
<li>%1$s</li>
<ul class="ppcp_avs_result_inner">
<li>%2$s</li>
<li>%3$s</li>
</ul>
<li>%4$s</li>
<li>%5$s</li>
</ul>';
$avs_response_order_note_result = sprintf(
$avs_response_order_note_result_format,
/* translators: %s is fraud AVS code */
sprintf( __( 'AVS: %s', 'woocommerce-paypal-payments' ), esc_html( $fraud_responses['avs_code'] ) ),
/* translators: %s is fraud AVS address match */
sprintf( __( 'Address Match: %s', 'woocommerce-paypal-payments' ), esc_html( $fraud_responses['address_match'] ) ),
/* translators: %s is fraud AVS postal match */
sprintf( __( 'Postal Match: %s', 'woocommerce-paypal-payments' ), esc_html( $fraud_responses['postal_match'] ) ),
/* translators: %s is card brand */
sprintf( __( 'Card Brand: %s', 'woocommerce-paypal-payments' ), esc_html( $card_brand ) ),
/* translators: %s card last digits */
sprintf( __( 'Card Last Digits: %s', 'woocommerce-paypal-payments' ), esc_html( $card_last_digits ) )
$response_order_note_result = sprintf(
$response_order_note_result_format,
/* translators: %1$s is card brand and %2$s card last 4 digits */
sprintf( __( 'Card: %1$s (%2$s)', 'woocommerce-paypal-payments' ), $card_brand, $card_last_digits ),
/* translators: %s is fraud AVS message */
sprintf( __( 'AVS: %s', 'woocommerce-paypal-payments' ), $fraud->get_avs_code_message() ),
/* translators: %s is fraud CVV message */
sprintf( __( 'CVV: %s', 'woocommerce-paypal-payments' ), $fraud->get_cvv2_code_message() ),
);
$avs_response_order_note = sprintf(
$avs_response_order_note_format,
esc_html( $avs_response_order_note_title ),
wp_kses_post( $avs_response_order_note_result )
$response_order_note = sprintf(
$response_order_note_format,
esc_html( $response_order_note_title ),
wp_kses_post( $response_order_note_result )
);
$wc_order->add_order_note( $avs_response_order_note );
$cvv_response_order_note_format = '<ul class="ppcp_cvv_result"><li>%1$s</li></ul>';
$cvv_response_order_note = sprintf(
$cvv_response_order_note_format,
/* translators: %s is fraud CVV match */
sprintf( __( 'CVV2 Match: %s', 'woocommerce-paypal-payments' ), esc_html( $fraud_responses['cvv_match'] ) )
);
$wc_order->add_order_note( $cvv_response_order_note );
$wc_order->add_order_note( $response_order_note );
$meta_details = array_merge(
$fraud_responses,
$fraud->to_array(),
array(
'card_brand' => $card_brand,
'card_last_digits' => $card_last_digits,

View file

@ -1,6 +1,6 @@
{
"name": "woocommerce-paypal-payments",
"version": "2.9.5",
"version": "2.9.6",
"description": "WooCommerce PayPal Payments",
"repository": "https://github.com/woocommerce/woocommerce-paypal-payments",
"license": "GPL-2.0",

View file

@ -1,10 +1,10 @@
=== WooCommerce PayPal Payments ===
Contributors: woocommerce, automattic, syde
Tags: woocommerce, paypal, payments, ecommerce, credit card
Requires at least: 6.3
Requires at least: 6.5
Tested up to: 6.7
Requires PHP: 7.4
Stable tag: 2.9.5
Stable tag: 2.9.6
License: GPLv2
License URI: http://www.gnu.org/licenses/gpl-2.0.html
@ -179,23 +179,35 @@ If you encounter issues with the PayPal buttons not appearing after an update, p
== Changelog ==
= 2.9.6 - XXXX-XX-XX =
* Fix - NOT_ENABLED_TO_VAULT_PAYMENT_SOURCE on PayPal transactions when using ACDC Vaulting without PayPal Vault approval #2955
* Fix - Express buttons for Free Trial Subscription products on Block Cart/Checkout trigger CANNOT_BE_ZERO_OR_NEGATIVE error #2872
* Fix - String translations not applied to Card Fields on Block Checkout #2934
* Fix - Fastlane component included in script when Fastlane is disabled #2911
* Fix - Zero amount line items may trigger CANNOT_BE_ZERO_OR_NEGATIVE error after rounding error #2906
* Fix - “Save changes” is grey and unclickable when switching from Sandbox to Live #2895
* Fix - plugin queries variations when button/messaging is disabled on single product page #2896
* Fix - Use get_id instead of get_order_number on setting custom_id (author @0verscore) #2930
* Enhancement - Improve fraud response order notes for Advanced Card Processing transactions #2905
* Tweak - Update the minimum plugin requirements to WordPress 6.5 & WooCommerce 9.2 #2920
= 2.9.5 - 2024-12-10 =
Fix - Early translation loading triggers `Function _load_textdomain_just_in_time was called incorrectly.` notice #2816
Fix - ACDC card fields not loading and payment not successful when Classic Checkout Smart Button Location disabled #2852
Fix - ACDC gateway does not appear for guests when is Fastlane enabled and a subscription product is in the cart #2745
Fix - "Voide authorization" button does not appear for Apple Pay/Google Pay orders when payment buttons are separated #2752
Fix - Additional payment tokens saved with new customer_id #2820
Fix - Vaulted payment method may not be displayed in PayPal button for return buyer #2809
Fix - Conflict with EasyShip plugin due to shipping methods loading too early #2845
Fix - Restore accidentally removed ACDC currencies #2838
Enhancement - Native gateway icon for PayPal & Pay upon Invoice gateways #2712
Enhancement - Allow disabling specific card types for Fastlane #2704
Enhancement - Fastlane Insights SDK implementation for block Checkout #2737
Enhancement - Hide split local APMs in Payments settings tab when PayPal is not enabled #2703
Enhancement - Do not load split local APMs on Checkout when PayPal is not enabled #2792
Enhancement - Add support for Button Options in the Block Checkout for Apple Pay & Google Pay buttons #2797 #2772
Enhancement - Disable “Add payment method” button while saving ACDC payment #2794
Enhancement - Sanitize soft_descriptor field #2846 #2854
* Fix - Early translation loading triggers `Function _load_textdomain_just_in_time was called incorrectly.` notice #2816
* Fix - ACDC card fields not loading and payment not successful when Classic Checkout Smart Button Location disabled #2852
* Fix - ACDC gateway does not appear for guests when is Fastlane enabled and a subscription product is in the cart #2745
* Fix - "Voide authorization" button does not appear for Apple Pay/Google Pay orders when payment buttons are separated #2752
* Fix - Additional payment tokens saved with new customer_id #2820
* Fix - Vaulted payment method may not be displayed in PayPal button for return buyer #2809
* Fix - Conflict with EasyShip plugin due to shipping methods loading too early #2845
* Fix - Restore accidentally removed ACDC currencies #2838
* Enhancement - Native gateway icon for PayPal & Pay upon Invoice gateways #2712
* Enhancement - Allow disabling specific card types for Fastlane #2704
* Enhancement - Fastlane Insights SDK implementation for block Checkout #2737
* Enhancement - Hide split local APMs in Payments settings tab when PayPal is not enabled #2703
* Enhancement - Do not load split local APMs on Checkout when PayPal is not enabled #2792
* Enhancement - Add support for Button Options in the Block Checkout for Apple Pay & Google Pay buttons #2797 #2772
* Enhancement - Disable “Add payment method” button while saving ACDC payment #2794
* Enhancement - Sanitize soft_descriptor field #2846 #2854
= 2.9.4 - 2024-11-11 =
* Fix - Apple Pay button preview missing in Standard payment and Advanced Processing tabs #2755

View file

@ -85,7 +85,7 @@ class FilePathPluginFactory implements FilePathPluginFactoryInterface {
'Title' => '',
'Description' => '',
'TextDomain' => '',
'RequiresWP' => '6.3',
'RequiresWP' => '6.5',
'RequiresPHP' => '7.4',
),
$plugin_data

View file

@ -3,14 +3,15 @@
* Plugin Name: WooCommerce PayPal Payments
* Plugin URI: https://woocommerce.com/products/woocommerce-paypal-payments/
* Description: PayPal's latest complete payments processing solution. Accept PayPal, Pay Later, credit/debit cards, alternative digital wallets local payment types and bank accounts. Turn on only PayPal options or process a full suite of payment methods. Enable global transaction with extensive currency and country coverage.
* Version: 2.9.5
* Version: 2.9.6
* Author: WooCommerce
* Author URI: https://woocommerce.com/
* License: GPL-2.0
* Requires PHP: 7.4
* Requires Plugins: woocommerce
* WC requires at least: 6.9
* WC tested up to: 9.4
* Requires at least: 6.5
* WC requires at least: 9.2
* WC tested up to: 9.5
* Text Domain: woocommerce-paypal-payments
*
* @package WooCommerce\PayPalCommerce
@ -26,7 +27,7 @@ define( 'PAYPAL_API_URL', 'https://api-m.paypal.com' );
define( 'PAYPAL_URL', 'https://www.paypal.com' );
define( 'PAYPAL_SANDBOX_API_URL', 'https://api-m.sandbox.paypal.com' );
define( 'PAYPAL_SANDBOX_URL', 'https://www.sandbox.paypal.com' );
define( 'PAYPAL_INTEGRATION_DATE', '2024-12-02' );
define( 'PAYPAL_INTEGRATION_DATE', '2024-12-31' );
define( 'PPCP_PAYPAL_BN_CODE', 'Woo_PPCP' );
! defined( 'CONNECT_WOO_CLIENT_ID' ) && define( 'CONNECT_WOO_CLIENT_ID', 'AcCAsWta_JTL__OfpjspNyH7c1GGHH332fLwonA5CwX4Y10mhybRZmHLA0GdRbwKwjQIhpDQy0pluX_P' );