renew( $order, $c ); }, 10, 2 ); add_action( 'woocommerce_scheduled_subscription_payment_' . CreditCardGateway::ID, function ( $amount, $order ) use ( $c ) { $this->renew( $order, $c ); }, 10, 2 ); add_action( 'woocommerce_subscription_payment_complete', function ( $subscription ) use ( $c ) { $paypal_subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? ''; if ( $paypal_subscription_id ) { return; } $payment_token_repository = $c->get( 'vaulting.repository.payment-token' ); $logger = $c->get( 'woocommerce.logger.woocommerce' ); $this->add_payment_token_id( $subscription, $payment_token_repository, $logger ); if ( count( $subscription->get_related_orders() ) === 1 ) { $parent_order = $subscription->get_parent(); if ( is_a( $parent_order, WC_Order::class ) ) { $order_repository = $c->get( 'api.repository.order' ); $order = $order_repository->for_wc_order( $parent_order ); $transaction_id = $this->get_paypal_order_transaction_id( $order ); if ( $transaction_id ) { $subscription->update_meta_data( 'ppcp_previous_transaction_reference', $transaction_id ); $subscription->save(); } } } } ); add_filter( 'woocommerce_gateway_description', function ( $description, $id ) use ( $c ) { $payment_token_repository = $c->get( 'vaulting.repository.payment-token' ); $settings = $c->get( 'wcgateway.settings' ); $subscription_helper = $c->get( 'subscription.helper' ); return $this->display_saved_paypal_payments( $settings, (string) $id, $payment_token_repository, (string) $description, $subscription_helper ); }, 10, 2 ); add_filter( 'woocommerce_credit_card_form_fields', function ( $default_fields, $id ) use ( $c ) { $payment_token_repository = $c->get( 'vaulting.repository.payment-token' ); $settings = $c->get( 'wcgateway.settings' ); $subscription_helper = $c->get( 'subscription.helper' ); return $this->display_saved_credit_cards( $settings, $id, $payment_token_repository, $default_fields, $subscription_helper ); }, 20, 2 ); add_filter( 'ppcp_create_order_request_body_data', function( array $data ) use ( $c ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing $wc_order_action = wc_clean( wp_unslash( $_POST['wc_order_action'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing $subscription_id = wc_clean( wp_unslash( $_POST['post_ID'] ?? '' ) ); if ( ! $subscription_id ) { return $data; } $subscription = wc_get_order( $subscription_id ); if ( ! is_a( $subscription, WC_Subscription::class ) ) { return $data; } if ( $wc_order_action === 'wcs_process_renewal' && $subscription->get_payment_method() === CreditCardGateway::ID && isset( $data['payment_source']['token'] ) && $data['payment_source']['token']['type'] === 'PAYMENT_METHOD_TOKEN' && isset( $data['payment_source']['token']['source']->card ) ) { $data['payment_source'] = array( 'card' => array( 'vault_id' => $data['payment_source']['token']['id'], 'stored_credential' => array( 'payment_initiator' => 'MERCHANT', 'payment_type' => 'RECURRING', 'usage' => 'SUBSEQUENT', ), ), ); $previous_transaction_reference = $subscription->get_meta( 'ppcp_previous_transaction_reference' ); if ( $previous_transaction_reference ) { $data['payment_source']['card']['stored_credential']['previous_transaction_reference'] = $previous_transaction_reference; } } return $data; } ); $this->subscriptions_api_integration( $c ); add_action( 'admin_enqueue_scripts', /** * Param types removed to avoid third-party issues. * * @psalm-suppress MissingClosureParamType */ function( $hook ) use ( $c ) { if ( ! is_string( $hook ) ) { return; } $settings = $c->get( 'wcgateway.settings' ); $subscription_mode = $settings->has( 'subscriptions_mode' ) ? $settings->get( 'subscriptions_mode' ) : ''; if ( $hook !== 'post.php' || $subscription_mode !== 'subscriptions_api' ) { return; } //phpcs:disable WordPress.Security.NonceVerification.Recommended $post_id = wc_clean( wp_unslash( $_GET['post'] ?? '' ) ); $product = wc_get_product( $post_id ); if ( ! ( is_a( $product, WC_Product::class ) ) ) { return; } $subscriptions_helper = $c->get( 'subscription.helper' ); assert( $subscriptions_helper instanceof SubscriptionHelper ); if ( ! $subscriptions_helper->plugin_is_active() || ! ( is_a( $product, WC_Product_Subscription::class ) || is_a( $product, WC_Product_Variable_Subscription::class ) || is_a( $product, WC_Product_Subscription_Variation::class ) ) || ! WC_Subscriptions_Product::is_subscription( $product ) ) { return; } $module_url = $c->get( 'subscription.module.url' ); wp_enqueue_script( 'ppcp-paypal-subscription', untrailingslashit( $module_url ) . '/assets/js/paypal-subscription.js', array( 'jquery' ), $c->get( 'ppcp.asset-version' ), true ); $products = array( $this->set_product_config( $product ) ); if ( $product->get_type() === 'variable-subscription' ) { $products = array(); /** * Suppress pslam. * * @psalm-suppress TypeDoesNotContainType * * WC_Product_Variable_Subscription extends WC_Product_Variable. */ assert( $product instanceof WC_Product_Variable ); $available_variations = $product->get_available_variations(); foreach ( $available_variations as $variation ) { /** * The method is defined in WooCommerce. * * @psalm-suppress UndefinedMethod */ $variation = wc_get_product_object( 'variation', $variation['variation_id'] ); $products[] = $this->set_product_config( $variation ); } } wp_localize_script( 'ppcp-paypal-subscription', 'PayPalCommerceGatewayPayPalSubscriptionProducts', $products ); } ); $endpoint = $c->get( 'subscription.deactivate-plan-endpoint' ); assert( $endpoint instanceof DeactivatePlanEndpoint ); add_action( 'wc_ajax_' . DeactivatePlanEndpoint::ENDPOINT, array( $endpoint, 'handle_request' ) ); add_action( 'add_meta_boxes', /** * Param types removed to avoid third-party issues. * * @psalm-suppress MissingClosureParamType */ function( string $post_type, $post_or_order_object ) use ( $c ) { if ( ! function_exists( 'wcs_get_subscription' ) ) { return; } $order = ( $post_or_order_object instanceof WP_Post ) ? wc_get_order( $post_or_order_object->ID ) : $post_or_order_object; if ( ! is_a( $order, WC_Order::class ) ) { return; } $subscription = wcs_get_subscription( $order->get_id() ); if ( ! is_a( $subscription, WC_Subscription::class ) ) { return; } $subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? ''; if ( ! $subscription_id ) { return; } $screen_id = wc_get_page_screen_id( 'shop_subscription' ); remove_meta_box( 'woocommerce-subscription-schedule', $screen_id, 'side' ); $environment = $c->get( 'onboarding.environment' ); add_meta_box( 'ppcp_paypal_subscription', __( 'PayPal Subscription', 'woocommerce-paypal-payments' ), function() use ( $subscription_id, $environment ) { $host = $environment->current_environment_is( Environment::SANDBOX ) ? 'https://www.sandbox.paypal.com' : 'https://www.paypal.com'; $url = trailingslashit( $host ) . 'billing/subscriptions/' . $subscription_id; echo '
' . esc_html__( 'This subscription is linked to a PayPal Subscription, Cancel it to unlink.', 'woocommerce-paypal-payments' ) . '
'; echo '' . esc_html__( 'Subscription:', 'woocommerce-paypal-payments' ) . ' ' . esc_attr( $subscription_id ) . '
'; }, $post_type, 'side' ); }, 30, 2 ); add_action( 'action_scheduler_before_execute', /** * Param types removed to avoid third-party issues. * * @psalm-suppress MissingClosureParamType */ function( $action_id ) { /** * Class exist in WooCommerce. * * @psalm-suppress UndefinedClass */ $store = ActionScheduler_Store::instance(); $action = $store->fetch_action( $action_id ); $subscription_id = $action->get_args()['subscription_id'] ?? null; if ( $subscription_id ) { $subscription = wcs_get_subscription( $subscription_id ); if ( is_a( $subscription, WC_Subscription::class ) ) { $paypal_subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? ''; if ( $paypal_subscription_id ) { as_unschedule_action( $action->get_hook(), $action->get_args() ); } } } } ); } /** * Returns the key for the module. * * @return string|void */ public function getKey() { } /** * Handles a Subscription product renewal. * * @param \WC_Order $order WooCommerce order. * @param ContainerInterface|null $container The container. * @return void */ protected function renew( $order, $container ) { if ( ! ( $order instanceof \WC_Order ) ) { return; } $handler = $container->get( 'subscription.renewal-handler' ); $handler->renew( $order ); } /** * Adds Payment token ID to subscription. * * @param \WC_Subscription $subscription The subscription. * @param PaymentTokenRepository $payment_token_repository The payment repository. * @param LoggerInterface $logger The logger. */ protected function add_payment_token_id( \WC_Subscription $subscription, PaymentTokenRepository $payment_token_repository, LoggerInterface $logger ) { try { $tokens = $payment_token_repository->all_for_user_id( $subscription->get_customer_id() ); if ( $tokens ) { $latest_token_id = end( $tokens )->id() ? end( $tokens )->id() : ''; $subscription->update_meta_data( 'payment_token_id', $latest_token_id ); $subscription->save(); } } catch ( RuntimeException $error ) { $message = sprintf( // translators: %1$s is the payment token Id, %2$s is the error message. __( 'Could not add token Id to subscription %1$s: %2$s', 'woocommerce-paypal-payments' ), $subscription->get_id(), $error->getMessage() ); $logger->log( 'warning', $message ); } } /** * Displays saved PayPal payments. * * @param Settings $settings The settings. * @param string $id The payment gateway Id. * @param PaymentTokenRepository $payment_token_repository The payment token repository. * @param string $description The payment gateway description. * @param SubscriptionHelper $subscription_helper The subscription helper. * @return string */ protected function display_saved_paypal_payments( Settings $settings, string $id, PaymentTokenRepository $payment_token_repository, string $description, SubscriptionHelper $subscription_helper ): string { if ( $settings->has( 'vault_enabled' ) && $settings->get( 'vault_enabled' ) && PayPalGateway::ID === $id && $subscription_helper->is_subscription_change_payment() ) { $tokens = $payment_token_repository->all_for_user_id( get_current_user_id() ); if ( ! $tokens || ! $payment_token_repository->tokens_contains_paypal( $tokens ) ) { return esc_html__( 'No PayPal payments saved, in order to use a saved payment you first need to create it through a purchase.', 'woocommerce-paypal-payments' ); } $output = sprintf( ''; return $output; } return $description; } /** * Displays saved credit cards. * * @param Settings $settings The settings. * @param string $id The payment gateway Id. * @param PaymentTokenRepository $payment_token_repository The payment token repository. * @param array $default_fields Default payment gateway fields. * @param SubscriptionHelper $subscription_helper The subscription helper. * @return array|mixed|string * @throws NotFoundException When setting was not found. */ protected function display_saved_credit_cards( Settings $settings, string $id, PaymentTokenRepository $payment_token_repository, array $default_fields, SubscriptionHelper $subscription_helper ) { if ( $settings->has( 'vault_enabled_dcc' ) && $settings->get( 'vault_enabled_dcc' ) && $subscription_helper->is_subscription_change_payment() && CreditCardGateway::ID === $id ) { $tokens = $payment_token_repository->all_for_user_id( get_current_user_id() ); if ( ! $tokens || ! $payment_token_repository->tokens_contains_card( $tokens ) ) { $default_fields = array(); $default_fields['saved-credit-card'] = esc_html__( 'No Credit Card saved, in order to use a saved Credit Card you first need to create it through a purchase.', 'woocommerce-paypal-payments' ); return $default_fields; } $output = sprintf( ''; $default_fields = array(); $default_fields['saved-credit-card'] = $output; return $default_fields; } return $default_fields; } /** * Adds PayPal subscriptions API integration. * * @param ContainerInterface $c The container. * @return void * @throws Exception When something went wrong. */ protected function subscriptions_api_integration( ContainerInterface $c ): void { add_action( 'save_post', /** * Param types removed to avoid third-party issues. * * @psalm-suppress MissingClosureParamType */ function( $product_id ) use ( $c ) { $settings = $c->get( 'wcgateway.settings' ); assert( $settings instanceof Settings ); try { $subscriptions_mode = $settings->get( 'subscriptions_mode' ); } catch ( NotFoundException $exception ) { return; } $nonce = wc_clean( wp_unslash( $_POST['_wcsnonce'] ?? '' ) ); if ( $subscriptions_mode !== 'subscriptions_api' || ! is_string( $nonce ) || ! wp_verify_nonce( $nonce, 'wcs_subscription_meta' ) ) { return; } $product = wc_get_product( $product_id ); if ( ! is_a( $product, WC_Product::class ) ) { return; } $subscriptions_api_handler = $c->get( 'subscription.api-handler' ); assert( $subscriptions_api_handler instanceof SubscriptionsApiHandler ); $this->update_subscription_product_meta( $product, $subscriptions_api_handler ); }, 12 ); add_action( 'woocommerce_save_product_variation', /** * Param types removed to avoid third-party issues. * * @psalm-suppress MissingClosureParamType */ function( $variation_id ) use ( $c ) { $wcsnonce_save_variations = wc_clean( wp_unslash( $_POST['_wcsnonce_save_variations'] ?? '' ) ); $subscriptions_helper = $c->get( 'subscription.helper' ); assert( $subscriptions_helper instanceof SubscriptionHelper ); if ( ! $subscriptions_helper->plugin_is_active() || ! WC_Subscriptions_Product::is_subscription( $variation_id ) || ! is_string( $wcsnonce_save_variations ) || ! wp_verify_nonce( $wcsnonce_save_variations, 'wcs_subscription_variations' ) ) { return; } $product = wc_get_product( $variation_id ); if ( ! is_a( $product, WC_Product_Subscription_Variation::class ) ) { return; } $subscriptions_api_handler = $c->get( 'subscription.api-handler' ); assert( $subscriptions_api_handler instanceof SubscriptionsApiHandler ); $this->update_subscription_product_meta( $product, $subscriptions_api_handler ); }, 30 ); add_action( 'woocommerce_process_shop_subscription_meta', /** * Param types removed to avoid third-party issues. * * @psalm-suppress MissingClosureParamType */ function( $id, $post ) use ( $c ) { $subscription = wcs_get_subscription( $id ); if ( ! is_a( $subscription, WC_Subscription::class ) ) { return; } $subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? ''; if ( ! $subscription_id ) { return; } $subscriptions_endpoint = $c->get( 'api.endpoint.billing-subscriptions' ); assert( $subscriptions_endpoint instanceof BillingSubscriptions ); if ( $subscription->get_status() === 'cancelled' ) { try { $subscriptions_endpoint->cancel( $subscription_id ); } catch ( RuntimeException $exception ) { $error = $exception->getMessage(); if ( is_a( $exception, PayPalApiException::class ) ) { $error = $exception->get_details( $error ); } $logger = $c->get( 'woocommerce.logger.woocommerce' ); $logger->error( 'Could not cancel subscription product on PayPal. ' . $error ); } } if ( $subscription->get_status() === 'pending-cancel' ) { try { $subscriptions_endpoint->suspend( $subscription_id ); } catch ( RuntimeException $exception ) { $error = $exception->getMessage(); if ( is_a( $exception, PayPalApiException::class ) ) { $error = $exception->get_details( $error ); } $logger = $c->get( 'woocommerce.logger.woocommerce' ); $logger->error( 'Could not suspend subscription product on PayPal. ' . $error ); } } if ( $subscription->get_status() === 'active' ) { try { $current_subscription = $subscriptions_endpoint->subscription( $subscription_id ); if ( $current_subscription->status === 'SUSPENDED' ) { $subscriptions_endpoint->activate( $subscription_id ); } } catch ( RuntimeException $exception ) { $error = $exception->getMessage(); if ( is_a( $exception, PayPalApiException::class ) ) { $error = $exception->get_details( $error ); } $logger = $c->get( 'woocommerce.logger.woocommerce' ); $logger->error( 'Could not reactivate subscription product on PayPal. ' . $error ); } } }, 20, 2 ); add_filter( 'woocommerce_order_actions', /** * Param types removed to avoid third-party issues. * * @psalm-suppress MissingClosureParamType */ function( $actions, $subscription ): array { if ( ! is_array( $actions ) || ! is_a( $subscription, WC_Subscription::class ) ) { return $actions; } $subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? ''; if ( $subscription_id && isset( $actions['wcs_process_renewal'] ) ) { unset( $actions['wcs_process_renewal'] ); } return $actions; }, 20, 2 ); add_filter( 'wcs_view_subscription_actions', /** * Param types removed to avoid third-party issues. * * @psalm-suppress MissingClosureParamType */ function( $actions, $subscription ): array { if ( ! is_a( $subscription, WC_Subscription::class ) ) { return $actions; } $subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? ''; if ( $subscription_id && $subscription->get_status() === 'active' ) { $url = wp_nonce_url( add_query_arg( array( 'change_subscription_to' => 'cancelled', 'ppcp_cancel_subscription' => $subscription->get_id(), ) ), 'ppcp_cancel_subscription_nonce' ); array_unshift( $actions, array( 'url' => esc_url( $url ), 'name' => esc_html__( 'Cancel', 'woocommerce-paypal-payments' ), ) ); $actions['cancel']['name'] = esc_html__( 'Suspend', 'woocommerce-paypal-payments' ); unset( $actions['subscription_renewal_early'] ); } return $actions; }, 11, 2 ); add_action( 'wp_loaded', function() use ( $c ) { if ( ! function_exists( 'wcs_get_subscription' ) ) { return; } $cancel_subscription_id = wc_clean( wp_unslash( $_GET['ppcp_cancel_subscription'] ?? '' ) ); $subscription = wcs_get_subscription( absint( $cancel_subscription_id ) ); if ( ! wcs_is_subscription( $subscription ) || $subscription === false ) { return; } $subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? ''; $nonce = wc_clean( wp_unslash( $_GET['_wpnonce'] ?? '' ) ); if ( ! is_string( $nonce ) ) { return; } if ( $subscription_id && $cancel_subscription_id && $nonce ) { if ( ! wp_verify_nonce( $nonce, 'ppcp_cancel_subscription_nonce' ) || ! user_can( get_current_user_id(), 'edit_shop_subscription_status', $subscription->get_id() ) ) { return; } $subscriptions_endpoint = $c->get( 'api.endpoint.billing-subscriptions' ); $subscription_id = $subscription->get_meta( 'ppcp_subscription' ); try { $subscriptions_endpoint->cancel( $subscription_id ); $subscription->update_status( 'cancelled' ); $subscription->add_order_note( __( 'Subscription cancelled by the subscriber from their account page.', 'woocommerce-paypal-payments' ) ); wc_add_notice( __( 'Your subscription has been cancelled.', 'woocommerce-paypal-payments' ) ); wp_safe_redirect( $subscription->get_view_order_url() ); exit; } catch ( RuntimeException $exception ) { $error = $exception->getMessage(); if ( is_a( $exception, PayPalApiException::class ) ) { $error = $exception->get_details( $error ); } $logger = $c->get( 'woocommerce.logger.woocommerce' ); $logger->error( 'Could not cancel subscription product on PayPal. ' . $error ); } } }, 100 ); add_action( 'woocommerce_subscription_before_actions', /** * Param types removed to avoid third-party issues. * * @psalm-suppress MissingClosureParamType */ function( $subscription ) use ( $c ) { $subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? ''; if ( $subscription_id ) { $environment = $c->get( 'onboarding.environment' ); $host = $environment->current_environment_is( Environment::SANDBOX ) ? 'https://www.sandbox.paypal.com' : 'https://www.paypal.com'; ?>'; echo sprintf( // translators: %1$s and %2$s are label open and close tags. esc_html__( '%1$sConnect to PayPal%2$s', 'woocommerce-paypal-payments' ), '' ); echo ''; echo sprintf( // translators: %1$s and %2$s are label open and close tags. esc_html__( '%1$sConnect Product to PayPal Subscriptions Plan%2$s', 'woocommerce-paypal-payments' ), '', '' ); echo wc_help_tip( esc_html__( 'Create a subscription product and plan to bill customers at regular intervals. Be aware that certain subscription settings cannot be modified once the PayPal Subscription is linked to this product. Unlink the product to edit disabled fields.', 'woocommerce-paypal-payments' ) ); echo '
'; $subscription_product = $product->get_meta( 'ppcp_subscription_product' ); $subscription_plan = $product->get_meta( 'ppcp_subscription_plan' ); $subscription_plan_name = $product->get_meta( '_ppcp_subscription_plan_name' ); if ( $subscription_product && $subscription_plan ) { if ( $enable_subscription_product !== 'yes' ) { echo sprintf( // translators: %1$s and %2$s are button and wrapper html tags. esc_html__( '%1$sUnlink PayPal Subscription Plan%2$s', 'woocommerce-paypal-payments' ), '' ); echo sprintf( // translators: %1$s and %2$s is open and closing paragraph tag. esc_html__( '%1$sPlan unlinked successfully ✔️%2$s', 'woocommerce-paypal-payments' ), ' ' ); } $host = $environment->current_environment_is( Environment::SANDBOX ) ? 'https://www.sandbox.paypal.com' : 'https://www.paypal.com'; echo sprintf( // translators: %1$s and %2$s are wrapper html tags. esc_html__( '%1$sProduct%2$s', 'woocommerce-paypal-payments' ), '' ); echo sprintf( // translators: %1$s and %2$s are wrapper html tags. esc_html__( '%1$sPlan%2$s', 'woocommerce-paypal-payments' ), '' ); } else { echo sprintf( // translators: %1$s and %2$s are wrapper html tags. esc_html__( '%1$sPlan Name%2$s', 'woocommerce-paypal-payments' ), '' ); } } /** * Updates subscription product meta. * * @param WC_Product $product The product. * @param SubscriptionsApiHandler $subscriptions_api_handler The subscription api handler. * @return void */ private function update_subscription_product_meta( WC_Product $product, SubscriptionsApiHandler $subscriptions_api_handler ): void { // phpcs:ignore WordPress.Security.NonceVerification $enable_subscription_product = wc_clean( wp_unslash( $_POST['_ppcp_enable_subscription_product'] ?? '' ) ); $product->update_meta_data( '_ppcp_enable_subscription_product', $enable_subscription_product ); $product->save(); if ( ( $product->get_type() === 'subscription' || $product->get_type() === 'subscription_variation' ) && $enable_subscription_product === 'yes' ) { if ( $product->meta_exists( 'ppcp_subscription_product' ) && $product->meta_exists( 'ppcp_subscription_plan' ) ) { $subscriptions_api_handler->update_product( $product ); $subscriptions_api_handler->update_plan( $product ); return; } if ( ! $product->meta_exists( 'ppcp_subscription_product' ) ) { $subscriptions_api_handler->create_product( $product ); } if ( $product->meta_exists( 'ppcp_subscription_product' ) && ! $product->meta_exists( 'ppcp_subscription_plan' ) ) { // phpcs:ignore WordPress.Security.NonceVerification $subscription_plan_name = wc_clean( wp_unslash( $_POST['_ppcp_subscription_plan_name'] ?? '' ) ); if ( ! is_string( $subscription_plan_name ) ) { return; } $product->update_meta_data( '_ppcp_subscription_plan_name', $subscription_plan_name ); $product->save(); $subscriptions_api_handler->create_plan( $subscription_plan_name, $product ); } } } /** * Returns subscription product configuration. * * @param WC_Product $product The product. * @return array */ private function set_product_config( WC_Product $product ): array { $plan = $product->get_meta( 'ppcp_subscription_plan' ) ?? array(); $plan_id = $plan['id'] ?? ''; return array( 'product_connected' => $product->get_meta( '_ppcp_enable_subscription_product' ) ?? '', 'plan_id' => $plan_id, 'product_id' => $product->get_id(), 'ajax' => array( 'deactivate_plan' => array( 'endpoint' => \WC_AJAX::get_endpoint( DeactivatePlanEndpoint::ENDPOINT ), 'nonce' => wp_create_nonce( DeactivatePlanEndpoint::ENDPOINT ), ), ), ); } }