mirror of
https://github.com/woocommerce/woocommerce-paypal-payments.git
synced 2025-09-04 08:47:23 +08:00
Merge branch 'trunk' of github.com:woocommerce/woocommerce-paypal-payments into PCP-4276-fastlane-is-broken-on-frontend
This commit is contained in:
commit
23d51063d2
16 changed files with 654 additions and 155 deletions
|
@ -5,7 +5,7 @@
|
|||
* @package WooCommerce\PayPalCommerce\ApiClient\Repository
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
declare( strict_types = 1 );
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\ApiClient\Repository;
|
||||
|
||||
|
@ -18,43 +18,21 @@ class PartnerReferralsData {
|
|||
/**
|
||||
* The DCC Applies Helper object.
|
||||
*
|
||||
* @deprecated Deprecates with the new UI. In this class, the products are
|
||||
* always explicit, and should not be deducted from the
|
||||
* DccApplies state at this point.
|
||||
* Remove this with the legacy UI code.
|
||||
* @var DccApplies
|
||||
*/
|
||||
private $dcc_applies;
|
||||
|
||||
/**
|
||||
* The list of products ('PPCP', 'EXPRESS_CHECKOUT').
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
private $products;
|
||||
private DccApplies $dcc_applies;
|
||||
|
||||
/**
|
||||
* PartnerReferralsData constructor.
|
||||
*
|
||||
* @param DccApplies $dcc_applies The DCC Applies helper.
|
||||
*/
|
||||
public function __construct(
|
||||
DccApplies $dcc_applies
|
||||
) {
|
||||
public function __construct( DccApplies $dcc_applies ) {
|
||||
$this->dcc_applies = $dcc_applies;
|
||||
$this->products = array(
|
||||
$this->dcc_applies->for_country_currency() ? 'PPCP' : 'EXPRESS_CHECKOUT',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new copy of this object with the given value set.
|
||||
*
|
||||
* @param string[] $products The list of products ('PPCP', 'EXPRESS_CHECKOUT').
|
||||
* @return static
|
||||
*/
|
||||
public function with_products( array $products ): self {
|
||||
$obj = clone $this;
|
||||
|
||||
$obj->products = $products;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -62,82 +40,119 @@ class PartnerReferralsData {
|
|||
*
|
||||
* @return string
|
||||
*/
|
||||
public function nonce(): string {
|
||||
public function nonce() : string {
|
||||
return 'a1233wtergfsdt4365tzrshgfbaewa36AGa1233wtergfsdt4365tzrshgfbaewa36AG';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data.
|
||||
*
|
||||
* @param string[] $products The list of products to use ('PPCP', 'EXPRESS_CHECKOUT').
|
||||
* Default is based on DCC availability.
|
||||
* @param string $onboarding_token A security token to finalize the onboarding process.
|
||||
* @param bool $use_subscriptions If the merchant requires subscription features.
|
||||
* @param bool $use_card_payments If the merchant wants to process credit card payments.
|
||||
* @return array
|
||||
*/
|
||||
public function data(): array {
|
||||
public function data( array $products = array(), string $onboarding_token = '', bool $use_subscriptions = null, bool $use_card_payments = true ) : array {
|
||||
if ( ! $products ) {
|
||||
$products = array(
|
||||
$this->dcc_applies->for_country_currency() ? 'PPCP' : 'EXPRESS_CHECKOUT',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the partners referrals data.
|
||||
* Filter the return-URL, which is called at the end of the OAuth onboarding
|
||||
* process, when the merchant clicks the "Return to your shop" button.
|
||||
*/
|
||||
return apply_filters(
|
||||
'ppcp_partner_referrals_data',
|
||||
array(
|
||||
'partner_config_override' => array(
|
||||
/**
|
||||
* Returns the URL which will be opened at the end of onboarding.
|
||||
*/
|
||||
'return_url' => apply_filters(
|
||||
'woocommerce_paypal_payments_partner_config_override_return_url',
|
||||
admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=ppcp-gateway' )
|
||||
),
|
||||
/**
|
||||
* Returns the description of the URL which will be opened at the end of onboarding.
|
||||
*/
|
||||
'return_url_description' => apply_filters(
|
||||
'woocommerce_paypal_payments_partner_config_override_return_url_description',
|
||||
__( 'Return to your shop.', 'woocommerce-paypal-payments' )
|
||||
),
|
||||
'show_add_credit_card' => true,
|
||||
$return_url = apply_filters(
|
||||
'woocommerce_paypal_payments_partner_config_override_return_url',
|
||||
admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=ppcp-gateway' )
|
||||
);
|
||||
|
||||
/**
|
||||
* Filter the label of the "Return to your shop" button.
|
||||
* It's displayed on the very last page of the onboarding popup.
|
||||
*/
|
||||
$return_url_label = apply_filters(
|
||||
'woocommerce_paypal_payments_partner_config_override_return_url_description',
|
||||
__( 'Return to your shop.', 'woocommerce-paypal-payments' )
|
||||
);
|
||||
|
||||
$capabilities = array();
|
||||
$first_party_features = array(
|
||||
'PAYMENT',
|
||||
'REFUND',
|
||||
'ADVANCED_TRANSACTIONS_SEARCH',
|
||||
'TRACKING_SHIPMENT_READWRITE',
|
||||
);
|
||||
|
||||
if ( true === $use_subscriptions ) {
|
||||
$capabilities[] = 'PAYPAL_WALLET_VAULTING_ADVANCED';
|
||||
}
|
||||
|
||||
// Backwards compatibility. Keep those features in the legacy UI (null-value).
|
||||
// Move this into the previous condition, once legacy code is removed.
|
||||
if ( false !== $use_subscriptions ) {
|
||||
$first_party_features[] = 'FUTURE_PAYMENT';
|
||||
$first_party_features[] = 'VAULT';
|
||||
}
|
||||
|
||||
if ( false === $use_subscriptions ) {
|
||||
// Only use "ADVANCED_VAULTING" product for onboarding with subscriptions.
|
||||
$products = array_filter(
|
||||
$products,
|
||||
static fn( $product ) => $product !== 'ADVANCED_VAULTING'
|
||||
);
|
||||
}
|
||||
|
||||
$payload = array(
|
||||
'partner_config_override' => array(
|
||||
'return_url' => $return_url,
|
||||
'return_url_description' => $return_url_label,
|
||||
'show_add_credit_card' => $use_card_payments,
|
||||
),
|
||||
'products' => $products,
|
||||
'capabilities' => $capabilities,
|
||||
'legal_consents' => array(
|
||||
array(
|
||||
'type' => 'SHARE_DATA_CONSENT',
|
||||
'granted' => true,
|
||||
),
|
||||
'products' => $this->products,
|
||||
'legal_consents' => array(
|
||||
array(
|
||||
'type' => 'SHARE_DATA_CONSENT',
|
||||
'granted' => true,
|
||||
),
|
||||
),
|
||||
'operations' => array(
|
||||
array(
|
||||
'operation' => 'API_INTEGRATION',
|
||||
'api_integration_preference' => array(
|
||||
'rest_api_integration' => array(
|
||||
'integration_method' => 'PAYPAL',
|
||||
'integration_type' => 'FIRST_PARTY',
|
||||
'first_party_details' => array(
|
||||
'features' => array(
|
||||
'PAYMENT',
|
||||
'FUTURE_PAYMENT',
|
||||
'REFUND',
|
||||
'ADVANCED_TRANSACTIONS_SEARCH',
|
||||
'VAULT',
|
||||
'TRACKING_SHIPMENT_READWRITE',
|
||||
),
|
||||
'seller_nonce' => $this->nonce(),
|
||||
),
|
||||
),
|
||||
'operations' => array(
|
||||
array(
|
||||
'operation' => 'API_INTEGRATION',
|
||||
'api_integration_preference' => array(
|
||||
'rest_api_integration' => array(
|
||||
'integration_method' => 'PAYPAL',
|
||||
'integration_type' => 'FIRST_PARTY',
|
||||
'first_party_details' => array(
|
||||
'features' => $first_party_features,
|
||||
'seller_nonce' => $this->nonce(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Append the validation token to the return_url
|
||||
*
|
||||
* @param array $data The referral data.
|
||||
* @param string $token The token to be appended.
|
||||
* @return array
|
||||
*/
|
||||
public function append_onboarding_token( array $data, string $token ): array {
|
||||
$data['partner_config_override']['return_url'] =
|
||||
add_query_arg( 'ppcpToken', $token, $data['partner_config_override']['return_url'] );
|
||||
return $data;
|
||||
/**
|
||||
* Filter the final partners referrals data collection.
|
||||
*/
|
||||
$payload = apply_filters( 'ppcp_partner_referrals_data', $payload );
|
||||
|
||||
// An empty array is not permitted.
|
||||
if ( isset( $payload['capabilities'] ) && ! $payload['capabilities'] ) {
|
||||
unset( $payload['capabilities'] );
|
||||
}
|
||||
|
||||
// Add the nonce in the end, to maintain backwards compatibility of filters.
|
||||
$payload['partner_config_override']['return_url'] = add_query_arg(
|
||||
array( 'ppcpToken' => $onboarding_token ),
|
||||
$payload['partner_config_override']['return_url']
|
||||
);
|
||||
|
||||
return $payload;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -104,6 +104,6 @@ class DisabledFundingSources {
|
|||
$disable_funding = $all_sources;
|
||||
}
|
||||
|
||||
return $disable_funding;
|
||||
return apply_filters( 'woocommerce_paypal_payments_disabled_funding_sources', $disable_funding );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -326,9 +326,9 @@ window.ppcp_onboarding_productionCallback = function ( ...args ) {
|
|||
|
||||
isDisconnecting = true;
|
||||
|
||||
const saveButton = document.querySelector( '.woocommerce-save-button' );
|
||||
saveButton.removeAttribute( 'disabled' );
|
||||
saveButton.click();
|
||||
const saveButton = document.querySelector( '.woocommerce-save-button' );
|
||||
saveButton.removeAttribute( 'disabled' );
|
||||
saveButton.click();
|
||||
};
|
||||
|
||||
// Prevent the message about unsaved checkbox/radiobutton when reloading the page.
|
||||
|
@ -345,9 +345,11 @@ window.ppcp_onboarding_productionCallback = function ( ...args ) {
|
|||
|
||||
const sandboxSwitchElement = document.querySelector( '#ppcp-sandbox_on' );
|
||||
|
||||
sandboxSwitchElement?.addEventListener( 'click', () => {
|
||||
document.querySelector( '.woocommerce-save-button' )?.removeAttribute( 'disabled' );
|
||||
});
|
||||
sandboxSwitchElement?.addEventListener( 'click', () => {
|
||||
document
|
||||
.querySelector( '.woocommerce-save-button' )
|
||||
?.removeAttribute( 'disabled' );
|
||||
} );
|
||||
|
||||
const validate = () => {
|
||||
const selectors = sandboxSwitchElement.checked
|
||||
|
@ -389,7 +391,8 @@ window.ppcp_onboarding_productionCallback = function ( ...args ) {
|
|||
|
||||
const isSandboxInBackend =
|
||||
PayPalCommerceGatewayOnboarding.current_env === 'sandbox';
|
||||
if ( sandboxSwitchElement.checked !== isSandboxInBackend ) {
|
||||
|
||||
if ( sandboxSwitchElement?.checked !== isSandboxInBackend ) {
|
||||
sandboxSwitchElement.checked = isSandboxInBackend;
|
||||
}
|
||||
|
||||
|
|
|
@ -103,12 +103,8 @@ class OnboardingRenderer {
|
|||
'displayMode' => 'minibrowser',
|
||||
);
|
||||
|
||||
$data = $this->partner_referrals_data
|
||||
->with_products( $products )
|
||||
->data();
|
||||
|
||||
$environment = $is_production ? 'production' : 'sandbox';
|
||||
$product = 'PPCP' === $data['products'][0] ? 'ppcp' : 'express_checkout';
|
||||
$product = strtolower( $products[0] ?? 'express_checkout' );
|
||||
$cache_key = $environment . '-' . $product;
|
||||
|
||||
$onboarding_url = new OnboardingUrl( $this->cache, $cache_key, get_current_user_id() );
|
||||
|
@ -122,8 +118,7 @@ class OnboardingRenderer {
|
|||
|
||||
$onboarding_url->init();
|
||||
|
||||
$data = $this->partner_referrals_data
|
||||
->append_onboarding_token( $data, $onboarding_url->token() ?: '' );
|
||||
$data = $this->partner_referrals_data->data( $products, $onboarding_url->token() ?: '' );
|
||||
|
||||
$url = $is_production ? $this->production_partner_referrals->signup_link( $data ) : $this->sandbox_partner_referrals->signup_link( $data );
|
||||
$url = add_query_arg( $args, $url );
|
||||
|
|
|
@ -68,10 +68,11 @@ export function sandboxOnboardingUrl() {
|
|||
/**
|
||||
* Side effect. Fetches the ISU-login URL for a production account.
|
||||
*
|
||||
* @param {string[]} products Which products/features to display in the ISU popup.
|
||||
* @param {string[]} [products=[]] Which products/features to display in the ISU popup.
|
||||
* @param {Object} [options={}] Options to customize the onboarding workflow.
|
||||
* @return {Function} The thunk function.
|
||||
*/
|
||||
export function productionOnboardingUrl( products = [] ) {
|
||||
export function productionOnboardingUrl( products = [], options = {} ) {
|
||||
return async () => {
|
||||
try {
|
||||
return apiFetch( {
|
||||
|
@ -80,6 +81,7 @@ export function productionOnboardingUrl( products = [] ) {
|
|||
data: {
|
||||
useSandbox: false,
|
||||
products,
|
||||
options,
|
||||
},
|
||||
} );
|
||||
} catch ( e ) {
|
||||
|
|
|
@ -41,8 +41,6 @@ const useHooks = () => {
|
|||
const { useTransient, usePersistent, dispatch, select } = useStoreData();
|
||||
const {
|
||||
persist,
|
||||
sandboxOnboardingUrl,
|
||||
productionOnboardingUrl,
|
||||
authenticateWithCredentials,
|
||||
authenticateWithOAuth,
|
||||
startWebhookSimulation,
|
||||
|
@ -53,7 +51,6 @@ const useHooks = () => {
|
|||
const [ activeModal, setActiveModal ] = useTransient( 'activeModal' );
|
||||
|
||||
// Persistent accessors.
|
||||
const [ isSandboxMode, setSandboxMode ] = usePersistent( 'useSandbox' );
|
||||
const [ isManualConnectionMode, setManualConnectionMode ] = usePersistent(
|
||||
'useManualConnection'
|
||||
);
|
||||
|
@ -71,16 +68,10 @@ const useHooks = () => {
|
|||
return {
|
||||
activeModal,
|
||||
setActiveModal,
|
||||
isSandboxMode,
|
||||
setSandboxMode: ( state ) => {
|
||||
return savePersistent( setSandboxMode, state );
|
||||
},
|
||||
isManualConnectionMode,
|
||||
setManualConnectionMode: ( state ) => {
|
||||
return savePersistent( setManualConnectionMode, state );
|
||||
},
|
||||
sandboxOnboardingUrl,
|
||||
productionOnboardingUrl,
|
||||
authenticateWithCredentials,
|
||||
authenticateWithOAuth,
|
||||
wooSettings,
|
||||
|
@ -105,13 +96,23 @@ export const useStore = () => {
|
|||
};
|
||||
|
||||
export const useSandbox = () => {
|
||||
const { isSandboxMode, setSandboxMode, sandboxOnboardingUrl } = useHooks();
|
||||
const { dispatch, usePersistent } = useStoreData();
|
||||
const [ isSandboxMode, setSandboxMode ] = usePersistent( 'useSandbox' );
|
||||
const { sandboxOnboardingUrl, persist } = dispatch;
|
||||
|
||||
return { isSandboxMode, setSandboxMode, sandboxOnboardingUrl };
|
||||
return {
|
||||
isSandboxMode,
|
||||
setSandboxMode: ( state ) => {
|
||||
setSandboxMode( state );
|
||||
return persist();
|
||||
},
|
||||
sandboxOnboardingUrl,
|
||||
};
|
||||
};
|
||||
|
||||
export const useProduction = () => {
|
||||
const { productionOnboardingUrl } = useHooks();
|
||||
const { dispatch } = useStoreData();
|
||||
const { productionOnboardingUrl } = dispatch;
|
||||
|
||||
return { productionOnboardingUrl };
|
||||
};
|
||||
|
|
|
@ -24,3 +24,9 @@ export const PRODUCT_TYPES = {
|
|||
PHYSICAL: 'physical',
|
||||
SUBSCRIPTIONS: 'subscriptions',
|
||||
};
|
||||
|
||||
export const PAYPAL_PRODUCTS = {
|
||||
ACDC: 'PPCP',
|
||||
BCDC: 'EXPRESS_CHECKOUT',
|
||||
VAULTING: 'ADVANCED_VAULTING',
|
||||
};
|
||||
|
|
|
@ -19,10 +19,6 @@ const useHooks = () => {
|
|||
|
||||
// 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,7 +76,6 @@ const useHooks = () => {
|
|||
);
|
||||
return savePersistent( setProducts, validProducts );
|
||||
},
|
||||
determineProducts,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -141,9 +136,9 @@ export const useNavigationState = () => {
|
|||
};
|
||||
|
||||
export const useDetermineProducts = () => {
|
||||
const { determineProducts } = useHooks();
|
||||
|
||||
return determineProducts;
|
||||
return useSelect( ( select ) => {
|
||||
return select( STORE_NAME ).determineProductsAndCaps();
|
||||
}, [] );
|
||||
};
|
||||
|
||||
export const useFlags = () => {
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
* @file
|
||||
*/
|
||||
|
||||
import { PAYPAL_PRODUCTS, PRODUCT_TYPES } from './configuration';
|
||||
|
||||
const EMPTY_OBJ = Object.freeze( {} );
|
||||
|
||||
const getState = ( state ) => state || EMPTY_OBJ;
|
||||
|
@ -25,44 +27,71 @@ export const flags = ( state ) => {
|
|||
};
|
||||
|
||||
/**
|
||||
* Returns the products that we use for the production login link in the last onboarding step.
|
||||
* Returns details about products and capabilities to 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.
|
||||
* @return {{products:string[], options:{}}} The ISU products, based on choices made in the onboarding wizard.
|
||||
*/
|
||||
export const determineProducts = ( state ) => {
|
||||
const derivedProducts = [];
|
||||
export const determineProductsAndCaps = ( state ) => {
|
||||
/**
|
||||
* An array of product-names that are used to build an onboarding URL via the
|
||||
* PartnerReferrals API. To avoid confusion with the "products" property from the
|
||||
* Redux store, this collection has a distinct name.
|
||||
*
|
||||
* On server-side, this value is referred to as "products" again.
|
||||
*/
|
||||
const apiModules = [];
|
||||
|
||||
const { isCasualSeller, areOptionalPaymentMethodsEnabled } =
|
||||
/**
|
||||
* Internal options that are parsed by the PartnerReferrals class to customize
|
||||
* the API payload.
|
||||
*/
|
||||
const options = {
|
||||
useSubscriptions: false,
|
||||
useCardPayments: false,
|
||||
};
|
||||
|
||||
const { isCasualSeller, areOptionalPaymentMethodsEnabled, products } =
|
||||
persistentData( state );
|
||||
const { canUseVaulting, canUseCardPayments } = flags( state );
|
||||
const cardPaymentsEligibleAndSelected =
|
||||
canUseCardPayments && areOptionalPaymentMethodsEnabled;
|
||||
|
||||
if ( ! canUseCardPayments || ! areOptionalPaymentMethodsEnabled ) {
|
||||
if ( ! cardPaymentsEligibleAndSelected ) {
|
||||
/**
|
||||
* Branch 1: Credit Card Payments not available.
|
||||
* The store uses the Express-checkout product.
|
||||
*/
|
||||
derivedProducts.push( 'EXPRESS_CHECKOUT' );
|
||||
apiModules.push( PAYPAL_PRODUCTS.BCDC );
|
||||
} else if ( isCasualSeller ) {
|
||||
/**
|
||||
* Branch 2: Merchant has no business.
|
||||
* The store uses the Express-checkout product.
|
||||
*/
|
||||
derivedProducts.push( 'EXPRESS_CHECKOUT' );
|
||||
apiModules.push( PAYPAL_PRODUCTS.BCDC );
|
||||
} else {
|
||||
/**
|
||||
* Branch 3: Merchant is business, and can use CC payments.
|
||||
* The store uses the advanced PPCP product.
|
||||
*
|
||||
* This is the only branch that can use subscriptions.
|
||||
*/
|
||||
derivedProducts.push( 'PPCP' );
|
||||
apiModules.push( PAYPAL_PRODUCTS.ACDC );
|
||||
|
||||
if ( products?.includes( PRODUCT_TYPES.SUBSCRIPTIONS ) ) {
|
||||
options.useSubscriptions = true;
|
||||
}
|
||||
|
||||
if ( canUseVaulting ) {
|
||||
apiModules.push( PAYPAL_PRODUCTS.VAULTING );
|
||||
}
|
||||
}
|
||||
|
||||
if ( canUseVaulting ) {
|
||||
derivedProducts.push( 'ADVANCED_VAULTING' );
|
||||
}
|
||||
options.useCardPayments = cardPaymentsEligibleAndSelected;
|
||||
|
||||
return derivedProducts;
|
||||
return { products: apiModules, options };
|
||||
};
|
||||
|
|
|
@ -0,0 +1,181 @@
|
|||
import '@testing-library/jest-dom';
|
||||
|
||||
import { PRODUCT_TYPES } from './configuration';
|
||||
import { determineProductsAndCaps } from './selectors';
|
||||
|
||||
describe( 'determineProductsAndCaps selector [casual seller]', () => {
|
||||
const testCases = [
|
||||
{
|
||||
name: 'should return EXPRESS_CHECKOUT when card payments are not available',
|
||||
state: {
|
||||
data: {
|
||||
isCasualSeller: true,
|
||||
areOptionalPaymentMethodsEnabled: true,
|
||||
},
|
||||
flags: { canUseCardPayments: false, canUseVaulting: false },
|
||||
},
|
||||
expected: {
|
||||
products: [ 'EXPRESS_CHECKOUT' ],
|
||||
options: { useSubscriptions: false, useCardPayments: false },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'should return EXPRESS_CHECKOUT when optional payment methods are disabled',
|
||||
state: {
|
||||
data: {
|
||||
isCasualSeller: true,
|
||||
areOptionalPaymentMethodsEnabled: false,
|
||||
},
|
||||
flags: { canUseCardPayments: true, canUseVaulting: false },
|
||||
},
|
||||
expected: {
|
||||
products: [ 'EXPRESS_CHECKOUT' ],
|
||||
options: { useSubscriptions: false, useCardPayments: false },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'should return EXPRESS_CHECKOUT for casual sellers with card payments',
|
||||
state: {
|
||||
data: {
|
||||
isCasualSeller: true,
|
||||
areOptionalPaymentMethodsEnabled: true,
|
||||
},
|
||||
flags: { canUseCardPayments: true, canUseVaulting: false },
|
||||
},
|
||||
expected: {
|
||||
products: [ 'EXPRESS_CHECKOUT' ],
|
||||
options: { useSubscriptions: false, useCardPayments: true },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'should return EXPRESS_CHECKOUT and ADVANCED_VAULTING when card payments are not available but vaulting is',
|
||||
state: {
|
||||
data: {
|
||||
isCasualSeller: true,
|
||||
areOptionalPaymentMethodsEnabled: true,
|
||||
},
|
||||
flags: { canUseCardPayments: false, canUseVaulting: true },
|
||||
},
|
||||
expected: {
|
||||
products: [ 'EXPRESS_CHECKOUT' ],
|
||||
options: { useSubscriptions: false, useCardPayments: false },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'should ignore SUBSCRIPTION product for casual sellers',
|
||||
state: {
|
||||
data: {
|
||||
isCasualSeller: true,
|
||||
areOptionalPaymentMethodsEnabled: true,
|
||||
products: [ PRODUCT_TYPES.SUBSCRIPTIONS ],
|
||||
},
|
||||
flags: { canUseCardPayments: false, canUseVaulting: true },
|
||||
},
|
||||
expected: {
|
||||
products: [ 'EXPRESS_CHECKOUT' ],
|
||||
options: { useSubscriptions: false, useCardPayments: false },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
test.each( testCases )( '$name', ( { state, expected } ) => {
|
||||
const result = determineProductsAndCaps( state );
|
||||
expect( result ).toEqual( expected );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'determineProductsAndCaps selector [business seller]', () => {
|
||||
const testCases = [
|
||||
{
|
||||
name: 'should return EXPRESS_CHECKOUT when card payments are not available',
|
||||
state: {
|
||||
data: {
|
||||
isCasualSeller: false,
|
||||
areOptionalPaymentMethodsEnabled: true,
|
||||
},
|
||||
flags: { canUseCardPayments: false, canUseVaulting: false },
|
||||
},
|
||||
expected: {
|
||||
products: [ 'EXPRESS_CHECKOUT' ],
|
||||
options: { useSubscriptions: false, useCardPayments: false },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'should return EXPRESS_CHECKOUT when optional payment methods are disabled',
|
||||
state: {
|
||||
data: {
|
||||
isCasualSeller: false,
|
||||
areOptionalPaymentMethodsEnabled: false,
|
||||
},
|
||||
flags: { canUseCardPayments: true, canUseVaulting: false },
|
||||
},
|
||||
expected: {
|
||||
products: [ 'EXPRESS_CHECKOUT' ],
|
||||
options: { useSubscriptions: false, useCardPayments: false },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'should return PPCP for business merchants with card payments',
|
||||
state: {
|
||||
data: {
|
||||
isCasualSeller: false,
|
||||
areOptionalPaymentMethodsEnabled: true,
|
||||
},
|
||||
flags: { canUseCardPayments: true, canUseVaulting: false },
|
||||
},
|
||||
expected: {
|
||||
products: [ 'PPCP' ],
|
||||
options: { useSubscriptions: false, useCardPayments: true },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'should include ADVANCED_VAULTING when vaulting is available',
|
||||
state: {
|
||||
data: {
|
||||
isCasualSeller: false,
|
||||
areOptionalPaymentMethodsEnabled: true,
|
||||
},
|
||||
flags: { canUseCardPayments: true, canUseVaulting: true },
|
||||
},
|
||||
expected: {
|
||||
products: [ 'PPCP', 'ADVANCED_VAULTING' ],
|
||||
options: { useSubscriptions: false, useCardPayments: true },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'should return EXPRESS_CHECKOUT and ADVANCED_VAULTING when card payments are not available but vaulting is',
|
||||
state: {
|
||||
data: {
|
||||
isCasualSeller: false,
|
||||
areOptionalPaymentMethodsEnabled: true,
|
||||
products: [ PRODUCT_TYPES.VIRTUAL ],
|
||||
},
|
||||
flags: { canUseCardPayments: false, canUseVaulting: true },
|
||||
},
|
||||
expected: {
|
||||
products: [ 'EXPRESS_CHECKOUT' ],
|
||||
options: { useSubscriptions: false, useCardPayments: false },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'should enable the SUBSCRIPTIONS option when a business seller selects the subscriptions-product',
|
||||
state: {
|
||||
data: {
|
||||
isCasualSeller: false,
|
||||
areOptionalPaymentMethodsEnabled: true,
|
||||
products: [ PRODUCT_TYPES.SUBSCRIPTIONS ],
|
||||
},
|
||||
flags: { canUseCardPayments: true, canUseVaulting: true },
|
||||
},
|
||||
expected: {
|
||||
products: [ 'PPCP', 'ADVANCED_VAULTING' ],
|
||||
options: { useSubscriptions: true, useCardPayments: true },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
test.each( testCases )( '$name', ( { state, expected } ) => {
|
||||
const result = determineProductsAndCaps( state );
|
||||
expect( result ).toEqual( expected );
|
||||
} );
|
||||
} );
|
|
@ -44,6 +44,7 @@ const defaultPersistent = Object.freeze( {
|
|||
threeDSecure: 'no-3d-secure',
|
||||
fastlaneCardholderName: false,
|
||||
fastlaneDisplayWatermark: false,
|
||||
__meta: false,
|
||||
} );
|
||||
|
||||
// Reducer logic.
|
||||
|
|
|
@ -30,7 +30,7 @@ const ACTIVITIES = {
|
|||
export const useHandleOnboardingButton = ( isSandbox ) => {
|
||||
const { sandboxOnboardingUrl } = CommonHooks.useSandbox();
|
||||
const { productionOnboardingUrl } = CommonHooks.useProduction();
|
||||
const products = OnboardingHooks.useDetermineProducts();
|
||||
const { products, options } = OnboardingHooks.useDetermineProducts();
|
||||
const { startActivity } = CommonHooks.useBusyState();
|
||||
const { authenticateWithOAuth } = CommonHooks.useAuthentication();
|
||||
const [ onboardingUrl, setOnboardingUrl ] = useState( '' );
|
||||
|
@ -43,7 +43,7 @@ export const useHandleOnboardingButton = ( isSandbox ) => {
|
|||
if ( isSandbox ) {
|
||||
res = await sandboxOnboardingUrl();
|
||||
} else {
|
||||
res = await productionOnboardingUrl( products );
|
||||
res = await productionOnboardingUrl( products, options );
|
||||
}
|
||||
|
||||
if ( res.success && res.data ) {
|
||||
|
|
|
@ -82,6 +82,16 @@ class LoginLinkRestEndpoint extends RestEndpoint {
|
|||
return array_map( 'sanitize_text_field', $products );
|
||||
},
|
||||
),
|
||||
'options' => array(
|
||||
'requires' => false,
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
'type' => 'bool',
|
||||
),
|
||||
'sanitize_callback' => function ( $flags ) {
|
||||
return array_map( array( $this, 'to_boolean' ), $flags );
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
@ -97,9 +107,10 @@ class LoginLinkRestEndpoint extends RestEndpoint {
|
|||
public function get_login_url( WP_REST_Request $request ) : WP_REST_Response {
|
||||
$use_sandbox = $request->get_param( 'useSandbox' );
|
||||
$products = $request->get_param( 'products' );
|
||||
$flags = (array) $request->get_param( 'options' );
|
||||
|
||||
try {
|
||||
$url = $this->url_generator->generate( $products, $use_sandbox );
|
||||
$url = $this->url_generator->generate( $products, $flags, $use_sandbox );
|
||||
|
||||
return $this->return_success( $url );
|
||||
} catch ( \Exception $e ) {
|
||||
|
|
|
@ -82,12 +82,13 @@ class ConnectionUrlGenerator {
|
|||
*
|
||||
* @param array $products An array of product identifiers to include in the sign-up process.
|
||||
* These determine the PayPal onboarding experience.
|
||||
* @param array $flags Onboarding choices that will customize the ISU payload.
|
||||
* @param bool $use_sandbox Whether to generate a sandbox URL.
|
||||
*
|
||||
* @return string The generated PayPal onboarding URL.
|
||||
*/
|
||||
public function generate( array $products = array(), bool $use_sandbox = false ) : string {
|
||||
$cache_key = $this->cache_key( $products, $use_sandbox );
|
||||
public function generate( array $products = array(), array $flags = array(), bool $use_sandbox = false ) : string {
|
||||
$cache_key = $this->cache_key( $products, $flags, $use_sandbox );
|
||||
$user_id = get_current_user_id();
|
||||
$onboarding_url = $this->url_manager->get( $cache_key, $user_id );
|
||||
$cached_url = $this->try_get_from_cache( $onboarding_url, $cache_key );
|
||||
|
@ -100,7 +101,7 @@ class ConnectionUrlGenerator {
|
|||
|
||||
$this->logger->info( 'Generating onboarding URL for: ' . $cache_key );
|
||||
|
||||
$url = $this->generate_new_url( $use_sandbox, $products, $onboarding_url, $cache_key );
|
||||
$url = $this->generate_new_url( $use_sandbox, $products, $flags, $onboarding_url, $cache_key );
|
||||
|
||||
if ( $url ) {
|
||||
$this->persist_url( $onboarding_url, $url );
|
||||
|
@ -112,18 +113,28 @@ class ConnectionUrlGenerator {
|
|||
/**
|
||||
* Generates a cache key from the environment and sorted product array.
|
||||
*
|
||||
* Q: Why do we cache the connection URL?
|
||||
* A: The URL is generated by a partner-referrals API, i.e. it requires a
|
||||
* remote request; caching the response avoids unnecessary API calls.
|
||||
*
|
||||
* @param array $products Product identifiers that are part of the cache key.
|
||||
* @param array $flags Onboarding flags.
|
||||
* @param bool $for_sandbox Whether the cache contains a sandbox URL.
|
||||
*
|
||||
* @return string The cache key, defining the product list and environment.
|
||||
*/
|
||||
protected function cache_key( array $products, bool $for_sandbox ) : string {
|
||||
protected function cache_key( array $products, array $flags, bool $for_sandbox ) : string {
|
||||
$environment = $for_sandbox ? 'sandbox' : 'production';
|
||||
|
||||
// Sort products alphabetically, to improve cache implementation.
|
||||
sort( $products );
|
||||
|
||||
return $environment . '-' . implode( '-', $products );
|
||||
// Extract the names of active flags.
|
||||
$active_flags = array_keys( array_filter( $flags ) );
|
||||
|
||||
return strtolower(
|
||||
$environment . '-' . implode( '-', $products ) . '-' . implode( '-', $active_flags )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -162,12 +173,13 @@ class ConnectionUrlGenerator {
|
|||
*
|
||||
* @param bool $for_sandbox Whether to generate a sandbox URL.
|
||||
* @param array $products The products array.
|
||||
* @param array $flags Onboarding flags.
|
||||
* @param OnboardingUrl $onboarding_url The OnboardingUrl object.
|
||||
* @param string $cache_key The cache key.
|
||||
*
|
||||
* @return string The generated URL or an empty string on failure.
|
||||
*/
|
||||
protected function generate_new_url( bool $for_sandbox, array $products, OnboardingUrl $onboarding_url, string $cache_key ) : string {
|
||||
protected function generate_new_url( bool $for_sandbox, array $products, array $flags, OnboardingUrl $onboarding_url, string $cache_key ) : string {
|
||||
$query_args = array( 'displayMode' => 'minibrowser' );
|
||||
$onboarding_url->init();
|
||||
|
||||
|
@ -179,7 +191,7 @@ class ConnectionUrlGenerator {
|
|||
return '';
|
||||
}
|
||||
|
||||
$data = $this->prepare_referral_data( $products, $onboarding_token );
|
||||
$data = $this->prepare_referral_data( $products, $flags, $onboarding_token );
|
||||
|
||||
try {
|
||||
$referral = $this->partner_referrals->get_value( $for_sandbox );
|
||||
|
@ -197,16 +209,18 @@ class ConnectionUrlGenerator {
|
|||
* Prepares the referral data.
|
||||
*
|
||||
* @param array $products The products array.
|
||||
* @param array $flags Onboarding flags.
|
||||
* @param string $onboarding_token The onboarding token.
|
||||
*
|
||||
* @return array The prepared referral data.
|
||||
*/
|
||||
protected function prepare_referral_data( array $products, string $onboarding_token ) : array {
|
||||
$data = $this->referrals_data
|
||||
->with_products( $products )
|
||||
->data();
|
||||
|
||||
return $this->referrals_data->append_onboarding_token( $data, $onboarding_token );
|
||||
protected function prepare_referral_data( array $products, array $flags, string $onboarding_token ) : array {
|
||||
return $this->referrals_data->data(
|
||||
$products,
|
||||
$onboarding_token,
|
||||
(bool) ( $flags['useSubscriptions'] ?? false ),
|
||||
(bool) ( $flags['useCardPayments'] ?? false )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -548,6 +548,35 @@ class SettingsModule implements ServiceModule, ExecutableModule {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsets the BCDC black button if merchant is eligible for ACDC.
|
||||
*/
|
||||
add_filter(
|
||||
'woocommerce_paypal_payments_disabled_funding_sources',
|
||||
/**
|
||||
* Unsets the BCDC black button if merchant is eligible for ACDC.
|
||||
*
|
||||
* @param int[]|string[]|mixed $disable_funding The disabled funding sources.
|
||||
* @return int[]|string[]|mixed The disabled funding sources.
|
||||
*
|
||||
* @psalm-suppress MissingClosureParamType
|
||||
*/
|
||||
static function ( $disable_funding ) use ( $container ) {
|
||||
if ( ! is_array( $disable_funding ) || in_array( 'card', $disable_funding, true ) ) {
|
||||
return $disable_funding;
|
||||
}
|
||||
|
||||
$dcc_product_status = $container->get( 'wcgateway.helper.dcc-product-status' );
|
||||
assert( $dcc_product_status instanceof DCCProductStatus );
|
||||
|
||||
if ( $dcc_product_status->is_active() ) {
|
||||
$disable_funding[] = 'card';
|
||||
}
|
||||
|
||||
return $disable_funding;
|
||||
}
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue