Merge branch 'trunk' into PCP-3917-things-to-do-next-component-functionality

This commit is contained in:
Emili Castells Guasch 2025-01-02 16:29:44 +01:00
commit 4d4ab689f5
121 changed files with 5004 additions and 3431 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

@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
php-versions: ['7.4', '8.0', '8.1', '8.2', '8.3']
php-versions: ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4']
name: PHP ${{ matrix.php-versions }}
steps:
@ -30,6 +30,7 @@ jobs:
run: vendor/bin/phpunit
- name: Psalm
if: ${{ matrix.php-versions == '7.4' }}
run: ./vendor/bin/psalm --show-info=false --threads=8 --diff
- name: Run PHPCS

View file

@ -1,22 +1,34 @@
*** Changelog ***
= 2.9.5 - xxxx-xx-xx =
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.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
= 2.9.4 - 2024-11-11 =
* Fix - Apple Pay button preview missing in Standard payment and Advanced Processing tabs #2755

View file

@ -2,7 +2,7 @@
"name": "woocommerce/woocommerce-paypal-payments",
"type": "wordpress-plugin",
"description": "PayPal Commerce Platform for WooCommerce",
"license": "GPL-2.0",
"license": "GPL-2.0-or-later",
"require": {
"php": "^7.4 | ^8.0",
"ext-json": "*",

6
composer.lock generated
View file

@ -5541,8 +5541,8 @@
"aliases": [],
"minimum-stability": "dev",
"stability-flags": {
"php-stubs/wordpress-stubs": 0,
"php-stubs/woocommerce-stubs": 0
"php-stubs/woocommerce-stubs": 0,
"php-stubs/wordpress-stubs": 0
},
"prefer-stable": true,
"prefer-lowest": false,
@ -5550,7 +5550,7 @@
"php": "^7.4 | ^8.0",
"ext-json": "*"
},
"platform-dev": [],
"platform-dev": {},
"platform-overrides": {
"php": "7.4"
},

View file

@ -16,6 +16,8 @@ use Psr\Log\LoggerInterface;
/**
* Class PartnerReferrals
*
* @see https://developer.paypal.com/docs/api/partner-referrals/v2/
*/
class PartnerReferrals {

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

@ -182,6 +182,26 @@ class ApplepayModule implements ServiceModule, ExtendingModule, ExecutableModule
2
);
add_filter(
'woocommerce_paypal_payments_rest_common_merchant_data',
function( array $merchant_data ) use ( $c ): array {
if ( ! isset( $merchant_data['features'] ) ) {
$merchant_data['features'] = array();
}
$product_status = $c->get( 'applepay.apple-product-status' );
assert( $product_status instanceof AppleProductStatus );
$apple_pay_enabled = $product_status->is_active();
$merchant_data['features']['apple_pay'] = array(
'enabled' => $apple_pay_enabled,
);
return $merchant_data;
}
);
return true;
}

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

@ -9,62 +9,149 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Compat;
use RuntimeException;
/**
* A helper for mapping the new/old settings.
* A helper class to manage the transition between legacy and new settings.
*
* This utility provides mapping from old setting keys to new ones and retrieves
* their corresponding values from the appropriate models. The class uses lazy
* loading and caching to optimize performance during runtime.
*/
class SettingsMapHelper {
/**
* A list of mapped settings.
* A list of settings maps containing mapping definitions.
*
* @var SettingsMap[]
*/
protected array $settings_map;
/**
* Indexed map for faster lookups, initialized lazily.
*
* @var array|null Associative array where old keys map to metadata.
*/
protected ?array $key_to_model = null;
/**
* Cache for results of `to_array()` calls on models.
*
* @var array Associative array where keys are model IDs.
*/
protected array $model_cache = array();
/**
* Constructor.
*
* @param SettingsMap[] $settings_map A list of mapped settings.
* @param SettingsMap[] $settings_map A list of settings maps containing key definitions.
* @throws RuntimeException When an old key has multiple mappings.
*/
public function __construct( array $settings_map ) {
$this->validate_settings_map( $settings_map );
$this->settings_map = $settings_map;
}
/**
* Retrieves the mapped value from the new settings.
* Validates the settings map for duplicate keys.
*
* @param string $key The key.
* @return ?mixed the mapped value or Null if it doesn't exist.
* @param SettingsMap[] $settings_map The settings map to validate.
* @throws RuntimeException When an old key has multiple mappings.
*/
public function mapped_value( string $key ) {
if ( ! $this->has_mapped_key( $key ) ) {
return null;
}
protected function validate_settings_map( array $settings_map ) : void {
$seen_keys = array();
foreach ( $this->settings_map as $settings_map ) {
$mapped_key = array_search( $key, $settings_map->get_map(), true );
$new_settings = $settings_map->get_model()->to_array();
if ( ! empty( $new_settings[ $mapped_key ] ) ) {
return $new_settings[ $mapped_key ];
foreach ( $settings_map as $settings_map_instance ) {
foreach ( $settings_map_instance->get_map() as $old_key => $new_key ) {
if ( isset( $seen_keys[ $old_key ] ) ) {
throw new RuntimeException( "Duplicate mapping for legacy key '$old_key'." );
}
$seen_keys[ $old_key ] = true;
}
}
return null;
}
/**
* Checks if the given key exists in the new settings.
* Retrieves the value of a mapped key from the new settings.
*
* @param string $key The key.
* @return bool true if the given key exists in the new settings, otherwise false.
* @param string $old_key The key from the legacy settings.
*
* @return mixed|null The value of the mapped setting, or null if not found.
*/
public function has_mapped_key( string $key ) : bool {
foreach ( $this->settings_map as $settings_map ) {
if ( in_array( $key, $settings_map->get_map(), true ) ) {
return true;
public function mapped_value( string $old_key ) {
$this->ensure_map_initialized();
if ( ! isset( $this->key_to_model[ $old_key ] ) ) {
return null;
}
$mapping = $this->key_to_model[ $old_key ];
$model_id = spl_object_id( $mapping['model'] );
return $this->get_cached_model_value( $model_id, $mapping['new_key'], $mapping['model'] );
}
/**
* Determines if a given legacy key exists in the new settings.
*
* @param string $old_key The key from the legacy settings.
*
* @return bool True if the key exists in the new settings, false otherwise.
*/
public function has_mapped_key( string $old_key ) : bool {
$this->ensure_map_initialized();
return isset( $this->key_to_model[ $old_key ] );
}
/**
* Retrieves a cached model value or caches it if not already cached.
*
* @param int $model_id The unique identifier for the model object.
* @param string $new_key The key in the new settings structure.
* @param object $model The model object.
*
* @return mixed|null The value of the key in the model, or null if not found.
*/
protected function get_cached_model_value( int $model_id, string $new_key, object $model ) {
if ( ! isset( $this->model_cache[ $model_id ] ) ) {
$this->model_cache[ $model_id ] = $model->to_array();
}
return $this->model_cache[ $model_id ][ $new_key ] ?? null;
}
/**
* Ensures the map of old-to-new settings is initialized.
*
* This method initializes the `key_to_model` array lazily to improve performance.
*
* @return void
*/
protected function ensure_map_initialized() : void {
if ( $this->key_to_model === null ) {
$this->initialize_key_map();
}
}
return false;
/**
* Initializes the indexed map of old-to-new settings keys.
*
* This method processes the provided settings maps and indexes the legacy
* keys to their corresponding metadata for efficient lookup.
*
* @return void
*/
protected function initialize_key_map() : void {
$this->key_to_model = array();
foreach ( $this->settings_map as $settings_map_instance ) {
foreach ( $settings_map_instance->get_map() as $old_key => $new_key ) {
$this->key_to_model[ $old_key ] = array(
'new_key' => $new_key,
'model' => $settings_map_instance->get_model(),
);
}
}
}
}

View file

@ -232,6 +232,26 @@ class GooglepayModule implements ServiceModule, ExtendingModule, ExecutableModul
2
);
add_filter(
'woocommerce_paypal_payments_rest_common_merchant_data',
function ( array $merchant_data ) use ( $c ): array {
if ( ! isset( $merchant_data['features'] ) ) {
$merchant_data['features'] = array();
}
$product_status = $c->get( 'googlepay.helpers.apm-product-status' );
assert( $product_status instanceof ApmProductStatus );
$google_pay_enabled = $product_status->is_active();
$merchant_data['features']['google_pay'] = array(
'enabled' => $google_pay_enabled,
);
return $merchant_data;
}
);
return true;
}
}

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

@ -7,14 +7,13 @@
"build": "wp-scripts build --webpack-src-dir=resources/js --output-path=assets"
},
"devDependencies": {
"@woocommerce/navigation": "~8.1.0",
"@wordpress/data": "^10.10.0",
"@wordpress/data-controls": "^4.10.0",
"@wordpress/scripts": "^30.3.0"
"@wordpress/scripts": "^30.3.0",
"classnames": "^2.5.1"
},
"dependencies": {
"@paypal/react-paypal-js": "^8.7.0",
"@woocommerce/settings": "^1.0.0",
"react-select": "^5.8.3"
}
}

View file

@ -10,6 +10,7 @@ $color-gray-500: #BBBBBB;
$color-gray-400: #CCCCCC;
$color-gray-300: #EBEBEB;
$color-gray-200: #E0E0E0;
$color-gray-100: #F0F0F0;
$color-gray: #646970;
$color-text-tertiary: #505050;
$color-text-text: #070707;
@ -27,6 +28,8 @@ $max-width-settings: 938px;
$card-vertical-gap: 48px;
/* define custom theming options */
:root {
--ppcp-color-app-bg: #{$color-white};
}
@ -37,4 +40,18 @@ $card-vertical-gap: 48px;
--max-width-onboarding-content: #{$max-width-onboarding-content};
--max-container-width: var(--max-width-settings);
--color-black: #{$color-black};
--color-white: #{$color-white};
--color-blueberry: #{$color-blueberry};
--color-gray-900: #{$color-gray-900};
--color-gray-800: #{$color-gray-800};
--color-gray-700: #{$color-gray-700};
--color-gray-600: #{$color-gray-600};
--color-gray-500: #{$color-gray-500};
--color-gray-400: #{$color-gray-400};
--color-gray-300: #{$color-gray-300};
--color-gray-200: #{$color-gray-200};
--color-gray-100: #{$color-gray-100};
--color-gradient-dark: #{$color-gradient-dark};
}

View file

@ -0,0 +1,22 @@
/**
* Global app-level styles
*/
.ppcp-r-app.loading {
height: 400px;
width: 400px;
position: absolute;
left: 50%;
transform: translate(-50%, 0);
text-align: center;
.ppcp-r-spinner-overlay {
display: flex;
flex-direction: column;
justify-content: center;
}
.ppcp-r-spinner-overlay__message {
transform: translate(0, 32px)
}
}

View file

@ -0,0 +1,10 @@
.ppcp-r-busy-wrapper {
position: relative;
&.ppcp--is-loading {
pointer-events: none;
user-select: none;
--spinner-overlay-color: #fff4;
}
}

View file

@ -1,48 +1,102 @@
button.components-button, a.components-button {
&.is-primary, &.is-secondary {
&:not(:disabled) {
background-color: $color-black;
%button-style-default {
background-color: var(--button-background);
color: var(--button-color);
box-shadow: inset 0 0 0 1px var(--button-border-color);
}
&:disabled {
color: $color-gray-700;
%button-style-hover {
background-color: var(--button-hover-background);
color: var(--button-hover-color);
box-shadow: inset 0 0 0 1px var(--button-hover-border-color);
}
%button-style-disabled {
background-color: var(--button-disabled-background);
color: var(--button-disabled-color);
box-shadow: inset 0 0 0 1px var(--button-disabled-border-color);
}
%button-shape-pill {
border-radius: 50px;
padding: 15px 32px;
height: auto;
}
button.components-button, a.components-button {
/* default theme */
--button-color: var(--color-gray-900);
--button-background: transparent;
--button-border-color: transparent;
--button-hover-color: var(--button-color);
--button-hover-background: var(--button-background);
--button-hover-border-color: var(--button-border-color);
--button-disabled-color: var(--color-gray-500);
--button-disabled-background: transparent;
--button-disabled-border-color: transparent;
/* style the button template */
&:not(:disabled) {
@extend %button-style-default;
}
&:hover {
@extend %button-style-hover;
}
&:disabled {
@extend %button-style-disabled;
}
/*
----------------------------------------------
Customize variants using the theming variables
*/
&.is-primary,
&.is-secondary {
@extend %button-shape-pill;
}
&.is-primary {
@include font(14, 18, 900);
&:not(:disabled) {
background-color: $color-blueberry;
color: $color-white;
}
--button-color: #{$color-white};
--button-background: #{$color-blueberry};
--button-disabled-color: #{$color-gray-100};
--button-disabled-background: #{$color-gray-500};
}
&.is-secondary:not(:disabled) {
border-color: $color-blueberry;
background-color: $color-white;
color: $color-blueberry;
&.is-secondary {
--button-color: #{$color-blueberry};
--button-background: #{$color-white};
--button-border-color: #{$color-blueberry};
&:hover {
background-color: $color-white;
background: none;
}
--button-disabled-color: #{$color-gray-600};
--button-disabled-background: #{$color-gray-100};
--button-disabled-border-color: #{$color-gray-400};
}
&.is-tertiary {
color: $color-blueberry;
&:hover {
color: $color-gradient-dark;
}
--button-color: #{$color-blueberry};
--button-hover-color: #{$color-gradient-dark};
&:focus:not(:disabled) {
border: none;
box-shadow: none;
}
}
&.small-button {
@include small-button;
}
}
.ppcp--is-loading {
button.components-button, a.components-button {
@extend %button-style-disabled;
}
}

View file

@ -57,6 +57,14 @@
&__content {
display: flex;
position: relative;
pointer-events: none;
*:not(a){
pointer-events: none;
}
a {
pointer-events: all;
}
}
&__title {

View file

@ -31,10 +31,4 @@
&__toggled-content {
margin-top: 24px;
}
&.ppcp--is-loading {
pointer-events: none;
--spinner-overlay-color: #fff4;
}
}

View file

@ -12,5 +12,6 @@
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
margin: 0;
}
}

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;
@ -109,6 +105,8 @@
span {
font-weight: 500;
}
margin-top:24px;
}
}
@ -239,6 +237,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

@ -20,12 +20,6 @@
margin: 0 0 24px 0;
}
.ppcp-r-toggle-block__toggled-content > button{
@include small-button;
color: $color-white;
border: none;
}
.client-id-error {
color: #cc1818;
margin: -16px 0 24px;

View file

@ -3,10 +3,11 @@
#ppcp-settings-container {
@import './global';
@import './components/reusable-components/onboarding-header';
@import './components/reusable-components/busy-state';
@import './components/reusable-components/button';
@import './components/reusable-components/settings-toggle-block';
@import './components/reusable-components/separator';
@import './components/reusable-components/onboarding-header';
@import './components/reusable-components/settings-toggle-block';
@import './components/reusable-components/payment-method-icons';
@import "./components/reusable-components/payment-method-item";
@import './components/reusable-components/settings-wrapper';
@ -22,6 +23,7 @@
@import './components/screens/onboarding';
@import './components/screens/settings';
@import './components/screens/overview/tab-styling';
@import './components/app';
}
@import './components/reusable-components/payment-method-modal';

