← All changes
|
app/Modules/PaymentMethods/PayPalGateway/API/API.php
+62
-13
1.5.5
→
1.6.6
View file →
| @@ -128,9 +128,11 @@ | ||
| 128 | 128 | |
| 129 | 129 | $response = wp_remote_post($paypal_api_url, [ |
| 130 | 130 | 'headers' => $headers, |
| 131 | 131 | 'method' => $method, |
| 132 | - 'body' => json_encode($args) | |
| 132 | + // An empty array encodes to a literal [], which PayPal rejects | |
| 133 | + // with MALFORMED_REQUEST_JSON — it requires a {} body. | |
| 134 | + 'body' => json_encode($args ?: new \stdClass()) | |
| 133 | 135 | ]); |
| 134 | 136 | |
| 135 | 137 | if (is_wp_error($response)) { |
| 136 | 138 | return new \WP_Error('general_error', $response->get_error_message(), $response); |
| @@ -278,24 +280,27 @@ | ||
| 278 | 280 | |
| 279 | 281 | return new \WP_Error($http_code, $message, $body); |
| 280 | 282 | } |
| 281 | 283 | |
| 282 | - /** | |
| 283 | - * Two-step order: no payment_source in the body, so the buyer approves and the JS SDK | |
| 284 | - * captures. PayPal-Request-Id is optional here and deliberately omitted — see | |
| 285 | - * .claude/skills/coding-rules/payment-idempotency.md. | |
| 286 | - * | |
| 287 | - * Adding payment_source (card, vault_id, billing_agreement_id) makes this a single-step | |
| 288 | - * call that moves money on create. PayPal then REQUIRES PayPal-Request-Id (max 108 chars, | |
| 289 | - * keys stored 6h), and the idempotency design must be revisited before doing so. | |
| 290 | - */ | |
| 291 | - public static function createOrder($purchaseUnit) | |
| 284 | + public static function createOrder($purchaseUnit, $extraBody = [], $extraHeaders = []) | |
| 292 | 285 | { |
| 293 | - return self::makeRequest('checkout/orders', 'v2', 'POST', [ | |
| 286 | + $body = [ | |
| 294 | 287 | 'intent' => 'CAPTURE', |
| 295 | 288 | 'purchase_units' => [$purchaseUnit], |
| 296 | 289 | 'application_context' => ['shipping_preference' => 'NO_SHIPPING'], |
| 297 | - ]); | |
| 290 | + ]; | |
| 291 | + | |
| 292 | + if ($extraBody) { | |
| 293 | + // The legacy application_context cannot be combined with the | |
| 294 | + // payment_source object (vaulting / merchant-initiated charges) — | |
| 295 | + // shipping preference then rides experience_context instead. | |
| 296 | + if (isset($extraBody['payment_source'])) { | |
| 297 | + unset($body['application_context']); | |
| 298 | + } | |
| 299 | + $body = array_merge($body, $extraBody); | |
| 300 | + } | |
| 301 | + | |
| 302 | + return self::makeRequest('checkout/orders', 'v2', 'POST', $body, '', $extraHeaders); | |
| 298 | 303 | } |
| 299 | 304 | |
| 300 | 305 | public static function verifyPayment($paymentId) |
| 301 | 306 | { |
| @@ -417,8 +422,52 @@ | ||
| 417 | 422 | 'access_token_error', |
| 418 | 423 | $errorMessage, |
| 419 | 424 | $error |
| 420 | 425 | ); |
| 426 | + } | |
| 427 | + | |
| 428 | + /** | |
| 429 | + * Browser-safe id token for the JS SDK vault (save-without-purchase) flow — | |
| 430 | + * rendered as the SDK script's data-user-id-token attribute. Short-lived | |
| 431 | + * (~15 min), so it is generated per checkout page render and never cached. | |
| 432 | + * | |
| 433 | + * @param string $mode The PayPal mode (live/test). | |
| 434 | + * @return string|\WP_Error | |
| 435 | + */ | |
| 436 | + public static function getUserIdToken($mode = '') | |
| 437 | + { | |
| 438 | + if (!$mode) { | |
| 439 | + $mode = self::getPayPalSettings()->getMode(); | |
| 440 | + } | |
| 441 | + | |
| 442 | + $headers = [ | |
| 443 | + 'Accept' => 'application/json', | |
| 444 | + 'PayPal-Partner-Attribution-ID' => 'FLUENTCART_SP_PPCP', | |
| 445 | + 'Authorization' => 'Basic ' . base64_encode( | |
| 446 | + self::getPayPalSettings()->getPublicKey($mode) . ':' . self::getPayPalSettings()->getApiKey($mode) | |
| 447 | + ), | |
| 448 | + ]; | |
| 449 | + | |
| 450 | + $response = wp_remote_post(self::getAuthAPI($mode), [ | |
| 451 | + 'headers' => $headers, | |
| 452 | + 'body' => [ | |
| 453 | + 'grant_type' => 'client_credentials', | |
| 454 | + 'response_type' => 'id_token' | |
| 455 | + ], | |
| 456 | + 'timeout' => 30 | |
| 457 | + ]); | |
| 458 | + | |
| 459 | + if (is_wp_error($response)) { | |
| 460 | + return $response; | |
| 461 | + } | |
| 462 | + | |
| 463 | + $body = json_decode(wp_remote_retrieve_body($response), true); | |
| 464 | + | |
| 465 | + if (wp_remote_retrieve_response_code($response) !== 200 || empty($body['id_token'])) { | |
| 466 | + return new \WP_Error('id_token_error', __('Could not generate a PayPal id token.', 'fluent-cart'), $body); | |
| 467 | + } | |
| 468 | + | |
| 469 | + return $body['id_token']; | |
| 421 | 470 | } |
| 422 | 471 | |
| 423 | 472 | /** |
| 424 | 473 | * Generate a PayPal-Auth-Assertion JWT header (unsigned, alg=none). |