Merge branch 'trunk' into PCP-4030-default-ui-legacy-for-existing-merchants-react-for-new-merchants

This commit is contained in:
Emili Castells Guasch 2025-01-23 10:55:13 +01:00
commit 44ce0d9e0b
242 changed files with 11365 additions and 5683 deletions

View file

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

View file

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

View file

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

View file

@ -79,6 +79,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Repository\PartnerReferralsData;
use WooCommerce\PayPalCommerce\ApiClient\Repository\PayeeRepository;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\ConnectBearer;
use WooCommerce\PayPalCommerce\WcGateway\Helper\EnvironmentConfig;
return array(
'api.host' => function( ContainerInterface $container ) : string {
@ -115,19 +116,13 @@ return array(
return 'WC-';
},
'api.bearer' => static function ( ContainerInterface $container ): Bearer {
$cache = new Cache( 'ppcp-paypal-bearer' );
$key = $container->get( 'api.key' );
$secret = $container->get( 'api.secret' );
$host = $container->get( 'api.host' );
$logger = $container->get( 'woocommerce.logger.woocommerce' );
$settings = $container->get( 'wcgateway.settings' );
return new PayPalBearer(
$cache,
$host,
$key,
$secret,
$logger,
$settings
$container->get( 'api.paypal-bearer-cache' ),
$container->get( 'api.host' ),
$container->get( 'api.key' ),
$container->get( 'api.secret' ),
$container->get( 'woocommerce.logger.woocommerce' ),
$container->get( 'wcgateway.settings' )
);
},
'api.endpoint.partners' => static function ( ContainerInterface $container ) : PartnersEndpoint {
@ -839,6 +834,9 @@ return array(
$container->get( 'wcgateway.settings' )
);
},
'api.paypal-bearer-cache' => static function( ContainerInterface $container ): Cache {
return new Cache( 'ppcp-paypal-bearer' );
},
'api.client-credentials-cache' => static function( ContainerInterface $container ): Cache {
return new Cache( 'ppcp-client-credentials-cache' );
},
@ -879,4 +877,54 @@ return array(
'api.partner_merchant_id-sandbox' => static function( ContainerInterface $container ) : string {
return CONNECT_WOO_SANDBOX_MERCHANT_ID;
},
'api.endpoint.login-seller-production' => static function ( ContainerInterface $container ) : LoginSeller {
return new LoginSeller(
$container->get( 'api.paypal-host-production' ),
$container->get( 'api.partner_merchant_id-production' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'api.endpoint.login-seller-sandbox' => static function ( ContainerInterface $container ) : LoginSeller {
return new LoginSeller(
$container->get( 'api.paypal-host-sandbox' ),
$container->get( 'api.partner_merchant_id-sandbox' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'api.env.paypal-host' => static function ( ContainerInterface $container ) : EnvironmentConfig {
/**
* Environment specific API host names.
*
* @type EnvironmentConfig<string>
*/
return EnvironmentConfig::create(
'string',
$container->get( 'api.paypal-host-production' ),
$container->get( 'api.paypal-host-sandbox' )
);
},
'api.env.endpoint.login-seller' => static function ( ContainerInterface $container ) : EnvironmentConfig {
/**
* Environment specific LoginSeller API instances.
*
* @type EnvironmentConfig<LoginSeller>
*/
return EnvironmentConfig::create(
LoginSeller::class,
$container->get( 'api.endpoint.login-seller-production' ),
$container->get( 'api.endpoint.login-seller-sandbox' )
);
},
'api.env.endpoint.partner-referrals' => static function ( ContainerInterface $container ) : EnvironmentConfig {
/**
* Environment specific PartnerReferrals API instances.
*
* @type EnvironmentConfig<PartnerReferrals>
*/
return EnvironmentConfig::create(
PartnerReferrals::class,
$container->get( 'api.endpoint.partner-referrals-production' ),
$container->get( 'api.endpoint.partner-referrals-sandbox' )
);
},
);

View file

@ -20,6 +20,8 @@ use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameI
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\PayPalBearer;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\SdkClientToken;
/**
* Class ApiModule
@ -103,6 +105,34 @@ class ApiModule implements ServiceModule, ExtendingModule, ExecutableModule {
2
);
/**
* Flushes the API client caches.
*/
add_action(
'woocommerce_paypal_payments_flush_api_cache',
static function () use ( $c ) {
$caches = array(
'api.paypal-bearer-cache' => array(
PayPalBearer::CACHE_KEY,
),
'api.client-credentials-cache' => array(
SdkClientToken::CACHE_KEY,
),
);
foreach ( $caches as $cache_id => $keys ) {
$cache = $c->get( $cache_id );
assert( $cache instanceof Cache );
foreach ( $keys as $key ) {
if ( $cache->has( $key ) ) {
$cache->delete( $key );
}
}
}
}
);
return true;
}
}

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

@ -184,21 +184,17 @@ class ApplepayModule implements ServiceModule, ExtendingModule, ExecutableModule
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();
}
function( array $features ) use ( $c ): 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(
$features['apple_pay'] = array(
'enabled' => $apple_pay_enabled,
);
return $merchant_data;
return $features;
}
);

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

@ -3,10 +3,10 @@ import { useEffect, useState } from '@wordpress/element';
import {
PayPalScriptProvider,
PayPalCardFieldsProvider,
PayPalNameField,
PayPalNumberField,
PayPalExpiryField,
PayPalCVVField,
PayPalNameField,
PayPalNumberField,
PayPalExpiryField,
PayPalCVVField,
} from '@paypal/react-paypal-js';
import { CheckoutHandler } from './checkout-handler';
@ -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,16 +92,36 @@ 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' ) }/>
</div>
<div style={ { width: "100%" } }>
<PayPalCVVField placeholder={ __( 'CVV', 'woocommerce-paypal-payments' ) }/>
</div>
</div>
<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>
</div>
<CheckoutHandler
getCardFieldsForm={ getCardFieldsForm }
getSavePayment={ getSavePayment }

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

@ -121,7 +121,7 @@ return array(
*
* @returns SettingsMap[]
*/
'compat.setting.new-to-old-map' => function( ContainerInterface $container ) : array {
'compat.setting.new-to-old-map' => static function( ContainerInterface $container ) : array {
$are_new_settings_enabled = $container->get( 'wcgateway.settings.admin-settings-enabled' );
if ( ! $are_new_settings_enabled ) {
return array();
@ -129,7 +129,7 @@ return array(
return array(
new SettingsMap(
$container->get( 'settings.data.common' ),
$container->get( 'settings.data.general' ),
array(
'client_id' => 'client_id',
'client_secret' => 'client_secret',
@ -137,16 +137,23 @@ return array(
),
new SettingsMap(
$container->get( 'settings.data.general' ),
/**
* The new GeneralSettings class stores the current connection
* details, without adding an environment-suffix (no `_sandbox`
* or `_production` in the field name)
* Only the `sandbox_merchant` flag indicates, which environment
* the credentials are used for.
*/
array(
'is_sandbox' => 'sandbox_on',
'live_client_id' => 'client_id_production',
'live_client_secret' => 'client_secret_production',
'live_merchant_id' => 'merchant_id_production',
'live_merchant_email' => 'merchant_email_production',
'sandbox_client_id' => 'client_id_sandbox',
'sandbox_client_secret' => 'client_secret_sandbox',
'sandbox_merchant_id' => 'merchant_id_sandbox',
'sandbox_merchant_email' => 'merchant_email_sandbox',
'is_sandbox' => 'sandbox_merchant',
'live_client_id' => 'client_id',
'live_client_secret' => 'client_secret',
'live_merchant_id' => 'merchant_id',
'live_merchant_email' => 'merchant_email',
'sandbox_client_id' => 'client_id',
'sandbox_client_secret' => 'client_secret',
'sandbox_merchant_id' => 'merchant_id',
'sandbox_merchant_email' => 'merchant_email',
)
),
);

View file

@ -87,6 +87,8 @@ class CompatModule implements ServiceModule, ExtendingModule, ExecutableModule {
$this->initialize_wc_bookings_compat_layer( $c );
}
add_action( 'woocommerce_paypal_payments_gateway_migrate', static fn() => delete_transient( 'ppcp_has_ppec_subscriptions' ) );
return true;
}

View file

@ -75,10 +75,10 @@ class PPECHelper {
}
global $wpdb;
if ( class_exists( OrderUtil::class ) && OrderUtil::custom_orders_table_usage_is_enabled() && isset( $wpdb->wc_orders ) ) {
if ( class_exists( OrderUtil::class ) && OrderUtil::custom_orders_table_usage_is_enabled() ) {
$result = $wpdb->get_var(
$wpdb->prepare(
"SELECT 1 FROM {$wpdb->wc_orders} WHERE payment_method = %s",
"SELECT 1 FROM {$wpdb->prefix}wc_orders WHERE payment_method = %s",
self::PPEC_GATEWAY_ID
)
);

View file

@ -234,21 +234,17 @@ class GooglepayModule implements ServiceModule, ExtendingModule, ExecutableModul
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();
}
function ( array $features ) use ( $c ): 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(
$features['google_pay'] = array(
'enabled' => $google_pay_enabled,
);
return $merchant_data;
return $features;
}
);

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

@ -14,17 +14,15 @@ use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\ConnectBearer;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\PayPalBearer;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\LoginSeller;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
use WooCommerce\PayPalCommerce\Onboarding\Assets\OnboardingAssets;
use WooCommerce\PayPalCommerce\Onboarding\Endpoint\LoginSellerEndpoint;
use WooCommerce\PayPalCommerce\Onboarding\Endpoint\UpdateSignupLinksEndpoint;
use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingSendOnlyNoticeRenderer;
use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingRenderer;
use WooCommerce\PayPalCommerce\Onboarding\OnboardingRESTController;
return array(
'api.sandbox-host' => static function ( ContainerInterface $container ): string {
'api.sandbox-host' => static function ( ContainerInterface $container ): string {
$state = $container->get( 'onboarding.state' );
@ -38,7 +36,7 @@ return array(
}
return CONNECT_WOO_SANDBOX_URL;
},
'api.production-host' => static function ( ContainerInterface $container ): string {
'api.production-host' => static function ( ContainerInterface $container ): string {
$state = $container->get( 'onboarding.state' );
@ -53,7 +51,7 @@ return array(
}
return CONNECT_WOO_URL;
},
'api.host' => static function ( ContainerInterface $container ): string {
'api.host' => static function ( ContainerInterface $container ): string {
$environment = $container->get( 'onboarding.environment' );
/**
@ -65,7 +63,7 @@ return array(
? (string) $container->get( 'api.sandbox-host' ) : (string) $container->get( 'api.production-host' );
},
'api.paypal-host' => function( ContainerInterface $container ) : string {
'api.paypal-host' => function( ContainerInterface $container ) : string {
$environment = $container->get( 'onboarding.environment' );
/**
* The current environment.
@ -78,7 +76,7 @@ return array(
return $container->get( 'api.paypal-host-production' );
},
'api.paypal-website-url' => function( ContainerInterface $container ) : string {
'api.paypal-website-url' => function( ContainerInterface $container ) : string {
$environment = $container->get( 'onboarding.environment' );
assert( $environment instanceof Environment );
if ( $environment->current_environment_is( Environment::SANDBOX ) ) {
@ -88,7 +86,7 @@ return array(
},
'api.bearer' => static function ( ContainerInterface $container ): Bearer {
'api.bearer' => static function ( ContainerInterface $container ): Bearer {
$state = $container->get( 'onboarding.state' );
@ -115,16 +113,16 @@ return array(
$settings
);
},
'onboarding.state' => function( ContainerInterface $container ) : State {
'onboarding.state' => function( ContainerInterface $container ) : State {
$settings = $container->get( 'wcgateway.settings' );
return new State( $settings );
},
'onboarding.environment' => function( ContainerInterface $container ) : Environment {
'onboarding.environment' => function( ContainerInterface $container ) : Environment {
$settings = $container->get( 'wcgateway.settings' );
return new Environment( $settings );
},
'onboarding.assets' => function( ContainerInterface $container ) : OnboardingAssets {
'onboarding.assets' => function( ContainerInterface $container ) : OnboardingAssets {
$state = $container->get( 'onboarding.state' );
$login_seller_endpoint = $container->get( 'onboarding.endpoint.login-seller' );
return new OnboardingAssets(
@ -137,34 +135,14 @@ return array(
);
},
'onboarding.url' => static function ( ContainerInterface $container ): string {
'onboarding.url' => static function ( ContainerInterface $container ): string {
return plugins_url(
'/modules/ppcp-onboarding/',
dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php'
);
},
'api.endpoint.login-seller-production' => static function ( ContainerInterface $container ) : LoginSeller {
$logger = $container->get( 'woocommerce.logger.woocommerce' );
return new LoginSeller(
$container->get( 'api.paypal-host-production' ),
$container->get( 'api.partner_merchant_id-production' ),
$logger
);
},
'api.endpoint.login-seller-sandbox' => static function ( ContainerInterface $container ) : LoginSeller {
$logger = $container->get( 'woocommerce.logger.woocommerce' );
return new LoginSeller(
$container->get( 'api.paypal-host-sandbox' ),
$container->get( 'api.partner_merchant_id-sandbox' ),
$logger
);
},
'onboarding.endpoint.login-seller' => static function ( ContainerInterface $container ) : LoginSellerEndpoint {
'onboarding.endpoint.login-seller' => static function ( ContainerInterface $container ) : LoginSellerEndpoint {
$request_data = $container->get( 'button.request-data' );
$login_seller_production = $container->get( 'api.endpoint.login-seller-production' );
@ -184,7 +162,7 @@ return array(
new Cache( 'ppcp-client-credentials-cache' )
);
},
'onboarding.endpoint.pui' => static function( ContainerInterface $container ) : UpdateSignupLinksEndpoint {
'onboarding.endpoint.pui' => static function( ContainerInterface $container ) : UpdateSignupLinksEndpoint {
return new UpdateSignupLinksEndpoint(
$container->get( 'wcgateway.settings' ),
$container->get( 'button.request-data' ),
@ -194,10 +172,10 @@ return array(
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'onboarding.signup-link-cache' => static function( ContainerInterface $container ): Cache {
'onboarding.signup-link-cache' => static function( ContainerInterface $container ): Cache {
return new Cache( 'ppcp-paypal-signup-link' );
},
'onboarding.signup-link-ids' => static function ( ContainerInterface $container ): array {
'onboarding.signup-link-ids' => static function ( ContainerInterface $container ): array {
return array(
'production-ppcp',
'production-express_checkout',
@ -205,12 +183,12 @@ return array(
'sandbox-express_checkout',
);
},
'onboarding.render-send-only-notice' => static function( ContainerInterface $container ) {
'onboarding.render-send-only-notice' => static function( ContainerInterface $container ) {
return new OnboardingSendOnlyNoticeRenderer(
$container->get( 'wcgateway.send-only-message' )
);
},
'onboarding.render' => static function ( ContainerInterface $container ) : OnboardingRenderer {
'onboarding.render' => static function ( ContainerInterface $container ) : OnboardingRenderer {
$partner_referrals = $container->get( 'api.endpoint.partner-referrals-production' );
$partner_referrals_sandbox = $container->get( 'api.endpoint.partner-referrals-sandbox' );
$partner_referrals_data = $container->get( 'api.repository.partner-referrals-data' );
@ -226,14 +204,14 @@ return array(
$logger
);
},
'onboarding.render-options' => static function ( ContainerInterface $container ) : OnboardingOptionsRenderer {
'onboarding.render-options' => static function ( ContainerInterface $container ) : OnboardingOptionsRenderer {
return new OnboardingOptionsRenderer(
$container->get( 'onboarding.url' ),
$container->get( 'api.shop.country' ),
$container->get( 'wcgateway.settings' )
);
},
'onboarding.rest' => static function( $container ) : OnboardingRESTController {
'onboarding.rest' => static function( $container ) : OnboardingRESTController {
return new OnboardingRESTController( $container );
},
);

View file

@ -69,7 +69,11 @@ class PayLaterConfiguratorModule implements ServiceModule, ExtendingModule, Exec
$is_wc_settings_page = $c->get( 'wcgateway.is-wc-settings-page' );
$messaging_locations = $c->get( 'paylater-configurator.messaging-locations' );
self::add_paylater_update_notice( $messaging_locations, $is_wc_settings_page, $current_page_id );
self::add_paylater_update_notice(
$messaging_locations,
$is_wc_settings_page,
$current_page_id
);
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
@ -159,9 +163,9 @@ class PayLaterConfiguratorModule implements ServiceModule, ExtendingModule, Exec
* The notice appears on any PayPal-Settings page, except for the Pay-Later settings page,
* when no Pay-Later messaging is used yet.
*
* @param array $message_locations PayLater messaging locations.
* @param bool $is_settings_page Whether the current page is a WC settings page.
* @param string $current_page_id ID of current settings page tab.
* @param array $message_locations PayLater messaging locations.
* @param bool $is_settings_page Whether the current page is a WC settings page.
* @param string $current_page_id ID of current settings page tab.
*
* @return void
*/

View file

@ -49,6 +49,8 @@ class RenewalHandler {
public function process( array $subscriptions, string $transaction_id ): void {
foreach ( $subscriptions as $subscription ) {
if ( $this->is_for_renewal_order( $subscription ) ) {
$subscription->update_status( 'on-hold' );
$renewal_order = wcs_create_renewal_order( $subscription );
if ( is_a( $renewal_order, WC_Order::class ) ) {
$this->logger->info(

View file

@ -20,75 +20,56 @@ return array(
$save_payment_methods_applies = $container->get( 'save-payment-methods.helpers.save-payment-methods-applies' );
assert( $save_payment_methods_applies instanceof SavePaymentMethodsApplies );
return $save_payment_methods_applies->for_country_currency();
return $save_payment_methods_applies->for_country();
},
'save-payment-methods.helpers.save-payment-methods-applies' => static function ( ContainerInterface $container ) : SavePaymentMethodsApplies {
return new SavePaymentMethodsApplies(
$container->get( 'save-payment-methods.supported-country-currency-matrix' ),
$container->get( 'api.shop.currency.getter' ),
$container->get( 'save-payment-methods.supported-countries' ),
$container->get( 'api.shop.country' )
);
},
'save-payment-methods.supported-country-currency-matrix' => static function ( ContainerInterface $container ) : array {
$default_currencies = array(
'AUD',
'BRL',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HUF',
'ILS',
'JPY',
'MXN',
'NOK',
'NZD',
'PHP',
'PLN',
'SEK',
'THB',
'TWD',
'USD',
);
'save-payment-methods.supported-countries' => static function ( ContainerInterface $container ) : array {
if ( has_filter( 'woocommerce_paypal_payments_save_payment_methods_supported_country_currency_matrix' ) ) {
_deprecated_hook( 'woocommerce_paypal_payments_save_payment_methods_supported_country_currency_matrix', '3.0.0', 'woocommerce_paypal_payments_save_payment_methods_supported_countries', esc_attr__( 'Please use the new Hook to filter countries for saved payments in PayPal Payments.', 'woocommerce-paypal-payments' ) );
}
return apply_filters(
'woocommerce_paypal_payments_save_payment_methods_supported_country_currency_matrix',
'woocommerce_paypal_payments_save_payment_methods_supported_countries',
array(
'AU' => $default_currencies,
'AT' => $default_currencies,
'BE' => $default_currencies,
'BG' => $default_currencies,
'CA' => $default_currencies,
'CN' => $default_currencies,
'CY' => $default_currencies,
'CZ' => $default_currencies,
'DK' => $default_currencies,
'EE' => $default_currencies,
'FI' => $default_currencies,
'FR' => $default_currencies,
'DE' => $default_currencies,
'GR' => $default_currencies,
'HU' => $default_currencies,
'IE' => $default_currencies,
'IT' => $default_currencies,
'LV' => $default_currencies,
'LI' => $default_currencies,
'LT' => $default_currencies,
'LU' => $default_currencies,
'MT' => $default_currencies,
'NO' => $default_currencies,
'NL' => $default_currencies,
'PL' => $default_currencies,
'PT' => $default_currencies,
'RO' => $default_currencies,
'SK' => $default_currencies,
'SI' => $default_currencies,
'ES' => $default_currencies,
'SE' => $default_currencies,
'GB' => $default_currencies,
'US' => $default_currencies,
'AU',
'AT',
'BE',
'BG',
'CA',
'CN',
'CY',
'CZ',
'DK',
'EE',
'FI',
'FR',
'DE',
'HK',
'HU',
'IE',
'IT',
'LV',
'LI',
'LT',
'LU',
'MT',
'NO',
'NL',
'PL',
'PT',
'RO',
'SG',
'SK',
'SI',
'ES',
'SE',
'GB',
'US',
)
);
},

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

@ -9,49 +9,37 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\SavePaymentMethods\Helper;
use WooCommerce\PayPalCommerce\ApiClient\Helper\CurrencyGetter;
/**
* Class SavePaymentMethodsApplies
*/
class SavePaymentMethodsApplies {
/**
* The matrix which countries and currency combinations can be used for Save Payment Methods.
* The countries can be used for Save Payment Methods.
*
* @var array
*/
private $allowed_country_currency_matrix;
/**
* The getter of the 3-letter currency code of the shop.
*
* @var CurrencyGetter
*/
private CurrencyGetter $currency;
private array $allowed_countries;
/**
* 2-letter country code of the shop.
*
* @var string
*/
private $country;
private string $country;
/**
* SavePaymentMethodsApplies constructor.
*
* @param array $allowed_country_currency_matrix The matrix which countries and currency combinations can be used for Save Payment Methods.
* @param CurrencyGetter $currency The getter of the 3-letter currency code of the shop.
* @param string $country 2-letter country code of the shop.
* @param array $allowed_countries The matrix which countries and currency combinations can be used for Save Payment Methods.
* @param string $country 2-letter country code of the shop.
*/
public function __construct(
array $allowed_country_currency_matrix,
CurrencyGetter $currency,
array $allowed_countries,
string $country
) {
$this->allowed_country_currency_matrix = $allowed_country_currency_matrix;
$this->currency = $currency;
$this->country = $country;
$this->allowed_countries = $allowed_countries;
$this->country = $country;
}
/**
@ -59,10 +47,8 @@ class SavePaymentMethodsApplies {
*
* @return bool
*/
public function for_country_currency(): bool {
if ( ! in_array( $this->country, array_keys( $this->allowed_country_currency_matrix ), true ) ) {
return false;
}
return in_array( $this->currency->get(), $this->allowed_country_currency_matrix[ $this->country ], true );
public function for_country(): bool {
return in_array( $this->country, $this->allowed_countries, true );
}
}

View file

@ -66,396 +66,382 @@ 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_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() ) {
return $localized_script_data;
}
$api = $c->get( 'api.user-id-token' );
assert( $api instanceof UserIdToken );
$logger = $c->get( 'woocommerce.logger.woocommerce' );
assert( $logger instanceof LoggerInterface );
return $this->add_id_token_to_script_data( $api, $logger, $localized_script_data );
}
);
// Adds attributes needed to save payment method.
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 ) ) {
return $data;
}
if ( $payment_method === CreditCardGateway::ID ) {
$save_payment_method = $request_data['save_payment_method'] ?? false;
if ( $save_payment_method ) {
$data['payment_source'] = array(
'card' => array(
'attributes' => array(
'vault' => array(
'store_in_vault' => 'ON_SUCCESS',
),
),
),
);
$target_customer_id = get_user_meta( get_current_user_id(), '_ppcp_target_customer_id', true );
if ( ! $target_customer_id ) {
$target_customer_id = get_user_meta( get_current_user_id(), 'ppcp_customer_id', true );
}
if ( $target_customer_id ) {
$data['payment_source']['card']['attributes']['customer'] = array(
'id' => $target_customer_id,
);
}
}
}
if ( $payment_method === PayPalGateway::ID ) {
$funding_source = $request_data['funding_source'] ?? null;
if ( $funding_source && $funding_source === 'venmo' ) {
$data['payment_source'] = array(
'venmo' => array(
'attributes' => array(
'vault' => array(
'store_in_vault' => 'ON_SUCCESS',
'usage_type' => 'MERCHANT',
'permit_multiple_payment_tokens' => apply_filters( 'woocommerce_paypal_payments_permit_multiple_payment_tokens', false ),
),
),
),
);
} elseif ( $funding_source && $funding_source === 'apple_pay' ) {
$data['payment_source'] = array(
'apple_pay' => array(
'stored_credential' => array(
'payment_initiator' => 'CUSTOMER',
'payment_type' => 'RECURRING',
),
'attributes' => array(
'vault' => array(
'store_in_vault' => 'ON_SUCCESS',
),
),
),
);
} else {
$data['payment_source'] = array(
'paypal' => array(
'attributes' => array(
'vault' => array(
'store_in_vault' => 'ON_SUCCESS',
'usage_type' => 'MERCHANT',
'permit_multiple_payment_tokens' => apply_filters( 'woocommerce_paypal_payments_permit_multiple_payment_tokens', false ),
),
),
),
);
}
}
return $data;
},
10,
3
);
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 );
$payment_vault_attributes = $payment_source->properties()->attributes->vault ?? null;
if ( $payment_vault_attributes ) {
$customer_id = $payment_vault_attributes->customer->id ?? '';
$token_id = $payment_vault_attributes->id ?? '';
if ( ! $customer_id || ! $token_id ) {
return;
}
update_user_meta( $wc_order->get_customer_id(), '_ppcp_target_customer_id', $customer_id );
$wc_payment_tokens = $c->get( 'vaulting.wc-payment-tokens' );
assert( $wc_payment_tokens instanceof WooCommercePaymentTokens );
if ( $wc_order->get_payment_method() === CreditCardGateway::ID ) {
$token = new \WC_Payment_Token_CC();
$token->set_token( $token_id );
$token->set_user_id( $wc_order->get_customer_id() );
$token->set_gateway_id( CreditCardGateway::ID );
$token->set_last4( $payment_source->properties()->last_digits ?? '' );
$expiry = explode( '-', $payment_source->properties()->expiry ?? '' );
$token->set_expiry_year( $expiry[0] ?? '' );
$token->set_expiry_month( $expiry[1] ?? '' );
$token->set_card_type( $payment_source->properties()->brand ?? '' );
$token->save();
}
if ( $wc_order->get_payment_method() === PayPalGateway::ID ) {
switch ( $payment_source->name() ) {
case 'venmo':
$wc_payment_tokens->create_payment_token_venmo(
$wc_order->get_customer_id(),
$token_id,
$payment_source->properties()->email_address ?? ''
);
break;
case 'apple_pay':
$wc_payment_tokens->create_payment_token_applepay(
$wc_order->get_customer_id(),
$token_id
);
break;
case 'paypal':
default:
$wc_payment_tokens->create_payment_token_paypal(
$wc_order->get_customer_id(),
$token_id,
$payment_source->properties()->email_address ?? ''
);
break;
}
}
}
},
10,
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_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 ) ) {
return;
'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;
}
$module_url = $c->get( 'save-payment-methods.module.url' );
wp_enqueue_script(
'ppcp-add-payment-method',
untrailingslashit( $module_url ) . '/assets/js/add-payment-method.js',
array( 'jquery' ),
$c->get( 'ppcp.asset-version' ),
true
add_filter(
'woocommerce_paypal_payments_localized_script_data',
function ( array $localized_script_data ) use ( $c ) {
$subscriptions_helper = $c->get( 'wc-subscriptions.helper' );
assert( $subscriptions_helper instanceof SubscriptionHelper );
if ( ! is_user_logged_in() && ! $subscriptions_helper->cart_contains_subscription() ) {
return $localized_script_data;
}
$api = $c->get( 'api.user-id-token' );
assert( $api instanceof UserIdToken );
$logger = $c->get( 'woocommerce.logger.woocommerce' );
assert( $logger instanceof LoggerInterface );
return $this->add_id_token_to_script_data( $api, $logger, $localized_script_data );
}
);
$api = $c->get( 'api.user-id-token' );
assert( $api instanceof UserIdToken );
// Adds attributes needed to save payment method.
add_filter(
'ppcp_create_order_request_body_data',
function ( array $data, string $payment_method, array $request_data ) use ( $c ): array {
$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;
}
try {
$target_customer_id = '';
if ( is_user_logged_in() ) {
$target_customer_id = get_user_meta( get_current_user_id(), '_ppcp_target_customer_id', true );
if ( ! $target_customer_id ) {
$target_customer_id = get_user_meta( get_current_user_id(), 'ppcp_customer_id', true );
$save_payment_method = $request_data['save_payment_method'] ?? false;
if ( $save_payment_method ) {
$data['payment_source'] = array(
'card' => array(
'attributes' => array(
'vault' => array(
'store_in_vault' => 'ON_SUCCESS',
),
),
),
);
$target_customer_id = get_user_meta( get_current_user_id(), '_ppcp_target_customer_id', true );
if ( ! $target_customer_id ) {
$target_customer_id = get_user_meta( get_current_user_id(), 'ppcp_customer_id', true );
}
if ( $target_customer_id ) {
$data['payment_source']['card']['attributes']['customer'] = array(
'id' => $target_customer_id,
);
}
}
}
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' ) {
$data['payment_source'] = array(
'venmo' => array(
'attributes' => array(
'vault' => array(
'store_in_vault' => 'ON_SUCCESS',
'usage_type' => 'MERCHANT',
'permit_multiple_payment_tokens' => apply_filters( 'woocommerce_paypal_payments_permit_multiple_payment_tokens', false ),
),
),
),
);
} elseif ( $funding_source && $funding_source === 'apple_pay' ) {
$data['payment_source'] = array(
'apple_pay' => array(
'stored_credential' => array(
'payment_initiator' => 'CUSTOMER',
'payment_type' => 'RECURRING',
),
'attributes' => array(
'vault' => array(
'store_in_vault' => 'ON_SUCCESS',
),
),
),
);
} else {
$data['payment_source'] = array(
'paypal' => array(
'attributes' => array(
'vault' => array(
'store_in_vault' => 'ON_SUCCESS',
'usage_type' => 'MERCHANT',
'permit_multiple_payment_tokens' => apply_filters( 'woocommerce_paypal_payments_permit_multiple_payment_tokens', false ),
),
),
),
);
}
}
return $data;
},
10,
3
);
add_action(
'woocommerce_paypal_payments_after_order_processor',
function ( WC_Order $wc_order, Order $order ) use ( $c ) {
$payment_source = $order->payment_source();
assert( $payment_source instanceof PaymentSource );
$payment_vault_attributes = $payment_source->properties()->attributes->vault ?? null;
if ( $payment_vault_attributes ) {
$customer_id = $payment_vault_attributes->customer->id ?? '';
$token_id = $payment_vault_attributes->id ?? '';
if ( ! $customer_id || ! $token_id ) {
return;
}
update_user_meta( $wc_order->get_customer_id(), '_ppcp_target_customer_id', $customer_id );
$wc_payment_tokens = $c->get( 'vaulting.wc-payment-tokens' );
assert( $wc_payment_tokens instanceof WooCommercePaymentTokens );
if ( $wc_order->get_payment_method() === CreditCardGateway::ID ) {
$token = new \WC_Payment_Token_CC();
$token->set_token( $token_id );
$token->set_user_id( $wc_order->get_customer_id() );
$token->set_gateway_id( CreditCardGateway::ID );
$token->set_last4( $payment_source->properties()->last_digits ?? '' );
$expiry = explode( '-', $payment_source->properties()->expiry ?? '' );
$token->set_expiry_year( $expiry[0] ?? '' );
$token->set_expiry_month( $expiry[1] ?? '' );
$token->set_card_type( $payment_source->properties()->brand ?? '' );
$token->save();
}
if ( $wc_order->get_payment_method() === PayPalGateway::ID ) {
switch ( $payment_source->name() ) {
case 'venmo':
$wc_payment_tokens->create_payment_token_venmo(
$wc_order->get_customer_id(),
$token_id,
$payment_source->properties()->email_address ?? ''
);
break;
case 'apple_pay':
$wc_payment_tokens->create_payment_token_applepay(
$wc_order->get_customer_id(),
$token_id
);
break;
case 'paypal':
default:
$wc_payment_tokens->create_payment_token_paypal(
$wc_order->get_customer_id(),
$token_id,
$payment_source->properties()->email_address ?? ''
);
break;
}
}
}
},
10,
2
);
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() ) ) {
return;
}
$module_url = $c->get( 'save-payment-methods.module.url' );
wp_enqueue_script(
'ppcp-add-payment-method',
untrailingslashit( $module_url ) . '/assets/js/add-payment-method.js',
array( 'jquery' ),
$c->get( 'ppcp.asset-version' ),
true
);
$api = $c->get( 'api.user-id-token' );
assert( $api instanceof UserIdToken );
try {
$target_customer_id = '';
if ( is_user_logged_in() ) {
$target_customer_id = get_user_meta( get_current_user_id(), '_ppcp_target_customer_id', true );
if ( ! $target_customer_id ) {
$target_customer_id = get_user_meta( get_current_user_id(), 'ppcp_customer_id', true );
}
}
$id_token = $api->id_token( $target_customer_id );
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
$verification_method =
$settings->has( '3d_secure_contingency' )
? apply_filters( 'woocommerce_paypal_payments_three_d_secure_contingency', $settings->get( '3d_secure_contingency' ) )
: '';
$change_payment_method = wc_clean( wp_unslash( $_GET['change_payment_method'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification
wp_localize_script(
'ppcp-add-payment-method',
'ppcp_add_payment_method',
array(
'client_id' => $c->get( 'button.client_id' ),
'merchant_id' => $c->get( 'api.merchant_id' ),
'id_token' => $id_token,
'payment_methods_page' => wc_get_account_endpoint_url( 'payment-methods' ),
'view_subscriptions_page' => wc_get_account_endpoint_url( 'view-subscription' ),
'is_subscription_change_payment_page' => $this->is_subscription_change_payment_method_page(),
'subscription_id_to_change_payment' => $this->is_subscription_change_payment_method_page() ? (int) $change_payment_method : 0,
'error_message' => __( 'Could not save payment method.', 'woocommerce-paypal-payments' ),
'verification_method' => $verification_method,
'ajax' => array(
'create_setup_token' => array(
'endpoint' => \WC_AJAX::get_endpoint( CreateSetupToken::ENDPOINT ),
'nonce' => wp_create_nonce( CreateSetupToken::nonce() ),
),
'create_payment_token' => array(
'endpoint' => \WC_AJAX::get_endpoint( CreatePaymentToken::ENDPOINT ),
'nonce' => wp_create_nonce( CreatePaymentToken::nonce() ),
),
'subscription_change_payment_method' => array(
'endpoint' => \WC_AJAX::get_endpoint( SubscriptionChangePaymentMethod::ENDPOINT ),
'nonce' => wp_create_nonce( SubscriptionChangePaymentMethod::nonce() ),
),
),
'labels' => array(
'error' => array(
'generic' => __(
'Something went wrong. Please try again or choose another payment source.',
'woocommerce-paypal-payments'
),
),
),
)
);
} catch ( RuntimeException $exception ) {
$logger = $c->get( 'woocommerce.logger.woocommerce' );
assert( $logger instanceof LoggerInterface );
$error = $exception->getMessage();
if ( is_a( $exception, PayPalApiException::class ) ) {
$error = $exception->get_details( $error );
}
$logger->error( $error );
}
}
);
$id_token = $api->id_token( $target_customer_id );
add_action(
'woocommerce_add_payment_method_form_bottom',
function () {
if ( ! is_user_logged_in() || ! is_add_payment_method_page() ) {
return;
}
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
$verification_method =
$settings->has( '3d_secure_contingency' )
? apply_filters( 'woocommerce_paypal_payments_three_d_secure_contingency', $settings->get( '3d_secure_contingency' ) )
: '';
$change_payment_method = wc_clean( wp_unslash( $_GET['change_payment_method'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification
wp_localize_script(
'ppcp-add-payment-method',
'ppcp_add_payment_method',
array(
'client_id' => $c->get( 'button.client_id' ),
'merchant_id' => $c->get( 'api.merchant_id' ),
'id_token' => $id_token,
'payment_methods_page' => wc_get_account_endpoint_url( 'payment-methods' ),
'view_subscriptions_page' => wc_get_account_endpoint_url( 'view-subscription' ),
'is_subscription_change_payment_page' => $this->is_subscription_change_payment_method_page(),
'subscription_id_to_change_payment' => $this->is_subscription_change_payment_method_page() ? (int) $change_payment_method : 0,
'error_message' => __( 'Could not save payment method.', 'woocommerce-paypal-payments' ),
'verification_method' => $verification_method,
'ajax' => array(
'create_setup_token' => array(
'endpoint' => \WC_AJAX::get_endpoint( CreateSetupToken::ENDPOINT ),
'nonce' => wp_create_nonce( CreateSetupToken::nonce() ),
),
'create_payment_token' => array(
'endpoint' => \WC_AJAX::get_endpoint( CreatePaymentToken::ENDPOINT ),
'nonce' => wp_create_nonce( CreatePaymentToken::nonce() ),
),
'subscription_change_payment_method' => array(
'endpoint' => \WC_AJAX::get_endpoint( SubscriptionChangePaymentMethod::ENDPOINT ),
'nonce' => wp_create_nonce( SubscriptionChangePaymentMethod::nonce() ),
),
),
'labels' => array(
'error' => array(
'generic' => __(
'Something went wrong. Please try again or choose another payment source.',
'woocommerce-paypal-payments'
),
),
),
)
);
} catch ( RuntimeException $exception ) {
$logger = $c->get( 'woocommerce.logger.woocommerce' );
assert( $logger instanceof LoggerInterface );
$error = $exception->getMessage();
if ( is_a( $exception, PayPalApiException::class ) ) {
$error = $exception->get_details( $error );
echo '<div id="ppc-button-' . esc_attr( PayPalGateway::ID ) . '-save-payment-method"></div>';
}
);
$logger->error( $error );
}
}
);
add_action(
'wc_ajax_' . CreateSetupToken::ENDPOINT,
static function () use ( $c ) {
$endpoint = $c->get( 'save-payment-methods.endpoint.create-setup-token' );
assert( $endpoint instanceof CreateSetupToken );
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 ) ) {
return;
}
echo '<div id="ppc-button-' . esc_attr( PayPalGateway::ID ) . '-save-payment-method"></div>';
}
);
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 );
$endpoint->handle_request();
}
);
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 );
$endpoint->handle_request();
}
);
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 );
$endpoint->handle_request();
}
);
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 );
$endpoint->delete( $token_id );
} catch ( RuntimeException $exception ) {
$logger = $c->get( 'woocommerce.logger.woocommerce' );
assert( $logger instanceof LoggerInterface );
$error = $exception->getMessage();
if ( is_a( $exception, PayPalApiException::class ) ) {
$error = $exception->get_details( $error );
$endpoint->handle_request();
}
);
$logger->error( $error );
}
}
);
add_action(
'wc_ajax_' . CreatePaymentToken::ENDPOINT,
static function () use ( $c ) {
$endpoint = $c->get( 'save-payment-methods.endpoint.create-payment-token' );
assert( $endpoint instanceof CreatePaymentToken );
add_filter(
'woocommerce_paypal_payments_credit_card_gateway_supports',
function( array $supports ) use ( $c ): array {
if ( ! self::vault_enabled( $c ) ) {
return $supports;
}
$supports[] = 'tokenization';
$supports[] = 'add_payment_method';
$endpoint->handle_request();
}
);
return $supports;
}
);
add_action(
'wc_ajax_' . CreatePaymentTokenForGuest::ENDPOINT,
static function () use ( $c ) {
$endpoint = $c->get( 'save-payment-methods.endpoint.create-payment-token-for-guest' );
assert( $endpoint instanceof CreatePaymentTokenForGuest );
add_filter(
'woocommerce_paypal_payments_save_payment_methods_eligible',
function( bool $value ) use ( $c ): bool {
if ( ! self::vault_enabled( $c ) ) {
return $value;
}
return true;
$endpoint->handle_request();
}
);
add_action(
'woocommerce_paypal_payments_before_delete_payment_token',
function( string $token_id ) use ( $c ) {
try {
$endpoint = $c->get( 'api.endpoint.payment-tokens' );
assert( $endpoint instanceof PaymentTokensEndpoint );
$endpoint->delete( $token_id );
} catch ( RuntimeException $exception ) {
$logger = $c->get( 'woocommerce.logger.woocommerce' );
assert( $logger instanceof LoggerInterface );
$error = $exception->getMessage();
if ( is_a( $exception, PayPalApiException::class ) ) {
$error = $exception->get_details( $error );
}
$logger->error( $error );
}
}
);
add_filter(
'woocommerce_paypal_payments_credit_card_gateway_supports',
function( array $supports ) use ( $c ): array {
$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;
}
);
add_filter(
'woocommerce_paypal_payments_save_payment_methods_eligible',
function() {
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

@ -1,8 +0,0 @@
<svg width="110" height="38" viewBox="0 0 110 38" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_4177_18052" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="110" height="38">
<path d="M0.416626 0.68335H109.583V37.3167H0.416626V0.68335Z" fill="white"/>
</mask>
<g mask="url(#mask0_4177_18052)">
<path d="M109.583 0.68335V28.0417H103.358V0.68335H109.583ZM101.067 9.91668V28.0917H95.5333V26.525C94.8333 27.2083 94.0333 27.725 93.15 28.0917C92.2583 28.475 91.2916 28.675 90.2583 28.675C88.9583 28.675 87.75 28.4333 86.6416 27.95C85.5333 27.4417 84.5666 26.75 83.75 25.8833C82.925 25.0083 82.275 23.9917 81.7916 22.8333C81.3333 21.65 81.1083 20.375 81.1083 19.0167C81.1083 17.6583 81.3333 16.4 81.7916 15.2417C82.275 14.0583 82.925 13.025 83.75 12.15C84.5647 11.2817 85.5489 10.5896 86.6416 10.1167C87.75 9.60835 88.9583 9.35002 90.2583 9.35002C91.2916 9.35002 92.2583 9.54168 93.15 9.93335C94.0416 10.3 94.8416 10.8167 95.5333 11.5V9.93335H101.067V9.91668ZM91.2583 23.1417C92.3916 23.1417 93.3166 22.7583 94.0416 21.975C94.7916 21.2 95.1666 20.2083 95.1666 19C95.1666 17.7917 94.7916 16.7917 94.0416 16.025C93.3166 15.25 92.3833 14.8583 91.2583 14.8583C90.1333 14.8583 89.1833 15.2417 88.4333 16.025C87.7083 16.8 87.35 17.7917 87.35 19C87.35 20.2083 87.7166 21.2083 88.4333 21.975C89.1833 22.75 90.125 23.1417 91.2583 23.1417ZM72.225 0.68335C73.8666 0.68335 75.2666 0.916683 76.425 1.37502C77.5833 1.83335 78.5583 2.47502 79.3583 3.30002C80.175 4.15002 80.8166 5.11668 81.275 6.20835C81.7333 7.30002 81.9583 8.47502 81.9583 9.73335C81.9583 10.9917 81.7333 12.1667 81.275 13.2583C80.8251 14.3399 80.1748 15.3267 79.3583 16.1667C78.5666 16.9917 77.5833 17.6333 76.425 18.0917C75.2666 18.55 73.8666 18.7833 72.225 18.7833H69.225V28.0833H62.8916V0.68335H72.225ZM71.3166 13.15C72.1666 13.15 72.8083 13.0667 73.275 12.8917C73.7583 12.7 74.1583 12.45 74.4666 12.1667C75.1166 11.5583 75.4416 10.75 75.4416 9.73335C75.4416 8.71668 75.1166 7.90835 74.4666 7.30002C74.15 7.00835 73.7583 6.77502 73.275 6.60835C72.8166 6.41668 72.1666 6.31668 71.3166 6.31668H69.2166V13.15H71.3166ZM39.5583 9.91668H46.4333L51.1 18.6333H51.175L55.3333 9.91668H61.7L48.0583 37.3167H41.725L47.95 24.7833L39.5583 9.91668ZM38.3333 9.91668V28.0917H32.8V26.525C32.1 27.2083 31.3 27.725 30.4166 28.0917C29.525 28.475 28.5583 28.675 27.525 28.675C26.225 28.675 25.0166 28.4333 23.9083 27.95C22.8 27.4417 21.8333 26.75 21.0166 25.8833C20.2 25.0083 19.5416 23.9917 19.0583 22.8333C18.6 21.65 18.375 20.375 18.375 19.0167C18.375 17.6583 18.6 16.4 19.0583 15.2417C19.5416 14.0583 20.1916 13.025 21.0166 12.15C21.8298 11.2799 22.8144 10.5875 23.9083 10.1167C25.0166 9.60835 26.225 9.35002 27.525 9.35002C28.5583 9.35002 29.525 9.54168 30.4166 9.93335C31.3083 10.3 32.1083 10.8167 32.8 11.5V9.93335H38.3333V9.91668ZM28.525 23.1417C29.6583 23.1417 30.5833 22.7583 31.3166 21.975C32.0666 21.2 32.4416 20.2083 32.4416 19C32.4416 17.7917 32.0666 16.7917 31.3166 16.025C30.5916 15.25 29.6583 14.8583 28.525 14.8583C27.3916 14.8583 26.45 15.2417 25.7 16.025C24.975 16.8 24.6166 17.7917 24.6166 19C24.6166 20.2083 24.9833 21.2083 25.7 21.975C26.45 22.75 27.3916 23.1417 28.525 23.1417ZM9.74996 0.68335C11.3916 0.68335 12.7916 0.916683 13.95 1.37502C15.1083 1.83335 16.0833 2.47502 16.8833 3.30002C17.7 4.15002 18.3416 5.11668 18.8 6.20835C19.2583 7.30002 19.4833 8.47502 19.4833 9.73335C19.4833 10.9917 19.2583 12.1667 18.8 13.2583C18.3501 14.3399 17.6998 15.3267 16.8833 16.1667C16.0916 16.9917 15.1083 17.6333 13.95 18.0917C12.7916 18.55 11.3916 18.7833 9.74996 18.7833H6.74996V28.0833H0.416626V0.68335H9.74996ZM8.84996 13.15C9.69996 13.15 10.3416 13.0667 10.8083 12.8917C11.2916 12.7 11.6916 12.45 12 12.1667C12.65 11.5583 12.975 10.75 12.975 9.73335C12.975 8.71668 12.65 7.90835 12 7.30002C11.6833 7.00835 11.2916 6.77502 10.8083 6.60835C10.35 6.41668 9.69996 6.31668 8.84996 6.31668H6.74996V13.15H8.84996Z" fill="black"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.8 KiB

View file

@ -9,6 +9,7 @@
"devDependencies": {
"@wordpress/data": "^10.10.0",
"@wordpress/data-controls": "^4.10.0",
"@wordpress/icons": "^10.14.0",
"@wordpress/scripts": "^30.3.0",
"classnames": "^2.5.1"
},

View file

@ -43,3 +43,14 @@
display: flex;
gap: $gap;
}
@mixin disabled-state($control-type) {
.components-#{$control-type}-control.is-disabled {
.components-#{$control-type}-control__input,
.components-#{$control-type}-control__label,
.components-base-control__help {
opacity: 0.3;
cursor: default;
}
}
}

View file

@ -54,4 +54,11 @@ $card-vertical-gap: 48px;
--color-gray-200: #{$color-gray-200};
--color-gray-100: #{$color-gray-100};
--color-gradient-dark: #{$color-gradient-dark};
--color-preview-background: #FAF8F5;
--color-separators: #{$color-gray-200};
--color-text-title: #{$color-gray-900};
--color-text-main: #{$color-text-text};
--color-text-teriary: #{$color-text-tertiary};
--color-text-description: #{$color-gray-700};
}

View file

@ -0,0 +1,20 @@
@import "./reusable-components/payment-method-item";
@import './reusable-components/accordion-section';
@import './reusable-components/badge-box';
@import './reusable-components/busy-state';
@import './reusable-components/button';
@import './reusable-components/fields';
@import './reusable-components/hstack';
@import './reusable-components/navigation';
@import './reusable-components/onboarding-header';
@import './reusable-components/payment-method-icons';
@import './reusable-components/select-box';
@import './reusable-components/separator';
@import './reusable-components/settings-block';
@import './reusable-components/settings-card';
@import './reusable-components/settings-toggle-block';
@import './reusable-components/settings-wrapper';
@import './reusable-components/spinner-overlay';
@import './reusable-components/tab-navigation';
@import './reusable-components/title-badge';
@import './reusable-components/welcome-docs';

View file

@ -1,27 +1,96 @@
.ppcp-r {
.ppcp-r__radio-value {
@include hide-input-field;
&__radio-value {
@include hide-input-field;
&:checked + .ppcp-r__radio-presentation {
position: relative;
&:checked + .ppcp-r__radio-presentation {
position: relative;
&::before {
content: '';
width: 12px;
height: 12px;
border-radius: 12px;
background-color: $color-blueberry;
display: block;
position: absolute;
transform: translate(-50%, -50%);
left: 50%;
top: 50%;
}
}
}
&::before {
content: '';
width: 12px;
height: 12px;
border-radius: 12px;
background-color: $color-blueberry;
display: block;
position: absolute;
transform: translate(-50%, -50%);
left: 50%;
top: 50%;
}
.ppcp-r__radio-presentation {
@include fake-input-field(20px);
}
.ppcp-r__checkbox-presentation {
@include fake-input-field(2px);
}
.ppcp-r__radio-wrapper {
display: flex;
gap: 18px;
align-items: center;
position: relative;
label {
@include font(13, 20, 400);
color: $color-gray-800;
}
}
.ppcp-r__radio-description {
@include font(13, 20, 400);
margin: 0;
color: $color-gray-800;
}
.ppcp-r__radio-content-additional {
padding-left: 38px;
padding-top: 18px;
}
.ppcp-r-app {
@include disabled-state('base');
@include disabled-state('checkbox');
.components-base-control__label {
@include font(13, 16, 600);
color: $color-gray-900;
text-transform: none;
}
.components-base-control__input {
border: 1px solid $color-gray-700;
border-radius: 2px;
box-shadow: none;
&:focus {
border-color: $color-blueberry;
}
}
&__checkbox {
.components-base-control__help {
margin-bottom: 0;
}
// Text input fields.
input[type='text'] {
@include font(14, 20, 400);
@include primaryFont;
padding: 7px 11px;
border-radius: 2px;
}
// Select lists.
select {
@include font(14, 20, 400);
padding: 7px 27px 7px 11px;
}
// Checkboxes.
.components-checkbox-control {
position: relative;
input {
@ -30,7 +99,7 @@
&:checked {
background-color: $color-blueberry;
border-color:$color-blueberry;
border-color: $color-blueberry;
}
}
@ -43,78 +112,17 @@
}
}
&__radio-presentation {
@include fake-input-field(20px);
// Custom styles.
.components-form-toggle.is-checked > .components-form-toggle__track {
background-color: $color-blueberry;
}
&__checkbox-presentation {
@include fake-input-field(2px);
}
&__radio-wrapper {
display: flex;
gap: 18px;
align-items: center;
position: relative;
label {
@include font(13, 20, 400);
color: $color-gray-800;
}
}
&__radio-description {
@include font(13, 20, 400);
margin: 0;
color: $color-gray-800;
}
&__radio-content-additional {
padding-left: 38px;
padding-top: 18px;
}
}
.components-base-control {
&__label {
color: $color-gray-900;
@include font(13, 16, 600);
text-transform: none;
}
&__input {
border: 1px solid $color-gray-700;
border-radius: 2px;
box-shadow: none;
&:focus {
border-color: $color-blueberry;
.ppcp-r-vertical-text-control {
.components-base-control__field {
display: flex;
flex-direction: column;
gap: 0;
margin: 0;
}
}
}
input[type='text'] {
padding: 7px 11px;
@include font(14, 20, 400);
@include primaryFont;
border-radius: 2px;
}
select {
padding: 7px 27px 7px 11px;
@include font(14, 20, 400);
}
.components-form-toggle.is-checked > .components-form-toggle__track {
background-color: $color-blueberry;
}
.ppcp-r-vertical-text-control {
.components-base-control__field {
display: flex;
flex-direction: column;
gap: 0;
margin: 0;
}
}

View file

@ -0,0 +1,20 @@
.components-flex {
display: flex;
-webkit-box-align: stretch;
align-items: stretch;
flex-direction: column;
-webkit-box-pack: center;
justify-content: center;
.components-h-stack {
flex-direction: row;
justify-content: flex-start;
gap: 32px;
}
// Fix layout for checkboxes inside a flex-stack.
.components-checkbox-control >.components-base-control__field > .components-flex {
flex-direction: row;
gap: 12px;
}
}

View file

@ -119,8 +119,6 @@
background-color: $color-white;
&::before {
transform: translate(3px, 3px);
border-width: 6px;
border-color: $color-blueberry;
}
}

View file

@ -0,0 +1,194 @@
/*
Styles the `SettingsBlock` and all its derived components.
*/
.ppcp-r-settings-block {
display: flex;
flex-direction: column;
gap: var(--block-item-gap, 16px);
&.ppcp-r-settings-block__input,
&.ppcp-r-settings-block__select {
gap: 6px 0;
}
.ppcp-r-settings-block__header {
display: flex;
flex-direction: column;
gap: 6px;
&:not(:last-child) {
padding-bottom: var(--block-header-gap, 6px);
}
}
.ppcp-r-settings-block__title {
@include font(11, 22, 600);
color: var(--color-text-title);
display: block;
text-transform: uppercase;
&.style-alt {
@include font(14, 16, 600);
text-transform: none;
}
&.style-big {
@include font(16, 20, 600);
}
.ppcp-r-title-badge {
text-transform: none;
margin-left: 6px;
}
}
.ppcp-r-settings-block__title-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
}
&.ppcp-r-settings-block__feature {
.ppcp-r-settings-block__title {
@include font(13, 20, 600);
color: var(--color-text-main);
text-transform: none;
}
.ppcp-r-settings-block__feature__description {
@include font(13, 20, 400);
color: var(--color-text-description);
}
}
&.ppcp-r-settings-block__toggle {
display: flex;
flex-direction: row;
.ppcp-r-settings-block__title {
@include font(13, 20, 400);
color: var(--color-text-main);
text-transform: none;
}
}
.ppcp-r-settings-block__description {
@include font(13, 20, 400);
margin: 0;
color: var(--color-text-description);
&:not(:last-child) {
padding-bottom: 1em;
}
a {
color: var(--color-blueberry);
}
strong {
color: var(--color-gray-800);
}
}
.ppcp-r-settings-block__supplementary-title-label {
@include font(13, 20, 400);
color: var(--color-text-teriary);
text-transform: none;
margin-left: 5px;
}
.ppcp-r-settings-block__action {
display: flex;
align-items: center;
.components-flex {
row-gap: 0;
}
}
+ .ppcp-r-settings-block:not(.no-gap) {
margin-top: var(--block-separator-gap, 32px);
padding-top: var(--block-separator-gap, 32px);
border-top: 1px solid var(--color-gray-200);
}
// Types
&--toggle-content {
&.ppcp-r-settings-block--content-visible {
.ppcp-r-settings-block__toggle-content {
transform: rotate(180deg);
}
}
.ppcp-r-settings-block__header {
user-select: none;
&:hover {
cursor: pointer;
}
}
}
&--sandbox-connected {
.ppcp-r-settings-block__content {
margin-top: 24px;
}
.ppcp-r-connection-status__data {
margin-bottom: 20px;
}
}
&--connect-sandbox {
button.components-button {
@include small-button;
}
.ppcp-r__radio-content-additional {
@include vertical-layout-event-gap(24px);
align-items: flex-start;
.ppcp-r-vertical-text-control,
input[type='text'] {
width: 100%;
}
}
}
}
.ppcp-r-settings-block {
&--order-intent,
&--save-payment-methods {
@include vertical-layout-event-gap(24px);
> .ppcp-r-settings-block__content {
@include vertical-layout-event-gap(24px);
}
}
}
.ppcp-r-settings-block--toggle-content {
.ppcp-r-settings-block__content {
margin-top: 32px;
}
}
.ppcp-r-settings-block__button {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 50px;
}
.ppcp-r-settings-block__accordion {
> .ppcp-r-accordion {
width: 100%;
.ppcp-r-accordion__toggler {
width: 100%;
margin: 0;
text-align: unset;
}
}
}

View file

@ -0,0 +1,63 @@
/*
Styles the `SettingsCard` layout component.
This is a 2-column row that displays a title + description on the
left side, and a "card" with settings content on the right side.
*/
.ppcp-r-settings-card {
// -- Theming
--card-width-header: 100%;
--card-width-content: 100%;
--card-gap: 0;
--card-layout: block;
@media screen and (min-width: 960px) {
--card-width-header: 280px;
--card-width-content: 610px;
--card-gap: 48px;
--card-layout: flex;
}
// -- Styling
display: var(--card-layout);
gap: var(--card-gap);
margin: 0 0 var(--card-gap) 0;
.ppcp-r-settings-card__header {
display: var(--card-layout);
width: var(--card-width-header);
flex: 0 0 var(--card-width-header);
gap: 18px;
padding-bottom: 18px;
}
.ppcp-r-settings-card__content-wrapper {
display: flex;
flex-direction: column;
gap: 24px;
}
.ppcp-r-settings-card__content {
flex: 1;
max-width: var(--card-width-content);
border: 1px solid var(--color-gray-200);
border-radius: 4px;
padding: 24px;
}
.ppcp-r-settings-card__title {
@include font(13, 24, 600);
color: var(--color-text-main);
margin: 0 0 4px 0;
display: block;
}
.ppcp-r-settings-card__description {
@include font(13, 20, 400);
color: var(--color-text-teriary);
margin: 0;
}
}

View file

@ -26,60 +26,4 @@
border-bottom: 1px solid $color-gray-200;
}
}
&-settings-card {
@media screen and (min-width: 960px) {
display: flex;
gap: 48px;
}
@media screen and (max-width: 480px) {
padding: 24px;
}
&__content-wrapper {
display: flex;
flex-direction: column;
gap: 24px;
}
&__header {
display: flex;
gap: 18px;
padding-bottom: 18px;
border-bottom: 2px solid $color-gray-700;
margin-bottom: 32px;
@media screen and (min-width: 960px) {
width: 280px;
flex-shrink: 0;
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
}
&__content {
border: 1px solid $color-gray-200;
border-radius: 4px;
padding: 24px;
@media screen and (min-width: 960px) {
flex: 1;
}
}
&__title {
@include font(13, 24, 600);
color: $color-text-text;
margin: 0 0 4px 0;
display: block;
}
&__description {
@include font(13, 20, 400);
color: $color-text-tertiary;
margin: 0;
}
}
}

View file

@ -1,4 +1,7 @@
@import "./settings/block-accordion";
@import './settings/input';
@import './settings/connection-status';
@import './settings/tab-styling';
@import './settings/tab-paylater-configurator';
// Container and Tab Settings
.ppcp-r-tabs.settings,
@ -11,10 +14,6 @@
}
// Todo List and Feature Items
.ppcp-r-tab-overview-todo {
margin: 0 0 48px 0;
}
.ppcp-r-todo-item {
position: relative;
display: flex;
@ -22,6 +21,16 @@
gap: 18px;
width: 100%;
&:hover {
cursor: pointer;
.ppcp-r-todo-item__inner {
.ppcp-r-todo-item__description {
color: $color-text-text;
}
}
}
&:not(:last-child) {
border-bottom: 1px solid $color-gray-400;
padding-bottom: 16px;
@ -66,6 +75,14 @@
@include font(13, 20, 400);
color: $color-blueberry;
}
&__icon {
border: 1px dashed #949494;
background: #fff;
border-radius: 50%;
width: 24px;
height: 24px;
}
}
.ppcp-r-feature-item {
@ -101,90 +118,8 @@
span {
font-weight: 500;
}
}
}
// Connection Status
.ppcp-r-connection-status {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 12px;
&__status-status {
margin: 0 0 8px 0;
strong {
@include font(14, 24, 700);
color: $color-black;
}
}
&__show-all-data {
margin-left: 12px;
}
&__status-label {
@include font(11, 22, 600);
color: $color-gray-900;
display: block;
text-transform: uppercase;
}
&__status-value {
@include font(13, 26, 400);
color: $color-text-tertiary;
}
&__data {
display: flex;
flex-direction: column;
gap: 12px;
}
&__status-toggle--toggled {
.ppcp-r-connection-status__show-all-data {
transform: rotate(180deg);
}
}
&__status-row {
display: flex;
flex-direction: column;
* {
user-select: none;
}
strong {
@include font(14, 24, 600);
color: $color-gray-800;
margin-right: 12px;
white-space: nowrap;
}
.ppcp-r-connection-status__status-toggle {
line-height: 0;
}
&--first {
&:hover {
cursor: pointer;
}
}
}
@media screen and (max-width: 767px) {
flex-wrap: wrap;
&__status {
width: 100%;
}
&__status-row {
flex-wrap: wrap;
strong {
width: 100%;
}
span {
word-break: break-all;
}
}
margin-top: 24px;
}
}
@ -230,306 +165,3 @@
gap: 48px;
}
// Settings Card and Block Styles
.ppcp-r-settings-card__content {
> .ppcp-r-settings-block {
&:not(:last-child) {
border-bottom: 1px solid $color-divider;
}
}
}
.ppcp-r-settings-block {
display: flex;
flex-direction: column;
gap: 16px 0;
&.ppcp-r-settings-block__input,
&.ppcp-r-settings-block__select {
gap: 6px 0;
}
.ppcp-r-settings-block__header {
display: flex;
flex-direction: column;
gap: 6px;
&:not(:last-child):not(.ppcp-r-settings-block--accordion__header) {
padding-bottom: 6px;
}
}
.ppcp-r-settings-block__title {
@include font(11, 22, 600);
color: $color-gray-900;
display: block;
text-transform: uppercase;
.ppcp-r-title-badge {
text-transform: none;
margin-left: 6px;
}
}
.ppcp-r-settings-block__title-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
}
&.ppcp-r-settings-block__feature {
.ppcp-r-settings-block__title {
@include font(13, 20, 600);
color: $color-text-text;
text-transform: none;
}
.ppcp-r-settings-block__feature__description {
color: $color-gray-700;
@include font(13, 20, 400);
}
}
&.ppcp-r-settings-block__toggle {
display: flex;
flex-direction: row;
.ppcp-r-settings-block__title {
color: $color-text-text;
@include font(13, 20, 400);
text-transform: none;
}
}
.ppcp-r-settings-block__description {
margin: 0;
@include font(13, 20, 400);
color: $color-gray-800;
&:not(:last-child) {
padding-bottom: 1em;
}
a {
color: $color-blueberry;
}
strong {
color: $color-gray-800;
}
}
.ppcp-r-settings-block__supplementary-title-label {
@include font(13, 20, 400);
color: $color-text-tertiary;
text-transform: none;
margin-left: 5px;
}
// Types
&--toggle-content {
&.ppcp-r-settings-block--content-visible {
.ppcp-r-settings-block__toggle-content {
transform: rotate(180deg);
}
}
.ppcp-r-settings-block__header {
user-select: none;
&:hover {
cursor: pointer;
}
}
}
&--sandbox-connected {
.ppcp-r-settings-block__content {
margin-top: 24px;
}
.ppcp-r-connection-status__data {
margin-bottom: 20px;
}
}
&--connect-sandbox {
button.components-button {
@include small-button;
}
.ppcp-r__radio-content-additional {
.ppcp-r-vertical-text-control {
width: 100%;
}
@include vertical-layout-event-gap(24px);
align-items: flex-start;
input[type='text'] {
width: 100%;
}
}
}
&--troubleshooting,
&--settings {
> .ppcp-r-settings-block__content > *:not(:last-child) {
padding-bottom: 32px;
margin-bottom: 32px;
border-bottom: 1px solid $color-gray-500;
}
}
// Fields
input[type='text'] {
border-color: $color-gray-700;
width: 100%;
max-width: 100%;
color: $color-gray-800;
&::placeholder {
color: $color-gray-700;
}
}
// MultiSelect control
.ppcp-r {
&__radio-wrapper {
align-items: flex-start;
gap: 12px;
}
&__radio-content {
display: flex;
flex-direction: column;
gap: 4px;
label {
font-weight: 600;
}
}
&__radio-content-additional {
padding-left: 32px;
}
// Select control styles
&__control {
border-radius: 2px;
border-color: $color-gray-700;
min-height: auto;
padding: 0;
}
&__input-container {
padding: 0;
margin: 0;
}
&__value-container {
padding: 0 0 0 7px;
}
&__indicator {
padding: 5px;
}
&__indicator-separator {
display: none;
}
&__value-container--has-value {
.ppcp-r__single-value {
color: $color-gray-800;
}
}
&__placeholder,
&__single-value {
@include font(13, 20, 400);
}
&__option {
&--is-selected {
background-color: $color-gray-200;
}
}
}
}
// Hooks table
.ppcp-r-table {
&__hooks-url {
width: 70%;
padding-right: 20%;
text-align: left;
vertical-align: top;
}
&__hooks-events {
vertical-align: top;
text-align: left;
width: 40%;
span {
display: block;
}
}
td.ppcp-r-table__hooks-url,
td.ppcp-r-table__hooks-events {
padding-top: 12px;
color: $color-gray-800;
@include font(14, 20, 400);
span {
color: inherit;
@include font(14, 20, 400);
}
}
th.ppcp-r-table__hooks-url,
th.ppcp-r-table__hooks-events {
@include font(14, 20, 700);
color: $color-gray-800;
border-bottom: 1px solid $color-gray-600;
padding-bottom: 4px;
}
}
// Settings specific styles
.ppcp-r-settings-card--common-settings .ppcp-r-settings-card__content,
.ppcp-r-settings-card--expert-settings .ppcp-r-settings-card__content {
> .ppcp-r-settings-block {
&:not(:last-child) {
padding-bottom: 32px;
margin-bottom: 32px;
}
}
}
.ppcp-r-settings-block {
&--order-intent,
&--save-payment-methods {
@include vertical-layout-event-gap(24px);
> .ppcp-r-settings-block__content {
@include vertical-layout-event-gap(24px);
}
}
}
.ppcp-r-settings-block--toggle-content {
.ppcp-r-settings-block__content {
margin-top: 32px;
}
}
.ppcp-r-settings-block__button {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 50px;
}

View file

@ -1,121 +0,0 @@
.ppcp-r-styling {
display: flex;
border: 1px solid $color-gray-200;
border-radius: 8px;
overflow: hidden;
&__section:not(:last-child) {
border-bottom: 1px solid black;
padding-bottom: 24px;
margin-bottom: 28px;
border-bottom: 1px solid $color-gray-600;
}
&__main-title {
@include font(14, 20, 600);
color: $color-gray-800;
margin: 0 0 8px 0;
display: block;
}
&__description {
@include font(13, 20, 400);
color: $color-gray-800;
margin: 0 0 18px 0;
}
&__settings {
width: 422px;
background-color: $color-white;
padding: 48px;
}
&__preview {
width: calc(100% - 422px);
background-color: #FAF8F5;
display: flex;
align-items: center;
&-inner {
width: 100%;
padding: 24px;
}
}
&__section--rc {
.ppcp-r-styling__title {
@include font(13, 20, 600);
color: $color-black;
display: block;
margin: 0 0 18px 0;
}
}
&__section--empty.ppcp-r-styling__section {
padding-bottom: 0;
margin-bottom: 0;
border-bottom: none;
}
&__select {
label {
@include font(13, 16, 600);
color: $color-black;
margin: 0;
text-transform: none;
}
select {
@include font(13, 20, 400);
}
}
.ppcp-r__checkbox {
.components-checkbox-control {
&__label {
@include font(13, 20, 400);
color: $color-black;
}
}
.components-flex {
gap: 12px;
}
}
&__payment-method-checkboxes {
display: flex;
flex-direction: column;
gap: 24px;
}
}
.ppcp-r {
&__horizontal-control {
.components-flex {
flex-direction: row;
justify-content: flex-start;
gap: 32px;
}
.components-radio-control {
&__option {
gap: 12px;
input {
margin: 0;
}
label {
@include font(13, 20, 400);
color: $color-black;
}
}
input {
margin: 0;
}
}
}
}

View file

@ -1,38 +0,0 @@
.ppcp-r-settings-block__accordion {
> .ppcp-r-accordion {
width: 100%;
.ppcp-r-accordion__toggler {
width: 100%;
margin: 0;
text-align: unset;
}
}
&.ppcp-r-settings-block {
gap: 0;
.ppcp-r-settings-block__title {
@include font(13, 20, 600);
color: $color-text-text;
text-transform: none;
}
.ppcp-r-settings-block--accordion__title {
@include font(14, 20, 600);
}
.ppcp-r-settings-block--accordion__description {
color: $color-gray-700;
@include font(13, 20, 400);
}
.ppcp-r-settings-block:not(:last-child) {
&:not(.ppcp-r__radio-content-additional .ppcp-r-settings-block) {
padding-bottom: 32px;
margin-bottom: 32px;
border-bottom: 1px solid $color-divider;
}
}
}
}

View file

@ -0,0 +1,78 @@
// Connection Status
.ppcp-r-connection-status {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 12px;
&__status-status {
margin: 0 0 8px 0;
strong {
@include font(14, 24, 700);
color: $color-black;
}
}
&__status-label {
@include font(11, 22, 600);
color: $color-gray-900;
display: block;
text-transform: uppercase;
}
&__status-value {
@include font(13, 26, 400);
color: $color-text-tertiary;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&__data {
display: flex;
flex-direction: column;
gap: 12px;
width: 100%;
}
&__status-toggle--toggled {
.ppcp-r-connection-status__show-all-data {
transform: rotate(180deg);
}
}
&__status-row {
display: flex;
flex-direction: column;
strong {
@include font(14, 24, 600);
color: $color-gray-800;
margin-right: 12px;
white-space: nowrap;
}
.ppcp-r-connection-status__status-toggle {
line-height: 0;
}
}
@media screen and (max-width: 767px) {
flex-wrap: wrap;
&__status {
width: 100%;
}
&__status-row {
flex-wrap: wrap;
strong {
width: 100%;
}
span {
word-break: break-all;
}
}
}
}

View file

@ -0,0 +1,77 @@
// Fields
.ppcp-r {
input[type='text'] {
border-color: $color-gray-700;
width: 100%;
max-width: 100%;
color: $color-gray-800;
&::placeholder {
color: $color-gray-700;
}
}
}
// MultiSelect control
.ppcp-r {
&__radio-wrapper {
align-items: flex-start;
gap: 12px;
}
&__radio-content {
display: flex;
flex-direction: column;
gap: 4px;
label {
font-weight: 600;
}
}
&__radio-content-additional {
padding-left: 32px;
}
// Select control styles
&__control {
border-radius: 2px;
border-color: $color-gray-700;
min-height: auto;
padding: 0;
}
&__input-container {
padding: 0;
margin: 0;
}
&__value-container {
padding: 0 0 0 7px;
}
&__indicator {
padding: 5px;
}
&__indicator-separator {
display: none;
}
&__value-container--has-value {
.ppcp-r__single-value {
color: $color-gray-800;
}
}
&__placeholder,
&__single-value {
@include font(13, 20, 400);
}
&__option {
&--is-selected {
background-color: $color-gray-200;
}
}
}

View file

@ -0,0 +1,100 @@
.ppcp-r-paylater-configurator {
display: flex;
border: 1px solid var(--color-separators);
border-radius: 8px;
overflow: hidden;
font-family: "PayPalPro", sans-serif;
-webkit-font-smoothing: antialiased;
.css-1snxoyf.eolpigi0 {
margin: 0;
}
#configurator-eligibleContainer.css-4nclxm.e1vy3g880 {
width: 100%;
max-width: 100%;
padding: 48px 0px 48px 48px;
#configurator-controlPanelContainer.css-5urmrq.e1vy3g880 {
width: 374px;
padding-right: 48px;
}
#configurator-previewSectionContainer.css-vojyxx.e1vy3g880 {
width: calc(100% - 374px);
.css-7xkxom, .css-8tvj6u {
height: auto;
}
.css-10nkerk.ej6n7t60 {
align-items: flex-start;
}
.css-1sgwra0-svg-size_sm {
height: 1.2rem;
width: 1.2rem;
}
.css-1vc34jy-handler {
height: 1.6rem;
width: 1.6rem;
}
.css-8vwtr6-state {
height: 1.6rem;
}
}
}
&__subheader, #configurator-controlPanelSubHeader {
color: var(--color-text-description);
margin: 0 0 18px 0;
}
&__header, #configurator-controlPanelHeader, #configurator-previewSectionSubHeaderText.css-14ujlqd-text_body, .css-16jt5za-text_body {
@include font(16, 20, 600);
color: var(--color-text-title);
margin-bottom: 6px;
font-family: "PayPalPro", sans-serif;
-webkit-font-smoothing: antialiased;
}
.css-1yo2lxy-text_body_strong {
color: var(--color-text-description);
margin: 0;
text-transform: none;
}
.css-rok10q, .css-dfgbdq-text_body_strong {
margin-top: 0;
}
&__publish-button {
display: none;
}
.css-udzaps {
padding: 0px;
}
.css-104jwuk,
.css-dpyjrq-text_body,
.css-1oxdnb3-dropdown_menu_button-text_field_value_sm-active,
.css-1wvwydd-dropdown_menu_button-text_field_value_sm-active-active,
.css-16jt5za-text_body,
.css-1caaugt-links_base-text_body_strong,
.css-dpyjrq-text_body,
&__subheader,
#configurator-controlPanelSubHeader,
.css-1yo2lxy-text_body_strong{
@include font(13, 20, 400);
font-family: "PayPalPro", sans-serif;
-webkit-font-smoothing: antialiased;
}
.css-1k9r7mv-text_body, .css-ra9ecy-text_body_strong {
font-family: "PayPalPro", sans-serif;
-webkit-font-smoothing: antialiased;
}
}

