mirror of
https://github.com/woocommerce/woocommerce-paypal-payments.git
synced 2025-09-05 08:59:14 +08:00
Merge branch 'trunk' into PCP-3917-things-to-do-next-component-functionality
This commit is contained in:
commit
4d4ab689f5
121 changed files with 5004 additions and 3431 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:
|
||||
|
||||
|
|
2
.github/workflows/package.yml
vendored
2
.github/workflows/package.yml
vendored
|
@ -50,7 +50,7 @@ jobs:
|
|||
if: github.event.inputs.filePrefix
|
||||
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.FILENAME }}
|
||||
path: dist/
|
||||
|
|
3
.github/workflows/php.yml
vendored
3
.github/workflows/php.yml
vendored
|
@ -7,7 +7,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
php-versions: ['7.4', '8.0', '8.1', '8.2', '8.3']
|
||||
php-versions: ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4']
|
||||
|
||||
name: PHP ${{ matrix.php-versions }}
|
||||
steps:
|
||||
|
@ -30,6 +30,7 @@ jobs:
|
|||
run: vendor/bin/phpunit
|
||||
|
||||
- name: Psalm
|
||||
if: ${{ matrix.php-versions == '7.4' }}
|
||||
run: ./vendor/bin/psalm --show-info=false --threads=8 --diff
|
||||
|
||||
- name: Run PHPCS
|
||||
|
|
|
@ -1,22 +1,34 @@
|
|||
*** Changelog ***
|
||||
|
||||
= 2.9.5 - xxxx-xx-xx =
|
||||
Fix - Early translation loading triggers `Function _load_textdomain_just_in_time was called incorrectly.` notice #2816
|
||||
Fix - ACDC card fields not loading and payment not successful when Classic Checkout Smart Button Location disabled #2852
|
||||
Fix - ACDC gateway does not appear for guests when is Fastlane enabled and a subscription product is in the cart #2745
|
||||
Fix - "Voide authorization" button does not appear for Apple Pay/Google Pay orders when payment buttons are separated #2752
|
||||
Fix - Additional payment tokens saved with new customer_id #2820
|
||||
Fix - Vaulted payment method may not be displayed in PayPal button for return buyer #2809
|
||||
Fix - Conflict with EasyShip plugin due to shipping methods loading too early #2845
|
||||
Fix - Restore accidentally removed ACDC currencies #2838
|
||||
Enhancement - Native gateway icon for PayPal & Pay upon Invoice gateways #2712
|
||||
Enhancement - Allow disabling specific card types for Fastlane #2704
|
||||
Enhancement - Fastlane Insights SDK implementation for block Checkout #2737
|
||||
Enhancement - Hide split local APMs in Payments settings tab when PayPal is not enabled #2703
|
||||
Enhancement - Do not load split local APMs on Checkout when PayPal is not enabled #2792
|
||||
Enhancement - Add support for Button Options in the Block Checkout for Apple Pay & Google Pay buttons #2797 #2772
|
||||
Enhancement - Disable “Add payment method” button while saving ACDC payment #2794
|
||||
Enhancement - Sanitize soft_descriptor field #2846 #2854
|
||||
= 2.9.6 - XXXX-XX-XX =
|
||||
* Fix - NOT_ENABLED_TO_VAULT_PAYMENT_SOURCE on PayPal transactions when using ACDC Vaulting without PayPal Vault approval #2955
|
||||
* Fix - Express buttons for Free Trial Subscription products on Block Cart/Checkout trigger CANNOT_BE_ZERO_OR_NEGATIVE error #2872
|
||||
* Fix - String translations not applied to Card Fields on Block Checkout #2934
|
||||
* Fix - Fastlane component included in script when Fastlane is disabled #2911
|
||||
* Fix - Zero amount line items may trigger CANNOT_BE_ZERO_OR_NEGATIVE error after rounding error #2906
|
||||
* Fix - “Save changes” is grey and unclickable when switching from Sandbox to Live #2895
|
||||
* Fix - plugin queries variations when button/messaging is disabled on single product page #2896
|
||||
* Fix - Use get_id instead of get_order_number on setting custom_id (author @0verscore) #2930
|
||||
* Enhancement - Improve fraud response order notes for Advanced Card Processing transactions #2905
|
||||
* Tweak - Update the minimum plugin requirements to WordPress 6.5 & WooCommerce 9.2 #2920
|
||||
|
||||
= 2.9.5 - 2024-12-10 =
|
||||
* Fix - Early translation loading triggers `Function _load_textdomain_just_in_time was called incorrectly.` notice #2816
|
||||
* Fix - ACDC card fields not loading and payment not successful when Classic Checkout Smart Button Location disabled #2852
|
||||
* Fix - ACDC gateway does not appear for guests when is Fastlane enabled and a subscription product is in the cart #2745
|
||||
* Fix - "Voide authorization" button does not appear for Apple Pay/Google Pay orders when payment buttons are separated #2752
|
||||
* Fix - Additional payment tokens saved with new customer_id #2820
|
||||
* Fix - Vaulted payment method may not be displayed in PayPal button for return buyer #2809
|
||||
* Fix - Conflict with EasyShip plugin due to shipping methods loading too early #2845
|
||||
* Fix - Restore accidentally removed ACDC currencies #2838
|
||||
* Enhancement - Native gateway icon for PayPal & Pay upon Invoice gateways #2712
|
||||
* Enhancement - Allow disabling specific card types for Fastlane #2704
|
||||
* Enhancement - Fastlane Insights SDK implementation for block Checkout #2737
|
||||
* Enhancement - Hide split local APMs in Payments settings tab when PayPal is not enabled #2703
|
||||
* Enhancement - Do not load split local APMs on Checkout when PayPal is not enabled #2792
|
||||
* Enhancement - Add support for Button Options in the Block Checkout for Apple Pay & Google Pay buttons #2797 #2772
|
||||
* Enhancement - Disable “Add payment method” button while saving ACDC payment #2794
|
||||
* Enhancement - Sanitize soft_descriptor field #2846 #2854
|
||||
|
||||
= 2.9.4 - 2024-11-11 =
|
||||
* Fix - Apple Pay button preview missing in Standard payment and Advanced Processing tabs #2755
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"name": "woocommerce/woocommerce-paypal-payments",
|
||||
"type": "wordpress-plugin",
|
||||
"description": "PayPal Commerce Platform for WooCommerce",
|
||||
"license": "GPL-2.0",
|
||||
"license": "GPL-2.0-or-later",
|
||||
"require": {
|
||||
"php": "^7.4 | ^8.0",
|
||||
"ext-json": "*",
|
||||
|
|
6
composer.lock
generated
6
composer.lock
generated
|
@ -5541,8 +5541,8 @@
|
|||
"aliases": [],
|
||||
"minimum-stability": "dev",
|
||||
"stability-flags": {
|
||||
"php-stubs/wordpress-stubs": 0,
|
||||
"php-stubs/woocommerce-stubs": 0
|
||||
"php-stubs/woocommerce-stubs": 0,
|
||||
"php-stubs/wordpress-stubs": 0
|
||||
},
|
||||
"prefer-stable": true,
|
||||
"prefer-lowest": false,
|
||||
|
@ -5550,7 +5550,7 @@
|
|||
"php": "^7.4 | ^8.0",
|
||||
"ext-json": "*"
|
||||
},
|
||||
"platform-dev": [],
|
||||
"platform-dev": {},
|
||||
"platform-overrides": {
|
||||
"php": "7.4"
|
||||
},
|
||||
|
|
|
@ -16,6 +16,8 @@ use Psr\Log\LoggerInterface;
|
|||
|
||||
/**
|
||||
* Class PartnerReferrals
|
||||
*
|
||||
* @see https://developer.paypal.com/docs/api/partner-referrals/v2/
|
||||
*/
|
||||
class PartnerReferrals {
|
||||
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -182,6 +182,26 @@ class ApplepayModule implements ServiceModule, ExtendingModule, ExecutableModule
|
|||
2
|
||||
);
|
||||
|
||||
add_filter(
|
||||
'woocommerce_paypal_payments_rest_common_merchant_data',
|
||||
function( array $merchant_data ) use ( $c ): array {
|
||||
if ( ! isset( $merchant_data['features'] ) ) {
|
||||
$merchant_data['features'] = array();
|
||||
}
|
||||
|
||||
$product_status = $c->get( 'applepay.apple-product-status' );
|
||||
assert( $product_status instanceof AppleProductStatus );
|
||||
|
||||
$apple_pay_enabled = $product_status->is_active();
|
||||
|
||||
$merchant_data['features']['apple_pay'] = array(
|
||||
'enabled' => $apple_pay_enabled,
|
||||
);
|
||||
|
||||
return $merchant_data;
|
||||
}
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -19,11 +19,7 @@ import {
|
|||
import { cartHasSubscriptionProducts } from '../Helper/Subscription';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
export function CardFields( {
|
||||
config,
|
||||
eventRegistration,
|
||||
emitResponse,
|
||||
} ) {
|
||||
export function CardFields( { config, eventRegistration, emitResponse } ) {
|
||||
const { onPaymentSetup } = eventRegistration;
|
||||
const { responseTypes } = emitResponse;
|
||||
|
||||
|
@ -96,14 +92,34 @@ export function CardFields( {
|
|||
console.error( err );
|
||||
} }
|
||||
>
|
||||
<PayPalNameField placeholder={ __( 'Cardholder Name (optional)', 'woocommerce-paypal-payments' ) }/>
|
||||
<PayPalNumberField placeholder={ __( 'Card number', 'woocommerce-paypal-payments' ) }/>
|
||||
<div style={ { display: "flex", width: "100%" } }>
|
||||
<div style={ { width: "100%" } }>
|
||||
<PayPalExpiryField placeholder={ __( 'MM / YY', 'woocommerce-paypal-payments' ) }/>
|
||||
<PayPalNameField
|
||||
placeholder={ __(
|
||||
'Cardholder Name (optional)',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
/>
|
||||
<PayPalNumberField
|
||||
placeholder={ __(
|
||||
'Card number',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
/>
|
||||
<div style={ { display: 'flex', width: '100%' } }>
|
||||
<div style={ { width: '100%' } }>
|
||||
<PayPalExpiryField
|
||||
placeholder={ __(
|
||||
'MM / YY',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
/>
|
||||
</div>
|
||||
<div style={ { width: "100%" } }>
|
||||
<PayPalCVVField placeholder={ __( 'CVV', 'woocommerce-paypal-payments' ) }/>
|
||||
<div style={ { width: '100%' } }>
|
||||
<PayPalCVVField
|
||||
placeholder={ __(
|
||||
'CVV',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<CheckoutHandler
|
||||
|
|
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.
|
||||
*
|
||||
|
|
|
@ -9,62 +9,149 @@ declare(strict_types=1);
|
|||
|
||||
namespace WooCommerce\PayPalCommerce\Compat;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* A helper for mapping the new/old settings.
|
||||
* A helper class to manage the transition between legacy and new settings.
|
||||
*
|
||||
* This utility provides mapping from old setting keys to new ones and retrieves
|
||||
* their corresponding values from the appropriate models. The class uses lazy
|
||||
* loading and caching to optimize performance during runtime.
|
||||
*/
|
||||
class SettingsMapHelper {
|
||||
|
||||
/**
|
||||
* A list of mapped settings.
|
||||
* A list of settings maps containing mapping definitions.
|
||||
*
|
||||
* @var SettingsMap[]
|
||||
*/
|
||||
protected array $settings_map;
|
||||
|
||||
/**
|
||||
* Indexed map for faster lookups, initialized lazily.
|
||||
*
|
||||
* @var array|null Associative array where old keys map to metadata.
|
||||
*/
|
||||
protected ?array $key_to_model = null;
|
||||
|
||||
/**
|
||||
* Cache for results of `to_array()` calls on models.
|
||||
*
|
||||
* @var array Associative array where keys are model IDs.
|
||||
*/
|
||||
protected array $model_cache = array();
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param SettingsMap[] $settings_map A list of mapped settings.
|
||||
* @param SettingsMap[] $settings_map A list of settings maps containing key definitions.
|
||||
* @throws RuntimeException When an old key has multiple mappings.
|
||||
*/
|
||||
public function __construct( array $settings_map ) {
|
||||
$this->validate_settings_map( $settings_map );
|
||||
$this->settings_map = $settings_map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the mapped value from the new settings.
|
||||
* Validates the settings map for duplicate keys.
|
||||
*
|
||||
* @param string $key The key.
|
||||
* @return ?mixed the mapped value or Null if it doesn't exist.
|
||||
* @param SettingsMap[] $settings_map The settings map to validate.
|
||||
* @throws RuntimeException When an old key has multiple mappings.
|
||||
*/
|
||||
public function mapped_value( string $key ) {
|
||||
if ( ! $this->has_mapped_key( $key ) ) {
|
||||
return null;
|
||||
}
|
||||
protected function validate_settings_map( array $settings_map ) : void {
|
||||
$seen_keys = array();
|
||||
|
||||
foreach ( $this->settings_map as $settings_map ) {
|
||||
$mapped_key = array_search( $key, $settings_map->get_map(), true );
|
||||
$new_settings = $settings_map->get_model()->to_array();
|
||||
if ( ! empty( $new_settings[ $mapped_key ] ) ) {
|
||||
return $new_settings[ $mapped_key ];
|
||||
foreach ( $settings_map as $settings_map_instance ) {
|
||||
foreach ( $settings_map_instance->get_map() as $old_key => $new_key ) {
|
||||
if ( isset( $seen_keys[ $old_key ] ) ) {
|
||||
throw new RuntimeException( "Duplicate mapping for legacy key '$old_key'." );
|
||||
}
|
||||
$seen_keys[ $old_key ] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given key exists in the new settings.
|
||||
* Retrieves the value of a mapped key from the new settings.
|
||||
*
|
||||
* @param string $key The key.
|
||||
* @return bool true if the given key exists in the new settings, otherwise false.
|
||||
* @param string $old_key The key from the legacy settings.
|
||||
*
|
||||
* @return mixed|null The value of the mapped setting, or null if not found.
|
||||
*/
|
||||
public function has_mapped_key( string $key ) : bool {
|
||||
foreach ( $this->settings_map as $settings_map ) {
|
||||
if ( in_array( $key, $settings_map->get_map(), true ) ) {
|
||||
return true;
|
||||
public function mapped_value( string $old_key ) {
|
||||
$this->ensure_map_initialized();
|
||||
|
||||
if ( ! isset( $this->key_to_model[ $old_key ] ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$mapping = $this->key_to_model[ $old_key ];
|
||||
$model_id = spl_object_id( $mapping['model'] );
|
||||
|
||||
return $this->get_cached_model_value( $model_id, $mapping['new_key'], $mapping['model'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a given legacy key exists in the new settings.
|
||||
*
|
||||
* @param string $old_key The key from the legacy settings.
|
||||
*
|
||||
* @return bool True if the key exists in the new settings, false otherwise.
|
||||
*/
|
||||
public function has_mapped_key( string $old_key ) : bool {
|
||||
$this->ensure_map_initialized();
|
||||
|
||||
return isset( $this->key_to_model[ $old_key ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a cached model value or caches it if not already cached.
|
||||
*
|
||||
* @param int $model_id The unique identifier for the model object.
|
||||
* @param string $new_key The key in the new settings structure.
|
||||
* @param object $model The model object.
|
||||
*
|
||||
* @return mixed|null The value of the key in the model, or null if not found.
|
||||
*/
|
||||
protected function get_cached_model_value( int $model_id, string $new_key, object $model ) {
|
||||
if ( ! isset( $this->model_cache[ $model_id ] ) ) {
|
||||
$this->model_cache[ $model_id ] = $model->to_array();
|
||||
}
|
||||
|
||||
return $this->model_cache[ $model_id ][ $new_key ] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the map of old-to-new settings is initialized.
|
||||
*
|
||||
* This method initializes the `key_to_model` array lazily to improve performance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function ensure_map_initialized() : void {
|
||||
if ( $this->key_to_model === null ) {
|
||||
$this->initialize_key_map();
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
/**
|
||||
* Initializes the indexed map of old-to-new settings keys.
|
||||
*
|
||||
* This method processes the provided settings maps and indexes the legacy
|
||||
* keys to their corresponding metadata for efficient lookup.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function initialize_key_map() : void {
|
||||
$this->key_to_model = array();
|
||||
|
||||
foreach ( $this->settings_map as $settings_map_instance ) {
|
||||
foreach ( $settings_map_instance->get_map() as $old_key => $new_key ) {
|
||||
$this->key_to_model[ $old_key ] = array(
|
||||
'new_key' => $new_key,
|
||||
'model' => $settings_map_instance->get_model(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -232,6 +232,26 @@ class GooglepayModule implements ServiceModule, ExtendingModule, ExecutableModul
|
|||
2
|
||||
);
|
||||
|
||||
add_filter(
|
||||
'woocommerce_paypal_payments_rest_common_merchant_data',
|
||||
function ( array $merchant_data ) use ( $c ): array {
|
||||
if ( ! isset( $merchant_data['features'] ) ) {
|
||||
$merchant_data['features'] = array();
|
||||
}
|
||||
|
||||
$product_status = $c->get( 'googlepay.helpers.apm-product-status' );
|
||||
assert( $product_status instanceof ApmProductStatus );
|
||||
|
||||
$google_pay_enabled = $product_status->is_active();
|
||||
|
||||
$merchant_data['features']['google_pay'] = array(
|
||||
'enabled' => $google_pay_enabled,
|
||||
);
|
||||
|
||||
return $merchant_data;
|
||||
}
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,23 +66,33 @@ class SavePaymentMethodsModule implements ServiceModule, ExtendingModule, Execut
|
|||
add_action(
|
||||
'woocommerce_paypal_payments_gateway_migrate_on_update',
|
||||
function() use ( $c ) {
|
||||
$settings = $c->get( 'wcgateway.settings' );
|
||||
assert( $settings instanceof Settings );
|
||||
|
||||
$billing_agreements_endpoint = $c->get( 'api.endpoint.billing-agreements' );
|
||||
assert( $billing_agreements_endpoint instanceof BillingAgreementsEndpoint );
|
||||
|
||||
$reference_transaction_enabled = $billing_agreements_endpoint->reference_transaction_enabled();
|
||||
if ( $reference_transaction_enabled !== true ) {
|
||||
$c->get( 'wcgateway.settings' )->set( 'vault_enabled', false );
|
||||
$c->get( 'wcgateway.settings' )->persist();
|
||||
$settings->set( 'vault_enabled', false );
|
||||
$settings->persist();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
add_action(
|
||||
'after_setup_theme',
|
||||
function () use ( $c ) {
|
||||
$settings = $c->get( 'wcgateway.settings' );
|
||||
if (
|
||||
( ! $settings->has( 'vault_enabled' ) || ! $settings->get( 'vault_enabled' ) )
|
||||
&& ( ! $settings->has( 'vault_enabled_dcc' ) || ! $settings->get( 'vault_enabled_dcc' ) )
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
add_filter(
|
||||
'woocommerce_paypal_payments_localized_script_data',
|
||||
function ( array $localized_script_data ) use ( $c ) {
|
||||
if ( ! self::vault_enabled( $c ) ) {
|
||||
return $localized_script_data;
|
||||
}
|
||||
$subscriptions_helper = $c->get( 'wc-subscriptions.helper' );
|
||||
assert( $subscriptions_helper instanceof SubscriptionHelper );
|
||||
if ( ! is_user_logged_in() && ! $subscriptions_helper->cart_contains_subscription() ) {
|
||||
|
@ -103,10 +113,13 @@ class SavePaymentMethodsModule implements ServiceModule, ExtendingModule, Execut
|
|||
add_filter(
|
||||
'ppcp_create_order_request_body_data',
|
||||
function ( array $data, string $payment_method, array $request_data ) use ( $c ): array {
|
||||
if ( ! self::vault_enabled( $c ) ) {
|
||||
$settings = $c->get( 'wcgateway.settings' );
|
||||
assert( $settings instanceof Settings );
|
||||
if ( $payment_method === CreditCardGateway::ID ) {
|
||||
if ( ! $settings->has( 'vault_enabled_dcc' ) || ! $settings->get( 'vault_enabled_dcc' ) ) {
|
||||
return $data;
|
||||
}
|
||||
if ( $payment_method === CreditCardGateway::ID ) {
|
||||
|
||||
$save_payment_method = $request_data['save_payment_method'] ?? false;
|
||||
if ( $save_payment_method ) {
|
||||
$data['payment_source'] = array(
|
||||
|
@ -133,6 +146,10 @@ class SavePaymentMethodsModule implements ServiceModule, ExtendingModule, Execut
|
|||
}
|
||||
|
||||
if ( $payment_method === PayPalGateway::ID ) {
|
||||
if ( ! $settings->has( 'vault_enabled' ) || ! $settings->get( 'vault_enabled' ) ) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$funding_source = $request_data['funding_source'] ?? null;
|
||||
|
||||
if ( $funding_source && $funding_source === 'venmo' ) {
|
||||
|
@ -185,9 +202,6 @@ class SavePaymentMethodsModule implements ServiceModule, ExtendingModule, Execut
|
|||
add_action(
|
||||
'woocommerce_paypal_payments_after_order_processor',
|
||||
function ( WC_Order $wc_order, Order $order ) use ( $c ) {
|
||||
if ( ! self::vault_enabled( $c ) ) {
|
||||
return;
|
||||
}
|
||||
$payment_source = $order->payment_source();
|
||||
assert( $payment_source instanceof PaymentSource );
|
||||
|
||||
|
@ -250,30 +264,13 @@ class SavePaymentMethodsModule implements ServiceModule, ExtendingModule, Execut
|
|||
2
|
||||
);
|
||||
|
||||
add_filter(
|
||||
'woocommerce_paypal_payments_disable_add_payment_method',
|
||||
function ( bool $value ) use ( $c ): bool {
|
||||
if ( ! self::vault_enabled( $c ) ) {
|
||||
return $value;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
);
|
||||
|
||||
add_filter(
|
||||
'woocommerce_paypal_payments_should_render_card_custom_fields',
|
||||
function ( bool $value ) use ( $c ): bool {
|
||||
if ( ! self::vault_enabled( $c ) ) {
|
||||
return $value;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
);
|
||||
add_filter( 'woocommerce_paypal_payments_disable_add_payment_method', '__return_false' );
|
||||
add_filter( 'woocommerce_paypal_payments_should_render_card_custom_fields', '__return_false' );
|
||||
|
||||
add_action(
|
||||
'wp_enqueue_scripts',
|
||||
function () use ( $c ) {
|
||||
if ( ! is_user_logged_in() || ! ( $this->is_add_payment_method_page() || $this->is_subscription_change_payment_method_page() ) || ! self::vault_enabled( $c ) ) {
|
||||
if ( ! is_user_logged_in() || ! ( $this->is_add_payment_method_page() || $this->is_subscription_change_payment_method_page() ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -363,8 +360,8 @@ class SavePaymentMethodsModule implements ServiceModule, ExtendingModule, Execut
|
|||
|
||||
add_action(
|
||||
'woocommerce_add_payment_method_form_bottom',
|
||||
function () use ( $c ) {
|
||||
if ( ! is_user_logged_in() || ! is_add_payment_method_page() || ! self::vault_enabled( $c ) ) {
|
||||
function () {
|
||||
if ( ! is_user_logged_in() || ! is_add_payment_method_page() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -375,9 +372,6 @@ class SavePaymentMethodsModule implements ServiceModule, ExtendingModule, Execut
|
|||
add_action(
|
||||
'wc_ajax_' . CreateSetupToken::ENDPOINT,
|
||||
static function () use ( $c ) {
|
||||
if ( ! self::vault_enabled( $c ) ) {
|
||||
return;
|
||||
}
|
||||
$endpoint = $c->get( 'save-payment-methods.endpoint.create-setup-token' );
|
||||
assert( $endpoint instanceof CreateSetupToken );
|
||||
|
||||
|
@ -388,9 +382,6 @@ class SavePaymentMethodsModule implements ServiceModule, ExtendingModule, Execut
|
|||
add_action(
|
||||
'wc_ajax_' . CreatePaymentToken::ENDPOINT,
|
||||
static function () use ( $c ) {
|
||||
if ( ! self::vault_enabled( $c ) ) {
|
||||
return;
|
||||
}
|
||||
$endpoint = $c->get( 'save-payment-methods.endpoint.create-payment-token' );
|
||||
assert( $endpoint instanceof CreatePaymentToken );
|
||||
|
||||
|
@ -401,9 +392,6 @@ class SavePaymentMethodsModule implements ServiceModule, ExtendingModule, Execut
|
|||
add_action(
|
||||
'wc_ajax_' . CreatePaymentTokenForGuest::ENDPOINT,
|
||||
static function () use ( $c ) {
|
||||
if ( ! self::vault_enabled( $c ) ) {
|
||||
return;
|
||||
}
|
||||
$endpoint = $c->get( 'save-payment-methods.endpoint.create-payment-token-for-guest' );
|
||||
assert( $endpoint instanceof CreatePaymentTokenForGuest );
|
||||
|
||||
|
@ -414,9 +402,6 @@ class SavePaymentMethodsModule implements ServiceModule, ExtendingModule, Execut
|
|||
add_action(
|
||||
'woocommerce_paypal_payments_before_delete_payment_token',
|
||||
function( string $token_id ) use ( $c ) {
|
||||
if ( ! self::vault_enabled( $c ) ) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
$endpoint = $c->get( 'api.endpoint.payment-tokens' );
|
||||
assert( $endpoint instanceof PaymentTokensEndpoint );
|
||||
|
@ -439,11 +424,13 @@ class SavePaymentMethodsModule implements ServiceModule, ExtendingModule, Execut
|
|||
add_filter(
|
||||
'woocommerce_paypal_payments_credit_card_gateway_supports',
|
||||
function( array $supports ) use ( $c ): array {
|
||||
if ( ! self::vault_enabled( $c ) ) {
|
||||
return $supports;
|
||||
}
|
||||
$settings = $c->get( 'wcgateway.settings' );
|
||||
assert( $settings instanceof ContainerInterface );
|
||||
|
||||
if ( $settings->has( 'vault_enabled_dcc' ) && $settings->get( 'vault_enabled_dcc' ) ) {
|
||||
$supports[] = 'tokenization';
|
||||
$supports[] = 'add_payment_method';
|
||||
}
|
||||
|
||||
return $supports;
|
||||
}
|
||||
|
@ -451,13 +438,12 @@ class SavePaymentMethodsModule implements ServiceModule, ExtendingModule, Execut
|
|||
|
||||
add_filter(
|
||||
'woocommerce_paypal_payments_save_payment_methods_eligible',
|
||||
function( bool $value ) use ( $c ): bool {
|
||||
if ( ! self::vault_enabled( $c ) ) {
|
||||
return $value;
|
||||
}
|
||||
function() {
|
||||
return true;
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -502,24 +488,4 @@ class SavePaymentMethodsModule implements ServiceModule, ExtendingModule, Execut
|
|||
|
||||
return $localized_script_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the vault functionality is enabled based on configuration settings.
|
||||
*
|
||||
* @param ContainerInterface $container The dependency injection container from which settings can be retrieved.
|
||||
*
|
||||
* @return bool Returns true if either 'vault_enabled' or 'vault_enabled_dcc' settings are enabled; otherwise, false.
|
||||
*/
|
||||
private static function vault_enabled( ContainerInterface $container ): bool {
|
||||
$settings = $container->get( 'wcgateway.settings' );
|
||||
assert( $settings instanceof Settings );
|
||||
|
||||
if (
|
||||
( ! $settings->has( 'vault_enabled' ) || ! $settings->get( 'vault_enabled' ) )
|
||||
&& ( ! $settings->has( 'vault_enabled_dcc' ) || ! $settings->get( 'vault_enabled_dcc' ) )
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,14 +7,13 @@
|
|||
"build": "wp-scripts build --webpack-src-dir=resources/js --output-path=assets"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@woocommerce/navigation": "~8.1.0",
|
||||
"@wordpress/data": "^10.10.0",
|
||||
"@wordpress/data-controls": "^4.10.0",
|
||||
"@wordpress/scripts": "^30.3.0"
|
||||
"@wordpress/scripts": "^30.3.0",
|
||||
"classnames": "^2.5.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@paypal/react-paypal-js": "^8.7.0",
|
||||
"@woocommerce/settings": "^1.0.0",
|
||||
"react-select": "^5.8.3"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ $color-gray-500: #BBBBBB;
|
|||
$color-gray-400: #CCCCCC;
|
||||
$color-gray-300: #EBEBEB;
|
||||
$color-gray-200: #E0E0E0;
|
||||
$color-gray-100: #F0F0F0;
|
||||
$color-gray: #646970;
|
||||
$color-text-tertiary: #505050;
|
||||
$color-text-text: #070707;
|
||||
|
@ -27,6 +28,8 @@ $max-width-settings: 938px;
|
|||
|
||||
$card-vertical-gap: 48px;
|
||||
|
||||
/* define custom theming options */
|
||||
|
||||
:root {
|
||||
--ppcp-color-app-bg: #{$color-white};
|
||||
}
|
||||
|
@ -37,4 +40,18 @@ $card-vertical-gap: 48px;
|
|||
--max-width-onboarding-content: #{$max-width-onboarding-content};
|
||||
|
||||
--max-container-width: var(--max-width-settings);
|
||||
|
||||
--color-black: #{$color-black};
|
||||
--color-white: #{$color-white};
|
||||
--color-blueberry: #{$color-blueberry};
|
||||
--color-gray-900: #{$color-gray-900};
|
||||
--color-gray-800: #{$color-gray-800};
|
||||
--color-gray-700: #{$color-gray-700};
|
||||
--color-gray-600: #{$color-gray-600};
|
||||
--color-gray-500: #{$color-gray-500};
|
||||
--color-gray-400: #{$color-gray-400};
|
||||
--color-gray-300: #{$color-gray-300};
|
||||
--color-gray-200: #{$color-gray-200};
|
||||
--color-gray-100: #{$color-gray-100};
|
||||
--color-gradient-dark: #{$color-gradient-dark};
|
||||
}
|
||||
|
|
22
modules/ppcp-settings/resources/css/components/_app.scss
Normal file
22
modules/ppcp-settings/resources/css/components/_app.scss
Normal file
|
@ -0,0 +1,22 @@
|
|||
/**
|
||||
* Global app-level styles
|
||||
*/
|
||||
|
||||
.ppcp-r-app.loading {
|
||||
height: 400px;
|
||||
width: 400px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
text-align: center;
|
||||
|
||||
.ppcp-r-spinner-overlay {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ppcp-r-spinner-overlay__message {
|
||||
transform: translate(0, 32px)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
.ppcp-r-busy-wrapper {
|
||||
position: relative;
|
||||
|
||||
&.ppcp--is-loading {
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
|
||||
--spinner-overlay-color: #fff4;
|
||||
}
|
||||
}
|
|
@ -1,48 +1,102 @@
|
|||
button.components-button, a.components-button {
|
||||
&.is-primary, &.is-secondary {
|
||||
&:not(:disabled) {
|
||||
background-color: $color-black;
|
||||
%button-style-default {
|
||||
background-color: var(--button-background);
|
||||
color: var(--button-color);
|
||||
box-shadow: inset 0 0 0 1px var(--button-border-color);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: $color-gray-700;
|
||||
%button-style-hover {
|
||||
background-color: var(--button-hover-background);
|
||||
color: var(--button-hover-color);
|
||||
box-shadow: inset 0 0 0 1px var(--button-hover-border-color);
|
||||
}
|
||||
|
||||
%button-style-disabled {
|
||||
background-color: var(--button-disabled-background);
|
||||
color: var(--button-disabled-color);
|
||||
box-shadow: inset 0 0 0 1px var(--button-disabled-border-color);
|
||||
}
|
||||
|
||||
%button-shape-pill {
|
||||
border-radius: 50px;
|
||||
padding: 15px 32px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
button.components-button, a.components-button {
|
||||
/* default theme */
|
||||
--button-color: var(--color-gray-900);
|
||||
--button-background: transparent;
|
||||
--button-border-color: transparent;
|
||||
|
||||
--button-hover-color: var(--button-color);
|
||||
--button-hover-background: var(--button-background);
|
||||
--button-hover-border-color: var(--button-border-color);
|
||||
|
||||
--button-disabled-color: var(--color-gray-500);
|
||||
--button-disabled-background: transparent;
|
||||
--button-disabled-border-color: transparent;
|
||||
|
||||
/* style the button template */
|
||||
|
||||
&:not(:disabled) {
|
||||
@extend %button-style-default;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@extend %button-style-hover;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
@extend %button-style-disabled;
|
||||
}
|
||||
|
||||
/*
|
||||
----------------------------------------------
|
||||
Customize variants using the theming variables
|
||||
*/
|
||||
|
||||
&.is-primary,
|
||||
&.is-secondary {
|
||||
@extend %button-shape-pill;
|
||||
}
|
||||
|
||||
&.is-primary {
|
||||
@include font(14, 18, 900);
|
||||
|
||||
&:not(:disabled) {
|
||||
background-color: $color-blueberry;
|
||||
color: $color-white;
|
||||
}
|
||||
--button-color: #{$color-white};
|
||||
--button-background: #{$color-blueberry};
|
||||
|
||||
--button-disabled-color: #{$color-gray-100};
|
||||
--button-disabled-background: #{$color-gray-500};
|
||||
}
|
||||
|
||||
&.is-secondary:not(:disabled) {
|
||||
border-color: $color-blueberry;
|
||||
background-color: $color-white;
|
||||
color: $color-blueberry;
|
||||
&.is-secondary {
|
||||
--button-color: #{$color-blueberry};
|
||||
--button-background: #{$color-white};
|
||||
--button-border-color: #{$color-blueberry};
|
||||
|
||||
&:hover {
|
||||
background-color: $color-white;
|
||||
background: none;
|
||||
}
|
||||
--button-disabled-color: #{$color-gray-600};
|
||||
--button-disabled-background: #{$color-gray-100};
|
||||
--button-disabled-border-color: #{$color-gray-400};
|
||||
}
|
||||
|
||||
&.is-tertiary {
|
||||
color: $color-blueberry;
|
||||
|
||||
&:hover {
|
||||
color: $color-gradient-dark;
|
||||
}
|
||||
--button-color: #{$color-blueberry};
|
||||
--button-hover-color: #{$color-gradient-dark};
|
||||
|
||||
&:focus:not(:disabled) {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.small-button {
|
||||
@include small-button;
|
||||
}
|
||||
}
|
||||
|
||||
.ppcp--is-loading {
|
||||
button.components-button, a.components-button {
|
||||
@extend %button-style-disabled;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,6 +57,14 @@
|
|||
|
||||
&__content {
|
||||
display: flex;
|
||||
position: relative;
|
||||
pointer-events: none;
|
||||
*:not(a){
|
||||
pointer-events: none;
|
||||
}
|
||||
a {
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
|
||||
&__title {
|
||||
|
|
|
@ -31,10 +31,4 @@
|
|||
&__toggled-content {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
&.ppcp--is-loading {
|
||||
pointer-events: none;
|
||||
|
||||
--spinner-overlay-color: #fff4;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,5 +12,6 @@
|
|||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,10 +11,6 @@
|
|||
}
|
||||
|
||||
// Todo List and Feature Items
|
||||
.ppcp-r-tab-overview-todo {
|
||||
margin: 0 0 48px 0;
|
||||
}
|
||||
|
||||
.ppcp-r-todo-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
@ -109,6 +105,8 @@
|
|||
span {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
margin-top:24px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -239,6 +237,10 @@
|
|||
}
|
||||
|
||||
// Settings Card and Block Styles
|
||||
.ppcp-r-settings-card {
|
||||
margin: 0 0 48px 0;
|
||||
}
|
||||
|
||||
.ppcp-r-settings-card__content {
|
||||
> .ppcp-r-settings-block {
|
||||
&:not(:last-child) {
|
||||
|
|
|
@ -20,12 +20,6 @@
|
|||
margin: 0 0 24px 0;
|
||||
}
|
||||
|
||||
.ppcp-r-toggle-block__toggled-content > button{
|
||||
@include small-button;
|
||||
color: $color-white;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.client-id-error {
|
||||
color: #cc1818;
|
||||
margin: -16px 0 24px;
|
||||
|
|
|
@ -3,10 +3,11 @@
|
|||
|
||||
#ppcp-settings-container {
|
||||
@import './global';
|
||||
@import './components/reusable-components/onboarding-header';
|
||||
@import './components/reusable-components/busy-state';
|
||||
@import './components/reusable-components/button';
|
||||
@import './components/reusable-components/settings-toggle-block';
|
||||
@import './components/reusable-components/separator';
|
||||
@import './components/reusable-components/onboarding-header';
|
||||
@import './components/reusable-components/settings-toggle-block';
|
||||
@import './components/reusable-components/payment-method-icons';
|
||||
@import "./components/reusable-components/payment-method-item";
|
||||
@import './components/reusable-components/settings-wrapper';
|
||||
|
@ -22,6 +23,7 @@
|
|||
@import './components/screens/onboarding';
|
||||
@import './components/screens/settings';
|
||||
@import './components/screens/overview/tab-styling';
|
||||
@import './components/app';
|
||||
}
|
||||
|
||||
@import './components/reusable-components/payment-method-modal';
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import { Icon } from '@wordpress/components';
|
||||
import { chevronDown, chevronUp } from '@wordpress/icons';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { useAccordionState } from '../../hooks/useAccordionState';
|
||||
|
||||
// Provide defaults for all layout components so the generic version just works.
|
||||
|
@ -24,6 +22,13 @@ const DefaultDescription = ( { children } ) => (
|
|||
<div className="ppcp-r-accordion__description">{ children }</div>
|
||||
);
|
||||
|
||||
const AccordionContent = ( { isOpen, children } ) => {
|
||||
if ( ! isOpen || ! children ) {
|
||||
return null;
|
||||
}
|
||||
return <div className="ppcp-r-accordion__content">{ children }</div>;
|
||||
};
|
||||
|
||||
const Accordion = ( {
|
||||
title,
|
||||
id = '',
|
||||
|
@ -65,9 +70,7 @@ const Accordion = ( {
|
|||
) }
|
||||
</Header>
|
||||
</button>
|
||||
{ isOpen && children && (
|
||||
<div className="ppcp-r-accordion__content">{ children }</div>
|
||||
) }
|
||||
<AccordionContent isOpen={ isOpen }>{ children }</AccordionContent>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import data from '../../utils/data';
|
||||
import TitleBadge, { TITLE_BADGE_INFO } from './TitleBadge';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
const BadgeBox = ( props ) => {
|
||||
const titleSize =
|
||||
|
@ -29,12 +27,7 @@ const BadgeBox = ( props ) => {
|
|||
</span>
|
||||
) }
|
||||
|
||||
{ props.textBadge && (
|
||||
<TitleBadge
|
||||
type={ TITLE_BADGE_INFO }
|
||||
text={ props.textBadge }
|
||||
/>
|
||||
) }
|
||||
{ props.textBadge }
|
||||
</span>
|
||||
<div className="ppcp-r-badge-box__description">
|
||||
{ props?.description && (
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
import {
|
||||
Children,
|
||||
isValidElement,
|
||||
cloneElement,
|
||||
useMemo,
|
||||
createContext,
|
||||
useContext,
|
||||
} from '@wordpress/element';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { CommonHooks } from '../../data';
|
||||
import SpinnerOverlay from './SpinnerOverlay';
|
||||
|
||||
// Create context to track the busy state across nested wrappers
|
||||
const BusyContext = createContext( false );
|
||||
|
||||
/**
|
||||
* Wraps interactive child elements and modifies their behavior based on the global `isBusy` state.
|
||||
* Allows custom processing of child props via the `onBusy` callback.
|
||||
*
|
||||
* @param {Object} props - Component properties.
|
||||
* @param {Children} props.children - Child components to wrap.
|
||||
* @param {boolean} props.enabled - Enables or disables the busy-state logic.
|
||||
* @param {boolean} props.busySpinner - Allows disabling the spinner in busy-state.
|
||||
* @param {string} props.className - Additional class names for the wrapper.
|
||||
* @param {Function} props.onBusy - Callback to process child props when busy.
|
||||
*/
|
||||
const BusyStateWrapper = ( {
|
||||
children,
|
||||
enabled = true,
|
||||
busySpinner = true,
|
||||
className = '',
|
||||
onBusy = () => ( { disabled: true } ),
|
||||
} ) => {
|
||||
const { isBusy } = CommonHooks.useBusyState();
|
||||
const hasBusyParent = useContext( BusyContext );
|
||||
|
||||
const isBusyComponent = isBusy && enabled;
|
||||
const showSpinner = busySpinner && isBusyComponent && ! hasBusyParent;
|
||||
|
||||
const wrapperClassName = classNames( 'ppcp-r-busy-wrapper', className, {
|
||||
'ppcp--is-loading': isBusyComponent,
|
||||
} );
|
||||
|
||||
const memoizedChildren = useMemo(
|
||||
() =>
|
||||
Children.map( children, ( child ) =>
|
||||
isValidElement( child )
|
||||
? cloneElement(
|
||||
child,
|
||||
isBusyComponent ? onBusy( child.props ) : {}
|
||||
)
|
||||
: child
|
||||
),
|
||||
[ children, isBusyComponent, onBusy ]
|
||||
);
|
||||
|
||||
return (
|
||||
<BusyContext.Provider value={ isBusyComponent }>
|
||||
<div className={ wrapperClassName }>
|
||||
{ showSpinner && <SpinnerOverlay /> }
|
||||
{ memoizedChildren }
|
||||
</div>
|
||||
</BusyContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default BusyStateWrapper;
|
|
@ -0,0 +1,3 @@
|
|||
export { default as openSignup } from './Icons/open-signup';
|
||||
export const NOTIFICATION_SUCCESS = '✔️';
|
||||
export const NOTIFICATION_ERROR = '❌';
|
|
@ -0,0 +1,12 @@
|
|||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { SVG, Path } from '@wordpress/primitives';
|
||||
|
||||
const openSignup = (
|
||||
<SVG xmlns="http://www.w3.org/2000/svg" viewBox="0 0 25 24">
|
||||
<Path d="M12.4999 12.75V18.75C12.4999 18.9489 12.4209 19.1397 12.2803 19.2803C12.1396 19.421 11.9488 19.5 11.7499 19.5C11.551 19.5 11.3603 19.421 11.2196 19.2803C11.0789 19.1397 10.9999 18.9489 10.9999 18.75V14.5613L4.78055 20.7806C4.71087 20.8503 4.62815 20.9056 4.5371 20.9433C4.44606 20.981 4.34847 21.0004 4.24993 21.0004C4.15138 21.0004 4.0538 20.981 3.96276 20.9433C3.87171 20.9056 3.78899 20.8503 3.7193 20.7806C3.64962 20.7109 3.59435 20.6282 3.55663 20.5372C3.51892 20.4461 3.49951 20.3485 3.49951 20.25C3.49951 20.1515 3.51892 20.0539 3.55663 19.9628C3.59435 19.8718 3.64962 19.7891 3.7193 19.7194L9.93868 13.5H5.74993C5.55102 13.5 5.36025 13.421 5.2196 13.2803C5.07895 13.1397 4.99993 12.9489 4.99993 12.75C4.99993 12.5511 5.07895 12.3603 5.2196 12.2197C5.36025 12.079 5.55102 12 5.74993 12H11.7499C11.9488 12 12.1396 12.079 12.2803 12.2197C12.4209 12.3603 12.4999 12.5511 12.4999 12.75ZM19.9999 3H7.99993C7.6021 3 7.22057 3.15804 6.93927 3.43934C6.65796 3.72064 6.49993 4.10218 6.49993 4.5V9C6.49993 9.19891 6.57895 9.38968 6.7196 9.53033C6.86025 9.67098 7.05102 9.75 7.24993 9.75C7.44884 9.75 7.63961 9.67098 7.78026 9.53033C7.92091 9.38968 7.99993 9.19891 7.99993 9V4.5H19.9999V16.5H15.4999C15.301 16.5 15.1103 16.579 14.9696 16.7197C14.8289 16.8603 14.7499 17.0511 14.7499 17.25C14.7499 17.4489 14.8289 17.6397 14.9696 17.7803C15.1103 17.921 15.301 18 15.4999 18H19.9999C20.3978 18 20.7793 17.842 21.0606 17.5607C21.3419 17.2794 21.4999 16.8978 21.4999 16.5V4.5C21.4999 4.10218 21.3419 3.72064 21.0606 3.43934C20.7793 3.15804 20.3978 3 19.9999 3Z" />
|
||||
</SVG>
|
||||
);
|
||||
|
||||
export default openSignup;
|
|
@ -1,14 +1,13 @@
|
|||
import BadgeBox from '../BadgeBox';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
|
||||
import BadgeBox from '../BadgeBox';
|
||||
import Separator from '../Separator';
|
||||
import generatePriceText from '../../../utils/badgeBoxUtils';
|
||||
import { countryPriceInfo } from '../../../utils/countryPriceInfo';
|
||||
import PricingTitleBadge from '../PricingTitleBadge';
|
||||
|
||||
const AcdcOptionalPaymentMethods = ( {
|
||||
isFastlane,
|
||||
isPayLater,
|
||||
storeCountry,
|
||||
storeCurrency,
|
||||
} ) => {
|
||||
if ( isFastlane && isPayLater && storeCountry === 'US' ) {
|
||||
return (
|
||||
|
@ -24,11 +23,7 @@ const AcdcOptionalPaymentMethods = ( {
|
|||
'icon-button-amex.svg',
|
||||
'icon-button-discover.svg',
|
||||
] }
|
||||
textBadge={ generatePriceText(
|
||||
'ccf',
|
||||
countryPriceInfo[ storeCountry ],
|
||||
storeCurrency
|
||||
) }
|
||||
textBadge={ <PricingTitleBadge item="ccf" /> }
|
||||
description={ sprintf(
|
||||
// translators: %s: Link to PayPal business fees guide
|
||||
__(
|
||||
|
@ -48,11 +43,7 @@ const AcdcOptionalPaymentMethods = ( {
|
|||
'icon-button-apple-pay.svg',
|
||||
'icon-button-google-pay.svg',
|
||||
] }
|
||||
textBadge={ generatePriceText(
|
||||
'dw',
|
||||
countryPriceInfo[ storeCountry ],
|
||||
storeCurrency
|
||||
) }
|
||||
textBadge={ <PricingTitleBadge item="dw" /> }
|
||||
description={ sprintf(
|
||||
// translators: %s: Link to PayPal business fees guide
|
||||
__(
|
||||
|
@ -69,16 +60,11 @@ const AcdcOptionalPaymentMethods = ( {
|
|||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
imageBadge={ [
|
||||
'icon-button-sepa.svg',
|
||||
'icon-button-ideal.svg',
|
||||
'icon-button-blik.svg',
|
||||
'icon-button-bancontact.svg',
|
||||
] }
|
||||
textBadge={ generatePriceText(
|
||||
'apm',
|
||||
countryPriceInfo[ storeCountry ],
|
||||
storeCurrency
|
||||
) }
|
||||
textBadge={ <PricingTitleBadge item="apm" /> }
|
||||
description={ sprintf(
|
||||
// translators: %s: Link to PayPal business fees guide
|
||||
__(
|
||||
|
@ -92,11 +78,9 @@ const AcdcOptionalPaymentMethods = ( {
|
|||
<BadgeBox
|
||||
title={ __( '', 'woocommerce-paypal-payments' ) }
|
||||
imageBadge={ [ 'icon-payment-method-fastlane-small.svg' ] }
|
||||
textBadge={ generatePriceText(
|
||||
'fastlane',
|
||||
countryPriceInfo[ storeCountry ],
|
||||
storeCurrency
|
||||
) }
|
||||
textBadge={
|
||||
<PricingTitleBadge item="fast country currency=storeCurrency=storeCountrylane" />
|
||||
}
|
||||
description={ sprintf(
|
||||
// translators: %s: Link to PayPal business fees guide
|
||||
__(
|
||||
|
@ -124,11 +108,7 @@ const AcdcOptionalPaymentMethods = ( {
|
|||
'icon-button-amex.svg',
|
||||
'icon-button-discover.svg',
|
||||
] }
|
||||
textBadge={ generatePriceText(
|
||||
'ccf',
|
||||
countryPriceInfo[ storeCountry ],
|
||||
storeCurrency
|
||||
) }
|
||||
textBadge={ <PricingTitleBadge item="ccf" /> }
|
||||
description={ sprintf(
|
||||
// translators: %s: Link to PayPal business fees guide
|
||||
__(
|
||||
|
@ -148,11 +128,7 @@ const AcdcOptionalPaymentMethods = ( {
|
|||
'icon-button-apple-pay.svg',
|
||||
'icon-button-google-pay.svg',
|
||||
] }
|
||||
textBadge={ generatePriceText(
|
||||
'dw',
|
||||
countryPriceInfo[ storeCountry ],
|
||||
storeCurrency
|
||||
) }
|
||||
textBadge={ <PricingTitleBadge item="dw" /> }
|
||||
description={ sprintf(
|
||||
// translators: %s: Link to PayPal business fees guide
|
||||
__(
|
||||
|
@ -174,11 +150,7 @@ const AcdcOptionalPaymentMethods = ( {
|
|||
'icon-button-blik.svg',
|
||||
'icon-button-bancontact.svg',
|
||||
] }
|
||||
textBadge={ generatePriceText(
|
||||
'apm',
|
||||
countryPriceInfo[ storeCountry ],
|
||||
storeCurrency
|
||||
) }
|
||||
textBadge={ <PricingTitleBadge item="apm" /> }
|
||||
description={ sprintf(
|
||||
// translators: %s: Link to PayPal business fees guide
|
||||
__(
|
||||
|
@ -205,11 +177,7 @@ const AcdcOptionalPaymentMethods = ( {
|
|||
'icon-button-amex.svg',
|
||||
'icon-button-discover.svg',
|
||||
] }
|
||||
textBadge={ generatePriceText(
|
||||
'ccf',
|
||||
countryPriceInfo[ storeCountry ],
|
||||
storeCurrency
|
||||
) }
|
||||
textBadge={ <PricingTitleBadge item="ccf" /> }
|
||||
description={ sprintf(
|
||||
// translators: %s: Link to PayPal business fees guide
|
||||
__(
|
||||
|
@ -226,11 +194,7 @@ const AcdcOptionalPaymentMethods = ( {
|
|||
'icon-button-apple-pay.svg',
|
||||
'icon-button-google-pay.svg',
|
||||
] }
|
||||
textBadge={ generatePriceText(
|
||||
'dw',
|
||||
countryPriceInfo[ storeCountry ],
|
||||
storeCurrency
|
||||
) }
|
||||
textBadge={ <PricingTitleBadge item="dw" /> }
|
||||
description={ sprintf(
|
||||
// translators: %s: Link to PayPal business fees guide
|
||||
__(
|
||||
|
@ -252,11 +216,7 @@ const AcdcOptionalPaymentMethods = ( {
|
|||
'icon-button-blik.svg',
|
||||
'icon-button-bancontact.svg',
|
||||
] }
|
||||
textBadge={ generatePriceText(
|
||||
'apm',
|
||||
countryPriceInfo[ storeCountry ],
|
||||
storeCurrency
|
||||
) }
|
||||
textBadge={ <PricingTitleBadge item="apm" /> }
|
||||
description={ sprintf(
|
||||
// translators: %s: Link to PayPal business fees guide
|
||||
__(
|
||||
|
|
|
@ -1,13 +1,9 @@
|
|||
import BadgeBox from '../BadgeBox';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import generatePriceText from '../../../utils/badgeBoxUtils';
|
||||
import { countryPriceInfo } from '../../../utils/countryPriceInfo';
|
||||
|
||||
const BcdcOptionalPaymentMethods = ( {
|
||||
isPayLater,
|
||||
storeCountry,
|
||||
storeCurrency,
|
||||
} ) => {
|
||||
import BadgeBox from '../BadgeBox';
|
||||
import PricingTitleBadge from '../PricingTitleBadge';
|
||||
|
||||
const BcdcOptionalPaymentMethods = ( { isPayLater, storeCountry } ) => {
|
||||
if ( isPayLater && storeCountry === 'us' ) {
|
||||
return (
|
||||
<div className="ppcp-r-optional-payment-methods__wrapper">
|
||||
|
@ -22,11 +18,9 @@ const BcdcOptionalPaymentMethods = ( {
|
|||
'icon-button-amex.svg',
|
||||
'icon-button-discover.svg',
|
||||
] }
|
||||
textBadge={ generatePriceText(
|
||||
'standardCardFields',
|
||||
countryPriceInfo[ storeCountry ],
|
||||
storeCurrency
|
||||
) }
|
||||
textBadge={
|
||||
<PricingTitleBadge item="standardCardFields" />
|
||||
}
|
||||
description={ sprintf(
|
||||
// translators: %s: Link to PayPal REST application guide
|
||||
__(
|
||||
|
@ -53,11 +47,7 @@ const BcdcOptionalPaymentMethods = ( {
|
|||
'icon-button-amex.svg',
|
||||
'icon-button-discover.svg',
|
||||
] }
|
||||
textBadge={ generatePriceText(
|
||||
'standardCardFields',
|
||||
countryPriceInfo[ storeCountry ],
|
||||
storeCurrency
|
||||
) }
|
||||
textBadge={ <PricingTitleBadge item="standardCardFields" /> }
|
||||
description={ sprintf(
|
||||
// translators: %s: Link to PayPal REST application guide
|
||||
__(
|
||||
|
|
|
@ -6,7 +6,6 @@ const OptionalPaymentMethods = ( {
|
|||
isFastlane,
|
||||
isPayLater,
|
||||
storeCountry,
|
||||
storeCurrency,
|
||||
} ) => {
|
||||
return (
|
||||
<div className="ppcp-r-optional-payment-methods">
|
||||
|
@ -15,13 +14,11 @@ const OptionalPaymentMethods = ( {
|
|||
isFastlane={ isFastlane }
|
||||
isPayLater={ isPayLater }
|
||||
storeCountry={ storeCountry }
|
||||
storeCurrency={ storeCurrency }
|
||||
/>
|
||||
) : (
|
||||
<BcdcOptionalPaymentMethods
|
||||
isPayLater={ isPayLater }
|
||||
storeCountry={ storeCountry }
|
||||
storeCurrency={ storeCurrency }
|
||||
/>
|
||||
) }
|
||||
</div>
|
||||
|
|
|
@ -11,7 +11,6 @@ const PaymentMethodIcons = ( props ) => {
|
|||
<PaymentMethodIcon type="discover" icons={ props.icons } />
|
||||
<PaymentMethodIcon type="apple-pay" icons={ props.icons } />
|
||||
<PaymentMethodIcon type="google-pay" icons={ props.icons } />
|
||||
<PaymentMethodIcon type="sepa" icons={ props.icons } />
|
||||
<PaymentMethodIcon type="ideal" icons={ props.icons } />
|
||||
<PaymentMethodIcon type="bancontact" icons={ props.icons } />
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
import { __, sprintf } from '@wordpress/i18n';
|
||||
|
||||
import { countryPriceInfo } from '../../utils/countryPriceInfo';
|
||||
import { CommonHooks } from '../../data';
|
||||
|
||||
const PricingDescription = () => {
|
||||
const { storeCountry } = CommonHooks.useWooSettings();
|
||||
|
||||
if ( ! countryPriceInfo[ storeCountry ] ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const lastDate = 'October 25th, 2024'; // TODO -- needs to be the last plugin update date.
|
||||
const detailsUrl =
|
||||
'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input';
|
||||
|
||||
const label = sprintf(
|
||||
// translators: %1$s: Pricing date, %2$s Link to PayPal price-details page.
|
||||
__(
|
||||
'Prices based on domestic transactions as of %1$s. <a target="_blank" href="%2$s">Click here</a> for full pricing details.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
lastDate,
|
||||
detailsUrl
|
||||
);
|
||||
|
||||
return (
|
||||
<p
|
||||
className="ppcp-r-optional-payment-methods__description"
|
||||
data-country={ storeCountry }
|
||||
>
|
||||
<sup>1</sup>
|
||||
<span dangerouslySetInnerHTML={ { __html: label } } />
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
export default PricingDescription;
|
|
@ -0,0 +1,46 @@
|
|||
import { __, sprintf } from '@wordpress/i18n';
|
||||
|
||||
import { CommonHooks } from '../../data';
|
||||
import { countryPriceInfo } from '../../utils/countryPriceInfo';
|
||||
import { formatPrice } from '../../utils/formatPrice';
|
||||
import TitleBadge, { TITLE_BADGE_INFO } from './TitleBadge';
|
||||
|
||||
const getFixedAmount = ( currency, priceList, itemFixedAmount ) => {
|
||||
if ( priceList[ currency ] ) {
|
||||
const sum = priceList[ currency ] + itemFixedAmount;
|
||||
return formatPrice( sum, currency );
|
||||
}
|
||||
|
||||
const [ defaultCurrency, defaultPrice ] = Object.entries( priceList )[ 0 ];
|
||||
const sum = defaultPrice + itemFixedAmount;
|
||||
return formatPrice( sum, defaultCurrency );
|
||||
};
|
||||
|
||||
const PricingTitleBadge = ( { item } ) => {
|
||||
const { storeCountry, storeCurrency } = CommonHooks.useWooSettings();
|
||||
const infos = countryPriceInfo[ storeCountry ];
|
||||
const itemKey = item.split(' ')[0]; // Extract the first word, fastlane has more than one
|
||||
|
||||
if ( ! infos || ! infos[ itemKey ] ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const percentage = typeof infos[itemKey] === 'number' ? infos[itemKey].toFixed(2) : infos[itemKey]['percentage'].toFixed(2);
|
||||
const itemFixedAmount = infos[itemKey]['fixedFee'] ? infos[itemKey]['fixedFee'] : 0;
|
||||
const fixedAmount = getFixedAmount( storeCurrency, infos.fixedFee, itemFixedAmount );
|
||||
|
||||
const label = sprintf(
|
||||
__( 'from %1$s%% + %2$s', 'woocommerce-paypal-payments' ),
|
||||
percentage,
|
||||
fixedAmount
|
||||
);
|
||||
|
||||
return (
|
||||
<TitleBadge
|
||||
type={ TITLE_BADGE_INFO }
|
||||
text={ `${ label }<sup>1</sup>` }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default PricingTitleBadge;
|
|
@ -9,11 +9,7 @@ import {
|
|||
} from './SettingsBlockElements';
|
||||
|
||||
const SettingsAccordion = ( { title, description, children, ...props } ) => (
|
||||
<SettingsBlock
|
||||
{ ...props }
|
||||
className="ppcp-r-settings-block__accordion"
|
||||
components={ [
|
||||
() => (
|
||||
<SettingsBlock { ...props } className="ppcp-r-settings-block__accordion">
|
||||
<Accordion
|
||||
title={ title }
|
||||
description={ description }
|
||||
|
@ -25,9 +21,7 @@ const SettingsAccordion = ( { title, description, children, ...props } ) => (
|
|||
>
|
||||
{ children }
|
||||
</Accordion>
|
||||
),
|
||||
] }
|
||||
/>
|
||||
</SettingsBlock>
|
||||
);
|
||||
|
||||
export default SettingsAccordion;
|
||||
|
|
|
@ -1,20 +1,16 @@
|
|||
import { Button } from '@wordpress/components';
|
||||
import SettingsBlock from './SettingsBlock';
|
||||
import { Header, Title, Action, Description } from './SettingsBlockElements';
|
||||
import { Action, Description, Header, Title } from './SettingsBlockElements';
|
||||
|
||||
const ButtonSettingsBlock = ( { title, description, ...props } ) => (
|
||||
<SettingsBlock
|
||||
{ ...props }
|
||||
className="ppcp-r-settings-block__button"
|
||||
components={ [
|
||||
() => (
|
||||
<>
|
||||
<SettingsBlock { ...props } className="ppcp-r-settings-block__button">
|
||||
<Header>
|
||||
<Title>{ title }</Title>
|
||||
<Description>{ description }</Description>
|
||||
</Header>
|
||||
<Action>
|
||||
<Button
|
||||
isBusy={ props.actionProps?.isBusy }
|
||||
variant={ props.actionProps?.buttonType }
|
||||
onClick={
|
||||
props.actionProps?.callback
|
||||
|
@ -25,10 +21,7 @@ const ButtonSettingsBlock = ( { title, description, ...props } ) => (
|
|||
{ props.actionProps.value }
|
||||
</Button>
|
||||
</Action>
|
||||
</>
|
||||
),
|
||||
] }
|
||||
/>
|
||||
</SettingsBlock>
|
||||
);
|
||||
|
||||
export default ButtonSettingsBlock;
|
||||
|
|
|
@ -11,30 +11,21 @@ const FeatureSettingsBlock = ( { title, description, ...props } ) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className="ppcp-r-feature-item__notes">
|
||||
{ notes.map( ( note, index ) => (
|
||||
<span key={ index }>{ note }</span>
|
||||
) ) }
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingsBlock
|
||||
{ ...props }
|
||||
className="ppcp-r-settings-block__feature"
|
||||
components={ [
|
||||
() => (
|
||||
<>
|
||||
<SettingsBlock { ...props } className="ppcp-r-settings-block__feature">
|
||||
<Header>
|
||||
<Title>
|
||||
{ title }
|
||||
{ props.actionProps?.featureStatus && (
|
||||
<TitleBadge
|
||||
{ ...props.actionProps?.badge }
|
||||
/>
|
||||
{ props.actionProps?.enabled && (
|
||||
<TitleBadge { ...props.actionProps?.badge } />
|
||||
) }
|
||||
</Title>
|
||||
<Description className="ppcp-r-settings-block__feature__description">
|
||||
|
@ -44,23 +35,19 @@ const FeatureSettingsBlock = ( { title, description, ...props } ) => {
|
|||
</Header>
|
||||
<Action>
|
||||
<div className="ppcp-r-feature-item__buttons">
|
||||
{ props.actionProps?.buttons.map(
|
||||
( button ) => (
|
||||
{ props.actionProps?.buttons.map( ( button ) => (
|
||||
<Button
|
||||
className={ button.class ? button.class : '' }
|
||||
href={ button.url }
|
||||
key={ button.text }
|
||||
variant={ button.type }
|
||||
>
|
||||
{ button.text }
|
||||
</Button>
|
||||
)
|
||||
) }
|
||||
) ) }
|
||||
</div>
|
||||
</Action>
|
||||
</>
|
||||
),
|
||||
] }
|
||||
/>
|
||||
</SettingsBlock>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -42,12 +42,7 @@ const InputSettingsBlock = ( {
|
|||
order = DEFAULT_ELEMENT_ORDER,
|
||||
...props
|
||||
} ) => (
|
||||
<SettingsBlock
|
||||
{ ...props }
|
||||
className="ppcp-r-settings-block__input"
|
||||
components={ [
|
||||
() => (
|
||||
<>
|
||||
<SettingsBlock { ...props } className="ppcp-r-settings-block__input">
|
||||
{ order.map( ( elementKey ) => {
|
||||
const RenderElement = ELEMENT_RENDERERS[ elementKey ];
|
||||
return RenderElement ? (
|
||||
|
@ -60,10 +55,7 @@ const InputSettingsBlock = ( {
|
|||
/>
|
||||
) : null;
|
||||
} ) }
|
||||
</>
|
||||
),
|
||||
] }
|
||||
/>
|
||||
</SettingsBlock>
|
||||
);
|
||||
|
||||
export default InputSettingsBlock;
|
||||
|
|
|
@ -5,20 +5,13 @@ import PaymentMethodIcon from '../PaymentMethodIcon';
|
|||
import data from '../../../utils/data';
|
||||
|
||||
const PaymentMethodItemBlock = ( props ) => {
|
||||
const [ paymentMethodState, setPaymentMethodState ] = useState();
|
||||
const [ toggleIsChecked, setToggleIsChecked ] = useState( false );
|
||||
const [ modalIsVisible, setModalIsVisible ] = useState( false );
|
||||
const Modal = props?.modal;
|
||||
|
||||
const handleCheckboxState = ( checked ) => {
|
||||
setPaymentMethodState( checked ? props.id : null );
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsBlock
|
||||
className="ppcp-r-settings-block__payment-methods__item"
|
||||
components={ [
|
||||
() => (
|
||||
<SettingsBlock className="ppcp-r-settings-block__payment-methods__item">
|
||||
<div className="ppcp-r-settings-block__payment-methods__item__inner">
|
||||
<div className="ppcp-r-settings-block__payment-methods__item__title-wrapper">
|
||||
<PaymentMethodIcon
|
||||
|
@ -35,26 +28,20 @@ const PaymentMethodItemBlock = ( props ) => {
|
|||
<div className="ppcp-r-settings-block__payment-methods__item__footer">
|
||||
<ToggleControl
|
||||
__nextHasNoMarginBottom={ true }
|
||||
checked={ props.id === paymentMethodState }
|
||||
onChange={ handleCheckboxState }
|
||||
checked={ toggleIsChecked }
|
||||
onChange={ setToggleIsChecked }
|
||||
/>
|
||||
{ Modal && (
|
||||
<div
|
||||
className="ppcp-r-settings-block__payment-methods__item__settings"
|
||||
onClick={ () =>
|
||||
setModalIsVisible( true )
|
||||
}
|
||||
onClick={ () => setModalIsVisible( true ) }
|
||||
>
|
||||
{ data().getImage(
|
||||
'icon-settings.svg'
|
||||
) }
|
||||
{ data().getImage( 'icon-settings.svg' ) }
|
||||
</div>
|
||||
) }
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
] }
|
||||
/>
|
||||
</SettingsBlock>
|
||||
{ Modal && modalIsVisible && (
|
||||
<Modal setModalIsVisible={ setModalIsVisible } />
|
||||
) }
|
||||
|
|
|
@ -1,7 +1,14 @@
|
|||
import { useState, useCallback } from '@wordpress/element';
|
||||
import SettingsBlock from './SettingsBlock';
|
||||
import PaymentMethodItemBlock from './PaymentMethodItemBlock';
|
||||
|
||||
const PaymentMethodsBlock = ( { paymentMethods, className = '' } ) => {
|
||||
const [ selectedMethod, setSelectedMethod ] = useState( null );
|
||||
|
||||
const handleSelect = useCallback( ( methodId, isSelected ) => {
|
||||
setSelectedMethod( isSelected ? methodId : null );
|
||||
}, [] );
|
||||
|
||||
if ( paymentMethods.length === 0 ) {
|
||||
return null;
|
||||
}
|
||||
|
@ -9,19 +16,18 @@ const PaymentMethodsBlock = ( { paymentMethods, className = '' } ) => {
|
|||
return (
|
||||
<SettingsBlock
|
||||
className={ `ppcp-r-settings-block__payment-methods ${ className }` }
|
||||
components={ [
|
||||
() => (
|
||||
<>
|
||||
>
|
||||
{ paymentMethods.map( ( paymentMethod ) => (
|
||||
<PaymentMethodItemBlock
|
||||
key={ paymentMethod.id }
|
||||
{ ...paymentMethod }
|
||||
isSelected={ selectedMethod === paymentMethod.id }
|
||||
onSelect={ ( checked ) =>
|
||||
handleSelect( paymentMethod.id, checked )
|
||||
}
|
||||
/>
|
||||
) ) }
|
||||
</>
|
||||
),
|
||||
] }
|
||||
/>
|
||||
</SettingsBlock>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -11,9 +11,7 @@ const RadioSettingsBlock = ( {
|
|||
<SettingsBlock
|
||||
{ ...props }
|
||||
className="ppcp-r-settings-block__radio ppcp-r-settings-block--expert-rdb"
|
||||
components={ [
|
||||
() => (
|
||||
<>
|
||||
>
|
||||
<Header>
|
||||
<Title>{ title }</Title>
|
||||
<Description>{ description }</Description>
|
||||
|
@ -38,10 +36,7 @@ const RadioSettingsBlock = ( {
|
|||
{ option.additionalContent }
|
||||
</PayPalRdbWithContent>
|
||||
) ) }
|
||||
</>
|
||||
),
|
||||
] }
|
||||
/>
|
||||
</SettingsBlock>
|
||||
);
|
||||
|
||||
export default RadioSettingsBlock;
|
||||
|
|
|
@ -35,12 +35,7 @@ const SelectSettingsBlock = ( {
|
|||
order = DEFAULT_ELEMENT_ORDER,
|
||||
...props
|
||||
} ) => (
|
||||
<SettingsBlock
|
||||
{ ...props }
|
||||
className="ppcp-r-settings-block__select"
|
||||
components={ [
|
||||
() => (
|
||||
<>
|
||||
<SettingsBlock { ...props } className="ppcp-r-settings-block__select">
|
||||
{ order.map( ( elementKey ) => {
|
||||
const RenderElement = ELEMENT_RENDERERS[ elementKey ];
|
||||
return RenderElement ? (
|
||||
|
@ -52,10 +47,7 @@ const SelectSettingsBlock = ( {
|
|||
/>
|
||||
) : null;
|
||||
} ) }
|
||||
</>
|
||||
),
|
||||
] }
|
||||
/>
|
||||
</SettingsBlock>
|
||||
);
|
||||
|
||||
export default SelectSettingsBlock;
|
||||
|
|
|
@ -1,15 +1,9 @@
|
|||
const SettingsBlock = ( { className, components = [] } ) => {
|
||||
const SettingsBlock = ( { className, children } ) => {
|
||||
const blockClassName = [ 'ppcp-r-settings-block', className ].filter(
|
||||
Boolean
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={ blockClassName.join( ' ' ) }>
|
||||
{ components.map( ( Component, index ) => (
|
||||
<Component key={ index } />
|
||||
) ) }
|
||||
</div>
|
||||
);
|
||||
return <div className={ blockClassName.join( ' ' ) }>{ children }</div>;
|
||||
};
|
||||
|
||||
export default SettingsBlock;
|
||||
|
|
|
@ -3,11 +3,7 @@ import SettingsBlock from './SettingsBlock';
|
|||
import { Header, Title, Action, Description } from './SettingsBlockElements';
|
||||
|
||||
const ToggleSettingsBlock = ( { title, description, ...props } ) => (
|
||||
<SettingsBlock
|
||||
{ ...props }
|
||||
className="ppcp-r-settings-block__toggle"
|
||||
components={ [
|
||||
() => (
|
||||
<SettingsBlock { ...props } className="ppcp-r-settings-block__toggle">
|
||||
<Action>
|
||||
<ToggleControl
|
||||
className="ppcp-r-settings-block__toggle-control"
|
||||
|
@ -21,17 +17,11 @@ const ToggleSettingsBlock = ( { title, description, ...props } ) => (
|
|||
}
|
||||
/>
|
||||
</Action>
|
||||
),
|
||||
() => (
|
||||
<Header>
|
||||
{ title && <Title>{ title }</Title> }
|
||||
{ description && (
|
||||
<Description>{ description }</Description>
|
||||
) }
|
||||
{ description && <Description>{ description }</Description> }
|
||||
</Header>
|
||||
),
|
||||
] }
|
||||
/>
|
||||
</SettingsBlock>
|
||||
);
|
||||
|
||||
export default ToggleSettingsBlock;
|
||||
|
|
|
@ -1,23 +1,17 @@
|
|||
import { ToggleControl } from '@wordpress/components';
|
||||
import { useRef } from '@wordpress/element';
|
||||
|
||||
import SpinnerOverlay from './SpinnerOverlay';
|
||||
|
||||
const SettingsToggleBlock = ( {
|
||||
isToggled,
|
||||
setToggled,
|
||||
isLoading = false,
|
||||
disabled = false,
|
||||
...props
|
||||
} ) => {
|
||||
const toggleRef = useRef( null );
|
||||
const blockClasses = [ 'ppcp-r-toggle-block' ];
|
||||
|
||||
if ( isLoading ) {
|
||||
blockClasses.push( 'ppcp--is-loading' );
|
||||
}
|
||||
|
||||
const handleLabelClick = () => {
|
||||
if ( ! toggleRef.current || isLoading ) {
|
||||
if ( ! toggleRef.current || disabled ) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -52,13 +46,12 @@ const SettingsToggleBlock = ( {
|
|||
ref={ toggleRef }
|
||||
checked={ isToggled }
|
||||
onChange={ ( newState ) => setToggled( newState ) }
|
||||
disabled={ isLoading }
|
||||
disabled={ disabled }
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{ props.children && isToggled && (
|
||||
<div className="ppcp-r-toggle-block__toggled-content">
|
||||
{ isLoading && <SpinnerOverlay /> }
|
||||
{ props.children }
|
||||
</div>
|
||||
) }
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
import { Spinner } from '@wordpress/components';
|
||||
|
||||
const SpinnerOverlay = () => {
|
||||
const SpinnerOverlay = ( { message = '' } ) => {
|
||||
return (
|
||||
<div className="ppcp-r-spinner-overlay">
|
||||
{ message && (
|
||||
<span className="ppcp-r-spinner-overlay__message">
|
||||
{ message }
|
||||
</span>
|
||||
) }
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { useCallback, useEffect, useState } from '@wordpress/element';
|
||||
import { TabPanel } from '@wordpress/components';
|
||||
import { getQuery, updateQueryString } from '@woocommerce/navigation';
|
||||
|
||||
import { getQuery, updateQueryString } from '../../utils/navigation';
|
||||
|
||||
const TabNavigation = ( { tabs } ) => {
|
||||
const { panel } = getQuery();
|
||||
|
@ -30,7 +31,7 @@ const TabNavigation = ( { tabs } ) => {
|
|||
);
|
||||
|
||||
useEffect( () => {
|
||||
updateQueryString( { panel: activePanel }, '/', getQuery() );
|
||||
updateQueryString( { panel: activePanel } );
|
||||
}, [ activePanel ] );
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,17 +1,11 @@
|
|||
import BadgeBox, { BADGE_BOX_TITLE_BIG } from '../BadgeBox';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
|
||||
import BadgeBox, { BADGE_BOX_TITLE_BIG } from '../BadgeBox';
|
||||
import Separator from '../Separator';
|
||||
import generatePriceText from '../../../utils/badgeBoxUtils';
|
||||
import { countryPriceInfo } from '../../../utils/countryPriceInfo';
|
||||
|
||||
import OptionalPaymentMethods from '../OptionalPaymentMethods/OptionalPaymentMethods';
|
||||
import PricingTitleBadge from '../PricingTitleBadge';
|
||||
|
||||
const AcdcFlow = ( {
|
||||
isFastlane,
|
||||
isPayLater,
|
||||
storeCountry,
|
||||
storeCurrency,
|
||||
} ) => {
|
||||
const AcdcFlow = ( { isFastlane, isPayLater, storeCountry } ) => {
|
||||
if ( isFastlane && isPayLater && storeCountry === 'US' ) {
|
||||
return (
|
||||
<div className="ppcp-r-welcome-docs__wrapper">
|
||||
|
@ -22,11 +16,7 @@ const AcdcFlow = ( {
|
|||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
titleType={ BADGE_BOX_TITLE_BIG }
|
||||
textBadge={ generatePriceText(
|
||||
'checkout',
|
||||
countryPriceInfo[ storeCountry ],
|
||||
storeCurrency
|
||||
) }
|
||||
textBadge={ <PricingTitleBadge item="checkout" /> }
|
||||
description={ __(
|
||||
'Our all-in-one checkout solution lets you offer PayPal, Venmo, Pay Later options, and more to help maximise conversion',
|
||||
'woocommerce-paypal-payments'
|
||||
|
@ -63,10 +53,13 @@ const AcdcFlow = ( {
|
|||
imageBadge={ [
|
||||
'icon-payment-method-paypal-small.svg',
|
||||
] }
|
||||
textBadge={
|
||||
<PricingTitleBadge item="plater" />
|
||||
}
|
||||
description={ sprintf(
|
||||
// translators: %s: Link to PayPal business fees guide
|
||||
__(
|
||||
'Offer installment payment options and get paid upfront - at no extra cost to you. <a target="_blank" href="%s">Learn more</a>',
|
||||
'Offer installment payment options and get paid upfront. <a target="_blank" href="%s">Learn more</a>',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
'https://www.paypal.com/us/business/paypal-business-fees'
|
||||
|
@ -116,7 +109,6 @@ const AcdcFlow = ( {
|
|||
isFastlane={ isFastlane }
|
||||
isPayLater={ isPayLater }
|
||||
storeCountry={ storeCountry }
|
||||
storeCurrency={ storeCurrency }
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -133,11 +125,7 @@ const AcdcFlow = ( {
|
|||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
titleType={ BADGE_BOX_TITLE_BIG }
|
||||
textBadge={ generatePriceText(
|
||||
'checkout',
|
||||
countryPriceInfo[ storeCountry ],
|
||||
storeCurrency
|
||||
) }
|
||||
textBadge={ <PricingTitleBadge item="checkout" /> }
|
||||
description={ __(
|
||||
'Our all-in-one checkout solution lets you offer PayPal, Venmo, Pay Later options, and more to help maximise conversion',
|
||||
'woocommerce-paypal-payments'
|
||||
|
@ -201,7 +189,6 @@ const AcdcFlow = ( {
|
|||
isFastlane={ isFastlane }
|
||||
isPayLater={ isPayLater }
|
||||
storeCountry={ storeCountry }
|
||||
storeCurrency={ storeCurrency }
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -217,11 +204,7 @@ const AcdcFlow = ( {
|
|||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
titleType={ BADGE_BOX_TITLE_BIG }
|
||||
textBadge={ generatePriceText(
|
||||
'checkout',
|
||||
countryPriceInfo[ storeCountry ],
|
||||
storeCurrency
|
||||
) }
|
||||
textBadge={ <PricingTitleBadge item="checkout" /> }
|
||||
description={ __(
|
||||
'Our all-in-one checkout solution lets you offer PayPal, Venmo, Pay Later options, and more to help maximise conversion',
|
||||
'woocommerce-paypal-payments'
|
||||
|
@ -256,7 +239,7 @@ const AcdcFlow = ( {
|
|||
description={ sprintf(
|
||||
// translators: %s: Link to PayPal REST application guide
|
||||
__(
|
||||
'Offer installment payment options and get paid upfront - at no extra cost to you. <a target="_blank" href="%s">Learn more</a>',
|
||||
'Offer installment payment options and get paid upfront. <a target="_blank" href="%s">Learn more</a>',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input '
|
||||
|
@ -280,7 +263,6 @@ const AcdcFlow = ( {
|
|||
isFastlane={ isFastlane }
|
||||
isPayLater={ isPayLater }
|
||||
storeCountry={ storeCountry }
|
||||
storeCurrency={ storeCurrency }
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import BadgeBox, { BADGE_BOX_TITLE_BIG } from '../BadgeBox';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import Separator from '../Separator';
|
||||
import generatePriceText from '../../../utils/badgeBoxUtils';
|
||||
import { countryPriceInfo } from '../../../utils/countryPriceInfo';
|
||||
import OptionalPaymentMethods from '../OptionalPaymentMethods/OptionalPaymentMethods';
|
||||
|
||||
const BcdcFlow = ( { isPayLater, storeCountry, storeCurrency } ) => {
|
||||
import BadgeBox, { BADGE_BOX_TITLE_BIG } from '../BadgeBox';
|
||||
import Separator from '../Separator';
|
||||
import OptionalPaymentMethods from '../OptionalPaymentMethods/OptionalPaymentMethods';
|
||||
import PricingTitleBadge from '../PricingTitleBadge';
|
||||
|
||||
const BcdcFlow = ( { isPayLater, storeCountry } ) => {
|
||||
if ( isPayLater && storeCountry === 'US' ) {
|
||||
return (
|
||||
<div className="ppcp-r-welcome-docs__wrapper">
|
||||
|
@ -16,11 +16,7 @@ const BcdcFlow = ( { isPayLater, storeCountry, storeCurrency } ) => {
|
|||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
titleType={ BADGE_BOX_TITLE_BIG }
|
||||
textBadge={ generatePriceText(
|
||||
'checkout',
|
||||
countryPriceInfo[ storeCountry ],
|
||||
storeCurrency
|
||||
) }
|
||||
textBadge={ <PricingTitleBadge item="checkout" /> }
|
||||
description={ __(
|
||||
'Our all-in-one checkout solution lets you offer PayPal, Venmo, Pay Later options, and more to help maximise conversion',
|
||||
'woocommerce-paypal-payments'
|
||||
|
@ -60,7 +56,7 @@ const BcdcFlow = ( { isPayLater, storeCountry, storeCurrency } ) => {
|
|||
description={ sprintf(
|
||||
// translators: %s: Link to PayPal REST application guide
|
||||
__(
|
||||
'Offer installment payment options and get paid upfront - at no extra cost to you. <a target="_blank" href="%s">Learn more</a>',
|
||||
'Offer installment payment options and get paid upfront. <a target="_blank" href="%s">Learn more</a>',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input '
|
||||
|
@ -110,7 +106,6 @@ const BcdcFlow = ( { isPayLater, storeCountry, storeCurrency } ) => {
|
|||
isFastlane={ false }
|
||||
isPayLater={ isPayLater }
|
||||
storeCountry={ storeCountry }
|
||||
storeCurrency={ storeCurrency }
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -122,11 +117,7 @@ const BcdcFlow = ( { isPayLater, storeCountry, storeCurrency } ) => {
|
|||
<BadgeBox
|
||||
title={ __( 'PayPal Checkout', 'woocommerce-paypal-payments' ) }
|
||||
titleType={ BADGE_BOX_TITLE_BIG }
|
||||
textBadge={ generatePriceText(
|
||||
'checkout',
|
||||
countryPriceInfo[ storeCountry ],
|
||||
storeCurrency
|
||||
) }
|
||||
textBadge={ <PricingTitleBadge item="checkout" /> }
|
||||
description={ __(
|
||||
'Our all-in-one checkout solution lets you offer PayPal, Venmo, Pay Later options, and more to help maximise conversion',
|
||||
'woocommerce-paypal-payments'
|
||||
|
@ -158,7 +149,7 @@ const BcdcFlow = ( { isPayLater, storeCountry, storeCurrency } ) => {
|
|||
description={ sprintf(
|
||||
// translators: %s: Link to PayPal REST application guide
|
||||
__(
|
||||
'Offer installment payment options and get paid upfront - at no extra cost to you. <a target="_blank" href="%s">Learn more</a>',
|
||||
'Offer installment payment options and get paid upfront. <a target="_blank" href="%s">Learn more</a>',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input '
|
||||
|
@ -181,7 +172,6 @@ const BcdcFlow = ( { isPayLater, storeCountry, storeCurrency } ) => {
|
|||
isFastlane={ false }
|
||||
isPayLater={ isPayLater }
|
||||
storeCountry={ storeCountry }
|
||||
storeCurrency={ storeCurrency }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,24 +1,10 @@
|
|||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
import PricingDescription from '../PricingDescription';
|
||||
import AcdcFlow from './AcdcFlow';
|
||||
import BcdcFlow from './BcdcFlow';
|
||||
import { Button } from '@wordpress/components';
|
||||
|
||||
const WelcomeDocs = ( {
|
||||
useAcdc,
|
||||
isFastlane,
|
||||
isPayLater,
|
||||
storeCountry,
|
||||
storeCurrency,
|
||||
} ) => {
|
||||
const pricesBasedDescription = sprintf(
|
||||
// translators: %s: Link to PayPal REST application guide
|
||||
__(
|
||||
'<sup>1</sup>Prices based on domestic transactions as of October 25th, 2024. <a target="_blank" href="%s">Click here</a> for full pricing details.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input '
|
||||
);
|
||||
|
||||
const WelcomeDocs = ( { useAcdc, isFastlane, isPayLater, storeCountry } ) => {
|
||||
return (
|
||||
<div className="ppcp-r-welcome-docs">
|
||||
<h2 className="ppcp-r-welcome-docs__title">
|
||||
|
@ -32,19 +18,14 @@ const WelcomeDocs = ( {
|
|||
isFastlane={ isFastlane }
|
||||
isPayLater={ isPayLater }
|
||||
storeCountry={ storeCountry }
|
||||
storeCurrency={ storeCurrency }
|
||||
/>
|
||||
) : (
|
||||
<BcdcFlow
|
||||
isPayLater={ isPayLater }
|
||||
storeCountry={ storeCountry }
|
||||
storeCurrency={ storeCurrency }
|
||||
/>
|
||||
) }
|
||||
<p
|
||||
className="ppcp-r-optional-payment-methods__description"
|
||||
dangerouslySetInnerHTML={ { __html: pricesBasedDescription } }
|
||||
></p>
|
||||
<PricingDescription />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,96 +1,118 @@
|
|||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { Button, TextControl } from '@wordpress/components';
|
||||
import { useRef, useMemo } from '@wordpress/element';
|
||||
import { useDispatch } from '@wordpress/data';
|
||||
import { store as noticesStore } from '@wordpress/notices';
|
||||
import {
|
||||
useRef,
|
||||
useState,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useCallback,
|
||||
} from '@wordpress/element';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import SettingsToggleBlock from '../../../ReusableComponents/SettingsToggleBlock';
|
||||
import Separator from '../../../ReusableComponents/Separator';
|
||||
import DataStoreControl from '../../../ReusableComponents/DataStoreControl';
|
||||
import { CommonHooks } from '../../../../data';
|
||||
import { openPopup } from '../../../../utils/window';
|
||||
import {
|
||||
useSandboxConnection,
|
||||
useManualConnection,
|
||||
} from '../../../../hooks/useHandleConnections';
|
||||
|
||||
import ConnectionButton from './ConnectionButton';
|
||||
import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper';
|
||||
|
||||
const FORM_ERRORS = {
|
||||
noClientId: __(
|
||||
'Please enter your Client ID',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
noClientSecret: __(
|
||||
'Please enter your Secret Key',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
invalidClientId: __(
|
||||
'Please enter a valid Client ID',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
};
|
||||
|
||||
const AdvancedOptionsForm = () => {
|
||||
const [ clientValid, setClientValid ] = useState( false );
|
||||
const [ secretValid, setSecretValid ] = useState( false );
|
||||
|
||||
const AdvancedOptionsForm = ( { setCompleted } ) => {
|
||||
const { isBusy } = CommonHooks.useBusyState();
|
||||
const { isSandboxMode, setSandboxMode, connectViaSandbox } =
|
||||
CommonHooks.useSandbox();
|
||||
const { isSandboxMode, setSandboxMode } = useSandboxConnection();
|
||||
const {
|
||||
handleConnectViaIdAndSecret,
|
||||
isManualConnectionMode,
|
||||
setManualConnectionMode,
|
||||
clientId,
|
||||
setClientId,
|
||||
clientSecret,
|
||||
setClientSecret,
|
||||
connectViaIdAndSecret,
|
||||
} = CommonHooks.useManualConnection();
|
||||
} = useManualConnection();
|
||||
|
||||
const { createSuccessNotice, createErrorNotice } =
|
||||
useDispatch( noticesStore );
|
||||
const refClientId = useRef( null );
|
||||
const refClientSecret = useRef( null );
|
||||
|
||||
const isValidClientId = useMemo( () => {
|
||||
return /^A[\w-]{79}$/.test( clientId );
|
||||
}, [ clientId ] );
|
||||
const validateManualConnectionForm = useCallback( () => {
|
||||
const checks = [
|
||||
{
|
||||
ref: refClientId,
|
||||
valid: () => clientId,
|
||||
errorMessage: FORM_ERRORS.noClientId,
|
||||
},
|
||||
{
|
||||
ref: refClientId,
|
||||
valid: () => clientValid,
|
||||
errorMessage: FORM_ERRORS.invalidClientId,
|
||||
},
|
||||
{
|
||||
ref: refClientSecret,
|
||||
valid: () => clientSecret && secretValid,
|
||||
errorMessage: FORM_ERRORS.noClientSecret,
|
||||
},
|
||||
];
|
||||
|
||||
const isFormValid = useMemo( () => {
|
||||
return isValidClientId && clientId && clientSecret;
|
||||
}, [ isValidClientId, clientId, clientSecret ] );
|
||||
|
||||
const handleServerError = ( res, genericMessage ) => {
|
||||
console.error( 'Connection error', res );
|
||||
createErrorNotice( res?.message ?? genericMessage );
|
||||
};
|
||||
|
||||
const handleServerSuccess = () => {
|
||||
createSuccessNotice(
|
||||
__( 'Connected to PayPal', 'woocommerce-paypal-payments' )
|
||||
);
|
||||
setCompleted( true );
|
||||
};
|
||||
|
||||
const handleSandboxConnect = async () => {
|
||||
const res = await connectViaSandbox();
|
||||
|
||||
if ( ! res.success || ! res.data ) {
|
||||
handleServerError(
|
||||
res,
|
||||
__(
|
||||
'Could not generate a Sandbox login link.',
|
||||
'woocommerce-paypal-payments'
|
||||
)
|
||||
);
|
||||
return;
|
||||
for ( const { ref, valid, errorMessage } of checks ) {
|
||||
if ( valid() ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const connectionUrl = res.data;
|
||||
const popup = openPopup( connectionUrl );
|
||||
|
||||
if ( ! popup ) {
|
||||
createErrorNotice(
|
||||
__(
|
||||
'Popup blocked. Please allow popups for this site to connect to PayPal.',
|
||||
'woocommerce-paypal-payments'
|
||||
)
|
||||
);
|
||||
ref?.current?.focus();
|
||||
throw new Error( errorMessage );
|
||||
}
|
||||
};
|
||||
}, [ clientId, clientSecret, clientValid, secretValid ] );
|
||||
|
||||
const handleManualConnect = async () => {
|
||||
const res = await connectViaIdAndSecret();
|
||||
|
||||
if ( res.success ) {
|
||||
handleServerSuccess();
|
||||
} else {
|
||||
handleServerError(
|
||||
res,
|
||||
__(
|
||||
'Could not connect to PayPal. Please make sure your Client ID and Secret Key are correct.',
|
||||
'woocommerce-paypal-payments'
|
||||
)
|
||||
const handleManualConnect = useCallback(
|
||||
() =>
|
||||
handleConnectViaIdAndSecret( {
|
||||
validation: validateManualConnectionForm,
|
||||
} ),
|
||||
[ handleConnectViaIdAndSecret, validateManualConnectionForm ]
|
||||
);
|
||||
|
||||
useEffect( () => {
|
||||
setClientValid( ! clientId || /^A[\w-]{79}$/.test( clientId ) );
|
||||
setSecretValid( clientSecret && clientSecret.length > 0 );
|
||||
}, [ clientId, clientSecret ] );
|
||||
|
||||
const clientIdLabel = useMemo(
|
||||
() =>
|
||||
isSandboxMode
|
||||
? __( 'Sandbox Client ID', 'woocommerce-paypal-payments' )
|
||||
: __( 'Live Client ID', 'woocommerce-paypal-payments' ),
|
||||
[ isSandboxMode ]
|
||||
);
|
||||
|
||||
const secretKeyLabel = useMemo(
|
||||
() =>
|
||||
isSandboxMode
|
||||
? __( 'Sandbox Secret Key', 'woocommerce-paypal-payments' )
|
||||
: __( 'Live Secret Key', 'woocommerce-paypal-payments' ),
|
||||
[ isSandboxMode ]
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const advancedUsersDescription = sprintf(
|
||||
// translators: %s: Link to PayPal REST application guide
|
||||
|
@ -103,6 +125,7 @@ const AdvancedOptionsForm = ( { setCompleted } ) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<BusyStateWrapper>
|
||||
<SettingsToggleBlock
|
||||
label={ __(
|
||||
'Enable Sandbox Mode',
|
||||
|
@ -114,77 +137,72 @@ const AdvancedOptionsForm = ( { setCompleted } ) => {
|
|||
) }
|
||||
isToggled={ !! isSandboxMode }
|
||||
setToggled={ setSandboxMode }
|
||||
isLoading={ isBusy }
|
||||
>
|
||||
<Button onClick={ handleSandboxConnect } variant="secondary">
|
||||
{ __( 'Connect Account', 'woocommerce-paypal-payments' ) }
|
||||
</Button>
|
||||
</SettingsToggleBlock>
|
||||
<Separator withLine={ false } />
|
||||
<SettingsToggleBlock
|
||||
label={
|
||||
__( 'Manually Connect', 'woocommerce-paypal-payments' ) +
|
||||
( isBusy ? ' ...' : '' )
|
||||
<ConnectionButton
|
||||
title={ __(
|
||||
'Connect Account',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
showIcon={ false }
|
||||
variant="secondary"
|
||||
className="small-button"
|
||||
isSandbox={
|
||||
true /* This button always connects to sandbox */
|
||||
}
|
||||
/>
|
||||
</SettingsToggleBlock>
|
||||
</BusyStateWrapper>
|
||||
<Separator withLine={ false } />
|
||||
<BusyStateWrapper
|
||||
onBusy={ ( props ) => ( {
|
||||
disabled: true,
|
||||
label: props.label + ' ...',
|
||||
} ) }
|
||||
>
|
||||
<SettingsToggleBlock
|
||||
label={ __(
|
||||
'Manually Connect',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
description={ advancedUsersDescription }
|
||||
isToggled={ !! isManualConnectionMode }
|
||||
setToggled={ setManualConnectionMode }
|
||||
isLoading={ isBusy }
|
||||
>
|
||||
<DataStoreControl
|
||||
control={ TextControl }
|
||||
ref={ refClientId }
|
||||
label={
|
||||
isSandboxMode
|
||||
? __(
|
||||
'Sandbox Client ID',
|
||||
'woocommerce-paypal-payments'
|
||||
)
|
||||
: __(
|
||||
'Live Client ID',
|
||||
'woocommerce-paypal-payments'
|
||||
)
|
||||
}
|
||||
label={ clientIdLabel }
|
||||
value={ clientId }
|
||||
onChange={ setClientId }
|
||||
className={
|
||||
clientId && ! isValidClientId ? 'has-error' : ''
|
||||
}
|
||||
className={ classNames( {
|
||||
'has-error': ! clientValid,
|
||||
} ) }
|
||||
/>
|
||||
{ clientId && ! isValidClientId && (
|
||||
{ clientValid || (
|
||||
<p className="client-id-error">
|
||||
{ __(
|
||||
'Please enter a valid Client ID',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
{ FORM_ERRORS.invalidClientId }
|
||||
</p>
|
||||
) }
|
||||
<DataStoreControl
|
||||
control={ TextControl }
|
||||
ref={ refClientSecret }
|
||||
label={
|
||||
isSandboxMode
|
||||
? __(
|
||||
'Sandbox Secret Key',
|
||||
'woocommerce-paypal-payments'
|
||||
)
|
||||
: __(
|
||||
'Live Secret Key',
|
||||
'woocommerce-paypal-payments'
|
||||
)
|
||||
}
|
||||
label={ secretKeyLabel }
|
||||
value={ clientSecret }
|
||||
onChange={ setClientSecret }
|
||||
type="password"
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="small-button"
|
||||
onClick={ handleManualConnect }
|
||||
disabled={ ! isFormValid }
|
||||
>
|
||||
{ __( 'Connect Account', 'woocommerce-paypal-payments' ) }
|
||||
{ __(
|
||||
'Connect Account',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
</Button>
|
||||
</SettingsToggleBlock>
|
||||
</BusyStateWrapper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
import { Button } from '@wordpress/components';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { CommonHooks } from '../../../../data';
|
||||
import { openSignup } from '../../../ReusableComponents/Icons';
|
||||
import {
|
||||
useProductionConnection,
|
||||
useSandboxConnection,
|
||||
} from '../../../../hooks/useHandleConnections';
|
||||
import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper';
|
||||
|
||||
const ConnectionButton = ( {
|
||||
title,
|
||||
isSandbox = false,
|
||||
variant = 'primary',
|
||||
showIcon = true,
|
||||
className = '',
|
||||
} ) => {
|
||||
const { handleSandboxConnect } = useSandboxConnection();
|
||||
const { handleProductionConnect } = useProductionConnection();
|
||||
const buttonClassName = classNames( 'ppcp-r-connection-button', className, {
|
||||
'sandbox-mode': isSandbox,
|
||||
'live-mode': ! isSandbox,
|
||||
} );
|
||||
|
||||
const handleConnectClick = async () => {
|
||||
if ( isSandbox ) {
|
||||
await handleSandboxConnect();
|
||||
} else {
|
||||
await handleProductionConnect();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<BusyStateWrapper>
|
||||
<Button
|
||||
className={ buttonClassName }
|
||||
variant={ variant }
|
||||
icon={ showIcon ? openSignup : null }
|
||||
onClick={ handleConnectClick }
|
||||
>
|
||||
<span className="button-title">{ title }</span>
|
||||
</Button>
|
||||
</BusyStateWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConnectionButton;
|
|
@ -6,6 +6,7 @@ import classNames from 'classnames';
|
|||
|
||||
import { OnboardingHooks } from '../../../../data';
|
||||
import useIsScrolled from '../../../../hooks/useIsScrolled';
|
||||
import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper';
|
||||
|
||||
const Navigation = ( { stepDetails, onNext, onPrev, onExit } ) => {
|
||||
const { title, isFirst, percentage, showNext, canProceed } = stepDetails;
|
||||
|
@ -20,7 +21,11 @@ const Navigation = ( { stepDetails, onNext, onPrev, onExit } ) => {
|
|||
return (
|
||||
<div className={ className }>
|
||||
<div className="ppcp-r-navigation">
|
||||
<div className="ppcp-r-navigation--left">
|
||||
<BusyStateWrapper
|
||||
className="ppcp-r-navigation--left"
|
||||
busySpinner={ false }
|
||||
enabled={ ! isFirst }
|
||||
>
|
||||
<Button
|
||||
variant="link"
|
||||
onClick={ isFirst ? onExit : onPrev }
|
||||
|
@ -31,7 +36,7 @@ const Navigation = ( { stepDetails, onNext, onPrev, onExit } ) => {
|
|||
{ title }
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</BusyStateWrapper>
|
||||
{ ! isFirst &&
|
||||
NextButton( { showNext, isDisabled, onNext, onExit } ) }
|
||||
<ProgressBar percent={ percentage } />
|
||||
|
@ -42,7 +47,10 @@ const Navigation = ( { stepDetails, onNext, onPrev, onExit } ) => {
|
|||
|
||||
const NextButton = ( { showNext, isDisabled, onNext, onExit } ) => {
|
||||
return (
|
||||
<div className="ppcp-r-navigation--right">
|
||||
<BusyStateWrapper
|
||||
className="ppcp-r-navigation--right"
|
||||
busySpinner={ false }
|
||||
>
|
||||
<Button variant="link" onClick={ onExit }>
|
||||
{ __( 'Save and exit', 'woocommerce-paypal-payments' ) }
|
||||
</Button>
|
||||
|
@ -55,7 +63,7 @@ const NextButton = ( { showNext, isDisabled, onNext, onExit } ) => {
|
|||
{ __( 'Continue', 'woocommerce-paypal-payments' ) }
|
||||
</Button>
|
||||
) }
|
||||
</div>
|
||||
</BusyStateWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -5,8 +5,7 @@ import { getSteps, getCurrentStep } from './availableSteps';
|
|||
import Navigation from './Components/Navigation';
|
||||
|
||||
const Onboarding = () => {
|
||||
const { step, setStep, setCompleted, flags } = OnboardingHooks.useSteps();
|
||||
|
||||
const { step, setStep, flags } = OnboardingHooks.useSteps();
|
||||
const Steps = getSteps( flags );
|
||||
const currentStep = getCurrentStep( step, Steps );
|
||||
|
||||
|
@ -30,7 +29,6 @@ const Onboarding = () => {
|
|||
<currentStep.StepComponent
|
||||
setStep={ setStep }
|
||||
currentStep={ step }
|
||||
setCompleted={ setCompleted }
|
||||
stepperOrder={ Steps }
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -1,28 +1,9 @@
|
|||
import { __ } from '@wordpress/i18n';
|
||||
import { Button, Icon } from '@wordpress/components';
|
||||
|
||||
import OnboardingHeader from '../../ReusableComponents/OnboardingHeader';
|
||||
import ConnectionButton from './Components/ConnectionButton';
|
||||
|
||||
const StepCompleteSetup = ( { setCompleted } ) => {
|
||||
const ButtonIcon = () => (
|
||||
<Icon
|
||||
icon={ () => (
|
||||
<svg
|
||||
width="25"
|
||||
height="24"
|
||||
viewBox="0 0 25 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12.4999 12.75V18.75C12.4999 18.9489 12.4209 19.1397 12.2803 19.2803C12.1396 19.421 11.9488 19.5 11.7499 19.5C11.551 19.5 11.3603 19.421 11.2196 19.2803C11.0789 19.1397 10.9999 18.9489 10.9999 18.75V14.5613L4.78055 20.7806C4.71087 20.8503 4.62815 20.9056 4.5371 20.9433C4.44606 20.981 4.34847 21.0004 4.24993 21.0004C4.15138 21.0004 4.0538 20.981 3.96276 20.9433C3.87171 20.9056 3.78899 20.8503 3.7193 20.7806C3.64962 20.7109 3.59435 20.6282 3.55663 20.5372C3.51892 20.4461 3.49951 20.3485 3.49951 20.25C3.49951 20.1515 3.51892 20.0539 3.55663 19.9628C3.59435 19.8718 3.64962 19.7891 3.7193 19.7194L9.93868 13.5H5.74993C5.55102 13.5 5.36025 13.421 5.2196 13.2803C5.07895 13.1397 4.99993 12.9489 4.99993 12.75C4.99993 12.5511 5.07895 12.3603 5.2196 12.2197C5.36025 12.079 5.55102 12 5.74993 12H11.7499C11.9488 12 12.1396 12.079 12.2803 12.2197C12.4209 12.3603 12.4999 12.5511 12.4999 12.75ZM19.9999 3H7.99993C7.6021 3 7.22057 3.15804 6.93927 3.43934C6.65796 3.72064 6.49993 4.10218 6.49993 4.5V9C6.49993 9.19891 6.57895 9.38968 6.7196 9.53033C6.86025 9.67098 7.05102 9.75 7.24993 9.75C7.44884 9.75 7.63961 9.67098 7.78026 9.53033C7.92091 9.38968 7.99993 9.19891 7.99993 9V4.5H19.9999V16.5H15.4999C15.301 16.5 15.1103 16.579 14.9696 16.7197C14.8289 16.8603 14.7499 17.0511 14.7499 17.25C14.7499 17.4489 14.8289 17.6397 14.9696 17.7803C15.1103 17.921 15.301 18 15.4999 18H19.9999C20.3978 18 20.7793 17.842 21.0606 17.5607C21.3419 17.2794 21.4999 16.8978 21.4999 16.5V4.5C21.4999 4.10218 21.3419 3.72064 21.0606 3.43934C20.7793 3.15804 20.3978 3 19.9999 3Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
) }
|
||||
/>
|
||||
);
|
||||
|
||||
const StepCompleteSetup = () => {
|
||||
return (
|
||||
<div className="ppcp-r-page-products">
|
||||
<OnboardingHeader
|
||||
|
@ -37,18 +18,12 @@ const StepCompleteSetup = ( { setCompleted } ) => {
|
|||
/>
|
||||
<div className="ppcp-r-inner-container">
|
||||
<div className="ppcp-r-onboarding-header__description">
|
||||
<Button
|
||||
variant="primary"
|
||||
icon={ ButtonIcon }
|
||||
onClick={ () => {
|
||||
setCompleted( true );
|
||||
} }
|
||||
>
|
||||
{ __(
|
||||
<ConnectionButton
|
||||
title={ __(
|
||||
'Connect to PayPal',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
</Button>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
import { CommonHooks, OnboardingHooks } from '../../../data';
|
||||
import OnboardingHeader from '../../ReusableComponents/OnboardingHeader';
|
||||
import SelectBoxWrapper from '../../ReusableComponents/SelectBoxWrapper';
|
||||
import SelectBox from '../../ReusableComponents/SelectBox';
|
||||
import { CommonHooks, OnboardingHooks } from '../../../data';
|
||||
import OptionalPaymentMethods from '../../ReusableComponents/OptionalPaymentMethods/OptionalPaymentMethods';
|
||||
import PricingDescription from '../../ReusableComponents/PricingDescription';
|
||||
|
||||
const OPM_RADIO_GROUP_NAME = 'optional-payment-methods';
|
||||
|
||||
|
@ -16,15 +17,6 @@ const StepPaymentMethods = ( {} ) => {
|
|||
|
||||
const { storeCountry, storeCurrency } = CommonHooks.useWooSettings();
|
||||
|
||||
const pricesBasedDescription = sprintf(
|
||||
// translators: %s: Link to PayPal REST application guide
|
||||
__(
|
||||
'<sup>1</sup>Prices based on domestic transactions as of October 25th, 2024. <a target="_blank" href="%s">Click here</a> for full pricing details.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input '
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="ppcp-r-page-optional-payment-methods">
|
||||
<OnboardingHeader
|
||||
|
@ -67,12 +59,7 @@ const StepPaymentMethods = ( {} ) => {
|
|||
type="radio"
|
||||
></SelectBox>
|
||||
</SelectBoxWrapper>
|
||||
<p
|
||||
className="ppcp-r-optional-payment-methods__description"
|
||||
dangerouslySetInnerHTML={ {
|
||||
__html: pricesBasedDescription,
|
||||
} }
|
||||
></p>
|
||||
<PricingDescription />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -9,9 +9,11 @@ import AccordionSection from '../../ReusableComponents/AccordionSection';
|
|||
|
||||
import AdvancedOptionsForm from './Components/AdvancedOptionsForm';
|
||||
import { CommonHooks } from '../../../data';
|
||||
import BusyStateWrapper from '../../ReusableComponents/BusyStateWrapper';
|
||||
|
||||
const StepWelcome = ( { setStep, currentStep } ) => {
|
||||
const { storeCountry } = CommonHooks.useWooSettings();
|
||||
|
||||
const StepWelcome = ( { setStep, currentStep, setCompleted } ) => {
|
||||
const { storeCountry, storeCurrency } = CommonHooks.useWooSettings();
|
||||
return (
|
||||
<div className="ppcp-r-page-welcome">
|
||||
<OnboardingHeader
|
||||
|
@ -33,6 +35,7 @@ const StepWelcome = ( { setStep, currentStep, setCompleted } ) => {
|
|||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
</p>
|
||||
<BusyStateWrapper>
|
||||
<Button
|
||||
className="ppcp-r-button-activate-paypal"
|
||||
variant="primary"
|
||||
|
@ -43,6 +46,7 @@ const StepWelcome = ( { setStep, currentStep, setCompleted } ) => {
|
|||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
</Button>
|
||||
</BusyStateWrapper>
|
||||
</div>
|
||||
<Separator className="ppcp-r-page-welcome-mode-separator" />
|
||||
<WelcomeDocs
|
||||
|
@ -50,7 +54,6 @@ const StepWelcome = ( { setStep, currentStep, setCompleted } ) => {
|
|||
isFastlane={ true }
|
||||
isPayLater={ true }
|
||||
storeCountry={ storeCountry }
|
||||
storeCurrency={ storeCurrency }
|
||||
/>
|
||||
<Separator text={ __( 'or', 'woocommerce-paypal-payments' ) } />
|
||||
<AccordionSection
|
||||
|
@ -61,7 +64,7 @@ const StepWelcome = ( { setStep, currentStep, setCompleted } ) => {
|
|||
className="onboarding-advanced-options"
|
||||
id="advanced-options"
|
||||
>
|
||||
<AdvancedOptionsForm setCompleted={ setCompleted } />
|
||||
<AdvancedOptionsForm />
|
||||
</AccordionSection>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,15 +1,49 @@
|
|||
import { __ } from '@wordpress/i18n';
|
||||
import { useState } from '@wordpress/element';
|
||||
import { Button } from '@wordpress/components';
|
||||
import { Button, Icon } from '@wordpress/components';
|
||||
import { useDispatch } from '@wordpress/data';
|
||||
import { reusableBlock } from '@wordpress/icons';
|
||||
|
||||
import SettingsCard from '../../ReusableComponents/SettingsCard';
|
||||
import TodoSettingsBlock from '../../ReusableComponents/SettingsBlocks/TodoSettingsBlock';
|
||||
import FeatureSettingsBlock from '../../ReusableComponents/SettingsBlocks/FeatureSettingsBlock';
|
||||
import { TITLE_BADGE_POSITIVE } from '../../ReusableComponents/TitleBadge';
|
||||
import data from '../../../utils/data';
|
||||
import { useMerchantInfo } from '../../../data/common/hooks';
|
||||
import { STORE_NAME } from '../../../data/common';
|
||||
|
||||
const TabOverview = () => {
|
||||
const [ todos, setTodos ] = useState( [] );
|
||||
const [ todosData, setTodosData ] = useState( todosDataDefault );
|
||||
const [ isRefreshing, setIsRefreshing ] = useState( false );
|
||||
|
||||
const { merchant } = useMerchantInfo();
|
||||
const { refreshFeatureStatuses } = useDispatch( STORE_NAME );
|
||||
|
||||
const features = featuresDefault.map( ( feature ) => {
|
||||
const merchantFeature = merchant?.features?.[ feature.id ];
|
||||
return {
|
||||
...feature,
|
||||
enabled: merchantFeature?.enabled ?? false,
|
||||
};
|
||||
} );
|
||||
|
||||
const refreshHandler = async () => {
|
||||
setIsRefreshing( true );
|
||||
|
||||
const result = await refreshFeatureStatuses();
|
||||
|
||||
// TODO: Implement the refresh logic, remove this debug code -- PCP-4024
|
||||
if ( result && ! result.success ) {
|
||||
console.error(
|
||||
'Failed to refresh features:',
|
||||
result.message || 'Unknown error'
|
||||
);
|
||||
} else {
|
||||
console.log( 'Features refreshed successfully.' );
|
||||
}
|
||||
|
||||
setIsRefreshing( false );
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="ppcp-r-tab-overview">
|
||||
|
@ -38,39 +72,118 @@ const TabOverview = () => {
|
|||
className="ppcp-r-tab-overview-features"
|
||||
title={ __( 'Features', 'woocommerce-paypal-payments' ) }
|
||||
description={
|
||||
<div>
|
||||
<p>{ __( 'Enable additional features…' ) }</p>
|
||||
<p>{ __( 'Click Refresh…' ) }</p>
|
||||
<Button variant="tertiary">
|
||||
{ data().getImage( 'icon-refresh.svg' ) }
|
||||
{ __( 'Refresh', 'woocommerce-paypal-payments' ) }
|
||||
<>
|
||||
<p>
|
||||
{ __(
|
||||
'Enable additional features and capabilities on your WooCommerce store.',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
</p>
|
||||
<p>
|
||||
{ __(
|
||||
'Click Refresh to update your current features after making changes.',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
</p>
|
||||
<Button
|
||||
variant="tertiary"
|
||||
onClick={ refreshHandler }
|
||||
disabled={ isRefreshing }
|
||||
>
|
||||
<Icon icon={ reusableBlock } size={ 18 } />
|
||||
{ isRefreshing
|
||||
? __(
|
||||
'Refreshing…',
|
||||
'woocommerce-paypal-payments'
|
||||
)
|
||||
: __(
|
||||
'Refresh',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
contentItems={ featuresDefault.map( ( feature ) => (
|
||||
contentItems={ features.map( ( feature ) => (
|
||||
<FeatureSettingsBlock
|
||||
key={ feature.id }
|
||||
title={ feature.title }
|
||||
description={ feature.description }
|
||||
actionProps={ {
|
||||
buttons: feature.buttons,
|
||||
featureStatus: feature.featureStatus,
|
||||
enabled: feature.enabled,
|
||||
notes: feature.notes,
|
||||
badge: {
|
||||
badge: feature.enabled
|
||||
? {
|
||||
text: __(
|
||||
'Active',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
type: TITLE_BADGE_POSITIVE,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
} }
|
||||
/>
|
||||
) ) }
|
||||
/>
|
||||
|
||||
<SettingsCard
|
||||
className="ppcp-r-tab-overview-help"
|
||||
title={ __( 'Help Center', 'woocommerce-paypal-payments' ) }
|
||||
description={ __(
|
||||
'Access detailed guides and responsive support to streamline setup and enhance your experience.',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
contentItems={ [
|
||||
<FeatureSettingsBlock
|
||||
key="documentation"
|
||||
title={ __(
|
||||
'Documentation',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
description={ __(
|
||||
'Find detailed guides and resources to help you set up, manage, and optimize your PayPal integration.',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
actionProps={ {
|
||||
buttons: [
|
||||
{
|
||||
type: 'tertiary',
|
||||
text: __(
|
||||
'View full documentation',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
url: '#',
|
||||
},
|
||||
],
|
||||
} }
|
||||
/>,
|
||||
<FeatureSettingsBlock
|
||||
key="support"
|
||||
title={ __( 'Support', 'woocommerce-paypal-payments' ) }
|
||||
description={ __(
|
||||
'Need help? Access troubleshooting tips or contact our support team for personalized assistance.',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
actionProps={ {
|
||||
buttons: [
|
||||
{
|
||||
type: 'tertiary',
|
||||
text: __(
|
||||
'View support options',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
url: '#',
|
||||
},
|
||||
],
|
||||
} }
|
||||
/>,
|
||||
] }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// TODO: This list should be refactored into a separate module, maybe utils/thingsToDoNext.js
|
||||
const todosDataDefault = [
|
||||
{
|
||||
value: 'paypal_later_messaging',
|
||||
|
@ -106,6 +219,7 @@ const todosDataDefault = [
|
|||
},
|
||||
];
|
||||
|
||||
// TODO: Hardcoding this list here is not the best idea. Can we move this to a REST API response?
|
||||
const featuresDefault = [
|
||||
{
|
||||
id: 'save_paypal_and_venmo',
|
||||
|
@ -117,6 +231,7 @@ const featuresDefault = [
|
|||
buttons: [
|
||||
{
|
||||
type: 'secondary',
|
||||
class: 'small-button',
|
||||
text: __( 'Configure', 'woocommerce-paypal-payments' ),
|
||||
url: '#',
|
||||
},
|
||||
|
@ -133,7 +248,6 @@ const featuresDefault = [
|
|||
'Advanced Credit and Debit Cards',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
featureStatus: true,
|
||||
description: __(
|
||||
'Process major credit and debit cards including Visa, Mastercard, American Express and Discover.',
|
||||
'woocommerce-paypal-payments'
|
||||
|
@ -141,6 +255,7 @@ const featuresDefault = [
|
|||
buttons: [
|
||||
{
|
||||
type: 'secondary',
|
||||
class: 'small-button',
|
||||
text: __( 'Configure', 'woocommerce-paypal-payments' ),
|
||||
url: '#',
|
||||
},
|
||||
|
@ -164,6 +279,7 @@ const featuresDefault = [
|
|||
buttons: [
|
||||
{
|
||||
type: 'secondary',
|
||||
class: 'small-button',
|
||||
text: __( 'Apply', 'woocommerce-paypal-payments' ),
|
||||
url: '#',
|
||||
},
|
||||
|
@ -181,10 +297,10 @@ const featuresDefault = [
|
|||
'Let customers pay using their Google Pay wallet.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
featureStatus: true,
|
||||
buttons: [
|
||||
{
|
||||
type: 'secondary',
|
||||
class: 'small-button',
|
||||
text: __( 'Configure', 'woocommerce-paypal-payments' ),
|
||||
url: '#',
|
||||
},
|
||||
|
@ -194,9 +310,6 @@ const featuresDefault = [
|
|||
url: '#',
|
||||
},
|
||||
],
|
||||
notes: [
|
||||
__( '¹PayPal Q2 Earnings-2021.', 'woocommerce-paypal-payments' ),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'apple_pay',
|
||||
|
@ -208,6 +321,7 @@ const featuresDefault = [
|
|||
buttons: [
|
||||
{
|
||||
type: 'secondary',
|
||||
class: 'small-button',
|
||||
text: __(
|
||||
'Domain registration',
|
||||
'woocommerce-paypal-payments'
|
||||
|
@ -231,6 +345,7 @@ const featuresDefault = [
|
|||
buttons: [
|
||||
{
|
||||
type: 'secondary',
|
||||
class: 'small-button',
|
||||
text: __( 'Configure', 'woocommerce-paypal-payments' ),
|
||||
url: '#',
|
||||
},
|
||||
|
@ -240,6 +355,9 @@ const featuresDefault = [
|
|||
url: '#',
|
||||
},
|
||||
],
|
||||
notes: [
|
||||
__( '¹PayPal Q2 Earnings-2021.', 'woocommerce-paypal-payments' ),
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -9,16 +9,10 @@ import {
|
|||
|
||||
const OrderIntent = ( { updateFormValue, settings } ) => {
|
||||
return (
|
||||
<SettingsBlock
|
||||
components={ [
|
||||
() => (
|
||||
<>
|
||||
<SettingsBlock>
|
||||
<Header>
|
||||
<Title>
|
||||
{ __(
|
||||
'Order Intent',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
{ __( 'Order Intent', 'woocommerce-paypal-payments' ) }
|
||||
</Title>
|
||||
<Description>
|
||||
{ __(
|
||||
|
@ -27,15 +21,9 @@ const OrderIntent = ( { updateFormValue, settings } ) => {
|
|||
) }
|
||||
</Description>
|
||||
</Header>
|
||||
</>
|
||||
),
|
||||
() => (
|
||||
<>
|
||||
|
||||
<ToggleSettingsBlock
|
||||
title={ __(
|
||||
'Authorize Only',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
title={ __( 'Authorize Only', 'woocommerce-paypal-payments' ) }
|
||||
actionProps={ {
|
||||
callback: updateFormValue,
|
||||
key: 'authorizeOnly',
|
||||
|
@ -54,10 +42,7 @@ const OrderIntent = ( { updateFormValue, settings } ) => {
|
|||
value: settings.captureVirtualOnlyOrders,
|
||||
} }
|
||||
/>
|
||||
</>
|
||||
),
|
||||
] }
|
||||
/>
|
||||
</SettingsBlock>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,19 +1,15 @@
|
|||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import {
|
||||
Header,
|
||||
SettingsBlock,
|
||||
ToggleSettingsBlock,
|
||||
Title,
|
||||
Description,
|
||||
} from '../../../../ReusableComponents/SettingsBlocks';
|
||||
import { Header } from '../../../../ReusableComponents/SettingsBlocks/SettingsBlockElements';
|
||||
|
||||
const SavePaymentMethods = ( { updateFormValue, settings } ) => {
|
||||
return (
|
||||
<SettingsBlock
|
||||
className="ppcp-r-settings-block--save-payment-methods"
|
||||
components={ [
|
||||
() => (
|
||||
<>
|
||||
<SettingsBlock className="ppcp-r-settings-block--save-payment-methods">
|
||||
<Header>
|
||||
<Title>
|
||||
{ __(
|
||||
|
@ -23,14 +19,12 @@ const SavePaymentMethods = ( { updateFormValue, settings } ) => {
|
|||
</Title>
|
||||
<Description>
|
||||
{ __(
|
||||
'Securely store customers’ payment methods for future payments and subscriptions, simplifying checkout and enabling recurring transactions.',
|
||||
"Securely store customers' payment methods for future payments and subscriptions, simplifying checkout and enabling recurring transactions.",
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
</Description>
|
||||
</Header>
|
||||
</>
|
||||
),
|
||||
() => (
|
||||
|
||||
<ToggleSettingsBlock
|
||||
title={ __(
|
||||
'Save PayPal and Venmo',
|
||||
|
@ -57,8 +51,7 @@ const SavePaymentMethods = ( { updateFormValue, settings } ) => {
|
|||
key: 'savePaypalAndVenmo',
|
||||
} }
|
||||
/>
|
||||
),
|
||||
() => (
|
||||
|
||||
<ToggleSettingsBlock
|
||||
title={ __(
|
||||
'Save Credit and Debit Cards',
|
||||
|
@ -74,9 +67,7 @@ const SavePaymentMethods = ( { updateFormValue, settings } ) => {
|
|||
value: settings.saveCreditCardAndDebitCard,
|
||||
} }
|
||||
/>
|
||||
),
|
||||
] }
|
||||
/>
|
||||
</SettingsBlock>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,174 +0,0 @@
|
|||
import { __ } from '@wordpress/i18n';
|
||||
import {
|
||||
Header,
|
||||
Title,
|
||||
Description,
|
||||
AccordionSettingsBlock,
|
||||
ToggleSettingsBlock,
|
||||
ButtonSettingsBlock,
|
||||
} from '../../../../ReusableComponents/SettingsBlocks';
|
||||
import SettingsBlock from '../../../../ReusableComponents/SettingsBlocks/SettingsBlock';
|
||||
|
||||
const Troubleshooting = ( { updateFormValue, settings } ) => {
|
||||
return (
|
||||
<AccordionSettingsBlock
|
||||
className="ppcp-r-settings-block--troubleshooting"
|
||||
title={ __( 'Troubleshooting', 'woocommerce-paypal-payments' ) }
|
||||
description={ __(
|
||||
'Access tools to help debug and resolve issues.',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
actionProps={ {
|
||||
callback: updateFormValue,
|
||||
key: 'payNowExperience',
|
||||
value: settings.payNowExperience,
|
||||
} }
|
||||
>
|
||||
<ToggleSettingsBlock
|
||||
title={ __( 'Logging', 'woocommerce-paypal-payments' ) }
|
||||
description={ __(
|
||||
'Log additional debugging information in the WooCommerce logs that can assist technical staff to determine issues.',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
actionProps={ {
|
||||
callback: updateFormValue,
|
||||
key: 'logging',
|
||||
value: settings.logging,
|
||||
} }
|
||||
/>
|
||||
<SettingsBlock
|
||||
components={ [
|
||||
() => (
|
||||
<>
|
||||
<Header>
|
||||
<Title>
|
||||
{ __(
|
||||
'Subscribed PayPal webhooks',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
</Title>
|
||||
<Description>
|
||||
{ __(
|
||||
'The following PayPal webhooks are subscribed. More information about the webhooks is available in the',
|
||||
'woocommerce-paypal-payments'
|
||||
) }{ ' ' }
|
||||
<a href="https://woocommerce.com/document/woocommerce-paypal-payments/#webhook-status">
|
||||
{ __(
|
||||
'Webhook Status documentation',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
</a>
|
||||
.
|
||||
</Description>
|
||||
</Header>
|
||||
<HooksTable data={ hooksExampleData() } />
|
||||
</>
|
||||
),
|
||||
] }
|
||||
/>
|
||||
|
||||
<ButtonSettingsBlock
|
||||
title={ __(
|
||||
'Resubscribe webhooks',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
description={ __(
|
||||
'Click to remove the current webhook subscription and subscribe again, for example, if the website domain or URL structure changed.',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
actionProps={ {
|
||||
buttonType: 'secondary',
|
||||
callback: () =>
|
||||
console.log(
|
||||
'Resubscribe webhooks',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
value: __(
|
||||
'Resubscribe webhooks',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
} }
|
||||
/>
|
||||
|
||||
<ButtonSettingsBlock
|
||||
title={ __(
|
||||
'Simulate webhooks',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
actionProps={ {
|
||||
buttonType: 'secondary',
|
||||
callback: () =>
|
||||
console.log(
|
||||
'Simulate webhooks',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
value: __(
|
||||
'Simulate webhooks',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
} }
|
||||
/>
|
||||
</AccordionSettingsBlock>
|
||||
);
|
||||
};
|
||||
|
||||
const hooksExampleData = () => {
|
||||
return {
|
||||
url: 'https://www.rt3.tech/wordpress/paypal-ux-testin/index.php?rest_route=/paypal/v1/incoming',
|
||||
hooks: [
|
||||
'billing plan pricing-change activated',
|
||||
'billing plan updated',
|
||||
'billing subscription cancelled',
|
||||
'catalog product updated',
|
||||
'checkout order approved',
|
||||
'checkout order completed',
|
||||
'checkout payment-approval reversed',
|
||||
'payment authorization voided',
|
||||
'payment capture completed',
|
||||
'payment capture denied',
|
||||
'payment capture pending',
|
||||
'payment capture refunded',
|
||||
'payment capture reversed',
|
||||
'payment order cancelled',
|
||||
'payment sale completed',
|
||||
'payment sale refunded',
|
||||
'vault payment-token created',
|
||||
'vault payment-token deleted',
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const HooksTable = ( { data } ) => {
|
||||
return (
|
||||
<table className="ppcp-r-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="ppcp-r-table__hooks-url">
|
||||
{ __( 'URL', 'woocommerce-paypal-payments' ) }
|
||||
</th>
|
||||
<th className="ppcp-r-table__hooks-events">
|
||||
{ __(
|
||||
'Tracked events',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="ppcp-r-table__hooks-url">{ data?.url }</td>
|
||||
<td className="ppcp-r-table__hooks-events">
|
||||
{ data.hooks.map( ( hook, index ) => (
|
||||
<span key={ hook }>
|
||||
{ hook }{ ' ' }
|
||||
{ index !== data.hooks.length - 1 && ',' }
|
||||
</span>
|
||||
) ) }
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
export default Troubleshooting;
|
|
@ -0,0 +1,37 @@
|
|||
import { __ } from '@wordpress/i18n';
|
||||
import { CommonHooks } from '../../../../../../data';
|
||||
|
||||
const HooksTableBlock = () => {
|
||||
const { webhooks } = CommonHooks.useWebhooks();
|
||||
|
||||
return (
|
||||
<table className="ppcp-r-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="ppcp-r-table__hooks-url">
|
||||
{ __( 'URL', 'woocommerce-paypal-payments' ) }
|
||||
</th>
|
||||
<th className="ppcp-r-table__hooks-events">
|
||||
{ __(
|
||||
'Tracked events',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="ppcp-r-table__hooks-url">
|
||||
{ webhooks?.url }
|
||||
</td>
|
||||
<td
|
||||
className="ppcp-r-table__hooks-events"
|
||||
dangerouslySetInnerHTML={ { __html: webhooks?.events } }
|
||||
></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
export default HooksTableBlock;
|
|
@ -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,72 @@
|
|||
import { __ } from '@wordpress/i18n';
|
||||
import {
|
||||
AccordionSettingsBlock,
|
||||
Description,
|
||||
Header,
|
||||
Title,
|
||||
ToggleSettingsBlock,
|
||||
} from '../../../../../ReusableComponents/SettingsBlocks';
|
||||
import SettingsBlock from '../../../../../ReusableComponents/SettingsBlocks/SettingsBlock';
|
||||
|
||||
import SimulationBlock from './SimulationBlock';
|
||||
import ResubscribeBlock from './ResubscribeBlock';
|
||||
import HooksTableBlock from './HooksTableBlock';
|
||||
|
||||
const Troubleshooting = ( { updateFormValue, settings } ) => {
|
||||
return (
|
||||
<AccordionSettingsBlock
|
||||
className="ppcp-r-settings-block--troubleshooting"
|
||||
title={ __( 'Troubleshooting', 'woocommerce-paypal-payments' ) }
|
||||
description={ __(
|
||||
'Access tools to help debug and resolve issues.',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
actionProps={ {
|
||||
callback: updateFormValue,
|
||||
key: 'payNowExperience',
|
||||
value: settings.payNowExperience,
|
||||
} }
|
||||
>
|
||||
<ToggleSettingsBlock
|
||||
title={ __( 'Logging', 'woocommerce-paypal-payments' ) }
|
||||
description={ __(
|
||||
'Log additional debugging information in the WooCommerce logs that can assist technical staff to determine issues.',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
actionProps={ {
|
||||
callback: updateFormValue,
|
||||
key: 'logging',
|
||||
value: settings.logging,
|
||||
} }
|
||||
/>
|
||||
<SettingsBlock>
|
||||
<Header>
|
||||
<Title>
|
||||
{ __(
|
||||
'Subscribed PayPal webhooks',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
</Title>
|
||||
<Description>
|
||||
{ __(
|
||||
'The following PayPal webhooks are subscribed. More information about the webhooks is available in the',
|
||||
'woocommerce-paypal-payments'
|
||||
) }{ ' ' }
|
||||
<a href="https://woocommerce.com/document/woocommerce-paypal-payments/#webhook-status">
|
||||
{ __(
|
||||
'Webhook Status documentation',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
</a>
|
||||
.
|
||||
</Description>
|
||||
</Header>
|
||||
<HooksTableBlock />
|
||||
<ResubscribeBlock />
|
||||
<SimulationBlock />
|
||||
</SettingsBlock>
|
||||
</AccordionSettingsBlock>
|
||||
);
|
||||
};
|
||||
|
||||
export default Troubleshooting;
|
|
@ -5,7 +5,7 @@ import {
|
|||
ContentWrapper,
|
||||
} from '../../../ReusableComponents/SettingsBlocks';
|
||||
import Sandbox from './Blocks/Sandbox';
|
||||
import Troubleshooting from './Blocks/Troubleshooting';
|
||||
import Troubleshooting from './Blocks/Troubleshooting/Troubleshooting';
|
||||
import PaypalSettings from './Blocks/PaypalSettings';
|
||||
import OtherSettings from './Blocks/OtherSettings';
|
||||
|
||||
|
|
|
@ -90,7 +90,7 @@ const TabStyling = () => {
|
|||
return (
|
||||
<div className="ppcp-r-styling">
|
||||
<div className="ppcp-r-styling__settings">
|
||||
<SectionIntro />
|
||||
<SectionIntro location={ location } />
|
||||
<SectionLocations
|
||||
locationOptions={ locationOptions }
|
||||
location={ location }
|
||||
|
@ -157,20 +157,17 @@ const TabStylingSection = ( props ) => {
|
|||
);
|
||||
};
|
||||
|
||||
const SectionIntro = () => {
|
||||
const buttonStyleDescription = sprintf(
|
||||
// translators: %s: Link to Classic checkout page
|
||||
__(
|
||||
'Customize the appearance of the PayPal smart buttons on the <a href="%s">[MISSING LINK]Classic Checkout page</a>. Checkout Buttons must be enabled to display the PayPal gateway on the Checkout page.'
|
||||
),
|
||||
'#'
|
||||
);
|
||||
const SectionIntro = ( { location } ) => {
|
||||
const { description, descriptionLink } =
|
||||
defaultLocationSettings[ location ];
|
||||
const buttonStyleDescription = sprintf( description, descriptionLink );
|
||||
|
||||
return (
|
||||
<TabStylingSection
|
||||
className="ppcp-r-styling__section--rc ppcp-r-styling__section--empty"
|
||||
title={ __( 'Button Styling', 'wooocommerce-paypal-payments' ) }
|
||||
description={ buttonStyleDescription }
|
||||
></TabStylingSection>
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -321,6 +318,7 @@ const SectionButtonPreview = ( { locationSettings } ) => {
|
|||
clientId: 'test',
|
||||
merchantId: 'QTQX5NP6N9WZU',
|
||||
components: 'buttons,googlepay',
|
||||
'disable-funding': 'card',
|
||||
'buyer-country': 'US',
|
||||
currency: 'USD',
|
||||
} }
|
||||
|
|
|
@ -1,13 +1,41 @@
|
|||
import { useEffect, useMemo } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { OnboardingHooks } from '../../data';
|
||||
import SpinnerOverlay from '../ReusableComponents/SpinnerOverlay';
|
||||
|
||||
import Onboarding from './Onboarding/Onboarding';
|
||||
import SettingsScreen from './SettingsScreen';
|
||||
|
||||
const Settings = () => {
|
||||
const onboardingProgress = OnboardingHooks.useSteps();
|
||||
|
||||
// Disable the "Changes you made might not be saved" browser warning.
|
||||
useEffect( () => {
|
||||
const suppressBeforeUnload = ( event ) => {
|
||||
event.stopImmediatePropagation();
|
||||
return undefined;
|
||||
};
|
||||
|
||||
window.addEventListener( 'beforeunload', suppressBeforeUnload );
|
||||
|
||||
return () => {
|
||||
window.removeEventListener( 'beforeunload', suppressBeforeUnload );
|
||||
};
|
||||
}, [] );
|
||||
|
||||
const wrapperClass = classNames( 'ppcp-r-app', {
|
||||
loading: ! onboardingProgress.isReady,
|
||||
} );
|
||||
|
||||
const Content = useMemo( () => {
|
||||
if ( ! onboardingProgress.isReady ) {
|
||||
// TODO: Use better loading state indicator.
|
||||
return <div>Loading...</div>;
|
||||
return (
|
||||
<SpinnerOverlay
|
||||
message={ __( 'Loading…', 'woocommerce-paypal-payments' ) }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if ( ! onboardingProgress.completed ) {
|
||||
|
@ -15,6 +43,9 @@ const Settings = () => {
|
|||
}
|
||||
|
||||
return <SettingsScreen />;
|
||||
}, [ onboardingProgress ] );
|
||||
|
||||
return <div className={ wrapperClass }>{ Content }</div>;
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
|
|
|
@ -10,10 +10,22 @@ export default {
|
|||
|
||||
// Persistent data.
|
||||
SET_PERSISTENT: 'COMMON:SET_PERSISTENT',
|
||||
RESET: 'COMMON:RESET',
|
||||
HYDRATE: 'COMMON:HYDRATE',
|
||||
|
||||
// Activity management (advanced solution that replaces the isBusy state).
|
||||
START_ACTIVITY: 'COMMON:START_ACTIVITY',
|
||||
STOP_ACTIVITY: 'COMMON:STOP_ACTIVITY',
|
||||
|
||||
// Controls - always start with "DO_".
|
||||
DO_PERSIST_DATA: 'COMMON:DO_PERSIST_DATA',
|
||||
DO_MANUAL_CONNECTION: 'COMMON:DO_MANUAL_CONNECTION',
|
||||
DO_SANDBOX_LOGIN: 'COMMON:DO_SANDBOX_LOGIN',
|
||||
DO_PRODUCTION_LOGIN: 'COMMON:DO_PRODUCTION_LOGIN',
|
||||
DO_REFRESH_MERCHANT: 'COMMON:DO_REFRESH_MERCHANT',
|
||||
DO_REFRESH_FEATURES: 'DO_REFRESH_FEATURES',
|
||||
DO_RESUBSCRIBE_WEBHOOKS: 'COMMON:DO_RESUBSCRIBE_WEBHOOKS',
|
||||
DO_START_WEBHOOK_SIMULATION: 'COMMON:DO_START_WEBHOOK_SIMULATION',
|
||||
DO_CHECK_WEBHOOK_SIMULATION_STATE:
|
||||
'COMMON:DO_CHECK_WEBHOOK_SIMULATION_STATE',
|
||||
};
|
||||
|
|
|
@ -18,6 +18,13 @@ import { STORE_NAME } from './constants';
|
|||
* @property {Object?} payload - Optional payload for the action.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Special. Resets all values in the onboarding store to initial defaults.
|
||||
*
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const reset = () => ( { type: ACTION_TYPES.RESET } );
|
||||
|
||||
/**
|
||||
* Persistent. Set the full onboarding details, usually during app initialization.
|
||||
*
|
||||
|
@ -52,14 +59,35 @@ export const setIsSaving = ( isSaving ) => ( {
|
|||
} );
|
||||
|
||||
/**
|
||||
* Transient. Changes the "manual connection is busy" flag.
|
||||
* Transient (Activity): Marks the start of an async activity
|
||||
* Think of it as "setIsBusy(true)"
|
||||
*
|
||||
* @param {boolean} isBusy
|
||||
* @param {string} id Internal ID/key of the action, used to stop it again.
|
||||
* @param {?string} description Optional, description for logging/debugging
|
||||
* @return {?Action} The action.
|
||||
*/
|
||||
export const startActivity = ( id, description = null ) => {
|
||||
if ( ! id || 'string' !== typeof id ) {
|
||||
console.warn( 'Activity ID must be a non-empty string' );
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
type: ACTION_TYPES.START_ACTIVITY,
|
||||
payload: { id, description },
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Transient (Activity): Marks the end of an async activity.
|
||||
* Think of it as "setIsBusy(false)"
|
||||
*
|
||||
* @param {string} id Internal ID/key of the action, used to stop it again.
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const setIsBusy = ( isBusy ) => ( {
|
||||
type: ACTION_TYPES.SET_TRANSIENT,
|
||||
payload: { isBusy },
|
||||
export const stopActivity = ( id ) => ( {
|
||||
type: ACTION_TYPES.STOP_ACTIVITY,
|
||||
payload: { id },
|
||||
} );
|
||||
|
||||
/**
|
||||
|
@ -118,17 +146,22 @@ export const persist = function* () {
|
|||
};
|
||||
|
||||
/**
|
||||
* Side effect. Initiates the sandbox login ISU.
|
||||
* Side effect. Fetches the ISU-login URL for a sandbox account.
|
||||
*
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const connectViaSandbox = function* () {
|
||||
yield setIsBusy( true );
|
||||
export const connectToSandbox = function* () {
|
||||
return yield { type: ACTION_TYPES.DO_SANDBOX_LOGIN };
|
||||
};
|
||||
|
||||
const result = yield { type: ACTION_TYPES.DO_SANDBOX_LOGIN };
|
||||
yield setIsBusy( false );
|
||||
|
||||
return result;
|
||||
/**
|
||||
* Side effect. Fetches the ISU-login URL for a production account.
|
||||
*
|
||||
* @param {string[]} products Which products/features to display in the ISU popup.
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const connectToProduction = function* ( products = [] ) {
|
||||
return yield { type: ACTION_TYPES.DO_PRODUCTION_LOGIN, products };
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -140,15 +173,89 @@ export const connectViaIdAndSecret = function* () {
|
|||
const { clientId, clientSecret, useSandbox } =
|
||||
yield select( STORE_NAME ).persistentData();
|
||||
|
||||
yield setIsBusy( true );
|
||||
|
||||
const result = yield {
|
||||
return yield {
|
||||
type: ACTION_TYPES.DO_MANUAL_CONNECTION,
|
||||
clientId,
|
||||
clientSecret,
|
||||
useSandbox,
|
||||
};
|
||||
yield setIsBusy( false );
|
||||
};
|
||||
|
||||
/**
|
||||
* Side effect. Clears and refreshes the merchant data via a REST request.
|
||||
*
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const refreshMerchantData = function* () {
|
||||
const result = yield { type: ACTION_TYPES.DO_REFRESH_MERCHANT };
|
||||
|
||||
if ( result.success && result.merchant ) {
|
||||
yield hydrate( result );
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Side effect.
|
||||
* Purges all feature status data via a REST request.
|
||||
* Refreshes the merchant data via a REST request.
|
||||
*
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const refreshFeatureStatuses = function* () {
|
||||
const result = yield { type: ACTION_TYPES.DO_REFRESH_FEATURES };
|
||||
|
||||
if ( result && result.success ) {
|
||||
// TODO: Review if we can get the updated feature details in the result.data instead of
|
||||
// doing a second refreshMerchantData() request.
|
||||
yield refreshMerchantData();
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Persistent. Changes the "webhooks" value.
|
||||
*
|
||||
* @param {string} webhooks
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const setWebhooks = ( webhooks ) => ( {
|
||||
type: ACTION_TYPES.SET_PERSISTENT,
|
||||
payload: { webhooks },
|
||||
} );
|
||||
|
||||
/**
|
||||
* Side effect
|
||||
* Refreshes subscribed webhooks via a REST request
|
||||
*
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const resubscribeWebhooks = function* () {
|
||||
const result = yield { type: ACTION_TYPES.DO_RESUBSCRIBE_WEBHOOKS };
|
||||
|
||||
if ( result && result.success ) {
|
||||
yield hydrate( result );
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Side effect. Starts webhook simulation.
|
||||
*
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const startWebhookSimulation = function* () {
|
||||
return yield { type: ACTION_TYPES.DO_START_WEBHOOK_SIMULATION };
|
||||
};
|
||||
|
||||
/**
|
||||
* Side effect. Checks webhook simulation.
|
||||
*
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const checkWebhookSimulationState = function* () {
|
||||
return yield { type: ACTION_TYPES.DO_CHECK_WEBHOOK_SIMULATION_STATE };
|
||||
};
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
export const STORE_NAME = 'wc/paypal/common';
|
||||
|
||||
/**
|
||||
* REST path to hydrate data of this module by loading data from the WP DB..
|
||||
* REST path to hydrate data of this module by loading data from the WP DB.
|
||||
*
|
||||
* Used by resolvers.
|
||||
*
|
||||
|
@ -16,6 +16,15 @@ export const STORE_NAME = 'wc/paypal/common';
|
|||
*/
|
||||
export const REST_HYDRATE_PATH = '/wc/v3/wc_paypal/common';
|
||||
|
||||
/**
|
||||
* REST path to fetch merchant details from the WordPress DB.
|
||||
*
|
||||
* Used by controls.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const REST_HYDRATE_MERCHANT_PATH = '/wc/v3/wc_paypal/common/merchant';
|
||||
|
||||
/**
|
||||
* REST path to persist data of this module to the WP DB.
|
||||
*
|
||||
|
@ -36,11 +45,42 @@ export const REST_PERSIST_PATH = '/wc/v3/wc_paypal/common';
|
|||
export const REST_MANUAL_CONNECTION_PATH = '/wc/v3/wc_paypal/connect_manual';
|
||||
|
||||
/**
|
||||
* REST path to generate an ISU URL for the sandbox-login.
|
||||
* REST path to generate an ISU URL for the PayPal-login.
|
||||
*
|
||||
* Used by: Controls
|
||||
* See: LoginLinkRestEndpoint.php
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const REST_SANDBOX_CONNECTION_PATH = '/wc/v3/wc_paypal/login_link';
|
||||
export const REST_CONNECTION_URL_PATH = '/wc/v3/wc_paypal/login_link';
|
||||
|
||||
/**
|
||||
* REST path to fetch webhooks data or resubscribe webhooks,
|
||||
*
|
||||
* Used by: Controls
|
||||
* See: WebhookSettingsEndpoint.php
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const REST_WEBHOOKS = '/wc/v3/wc_paypal/webhook_settings';
|
||||
|
||||
/**
|
||||
* REST path to start webhook simulation and observe the state,
|
||||
*
|
||||
* Used by: Controls
|
||||
* See: WebhookSettingsEndpoint.php
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const REST_WEBHOOKS_SIMULATE = '/wc/v3/wc_paypal/webhook_simulate';
|
||||
|
||||
/**
|
||||
* REST path to refresh the feature status.
|
||||
*
|
||||
* Used by: Controls
|
||||
* See: RefreshFeatureStatusEndpoint.php
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const REST_REFRESH_FEATURES_PATH =
|
||||
'/wc/v3/wc_paypal/refresh-feature-status';
|
||||
|
|
|
@ -10,16 +10,20 @@
|
|||
import apiFetch from '@wordpress/api-fetch';
|
||||
|
||||
import {
|
||||
REST_PERSIST_PATH,
|
||||
REST_CONNECTION_URL_PATH,
|
||||
REST_HYDRATE_MERCHANT_PATH,
|
||||
REST_MANUAL_CONNECTION_PATH,
|
||||
REST_SANDBOX_CONNECTION_PATH,
|
||||
REST_PERSIST_PATH,
|
||||
REST_REFRESH_FEATURES_PATH,
|
||||
REST_WEBHOOKS,
|
||||
REST_WEBHOOKS_SIMULATE,
|
||||
} from './constants';
|
||||
import ACTION_TYPES from './action-types';
|
||||
|
||||
export const controls = {
|
||||
async [ ACTION_TYPES.DO_PERSIST_DATA ]( { data } ) {
|
||||
try {
|
||||
return await apiFetch( {
|
||||
await apiFetch( {
|
||||
path: REST_PERSIST_PATH,
|
||||
method: 'POST',
|
||||
data,
|
||||
|
@ -30,25 +34,39 @@ export const controls = {
|
|||
},
|
||||
|
||||
async [ ACTION_TYPES.DO_SANDBOX_LOGIN ]() {
|
||||
let result = null;
|
||||
|
||||
try {
|
||||
result = await apiFetch( {
|
||||
path: REST_SANDBOX_CONNECTION_PATH,
|
||||
return apiFetch( {
|
||||
path: REST_CONNECTION_URL_PATH,
|
||||
method: 'POST',
|
||||
data: {
|
||||
environment: 'sandbox',
|
||||
products: [ 'EXPRESS_CHECKOUT' ],
|
||||
products: [ 'EXPRESS_CHECKOUT' ], // Sandbox always uses EXPRESS_CHECKOUT.
|
||||
},
|
||||
} );
|
||||
} catch ( e ) {
|
||||
result = {
|
||||
return {
|
||||
success: false,
|
||||
error: e,
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
return result;
|
||||
async [ ACTION_TYPES.DO_PRODUCTION_LOGIN ]( { products } ) {
|
||||
try {
|
||||
return apiFetch( {
|
||||
path: REST_CONNECTION_URL_PATH,
|
||||
method: 'POST',
|
||||
data: {
|
||||
environment: 'production',
|
||||
products,
|
||||
},
|
||||
} );
|
||||
} catch ( e ) {
|
||||
return {
|
||||
success: false,
|
||||
error: e,
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
async [ ACTION_TYPES.DO_MANUAL_CONNECTION ]( {
|
||||
|
@ -56,10 +74,8 @@ export const controls = {
|
|||
clientSecret,
|
||||
useSandbox,
|
||||
} ) {
|
||||
let result = null;
|
||||
|
||||
try {
|
||||
result = await apiFetch( {
|
||||
return await apiFetch( {
|
||||
path: REST_MANUAL_CONNECTION_PATH,
|
||||
method: 'POST',
|
||||
data: {
|
||||
|
@ -69,12 +85,56 @@ export const controls = {
|
|||
},
|
||||
} );
|
||||
} catch ( e ) {
|
||||
result = {
|
||||
return {
|
||||
success: false,
|
||||
error: e,
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
return result;
|
||||
async [ ACTION_TYPES.DO_REFRESH_MERCHANT ]() {
|
||||
try {
|
||||
return await apiFetch( { path: REST_HYDRATE_MERCHANT_PATH } );
|
||||
} catch ( e ) {
|
||||
return {
|
||||
success: false,
|
||||
error: e,
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
async [ ACTION_TYPES.DO_REFRESH_FEATURES ]() {
|
||||
try {
|
||||
return await apiFetch( {
|
||||
path: REST_REFRESH_FEATURES_PATH,
|
||||
method: 'POST',
|
||||
} );
|
||||
} catch ( e ) {
|
||||
return {
|
||||
success: false,
|
||||
error: e,
|
||||
message: e.message,
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
async [ ACTION_TYPES.DO_RESUBSCRIBE_WEBHOOKS ]() {
|
||||
return await apiFetch( {
|
||||
method: 'POST',
|
||||
path: REST_WEBHOOKS,
|
||||
} );
|
||||
},
|
||||
|
||||
async [ ACTION_TYPES.DO_START_WEBHOOK_SIMULATION ]() {
|
||||
return await apiFetch( {
|
||||
method: 'POST',
|
||||
path: REST_WEBHOOKS_SIMULATE,
|
||||
} );
|
||||
},
|
||||
|
||||
async [ ACTION_TYPES.DO_CHECK_WEBHOOK_SIMULATION_STATE ]() {
|
||||
return await apiFetch( {
|
||||
path: REST_WEBHOOKS_SIMULATE,
|
||||
} );
|
||||
},
|
||||
};
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { useCallback } from '@wordpress/element';
|
||||
|
||||
import { STORE_NAME } from './constants';
|
||||
|
||||
const useTransient = ( key ) =>
|
||||
|
@ -31,8 +30,11 @@ const useHooks = () => {
|
|||
setManualConnectionMode,
|
||||
setClientId,
|
||||
setClientSecret,
|
||||
connectViaSandbox,
|
||||
connectToSandbox,
|
||||
connectToProduction,
|
||||
connectViaIdAndSecret,
|
||||
startWebhookSimulation,
|
||||
checkWebhookSimulationState,
|
||||
} = useDispatch( STORE_NAME );
|
||||
|
||||
// Transient accessors.
|
||||
|
@ -43,7 +45,11 @@ const useHooks = () => {
|
|||
const clientSecret = usePersistent( 'clientSecret' );
|
||||
const isSandboxMode = usePersistent( 'useSandbox' );
|
||||
const isManualConnectionMode = usePersistent( 'useManualConnection' );
|
||||
|
||||
const webhooks = usePersistent( 'webhooks' );
|
||||
const merchant = useSelect(
|
||||
( select ) => select( STORE_NAME ).merchant(),
|
||||
[]
|
||||
);
|
||||
const wooSettings = useSelect(
|
||||
( select ) => select( STORE_NAME ).wooSettings(),
|
||||
[]
|
||||
|
@ -72,26 +78,27 @@ const useHooks = () => {
|
|||
setClientSecret: ( value ) => {
|
||||
return savePersistent( setClientSecret, value );
|
||||
},
|
||||
connectViaSandbox,
|
||||
connectToSandbox,
|
||||
connectToProduction,
|
||||
connectViaIdAndSecret,
|
||||
merchant,
|
||||
wooSettings,
|
||||
};
|
||||
};
|
||||
|
||||
export const useBusyState = () => {
|
||||
const { setIsBusy } = useDispatch( STORE_NAME );
|
||||
const isBusy = useTransient( 'isBusy' );
|
||||
|
||||
return {
|
||||
isBusy,
|
||||
setIsBusy: useCallback( ( busy ) => setIsBusy( busy ), [ setIsBusy ] ),
|
||||
webhooks,
|
||||
startWebhookSimulation,
|
||||
checkWebhookSimulationState,
|
||||
};
|
||||
};
|
||||
|
||||
export const useSandbox = () => {
|
||||
const { isSandboxMode, setSandboxMode, connectViaSandbox } = useHooks();
|
||||
const { isSandboxMode, setSandboxMode, connectToSandbox } = useHooks();
|
||||
|
||||
return { isSandboxMode, setSandboxMode, connectViaSandbox };
|
||||
return { isSandboxMode, setSandboxMode, connectToSandbox };
|
||||
};
|
||||
|
||||
export const useProduction = () => {
|
||||
const { connectToProduction } = useHooks();
|
||||
|
||||
return { connectToProduction };
|
||||
};
|
||||
|
||||
export const useManualConnection = () => {
|
||||
|
@ -118,5 +125,77 @@ export const useManualConnection = () => {
|
|||
|
||||
export const useWooSettings = () => {
|
||||
const { wooSettings } = useHooks();
|
||||
|
||||
return wooSettings;
|
||||
};
|
||||
|
||||
export const useWebhooks = () => {
|
||||
const {
|
||||
webhooks,
|
||||
setWebhooks,
|
||||
registerWebhooks,
|
||||
startWebhookSimulation,
|
||||
checkWebhookSimulationState,
|
||||
} = useHooks();
|
||||
return {
|
||||
webhooks,
|
||||
setWebhooks,
|
||||
registerWebhooks,
|
||||
startWebhookSimulation,
|
||||
checkWebhookSimulationState,
|
||||
};
|
||||
};
|
||||
export const useMerchantInfo = () => {
|
||||
const { merchant } = useHooks();
|
||||
const { refreshMerchantData } = useDispatch( STORE_NAME );
|
||||
|
||||
const verifyLoginStatus = useCallback( async () => {
|
||||
const result = await refreshMerchantData();
|
||||
|
||||
if ( ! result.success ) {
|
||||
throw new Error( result?.message || result?.error?.message );
|
||||
}
|
||||
|
||||
// Verify if the server state is "connected" and we have a merchant ID.
|
||||
return merchant?.isConnected && merchant?.id;
|
||||
}, [ refreshMerchantData, merchant ] );
|
||||
|
||||
return {
|
||||
merchant, // Merchant details
|
||||
verifyLoginStatus, // Callback
|
||||
};
|
||||
};
|
||||
|
||||
// -- Not using the `useHooks()` data provider --
|
||||
|
||||
export const useBusyState = () => {
|
||||
const { startActivity, stopActivity } = useDispatch( STORE_NAME );
|
||||
|
||||
// Resolved value (object), contains a list of all running actions.
|
||||
const activities = useSelect(
|
||||
( select ) => select( STORE_NAME ).getActivityList(),
|
||||
[]
|
||||
);
|
||||
|
||||
// Derive isBusy state from activities
|
||||
const isBusy = Object.keys( activities ).length > 0;
|
||||
|
||||
// HOC that starts and stops an activity while the callback is executed.
|
||||
const withActivity = useCallback(
|
||||
async ( id, description, asyncFn ) => {
|
||||
startActivity( id, description );
|
||||
try {
|
||||
return await asyncFn();
|
||||
} finally {
|
||||
stopActivity( id );
|
||||
}
|
||||
},
|
||||
[ startActivity, stopActivity ]
|
||||
);
|
||||
|
||||
return {
|
||||
withActivity, // HOC
|
||||
isBusy, // Boolean.
|
||||
activities, // Object.
|
||||
};
|
||||
};
|
||||
|
|
|
@ -12,23 +12,31 @@ import ACTION_TYPES from './action-types';
|
|||
|
||||
// Store structure.
|
||||
|
||||
const defaultTransient = {
|
||||
const defaultTransient = Object.freeze( {
|
||||
isReady: false,
|
||||
isBusy: false,
|
||||
activities: new Map(),
|
||||
|
||||
// Read only values, provided by the server via hydrate.
|
||||
wooSettings: {
|
||||
merchant: Object.freeze( {
|
||||
isConnected: false,
|
||||
isSandbox: false,
|
||||
id: '',
|
||||
email: '',
|
||||
} ),
|
||||
|
||||
wooSettings: Object.freeze( {
|
||||
storeCountry: '',
|
||||
storeCurrency: '',
|
||||
},
|
||||
};
|
||||
} ),
|
||||
} );
|
||||
|
||||
const defaultPersistent = {
|
||||
const defaultPersistent = Object.freeze( {
|
||||
useSandbox: false,
|
||||
useManualConnection: false,
|
||||
clientId: '',
|
||||
clientSecret: '',
|
||||
};
|
||||
webhooks: [],
|
||||
} );
|
||||
|
||||
// Reducer logic.
|
||||
|
||||
|
@ -44,16 +52,54 @@ const commonReducer = createReducer( defaultTransient, defaultPersistent, {
|
|||
[ ACTION_TYPES.SET_PERSISTENT ]: ( state, action ) =>
|
||||
setPersistent( state, action ),
|
||||
|
||||
[ ACTION_TYPES.RESET ]: ( state ) => {
|
||||
const cleanState = setTransient(
|
||||
setPersistent( state, defaultPersistent ),
|
||||
defaultTransient
|
||||
);
|
||||
|
||||
// Keep "read-only" details and initialization flags.
|
||||
cleanState.wooSettings = { ...state.wooSettings };
|
||||
cleanState.isReady = true;
|
||||
|
||||
return cleanState;
|
||||
},
|
||||
|
||||
[ ACTION_TYPES.START_ACTIVITY ]: ( state, payload ) => {
|
||||
return setTransient( state, {
|
||||
activities: new Map( state.activities ).set(
|
||||
payload.id,
|
||||
payload.description
|
||||
),
|
||||
} );
|
||||
},
|
||||
|
||||
[ ACTION_TYPES.STOP_ACTIVITY ]: ( state, payload ) => {
|
||||
const newActivities = new Map( state.activities );
|
||||
newActivities.delete( payload.id );
|
||||
return setTransient( state, { activities: newActivities } );
|
||||
},
|
||||
|
||||
[ ACTION_TYPES.DO_REFRESH_MERCHANT ]: ( state ) => ( {
|
||||
...state,
|
||||
merchant: Object.freeze( { ...defaultTransient.merchant } ),
|
||||
} ),
|
||||
|
||||
[ ACTION_TYPES.HYDRATE ]: ( state, payload ) => {
|
||||
const newState = setPersistent( state, payload.data );
|
||||
|
||||
if ( payload.wooSettings ) {
|
||||
newState.wooSettings = {
|
||||
...newState.wooSettings,
|
||||
...payload.wooSettings,
|
||||
};
|
||||
// Populate read-only properties.
|
||||
[ 'wooSettings', 'merchant' ].forEach( ( key ) => {
|
||||
if ( ! payload[ key ] ) {
|
||||
return;
|
||||
}
|
||||
|
||||
newState[ key ] = Object.freeze( {
|
||||
...newState[ key ],
|
||||
...payload[ key ],
|
||||
} );
|
||||
} );
|
||||
|
||||
return newState;
|
||||
},
|
||||
} );
|
||||
|
|
|
@ -12,7 +12,7 @@ import { dispatch } from '@wordpress/data';
|
|||
import { __ } from '@wordpress/i18n';
|
||||
import { apiFetch } from '@wordpress/data-controls';
|
||||
|
||||
import { STORE_NAME, REST_HYDRATE_PATH } from './constants';
|
||||
import { STORE_NAME, REST_HYDRATE_PATH, REST_WEBHOOKS } from './constants';
|
||||
|
||||
export const resolvers = {
|
||||
/**
|
||||
|
@ -21,6 +21,9 @@ export const resolvers = {
|
|||
*persistentData() {
|
||||
try {
|
||||
const result = yield apiFetch( { path: REST_HYDRATE_PATH } );
|
||||
const webhooks = yield apiFetch( { path: REST_WEBHOOKS } );
|
||||
|
||||
result.data = { ...result.data, ...webhooks.data };
|
||||
|
||||
yield dispatch( STORE_NAME ).hydrate( result );
|
||||
yield dispatch( STORE_NAME ).setIsReady( true );
|
||||
|
|
|
@ -16,10 +16,24 @@ export const persistentData = ( state ) => {
|
|||
};
|
||||
|
||||
export const transientData = ( state ) => {
|
||||
const { data, wooSettings, ...transientState } = getState( state );
|
||||
const { data, merchant, wooSettings, ...transientState } =
|
||||
getState( state );
|
||||
return transientState || EMPTY_OBJ;
|
||||
};
|
||||
|
||||
export const getActivityList = ( state ) => {
|
||||
const { activities = new Map() } = state;
|
||||
return Object.fromEntries( activities );
|
||||
};
|
||||
|
||||
export const merchant = ( state ) => {
|
||||
return getState( state ).merchant || EMPTY_OBJ;
|
||||
};
|
||||
|
||||
export const wooSettings = ( state ) => {
|
||||
return getState( state ).wooSettings || EMPTY_OBJ;
|
||||
};
|
||||
|
||||
export const webhooks = ( state ) => {
|
||||
return getState( state ).webhooks || EMPTY_OBJ;
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { OnboardingStoreName } from './index';
|
||||
import { OnboardingStoreName, CommonStoreName } from './index';
|
||||
|
||||
export const addDebugTools = ( context, modules ) => {
|
||||
if ( ! context || ! context?.debug ) {
|
||||
|
@ -33,9 +33,14 @@ export const addDebugTools = ( context, modules ) => {
|
|||
};
|
||||
|
||||
context.resetStore = () => {
|
||||
const onboarding = wp.data.dispatch( OnboardingStoreName );
|
||||
onboarding.reset();
|
||||
onboarding.persist();
|
||||
const stores = [ OnboardingStoreName, CommonStoreName ];
|
||||
|
||||
stores.forEach( ( storeName ) => {
|
||||
const store = wp.data.dispatch( storeName );
|
||||
|
||||
store.reset();
|
||||
store.persist();
|
||||
} );
|
||||
};
|
||||
|
||||
context.startOnboarding = () => {
|
||||
|
|
|
@ -34,8 +34,12 @@ const useHooks = () => {
|
|||
setProducts,
|
||||
} = useDispatch( STORE_NAME );
|
||||
|
||||
// Read-only flags.
|
||||
// Read-only flags and derived state.
|
||||
const flags = useSelect( ( select ) => select( STORE_NAME ).flags(), [] );
|
||||
const determineProducts = useSelect(
|
||||
( select ) => select( STORE_NAME ).determineProducts(),
|
||||
[]
|
||||
);
|
||||
|
||||
// Transient accessors.
|
||||
const isReady = useTransient( 'isReady' );
|
||||
|
@ -80,6 +84,7 @@ const useHooks = () => {
|
|||
);
|
||||
return savePersistent( setProducts, validProducts );
|
||||
},
|
||||
determineProducts,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -124,6 +129,12 @@ export const useNavigationState = () => {
|
|||
};
|
||||
};
|
||||
|
||||
export const useDetermineProducts = () => {
|
||||
const { determineProducts } = useHooks();
|
||||
|
||||
return determineProducts;
|
||||
};
|
||||
|
||||
export const useFlags = () => {
|
||||
const { flags } = useHooks();
|
||||
return flags;
|
||||
|
|
|
@ -12,25 +12,25 @@ import ACTION_TYPES from './action-types';
|
|||
|
||||
// Store structure.
|
||||
|
||||
const defaultTransient = {
|
||||
const defaultTransient = Object.freeze( {
|
||||
isReady: false,
|
||||
|
||||
// Read only values, provided by the server.
|
||||
flags: {
|
||||
flags: Object.freeze( {
|
||||
canUseCasualSelling: false,
|
||||
canUseVaulting: false,
|
||||
canUseCardPayments: false,
|
||||
canUseSubscriptions: false,
|
||||
},
|
||||
};
|
||||
} ),
|
||||
} );
|
||||
|
||||
const defaultPersistent = {
|
||||
const defaultPersistent = Object.freeze( {
|
||||
completed: false,
|
||||
step: 0,
|
||||
isCasualSeller: null, // null value will uncheck both options in the UI.
|
||||
areOptionalPaymentMethodsEnabled: null,
|
||||
products: [],
|
||||
};
|
||||
} );
|
||||
|
||||
// Reducer logic.
|
||||
|
||||
|
@ -46,15 +46,28 @@ const onboardingReducer = createReducer( defaultTransient, defaultPersistent, {
|
|||
[ ACTION_TYPES.SET_PERSISTENT ]: ( state, payload ) =>
|
||||
setPersistent( state, payload ),
|
||||
|
||||
[ ACTION_TYPES.RESET ]: ( state ) =>
|
||||
[ ACTION_TYPES.RESET ]: ( state ) => {
|
||||
const cleanState = setTransient(
|
||||
setPersistent( state, defaultPersistent ),
|
||||
defaultTransient
|
||||
);
|
||||
|
||||
// Keep "read-only" details and initialization flags.
|
||||
cleanState.flags = { ...state.flags };
|
||||
cleanState.isReady = true;
|
||||
|
||||
return cleanState;
|
||||
},
|
||||
|
||||
[ ACTION_TYPES.HYDRATE ]: ( state, payload ) => {
|
||||
const newState = setPersistent( state, payload.data );
|
||||
|
||||
// Flags are not updated by `setPersistent()`.
|
||||
if ( payload.flags ) {
|
||||
newState.flags = { ...newState.flags, ...payload.flags };
|
||||
newState.flags = Object.freeze( {
|
||||
...newState.flags,
|
||||
...payload.flags,
|
||||
} );
|
||||
}
|
||||
|
||||
return newState;
|
||||
|
|
|
@ -23,3 +23,50 @@ export const transientData = ( state ) => {
|
|||
export const flags = ( state ) => {
|
||||
return getState( state ).flags || EMPTY_OBJ;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the products that we use for the production login link in the last onboarding step.
|
||||
*
|
||||
* This selector does not return state-values, but uses the state to derive the products-array
|
||||
* that should be returned.
|
||||
*
|
||||
* @param {{}} state
|
||||
* @return {string[]} The ISU products, based on choices made in the onboarding wizard.
|
||||
*/
|
||||
export const determineProducts = ( state ) => {
|
||||
const derivedProducts = [];
|
||||
|
||||
const { isCasualSeller, areOptionalPaymentMethodsEnabled } =
|
||||
persistentData( state );
|
||||
const { canUseVaulting, canUseCardPayments } = flags( state );
|
||||
|
||||
if ( ! canUseCardPayments || ! areOptionalPaymentMethodsEnabled ) {
|
||||
/**
|
||||
* Branch 1: Credit Card Payments not available.
|
||||
* The store uses the Express-checkout product.
|
||||
*/
|
||||
derivedProducts.push( 'EXPRESS_CHECKOUT' );
|
||||
} else if ( isCasualSeller ) {
|
||||
/**
|
||||
* Branch 2: Merchant has no business.
|
||||
* The store uses the Express-checkout product.
|
||||
*/
|
||||
derivedProducts.push( 'EXPRESS_CHECKOUT' );
|
||||
|
||||
// TODO: Add the "BCDC" product/feature
|
||||
// Requirement: "EXPRESS_CHECKOUT with BCDC"
|
||||
} else {
|
||||
/**
|
||||
* Branch 3: Merchant is business, and can use CC payments.
|
||||
* The store uses the advanced PPCP product.
|
||||
*/
|
||||
derivedProducts.push( 'PPCP' );
|
||||
}
|
||||
|
||||
if ( canUseVaulting ) {
|
||||
// TODO: Add the "Vaulting" product/feature
|
||||
// Requirement: "... with Vault"
|
||||
}
|
||||
|
||||
return derivedProducts;
|
||||
};
|
||||
|
|
|
@ -25,26 +25,56 @@ export const defaultLocationSettings = {
|
|||
value: 'cart',
|
||||
label: __( 'Cart', 'woocommerce-paypal-payments' ),
|
||||
settings: { ...cartAndExpressCheckoutSettings },
|
||||
// translators: %s: Link to Cart page
|
||||
description: __(
|
||||
'Customize the appearance of the PayPal smart buttons on the <a href="%s">[MISSING LINK]Cart page</a> and select which additional payment buttons to display in this location.',
|
||||
'wooocommerce-paypal-payments'
|
||||
),
|
||||
descriptionLink: '#',
|
||||
},
|
||||
'classic-checkout': {
|
||||
value: 'classic-checkout',
|
||||
label: __( 'Classic Checkout', 'woocommerce-paypal-payments' ),
|
||||
settings: { ...settings },
|
||||
// translators: %s: Link to Classic Checkout page
|
||||
description: __(
|
||||
'Customize the appearance of the PayPal smart buttons on the <a href="%s">[MISSING LINK]Classic Checkout page</a> and choose which additional payment buttons to display in this location.',
|
||||
'wooocommerce-paypal-payments'
|
||||
),
|
||||
descriptionLink: '#',
|
||||
},
|
||||
'express-checkout': {
|
||||
value: 'express-checkout',
|
||||
label: __( 'Express Checkout', 'woocommerce-paypal-payments' ),
|
||||
settings: { ...cartAndExpressCheckoutSettings },
|
||||
// translators: %s: Link to Express Checkout location
|
||||
description: __(
|
||||
'Customize the appearance of the PayPal smart buttons on the <a href="%s">[MISSING LINK]Express Checkout location</a> and choose which additional payment buttons to display in this location.',
|
||||
'wooocommerce-paypal-payments'
|
||||
),
|
||||
descriptionLink: '#',
|
||||
},
|
||||
'mini-cart': {
|
||||
value: 'mini-cart',
|
||||
label: __( 'Mini Cart', 'woocommerce-paypel-payements' ),
|
||||
settings: { ...settings },
|
||||
// translators: %s: Link to Mini Cart
|
||||
description: __(
|
||||
'Customize the appearance of the PayPal smart buttons on the <a href="%s">[MISSING LINK]Mini Cart</a> and choose which additional payment buttons to display in this location.',
|
||||
'wooocommerce-paypal-payments'
|
||||
),
|
||||
descriptionLink: '#',
|
||||
},
|
||||
'product-page': {
|
||||
value: 'product-page',
|
||||
label: __( 'Product Page', 'woocommerce-paypal-payments' ),
|
||||
settings: { ...settings },
|
||||
// translators: %s: Link to Product Page
|
||||
description: __(
|
||||
'Customize the appearance of the PayPal smart buttons on the <a href="%s">[MISSING LINK]Product Page</a> and choose which additional payment buttons to display in this location.',
|
||||
'wooocommerce-paypal-payments'
|
||||
),
|
||||
descriptionLink: '#',
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -57,10 +87,6 @@ export const paymentMethodOptions = [
|
|||
value: 'paylater',
|
||||
label: __( 'Pay Later', 'woocommerce-paypal-payments' ),
|
||||
},
|
||||
{
|
||||
value: 'card',
|
||||
label: __( 'Debit or Credit Card', 'woocommerce-paypal-payments' ),
|
||||
},
|
||||
{
|
||||
value: 'googlepay',
|
||||
label: __( 'Google Pay', 'woocommerce-paypal-payments' ),
|
||||
|
|
214
modules/ppcp-settings/resources/js/hooks/useHandleConnections.js
Normal file
214
modules/ppcp-settings/resources/js/hooks/useHandleConnections.js
Normal file
|
@ -0,0 +1,214 @@
|
|||
import { __ } from '@wordpress/i18n';
|
||||
import { useDispatch } from '@wordpress/data';
|
||||
import { store as noticesStore } from '@wordpress/notices';
|
||||
|
||||
import { CommonHooks, OnboardingHooks } from '../data';
|
||||
import { openPopup } from '../utils/window';
|
||||
|
||||
const MESSAGES = {
|
||||
CONNECTED: __( 'Connected to PayPal', 'woocommerce-paypal-payments' ),
|
||||
POPUP_BLOCKED: __(
|
||||
'Popup blocked. Please allow popups for this site to connect to PayPal.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
SANDBOX_ERROR: __(
|
||||
'Could not generate a Sandbox login link.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
PRODUCTION_ERROR: __(
|
||||
'Could not generate a login link.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
MANUAL_ERROR: __(
|
||||
'Could not connect to PayPal. Please make sure your Client ID and Secret Key are correct.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
LOGIN_FAILED: __(
|
||||
'Login was not successful. Please try again.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
};
|
||||
|
||||
const ACTIVITIES = {
|
||||
CONNECT_SANDBOX: 'ISU_LOGIN_SANDBOX',
|
||||
CONNECT_PRODUCTION: 'ISU_LOGIN_PRODUCTION',
|
||||
CONNECT_MANUAL: 'MANUAL_LOGIN',
|
||||
};
|
||||
|
||||
const handlePopupWithCompletion = ( url, onError ) => {
|
||||
return new Promise( ( resolve ) => {
|
||||
const popup = openPopup( url );
|
||||
|
||||
if ( ! popup ) {
|
||||
onError( MESSAGES.POPUP_BLOCKED );
|
||||
resolve( false );
|
||||
return;
|
||||
}
|
||||
|
||||
// Check popup state every 500ms
|
||||
const checkPopup = setInterval( () => {
|
||||
if ( popup.closed ) {
|
||||
clearInterval( checkPopup );
|
||||
resolve( true );
|
||||
}
|
||||
}, 500 );
|
||||
|
||||
return () => {
|
||||
clearInterval( checkPopup );
|
||||
|
||||
if ( popup && ! popup.closed ) {
|
||||
popup.close();
|
||||
}
|
||||
};
|
||||
} );
|
||||
};
|
||||
|
||||
const useConnectionBase = () => {
|
||||
const { setCompleted } = OnboardingHooks.useSteps();
|
||||
const { createSuccessNotice, createErrorNotice } =
|
||||
useDispatch( noticesStore );
|
||||
const { verifyLoginStatus } = CommonHooks.useMerchantInfo();
|
||||
|
||||
return {
|
||||
handleFailed: ( res, genericMessage ) => {
|
||||
console.error( 'Connection error', res );
|
||||
createErrorNotice( res?.message ?? genericMessage );
|
||||
},
|
||||
handleCompleted: async () => {
|
||||
try {
|
||||
const loginSuccessful = await verifyLoginStatus();
|
||||
|
||||
if ( loginSuccessful ) {
|
||||
createSuccessNotice( MESSAGES.CONNECTED );
|
||||
await setCompleted( true );
|
||||
} else {
|
||||
createErrorNotice( MESSAGES.LOGIN_FAILED );
|
||||
}
|
||||
} catch ( error ) {
|
||||
createErrorNotice( error.message ?? MESSAGES.LOGIN_FAILED );
|
||||
}
|
||||
},
|
||||
createErrorNotice,
|
||||
};
|
||||
};
|
||||
|
||||
const useConnectionAttempt = ( connectFn, errorMessage ) => {
|
||||
const { handleFailed, createErrorNotice, handleCompleted } =
|
||||
useConnectionBase();
|
||||
|
||||
return async ( ...args ) => {
|
||||
const res = await connectFn( ...args );
|
||||
|
||||
if ( ! res.success || ! res.data ) {
|
||||
handleFailed( res, errorMessage );
|
||||
return false;
|
||||
}
|
||||
|
||||
const popupClosed = await handlePopupWithCompletion(
|
||||
res.data,
|
||||
createErrorNotice
|
||||
);
|
||||
|
||||
if ( popupClosed ) {
|
||||
await handleCompleted();
|
||||
}
|
||||
|
||||
return popupClosed;
|
||||
};
|
||||
};
|
||||
|
||||
export const useSandboxConnection = () => {
|
||||
const { connectToSandbox, isSandboxMode, setSandboxMode } =
|
||||
CommonHooks.useSandbox();
|
||||
const { withActivity } = CommonHooks.useBusyState();
|
||||
const connectionAttempt = useConnectionAttempt(
|
||||
connectToSandbox,
|
||||
MESSAGES.SANDBOX_ERROR
|
||||
);
|
||||
|
||||
const handleSandboxConnect = async () => {
|
||||
return withActivity(
|
||||
ACTIVITIES.CONNECT_SANDBOX,
|
||||
'Connecting to sandbox account',
|
||||
connectionAttempt
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
handleSandboxConnect,
|
||||
isSandboxMode,
|
||||
setSandboxMode,
|
||||
};
|
||||
};
|
||||
|
||||
export const useProductionConnection = () => {
|
||||
const { connectToProduction } = CommonHooks.useProduction();
|
||||
const { withActivity } = CommonHooks.useBusyState();
|
||||
const products = OnboardingHooks.useDetermineProducts();
|
||||
const connectionAttempt = useConnectionAttempt(
|
||||
() => connectToProduction( products ),
|
||||
MESSAGES.PRODUCTION_ERROR
|
||||
);
|
||||
|
||||
const handleProductionConnect = async () => {
|
||||
return withActivity(
|
||||
ACTIVITIES.CONNECT_PRODUCTION,
|
||||
'Connecting to production account',
|
||||
connectionAttempt
|
||||
);
|
||||
};
|
||||
|
||||
return { handleProductionConnect };
|
||||
};
|
||||
|
||||
export const useManualConnection = () => {
|
||||
const { handleFailed, handleCompleted, createErrorNotice } =
|
||||
useConnectionBase();
|
||||
const { withActivity } = CommonHooks.useBusyState();
|
||||
const {
|
||||
connectViaIdAndSecret,
|
||||
isManualConnectionMode,
|
||||
setManualConnectionMode,
|
||||
clientId,
|
||||
setClientId,
|
||||
clientSecret,
|
||||
setClientSecret,
|
||||
} = CommonHooks.useManualConnection();
|
||||
|
||||
const handleConnectViaIdAndSecret = async ( { validation } = {} ) => {
|
||||
return withActivity(
|
||||
ACTIVITIES.CONNECT_MANUAL,
|
||||
'Connecting manually via Client ID and Secret',
|
||||
async () => {
|
||||
if ( 'function' === typeof validation ) {
|
||||
try {
|
||||
validation();
|
||||
} catch ( exception ) {
|
||||
createErrorNotice( exception.message );
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const res = await connectViaIdAndSecret();
|
||||
|
||||
if ( res.success ) {
|
||||
await handleCompleted();
|
||||
} else {
|
||||
handleFailed( res, MESSAGES.MANUAL_ERROR );
|
||||
}
|
||||
|
||||
return res.success;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
handleConnectViaIdAndSecret,
|
||||
isManualConnectionMode,
|
||||
setManualConnectionMode,
|
||||
clientId,
|
||||
setClientId,
|
||||
clientSecret,
|
||||
setClientSecret,
|
||||
};
|
||||
};
|
|
@ -1,18 +0,0 @@
|
|||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
const generatePriceText = ( type, selectedCountryPrice, storeCurrency ) => {
|
||||
if ( ! selectedCountryPrice || ! selectedCountryPrice[ type ] ) {
|
||||
console.warn( `Invalid type or price data for: ${ type }` );
|
||||
return '';
|
||||
}
|
||||
|
||||
const percentage = selectedCountryPrice[ type ].toFixed( 2 );
|
||||
const fixedFee = `${ selectedCountryPrice.currencySymbol }${ selectedCountryPrice.fixedFee }`;
|
||||
|
||||
return __(
|
||||
`from ${ percentage }% + ${ fixedFee } ${ storeCurrency }<sup>1</sup>`,
|
||||
'woocommerce-paypal-payments'
|
||||
);
|
||||
};
|
||||
|
||||
export default generatePriceText;
|
|
@ -1,74 +1,140 @@
|
|||
export const countryPriceInfo = {
|
||||
US: {
|
||||
currencySymbol: '$',
|
||||
fixedFee: 0.49,
|
||||
fixedFee: {
|
||||
USD: 0.49,
|
||||
GBP: 0.39,
|
||||
CAD: 0.59,
|
||||
AUD: 0.59,
|
||||
EUR: 0.39,
|
||||
},
|
||||
checkout: 3.49,
|
||||
ccf: 2.59,
|
||||
dw: 2.59,
|
||||
apm: 2.59,
|
||||
fastlane: 2.59,
|
||||
plater: 4.99,
|
||||
ccf: {
|
||||
percentage: 2.59,
|
||||
fixedFee: 0.29,
|
||||
},
|
||||
dw: {
|
||||
percentage: 2.59,
|
||||
fixedFee: 0.29,
|
||||
},
|
||||
apm: {
|
||||
percentage: 2.89,
|
||||
fixedFee: 0.29,
|
||||
},
|
||||
fast: {
|
||||
percentage: 2.59,
|
||||
fixedFee: 0.29,
|
||||
},
|
||||
standardCardFields: 2.99,
|
||||
},
|
||||
UK: {
|
||||
currencySymbol: '£',
|
||||
fixedFee: 0.3,
|
||||
fixedFee: {
|
||||
GPB: 0.3,
|
||||
USD: 0.3,
|
||||
CAD: 0.3,
|
||||
AUD: 0.3,
|
||||
EUR: 0.35,
|
||||
},
|
||||
checkout: 2.9,
|
||||
plater: 2.9,
|
||||
ccf: 1.2,
|
||||
dw: 1.2,
|
||||
fast: 1.2,
|
||||
apm: 1.2,
|
||||
standardCardFields: 1.2,
|
||||
},
|
||||
CA: {
|
||||
currencySymbol: '$',
|
||||
fixedFee: 0.3,
|
||||
fixedFee: {
|
||||
CAD: 0.3,
|
||||
USD: 0.3,
|
||||
GBP: 0.2,
|
||||
AUD: 0.3,
|
||||
EUR: 0.35,
|
||||
},
|
||||
checkout: 2.9,
|
||||
ccf: 2.7,
|
||||
dw: 2.7,
|
||||
fast: 2.7,
|
||||
apm: 2.9,
|
||||
standardCardFields: 2.9,
|
||||
},
|
||||
AU: {
|
||||
currencySymbol: '$',
|
||||
fixedFee: 0.3,
|
||||
fixedFee: {
|
||||
AUD: 0.3,
|
||||
USD: 0.3,
|
||||
GBP: 0.2,
|
||||
CAD: 0.3,
|
||||
EUR: 0.35,
|
||||
},
|
||||
checkout: 2.6,
|
||||
plater: 2.6,
|
||||
ccf: 1.75,
|
||||
dw: 1.75,
|
||||
fast: 1.75,
|
||||
apm: 2.6,
|
||||
standardCardFields: 2.6,
|
||||
},
|
||||
FR: {
|
||||
currencySymbol: '€',
|
||||
fixedFee: 0.35,
|
||||
fixedFee: {
|
||||
EUR: 0.35,
|
||||
USD: 0.3,
|
||||
GBP: 0.3,
|
||||
CAD: 0.3,
|
||||
AUD: 0.3,
|
||||
},
|
||||
checkout: 2.9,
|
||||
plater: 2.9,
|
||||
ccf: 1.2,
|
||||
dw: 1.2,
|
||||
fast: 1.2,
|
||||
apm: 1.2,
|
||||
standardCardFields: 1.2,
|
||||
},
|
||||
IT: {
|
||||
currencySymbol: '€',
|
||||
fixedFee: 0.35,
|
||||
fixedFee: {
|
||||
EUR: 0.35,
|
||||
USD: 0.3,
|
||||
GBP: 0.3,
|
||||
CAD: 0.3,
|
||||
AUD: 0.3,
|
||||
},
|
||||
checkout: 3.4,
|
||||
plater: 3.4,
|
||||
ccf: 1.2,
|
||||
dw: 1.2,
|
||||
fast: 1.2,
|
||||
apm: 1.2,
|
||||
standardCardFields: 1.2,
|
||||
},
|
||||
DE: {
|
||||
currencySymbol: '€',
|
||||
fixedFee: 0.39,
|
||||
fixedFee: {
|
||||
EUR: 0.39,
|
||||
USD: 0.49,
|
||||
GBP: 0.29,
|
||||
CAD: 0.59,
|
||||
AUD: 0.59,
|
||||
},
|
||||
checkout: 2.99,
|
||||
plater: 2.99,
|
||||
ccf: 2.99,
|
||||
dw: 2.99,
|
||||
fast: 2.99,
|
||||
apm: 2.99,
|
||||
standardCardFields: 2.99,
|
||||
},
|
||||
ES: {
|
||||
currencySymbol: '€',
|
||||
fixedFee: 0.35,
|
||||
fixedFee: {
|
||||
EUR: 0.35,
|
||||
USD: 0.3,
|
||||
GBP: 0.3,
|
||||
CAD: 0.3,
|
||||
AUD: 0.3,
|
||||
},
|
||||
checkout: 2.9,
|
||||
plater: 2.9,
|
||||
ccf: 1.2,
|
||||
dw: 1.2,
|
||||
fast: 1.2,
|
||||
apm: 1.2,
|
||||
standardCardFields: 1.2,
|
||||
},
|
||||
|
|
34
modules/ppcp-settings/resources/js/utils/formatPrice.js
Normal file
34
modules/ppcp-settings/resources/js/utils/formatPrice.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
const priceFormatInfo = {
|
||||
USD: {
|
||||
prefix: '$',
|
||||
suffix: 'USD',
|
||||
},
|
||||
CAD: {
|
||||
prefix: '$',
|
||||
suffix: 'CAD',
|
||||
},
|
||||
AUD: {
|
||||
prefix: '$',
|
||||
suffix: 'AUD',
|
||||
},
|
||||
EUR: {
|
||||
prefix: '€',
|
||||
suffix: '',
|
||||
},
|
||||
GPB: {
|
||||
prefix: '£',
|
||||
suffix: '',
|
||||
},
|
||||
};
|
||||
|
||||
export const formatPrice = ( value, currency ) => {
|
||||
const currencyInfo = priceFormatInfo[ currency ];
|
||||
const amount = value.toFixed( 2 );
|
||||
|
||||
if ( ! currencyInfo ) {
|
||||
console.error( `Unsupported currency: ${ currency }` );
|
||||
return amount;
|
||||
}
|
||||
|
||||
return `${ currencyInfo.prefix }${ amount } ${ currencyInfo.suffix }`;
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue