| 1 |
<?php |
| 2 |
|
| 3 |
namespace FluentCart\App\Modules\PaymentMethods\StripeGateway; |
| 4 |
|
| 5 |
use FluentCart\Api\CurrencySettings; |
| 6 |
use FluentCart\Api\Orders; |
| 7 |
use FluentCart\Api\StoreSettings; |
| 8 |
use FluentCart\App\Helpers\CartCheckoutHelper; |
| 9 |
use FluentCart\App\Helpers\CartHelper; |
| 10 |
use FluentCart\App\Helpers\CurrenciesHelper; |
| 11 |
use FluentCart\App\Helpers\Helper; |
| 12 |
use FluentCart\App\Hooks\Cart\WebCheckoutHandler; |
| 13 |
use FluentCart\App\Modules\PaymentMethods\Core\AbstractPaymentGateway; |
| 14 |
use FluentCart\App\Modules\PaymentMethods\Core\BaseGatewaySettings; |
| 15 |
use FluentCart\App\Modules\PaymentMethods\StripeGateway\API\API; |
| 16 |
use FluentCart\App\Modules\PaymentMethods\StripeGateway\Connect\ConnectConfig; |
| 17 |
use FluentCart\App\Modules\PaymentMethods\StripeGateway\Webhook\IPN; |
| 18 |
use FluentCart\App\Modules\PaymentMethods\StripeGateway\Webhook\Webhook; |
| 19 |
use FluentCart\App\Services\CustomPayment\PaymentIntent; |
| 20 |
use FluentCart\App\Services\Payments\PaymentHelper; |
| 21 |
use FluentCart\App\Services\Payments\PaymentInstance; |
| 22 |
use FluentCart\App\Vite; |
| 23 |
use FluentCart\Framework\Support\Arr; |
| 24 |
|
| 25 |
class Stripe extends AbstractPaymentGateway |
| 26 |
{ |
| 27 |
|
| 28 |
private $methodSlug = 'stripe'; |
| 29 |
|
| 30 |
public array $supportedFeatures = ['payment', 'refund', 'webhook', 'custom_payment', 'card_update', 'switch_payment_method' => [ |
| 31 |
'supported_gateways' => ['stripe', 'paypal'], |
| 32 |
], 'dispute_handler', 'subscriptions', 'zero_recurring', 'system_subscription', 'manual_subscription', 'verify_vendor_ids']; |
| 33 |
|
| 34 |
public BaseGatewaySettings $settings; |
| 35 |
|
| 36 |
public function __construct() |
| 37 |
{ |
| 38 |
parent::__construct( |
| 39 |
new StripeSettingsBase(), |
| 40 |
new StripeSubscriptions() |
| 41 |
); |
| 42 |
|
| 43 |
add_action('fluent_cart_action_stripe_connect', function ($data) { |
| 44 |
ConnectConfig::handleConnect($data); |
| 45 |
}); |
| 46 |
|
| 47 |
} |
| 48 |
|
| 49 |
public function boot() |
| 50 |
{ |
| 51 |
(new IPN)->init(); |
| 52 |
(new Confirmations)->init(); |
| 53 |
add_filter('fluent_cart/payment_methods/stripe_pub_key', [$this, 'getPublicKey'], 10); |
| 54 |
} |
| 55 |
|
| 56 |
public function meta(): array |
| 57 |
{ |
| 58 |
return [ |
| 59 |
'title' => __('Card', 'fluent-cart'), |
| 60 |
'route' => 'stripe', |
| 61 |
'slug' => 'stripe', |
| 62 |
'label' => 'Stripe', |
| 63 |
'admin_title' => 'Stripe', |
| 64 |
'description' => __("Stripe's payments platform lets you accept credit cards, debit cards, and popular payment methods around the world all with a single integration.", "fluent-cart"), |
| 65 |
'logo' => Vite::getAssetUrl('images/payment-methods/card.svg'), |
| 66 |
'icon' => Vite::getAssetUrl('images/payment-methods/stripe-icon.svg'), |
| 67 |
'status' => $this->settings->get('is_active') === 'yes', |
| 68 |
'brand_color' => '#635bff', |
| 69 |
'upcoming' => false, |
| 70 |
'supported_features' => $this->supportedFeatures |
| 71 |
]; |
| 72 |
} |
| 73 |
|
| 74 |
public function makePaymentFromPaymentInstance(PaymentInstance $paymentInstance) |
| 75 |
{ |
| 76 |
$order = $paymentInstance->order; |
| 77 |
|
| 78 |
$storeName = (new StoreSettings())->get('store_name'); |
| 79 |
|
| 80 |
$transactionCurrency = $paymentInstance->transaction->currency; |
| 81 |
$chargeAmount = (int)$paymentInstance->transaction->total; |
| 82 |
|
| 83 |
if ($transactionCurrency && CurrenciesHelper::isZeroDecimal($transactionCurrency)) { |
| 84 |
$chargeAmount = (int)round($chargeAmount / 100); |
| 85 |
} |
| 86 |
|
| 87 |
$paymentArgs = array( |
| 88 |
'client_reference_id' => $order->uuid, |
| 89 |
'amount' => $chargeAmount, |
| 90 |
'currency' => strtolower($transactionCurrency), |
| 91 |
'description' => $storeName . ' #' . $order->invoice_no, // @todo: We will replace with order summary with item names later |
| 92 |
'customer_email' => $paymentInstance->order->email, |
| 93 |
'success_url' => $paymentInstance->transaction->getSuccessUrl(), |
| 94 |
'gateway_return_url' => Processor::getOnsiteGatewayReturnUrl($paymentInstance->transaction), |
| 95 |
'custom_payment_url' => PaymentHelper::getCustomPaymentLink($paymentInstance->order->uuid) |
| 96 |
); |
| 97 |
|
| 98 |
if ($paymentInstance->subscription) { |
| 99 |
$subscription = $paymentInstance->subscription; |
| 100 |
|
| 101 |
// Store-managed mode: charge the first order / renewal invoice one-time. |
| 102 |
// No Stripe subscription object, no manual→automatic conversion — the |
| 103 |
// invoice engine owns all future renewals. |
| 104 |
if ($this->shouldChargeSubscriptionAsOneTime($paymentInstance)) { |
| 105 |
// System subscriptions save the payment method for off-session |
| 106 |
// auto-charging of future renewal invoices (consent shown at checkout). |
| 107 |
if ($subscription->collection_method === 'system') { |
| 108 |
// Nothing payable now (free trial): a $0 PaymentIntent is invalid — |
| 109 |
// save the card via a SetupIntent instead. The trial-end invoice is |
| 110 |
// then charged off-session like any other system renewal. Hosted mode |
| 111 |
// never loads Stripe.js/Elements, so it needs a redirect-based |
| 112 |
// Checkout Session (mode: setup) instead of a client-side SetupIntent. |
| 113 |
if ((int) $paymentInstance->transaction->total <= 0) { |
| 114 |
$checkoutMode = $this->settings->get('checkout_mode') ?? 'onsite'; |
| 115 |
if ($checkoutMode === 'hosted') { |
| 116 |
return (new Processor())->handleHostedSetupOnlyCheckout($paymentInstance, $paymentArgs); |
| 117 |
} |
| 118 |
|
| 119 |
return (new Processor())->handleSetupOnlyPayment($paymentInstance, $paymentArgs); |
| 120 |
} |
| 121 |
|
| 122 |
$paymentArgs['setup_future_usage'] = 'off_session'; |
| 123 |
} |
| 124 |
|
| 125 |
return (new Processor())->handleSinglePayment($paymentInstance, $paymentArgs); |
| 126 |
} |
| 127 |
|
| 128 |
if ($subscription->collection_method === 'manual') { |
| 129 |
$previousPaymentMethod = $subscription->current_payment_method; |
| 130 |
$conversionResult = $this->convertManualSubscription($paymentInstance, $paymentArgs); |
| 131 |
if (is_wp_error($conversionResult)) { |
| 132 |
return $conversionResult; |
| 133 |
} |
| 134 |
|
| 135 |
$result = (new Processor())->handleSubscription($paymentInstance, $paymentArgs); |
| 136 |
|
| 137 |
if (is_wp_error($result)) { |
| 138 |
$subscription->update([ |
| 139 |
'collection_method' => 'manual', |
| 140 |
'current_payment_method' => $previousPaymentMethod, |
| 141 |
]); |
| 142 |
} else { |
| 143 |
$subscription->addLog( |
| 144 |
'Converted to automatic billing', |
| 145 |
sprintf('Subscription converted from manual to automatic billing via %s', 'Stripe'), |
| 146 |
'info' |
| 147 |
); |
| 148 |
do_action('fluent_cart/subscription_converted_to_automatic', [ |
| 149 |
'subscription' => $subscription, |
| 150 |
'payment_method' => 'stripe', |
| 151 |
]); |
| 152 |
} |
| 153 |
return $result; |
| 154 |
} |
| 155 |
|
| 156 |
return (new Processor())->handleSubscription($paymentInstance, $paymentArgs); |
| 157 |
} |
| 158 |
|
| 159 |
return (new Processor())->handleSinglePayment($paymentInstance, $paymentArgs); |
| 160 |
} |
| 161 |
|
| 162 |
public function convertManualSubscription($paymentInstance, $paymentArgs) |
| 163 |
{ |
| 164 |
$subscription = $paymentInstance->subscription; |
| 165 |
|
| 166 |
if (!$subscription || $subscription->collection_method !== 'manual') { |
| 167 |
return new \WP_Error('invalid_subscription', __('Subscription is not manual or does not exist', 'fluent-cart')); |
| 168 |
} |
| 169 |
|
| 170 |
if (in_array($subscription->status, ['completed'])) { |
| 171 |
return new \WP_Error('subscription_invalid_status', __('Cannot convert completed subscriptions', 'fluent-cart')); |
| 172 |
} |
| 173 |
|
| 174 |
$subscription->collection_method = 'automatic'; |
| 175 |
$subscription->current_payment_method = 'stripe'; |
| 176 |
$subscription->save(); |
| 177 |
|
| 178 |
return true; |
| 179 |
} |
| 180 |
|
| 181 |
/** |
| 182 |
* Stripe can vault a card without charging — the onsite Elements flow uses a |
| 183 |
* client-side SetupIntent, hosted mode redirects to a Checkout Session in |
| 184 |
* `mode: setup` (Processor::handleHostedSetupOnlyCheckout()). |
| 185 |
*/ |
| 186 |
public function supportsSetupWithoutCharge(): bool |
| 187 |
{ |
| 188 |
return true; |
| 189 |
} |
| 190 |
|
| 191 |
/** |
| 192 |
* Off-session charge of a system subscription's renewal invoice using the |
| 193 |
* stored token. Success flows through confirmPaymentSuccessByCharge so the |
| 194 |
* normal renewal-paid path (syncOrderStatuses / handleRenewalPaid) runs. |
| 195 |
* |
| 196 |
* @param PaymentInstance $paymentInstance |
| 197 |
* @param array $args ['attempt' => int] |
| 198 |
* @return true|'processing'|\WP_Error true = confirmed; 'processing' = charge |
| 199 |
* accepted, webhook will confirm |
| 200 |
*/ |
| 201 |
public function chargeRenewal(PaymentInstance $paymentInstance, $args = []) |
| 202 |
{ |
| 203 |
$order = $paymentInstance->order; |
| 204 |
$transaction = $paymentInstance->transaction; |
| 205 |
$subscription = $paymentInstance->subscription; |
| 206 |
|
| 207 |
if (!$order || !$transaction || !$subscription) { |
| 208 |
return new \WP_Error('invalid_instance', __('Renewal invoice is missing its order, transaction, or subscription.', 'fluent-cart')); |
| 209 |
} |
| 210 |
|
| 211 |
$customerId = $subscription->vendor_customer_id; |
| 212 |
|
| 213 |
// Token read AT FIRE TIME — never snapshotted. The meta has two shapes in |
| 214 |
// the wild: vendor_method_id (confirmation paths) and |
| 215 |
// details.payment_method_id (card-switch flow) — accept both. |
| 216 |
$paymentMethodMeta = $subscription->getMeta('active_payment_method', []) ?: []; |
| 217 |
$token = Arr::get($paymentMethodMeta, 'vendor_method_id') ?: Arr::get($paymentMethodMeta, 'details.payment_method_id'); |
| 218 |
|
| 219 |
if (!$customerId || !$token) { |
| 220 |
return new \WP_Error('missing_token', __('No saved payment method is available for this subscription.', 'fluent-cart')); |
| 221 |
} |
| 222 |
|
| 223 |
// The saved customer + payment method were created in the order's Stripe |
| 224 |
// mode; charging them requires that mode's secret key. If it is missing |
| 225 |
// (e.g. a live-mode order on a store configured with test keys only, common |
| 226 |
// on staging clones), fail with a clear message rather than sending an empty |
| 227 |
// Authorization header to Stripe. |
| 228 |
if ($keyError = $this->guardSecretKeyForMode($order->mode)) { |
| 229 |
return $keyError; |
| 230 |
} |
| 231 |
|
| 232 |
$chargeAmount = (int) $transaction->total; |
| 233 |
if ($transaction->currency && CurrenciesHelper::isZeroDecimal($transaction->currency)) { |
| 234 |
$chargeAmount = (int) round($chargeAmount / 100); |
| 235 |
} |
| 236 |
|
| 237 |
$intentData = [ |
| 238 |
'amount' => $chargeAmount, |
| 239 |
'currency' => strtolower($transaction->currency), |
| 240 |
'customer' => $customerId, |
| 241 |
'payment_method' => $token, |
| 242 |
'off_session' => 'true', |
| 243 |
'confirm' => 'true', |
| 244 |
'expand' => ['latest_charge'], |
| 245 |
'metadata' => apply_filters('fluent_cart/payments/stripe_metadata_onetime', [ |
| 246 |
'fct_ref_id' => $order->uuid, |
| 247 |
'Name' => $order->customer ? $order->customer->full_name : '', |
| 248 |
'Email' => $order->customer ? $order->customer->email : '', |
| 249 |
'order_reference' => 'fct_order_id_' . $order->id, |
| 250 |
], [ |
| 251 |
'order' => $order, |
| 252 |
'transaction' => $transaction |
| 253 |
]), |
| 254 |
]; |
| 255 |
|
| 256 |
$attempt = max(1, (int) Arr::get($args, 'attempt', 1)); |
| 257 |
|
| 258 |
$intent = (new API())->createStripeObject('payment_intents', $intentData, $order->mode, [ |
| 259 |
'Idempotency-Key' => 'fct_system_charge_' . $order->uuid . '_' . $attempt, |
| 260 |
]); |
| 261 |
|
| 262 |
if (is_wp_error($intent)) { |
| 263 |
return $intent; |
| 264 |
} |
| 265 |
|
| 266 |
$intentStatus = Arr::get($intent, 'status'); |
| 267 |
|
| 268 |
if ($intentStatus === 'succeeded') { |
| 269 |
$transaction->update(['vendor_charge_id' => Arr::get($intent, 'id')]); |
| 270 |
|
| 271 |
(new Confirmations())->confirmPaymentSuccessByCharge($transaction, [ |
| 272 |
'charge' => Arr::get($intent, 'latest_charge', []), |
| 273 |
'intent_id' => Arr::get($intent, 'id'), |
| 274 |
]); |
| 275 |
|
| 276 |
return true; |
| 277 |
} |
| 278 |
|
| 279 |
if ($intentStatus === 'processing') { |
| 280 |
// Charge accepted but still settling (e.g. bank debits) — the webhook |
| 281 |
// confirms it; keep the invoice scheduled rather than failing it. The |
| 282 |
// distinct return keeps the success contract honest: the service fires |
| 283 |
// system_charge_succeeded only once the payment is actually confirmed. |
| 284 |
$transaction->update(['vendor_charge_id' => Arr::get($intent, 'id')]); |
| 285 |
return 'processing'; |
| 286 |
} |
| 287 |
|
| 288 |
// requires_action (off-session SCA challenge), declines, and anything else: |
| 289 |
// the customer must pay interactively — surface the gateway's reason. |
| 290 |
$failureMessage = Arr::get($intent, 'last_payment_error.message'); |
| 291 |
if (!$failureMessage) { |
| 292 |
$failureMessage = sprintf( |
| 293 |
/* translators: %1$s: Stripe payment intent status */ |
| 294 |
__('Automatic charge could not be completed (status: %1$s).', 'fluent-cart'), |
| 295 |
$intentStatus ?: 'unknown' |
| 296 |
); |
| 297 |
} |
| 298 |
|
| 299 |
return new \WP_Error('charge_failed', $failureMessage); |
| 300 |
} |
| 301 |
|
| 302 |
/** |
| 303 |
* Re-check a processing off-session renewal charge. Recovers missed webhooks: |
| 304 |
* a settled intent is confirmed through confirmPaymentSuccessByCharge. |
| 305 |
* |
| 306 |
* @param PaymentInstance $paymentInstance |
| 307 |
* @return true|'processing'|\WP_Error |
| 308 |
*/ |
| 309 |
public function reconcileRenewalCharge(PaymentInstance $paymentInstance) |
| 310 |
{ |
| 311 |
$order = $paymentInstance->order; |
| 312 |
$transaction = $paymentInstance->transaction; |
| 313 |
|
| 314 |
if (!$order || !$transaction || !$transaction->vendor_charge_id) { |
| 315 |
return new \WP_Error('missing_intent', __('No payment intent is recorded for this renewal order.', 'fluent-cart')); |
| 316 |
} |
| 317 |
|
| 318 |
// A missing key is a configuration problem, not a transient API failure — |
| 319 |
// surface it (the transient handling below would otherwise loop forever). |
| 320 |
if ($keyError = $this->guardSecretKeyForMode($order->mode)) { |
| 321 |
return $keyError; |
| 322 |
} |
| 323 |
|
| 324 |
$intent = (new API())->getStripeObject('payment_intents/' . $transaction->vendor_charge_id, [ |
| 325 |
'expand' => ['latest_charge'] |
| 326 |
], $order->mode); |
| 327 |
|
| 328 |
if (is_wp_error($intent)) { |
| 329 |
// Transient API failure must not fail a possibly-settled payment — |
| 330 |
// report still-processing so the reconciliation loop retries later. |
| 331 |
return 'processing'; |
| 332 |
} |
| 333 |
|
| 334 |
$intentStatus = Arr::get($intent, 'status'); |
| 335 |
|
| 336 |
if ($intentStatus === 'succeeded') { |
| 337 |
(new Confirmations())->confirmPaymentSuccessByCharge($transaction, [ |
| 338 |
'charge' => Arr::get($intent, 'latest_charge', []), |
| 339 |
'intent_id' => Arr::get($intent, 'id'), |
| 340 |
]); |
| 341 |
|
| 342 |
return true; |
| 343 |
} |
| 344 |
|
| 345 |
if ($intentStatus === 'processing') { |
| 346 |
return 'processing'; |
| 347 |
} |
| 348 |
|
| 349 |
$failureMessage = Arr::get($intent, 'last_payment_error.message'); |
| 350 |
if (!$failureMessage) { |
| 351 |
$failureMessage = sprintf( |
| 352 |
/* translators: %1$s: Stripe payment intent status */ |
| 353 |
__('The pending payment could not be completed (status: %1$s).', 'fluent-cart'), |
| 354 |
$intentStatus ?: 'unknown' |
| 355 |
); |
| 356 |
} |
| 357 |
|
| 358 |
return new \WP_Error('charge_failed', $failureMessage); |
| 359 |
} |
| 360 |
|
| 361 |
public function syncRemoteTransaction(\FluentCart\App\Models\OrderTransaction $transaction) |
| 362 |
{ |
| 363 |
return (new Confirmations())->syncRemoteTransaction($transaction); |
| 364 |
} |
| 365 |
|
| 366 |
/** |
| 367 |
* Ensure the Stripe secret key for the given order mode is configured before an |
| 368 |
* off-session charge / reconcile. Returns a clear WP_Error when it is missing — |
| 369 |
* otherwise Stripe replies with the opaque "You did not provide an API key" |
| 370 |
* message. Null when the key is present. |
| 371 |
* |
| 372 |
* @param string $mode The order's Stripe mode ('test' | 'live'). |
| 373 |
* @return \WP_Error|null |
| 374 |
*/ |
| 375 |
private function guardSecretKeyForMode($mode) |
| 376 |
{ |
| 377 |
if ((new StripeSettingsBase())->getApiKey($mode ?: 'current')) { |
| 378 |
return null; |
| 379 |
} |
| 380 |
|
| 381 |
return new \WP_Error('stripe_missing_api_key', sprintf( |
| 382 |
/* translators: %1$s: Stripe mode (test or live) */ |
| 383 |
__('This subscription was created in %1$s mode, but no Stripe %1$s secret key is configured for this store. Add the matching Stripe keys in Payment Settings to charge the saved payment method.', 'fluent-cart'), |
| 384 |
$mode ?: 'current' |
| 385 |
)); |
| 386 |
} |
| 387 |
|
| 388 |
private function shouldRenderAsSubscriptionMode($hasSubscription): bool |
| 389 |
{ |
| 390 |
// One-time-charged subscription payments (store-managed mode, or a renewal of |
| 391 |
// a store-managed-born subscription) go through handleSinglePayment, so |
| 392 |
// Elements must initialize with intent mode `payment`, not `subscription`. |
| 393 |
if (\FluentCart\App\Modules\Subscriptions\Services\SubscriptionManagementMode::currentCheckoutChargesOneTime()) { |
| 394 |
return false; |
| 395 |
} |
| 396 |
|
| 397 |
return $hasSubscription; |
| 398 |
} |
| 399 |
|
| 400 |
public function processRefund($transaction, $amount, $args) |
| 401 |
{ |
| 402 |
if (!$amount) { |
| 403 |
return new \WP_Error( |
| 404 |
'fluent_cart_stripe_refund_error', |
| 405 |
__('Refund amount is required.', 'fluent-cart') |
| 406 |
); |
| 407 |
} |
| 408 |
|
| 409 |
return \FluentCart\App\Modules\PaymentMethods\StripeGateway\StripeHelper::processRemoteRefund($transaction, $amount, $args); |
| 410 |
} |
| 411 |
|
| 412 |
public function webHookPaymentMethodName() |
| 413 |
{ |
| 414 |
return $this->getMeta('route'); |
| 415 |
} |
| 416 |
|
| 417 |
public function handleIPN(): void |
| 418 |
{ |
| 419 |
(new IPN($this))->verifyAndProcess(); |
| 420 |
} |
| 421 |
|
| 422 |
public function getEnqueueScriptSrc($hasSubscription = 'no'): array |
| 423 |
{ |
| 424 |
$checkoutMode = $this->settings->get('checkout_mode') ?? 'onsite'; |
| 425 |
|
| 426 |
if ($checkoutMode == 'hosted') { |
| 427 |
return [ |
| 428 |
[ |
| 429 |
'handle' => 'fluent-cart-checkout-handler-stripe-hosted', |
| 430 |
'src' => Vite::getEnqueuePath('public/payment-methods/stripe-hosted-checkout.js'), |
| 431 |
] |
| 432 |
]; |
| 433 |
} |
| 434 |
|
| 435 |
// For embedded/onsite mode, load Stripe SDK and full handler |
| 436 |
return [ |
| 437 |
[ |
| 438 |
'handle' => 'fluent-cart-checkout-sdk-stripe', |
| 439 |
'src' => 'https://js.stripe.com/v3/', |
| 440 |
], |
| 441 |
[ |
| 442 |
'handle' => 'fluent-cart-checkout-handler-stripe', |
| 443 |
'src' => Vite::getEnqueuePath('public/payment-methods/stripe-checkout.js'), |
| 444 |
'deps' => ['fluent-cart-checkout-sdk-stripe'] |
| 445 |
] |
| 446 |
]; |
| 447 |
} |
| 448 |
|
| 449 |
private function getStripeLocale(): string |
| 450 |
{ |
| 451 |
$parts = explode('_', get_locale()); |
| 452 |
$lang = strtolower($parts[0]); |
| 453 |
$region = isset($parts[1]) ? strtoupper($parts[1]) : ''; |
| 454 |
|
| 455 |
if ($region) { |
| 456 |
$full = $lang . '-' . $region; |
| 457 |
if (in_array($full, ['en-GB', 'fr-CA', 'zh-HK', 'zh-TW', 'pt-BR', 'es-419'])) { |
| 458 |
return $full; |
| 459 |
} |
| 460 |
} |
| 461 |
|
| 462 |
return $lang ?: 'auto'; |
| 463 |
} |
| 464 |
|
| 465 |
public function getLocalizeData(): array |
| 466 |
{ |
| 467 |
return [ |
| 468 |
'fct_stripe_data' => [ |
| 469 |
'locale' => $this->getStripeLocale(), |
| 470 |
'translations' => [ |
| 471 |
'Payment module not available to checkout! Please reload again, or contact admin!' => __('Payment module not available to checkout! Please reload again, or contact admin!', 'fluent-cart'), |
| 472 |
'See Errors' => __('See Errors', 'fluent-cart'), |
| 473 |
'Pay Now' => __('Pay Now', 'fluent-cart'), |
| 474 |
'Place Order' => __('Place Order', 'fluent-cart'), |
| 475 |
'Card details are not valid!' => __('Card details are not valid!', 'fluent-cart'), |
| 476 |
'Total amount is not valid, please add some items to cart!' => __('Total amount is not valid, please add some items to cart!', 'fluent-cart'), |
| 477 |
'An error occurred while parsing the response.' => __('An error occurred while parsing the response.', 'fluent-cart'), |
| 478 |
'An error occurred while loading the payment method.' => __('An error occurred while loading the payment method.', 'fluent-cart'), |
| 479 |
'Loading Payment Processor...' => __('Loading Payment Processor...', 'fluent-cart'), |
| 480 |
'redirecting for action' => __('redirecting for action', 'fluent-cart'), |
| 481 |
'You will be redirected to Stripe to complete your payment securely.' => __('You will be redirected to Stripe to complete your payment securely.', 'fluent-cart'), |
| 482 |
'Something went wrong' => __('Something went wrong', 'fluent-cart'), |
| 483 |
'Payment confirmation failed' => __('Payment confirmation failed', 'fluent-cart'), |
| 484 |
] |
| 485 |
] |
| 486 |
]; |
| 487 |
} |
| 488 |
|
| 489 |
public static function beforeSettingsUpdate($data, $oldSettings): array |
| 490 |
{ |
| 491 |
$provider = Arr::get($data, 'provider', 'connect'); |
| 492 |
$mode = Arr::get($data, 'payment_mode', 'test'); |
| 493 |
|
| 494 |
if ('connect' == $provider) { |
| 495 |
$currentKey = Arr::get($data, $mode . '_secret_key', ''); |
| 496 |
$oldKey = Arr::get($oldSettings, $mode . '_secret_key', ''); |
| 497 |
|
| 498 |
if ($currentKey !== $oldKey) { |
| 499 |
$data[$mode . '_secret_key'] = Helper::encryptKey($currentKey); |
| 500 |
} |
| 501 |
} |
| 502 |
|
| 503 |
if (Arr::get($data, 'provider') === 'api_keys') { |
| 504 |
$data['test_publishable_key'] = ''; |
| 505 |
$data['live_publishable_key'] = ''; |
| 506 |
$data['test_secret_key'] = ''; |
| 507 |
$data['live_secret_key'] = ''; |
| 508 |
} |
| 509 |
|
| 510 |
return $data; |
| 511 |
} |
| 512 |
|
| 513 |
public static function validateSettings($data): array |
| 514 |
{ |
| 515 |
$mode = Arr::get($data, 'payment_mode', 'test'); |
| 516 |
$provider = Arr::get($data, 'provider', 'connect'); |
| 517 |
|
| 518 |
if ($provider === 'api_keys') { |
| 519 |
if ($mode === 'live') { |
| 520 |
$sk = defined('FCT_STRIPE_LIVE_SECRET_KEY') ? FCT_STRIPE_LIVE_SECRET_KEY : Arr::get($data, 'live_secret_key'); |
| 521 |
} else { |
| 522 |
$sk = defined('FCT_STRIPE_TEST_SECRET_KEY') ? FCT_STRIPE_TEST_SECRET_KEY : Arr::get($data, 'test_secret_key'); |
| 523 |
} |
| 524 |
} else { |
| 525 |
$sk = $mode === 'live' ? Arr::get($data, 'live_secret_key') : Arr::get($data, 'test_secret_key'); |
| 526 |
if (empty($sk)) { |
| 527 |
$errorMessage = $mode === 'live' ? __('Stripe not connected in live mode!', 'fluent-cart') : __('Stripe not connected in test mode!', 'fluent-cart'); |
| 528 |
return [ |
| 529 |
'status' => 'failed', |
| 530 |
'message' => $errorMessage |
| 531 |
]; |
| 532 |
} else { |
| 533 |
return [ |
| 534 |
'status' => 'success', |
| 535 |
'message' => __('Stripe account already verified!', 'fluent-cart') |
| 536 |
]; |
| 537 |
} |
| 538 |
} |
| 539 |
|
| 540 |
if (empty($sk)) { |
| 541 |
return [ |
| 542 |
'status' => 'failed', |
| 543 |
'message' => __('Please provide a valid secret key!', 'fluent-cart') |
| 544 |
]; |
| 545 |
} |
| 546 |
|
| 547 |
if ($mode === 'live' && !str_contains($sk, 'sk_live')) { |
| 548 |
return [ |
| 549 |
'status' => 'failed', |
| 550 |
'message' => __('Please provide a valid LIVE secret key!', 'fluent-cart') |
| 551 |
]; |
| 552 |
} else if ($mode === 'test' && !str_contains($sk, 'sk_test')) { |
| 553 |
return [ |
| 554 |
'status' => 'failed', |
| 555 |
'message' => __('Please provide a valid TEST secret key!', 'fluent-cart') |
| 556 |
]; |
| 557 |
} |
| 558 |
|
| 559 |
$response = (new API)->remoteRequest('account', [], $sk, 'GET'); |
| 560 |
|
| 561 |
if (isset($response['error'])) { |
| 562 |
return [ |
| 563 |
'status' => 'failed', |
| 564 |
'message' => $response['error']['message'] ? $response['error']['message'] : __('Invalid credentials!', 'fluent-cart') |
| 565 |
]; |
| 566 |
} |
| 567 |
|
| 568 |
if (!isset($response['id'])) { |
| 569 |
return [ |
| 570 |
'status' => 'failed', |
| 571 |
'message' => $response['error']['message'] ? $response['error']['message'] : __('Invalid credentials!', 'fluent-cart') |
| 572 |
]; |
| 573 |
} |
| 574 |
|
| 575 |
return [ |
| 576 |
'status' => 'success', |
| 577 |
'message' => __('Stripe account verified!', 'fluent-cart') |
| 578 |
]; |
| 579 |
} |
| 580 |
|
| 581 |
public function fields(): array |
| 582 |
{ |
| 583 |
$disabled = false; |
| 584 |
$providerValue = apply_filters('fluent_cart/form_disable_stripe_connect', $disabled, []) ? 'api_keys' : 'connect'; |
| 585 |
|
| 586 |
return array( |
| 587 |
'notice' => [ |
| 588 |
'value' => $this->renderStoreModeNotice(), |
| 589 |
'label' => __('Store Mode notice', 'fluent-cart'), |
| 590 |
'type' => 'notice' |
| 591 |
], |
| 592 |
'payment_mode' => [ |
| 593 |
'type' => 'tabs', |
| 594 |
'schema' => [ |
| 595 |
[ |
| 596 |
'type' => 'tab', |
| 597 |
'label' => __('Live credentials', 'fluent-cart'), |
| 598 |
'value' => 'live', |
| 599 |
'schema' => [] |
| 600 |
], |
| 601 |
[ |
| 602 |
'type' => 'tab', |
| 603 |
'label' => __('Test credentials', 'fluent-cart'), |
| 604 |
'value' => 'test', |
| 605 |
'schema' => [], |
| 606 |
] |
| 607 |
] |
| 608 |
], |
| 609 |
'provider' => array( |
| 610 |
'value' => $providerValue, |
| 611 |
'label' => __('Provider', 'fluent-cart'), |
| 612 |
'type' => 'provider' |
| 613 |
), |
| 614 |
'setup_guide' => array( |
| 615 |
'value' => '<h4>' . __('Or Setup keys manually.', 'fluent-cart') . '</h4><hr/>', |
| 616 |
'label' => __('Or Setup keys manually', 'fluent-cart'), |
| 617 |
'type' => 'html_attr', |
| 618 |
'visible' => 'no' |
| 619 |
), |
| 620 |
'checkout_mode' => array( |
| 621 |
'value' => 'onsite', |
| 622 |
'label' => __('Checkout Mode', 'fluent-cart'), |
| 623 |
'type' => 'radio', |
| 624 |
'options' => [ |
| 625 |
'onsite' => [ |
| 626 |
'label' => __('Embedded checkout (Recommended)', 'fluent-cart'), |
| 627 |
'text' => __('Renders inside your checkout page. Supports all standard card payments with a seamless branded experience.', 'fluent-cart'), |
| 628 |
'icon' => Vite::getAssetUrl('images/bill-line.svg') |
| 629 |
], |
| 630 |
'hosted' => [ |
| 631 |
'label' => __('Stripe Hosted Checkout', 'fluent-cart'), |
| 632 |
'text' => __("Redirects to Stripe's hosted page. Required for Bank Transfers and methods not supported in embedded mode.", 'fluent-cart'), |
| 633 |
'icon' => Vite::getAssetUrl('images/external-link-line.svg') |
| 634 |
] |
| 635 |
], |
| 636 |
'tooltip' => __('Choose between Embedded and Hosted checkout modes. Embedded mode is recommended for most use cases. For Bank transfers, use Hosted mode. (checkout.session.completed webhook event will be triggered only for hosted mode)', 'fluent-cart'), |
| 637 |
'description' => __("Embedded checkout is recommended for most use cases. For Bank transfers , or if any payment methods are not showing up on embedded checkout, try Hosted checkout. ('checkout.session.completed' webhook event will be triggered only for hosted checkout)", 'fluent-cart') |
| 638 |
), |
| 639 |
'submit_type' => array( |
| 640 |
'value' => 'auto', |
| 641 |
'label' => __('Submit Button Label', 'fluent-cart'), |
| 642 |
'type' => 'select', |
| 643 |
'filterable' => false, |
| 644 |
'options' => [ |
| 645 |
['value' => 'auto', 'label' => __('Automatic (Pay / Subscribe)', 'fluent-cart')], |
| 646 |
['value' => 'pay', 'label' => __('Pay', 'fluent-cart')], |
| 647 |
['value' => 'book', 'label' => __('Book', 'fluent-cart')], |
| 648 |
['value' => 'donate', 'label' => __('Donate', 'fluent-cart')], |
| 649 |
['value' => 'subscribe', 'label' => __('Subscribe', 'fluent-cart')], |
| 650 |
], |
| 651 |
'tooltip' => __('Stripe customises the submit button and surrounding copy from this. Applies to Stripe Hosted Checkout only.', 'fluent-cart'), |
| 652 |
'description' => __('Applies to Stripe Hosted Checkout only, and is ignored when a card is being saved without a charge. Automatic gives one-time orders the "Buy" button and subscriptions the "Subscribe" button.', 'fluent-cart') |
| 653 |
), |
| 654 |
'webhook_desc' => array( |
| 655 |
'value' => Webhook::webhookInstruction(), |
| 656 |
'label' => __('Webhook URL', 'fluent-cart'), |
| 657 |
'type' => 'html_attr' |
| 658 |
), |
| 659 |
'test_active_methods' => [ |
| 660 |
'value' => (new API())->getActivatedPaymentMethodsConfigs('test'), |
| 661 |
'label' => __('Activated Methods', 'fluent-cart'), |
| 662 |
'type' => 'active_methods' |
| 663 |
], |
| 664 |
'live_active_methods' => [ |
| 665 |
'value' => (new API())->getActivatedPaymentMethodsConfigs('live'), |
| 666 |
'label' => __('Activated Methods', 'fluent-cart'), |
| 667 |
'type' => 'active_methods' |
| 668 |
], |
| 669 |
); |
| 670 |
|
| 671 |
} |
| 672 |
|
| 673 |
public function getPublicKey($pre = '') |
| 674 |
{ |
| 675 |
return $this->settings->getPublicKey(); |
| 676 |
} |
| 677 |
|
| 678 |
public function getTransactionUrl($url, $data): string |
| 679 |
{ |
| 680 |
$transaction = Arr::get($data, 'transaction', null); |
| 681 |
if (!$transaction) { |
| 682 |
return $url; |
| 683 |
} |
| 684 |
|
| 685 |
if ($transaction->transaction_type === 'refund') { |
| 686 |
return 'https://dashboard.stripe.com/refunds/' . $transaction->vendor_charge_id; |
| 687 |
} |
| 688 |
|
| 689 |
return 'https://dashboard.stripe.com/payments/' . $transaction->vendor_charge_id; |
| 690 |
} |
| 691 |
|
| 692 |
public function getSubscriptionUrl($url, $data): string |
| 693 |
{ |
| 694 |
return 'https://dashboard.stripe.com/subscriptions/' . Arr::get($data, 'vendor_subscription_id'); |
| 695 |
} |
| 696 |
|
| 697 |
public function getOrderInfo($data) |
| 698 |
{ |
| 699 |
// For hosted mode, we don't need to return intent data as checkout session is created on order placement |
| 700 |
|
| 701 |
/* |
| 702 |
* Filter the Stripe Elements appearance configuration |
| 703 |
* |
| 704 |
* This filter allows developers to customize the appearance of Stripe Elements. |
| 705 |
* For example: |
| 706 |
* |
| 707 |
* function stripe_appearance($appearance) { |
| 708 |
* return array( |
| 709 |
* 'theme' => 'night', |
| 710 |
* 'labels' => 'floating', |
| 711 |
* 'variables' => array( |
| 712 |
* 'colorPrimary' => '#0570de', |
| 713 |
* 'colorBackground' => '#ffffff', |
| 714 |
* 'colorText' => '#30313d', |
| 715 |
* 'colorDanger' => '#df1b41', |
| 716 |
* 'fontFamily' => 'Ideal Sans, system-ui, sans-serif', |
| 717 |
* 'spacingUnit' => '2px', |
| 718 |
* 'borderRadius' => '4px', |
| 719 |
* ) |
| 720 |
* ); |
| 721 |
* } |
| 722 |
* add_filter('fluent_cart/stripe_appearance', 'stripe_appearance', 10, 1); |
| 723 |
* |
| 724 |
* @see https://docs.stripe.com/elements/appearance-api for all available options |
| 725 |
* @param array $appearance The appearance configuration |
| 726 |
* @return array The modified appearance configuration |
| 727 |
*/ |
| 728 |
if (($this->settings->get('checkout_mode') ?? 'onsite') == 'hosted') { |
| 729 |
// Same off-session consent contract as onsite checkout — the hosted |
| 730 |
// Checkout Session vaults the card via setup_future_usage too, so the |
| 731 |
// customer must see the same authorization copy before redirecting. |
| 732 |
$hasSubscription = $this->validateSubscriptions($this->getCheckoutItems()); |
| 733 |
$systemConsent = ''; |
| 734 |
$consentRequired = false; |
| 735 |
if (!$this->shouldRenderAsSubscriptionMode($hasSubscription) |
| 736 |
&& \FluentCart\App\Modules\Subscriptions\Services\SubscriptionManagementMode::currentCheckoutIsSystem($this) |
| 737 |
) { |
| 738 |
$systemConsent = __('Your payment method will be saved securely and charged automatically on each renewal date. You can update or replace it any time from your account.', 'fluent-cart'); |
| 739 |
|
| 740 |
// Zero-payable (free trial): handleSetupOnlyPayment REQUIRES |
| 741 |
// _fct_system_consent=yes — same gate as the onsite setup-mode path. |
| 742 |
$cart = CartHelper::getCart(); |
| 743 |
$payableNow = \FluentCart\App\Services\OrderService::getItemsAmountTotal($cart->cart_data ?? [], false, false); |
| 744 |
if ($payableNow <= 0) { |
| 745 |
$consentRequired = true; |
| 746 |
} |
| 747 |
} |
| 748 |
|
| 749 |
wp_send_json( |
| 750 |
[ |
| 751 |
'status' => 'success', |
| 752 |
'message' => __('Order info retrieved!', 'fluent-cart'), |
| 753 |
'data' => [], |
| 754 |
'payment_args' => [ |
| 755 |
'checkout_mode' => 'hosted' |
| 756 |
], |
| 757 |
'system_consent' => $systemConsent, |
| 758 |
'consent_required' => $consentRequired, |
| 759 |
], |
| 760 |
200 |
| 761 |
); |
| 762 |
} |
| 763 |
|
| 764 |
$cart = CartHelper::getCart(); |
| 765 |
$checkOutHelper = CartCheckoutHelper::make(); |
| 766 |
$shippingChargeData = (new WebCheckoutHandler())->getShippingChargeData($cart); |
| 767 |
$shippingCharge = Arr::get($shippingChargeData, 'charge'); |
| 768 |
$totalPrice = $checkOutHelper->getItemsAmountTotal(false) + $shippingCharge; |
| 769 |
|
| 770 |
$tax = $checkOutHelper->getCart()->checkout_data['tax_data'] ?? []; |
| 771 |
$taxBehavior = (int) Arr::get($tax, 'tax_behavior', 0); |
| 772 |
$storeTaxBehavior = (int) Arr::get($tax, 'store_tax_behavior', $taxBehavior); |
| 773 |
|
| 774 |
if ($taxBehavior === 1) { |
| 775 |
// Pure exclusive — add all tax including fee tax (tax_total contains both). |
| 776 |
$totalPrice = $totalPrice + (int) Arr::get($tax, 'tax_total', 0) |
| 777 |
+ (int) Arr::get($tax, 'shipping_tax', 0); |
| 778 |
} elseif ($taxBehavior === 3) { |
| 779 |
// Mixed — add only exclusive product tax + fee/shipping if store is exclusive. |
| 780 |
$totalPrice = $totalPrice + (int) Arr::get($tax, 'exclusive_tax_total', 0); |
| 781 |
if ($storeTaxBehavior === 1) { |
| 782 |
$totalPrice = $totalPrice + (int) Arr::get($tax, 'fee_tax', 0) |
| 783 |
+ (int) Arr::get($tax, 'shipping_tax', 0); |
| 784 |
} |
| 785 |
} |
| 786 |
|
| 787 |
$items = $this->getCheckoutItems(); |
| 788 |
|
| 789 |
$hasSubscription = $this->validateSubscriptions($items); |
| 790 |
|
| 791 |
$stripeSettings = new StripeSettingsBase(); |
| 792 |
$publicKey = $stripeSettings->getPublicKey(); |
| 793 |
|
| 794 |
if (empty($publicKey)) { |
| 795 |
fluent_cart_add_log( |
| 796 |
'Stripe Credential Validation', |
| 797 |
sprintf('Stripe %s keys are missing or invalid.', $stripeSettings->getMode()), |
| 798 |
'error', |
| 799 |
['log_type' => 'payment'] |
| 800 |
); |
| 801 |
wp_send_json([ |
| 802 |
'status' => 'failed', |
| 803 |
'message' => __('No valid public key found! Please contact the site administrator.', 'fluent-cart') |
| 804 |
], 422); |
| 805 |
} |
| 806 |
|
| 807 |
$paymentArgs['public_key'] = $publicKey; |
| 808 |
$appearance = $this->getElementsAppearance(); |
| 809 |
$fonts = $this->getElementsFonts(); |
| 810 |
|
| 811 |
$storeCurrency = CurrencySettings::get('currency'); |
| 812 |
$intentAmount = (int)$totalPrice; |
| 813 |
|
| 814 |
if ($storeCurrency && CurrenciesHelper::isZeroDecimal($storeCurrency)) { |
| 815 |
$intentAmount = (int)($intentAmount / 100); |
| 816 |
} |
| 817 |
|
| 818 |
$intentData = [ |
| 819 |
'mode' => 'payment', |
| 820 |
'amount' => $intentAmount, |
| 821 |
'currency' => strtolower($storeCurrency), |
| 822 |
'automatic_payment_methods' => ['enabled' => true] |
| 823 |
]; |
| 824 |
|
| 825 |
// Determine if we should render as subscription mode |
| 826 |
// This considers global mode, auto-convert, and fallback settings |
| 827 |
$renderAsSubscription = $this->shouldRenderAsSubscriptionMode($hasSubscription); |
| 828 |
|
| 829 |
// System (auto-charged, store-billed) checkout: one-time payment intent that |
| 830 |
// also stores an off-session mandate, plus the save-and-auto-charge consent |
| 831 |
// notice rendered under the payment element. |
| 832 |
$systemConsent = ''; |
| 833 |
$consentRequired = false; |
| 834 |
if (!$renderAsSubscription && \FluentCart\App\Modules\Subscriptions\Services\SubscriptionManagementMode::currentCheckoutIsSystem($this)) { |
| 835 |
$intentData['setup_future_usage'] = 'off_session'; |
| 836 |
$systemConsent = __('Your payment method will be saved securely and charged automatically on each renewal date. You can update or replace it any time from your account.', 'fluent-cart'); |
| 837 |
|
| 838 |
// Nothing payable now (free trial): Elements must initialize in SETUP |
| 839 |
// mode — a zero-amount payment mode is invalid — and consent becomes a |
| 840 |
// required checkbox: without a saved card the trial can never bill. |
| 841 |
$payableNow = \FluentCart\App\Services\OrderService::getItemsAmountTotal($cart->cart_data ?? [], false, false); |
| 842 |
if ($payableNow <= 0) { |
| 843 |
$intentData = [ |
| 844 |
'mode' => 'setup', |
| 845 |
'currency' => strtolower($storeCurrency), |
| 846 |
]; |
| 847 |
$consentRequired = true; |
| 848 |
} |
| 849 |
} |
| 850 |
|
| 851 |
if ($renderAsSubscription) { |
| 852 |
$intentData['mode'] = 'subscription'; |
| 853 |
$intentData['setup_future_usage'] = 'off_session'; |
| 854 |
} elseif (empty($intentData['setup_future_usage']) && Arr::get($data, 'save_payment_method') === 'yes') { |
| 855 |
$intentData['setup_future_usage'] = 'on_session'; |
| 856 |
} |
| 857 |
|
| 858 |
// The browser cannot pass setup_future_usage per-request (this endpoint |
| 859 |
// receives no body), so extensions that vault cards (e.g. saved payment |
| 860 |
// methods) resolve it here, server-side. Must be matched on the actual |
| 861 |
// PaymentIntent at place-order (fluent_cart/payments/stripe_onetime_intent_args) |
| 862 |
// or Stripe rejects the confirmation for a setup_future_usage mismatch. |
| 863 |
$setupFutureUsage = apply_filters( |
| 864 |
'fluent_cart/stripe/client_setup_future_usage', |
| 865 |
Arr::get($intentData, 'setup_future_usage'), |
| 866 |
['data' => $data, 'has_subscription' => $hasSubscription] |
| 867 |
); |
| 868 |
if ($setupFutureUsage) { |
| 869 |
$intentData['setup_future_usage'] = $setupFutureUsage; |
| 870 |
} else { |
| 871 |
unset($intentData['setup_future_usage']); |
| 872 |
} |
| 873 |
|
| 874 |
wp_send_json( |
| 875 |
[ |
| 876 |
'status' => 'success', |
| 877 |
'message' => __('Order info retrieved!', 'fluent-cart'), |
| 878 |
'data' => [], |
| 879 |
'payment_args' => $paymentArgs, |
| 880 |
'intent' => $intentData, |
| 881 |
'appearance' => $appearance, |
| 882 |
'fonts' => $fonts, |
| 883 |
'system_consent' => $systemConsent, |
| 884 |
'consent_required' => $consentRequired, |
| 885 |
], |
| 886 |
200 |
| 887 |
); |
| 888 |
} |
| 889 |
|
| 890 |
public function getElementsAppearance(): array |
| 891 |
{ |
| 892 |
$appearance = ['theme' => 'stripe']; |
| 893 |
|
| 894 |
return (array) apply_filters('fluent_cart/stripe_appearance', $appearance); |
| 895 |
} |
| 896 |
|
| 897 |
public function getElementsFonts(): array |
| 898 |
{ |
| 899 |
$fonts = (array) apply_filters('fluent_cart/stripe_elements_fonts', []); |
| 900 |
|
| 901 |
return array_values(array_filter($fonts, 'is_array')); |
| 902 |
} |
| 903 |
|
| 904 |
public function getConnectInfo(): array |
| 905 |
{ |
| 906 |
return ConnectConfig::getConnectConfig(); |
| 907 |
} |
| 908 |
|
| 909 |
public function disconnect($mode): bool |
| 910 |
{ |
| 911 |
return ConnectConfig::disconnect($mode); |
| 912 |
} |
| 913 |
|
| 914 |
public function getCounterDisputeUrl($transaction) |
| 915 |
{ |
| 916 |
if (!$transaction->meta['dispute_id']) { |
| 917 |
return ''; |
| 918 |
} |
| 919 |
|
| 920 |
return 'https://dashboard.stripe.com/disputes/' . $transaction->meta['dispute_id']; |
| 921 |
} |
| 922 |
|
| 923 |
public function acceptRemoteDispute($transaction, $args = []) |
| 924 |
{ |
| 925 |
$disputeId = Arr::get($transaction->meta, 'dispute_id'); |
| 926 |
if (!$disputeId) { |
| 927 |
$charge = (new API())->getStripeObject('payment_intents/' . $transaction->vendor_charge_id, ['expand' => ['latest_charge']], $transaction->payment_mode); |
| 928 |
|
| 929 |
if (is_wp_error($charge) || empty($charge['dispute'])) { |
| 930 |
new \WP_Error('No dispute ID found!', __('Please check stripe if the dispute is already accepted or not!', 'fluent-cart')); |
| 931 |
} |
| 932 |
|
| 933 |
$disputeId = Arr::get($charge, 'dispute', ''); |
| 934 |
} |
| 935 |
|
| 936 |
$closeDispute = (new API())->createStripeObject('disputes/' . $disputeId . '/close', [], $transaction->payment_mode); |
| 937 |
|
| 938 |
if (is_wp_error($closeDispute)) { |
| 939 |
return $closeDispute; |
| 940 |
} |
| 941 |
|
| 942 |
return $closeDispute; |
| 943 |
} |
| 944 |
|
| 945 |
public function isCurrencySupported(): bool |
| 946 |
{ |
| 947 |
// stripe support all listed currencies except for IRR (Iranian Rial) |
| 948 |
return strtoupper(CurrencySettings::get('currency')) !== 'IRR'; |
| 949 |
} |
| 950 |
|
| 951 |
} |
| 952 |
|