View file

@ -0,0 +1,96 @@
.ppcp-r-styling {
--block-item-gap: 0;
--block-separator-gap: 24px;
--block-header-gap: 18px;
--panel-width: 422px;
--sticky-offset-top: 92px; // 32px admin-bar + 60px TopNavigation height
--preview-height-reduction: 236px; // 32px admin-bar + 60px TopNavigation height + 48px TopNavigation margin + 48px TabList height + 48px TabList margin
display: flex;
border: 1px solid var(--color-separators);
border-radius: 8px;
.ppcp-r-settings-block {
&.header-section {
margin-bottom: 6px
}
&.location-selector {
position: sticky;
top: var(--sticky-offset-top);
background: var(--ppcp-color-app-bg);
z-index: 5;
padding: 16px 10px 8px;
margin: 0 -10px -8px;
.section-content {
display: flex;
& > .components-base-control:first-of-type {
width: 100%;
}
}
}
// Select-fields have a smaller gap between the header and input field.
&.has-select {
--block-header-gap: 8px;
}
// Above the payment methods is a slightly larger gap.
&.payment-methods {
--block-separator-gap: 28px;
}
// It has no header; adjusts the gap to the control right above the tagline.
&.tagline {
--block-header-gap: 24px;
}
}
/* The settings-panel (left side) */
.settings-panel {
width: var(--panel-width);
padding: 48px;
.ppcp-r-styling__section {
padding-bottom: 24px;
margin-bottom: 28px;
border-bottom: 1px solid var(--color-separators);
&.no-gap,
&:last-child {
padding-bottom: 0;
margin-bottom: 0;
border-bottom-style: none;
}
}
// Horizontal radio buttons have a width of 100px.
.components-radio-control__option {
min-width: 100px;
}
}
/* The preview area (right side) */
.preview-panel {
width: calc(100% - var(--panel-width));
background-color: var(--color-preview-background);
z-index: 0;
.preview-panel-inner {
position: sticky;
display: flex;
flex-direction: column;
justify-content: center;
width: 100%;
padding: 24px;
height: calc(100vh - var(--preview-height-reduction));
top: var(--sticky-offset-top);
// Disable interactions with the preview.
pointer-events: none;
user-select: none;
}
}
}

View file

@ -3,26 +3,9 @@
#ppcp-settings-container {
@import './global';
@import './components/reusable-components/busy-state';
@import './components/reusable-components/button';
@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';
@import './components/reusable-components/select-box';
@import './components/reusable-components/tab-navigation';
@import './components/reusable-components/navigation';
@import './components/reusable-components/fields';
@import './components/reusable-components/title-badge';
@import './components/reusable-components/accordion-section';
@import './components/reusable-components/badge-box';
@import './components/reusable-components/spinner-overlay';
@import './components/reusable-components/welcome-docs';
@import './components/reusable';
@import './components/screens/onboarding';
@import './components/screens/settings';
@import './components/screens/overview/tab-styling';
@import './components/app';
}

View file

@ -1,8 +0,0 @@
import Settings from './Components/Screens/Settings';
export function App() {
return (
<div>
<Settings />
</div>
);
}

View file

@ -0,0 +1,69 @@
import { useEffect, useMemo } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import classNames from 'classnames';
import { OnboardingHooks, CommonHooks } from '../data';
import SpinnerOverlay from './ReusableComponents/SpinnerOverlay';
import SendOnlyMessage from './Screens/SendOnlyMessage';
import OnboardingScreen from './Screens/Onboarding';
import SettingsScreen from './Screens/Settings';
import { initStore as initSettingsStore } from '../data/settings-tab';
import { useSettingsState } from '../data/settings-tab/hooks';
// Initialize the settings store
initSettingsStore();
const SettingsApp = () => {
const onboardingProgress = OnboardingHooks.useSteps();
const { isReady: settingsIsReady } = useSettingsState();
const {
isReady: merchantIsReady,
merchant: { isSendOnlyCountry },
} = CommonHooks.useMerchantInfo();
// 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 || ! settingsIsReady,
} );
const Content = useMemo( () => {
if (
! onboardingProgress.isReady ||
! merchantIsReady ||
! settingsIsReady
) {
return (
<SpinnerOverlay
message={ __( 'Loading…', 'woocommerce-paypal-payments' ) }
/>
);
}
if ( isSendOnlyCountry ) {
return <SendOnlyMessage />;
}
if ( ! onboardingProgress.completed ) {
return <OnboardingScreen />;
}
return <SettingsScreen />;
}, [
isSendOnlyCountry,
merchantIsReady,
onboardingProgress.completed,
onboardingProgress.isReady,
settingsIsReady,
] );
return <div className={ wrapperClass }>{ Content }</div>;
};
export default SettingsApp;

View file

