mirror of
https://github.com/woocommerce/woocommerce-paypal-payments.git
synced 2025-09-05 08:59:14 +08:00
🔀 Merge branch 'trunk'
# Conflicts: # modules/ppcp-settings/src/Data/CommonSettings.php # modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php
This commit is contained in:
commit
0bc06deebf
107 changed files with 5635 additions and 3524 deletions
|
@ -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:
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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' )
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 );
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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 }
|
||||
|
|
14
modules/ppcp-blocks/resources/js/Components/paypal-label.js
Normal file
14
modules/ppcp-blocks/resources/js/Components/paypal-label.js
Normal 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" />
|
||||
</>
|
||||
);
|
||||
};
|
493
modules/ppcp-blocks/resources/js/Components/paypal.js
Normal file
493
modules/ppcp-blocks/resources/js/Components/paypal.js
Normal 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
|
||||
) }
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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 }
|
||||
/>
|
||||
),
|
||||
|
|
316
modules/ppcp-blocks/resources/js/paypal-config.js
Normal file
316
modules/ppcp-blocks/resources/js/paypal-config.js
Normal 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 );
|
||||
};
|
|
@ -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' );
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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',
|
||||
)
|
||||
),
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -14,14 +14,12 @@ 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 {
|
||||
|
@ -144,26 +142,6 @@ return array(
|
|||
);
|
||||
},
|
||||
|
||||
'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 {
|
||||
|
||||
$request_data = $container->get( 'button.request-data' );
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -119,8 +119,6 @@
|
|||
background-color: $color-white;
|
||||
|
||||
&::before {
|
||||
transform: translate(3px, 3px);
|
||||
border-width: 6px;
|
||||
border-color: $color-blueberry;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,10 +11,6 @@
|
|||
}
|
||||
|
||||
// Todo List and Feature Items
|
||||
.ppcp-r-tab-overview-todo {
|
||||
margin: 0 0 48px 0;
|
||||
}
|
||||
|
||||
.ppcp-r-todo-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
@ -22,6 +18,15 @@
|
|||
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 +71,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,6 +114,8 @@
|
|||
span {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
margin-top:24px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -231,6 +246,10 @@
|
|||
}
|
||||
|
||||
// Settings Card and Block Styles
|
||||
.ppcp-r-settings-card {
|
||||
margin: 0 0 48px 0;
|
||||
}
|
||||
|
||||
.ppcp-r-settings-card__content {
|
||||
> .ppcp-r-settings-block {
|
||||
&:not(:last-child) {
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -1 +1,3 @@
|
|||
export { default as openSignup } from './Icons/open-signup';
|
||||
export const NOTIFICATION_SUCCESS = '✔️';
|
||||
export const NOTIFICATION_ERROR = '❌';
|
||||
|
|
|
@ -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' ),
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -2,14 +2,21 @@ import { useState, useCallback } from '@wordpress/element';
|
|||
import SettingsBlock from './SettingsBlock';
|
||||
import PaymentMethodItemBlock from './PaymentMethodItemBlock';
|
||||
|
||||
const PaymentMethodsBlock = ( { paymentMethods, className = '' } ) => {
|
||||
const [ selectedMethod, setSelectedMethod ] = useState( null );
|
||||
const PaymentMethodsBlock = ( {
|
||||
paymentMethods,
|
||||
className = '',
|
||||
onTriggerModal,
|
||||
} ) => {
|
||||
const [ selectedMethods, setSelectedMethods ] = useState( {} );
|
||||
|
||||
const handleSelect = useCallback( ( methodId, isSelected ) => {
|
||||
setSelectedMethod( isSelected ? methodId : null );
|
||||
setSelectedMethods( ( prev ) => ( {
|
||||
...prev,
|
||||
[ methodId ]: isSelected,
|
||||
} ) );
|
||||
}, [] );
|
||||
|
||||
if ( paymentMethods.length === 0 ) {
|
||||
if ( ! paymentMethods?.length ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -21,10 +28,15 @@ const PaymentMethodsBlock = ( { paymentMethods, className = '' } ) => {
|
|||
<PaymentMethodItemBlock
|
||||
key={ paymentMethod.id }
|
||||
{ ...paymentMethod }
|
||||
isSelected={ selectedMethod === paymentMethod.id }
|
||||
isSelected={ Boolean(
|
||||
selectedMethods[ paymentMethod.id ]
|
||||
) }
|
||||
onSelect={ ( checked ) =>
|
||||
handleSelect( paymentMethod.id, checked )
|
||||
}
|
||||
onTriggerModal={ () =>
|
||||
onTriggerModal?.( paymentMethod.id )
|
||||
}
|
||||
/>
|
||||
) ) }
|
||||
</SettingsBlock>
|
||||
|
|
|
@ -33,8 +33,10 @@ export const Header = ( { children, className = '' } ) => (
|
|||
);
|
||||
|
||||
// Card Elements
|
||||
export const Content = ( { children } ) => (
|
||||
<div className="ppcp-r-settings-card__content">{ children }</div>
|
||||
export const Content = ( { children, id = '' } ) => (
|
||||
<div id={ id } className="ppcp-r-settings-card__content">
|
||||
{ children }
|
||||
</div>
|
||||
);
|
||||
|
||||
export const ContentWrapper = ( { children } ) => (
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Content, ContentWrapper } from './SettingsBlocks';
|
||||
|
||||
const SettingsCard = ( {
|
||||
id,
|
||||
className: extraClassName,
|
||||
title,
|
||||
description,
|
||||
|
@ -17,8 +18,10 @@ const SettingsCard = ( {
|
|||
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">
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -53,6 +53,9 @@ const AcdcFlow = ( { isFastlane, isPayLater, storeCountry } ) => {
|
|||
imageBadge={ [
|
||||
'icon-payment-method-paypal-small.svg',
|
||||
] }
|
||||
textBadge={
|
||||
<PricingTitleBadge item="plater" />
|
||||
}
|
||||
description={ sprintf(
|
||||
// translators: %s: Link to PayPal business fees guide
|
||||
__(
|
||||
|
|
|
@ -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 />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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;
|
|
@ -1,7 +1,6 @@
|
|||
import { Button, Icon } from '@wordpress/components';
|
||||
import { chevronLeft } from '@wordpress/icons';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { OnboardingHooks } from '../../../../data';
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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: '#',
|
||||
},
|
||||
],
|
||||
} }
|
||||
/>
|
||||
) ) }
|
||||
/>,
|
||||
<FeatureSettingsBlock
|
||||
key="support"
|
||||
title={ __( 'Support', 'woocommerce-paypal-payments' ) }
|
||||
description={ __(
|
||||
'Need help? Access troubleshooting tips or contact our support team for personalized assistance.',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
actionProps={ {
|
||||
buttons: [
|
||||
{
|
||||
type: 'tertiary',
|
||||
text: __(
|
||||
'View support options',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
url: '#',
|
||||
},
|
||||
],
|
||||
} }
|
||||
/>,
|
||||
] }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// TODO: This list should be refactored into a separate module, maybe utils/thingsToDoNext.js
|
||||
const todosDataDefault = [
|
||||
{
|
||||
value: 'paypal_later_messaging',
|
||||
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;
|
||||
|
|
|
@ -4,12 +4,12 @@ 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 { useActiveModal } from '../../../data/common/hooks';
|
||||
import Modal from './TabSettingsElements/Blocks/Modal';
|
||||
|
||||
const TabPaymentMethods = () => {
|
||||
const { storeCountry, storeCurrency } = CommonHooks.useWooSettings();
|
||||
const { activeModal, setActiveModal } = useActiveModal();
|
||||
|
||||
const filteredPaymentMethods = useMemo( () => {
|
||||
const contextProps = { storeCountry, storeCurrency };
|
||||
|
@ -30,9 +30,24 @@ const TabPaymentMethods = () => {
|
|||
};
|
||||
}, [ storeCountry, storeCurrency ] );
|
||||
|
||||
const getActiveMethod = () => {
|
||||
if ( ! activeModal ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const allMethods = [
|
||||
...filteredPaymentMethods.payPalCheckout,
|
||||
...filteredPaymentMethods.onlineCardPayments,
|
||||
...filteredPaymentMethods.alternative,
|
||||
];
|
||||
|
||||
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.',
|
||||
|
@ -43,9 +58,11 @@ const TabPaymentMethods = () => {
|
|||
>
|
||||
<PaymentMethodsBlock
|
||||
paymentMethods={ filteredPaymentMethods.payPalCheckout }
|
||||
onTriggerModal={ setActiveModal }
|
||||
/>
|
||||
</SettingsCard>
|
||||
<SettingsCard
|
||||
id="ppcp-card-payments-card"
|
||||
title={ __(
|
||||
'Online Card Payments',
|
||||
'woocommerce-paypal-payments'
|
||||
|
@ -59,9 +76,11 @@ const TabPaymentMethods = () => {
|
|||
>
|
||||
<PaymentMethodsBlock
|
||||
paymentMethods={ filteredPaymentMethods.onlineCardPayments }
|
||||
onTriggerModal={ setActiveModal }
|
||||
/>
|
||||
</SettingsCard>
|
||||
<SettingsCard
|
||||
id="ppcp-alternative-payments-card"
|
||||
title={ __(
|
||||
'Alternative Payment Methods',
|
||||
'woocommerce-paypal-payments'
|
||||
|
@ -75,8 +94,24 @@ const TabPaymentMethods = () => {
|
|||
>
|
||||
<PaymentMethodsBlock
|
||||
paymentMethods={ filteredPaymentMethods.alternative }
|
||||
onTriggerModal={ setActiveModal }
|
||||
/>
|
||||
</SettingsCard>
|
||||
|
||||
{ activeModal && (
|
||||
<Modal
|
||||
method={ getActiveMethod() }
|
||||
setModalIsVisible={ () => setActiveModal( null ) }
|
||||
onSave={ ( methodId, settings ) => {
|
||||
console.log(
|
||||
'Saving settings for:',
|
||||
methodId,
|
||||
settings
|
||||
);
|
||||
setActiveModal( null );
|
||||
} }
|
||||
/>
|
||||
) }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -98,7 +133,6 @@ const paymentMethodsPayPalCheckout = [
|
|||
'woocommerce-paypal-payments'
|
||||
),
|
||||
icon: 'payment-method-paypal',
|
||||
modal: ModalPayPal,
|
||||
},
|
||||
{
|
||||
id: 'venmo',
|
||||
|
@ -111,9 +145,9 @@ const paymentMethodsPayPalCheckout = [
|
|||
},
|
||||
{
|
||||
id: 'paypal_credit',
|
||||
title: __( 'PayPal Credit', 'woocommerce-paypal-payments' ),
|
||||
title: __( 'Pay Later', '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.',
|
||||
'Get paid in full at checkout while giving your customers the flexibility to pay in installments over time with no late fees.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
icon: 'payment-method-paypal',
|
||||
|
@ -144,7 +178,6 @@ const paymentMethodsOnlineCardPayments = [
|
|||
'woocommerce-paypal-payments'
|
||||
),
|
||||
icon: 'payment-method-advanced-cards',
|
||||
modal: ModalAcdc,
|
||||
},
|
||||
{
|
||||
id: 'fastlane',
|
||||
|
@ -154,10 +187,9 @@ const paymentMethodsOnlineCardPayments = [
|
|||
'woocommerce-paypal-payments'
|
||||
),
|
||||
icon: 'payment-method-fastlane',
|
||||
modal: ModalFastlane,
|
||||
},
|
||||
{
|
||||
id: 'apply_pay',
|
||||
id: 'apple_pay',
|
||||
title: __( 'Apple Pay', 'woocommerce-paypal-payments' ),
|
||||
description: __(
|
||||
'Allow customers to pay via their Apple Pay digital wallet.',
|
||||
|
|
|
@ -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;
|
|
@ -0,0 +1,297 @@
|
|||
import { __ } from '@wordpress/i18n';
|
||||
import { TAB_IDS, selectTab } from '../../../../../utils/tabSelector';
|
||||
|
||||
const Features = {
|
||||
getFeatures: ( setActiveModal ) => [
|
||||
{
|
||||
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' ),
|
||||
onClick: () => {
|
||||
selectTab(
|
||||
TAB_IDS.PAYMENT_METHODS,
|
||||
'ppcp-paypal-checkout-card'
|
||||
).then( () => {
|
||||
setActiveModal( 'paypal' );
|
||||
} );
|
||||
},
|
||||
showWhen: 'enabled',
|
||||
class: 'small-button',
|
||||
},
|
||||
{
|
||||
type: 'secondary',
|
||||
text: __( 'Apply', 'woocommerce-paypal-payments' ),
|
||||
urls: {
|
||||
sandbox:
|
||||
'https://www.sandbox.paypal.com/bizsignup/entry?product=ADVANCED_VAULTING',
|
||||
live: 'https://www.paypal.com/bizsignup/entry?product=ADVANCED_VAULTING',
|
||||
},
|
||||
showWhen: 'disabled',
|
||||
class: 'small-button',
|
||||
},
|
||||
{
|
||||
type: 'tertiary',
|
||||
text: __( 'Learn more', 'woocommerce-paypal-payments' ),
|
||||
urls: {
|
||||
sandbox: '#',
|
||||
live: '#',
|
||||
},
|
||||
class: 'small-button',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
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' ),
|
||||
onClick: () => {
|
||||
selectTab(
|
||||
TAB_IDS.PAYMENT_METHODS,
|
||||
'ppcp-card-payments-card'
|
||||
).then( () => {
|
||||
setActiveModal(
|
||||
'advanced_credit_and_debit_card_payments'
|
||||
);
|
||||
} );
|
||||
},
|
||||
showWhen: 'enabled',
|
||||
class: 'small-button',
|
||||
},
|
||||
{
|
||||
type: 'secondary',
|
||||
text: __( 'Apply', 'woocommerce-paypal-payments' ),
|
||||
urls: {
|
||||
sandbox:
|
||||
'https://www.sandbox.paypal.com/bizsignup/entry?product=ppcp',
|
||||
live: 'https://www.paypal.com/bizsignup/entry?product=ppcp',
|
||||
},
|
||||
showWhen: 'disabled',
|
||||
class: 'small-button',
|
||||
},
|
||||
{
|
||||
type: 'tertiary',
|
||||
text: __( 'Learn more', 'woocommerce-paypal-payments' ),
|
||||
urls: {
|
||||
sandbox: '#',
|
||||
live: '#',
|
||||
},
|
||||
class: 'small-button',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
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: __( 'Configure', 'woocommerce-paypal-payments' ),
|
||||
onClick: () => {
|
||||
selectTab(
|
||||
TAB_IDS.PAYMENT_METHODS,
|
||||
'ppcp-alternative-payments-card'
|
||||
);
|
||||
},
|
||||
showWhen: 'enabled',
|
||||
class: 'small-button',
|
||||
},
|
||||
{
|
||||
type: 'secondary',
|
||||
text: __( 'Apply', 'woocommerce-paypal-payments' ),
|
||||
urls: {
|
||||
sandbox: '#',
|
||||
live: '#',
|
||||
},
|
||||
showWhen: 'disabled',
|
||||
class: 'small-button',
|
||||
},
|
||||
{
|
||||
type: 'tertiary',
|
||||
text: __( 'Learn more', 'woocommerce-paypal-payments' ),
|
||||
urls: {
|
||||
sandbox: '#',
|
||||
live: '#',
|
||||
},
|
||||
class: 'small-button',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
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' ),
|
||||
onClick: () => {
|
||||
selectTab(
|
||||
TAB_IDS.PAYMENT_METHODS,
|
||||
'ppcp-card-payments-card'
|
||||
).then( () => {
|
||||
setActiveModal( 'google_pay' );
|
||||
} );
|
||||
},
|
||||
showWhen: 'enabled',
|
||||
class: 'small-button',
|
||||
},
|
||||
{
|
||||
type: 'secondary',
|
||||
text: __( 'Apply', 'woocommerce-paypal-payments' ),
|
||||
urls: {
|
||||
sandbox:
|
||||
'https://www.sandbox.paypal.com/bizsignup/add-product?product=payment_methods&capabilities=GOOGLE_PAY',
|
||||
live: 'https://www.paypal.com/bizsignup/add-product?product=payment_methods&capabilities=GOOGLE_PAY',
|
||||
},
|
||||
showWhen: 'disabled',
|
||||
class: 'small-button',
|
||||
},
|
||||
{
|
||||
type: 'tertiary',
|
||||
text: __( 'Learn more', 'woocommerce-paypal-payments' ),
|
||||
urls: {
|
||||
sandbox: '#',
|
||||
live: '#',
|
||||
},
|
||||
class: 'small-button',
|
||||
},
|
||||
],
|
||||
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: __( 'Configure', 'woocommerce-paypal-payments' ),
|
||||
onClick: () => {
|
||||
selectTab(
|
||||
TAB_IDS.PAYMENT_METHODS,
|
||||
'ppcp-card-payments-card'
|
||||
).then( () => {
|
||||
setActiveModal( 'apple_pay' );
|
||||
} );
|
||||
},
|
||||
showWhen: 'enabled',
|
||||
class: 'small-button',
|
||||
},
|
||||
{
|
||||
type: 'secondary',
|
||||
text: __(
|
||||
'Domain registration',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
urls: {
|
||||
sandbox:
|
||||
'https://www.sandbox.paypal.com/uccservicing/apm/applepay',
|
||||
live: 'https://www.paypal.com/uccservicing/apm/applepay',
|
||||
},
|
||||
showWhen: 'enabled',
|
||||
class: 'small-button',
|
||||
},
|
||||
{
|
||||
type: 'secondary',
|
||||
text: __( 'Apply', 'woocommerce-paypal-payments' ),
|
||||
urls: {
|
||||
sandbox:
|
||||
'https://www.sandbox.paypal.com/bizsignup/add-product?product=payment_methods&capabilities=APPLE_PAY',
|
||||
live: 'https://www.paypal.com/bizsignup/add-product?product=payment_methods&capabilities=APPLE_PAY',
|
||||
},
|
||||
showWhen: 'disabled',
|
||||
class: 'small-button',
|
||||
},
|
||||
{
|
||||
type: 'tertiary',
|
||||
text: __( 'Learn more', 'woocommerce-paypal-payments' ),
|
||||
urls: {
|
||||
sandbox: '#',
|
||||
live: '#',
|
||||
},
|
||||
class: 'small-button',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
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' ),
|
||||
onClick: () => {
|
||||
selectTab(
|
||||
TAB_IDS.PAYMENT_METHODS,
|
||||
'ppcp-paypal-checkout-card'
|
||||
).then( () => {
|
||||
setActiveModal( 'paypal' );
|
||||
} );
|
||||
},
|
||||
showWhen: 'enabled',
|
||||
class: 'small-button',
|
||||
},
|
||||
{
|
||||
type: 'secondary',
|
||||
text: __( 'Apply', 'woocommerce-paypal-payments' ),
|
||||
urls: {
|
||||
sandbox: '#',
|
||||
live: '#',
|
||||
},
|
||||
showWhen: 'disabled',
|
||||
class: 'small-button',
|
||||
},
|
||||
{
|
||||
type: 'tertiary',
|
||||
text: __( 'Learn more', 'woocommerce-paypal-payments' ),
|
||||
urls: {
|
||||
sandbox: '#',
|
||||
live: '#',
|
||||
},
|
||||
class: 'small-button',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default Features;
|
|
@ -0,0 +1,131 @@
|
|||
import { __ } from '@wordpress/i18n';
|
||||
import {
|
||||
Button,
|
||||
TextControl,
|
||||
ToggleControl,
|
||||
RadioControl,
|
||||
} from '@wordpress/components';
|
||||
import { useState } from '@wordpress/element';
|
||||
import PaymentMethodModal from '../../../../ReusableComponents/PaymentMethodModal';
|
||||
import { getPaymentMethods } from './PaymentMethods';
|
||||
|
||||
const Modal = ( { method, setModalIsVisible, onSave } ) => {
|
||||
const [ settings, setSettings ] = useState( () => {
|
||||
if ( ! method?.id ) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const methodConfig = getPaymentMethods( method );
|
||||
if ( ! methodConfig?.fields ) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const initialSettings = {};
|
||||
Object.entries( methodConfig.fields ).forEach( ( [ key, field ] ) => {
|
||||
initialSettings[ key ] = field.default;
|
||||
} );
|
||||
return initialSettings;
|
||||
} );
|
||||
|
||||
if ( ! method?.id ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const methodConfig = getPaymentMethods( method );
|
||||
if ( ! methodConfig?.fields ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const renderField = ( key, field ) => {
|
||||
switch ( field.type ) {
|
||||
case 'text':
|
||||
return (
|
||||
<div className="ppcp-r-modal__field-row">
|
||||
<TextControl
|
||||
className="ppcp-r-vertical-text-control"
|
||||
label={ field.label }
|
||||
value={ settings[ key ] }
|
||||
onChange={ ( value ) =>
|
||||
setSettings( ( prev ) => ( {
|
||||
...prev,
|
||||
[ key ]: value,
|
||||
} ) )
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'toggle':
|
||||
return (
|
||||
<div className="ppcp-r-modal__field-row">
|
||||
<ToggleControl
|
||||
label={ field.label }
|
||||
checked={ settings[ key ] }
|
||||
onChange={ ( value ) =>
|
||||
setSettings( ( prev ) => ( {
|
||||
...prev,
|
||||
[ key ]: value,
|
||||
} ) )
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'radio':
|
||||
return (
|
||||
<>
|
||||
<strong className="ppcp-r-modal__content-title">
|
||||
{ field.label }
|
||||
</strong>
|
||||
{ field.description && (
|
||||
<p className="ppcp-r-modal__description">
|
||||
{ field.description }
|
||||
</p>
|
||||
) }
|
||||
<div className="ppcp-r-modal__field-row">
|
||||
<RadioControl
|
||||
selected={ settings[ key ] }
|
||||
options={ field.options }
|
||||
onChange={ ( value ) =>
|
||||
setSettings( ( prev ) => ( {
|
||||
...prev,
|
||||
[ key ]: value,
|
||||
} ) )
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
onSave?.( method.id, settings );
|
||||
setModalIsVisible( false );
|
||||
};
|
||||
|
||||
return (
|
||||
<PaymentMethodModal
|
||||
setModalIsVisible={ setModalIsVisible }
|
||||
icon={ methodConfig.icon }
|
||||
title={ method.title }
|
||||
>
|
||||
<div className="ppcp-r-modal__field-rows">
|
||||
{ Object.entries( methodConfig.fields ).map(
|
||||
( [ key, field ] ) => renderField( key, field )
|
||||
) }
|
||||
|
||||
<div className="ppcp-r-modal__field-row ppcp-r-modal__field-row--save">
|
||||
<Button variant="primary" onClick={ handleSave }>
|
||||
{ __( 'Save changes', 'woocommerce-paypal-payments' ) }
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</PaymentMethodModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default Modal;
|
|
@ -0,0 +1,179 @@
|
|||
import { __, sprintf } from '@wordpress/i18n';
|
||||
|
||||
const createStandardFields = ( methodId, defaultTitle ) => ( {
|
||||
checkoutPageTitle: {
|
||||
type: 'text',
|
||||
default: defaultTitle,
|
||||
label: __( 'Checkout page title', 'woocommerce-paypal-payments' ),
|
||||
},
|
||||
checkoutPageDescription: {
|
||||
type: 'text',
|
||||
default: sprintf(
|
||||
/* translators: %s: payment method title */
|
||||
__( 'Pay with %s', 'woocommerce-paypal-payments' ),
|
||||
defaultTitle
|
||||
),
|
||||
label: __( 'Checkout page description', 'woocommerce-paypal-payments' ),
|
||||
},
|
||||
} );
|
||||
|
||||
const paymentMethods = {
|
||||
// PayPal Checkout methods
|
||||
paypal: {
|
||||
fields: {
|
||||
...createStandardFields( 'paypal', 'PayPal' ),
|
||||
showLogo: {
|
||||
type: 'toggle',
|
||||
default: false,
|
||||
label: __( 'Show logo', 'woocommerce-paypal-payments' ),
|
||||
},
|
||||
},
|
||||
},
|
||||
venmo: {
|
||||
fields: createStandardFields( 'venmo', 'Venmo' ),
|
||||
},
|
||||
paypal_credit: {
|
||||
fields: createStandardFields( 'paypal_credit', 'PayPal Credit' ),
|
||||
},
|
||||
credit_and_debit_card_payments: {
|
||||
fields: createStandardFields(
|
||||
'credit_and_debit_card_payments',
|
||||
__(
|
||||
'Credit and debit card payments',
|
||||
'woocommerce-paypal-payments'
|
||||
)
|
||||
),
|
||||
},
|
||||
|
||||
// Online Card Payments
|
||||
advanced_credit_and_debit_card_payments: {
|
||||
fields: {
|
||||
...createStandardFields(
|
||||
'advanced_credit_and_debit_card_payments',
|
||||
__(
|
||||
'Advanced Credit and Debit Card Payments',
|
||||
'woocommerce-paypal-payments'
|
||||
)
|
||||
),
|
||||
threeDSecure: {
|
||||
type: 'radio',
|
||||
default: 'no-3d-secure',
|
||||
label: __( '3D Secure', 'woocommerce-paypal-payments' ),
|
||||
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'
|
||||
),
|
||||
options: [
|
||||
{
|
||||
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',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
fastlane: {
|
||||
fields: {
|
||||
...createStandardFields( 'fastlane', 'Fastlane by PayPal' ),
|
||||
cardholderName: {
|
||||
type: 'toggle',
|
||||
default: false,
|
||||
label: __(
|
||||
'Display cardholder name',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
},
|
||||
displayWatermark: {
|
||||
type: 'toggle',
|
||||
default: false,
|
||||
label: __(
|
||||
'Display Fastlane Watermark',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// Digital Wallets
|
||||
apple_pay: {
|
||||
fields: createStandardFields( 'apple_pay', 'Apple Pay' ),
|
||||
},
|
||||
google_pay: {
|
||||
fields: createStandardFields( 'google_pay', 'Google Pay' ),
|
||||
},
|
||||
|
||||
// Alternative Payment Methods
|
||||
bancontact: {
|
||||
fields: createStandardFields( 'bancontact', 'Bancontact' ),
|
||||
},
|
||||
ideal: {
|
||||
fields: createStandardFields( 'ideal', 'iDEAL' ),
|
||||
},
|
||||
eps: {
|
||||
fields: createStandardFields( 'eps', 'eps' ),
|
||||
},
|
||||
blik: {
|
||||
fields: createStandardFields( 'blik', 'BLIK' ),
|
||||
},
|
||||
mybank: {
|
||||
fields: createStandardFields( 'mybank', 'MyBank' ),
|
||||
},
|
||||
przelewy24: {
|
||||
fields: createStandardFields( 'przelewy24', 'Przelewy24' ),
|
||||
},
|
||||
trustly: {
|
||||
fields: createStandardFields( 'trustly', 'Trustly' ),
|
||||
},
|
||||
multibanco: {
|
||||
fields: createStandardFields( 'multibanco', 'Multibanco' ),
|
||||
},
|
||||
pui: {
|
||||
fields: createStandardFields( 'pui', 'Pay upon Invoice' ),
|
||||
},
|
||||
oxxo: {
|
||||
fields: createStandardFields( 'oxxo', 'OXXO' ),
|
||||
},
|
||||
};
|
||||
|
||||
// Function to get configuration for a payment method
|
||||
export const getPaymentMethods = ( method ) => {
|
||||
if ( ! method?.id ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If method has specific config, return it
|
||||
if ( paymentMethods[ method.id ] ) {
|
||||
return {
|
||||
...paymentMethods[ method.id ],
|
||||
icon: method.icon,
|
||||
};
|
||||
}
|
||||
|
||||
// Return standard config for new payment methods
|
||||
return {
|
||||
fields: createStandardFields( method.id, method.title ),
|
||||
icon: method.icon,
|
||||
};
|
||||
};
|
||||
|
||||
// Function to check if a method has settings defined
|
||||
export const hasSettings = ( methodId ) => {
|
||||
return Boolean( methodId && paymentMethods[ methodId ] );
|
||||
};
|
|
@ -1,202 +0,0 @@
|
|||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { Button } from '@wordpress/components';
|
||||
import {
|
||||
AccordionSettingsBlock,
|
||||
ButtonSettingsBlock,
|
||||
RadioSettingsBlock,
|
||||
ToggleSettingsBlock,
|
||||
InputSettingsBlock,
|
||||
} from '../../../../ReusableComponents/SettingsBlocks';
|
||||
import TitleBadge, {
|
||||
TITLE_BADGE_POSITIVE,
|
||||
} from '../../../../ReusableComponents/TitleBadge';
|
||||
import ConnectionInfo, {
|
||||
connectionStatusDataDefault,
|
||||
} from '../../../../ReusableComponents/ConnectionInfo';
|
||||
|
||||
const Sandbox = ( { settings, updateFormValue } ) => {
|
||||
const className = settings.sandboxConnected
|
||||
? 'ppcp-r-settings-block--sandbox-connected'
|
||||
: 'ppcp-r-settings-block--sandbox-disconnected';
|
||||
|
||||
return (
|
||||
<AccordionSettingsBlock
|
||||
title={ __( 'Sandbox', 'woocommerce-paypal-payments' ) }
|
||||
className={ className }
|
||||
description={ __(
|
||||
"Test your site in PayPal's Sandbox environment.",
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
actionProps={ {
|
||||
callback: updateFormValue,
|
||||
key: 'payNowExperience',
|
||||
value: settings.payNowExperience,
|
||||
} }
|
||||
>
|
||||
{ settings.sandboxConnected && (
|
||||
<ButtonSettingsBlock
|
||||
title={ __(
|
||||
'Sandbox account credentials',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
description={ __(
|
||||
'Your account is connected to sandbox, no real charging takes place. To accept live payments, turn off sandbox mode and connect your live PayPal account.',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
tag={
|
||||
<TitleBadge
|
||||
type={ TITLE_BADGE_POSITIVE }
|
||||
text={ __(
|
||||
'Connected',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="ppcp-r-settings-block--sandbox">
|
||||
<ToggleSettingsBlock
|
||||
title={ __(
|
||||
'Enable sandbox mode',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
actionProps={ {
|
||||
callback: updateFormValue,
|
||||
key: 'sandboxEnabled',
|
||||
value: settings.sandboxEnabled,
|
||||
} }
|
||||
/>
|
||||
<ConnectionInfo
|
||||
connectionStatusDataDefault={
|
||||
connectionStatusDataDefault
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={ () =>
|
||||
updateFormValue( 'sandboxConnected', false )
|
||||
}
|
||||
>
|
||||
{ __(
|
||||
'Disconnect Sandbox',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
</Button>
|
||||
</div>
|
||||
</ButtonSettingsBlock>
|
||||
) }
|
||||
{ ! settings.sandboxConnected && (
|
||||
<RadioSettingsBlock
|
||||
title={ __(
|
||||
'Connect Sandbox Account',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
description={ __(
|
||||
'Connect a PayPal Sandbox account in order to test your website. Transactions made will not result in actual money movement. Do not fulfil orders completed in Sandbox mode.',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
options={ [
|
||||
{
|
||||
id: 'sandbox_mode',
|
||||
value: 'sandbox_mode',
|
||||
label: __(
|
||||
'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'
|
||||
),
|
||||
additionalContent: (
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={ () =>
|
||||
updateFormValue(
|
||||
'sandboxConnected',
|
||||
true
|
||||
)
|
||||
}
|
||||
>
|
||||
{ __(
|
||||
'Connect Sandbox Account',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'manual_connect',
|
||||
value: 'manual_connect',
|
||||
label: __(
|
||||
'Manual Connect',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
description: sprintf(
|
||||
__(
|
||||
'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'
|
||||
),
|
||||
'#'
|
||||
),
|
||||
additionalContent: (
|
||||
<>
|
||||
<InputSettingsBlock
|
||||
title={ __(
|
||||
'Sandbox Client ID',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
actionProps={ {
|
||||
value: settings.sandboxClientId, // Add this to settings if not present
|
||||
callback: updateFormValue,
|
||||
key: 'sandboxClientId',
|
||||
placeholder: __(
|
||||
'Enter Client ID',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
} }
|
||||
/>
|
||||
<InputSettingsBlock
|
||||
title={ __(
|
||||
'Sandbox Secret Key',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
actionProps={ {
|
||||
value: settings.sandboxSecretKey, // Add this to settings if not present
|
||||
callback: updateFormValue,
|
||||
key: 'sandboxSecretKey',
|
||||
placeholder: __(
|
||||
'Enter Secret Key',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
} }
|
||||
/>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={ () =>
|
||||
updateFormValue(
|
||||
'sandboxManuallyConnected',
|
||||
true
|
||||
)
|
||||
} // Add this handler if needed
|
||||
>
|
||||
{ __(
|
||||
'Connect Account',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
</Button>
|
||||
</>
|
||||
),
|
||||
},
|
||||
] }
|
||||
actionProps={ {
|
||||
name: 'paypal_connect_sandbox',
|
||||
key: 'sandboxMode',
|
||||
currentValue: settings.sandboxMode,
|
||||
callback: updateFormValue,
|
||||
} }
|
||||
/>
|
||||
) }
|
||||
</AccordionSettingsBlock>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sandbox;
|
|
@ -1,168 +0,0 @@
|
|||
import { __ } from '@wordpress/i18n';
|
||||
import {
|
||||
Header,
|
||||
Title,
|
||||
Description,
|
||||
AccordionSettingsBlock,
|
||||
ToggleSettingsBlock,
|
||||
ButtonSettingsBlock,
|
||||
} from '../../../../ReusableComponents/SettingsBlocks';
|
||||
import SettingsBlock from '../../../../ReusableComponents/SettingsBlocks/SettingsBlock';
|
||||
|
||||
const Troubleshooting = ( { updateFormValue, settings } ) => {
|
||||
return (
|
||||
<AccordionSettingsBlock
|
||||
className="ppcp-r-settings-block--troubleshooting"
|
||||
title={ __( 'Troubleshooting', 'woocommerce-paypal-payments' ) }
|
||||
description={ __(
|
||||
'Access tools to help debug and resolve issues.',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
actionProps={ {
|
||||
callback: updateFormValue,
|
||||
key: 'payNowExperience',
|
||||
value: settings.payNowExperience,
|
||||
} }
|
||||
>
|
||||
<ToggleSettingsBlock
|
||||
title={ __( 'Logging', 'woocommerce-paypal-payments' ) }
|
||||
description={ __(
|
||||
'Log additional debugging information in the WooCommerce logs that can assist technical staff to determine issues.',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
actionProps={ {
|
||||
callback: updateFormValue,
|
||||
key: 'logging',
|
||||
value: settings.logging,
|
||||
} }
|
||||
/>
|
||||
<SettingsBlock>
|
||||
<Header>
|
||||
<Title>
|
||||
{ __(
|
||||
'Subscribed PayPal webhooks',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
</Title>
|
||||
<Description>
|
||||
{ __(
|
||||
'The following PayPal webhooks are subscribed. More information about the webhooks is available in the',
|
||||
'woocommerce-paypal-payments'
|
||||
) }{ ' ' }
|
||||
<a href="https://woocommerce.com/document/woocommerce-paypal-payments/#webhook-status">
|
||||
{ __(
|
||||
'Webhook Status documentation',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
</a>
|
||||
.
|
||||
</Description>
|
||||
</Header>
|
||||
<HooksTable data={ hooksExampleData() } />
|
||||
</SettingsBlock>
|
||||
|
||||
<ButtonSettingsBlock
|
||||
title={ __(
|
||||
'Resubscribe webhooks',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
description={ __(
|
||||
'Click to remove the current webhook subscription and subscribe again, for example, if the website domain or URL structure changed.',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
actionProps={ {
|
||||
buttonType: 'secondary',
|
||||
callback: () =>
|
||||
console.log(
|
||||
'Resubscribe webhooks',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
value: __(
|
||||
'Resubscribe webhooks',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
} }
|
||||
/>
|
||||
|
||||
<ButtonSettingsBlock
|
||||
title={ __(
|
||||
'Simulate webhooks',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
actionProps={ {
|
||||
buttonType: 'secondary',
|
||||
callback: () =>
|
||||
console.log(
|
||||
'Simulate webhooks',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
value: __(
|
||||
'Simulate webhooks',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
} }
|
||||
/>
|
||||
</AccordionSettingsBlock>
|
||||
);
|
||||
};
|
||||
|
||||
const hooksExampleData = () => {
|
||||
return {
|
||||
url: 'https://www.rt3.tech/wordpress/paypal-ux-testin/index.php?rest_route=/paypal/v1/incoming',
|
||||
hooks: [
|
||||
'billing plan pricing-change activated',
|
||||
'billing plan updated',
|
||||
'billing subscription cancelled',
|
||||
'catalog product updated',
|
||||
'checkout order approved',
|
||||
'checkout order completed',
|
||||
'checkout payment-approval reversed',
|
||||
'payment authorization voided',
|
||||
'payment capture completed',
|
||||
'payment capture denied',
|
||||
'payment capture pending',
|
||||
'payment capture refunded',
|
||||
'payment capture reversed',
|
||||
'payment order cancelled',
|
||||
'payment sale completed',
|
||||
'payment sale refunded',
|
||||
'vault payment-token created',
|
||||
'vault payment-token deleted',
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const HooksTable = ( { data } ) => {
|
||||
return (
|
||||
<table className="ppcp-r-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="ppcp-r-table__hooks-url">
|
||||
{ __( 'URL', 'woocommerce-paypal-payments' ) }
|
||||
</th>
|
||||
<th className="ppcp-r-table__hooks-events">
|
||||
{ __(
|
||||
'Tracked events',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="ppcp-r-table__hooks-url">{ data?.url }</td>
|
||||
<td className="ppcp-r-table__hooks-events">
|
||||
{ data.hooks.map( ( hook, index ) => (
|
||||
<span key={ hook }>
|
||||
{ hook }{ ' ' }
|
||||
{ index !== data.hooks.length - 1 && ',' }
|
||||
</span>
|
||||
) ) }
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
export default Troubleshooting;
|
|
@ -0,0 +1,47 @@
|
|||
import { __ } from '@wordpress/i18n';
|
||||
import { CommonHooks } from '../../../../../../data';
|
||||
import { Title } from '../../../../../ReusableComponents/SettingsBlocks';
|
||||
|
||||
const HooksTableBlock = () => {
|
||||
const { webhooks } = CommonHooks.useWebhooks();
|
||||
const { url, events } = webhooks;
|
||||
|
||||
if ( ! url || ! events?.length ) {
|
||||
return <div>...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<WebhookUrl url={ url } />
|
||||
<WebhookEvents events={ events } />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const WebhookUrl = ( { url } ) => {
|
||||
return (
|
||||
<div>
|
||||
<Title>
|
||||
{ __( 'Notification URL', 'woocommerce-paypal-payments' ) }
|
||||
</Title>
|
||||
<p>{ url }</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const WebhookEvents = ( { events } ) => {
|
||||
return (
|
||||
<div>
|
||||
<Title>
|
||||
{ __( 'Subscribed Events', 'woocommerce-paypal-payments' ) }
|
||||
</Title>
|
||||
<ul>
|
||||
{ events.map( ( event, index ) => (
|
||||
<li key={ index }>{ event }</li>
|
||||
) ) }
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HooksTableBlock;
|
|
@ -0,0 +1,72 @@
|
|||
import { useState } from '@wordpress/element';
|
||||
import { STORE_NAME } from '../../../../../../data/common';
|
||||
import { ButtonSettingsBlock } from '../../../../../ReusableComponents/SettingsBlocks';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useDispatch } from '@wordpress/data';
|
||||
import { store as noticesStore } from '@wordpress/notices';
|
||||
import {
|
||||
NOTIFICATION_ERROR,
|
||||
NOTIFICATION_SUCCESS,
|
||||
} from '../../../../../ReusableComponents/Icons';
|
||||
|
||||
const ResubscribeBlock = () => {
|
||||
const { createSuccessNotice, createErrorNotice } =
|
||||
useDispatch( noticesStore );
|
||||
const [ resubscribing, setResubscribing ] = useState( false );
|
||||
|
||||
const { resubscribeWebhooks } = useDispatch( STORE_NAME );
|
||||
|
||||
const startResubscribingWebhooks = async () => {
|
||||
setResubscribing( true );
|
||||
try {
|
||||
await resubscribeWebhooks();
|
||||
} catch ( error ) {
|
||||
setResubscribing( false );
|
||||
createErrorNotice(
|
||||
__(
|
||||
'Operation failed. Check WooCommerce logs for more details.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
{
|
||||
icon: NOTIFICATION_ERROR,
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setResubscribing( false );
|
||||
createSuccessNotice(
|
||||
__(
|
||||
'Webhooks were successfully re-subscribed.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
{
|
||||
icon: NOTIFICATION_SUCCESS,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ButtonSettingsBlock
|
||||
title={ __(
|
||||
'Resubscribe webhooks',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
description={ __(
|
||||
'Click to remove the current webhook subscription and subscribe again, for example, if the website domain or URL structure changed.',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
actionProps={ {
|
||||
buttonType: 'secondary',
|
||||
isBusy: resubscribing,
|
||||
callback: () => startResubscribingWebhooks(),
|
||||
value: __(
|
||||
'Resubscribe webhooks',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
} }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResubscribeBlock;
|
|
@ -0,0 +1,129 @@
|
|||
import { useState } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { ButtonSettingsBlock } from '../../../../../ReusableComponents/SettingsBlocks';
|
||||
import { useDispatch } from '@wordpress/data';
|
||||
import { store as noticesStore } from '@wordpress/notices';
|
||||
import { CommonHooks } from '../../../../../../data';
|
||||
import {
|
||||
NOTIFICATION_ERROR,
|
||||
NOTIFICATION_SUCCESS,
|
||||
} from '../../../../../ReusableComponents/Icons';
|
||||
|
||||
const SimulationBlock = () => {
|
||||
const {
|
||||
createSuccessNotice,
|
||||
createInfoNotice,
|
||||
createErrorNotice,
|
||||
removeNotice,
|
||||
} = useDispatch( noticesStore );
|
||||
const { startWebhookSimulation, checkWebhookSimulationState } =
|
||||
CommonHooks.useWebhooks();
|
||||
const [ simulating, setSimulating ] = useState( false );
|
||||
const sleep = ( ms ) => {
|
||||
return new Promise( ( resolve ) => setTimeout( resolve, ms ) );
|
||||
};
|
||||
const startSimulation = async ( maxRetries ) => {
|
||||
const webhookInfoNoticeId = 'paypal-webhook-simulation-info-notice';
|
||||
const triggerWebhookInfoNotice = () => {
|
||||
createInfoNotice(
|
||||
__(
|
||||
'Waiting for the webhook to arrive…',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
{
|
||||
id: webhookInfoNoticeId,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const stopSimulation = () => {
|
||||
removeNotice( webhookInfoNoticeId );
|
||||
setSimulating( false );
|
||||
};
|
||||
|
||||
setSimulating( true );
|
||||
|
||||
triggerWebhookInfoNotice();
|
||||
|
||||
try {
|
||||
await startWebhookSimulation();
|
||||
} catch ( error ) {
|
||||
console.error( error );
|
||||
setSimulating( false );
|
||||
createErrorNotice(
|
||||
__(
|
||||
'Operation failed. Check WooCommerce logs for more details.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
{
|
||||
icon: NOTIFICATION_ERROR,
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
for ( let i = 0; i < maxRetries; i++ ) {
|
||||
await sleep( 2000 );
|
||||
|
||||
const simulationStateResponse = await checkWebhookSimulationState();
|
||||
try {
|
||||
if ( ! simulationStateResponse.success ) {
|
||||
console.error(
|
||||
'Simulation state query failed: ' +
|
||||
simulationStateResponse?.data
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( simulationStateResponse?.data?.state === 'received' ) {
|
||||
createSuccessNotice(
|
||||
__(
|
||||
'The webhook was received successfully.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
{
|
||||
icon: NOTIFICATION_SUCCESS,
|
||||
}
|
||||
);
|
||||
stopSimulation();
|
||||
return;
|
||||
}
|
||||
removeNotice( webhookInfoNoticeId );
|
||||
triggerWebhookInfoNotice();
|
||||
} catch ( error ) {
|
||||
console.error( error );
|
||||
}
|
||||
}
|
||||
stopSimulation();
|
||||
createErrorNotice(
|
||||
__(
|
||||
'Looks like the webhook cannot be received. Check that your website is accessible from the internet.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
{
|
||||
icon: NOTIFICATION_ERROR,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ButtonSettingsBlock
|
||||
title={ __(
|
||||
'Simulate webhooks',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
actionProps={ {
|
||||
buttonType: 'secondary',
|
||||
isBusy: simulating,
|
||||
callback: () => startSimulation( 30 ),
|
||||
value: __(
|
||||
'Simulate webhooks',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
} }
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default SimulationBlock;
|
|
@ -0,0 +1,69 @@
|
|||
import { __ } from '@wordpress/i18n';
|
||||
import {
|
||||
AccordionSettingsBlock,
|
||||
Description,
|
||||
Header,
|
||||
Title,
|
||||
ToggleSettingsBlock,
|
||||
} from '../../../../../ReusableComponents/SettingsBlocks';
|
||||
import SettingsBlock from '../../../../../ReusableComponents/SettingsBlocks/SettingsBlock';
|
||||
|
||||
import SimulationBlock from './SimulationBlock';
|
||||
import ResubscribeBlock from './ResubscribeBlock';
|
||||
import HooksTableBlock from './HooksTableBlock';
|
||||
|
||||
const Troubleshooting = ( { updateFormValue, settings } ) => {
|
||||
return (
|
||||
<AccordionSettingsBlock
|
||||
className="ppcp-r-settings-block--troubleshooting"
|
||||
title={ __( 'Troubleshooting', 'woocommerce-paypal-payments' ) }
|
||||
description={ __(
|
||||
'Access tools to help debug and resolve issues.',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
actionProps={ {
|
||||
callback: updateFormValue,
|
||||
key: 'payNowExperience',
|
||||
value: settings.payNowExperience,
|
||||
} }
|
||||
>
|
||||
<ToggleSettingsBlock
|
||||
title={ __( 'Logging', 'woocommerce-paypal-payments' ) }
|
||||
description={ __(
|
||||
'Log additional debugging information in the WooCommerce logs that can assist technical staff to determine issues.',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
actionProps={ {
|
||||
callback: updateFormValue,
|
||||
key: 'logging',
|
||||
value: settings.logging,
|
||||
} }
|
||||
/>
|
||||
<SettingsBlock>
|
||||
<Header>
|
||||
<Title>
|
||||
{ __( 'Webhooks', 'woocommerce-paypal-payments' ) }
|
||||
</Title>
|
||||
<Description>
|
||||
{ __(
|
||||
'The following PayPal webhooks are subscribed. More information about the webhooks is available in the',
|
||||
'woocommerce-paypal-payments'
|
||||
) }{ ' ' }
|
||||
<a href="https://woocommerce.com/document/woocommerce-paypal-payments/#webhook-status">
|
||||
{ __(
|
||||
'Webhook Status documentation',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
</a>
|
||||
.
|
||||
</Description>
|
||||
</Header>
|
||||
<HooksTableBlock />
|
||||
<ResubscribeBlock />
|
||||
<SimulationBlock />
|
||||
</SettingsBlock>
|
||||
</AccordionSettingsBlock>
|
||||
);
|
||||
};
|
||||
|
||||
export default Troubleshooting;
|
|
@ -4,8 +4,8 @@ import {
|
|||
Content,
|
||||
ContentWrapper,
|
||||
} from '../../../ReusableComponents/SettingsBlocks';
|
||||
import Sandbox from './Blocks/Sandbox';
|
||||
import Troubleshooting from './Blocks/Troubleshooting';
|
||||
import ConnectionDetails from './Blocks/ConnectionDetails';
|
||||
import Troubleshooting from './Blocks/Troubleshooting/Troubleshooting';
|
||||
import PaypalSettings from './Blocks/PaypalSettings';
|
||||
import OtherSettings from './Blocks/OtherSettings';
|
||||
|
||||
|
@ -27,7 +27,7 @@ const ExpertSettings = ( { updateFormValue, settings } ) => {
|
|||
>
|
||||
<ContentWrapper>
|
||||
<Content>
|
||||
<Sandbox
|
||||
<ConnectionDetails
|
||||
updateFormValue={ updateFormValue }
|
||||
settings={ settings }
|
||||
/>
|
||||
|
|
|
@ -19,9 +19,13 @@ export default {
|
|||
|
||||
// Controls - always start with "DO_".
|
||||
DO_PERSIST_DATA: 'COMMON:DO_PERSIST_DATA',
|
||||
DO_MANUAL_CONNECTION: 'COMMON:DO_MANUAL_CONNECTION',
|
||||
DO_SANDBOX_LOGIN: 'COMMON:DO_SANDBOX_LOGIN',
|
||||
DO_PRODUCTION_LOGIN: 'COMMON:DO_PRODUCTION_LOGIN',
|
||||
DO_DIRECT_API_AUTHENTICATION: 'COMMON:DO_DIRECT_API_AUTHENTICATION',
|
||||
DO_OAUTH_AUTHENTICATION: 'COMMON:DO_OAUTH_AUTHENTICATION',
|
||||
DO_GENERATE_ONBOARDING_URL: 'COMMON:DO_GENERATE_ONBOARDING_URL',
|
||||
DO_REFRESH_MERCHANT: 'COMMON:DO_REFRESH_MERCHANT',
|
||||
DO_REFRESH_FEATURES: 'DO_REFRESH_FEATURES',
|
||||
DO_REFRESH_FEATURES: 'COMMON:DO_REFRESH_FEATURES',
|
||||
DO_RESUBSCRIBE_WEBHOOKS: 'COMMON:DO_RESUBSCRIBE_WEBHOOKS',
|
||||
DO_START_WEBHOOK_SIMULATION: 'COMMON:DO_START_WEBHOOK_SIMULATION',
|
||||
DO_CHECK_WEBHOOK_SIMULATION_STATE:
|
||||
'COMMON:DO_CHECK_WEBHOOK_SIMULATION_STATE',
|
||||
};
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
* @file
|
||||
*/
|
||||
|
||||
import { dispatch, select } from '@wordpress/data';
|
||||
import { select } from '@wordpress/data';
|
||||
|
||||
import ACTION_TYPES from './action-types';
|
||||
import { STORE_NAME } from './constants';
|
||||
|
@ -47,6 +47,17 @@ export const setIsReady = ( isReady ) => ( {
|
|||
payload: { isReady },
|
||||
} );
|
||||
|
||||
/**
|
||||
* Transient. Sets the active settings tab.
|
||||
*
|
||||
* @param {string} activeModal
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const setActiveModal = ( activeModal ) => ( {
|
||||
type: ACTION_TYPES.SET_TRANSIENT,
|
||||
payload: { activeModal },
|
||||
} );
|
||||
|
||||
/**
|
||||
* Transient. Changes the "saving" flag.
|
||||
*
|
||||
|
@ -112,28 +123,6 @@ export const setManualConnectionMode = ( useManualConnection ) => ( {
|
|||
payload: { useManualConnection },
|
||||
} );
|
||||
|
||||
/**
|
||||
* Persistent. Changes the "client ID" value.
|
||||
*
|
||||
* @param {string} clientId
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const setClientId = ( clientId ) => ( {
|
||||
type: ACTION_TYPES.SET_PERSISTENT,
|
||||
payload: { clientId },
|
||||
} );
|
||||
|
||||
/**
|
||||
* Persistent. Changes the "client secret" value.
|
||||
*
|
||||
* @param {string} clientSecret
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const setClientSecret = ( clientSecret ) => ( {
|
||||
type: ACTION_TYPES.SET_PERSISTENT,
|
||||
payload: { clientSecret },
|
||||
} );
|
||||
|
||||
/**
|
||||
* Side effect. Saves the persistent details to the WP database.
|
||||
*
|
||||
|
@ -150,8 +139,12 @@ export const persist = function* () {
|
|||
*
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const connectToSandbox = function* () {
|
||||
return yield { type: ACTION_TYPES.DO_SANDBOX_LOGIN };
|
||||
export const sandboxOnboardingUrl = function* () {
|
||||
return yield {
|
||||
type: ACTION_TYPES.DO_GENERATE_ONBOARDING_URL,
|
||||
useSandbox: true,
|
||||
products: [ 'EXPRESS_CHECKOUT' ],
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -160,27 +153,65 @@ export const connectToSandbox = function* () {
|
|||
* @param {string[]} products Which products/features to display in the ISU popup.
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const connectToProduction = function* ( products = [] ) {
|
||||
return yield { type: ACTION_TYPES.DO_PRODUCTION_LOGIN, products };
|
||||
export const productionOnboardingUrl = function* ( products = [] ) {
|
||||
return yield {
|
||||
type: ACTION_TYPES.DO_GENERATE_ONBOARDING_URL,
|
||||
useSandbox: false,
|
||||
products,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Side effect. Initiates a manual connection attempt using the provided client ID and secret.
|
||||
* Side effect. Initiates a direct connection attempt using the provided client ID and secret.
|
||||
*
|
||||
* This action accepts parameters instead of fetching data from the Redux state because the
|
||||
* values (ID and secret) are not managed by a central redux store, but might come from private
|
||||
* component state.
|
||||
*
|
||||
* @param {string} clientId - AP client ID (always 80-characters, starting with "A").
|
||||
* @param {string} clientSecret - API client secret.
|
||||
* @param {boolean} useSandbox - Whether the credentials are for a sandbox account.
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const connectViaIdAndSecret = function* () {
|
||||
const { clientId, clientSecret, useSandbox } =
|
||||
yield select( STORE_NAME ).persistentData();
|
||||
|
||||
export const authenticateWithCredentials = function* (
|
||||
clientId,
|
||||
clientSecret,
|
||||
useSandbox
|
||||
) {
|
||||
return yield {
|
||||
type: ACTION_TYPES.DO_MANUAL_CONNECTION,
|
||||
type: ACTION_TYPES.DO_DIRECT_API_AUTHENTICATION,
|
||||
clientId,
|
||||
clientSecret,
|
||||
useSandbox,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Side effect. Completes the ISU login by authenticating the user via the one time sharedId and
|
||||
* authCode provided by PayPal.
|
||||
*
|
||||
* This action accepts parameters instead of fetching data from the Redux state because all
|
||||
* parameters are dynamically generated during the authentication process, and not managed by our
|
||||
* Redux store.
|
||||
*
|
||||
* @param {string} sharedId - OAuth client ID; called "sharedId" to prevent confusion with the API client ID.
|
||||
* @param {string} authCode - OAuth authorization code provided during onboarding.
|
||||
* @param {boolean} useSandbox - Whether the credentials are for a sandbox account.
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const authenticateWithOAuth = function* (
|
||||
sharedId,
|
||||
authCode,
|
||||
useSandbox
|
||||
) {
|
||||
return yield {
|
||||
type: ACTION_TYPES.DO_OAUTH_AUTHENTICATION,
|
||||
sharedId,
|
||||
authCode,
|
||||
useSandbox,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Side effect. Clears and refreshes the merchant data via a REST request.
|
||||
*
|
||||
|
@ -214,3 +245,48 @@ export const refreshFeatureStatuses = function* () {
|
|||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Persistent. Changes the "webhooks" value.
|
||||
*
|
||||
* @param {string} webhooks
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const setWebhooks = ( webhooks ) => ( {
|
||||
type: ACTION_TYPES.SET_PERSISTENT,
|
||||
payload: { webhooks },
|
||||
} );
|
||||
|
||||
/**
|
||||
* Side effect
|
||||
* Refreshes subscribed webhooks via a REST request
|
||||
*
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const resubscribeWebhooks = function* () {
|
||||
const result = yield { type: ACTION_TYPES.DO_RESUBSCRIBE_WEBHOOKS };
|
||||
|
||||
if ( result && result.success ) {
|
||||
yield hydrate( result );
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Side effect. Starts webhook simulation.
|
||||
*
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const startWebhookSimulation = function* () {
|
||||
return yield { type: ACTION_TYPES.DO_START_WEBHOOK_SIMULATION };
|
||||
};
|
||||
|
||||
/**
|
||||
* Side effect. Checks webhook simulation.
|
||||
*
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const checkWebhookSimulationState = function* () {
|
||||
return yield { type: ACTION_TYPES.DO_CHECK_WEBHOOK_SIMULATION_STATE };
|
||||
};
|
||||
|
|
|
@ -35,14 +35,25 @@ export const REST_HYDRATE_MERCHANT_PATH = '/wc/v3/wc_paypal/common/merchant';
|
|||
export const REST_PERSIST_PATH = '/wc/v3/wc_paypal/common';
|
||||
|
||||
/**
|
||||
* REST path to perform the manual connection check, using client ID and secret,
|
||||
* REST path to perform the manual connection authentication, using client ID and secret.
|
||||
*
|
||||
* Used by: Controls
|
||||
* See: ConnectManualRestEndpoint.php
|
||||
* See: AuthenticateRestEndpoint.php
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const REST_MANUAL_CONNECTION_PATH = '/wc/v3/wc_paypal/connect_manual';
|
||||
export const REST_DIRECT_AUTHENTICATION_PATH =
|
||||
'/wc/v3/wc_paypal/authenticate/direct';
|
||||
|
||||
/**
|
||||
* REST path to perform the ISU authentication check, using shared ID and authCode.
|
||||
*
|
||||
* Used by: Controls
|
||||
* See: AuthenticateRestEndpoint.php
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const REST_ISU_AUTHENTICATION_PATH = '/wc/v3/wc_paypal/authenticate/isu';
|
||||
|
||||
/**
|
||||
* REST path to generate an ISU URL for the PayPal-login.
|
||||
|
@ -54,6 +65,26 @@ export const REST_MANUAL_CONNECTION_PATH = '/wc/v3/wc_paypal/connect_manual';
|
|||
*/
|
||||
export const REST_CONNECTION_URL_PATH = '/wc/v3/wc_paypal/login_link';
|
||||
|
||||
/**
|
||||
* REST path to fetch webhooks data or resubscribe webhooks,
|
||||
*
|
||||
* Used by: Controls
|
||||
* See: WebhookSettingsEndpoint.php
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const REST_WEBHOOKS = '/wc/v3/wc_paypal/webhook_settings';
|
||||
|
||||
/**
|
||||
* REST path to start webhook simulation and observe the state,
|
||||
*
|
||||
* Used by: Controls
|
||||
* See: WebhookSettingsEndpoint.php
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const REST_WEBHOOKS_SIMULATE = '/wc/v3/wc_paypal/webhook_simulate';
|
||||
|
||||
/**
|
||||
* REST path to refresh the feature status.
|
||||
*
|
||||
|
|
|
@ -11,10 +11,13 @@ import apiFetch from '@wordpress/api-fetch';
|
|||
|
||||
import {
|
||||
REST_PERSIST_PATH,
|
||||
REST_MANUAL_CONNECTION_PATH,
|
||||
REST_DIRECT_AUTHENTICATION_PATH,
|
||||
REST_CONNECTION_URL_PATH,
|
||||
REST_HYDRATE_MERCHANT_PATH,
|
||||
REST_REFRESH_FEATURES_PATH,
|
||||
REST_ISU_AUTHENTICATION_PATH,
|
||||
REST_WEBHOOKS,
|
||||
REST_WEBHOOKS_SIMULATE,
|
||||
} from './constants';
|
||||
import ACTION_TYPES from './action-types';
|
||||
|
||||
|
@ -31,15 +34,15 @@ export const controls = {
|
|||
}
|
||||
},
|
||||
|
||||
async [ ACTION_TYPES.DO_SANDBOX_LOGIN ]() {
|
||||
async [ ACTION_TYPES.DO_GENERATE_ONBOARDING_URL ]( {
|
||||
products,
|
||||
useSandbox,
|
||||
} ) {
|
||||
try {
|
||||
return apiFetch( {
|
||||
path: REST_CONNECTION_URL_PATH,
|
||||
method: 'POST',
|
||||
data: {
|
||||
environment: 'sandbox',
|
||||
products: [ 'EXPRESS_CHECKOUT' ], // Sandbox always uses EXPRESS_CHECKOUT.
|
||||
},
|
||||
data: { useSandbox, products },
|
||||
} );
|
||||
} catch ( e ) {
|
||||
return {
|
||||
|
@ -49,32 +52,14 @@ export const controls = {
|
|||
}
|
||||
},
|
||||
|
||||
async [ ACTION_TYPES.DO_PRODUCTION_LOGIN ]( { products } ) {
|
||||
try {
|
||||
return apiFetch( {
|
||||
path: REST_CONNECTION_URL_PATH,
|
||||
method: 'POST',
|
||||
data: {
|
||||
environment: 'production',
|
||||
products,
|
||||
},
|
||||
} );
|
||||
} catch ( e ) {
|
||||
return {
|
||||
success: false,
|
||||
error: e,
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
async [ ACTION_TYPES.DO_MANUAL_CONNECTION ]( {
|
||||
async [ ACTION_TYPES.DO_DIRECT_API_AUTHENTICATION ]( {
|
||||
clientId,
|
||||
clientSecret,
|
||||
useSandbox,
|
||||
} ) {
|
||||
try {
|
||||
return await apiFetch( {
|
||||
path: REST_MANUAL_CONNECTION_PATH,
|
||||
path: REST_DIRECT_AUTHENTICATION_PATH,
|
||||
method: 'POST',
|
||||
data: {
|
||||
clientId,
|
||||
|
@ -90,6 +75,29 @@ export const controls = {
|
|||
}
|
||||
},
|
||||
|
||||
async [ ACTION_TYPES.DO_OAUTH_AUTHENTICATION ]( {
|
||||
sharedId,
|
||||
authCode,
|
||||
useSandbox,
|
||||
} ) {
|
||||
try {
|
||||
return await apiFetch( {
|
||||
path: REST_ISU_AUTHENTICATION_PATH,
|
||||
method: 'POST',
|
||||
data: {
|
||||
sharedId,
|
||||
authCode,
|
||||
useSandbox,
|
||||
},
|
||||
} );
|
||||
} catch ( e ) {
|
||||
return {
|
||||
success: false,
|
||||
error: e,
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
async [ ACTION_TYPES.DO_REFRESH_MERCHANT ]() {
|
||||
try {
|
||||
return await apiFetch( { path: REST_HYDRATE_MERCHANT_PATH } );
|
||||
|
@ -115,4 +123,24 @@ export const controls = {
|
|||
};
|
||||
}
|
||||
},
|
||||
|
||||
async [ ACTION_TYPES.DO_RESUBSCRIBE_WEBHOOKS ]() {
|
||||
return await apiFetch( {
|
||||
method: 'POST',
|
||||
path: REST_WEBHOOKS,
|
||||
} );
|
||||
},
|
||||
|
||||
async [ ACTION_TYPES.DO_START_WEBHOOK_SIMULATION ]() {
|
||||
return await apiFetch( {
|
||||
method: 'POST',
|
||||
path: REST_WEBHOOKS_SIMULATE,
|
||||
} );
|
||||
},
|
||||
|
||||
async [ ACTION_TYPES.DO_CHECK_WEBHOOK_SIMULATION_STATE ]() {
|
||||
return await apiFetch( {
|
||||
path: REST_WEBHOOKS_SIMULATE,
|
||||
} );
|
||||
},
|
||||
};
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { useCallback } from '@wordpress/element';
|
||||
|
||||
import { STORE_NAME } from './constants';
|
||||
|
||||
const useTransient = ( key ) =>
|
||||
|
@ -29,30 +28,40 @@ const useHooks = () => {
|
|||
persist,
|
||||
setSandboxMode,
|
||||
setManualConnectionMode,
|
||||
setClientId,
|
||||
setClientSecret,
|
||||
connectToSandbox,
|
||||
connectToProduction,
|
||||
connectViaIdAndSecret,
|
||||
sandboxOnboardingUrl,
|
||||
productionOnboardingUrl,
|
||||
authenticateWithCredentials,
|
||||
authenticateWithOAuth,
|
||||
setActiveModal,
|
||||
startWebhookSimulation,
|
||||
checkWebhookSimulationState,
|
||||
} = useDispatch( STORE_NAME );
|
||||
|
||||
// Transient accessors.
|
||||
const isReady = useTransient( 'isReady' );
|
||||
const activeModal = useTransient( 'activeModal' );
|
||||
|
||||
// Persistent accessors.
|
||||
const clientId = usePersistent( 'clientId' );
|
||||
const clientSecret = usePersistent( 'clientSecret' );
|
||||
const isSandboxMode = usePersistent( 'useSandbox' );
|
||||
const isManualConnectionMode = usePersistent( 'useManualConnection' );
|
||||
|
||||
const merchant = useSelect(
|
||||
( select ) => select( STORE_NAME ).merchant(),
|
||||
[]
|
||||
);
|
||||
|
||||
// Read-only properties.
|
||||
const wooSettings = useSelect(
|
||||
( select ) => select( STORE_NAME ).wooSettings(),
|
||||
[]
|
||||
);
|
||||
const features = useSelect(
|
||||
( select ) => select( STORE_NAME ).features(),
|
||||
[]
|
||||
);
|
||||
const webhooks = useSelect(
|
||||
( select ) => select( STORE_NAME ).webhooks(),
|
||||
[]
|
||||
);
|
||||
|
||||
const savePersistent = async ( setter, value ) => {
|
||||
setter( value );
|
||||
|
@ -61,6 +70,8 @@ const useHooks = () => {
|
|||
|
||||
return {
|
||||
isReady,
|
||||
activeModal,
|
||||
setActiveModal,
|
||||
isSandboxMode,
|
||||
setSandboxMode: ( state ) => {
|
||||
return savePersistent( setSandboxMode, state );
|
||||
|
@ -69,53 +80,44 @@ const useHooks = () => {
|
|||
setManualConnectionMode: ( state ) => {
|
||||
return savePersistent( setManualConnectionMode, state );
|
||||
},
|
||||
clientId,
|
||||
setClientId: ( value ) => {
|
||||
return savePersistent( setClientId, value );
|
||||
},
|
||||
clientSecret,
|
||||
setClientSecret: ( value ) => {
|
||||
return savePersistent( setClientSecret, value );
|
||||
},
|
||||
connectToSandbox,
|
||||
connectToProduction,
|
||||
connectViaIdAndSecret,
|
||||
sandboxOnboardingUrl,
|
||||
productionOnboardingUrl,
|
||||
authenticateWithCredentials,
|
||||
authenticateWithOAuth,
|
||||
merchant,
|
||||
wooSettings,
|
||||
features,
|
||||
webhooks,
|
||||
startWebhookSimulation,
|
||||
checkWebhookSimulationState,
|
||||
};
|
||||
};
|
||||
|
||||
export const useSandbox = () => {
|
||||
const { isSandboxMode, setSandboxMode, connectToSandbox } = useHooks();
|
||||
const { isSandboxMode, setSandboxMode, sandboxOnboardingUrl } = useHooks();
|
||||
|
||||
return { isSandboxMode, setSandboxMode, connectToSandbox };
|
||||
return { isSandboxMode, setSandboxMode, sandboxOnboardingUrl };
|
||||
};
|
||||
|
||||
export const useProduction = () => {
|
||||
const { connectToProduction } = useHooks();
|
||||
const { productionOnboardingUrl } = useHooks();
|
||||
|
||||
return { connectToProduction };
|
||||
return { productionOnboardingUrl };
|
||||
};
|
||||
|
||||
export const useManualConnection = () => {
|
||||
export const useAuthentication = () => {
|
||||
const {
|
||||
isManualConnectionMode,
|
||||
setManualConnectionMode,
|
||||
clientId,
|
||||
setClientId,
|
||||
clientSecret,
|
||||
setClientSecret,
|
||||
connectViaIdAndSecret,
|
||||
authenticateWithCredentials,
|
||||
authenticateWithOAuth,
|
||||
} = useHooks();
|
||||
|
||||
return {
|
||||
isManualConnectionMode,
|
||||
setManualConnectionMode,
|
||||
clientId,
|
||||
setClientId,
|
||||
clientSecret,
|
||||
setClientSecret,
|
||||
connectViaIdAndSecret,
|
||||
authenticateWithCredentials,
|
||||
authenticateWithOAuth,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -125,8 +127,24 @@ export const useWooSettings = () => {
|
|||
return wooSettings;
|
||||
};
|
||||
|
||||
export const useWebhooks = () => {
|
||||
const {
|
||||
webhooks,
|
||||
setWebhooks,
|
||||
registerWebhooks,
|
||||
startWebhookSimulation,
|
||||
checkWebhookSimulationState,
|
||||
} = useHooks();
|
||||
return {
|
||||
webhooks,
|
||||
setWebhooks,
|
||||
registerWebhooks,
|
||||
startWebhookSimulation,
|
||||
checkWebhookSimulationState,
|
||||
};
|
||||
};
|
||||
export const useMerchantInfo = () => {
|
||||
const { merchant } = useHooks();
|
||||
const { merchant, features } = useHooks();
|
||||
const { refreshMerchantData } = useDispatch( STORE_NAME );
|
||||
|
||||
const verifyLoginStatus = useCallback( async () => {
|
||||
|
@ -142,10 +160,16 @@ export const useMerchantInfo = () => {
|
|||
|
||||
return {
|
||||
merchant, // Merchant details
|
||||
features, // Eligible merchant features
|
||||
verifyLoginStatus, // Callback
|
||||
};
|
||||
};
|
||||
|
||||
export const useActiveModal = () => {
|
||||
const { activeModal, setActiveModal } = useHooks();
|
||||
return { activeModal, setActiveModal };
|
||||
};
|
||||
|
||||
// -- Not using the `useHooks()` data provider --
|
||||
|
||||
export const useBusyState = () => {
|
||||
|
|
|
@ -15,6 +15,7 @@ import ACTION_TYPES from './action-types';
|
|||
const defaultTransient = Object.freeze( {
|
||||
isReady: false,
|
||||
activities: new Map(),
|
||||
activeModal: '',
|
||||
|
||||
// Read only values, provided by the server via hydrate.
|
||||
merchant: Object.freeze( {
|
||||
|
@ -22,19 +23,36 @@ const defaultTransient = Object.freeze( {
|
|||
isSandbox: false,
|
||||
id: '',
|
||||
email: '',
|
||||
clientId: '',
|
||||
clientSecret: '',
|
||||
} ),
|
||||
|
||||
wooSettings: Object.freeze( {
|
||||
storeCountry: '',
|
||||
storeCurrency: '',
|
||||
} ),
|
||||
|
||||
features: Object.freeze( {
|
||||
save_paypal_and_venmo: {
|
||||
enabled: false,
|
||||
},
|
||||
advanced_credit_and_debit_cards: {
|
||||
enabled: false,
|
||||
},
|
||||
apple_pay: {
|
||||
enabled: false,
|
||||
},
|
||||
google_pay: {
|
||||
enabled: false,
|
||||
},
|
||||
} ),
|
||||
|
||||
webhooks: Object.freeze( [] ),
|
||||
} );
|
||||
|
||||
const defaultPersistent = Object.freeze( {
|
||||
useSandbox: false,
|
||||
useManualConnection: false,
|
||||
clientId: '',
|
||||
clientSecret: '',
|
||||
} );
|
||||
|
||||
// Reducer logic.
|
||||
|
@ -82,22 +100,25 @@ const commonReducer = createReducer( defaultTransient, defaultPersistent, {
|
|||
[ ACTION_TYPES.DO_REFRESH_MERCHANT ]: ( state ) => ( {
|
||||
...state,
|
||||
merchant: Object.freeze( { ...defaultTransient.merchant } ),
|
||||
features: Object.freeze( { ...defaultTransient.features } ),
|
||||
} ),
|
||||
|
||||
[ ACTION_TYPES.HYDRATE ]: ( state, payload ) => {
|
||||
const newState = setPersistent( state, payload.data );
|
||||
|
||||
// Populate read-only properties.
|
||||
[ 'wooSettings', 'merchant' ].forEach( ( key ) => {
|
||||
if ( ! payload[ key ] ) {
|
||||
return;
|
||||
}
|
||||
[ 'wooSettings', 'merchant', 'features', 'webhooks' ].forEach(
|
||||
( key ) => {
|
||||
if ( ! payload[ key ] ) {
|
||||
return;
|
||||
}
|
||||
|
||||
newState[ key ] = Object.freeze( {
|
||||
...newState[ key ],
|
||||
...payload[ key ],
|
||||
} );
|
||||
} );
|
||||
newState[ key ] = Object.freeze( {
|
||||
...newState[ key ],
|
||||
...payload[ key ],
|
||||
} );
|
||||
}
|
||||
);
|
||||
|
||||
return newState;
|
||||
},
|
||||
|
|
|
@ -12,7 +12,7 @@ import { dispatch } from '@wordpress/data';
|
|||
import { __ } from '@wordpress/i18n';
|
||||
import { apiFetch } from '@wordpress/data-controls';
|
||||
|
||||
import { STORE_NAME, REST_HYDRATE_PATH } from './constants';
|
||||
import { STORE_NAME, REST_HYDRATE_PATH, REST_WEBHOOKS } from './constants';
|
||||
|
||||
export const resolvers = {
|
||||
/**
|
||||
|
@ -21,6 +21,11 @@ export const resolvers = {
|
|||
*persistentData() {
|
||||
try {
|
||||
const result = yield apiFetch( { path: REST_HYDRATE_PATH } );
|
||||
const webhooks = yield apiFetch( { path: REST_WEBHOOKS } );
|
||||
|
||||
if ( webhooks.success && webhooks.data ) {
|
||||
result.webhooks = webhooks.data;
|
||||
}
|
||||
|
||||
yield dispatch( STORE_NAME ).hydrate( result );
|
||||
yield dispatch( STORE_NAME ).setIsReady( true );
|
||||
|
|
|
@ -16,8 +16,14 @@ export const persistentData = ( state ) => {
|
|||
};
|
||||
|
||||
export const transientData = ( state ) => {
|
||||
const { data, merchant, wooSettings, ...transientState } =
|
||||
getState( state );
|
||||
const {
|
||||
data,
|
||||
merchant,
|
||||
features,
|
||||
wooSettings,
|
||||
webhooks,
|
||||
...transientState
|
||||
} = getState( state );
|
||||
return transientState || EMPTY_OBJ;
|
||||
};
|
||||
|
||||
|
@ -30,6 +36,14 @@ export const merchant = ( state ) => {
|
|||
return getState( state ).merchant || EMPTY_OBJ;
|
||||
};
|
||||
|
||||
export const features = ( state ) => {
|
||||
return getState( state ).features || EMPTY_OBJ;
|
||||
};
|
||||
|
||||
export const wooSettings = ( state ) => {
|
||||
return getState( state ).wooSettings || EMPTY_OBJ;
|
||||
};
|
||||
|
||||
export const webhooks = ( state ) => {
|
||||
return getState( state ).webhooks || EMPTY_OBJ;
|
||||
};
|
||||
|
|
|
@ -47,6 +47,28 @@ export const setIsReady = ( isReady ) => ( {
|
|||
payload: { isReady },
|
||||
} );
|
||||
|
||||
/**
|
||||
* Transient. Sets the "manualClientId" value.
|
||||
*
|
||||
* @param {string} manualClientId
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const setManualClientId = ( manualClientId ) => ( {
|
||||
type: ACTION_TYPES.SET_TRANSIENT,
|
||||
payload: { manualClientId },
|
||||
} );
|
||||
|
||||
/**
|
||||
* Transient. Sets the "manualClientSecret" value.
|
||||
*
|
||||
* @param {string} manualClientSecret
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const setManualClientSecret = ( manualClientSecret ) => ( {
|
||||
type: ACTION_TYPES.SET_TRANSIENT,
|
||||
payload: { manualClientSecret },
|
||||
} );
|
||||
|
||||
/**
|
||||
* Persistent.Set the "onboarding completed" flag which shows or hides the wizard.
|
||||
*
|
||||
|
|
|
@ -30,6 +30,8 @@ const useHooks = () => {
|
|||
setStep,
|
||||
setCompleted,
|
||||
setIsCasualSeller,
|
||||
setManualClientId,
|
||||
setManualClientSecret,
|
||||
setAreOptionalPaymentMethodsEnabled,
|
||||
setProducts,
|
||||
} = useDispatch( STORE_NAME );
|
||||
|
@ -43,6 +45,8 @@ const useHooks = () => {
|
|||
|
||||
// Transient accessors.
|
||||
const isReady = useTransient( 'isReady' );
|
||||
const manualClientId = useTransient( 'manualClientId' );
|
||||
const manualClientSecret = useTransient( 'manualClientSecret' );
|
||||
|
||||
// Persistent accessors.
|
||||
const step = usePersistent( 'step' );
|
||||
|
@ -73,6 +77,14 @@ const useHooks = () => {
|
|||
setIsCasualSeller: ( value ) => {
|
||||
return savePersistent( setIsCasualSeller, value );
|
||||
},
|
||||
manualClientId,
|
||||
setManualClientId: ( value ) => {
|
||||
return savePersistent( setManualClientId, value );
|
||||
},
|
||||
manualClientSecret,
|
||||
setManualClientSecret: ( value ) => {
|
||||
return savePersistent( setManualClientSecret, value );
|
||||
},
|
||||
areOptionalPaymentMethodsEnabled,
|
||||
setAreOptionalPaymentMethodsEnabled: ( value ) => {
|
||||
return savePersistent( setAreOptionalPaymentMethodsEnabled, value );
|
||||
|
@ -88,6 +100,22 @@ const useHooks = () => {
|
|||
};
|
||||
};
|
||||
|
||||
export const useManualConnectionForm = () => {
|
||||
const {
|
||||
manualClientId,
|
||||
setManualClientId,
|
||||
manualClientSecret,
|
||||
setManualClientSecret,
|
||||
} = useHooks();
|
||||
|
||||
return {
|
||||
manualClientId,
|
||||
setManualClientId,
|
||||
manualClientSecret,
|
||||
setManualClientSecret,
|
||||
};
|
||||
};
|
||||
|
||||
export const useBusiness = () => {
|
||||
const { isCasualSeller, setIsCasualSeller } = useHooks();
|
||||
|
||||
|
|
|
@ -14,6 +14,8 @@ import ACTION_TYPES from './action-types';
|
|||
|
||||
const defaultTransient = Object.freeze( {
|
||||
isReady: false,
|
||||
manualClientId: '',
|
||||
manualClientSecret: '',
|
||||
|
||||
// Read only values, provided by the server.
|
||||
flags: Object.freeze( {
|
||||
|
|
|
@ -52,9 +52,6 @@ export const determineProducts = ( state ) => {
|
|||
* The store uses the Express-checkout product.
|
||||
*/
|
||||
derivedProducts.push( 'EXPRESS_CHECKOUT' );
|
||||
|
||||
// TODO: Add the "BCDC" product/feature
|
||||
// Requirement: "EXPRESS_CHECKOUT with BCDC"
|
||||
} else {
|
||||
/**
|
||||
* Branch 3: Merchant is business, and can use CC payments.
|
||||
|
@ -64,8 +61,7 @@ export const determineProducts = ( state ) => {
|
|||
}
|
||||
|
||||
if ( canUseVaulting ) {
|
||||
// TODO: Add the "Vaulting" product/feature
|
||||
// Requirement: "... with Vault"
|
||||
derivedProducts.push( 'ADVANCED_VAULTING' );
|
||||
}
|
||||
|
||||
return derivedProducts;
|
||||
|
|
|
@ -0,0 +1,178 @@
|
|||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { InputSettingsBlock } from '../../Components/ReusableComponents/SettingsBlocks';
|
||||
import { Button } from '@wordpress/components';
|
||||
|
||||
/**
|
||||
* Generates options for the environment mode settings.
|
||||
*
|
||||
* @param {Object} config - Configuration for the mode.
|
||||
* @param {Object} settings - Current settings.
|
||||
* @param {Function} updateFormValue - Callback to update settings.
|
||||
* @return {Array} Options array.
|
||||
*/
|
||||
const generateOptions = ( config, settings, updateFormValue ) => [
|
||||
{
|
||||
id: `${ config.mode }_mode`,
|
||||
value: `${ config.mode }_mode`,
|
||||
label: config.labelTitle,
|
||||
description: config.labelDescription,
|
||||
additionalContent: (
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={ () => {
|
||||
updateFormValue( `${ config.mode }Connected`, true );
|
||||
if ( config.mode === 'production' ) {
|
||||
global.ppcpSettings.startOnboarding();
|
||||
}
|
||||
} }
|
||||
>
|
||||
{ config.buttonText }
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'manual_connect',
|
||||
value: 'manual_connect',
|
||||
label: __( 'Manual Connect', 'woocommerce-paypal-payments' ),
|
||||
description: sprintf(
|
||||
__(
|
||||
'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'
|
||||
),
|
||||
'#'
|
||||
),
|
||||
additionalContent: (
|
||||
<>
|
||||
<InputSettingsBlock
|
||||
title={ config.clientIdTitle }
|
||||
actionProps={ {
|
||||
value: settings[ `${ config.mode }ClientId` ],
|
||||
callback: updateFormValue,
|
||||
key: `${ config.mode }ClientId`,
|
||||
placeholder: __(
|
||||
'Enter Client ID',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
} }
|
||||
/>
|
||||
<InputSettingsBlock
|
||||
title={ config.secretKeyTitle }
|
||||
actionProps={ {
|
||||
value: settings[ `${ config.mode }SecretKey` ],
|
||||
callback: updateFormValue,
|
||||
key: `${ config.mode }SecretKey`,
|
||||
placeholder: __(
|
||||
'Enter Secret Key',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
} }
|
||||
/>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={ () =>
|
||||
updateFormValue(
|
||||
`${ config.mode }ManuallyConnected`,
|
||||
true
|
||||
)
|
||||
}
|
||||
>
|
||||
{ __( 'Connect Account', 'woocommerce-paypal-payments' ) }
|
||||
</Button>
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Generates data for a given mode (sandbox or production).
|
||||
*
|
||||
* @param {Object} config - Configuration for the mode.
|
||||
* @param {Object} settings - Current settings.
|
||||
* @param {Function} updateFormValue - Callback to update settings.
|
||||
* @return {Object} Mode configuration.
|
||||
*/
|
||||
const generateModeData = ( config, settings, updateFormValue ) => ( {
|
||||
title: config.title,
|
||||
description: config.description,
|
||||
connectTitle: __(
|
||||
`Connect ${ config.label } Account`,
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
connectDescription: config.connectDescription,
|
||||
options: generateOptions( config, settings, updateFormValue ),
|
||||
} );
|
||||
|
||||
export const sandboxData = ( { settings = {}, updateFormValue = () => {} } ) =>
|
||||
generateModeData(
|
||||
{
|
||||
mode: 'sandbox',
|
||||
label: 'Sandbox',
|
||||
title: __( 'Sandbox', 'woocommerce-paypal-payments' ),
|
||||
description: __(
|
||||
"Test your site in PayPal's Sandbox environment.",
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
connectDescription: __(
|
||||
'Connect a PayPal Sandbox account in order to test your website. Transactions made will not result in actual money movement. Do not fulfil orders completed in Sandbox mode.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
labelTitle: __( 'Sandbox Mode', 'woocommerce-paypal-payments' ),
|
||||
labelDescription: __(
|
||||
'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'
|
||||
),
|
||||
buttonText: __(
|
||||
'Connect Sandbox Account',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
clientIdTitle: __(
|
||||
'Sandbox Client ID',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
secretKeyTitle: __(
|
||||
'Sandbox Secret Key',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
},
|
||||
settings,
|
||||
updateFormValue
|
||||
);
|
||||
|
||||
export const productionData = ( {
|
||||
settings = {},
|
||||
updateFormValue = () => {},
|
||||
} ) =>
|
||||
generateModeData(
|
||||
{
|
||||
mode: 'production',
|
||||
label: 'Live',
|
||||
title: __( 'Live Payments', 'woocommerce-paypal-payments' ),
|
||||
description: __(
|
||||
'Your site is currently configured in Sandbox mode to test payments. When you are ready, launch your site and receive live payments via PayPal.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
connectDescription: __(
|
||||
'Connect a live PayPal account to launch your site and receive live payments via PayPal. PayPal will guide you through the setup process.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
labelTitle: __( 'Production Mode', 'woocommerce-paypal-payments' ),
|
||||
labelDescription: __(
|
||||
'Activate Production mode to connect your live account and receive live payments via PayPal. Stay connected in Sandbox mode to continue testing payments before going live.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
buttonText: __(
|
||||
'Set up and connect live PayPal Account',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
clientIdTitle: __(
|
||||
'Live Account Client ID',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
secretKeyTitle: __(
|
||||
'Live Account Secret Key',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
},
|
||||
settings,
|
||||
updateFormValue
|
||||
);
|
|
@ -0,0 +1,170 @@
|
|||
import { __ } from '@wordpress/i18n';
|
||||
import { selectTab, TAB_IDS } from '../../utils/tabSelector';
|
||||
|
||||
export const todosData = [
|
||||
{
|
||||
id: 'enable_fastlane',
|
||||
title: __( 'Enable Fastlane', 'woocommerce-paypal-payments' ),
|
||||
description: __(
|
||||
'Accelerate your guest checkout with Fastlane by PayPal.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
isCompleted: () => {
|
||||
return false;
|
||||
},
|
||||
onClick: () => {
|
||||
selectTab( TAB_IDS.PAYMENT_METHODS, 'ppcp-card-payments-card' );
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'enable_credit_debit_cards',
|
||||
title: __(
|
||||
'Enable Credit and Debit Cards on your checkout',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
description: __(
|
||||
'Credit and Debit Cards is now available for Blocks checkout pages.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
isCompleted: () => {
|
||||
return false;
|
||||
},
|
||||
onClick: () => {
|
||||
selectTab( TAB_IDS.PAYMENT_METHODS, 'ppcp-card-payments-card' );
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'enable_pay_later_messaging',
|
||||
title: __(
|
||||
'Enable Pay Later messaging',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
description: __(
|
||||
'Show Pay Later messaging to boost conversion rate and increase cart size.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
isCompleted: () => {
|
||||
return false;
|
||||
},
|
||||
onClick: () => {
|
||||
selectTab( TAB_IDS.OVERVIEW, 'pay_later_messaging' );
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'configure_paypal_subscription',
|
||||
title: __(
|
||||
'Configure a PayPal Subscription',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
description: __(
|
||||
'Connect a subscriptions-type product from WooCommerce with PayPal.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
isCompleted: () => {
|
||||
return false;
|
||||
},
|
||||
onClick: () => {
|
||||
console.log(
|
||||
'Take merchant to product list, filtered with subscription-type products'
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'register_domain_apple_pay',
|
||||
title: __(
|
||||
'Register Domain for Apple Pay',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
description: __(
|
||||
'To enable Apple Pay, you must register your domain with PayPal.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
isCompleted: () => {
|
||||
return false;
|
||||
},
|
||||
onClick: () => {
|
||||
selectTab( TAB_IDS.OVERVIEW, 'apple_pay' );
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'add_digital_wallets_to_account',
|
||||
title: __(
|
||||
'Add digital wallets to your account',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
description: __(
|
||||
'Add the ability to accept Apple Pay & Google Pay to your PayPal account.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
isCompleted: () => {
|
||||
return false;
|
||||
},
|
||||
onClick: () => {
|
||||
console.log(
|
||||
'Take merchant to PayPal to enable Apple Pay & Google Pay'
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'add_apple_pay_to_account',
|
||||
title: __(
|
||||
'Add Apple Pay to your account',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
description: __(
|
||||
'Add the ability to accept Apple Pay to your PayPal account.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
isCompleted: () => {
|
||||
return false;
|
||||
},
|
||||
onClick: () => {
|
||||
console.log( 'Take merchant to PayPal to enable Apple Pay' );
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'add_google_pay_to_account',
|
||||
title: __(
|
||||
'Add Google Pay to your account',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
description: __(
|
||||
'Add the ability to accept Google Pay to your PayPal account.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
isCompleted: () => {
|
||||
return false;
|
||||
},
|
||||
onClick: () => {
|
||||
console.log( 'Take merchant to PayPal to enable Google Pay' );
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'enable_apple_pay',
|
||||
title: __( 'Enable Apple Pay', 'woocommerce-paypal-payments' ),
|
||||
description: __(
|
||||
'Allow your buyers to check out via Apple Pay.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
isCompleted: () => {
|
||||
return false;
|
||||
},
|
||||
onClick: () => {
|
||||
selectTab( TAB_IDS.OVERVIEW, 'apple_pay' );
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'enable_google_pay',
|
||||
title: __( 'Enable Google Pay', 'woocommerce-paypal-payments' ),
|
||||
description: __(
|
||||
'Allow your buyers to check out via Google Pay.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
isCompleted: () => {
|
||||
return false;
|
||||
},
|
||||
onClick: () => {
|
||||
selectTab( TAB_IDS.OVERVIEW, 'google_pay' );
|
||||
},
|
||||
},
|
||||
];
|
|
@ -1,9 +1,12 @@
|
|||
import { __ } from '@wordpress/i18n';
|
||||
import { useDispatch } from '@wordpress/data';
|
||||
import { useState, useEffect, useCallback, useRef } from '@wordpress/element';
|
||||
import { store as noticesStore } from '@wordpress/notices';
|
||||
|
||||
import { CommonHooks, OnboardingHooks } from '../data';
|
||||
import { openPopup } from '../utils/window';
|
||||
|
||||
const PAYPAL_PARTNER_SDK_URL =
|
||||
'https://www.paypal.com/webapps/merchantboarding/js/lib/lightbox/partner.js';
|
||||
|
||||
const MESSAGES = {
|
||||
CONNECTED: __( 'Connected to PayPal', 'woocommerce-paypal-payments' ),
|
||||
|
@ -32,35 +35,137 @@ const MESSAGES = {
|
|||
const ACTIVITIES = {
|
||||
CONNECT_SANDBOX: 'ISU_LOGIN_SANDBOX',
|
||||
CONNECT_PRODUCTION: 'ISU_LOGIN_PRODUCTION',
|
||||
CONNECT_ISU: 'ISU_LOGIN',
|
||||
CONNECT_MANUAL: 'MANUAL_LOGIN',
|
||||
};
|
||||
|
||||
const handlePopupWithCompletion = ( url, onError ) => {
|
||||
return new Promise( ( resolve ) => {
|
||||
const popup = openPopup( url );
|
||||
export const useHandleOnboardingButton = ( isSandbox ) => {
|
||||
const { sandboxOnboardingUrl } = CommonHooks.useSandbox();
|
||||
const { productionOnboardingUrl } = CommonHooks.useProduction();
|
||||
const products = OnboardingHooks.useDetermineProducts();
|
||||
const { withActivity } = CommonHooks.useBusyState();
|
||||
const { authenticateWithOAuth } = CommonHooks.useAuthentication();
|
||||
const [ onboardingUrl, setOnboardingUrl ] = useState( '' );
|
||||
const [ scriptLoaded, setScriptLoaded ] = useState( false );
|
||||
const timerRef = useRef( null );
|
||||
|
||||
if ( ! popup ) {
|
||||
onError( MESSAGES.POPUP_BLOCKED );
|
||||
resolve( false );
|
||||
useEffect( () => {
|
||||
const fetchOnboardingUrl = async () => {
|
||||
let res;
|
||||
if ( isSandbox ) {
|
||||
res = await sandboxOnboardingUrl();
|
||||
} else {
|
||||
res = await productionOnboardingUrl( products );
|
||||
}
|
||||
|
||||
if ( res.success && res.data ) {
|
||||
setOnboardingUrl( res.data );
|
||||
} else {
|
||||
console.error( 'Failed to fetch onboarding URL' );
|
||||
}
|
||||
};
|
||||
|
||||
fetchOnboardingUrl();
|
||||
}, [ isSandbox, productionOnboardingUrl, products, sandboxOnboardingUrl ] );
|
||||
|
||||
useEffect( () => {
|
||||
/**
|
||||
* The partner.js script initializes all onboarding buttons in the onload event.
|
||||
* When no buttons are present, a JS error is displayed; i.e. we should load this script
|
||||
* only when the button is ready (with a valid href and data-attributes).
|
||||
*/
|
||||
if ( ! onboardingUrl ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check popup state every 500ms
|
||||
const checkPopup = setInterval( () => {
|
||||
if ( popup.closed ) {
|
||||
clearInterval( checkPopup );
|
||||
resolve( true );
|
||||
}
|
||||
}, 500 );
|
||||
const script = document.createElement( 'script' );
|
||||
script.id = 'partner-js';
|
||||
script.src = PAYPAL_PARTNER_SDK_URL;
|
||||
script.onload = () => {
|
||||
setScriptLoaded( true );
|
||||
};
|
||||
document.body.appendChild( script );
|
||||
|
||||
return () => {
|
||||
clearInterval( checkPopup );
|
||||
/**
|
||||
* When the component is unmounted, remove the partner.js script, as well as the
|
||||
* dynamic scripts it loaded (signup-js and rampConfig-js)
|
||||
*
|
||||
* This is important, as the onboarding button is only initialized during the onload
|
||||
* event of those scripts; i.e. we need to load the scripts again, when the button is
|
||||
* rendered again.
|
||||
*/
|
||||
const onboardingScripts = [
|
||||
'partner-js',
|
||||
'signup-js',
|
||||
'rampConfig-js',
|
||||
];
|
||||
|
||||
if ( popup && ! popup.closed ) {
|
||||
popup.close();
|
||||
}
|
||||
onboardingScripts.forEach( ( id ) => {
|
||||
const el = document.querySelector( `script[id="${ id }"]` );
|
||||
|
||||
if ( el?.parentNode ) {
|
||||
el.parentNode.removeChild( el );
|
||||
}
|
||||
} );
|
||||
};
|
||||
} );
|
||||
}, [ onboardingUrl ] );
|
||||
|
||||
const setCompleteHandler = useCallback(
|
||||
( environment ) => {
|
||||
const onComplete = async ( authCode, sharedId ) => {
|
||||
/**
|
||||
* Until now, the full page is blocked by PayPal's semi-transparent, black overlay.
|
||||
* But at this point, the overlay is removed, while we process the sharedId and
|
||||
* authCode via a REST call.
|
||||
*
|
||||
* Note: The REST response is irrelevant, since PayPal will most likely refresh this
|
||||
* frame before the REST endpoint returns a value. Using "withActivity" is more of a
|
||||
* visual cue to the user that something is still processing in the background.
|
||||
*/
|
||||
await withActivity(
|
||||
ACTIVITIES.CONNECT_ISU,
|
||||
'Validating the connection details',
|
||||
async () => {
|
||||
await authenticateWithOAuth(
|
||||
sharedId,
|
||||
authCode,
|
||||
'sandbox' === environment
|
||||
);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const addHandler = () => {
|
||||
const MiniBrowser = window.PAYPAL?.apps?.Signup?.MiniBrowser;
|
||||
if ( ! MiniBrowser || MiniBrowser.onOnboardComplete ) {
|
||||
return;
|
||||
}
|
||||
|
||||
MiniBrowser.onOnboardComplete = onComplete;
|
||||
};
|
||||
|
||||
// Ensure the onComplete handler is not removed by a PayPal init script.
|
||||
timerRef.current = setInterval( addHandler, 250 );
|
||||
},
|
||||
[ authenticateWithOAuth, withActivity ]
|
||||
);
|
||||
|
||||
const removeCompleteHandler = useCallback( () => {
|
||||
if ( timerRef.current ) {
|
||||
clearInterval( timerRef.current );
|
||||
timerRef.current = null;
|
||||
}
|
||||
|
||||
delete window.PAYPAL?.apps?.Signup?.MiniBrowser?.onOnboardComplete;
|
||||
}, [] );
|
||||
|
||||
return {
|
||||
onboardingUrl,
|
||||
scriptLoaded,
|
||||
setCompleteHandler,
|
||||
removeCompleteHandler,
|
||||
};
|
||||
};
|
||||
|
||||
const useConnectionBase = () => {
|
||||
|
@ -92,104 +197,55 @@ const useConnectionBase = () => {
|
|||
};
|
||||
};
|
||||
|
||||
const useConnectionAttempt = ( connectFn, errorMessage ) => {
|
||||
const { handleFailed, createErrorNotice, handleCompleted } =
|
||||
useConnectionBase();
|
||||
|
||||
return async ( ...args ) => {
|
||||
const res = await connectFn( ...args );
|
||||
|
||||
if ( ! res.success || ! res.data ) {
|
||||
handleFailed( res, errorMessage );
|
||||
return false;
|
||||
}
|
||||
|
||||
const popupClosed = await handlePopupWithCompletion(
|
||||
res.data,
|
||||
createErrorNotice
|
||||
);
|
||||
|
||||
if ( popupClosed ) {
|
||||
await handleCompleted();
|
||||
}
|
||||
|
||||
return popupClosed;
|
||||
};
|
||||
};
|
||||
|
||||
export const useSandboxConnection = () => {
|
||||
const { connectToSandbox, isSandboxMode, setSandboxMode } =
|
||||
CommonHooks.useSandbox();
|
||||
const { withActivity } = CommonHooks.useBusyState();
|
||||
const connectionAttempt = useConnectionAttempt(
|
||||
connectToSandbox,
|
||||
MESSAGES.SANDBOX_ERROR
|
||||
);
|
||||
|
||||
const handleSandboxConnect = async () => {
|
||||
return withActivity(
|
||||
ACTIVITIES.CONNECT_SANDBOX,
|
||||
'Connecting to sandbox account',
|
||||
connectionAttempt
|
||||
);
|
||||
};
|
||||
const { isSandboxMode, setSandboxMode } = CommonHooks.useSandbox();
|
||||
|
||||
return {
|
||||
handleSandboxConnect,
|
||||
isSandboxMode,
|
||||
setSandboxMode,
|
||||
};
|
||||
};
|
||||
|
||||
export const useProductionConnection = () => {
|
||||
const { connectToProduction } = CommonHooks.useProduction();
|
||||
const { withActivity } = CommonHooks.useBusyState();
|
||||
const products = OnboardingHooks.useDetermineProducts();
|
||||
const connectionAttempt = useConnectionAttempt(
|
||||
() => connectToProduction( products ),
|
||||
MESSAGES.PRODUCTION_ERROR
|
||||
);
|
||||
|
||||
const handleProductionConnect = async () => {
|
||||
return withActivity(
|
||||
ACTIVITIES.CONNECT_PRODUCTION,
|
||||
'Connecting to production account',
|
||||
connectionAttempt
|
||||
);
|
||||
};
|
||||
|
||||
return { handleProductionConnect };
|
||||
};
|
||||
|
||||
export const useManualConnection = () => {
|
||||
export const useDirectAuthentication = () => {
|
||||
const { handleFailed, handleCompleted, createErrorNotice } =
|
||||
useConnectionBase();
|
||||
const { withActivity } = CommonHooks.useBusyState();
|
||||
const {
|
||||
connectViaIdAndSecret,
|
||||
authenticateWithCredentials,
|
||||
isManualConnectionMode,
|
||||
setManualConnectionMode,
|
||||
clientId,
|
||||
setClientId,
|
||||
clientSecret,
|
||||
setClientSecret,
|
||||
} = CommonHooks.useManualConnection();
|
||||
} = CommonHooks.useAuthentication();
|
||||
|
||||
const handleConnectViaIdAndSecret = async ( { validation } = {} ) => {
|
||||
const handleDirectAuthentication = async ( connectionDetails ) => {
|
||||
return withActivity(
|
||||
ACTIVITIES.CONNECT_MANUAL,
|
||||
'Connecting manually via Client ID and Secret',
|
||||
async () => {
|
||||
if ( 'function' === typeof validation ) {
|
||||
let data;
|
||||
|
||||
if ( 'function' === typeof connectionDetails ) {
|
||||
try {
|
||||
validation();
|
||||
data = connectionDetails();
|
||||
} catch ( exception ) {
|
||||
createErrorNotice( exception.message );
|
||||
return;
|
||||
}
|
||||
} else if ( 'object' === typeof connectionDetails ) {
|
||||
data = connectionDetails;
|
||||
}
|
||||
|
||||
const res = await connectViaIdAndSecret();
|
||||
if ( ! data || ! data.clientId || ! data.clientSecret ) {
|
||||
createErrorNotice(
|
||||
'Invalid connection details (clientID or clientSecret missing)'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await authenticateWithCredentials(
|
||||
data.clientId,
|
||||
data.clientSecret,
|
||||
!! data.isSandbox
|
||||
);
|
||||
|
||||
if ( res.success ) {
|
||||
await handleCompleted();
|
||||
|
@ -203,12 +259,8 @@ export const useManualConnection = () => {
|
|||
};
|
||||
|
||||
return {
|
||||
handleConnectViaIdAndSecret,
|
||||
handleDirectAuthentication,
|
||||
isManualConnectionMode,
|
||||
setManualConnectionMode,
|
||||
clientId,
|
||||
setClientId,
|
||||
clientSecret,
|
||||
setClientSecret,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -2,81 +2,139 @@ export const countryPriceInfo = {
|
|||
US: {
|
||||
fixedFee: {
|
||||
USD: 0.49,
|
||||
GBP: 0.39,
|
||||
CAD: 0.59,
|
||||
AUD: 0.59,
|
||||
EUR: 0.39,
|
||||
},
|
||||
checkout: 3.49,
|
||||
ccf: 2.59,
|
||||
dw: 2.59,
|
||||
apm: 2.59,
|
||||
fastlane: 2.59,
|
||||
plater: 4.99,
|
||||
ccf: {
|
||||
percentage: 2.59,
|
||||
fixedFee: 0.29,
|
||||
},
|
||||
dw: {
|
||||
percentage: 2.59,
|
||||
fixedFee: 0.29,
|
||||
},
|
||||
apm: {
|
||||
percentage: 2.89,
|
||||
fixedFee: 0.29,
|
||||
},
|
||||
fast: {
|
||||
percentage: 2.59,
|
||||
fixedFee: 0.29,
|
||||
},
|
||||
standardCardFields: 2.99,
|
||||
},
|
||||
UK: {
|
||||
fixedFee: {
|
||||
GPB: 0.3,
|
||||
USD: 0.3,
|
||||
CAD: 0.3,
|
||||
AUD: 0.3,
|
||||
EUR: 0.35,
|
||||
},
|
||||
checkout: 2.9,
|
||||
plater: 2.9,
|
||||
ccf: 1.2,
|
||||
dw: 1.2,
|
||||
fast: 1.2,
|
||||
apm: 1.2,
|
||||
standardCardFields: 1.2,
|
||||
},
|
||||
CA: {
|
||||
fixedFee: {
|
||||
CAD: 0.3,
|
||||
USD: 0.3,
|
||||
GBP: 0.2,
|
||||
AUD: 0.3,
|
||||
EUR: 0.35,
|
||||
},
|
||||
checkout: 2.9,
|
||||
ccf: 2.7,
|
||||
dw: 2.7,
|
||||
fast: 2.7,
|
||||
apm: 2.9,
|
||||
standardCardFields: 2.9,
|
||||
},
|
||||
AU: {
|
||||
fixedFee: {
|
||||
AUD: 0.3,
|
||||
USD: 0.3,
|
||||
GBP: 0.2,
|
||||
CAD: 0.3,
|
||||
EUR: 0.35,
|
||||
},
|
||||
checkout: 2.6,
|
||||
plater: 2.6,
|
||||
ccf: 1.75,
|
||||
dw: 1.75,
|
||||
fast: 1.75,
|
||||
apm: 2.6,
|
||||
standardCardFields: 2.6,
|
||||
},
|
||||
FR: {
|
||||
fixedFee: {
|
||||
EUR: 0.35,
|
||||
USD: 0.3,
|
||||
GBP: 0.3,
|
||||
CAD: 0.3,
|
||||
AUD: 0.3,
|
||||
},
|
||||
checkout: 2.9,
|
||||
plater: 2.9,
|
||||
ccf: 1.2,
|
||||
dw: 1.2,
|
||||
fast: 1.2,
|
||||
apm: 1.2,
|
||||
standardCardFields: 1.2,
|
||||
},
|
||||
IT: {
|
||||
fixedFee: {
|
||||
EUR: 0.35,
|
||||
EUR: 0.35,
|
||||
USD: 0.3,
|
||||
GBP: 0.3,
|
||||
CAD: 0.3,
|
||||
AUD: 0.3,
|
||||
},
|
||||
checkout: 3.4,
|
||||
plater: 3.4,
|
||||
ccf: 1.2,
|
||||
dw: 1.2,
|
||||
fast: 1.2,
|
||||
apm: 1.2,
|
||||
standardCardFields: 1.2,
|
||||
},
|
||||
DE: {
|
||||
fixedFee: {
|
||||
EUR: 0.39,
|
||||
USD: 0.49,
|
||||
GBP: 0.29,
|
||||
CAD: 0.59,
|
||||
AUD: 0.59,
|
||||
},
|
||||
checkout: 2.99,
|
||||
plater: 2.99,
|
||||
ccf: 2.99,
|
||||
dw: 2.99,
|
||||
fast: 2.99,
|
||||
apm: 2.99,
|
||||
standardCardFields: 2.99,
|
||||
},
|
||||
ES: {
|
||||
fixedFee: {
|
||||
EUR: 0.35,
|
||||
EUR: 0.35,
|
||||
USD: 0.3,
|
||||
GBP: 0.3,
|
||||
CAD: 0.3,
|
||||
AUD: 0.3,
|
||||
},
|
||||
checkout: 2.9,
|
||||
plater: 2.9,
|
||||
ccf: 1.2,
|
||||
dw: 1.2,
|
||||
fast: 1.2,
|
||||
apm: 1.2,
|
||||
standardCardFields: 1.2,
|
||||
},
|
||||
|
|
59
modules/ppcp-settings/resources/js/utils/tabSelector.js
Normal file
59
modules/ppcp-settings/resources/js/utils/tabSelector.js
Normal file
|
@ -0,0 +1,59 @@
|
|||
// Tab panel IDs
|
||||
export const TAB_IDS = {
|
||||
OVERVIEW: 'tab-panel-0-overview',
|
||||
PAYMENT_METHODS: 'tab-panel-0-payment-methods',
|
||||
SETTINGS: 'tab-panel-0-settings',
|
||||
STYLING: 'tab-panel-0-styling',
|
||||
};
|
||||
|
||||
/**
|
||||
* Select a tab by simulating a click event and scroll to specified element,
|
||||
* accounting for navigation container height
|
||||
*
|
||||
* TODO: Once the TabPanel gets migrated to Tabs (TabPanel v2) we need to remove this in favor of programmatic tab switching: https://github.com/WordPress/gutenberg/issues/52997
|
||||
*
|
||||
* @param {string} tabId - The ID of the tab to select
|
||||
* @param {string} [scrollToId] - Optional ID of the element to scroll to
|
||||
* @return {Promise} - Resolves when tab switch and scroll are complete
|
||||
*/
|
||||
export const selectTab = ( tabId, scrollToId ) => {
|
||||
return new Promise( ( resolve ) => {
|
||||
const tab = document.getElementById( tabId );
|
||||
if ( tab ) {
|
||||
tab.click();
|
||||
setTimeout( () => {
|
||||
const scrollTarget = scrollToId
|
||||
? document.getElementById( scrollToId )
|
||||
: document.getElementById( 'ppcp-settings-container' );
|
||||
|
||||
if ( scrollTarget ) {
|
||||
const navContainer = document.querySelector(
|
||||
'.ppcp-r-navigation-container'
|
||||
);
|
||||
const navHeight = navContainer
|
||||
? navContainer.offsetHeight
|
||||
: 0;
|
||||
|
||||
// Get the current scroll position and element's position relative to viewport
|
||||
const rect = scrollTarget.getBoundingClientRect();
|
||||
|
||||
// Calculate the final position with offset
|
||||
const scrollPosition =
|
||||
rect.top + window.scrollY - ( navHeight + 55 );
|
||||
|
||||
window.scrollTo( {
|
||||
top: scrollPosition,
|
||||
behavior: 'smooth',
|
||||
} );
|
||||
|
||||
// Resolve after scroll animation
|
||||
setTimeout( resolve, 300 );
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
}, 100 );
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
} );
|
||||
};
|
|
@ -1,42 +0,0 @@
|
|||
/**
|
||||
* Opens the provided URL, preferably in a popup window.
|
||||
*
|
||||
* Popups are usually only supported on desktop devices, when the browser is not in fullscreen mode.
|
||||
*
|
||||
* @param {string} url
|
||||
* @param {Object} options
|
||||
* @param {string} options.name
|
||||
* @param {number} options.width
|
||||
* @param {number} options.height
|
||||
* @param {boolean} options.resizeable
|
||||
* @return {null|Window} Popup window instance, or null.
|
||||
*/
|
||||
export const openPopup = (
|
||||
url,
|
||||
{ name = '_blank', width = 450, height = 720, resizeable = false } = {}
|
||||
) => {
|
||||
width = Math.max( 100, Math.min( window.screen.width - 40, width ) );
|
||||
height = Math.max( 100, Math.min( window.screen.height - 40, height ) );
|
||||
|
||||
const left = ( window.screen.width - width ) / 2;
|
||||
const top = ( window.screen.height - height ) / 2;
|
||||
|
||||
const features = [
|
||||
`width=${ width }`,
|
||||
`height=${ height }`,
|
||||
`left=${ left }`,
|
||||
`top=${ top }`,
|
||||
`resizable=${ resizeable ? 'yes' : 'no' }`,
|
||||
`scrollbars=yes`,
|
||||
`status=no`,
|
||||
];
|
||||
|
||||
const popup = window.open( url, name, features.join( ',' ) );
|
||||
|
||||
if ( popup && ! popup.closed ) {
|
||||
popup.focus();
|
||||
return popup;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
|
@ -10,19 +10,20 @@ declare( strict_types = 1 );
|
|||
namespace WooCommerce\PayPalCommerce\Settings;
|
||||
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
|
||||
use WooCommerce\PayPalCommerce\Settings\Data\CommonSettings;
|
||||
use WooCommerce\PayPalCommerce\Settings\Ajax\SwitchSettingsUiEndpoint;
|
||||
use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings;
|
||||
use WooCommerce\PayPalCommerce\Settings\Data\OnboardingProfile;
|
||||
use WooCommerce\PayPalCommerce\Settings\Endpoint\AuthenticationRestEndpoint;
|
||||
use WooCommerce\PayPalCommerce\Settings\Endpoint\CommonRestEndpoint;
|
||||
use WooCommerce\PayPalCommerce\Settings\Endpoint\ConnectManualRestEndpoint;
|
||||
use WooCommerce\PayPalCommerce\Settings\Endpoint\LoginLinkRestEndpoint;
|
||||
use WooCommerce\PayPalCommerce\Settings\Endpoint\OnboardingRestEndpoint;
|
||||
use WooCommerce\PayPalCommerce\Settings\Endpoint\RefreshFeatureStatusEndpoint;
|
||||
use WooCommerce\PayPalCommerce\Settings\Endpoint\SwitchSettingsUiEndpoint;
|
||||
use WooCommerce\PayPalCommerce\Settings\Endpoint\WebhookSettingsEndpoint;
|
||||
use WooCommerce\PayPalCommerce\Settings\Handler\ConnectionListener;
|
||||
use WooCommerce\PayPalCommerce\Settings\Service\AuthenticationManager;
|
||||
use WooCommerce\PayPalCommerce\Settings\Service\ConnectionUrlGenerator;
|
||||
use WooCommerce\PayPalCommerce\Settings\Service\OnboardingUrlManager;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
|
||||
use WooCommerce\PayPalCommerce\Settings\Handler\ConnectionListener;
|
||||
|
||||
return array(
|
||||
'settings.url' => static function ( ContainerInterface $container ) : string {
|
||||
|
@ -57,10 +58,7 @@ return array(
|
|||
);
|
||||
},
|
||||
'settings.data.general' => static function ( ContainerInterface $container ) : GeneralSettings {
|
||||
return new GeneralSettings();
|
||||
},
|
||||
'settings.data.common' => static function ( ContainerInterface $container ) : CommonSettings {
|
||||
return new CommonSettings(
|
||||
return new GeneralSettings(
|
||||
$container->get( 'api.shop.country' ),
|
||||
$container->get( 'api.shop.currency.getter' )->get(),
|
||||
$container->get( 'wcgateway.is-send-only-country' )
|
||||
|
@ -70,7 +68,7 @@ return array(
|
|||
return new OnboardingRestEndpoint( $container->get( 'settings.data.onboarding' ) );
|
||||
},
|
||||
'settings.rest.common' => static function ( ContainerInterface $container ) : CommonRestEndpoint {
|
||||
return new CommonRestEndpoint( $container->get( 'settings.data.common' ) );
|
||||
return new CommonRestEndpoint( $container->get( 'settings.data.general' ) );
|
||||
},
|
||||
'settings.rest.refresh_feature_status' => static function ( ContainerInterface $container ) : RefreshFeatureStatusEndpoint {
|
||||
return new RefreshFeatureStatusEndpoint(
|
||||
|
@ -79,17 +77,21 @@ return array(
|
|||
$container->get( 'woocommerce.logger.woocommerce' )
|
||||
);
|
||||
},
|
||||
'settings.rest.connect_manual' => static function ( ContainerInterface $container ) : ConnectManualRestEndpoint {
|
||||
return new ConnectManualRestEndpoint(
|
||||
$container->get( 'api.paypal-host-production' ),
|
||||
$container->get( 'api.paypal-host-sandbox' ),
|
||||
$container->get( 'woocommerce.logger.woocommerce' ),
|
||||
$container->get( 'settings.data.general' )
|
||||
'settings.rest.connect_manual' => static function ( ContainerInterface $container ) : AuthenticationRestEndpoint {
|
||||
return new AuthenticationRestEndpoint(
|
||||
$container->get( 'settings.service.authentication_manager' ),
|
||||
);
|
||||
},
|
||||
'settings.rest.login_link' => static function ( ContainerInterface $container ) : LoginLinkRestEndpoint {
|
||||
return new LoginLinkRestEndpoint(
|
||||
$container->get( 'settings.service.connection-url-generators' ),
|
||||
$container->get( 'settings.service.connection-url-generator' ),
|
||||
);
|
||||
},
|
||||
'settings.rest.webhooks' => static function ( ContainerInterface $container ) : WebhookSettingsEndpoint {
|
||||
return new WebhookSettingsEndpoint(
|
||||
$container->get( 'api.endpoint.webhook' ),
|
||||
$container->get( 'webhook.registrar' ),
|
||||
$container->get( 'webhook.status.simulation' )
|
||||
);
|
||||
},
|
||||
'settings.casual-selling.supported-countries' => static function ( ContainerInterface $container ) : array {
|
||||
|
@ -153,8 +155,9 @@ return array(
|
|||
|
||||
return new ConnectionListener(
|
||||
$page_id,
|
||||
$container->get( 'settings.data.common' ),
|
||||
$container->get( 'settings.service.onboarding-url-manager' ),
|
||||
$container->get( 'settings.service.authentication_manager' ),
|
||||
$container->get( 'http.redirector' ),
|
||||
$container->get( 'woocommerce.logger.woocommerce' )
|
||||
);
|
||||
},
|
||||
|
@ -167,33 +170,24 @@ return array(
|
|||
$container->get( 'woocommerce.logger.woocommerce' )
|
||||
);
|
||||
},
|
||||
'settings.service.connection-url-generators' => static function ( ContainerInterface $container ) : array {
|
||||
// Define available environments.
|
||||
$environments = array(
|
||||
'production' => array(
|
||||
'partner_referrals' => $container->get( 'api.endpoint.partner-referrals-production' ),
|
||||
),
|
||||
'sandbox' => array(
|
||||
'partner_referrals' => $container->get( 'api.endpoint.partner-referrals-sandbox' ),
|
||||
),
|
||||
'settings.service.connection-url-generator' => static function ( ContainerInterface $container ) : ConnectionUrlGenerator {
|
||||
return new ConnectionUrlGenerator(
|
||||
$container->get( 'api.env.endpoint.partner-referrals' ),
|
||||
$container->get( 'api.repository.partner-referrals-data' ),
|
||||
$container->get( 'settings.service.onboarding-url-manager' ),
|
||||
$container->get( 'woocommerce.logger.woocommerce' )
|
||||
);
|
||||
|
||||
$generators = array();
|
||||
|
||||
// Instantiate URL generators for each environment.
|
||||
foreach ( $environments as $environment => $config ) {
|
||||
$generators[ $environment ] = new ConnectionUrlGenerator(
|
||||
$config['partner_referrals'],
|
||||
$container->get( 'api.repository.partner-referrals-data' ),
|
||||
$environment,
|
||||
$container->get( 'settings.service.onboarding-url-manager' ),
|
||||
$container->get( 'woocommerce.logger.woocommerce' )
|
||||
);
|
||||
}
|
||||
|
||||
return $generators;
|
||||
},
|
||||
'settings.switch-ui.endpoint' => static function ( ContainerInterface $container ) : SwitchSettingsUiEndpoint {
|
||||
'settings.service.authentication_manager' => static function ( ContainerInterface $container ) : AuthenticationManager {
|
||||
return new AuthenticationManager(
|
||||
$container->get( 'settings.data.general' ),
|
||||
$container->get( 'api.env.paypal-host' ),
|
||||
$container->get( 'api.env.endpoint.login-seller' ),
|
||||
$container->get( 'api.repository.partner-referrals-data' ),
|
||||
$container->get( 'woocommerce.logger.woocommerce' ),
|
||||
);
|
||||
},
|
||||
'settings.ajax.switch_ui' => static function ( ContainerInterface $container ) : SwitchSettingsUiEndpoint {
|
||||
return new SwitchSettingsUiEndpoint(
|
||||
$container->get( 'woocommerce.logger.woocommerce' ),
|
||||
$container->get( 'button.request-data' ),
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
<?php
|
||||
/**
|
||||
* The settings UI switching Endpoint.
|
||||
* The settings UI switching Ajax endpoint.
|
||||
*
|
||||
* @package WooCommerce\PayPalCommerce\Settings\Endpoint
|
||||
*/
|
||||
|
||||
declare( strict_types=1 );
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\Settings\Endpoint;
|
||||
namespace WooCommerce\PayPalCommerce\Settings\Ajax;
|
||||
|
||||
use Exception;
|
||||
use Psr\Log\LoggerInterface;
|
75
modules/ppcp-settings/src/DTO/MerchantConnectionDTO.php
Normal file
75
modules/ppcp-settings/src/DTO/MerchantConnectionDTO.php
Normal file
|
@ -0,0 +1,75 @@
|
|||
<?php
|
||||
/**
|
||||
* Data transfer object. Stores all connection credentials of the PayPal merchant connection.
|
||||
*
|
||||
* @package WooCommerce\PayPalCommerce\Settings\DTO;
|
||||
*/
|
||||
|
||||
declare( strict_types = 1 );
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\Settings\DTO;
|
||||
|
||||
/**
|
||||
* DTO that collects all details of a "merchant connection".
|
||||
*
|
||||
* Intentionally has no internal logic, sanitation or validation.
|
||||
*/
|
||||
class MerchantConnectionDTO {
|
||||
/**
|
||||
* Whether this connection is a sandbox account.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public bool $is_sandbox = false;
|
||||
|
||||
/**
|
||||
* API client ID.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $client_id = '';
|
||||
|
||||
/**
|
||||
* API client secret.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $client_secret = '';
|
||||
|
||||
/**
|
||||
* PayPal's 13-character merchant ID.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $merchant_id = '';
|
||||
|
||||
/**
|
||||
* Email address of the merchant account.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $merchant_email = '';
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param bool $is_sandbox Whether this connection is a sandbox account.
|
||||
* @param string $client_id API client ID.
|
||||
* @param string $client_secret API client secret.
|
||||
* @param string $merchant_id PayPal's 13-character merchant ID.
|
||||
* @param string $merchant_email Email address of the merchant account.
|
||||
*/
|
||||
public function __construct(
|
||||
bool $is_sandbox,
|
||||
string $client_id,
|
||||
string $client_secret,
|
||||
string $merchant_id,
|
||||
string $merchant_email
|
||||
) {
|
||||
$this->is_sandbox = $is_sandbox;
|
||||
$this->client_id = $client_id;
|
||||
$this->client_secret = $client_secret;
|
||||
$this->merchant_id = $merchant_id;
|
||||
$this->merchant_email = $merchant_email;
|
||||
}
|
||||
}
|
|
@ -122,5 +122,4 @@ abstract class AbstractDataModel {
|
|||
|
||||
return $stripped_key ? "set_$stripped_key" : '';
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,209 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Common settings class
|
||||
*
|
||||
* @package WooCommerce\PayPalCommerce\Settings\Data
|
||||
*/
|
||||
|
||||
declare( strict_types = 1 );
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\Settings\Data;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Class CommonSettings
|
||||
*
|
||||
* This class serves as a container for managing the common settings that
|
||||
* are used and managed in various areas of the settings UI
|
||||
*
|
||||
* Those settings mainly describe connection details and are initially collected
|
||||
* in the onboarding wizard, and also appear in the settings screen.
|
||||
*/
|
||||
class CommonSettings extends AbstractDataModel {
|
||||
|
||||
/**
|
||||
* Option key where profile details are stored.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected const OPTION_KEY = 'woocommerce-ppcp-data-common';
|
||||
|
||||
/**
|
||||
* List of customization flags, provided by the server (read-only).
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $woo_settings = array();
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param string $country WooCommerce store country.
|
||||
* @param string $currency WooCommerce store currency.
|
||||
* @param bool $is_current_country_send_only Indicates whether the current store's country is classified as a send-only country.
|
||||
*/
|
||||
public function __construct( string $country, string $currency, bool $is_current_country_send_only ) {
|
||||
parent::__construct();
|
||||
$this->woo_settings['country'] = $country;
|
||||
$this->woo_settings['currency'] = $currency;
|
||||
$this->data['is_current_country_send_only'] = $is_current_country_send_only;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default values for the model.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function get_defaults() : array {
|
||||
return array(
|
||||
'use_sandbox' => false,
|
||||
'use_manual_connection' => false,
|
||||
'client_id' => '',
|
||||
'client_secret' => '',
|
||||
|
||||
// Details about connected merchant account.
|
||||
'merchant_connected' => false,
|
||||
'sandbox_merchant' => false,
|
||||
'merchant_id' => '',
|
||||
'merchant_email' => '',
|
||||
);
|
||||
}
|
||||
|
||||
// -----
|
||||
|
||||
/**
|
||||
* Gets the 'use sandbox' setting.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function get_sandbox() : bool {
|
||||
return (bool) $this->data['use_sandbox'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the 'use sandbox' setting.
|
||||
*
|
||||
* @param bool $use_sandbox Whether to use sandbox mode.
|
||||
*/
|
||||
public function set_sandbox( bool $use_sandbox ) : void {
|
||||
$this->data['use_sandbox'] = $use_sandbox;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the 'use manual connection' setting.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function get_manual_connection() : bool {
|
||||
return (bool) $this->data['use_manual_connection'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the 'use manual connection' setting.
|
||||
*
|
||||
* @param bool $use_manual_connection Whether to use manual connection.
|
||||
*/
|
||||
public function set_manual_connection( bool $use_manual_connection ) : void {
|
||||
$this->data['use_manual_connection'] = $use_manual_connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the client ID.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_client_id() : string {
|
||||
return $this->data['client_id'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the client ID.
|
||||
*
|
||||
* @param string $client_id The client ID.
|
||||
*/
|
||||
public function set_client_id( string $client_id ) : void {
|
||||
$this->data['client_id'] = sanitize_text_field( $client_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the client secret.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_client_secret() : string {
|
||||
return $this->data['client_secret'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the client secret.
|
||||
*
|
||||
* @param string $client_secret The client secret.
|
||||
*/
|
||||
public function set_client_secret( string $client_secret ) : void {
|
||||
$this->data['client_secret'] = sanitize_text_field( $client_secret );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of read-only customization flags.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_woo_settings() : array {
|
||||
return $this->woo_settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter to update details of the connected merchant account.
|
||||
*
|
||||
* Those details cannot be changed individually.
|
||||
*
|
||||
* @param bool $is_sandbox Whether the details are for a sandbox account.
|
||||
* @param string $merchant_id The merchant ID.
|
||||
* @param string $merchant_email The merchant's email.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function set_merchant_data( bool $is_sandbox, string $merchant_id, string $merchant_email ) : void {
|
||||
$this->data['sandbox_merchant'] = $is_sandbox;
|
||||
$this->data['merchant_id'] = sanitize_text_field( $merchant_id );
|
||||
$this->data['merchant_email'] = sanitize_email( $merchant_email );
|
||||
$this->data['merchant_connected'] = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the currently connected merchant is a sandbox account.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_sandbox_merchant() : bool {
|
||||
return $this->data['sandbox_merchant'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the merchant successfully logged into their PayPal account.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_merchant_connected() : bool {
|
||||
return $this->data['merchant_connected'] && $this->data['merchant_id'] && $this->data['merchant_email'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the currently connected merchant ID.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_merchant_id() : string {
|
||||
return $this->data['merchant_id'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the currently connected merchant's email.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_merchant_email() : string {
|
||||
return $this->data['merchant_email'];
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
/**
|
||||
* GeneralSettings class
|
||||
* General plugin settings class
|
||||
*
|
||||
* @package WooCommerce\PayPalCommerce\Settings\Data
|
||||
*/
|
||||
|
@ -10,10 +10,16 @@ declare( strict_types = 1 );
|
|||
namespace WooCommerce\PayPalCommerce\Settings\Data;
|
||||
|
||||
use RuntimeException;
|
||||
use WooCommerce\PayPalCommerce\Settings\DTO\MerchantConnectionDTO;
|
||||
|
||||
/**
|
||||
* This class serves as a container for managing the general plugin settings,
|
||||
* such as the connection credentials.
|
||||
* Class GeneralSettings
|
||||
*
|
||||
* This class serves as a container for managing the common settings that
|
||||
* are used and managed in various areas of the settings UI
|
||||
*
|
||||
* Those settings mainly describe connection details and are initially collected
|
||||
* in the onboarding wizard, and also appear in the settings screen.
|
||||
*/
|
||||
class GeneralSettings extends AbstractDataModel {
|
||||
|
||||
|
@ -22,7 +28,34 @@ class GeneralSettings extends AbstractDataModel {
|
|||
*
|
||||
* @var string
|
||||
*/
|
||||
protected const OPTION_KEY = 'woocommerce-ppcp-data-general-settings';
|
||||
protected const OPTION_KEY = 'woocommerce-ppcp-data-common';
|
||||
|
||||
/**
|
||||
* List of customization flags, provided by the server (read-only).
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $woo_settings = array();
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param string $country WooCommerce store country.
|
||||
* @param string $currency WooCommerce store currency.
|
||||
* @param bool $is_send_only_country Whether the store's country is classified as a send-only
|
||||
* country.
|
||||
*
|
||||
* @throws RuntimeException When forgetting to define the OPTION_KEY in this class.
|
||||
*/
|
||||
public function __construct( string $country, string $currency, bool $is_send_only_country ) {
|
||||
parent::__construct();
|
||||
|
||||
$this->woo_settings['country'] = $country;
|
||||
$this->woo_settings['currency'] = $currency;
|
||||
|
||||
$this->data['is_current_country_send_only'] = $is_send_only_country;
|
||||
$this->data['merchant_connected'] = $this->is_merchant_connected();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default values for the model.
|
||||
|
@ -31,161 +64,150 @@ class GeneralSettings extends AbstractDataModel {
|
|||
*/
|
||||
protected function get_defaults() : array {
|
||||
return array(
|
||||
'is_sandbox' => false,
|
||||
'live_client_id' => '',
|
||||
'live_client_secret' => '',
|
||||
'live_merchant_id' => '',
|
||||
'live_merchant_email' => '',
|
||||
'sandbox_client_id' => '',
|
||||
'sandbox_client_secret' => '',
|
||||
'sandbox_merchant_id' => '',
|
||||
'sandbox_merchant_email' => '',
|
||||
'use_sandbox' => false, // UI state, not a connection detail.
|
||||
'use_manual_connection' => false, // UI state, not a connection detail.
|
||||
'is_current_country_send_only' => false, // Read-only flag.
|
||||
|
||||
// Details about connected merchant account.
|
||||
'merchant_connected' => false,
|
||||
'sandbox_merchant' => false,
|
||||
'merchant_id' => '',
|
||||
'merchant_email' => '',
|
||||
'client_id' => '',
|
||||
'client_secret' => '',
|
||||
);
|
||||
}
|
||||
|
||||
// -----
|
||||
|
||||
/**
|
||||
* Gets the 'is_sandbox' flag.
|
||||
*/
|
||||
public function is_sandbox() : bool {
|
||||
return (bool) $this->data['is_sandbox'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the 'is_sandbox' flag.
|
||||
* Gets the 'use sandbox' setting.
|
||||
*
|
||||
* @param bool $value The value to set.
|
||||
* @return bool
|
||||
*/
|
||||
public function set_is_sandbox( bool $value ) : void {
|
||||
$this->data['is_sandbox'] = $value;
|
||||
public function get_sandbox() : bool {
|
||||
return (bool) $this->data['use_sandbox'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the live client ID.
|
||||
*/
|
||||
public function live_client_id() : string {
|
||||
return $this->data['live_client_id'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the live client ID.
|
||||
* Sets the 'use sandbox' setting.
|
||||
*
|
||||
* @param string $value The value to set.
|
||||
* @param bool $use_sandbox Whether to use sandbox mode.
|
||||
*/
|
||||
public function set_live_client_id( string $value ) : void {
|
||||
$this->data['live_client_id'] = sanitize_text_field( $value );
|
||||
public function set_sandbox( bool $use_sandbox ) : void {
|
||||
$this->data['use_sandbox'] = $use_sandbox;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the live client secret.
|
||||
*/
|
||||
public function live_client_secret() : string {
|
||||
return $this->data['live_client_secret'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the live client secret.
|
||||
* Gets the 'use manual connection' setting.
|
||||
*
|
||||
* @param string $value The value to set.
|
||||
* @return bool
|
||||
*/
|
||||
public function set_live_client_secret( string $value ) : void {
|
||||
$this->data['live_client_secret'] = sanitize_text_field( $value );
|
||||
public function get_manual_connection() : bool {
|
||||
return (bool) $this->data['use_manual_connection'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the live merchant ID.
|
||||
*/
|
||||
public function live_merchant_id() : string {
|
||||
return $this->data['live_merchant_id'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the live merchant ID.
|
||||
* Sets the 'use manual connection' setting.
|
||||
*
|
||||
* @param string $value The value to set.
|
||||
* @param bool $use_manual_connection Whether to use manual connection.
|
||||
*/
|
||||
public function set_live_merchant_id( string $value ) : void {
|
||||
$this->data['live_merchant_id'] = sanitize_text_field( $value );
|
||||
public function set_manual_connection( bool $use_manual_connection ) : void {
|
||||
$this->data['use_manual_connection'] = $use_manual_connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the live merchant email.
|
||||
*/
|
||||
public function live_merchant_email() : string {
|
||||
return $this->data['live_merchant_email'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the live merchant email.
|
||||
* Returns the list of read-only customization flags.
|
||||
*
|
||||
* @param string $value The value to set.
|
||||
* @return array
|
||||
*/
|
||||
public function set_live_merchant_email( string $value ) : void {
|
||||
$this->data['live_merchant_email'] = sanitize_email( $value );
|
||||
public function get_woo_settings() : array {
|
||||
return $this->woo_settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the sandbox client ID.
|
||||
*/
|
||||
public function sandbox_client_id() : string {
|
||||
return $this->data['sandbox_client_id'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the sandbox client ID.
|
||||
* Setter to update details of the connected merchant account.
|
||||
*
|
||||
* @param string $value The value to set.
|
||||
*/
|
||||
public function set_sandbox_client_id( string $value ) : void {
|
||||
$this->data['sandbox_client_id'] = sanitize_text_field( $value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the sandbox client secret.
|
||||
*/
|
||||
public function sandbox_client_secret() : string {
|
||||
return $this->data['sandbox_client_secret'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the sandbox client secret.
|
||||
* @param MerchantConnectionDTO $connection Connection details.
|
||||
*
|
||||
* @param string $value The value to set.
|
||||
* @return void
|
||||
*/
|
||||
public function set_sandbox_client_secret( string $value ) : void {
|
||||
$this->data['sandbox_client_secret'] = sanitize_text_field( $value );
|
||||
public function set_merchant_data( MerchantConnectionDTO $connection ) : void {
|
||||
$this->data['sandbox_merchant'] = $connection->is_sandbox;
|
||||
$this->data['merchant_id'] = sanitize_text_field( $connection->merchant_id );
|
||||
$this->data['merchant_email'] = sanitize_email( $connection->merchant_email );
|
||||
$this->data['client_id'] = sanitize_text_field( $connection->client_id );
|
||||
$this->data['client_secret'] = sanitize_text_field( $connection->client_secret );
|
||||
$this->data['merchant_connected'] = $this->is_merchant_connected();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the sandbox merchant ID.
|
||||
*/
|
||||
public function sandbox_merchant_id() : string {
|
||||
return $this->data['sandbox_merchant_id'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the sandbox merchant ID.
|
||||
* Returns the full merchant connection DTO for the current connection.
|
||||
*
|
||||
* @param string $value The value to set.
|
||||
* @return MerchantConnectionDTO All connection details.
|
||||
*/
|
||||
public function set_sandbox_merchant_id( string $value ) : void {
|
||||
$this->data['sandbox_merchant_id'] = sanitize_text_field( $value );
|
||||
public function get_merchant_data() : MerchantConnectionDTO {
|
||||
return new MerchantConnectionDTO(
|
||||
$this->is_sandbox_merchant(),
|
||||
$this->data['client_id'],
|
||||
$this->data['client_secret'],
|
||||
$this->data['merchant_id'],
|
||||
$this->data['merchant_email']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the sandbox merchant email.
|
||||
*/
|
||||
public function sandbox_merchant_email() : string {
|
||||
return $this->data['sandbox_merchant_email'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the sandbox merchant email.
|
||||
* Reset all connection details to the initial, disconnected state.
|
||||
*
|
||||
* @param string $value The value to set.
|
||||
* @return void
|
||||
*/
|
||||
public function set_sandbox_merchant_email( string $value ) : void {
|
||||
$this->data['sandbox_merchant_email'] = sanitize_email( $value );
|
||||
public function reset_merchant_data() : void {
|
||||
$defaults = $this->get_defaults();
|
||||
|
||||
$this->data['sandbox_merchant'] = $defaults['sandbox_merchant'];
|
||||
$this->data['merchant_id'] = $defaults['merchant_id'];
|
||||
$this->data['merchant_email'] = $defaults['merchant_email'];
|
||||
$this->data['client_id'] = $defaults['client_id'];
|
||||
$this->data['client_secret'] = $defaults['client_secret'];
|
||||
$this->data['merchant_connected'] = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the currently connected merchant is a sandbox account.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_sandbox_merchant() : bool {
|
||||
return $this->data['sandbox_merchant'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the merchant successfully logged into their PayPal account.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_merchant_connected() : bool {
|
||||
return $this->data['merchant_email']
|
||||
&& $this->data['merchant_id']
|
||||
&& $this->data['client_id']
|
||||
&& $this->data['client_secret'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the currently connected merchant ID.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_merchant_id() : string {
|
||||
return $this->data['merchant_id'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the currently connected merchant's email.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_merchant_email() : string {
|
||||
return $this->data['merchant_email'];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,179 @@
|
|||
<?php
|
||||
/**
|
||||
* REST controller for authenticating a PayPal merchant.
|
||||
*
|
||||
* @package WooCommerce\PayPalCommerce\Settings\Endpoint
|
||||
*/
|
||||
|
||||
declare( strict_types = 1 );
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\Settings\Endpoint;
|
||||
|
||||
use Exception;
|
||||
use WP_REST_Request;
|
||||
use WP_REST_Response;
|
||||
use WP_REST_Server;
|
||||
use WooCommerce\PayPalCommerce\Settings\Service\AuthenticationManager;
|
||||
|
||||
/**
|
||||
* REST controller for authenticating and connecting to a PayPal merchant account.
|
||||
*
|
||||
* This endpoint is responsible for verifying credentials and establishing
|
||||
* a connection, regardless of whether they are provided via:
|
||||
* 1. Direct login (clientId + secret)
|
||||
* 2. UI-driven login (sharedId + authCode)
|
||||
*
|
||||
* It handles the actual authentication process after the login URL has been used.
|
||||
*/
|
||||
class AuthenticationRestEndpoint extends RestEndpoint {
|
||||
/**
|
||||
* The base path for this REST controller.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rest_base = 'authenticate';
|
||||
|
||||
/**
|
||||
* Authentication manager service.
|
||||
*
|
||||
* @var AuthenticationManager
|
||||
*/
|
||||
private AuthenticationManager $authentication_manager;
|
||||
|
||||
/**
|
||||
* Defines the JSON response format (when connection was successful).
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private array $response_map = array(
|
||||
'merchant_id' => array(
|
||||
'js_name' => 'merchantId',
|
||||
),
|
||||
'merchant_email' => array(
|
||||
'js_name' => 'email',
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param AuthenticationManager $authentication_manager The authentication manager.
|
||||
*/
|
||||
public function __construct( AuthenticationManager $authentication_manager ) {
|
||||
$this->authentication_manager = $authentication_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure REST API routes.
|
||||
*/
|
||||
public function register_routes() : void {
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/direct',
|
||||
array(
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'callback' => array( $this, 'connect_direct' ),
|
||||
'permission_callback' => array( $this, 'check_permission' ),
|
||||
'args' => array(
|
||||
'clientId' => array(
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
'minLength' => 80,
|
||||
'maxLength' => 80,
|
||||
),
|
||||
'clientSecret' => array(
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
),
|
||||
'useSandbox' => array(
|
||||
'required' => false,
|
||||
'type' => 'boolean',
|
||||
'default' => false,
|
||||
'sanitize_callback' => array( $this, 'to_boolean' ),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/isu',
|
||||
array(
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'callback' => array( $this, 'connect_isu' ),
|
||||
'permission_callback' => array( $this, 'check_permission' ),
|
||||
'args' => array(
|
||||
'sharedId' => array(
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
),
|
||||
'authCode' => array(
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
),
|
||||
'useSandbox' => array(
|
||||
'default' => 0,
|
||||
'type' => 'boolean',
|
||||
'sanitize_callback' => array( $this, 'to_boolean' ),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Direct login: Retrieves merchantId and email using clientId and clientSecret.
|
||||
*
|
||||
* This is the "Manual Login" logic, when a merchant already knows their
|
||||
* API credentials.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*/
|
||||
public function connect_direct( WP_REST_Request $request ) : WP_REST_Response {
|
||||
$client_id = $request->get_param( 'clientId' );
|
||||
$client_secret = $request->get_param( 'clientSecret' );
|
||||
$use_sandbox = $request->get_param( 'useSandbox' );
|
||||
|
||||
try {
|
||||
$this->authentication_manager->validate_id_and_secret( $client_id, $client_secret );
|
||||
$this->authentication_manager->authenticate_via_direct_api( $use_sandbox, $client_id, $client_secret );
|
||||
} catch ( Exception $exception ) {
|
||||
return $this->return_error( $exception->getMessage() );
|
||||
}
|
||||
|
||||
$account = $this->authentication_manager->get_account_details();
|
||||
$response = $this->sanitize_for_javascript( $this->response_map, $account );
|
||||
|
||||
return $this->return_success( $response );
|
||||
}
|
||||
|
||||
/**
|
||||
* ISU login: Retrieves clientId and clientSecret using a sharedId and authCode.
|
||||
*
|
||||
* This is the final step in the UI-driven login via the ISU popup, which
|
||||
* is triggered by the LoginLinkRestEndpoint URL.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*/
|
||||
public function connect_isu( WP_REST_Request $request ) : WP_REST_Response {
|
||||
$shared_id = $request->get_param( 'sharedId' );
|
||||
$auth_code = $request->get_param( 'authCode' );
|
||||
$use_sandbox = $request->get_param( 'useSandbox' );
|
||||
|
||||
try {
|
||||
$this->authentication_manager->validate_id_and_auth_code( $shared_id, $auth_code );
|
||||
$this->authentication_manager->authenticate_via_oauth( $use_sandbox, $shared_id, $auth_code );
|
||||
} catch ( Exception $exception ) {
|
||||
return $this->return_error( $exception->getMessage() );
|
||||
}
|
||||
|
||||
$account = $this->authentication_manager->get_account_details();
|
||||
$response = $this->sanitize_for_javascript( $this->response_map, $account );
|
||||
|
||||
return $this->return_success( $response );
|
||||
}
|
||||
}
|
|
@ -12,7 +12,7 @@ namespace WooCommerce\PayPalCommerce\Settings\Endpoint;
|
|||
use WP_REST_Server;
|
||||
use WP_REST_Response;
|
||||
use WP_REST_Request;
|
||||
use WooCommerce\PayPalCommerce\Settings\Data\CommonSettings;
|
||||
use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings;
|
||||
|
||||
/**
|
||||
* REST controller for "common" settings, which are used and modified by
|
||||
|
@ -32,9 +32,9 @@ class CommonRestEndpoint extends RestEndpoint {
|
|||
/**
|
||||
* The settings instance.
|
||||
*
|
||||
* @var CommonSettings
|
||||
* @var GeneralSettings
|
||||
*/
|
||||
protected CommonSettings $settings;
|
||||
protected GeneralSettings $settings;
|
||||
|
||||
/**
|
||||
* Field mapping for request to profile transformation.
|
||||
|
@ -50,13 +50,8 @@ class CommonRestEndpoint extends RestEndpoint {
|
|||
'js_name' => 'useManualConnection',
|
||||
'sanitize' => 'to_boolean',
|
||||
),
|
||||
'client_id' => array(
|
||||
'js_name' => 'clientId',
|
||||
'sanitize' => 'sanitize_text_field',
|
||||
),
|
||||
'client_secret' => array(
|
||||
'js_name' => 'clientSecret',
|
||||
'sanitize' => 'sanitize_text_field',
|
||||
'webhooks' => array(
|
||||
'js_name' => 'webhooks',
|
||||
),
|
||||
);
|
||||
|
||||
|
@ -78,6 +73,12 @@ class CommonRestEndpoint extends RestEndpoint {
|
|||
'merchant_email' => array(
|
||||
'js_name' => 'email',
|
||||
),
|
||||
'client_id' => array(
|
||||
'js_name' => 'clientId',
|
||||
),
|
||||
'client_secret' => array(
|
||||
'js_name' => 'clientSecret',
|
||||
),
|
||||
'is_current_country_send_only' => array(
|
||||
'js_name' => 'isCurrentCountrySendOnly',
|
||||
),
|
||||
|
@ -100,9 +101,9 @@ class CommonRestEndpoint extends RestEndpoint {
|
|||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param CommonSettings $settings The settings instance.
|
||||
* @param GeneralSettings $settings The settings instance.
|
||||
*/
|
||||
public function __construct( CommonSettings $settings ) {
|
||||
public function __construct( GeneralSettings $settings ) {
|
||||
$this->settings = $settings;
|
||||
}
|
||||
|
||||
|
@ -114,11 +115,9 @@ class CommonRestEndpoint extends RestEndpoint {
|
|||
$this->namespace,
|
||||
'/' . $this->rest_base,
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_details' ),
|
||||
'permission_callback' => array( $this, 'check_permission' ),
|
||||
),
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_details' ),
|
||||
'permission_callback' => array( $this, 'check_permission' ),
|
||||
)
|
||||
);
|
||||
|
||||
|
@ -126,11 +125,9 @@ class CommonRestEndpoint extends RestEndpoint {
|
|||
$this->namespace,
|
||||
'/' . $this->rest_base,
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'callback' => array( $this, 'update_details' ),
|
||||
'permission_callback' => array( $this, 'check_permission' ),
|
||||
),
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'callback' => array( $this, 'update_details' ),
|
||||
'permission_callback' => array( $this, 'check_permission' ),
|
||||
)
|
||||
);
|
||||
|
||||
|
@ -138,11 +135,9 @@ class CommonRestEndpoint extends RestEndpoint {
|
|||
$this->namespace,
|
||||
"/$this->rest_base/merchant",
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_merchant_details' ),
|
||||
'permission_callback' => array( $this, 'check_permission' ),
|
||||
),
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_merchant_details' ),
|
||||
'permission_callback' => array( $this, 'check_permission' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -209,10 +204,12 @@ class CommonRestEndpoint extends RestEndpoint {
|
|||
$this->merchant_info_map
|
||||
);
|
||||
|
||||
$extra_data['merchant'] = apply_filters(
|
||||
'woocommerce_paypal_payments_rest_common_merchant_data',
|
||||
$extra_data['merchant'],
|
||||
);
|
||||
if ( $this->settings->is_merchant_connected() ) {
|
||||
$extra_data['features'] = apply_filters(
|
||||
'woocommerce_paypal_payments_rest_common_merchant_data',
|
||||
array(),
|
||||
);
|
||||
}
|
||||
|
||||
return $extra_data;
|
||||
}
|
||||
|
|
|
@ -1,238 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* REST controller for connection via manual credentials input.
|
||||
*
|
||||
* @package WooCommerce\PayPalCommerce\Settings\Endpoint
|
||||
*/
|
||||
|
||||
declare( strict_types = 1 );
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\Settings\Endpoint;
|
||||
|
||||
use Exception;
|
||||
use stdClass;
|
||||
use RuntimeException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use WP_REST_Request;
|
||||
use WP_REST_Response;
|
||||
use WP_REST_Server;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Authentication\PayPalBearer;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\Orders;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Helper\InMemoryCache;
|
||||
use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings;
|
||||
|
||||
/**
|
||||
* REST controller for connection via manual credentials input.
|
||||
*/
|
||||
class ConnectManualRestEndpoint extends RestEndpoint {
|
||||
|
||||
/**
|
||||
* The API host for the live mode.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private string $live_host;
|
||||
|
||||
/**
|
||||
* The API host for the sandbox mode.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private string $sandbox_host;
|
||||
|
||||
/**
|
||||
* The logger.
|
||||
*
|
||||
* @var LoggerInterface
|
||||
*/
|
||||
private $logger;
|
||||
|
||||
/**
|
||||
* The base path for this REST controller.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rest_base = 'connect_manual';
|
||||
|
||||
/**
|
||||
* Settings instance.
|
||||
*
|
||||
* @var GeneralSettings
|
||||
*/
|
||||
private $settings = null;
|
||||
|
||||
/**
|
||||
* Field mapping for request.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private array $field_map = array(
|
||||
'client_id' => array(
|
||||
'js_name' => 'clientId',
|
||||
'sanitize' => 'sanitize_text_field',
|
||||
),
|
||||
'client_secret' => array(
|
||||
'js_name' => 'clientSecret',
|
||||
'sanitize' => 'sanitize_text_field',
|
||||
),
|
||||
'use_sandbox' => array(
|
||||
'js_name' => 'useSandbox',
|
||||
'sanitize' => 'to_boolean',
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* ConnectManualRestEndpoint constructor.
|
||||
*
|
||||
* @param string $live_host The API host for the live mode.
|
||||
* @param string $sandbox_host The API host for the sandbox mode.
|
||||
* @param LoggerInterface $logger The logger.
|
||||
* @param GeneralSettings $settings Settings instance.
|
||||
*/
|
||||
public function __construct(
|
||||
string $live_host,
|
||||
string $sandbox_host,
|
||||
LoggerInterface $logger,
|
||||
GeneralSettings $settings
|
||||
) {
|
||||
$this->live_host = $live_host;
|
||||
$this->sandbox_host = $sandbox_host;
|
||||
$this->logger = $logger;
|
||||
$this->settings = $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure REST API routes.
|
||||
*/
|
||||
public function register_routes() {
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base,
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'callback' => array( $this, 'connect_manual' ),
|
||||
'permission_callback' => array( $this, 'check_permission' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves merchantId and email.
|
||||
*
|
||||
* @param WP_REST_Request $request Full data about the request.
|
||||
*/
|
||||
public function connect_manual( WP_REST_Request $request ) : WP_REST_Response {
|
||||
$data = $this->sanitize_for_wordpress(
|
||||
$request->get_params(),
|
||||
$this->field_map
|
||||
);
|
||||
|
||||
$client_id = $data['client_id'] ?? '';
|
||||
$client_secret = $data['client_secret'] ?? '';
|
||||
$use_sandbox = (bool) ( $data['use_sandbox'] ?? false );
|
||||
|
||||
if ( empty( $client_id ) || empty( $client_secret ) ) {
|
||||
return $this->return_error( 'No client ID or secret provided.' );
|
||||
}
|
||||
|
||||
try {
|
||||
$payee = $this->request_payee( $client_id, $client_secret, $use_sandbox );
|
||||
} catch ( Exception $exception ) {
|
||||
return $this->return_error( $exception->getMessage() );
|
||||
}
|
||||
|
||||
if ( $use_sandbox ) {
|
||||
$this->settings->set_is_sandbox( true );
|
||||
$this->settings->set_sandbox_client_id( $client_id );
|
||||
$this->settings->set_sandbox_client_secret( $client_secret );
|
||||
$this->settings->set_sandbox_merchant_id( $payee->merchant_id );
|
||||
$this->settings->set_sandbox_merchant_email( $payee->email_address );
|
||||
} else {
|
||||
$this->settings->set_is_sandbox( false );
|
||||
$this->settings->set_live_client_id( $client_id );
|
||||
$this->settings->set_live_client_secret( $client_secret );
|
||||
$this->settings->set_live_merchant_id( $payee->merchant_id );
|
||||
$this->settings->set_live_merchant_email( $payee->email_address );
|
||||
}
|
||||
$this->settings->save();
|
||||
|
||||
return $this->return_success(
|
||||
array(
|
||||
'merchantId' => $payee->merchant_id,
|
||||
'email' => $payee->email_address,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the payee object with the merchant data
|
||||
* by creating a minimal PayPal order.
|
||||
*
|
||||
* @throws Exception When failed to retrieve payee.
|
||||
*
|
||||
* phpcs:disable Squiz.Commenting
|
||||
* phpcs:disable Generic.Commenting
|
||||
*
|
||||
* @param string $client_secret The client secret.
|
||||
* @param bool $use_sandbox Whether to use the sandbox mode.
|
||||
* @param string $client_id The client ID.
|
||||
*
|
||||
* @return stdClass The payee object.
|
||||
*/
|
||||
private function request_payee(
|
||||
string $client_id,
|
||||
string $client_secret,
|
||||
bool $use_sandbox
|
||||
) : stdClass {
|
||||
|
||||
$host = $use_sandbox ? $this->sandbox_host : $this->live_host;
|
||||
|
||||
$bearer = new PayPalBearer(
|
||||
new InMemoryCache(),
|
||||
$host,
|
||||
$client_id,
|
||||
$client_secret,
|
||||
$this->logger,
|
||||
null
|
||||
);
|
||||
|
||||
$orders = new Orders(
|
||||
$host,
|
||||
$bearer,
|
||||
$this->logger
|
||||
);
|
||||
|
||||
$request_body = array(
|
||||
'intent' => 'CAPTURE',
|
||||
'purchase_units' => array(
|
||||
array(
|
||||
'amount' => array(
|
||||
'currency_code' => 'USD',
|
||||
'value' => 1.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$response = $orders->create( $request_body );
|
||||
$body = json_decode( $response['body'] );
|
||||
|
||||
$order_id = $body->id;
|
||||
|
||||
$order_response = $orders->order( $order_id );
|
||||
$order_body = json_decode( $order_response['body'] );
|
||||
|
||||
$pu = $order_body->purchase_units[0];
|
||||
$payee = $pu->payee;
|
||||
if ( ! is_object( $payee ) ) {
|
||||
throw new RuntimeException( 'Payee not found.' );
|
||||
}
|
||||
if ( ! isset( $payee->merchant_id ) || ! isset( $payee->email_address ) ) {
|
||||
throw new RuntimeException( 'Payee info not found.' );
|
||||
}
|
||||
|
||||
return $payee;
|
||||
}
|
||||
}
|
|
@ -15,7 +15,14 @@ use WP_REST_Request;
|
|||
use WooCommerce\PayPalCommerce\Settings\Service\ConnectionUrlGenerator;
|
||||
|
||||
/**
|
||||
* REST controller that generates merchant login URLs.
|
||||
* REST controller that generates merchant login URLs for PayPal.
|
||||
*
|
||||
* This endpoint is responsible solely for generating a URL that initiates
|
||||
* the PayPal login flow. It does not handle the authentication itself.
|
||||
*
|
||||
* The generated URL is typically used to redirect merchants to PayPal's login page.
|
||||
* After successful login, the authentication process is completed via the
|
||||
* AuthenticationRestEndpoint.
|
||||
*/
|
||||
class LoginLinkRestEndpoint extends RestEndpoint {
|
||||
/**
|
||||
|
@ -26,48 +33,47 @@ class LoginLinkRestEndpoint extends RestEndpoint {
|
|||
protected $rest_base = 'login_link';
|
||||
|
||||
/**
|
||||
* Link generator list, with environment name as array key.
|
||||
* Login-URL generator.
|
||||
*
|
||||
* @var ConnectionUrlGenerator[]
|
||||
* @var ConnectionUrlGenerator
|
||||
*/
|
||||
protected array $url_generators;
|
||||
protected ConnectionUrlGenerator $url_generator;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param ConnectionUrlGenerator[] $url_generators Array of environment-specific URL generators.
|
||||
* @param ConnectionUrlGenerator $url_generator Login-URL generator.
|
||||
*/
|
||||
public function __construct( array $url_generators ) {
|
||||
$this->url_generators = $url_generators;
|
||||
public function __construct( ConnectionUrlGenerator $url_generator ) {
|
||||
$this->url_generator = $url_generator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure REST API routes.
|
||||
*/
|
||||
public function register_routes() {
|
||||
public function register_routes() : void {
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base,
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'callback' => array( $this, 'get_login_url' ),
|
||||
'permission_callback' => array( $this, 'check_permission' ),
|
||||
'args' => array(
|
||||
'environment' => array(
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
),
|
||||
'products' => array(
|
||||
'required' => true,
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
'type' => 'string',
|
||||
),
|
||||
'sanitize_callback' => function ( $products ) {
|
||||
return array_map( 'sanitize_text_field', $products );
|
||||
},
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'callback' => array( $this, 'get_login_url' ),
|
||||
'permission_callback' => array( $this, 'check_permission' ),
|
||||
'args' => array(
|
||||
'useSandbox' => array(
|
||||
'default' => 0,
|
||||
'type' => 'boolean',
|
||||
'sanitize_callback' => array( $this, 'to_boolean' ),
|
||||
),
|
||||
'products' => array(
|
||||
'required' => true,
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
'type' => 'string',
|
||||
),
|
||||
'sanitize_callback' => function ( $products ) {
|
||||
return array_map( 'sanitize_text_field', $products );
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
@ -82,20 +88,11 @@ class LoginLinkRestEndpoint extends RestEndpoint {
|
|||
* @return WP_REST_Response The login URL or an error response.
|
||||
*/
|
||||
public function get_login_url( WP_REST_Request $request ) : WP_REST_Response {
|
||||
$environment = $request->get_param( 'environment' );
|
||||
$use_sandbox = $request->get_param( 'useSandbox' );
|
||||
$products = $request->get_param( 'products' );
|
||||
|
||||
if ( ! isset( $this->url_generators[ $environment ] ) ) {
|
||||
return new WP_REST_Response(
|
||||
array( 'error' => 'Invalid environment specified.' ),
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
$url_generator = $this->url_generators[ $environment ];
|
||||
|
||||
try {
|
||||
$url = $url_generator->generate( $products );
|
||||
$url = $this->url_generator->generate( $products, $use_sandbox );
|
||||
|
||||
return $this->return_success( $url );
|
||||
} catch ( \Exception $e ) {
|
||||
|
|
|
@ -101,11 +101,9 @@ class OnboardingRestEndpoint extends RestEndpoint {
|
|||
$this->namespace,
|
||||
'/' . $this->rest_base,
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_details' ),
|
||||
'permission_callback' => array( $this, 'check_permission' ),
|
||||
),
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_details' ),
|
||||
'permission_callback' => array( $this, 'check_permission' ),
|
||||
)
|
||||
);
|
||||
|
||||
|
@ -113,11 +111,9 @@ class OnboardingRestEndpoint extends RestEndpoint {
|
|||
$this->namespace,
|
||||
'/' . $this->rest_base,
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'callback' => array( $this, 'update_details' ),
|
||||
'permission_callback' => array( $this, 'check_permission' ),
|
||||
),
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'callback' => array( $this, 'update_details' ),
|
||||
'permission_callback' => array( $this, 'check_permission' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* @package WooCommerce\PayPalCommerce\Settings\Endpoint
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
declare( strict_types = 1 );
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\Settings\Endpoint;
|
||||
|
||||
|
@ -87,11 +87,9 @@ class RefreshFeatureStatusEndpoint extends RestEndpoint {
|
|||
$this->namespace,
|
||||
'/' . $this->rest_base,
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'callback' => array( $this, 'refresh_status' ),
|
||||
'permission_callback' => array( $this, 'check_permission' ),
|
||||
),
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'callback' => array( $this, 'refresh_status' ),
|
||||
'permission_callback' => array( $this, 'check_permission' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -102,7 +100,7 @@ class RefreshFeatureStatusEndpoint extends RestEndpoint {
|
|||
* @param WP_REST_Request $request Full data about the request.
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public function refresh_status( WP_REST_Request $request ): WP_REST_Response {
|
||||
public function refresh_status( WP_REST_Request $request ) : WP_REST_Response {
|
||||
$now = time();
|
||||
$last_request_time = $this->cache->get( self::CACHE_KEY ) ?: 0;
|
||||
$seconds_missing = $last_request_time + self::TIMEOUT - $now;
|
||||
|
|
|
@ -81,7 +81,7 @@ abstract class RestEndpoint extends WC_REST_Controller {
|
|||
}
|
||||
|
||||
/**
|
||||
* Sanitizes parameters based on a field mapping.
|
||||
* Sanitizes and renames input parameters, based on a field mapping.
|
||||
*
|
||||
* This method iterates through a field map, applying sanitization methods
|
||||
* to the corresponding values in the input parameters array.
|
||||
|
@ -122,7 +122,7 @@ abstract class RestEndpoint extends WC_REST_Controller {
|
|||
}
|
||||
|
||||
/**
|
||||
* Sanitizes data for JavaScript based on a field mapping.
|
||||
* Sanitizes and renames data for JavaScript, based on a field mapping.
|
||||
*
|
||||
* This method transforms the input data array according to the provided field map,
|
||||
* renaming keys to their JavaScript equivalents as specified in the mapping.
|
||||
|
@ -151,24 +151,28 @@ abstract class RestEndpoint extends WC_REST_Controller {
|
|||
}
|
||||
|
||||
/**
|
||||
* Convert a value to a boolean.
|
||||
* Sanitation callback: Convert a value to a boolean.
|
||||
*
|
||||
* @param mixed $value The value to convert.
|
||||
* @param mixed $value The value to sanitize.
|
||||
*
|
||||
* @return bool|null The boolean value, or null if not set.
|
||||
*/
|
||||
protected function to_boolean( $value ) : ?bool {
|
||||
public function to_boolean( $value ) : ?bool {
|
||||
return $value !== null ? (bool) $value : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a value to a number.
|
||||
* Sanitation callback: Convert a value to a number.
|
||||
*
|
||||
* @param mixed $value The value to convert.
|
||||
* @param mixed $value The value to sanitize.
|
||||
*
|
||||
* @return int|float|null The numeric value, or null if not set.
|
||||
*/
|
||||
protected function to_number( $value ) {
|
||||
return $value !== null ? ( is_numeric( $value ) ? $value + 0 : null ) : null;
|
||||
public function to_number( $value ) {
|
||||
if ( $value !== null ) {
|
||||
$value = is_numeric( $value ) ? $value + 0 : null;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
|
205
modules/ppcp-settings/src/Endpoint/WebhookSettingsEndpoint.php
Normal file
205
modules/ppcp-settings/src/Endpoint/WebhookSettingsEndpoint.php
Normal file
|
@ -0,0 +1,205 @@
|
|||
<?php
|
||||
/**
|
||||
* REST endpoint to manage the onboarding module.
|
||||
*
|
||||
* @package WooCommerce\PayPalCommerce\Settings\Endpoint
|
||||
*/
|
||||
|
||||
declare( strict_types = 1 );
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\Settings\Endpoint;
|
||||
|
||||
use Throwable;
|
||||
use WP_REST_Response;
|
||||
use WP_REST_Server;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\WebhookEndpoint;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\Webhook;
|
||||
use WooCommerce\PayPalCommerce\Webhooks\Status\WebhookSimulation;
|
||||
use WooCommerce\PayPalCommerce\Webhooks\WebhookRegistrar;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* Class WebhookSettingsEndpoint
|
||||
*
|
||||
* Note: Endpoint for webhook related requests
|
||||
*/
|
||||
class WebhookSettingsEndpoint extends RestEndpoint {
|
||||
/**
|
||||
* Endpoint base to fetch webhook settings and resubscribe
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rest_base = 'webhook_settings';
|
||||
|
||||
/**
|
||||
* Endpoint base to start webhook simulation and check the state
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $rest_simulate_base = 'webhook_simulate';
|
||||
|
||||
/**
|
||||
* Application webhook endpoint
|
||||
*
|
||||
* @var WebhookEndpoint
|
||||
*/
|
||||
private WebhookEndpoint $webhook_endpoint;
|
||||
|
||||
/**
|
||||
* A service that allows resubscribing webhooks
|
||||
*
|
||||
* @var WebhookRegistrar
|
||||
*/
|
||||
private WebhookRegistrar $webhook_registrar;
|
||||
|
||||
/**
|
||||
* A service that allows webhook simulations
|
||||
*
|
||||
* @var WebhookSimulation
|
||||
*/
|
||||
private WebhookSimulation $webhook_simulation;
|
||||
|
||||
/**
|
||||
* WebhookSettingsEndpoint constructor.
|
||||
*
|
||||
* @param WebhookEndpoint $webhook_endpoint A list of subscribed webhooks and a webhook
|
||||
* endpoint URL.
|
||||
* @param WebhookRegistrar $webhook_registrar A service that allows resubscribing webhooks.
|
||||
* @param WebhookSimulation $webhook_simulation A service that allows webhook simulations.
|
||||
*/
|
||||
public function __construct( WebhookEndpoint $webhook_endpoint, WebhookRegistrar $webhook_registrar, WebhookSimulation $webhook_simulation ) {
|
||||
$this->webhook_endpoint = $webhook_endpoint;
|
||||
$this->webhook_registrar = $webhook_registrar;
|
||||
$this->webhook_simulation = $webhook_simulation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure REST API routes.
|
||||
*/
|
||||
public function register_routes() : void {
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base,
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_webhooks' ),
|
||||
'permission_callback' => array( $this, 'check_permission' ),
|
||||
),
|
||||
array(
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => array( $this, 'resubscribe_webhooks' ),
|
||||
'permission_callback' => array( $this, 'check_permission' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_simulate_base,
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'check_simulated_webhook_state' ),
|
||||
'permission_callback' => array( $this, 'check_permission' ),
|
||||
),
|
||||
array(
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => array( $this, 'simulate_webhooks_start' ),
|
||||
'permission_callback' => array( $this, 'check_permission' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a webhook endpoint URL and list of subscribed webhooks
|
||||
*
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public function get_webhooks() : WP_REST_Response {
|
||||
$webhooks = $this->get_webhook_data();
|
||||
if ( ! $webhooks ) {
|
||||
return $this->return_error( 'No webhooks found.' );
|
||||
}
|
||||
|
||||
try {
|
||||
$webhook_url = $webhooks->url();
|
||||
$webhook_events = array_map(
|
||||
static fn( stdClass $webhooks ) => strtolower( $webhooks->name ),
|
||||
$webhooks->event_types()
|
||||
);
|
||||
} catch ( Throwable $error ) {
|
||||
return $this->return_error( $error->getMessage() );
|
||||
}
|
||||
|
||||
return $this->return_success(
|
||||
array(
|
||||
'url' => $webhook_url,
|
||||
'events' => $webhook_events,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-subscribes webhooks and returns webhooks
|
||||
*
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public function resubscribe_webhooks() : WP_REST_Response {
|
||||
if ( ! $this->webhook_registrar->register() ) {
|
||||
return $this->return_error( 'Webhook subscription failed.' );
|
||||
}
|
||||
|
||||
return $this->get_webhooks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts webhook simulation
|
||||
*
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public function simulate_webhooks_start() : WP_REST_Response {
|
||||
try {
|
||||
$this->webhook_simulation->start();
|
||||
|
||||
return $this->return_success( array() );
|
||||
} catch ( \Exception $error ) {
|
||||
return $this->return_error( $error->getMessage() );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks webhook simulation state
|
||||
*
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public function check_simulated_webhook_state() : WP_REST_Response {
|
||||
try {
|
||||
$state = $this->webhook_simulation->get_state();
|
||||
|
||||
return $this->return_success(
|
||||
array( 'state' => $state )
|
||||
);
|
||||
|
||||
} catch ( \Exception $error ) {
|
||||
return $this->return_error( $error->getMessage() );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieves the Webhooks API response object.
|
||||
*
|
||||
* @return Webhook|null The webhook data instance, or null.
|
||||
*/
|
||||
private function get_webhook_data() : ?Webhook {
|
||||
try {
|
||||
$api_response = $this->webhook_endpoint->list();
|
||||
|
||||
return $api_response[0] ?? null;
|
||||
} catch ( Throwable $error ) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10,10 +10,13 @@ declare( strict_types = 1 );
|
|||
|
||||
namespace WooCommerce\PayPalCommerce\Settings\Handler;
|
||||
|
||||
use WooCommerce\PayPalCommerce\Settings\Data\CommonSettings;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use RuntimeException;
|
||||
use WooCommerce\PayPalCommerce\Settings\Service\AuthenticationManager;
|
||||
use WooCommerce\PayPalCommerce\Settings\Service\OnboardingUrlManager;
|
||||
use WooCommerce\WooCommerce\Logging\Logger\NullLogger;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
|
||||
use WooCommerce\PayPalCommerce\Http\RedirectorInterface;
|
||||
|
||||
/**
|
||||
* Provides a listener that handles merchant-connection requests.
|
||||
|
@ -31,13 +34,6 @@ class ConnectionListener {
|
|||
*/
|
||||
private string $settings_page_id;
|
||||
|
||||
/**
|
||||
* Access to connection settings.
|
||||
*
|
||||
* @var CommonSettings
|
||||
*/
|
||||
private CommonSettings $settings;
|
||||
|
||||
/**
|
||||
* Access to the onboarding URL manager.
|
||||
*
|
||||
|
@ -45,6 +41,21 @@ class ConnectionListener {
|
|||
*/
|
||||
private OnboardingUrlManager $url_manager;
|
||||
|
||||
/**
|
||||
* Authentication manager service, responsible to update connection details.
|
||||
*
|
||||
* @var AuthenticationManager
|
||||
*/
|
||||
private AuthenticationManager $authentication_manager;
|
||||
|
||||
/**
|
||||
* A redirector-instance to redirect the merchant after authentication.
|
||||
* ™
|
||||
*
|
||||
* @var RedirectorInterface
|
||||
*/
|
||||
private RedirectorInterface $redirector;
|
||||
|
||||
/**
|
||||
* Logger instance, mainly used for debugging purposes.
|
||||
*
|
||||
|
@ -62,16 +73,24 @@ class ConnectionListener {
|
|||
/**
|
||||
* Prepare the instance.
|
||||
*
|
||||
* @param string $settings_page_id Current plugin settings page ID.
|
||||
* @param CommonSettings $settings Access to saved connection details.
|
||||
* @param OnboardingUrlManager $url_manager Get OnboardingURL instances.
|
||||
* @param ?LoggerInterface $logger The logger, for debugging purposes.
|
||||
* @param string $settings_page_id Current plugin settings page ID.
|
||||
* @param OnboardingUrlManager $url_manager Get OnboardingURL instances.
|
||||
* @param AuthenticationManager $authentication_manager Authentication manager service.
|
||||
* @param RedirectorInterface $redirector Redirect-handler.
|
||||
* @param ?LoggerInterface $logger The logger, for debugging purposes.
|
||||
*/
|
||||
public function __construct( string $settings_page_id, CommonSettings $settings, OnboardingUrlManager $url_manager, LoggerInterface $logger = null ) {
|
||||
$this->settings_page_id = $settings_page_id;
|
||||
$this->settings = $settings;
|
||||
$this->url_manager = $url_manager;
|
||||
$this->logger = $logger ?: new NullLogger();
|
||||
public function __construct(
|
||||
string $settings_page_id,
|
||||
OnboardingUrlManager $url_manager,
|
||||
AuthenticationManager $authentication_manager,
|
||||
RedirectorInterface $redirector,
|
||||
LoggerInterface $logger = null
|
||||
) {
|
||||
$this->settings_page_id = $settings_page_id;
|
||||
$this->url_manager = $url_manager;
|
||||
$this->authentication_manager = $authentication_manager;
|
||||
$this->redirector = $redirector;
|
||||
$this->logger = $logger ?: new NullLogger();
|
||||
|
||||
// Initialize as "guest", the real ID is provided via process().
|
||||
$this->user_id = 0;
|
||||
|
@ -82,6 +101,8 @@ class ConnectionListener {
|
|||
*
|
||||
* @param int $user_id The current user ID.
|
||||
* @param array $request Request details to process.
|
||||
*
|
||||
* @throws RuntimeException If the merchant ID does not match the ID previously set via OAuth.
|
||||
*/
|
||||
public function process( int $user_id, array $request ) : void {
|
||||
$this->user_id = $user_id;
|
||||
|
@ -100,13 +121,15 @@ class ConnectionListener {
|
|||
return;
|
||||
}
|
||||
|
||||
$this->logger->info( 'Found merchant data in request', $data );
|
||||
$this->logger->info( 'Found OAuth merchant data in request', $data );
|
||||
|
||||
$this->store_data(
|
||||
$data['is_sandbox'],
|
||||
$data['merchant_id'],
|
||||
$data['merchant_email']
|
||||
);
|
||||
try {
|
||||
$this->authentication_manager->finish_oauth_authentication( $data );
|
||||
} catch ( \Exception $e ) {
|
||||
$this->logger->error( 'Failed to complete authentication: ' . $e->getMessage() );
|
||||
}
|
||||
|
||||
$this->redirect_after_authentication();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -117,7 +140,7 @@ class ConnectionListener {
|
|||
*
|
||||
* @return bool True, if the request contains valid connection details.
|
||||
*/
|
||||
protected function is_valid_request( array $request ) : bool {
|
||||
private function is_valid_request( array $request ) : bool {
|
||||
if ( $this->user_id < 1 || ! $this->settings_page_id ) {
|
||||
return false;
|
||||
}
|
||||
|
@ -149,7 +172,7 @@ class ConnectionListener {
|
|||
* @return array Structured array with 'is_sandbox', 'merchant_id', and 'merchant_email' keys,
|
||||
* or an empty array on failure.
|
||||
*/
|
||||
protected function extract_data( array $request ) : array {
|
||||
private function extract_data( array $request ) : array {
|
||||
$this->logger->info( 'Extracting connection data from request...' );
|
||||
|
||||
$merchant_id = $this->get_merchant_id_from_request( $request );
|
||||
|
@ -160,24 +183,20 @@ class ConnectionListener {
|
|||
}
|
||||
|
||||
return array(
|
||||
'is_sandbox' => $this->settings->get_sandbox(),
|
||||
'merchant_id' => $merchant_id,
|
||||
'merchant_email' => $merchant_email,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist the merchant details to the database.
|
||||
* Redirects the browser page at the end of the authentication flow.
|
||||
*
|
||||
* @param bool $is_sandbox Whether the details are for a sandbox account.
|
||||
* @param string $merchant_id The anonymized merchant ID.
|
||||
* @param string $merchant_email The merchant's email.
|
||||
* @return void
|
||||
*/
|
||||
protected function store_data( bool $is_sandbox, string $merchant_id, string $merchant_email ) : void {
|
||||
$this->logger->info( "Save merchant details to the DB: $merchant_email ($merchant_id)" );
|
||||
private function redirect_after_authentication() : void {
|
||||
$redirect_url = $this->get_onboarding_redirect_url();
|
||||
|
||||
$this->settings->set_merchant_data( $is_sandbox, $merchant_id, $merchant_email );
|
||||
$this->settings->save();
|
||||
$this->redirector->redirect( $redirect_url );
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -187,7 +206,7 @@ class ConnectionListener {
|
|||
*
|
||||
* @return string The sanitized token, or an empty string.
|
||||
*/
|
||||
protected function get_token_from_request( array $request ) : string {
|
||||
private function get_token_from_request( array $request ) : string {
|
||||
return $this->sanitize_string( $request['ppcpToken'] ?? '' );
|
||||
}
|
||||
|
||||
|
@ -198,7 +217,7 @@ class ConnectionListener {
|
|||
*
|
||||
* @return string The sanitized merchant ID, or an empty string.
|
||||
*/
|
||||
protected function get_merchant_id_from_request( array $request ) : string {
|
||||
private function get_merchant_id_from_request( array $request ) : string {
|
||||
return $this->sanitize_string( $request['merchantIdInPayPal'] ?? '' );
|
||||
}
|
||||
|
||||
|
@ -213,7 +232,7 @@ class ConnectionListener {
|
|||
*
|
||||
* @return string The sanitized merchant email, or an empty string.
|
||||
*/
|
||||
protected function get_merchant_email_from_request( array $request ) : string {
|
||||
private function get_merchant_email_from_request( array $request ) : string {
|
||||
return $this->sanitize_merchant_email( $request['merchantId'] ?? '' );
|
||||
}
|
||||
|
||||
|
@ -224,7 +243,7 @@ class ConnectionListener {
|
|||
*
|
||||
* @return string Sanitized value.
|
||||
*/
|
||||
protected function sanitize_string( string $value ) : string {
|
||||
private function sanitize_string( string $value ) : string {
|
||||
return trim( sanitize_text_field( wp_unslash( $value ) ) );
|
||||
}
|
||||
|
||||
|
@ -235,7 +254,22 @@ class ConnectionListener {
|
|||
*
|
||||
* @return string Sanitized email address.
|
||||
*/
|
||||
protected function sanitize_merchant_email( string $email ) : string {
|
||||
private function sanitize_merchant_email( string $email ) : string {
|
||||
return sanitize_text_field( str_replace( ' ', '+', $email ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URL opened at the end of onboarding.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function get_onboarding_redirect_url() : string {
|
||||
/**
|
||||
* The URL opened at the end of onboarding after saving the merchant ID/email.
|
||||
*/
|
||||
return apply_filters(
|
||||
'woocommerce_paypal_payments_onboarding_redirect_url',
|
||||
admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=ppcp-gateway&ppcp-tab=' . Settings::CONNECTION_TAB_ID )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
429
modules/ppcp-settings/src/Service/AuthenticationManager.php
Normal file
429
modules/ppcp-settings/src/Service/AuthenticationManager.php
Normal file
|
@ -0,0 +1,429 @@
|
|||
<?php
|
||||
/**
|
||||
* Manages the merchant connection between this plugin and PayPal.
|
||||
*
|
||||
* @package WooCommerce\PayPalCommerce\Settings\Service
|
||||
*/
|
||||
|
||||
declare( strict_types = 1 );
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\Settings\Service;
|
||||
|
||||
use JsonException;
|
||||
use Throwable;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Authentication\PayPalBearer;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\LoginSeller;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\Orders;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Helper\InMemoryCache;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Repository\PartnerReferralsData;
|
||||
use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Helper\EnvironmentConfig;
|
||||
use WooCommerce\WooCommerce\Logging\Logger\NullLogger;
|
||||
use WooCommerce\PayPalCommerce\Settings\DTO\MerchantConnectionDTO;
|
||||
use WooCommerce\PayPalCommerce\Webhooks\WebhookRegistrar;
|
||||
|
||||
/**
|
||||
* Class that manages the connection to PayPal.
|
||||
*/
|
||||
class AuthenticationManager {
|
||||
/**
|
||||
* Data model that stores the connection details.
|
||||
*
|
||||
* @var GeneralSettings
|
||||
*/
|
||||
private GeneralSettings $common_settings;
|
||||
|
||||
/**
|
||||
* Logging instance.
|
||||
*
|
||||
* @var LoggerInterface
|
||||
*/
|
||||
private LoggerInterface $logger;
|
||||
|
||||
/**
|
||||
* Base URLs for the manual connection attempt, by environment.
|
||||
*
|
||||
* @var EnvironmentConfig<string>
|
||||
*/
|
||||
private EnvironmentConfig $connection_host;
|
||||
|
||||
/**
|
||||
* Login API handler instances, by environment.
|
||||
*
|
||||
* @var EnvironmentConfig<LoginSeller>
|
||||
*/
|
||||
private EnvironmentConfig $login_endpoint;
|
||||
|
||||
/**
|
||||
* Onboarding referrals data.
|
||||
*
|
||||
* @var PartnerReferralsData
|
||||
*/
|
||||
private PartnerReferralsData $referrals_data;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param GeneralSettings $common_settings Data model that stores the connection details.
|
||||
* @param EnvironmentConfig $connection_host API host for direct authentication.
|
||||
* @param EnvironmentConfig $login_endpoint API handler to fetch merchant credentials.
|
||||
* @param PartnerReferralsData $referrals_data Partner referrals data.
|
||||
* @param ?LoggerInterface $logger Logging instance.
|
||||
*/
|
||||
public function __construct(
|
||||
GeneralSettings $common_settings,
|
||||
EnvironmentConfig $connection_host,
|
||||
EnvironmentConfig $login_endpoint,
|
||||
PartnerReferralsData $referrals_data,
|
||||
?LoggerInterface $logger = null
|
||||
) {
|
||||
$this->common_settings = $common_settings;
|
||||
$this->connection_host = $connection_host;
|
||||
$this->login_endpoint = $login_endpoint;
|
||||
$this->referrals_data = $referrals_data;
|
||||
$this->logger = $logger ?: new NullLogger();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns details about the currently connected merchant.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_account_details() : array {
|
||||
return array(
|
||||
'is_sandbox' => $this->common_settings->is_sandbox_merchant(),
|
||||
'is_connected' => $this->common_settings->is_merchant_connected(),
|
||||
'merchant_id' => $this->common_settings->get_merchant_id(),
|
||||
'merchant_email' => $this->common_settings->get_merchant_email(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes any connection details we currently have stored.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function disconnect() : void {
|
||||
$this->logger->info( 'Disconnecting merchant from PayPal...' );
|
||||
|
||||
$this->common_settings->reset_merchant_data();
|
||||
$this->common_settings->save();
|
||||
|
||||
/**
|
||||
* Broadcast, that the plugin disconnected from PayPal. This allows other
|
||||
* modules to clean up merchant-related details, such as eligibility flags.
|
||||
*/
|
||||
do_action( 'woocommerce_paypal_payments_merchant_disconnected' );
|
||||
|
||||
/**
|
||||
* Request to flush caches after disconnecting the merchant. While there
|
||||
* is no need for it here, it's good house-keeping practice to clean up.
|
||||
*/
|
||||
do_action( 'woocommerce_paypal_payments_flush_api_cache' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the provided ID and secret have a valid format.
|
||||
*
|
||||
* Part of the "Direct Connection" (Manual Connection) flow.
|
||||
*
|
||||
* On failure, an Exception is thrown, while a successful check does not
|
||||
* generate any return value.
|
||||
*
|
||||
* @param string $client_id The client ID.
|
||||
* @param string $client_secret The client secret.
|
||||
* @return void
|
||||
* @throws RuntimeException When invalid client ID or secret provided.
|
||||
*/
|
||||
public function validate_id_and_secret( string $client_id, string $client_secret ) : void {
|
||||
if ( empty( $client_id ) ) {
|
||||
throw new RuntimeException( 'No client ID provided.' );
|
||||
}
|
||||
|
||||
if ( false === preg_match( '/^A[\w-]{79}$/', $client_secret ) ) {
|
||||
throw new RuntimeException( 'Invalid client ID provided.' );
|
||||
}
|
||||
|
||||
if ( empty( $client_secret ) ) {
|
||||
throw new RuntimeException( 'No client secret provided.' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects the current merchant, and then attempts to connect to a
|
||||
* PayPal account using a client ID and secret.
|
||||
*
|
||||
* Part of the "Direct Connection" (Manual Connection) flow.
|
||||
*
|
||||
* @param bool $use_sandbox Whether to use the sandbox mode.
|
||||
* @param string $client_id The client ID.
|
||||
* @param string $client_secret The client secret.
|
||||
* @return void
|
||||
* @throws RuntimeException When failed to retrieve payee.
|
||||
*/
|
||||
public function authenticate_via_direct_api( bool $use_sandbox, string $client_id, string $client_secret ) : void {
|
||||
$this->disconnect();
|
||||
|
||||
$this->logger->info(
|
||||
'Attempting manual connection to PayPal...',
|
||||
array(
|
||||
'sandbox' => $use_sandbox,
|
||||
'client_id' => $client_id,
|
||||
)
|
||||
);
|
||||
|
||||
$payee = $this->request_payee( $client_id, $client_secret, $use_sandbox );
|
||||
|
||||
$connection = new MerchantConnectionDTO(
|
||||
$use_sandbox,
|
||||
$client_id,
|
||||
$client_secret,
|
||||
$payee['merchant_id'],
|
||||
$payee['email_address']
|
||||
);
|
||||
|
||||
$this->update_connection_details( $connection );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if the provided ID and auth-code have a valid format.
|
||||
*
|
||||
* Part of the "ISU Connection" (login via Popup) flow.
|
||||
*
|
||||
* On failure, an Exception is thrown, while a successful check does not
|
||||
* generate any return value. Note, that we did not find official documentation
|
||||
* on those values, so we only check if they are non-empty strings.
|
||||
*
|
||||
* @param string $shared_id The shared onboarding ID.
|
||||
* @param string $auth_code The authorization code.
|
||||
* @return void
|
||||
* @throws RuntimeException When invalid shared ID or auth provided.
|
||||
*/
|
||||
public function validate_id_and_auth_code( string $shared_id, string $auth_code ) : void {
|
||||
if ( empty( $shared_id ) ) {
|
||||
throw new RuntimeException( 'No onboarding ID provided.' );
|
||||
}
|
||||
|
||||
if ( empty( $auth_code ) ) {
|
||||
throw new RuntimeException( 'No authorization code provided.' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects the current merchant, and then attempts to connect to a
|
||||
* PayPal account the onboarding ID and authorization ID.
|
||||
*
|
||||
* Part of the "ISU Connection" (login via Popup) flow.
|
||||
*
|
||||
* @param bool $use_sandbox Whether to use the sandbox mode.
|
||||
* @param string $shared_id The OAuth client ID.
|
||||
* @param string $auth_code The OAuth authorization code.
|
||||
* @return void
|
||||
* @throws RuntimeException When failed to retrieve payee.
|
||||
*/
|
||||
public function authenticate_via_oauth( bool $use_sandbox, string $shared_id, string $auth_code ) : void {
|
||||
$this->disconnect();
|
||||
|
||||
$this->logger->info(
|
||||
'Attempting OAuth login to PayPal...',
|
||||
array(
|
||||
'sandbox' => $use_sandbox,
|
||||
'shared_id' => $shared_id,
|
||||
)
|
||||
);
|
||||
|
||||
$credentials = $this->get_credentials( $shared_id, $auth_code, $use_sandbox );
|
||||
|
||||
/**
|
||||
* The merchant's email is set by `ConnectionListener`. That listener
|
||||
* is invoked during the page reload, once the user clicks the blue
|
||||
* "Return to Store" button in PayPal's login popup.
|
||||
*/
|
||||
$connection = $this->common_settings->get_merchant_data();
|
||||
|
||||
$connection->is_sandbox = $use_sandbox;
|
||||
$connection->client_id = $credentials['client_id'];
|
||||
$connection->client_secret = $credentials['client_secret'];
|
||||
$connection->merchant_id = $credentials['merchant_id'];
|
||||
|
||||
$this->update_connection_details( $connection );
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the merchant details in the final OAuth redirect and extracts
|
||||
* missing credentials from the URL.
|
||||
*
|
||||
* @param array $request_data Array of request parameters to process.
|
||||
* @return void
|
||||
*
|
||||
* @throws RuntimeException Missing or invalid credentials.
|
||||
*/
|
||||
public function finish_oauth_authentication( array $request_data ) : void {
|
||||
$merchant_id = $request_data['merchant_id'];
|
||||
$merchant_email = $request_data['merchant_email'];
|
||||
|
||||
if ( empty( $merchant_id ) || empty( $merchant_email ) ) {
|
||||
throw new RuntimeException( 'Missing merchant ID or email in request' );
|
||||
}
|
||||
|
||||
$connection = $this->common_settings->get_merchant_data();
|
||||
|
||||
if ( $connection->merchant_id && $connection->merchant_id !== $merchant_id ) {
|
||||
throw new RuntimeException( 'Unexpected merchant ID in request' );
|
||||
}
|
||||
|
||||
$connection->merchant_id = $merchant_id;
|
||||
$connection->merchant_email = $merchant_email;
|
||||
|
||||
$this->update_connection_details( $connection );
|
||||
}
|
||||
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Internal helper methods
|
||||
|
||||
|
||||
/**
|
||||
* Retrieves the payee object with the merchant data by creating a minimal PayPal order.
|
||||
*
|
||||
* Part of the "Direct Connection" (Manual Connection) flow.
|
||||
*
|
||||
* @param string $client_id The client ID.
|
||||
* @param string $client_secret The client secret.
|
||||
* @param bool $use_sandbox Whether to use the sandbox mode.
|
||||
*
|
||||
* @return array Payee details, containing 'merchant_id' and 'merchant_email' keys.
|
||||
* @throws RuntimeException When failed to retrieve payee.
|
||||
*/
|
||||
private function request_payee(
|
||||
string $client_id,
|
||||
string $client_secret,
|
||||
bool $use_sandbox
|
||||
) : array {
|
||||
$host = $this->connection_host->get_value( $use_sandbox );
|
||||
|
||||
$bearer = new PayPalBearer(
|
||||
new InMemoryCache(),
|
||||
$host,
|
||||
$client_id,
|
||||
$client_secret,
|
||||
$this->logger,
|
||||
null
|
||||
);
|
||||
|
||||
$orders = new Orders(
|
||||
$host,
|
||||
$bearer,
|
||||
$this->logger
|
||||
);
|
||||
|
||||
$request_body = array(
|
||||
'intent' => 'CAPTURE',
|
||||
'purchase_units' => array(
|
||||
array(
|
||||
'amount' => array(
|
||||
'currency_code' => 'USD',
|
||||
'value' => 1.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
try {
|
||||
$response = $orders->create( $request_body );
|
||||
$body = json_decode( $response['body'], false, 512, JSON_THROW_ON_ERROR );
|
||||
$order_id = $body->id;
|
||||
|
||||
$order_response = $orders->order( $order_id );
|
||||
$order_body = json_decode( $order_response['body'], false, 512, JSON_THROW_ON_ERROR );
|
||||
} catch ( JsonException $exception ) {
|
||||
// Cast JsonException to a RuntimeException.
|
||||
throw new RuntimeException( 'Could not decode JSON response: ' . $exception->getMessage() );
|
||||
} catch ( Throwable $exception ) {
|
||||
// Cast any other Throwable to a RuntimeException.
|
||||
throw new RuntimeException( $exception->getMessage() );
|
||||
}
|
||||
|
||||
$pu = $order_body->purchase_units[0];
|
||||
$payee = $pu->payee;
|
||||
|
||||
if ( ! is_object( $payee ) ) {
|
||||
throw new RuntimeException( 'Payee not found.' );
|
||||
}
|
||||
if ( ! isset( $payee->merchant_id, $payee->email_address ) ) {
|
||||
throw new RuntimeException( 'Payee info not found.' );
|
||||
}
|
||||
|
||||
return array(
|
||||
'merchant_id' => $payee->merchant_id,
|
||||
'email_address' => $payee->email_address,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches merchant API credentials using a shared onboarding ID and
|
||||
* authorization code.
|
||||
*
|
||||
* Part of the "ISU Connection" (login via Popup) flow.
|
||||
*
|
||||
* @param string $shared_id The shared onboarding ID.
|
||||
* @param string $auth_code The authorization code.
|
||||
* @param bool $use_sandbox Whether to use the sandbox mode.
|
||||
* @return array
|
||||
* @throws RuntimeException When failed to fetch credentials.
|
||||
*/
|
||||
private function get_credentials( string $shared_id, string $auth_code, bool $use_sandbox ) : array {
|
||||
$login_handler = $this->login_endpoint->get_value( $use_sandbox );
|
||||
$nonce = $this->referrals_data->nonce();
|
||||
|
||||
$response = $login_handler->credentials_for( $shared_id, $auth_code, $nonce );
|
||||
|
||||
return array(
|
||||
'client_id' => (string) ( $response->client_id ?? '' ),
|
||||
'client_secret' => (string) ( $response->client_secret ?? '' ),
|
||||
'merchant_id' => (string) ( $response->payer_id ?? '' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the provided details in the data model.
|
||||
*
|
||||
* @param MerchantConnectionDTO $connection Connection details to persist.
|
||||
* @return void
|
||||
*/
|
||||
private function update_connection_details( MerchantConnectionDTO $connection ) : void {
|
||||
$this->logger->info(
|
||||
'Updating connection details',
|
||||
(array) $connection
|
||||
);
|
||||
|
||||
$this->common_settings->set_merchant_data( $connection );
|
||||
$this->common_settings->save();
|
||||
|
||||
if ( $this->common_settings->is_merchant_connected() ) {
|
||||
$this->logger->info( 'Merchant successfully connected to PayPal' );
|
||||
|
||||
/**
|
||||
* Request to flush caches before authenticating the merchant, to
|
||||
* ensure the new merchant does not use stale data from previous
|
||||
* connections.
|
||||
*/
|
||||
do_action( 'woocommerce_paypal_payments_flush_api_cache' );
|
||||
|
||||
/**
|
||||
* Broadcast that the plugin connected to a new PayPal merchant account.
|
||||
* This is the right time to initialize merchant relative flags for the
|
||||
* first time.
|
||||
*/
|
||||
do_action( 'woocommerce_paypal_payments_authenticated_merchant' );
|
||||
|
||||
/**
|
||||
* Subscribe the new merchant to relevant PayPal webhooks.
|
||||
*/
|
||||
do_action( WebhookRegistrar::EVENT_HOOK );
|
||||
}
|
||||
}
|
||||
}
|
|
@ -12,9 +12,9 @@ namespace WooCommerce\PayPalCommerce\Settings\Service;
|
|||
use Exception;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnerReferrals;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Repository\PartnerReferralsData;
|
||||
use WooCommerce\WooCommerce\Logging\Logger\NullLogger;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Helper\EnvironmentConfig;
|
||||
|
||||
// TODO: Replace the OnboardingUrl with a new implementation for this module.
|
||||
use WooCommerce\PayPalCommerce\Onboarding\Helper\OnboardingUrl;
|
||||
|
@ -26,9 +26,9 @@ class ConnectionUrlGenerator {
|
|||
/**
|
||||
* The partner referrals endpoint.
|
||||
*
|
||||
* @var PartnerReferrals
|
||||
* @var EnvironmentConfig<PartnerReferrals>
|
||||
*/
|
||||
protected PartnerReferrals $partner_referrals;
|
||||
protected EnvironmentConfig $partner_referrals;
|
||||
|
||||
/**
|
||||
* The default partner referrals data.
|
||||
|
@ -44,13 +44,6 @@ class ConnectionUrlGenerator {
|
|||
*/
|
||||
protected OnboardingUrlManager $url_manager;
|
||||
|
||||
/**
|
||||
* Which environment is used for the connection URL.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $environment = '';
|
||||
|
||||
/**
|
||||
* The logger
|
||||
*
|
||||
|
@ -63,36 +56,23 @@ class ConnectionUrlGenerator {
|
|||
*
|
||||
* Initializes the cache and logger properties of the class.
|
||||
*
|
||||
* @param PartnerReferrals $partner_referrals PartnerReferrals for URL generation.
|
||||
* @param EnvironmentConfig $partner_referrals PartnerReferrals for URL generation.
|
||||
* @param PartnerReferralsData $referrals_data Default partner referrals data.
|
||||
* @param string $environment Environment that is used to generate the URL.
|
||||
* ['production'|'sandbox'].
|
||||
* @param OnboardingUrlManager $url_manager Manages access to OnboardingUrl instances.
|
||||
* @param ?LoggerInterface $logger The logger object for logging messages.
|
||||
*/
|
||||
public function __construct(
|
||||
PartnerReferrals $partner_referrals,
|
||||
EnvironmentConfig $partner_referrals,
|
||||
PartnerReferralsData $referrals_data,
|
||||
string $environment,
|
||||
OnboardingUrlManager $url_manager,
|
||||
?LoggerInterface $logger = null
|
||||
) {
|
||||
$this->partner_referrals = $partner_referrals;
|
||||
$this->referrals_data = $referrals_data;
|
||||
$this->environment = $environment;
|
||||
$this->url_manager = $url_manager;
|
||||
$this->logger = $logger ?: new NullLogger();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the environment for which the URL is being generated.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function environment() : string {
|
||||
return $this->environment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a PayPal onboarding URL for merchant sign-up.
|
||||
*
|
||||
|
@ -100,13 +80,14 @@ class ConnectionUrlGenerator {
|
|||
* It handles caching of the URL, generation of new URLs when necessary,
|
||||
* and works for both production and sandbox environments.
|
||||
*
|
||||
* @param array $products An array of product identifiers to include in the sign-up process.
|
||||
* These determine the PayPal onboarding experience.
|
||||
* @param array $products An array of product identifiers to include in the sign-up process.
|
||||
* These determine the PayPal onboarding experience.
|
||||
* @param bool $use_sandbox Whether to generate a sandbox URL.
|
||||
*
|
||||
* @return string The generated PayPal onboarding URL.
|
||||
*/
|
||||
public function generate( array $products = array() ) : string {
|
||||
$cache_key = $this->cache_key( $products );
|
||||
public function generate( array $products = array(), bool $use_sandbox = false ) : string {
|
||||
$cache_key = $this->cache_key( $products, $use_sandbox );
|
||||
$user_id = get_current_user_id();
|
||||
$onboarding_url = $this->url_manager->get( $cache_key, $user_id );
|
||||
$cached_url = $this->try_get_from_cache( $onboarding_url, $cache_key );
|
||||
|
@ -119,7 +100,7 @@ class ConnectionUrlGenerator {
|
|||
|
||||
$this->logger->info( 'Generating onboarding URL for: ' . $cache_key );
|
||||
|
||||
$url = $this->generate_new_url( $products, $onboarding_url, $cache_key );
|
||||
$url = $this->generate_new_url( $use_sandbox, $products, $onboarding_url, $cache_key );
|
||||
|
||||
if ( $url ) {
|
||||
$this->persist_url( $onboarding_url, $url );
|
||||
|
@ -131,15 +112,18 @@ class ConnectionUrlGenerator {
|
|||
/**
|
||||
* Generates a cache key from the environment and sorted product array.
|
||||
*
|
||||
* @param array $products Product identifiers that are part of the cache key.
|
||||
* @param array $products Product identifiers that are part of the cache key.
|
||||
* @param bool $for_sandbox Whether the cache contains a sandbox URL.
|
||||
*
|
||||
* @return string The cache key, defining the product list and environment.
|
||||
*/
|
||||
protected function cache_key( array $products = array() ) : string {
|
||||
protected function cache_key( array $products, bool $for_sandbox ) : string {
|
||||
$environment = $for_sandbox ? 'sandbox' : 'production';
|
||||
|
||||
// Sort products alphabetically, to improve cache implementation.
|
||||
sort( $products );
|
||||
|
||||
return $this->environment() . '-' . implode( '-', $products );
|
||||
return $environment . '-' . implode( '-', $products );
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -168,13 +152,14 @@ class ConnectionUrlGenerator {
|
|||
/**
|
||||
* Generates a new URL.
|
||||
*
|
||||
* @param bool $for_sandbox Whether to generate a sandbox URL.
|
||||
* @param array $products The products array.
|
||||
* @param OnboardingUrl $onboarding_url The OnboardingUrl object.
|
||||
* @param string $cache_key The cache key.
|
||||
*
|
||||
* @return string The generated URL or an empty string on failure.
|
||||
*/
|
||||
protected function generate_new_url( array $products, OnboardingUrl $onboarding_url, string $cache_key ) : string {
|
||||
protected function generate_new_url( bool $for_sandbox, array $products, OnboardingUrl $onboarding_url, string $cache_key ) : string {
|
||||
$query_args = array( 'displayMode' => 'minibrowser' );
|
||||
$onboarding_url->init();
|
||||
|
||||
|
@ -189,7 +174,8 @@ class ConnectionUrlGenerator {
|
|||
$data = $this->prepare_referral_data( $products, $onboarding_token );
|
||||
|
||||
try {
|
||||
$url = $this->partner_referrals->signup_link( $data );
|
||||
$referral = $this->partner_referrals->get_value( $for_sandbox );
|
||||
$url = $referral->signup_link( $data );
|
||||
} catch ( Exception $e ) {
|
||||
$this->logger->warning( 'Could not generate an onboarding URL for: ' . $cache_key );
|
||||
|
||||
|
|
|
@ -9,8 +9,9 @@ declare( strict_types = 1 );
|
|||
|
||||
namespace WooCommerce\PayPalCommerce\Settings;
|
||||
|
||||
use WooCommerce\PayPalCommerce\Settings\Ajax\SwitchSettingsUiEndpoint;
|
||||
use WooCommerce\PayPalCommerce\Settings\Data\OnboardingProfile;
|
||||
use WooCommerce\PayPalCommerce\Settings\Endpoint\RestEndpoint;
|
||||
use WooCommerce\PayPalCommerce\Settings\Endpoint\SwitchSettingsUiEndpoint;
|
||||
use WooCommerce\PayPalCommerce\Settings\Handler\ConnectionListener;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait;
|
||||
|
@ -86,7 +87,7 @@ class SettingsModule implements ServiceModule, ExecutableModule {
|
|||
}
|
||||
);
|
||||
|
||||
$endpoint = $container->get( 'settings.switch-ui.endpoint' ) ? $container->get( 'settings.switch-ui.endpoint' ) : null;
|
||||
$endpoint = $container->get( 'settings.ajax.switch_ui' ) ? $container->get( 'settings.ajax.switch_ui' ) : null;
|
||||
assert( $endpoint instanceof SwitchSettingsUiEndpoint );
|
||||
|
||||
add_action(
|
||||
|
@ -181,6 +182,7 @@ class SettingsModule implements ServiceModule, ExecutableModule {
|
|||
$container->get( 'settings.rest.common' ),
|
||||
$container->get( 'settings.rest.connect_manual' ),
|
||||
$container->get( 'settings.rest.login_link' ),
|
||||
$container->get( 'settings.rest.webhooks' ),
|
||||
$container->get( 'settings.rest.refresh_feature_status' ),
|
||||
);
|
||||
|
||||
|
@ -202,6 +204,29 @@ class SettingsModule implements ServiceModule, ExecutableModule {
|
|||
}
|
||||
);
|
||||
|
||||
add_action(
|
||||
'woocommerce_paypal_payments_merchant_disconnected',
|
||||
static function () use ( $container ) : void {
|
||||
$onboarding_profile = $container->get( 'settings.data.onboarding' );
|
||||
assert( $onboarding_profile instanceof OnboardingProfile );
|
||||
|
||||
$onboarding_profile->set_completed( false );
|
||||
$onboarding_profile->set_step( 0 );
|
||||
$onboarding_profile->save();
|
||||
}
|
||||
);
|
||||
|
||||
add_action(
|
||||
'woocommerce_paypal_payments_authenticated_merchant',
|
||||
static function () use ( $container ) : void {
|
||||
$onboarding_profile = $container->get( 'settings.data.onboarding' );
|
||||
assert( $onboarding_profile instanceof OnboardingProfile );
|
||||
|
||||
$onboarding_profile->set_completed( true );
|
||||
$onboarding_profile->save();
|
||||
}
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -2919,6 +2919,15 @@
|
|||
sprintf-js "^1.1.1"
|
||||
tannin "^1.2.0"
|
||||
|
||||
"@wordpress/icons@^10.14.0":
|
||||
version "10.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@wordpress/icons/-/icons-10.14.0.tgz#a27298b438653a9a502eb4ee3b02b42ce516da2e"
|
||||
integrity sha512-4S1AaBeqvTpsTC23y0+4WPiSyz7j+b7vJ4vQ4nqnPeBF7ZeC8J/UXWQnEuKY38n8TiutXljgagkEqGNC9pF2Mw==
|
||||
dependencies:
|
||||
"@babel/runtime" "7.25.7"
|
||||
"@wordpress/element" "*"
|
||||
"@wordpress/primitives" "*"
|
||||
|
||||
"@wordpress/is-shallow-equal@*":
|
||||
version "5.11.0"
|
||||
resolved "https://registry.yarnpkg.com/@wordpress/is-shallow-equal/-/is-shallow-equal-5.11.0.tgz#2f273d6d4de24a66a7a8316b770cf832d22bfc37"
|
||||
|
@ -2968,6 +2977,15 @@
|
|||
resolved "https://registry.yarnpkg.com/@wordpress/prettier-config/-/prettier-config-4.11.0.tgz#6b3f9aa7e2698c0d78e644037c6778b5c1da12ce"
|
||||
integrity sha512-Aoc8+xWOyiXekodjaEjS44z85XK877LzHZqsQuhC0kNgneDLrKkwI5qNgzwzAMbJ9jI58MPqVISCOX0bDLUPbw==
|
||||
|
||||
"@wordpress/primitives@*":
|
||||
version "4.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@wordpress/primitives/-/primitives-4.14.0.tgz#1769f45bc541fd48be2d57626a9f6bdece39942a"
|
||||
integrity sha512-IZibRVbvWoIQ+uynH0N5bmfWz83hD8lJj6jJFhSFuALK+4U5mRGg6tl0ZV0YllR6cjheD9UhTmfrAcOx+gQAjA==
|
||||
dependencies:
|
||||
"@babel/runtime" "7.25.7"
|
||||
"@wordpress/element" "*"
|
||||
clsx "^2.1.1"
|
||||
|
||||
"@wordpress/priority-queue@*":
|
||||
version "3.11.0"
|
||||
resolved "https://registry.yarnpkg.com/@wordpress/priority-queue/-/priority-queue-3.11.0.tgz#01e1570a7a29372bb1d07cd22fd9cbc5b5d03b09"
|
||||
|
@ -3976,6 +3994,11 @@ clone-deep@^4.0.1:
|
|||
kind-of "^6.0.2"
|
||||
shallow-clone "^3.0.0"
|
||||
|
||||
clsx@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
|
||||
integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
|
||||
|
||||
co@^4.6.0:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
|
||||
|
|
|
@ -10,7 +10,7 @@ declare(strict_types=1);
|
|||
namespace WooCommerce\PayPalCommerce\Uninstall;
|
||||
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Repository\PayPalRequestIdRepository;
|
||||
use WooCommerce\PayPalCommerce\Settings\Endpoint\SwitchSettingsUiEndpoint;
|
||||
use WooCommerce\PayPalCommerce\Settings\Ajax\SwitchSettingsUiEndpoint;
|
||||
use WooCommerce\PayPalCommerce\Uninstall\Assets\ClearDatabaseAssets;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway;
|
||||
|
|
|
@ -466,7 +466,7 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
|
|||
continue;
|
||||
}
|
||||
|
||||
$custom_id = $wc_order->get_order_number();
|
||||
$custom_id = (string) $wc_order->get_id();
|
||||
$invoice_id = $this->prefix . $wc_order->get_order_number();
|
||||
$create_order = $this->capture_card_payment->create_order( $token->get_token(), $custom_id, $invoice_id, $wc_order );
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue