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->logger = $logger; } /** * Registers the necessary action hooks to render the HTML depending on the settings. * * @return bool */ public function render_wrapper(): bool { if ( $this->settings->has( 'enabled' ) && $this->settings->get( 'enabled' ) ) { $this->render_button_wrapper_registrar(); $this->render_message_wrapper_registrar(); } if ( $this->settings->has( 'dcc_enabled' ) && $this->settings->get( 'dcc_enabled' ) ) { 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 ( is_user_logged_in() && $this->settings->has( 'vault_enabled_dcc' ) && $this->settings->get( 'vault_enabled_dcc' ) && CreditCardGateway::ID === $id ) { $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 ); } 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 the hooks to render the credit messaging HTML depending on the settings. * * @return bool * @throws NotFoundException When a setting was not found. */ private function render_message_wrapper_registrar(): bool { if ( ! $this->settings_status->is_pay_later_messaging_enabled() ) { return false; } $selected_locations = $this->settings->has( 'pay_later_messaging_locations' ) ? $this->settings->get( 'pay_later_messaging_locations' ) : array(); $not_enabled_on_cart = ! in_array( 'cart', $selected_locations, true ); add_action( $this->proceed_to_checkout_button_renderer_hook(), function() use ( $not_enabled_on_cart ) { if ( ! is_cart() || $not_enabled_on_cart ) { return; } $this->message_renderer(); }, 19 ); $not_enabled_on_product_page = ! in_array( 'product', $selected_locations, true ); if ( ( is_product() || wc_post_content_has_shortcode( 'product_page' ) ) && ! $not_enabled_on_product_page && ! is_checkout() ) { add_action( $this->single_product_renderer_hook(), array( $this, 'message_renderer' ), 30 ); } $not_enabled_on_checkout = ! in_array( 'checkout', $selected_locations, true ); if ( ! $not_enabled_on_checkout ) { add_action( $this->checkout_dcc_button_renderer_hook(), array( $this, 'message_renderer' ), 11 ); add_action( $this->pay_order_renderer_hook(), array( $this, 'message_renderer' ), 15 ); } 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() ) { 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 { $buttons_enabled = $this->settings->has( 'enabled' ) && $this->settings->get( 'enabled' ); if ( ! $buttons_enabled ) { return false; } if ( in_array( $this->context(), array( 'checkout-block', 'cart-block' ), true ) ) { return false; } return $this->should_load_buttons() || $this->can_render_dcc(); } /** * Determines whether the button component should be loaded. */ public function should_load_buttons() : bool { $buttons_enabled = $this->settings->has( 'enabled' ) && $this->settings->get( 'enabled' ); if ( ! $buttons_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' ); $messaging_enabled_for_current_location = $this->settings_status->is_pay_later_messaging_enabled_for_location( $this->context() ); switch ( $this->context() ) { case 'checkout': case 'cart': case 'pay-now': return $smart_button_enabled_for_current_location || $messaging_enabled_for_current_location; case 'checkout-block': case 'cart-block': return $smart_button_enabled_for_current_location; case 'product': return $smart_button_enabled_for_current_location || $messaging_enabled_for_current_location || $smart_button_enabled_for_mini_cart; default: return $smart_button_enabled_for_mini_cart; } } /** * Whether DCC fields can be rendered. */ public function can_render_dcc() : bool { return $this->settings->has( 'dcc_enabled' ) && $this->settings->get( 'dcc_enabled' ) && $this->settings->has( 'client_id' ) && $this->settings->get( 'client_id' ) && $this->dcc_applies->for_country_currency() && in_array( $this->context(), array( 'checkout', 'pay-now' ), 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 '

'; echo '
'; if ( null !== $action_name ) { do_action( $action_name ); } echo '
'; } /** * Renders the HTML for the credit messaging. */ public function message_renderer() { $product = wc_get_product(); if ( ! is_checkout() && 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; } echo '
'; } /** * The values for the credit messaging. * * @return array * @throws NotFoundException When a setting was not found. */ private function message_values(): array { if ( ! $this->settings_status->is_pay_later_messaging_enabled() ) { return array(); } $placement = is_checkout() ? 'payment' : ( is_cart() ? 'cart' : 'product' ); $product = wc_get_product(); $amount = ( is_a( $product, WC_Product::class ) ) ? wc_get_price_including_tax( $product ) : 0; if ( is_checkout() || is_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' ); $per_location = is_checkout() ? 'checkout' : ( is_cart() ? 'cart' : 'product' ); $location = $styling_per_location ? $per_location : 'general'; $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'; return array( 'wrapper' => '#ppcp-messages', 'is_hidden' => ! $this->is_pay_later_filter_enabled_for_location( $this->context() ), 'amount' => $amount, 'placement' => $placement, 'style' => array( 'layout' => $layout, 'logo' => array( 'type' => $logo_type, 'position' => $logo_position, ), 'text' => array( 'color' => $text_color, ), 'color' => $style_color, 'ratio' => $ratio, ), ); } /** * Renders the HTML for the DCC fields. */ public function dcc_renderer() { $id = 'ppcp-hosted-fields'; 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_attr( $id ), 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->accept_only_automatic_payment_gateways() ) { 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 $value; } } return 'SCA_WHEN_REQUIRED'; } /** * The configuration for the smart buttons. * * @return array */ public function script_data(): array { $is_free_trial_cart = $this->is_free_trial_cart(); $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, 'data_client_id' => array( 'set_attribute' => ( is_checkout() && $this->dcc_is_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() ), ), '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 ), ), ), 'subscription_plan_id' => $this->subscription_helper->paypal_subscription_id(), 'variable_paypal_subscription_variations' => $this->subscription_helper->variable_paypal_subscription_variations(), 'subscription_product_allowed' => $this->subscription_helper->checkout_subscription_product_allowed(), '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' => 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->settings->has( 'button_mini-cart_height' ) && $this->settings->get( 'button_mini-cart_height' ) ? $this->normalize_height( (int) $this->settings->get( 'button_mini-cart_height' ) ) : 35, ), '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() ), ), ), 'separate_buttons' => array( 'card' => array( 'id' => CardButtonGateway::ID, 'wrapper' => '#ppc-button-' . CardButtonGateway::ID, 'style' => array( 'shape' => $this->style_for_context( 'shape', $this->context() ), // TODO: color black, white from the gateway settings. ), ), ), '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' ), ), '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, ); if ( $this->style_for_context( 'layout', 'mini-cart' ) !== 'horizontal' ) { $localize['button']['mini_cart_style']['tagline'] = false; } if ( $this->style_for_context( 'layout', $this->context() ) !== 'horizontal' ) { $localize['button']['style']['tagline'] = false; } 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 $localize; } /** * 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, '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->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; } } } } $disable_funding = $this->settings->has( 'disable_funding' ) ? $this->settings->get( 'disable_funding' ) : array(); if ( ! is_checkout() ) { $disable_funding[] = 'card'; } $is_dcc_enabled = $this->settings->has( 'dcc_enabled' ) && $this->settings->get( 'dcc_enabled' ); $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); $is_separate_card_enabled = isset( $available_gateways[ CardButtonGateway::ID ] ); if ( is_checkout() && ( $is_dcc_enabled || $is_separate_card_enabled ) ) { $key = array_search( 'card', $disable_funding, true ); if ( false !== $key ) { unset( $disable_funding[ $key ] ); } } if ( in_array( $context, array( 'checkout-block', 'cart-block' ), true ) ) { $disable_funding = array_diff( array_keys( $this->all_funding_sources ), array( 'venmo', 'paylater' ) ); } if ( $this->is_free_trial_cart() ) { $all_sources = array_keys( $this->all_funding_sources ); if ( $is_dcc_enabled || $is_separate_card_enabled ) { $all_sources = array_diff( $all_sources, array( 'card' ) ); } $disable_funding = $all_sources; } $enable_funding = array( 'venmo' ); if ( $this->is_pay_later_button_enabled_for_location( $context ) ) { $enable_funding[] = 'paylater'; } else { $disable_funding[] = 'paylater'; } if ( count( $disable_funding ) > 0 ) { $params['disable-funding'] = implode( ',', array_unique( $disable_funding ) ); } if ( $this->is_free_trial_cart() ) { $enable_funding = array(); } if ( count( $enable_funding ) > 0 ) { $params['enable-funding'] = implode( ',', array_unique( $enable_funding ) ); } return $params; } /** * The attributes we need to load for the JS SDK. * * @return array */ private function attributes(): array { return array( 'data-partner-attribution-id' => $this->bn_code_for_context( $this->context() ), ); } /** * 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 ] : ''; } /** * BN Codes to use. * * @return array */ private function bn_codes(): array { return array( 'checkout' => 'Woo_PPCP', 'cart' => 'Woo_PPCP', 'mini-cart' => 'Woo_PPCP', 'product' => 'Woo_PPCP', ); } /** * The JS SKD components we need to load. * * @return array * @throws NotFoundException If a setting was not found. */ private function components(): array { $components = array(); if ( $this->should_load_buttons() ) { $components[] = 'buttons'; $components[] = 'funding-eligibility'; } if ( $this->messages_apply->for_country() && ! $this->is_free_trial_cart() ) { $components[] = 'messages'; } if ( $this->dcc_is_enabled() ) { $components[] = 'hosted-fields'; } /** * Filter to add further components from the extensions. * * @internal Matches filter name in APM extension. * @since TODO * * @param array $components The array of components already registered. */ return apply_filters( 'woocommerce_paypal_payments_sdk_components_hook', $components ); } /** * Whether DCC is enabled or not. * * @return bool */ private function dcc_is_enabled(): bool { if ( ! is_checkout() ) { return false; } if ( ! $this->dcc_applies->for_country_currency() ) { return false; } $keys = array( 'client_id', 'client_secret', 'dcc_enabled', ); foreach ( $keys as $key ) { if ( ! $this->settings->has( $key ) || ! $this->settings->get( $key ) ) { return false; } } return true; } /** * Determines the style for a given indicator in a given context. * * @param string $style The style. * @param string $context The context. * * @return string */ private function style_for_context( string $style, string $context ): string { // Use the cart/checkout styles for blocks. $context = str_replace( '-block', '', $context ); $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'; } $value = isset( $defaults[ $style ] ) ? $defaults[ $style ] : ''; $value = $this->settings->has( 'button_' . $style ) ? $this->settings->get( 'button_' . $style ) : $value; $value = $this->settings->has( 'button_' . $context . '_' . $style ) ? $this->settings->get( 'button_' . $context . '_' . $style ) : $value; if ( is_bool( $value ) ) { $value = $value ? 'true' : 'false'; } return (string) $value; } /** * Returns a value between 25 and 55. * * @param int $height The input value. * @return int The normalized output value. */ private function normalize_height( int $height ): int { if ( $height < 25 ) { return 25; } if ( $height > 55 ) { return 55; } 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 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 ( $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 ); } /** * 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', ! $product->is_type( array( 'external', 'grouped' ) ) && $in_stock, $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 { $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 ); } }