View file

@ -1,8 +1,6 @@
import { Icon } from '@wordpress/components';
import { chevronDown, chevronUp } from '@wordpress/icons';
import classNames from 'classnames';
import { useAccordionState } from '../../hooks/useAccordionState';
// Provide defaults for all layout components so the generic version just works.
@ -24,6 +22,13 @@ const DefaultDescription = ( { children } ) => (
<div className="ppcp-r-accordion__description">{ children }</div>
);
const AccordionContent = ( { isOpen, children } ) => {
if ( ! isOpen || ! children ) {
return null;
}
return <div className="ppcp-r-accordion__content">{ children }</div>;
};
const Accordion = ( {
title,
id = '',
@ -65,9 +70,7 @@ const Accordion = ( {
) }
</Header>
</button>
{ isOpen && children && (
<div className="ppcp-r-accordion__content">{ children }</div>
) }
<AccordionContent isOpen={ isOpen }>{ children }</AccordionContent>
</div>
);
};

View file

@ -1,6 +1,4 @@
import data from '../../utils/data';
import TitleBadge, { TITLE_BADGE_INFO } from './TitleBadge';
import { __ } from '@wordpress/i18n';
const BadgeBox = ( props ) => {
const titleSize =
@ -29,12 +27,7 @@ const BadgeBox = ( props ) => {
</span>
) }
{ props.textBadge && (
<TitleBadge
type={ TITLE_BADGE_INFO }
text={ props.textBadge }
/>
) }
{ props.textBadge }
</span>
<div className="ppcp-r-badge-box__description">
{ props?.description && (

View file

@ -0,0 +1,68 @@
import {
Children,
isValidElement,
cloneElement,
useMemo,
createContext,
useContext,
} from '@wordpress/element';
import classNames from 'classnames';
import { CommonHooks } from '../../data';
import SpinnerOverlay from './SpinnerOverlay';
// Create context to track the busy state across nested wrappers
const BusyContext = createContext( false );
/**
* Wraps interactive child elements and modifies their behavior based on the global `isBusy` state.
* Allows custom processing of child props via the `onBusy` callback.
*
* @param {Object} props - Component properties.
* @param {Children} props.children - Child components to wrap.
* @param {boolean} props.enabled - Enables or disables the busy-state logic.
* @param {boolean} props.busySpinner - Allows disabling the spinner in busy-state.
* @param {string} props.className - Additional class names for the wrapper.
* @param {Function} props.onBusy - Callback to process child props when busy.
*/
const BusyStateWrapper = ( {
children,
enabled = true,
busySpinner = true,
className = '',
onBusy = () => ( { disabled: true } ),
} ) => {
const { isBusy } = CommonHooks.useBusyState();
const hasBusyParent = useContext( BusyContext );
const isBusyComponent = isBusy && enabled;
const showSpinner = busySpinner && isBusyComponent && ! hasBusyParent;
const wrapperClassName = classNames( 'ppcp-r-busy-wrapper', className, {
'ppcp--is-loading': isBusyComponent,
} );
const memoizedChildren = useMemo(
() =>
Children.map( children, ( child ) =>
isValidElement( child )
? cloneElement(
child,
isBusyComponent ? onBusy( child.props ) : {}
)
: child
),
[ children, isBusyComponent, onBusy ]
);
return (
<BusyContext.Provider value={ isBusyComponent }>
<div className={ wrapperClassName }>
{ showSpinner && <SpinnerOverlay /> }
{ memoizedChildren }
</div>
</BusyContext.Provider>
);
};
export default BusyStateWrapper;

View file

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

View file

@ -0,0 +1,12 @@
/**
* WordPress dependencies
*/
import { SVG, Path } from '@wordpress/primitives';
const openSignup = (
<SVG xmlns="http://www.w3.org/2000/svg" viewBox="0 0 25 24">
<Path d="M12.4999 12.75V18.75C12.4999 18.9489 12.4209 19.1397 12.2803 19.2803C12.1396 19.421 11.9488 19.5 11.7499 19.5C11.551 19.5 11.3603 19.421 11.2196 19.2803C11.0789 19.1397 10.9999 18.9489 10.9999 18.75V14.5613L4.78055 20.7806C4.71087 20.8503 4.62815 20.9056 4.5371 20.9433C4.44606 20.981 4.34847 21.0004 4.24993 21.0004C4.15138 21.0004 4.0538 20.981 3.96276 20.9433C3.87171 20.9056 3.78899 20.8503 3.7193 20.7806C3.64962 20.7109 3.59435 20.6282 3.55663 20.5372C3.51892 20.4461 3.49951 20.3485 3.49951 20.25C3.49951 20.1515 3.51892 20.0539 3.55663 19.9628C3.59435 19.8718 3.64962 19.7891 3.7193 19.7194L9.93868 13.5H5.74993C5.55102 13.5 5.36025 13.421 5.2196 13.2803C5.07895 13.1397 4.99993 12.9489 4.99993 12.75C4.99993 12.5511 5.07895 12.3603 5.2196 12.2197C5.36025 12.079 5.55102 12 5.74993 12H11.7499C11.9488 12 12.1396 12.079 12.2803 12.2197C12.4209 12.3603 12.4999 12.5511 12.4999 12.75ZM19.9999 3H7.99993C7.6021 3 7.22057 3.15804 6.93927 3.43934C6.65796 3.72064 6.49993 4.10218 6.49993 4.5V9C6.49993 9.19891 6.57895 9.38968 6.7196 9.53033C6.86025 9.67098 7.05102 9.75 7.24993 9.75C7.44884 9.75 7.63961 9.67098 7.78026 9.53033C7.92091 9.38968 7.99993 9.19891 7.99993 9V4.5H19.9999V16.5H15.4999C15.301 16.5 15.1103 16.579 14.9696 16.7197C14.8289 16.8603 14.7499 17.0511 14.7499 17.25C14.7499 17.4489 14.8289 17.6397 14.9696 17.7803C15.1103 17.921 15.301 18 15.4999 18H19.9999C20.3978 18 20.7793 17.842 21.0606 17.5607C21.3419 17.2794 21.4999 16.8978 21.4999 16.5V4.5C21.4999 4.10218 21.3419 3.72064 21.0606 3.43934C20.7793 3.15804 20.3978 3 19.9999 3Z" />
</SVG>
);
export default openSignup;

View file

@ -1,14 +1,13 @@
import BadgeBox from '../BadgeBox';
import { __, sprintf } from '@wordpress/i18n';
import BadgeBox from '../BadgeBox';
import Separator from '../Separator';
import generatePriceText from '../../../utils/badgeBoxUtils';
import { countryPriceInfo } from '../../../utils/countryPriceInfo';
import PricingTitleBadge from '../PricingTitleBadge';
const AcdcOptionalPaymentMethods = ( {
isFastlane,
isPayLater,
storeCountry,
storeCurrency,
} ) => {
if ( isFastlane && isPayLater && storeCountry === 'US' ) {
return (
@ -24,11 +23,7 @@ const AcdcOptionalPaymentMethods = ( {
'icon-button-amex.svg',
'icon-button-discover.svg',
] }
textBadge={ generatePriceText(
'ccf',
countryPriceInfo[ storeCountry ],
storeCurrency
) }
textBadge={ <PricingTitleBadge item="ccf" /> }
description={ sprintf(
// translators: %s: Link to PayPal business fees guide
__(
@ -48,11 +43,7 @@ const AcdcOptionalPaymentMethods = ( {
'icon-button-apple-pay.svg',
'icon-button-google-pay.svg',
] }
textBadge={ generatePriceText(
'dw',
countryPriceInfo[ storeCountry ],
storeCurrency
) }
textBadge={ <PricingTitleBadge item="dw" /> }
description={ sprintf(
// translators: %s: Link to PayPal business fees guide
__(
@ -69,16 +60,11 @@ const AcdcOptionalPaymentMethods = ( {
'woocommerce-paypal-payments'
) }
imageBadge={ [
'icon-button-sepa.svg',
'icon-button-ideal.svg',
'icon-button-blik.svg',
'icon-button-bancontact.svg',
] }
textBadge={ generatePriceText(
'apm',
countryPriceInfo[ storeCountry ],
storeCurrency
) }
textBadge={ <PricingTitleBadge item="apm" /> }
description={ sprintf(
// translators: %s: Link to PayPal business fees guide
__(
@ -92,11 +78,9 @@ const AcdcOptionalPaymentMethods = ( {
<BadgeBox
title={ __( '', 'woocommerce-paypal-payments' ) }
imageBadge={ [ 'icon-payment-method-fastlane-small.svg' ] }
textBadge={ generatePriceText(
'fastlane',
countryPriceInfo[ storeCountry ],
storeCurrency
) }
textBadge={
<PricingTitleBadge item="fast country currency=storeCurrency=storeCountrylane" />
}
description={ sprintf(
// translators: %s: Link to PayPal business fees guide
__(
@ -124,11 +108,7 @@ const AcdcOptionalPaymentMethods = ( {
'icon-button-amex.svg',
'icon-button-discover.svg',
] }
textBadge={ generatePriceText(
'ccf',
countryPriceInfo[ storeCountry ],
storeCurrency
) }
textBadge={ <PricingTitleBadge item="ccf" /> }
description={ sprintf(
// translators: %s: Link to PayPal business fees guide
__(
@ -148,11 +128,7 @@ const AcdcOptionalPaymentMethods = ( {
'icon-button-apple-pay.svg',
'icon-button-google-pay.svg',
] }
textBadge={ generatePriceText(
'dw',
countryPriceInfo[ storeCountry ],
storeCurrency
) }
textBadge={ <PricingTitleBadge item="dw" /> }
description={ sprintf(
// translators: %s: Link to PayPal business fees guide
__(
@ -174,11 +150,7 @@ const AcdcOptionalPaymentMethods = ( {
'icon-button-blik.svg',
'icon-button-bancontact.svg',
] }
textBadge={ generatePriceText(
'apm',
countryPriceInfo[ storeCountry ],
storeCurrency
) }
textBadge={ <PricingTitleBadge item="apm" /> }
description={ sprintf(
// translators: %s: Link to PayPal business fees guide
__(
@ -205,11 +177,7 @@ const AcdcOptionalPaymentMethods = ( {
'icon-button-amex.svg',
'icon-button-discover.svg',
] }
textBadge={ generatePriceText(
'ccf',
countryPriceInfo[ storeCountry ],
storeCurrency
) }
textBadge={ <PricingTitleBadge item="ccf" /> }
description={ sprintf(
// translators: %s: Link to PayPal business fees guide
__(
@ -226,11 +194,7 @@ const AcdcOptionalPaymentMethods = ( {
'icon-button-apple-pay.svg',
'icon-button-google-pay.svg',
] }
textBadge={ generatePriceText(
'dw',
countryPriceInfo[ storeCountry ],
storeCurrency
) }
textBadge={ <PricingTitleBadge item="dw" /> }
description={ sprintf(
// translators: %s: Link to PayPal business fees guide
__(
@ -252,11 +216,7 @@ const AcdcOptionalPaymentMethods = ( {
'icon-button-blik.svg',
'icon-button-bancontact.svg',
] }
textBadge={ generatePriceText(
'apm',
countryPriceInfo[ storeCountry ],
storeCurrency
) }
textBadge={ <PricingTitleBadge item="apm" /> }
description={ sprintf(
// translators: %s: Link to PayPal business fees guide
__(

View file

@ -1,13 +1,9 @@
import BadgeBox from '../BadgeBox';
import { __, sprintf } from '@wordpress/i18n';
import generatePriceText from '../../../utils/badgeBoxUtils';
import { countryPriceInfo } from '../../../utils/countryPriceInfo';
const BcdcOptionalPaymentMethods = ( {
isPayLater,
storeCountry,
storeCurrency,
} ) => {
import BadgeBox from '../BadgeBox';
import PricingTitleBadge from '../PricingTitleBadge';
const BcdcOptionalPaymentMethods = ( { isPayLater, storeCountry } ) => {
if ( isPayLater && storeCountry === 'us' ) {
return (
<div className="ppcp-r-optional-payment-methods__wrapper">
@ -22,11 +18,9 @@ const BcdcOptionalPaymentMethods = ( {
'icon-button-amex.svg',
'icon-button-discover.svg',
] }
textBadge={ generatePriceText(
'standardCardFields',
countryPriceInfo[ storeCountry ],
storeCurrency
) }
textBadge={
<PricingTitleBadge item="standardCardFields" />
}
description={ sprintf(
// translators: %s: Link to PayPal REST application guide
__(
@ -53,11 +47,7 @@ const BcdcOptionalPaymentMethods = ( {
'icon-button-amex.svg',
'icon-button-discover.svg',
] }
textBadge={ generatePriceText(
'standardCardFields',
countryPriceInfo[ storeCountry ],
storeCurrency
) }
textBadge={ <PricingTitleBadge item="standardCardFields" /> }
description={ sprintf(
// translators: %s: Link to PayPal REST application guide
__(

View file

@ -6,7 +6,6 @@ const OptionalPaymentMethods = ( {
isFastlane,
isPayLater,
storeCountry,
storeCurrency,
} ) => {
return (
<div className="ppcp-r-optional-payment-methods">
@ -15,13 +14,11 @@ const OptionalPaymentMethods = ( {
isFastlane={ isFastlane }
isPayLater={ isPayLater }
storeCountry={ storeCountry }
storeCurrency={ storeCurrency }
/>
) : (
<BcdcOptionalPaymentMethods
isPayLater={ isPayLater }
storeCountry={ storeCountry }
storeCurrency={ storeCurrency }
/>
) }
</div>

View file

@ -11,7 +11,6 @@ const PaymentMethodIcons = ( props ) => {
<PaymentMethodIcon type="discover" icons={ props.icons } />
<PaymentMethodIcon type="apple-pay" icons={ props.icons } />
<PaymentMethodIcon type="google-pay" icons={ props.icons } />
<PaymentMethodIcon type="sepa" icons={ props.icons } />
<PaymentMethodIcon type="ideal" icons={ props.icons } />
<PaymentMethodIcon type="bancontact" icons={ props.icons } />
</div>

View file

@ -0,0 +1,38 @@
import { __, sprintf } from '@wordpress/i18n';
import { countryPriceInfo } from '../../utils/countryPriceInfo';
import { CommonHooks } from '../../data';
const PricingDescription = () => {
const { storeCountry } = CommonHooks.useWooSettings();
if ( ! countryPriceInfo[ storeCountry ] ) {
return null;
}
const lastDate = 'October 25th, 2024'; // TODO -- needs to be the last plugin update date.
const detailsUrl =
'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input';
const label = sprintf(
// translators: %1$s: Pricing date, %2$s Link to PayPal price-details page.
__(
'Prices based on domestic transactions as of %1$s. <a target="_blank" href="%2$s">Click here</a> for full pricing details.',
'woocommerce-paypal-payments'
),
lastDate,
detailsUrl
);
return (
<p
className="ppcp-r-optional-payment-methods__description"
data-country={ storeCountry }
>
<sup>1</sup>
<span dangerouslySetInnerHTML={ { __html: label } } />
</p>
);
};
export default PricingDescription;

View file

@ -0,0 +1,46 @@
import { __, sprintf } from '@wordpress/i18n';
import { CommonHooks } from '../../data';
import { countryPriceInfo } from '../../utils/countryPriceInfo';
import { formatPrice } from '../../utils/formatPrice';
import TitleBadge, { TITLE_BADGE_INFO } from './TitleBadge';
const getFixedAmount = ( currency, priceList, itemFixedAmount ) => {
if ( priceList[ currency ] ) {
const sum = priceList[ currency ] + itemFixedAmount;
return formatPrice( sum, currency );
}
const [ defaultCurrency, defaultPrice ] = Object.entries( priceList )[ 0 ];
const sum = defaultPrice + itemFixedAmount;
return formatPrice( sum, defaultCurrency );
};
const PricingTitleBadge = ( { item } ) => {
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[ itemKey ] ) {
return null;
}
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' ),
percentage,
fixedAmount
);
return (
<TitleBadge
type={ TITLE_BADGE_INFO }
text={ `${ label }<sup>1</sup>` }
/>
);
};
export default PricingTitleBadge;

View file

@ -9,11 +9,7 @@ import {
} from './SettingsBlockElements';
const SettingsAccordion = ( { title, description, children, ...props } ) => (
<SettingsBlock
{ ...props }
className="ppcp-r-settings-block__accordion"
components={ [
() => (
<SettingsBlock { ...props } className="ppcp-r-settings-block__accordion">
<Accordion
title={ title }
description={ description }
@ -25,9 +21,7 @@ const SettingsAccordion = ( { title, description, children, ...props } ) => (
>
{ children }
</Accordion>
),
] }
/>
</SettingsBlock>
);
export default SettingsAccordion;

View file

@ -1,20 +1,16 @@
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"
components={ [
() => (
<>
<SettingsBlock { ...props } className="ppcp-r-settings-block__button">
<Header>
<Title>{ title }</Title>
<Description>{ description }</Description>
</Header>
<Action>
<Button
isBusy={ props.actionProps?.isBusy }
variant={ props.actionProps?.buttonType }
onClick={
props.actionProps?.callback
@ -25,10 +21,7 @@ const ButtonSettingsBlock = ( { title, description, ...props } ) => (
{ props.actionProps.value }
</Button>
</Action>
</>
),
] }
/>
</SettingsBlock>
);
export default ButtonSettingsBlock;

View file

@ -11,30 +11,21 @@ const FeatureSettingsBlock = ( { title, description, ...props } ) => {
}
return (
<>
<span className="ppcp-r-feature-item__notes">
{ notes.map( ( note, index ) => (
<span key={ index }>{ note }</span>
) ) }
</span>
</>
);
};
return (
<SettingsBlock
{ ...props }
className="ppcp-r-settings-block__feature"
components={ [
() => (
<>
<SettingsBlock { ...props } className="ppcp-r-settings-block__feature">
<Header>
<Title>
{ title }
{ props.actionProps?.featureStatus && (
<TitleBadge
{ ...props.actionProps?.badge }
/>
{ props.actionProps?.enabled && (
<TitleBadge { ...props.actionProps?.badge } />
) }
</Title>
<Description className="ppcp-r-settings-block__feature__description">
@ -44,23 +35,19 @@ const FeatureSettingsBlock = ( { title, description, ...props } ) => {
</Header>
<Action>
<div className="ppcp-r-feature-item__buttons">
{ props.actionProps?.buttons.map(
( button ) => (
{ props.actionProps?.buttons.map( ( button ) => (
<Button
className={ button.class ? button.class : '' }
href={ button.url }
key={ button.text }
variant={ button.type }
>
{ button.text }
</Button>
)
) }
) ) }
</div>
</Action>
</>
),
] }
/>
</SettingsBlock>
);
};

View file

@ -42,12 +42,7 @@ const InputSettingsBlock = ( {
order = DEFAULT_ELEMENT_ORDER,
...props
} ) => (
<SettingsBlock
{ ...props }
className="ppcp-r-settings-block__input"
components={ [
() => (
<>
<SettingsBlock { ...props } className="ppcp-r-settings-block__input">
{ order.map( ( elementKey ) => {
const RenderElement = ELEMENT_RENDERERS[ elementKey ];
return RenderElement ? (
@ -60,10 +55,7 @@ const InputSettingsBlock = ( {
/>
) : null;
} ) }
</>
),
] }
/>
</SettingsBlock>
);
export default InputSettingsBlock;

View file

@ -5,20 +5,13 @@ import PaymentMethodIcon from '../PaymentMethodIcon';
import data from '../../../utils/data';
const PaymentMethodItemBlock = ( props ) => {
const [ paymentMethodState, setPaymentMethodState ] = useState();
const [ toggleIsChecked, setToggleIsChecked ] = useState( false );
const [ modalIsVisible, setModalIsVisible ] = useState( false );
const Modal = props?.modal;
const handleCheckboxState = ( checked ) => {
setPaymentMethodState( checked ? props.id : null );
};
return (
<>
<SettingsBlock
className="ppcp-r-settings-block__payment-methods__item"
components={ [
() => (
<SettingsBlock className="ppcp-r-settings-block__payment-methods__item">
<div className="ppcp-r-settings-block__payment-methods__item__inner">
<div className="ppcp-r-settings-block__payment-methods__item__title-wrapper">
<PaymentMethodIcon
@ -35,26 +28,20 @@ const PaymentMethodItemBlock = ( props ) => {
<div className="ppcp-r-settings-block__payment-methods__item__footer">
<ToggleControl
__nextHasNoMarginBottom={ true }
checked={ props.id === paymentMethodState }
onChange={ handleCheckboxState }
checked={ toggleIsChecked }
onChange={ setToggleIsChecked }
/>
{ Modal && (
<div
className="ppcp-r-settings-block__payment-methods__item__settings"
onClick={ () =>
setModalIsVisible( true )
}
onClick={ () => setModalIsVisible( true ) }
>
{ data().getImage(
'icon-settings.svg'
) }
{ data().getImage( 'icon-settings.svg' ) }
</div>
) }
</div>
</div>
),
] }
/>
</SettingsBlock>
{ Modal && modalIsVisible && (
<Modal setModalIsVisible={ setModalIsVisible } />
) }

View file

@ -1,7 +1,14 @@
import { useState, useCallback } from '@wordpress/element';
import SettingsBlock from './SettingsBlock';
import PaymentMethodItemBlock from './PaymentMethodItemBlock';
const PaymentMethodsBlock = ( { paymentMethods, className = '' } ) => {
const [ selectedMethod, setSelectedMethod ] = useState( null );
const handleSelect = useCallback( ( methodId, isSelected ) => {
setSelectedMethod( isSelected ? methodId : null );
}, [] );
if ( paymentMethods.length === 0 ) {
return null;
}
@ -9,19 +16,18 @@ const PaymentMethodsBlock = ( { paymentMethods, className = '' } ) => {
return (
<SettingsBlock
className={ `ppcp-r-settings-block__payment-methods ${ className }` }
components={ [
() => (
<>
>
{ paymentMethods.map( ( paymentMethod ) => (
<PaymentMethodItemBlock
key={ paymentMethod.id }
{ ...paymentMethod }
isSelected={ selectedMethod === paymentMethod.id }
onSelect={ ( checked ) =>
handleSelect( paymentMethod.id, checked )
}
/>
) ) }
</>
),
] }
/>
</SettingsBlock>
);
};

View file

@ -11,9 +11,7 @@ const RadioSettingsBlock = ( {
<SettingsBlock
{ ...props }
className="ppcp-r-settings-block__radio ppcp-r-settings-block--expert-rdb"
components={ [
() => (
<>
>
<Header>
<Title>{ title }</Title>
<Description>{ description }</Description>
@ -38,10 +36,7 @@ const RadioSettingsBlock = ( {
{ option.additionalContent }
</PayPalRdbWithContent>
) ) }
</>
),
] }
/>
</SettingsBlock>
);
export default RadioSettingsBlock;

View file

@ -35,12 +35,7 @@ const SelectSettingsBlock = ( {
order = DEFAULT_ELEMENT_ORDER,
...props
} ) => (
<SettingsBlock
{ ...props }
className="ppcp-r-settings-block__select"
components={ [
() => (
<>
<SettingsBlock { ...props } className="ppcp-r-settings-block__select">
{ order.map( ( elementKey ) => {
const RenderElement = ELEMENT_RENDERERS[ elementKey ];
return RenderElement ? (
@ -52,10 +47,7 @@ const SelectSettingsBlock = ( {
/>
) : null;
} ) }
</>
),
] }
/>
</SettingsBlock>
);
export default SelectSettingsBlock;

View file

@ -1,15 +1,9 @@
const SettingsBlock = ( { className, components = [] } ) => {
const SettingsBlock = ( { className, children } ) => {
const blockClassName = [ 'ppcp-r-settings-block', className ].filter(
Boolean
);
return (
<div className={ blockClassName.join( ' ' ) }>
{ components.map( ( Component, index ) => (
<Component key={ index } />
) ) }
</div>
);
return <div className={ blockClassName.join( ' ' ) }>{ children }</div>;
};
export default SettingsBlock;

View file

@ -3,11 +3,7 @@ import SettingsBlock from './SettingsBlock';
import { Header, Title, Action, Description } from './SettingsBlockElements';
const ToggleSettingsBlock = ( { title, description, ...props } ) => (
<SettingsBlock
{ ...props }
className="ppcp-r-settings-block__toggle"
components={ [
() => (
<SettingsBlock { ...props } className="ppcp-r-settings-block__toggle">
<Action>
<ToggleControl
className="ppcp-r-settings-block__toggle-control"
@ -21,17 +17,11 @@ const ToggleSettingsBlock = ( { title, description, ...props } ) => (
}
/>
</Action>
),
() => (
<Header>
{ title && <Title>{ title }</Title> }
{ description && (
<Description>{ description }</Description>
) }
{ description && <Description>{ description }</Description> }
</Header>
),
] }
/>
</SettingsBlock>
);
export default ToggleSettingsBlock;

View file

@ -1,23 +1,17 @@
import { ToggleControl } from '@wordpress/components';
import { useRef } from '@wordpress/element';
import SpinnerOverlay from './SpinnerOverlay';
const SettingsToggleBlock = ( {
isToggled,
setToggled,
isLoading = false,
disabled = false,
...props
} ) => {
const toggleRef = useRef( null );
const blockClasses = [ 'ppcp-r-toggle-block' ];
if ( isLoading ) {
blockClasses.push( 'ppcp--is-loading' );
}
const handleLabelClick = () => {
if ( ! toggleRef.current || isLoading ) {
if ( ! toggleRef.current || disabled ) {
return;
}
@ -52,13 +46,12 @@ const SettingsToggleBlock = ( {
ref={ toggleRef }
checked={ isToggled }
onChange={ ( newState ) => setToggled( newState ) }
disabled={ isLoading }
disabled={ disabled }
/>
</div>
</div>
{ props.children && isToggled && (
<div className="ppcp-r-toggle-block__toggled-content">
{ isLoading && <SpinnerOverlay /> }
{ props.children }
</div>
) }

View file

@ -1,8 +1,13 @@
import { Spinner } from '@wordpress/components';
const SpinnerOverlay = () => {
const SpinnerOverlay = ( { message = '' } ) => {
return (
<div className="ppcp-r-spinner-overlay">
{ message && (
<span className="ppcp-r-spinner-overlay__message">
{ message }
</span>
) }
<Spinner />
</div>
);

View file

@ -1,6 +1,7 @@
import { useCallback, useEffect, useState } from '@wordpress/element';
import { TabPanel } from '@wordpress/components';
import { getQuery, updateQueryString } from '@woocommerce/navigation';
import { getQuery, updateQueryString } from '../../utils/navigation';
const TabNavigation = ( { tabs } ) => {
const { panel } = getQuery();
@ -30,7 +31,7 @@ const TabNavigation = ( { tabs } ) => {
);
useEffect( () => {
updateQueryString( { panel: activePanel }, '/', getQuery() );
updateQueryString( { panel: activePanel } );
}, [ activePanel ] );
return (

View file

@ -1,17 +1,11 @@
import BadgeBox, { BADGE_BOX_TITLE_BIG } from '../BadgeBox';
import { __, sprintf } from '@wordpress/i18n';
import BadgeBox, { BADGE_BOX_TITLE_BIG } from '../BadgeBox';
import Separator from '../Separator';
import generatePriceText from '../../../utils/badgeBoxUtils';
import { countryPriceInfo } from '../../../utils/countryPriceInfo';
import OptionalPaymentMethods from '../OptionalPaymentMethods/OptionalPaymentMethods';
import PricingTitleBadge from '../PricingTitleBadge';
const AcdcFlow = ( {
isFastlane,
isPayLater,
storeCountry,
storeCurrency,
} ) => {
const AcdcFlow = ( { isFastlane, isPayLater, storeCountry } ) => {
if ( isFastlane && isPayLater && storeCountry === 'US' ) {
return (
<div className="ppcp-r-welcome-docs__wrapper">
@ -22,11 +16,7 @@ const AcdcFlow = ( {
'woocommerce-paypal-payments'
) }
titleType={ BADGE_BOX_TITLE_BIG }
textBadge={ generatePriceText(
'checkout',
countryPriceInfo[ storeCountry ],
storeCurrency
) }
textBadge={ <PricingTitleBadge item="checkout" /> }
description={ __(
'Our all-in-one checkout solution lets you offer PayPal, Venmo, Pay Later options, and more to help maximise conversion',
'woocommerce-paypal-payments'
@ -63,10 +53,13 @@ const AcdcFlow = ( {
imageBadge={ [
'icon-payment-method-paypal-small.svg',
] }
textBadge={
<PricingTitleBadge item="plater" />
}
description={ sprintf(
// translators: %s: Link to PayPal business fees guide
__(
'Offer installment payment options and get paid upfront - at no extra cost to you. <a target="_blank" href="%s">Learn more</a>',
'Offer installment payment options and get paid upfront. <a target="_blank" href="%s">Learn more</a>',
'woocommerce-paypal-payments'
),
'https://www.paypal.com/us/business/paypal-business-fees'
@ -116,7 +109,6 @@ const AcdcFlow = ( {
isFastlane={ isFastlane }
isPayLater={ isPayLater }
storeCountry={ storeCountry }
storeCurrency={ storeCurrency }
/>
</div>
</div>
@ -133,11 +125,7 @@ const AcdcFlow = ( {
'woocommerce-paypal-payments'
) }
titleType={ BADGE_BOX_TITLE_BIG }
textBadge={ generatePriceText(
'checkout',
countryPriceInfo[ storeCountry ],
storeCurrency
) }
textBadge={ <PricingTitleBadge item="checkout" /> }
description={ __(
'Our all-in-one checkout solution lets you offer PayPal, Venmo, Pay Later options, and more to help maximise conversion',
'woocommerce-paypal-payments'
@ -201,7 +189,6 @@ const AcdcFlow = ( {
isFastlane={ isFastlane }
isPayLater={ isPayLater }
storeCountry={ storeCountry }
storeCurrency={ storeCurrency }
/>
</div>
</div>
@ -217,11 +204,7 @@ const AcdcFlow = ( {
'woocommerce-paypal-payments'
) }
titleType={ BADGE_BOX_TITLE_BIG }
textBadge={ generatePriceText(
'checkout',
countryPriceInfo[ storeCountry ],
storeCurrency
) }
textBadge={ <PricingTitleBadge item="checkout" /> }
description={ __(
'Our all-in-one checkout solution lets you offer PayPal, Venmo, Pay Later options, and more to help maximise conversion',
'woocommerce-paypal-payments'
@ -256,7 +239,7 @@ const AcdcFlow = ( {
description={ sprintf(
// translators: %s: Link to PayPal REST application guide
__(
'Offer installment payment options and get paid upfront - at no extra cost to you. <a target="_blank" href="%s">Learn more</a>',
'Offer installment payment options and get paid upfront. <a target="_blank" href="%s">Learn more</a>',
'woocommerce-paypal-payments'
),
'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input '
@ -280,7 +263,6 @@ const AcdcFlow = ( {
isFastlane={ isFastlane }
isPayLater={ isPayLater }
storeCountry={ storeCountry }
storeCurrency={ storeCurrency }
/>
</div>
</div>

View file

@ -1,11 +1,11 @@
import BadgeBox, { BADGE_BOX_TITLE_BIG } from '../BadgeBox';
import { __, sprintf } from '@wordpress/i18n';
import Separator from '../Separator';
import generatePriceText from '../../../utils/badgeBoxUtils';
import { countryPriceInfo } from '../../../utils/countryPriceInfo';
import OptionalPaymentMethods from '../OptionalPaymentMethods/OptionalPaymentMethods';
const BcdcFlow = ( { isPayLater, storeCountry, storeCurrency } ) => {
import BadgeBox, { BADGE_BOX_TITLE_BIG } from '../BadgeBox';
import Separator from '../Separator';
import OptionalPaymentMethods from '../OptionalPaymentMethods/OptionalPaymentMethods';
import PricingTitleBadge from '../PricingTitleBadge';
const BcdcFlow = ( { isPayLater, storeCountry } ) => {
if ( isPayLater && storeCountry === 'US' ) {
return (
<div className="ppcp-r-welcome-docs__wrapper">
@ -16,11 +16,7 @@ const BcdcFlow = ( { isPayLater, storeCountry, storeCurrency } ) => {
'woocommerce-paypal-payments'
) }
titleType={ BADGE_BOX_TITLE_BIG }
textBadge={ generatePriceText(
'checkout',
countryPriceInfo[ storeCountry ],
storeCurrency
) }
textBadge={ <PricingTitleBadge item="checkout" /> }
description={ __(
'Our all-in-one checkout solution lets you offer PayPal, Venmo, Pay Later options, and more to help maximise conversion',
'woocommerce-paypal-payments'
@ -60,7 +56,7 @@ const BcdcFlow = ( { isPayLater, storeCountry, storeCurrency } ) => {
description={ sprintf(
// translators: %s: Link to PayPal REST application guide
__(
'Offer installment payment options and get paid upfront - at no extra cost to you. <a target="_blank" href="%s">Learn more</a>',
'Offer installment payment options and get paid upfront. <a target="_blank" href="%s">Learn more</a>',
'woocommerce-paypal-payments'
),
'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input '
@ -110,7 +106,6 @@ const BcdcFlow = ( { isPayLater, storeCountry, storeCurrency } ) => {
isFastlane={ false }
isPayLater={ isPayLater }
storeCountry={ storeCountry }
storeCurrency={ storeCurrency }
/>
</div>
</div>
@ -122,11 +117,7 @@ const BcdcFlow = ( { isPayLater, storeCountry, storeCurrency } ) => {
<BadgeBox
title={ __( 'PayPal Checkout', 'woocommerce-paypal-payments' ) }
titleType={ BADGE_BOX_TITLE_BIG }
textBadge={ generatePriceText(
'checkout',
countryPriceInfo[ storeCountry ],
storeCurrency
) }
textBadge={ <PricingTitleBadge item="checkout" /> }
description={ __(
'Our all-in-one checkout solution lets you offer PayPal, Venmo, Pay Later options, and more to help maximise conversion',
'woocommerce-paypal-payments'
@ -158,7 +149,7 @@ const BcdcFlow = ( { isPayLater, storeCountry, storeCurrency } ) => {
description={ sprintf(
// translators: %s: Link to PayPal REST application guide
__(
'Offer installment payment options and get paid upfront - at no extra cost to you. <a target="_blank" href="%s">Learn more</a>',
'Offer installment payment options and get paid upfront. <a target="_blank" href="%s">Learn more</a>',
'woocommerce-paypal-payments'
),
'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input '
@ -181,7 +172,6 @@ const BcdcFlow = ( { isPayLater, storeCountry, storeCurrency } ) => {
isFastlane={ false }
isPayLater={ isPayLater }
storeCountry={ storeCountry }
storeCurrency={ storeCurrency }
/>
</div>
);

View file

@ -1,24 +1,10 @@
import { __, sprintf } from '@wordpress/i18n';
import { __ } from '@wordpress/i18n';
import PricingDescription from '../PricingDescription';
import AcdcFlow from './AcdcFlow';
import BcdcFlow from './BcdcFlow';
import { Button } from '@wordpress/components';
const WelcomeDocs = ( {
useAcdc,
isFastlane,
isPayLater,
storeCountry,
storeCurrency,
} ) => {
const pricesBasedDescription = sprintf(
// translators: %s: Link to PayPal REST application guide
__(
'<sup>1</sup>Prices based on domestic transactions as of October 25th, 2024. <a target="_blank" href="%s">Click here</a> for full pricing details.',
'woocommerce-paypal-payments'
),
'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input '
);
const WelcomeDocs = ( { useAcdc, isFastlane, isPayLater, storeCountry } ) => {
return (
<div className="ppcp-r-welcome-docs">
<h2 className="ppcp-r-welcome-docs__title">
@ -32,19 +18,14 @@ const WelcomeDocs = ( {
isFastlane={ isFastlane }
isPayLater={ isPayLater }
storeCountry={ storeCountry }
storeCurrency={ storeCurrency }
/>
) : (
<BcdcFlow
isPayLater={ isPayLater }
storeCountry={ storeCountry }
storeCurrency={ storeCurrency }
/>
) }
<p
className="ppcp-r-optional-payment-methods__description"
dangerouslySetInnerHTML={ { __html: pricesBasedDescription } }
></p>
<PricingDescription />
</div>
);
};

View file

@ -1,96 +1,118 @@
import { __, sprintf } from '@wordpress/i18n';
import { Button, TextControl } from '@wordpress/components';
import { useRef, useMemo } from '@wordpress/element';
import { useDispatch } from '@wordpress/data';
import { store as noticesStore } from '@wordpress/notices';
import {
useRef,
useState,
useEffect,
useMemo,
useCallback,
} from '@wordpress/element';
import classNames from 'classnames';
import SettingsToggleBlock from '../../../ReusableComponents/SettingsToggleBlock';
import Separator from '../../../ReusableComponents/Separator';
import DataStoreControl from '../../../ReusableComponents/DataStoreControl';
import { CommonHooks } from '../../../../data';
import { openPopup } from '../../../../utils/window';
import {
useSandboxConnection,
useManualConnection,
} from '../../../../hooks/useHandleConnections';
import ConnectionButton from './ConnectionButton';
import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper';
const FORM_ERRORS = {
noClientId: __(
'Please enter your Client ID',
'woocommerce-paypal-payments'
),
noClientSecret: __(
'Please enter your Secret Key',
'woocommerce-paypal-payments'
),
invalidClientId: __(
'Please enter a valid Client ID',
'woocommerce-paypal-payments'
),
};
const AdvancedOptionsForm = () => {
const [ clientValid, setClientValid ] = useState( false );
const [ secretValid, setSecretValid ] = useState( false );
const AdvancedOptionsForm = ( { setCompleted } ) => {
const { isBusy } = CommonHooks.useBusyState();
const { isSandboxMode, setSandboxMode, connectViaSandbox } =
CommonHooks.useSandbox();
const { isSandboxMode, setSandboxMode } = useSandboxConnection();
const {
handleConnectViaIdAndSecret,
isManualConnectionMode,
setManualConnectionMode,
clientId,
setClientId,
clientSecret,
setClientSecret,
connectViaIdAndSecret,
} = CommonHooks.useManualConnection();
} = useManualConnection();
const { createSuccessNotice, createErrorNotice } =
useDispatch( noticesStore );
const refClientId = useRef( null );
const refClientSecret = useRef( null );
const isValidClientId = useMemo( () => {
return /^A[\w-]{79}$/.test( clientId );
}, [ clientId ] );
const validateManualConnectionForm = useCallback( () => {
const checks = [
{
ref: refClientId,
valid: () => clientId,
errorMessage: FORM_ERRORS.noClientId,
},
{
ref: refClientId,
valid: () => clientValid,
errorMessage: FORM_ERRORS.invalidClientId,
},
{
ref: refClientSecret,
valid: () => clientSecret && secretValid,
errorMessage: FORM_ERRORS.noClientSecret,
},
];
const isFormValid = useMemo( () => {
return isValidClientId && clientId && clientSecret;
}, [ isValidClientId, clientId, clientSecret ] );
const handleServerError = ( res, genericMessage ) => {
console.error( 'Connection error', res );
createErrorNotice( res?.message ?? genericMessage );
};
const handleServerSuccess = () => {
createSuccessNotice(
__( 'Connected to PayPal', 'woocommerce-paypal-payments' )
);
setCompleted( true );
};
const handleSandboxConnect = async () => {
const res = await connectViaSandbox();
if ( ! res.success || ! res.data ) {
handleServerError(
res,
__(
'Could not generate a Sandbox login link.',
'woocommerce-paypal-payments'
)
);
return;
for ( const { ref, valid, errorMessage } of checks ) {
if ( valid() ) {
continue;
}
const connectionUrl = res.data;
const popup = openPopup( connectionUrl );
if ( ! popup ) {
createErrorNotice(
__(
'Popup blocked. Please allow popups for this site to connect to PayPal.',
'woocommerce-paypal-payments'
)
);
ref?.current?.focus();
throw new Error( errorMessage );
}
};
}, [ clientId, clientSecret, clientValid, secretValid ] );
const handleManualConnect = async () => {
const res = await connectViaIdAndSecret();
if ( res.success ) {
handleServerSuccess();
} else {
handleServerError(
res,
__(
'Could not connect to PayPal. Please make sure your Client ID and Secret Key are correct.',
'woocommerce-paypal-payments'
)
const handleManualConnect = useCallback(
() =>
handleConnectViaIdAndSecret( {
validation: validateManualConnectionForm,
} ),
[ handleConnectViaIdAndSecret, validateManualConnectionForm ]
);
useEffect( () => {
setClientValid( ! clientId || /^A[\w-]{79}$/.test( clientId ) );
setSecretValid( clientSecret && clientSecret.length > 0 );
}, [ clientId, clientSecret ] );
const clientIdLabel = useMemo(
() =>
isSandboxMode
? __( 'Sandbox Client ID', 'woocommerce-paypal-payments' )
: __( 'Live Client ID', 'woocommerce-paypal-payments' ),
[ isSandboxMode ]
);
const secretKeyLabel = useMemo(
() =>
isSandboxMode
? __( 'Sandbox Secret Key', 'woocommerce-paypal-payments' )
: __( 'Live Secret Key', 'woocommerce-paypal-payments' ),
[ isSandboxMode ]
);
}
};
const advancedUsersDescription = sprintf(
// translators: %s: Link to PayPal REST application guide
@ -103,6 +125,7 @@ const AdvancedOptionsForm = ( { setCompleted } ) => {
return (
<>
<BusyStateWrapper>
<SettingsToggleBlock
label={ __(
'Enable Sandbox Mode',
@ -114,77 +137,72 @@ const AdvancedOptionsForm = ( { setCompleted } ) => {
) }
isToggled={ !! isSandboxMode }
setToggled={ setSandboxMode }
isLoading={ isBusy }
>
<Button onClick={ handleSandboxConnect } variant="secondary">
{ __( 'Connect Account', 'woocommerce-paypal-payments' ) }
</Button>
</SettingsToggleBlock>
<Separator withLine={ false } />
<SettingsToggleBlock
label={
__( 'Manually Connect', 'woocommerce-paypal-payments' ) +
( isBusy ? ' ...' : '' )
<ConnectionButton
title={ __(
'Connect Account',
'woocommerce-paypal-payments'
) }
showIcon={ false }
variant="secondary"
className="small-button"
isSandbox={
true /* This button always connects to sandbox */
}
/>
</SettingsToggleBlock>
</BusyStateWrapper>
<Separator withLine={ false } />
<BusyStateWrapper
onBusy={ ( props ) => ( {
disabled: true,
label: props.label + ' ...',
} ) }
>
<SettingsToggleBlock
label={ __(
'Manually Connect',
'woocommerce-paypal-payments'
) }
description={ advancedUsersDescription }
isToggled={ !! isManualConnectionMode }
setToggled={ setManualConnectionMode }
isLoading={ isBusy }
>
<DataStoreControl
control={ TextControl }
ref={ refClientId }
label={
isSandboxMode
? __(
'Sandbox Client ID',
'woocommerce-paypal-payments'
)
: __(
'Live Client ID',
'woocommerce-paypal-payments'
)
}
label={ clientIdLabel }
value={ clientId }
onChange={ setClientId }
className={
clientId && ! isValidClientId ? 'has-error' : ''
}
className={ classNames( {
'has-error': ! clientValid,
} ) }
/>
{ clientId && ! isValidClientId && (
{ clientValid || (
<p className="client-id-error">
{ __(
'Please enter a valid Client ID',
'woocommerce-paypal-payments'
) }
{ FORM_ERRORS.invalidClientId }
</p>
) }
<DataStoreControl
control={ TextControl }
ref={ refClientSecret }
label={
isSandboxMode
? __(
'Sandbox Secret Key',
'woocommerce-paypal-payments'
)
: __(
'Live Secret Key',
'woocommerce-paypal-payments'
)
}
label={ secretKeyLabel }
value={ clientSecret }
onChange={ setClientSecret }
type="password"
/>
<Button
variant="secondary"
className="small-button"
onClick={ handleManualConnect }
disabled={ ! isFormValid }
>
{ __( 'Connect Account', 'woocommerce-paypal-payments' ) }
{ __(
'Connect Account',
'woocommerce-paypal-payments'
) }
</Button>
</SettingsToggleBlock>
</BusyStateWrapper>
</>
);
};

View file

@ -0,0 +1,49 @@
import { Button } from '@wordpress/components';
import classNames from 'classnames';
import { CommonHooks } from '../../../../data';
import { openSignup } from '../../../ReusableComponents/Icons';
import {
useProductionConnection,
useSandboxConnection,
} from '../../../../hooks/useHandleConnections';
import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper';
const ConnectionButton = ( {
title,
isSandbox = false,
variant = 'primary',
showIcon = true,
className = '',
} ) => {
const { handleSandboxConnect } = useSandboxConnection();
const { handleProductionConnect } = useProductionConnection();
const buttonClassName = classNames( 'ppcp-r-connection-button', className, {
'sandbox-mode': isSandbox,
'live-mode': ! isSandbox,
} );
const handleConnectClick = async () => {
if ( isSandbox ) {
await handleSandboxConnect();
} else {
await handleProductionConnect();
}
};
return (
<BusyStateWrapper>
<Button
className={ buttonClassName }
variant={ variant }
icon={ showIcon ? openSignup : null }
onClick={ handleConnectClick }
>
<span className="button-title">{ title }</span>
</Button>
</BusyStateWrapper>
);
};
export default ConnectionButton;

View file

@ -6,6 +6,7 @@ import classNames from 'classnames';
import { OnboardingHooks } from '../../../../data';
import useIsScrolled from '../../../../hooks/useIsScrolled';
import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper';
const Navigation = ( { stepDetails, onNext, onPrev, onExit } ) => {
const { title, isFirst, percentage, showNext, canProceed } = stepDetails;
@ -20,7 +21,11 @@ const Navigation = ( { stepDetails, onNext, onPrev, onExit } ) => {
return (
<div className={ className }>
<div className="ppcp-r-navigation">
<div className="ppcp-r-navigation--left">
<BusyStateWrapper
className="ppcp-r-navigation--left"
busySpinner={ false }
enabled={ ! isFirst }
>
<Button
variant="link"
onClick={ isFirst ? onExit : onPrev }
@ -31,7 +36,7 @@ const Navigation = ( { stepDetails, onNext, onPrev, onExit } ) => {
{ title }
</span>
</Button>
</div>
</BusyStateWrapper>
{ ! isFirst &&
NextButton( { showNext, isDisabled, onNext, onExit } ) }
<ProgressBar percent={ percentage } />
@ -42,7 +47,10 @@ const Navigation = ( { stepDetails, onNext, onPrev, onExit } ) => {
const NextButton = ( { showNext, isDisabled, onNext, onExit } ) => {
return (
<div className="ppcp-r-navigation--right">
<BusyStateWrapper
className="ppcp-r-navigation--right"
busySpinner={ false }
>
<Button variant="link" onClick={ onExit }>
{ __( 'Save and exit', 'woocommerce-paypal-payments' ) }
</Button>
@ -55,7 +63,7 @@ const NextButton = ( { showNext, isDisabled, onNext, onExit } ) => {
{ __( 'Continue', 'woocommerce-paypal-payments' ) }
</Button>
) }
</div>
</BusyStateWrapper>
);
};

View file

@ -5,8 +5,7 @@ import { getSteps, getCurrentStep } from './availableSteps';
import Navigation from './Components/Navigation';
const Onboarding = () => {
const { step, setStep, setCompleted, flags } = OnboardingHooks.useSteps();
const { step, setStep, flags } = OnboardingHooks.useSteps();
const Steps = getSteps( flags );
const currentStep = getCurrentStep( step, Steps );
@ -30,7 +29,6 @@ const Onboarding = () => {
<currentStep.StepComponent
setStep={ setStep }
currentStep={ step }
setCompleted={ setCompleted }
stepperOrder={ Steps }
/>
</div>

View file

@ -1,28 +1,9 @@
import { __ } from '@wordpress/i18n';
import { Button, Icon } from '@wordpress/components';
import OnboardingHeader from '../../ReusableComponents/OnboardingHeader';
import ConnectionButton from './Components/ConnectionButton';
const StepCompleteSetup = ( { setCompleted } ) => {
const ButtonIcon = () => (
<Icon
icon={ () => (
<svg
width="25"
height="24"
viewBox="0 0 25 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12.4999 12.75V18.75C12.4999 18.9489 12.4209 19.1397 12.2803 19.2803C12.1396 19.421 11.9488 19.5 11.7499 19.5C11.551 19.5 11.3603 19.421 11.2196 19.2803C11.0789 19.1397 10.9999 18.9489 10.9999 18.75V14.5613L4.78055 20.7806C4.71087 20.8503 4.62815 20.9056 4.5371 20.9433C4.44606 20.981 4.34847 21.0004 4.24993 21.0004C4.15138 21.0004 4.0538 20.981 3.96276 20.9433C3.87171 20.9056 3.78899 20.8503 3.7193 20.7806C3.64962 20.7109 3.59435 20.6282 3.55663 20.5372C3.51892 20.4461 3.49951 20.3485 3.49951 20.25C3.49951 20.1515 3.51892 20.0539 3.55663 19.9628C3.59435 19.8718 3.64962 19.7891 3.7193 19.7194L9.93868 13.5H5.74993C5.55102 13.5 5.36025 13.421 5.2196 13.2803C5.07895 13.1397 4.99993 12.9489 4.99993 12.75C4.99993 12.5511 5.07895 12.3603 5.2196 12.2197C5.36025 12.079 5.55102 12 5.74993 12H11.7499C11.9488 12 12.1396 12.079 12.2803 12.2197C12.4209 12.3603 12.4999 12.5511 12.4999 12.75ZM19.9999 3H7.99993C7.6021 3 7.22057 3.15804 6.93927 3.43934C6.65796 3.72064 6.49993 4.10218 6.49993 4.5V9C6.49993 9.19891 6.57895 9.38968 6.7196 9.53033C6.86025 9.67098 7.05102 9.75 7.24993 9.75C7.44884 9.75 7.63961 9.67098 7.78026 9.53033C7.92091 9.38968 7.99993 9.19891 7.99993 9V4.5H19.9999V16.5H15.4999C15.301 16.5 15.1103 16.579 14.9696 16.7197C14.8289 16.8603 14.7499 17.0511 14.7499 17.25C14.7499 17.4489 14.8289 17.6397 14.9696 17.7803C15.1103 17.921 15.301 18 15.4999 18H19.9999C20.3978 18 20.7793 17.842 21.0606 17.5607C21.3419 17.2794 21.4999 16.8978 21.4999 16.5V4.5C21.4999 4.10218 21.3419 3.72064 21.0606 3.43934C20.7793 3.15804 20.3978 3 19.9999 3Z"
fill="white"
/>
</svg>
) }
/>
);
const StepCompleteSetup = () => {
return (
<div className="ppcp-r-page-products">
<OnboardingHeader
@ -37,18 +18,12 @@ const StepCompleteSetup = ( { setCompleted } ) => {
/>
<div className="ppcp-r-inner-container">
<div className="ppcp-r-onboarding-header__description">
<Button
variant="primary"
icon={ ButtonIcon }
onClick={ () => {
setCompleted( true );
} }
>
{ __(
<ConnectionButton
title={ __(
'Connect to PayPal',
'woocommerce-paypal-payments'
) }
</Button>
/>
</div>
</div>
</div>

View file

@ -1,10 +1,11 @@
import { __, sprintf } from '@wordpress/i18n';
import { __ } from '@wordpress/i18n';
import { CommonHooks, OnboardingHooks } from '../../../data';
import OnboardingHeader from '../../ReusableComponents/OnboardingHeader';
import SelectBoxWrapper from '../../ReusableComponents/SelectBoxWrapper';
import SelectBox from '../../ReusableComponents/SelectBox';
import { CommonHooks, OnboardingHooks } from '../../../data';
import OptionalPaymentMethods from '../../ReusableComponents/OptionalPaymentMethods/OptionalPaymentMethods';
import PricingDescription from '../../ReusableComponents/PricingDescription';
const OPM_RADIO_GROUP_NAME = 'optional-payment-methods';
@ -16,15 +17,6 @@ const StepPaymentMethods = ( {} ) => {
const { storeCountry, storeCurrency } = CommonHooks.useWooSettings();
const pricesBasedDescription = sprintf(
// translators: %s: Link to PayPal REST application guide
__(
'<sup>1</sup>Prices based on domestic transactions as of October 25th, 2024. <a target="_blank" href="%s">Click here</a> for full pricing details.',
'woocommerce-paypal-payments'
),
'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input '
);
return (
<div className="ppcp-r-page-optional-payment-methods">
<OnboardingHeader
@ -67,12 +59,7 @@ const StepPaymentMethods = ( {} ) => {
type="radio"
></SelectBox>
</SelectBoxWrapper>
<p
className="ppcp-r-optional-payment-methods__description"
dangerouslySetInnerHTML={ {
__html: pricesBasedDescription,
} }
></p>
<PricingDescription />
</div>
</div>
);

View file

@ -9,9 +9,11 @@ import AccordionSection from '../../ReusableComponents/AccordionSection';
import AdvancedOptionsForm from './Components/AdvancedOptionsForm';
import { CommonHooks } from '../../../data';
import BusyStateWrapper from '../../ReusableComponents/BusyStateWrapper';
const StepWelcome = ( { setStep, currentStep } ) => {
const { storeCountry } = CommonHooks.useWooSettings();
const StepWelcome = ( { setStep, currentStep, setCompleted } ) => {
const { storeCountry, storeCurrency } = CommonHooks.useWooSettings();
return (
<div className="ppcp-r-page-welcome">
<OnboardingHeader
@ -33,6 +35,7 @@ const StepWelcome = ( { setStep, currentStep, setCompleted } ) => {
'woocommerce-paypal-payments'
) }
</p>
<BusyStateWrapper>
<Button
className="ppcp-r-button-activate-paypal"
variant="primary"
@ -43,6 +46,7 @@ const StepWelcome = ( { setStep, currentStep, setCompleted } ) => {
'woocommerce-paypal-payments'
) }
</Button>
</BusyStateWrapper>
</div>
<Separator className="ppcp-r-page-welcome-mode-separator" />
<WelcomeDocs
@ -50,7 +54,6 @@ const StepWelcome = ( { setStep, currentStep, setCompleted } ) => {
isFastlane={ true }
isPayLater={ true }
storeCountry={ storeCountry }
storeCurrency={ storeCurrency }
/>
<Separator text={ __( 'or', 'woocommerce-paypal-payments' ) } />
<AccordionSection
@ -61,7 +64,7 @@ const StepWelcome = ( { setStep, currentStep, setCompleted } ) => {
className="onboarding-advanced-options"
id="advanced-options"
>
<AdvancedOptionsForm setCompleted={ setCompleted } />
<AdvancedOptionsForm />
</AccordionSection>
</div>
);

View file

@ -1,15 +1,49 @@
import { __ } from '@wordpress/i18n';
import { useState } from '@wordpress/element';
import { Button } from '@wordpress/components';
import { Button, Icon } from '@wordpress/components';
import { useDispatch } from '@wordpress/data';
import { reusableBlock } from '@wordpress/icons';
import SettingsCard from '../../ReusableComponents/SettingsCard';
import TodoSettingsBlock from '../../ReusableComponents/SettingsBlocks/TodoSettingsBlock';
import FeatureSettingsBlock from '../../ReusableComponents/SettingsBlocks/FeatureSettingsBlock';
import { TITLE_BADGE_POSITIVE } from '../../ReusableComponents/TitleBadge';
import data from '../../../utils/data';
import { useMerchantInfo } from '../../../data/common/hooks';
import { STORE_NAME } from '../../../data/common';
const TabOverview = () => {
const [ todos, setTodos ] = useState( [] );
const [ todosData, setTodosData ] = useState( todosDataDefault );
const [ isRefreshing, setIsRefreshing ] = useState( false );
const { merchant } = useMerchantInfo();
const { refreshFeatureStatuses } = useDispatch( STORE_NAME );
const features = featuresDefault.map( ( feature ) => {
const merchantFeature = merchant?.features?.[ feature.id ];
return {
...feature,
enabled: merchantFeature?.enabled ?? false,
};
} );
const refreshHandler = async () => {
setIsRefreshing( true );
const result = await refreshFeatureStatuses();
// TODO: Implement the refresh logic, remove this debug code -- PCP-4024
if ( result && ! result.success ) {
console.error(
'Failed to refresh features:',
result.message || 'Unknown error'
);
} else {
console.log( 'Features refreshed successfully.' );
}
setIsRefreshing( false );
};
return (
<div className="ppcp-r-tab-overview">
@ -38,39 +72,118 @@ const TabOverview = () => {
className="ppcp-r-tab-overview-features"
title={ __( 'Features', 'woocommerce-paypal-payments' ) }
description={
<div>
<p>{ __( 'Enable additional features…' ) }</p>
<p>{ __( 'Click Refresh…' ) }</p>
<Button variant="tertiary">
{ data().getImage( 'icon-refresh.svg' ) }
{ __( 'Refresh', 'woocommerce-paypal-payments' ) }
<>
<p>
{ __(
'Enable additional features and capabilities on your WooCommerce store.',
'woocommerce-paypal-payments'
) }
</p>
<p>
{ __(
'Click Refresh to update your current features after making changes.',
'woocommerce-paypal-payments'
) }
</p>
<Button
variant="tertiary"
onClick={ refreshHandler }
disabled={ isRefreshing }
>
<Icon icon={ reusableBlock } size={ 18 } />
{ isRefreshing
? __(
'Refreshing…',
'woocommerce-paypal-payments'
)
: __(
'Refresh',
'woocommerce-paypal-payments'
) }
</Button>
</div>
</>
}
contentItems={ featuresDefault.map( ( feature ) => (
contentItems={ features.map( ( feature ) => (
<FeatureSettingsBlock
key={ feature.id }
title={ feature.title }
description={ feature.description }
actionProps={ {
buttons: feature.buttons,
featureStatus: feature.featureStatus,
enabled: feature.enabled,
notes: feature.notes,
badge: {
badge: feature.enabled
? {
text: __(
'Active',
'woocommerce-paypal-payments'
),
type: TITLE_BADGE_POSITIVE,
},
}
: undefined,
} }
/>
) ) }
/>
<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>
);
};
// TODO: This list should be refactored into a separate module, maybe utils/thingsToDoNext.js
const todosDataDefault = [
{
value: 'paypal_later_messaging',
@ -106,6 +219,7 @@ const todosDataDefault = [
},
];
// TODO: Hardcoding this list here is not the best idea. Can we move this to a REST API response?
const featuresDefault = [
{
id: 'save_paypal_and_venmo',
@ -117,6 +231,7 @@ const featuresDefault = [
buttons: [
{
type: 'secondary',
class: 'small-button',
text: __( 'Configure', 'woocommerce-paypal-payments' ),
url: '#',
},
@ -133,7 +248,6 @@ const featuresDefault = [
'Advanced Credit and Debit Cards',
'woocommerce-paypal-payments'
),
featureStatus: true,
description: __(
'Process major credit and debit cards including Visa, Mastercard, American Express and Discover.',
'woocommerce-paypal-payments'
@ -141,6 +255,7 @@ const featuresDefault = [
buttons: [
{
type: 'secondary',
class: 'small-button',
text: __( 'Configure', 'woocommerce-paypal-payments' ),
url: '#',
},
@ -164,6 +279,7 @@ const featuresDefault = [
buttons: [
{
type: 'secondary',
class: 'small-button',
text: __( 'Apply', 'woocommerce-paypal-payments' ),
url: '#',
},
@ -181,10 +297,10 @@ const featuresDefault = [
'Let customers pay using their Google Pay wallet.',
'woocommerce-paypal-payments'
),
featureStatus: true,
buttons: [
{
type: 'secondary',
class: 'small-button',
text: __( 'Configure', 'woocommerce-paypal-payments' ),
url: '#',
},
@ -194,9 +310,6 @@ const featuresDefault = [
url: '#',
},
],
notes: [
__( '¹PayPal Q2 Earnings-2021.', 'woocommerce-paypal-payments' ),
],
},
{
id: 'apple_pay',
@ -208,6 +321,7 @@ const featuresDefault = [
buttons: [
{
type: 'secondary',
class: 'small-button',
text: __(
'Domain registration',
'woocommerce-paypal-payments'
@ -231,6 +345,7 @@ const featuresDefault = [
buttons: [
{
type: 'secondary',
class: 'small-button',
text: __( 'Configure', 'woocommerce-paypal-payments' ),
url: '#',
},
@ -240,6 +355,9 @@ const featuresDefault = [
url: '#',
},
],
notes: [
__( '¹PayPal Q2 Earnings-2021.', 'woocommerce-paypal-payments' ),
],
},
];

View file

@ -9,16 +9,10 @@ import {
const OrderIntent = ( { updateFormValue, settings } ) => {
return (
<SettingsBlock
components={ [
() => (
<>
<SettingsBlock>
<Header>
<Title>
{ __(
'Order Intent',
'woocommerce-paypal-payments'
) }
{ __( 'Order Intent', 'woocommerce-paypal-payments' ) }
</Title>
<Description>
{ __(
@ -27,15 +21,9 @@ const OrderIntent = ( { updateFormValue, settings } ) => {
) }
</Description>
</Header>
</>
),
() => (
<>
<ToggleSettingsBlock
title={ __(
'Authorize Only',
'woocommerce-paypal-payments'
) }
title={ __( 'Authorize Only', 'woocommerce-paypal-payments' ) }
actionProps={ {
callback: updateFormValue,
key: 'authorizeOnly',
@ -54,10 +42,7 @@ const OrderIntent = ( { updateFormValue, settings } ) => {
value: settings.captureVirtualOnlyOrders,
} }
/>
</>
),
] }
/>
</SettingsBlock>
);
};

View file

@ -1,19 +1,15 @@
import { __, sprintf } from '@wordpress/i18n';
import {
Header,
SettingsBlock,
ToggleSettingsBlock,
Title,
Description,
} from '../../../../ReusableComponents/SettingsBlocks';
import { Header } from '../../../../ReusableComponents/SettingsBlocks/SettingsBlockElements';
const SavePaymentMethods = ( { updateFormValue, settings } ) => {
return (
<SettingsBlock
className="ppcp-r-settings-block--save-payment-methods"
components={ [
() => (
<>
<SettingsBlock className="ppcp-r-settings-block--save-payment-methods">
<Header>
<Title>
{ __(
@ -23,14 +19,12 @@ const SavePaymentMethods = ( { updateFormValue, settings } ) => {
</Title>
<Description>
{ __(
'Securely store customers payment methods for future payments and subscriptions, simplifying checkout and enabling recurring transactions.',
"Securely store customers' payment methods for future payments and subscriptions, simplifying checkout and enabling recurring transactions.",
'woocommerce-paypal-payments'
) }
</Description>
</Header>
</>
),
() => (
<ToggleSettingsBlock
title={ __(
'Save PayPal and Venmo',
@ -57,8 +51,7 @@ const SavePaymentMethods = ( { updateFormValue, settings } ) => {
key: 'savePaypalAndVenmo',
} }
/>
),
() => (
<ToggleSettingsBlock
title={ __(
'Save Credit and Debit Cards',
@ -74,9 +67,7 @@ const SavePaymentMethods = ( { updateFormValue, settings } ) => {
value: settings.saveCreditCardAndDebitCard,
} }
/>
),
] }
/>
</SettingsBlock>
);
};

View file

@ -1,174 +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
components={ [
() => (
<>
<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() } />
</>
),
] }
/>
<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

@ -90,7 +90,7 @@ const TabStyling = () => {
return (
<div className="ppcp-r-styling">
<div className="ppcp-r-styling__settings">
<SectionIntro />
<SectionIntro location={ location } />
<SectionLocations
locationOptions={ locationOptions }
location={ location }
@ -157,20 +157,17 @@ const TabStylingSection = ( props ) => {
);
};
const SectionIntro = () => {
const buttonStyleDescription = sprintf(
// translators: %s: Link to Classic checkout page
__(
'Customize the appearance of the PayPal smart buttons on the <a href="%s">[MISSING LINK]Classic Checkout page</a>. Checkout Buttons must be enabled to display the PayPal gateway on the Checkout page.'
),
'#'
);
const SectionIntro = ( { location } ) => {
const { description, descriptionLink } =
defaultLocationSettings[ location ];
const buttonStyleDescription = sprintf( description, descriptionLink );
return (
<TabStylingSection
className="ppcp-r-styling__section--rc ppcp-r-styling__section--empty"
title={ __( 'Button Styling', 'wooocommerce-paypal-payments' ) }
description={ buttonStyleDescription }
></TabStylingSection>
/>
);
};
@ -321,6 +318,7 @@ const SectionButtonPreview = ( { locationSettings } ) => {
clientId: 'test',
merchantId: 'QTQX5NP6N9WZU',
components: 'buttons,googlepay',
'disable-funding': 'card',
'buyer-country': 'US',
currency: 'USD',
} }

View file

@ -1,13 +1,41 @@
import { useEffect, useMemo } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import classNames from 'classnames';
import { OnboardingHooks } from '../../data';
import SpinnerOverlay from '../ReusableComponents/SpinnerOverlay';
import Onboarding from './Onboarding/Onboarding';
import SettingsScreen from './SettingsScreen';
const Settings = () => {
const onboardingProgress = OnboardingHooks.useSteps();
// Disable the "Changes you made might not be saved" browser warning.
useEffect( () => {
const suppressBeforeUnload = ( event ) => {
event.stopImmediatePropagation();
return undefined;
};
window.addEventListener( 'beforeunload', suppressBeforeUnload );
return () => {
window.removeEventListener( 'beforeunload', suppressBeforeUnload );
};
}, [] );
const wrapperClass = classNames( 'ppcp-r-app', {
loading: ! onboardingProgress.isReady,
} );
const Content = useMemo( () => {
if ( ! onboardingProgress.isReady ) {
// TODO: Use better loading state indicator.
return <div>Loading...</div>;
return (
<SpinnerOverlay
message={ __( 'Loading…', 'woocommerce-paypal-payments' ) }
/>
);
}
if ( ! onboardingProgress.completed ) {
@ -15,6 +43,9 @@ const Settings = () => {
}
return <SettingsScreen />;
}, [ onboardingProgress ] );
return <div className={ wrapperClass }>{ Content }</div>;
};
export default Settings;

View file

@ -10,10 +10,22 @@ export default {
// Persistent data.
SET_PERSISTENT: 'COMMON:SET_PERSISTENT',
RESET: 'COMMON:RESET',
HYDRATE: 'COMMON:HYDRATE',
// Activity management (advanced solution that replaces the isBusy state).
START_ACTIVITY: 'COMMON:START_ACTIVITY',
STOP_ACTIVITY: 'COMMON:STOP_ACTIVITY',
// Controls - always start with "DO_".
DO_PERSIST_DATA: 'COMMON:DO_PERSIST_DATA',
DO_MANUAL_CONNECTION: 'COMMON:DO_MANUAL_CONNECTION',
DO_SANDBOX_LOGIN: 'COMMON:DO_SANDBOX_LOGIN',
DO_PRODUCTION_LOGIN: 'COMMON:DO_PRODUCTION_LOGIN',
DO_REFRESH_MERCHANT: 'COMMON:DO_REFRESH_MERCHANT',
DO_REFRESH_FEATURES: '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

@ -18,6 +18,13 @@ import { STORE_NAME } from './constants';
* @property {Object?} payload - Optional payload for the action.
*/
/**
* Special. Resets all values in the onboarding store to initial defaults.
*
* @return {Action} The action.
*/
export const reset = () => ( { type: ACTION_TYPES.RESET } );
/**
* Persistent. Set the full onboarding details, usually during app initialization.
*
@ -52,14 +59,35 @@ export const setIsSaving = ( isSaving ) => ( {
} );
/**
* Transient. Changes the "manual connection is busy" flag.
* Transient (Activity): Marks the start of an async activity
* Think of it as "setIsBusy(true)"
*
* @param {boolean} isBusy
* @param {string} id Internal ID/key of the action, used to stop it again.
* @param {?string} description Optional, description for logging/debugging
* @return {?Action} The action.
*/
export const startActivity = ( id, description = null ) => {
if ( ! id || 'string' !== typeof id ) {
console.warn( 'Activity ID must be a non-empty string' );
return null;
}
return {
type: ACTION_TYPES.START_ACTIVITY,
payload: { id, description },
};
};
/**
* Transient (Activity): Marks the end of an async activity.
* Think of it as "setIsBusy(false)"
*
* @param {string} id Internal ID/key of the action, used to stop it again.
* @return {Action} The action.
*/
export const setIsBusy = ( isBusy ) => ( {
type: ACTION_TYPES.SET_TRANSIENT,
payload: { isBusy },
export const stopActivity = ( id ) => ( {
type: ACTION_TYPES.STOP_ACTIVITY,
payload: { id },
} );
/**
@ -118,17 +146,22 @@ export const persist = function* () {
};
/**
* Side effect. Initiates the sandbox login ISU.
* Side effect. Fetches the ISU-login URL for a sandbox account.
*
* @return {Action} The action.
*/
export const connectViaSandbox = function* () {
yield setIsBusy( true );
export const connectToSandbox = function* () {
return yield { type: ACTION_TYPES.DO_SANDBOX_LOGIN };
};
const result = yield { type: ACTION_TYPES.DO_SANDBOX_LOGIN };
yield setIsBusy( false );
return result;
/**
* Side effect. Fetches the ISU-login URL for a production account.
*
* @param {string[]} products Which products/features to display in the ISU popup.
* @return {Action} The action.
*/
export const connectToProduction = function* ( products = [] ) {
return yield { type: ACTION_TYPES.DO_PRODUCTION_LOGIN, products };
};
/**
@ -140,15 +173,89 @@ export const connectViaIdAndSecret = function* () {
const { clientId, clientSecret, useSandbox } =
yield select( STORE_NAME ).persistentData();
yield setIsBusy( true );
const result = yield {
return yield {
type: ACTION_TYPES.DO_MANUAL_CONNECTION,
clientId,
clientSecret,
useSandbox,
};
yield setIsBusy( false );
};
/**
* Side effect. Clears and refreshes the merchant data via a REST request.
*
* @return {Action} The action.
*/
export const refreshMerchantData = function* () {
const result = yield { type: ACTION_TYPES.DO_REFRESH_MERCHANT };
if ( result.success && result.merchant ) {
yield hydrate( result );
}
return result;
};
/**
* Side effect.
* Purges all feature status data via a REST request.
* Refreshes the merchant data via a REST request.
*
* @return {Action} The action.
*/
export const refreshFeatureStatuses = function* () {
const result = yield { type: ACTION_TYPES.DO_REFRESH_FEATURES };
if ( result && result.success ) {
// TODO: Review if we can get the updated feature details in the result.data instead of
// doing a second refreshMerchantData() request.
yield refreshMerchantData();
}
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

@ -8,7 +8,7 @@
export const STORE_NAME = 'wc/paypal/common';
/**
* REST path to hydrate data of this module by loading data from the WP DB..
* REST path to hydrate data of this module by loading data from the WP DB.
*
* Used by resolvers.
*
@ -16,6 +16,15 @@ export const STORE_NAME = 'wc/paypal/common';
*/
export const REST_HYDRATE_PATH = '/wc/v3/wc_paypal/common';
/**
* REST path to fetch merchant details from the WordPress DB.
*
* Used by controls.
*
* @type {string}
*/
export const REST_HYDRATE_MERCHANT_PATH = '/wc/v3/wc_paypal/common/merchant';
/**
* REST path to persist data of this module to the WP DB.
*
@ -36,11 +45,42 @@ export const REST_PERSIST_PATH = '/wc/v3/wc_paypal/common';
export const REST_MANUAL_CONNECTION_PATH = '/wc/v3/wc_paypal/connect_manual';
/**
* REST path to generate an ISU URL for the sandbox-login.
* REST path to generate an ISU URL for the PayPal-login.
*
* Used by: Controls
* See: LoginLinkRestEndpoint.php
*
* @type {string}
*/
export const REST_SANDBOX_CONNECTION_PATH = '/wc/v3/wc_paypal/login_link';
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.
*
* Used by: Controls
* See: RefreshFeatureStatusEndpoint.php
*
* @type {string}
*/
export const REST_REFRESH_FEATURES_PATH =
'/wc/v3/wc_paypal/refresh-feature-status';

View file

@ -10,16 +10,20 @@
import apiFetch from '@wordpress/api-fetch';
import {
REST_PERSIST_PATH,
REST_CONNECTION_URL_PATH,
REST_HYDRATE_MERCHANT_PATH,
REST_MANUAL_CONNECTION_PATH,
REST_SANDBOX_CONNECTION_PATH,
REST_PERSIST_PATH,
REST_REFRESH_FEATURES_PATH,
REST_WEBHOOKS,
REST_WEBHOOKS_SIMULATE,
} from './constants';
import ACTION_TYPES from './action-types';
export const controls = {
async [ ACTION_TYPES.DO_PERSIST_DATA ]( { data } ) {
try {
return await apiFetch( {
await apiFetch( {
path: REST_PERSIST_PATH,
method: 'POST',
data,
@ -30,25 +34,39 @@ export const controls = {
},
async [ ACTION_TYPES.DO_SANDBOX_LOGIN ]() {
let result = null;
try {
result = await apiFetch( {
path: REST_SANDBOX_CONNECTION_PATH,
return apiFetch( {
path: REST_CONNECTION_URL_PATH,
method: 'POST',
data: {
environment: 'sandbox',
products: [ 'EXPRESS_CHECKOUT' ],
products: [ 'EXPRESS_CHECKOUT' ], // Sandbox always uses EXPRESS_CHECKOUT.
},
} );
} catch ( e ) {
result = {
return {
success: false,
error: e,
};
}
},
return result;
async [ ACTION_TYPES.DO_PRODUCTION_LOGIN ]( { products } ) {
try {
return apiFetch( {
path: REST_CONNECTION_URL_PATH,
method: 'POST',
data: {
environment: 'production',
products,
},
} );
} catch ( e ) {
return {
success: false,
error: e,
};
}
},
async [ ACTION_TYPES.DO_MANUAL_CONNECTION ]( {
@ -56,10 +74,8 @@ export const controls = {
clientSecret,
useSandbox,
} ) {
let result = null;
try {
result = await apiFetch( {
return await apiFetch( {
path: REST_MANUAL_CONNECTION_PATH,
method: 'POST',
data: {
@ -69,12 +85,56 @@ export const controls = {
},
} );
} catch ( e ) {
result = {
return {
success: false,
error: e,
};
}
},
return result;
async [ ACTION_TYPES.DO_REFRESH_MERCHANT ]() {
try {
return await apiFetch( { path: REST_HYDRATE_MERCHANT_PATH } );
} catch ( e ) {
return {
success: false,
error: e,
};
}
},
async [ ACTION_TYPES.DO_REFRESH_FEATURES ]() {
try {
return await apiFetch( {
path: REST_REFRESH_FEATURES_PATH,
method: 'POST',
} );
} catch ( e ) {
return {
success: false,
error: e,
message: e.message,
};
}
},
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 ) =>
@ -31,8 +30,11 @@ const useHooks = () => {
setManualConnectionMode,
setClientId,
setClientSecret,
connectViaSandbox,
connectToSandbox,
connectToProduction,
connectViaIdAndSecret,
startWebhookSimulation,
checkWebhookSimulationState,
} = useDispatch( STORE_NAME );
// Transient accessors.
@ -43,7 +45,11 @@ 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(),
[]
);
const wooSettings = useSelect(
( select ) => select( STORE_NAME ).wooSettings(),
[]
@ -72,26 +78,27 @@ const useHooks = () => {
setClientSecret: ( value ) => {
return savePersistent( setClientSecret, value );
},
connectViaSandbox,
connectToSandbox,
connectToProduction,
connectViaIdAndSecret,
merchant,
wooSettings,
};
};
export const useBusyState = () => {
const { setIsBusy } = useDispatch( STORE_NAME );
const isBusy = useTransient( 'isBusy' );
return {
isBusy,
setIsBusy: useCallback( ( busy ) => setIsBusy( busy ), [ setIsBusy ] ),
webhooks,
startWebhookSimulation,
checkWebhookSimulationState,
};
};
export const useSandbox = () => {
const { isSandboxMode, setSandboxMode, connectViaSandbox } = useHooks();
const { isSandboxMode, setSandboxMode, connectToSandbox } = useHooks();
return { isSandboxMode, setSandboxMode, connectViaSandbox };
return { isSandboxMode, setSandboxMode, connectToSandbox };
};
export const useProduction = () => {
const { connectToProduction } = useHooks();
return { connectToProduction };
};
export const useManualConnection = () => {
@ -118,5 +125,77 @@ export const useManualConnection = () => {
export const useWooSettings = () => {
const { wooSettings } = useHooks();
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 );
const verifyLoginStatus = useCallback( async () => {
const result = await refreshMerchantData();
if ( ! result.success ) {
throw new Error( result?.message || result?.error?.message );
}
// Verify if the server state is "connected" and we have a merchant ID.
return merchant?.isConnected && merchant?.id;
}, [ refreshMerchantData, merchant ] );
return {
merchant, // Merchant details
verifyLoginStatus, // Callback
};
};
// -- Not using the `useHooks()` data provider --
export const useBusyState = () => {
const { startActivity, stopActivity } = useDispatch( STORE_NAME );
// Resolved value (object), contains a list of all running actions.
const activities = useSelect(
( select ) => select( STORE_NAME ).getActivityList(),
[]
);
// Derive isBusy state from activities
const isBusy = Object.keys( activities ).length > 0;
// HOC that starts and stops an activity while the callback is executed.
const withActivity = useCallback(
async ( id, description, asyncFn ) => {
startActivity( id, description );
try {
return await asyncFn();
} finally {
stopActivity( id );
}
},
[ startActivity, stopActivity ]
);
return {
withActivity, // HOC
isBusy, // Boolean.
activities, // Object.
};
};

View file

@ -12,23 +12,31 @@ import ACTION_TYPES from './action-types';
// Store structure.
const defaultTransient = {
const defaultTransient = Object.freeze( {
isReady: false,
isBusy: false,
activities: new Map(),
// Read only values, provided by the server via hydrate.
wooSettings: {
merchant: Object.freeze( {
isConnected: false,
isSandbox: false,
id: '',
email: '',
} ),
wooSettings: Object.freeze( {
storeCountry: '',
storeCurrency: '',
},
};
} ),
} );
const defaultPersistent = {
const defaultPersistent = Object.freeze( {
useSandbox: false,
useManualConnection: false,
clientId: '',
clientSecret: '',
};
webhooks: [],
} );
// Reducer logic.
@ -44,16 +52,54 @@ const commonReducer = createReducer( defaultTransient, defaultPersistent, {
[ ACTION_TYPES.SET_PERSISTENT ]: ( state, action ) =>
setPersistent( state, action ),
[ ACTION_TYPES.RESET ]: ( state ) => {
const cleanState = setTransient(
setPersistent( state, defaultPersistent ),
defaultTransient
);
// Keep "read-only" details and initialization flags.
cleanState.wooSettings = { ...state.wooSettings };
cleanState.isReady = true;
return cleanState;
},
[ ACTION_TYPES.START_ACTIVITY ]: ( state, payload ) => {
return setTransient( state, {
activities: new Map( state.activities ).set(
payload.id,
payload.description
),
} );
},
[ ACTION_TYPES.STOP_ACTIVITY ]: ( state, payload ) => {
const newActivities = new Map( state.activities );
newActivities.delete( payload.id );
return setTransient( state, { activities: newActivities } );
},
[ ACTION_TYPES.DO_REFRESH_MERCHANT ]: ( state ) => ( {
...state,
merchant: Object.freeze( { ...defaultTransient.merchant } ),
} ),
[ ACTION_TYPES.HYDRATE ]: ( state, payload ) => {
const newState = setPersistent( state, payload.data );
if ( payload.wooSettings ) {
newState.wooSettings = {
...newState.wooSettings,
...payload.wooSettings,
};
// Populate read-only properties.
[ 'wooSettings', 'merchant' ].forEach( ( key ) => {
if ( ! payload[ key ] ) {
return;
}
newState[ key ] = Object.freeze( {
...newState[ key ],
...payload[ key ],
} );
} );
return newState;
},
} );

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

@ -16,10 +16,24 @@ export const persistentData = ( state ) => {
};
export const transientData = ( state ) => {
const { data, wooSettings, ...transientState } = getState( state );
const { data, merchant, wooSettings, ...transientState } =
getState( state );
return transientState || EMPTY_OBJ;
};
export const getActivityList = ( state ) => {
const { activities = new Map() } = state;
return Object.fromEntries( activities );
};
export const merchant = ( state ) => {
return getState( state ).merchant || EMPTY_OBJ;
};
export const wooSettings = ( state ) => {
return getState( state ).wooSettings || EMPTY_OBJ;
};
export const webhooks = ( state ) => {
return getState( state ).webhooks || EMPTY_OBJ;
};

View file

@ -1,4 +1,4 @@
import { OnboardingStoreName } from './index';
import { OnboardingStoreName, CommonStoreName } from './index';
export const addDebugTools = ( context, modules ) => {
if ( ! context || ! context?.debug ) {
@ -33,9 +33,14 @@ export const addDebugTools = ( context, modules ) => {
};
context.resetStore = () => {
const onboarding = wp.data.dispatch( OnboardingStoreName );
onboarding.reset();
onboarding.persist();
const stores = [ OnboardingStoreName, CommonStoreName ];
stores.forEach( ( storeName ) => {
const store = wp.data.dispatch( storeName );
store.reset();
store.persist();
} );
};
context.startOnboarding = () => {

View file

@ -34,8 +34,12 @@ const useHooks = () => {
setProducts,
} = useDispatch( STORE_NAME );
// Read-only flags.
// Read-only flags and derived state.
const flags = useSelect( ( select ) => select( STORE_NAME ).flags(), [] );
const determineProducts = useSelect(
( select ) => select( STORE_NAME ).determineProducts(),
[]
);
// Transient accessors.
const isReady = useTransient( 'isReady' );
@ -80,6 +84,7 @@ const useHooks = () => {
);
return savePersistent( setProducts, validProducts );
},
determineProducts,
};
};
@ -124,6 +129,12 @@ export const useNavigationState = () => {
};
};
export const useDetermineProducts = () => {
const { determineProducts } = useHooks();
return determineProducts;
};
export const useFlags = () => {
const { flags } = useHooks();
return flags;

View file

@ -12,25 +12,25 @@ import ACTION_TYPES from './action-types';
// Store structure.
const defaultTransient = {
const defaultTransient = Object.freeze( {
isReady: false,
// Read only values, provided by the server.
flags: {
flags: Object.freeze( {
canUseCasualSelling: false,
canUseVaulting: false,
canUseCardPayments: false,
canUseSubscriptions: false,
},
};
} ),
} );
const defaultPersistent = {
const defaultPersistent = Object.freeze( {
completed: false,
step: 0,
isCasualSeller: null, // null value will uncheck both options in the UI.
areOptionalPaymentMethodsEnabled: null,
products: [],
};
} );
// Reducer logic.
@ -46,15 +46,28 @@ const onboardingReducer = createReducer( defaultTransient, defaultPersistent, {
[ ACTION_TYPES.SET_PERSISTENT ]: ( state, payload ) =>
setPersistent( state, payload ),
[ ACTION_TYPES.RESET ]: ( state ) =>
[ ACTION_TYPES.RESET ]: ( state ) => {
const cleanState = setTransient(
setPersistent( state, defaultPersistent ),
defaultTransient
);
// Keep "read-only" details and initialization flags.
cleanState.flags = { ...state.flags };
cleanState.isReady = true;
return cleanState;
},
[ ACTION_TYPES.HYDRATE ]: ( state, payload ) => {
const newState = setPersistent( state, payload.data );
// Flags are not updated by `setPersistent()`.
if ( payload.flags ) {
newState.flags = { ...newState.flags, ...payload.flags };
newState.flags = Object.freeze( {
...newState.flags,
...payload.flags,
} );
}
return newState;

View file

@ -23,3 +23,50 @@ export const transientData = ( state ) => {
export const flags = ( state ) => {
return getState( state ).flags || EMPTY_OBJ;
};
/**
* Returns the products that we use for the production login link in the last onboarding step.
*
* This selector does not return state-values, but uses the state to derive the products-array
* that should be returned.
*
* @param {{}} state
* @return {string[]} The ISU products, based on choices made in the onboarding wizard.
*/
export const determineProducts = ( state ) => {
const derivedProducts = [];
const { isCasualSeller, areOptionalPaymentMethodsEnabled } =
persistentData( state );
const { canUseVaulting, canUseCardPayments } = flags( state );
if ( ! canUseCardPayments || ! areOptionalPaymentMethodsEnabled ) {
/**
* Branch 1: Credit Card Payments not available.
* The store uses the Express-checkout product.
*/
derivedProducts.push( 'EXPRESS_CHECKOUT' );
} else if ( isCasualSeller ) {
/**
* Branch 2: Merchant has no business.
* The store uses the Express-checkout product.
*/
derivedProducts.push( 'EXPRESS_CHECKOUT' );
// TODO: Add the "BCDC" product/feature
// Requirement: "EXPRESS_CHECKOUT with BCDC"
} else {
/**
* Branch 3: Merchant is business, and can use CC payments.
* The store uses the advanced PPCP product.
*/
derivedProducts.push( 'PPCP' );
}
if ( canUseVaulting ) {
// TODO: Add the "Vaulting" product/feature
// Requirement: "... with Vault"
}
return derivedProducts;
};

View file

@ -25,26 +25,56 @@ export const defaultLocationSettings = {
value: 'cart',
label: __( 'Cart', 'woocommerce-paypal-payments' ),
settings: { ...cartAndExpressCheckoutSettings },
// translators: %s: Link to Cart page
description: __(
'Customize the appearance of the PayPal smart buttons on the <a href="%s">[MISSING LINK]Cart page</a> and select which additional payment buttons to display in this location.',
'wooocommerce-paypal-payments'
),
descriptionLink: '#',
},
'classic-checkout': {
value: 'classic-checkout',
label: __( 'Classic Checkout', 'woocommerce-paypal-payments' ),
settings: { ...settings },
// translators: %s: Link to Classic Checkout page
description: __(
'Customize the appearance of the PayPal smart buttons on the <a href="%s">[MISSING LINK]Classic Checkout page</a> and choose which additional payment buttons to display in this location.',
'wooocommerce-paypal-payments'
),
descriptionLink: '#',
},
'express-checkout': {
value: 'express-checkout',
label: __( 'Express Checkout', 'woocommerce-paypal-payments' ),
settings: { ...cartAndExpressCheckoutSettings },
// translators: %s: Link to Express Checkout location
description: __(
'Customize the appearance of the PayPal smart buttons on the <a href="%s">[MISSING LINK]Express Checkout location</a> and choose which additional payment buttons to display in this location.',
'wooocommerce-paypal-payments'
),
descriptionLink: '#',
},
'mini-cart': {
value: 'mini-cart',
label: __( 'Mini Cart', 'woocommerce-paypel-payements' ),
settings: { ...settings },
// translators: %s: Link to Mini Cart
description: __(
'Customize the appearance of the PayPal smart buttons on the <a href="%s">[MISSING LINK]Mini Cart</a> and choose which additional payment buttons to display in this location.',
'wooocommerce-paypal-payments'
),
descriptionLink: '#',
},
'product-page': {
value: 'product-page',
label: __( 'Product Page', 'woocommerce-paypal-payments' ),
settings: { ...settings },
// translators: %s: Link to Product Page
description: __(
'Customize the appearance of the PayPal smart buttons on the <a href="%s">[MISSING LINK]Product Page</a> and choose which additional payment buttons to display in this location.',
'wooocommerce-paypal-payments'
),
descriptionLink: '#',
},
};
@ -57,10 +87,6 @@ export const paymentMethodOptions = [
value: 'paylater',
label: __( 'Pay Later', 'woocommerce-paypal-payments' ),
},
{
value: 'card',
label: __( 'Debit or Credit Card', 'woocommerce-paypal-payments' ),
},
{
value: 'googlepay',
label: __( 'Google Pay', 'woocommerce-paypal-payments' ),

View file

@ -0,0 +1,214 @@
import { __ } from '@wordpress/i18n';
import { useDispatch } from '@wordpress/data';
import { store as noticesStore } from '@wordpress/notices';
import { CommonHooks, OnboardingHooks } from '../data';
import { openPopup } from '../utils/window';
const MESSAGES = {
CONNECTED: __( 'Connected to PayPal', 'woocommerce-paypal-payments' ),
POPUP_BLOCKED: __(
'Popup blocked. Please allow popups for this site to connect to PayPal.',
'woocommerce-paypal-payments'
),
SANDBOX_ERROR: __(
'Could not generate a Sandbox login link.',
'woocommerce-paypal-payments'
),
PRODUCTION_ERROR: __(
'Could not generate a login link.',
'woocommerce-paypal-payments'
),
MANUAL_ERROR: __(
'Could not connect to PayPal. Please make sure your Client ID and Secret Key are correct.',
'woocommerce-paypal-payments'
),
LOGIN_FAILED: __(
'Login was not successful. Please try again.',
'woocommerce-paypal-payments'
),
};
const ACTIVITIES = {
CONNECT_SANDBOX: 'ISU_LOGIN_SANDBOX',
CONNECT_PRODUCTION: 'ISU_LOGIN_PRODUCTION',
CONNECT_MANUAL: 'MANUAL_LOGIN',
};
const handlePopupWithCompletion = ( url, onError ) => {
return new Promise( ( resolve ) => {
const popup = openPopup( url );
if ( ! popup ) {
onError( MESSAGES.POPUP_BLOCKED );
resolve( false );
return;
}
// Check popup state every 500ms
const checkPopup = setInterval( () => {
if ( popup.closed ) {
clearInterval( checkPopup );
resolve( true );
}
}, 500 );
return () => {
clearInterval( checkPopup );
if ( popup && ! popup.closed ) {
popup.close();
}
};
} );
};
const useConnectionBase = () => {
const { setCompleted } = OnboardingHooks.useSteps();
const { createSuccessNotice, createErrorNotice } =
useDispatch( noticesStore );
const { verifyLoginStatus } = CommonHooks.useMerchantInfo();
return {
handleFailed: ( res, genericMessage ) => {
console.error( 'Connection error', res );
createErrorNotice( res?.message ?? genericMessage );
},
handleCompleted: async () => {
try {
const loginSuccessful = await verifyLoginStatus();
if ( loginSuccessful ) {
createSuccessNotice( MESSAGES.CONNECTED );
await setCompleted( true );
} else {
createErrorNotice( MESSAGES.LOGIN_FAILED );
}
} catch ( error ) {
createErrorNotice( error.message ?? MESSAGES.LOGIN_FAILED );
}
},
createErrorNotice,
};
};
const useConnectionAttempt = ( connectFn, errorMessage ) => {
const { handleFailed, createErrorNotice, handleCompleted } =
useConnectionBase();
return async ( ...args ) => {
const res = await connectFn( ...args );
if ( ! res.success || ! res.data ) {
handleFailed( res, errorMessage );
return false;
}
const popupClosed = await handlePopupWithCompletion(
res.data,
createErrorNotice
);
if ( popupClosed ) {
await handleCompleted();
}
return popupClosed;
};
};
export const useSandboxConnection = () => {
const { connectToSandbox, isSandboxMode, setSandboxMode } =
CommonHooks.useSandbox();
const { withActivity } = CommonHooks.useBusyState();
const connectionAttempt = useConnectionAttempt(
connectToSandbox,
MESSAGES.SANDBOX_ERROR
);
const handleSandboxConnect = async () => {
return withActivity(
ACTIVITIES.CONNECT_SANDBOX,
'Connecting to sandbox account',
connectionAttempt
);
};
return {
handleSandboxConnect,
isSandboxMode,
setSandboxMode,
};
};
export const useProductionConnection = () => {
const { connectToProduction } = CommonHooks.useProduction();
const { withActivity } = CommonHooks.useBusyState();
const products = OnboardingHooks.useDetermineProducts();
const connectionAttempt = useConnectionAttempt(
() => connectToProduction( products ),
MESSAGES.PRODUCTION_ERROR
);
const handleProductionConnect = async () => {
return withActivity(
ACTIVITIES.CONNECT_PRODUCTION,
'Connecting to production account',
connectionAttempt
);
};
return { handleProductionConnect };
};
export const useManualConnection = () => {
const { handleFailed, handleCompleted, createErrorNotice } =
useConnectionBase();
const { withActivity } = CommonHooks.useBusyState();
const {
connectViaIdAndSecret,
isManualConnectionMode,
setManualConnectionMode,
clientId,
setClientId,
clientSecret,
setClientSecret,
} = CommonHooks.useManualConnection();
const handleConnectViaIdAndSecret = async ( { validation } = {} ) => {
return withActivity(
ACTIVITIES.CONNECT_MANUAL,
'Connecting manually via Client ID and Secret',
async () => {
if ( 'function' === typeof validation ) {
try {
validation();
} catch ( exception ) {
createErrorNotice( exception.message );
return;
}
}
const res = await connectViaIdAndSecret();
if ( res.success ) {
await handleCompleted();
} else {
handleFailed( res, MESSAGES.MANUAL_ERROR );
}
return res.success;
}
);
};
return {
handleConnectViaIdAndSecret,
isManualConnectionMode,
setManualConnectionMode,
clientId,
setClientId,
clientSecret,
setClientSecret,
};
};

View file

@ -1,18 +0,0 @@
import { __ } from '@wordpress/i18n';
const generatePriceText = ( type, selectedCountryPrice, storeCurrency ) => {
if ( ! selectedCountryPrice || ! selectedCountryPrice[ type ] ) {
console.warn( `Invalid type or price data for: ${ type }` );
return '';
}
const percentage = selectedCountryPrice[ type ].toFixed( 2 );
const fixedFee = `${ selectedCountryPrice.currencySymbol }${ selectedCountryPrice.fixedFee }`;
return __(
`from ${ percentage }% + ${ fixedFee } ${ storeCurrency }<sup>1</sup>`,
'woocommerce-paypal-payments'
);
};
export default generatePriceText;

View file

@ -1,74 +1,140 @@
export const countryPriceInfo = {
US: {
currencySymbol: '$',
fixedFee: 0.49,
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: {
currencySymbol: '£',
fixedFee: 0.3,
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: {
currencySymbol: '$',
fixedFee: 0.3,
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: {
currencySymbol: '$',
fixedFee: 0.3,
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: {
currencySymbol: '€',
fixedFee: 0.35,
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: {
currencySymbol: '€',
fixedFee: 0.35,
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: {
currencySymbol: '€',
fixedFee: 0.39,
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: {
currencySymbol: '€',
fixedFee: 0.35,
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

@ -0,0 +1,34 @@
const priceFormatInfo = {
USD: {
prefix: '$',
suffix: 'USD',
},
CAD: {
prefix: '$',
suffix: 'CAD',
},
AUD: {
prefix: '$',
suffix: 'AUD',
},
EUR: {
prefix: '€',
suffix: '',
},
GPB: {
prefix: '£',
suffix: '',
},
};
export const formatPrice = ( value, currency ) => {
const currencyInfo = priceFormatInfo[ currency ];
const amount = value.toFixed( 2 );
if ( ! currencyInfo ) {
console.error( `Unsupported currency: ${ currency }` );
return amount;
}
return `${ currencyInfo.prefix }${ amount } ${ currencyInfo.suffix }`;
};

Some files were not shown because too many files have changed in this diff Show more