diff --git a/modules/ppcp-api-client/src/Endpoint/class-paymenttokenendpoint.php b/modules/ppcp-api-client/src/Endpoint/class-paymenttokenendpoint.php index f76f419f5..062638c07 100644 --- a/modules/ppcp-api-client/src/Endpoint/class-paymenttokenendpoint.php +++ b/modules/ppcp-api-client/src/Endpoint/class-paymenttokenendpoint.php @@ -200,4 +200,69 @@ class PaymentTokenEndpoint { return wp_remote_retrieve_response_code( $response ) === 204; } + + /** + * Creates a payment token for a user. + * + * @param int $customer_id The customer Id. + * @param array $source The payment source. + * @return PaymentToken + * @throws RuntimeException If the request fails. + */ + public function create( $customer_id, $source ): PaymentToken { + $data = array( + 'customer_id' => $this->prefix . $customer_id, + 'source' => array( + 'card' => array( + 'number' => $source['credit_card_number'], + 'expiry' => $source['credit_card_expiry'], + 'security_code' => $source['credit_card_cvc'], + ), + ), + ); + + $bearer = $this->bearer->bearer(); + $url = trailingslashit( $this->host ) . 'v2/vault/payment-tokens'; + $args = array( + 'method' => 'POST', + 'headers' => array( + 'Authorization' => 'Bearer ' . $bearer->token(), + 'Content-Type' => 'application/json', + 'PayPal-Request-Id' => uniqid( 'ppcp-', true ), + 'body' => wp_json_encode( $data ), + ), + ); + + $response = $this->request( $url, $args ); + + if ( is_wp_error( $response ) ) { + $error = new RuntimeException( + sprintf( + // translators: %s is the error message. + __( 'Could not create payment token: %s', 'woocommerce-paypal-payments' ), + $response->get_error_message() + ) + ); + + $this->logger->log( 'error', $error->getMessage() ); + throw $error; + } + + $json = json_decode( $response['body'] ); + $status_code = (int) wp_remote_retrieve_response_code( $response ); + if ( 200 !== $status_code ) { + $error = new RuntimeException( + sprintf( + // translators: %s is the error message. + __( 'Could not create payment token: %s', 'woocommerce-paypal-payments' ), + $json->message + ) + ); + + $this->logger->log( 'error', $error->getMessage() ); + throw $error; + } + + return $this->factory->from_paypal_response( $json ); + } } diff --git a/modules/ppcp-subscription/src/Repository/class-paymenttokenrepository.php b/modules/ppcp-subscription/src/Repository/class-paymenttokenrepository.php index 8d2b47351..739d16cbd 100644 --- a/modules/ppcp-subscription/src/Repository/class-paymenttokenrepository.php +++ b/modules/ppcp-subscription/src/Repository/class-paymenttokenrepository.php @@ -101,6 +101,22 @@ class PaymentTokenRepository { return $this->endpoint->delete_token( $token ); } + /** + * Create payment token for customer. + * + * @param int $customer_id The customer Id. + * @param array $source The payment source. + * @return PaymentToken + * @throws RuntimeException If the request fails. + */ + public function create( $customer_id, $source ): PaymentToken { + try { + return $this->endpoint->create( $customer_id, $source ); + } catch ( RuntimeException $exception ) { + throw new RuntimeException( $exception->getMessage() ); + } + } + /** * Fetch PaymentToken from PayPal for a user. * diff --git a/modules/ppcp-subscription/src/class-subscriptionmodule.php b/modules/ppcp-subscription/src/class-subscriptionmodule.php index c9d527197..7732a678c 100644 --- a/modules/ppcp-subscription/src/class-subscriptionmodule.php +++ b/modules/ppcp-subscription/src/class-subscriptionmodule.php @@ -111,7 +111,7 @@ class SubscriptionModule implements ModuleInterface { $tokens = $payment_token_repository->all_for_user_id( $subscription->get_customer_id() ); if ( $tokens ) { $subscription_id = $subscription->get_id(); - $latest_token_id = end( $tokens )->id() ?: ''; + $latest_token_id = end( $tokens )->id() ? end( $tokens )->id() : ''; update_post_meta( $subscription_id, 'payment_token_id', $latest_token_id, true ); } } catch ( RuntimeException $error ) { diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index c3dfa1548..59d90232c 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -635,8 +635,9 @@ return array( 'title' => __( 'Vaulting', 'woocommerce-paypal-payments' ), 'type' => 'checkbox', 'desc_tip' => true, - 'label' => sprintf( - __('To use vaulting features, you must %1$senable vaulting on your account%2$s.', 'woocommerce-paypal-payments'), + 'label' => sprintf( + // translators: %1$s and %2$s are the opening and closing href link tags. + __( 'To use vaulting features, you must %1$senable vaulting on your account%2$s.', 'woocommerce-paypal-payments' ), 'has_subscription( $order_id ) && $this->is_subscription_change_payment() ) { + if ( $saved_credit_card ) { + update_post_meta( $order_id, 'payment_token_id', $saved_credit_card ); + + $this->session_handler->destroy_session_data(); + return array( + 'result' => 'success', + 'redirect' => $this->get_return_url( $wc_order ), + ); + } + + $credit_card_number = filter_input( INPUT_POST, 'ppcp-credit-card-gateway-card-number', FILTER_SANITIZE_STRING ); + $credit_card_expiry = filter_input( INPUT_POST, 'ppcp-credit-card-gateway-card-expiry', FILTER_SANITIZE_STRING ); + $credit_card_cvc = filter_input( INPUT_POST, 'ppcp-credit-card-gateway-card-cvc', FILTER_SANITIZE_STRING ); + + if ( $credit_card_number && $credit_card_expiry && $credit_card_cvc ) { + try { + $token = $this->payment_token_repository->create( + $wc_order->get_customer_id(), + array( + 'credit_card_number' => $credit_card_number, + 'credit_card_expiry' => $credit_card_expiry, + 'credit_card_cvc' => $credit_card_cvc, + ) + ); + + update_post_meta( $order_id, 'payment_token_id', $token->id() ); + + $this->session_handler->destroy_session_data(); + return array( + 'result' => 'success', + 'redirect' => $this->get_return_url( $wc_order ), + ); + } catch ( RuntimeException $exception ) { + wc_add_notice( + $exception->getMessage(), + 'error' + ); + + $this->session_handler->destroy_session_data(); + return array( + 'result' => 'failure', + 'redirect' => $this->get_return_url( $wc_order ), + ); + } + } + } + /** * If the WC_Order is payed through the approved webhook. */ @@ -175,4 +226,25 @@ trait ProcessPaymentTrait { } return false; } + + /** + * Is $order_id a subscription? + * + * @param int $order_id The order Id. + * @return boolean Whether order is a subscription or not. + */ + private function has_subscription( $order_id ): bool { + return ( function_exists( 'wcs_order_contains_subscription' ) && ( wcs_order_contains_subscription( $order_id ) || wcs_is_subscription( $order_id ) || wcs_order_contains_renewal( $order_id ) ) ); + } + + /** + * Checks if page is pay for order and change subscription payment page. + * + * @return bool + */ + private function is_subscription_change_payment(): bool { + $pay_for_order = filter_input( INPUT_GET, 'pay_for_order', FILTER_SANITIZE_STRING ); + $change_payment_method = filter_input( INPUT_GET, 'change_payment_method', FILTER_SANITIZE_STRING ); + return ( isset( $pay_for_order ) && isset( $change_payment_method ) ); + } }