PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.6
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.6
1.6.6 1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 All 49 releases
← 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).