module_url = $module_url; $this->version = $version; $this->session_handler = $session_handler; $this->settings = $settings; $this->payer_factory = $payer_factory; $this->client_id = $client_id; $this->request_data = $request_data; $this->dcc_applies = $dcc_applies; $this->subscription_helper = $subscription_helper; $this->messages_apply = $messages_apply; $this->environment = $environment; $this->payment_token_repository = $payment_token_repository; $this->settings_status = $settings_status; $this->currency = $currency; $this->all_funding_sources = $all_funding_sources; $this->basic_checkout_validation_enabled = $basic_checkout_validation_enabled; $this->early_validation_enabled = $early_validation_enabled; $this->pay_now_contexts = $pay_now_contexts; $this->funding_sources_without_redirect = $funding_sources_without_redirect; $this->vault_v3_enabled = $vault_v3_enabled; $this->logger = $logger; $this->payment_tokens_endpoint = $payment_tokens_endpoint; $this->should_handle_shipping_in_paypal = $should_handle_shipping_in_paypal; $this->server_side_shipping_callback_enabled = $server_side_shipping_callback_enabled; $this->appswitch_enabled = $appswitch_enabled; $this->disabled_funding_sources = $disabled_funding_sources; $this->dcc_configuration = $dcc_configuration; $this->partner_attribution = $partner_attribution; $this->final_review_enabled = $final_review_enabled; } /** * Registers the necessary action hooks to render the HTML depending on the settings. * * @return bool */ public function render_wrapper(): bool { $this->init_context(); if ( $this->settings->has( 'enabled' ) && $this->settings->get( 'enabled' ) ) { $this->render_button_wrapper_registrar(); $this->render_message_wrapper_registrar(); } if ( $this->dcc_configuration->is_enabled() ) { $this->render_dcc_wrapper(); } if ( $this->is_free_trial_cart() ) { add_action( 'woocommerce_review_order_after_submit', function () { $vaulted_email = $this->get_vaulted_paypal_email(); if ( ! $vaulted_email ) { return; } ?>
', '' ) ); ?>
sanitize_woocommerce_filters(); return true; } /** * Registers hooks and callbacks that are only relevant for DCC (ACDC) payments. * * @return void */ private function render_dcc_wrapper(): void { add_action( $this->checkout_dcc_button_renderer_hook(), array( $this, 'dcc_renderer' ), 11 ); add_action( $this->pay_order_renderer_hook(), array( $this, 'dcc_renderer' ), 11 ); $subscription_helper = $this->subscription_helper; add_filter( 'woocommerce_credit_card_form_fields', function ( array $default_fields, $id ) use ( $subscription_helper ) : array { if ( CreditCardGateway::ID === $id && is_user_logged_in() && $this->settings->has( 'vault_enabled_dcc' ) && $this->settings->get( 'vault_enabled_dcc' ) && apply_filters( 'woocommerce_paypal_payments_should_render_card_custom_fields', true ) ) { $default_fields['card-vault'] = sprintf( '

', esc_html__( 'Save your Credit Card', 'woocommerce-paypal-payments' ) ); if ( $subscription_helper->cart_contains_subscription() || $subscription_helper->order_pay_contains_subscription() ) { $default_fields['card-vault'] = ''; } $tokens = $this->payment_token_repository->all_for_user_id( get_current_user_id() ); if ( $tokens && $this->payment_token_repository->tokens_contains_card( $tokens ) ) { $output = sprintf( '

'; $default_fields['saved-credit-card'] = $output; } } return $default_fields; }, 10, 2 ); } /** * Registers the hooks to render the credit messaging HTML depending on the settings. * * @return bool */ private function render_message_wrapper_registrar(): bool { if ( ! apply_filters( 'woocommerce_paypal_payments_should_render_pay_later_messaging', true ) ) { return false; } if ( ! $this->settings_status->is_pay_later_messaging_enabled() || ! $this->settings_status->has_pay_later_messaging_locations() ) { return false; } $location = $this->location(); if ( ! $this->settings_status->is_pay_later_messaging_enabled_for_location( $location ) ) { return false; } $default_pay_order_hook = 'woocommerce_pay_order_before_submit'; /** * The filter returning if the current theme is a block theme or not. */ $is_block_theme = (bool) apply_filters( 'woocommerce_paypal_payments_messages_renderer_is_block', wp_is_block_theme() ); $has_paylater_block = ( PayLaterBlockModule::is_block_enabled( $this->settings_status ) && has_block( 'woocommerce-paypal-payments/paylater-messages' ) ) || ( PayLaterWCBlocksModule::is_block_enabled( $this->settings_status, $location ) && ( has_block( 'woocommerce-paypal-payments/checkout-paylater-messages' ) || has_block( 'woocommerce-paypal-payments/cart-paylater-messages' ) ) ); $get_hook = function ( string $location ) use ( $default_pay_order_hook, $is_block_theme, $has_paylater_block ): ?array { switch ( $location ) { case 'checkout': return $this->messages_renderer_hook( $location, 'woocommerce_review_order_before_payment', 10 ); case 'cart': return $this->messages_renderer_hook( $location, $this->proceed_to_checkout_button_renderer_hook(), 19 ); case 'pay-now': return $this->messages_renderer_hook( $location, $default_pay_order_hook, 10 ); case 'product': return $this->messages_renderer_hook( $location, $this->single_product_renderer_hook(), 30 ); case 'shop': return $is_block_theme ? $this->messages_renderer_block( $location, 'core/query-title', 10 ) : $this->messages_renderer_hook( $location, 'woocommerce_archive_description', 10 ); case 'home': return $is_block_theme ? $this->messages_renderer_block( $location, 'core/navigation', 10 ) : $this->messages_renderer_hook( $location, 'loop_start', 20 ); default: return $has_paylater_block ? $this->messages_renderer_hook( $location, 'ppcp_paylater_message_block', 10 ) : null; } }; $hook = $get_hook( $location ); if ( ! $hook ) { return false; } if ( $hook['blockName'] ?? false ) { $this->message_renderer( $hook ); } else { add_action( $hook['name'], array( $this, 'message_renderer' ), $hook['priority'] ); } // Looks like there are no hooks like woocommerce_review_order_before_payment on the pay for order page, so have to move using JS. if ( $location === 'pay-now' && $hook['name'] === $default_pay_order_hook && /** * The filter returning true if Pay Later messages should be displayed before payment methods * on the pay for order page, like in checkout. */ apply_filters( 'woocommerce_paypal_payments_put_pay_order_messages_before_payment_methods', true ) ) { add_action( 'ppcp_after_pay_order_message_wrapper', function () { echo ' '; } ); } return true; } /** * Registers the hooks where to render the button HTML according to the settings. * * @return bool * @throws NotFoundException When a setting was not found. */ private function render_button_wrapper_registrar(): bool { if ( ( is_product() || wc_post_content_has_shortcode( 'product_page' ) ) && $this->settings_status->is_smart_button_enabled_for_location( 'product' ) // TODO: it seems like there is no easy way to properly handle vaulted PayPal free trial, // so disable the buttons for now everywhere except checkout for free trial. && ! $this->is_free_trial_product() && ! is_checkout() ) { add_action( $this->single_product_renderer_hook(), function () { $product = wc_get_product(); if ( is_a( $product, WC_Product::class ) && ! $this->product_supports_payment( $product ) ) { return; } $this->button_renderer( PayPalGateway::ID, 'woocommerce_paypal_payments_single_product_button_render' ); }, 31 ); } if ( $this->settings_status->is_smart_button_enabled_for_location( 'mini-cart' ) && ! $this->is_free_trial_cart() ) { add_action( $this->mini_cart_button_renderer_hook(), function () { if ( $this->is_cart_price_total_zero() || $this->is_free_trial_cart() ) { return; } echo '

'; echo ''; do_action( 'woocommerce_paypal_payments_minicart_button_render' ); echo '

'; }, 30 ); } $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); if ( isset( $available_gateways['ppcp-gateway'] ) ) { add_action( $this->pay_order_renderer_hook(), function (): void { $this->button_renderer( PayPalGateway::ID, 'woocommerce_paypal_payments_payorder_button_render' ); $this->button_renderer( CardButtonGateway::ID ); }, 20 ); add_action( $this->checkout_button_renderer_hook(), function (): void { $this->button_renderer( PayPalGateway::ID, 'woocommerce_paypal_payments_checkout_button_render' ); $this->button_renderer( CardButtonGateway::ID ); } ); $enabled_on_cart = $this->settings_status->is_smart_button_enabled_for_location( 'cart' ); add_action( $this->proceed_to_checkout_button_renderer_hook(), function() use ( $enabled_on_cart ) { if ( ! is_cart() || ! $enabled_on_cart || $this->is_free_trial_cart() || $this->is_cart_price_total_zero() || isset( reset( WC()->cart->cart_contents )['subscription_switch'] ) ) { return; } $this->button_renderer( PayPalGateway::ID, 'woocommerce_paypal_payments_cart_button_render' ); }, 20 ); } return true; } /** * Whether any of our scripts (for DCC or product, mini-cart, non-block cart/checkout) should be loaded. */ public function should_load_ppcp_script(): bool { $pcp_gateway_enabled = $this->settings->has( 'enabled' ) && $this->settings->get( 'enabled' ); if ( ! $pcp_gateway_enabled ) { return false; } return $this->should_load_buttons() || $this->should_load_messages() || $this->can_render_dcc(); } /** * Determines whether the button component should be loaded. */ public function should_load_buttons() : bool { $pcp_gateway_enabled = $this->settings->has( 'enabled' ) && $this->settings->get( 'enabled' ); if ( ! $pcp_gateway_enabled ) { return false; } $smart_button_enabled_for_current_location = $this->settings_status->is_smart_button_enabled_for_location( $this->context() ); $smart_button_enabled_for_mini_cart = $this->settings_status->is_smart_button_enabled_for_location( 'mini-cart' ); switch ( $this->context() ) { case 'checkout': case 'cart': case 'pay-now': case 'checkout-block': case 'cart-block': return $smart_button_enabled_for_current_location; case 'product': return $smart_button_enabled_for_current_location || $smart_button_enabled_for_mini_cart; default: return $smart_button_enabled_for_mini_cart || $this->is_block_editor(); } } /** * Determines whether the Pay Later messages component should be loaded. */ public function should_load_messages() : bool { $pcp_gateway_enabled = $this->settings->has( 'enabled' ) && $this->settings->get( 'enabled' ); if ( ! $pcp_gateway_enabled ) { return false; } if ( ! $this->settings_status->is_pay_later_messaging_enabled() ) { return false; } if ( ! $this->messages_apply->for_country() || $this->is_free_trial_cart() ) { return false; } $location = $this->location(); $messaging_enabled_for_current_location = $this->settings_status->is_pay_later_messaging_enabled_for_location( $location ); $has_paylater_block = PayLaterBlockModule::is_block_enabled( $this->settings_status ) && has_block( 'woocommerce-paypal-payments/paylater-messages' ); if ( 'cart-block' === $location || 'checkout-block' === $location ) { return true; } switch ( $location ) { case 'checkout': case 'cart': case 'pay-now': case 'product': case 'shop': case 'home': return $messaging_enabled_for_current_location; case 'block-editor': return true; default: return $has_paylater_block; } } /** * Whether DCC fields can be rendered. */ public function can_render_dcc() : bool { return $this->dcc_configuration->is_acdc_enabled() && in_array( $this->context(), apply_filters( 'woocommerce_paypal_payments_can_render_dcc_contexts', array( 'checkout', 'pay-now', 'add-payment-method' ) ), true ); } /** * Enqueues our scripts/styles (for DCC and product, mini-cart and non-block cart/checkout) */ public function enqueue(): void { if ( $this->can_render_dcc() ) { wp_enqueue_style( 'ppcp-hosted-fields', untrailingslashit( $this->module_url ) . '/assets/css/hosted-fields.css', array(), $this->version ); } wp_enqueue_style( 'gateway', untrailingslashit( $this->module_url ) . '/assets/css/gateway.css', array(), $this->version ); wp_enqueue_script( 'ppcp-smart-button', untrailingslashit( $this->module_url ) . '/assets/js/button.js', array( 'jquery' ), $this->version, true ); wp_localize_script( 'ppcp-smart-button', 'PayPalCommerceGateway', $this->script_data() ); } /** * Renders the HTML for the buttons. * * @param string $gateway_id The gateway ID, like 'ppcp-gateway'. * @param string|null $action_name The action name to be called. */ public function button_renderer( string $gateway_id, string $action_name = null ) { $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); if ( ! isset( $available_gateways[ $gateway_id ] ) ) { return; } // The wrapper is needed for the loading spinner, // otherwise jQuery block() prevents buttons rendering. echo '
'; $hook_gateway_id = str_replace( '-', '_', $gateway_id ); /** * A hook executed after rendering of the opening tag for the PCP wrapper (before the inner wrapper for the buttons). * * For the PayPal gateway the hook name is ppcp_start_button_wrapper_ppcp_gateway. */ do_action( 'ppcp_start_button_wrapper_' . $hook_gateway_id ); echo '
'; /** * A hook executed before rendering of the closing tag for the PCP wrapper (before the inner wrapper for the buttons). * * For the PayPal gateway the hook name is ppcp_end_button_wrapper_ppcp_gateway. */ do_action( 'ppcp_end_button_wrapper_' . $hook_gateway_id ); if ( null !== $action_name ) { do_action( $action_name ); } echo '
'; } /** * Renders the HTML for the credit messaging. * * @param array|null $block_params If it's to be rendered after a block, contains the block params. * @return void */ public function message_renderer( $block_params = array() ): void { $product = wc_get_product(); $location = $this->location(); $location_hook = $this->location_to_hook( $location ); if ( $location === 'product' && is_a( $product, WC_Product::class ) /** * The filter returning true if PayPal buttons can be rendered, or false otherwise. */ && ! $this->product_supports_payment( $product ) ) { return; } /** * A hook executed before rendering of the PCP Pay Later messages wrapper. */ do_action( "ppcp_before_{$location_hook}_message_wrapper" ); $messages_placeholder = '
'; if ( is_array( $block_params ) && ( $block_params['blockName'] ?? false ) ) { $this->render_after_block( $block_params['blockName'], '
' . $messages_placeholder . '
', $block_params['priority'] ?? 10 ); } else { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo $messages_placeholder; } /** * A hook executed after rendering of the PCP Pay Later messages wrapper. */ do_action( "ppcp_after_{$location_hook}_message_wrapper" ); } /** * Renders content after a given block. * * @param string $name The name of the block to render after. * @param string $content The content to be rendered. * @param int $priority The 'render_block' hook priority. * @return void */ private function render_after_block( string $name, string $content, int $priority = 10 ): void { add_filter( 'render_block', /** * Adds content after a given block. * * @param string $block_content The block content. * @param array|mixed $block_params The block params. * @return string * * @psalm-suppress MissingClosureParamType */ function ( $block_content, $block_params ) use ( $name, $content, $priority ) { if ( is_array( $block_params ) && ( $block_params['blockName'] ?? null ) === $name ) { $block_content .= $content; } return $block_content; }, $priority, 2 ); } /** * The values for the credit messaging. * * @return array */ private function message_values(): array { if ( ! $this->settings_status->is_pay_later_messaging_enabled() ) { return array(); } $location = $this->location(); switch ( $location ) { case 'checkout': case 'checkout-block': case 'pay-now': $placement = 'payment'; break; case 'cart': case 'cart-block': $placement = 'cart'; break; case 'product': $placement = 'product'; break; case 'shop': $placement = 'product-list'; break; case 'home': $placement = 'home'; break; default: $placement = 'payment'; break; } $product = wc_get_product(); $amount = 0; if ( is_a( $product, WC_Product::class ) ) { $amount = wc_get_price_including_tax( $product ); } elseif ( isset( WC()->cart ) ) { $amount = WC()->cart->get_total( 'raw' ); } $styling_per_location = $this->settings->has( 'pay_later_enable_styling_per_messaging_location' ) && $this->settings->get( 'pay_later_enable_styling_per_messaging_location' ); $location = $styling_per_location ? $location : 'general'; // Map checkout-block and cart-block message options to checkout and cart options. switch ( $location ) { case 'checkout-block': $location = 'checkout'; break; case 'cart-block': $location = 'cart'; break; default: break; } $setting_name_prefix = "pay_later_{$location}_message"; $layout = $this->settings->has( "{$setting_name_prefix}_layout" ) ? $this->settings->get( "{$setting_name_prefix}_layout" ) : 'text'; $logo_type = $this->settings->has( "{$setting_name_prefix}_logo" ) ? $this->settings->get( "{$setting_name_prefix}_logo" ) : 'primary'; $logo_position = $this->settings->has( "{$setting_name_prefix}_position" ) ? $this->settings->get( "{$setting_name_prefix}_position" ) : 'left'; $text_color = $this->settings->has( "{$setting_name_prefix}_color" ) ? $this->settings->get( "{$setting_name_prefix}_color" ) : 'black'; $style_color = $this->settings->has( "{$setting_name_prefix}_flex_color" ) ? $this->settings->get( "{$setting_name_prefix}_flex_color" ) : 'blue'; $ratio = $this->settings->has( "{$setting_name_prefix}_flex_ratio" ) ? $this->settings->get( "{$setting_name_prefix}_flex_ratio" ) : '1x1'; $text_size = $this->settings->has( "{$setting_name_prefix}_text_size" ) ? $this->settings->get( "{$setting_name_prefix}_text_size" ) : '12'; return array( 'wrapper' => '.ppcp-messages', 'is_hidden' => ! $this->is_pay_later_filter_enabled_for_location( $this->context() ), 'block' => array( 'enabled' => PayLaterBlockModule::is_block_enabled( $this->settings_status ), ), 'amount' => $amount, 'placement' => $placement, 'style' => array( 'layout' => $layout, 'logo' => array( 'type' => $logo_type, 'position' => $logo_position, ), 'text' => array( 'color' => $text_color, 'size' => $text_size, ), 'color' => $style_color, 'ratio' => $ratio, ), ); } /** * Renders the HTML for the DCC fields. */ public function dcc_renderer() { if ( ! $this->can_render_dcc() ) { return; } /** * The WC filter returning the WC order button text. * phpcs:disable WordPress.WP.I18n.TextDomainMismatch */ $label = 'checkout' === $this->context() ? apply_filters( 'woocommerce_order_button_text', __( 'Place order', 'woocommerce' ) ) : __( 'Pay for order', 'woocommerce' ); // phpcs:enable WordPress.WP.I18n.TextDomainMismatch printf( '
', esc_html( $label ) ); } /** * Whether we can store vault tokens or not. * * @return bool */ public function can_save_vault_token(): bool { if ( ! $this->settings->has( 'client_id' ) || ! $this->settings->get( 'client_id' ) ) { return false; } if ( ! $this->settings->has( 'vault_enabled' ) || ! $this->settings->get( 'vault_enabled' ) ) { return false; } return true; } /** * Whether we need to initialize the script to enable tokenization for subscriptions or not. * * @return bool */ private function has_subscriptions(): bool { if ( ! $this->subscription_helper->plugin_is_active() ) { return false; } if ( $this->subscription_helper->accept_manual_renewals() && $this->paypal_subscriptions_enabled() !== true ) { return false; } if ( is_product() ) { return $this->subscription_helper->current_product_is_subscription(); } if ( is_wc_endpoint_url( 'order-pay' ) ) { return $this->subscription_helper->order_pay_contains_subscription(); } return $this->subscription_helper->cart_contains_subscription(); } /** * Whether PayPal subscriptions is enabled or not. * * @return bool */ private function paypal_subscriptions_enabled(): bool { try { $subscriptions_mode = $this->settings->get( 'subscriptions_mode' ); } catch ( NotFoundException $exception ) { return false; } return $subscriptions_mode === 'subscriptions_api'; } /** * Retrieves the 3D Secure contingency settings. * * @return string */ private function get_3ds_contingency(): string { if ( $this->settings->has( '3d_secure_contingency' ) ) { $value = $this->settings->get( '3d_secure_contingency' ); if ( $value ) { return $this->return_3ds_contingency( $value ); } } return $this->return_3ds_contingency( 'SCA_WHEN_REQUIRED' ); } /** * Processes and returns the 3D Secure contingency. * * @param string $contingency The ThreeD secure contingency. * @return string */ private function return_3ds_contingency( string $contingency ): string { return apply_filters( 'woocommerce_paypal_payments_three_d_secure_contingency', $contingency ); } /** * Whether the current cart contains a product that requires physical shipping. * * @return bool True, if any cart item requires shipping. */ private function need_shipping() : bool { /** * Cart instance; might be null, esp. in customizer or in Block Editor. * * @var null|WC_Cart $cart */ $cart = WC()->cart; return $cart && $cart->needs_shipping(); } /** * The configuration for the smart buttons. * * @return array */ public function script_data(): array { $is_free_trial_cart = $this->is_free_trial_cart(); $is_acdc_enabled = $this->dcc_configuration->is_acdc_enabled(); $url_params = $this->url_params(); $this->request_data->enqueue_nonce_fix(); $localize = array( 'url' => add_query_arg( $url_params, 'https://www.paypal.com/sdk/js' ), 'url_params' => $url_params, 'script_attributes' => $this->attributes(), 'client_id' => $this->client_id, 'currency' => $this->currency->get(), 'data_client_id' => array( 'set_attribute' => ( is_checkout() && $is_acdc_enabled ) || $this->can_save_vault_token(), 'endpoint' => \WC_AJAX::get_endpoint( DataClientIdEndpoint::ENDPOINT ), 'nonce' => wp_create_nonce( DataClientIdEndpoint::nonce() ), 'user' => get_current_user_id(), 'has_subscriptions' => $this->has_subscriptions(), 'paypal_subscriptions_enabled' => $this->paypal_subscriptions_enabled(), ), 'redirect' => wc_get_checkout_url(), 'context' => $this->context(), 'ajax' => array( 'simulate_cart' => array( 'endpoint' => \WC_AJAX::get_endpoint( SimulateCartEndpoint::ENDPOINT ), 'nonce' => wp_create_nonce( SimulateCartEndpoint::nonce() ), ), 'change_cart' => array( 'endpoint' => \WC_AJAX::get_endpoint( ChangeCartEndpoint::ENDPOINT ), 'nonce' => wp_create_nonce( ChangeCartEndpoint::nonce() ), ), 'create_order' => array( 'endpoint' => \WC_AJAX::get_endpoint( CreateOrderEndpoint::ENDPOINT ), 'nonce' => wp_create_nonce( CreateOrderEndpoint::nonce() ), ), 'approve_order' => array( 'endpoint' => \WC_AJAX::get_endpoint( ApproveOrderEndpoint::ENDPOINT ), 'nonce' => wp_create_nonce( ApproveOrderEndpoint::nonce() ), ), 'get_order' => array( 'endpoint' => \WC_AJAX::get_endpoint( GetOrderEndpoint::ENDPOINT ), 'nonce' => wp_create_nonce( GetOrderEndpoint::nonce() ), ), 'approve_subscription' => array( 'endpoint' => \WC_AJAX::get_endpoint( ApproveSubscriptionEndpoint::ENDPOINT ), 'nonce' => wp_create_nonce( ApproveSubscriptionEndpoint::nonce() ), ), 'vault_paypal' => array( 'endpoint' => \WC_AJAX::get_endpoint( StartPayPalVaultingEndpoint::ENDPOINT ), 'nonce' => wp_create_nonce( StartPayPalVaultingEndpoint::nonce() ), ), 'save_checkout_form' => array( 'endpoint' => \WC_AJAX::get_endpoint( SaveCheckoutFormEndpoint::ENDPOINT ), 'nonce' => wp_create_nonce( SaveCheckoutFormEndpoint::nonce() ), ), 'validate_checkout' => array( 'endpoint' => \WC_AJAX::get_endpoint( ValidateCheckoutEndpoint::ENDPOINT ), 'nonce' => wp_create_nonce( ValidateCheckoutEndpoint::nonce() ), ), 'cart_script_params' => array( 'endpoint' => \WC_AJAX::get_endpoint( CartScriptParamsEndpoint::ENDPOINT ), ), 'create_setup_token' => array( 'endpoint' => \WC_AJAX::get_endpoint( CreateSetupToken::ENDPOINT ), 'nonce' => wp_create_nonce( CreateSetupToken::nonce() ), ), 'create_payment_token' => array( 'endpoint' => \WC_AJAX::get_endpoint( CreatePaymentToken::ENDPOINT ), 'nonce' => wp_create_nonce( CreatePaymentToken::nonce() ), ), 'create_payment_token_for_guest' => array( 'endpoint' => \WC_AJAX::get_endpoint( CreatePaymentTokenForGuest::ENDPOINT ), 'nonce' => wp_create_nonce( CreatePaymentTokenForGuest::nonce() ), ), 'update_shipping' => array( 'endpoint' => \WC_AJAX::get_endpoint( UpdateShippingEndpoint::ENDPOINT ), 'nonce' => wp_create_nonce( UpdateShippingEndpoint::nonce() ), ), 'update_customer_shipping' => array( 'shipping_options' => array( 'endpoint' => home_url( UpdateShippingEndpoint::WC_STORE_API_ENDPOINT . 'select-shipping-rate' ), ), 'shipping_address' => array( 'cart_endpoint' => home_url( UpdateShippingEndpoint::WC_STORE_API_ENDPOINT ), 'update_customer_endpoint' => home_url( UpdateShippingEndpoint::WC_STORE_API_ENDPOINT . 'update-customer' ), ), 'wp_rest_nonce' => wp_create_nonce( 'wc_store_api' ), 'update_shipping_method' => \WC_AJAX::get_endpoint( 'update_shipping_method' ), ), ), 'cart_contains_subscription' => $this->subscription_helper->cart_contains_subscription(), 'subscription_plan_id' => $this->subscription_helper->paypal_subscription_id(), 'vault_v3_enabled' => $this->vault_v3_enabled, 'variable_paypal_subscription_variations' => $this->subscription_helper->variable_paypal_subscription_variations(), 'variable_paypal_subscription_variation_from_cart' => $this->subscription_helper->paypal_subscription_variation_from_cart(), 'subscription_product_allowed' => $this->subscription_helper->checkout_subscription_product_allowed(), 'locations_with_subscription_product' => $this->subscription_helper->locations_with_subscription_product(), 'enforce_vault' => $this->has_subscriptions(), 'can_save_vault_token' => $this->can_save_vault_token(), 'is_free_trial_cart' => $is_free_trial_cart, 'vaulted_paypal_email' => ( is_checkout() && $is_free_trial_cart ) ? $this->get_vaulted_paypal_email() : '', 'bn_codes' => $this->bn_codes(), 'payer' => $this->payerData(), 'button' => array( 'wrapper' => '#ppc-button-' . PayPalGateway::ID, 'is_disabled' => $this->is_button_disabled(), 'mini_cart_wrapper' => '#ppc-button-minicart', 'is_mini_cart_disabled' => $this->is_button_disabled( 'mini-cart' ), 'cancel_wrapper' => '#ppcp-cancel', 'mini_cart_style' => $this->normalize_style( array( 'layout' => $this->style_for_context( 'layout', 'mini-cart' ), 'color' => $this->style_for_context( 'color', 'mini-cart' ), 'shape' => $this->style_for_context( 'shape', 'mini-cart' ), 'label' => $this->style_for_context( 'label', 'mini-cart' ), 'tagline' => $this->style_for_context( 'tagline', 'mini-cart' ), 'height' => $this->normalize_height( $this->style_for_context( 'height', 'mini-cart', 35 ), 25, 55 ), ) ), 'style' => $this->normalize_style( array( 'layout' => $this->style_for_context( 'layout', $this->context() ), 'color' => $this->style_for_context( 'color', $this->context() ), 'shape' => $this->style_for_context( 'shape', $this->context() ), 'label' => $this->style_for_context( 'label', $this->context() ), 'tagline' => $this->style_for_context( 'tagline', $this->context() ), 'height' => in_array( $this->context(), array( 'cart-block', 'checkout-block' ), true ) ? $this->normalize_height( $this->style_for_context( 'height', $this->context(), 48 ), 40, 55 ) : null, ) ), ), 'separate_buttons' => array( 'card' => array( 'id' => CardButtonGateway::ID, 'wrapper' => '#ppc-button-' . CardButtonGateway::ID, 'style' => $this->normalize_style( array( 'shape' => $this->style_for_apm( 'shape', 'card' ), 'color' => $this->style_for_apm( 'color', 'card', 'black' ), 'layout' => $this->style_for_apm( 'poweredby_tagline', 'card', false ) === $this->normalize_style_value( true ) ? 'vertical' : 'horizontal', ) ), ), ), 'hosted_fields' => array( 'wrapper' => '#ppcp-hosted-fields', 'labels' => array( 'credit_card_number' => '', 'cvv' => '', 'mm_yy' => __( 'MM/YY', 'woocommerce-paypal-payments' ), 'fields_empty' => __( 'Card payment details are missing. Please fill in all required fields.', 'woocommerce-paypal-payments' ), 'fields_not_valid' => __( 'Unfortunately, your credit card details are not valid.', 'woocommerce-paypal-payments' ), 'card_not_supported' => __( 'Unfortunately, we do not support your credit card.', 'woocommerce-paypal-payments' ), 'cardholder_name_required' => __( 'Cardholder\'s first and last name are required, please fill the checkout form required fields.', 'woocommerce-paypal-payments' ), ), 'valid_cards' => $this->dcc_applies->valid_cards(), 'contingency' => $this->get_3ds_contingency(), ), 'messages' => $this->message_values(), 'labels' => array( 'error' => array( 'generic' => __( 'Something went wrong. Please try again or choose another payment source.', 'woocommerce-paypal-payments' ), 'required' => array( 'generic' => __( 'Required form fields are not filled.', 'woocommerce-paypal-payments' ), // phpcs:ignore WordPress.WP.I18n 'field' => __( '%s is a required field.', 'woocommerce' ), 'elements' => array( // Map
=> text for error messages. 'terms' => __( 'Please read and accept the terms and conditions to proceed with your order.', // phpcs:ignore WordPress.WP.I18n.TextDomainMismatch 'woocommerce' ), ), ), ), // phpcs:ignore WordPress.WP.I18n 'billing_field' => _x( 'Billing %s', 'checkout-validation', 'woocommerce' ), // phpcs:ignore WordPress.WP.I18n 'shipping_field' => _x( 'Shipping %s', 'checkout-validation', 'woocommerce' ), ), 'simulate_cart' => array( 'enabled' => apply_filters( 'woocommerce_paypal_payments_simulate_cart_enabled', true ), 'throttling' => apply_filters( 'woocommerce_paypal_payments_simulate_cart_throttling', 5000 ), ), 'order_id' => 'pay-now' === $this->context() ? $this->get_order_pay_id() : 0, 'single_product_buttons_enabled' => $this->settings_status->is_smart_button_enabled_for_location( 'product' ), 'mini_cart_buttons_enabled' => $this->settings_status->is_smart_button_enabled_for_location( 'mini-cart' ), 'basic_checkout_validation_enabled' => $this->basic_checkout_validation_enabled, 'early_checkout_validation_enabled' => $this->early_validation_enabled, 'funding_sources_without_redirect' => $this->funding_sources_without_redirect, 'user' => array( 'is_logged' => is_user_logged_in(), 'has_wc_card_payment_tokens' => $this->user_has_wc_card_payment_tokens( get_current_user_id() ), ), 'should_handle_shipping_in_paypal' => $this->should_handle_shipping_in_paypal && ! $this->is_checkout(), 'server_side_shipping_callback' => array( 'enabled' => $this->server_side_shipping_callback_enabled, ), 'appswitch' => array( 'enabled' => $this->appswitch_enabled, ), 'needShipping' => $this->need_shipping(), 'vaultingEnabled' => $this->settings->has( 'vault_enabled' ) && $this->settings->get( 'vault_enabled' ), 'productType' => null, 'manualRenewalEnabled' => $this->subscription_helper->accept_manual_renewals(), 'final_review_enabled' => $this->final_review_enabled, ); if ( is_product() ) { $product = wc_get_product( get_the_ID() ); if ( is_a( $product, \WC_Product::class ) ) { $localize['productType'] = $product->get_type(); } } if ( 'pay-now' === $this->context() ) { $localize['pay_now'] = $this->pay_now_script_data(); } if ( $this->is_paypal_continuation() ) { $order = $this->session_handler->order(); assert( $order !== null ); $localize['continuation'] = array( 'order_id' => $order->id(), ); } $this->request_data->dequeue_nonce_fix(); return apply_filters( 'woocommerce_paypal_payments_localized_script_data', $localize ); } /** * Returns pay-now payment data. * * @return array */ private function pay_now_script_data(): array { $order_id = $this->get_order_pay_id(); $base_location = wc_get_base_location(); $shop_country_code = $base_location['country'] ?? ''; $currency_code = get_woocommerce_currency(); $wc_order = wc_get_order( $order_id ); if ( ! $wc_order instanceof WC_Order ) { return array(); } $total = (float) $wc_order->get_total( 'numeric' ); return array( 'total' => $total, 'total_str' => ( new Money( $total, $currency_code ) )->value_str(), 'currency_code' => $currency_code, 'country_code' => $shop_country_code, ); } /** * If we can find the payer data for a current customer, we will return it. * * @return array|null */ private function payerData() { $customer = WC()->customer; if ( ! is_user_logged_in() || ! ( $customer instanceof \WC_Customer ) ) { return null; } return $this->payer_factory->from_customer( $customer )->to_array(); } /** * The JavaScript SDK url parameters. * * @return array */ private function url_params(): array { $context = $this->context(); try { $intent = $this->intent(); } catch ( NotFoundException $exception ) { $intent = 'capture'; } $subscription_mode = $this->settings->has( 'subscriptions_mode' ) ? $this->settings->get( 'subscriptions_mode' ) : ''; $params = array( 'client-id' => $this->client_id, 'currency' => $this->currency->get(), 'integration-date' => PAYPAL_INTEGRATION_DATE, 'components' => implode( ',', $this->components() ), 'vault' => ( $this->can_save_vault_token() || $this->subscription_helper->need_subscription_intent( $subscription_mode ) ) ? 'true' : 'false', 'commit' => in_array( $context, $this->pay_now_contexts, true ) ? 'true' : 'false', 'intent' => $intent, ); if ( $this->settings->has( 'subscriptions_mode' ) && $this->settings->get( 'subscriptions_mode' ) === 'vaulting_api' && apply_filters( 'woocommerce_paypal_payments_save_payment_methods_eligible', false ) ) { // Remove vault parameter to allow for Venmo with Save Payment Methods (Vault V3). unset( $params['vault'] ); } if ( $this->environment->current_environment_is( Environment::SANDBOX ) && defined( 'WP_DEBUG' ) && \WP_DEBUG && WC()->customer instanceof \WC_Customer && WC()->customer->get_billing_country() && 2 === strlen( WC()->customer->get_billing_country() ) ) { $params['buyer-country'] = WC()->customer->get_billing_country(); } if ( 'pay-now' === $this->context() ) { $wc_order_id = $this->get_order_pay_id(); if ( $wc_order_id ) { $wc_order = wc_get_order( $wc_order_id ); if ( $wc_order instanceof WC_Order ) { $currency = $wc_order->get_currency(); if ( $currency ) { $params['currency'] = $currency; } } } } $disabled_funding_sources = $this->disabled_funding_sources->sources( $context ); $enable_funding = array( 'venmo' ); if ( $this->is_pay_later_button_enabled_for_location( $context ) ) { $enable_funding[] = 'paylater'; } else { $disabled_funding_sources[] = 'paylater'; } if ( count( $disabled_funding_sources ) > 0 ) { $params['disable-funding'] = implode( ',', array_unique( $disabled_funding_sources ) ); } if ( $this->is_free_trial_cart() ) { $enable_funding = array(); } if ( count( $enable_funding ) > 0 ) { $params['enable-funding'] = implode( ',', array_unique( $enable_funding ) ); } $locale = $this->settings->has( 'smart_button_language' ) ? $this->settings->get( 'smart_button_language' ) : ''; $locale = (string) apply_filters( 'woocommerce_paypal_payments_smart_buttons_locale', $locale ); if ( $locale ) { $params['locale'] = $locale; } return $params; } /** * The attributes we need to load for the JS SDK. * * @return array */ private function attributes(): array { $attributes = array( 'data-partner-attribution-id' => $this->bn_code_for_context( $this->context() ), ); $page_type_attribute = $this->page_type_attribute(); if ( $page_type_attribute ) { $attributes['data-page-type'] = $page_type_attribute; } return $attributes; } /** * Retrieves the value for page type attribute(data-page-type) used for the JS SDK. * * @return string */ protected function page_type_attribute(): string { if ( is_search() ) { return 'search-results'; } switch ( $this->location() ) { case 'product': return 'product-details'; case 'shop': return 'product-listing'; case 'home': return 'mini-cart'; case 'cart-block': case 'cart': return 'cart'; case 'checkout-block': case 'checkout': case 'pay-now': return 'checkout'; default: return ''; } } /** * What BN Code to use in a given context. * * @param string $context The context. * @return string */ private function bn_code_for_context( string $context ): string { $codes = $this->bn_codes(); return ( isset( $codes[ $context ] ) ) ? $codes[ $context ] : $this->partner_attribution->get_bn_code(); } /** * BN Codes to use. * * @return array */ private function bn_codes() : array { $bn_code = $this->partner_attribution->get_bn_code(); return array( 'checkout' => $bn_code, 'cart' => $bn_code, 'mini-cart' => $bn_code, 'product' => $bn_code, ); } /** * The JS SKD components we need to load. * * @return array */ private function components(): array { $components = array(); if ( $this->should_load_buttons() ) { $components[] = 'buttons'; $components[] = 'funding-eligibility'; } if ( $this->should_load_messages() ) { $components[] = 'messages'; } // Card payments are only available on a checkout page. if ( is_checkout() && $this->dcc_configuration->is_bcdc_enabled() ) { $components[] = 'hosted-fields'; } /** * Filter to add further components from the extensions. * * @internal Matches filter name in APM extension. * * @param array $components The array of components already registered. * @param string $context The SmartButton context. */ return array_unique( (array) apply_filters( 'woocommerce_paypal_payments_sdk_components_hook', $components, $this->context() ) ); } /** * Determines the style for a given property in a given context. * * @param string $style The name of the style property. * @param string $context The context. * @param ?mixed $default The default value. * * @return string|int */ private function style_for_context( string $style, string $context, $default = null ) { if ( $context === 'checkout-block' ) { $context = 'checkout-block-express'; } $defaults = array( 'layout' => 'vertical', 'size' => 'responsive', 'color' => 'gold', 'shape' => 'pill', 'label' => 'paypal', 'tagline' => true, ); $enable_styling_per_location = $this->settings->has( 'smart_button_enable_styling_per_location' ) && $this->settings->get( 'smart_button_enable_styling_per_location' ); if ( ! $enable_styling_per_location ) { $context = 'general'; } return $this->get_style_value( "button_{$context}_{$style}" ) ?? $this->get_style_value( "button_{$style}" ) ?? ( $default ? $this->normalize_style_value( $default ) : null ) ?? $this->normalize_style_value( $defaults[ $style ] ?? '' ); } /** * Determines the style for a given property in a given APM. * * @param string $style The name of the style property. * @param string $apm The APM name, such as 'card'. * @param ?mixed $default The default value. * * @return string|int */ private function style_for_apm( string $style, string $apm, $default = null ) { return $this->get_style_value( "{$apm}_button_{$style}" ) ?? ( $default ? $this->normalize_style_value( $default ) : null ) ?? $this->style_for_context( $style, 'checkout' ); } /** * Returns the style property value or null. * * @param string $key The style property key in the settings. * @return string|int|null */ private function get_style_value( string $key ) { if ( ! $this->settings->has( $key ) ) { return null; } return $this->normalize_style_value( $this->settings->get( $key ) ); } /** * Converts the style property value to string. * * @param mixed $value The style property value. * @return string|int */ private function normalize_style_value( $value ) { if ( is_bool( $value ) ) { $value = $value ? 'true' : 'false'; } if ( is_int( $value ) ) { return $value; } return (string) $value; } /** * Fixes the style. * * @param array $style The style properties. * @return array */ private function normalize_style( array $style ): array { if ( array_key_exists( 'tagline', $style ) && ( ! array_key_exists( 'layout', $style ) || $style['layout'] !== 'horizontal' ) ) { $style['tagline'] = false; } if ( array_key_exists( 'height', $style ) && ! $style['height'] ) { unset( $style['height'] ); } return $style; } /** * Returns a number between min and max. * * @param mixed $height The input value. * @param int $min The minimum value. * @param int $max The maximum value. * @return int The normalized output value. */ private function normalize_height( $height, int $min, int $max ): int { $height = (int) $height; if ( $height < $min ) { return $min; } if ( $height > $max ) { return $max; } return $height; } /** * Returns the action name that PayPal button will use for rendering on the checkout page. * * @return string Action name. */ private function checkout_button_renderer_hook(): string { /** * The filter returning the action name that PayPal button will use for rendering on the checkout page. */ return (string) apply_filters( 'woocommerce_paypal_payments_checkout_button_renderer_hook', 'woocommerce_review_order_after_payment' ); } /** * Returns the action name that PayPal DCC button will use for rendering on the checkout page. * * @return string */ private function checkout_dcc_button_renderer_hook(): string { /** * The filter returning the action name that PayPal DCC button will use for rendering on the checkout page. */ return (string) apply_filters( 'woocommerce_paypal_payments_checkout_dcc_renderer_hook', 'woocommerce_review_order_after_submit' ); } /** * Returns the action name that PayPal button and Pay Later message will use for rendering on the pay-order page. * * @return string */ private function pay_order_renderer_hook(): string { /** * The filter returning the action name that PayPal button and Pay Later message will use for rendering on the pay-order page. */ return (string) apply_filters( 'woocommerce_paypal_payments_pay_order_dcc_renderer_hook', 'woocommerce_pay_order_after_submit' ); } /** * Returns the action name that will be used for rendering Pay Later messages. * * @param string $location The location name like 'checkout', 'shop'. See render_message_wrapper_registrar. * @param string $default_hook The default name of the hook. * @param int $default_priority The default priority of the hook. * @return array An array with 'name' and 'priority' keys. */ private function messages_renderer_hook( string $location, string $default_hook, int $default_priority ): array { $location_hook = $this->location_to_hook( $location ); /** * The filter returning the action name that will be used for rendering Pay Later messages. */ $hook = (string) apply_filters( "woocommerce_paypal_payments_{$location_hook}_messages_renderer_hook", $default_hook ); /** * The filter returning the action priority that will be used for rendering Pay Later messages. */ $priority = (int) apply_filters( "woocommerce_paypal_payments_{$location_hook}_messages_renderer_priority", $default_priority ); return array( 'name' => $hook, 'priority' => $priority, ); } /** * Returns the block name that will be used for rendering Pay Later messages after. * * @param string $location The location name like 'checkout', 'shop'. See render_message_wrapper_registrar. * @param string $default_block The default name of the block. * @param int $default_priority The default priority of the 'render_block' hook. * @return array An array with 'blockName' and 'priority' keys. */ private function messages_renderer_block( string $location, string $default_block, int $default_priority ): array { $location_hook = $this->location_to_hook( $location ); /** * The filter returning the action name that will be used for rendering Pay Later messages. */ $block_name = (string) apply_filters( "woocommerce_paypal_payments_{$location_hook}_messages_renderer_block", $default_block ); /** * The filter returning the action priority that will be used for rendering Pay Later messages. */ $priority = (int) apply_filters( "woocommerce_paypal_payments_{$location_hook}_messages_renderer_block_priority", $default_priority ); return array( 'blockName' => $block_name, 'priority' => $priority, ); } /** * Returns action name that PayPal button will use for rendering next to Proceed to checkout button (normally displayed in cart). * * @return string */ private function proceed_to_checkout_button_renderer_hook(): string { /** * The filter returning the action name that PayPal button will use for rendering next to Proceed to checkout button (normally displayed in cart). */ return (string) apply_filters( 'woocommerce_paypal_payments_proceed_to_checkout_button_renderer_hook', 'woocommerce_proceed_to_checkout' ); } /** * Returns the action name that PayPal button will use for rendering in the WC mini cart. * * @return string */ private function mini_cart_button_renderer_hook(): string { /** * The filter returning the action name that PayPal button will use for rendering in the WC mini cart. */ return (string) apply_filters( 'woocommerce_paypal_payments_mini_cart_button_renderer_hook', 'woocommerce_widget_shopping_cart_after_buttons' ); } /** * Returns the action name that PayPal button and Pay Later message will use for rendering on the single product page. * * @return string */ private function single_product_renderer_hook(): string { /** * The filter returning the action name that PayPal button and Pay Later message will use for rendering on the single product page. */ return (string) apply_filters( 'woocommerce_paypal_payments_single_product_renderer_hook', 'woocommerce_single_product_summary' ); } /** * Check if cart product price total is 0. * * @return bool true if is 0, otherwise false. * @psalm-suppress RedundantConditionGivenDocblockType */ protected function is_cart_price_total_zero(): bool { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison return WC()->cart && WC()->cart->get_total( 'numeric' ) == 0; } /** * Checks if PayPal buttons/messages can be rendered for the given product. * * @param WC_Product $product The product. * * @return bool */ protected function product_supports_payment( WC_Product $product ): bool { /** * The filter returning true if PayPal buttons/messages can be rendered for this product, or false otherwise. */ $in_stock = $product->is_in_stock(); if ( ! $in_stock && $product->is_type( 'variable' ) ) { /** * The method is defined in WC_Product_Variable class. * * @psalm-suppress UndefinedMethod */ $variations = $product->get_available_variations( 'objects' ); $in_stock = $this->has_in_stock_variation( $variations ); } $enable_button = ! $product->is_type( array( 'external', 'grouped' ) ) && $in_stock && ! ( ( $product->is_type( 'subscription' ) || $product->is_type( 'variable-subscription' ) ) && ! empty( $_GET['switch-subscription'] ) ); /** * Allows to filter if PayPal buttons/messages can be rendered for the given product. */ return apply_filters( 'woocommerce_paypal_payments_product_supports_payment_request_button', $enable_button, $product ); } /** * Fills and returns the product context_data array to be used in filters. * * @param array $context_data The context data for this filter. * @return array */ private function product_filter_context_data( array $context_data = array() ): array { if ( ! isset( $context_data['product'] ) ) { $context_data['product'] = wc_get_product(); } if ( ! $context_data['product'] ) { return array(); } if ( ! isset( $context_data['order_total'] ) && ( $context_data['product'] instanceof WC_Product ) ) { $context_data['order_total'] = (float) $context_data['product']->get_price( 'raw' ); } return $context_data; } /** * Checks if PayPal buttons/messages should be rendered for the current page. * * @param string|null $context The context that should be checked, use default otherwise. * @param array $context_data The context data for this filter. * @return bool */ public function is_button_disabled( string $context = null, array $context_data = array() ): bool { if ( null === $context ) { $context = $this->context(); } if ( 'product' === $context ) { /** * Allows to decide if the button should be disabled for a given product. */ return apply_filters( 'woocommerce_paypal_payments_product_buttons_disabled', false, $this->product_filter_context_data( $context_data ) ); } /** * Allows to decide if the button should be disabled globally or on a given context. */ return apply_filters( 'woocommerce_paypal_payments_buttons_disabled', false, $context ); } /** * Checks a filter if pay_later/messages should be rendered on a given location / context. * * @param string $location The location. * @param array $context_data The context data for this filter. * @return bool */ private function is_pay_later_filter_enabled_for_location( string $location, array $context_data = array() ): bool { if ( 'product' === $location ) { /** * Allows to decide if the button should be disabled for a given product. */ return ! apply_filters( 'woocommerce_paypal_payments_product_buttons_paylater_disabled', false, $this->product_filter_context_data( $context_data ) ); } /** * Allows to decide if the button should be disabled on a given context. */ return ! apply_filters( 'woocommerce_paypal_payments_buttons_paylater_disabled', false, $location ); } /** * Check whether Pay Later button is enabled for a given location. * * @param string $location The location. * @param array $context_data The context data for this filter. * @return bool true if is enabled, otherwise false. */ public function is_pay_later_button_enabled_for_location( string $location, array $context_data = array() ): bool { return $this->is_pay_later_filter_enabled_for_location( $location, $context_data ) && $this->settings_status->is_pay_later_button_enabled_for_location( $location ); } /** * Check whether Pay Later message is enabled for a given location. * * @param string $location The location setting name. * @param array $context_data The context data for this filter. * @return bool true if is enabled, otherwise false. */ public function is_pay_later_messaging_enabled_for_location( string $location, array $context_data = array() ): bool { return $this->is_pay_later_filter_enabled_for_location( $location, $context_data ) && $this->settings_status->is_pay_later_messaging_enabled_for_location( $location ); } /** * Retrieves all payment tokens for the user, via API or cached if already queried. * * @return PaymentToken[] */ private function get_payment_tokens(): array { if ( null === $this->payment_tokens ) { $this->payment_tokens = $this->payment_token_repository->all_for_user_id( get_current_user_id() ); } return $this->payment_tokens; } /** * Returns the vaulted PayPal email or empty string. * * @return string */ private function get_vaulted_paypal_email(): string { try { $customer_id = get_user_meta( get_current_user_id(), '_ppcp_target_customer_id', true ); if ( $customer_id ) { $customer_tokens = $this->payment_tokens_endpoint->payment_tokens_for_customer( $customer_id ); foreach ( $customer_tokens as $token ) { $email_address = $token['payment_source']->properties()->email_address ?? ''; if ( $email_address ) { return $email_address; } } } $tokens = $this->get_payment_tokens(); foreach ( $tokens as $token ) { if ( isset( $token->source()->paypal ) ) { return $token->source()->paypal->payer->email_address; } } } catch ( Exception $exception ) { $this->logger->error( 'Failed to get PayPal vaulted email. ' . $exception->getMessage() ); } return ''; } /** * Checks if variations contain any in stock variation. * * @param WC_Product_Variation[] $variations The list of variations. * @return bool True if any in stock variation, false otherwise. */ protected function has_in_stock_variation( array $variations ): bool { foreach ( $variations as $variation ) { if ( $variation->is_in_stock() ) { return true; } } return false; } /** * Returns the intent. * * @return string * @throws NotFoundException If intent is not found. */ private function intent(): string { $subscription_mode = $this->settings->has( 'subscriptions_mode' ) ? $this->settings->get( 'subscriptions_mode' ) : ''; if ( $this->subscription_helper->need_subscription_intent( $subscription_mode ) ) { return 'subscription'; } $intent = $this->settings->has( 'intent' ) ? $this->settings->get( 'intent' ) : 'capture'; return strtolower( apply_filters( 'woocommerce_paypal_payments_order_intent', $intent ) ); } /** * Returns the ID of WC order on the order-pay page, or 0. * * @return int */ protected function get_order_pay_id(): int { global $wp; if ( ! isset( $wp->query_vars['order-pay'] ) ) { return 0; } return absint( $wp->query_vars['order-pay'] ); } /** * Sanitize woocommerce filter on unexpected states. * * @return void */ private function sanitize_woocommerce_filters(): void { add_filter( 'woocommerce_widget_cart_is_hidden', /** * Sometimes external plugins like "woocommerce-one-page-checkout" set the $value to null, handle that case here. * Here we also disable the mini-cart on cart-block and checkout-block pages where our buttons aren't supported yet. * * @psalm-suppress MissingClosureParamType */ function ( $value ) { if ( null === $value ) { if ( is_product() ) { return false; } return in_array( $this->context(), array( 'cart', 'checkout', 'cart-block', 'checkout-block' ), true ); } return in_array( $this->context(), array( 'cart-block', 'checkout-block' ), true ) ? true : $value; }, 11 ); } /** * Converts the location name into the name used in hooks. * * @param string $location The location. * @return string */ private function location_to_hook( string $location ): string { switch ( $location ) { case 'pay-now': return 'pay_order'; default: return $location; } } /** * Whether the given user has WC card payment tokens. * * @param int $user_id The user ID. * @return bool */ private function user_has_wc_card_payment_tokens( int $user_id ): bool { $tokens = WC_Payment_Tokens::get_customer_tokens( $user_id, CreditCardGateway::ID ); if ( $tokens ) { return true; } return false; } }