@ -1,40 +1,62 @@
import data from '../../utils/data';
const BadgeBox = ( props ) => {
const titleSize =
props.titleType && props.titleType === BADGE_BOX_TITLE_BIG
? BADGE_BOX_TITLE_BIG
: BADGE_BOX_TITLE_SMALL;
const ImageBadge = ( { images } ) => {
if ( ! images || ! images.length ) {
return null;
}
return (
<BadgeContent>
<span className="ppcp-r-badge-box__title-image-badge">
{ images.map( ( badge ) => data().getImage( badge ) ) }
</span>
</BadgeContent>
);
};
// If `children` is not empty, it's output and wrapped in spaces.
const BadgeContent = ( { children } ) => {
if ( ! children ) {
return null;
}
return <> { children } </>;
};
const BadgeBox = ( {
title,
textBadge,
imageBadge = [],
titleType = BADGE_BOX_TITLE_BIG,
description = '',
} ) => {
let titleSize = BADGE_BOX_TITLE_SMALL;
if ( BADGE_BOX_TITLE_BIG === titleType ) {
titleSize = BADGE_BOX_TITLE_BIG;
}
const titleTextClassName =
'ppcp-r-badge-box__title-text ' +
`ppcp-r-badge-box__title-text--${ titleSize }`;
const titleBaseClassName = 'ppcp-r-badge-box__title';
const titleClassName = props.imageBadge
const titleClassName = imageBadge.length
? `${ titleBaseClassName } ppcp-r-badge-box__title--has-image-badge`
: titleBaseClassName;
return (
<div className="ppcp-r-badge-box">
<span className={ titleClassName }>
<span className={ titleTextClassName }>{ props.title }</span>
<span className={ titleTextClassName }>{ title }</span>
{ props.imageBadge && (
<span className="ppcp-r-badge-box__title-image-badge">
{ props.imageBadge.map( ( badge ) =>
data().getImage( badge )
) }
</span>
) }
{ props.textBadge }
<ImageBadge images={ imageBadge } />
<BadgeContent>{ textBadge }</BadgeContent>
</span>
<div className="ppcp-r-badge-box__description">
{ props?.description && (
{ description && (
<p
className="ppcp-r-badge-box__description"
dangerouslySetInnerHTML={ {
__html: props.description,
__html: description,
} }
></p>
) }

View file

@ -24,6 +24,7 @@ const BusyContext = createContext( false );
* @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.
* @param {boolean} props.isBusy - Optional. Additional condition to determine if the component is busy.
*/
const BusyStateWrapper = ( {
children,
@ -31,11 +32,12 @@ const BusyStateWrapper = ( {
busySpinner = true,
className = '',
onBusy = () => ( { disabled: true } ),
isBusy = false,
} ) => {
const { isBusy } = CommonHooks.useBusyState();
const { isBusy: globalIsBusy } = CommonHooks.useBusyState();
const hasBusyParent = useContext( BusyContext );
const isBusyComponent = isBusy && enabled;
const isBusyComponent = ( isBusy || globalIsBusy ) && enabled;
const showSpinner = busySpinner && isBusyComponent && ! hasBusyParent;
const wrapperClassName = classNames( 'ppcp-r-busy-wrapper', className, {

View file

@ -1,54 +1,35 @@
import { __ } from '@wordpress/i18n';
import { useState } from '@wordpress/element';
import { CommonHooks } from '../../data';
const ConnectionInfo = ( { connectionStatusDataDefault } ) => {
const [ connectionData, setConnectionData ] = useState( {
...connectionStatusDataDefault,
} );
const toggleStatusClassName = [ 'ppcp-r-connection-status__status-toggle' ];
if ( connectionData.showAllData ) {
toggleStatusClassName.push(
'ppcp-r-connection-status__status-toggle--toggled'
);
}
const ConnectionInfo = () => {
const { merchant } = CommonHooks.useMerchantInfo();
return (
<div className="ppcp-r-connection-status__data">
<div className="ppcp-r-connection-status__status-row ppcp-r-connection-status__status-row--first">
<span className="ppcp-r-connection-status__status-label">
{ __( 'Merchant ID', 'woocommerce-paypal-payments' ) }
</span>
<span className="ppcp-r-connection-status__status-value">
{ connectionData.merchantId }
</span>
</div>
<div className="ppcp-r-connection-status__status-row">
<span className="ppcp-r-connection-status__status-label">
{ __( 'Email address', 'woocommerce-paypal-payments' ) }
</span>
<span className="ppcp-r-connection-status__status-value">
{ connectionData.email }
</span>
</div>
<div className="ppcp-r-connection-status__status-row">
<span className="ppcp-r-connection-status__status-label">
{ __( 'Client ID', 'woocommerce-paypal-payments' ) }
</span>
<span className="ppcp-r-connection-status__status-value">
{ connectionData.clientId }
</span>
</div>
<StatusRow
label={ __( 'Merchant ID', 'woocommerce-paypal-payments' ) }
value={ merchant.id }
/>
<StatusRow
label={ __( 'Email address', 'woocommerce-paypal-payments' ) }
value={ merchant.email }
/>
<StatusRow
label={ __( 'Client ID', 'woocommerce-paypal-payments' ) }
value={ merchant.clientId }
/>
</div>
);
};
export default ConnectionInfo;
export const connectionStatusDataDefault = {
connectionStatus: true,
showAllData: false,
email: 'bt_us@woocommerce.com',
merchantId: 'AT45V2DGMKLRY',
clientId: 'BAARTJLxtUNN4d2GMB6Eut3suMDYad72xQA-FntdIFuJ6FmFJITxAY8',
};
const StatusRow = ( { label, value } ) => (
<div className="ppcp-r-connection-status__status-row">
<span className="ppcp-r-connection-status__status-label">
{ label }
</span>
<span className="ppcp-r-connection-status__status-value">
{ value }
</span>
</div>
);

View file

@ -1,107 +1,138 @@
import { CheckboxControl } from '@wordpress/components';
import classNames from 'classnames';
export const PayPalCheckbox = ( props ) => {
let isChecked = null;
export const PayPalCheckbox = ( {
currentValue,
label,
value,
checked = null,
disabled = null,
changeCallback,
} ) => {
let isChecked = checked;
if ( Array.isArray( props.currentValue ) ) {
isChecked = props.currentValue.includes( props.value );
} else {
isChecked = props.currentValue;
if ( null === isChecked ) {
if ( Array.isArray( currentValue ) ) {
isChecked = currentValue.includes( value );
} else {
isChecked = currentValue;
}
}
const className = classNames( { 'is-disabled': disabled } );
const onChange = ( newState ) => {
let newValue;
if ( ! Array.isArray( currentValue ) ) {
newValue = newState;
} else if ( newState ) {
newValue = [ ...currentValue, value ];
} else {
newValue = currentValue.filter(
( optionValue ) => optionValue !== value
);
}
changeCallback( newValue );
};
return (
<div className="ppcp-r__checkbox">
<CheckboxControl
label={ props?.label ? props.label : '' }
value={ props.value }
checked={ isChecked }
onChange={ ( checked ) =>
handleCheckboxState( checked, props )
}
/>
</div>
<CheckboxControl
label={ label }
value={ value }
checked={ isChecked }
disabled={ disabled }
onChange={ onChange }
className={ className }
/>
);
};
export const PayPalCheckboxGroup = ( props ) => {
const renderCheckboxGroup = () => {
return props.value.map( ( checkbox ) => {
return (
<PayPalCheckbox
label={ checkbox.label }
value={ checkbox.value }
key={ checkbox.value }
currentValue={ props.currentValue }
changeCallback={ props.changeCallback }
/>
);
} );
};
export const CheckboxGroup = ( { options, value, onChange } ) => (
<>
{ options.map( ( checkbox ) => (
<PayPalCheckbox
key={ checkbox.value }
label={ checkbox.label }
value={ checkbox.value }
checked={ checkbox.checked }
disabled={ checkbox.disabled }
description={ checkbox.description }
tooltip={ checkbox.tooltip }
currentValue={ value }
changeCallback={ onChange }
/>
) ) }
</>
);
return <>{ renderCheckboxGroup() }</>;
};
export const PayPalRdb = ( props ) => {
export const PayPalRdb = ( {
id,
name,
value,
currentValue,
handleRdbState,
} ) => {
return (
<div className="ppcp-r__radio">
{ /* todo: Can we remove the wrapper div? */ }
<input
id={ props?.id }
className="ppcp-r__radio-value"
type="radio"
checked={ props.value === props.currentValue }
name={ props.name }
value={ props.value }
onChange={ () => props.handleRdbState( props.value ) }
id={ id }
checked={ value === currentValue }
name={ name }
value={ value }
onChange={ () => handleRdbState( value ) }
/>
<span className="ppcp-r__radio-presentation"></span>
</div>
);
};
export const PayPalRdbWithContent = ( props ) => {
const className = [ 'ppcp-r__radio-wrapper' ];
if ( props?.className ) {
className.push( props.className );
}
export const PayPalRdbWithContent = ( {
className,
id,
name,
label,
description,
value,
currentValue,
handleRdbState,
toggleAdditionalContent,
children,
} ) => {
const wrapperClasses = classNames( 'ppcp-r__radio-wrapper', className );
return (
<div className="ppcp-r__radio-outer-wrapper">
<div className={ className }>
<PayPalRdb { ...props } />
<div className={ wrapperClasses }>
<PayPalRdb
id={ id }
name={ name }
value={ value }
currentValue={ currentValue }
handleRdbState={ handleRdbState }
/>
<div className="ppcp-r__radio-content">
<label htmlFor={ props?.id }>{ props.label }</label>
{ props.description && (
<label htmlFor={ id }>{ label }</label>
{ description && (
<p
className="ppcp-r__radio-description"
dangerouslySetInnerHTML={ {
__html: props.description,
__html: description,
} }
/>
) }
</div>
</div>
{ props?.toggleAdditionalContent &&
props.children &&
props.value === props.currentValue && (
<div className="ppcp-r__radio-content-additional">
{ props.children }
</div>
) }
{ toggleAdditionalContent && children && value === currentValue && (
<div className="ppcp-r__radio-content-additional">
{ children }
</div>
) }
</div>
);
};
export const handleCheckboxState = ( checked, props ) => {
let newValue = null;
if ( ! Array.isArray( props.currentValue ) ) {
newValue = checked;
} else if ( checked ) {
newValue = [ ...props.currentValue, props.value ];
} else {
newValue = props.currentValue.filter(
( value ) => value !== props.value
);
}
props.changeCallback( newValue );
};

View file

@ -0,0 +1,26 @@
/**
* Temporary component, until the experimental HStack block editor component is stable.
*
* @see https://wordpress.github.io/gutenberg/?path=/docs/components-experimental-hstack--docs
* @file
*/
import classNames from 'classnames';
const HStack = ( { className, spacing = 3, children } ) => {
const wrapperClass = classNames(
'components-flex components-h-stack',
className
);
const styles = {
gap: `calc(${ 4 * spacing }px)`,
};
return (
<div className={ wrapperClass } style={ styles }>
{ children }
</div>
);
};
export default HStack;

View file

@ -1 +0,0 @@
export { default as openSignup } from './Icons/open-signup';

View file

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

View file

@ -0,0 +1,12 @@
import { SVG, Path } from '@wordpress/primitives';
const logoPayPal = (
<SVG fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 38">
<Path
d="M109.583.683v27.359h-6.225V.683h6.225Zm-8.516 9.234v18.175h-5.534v-1.567c-.7.683-1.5 1.2-2.383 1.567a7.259 7.259 0 0 1-2.892.583c-1.3 0-2.508-.242-3.616-.725a9.216 9.216 0 0 1-2.892-2.067 10.021 10.021 0 0 1-1.958-3.05c-.459-1.183-.684-2.458-.684-3.816 0-1.359.225-2.617.684-3.775.483-1.184 1.133-2.217 1.958-3.092a8.708 8.708 0 0 1 2.892-2.033c1.108-.509 2.316-.767 3.616-.767 1.034 0 2 .192 2.892.583a7.312 7.312 0 0 1 2.383 1.567V9.933h5.534v-.016Zm-9.809 13.225c1.134 0 2.059-.384 2.784-1.167.75-.775 1.125-1.767 1.125-2.975 0-1.208-.375-2.208-1.125-2.975-.725-.775-1.659-1.167-2.784-1.167-1.125 0-2.075.384-2.825 1.167-.725.775-1.083 1.767-1.083 2.975 0 1.208.367 2.208 1.083 2.975.75.775 1.692 1.167 2.825 1.167ZM72.225.683c1.642 0 3.042.234 4.2.692 1.158.458 2.133 1.1 2.933 1.925a9.439 9.439 0 0 1 1.917 2.908c.458 1.092.683 2.267.683 3.525 0 1.259-.225 2.434-.683 3.525a9.293 9.293 0 0 1-1.917 2.909c-.791.825-1.775 1.466-2.933 1.925-1.158.458-2.558.691-4.2.691h-3v9.3h-6.333V.683h9.333Zm-.908 12.467c.85 0 1.491-.083 1.958-.258a3.853 3.853 0 0 0 1.192-.725c.65-.609.975-1.417.975-2.434 0-1.016-.325-1.825-.975-2.433a3.329 3.329 0 0 0-1.192-.692c-.458-.191-1.108-.291-1.958-.291h-2.1v6.833h2.1ZM39.558 9.917h6.875l4.667 8.716h.075l4.158-8.716H61.7l-13.642 27.4h-6.333l6.225-12.534-8.392-14.866Zm-1.225 0v18.175H32.8v-1.567c-.7.683-1.5 1.2-2.383 1.567a7.258 7.258 0 0 1-2.892.583c-1.3 0-2.508-.242-3.617-.725a9.218 9.218 0 0 1-2.891-2.067 10.18 10.18 0 0 1-1.959-3.05c-.458-1.183-.683-2.458-.683-3.816 0-1.359.225-2.617.683-3.775.484-1.184 1.134-2.217 1.959-3.092a8.626 8.626 0 0 1 2.891-2.033c1.109-.509 2.317-.767 3.617-.767 1.033 0 2 .192 2.892.583A7.312 7.312 0 0 1 32.8 11.5V9.933h5.533v-.016Zm-9.808 13.225c1.133 0 2.058-.384 2.792-1.167.75-.775 1.125-1.767 1.125-2.975 0-1.208-.375-2.208-1.125-2.975-.725-.775-1.659-1.167-2.792-1.167-1.133 0-2.075.384-2.825 1.167-.725.775-1.083 1.767-1.083 2.975 0 1.208.366 2.208 1.083 2.975.75.775 1.692 1.167 2.825 1.167ZM9.75.683c1.642 0 3.042.234 4.2.692 1.158.458 2.133 1.1 2.933 1.925A9.439 9.439 0 0 1 18.8 6.208c.458 1.092.683 2.267.683 3.525 0 1.259-.225 2.434-.683 3.525a9.293 9.293 0 0 1-1.917 2.909c-.791.825-1.775 1.466-2.933 1.925-1.158.458-2.558.691-4.2.691h-3v9.3H.417V.683H9.75Zm-.9 12.467c.85 0 1.492-.083 1.958-.258A3.855 3.855 0 0 0 12 12.167c.65-.609.975-1.417.975-2.434 0-1.016-.325-1.825-.975-2.433a3.33 3.33 0 0 0-1.192-.692c-.458-.191-1.108-.291-1.958-.291h-2.1v6.833h2.1Z"
fill="#000"
/>
</SVG>
);
export default logoPayPal;

View file

@ -1,6 +1,3 @@
/**
* WordPress dependencies
*/
import { SVG, Path } from '@wordpress/primitives';
const openSignup = (

View file

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

View file

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

View file

@ -19,6 +19,28 @@ const FeatureSettingsBlock = ( { title, description, ...props } ) => {
);
};
const renderButton = ( button ) => {
const buttonElement = (
<Button
className={ button.class ? button.class : '' }
key={ button.text }
isBusy={ props.actionProps?.isBusy }
variant={ button.type }
onClick={ button.onClick }
>
{ button.text }
</Button>
);
return button.urls ? (
<a href={ button.urls.live } key={ button.text }>
{ buttonElement }
</a>
) : (
buttonElement
);
};
return (
<SettingsBlock { ...props } className="ppcp-r-settings-block__feature">
<Header>
@ -35,15 +57,7 @@ const FeatureSettingsBlock = ( { title, description, ...props } ) => {
</Header>
<Action>
<div className="ppcp-r-feature-item__buttons">
{ props.actionProps?.buttons.map( ( button ) => (
<Button
href={ button.url }
key={ button.text }
variant={ button.type }
>
{ button.text }
</Button>
) ) }
{ props.actionProps?.buttons.map( renderButton ) }
</div>
</Action>
</SettingsBlock>

View file

@ -1,51 +1,50 @@
import { useState } from '@wordpress/element';
import { ToggleControl } from '@wordpress/components';
import SettingsBlock from './SettingsBlock';
import PaymentMethodIcon from '../PaymentMethodIcon';
import data from '../../../utils/data';
import { hasSettings } from '../../Screens/Overview/TabSettingsElements/Blocks/PaymentMethods';
const PaymentMethodItemBlock = ( props ) => {
const [ toggleIsChecked, setToggleIsChecked ] = useState( false );
const [ modalIsVisible, setModalIsVisible ] = useState( false );
const Modal = props?.modal;
const PaymentMethodItemBlock = ( {
id,
title,
description,
icon,
onTriggerModal,
onSelect,
isSelected,
} ) => {
// Only show settings icon if this method has fields configured
const hasModal = hasSettings( id );
return (
<>
<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
icons={ [ props.icon ] }
type={ props.icon }
/>
<span className="ppcp-r-settings-block__payment-methods__item__title">
{ props.title }
</span>
</div>
<p className="ppcp-r-settings-block__payment-methods__item__description">
{ props.description }
</p>
<div className="ppcp-r-settings-block__payment-methods__item__footer">
<ToggleControl
__nextHasNoMarginBottom={ true }
checked={ toggleIsChecked }
onChange={ setToggleIsChecked }
/>
{ Modal && (
<div
className="ppcp-r-settings-block__payment-methods__item__settings"
onClick={ () => setModalIsVisible( true ) }
>
{ data().getImage( 'icon-settings.svg' ) }
</div>
) }
</div>
<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 icons={ [ icon ] } type={ icon } />
<span className="ppcp-r-settings-block__payment-methods__item__title">
{ title }
</span>
</div>
</SettingsBlock>
{ Modal && modalIsVisible && (
<Modal setModalIsVisible={ setModalIsVisible } />
) }
</>
<p className="ppcp-r-settings-block__payment-methods__item__description">
{ description }
</p>
<div className="ppcp-r-settings-block__payment-methods__item__footer">
<ToggleControl
__nextHasNoMarginBottom={ true }
checked={ isSelected }
onChange={ onSelect }
/>
{ hasModal && onTriggerModal && (
<div
className="ppcp-r-settings-block__payment-methods__item__settings"
onClick={ onTriggerModal }
>
{ data().getImage( 'icon-settings.svg' ) }
</div>
) }
</div>
</div>
</SettingsBlock>
);
};

View file

@ -1,18 +1,25 @@
import { useState, useCallback } from '@wordpress/element';
import SettingsBlock from './SettingsBlock';
import PaymentMethodItemBlock from './PaymentMethodItemBlock';
import { usePaymentMethods } from '../../../data/payment/hooks';
const PaymentMethodsBlock = ( { paymentMethods, className = '' } ) => {
const [ selectedMethod, setSelectedMethod ] = useState( null );
const PaymentMethodsBlock = ( {
paymentMethods,
className = '',
onTriggerModal,
} ) => {
const { setPersistent } = usePaymentMethods();
const handleSelect = useCallback( ( methodId, isSelected ) => {
setSelectedMethod( isSelected ? methodId : null );
}, [] );
if ( paymentMethods.length === 0 ) {
if ( ! paymentMethods?.length ) {
return null;
}
const handleSelect = ( paymentMethod, isSelected ) => {
setPersistent( paymentMethod.id, {
...paymentMethod,
enabled: isSelected,
} );
};
return (
<SettingsBlock
className={ `ppcp-r-settings-block__payment-methods ${ className }` }
@ -21,9 +28,12 @@ const PaymentMethodsBlock = ( { paymentMethods, className = '' } ) => {
<PaymentMethodItemBlock
key={ paymentMethod.id }
{ ...paymentMethod }
isSelected={ selectedMethod === paymentMethod.id }
isSelected={ paymentMethod.enabled }
onSelect={ ( checked ) =>
handleSelect( paymentMethod.id, checked )
handleSelect( paymentMethod, checked )
}
onTriggerModal={ () =>
onTriggerModal?.( paymentMethod.id )
}
/>
) ) }

View file

@ -1,9 +1,11 @@
const SettingsBlock = ( { className, children } ) => {
const blockClassName = [ 'ppcp-r-settings-block', className ].filter(
Boolean
);
import classNames from 'classnames';
return <div className={ blockClassName.join( ' ' ) }>{ children }</div>;
const SettingsBlock = ( { className, children, separatorAndGap = true } ) => {
const blockClassName = classNames( 'ppcp-r-settings-block', className, {
'no-gap': ! separatorAndGap,
} );
return <div className={ blockClassName }>{ children }</div>;
};
export default SettingsBlock;

View file

@ -1,9 +1,24 @@
import classNames from 'classnames';
// Block Elements
export const Title = ( { children, className = '' } ) => (
<span className={ `ppcp-r-settings-block__title ${ className }`.trim() }>
{ children }
</span>
);
export const Title = ( {
children,
altStyle = false,
big = false,
className = '',
} ) => {
const elementClasses = classNames(
'ppcp-r-settings-block__title',
className,
{
'style-alt': altStyle,
'style-big': big,
}
);
return <span className={ elementClasses }>{ children }</span>;
};
export const TitleWrapper = ( { children } ) => (
<span className="ppcp-r-settings-block__title-wrapper">{ children }</span>
);
@ -14,13 +29,28 @@ export const SupplementaryLabel = ( { children } ) => (
</span>
);
export const Description = ( { children, className = '' } ) => (
<span
className={ `ppcp-r-settings-block__description ${ className }`.trim() }
>
{ children }
</span>
);
export const Description = ( { children, asHtml = false, className = '' } ) => {
// Don't output anything if description is empty.
if ( ! children ) {
return null;
}
const elementClasses = classNames(
'ppcp-r-settings-block__description',
className
);
if ( ! asHtml ) {
return <span className={ elementClasses }>{ children }</span>;
}
return (
<span
className={ elementClasses }
dangerouslySetInnerHTML={ { __html: children } }
/>
);
};
export const Action = ( { children } ) => (
<div className="ppcp-r-settings-block__action">{ children }</div>
@ -33,9 +63,18 @@ export const Header = ( { children, className = '' } ) => (
);
// Card Elements
export const Content = ( { children } ) => (
<div className="ppcp-r-settings-card__content">{ children }</div>
);
export const Content = ( { children, className = '', id = '' } ) => {
const elementClasses = classNames(
'ppcp-r-settings-card__content',
className
);
return (
<div id={ id } className={ elementClasses }>
{ children }
</div>
);
};
export const ContentWrapper = ( { children } ) => (
<div className="ppcp-r-settings-card__content-wrapper">{ children }</div>

View file

@ -1,13 +1,4 @@
import { PayPalCheckbox, handleCheckboxState } from '../Fields';
import data from '../../../utils/data';
const TodoSettingsBlock = ( {
todos,
setTodos,
todosData,
setTodosData,
className = '',
} ) => {
const TodoSettingsBlock = ( { todosData, className = '' } ) => {
if ( todosData.length === 0 ) {
return null;
}
@ -16,54 +7,33 @@ const TodoSettingsBlock = ( {
<div
className={ `ppcp-r-settings-block__todo ppcp-r-todo-items ${ className }` }
>
{ todosData.map( ( todo ) => (
<TodoItem
name="todo_items"
key={ todo.value }
value={ todo.value }
currentValue={ todos }
changeCallback={ setTodos }
description={ todo.description }
changeTodos={ setTodosData }
todosData={ todosData }
/>
) ) }
{ todosData
.slice( 0, 5 )
.filter( ( todo ) => {
return ! todo.isCompleted();
} )
.map( ( todo ) => (
<TodoItem
key={ todo.id }
title={ todo.title }
onClick={ todo.onClick }
/>
) ) }
</div>
);
};
const TodoItem = ( props ) => {
return (
<div className="ppcp-r-todo-item">
<div className="ppcp-r-todo-item" onClick={ props.onClick }>
<div className="ppcp-r-todo-item__inner">
<PayPalCheckbox
{ ...{
...props,
handleCheckboxState,
} }
/>
<div className="ppcp-r-todo-item__icon"></div>
<div className="ppcp-r-todo-item__description">
{ props.description }
{ props.title }
</div>
</div>
<div
className="ppcp-r-todo-item__close"
onClick={ () =>
removeTodo(
props.value,
props.todosData,
props.changeTodos
)
}
>
{ data().getImage( 'icon-close.svg' ) }
</div>
</div>
);
};
const removeTodo = ( todoValue, todosData, changeTodos ) => {
changeTodos( todosData.filter( ( todo ) => todo.value !== todoValue ) );
};
export default TodoSettingsBlock;

View file

@ -1,6 +1,9 @@
import classNames from 'classnames';
import { Content, ContentWrapper } from './SettingsBlocks';
const SettingsCard = ( {
id,
className: extraClassName,
title,
description,
@ -8,17 +11,17 @@ const SettingsCard = ( {
contentItems,
contentContainer = true,
} ) => {
const className = [ 'ppcp-r-settings-card', extraClassName ]
.filter( Boolean )
.join( ' ' );
const className = classNames( 'ppcp-r-settings-card', extraClassName );
const renderContent = () => {
// If contentItems array is provided, wrap each item in Content component
if ( contentItems ) {
return (
<ContentWrapper>
{ contentItems.map( ( item, index ) => (
<Content key={ index }>{ item }</Content>
{ contentItems.map( ( item ) => (
<Content key={ item.key } id={ item.key }>
{ item }
</Content>
) ) }
</ContentWrapper>
);
@ -33,7 +36,7 @@ const SettingsCard = ( {
};
return (
<div className={ className }>
<div id={ id } className={ className }>
<div className="ppcp-r-settings-card__header">
<div className="ppcp-r-settings-card__content-inner">
<span className="ppcp-r-settings-card__title">

View file

@ -1,4 +1,6 @@
import { useCallback, useEffect, useState } from '@wordpress/element';
// TODO: Migrate to Tabs (TabPanel v2) once its API is publicly available, as it provides programmatic tab switching support: https://github.com/WordPress/gutenberg/issues/52997
import { TabPanel } from '@wordpress/components';
import { getQuery, updateQueryString } from '../../utils/navigation';
@ -41,9 +43,7 @@ const TabNavigation = ( { tabs } ) => {
onSelect={ updateActivePanel }
tabs={ tabs }
>
{ ( tab ) => {
return tab.component || <>{ tab.title ?? tab.name }</>;
} }
{ ( { Component } ) => Component }
</TabPanel>
);
};

View file

@ -0,0 +1,86 @@
import { useCallback, useLayoutEffect } from '@wordpress/element';
import { Button, Icon } from '@wordpress/components';
import { chevronLeft } from '@wordpress/icons';
import classNames from 'classnames';
import useIsScrolled from '../../hooks/useIsScrolled';
import { useNavigation } from '../../hooks/useNavigation';
import BusyStateWrapper from './BusyStateWrapper';
const TopNavigation = ( {
title,
children,
isMainTitle = true,
exitOnTitleClick = false,
onTitleClick = null,
showProgressBar = false,
progressBarPercent = 0,
} ) => {
const { goToWooCommercePaymentsTab } = useNavigation();
const { isScrolled } = useIsScrolled();
const className = classNames( 'ppcp-r-navigation-container', {
'is-scrolled': isScrolled,
} );
const titleClassName = classNames( 'title', {
big: isMainTitle,
} );
const handleTitleClick = useCallback( () => {
if ( exitOnTitleClick ) {
goToWooCommercePaymentsTab();
} else if ( 'function' === typeof onTitleClick ) {
onTitleClick();
}
}, [ exitOnTitleClick, goToWooCommercePaymentsTab, onTitleClick ] );
// Removes the excess padding at the top of the navigation bar.
useLayoutEffect( () => {
window.dispatchEvent( new Event( 'resize' ) );
}, [] );
return (
<div className={ className }>
<div className="ppcp-r-navigation">
<BusyStateWrapper
className="ppcp-r-navigation--left"
busySpinner={ false }
enabled={ ! exitOnTitleClick }
>
<Button
variant="link"
onClick={ handleTitleClick }
className="is-title"
>
<Icon icon={ chevronLeft } />
<span className={ titleClassName }>{ title }</span>
</Button>
</BusyStateWrapper>
<BusyStateWrapper
className="ppcp-r-navigation--right"
busySpinner={ false }
>
{ children }
</BusyStateWrapper>
{ showProgressBar && (
<ProgressBar percent={ progressBarPercent } />
) }
</div>
</div>
);
};
const ProgressBar = ( { percent } ) => {
percent = Math.min( Math.max( percent, 0 ), 100 );
return (
<div
className="ppcp-r-navigation--progress-bar"
style={ { width: `${ percent }%` } }
/>
);
};
export default TopNavigation;

View file

@ -1,9 +1,11 @@
import { __, sprintf } from '@wordpress/i18n';
import BadgeBox, { BADGE_BOX_TITLE_BIG } from '../BadgeBox';
import Separator from '../Separator';
import OptionalPaymentMethods from '../OptionalPaymentMethods/OptionalPaymentMethods';
import PricingTitleBadge from '../PricingTitleBadge';
import BadgeBox, {
BADGE_BOX_TITLE_BIG,
} from '../../../ReusableComponents/BadgeBox';
import Separator from '../../../ReusableComponents/Separator';
import PricingTitleBadge from '../../../ReusableComponents/PricingTitleBadge';
import OptionalPaymentMethods from './OptionalPaymentMethods';
const AcdcFlow = ( { isFastlane, isPayLater, storeCountry } ) => {
if ( isFastlane && isPayLater && storeCountry === 'US' ) {
@ -53,6 +55,7 @@ const AcdcFlow = ( { isFastlane, isPayLater, storeCountry } ) => {
imageBadge={ [
'icon-payment-method-paypal-small.svg',
] }
textBadge={ <PricingTitleBadge item="plater" /> }
description={ sprintf(
// translators: %s: Link to PayPal business fees guide
__(
@ -92,12 +95,12 @@ const AcdcFlow = ( { isFastlane, isPayLater, storeCountry } ) => {
<div className="ppcp-r-welcome-docs__col">
<BadgeBox
title={ __(
'Optional payment methods',
'Expanded Checkout',
'woocommerce-paypal-payments'
) }
titleType={ BADGE_BOX_TITLE_BIG }
description={ __(
'with additional application',
'Accept debit/credit cards, PayPal, Apple Pay, Google Pay, and more. Note: Additional application required for more methods',
'woocommerce-paypal-payments'
) }
/>

View file

@ -1,8 +1,8 @@
import { __, sprintf } from '@wordpress/i18n';
import BadgeBox from '../BadgeBox';
import Separator from '../Separator';
import PricingTitleBadge from '../PricingTitleBadge';
import BadgeBox from '../../../ReusableComponents/BadgeBox';
import Separator from '../../../ReusableComponents/Separator';
import PricingTitleBadge from '../../../ReusableComponents/PricingTitleBadge';
const AcdcOptionalPaymentMethods = ( {
isFastlane,

View file

@ -1,208 +1,13 @@
import { __, sprintf } from '@wordpress/i18n';
import { Button, TextControl } from '@wordpress/components';
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 {
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'
),
};
import SandboxConnectionForm from './SandboxConnectionForm';
import ManualConnectionForm from './ManualConnectionForm';
const AdvancedOptionsForm = () => {
const [ clientValid, setClientValid ] = useState( false );
const [ secretValid, setSecretValid ] = useState( false );
const { isBusy } = CommonHooks.useBusyState();
const { isSandboxMode, setSandboxMode } = useSandboxConnection();
const {
handleConnectViaIdAndSecret,
isManualConnectionMode,
setManualConnectionMode,
clientId,
setClientId,
clientSecret,
setClientSecret,
} = useManualConnection();
const refClientId = useRef( null );
const refClientSecret = useRef( null );
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,
},
];
for ( const { ref, valid, errorMessage } of checks ) {
if ( valid() ) {
continue;
}
ref?.current?.focus();
throw new Error( errorMessage );
}
}, [ clientId, clientSecret, clientValid, secretValid ] );
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
__(
'For advanced users: Connect a custom PayPal REST app for full control over your integration. For more information on creating a PayPal REST application, <a target="_blank" href="%s">click here</a>.',
'woocommerce-paypal-payments'
),
'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input'
);
return (
<>
<BusyStateWrapper>
<SettingsToggleBlock
label={ __(
'Enable Sandbox Mode',
'woocommerce-paypal-payments'
) }
description={ __(
'Activate Sandbox mode to safely test PayPal with sample data. Once your store is ready to go live, you can easily switch to your production account.',
'woocommerce-paypal-payments'
) }
isToggled={ !! isSandboxMode }
setToggled={ setSandboxMode }
>
<ConnectionButton
title={ __(
'Connect Account',
'woocommerce-paypal-payments'
) }
showIcon={ false }
variant="secondary"
className="small-button"
isSandbox={
true /* This button always connects to sandbox */
}
/>
</SettingsToggleBlock>
</BusyStateWrapper>
<SandboxConnectionForm />
<Separator withLine={ false } />
<BusyStateWrapper
onBusy={ ( props ) => ( {
disabled: true,
label: props.label + ' ...',
} ) }
>
<SettingsToggleBlock
label={ __(
'Manually Connect',
'woocommerce-paypal-payments'
) }
description={ advancedUsersDescription }
isToggled={ !! isManualConnectionMode }
setToggled={ setManualConnectionMode }
>
<DataStoreControl
control={ TextControl }
ref={ refClientId }
label={ clientIdLabel }
value={ clientId }
onChange={ setClientId }
className={ classNames( {
'has-error': ! clientValid,
} ) }
/>
{ clientValid || (
<p className="client-id-error">
{ FORM_ERRORS.invalidClientId }
</p>
) }
<DataStoreControl
control={ TextControl }
ref={ refClientSecret }
label={ secretKeyLabel }
value={ clientSecret }
onChange={ setClientSecret }
type="password"
/>
<Button
variant="secondary"
className="small-button"
onClick={ handleManualConnect }
>
{ __(
'Connect Account',
'woocommerce-paypal-payments'
) }
</Button>
</SettingsToggleBlock>
</BusyStateWrapper>
<ManualConnectionForm />
</>
);
};

View file

@ -1,9 +1,11 @@
import { __, sprintf } from '@wordpress/i18n';
import BadgeBox, { BADGE_BOX_TITLE_BIG } from '../BadgeBox';
import Separator from '../Separator';
import OptionalPaymentMethods from '../OptionalPaymentMethods/OptionalPaymentMethods';
import PricingTitleBadge from '../PricingTitleBadge';
import BadgeBox, {
BADGE_BOX_TITLE_BIG,
} from '../../../ReusableComponents/BadgeBox';
import Separator from '../../../ReusableComponents/Separator';
import PricingTitleBadge from '../../../ReusableComponents/PricingTitleBadge';
import OptionalPaymentMethods from './OptionalPaymentMethods';
const BcdcFlow = ( { isPayLater, storeCountry } ) => {
if ( isPayLater && storeCountry === 'US' ) {
@ -92,12 +94,12 @@ const BcdcFlow = ( { isPayLater, storeCountry } ) => {
<div className="ppcp-r-welcome-docs__col">
<BadgeBox
title={ __(
'Optional payment methods',
'Expanded Checkout',
'woocommerce-paypal-payments'
) }
titleType={ BADGE_BOX_TITLE_BIG }
description={ __(
'with additional application',
'Accept debit/credit cards, PayPal, Apple Pay, Google Pay, and more. Note: Additional application required for more methods',
'woocommerce-paypal-payments'
) }
/>

View file

@ -1,7 +1,7 @@
import { __, sprintf } from '@wordpress/i18n';
import BadgeBox from '../BadgeBox';
import PricingTitleBadge from '../PricingTitleBadge';
import BadgeBox from '../../../ReusableComponents/BadgeBox';
import PricingTitleBadge from '../../../ReusableComponents/PricingTitleBadge';
const BcdcOptionalPaymentMethods = ( { isPayLater, storeCountry } ) => {
if ( isPayLater && storeCountry === 'us' ) {

View file

@ -1,15 +1,45 @@
import { Button } from '@wordpress/components';
import { useEffect } from '@wordpress/element';
import classNames from 'classnames';
import { CommonHooks } from '../../../../data';
import { openSignup } from '../../../ReusableComponents/Icons';
import {
useProductionConnection,
useSandboxConnection,
} from '../../../../hooks/useHandleConnections';
import { useHandleOnboardingButton } from '../../../../hooks/useHandleConnections';
import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper';
/**
* Button component that outputs a placeholder button when no onboardingUrl is present yet - the
* placeholder button looks identical to the working button, but has no href, target, or
* custom connection attributes.
*
* @param {Object} props
* @param {string} props.className
* @param {string} props.variant
* @param {boolean} props.showIcon
* @param {?string} props.href
* @param {Element} props.children
*/
const ButtonOrPlaceholder = ( {
className,
variant,
showIcon,
href,
children,
} ) => {
const buttonProps = {
className,
variant,
icon: showIcon ? openSignup : null,
};
if ( href ) {
buttonProps.href = href;
buttonProps.target = 'PPFrame';
buttonProps[ 'data-paypal-button' ] = 'true';
buttonProps[ 'data-paypal-onboard-button' ] = 'true';
}
return <Button { ...buttonProps }>{ children }</Button>;
};
const ConnectionButton = ( {
title,
isSandbox = false,
@ -17,31 +47,45 @@ const ConnectionButton = ( {
showIcon = true,
className = '',
} ) => {
const { handleSandboxConnect } = useSandboxConnection();
const { handleProductionConnect } = useProductionConnection();
const {
onboardingUrl,
scriptLoaded,
setCompleteHandler,
removeCompleteHandler,
} = useHandleOnboardingButton( isSandbox );
const buttonClassName = classNames( 'ppcp-r-connection-button', className, {
'sandbox-mode': isSandbox,
'live-mode': ! isSandbox,
} );
const environment = isSandbox ? 'sandbox' : 'production';
const handleConnectClick = async () => {
if ( isSandbox ) {
await handleSandboxConnect();
} else {
await handleProductionConnect();
useEffect( () => {
if ( scriptLoaded && onboardingUrl ) {
window.PAYPAL.apps.Signup.render();
setCompleteHandler( environment );
}
};
return () => {
removeCompleteHandler();
};
}, [
scriptLoaded,
onboardingUrl,
environment,
setCompleteHandler,
removeCompleteHandler,
] );
return (
<BusyStateWrapper>
<Button
<BusyStateWrapper isBusy={ ! onboardingUrl }>
<ButtonOrPlaceholder
className={ buttonClassName }
variant={ variant }
icon={ showIcon ? openSignup : null }
onClick={ handleConnectClick }
showIcon={ showIcon }
href={ onboardingUrl }
>
<span className="button-title">{ title }</span>
</Button>
</ButtonOrPlaceholder>
</BusyStateWrapper>
);
};

View file

@ -0,0 +1,188 @@
import {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from '@wordpress/element';
import { Button, TextControl } from '@wordpress/components';
import { __, sprintf } from '@wordpress/i18n';
import classNames from 'classnames';
import SettingsToggleBlock from '../../../ReusableComponents/SettingsToggleBlock';
import DataStoreControl from '../../../ReusableComponents/DataStoreControl';
import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper';
import {
useDirectAuthentication,
useSandboxConnection,
} from '../../../../hooks/useHandleConnections';
import { OnboardingHooks } from '../../../../data';
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 ManualConnectionForm = () => {
const [ clientValid, setClientValid ] = useState( false );
const [ secretValid, setSecretValid ] = useState( false );
const { isSandboxMode } = useSandboxConnection();
const {
manualClientId,
setManualClientId,
manualClientSecret,
setManualClientSecret,
} = OnboardingHooks.useManualConnectionForm();
const {
handleDirectAuthentication,
isManualConnectionMode,
setManualConnectionMode,
} = useDirectAuthentication();
const refClientId = useRef( null );
const refClientSecret = useRef( null );
// Form data validation and sanitation.
const getManualConnectionDetails = useCallback( () => {
const checks = [
{
ref: refClientId,
valid: () => manualClientId,
errorMessage: FORM_ERRORS.noClientId,
},
{
ref: refClientId,
valid: () => clientValid,
errorMessage: FORM_ERRORS.invalidClientId,
},
{
ref: refClientSecret,
valid: () => manualClientSecret && secretValid,
errorMessage: FORM_ERRORS.noClientSecret,
},
];
for ( const { ref, valid, errorMessage } of checks ) {
if ( valid() ) {
continue;
}
ref?.current?.focus();
throw new Error( errorMessage );
}
return {
clientId: manualClientId,
clientSecret: manualClientSecret,
isSandbox: isSandboxMode,
};
}, [
manualClientId,
manualClientSecret,
isSandboxMode,
clientValid,
secretValid,
] );
// On-the-fly form validation.
useEffect( () => {
setClientValid(
! manualClientId || /^A[\w-]{79}$/.test( manualClientId )
);
setSecretValid( manualClientSecret && manualClientSecret.length > 0 );
}, [ manualClientId, manualClientSecret ] );
// Environment-specific field labels.
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 ]
);
// Translations with placeholders.
const advancedUsersDescription = sprintf(
// translators: %s: Link to PayPal REST application guide
__(
'For advanced users: Connect a custom PayPal REST app for full control over your integration. For more information on creating a PayPal REST application, <a target="_blank" href="%s">click here</a>.',
'woocommerce-paypal-payments'
),
'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input'
);
// Button click handler.
const handleManualConnect = useCallback(
() => handleDirectAuthentication( getManualConnectionDetails ),
[ handleDirectAuthentication, getManualConnectionDetails ]
);
return (
<BusyStateWrapper
onBusy={ ( props ) => ( {
disabled: true,
label: props.label + ' ...',
} ) }
>
<SettingsToggleBlock
label={ __(
'Manually Connect',
'woocommerce-paypal-payments'
) }
description={ advancedUsersDescription }
isToggled={ !! isManualConnectionMode }
setToggled={ setManualConnectionMode }
>
<DataStoreControl
control={ TextControl }
ref={ refClientId }
label={ clientIdLabel }
value={ manualClientId }
onChange={ setManualClientId }
className={ classNames( {
'has-error': ! clientValid,
} ) }
/>
{ clientValid || (
<p className="client-id-error">
{ FORM_ERRORS.invalidClientId }
</p>
) }
<DataStoreControl
control={ TextControl }
ref={ refClientSecret }
label={ secretKeyLabel }
value={ manualClientSecret }
onChange={ setManualClientSecret }
type="password"
/>
<Button
variant="secondary"
className="small-button"
onClick={ handleManualConnect }
>
{ __( 'Connect Account', 'woocommerce-paypal-payments' ) }
</Button>
</SettingsToggleBlock>
</BusyStateWrapper>
);
};
export default ManualConnectionForm;

View file

@ -1,57 +1,27 @@
import { Button, Icon } from '@wordpress/components';
import { chevronLeft } from '@wordpress/icons';
import { Button } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import classNames from 'classnames';
import { OnboardingHooks } from '../../../../data';
import useIsScrolled from '../../../../hooks/useIsScrolled';
import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper';
import { useNavigation } from '../../../../hooks/useNavigation';
import TopNavigation from '../../../ReusableComponents/TopNavigation';
const Navigation = ( { stepDetails, onNext, onPrev, onExit } ) => {
const OnboardingNavigation = ( { stepDetails, onNext, onPrev } ) => {
const { goToWooCommercePaymentsTab } = useNavigation();
const { title, isFirst, percentage, showNext, canProceed } = stepDetails;
const { isScrolled } = useIsScrolled();
const state = OnboardingHooks.useNavigationState();
const isDisabled = ! canProceed( state );
const className = classNames( 'ppcp-r-navigation-container', {
'is-scrolled': isScrolled,
} );
return (
<div className={ className }>
<div className="ppcp-r-navigation">
<BusyStateWrapper
className="ppcp-r-navigation--left"
busySpinner={ false }
enabled={ ! isFirst }
>
<Button
variant="link"
onClick={ isFirst ? onExit : onPrev }
className="is-title"
>
<Icon icon={ chevronLeft } />
<span className={ 'title ' + ( isFirst ? 'big' : '' ) }>
{ title }
</span>
</Button>
</BusyStateWrapper>
{ ! isFirst &&
NextButton( { showNext, isDisabled, onNext, onExit } ) }
<ProgressBar percent={ percentage } />
</div>
</div>
);
};
const NextButton = ( { showNext, isDisabled, onNext, onExit } ) => {
return (
<BusyStateWrapper
className="ppcp-r-navigation--right"
busySpinner={ false }
<TopNavigation
title={ title }
isMainTitle={ isFirst }
exitOnTitleClick={ isFirst }
onTitleClick={ onPrev }
showProgressBar={ true }
progressBarPercent={ percentage * 0.9 }
>
<Button variant="link" onClick={ onExit }>
<Button variant="link" onClick={ goToWooCommercePaymentsTab }>
{ __( 'Save and exit', 'woocommerce-paypal-payments' ) }
</Button>
{ showNext && (
@ -63,19 +33,8 @@ const NextButton = ( { showNext, isDisabled, onNext, onExit } ) => {
{ __( 'Continue', 'woocommerce-paypal-payments' ) }
</Button>
) }
</BusyStateWrapper>
</TopNavigation>
);
};
const ProgressBar = ( { percent } ) => {
percent = Math.min( Math.max( percent, 0 ), 100 );
return (
<div
className="ppcp-r-navigation--progress-bar"
style={ { width: `${ percent * 0.9 }%` } }
/>
);
};
export default Navigation;
export default OnboardingNavigation;

View file

@ -1,11 +1,13 @@
import data from '../../utils/data';
import { Icon } from '@wordpress/components';
import { logoPayPal } from '../../../ReusableComponents/Icons';
const OnboardingHeader = ( props ) => {
return (
<section className="ppcp-r-onboarding-header">
<div className="ppcp-r-onboarding-header__logo">
<div className="ppcp-r-onboarding-header__logo-wrapper">
{ data().getImage( 'logo-paypal.svg' ) }
<Icon icon={ logoPayPal } width="auto" height={ 38 } />
</div>
</div>
<div className="ppcp-r-onboarding-header__content">
@ -14,8 +16,10 @@ const OnboardingHeader = ( props ) => {
</h1>
{ props.description && (
<p
className="ppcp-r-onboarding-header__description"
dangerouslySetInnerHTML={ { __html: props.description, } }
className="ppcp-r-onboarding-header__description"
dangerouslySetInnerHTML={ {
__html: props.description,
} }
></p>
) }
</div>

View file

@ -0,0 +1,42 @@
import { __ } from '@wordpress/i18n';
import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper';
import SettingsToggleBlock from '../../../ReusableComponents/SettingsToggleBlock';
import { useSandboxConnection } from '../../../../hooks/useHandleConnections';
import ConnectionButton from './ConnectionButton';
const SandboxConnectionForm = () => {
const { isSandboxMode, setSandboxMode } = useSandboxConnection();
return (
<BusyStateWrapper>
<SettingsToggleBlock
label={ __(
'Enable Sandbox Mode',
'woocommerce-paypal-payments'
) }
description={ __(
'Activate Sandbox mode to safely test PayPal with sample data. Once your store is ready to go live, you can easily switch to your production account.',
'woocommerce-paypal-payments'
) }
isToggled={ !! isSandboxMode }
setToggled={ setSandboxMode }
>
<ConnectionButton
title={ __(
'Connect Account',
'woocommerce-paypal-payments'
) }
showIcon={ false }
variant="secondary"
className="small-button"
isSandbox={
true /* This button always connects to sandbox */
}
/>
</SettingsToggleBlock>
</BusyStateWrapper>
);
};
export default SandboxConnectionForm;

View file

@ -1,6 +1,6 @@
import { __ } from '@wordpress/i18n';
import PricingDescription from '../PricingDescription';
import PricingDescription from '../../../ReusableComponents/PricingDescription';
import AcdcFlow from './AcdcFlow';
import BcdcFlow from './BcdcFlow';

View file

@ -1,9 +1,9 @@
import { __ } from '@wordpress/i18n';
import OnboardingHeader from '../../ReusableComponents/OnboardingHeader';
import SelectBoxWrapper from '../../ReusableComponents/SelectBoxWrapper';
import SelectBox from '../../ReusableComponents/SelectBox';
import { OnboardingHooks, BUSINESS_TYPES } from '../../../data';
import SelectBoxWrapper from '../../../ReusableComponents/SelectBoxWrapper';
import SelectBox from '../../../ReusableComponents/SelectBox';
import { OnboardingHooks, BUSINESS_TYPES } from '../../../../data';
import OnboardingHeader from '../Components/OnboardingHeader';
const BUSINESS_RADIO_GROUP_NAME = 'business';

View file

@ -1,7 +1,7 @@
import { __ } from '@wordpress/i18n';
import OnboardingHeader from '../../ReusableComponents/OnboardingHeader';
import ConnectionButton from './Components/ConnectionButton';
import OnboardingHeader from '../Components/OnboardingHeader';
import ConnectionButton from '../Components/ConnectionButton';
const StepCompleteSetup = () => {
return (

View file

@ -1,11 +1,11 @@
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 OptionalPaymentMethods from '../../ReusableComponents/OptionalPaymentMethods/OptionalPaymentMethods';
import PricingDescription from '../../ReusableComponents/PricingDescription';
import { CommonHooks, OnboardingHooks } from '../../../../data';
import SelectBoxWrapper from '../../../ReusableComponents/SelectBoxWrapper';
import SelectBox from '../../../ReusableComponents/SelectBox';
import PricingDescription from '../../../ReusableComponents/PricingDescription';
import OnboardingHeader from '../Components/OnboardingHeader';
import OptionalPaymentMethods from '../Components/OptionalPaymentMethods';
const OPM_RADIO_GROUP_NAME = 'optional-payment-methods';
@ -17,14 +17,21 @@ const StepPaymentMethods = ( {} ) => {
const { storeCountry, storeCurrency } = CommonHooks.useWooSettings();
let screenTitle = __(
'Add optional payment methods to your Checkout',
'woocommerce-paypal-payments'
);
if ( 'US' === storeCountry ) {
screenTitle = __(
'Add Expanded Checkout for More Ways to Pay',
'woocommerce-paypal-payments'
);
}
return (
<div className="ppcp-r-page-optional-payment-methods">
<OnboardingHeader
title={ __(
'Add optional payment methods to your Checkout',
'woocommerce-paypal-payments'
) }
/>
<OnboardingHeader title={ screenTitle } />
<div className="ppcp-r-inner-container">
<SelectBoxWrapper>
<SelectBox

View file

@ -1,9 +1,9 @@
import { __ } from '@wordpress/i18n';
import OnboardingHeader from '../../ReusableComponents/OnboardingHeader';
import SelectBox from '../../ReusableComponents/SelectBox';
import SelectBoxWrapper from '../../ReusableComponents/SelectBoxWrapper';
import { OnboardingHooks, PRODUCT_TYPES } from '../../../data';
import SelectBox from '../../../ReusableComponents/SelectBox';
import SelectBoxWrapper from '../../../ReusableComponents/SelectBoxWrapper';
import { OnboardingHooks, PRODUCT_TYPES } from '../../../../data';
import OnboardingHeader from '../Components/OnboardingHeader';
const PRODUCTS_CHECKBOX_GROUP_NAME = 'products';

View file

@ -1,15 +1,14 @@
import { __ } from '@wordpress/i18n';
import { Button } from '@wordpress/components';
import OnboardingHeader from '../../ReusableComponents/OnboardingHeader';
import PaymentMethodIcons from '../../ReusableComponents/PaymentMethodIcons';
import Separator from '../../ReusableComponents/Separator';
import WelcomeDocs from '../../ReusableComponents/WelcomeDocs/WelcomeDocs';
import AccordionSection from '../../ReusableComponents/AccordionSection';
import AdvancedOptionsForm from './Components/AdvancedOptionsForm';
import { CommonHooks } from '../../../data';
import BusyStateWrapper from '../../ReusableComponents/BusyStateWrapper';
import PaymentMethodIcons from '../../../ReusableComponents/PaymentMethodIcons';
import Separator from '../../../ReusableComponents/Separator';
import AccordionSection from '../../../ReusableComponents/AccordionSection';
import { CommonHooks } from '../../../../data';
import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper';
import OnboardingHeader from '../Components/OnboardingHeader';
import WelcomeDocs from '../Components/WelcomeDocs';
import AdvancedOptionsForm from '../Components/AdvancedOptionsForm';
const StepWelcome = ( { setStep, currentStep } ) => {
const { storeCountry } = CommonHooks.useWooSettings();

View file

@ -1,27 +1,23 @@
import Container from '../../ReusableComponents/Container';
import { OnboardingHooks } from '../../../data';
import { getSteps, getCurrentStep } from './availableSteps';
import Navigation from './Components/Navigation';
import { getSteps, getCurrentStep } from './Steps';
import OnboardingNavigation from './Components/Navigation';
const Onboarding = () => {
const OnboardingScreen = () => {
const { step, setStep, flags } = OnboardingHooks.useSteps();
const Steps = getSteps( flags );
const currentStep = getCurrentStep( step, Steps );
const handleNext = () => setStep( currentStep.nextStep );
const handlePrev = () => setStep( currentStep.prevStep );
const handleExit = () => {
window.location.href = window.ppcpSettings.wcPaymentsTabUrl;
};
return (
<>
<Navigation
<OnboardingNavigation
stepDetails={ currentStep }
onNext={ handleNext }
onPrev={ handlePrev }
onExit={ handleExit }
/>
<Container page="onboarding">
@ -37,4 +33,4 @@ const Onboarding = () => {
);
};
export default Onboarding;
export default OnboardingScreen;

View file

@ -1,62 +0,0 @@
import PaymentMethodModal from '../../../ReusableComponents/PaymentMethodModal';
import { __ } from '@wordpress/i18n';
import { Button } from '@wordpress/components';
import { useState } from '@wordpress/element';
import { RadioControl } from '@wordpress/components';
const ModalAcdc = ( { setModalIsVisible } ) => {
const [ threeDSecure, setThreeDSecure ] = useState( 'no-3d-secure' );
const acdcOptions = [
{
label: __( 'No 3D Secure', 'woocommerce-paypal-payments' ),
value: 'no-3d-secure',
},
{
label: __( 'Only when required', 'woocommerce-paypal-payments' ),
value: 'only-required-3d-secure',
},
{
label: __(
'Always require 3D Secure',
'woocommerce-paypal-payments'
),
value: 'always-3d-secure',
},
];
return (
<PaymentMethodModal
setModalIsVisible={ setModalIsVisible }
icon="payment-method-cards-big"
title={ __(
'Advanced Credit and Debit Card Payments',
'woocommerce-paypal-payments'
) }
>
<strong className="ppcp-r-modal__content-title">
{ __( '3D Secure', 'woocommerce-paypal-payments' ) }
</strong>
<p className="ppcp-r-modal__description">
{ __(
'Authenticate cardholders through their card issuers to reduce fraud and improve transaction security. Successful 3D Secure authentication can shift liability for fraudulent chargebacks to the card issuer.',
'woocommerce-paypal-payments'
) }
</p>
<div className="ppcp-r-modal__field-rows ppcp-r-modal__field-rows--acdc">
<RadioControl
onChange={ setThreeDSecure }
selected={ threeDSecure }
options={ acdcOptions }
/>
<div className="ppcp-r-modal__field-row ppcp-r-modal__field-row--save">
<Button variant="primary">
{ __( 'Save changes', 'woocommerce-paypal-payments' ) }
</Button>
</div>
</div>
</PaymentMethodModal>
);
};
export default ModalAcdc;

View file

@ -1,63 +0,0 @@
import PaymentMethodModal from '../../../ReusableComponents/PaymentMethodModal';
import { __ } from '@wordpress/i18n';
import { Button, ToggleControl } from '@wordpress/components';
import { PayPalRdb } from '../../../ReusableComponents/Fields';
import { useState } from '@wordpress/element';
const ModalFastlane = ( { setModalIsVisible } ) => {
const [ fastlaneSettings, setFastlaneSettings ] = useState( {
cardholderName: false,
displayWatermark: false,
} );
const updateFormValue = ( key, value ) => {
setFastlaneSettings( { ...fastlaneSettings, [ key ]: value } );
};
return (
<PaymentMethodModal
setModalIsVisible={ setModalIsVisible }
icon="payment-method-fastlane-big"
title={ __( 'Fastlane by PayPal', 'woocommerce-paypal-payments' ) }
size="small"
>
<div className="ppcp-r-modal__field-rows ppcp-r-modal__field-rows--fastlane">
<div className="ppcp-r-modal__field-row">
<ToggleControl
className="ppcp-r-modal__inverted-toggle-control"
checked={ fastlaneSettings.cardholderName }
onChange={ ( newValue ) =>
updateFormValue( 'cardholderName', newValue )
}
label={ __(
'Display cardholder name',
'woocommerce-paypal-payments'
) }
id="ppcp-r-fastlane-settings-cardholder"
/>
</div>
<div className="ppcp-r-modal__field-row">
<ToggleControl
className="ppcp-r-modal__inverted-toggle-control"
checked={ fastlaneSettings.displayWatermark }
onChange={ ( newValue ) =>
updateFormValue( 'displayWatermark', newValue )
}
label={ __(
'Display Fastlane Watermark',
'woocommerce-paypal-payments'
) }
id="ppcp-r-fastlane-settings-watermark"
/>
</div>
<div className="ppcp-r-modal__field-row ppcp-r-modal__field-row--save">
<Button variant="primary">
{ __( 'Save changes', 'woocommerce-paypal-payments' ) }
</Button>
</div>
</div>
</PaymentMethodModal>
);
};
export default ModalFastlane;

View file

@ -1,76 +0,0 @@
import PaymentMethodModal from '../../../ReusableComponents/PaymentMethodModal';
import { __ } from '@wordpress/i18n';
import { ToggleControl, Button, TextControl } from '@wordpress/components';
import { useState } from '@wordpress/element';
const ModalPayPal = ( { setModalIsVisible } ) => {
const [ paypalSettings, setPaypalSettings ] = useState( {
checkoutPageTitle: 'PayPal',
checkoutPageDescription: 'Pay via PayPal',
showLogo: false,
} );
const updateFormValue = ( key, value ) => {
setPaypalSettings( { ...paypalSettings, [ key ]: value } );
};
return (
<PaymentMethodModal
setModalIsVisible={ setModalIsVisible }
icon="payment-method-paypal-big"
title={ __( 'PayPal', 'woocommerce-paypal-payments' ) }
>
<div className="ppcp-r-modal__field-rows">
<div className="ppcp-r-modal__field-row">
<TextControl
className="ppcp-r-vertical-text-control"
label={ __(
'Checkout page title',
'woocommerce-paypal-payments'
) }
value={ paypalSettings.checkoutPageTitle }
onChange={ ( newValue ) =>
updateFormValue( 'checkoutPageTitle', newValue )
}
/>
</div>
<div className="ppcp-r-modal__field-row">
<TextControl
className="ppcp-r-vertical-text-control"
label={ __(
'Checkout page description',
'woocommerce-paypal-payments'
) }
value={ paypalSettings.checkoutPageDescription }
onChange={ ( newValue ) =>
updateFormValue(
'checkoutPageDescription',
newValue
)
}
/>
</div>
<div className="ppcp-r-modal__field-row">
<ToggleControl
label={ __(
'Show logo',
'woocommerce-paypal-payments'
) }
id="ppcp-r-paypal-settings-show-logo"
checked={ paypalSettings.showLogo }
onChange={ ( newValue ) => {
updateFormValue( 'showLogo', newValue );
} }
/>
</div>
<div className="ppcp-r-modal__field-row ppcp-r-modal__field-row--save">
<Button variant="primary">
{ __( 'Save changes', 'woocommerce-paypal-payments' ) }
</Button>
</div>
</div>
</PaymentMethodModal>
);
};
export default ModalPayPal;

View file

@ -1,8 +1,9 @@
import { __ } from '@wordpress/i18n';
import { useState } from '@wordpress/element';
import { useState, useMemo } from '@wordpress/element';
import { Button, Icon } from '@wordpress/components';
import { useDispatch } from '@wordpress/data';
import { reusableBlock } from '@wordpress/icons';
import { store as noticesStore } from '@wordpress/notices';
import SettingsCard from '../../ReusableComponents/SettingsCard';
import TodoSettingsBlock from '../../ReusableComponents/SettingsBlocks/TodoSettingsBlock';
@ -10,39 +11,76 @@ import FeatureSettingsBlock from '../../ReusableComponents/SettingsBlocks/Featur
import { TITLE_BADGE_POSITIVE } from '../../ReusableComponents/TitleBadge';
import { useMerchantInfo } from '../../../data/common/hooks';
import { STORE_NAME } from '../../../data/common';
import Features from './TabSettingsElements/Blocks/Features';
import { todosData } from '../../../data/settings/tab-overview-todos-data';
import {
NOTIFICATION_ERROR,
NOTIFICATION_SUCCESS,
} from '../../ReusableComponents/Icons';
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 { merchant, merchantFeatures } = useMerchantInfo();
const { refreshFeatureStatuses, setActiveModal } =
useDispatch( STORE_NAME );
const { createSuccessNotice, createErrorNotice } =
useDispatch( noticesStore );
const features = featuresDefault.map( ( feature ) => {
const merchantFeature = merchant?.features?.[ feature.id ];
return {
...feature,
enabled: merchantFeature?.enabled ?? false,
};
} );
// Get the features data with access to setActiveModal
const featuresData = useMemo(
() => Features.getFeatures( setActiveModal ),
[ setActiveModal ]
);
// Map merchant features status to our config
const features = useMemo( () => {
return featuresData.map( ( feature ) => {
const merchantFeature = merchantFeatures?.[ feature.id ];
return {
...feature,
enabled: merchantFeature?.enabled ?? false,
};
} );
}, [ featuresData, merchantFeatures ] );
const refreshHandler = async () => {
setIsRefreshing( true );
try {
const result = await refreshFeatureStatuses();
if ( result && ! result.success ) {
const errorMessage = sprintf(
/* translators: %s: error message */
__(
'Operation failed: %s Check WooCommerce logs for more details.',
'woocommerce-paypal-payments'
),
result.message ||
__( 'Unknown error', 'woocommerce-paypal-payments' )
);
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.' );
createErrorNotice( errorMessage, {
icon: NOTIFICATION_ERROR,
} );
console.error(
'Failed to refresh features:',
result.message || 'Unknown error'
);
} else {
createSuccessNotice(
__(
'Features refreshed successfully.',
'woocommerce-paypal-payments'
),
{
icon: NOTIFICATION_SUCCESS,
}
);
console.log( 'Features refreshed successfully.' );
}
} finally {
setIsRefreshing( false );
}
setIsRefreshing( false );
};
return (
@ -59,12 +97,7 @@ const TabOverview = () => {
'woocommerce-paypal-payments'
) }
>
<TodoSettingsBlock
todos={ todos }
setTodos={ setTodos }
todosData={ todosData }
setTodosData={ setTodosData }
/>
<TodoSettingsBlock todosData={ todosData } />
</SettingsCard>
) }
@ -72,16 +105,16 @@ const TabOverview = () => {
className="ppcp-r-tab-overview-features"
title={ __( 'Features', 'woocommerce-paypal-payments' ) }
description={
<div>
<>
<p>
{ __(
'Enable additional features',
'Enable additional features and capabilities on your WooCommerce store.',
'woocommerce-paypal-payments'
) }
</p>
<p>
{ __(
'Click Refresh',
'Click Refresh to update your current features after making changes.',
'woocommerce-paypal-payments'
) }
</p>
@ -101,204 +134,106 @@ const TabOverview = () => {
'woocommerce-paypal-payments'
) }
</Button>
</div>
</>
}
contentItems={ features.map( ( feature ) => (
contentItems={ features.map( ( feature ) => {
return (
<FeatureSettingsBlock
key={ feature.id }
title={ feature.title }
description={ feature.description }
actionProps={ {
buttons: feature.buttons
.filter(
( button ) =>
! button.showWhen || // Learn more buttons
( feature.enabled &&
button.showWhen ===
'enabled' ) ||
( ! feature.enabled &&
button.showWhen === 'disabled' )
)
.map( ( button ) => ( {
...button,
url: button.urls
? merchant?.isSandbox
? button.urls.sandbox
: button.urls.live
: button.url,
} ) ),
isBusy: isRefreshing,
enabled: feature.enabled,
notes: feature.notes,
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={ feature.id }
title={ feature.title }
description={ feature.description }
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: feature.buttons,
enabled: feature.enabled,
notes: feature.notes,
badge: feature.enabled
? {
text: __(
'Active',
'woocommerce-paypal-payments'
),
type: TITLE_BADGE_POSITIVE,
}
: undefined,
buttons: [
{
type: 'tertiary',
text: __(
'View full documentation',
'woocommerce-paypal-payments'
),
url: 'https://woocommerce.com/document/woocommerce-paypal-payments/ ',
},
],
} }
/>
) ) }
/>,
<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: 'https://woocommerce.com/document/woocommerce-paypal-payments/#get-help ',
},
],
} }
/>,
] }
/>
</div>
);
};
// TODO: This list should be refactored into a separate module, maybe utils/thingsToDoNext.js
const todosDataDefault = [
{
value: 'paypal_later_messaging',
description: __(
'Enable Pay Later messaging',
'woocommerce-paypal-payments'
),
},
{
value: 'capture_authorized_payments',
description: __(
'Capture authorized payments',
'woocommerce-paypal-payments'
),
},
{
value: 'enable_google_pay',
description: __( 'Enable Google Pay', 'woocommerce-paypal-payments' ),
},
{
value: 'paypal_shortcut',
description: __(
'Add PayPal shortcut to the Cart page',
'woocommerce-paypal-payments'
),
},
{
value: 'advanced_cards',
description: __(
'Add Advanced Cards to Blocks Checkout',
'woocommerce-paypal-payments'
),
},
];
// 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',
title: __( 'Save PayPal and Venmo', 'woocommerce-paypal-payments' ),
description: __(
'Securely save PayPal and Venmo payment methods for subscriptions or return buyers.',
'woocommerce-paypal-payments'
),
buttons: [
{
type: 'secondary',
text: __( 'Configure', 'woocommerce-paypal-payments' ),
url: '#',
},
{
type: 'tertiary',
text: __( 'Learn more', 'woocommerce-paypal-payments' ),
url: '#',
},
],
},
{
id: 'advanced_credit_and_debit_cards',
title: __(
'Advanced Credit and Debit Cards',
'woocommerce-paypal-payments'
),
description: __(
'Process major credit and debit cards including Visa, Mastercard, American Express and Discover.',
'woocommerce-paypal-payments'
),
buttons: [
{
type: 'secondary',
text: __( 'Configure', 'woocommerce-paypal-payments' ),
url: '#',
},
{
type: 'tertiary',
text: __( 'Learn more', 'woocommerce-paypal-payments' ),
url: '#',
},
],
},
{
id: 'alternative_payment_methods',
title: __(
'Alternative Payment Methods',
'woocommerce-paypal-payments'
),
description: __(
'Offer global, country-specific payment options for your customers.',
'woocommerce-paypal-payments'
),
buttons: [
{
type: 'secondary',
text: __( 'Apply', 'woocommerce-paypal-payments' ),
url: '#',
},
{
type: 'tertiary',
text: __( 'Learn more', 'woocommerce-paypal-payments' ),
url: '#',
},
],
},
{
id: 'google_pay',
title: __( 'Google Pay', 'woocommerce-paypal-payments' ),
description: __(
'Let customers pay using their Google Pay wallet.',
'woocommerce-paypal-payments'
),
buttons: [
{
type: 'secondary',
text: __( 'Configure', 'woocommerce-paypal-payments' ),
url: '#',
},
{
type: 'tertiary',
text: __( 'Learn more', 'woocommerce-paypal-payments' ),
url: '#',
},
],
notes: [
__( '¹PayPal Q2 Earnings-2021.', 'woocommerce-paypal-payments' ),
],
},
{
id: 'apple_pay',
title: __( 'Apple Pay', 'woocommerce-paypal-payments' ),
description: __(
'Let customers pay using their Apple Pay wallet.',
'woocommerce-paypal-payments'
),
buttons: [
{
type: 'secondary',
text: __(
'Domain registration',
'woocommerce-paypal-payments'
),
url: '#',
},
{
type: 'tertiary',
text: __( 'Learn more', 'woocommerce-paypal-payments' ),
url: '#',
},
],
},
{
id: 'pay_later_messaging',
title: __( 'Pay Later Messaging', 'woocommerce-paypal-payments' ),
description: __(
'Let customers know they can buy now and pay later with PayPal. Adding this messaging can boost conversion rates and increase cart sizes by 39%¹, with no extra cost to you—plus, you get paid up front.',
'woocommerce-paypal-payments'
),
buttons: [
{
type: 'secondary',
text: __( 'Configure', 'woocommerce-paypal-payments' ),
url: '#',
},
{
type: 'tertiary',
text: __( 'Learn more', 'woocommerce-paypal-payments' ),
url: '#',
},
],
},
];
export default TabOverview;

View file

@ -0,0 +1,50 @@
import React, { useEffect } from 'react';
const TabPayLaterMessaging = () => {
const config = {}; // Replace with the appropriate/saved configuration.
const PcpPayLaterConfigurator =
window.ppcpSettings?.PcpPayLaterConfigurator;
useEffect( () => {
if ( window.merchantConfigurators && PcpPayLaterConfigurator ) {
window.merchantConfigurators.Messaging( {
config,
merchantClientId: PcpPayLaterConfigurator.merchantClientId,
partnerClientId: PcpPayLaterConfigurator.partnerClientId,
partnerName: 'WooCommerce',
bnCode: PcpPayLaterConfigurator.bnCode,
placements: [
'cart',
'checkout',
'product',
'shop',
'home',
'custom_placement',
],
styleOverrides: {
button: 'ppcp-r-paylater-configurator__publish-button',
header: 'ppcp-r-paylater-configurator__header',
subheader: 'ppcp-r-paylater-configurator__subheader',
},
onSave: ( data ) => {
/*
TODO:
- The saving will be handled in a separate PR.
- One option could be:
- When saving the settings, programmatically click on the configurator's
"Save Changes" button and send the request to PHP.
*/
},
} );
}
}, [ PcpPayLaterConfigurator ] );
return (
<div
id="messaging-configurator"
className="ppcp-r-paylater-configurator"
></div>
);
};
export default TabPayLaterMessaging;

View file

@ -1,38 +1,39 @@
import { __ } from '@wordpress/i18n';
import { useMemo } from '@wordpress/element';
import SettingsCard from '../../ReusableComponents/SettingsCard';
import PaymentMethodsBlock from '../../ReusableComponents/SettingsBlocks/PaymentMethodsBlock';
import { CommonHooks } from '../../../data';
import ModalPayPal from './Modals/ModalPayPal';
import ModalFastlane from './Modals/ModalFastlane';
import ModalAcdc from './Modals/ModalAcdc';
import { PaymentHooks } from '../../../data';
import { useActiveModal } from '../../../data/common/hooks';
import Modal from './TabSettingsElements/Blocks/Modal';
const TabPaymentMethods = () => {
const { storeCountry, storeCurrency } = CommonHooks.useWooSettings();
const { paymentMethodsPayPalCheckout } =
PaymentHooks.usePaymentMethodsPayPalCheckout();
const { paymentMethodsOnlineCardPayments } =
PaymentHooks.usePaymentMethodsOnlineCardPayments();
const { paymentMethodsAlternative } =
PaymentHooks.usePaymentMethodsAlternative();
const filteredPaymentMethods = useMemo( () => {
const contextProps = { storeCountry, storeCurrency };
const { activeModal, setActiveModal } = useActiveModal();
return {
payPalCheckout: filterPaymentMethods(
paymentMethodsPayPalCheckout,
contextProps
),
onlineCardPayments: filterPaymentMethods(
paymentMethodsOnlineCardPayments,
contextProps
),
alternative: filterPaymentMethods(
paymentMethodsAlternative,
contextProps
),
};
}, [ storeCountry, storeCurrency ] );
const getActiveMethod = () => {
if ( ! activeModal ) {
return null;
}
const allMethods = [
...paymentMethodsPayPalCheckout,
...paymentMethodsOnlineCardPayments,
...paymentMethodsAlternative,
];
return allMethods.find( ( method ) => method.id === activeModal );
};
return (
<div className="ppcp-r-payment-methods">
<SettingsCard
id="ppcp-paypal-checkout-card"
title={ __( 'PayPal Checkout', 'woocommerce-paypal-payments' ) }
description={ __(
'Select your preferred checkout option with PayPal for easy payment processing.',
@ -42,10 +43,12 @@ const TabPaymentMethods = () => {
contentContainer={ false }
>
<PaymentMethodsBlock
paymentMethods={ filteredPaymentMethods.payPalCheckout }
paymentMethods={ paymentMethodsPayPalCheckout }
onTriggerModal={ setActiveModal }
/>
</SettingsCard>
<SettingsCard
id="ppcp-card-payments-card"
title={ __(
'Online Card Payments',
'woocommerce-paypal-payments'
@ -58,10 +61,12 @@ const TabPaymentMethods = () => {
contentContainer={ false }
>
<PaymentMethodsBlock
paymentMethods={ filteredPaymentMethods.onlineCardPayments }
paymentMethods={ paymentMethodsOnlineCardPayments }
onTriggerModal={ setActiveModal }
/>
</SettingsCard>
<SettingsCard
id="ppcp-alternative-payments-card"
title={ __(
'Alternative Payment Methods',
'woocommerce-paypal-payments'
@ -74,203 +79,27 @@ const TabPaymentMethods = () => {
contentContainer={ false }
>
<PaymentMethodsBlock
paymentMethods={ filteredPaymentMethods.alternative }
paymentMethods={ paymentMethodsAlternative }
onTriggerModal={ setActiveModal }
/>
</SettingsCard>
{ activeModal && (
<Modal
method={ getActiveMethod() }
setModalIsVisible={ () => setActiveModal( null ) }
onSave={ ( methodId, settings ) => {
console.log(
'Saving settings for:',
methodId,
settings
);
setActiveModal( null );
} }
/>
) }
</div>
);
};
function filterPaymentMethods( paymentMethods, contextProps ) {
return paymentMethods.filter( ( method ) =>
typeof method.condition === 'function'
? method.condition( contextProps )
: true
);
}
const paymentMethodsPayPalCheckout = [
{
id: 'paypal',
title: __( 'PayPal', 'woocommerce-paypal-payments' ),
description: __(
'Our all-in-one checkout solution lets you offer PayPal, Venmo, Pay Later options, and more to help maximize conversion.',
'woocommerce-paypal-payments'
),
icon: 'payment-method-paypal',
modal: ModalPayPal,
},
{
id: 'venmo',
title: __( 'Venmo', 'woocommerce-paypal-payments' ),
description: __(
'Offer Venmo at checkout to millions of active users.',
'woocommerce-paypal-payments'
),
icon: 'payment-method-venmo',
},
{
id: 'paypal_credit',
title: __( 'PayPal Credit', 'woocommerce-paypal-payments' ),
description: __(
'Get paid in full at checkout while giving your customers the option to pay interest free if paid within 6 months on orders over $99.',
'woocommerce-paypal-payments'
),
icon: 'payment-method-paypal',
},
{
id: 'credit_and_debit_card_payments',
title: __(
'Credit and debit card payments',
'woocommerce-paypal-payments'
),
description: __(
"Accept all major credit and debit cards - even if your customer doesn't have a PayPal account.",
'woocommerce-paypal-payments'
),
icon: 'payment-method-cards',
},
];
const paymentMethodsOnlineCardPayments = [
{
id: 'advanced_credit_and_debit_card_payments',
title: __(
'Advanced Credit and Debit Card Payments',
'woocommerce-paypal-payments'
),
description: __(
"Present custom credit and debit card fields to your payers so they can pay with credit and debit cards using your site's branding.",
'woocommerce-paypal-payments'
),
icon: 'payment-method-advanced-cards',
modal: ModalAcdc,
},
{
id: 'fastlane',
title: __( 'Fastlane by PayPal', 'woocommerce-paypal-payments' ),
description: __(
"Tap into the scale and trust of PayPal's customer network to recognize shoppers and make guest checkout more seamless than ever.",
'woocommerce-paypal-payments'
),
icon: 'payment-method-fastlane',
modal: ModalFastlane,
},
{
id: 'apply_pay',
title: __( 'Apple Pay', 'woocommerce-paypal-payments' ),
description: __(
'Allow customers to pay via their Apple Pay digital wallet.',
'woocommerce-paypal-payments'
),
icon: 'payment-method-apple-pay',
},
{
id: 'google_pay',
title: __( 'Google Pay', 'woocommerce-paypal-payments' ),
description: __(
'Allow customers to pay via their Google Pay digital wallet.',
'woocommerce-paypal-payments'
),
icon: 'payment-method-google-pay',
},
];
const paymentMethodsAlternative = [
{
id: 'bancontact',
title: __( 'Bancontact', 'woocommerce-paypal-payments' ),
description: __(
'Bancontact is the most widely used, accepted and trusted electronic payment method in Belgium. Bancontact makes it possible to pay directly through the online payment systems of all major Belgian banks.',
'woocommerce-paypal-payments'
),
icon: 'payment-method-bancontact',
},
{
id: 'ideal',
title: __( 'iDEAL', 'woocommerce-paypal-payments' ),
description: __(
'iDEAL is a payment method in the Netherlands that allows buyers to select their issuing bank from a list of options.',
'woocommerce-paypal-payments'
),
icon: 'payment-method-ideal',
},
{
id: 'eps',
title: __( 'eps', 'woocommerce-paypal-payments' ),
description: __(
'An online payment method in Austria, enabling Austrian buyers to make secure payments directly through their bank accounts. Transactions are processed in EUR.',
'woocommerce-paypal-payments'
),
icon: 'payment-method-eps',
},
{
id: 'blik',
title: __( 'BLIK', 'woocommerce-paypal-payments' ),
description: __(
'A widely used mobile payment method in Poland, allowing Polish customers to pay directly via their banking apps. Transactions are processed in PLN.',
'woocommerce-paypal-payments'
),
icon: 'payment-method-blik',
},
{
id: 'mybank',
title: __( 'MyBank', 'woocommerce-paypal-payments' ),
description: __(
'A European online banking payment solution primarily used in Italy, enabling customers to make secure bank transfers during checkout. Transactions are processed in EUR.',
'woocommerce-paypal-payments'
),
icon: 'payment-method-mybank',
},
{
id: 'przelewy24',
title: __( 'Przelewy24', 'woocommerce-paypal-payments' ),
description: __(
'A popular online payment gateway in Poland, offering various payment options for Polish customers. Transactions can be processed in PLN or EUR.',
'woocommerce-paypal-payments'
),
icon: 'payment-method-przelewy24',
},
{
id: 'trustly',
title: __( 'Trustly', 'woocommerce-paypal-payments' ),
description: __(
'A European payment method that allows buyers to make payments directly from their bank accounts, suitable for customers across multiple European countries. Supported currencies include EUR, DKK, SEK, GBP, and NOK.',
'woocommerce-paypal-payments'
),
icon: 'payment-method-trustly',
},
{
id: 'multibanco',
title: __( 'Multibanco', 'woocommerce-paypal-payments' ),
description: __(
'An online payment method in Portugal, enabling Portuguese buyers to make secure payments directly through their bank accounts. Transactions are processed in EUR.',
'woocommerce-paypal-payments'
),
icon: 'payment-method-multibanco',
},
{
id: 'pui',
title: __( 'Pay upon Invoice', 'woocommerce-paypal-payments' ),
description: __(
'Pay upon Invoice is an invoice payment method in Germany. It is a local buy now, pay later payment method that allows the buyer to place an order, receive the goods, try them, verify they are in good order, and then pay the invoice within 30 days.',
'woocommerce-paypal-payments'
),
icon: 'payment-method-ratepay',
condition: ( { storeCountry, storeCurrency } ) =>
storeCountry === 'DE' && storeCurrency === 'EUR',
},
{
id: 'oxxo',
title: __( 'OXXO', 'woocommerce-paypal-payments' ),
description: __(
'OXXO is a Mexican chain of convenience stores. *Get PayPal account permission to use OXXO payment functionality by contacting us at (+52) 8009250304',
'woocommerce-paypal-payments'
),
icon: 'payment-method-oxxo',
condition: ( { storeCountry, storeCurrency } ) =>
storeCountry === 'MX' && storeCurrency === 'MXN',
},
];
export default TabPaymentMethods;

View file

@ -1,31 +1,16 @@
import { useState } from '@wordpress/element';
import ConnectionStatus from './TabSettingsElements/ConnectionStatus';
import CommonSettings from './TabSettingsElements/CommonSettings';
import ExpertSettings from './TabSettingsElements/ExpertSettings';
import { useSettings } from '../../../data/settings-tab/hooks';
const TabSettings = () => {
const [ settings, setSettings ] = useState( {
invoicePrefix: '',
authorizeOnly: false,
captureVirtualOnlyOrders: false,
savePaypalAndVenmo: false,
saveCreditCardAndDebitCard: false,
payNowExperience: false,
sandboxAccountCredentials: false,
sandboxMode: null,
sandboxEnabled: false,
sandboxClientId: '',
sandboxSecretKey: '',
sandboxConnected: false,
logging: false,
subtotalMismatchFallback: null,
brandName: '',
softDescriptor: '',
paypalLandingPage: null,
buttonLanguage: '',
} );
const { settings, setSettings } = useSettings();
const updateFormValue = ( key, value ) => {
setSettings( { ...settings, [ key ]: value } );
setSettings( {
...settings,
[ key ]: value,
} );
};
return (

View file

@ -0,0 +1,41 @@
import { __, sprintf } from '@wordpress/i18n';
import { Button } from '@wordpress/components';
import {
AccordionSettingsBlock,
RadioSettingsBlock,
InputSettingsBlock,
} from '../../../../ReusableComponents/SettingsBlocks';
import {
sandboxData,
productionData,
} from '../../../../../data/settings/connection-details-data';
const ConnectionDetails = ( { settings, updateFormValue } ) => {
const isSandbox = settings.sandboxConnected;
const modeConfig = isSandbox
? productionData( { settings, updateFormValue } )
: sandboxData( { settings, updateFormValue } );
const modeKey = isSandbox ? 'productionMode' : 'sandboxMode';
return (
<AccordionSettingsBlock
title={ modeConfig.title }
description={ modeConfig.description }
>
<RadioSettingsBlock
title={ modeConfig.connectTitle }
description={ modeConfig.connectDescription }
options={ modeConfig.options }
actionProps={ {
key: modeKey,
currentValue: settings[ modeKey ],
callback: updateFormValue,
} }
/>
</AccordionSettingsBlock>
);
};
export default ConnectionDetails;

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