PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.5.3
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.5.3
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 trunk All 48 releases
fluent-cart / app / Modules / PaymentMethods / StripeGateway / Stripe.php

Stripe.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.5.3, at app/Modules/PaymentMethods/StripeGateway/Stripe.php

564 lines 22.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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'];
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',
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' => $this->getSuccessUrl($paymentInstance->transaction),
94 'custom_payment_url' => PaymentHelper::getCustomPaymentLink($paymentInstance->order->uuid)
95 );
96
97 if ($paymentInstance->subscription) {
98 return (new Processor())->handleSubscription($paymentInstance, $paymentArgs);
99 }
100
101 return (new Processor())->handleSinglePayment($paymentInstance, $paymentArgs);
102 }
103
104 public function processRefund($transaction, $amount, $args)
105 {
106 if (!$amount) {
107 return new \WP_Error(
108 'fluent_cart_stripe_refund_error',
109 __('Refund amount is required.', 'fluent-cart')
110 );
111 }
112
113 return \FluentCart\App\Modules\PaymentMethods\StripeGateway\StripeHelper::processRemoteRefund($transaction, $amount, $args);
114 }
115
116 public function webHookPaymentMethodName()
117 {
118 return $this->getMeta('route');
119 }
120
121 public function handleIPN(): void
122 {
123 (new IPN($this))->verifyAndProcess();
124 }
125
126 public function getEnqueueScriptSrc($hasSubscription = 'no'): array
127 {
128 $checkoutMode = $this->settings->get('checkout_mode') ?? 'onsite';
129
130 if ($checkoutMode == 'hosted') {
131 return [
132 [
133 'handle' => 'fluent-cart-checkout-handler-stripe-hosted',
134 'src' => Vite::getEnqueuePath('public/payment-methods/stripe-hosted-checkout.js'),
135 ]
136 ];
137 }
138
139 // For embedded/onsite mode, load Stripe SDK and full handler
140 return [
141 [
142 'handle' => 'fluent-cart-checkout-sdk-stripe',
143 'src' => 'https://js.stripe.com/v3/',
144 ],
145 [
146 'handle' => 'fluent-cart-checkout-handler-stripe',
147 'src' => Vite::getEnqueuePath('public/payment-methods/stripe-checkout.js'),
148 'deps' => ['fluent-cart-checkout-sdk-stripe']
149 ]
150 ];
151 }
152
153 private function getStripeLocale(): string
154 {
155 $parts = explode('_', get_locale());
156 $lang = strtolower($parts[0]);
157 $region = isset($parts[1]) ? strtoupper($parts[1]) : '';
158
159 if ($region) {
160 $full = $lang . '-' . $region;
161 if (in_array($full, ['en-GB', 'fr-CA', 'zh-HK', 'zh-TW', 'pt-BR', 'es-419'])) {
162 return $full;
163 }
164 }
165
166 return $lang ?: 'auto';
167 }
168
169 public function getLocalizeData(): array
170 {
171 return [
172 'fct_stripe_data' => [
173 'locale' => $this->getStripeLocale(),
174 'translations' => [
175 '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'),
176 'See Errors' => __('See Errors', 'fluent-cart'),
177 'Pay Now' => __('Pay Now', 'fluent-cart'),
178 'Place Order' => __('Place Order', 'fluent-cart'),
179 'Card details are not valid!' => __('Card details are not valid!', 'fluent-cart'),
180 'Total amount is not valid, please add some items to cart!' => __('Total amount is not valid, please add some items to cart!', 'fluent-cart'),
181 'An error occurred while parsing the response.' => __('An error occurred while parsing the response.', 'fluent-cart'),
182 'An error occurred while loading the payment method.' => __('An error occurred while loading the payment method.', 'fluent-cart'),
183 'Loading Payment Processor...' => __('Loading Payment Processor...', 'fluent-cart'),
184 'redirecting for action' => __('redirecting for action', 'fluent-cart'),
185 'You will be redirected to Stripe to complete your payment securely.' => __('You will be redirected to Stripe to complete your payment securely.', 'fluent-cart'),
186 'Something went wrong' => __('Something went wrong', 'fluent-cart'),
187 'Payment confirmation failed' => __('Payment confirmation failed', 'fluent-cart'),
188 ]
189 ]
190 ];
191 }
192
193 public static function beforeSettingsUpdate($data, $oldSettings): array
194 {
195 $provider = Arr::get($data, 'provider', 'connect');
196 $mode = Arr::get($data, 'payment_mode', 'test');
197
198 if ('connect' == $provider) {
199 $currentKey = Arr::get($data, $mode . '_secret_key', '');
200 $oldKey = Arr::get($oldSettings, $mode . '_secret_key', '');
201
202 if ($currentKey !== $oldKey) {
203 $data[$mode . '_secret_key'] = Helper::encryptKey($currentKey);
204 }
205 }
206
207 if (Arr::get($data, 'provider') === 'api_keys') {
208 $data['test_publishable_key'] = '';
209 $data['live_publishable_key'] = '';
210 $data['test_secret_key'] = '';
211 $data['live_secret_key'] = '';
212 }
213
214 return $data;
215 }
216
217 public static function validateSettings($data): array
218 {
219 $mode = Arr::get($data, 'payment_mode', 'test');
220 $provider = Arr::get($data, 'provider', 'connect');
221
222 if ($provider === 'api_keys') {
223 if ($mode === 'live') {
224 $sk = defined('FCT_STRIPE_LIVE_SECRET_KEY') ? FCT_STRIPE_LIVE_SECRET_KEY : Arr::get($data, 'live_secret_key');
225 } else {
226 $sk = defined('FCT_STRIPE_TEST_SECRET_KEY') ? FCT_STRIPE_TEST_SECRET_KEY : Arr::get($data, 'test_secret_key');
227 }
228 } else {
229 $sk = $mode === 'live' ? Arr::get($data, 'live_secret_key') : Arr::get($data, 'test_secret_key');
230 if (empty($sk)) {
231 $errorMessage = $mode === 'live' ? __('Stripe not connected in live mode!', 'fluent-cart') : __('Stripe not connected in test mode!', 'fluent-cart');
232 return [
233 'status' => 'failed',
234 'message' => $errorMessage
235 ];
236 } else {
237 return [
238 'status' => 'success',
239 'message' => __('Stripe account already verified!', 'fluent-cart')
240 ];
241 }
242 }
243
244 if (empty($sk)) {
245 return [
246 'status' => 'failed',
247 'message' => __('Please provide a valid secret key!', 'fluent-cart')
248 ];
249 }
250
251 if ($mode === 'live' && !str_contains($sk, 'sk_live')) {
252 return [
253 'status' => 'failed',
254 'message' => __('Please provide a valid LIVE secret key!', 'fluent-cart')
255 ];
256 } else if ($mode === 'test' && !str_contains($sk, 'sk_test')) {
257 return [
258 'status' => 'failed',
259 'message' => __('Please provide a valid TEST secret key!', 'fluent-cart')
260 ];
261 }
262
263 $response = (new API)->remoteRequest('account', [], $sk, 'GET');
264
265 if (isset($response['error'])) {
266 return [
267 'status' => 'failed',
268 'message' => $response['error']['message'] ? $response['error']['message'] : __('Invalid credentials!', 'fluent-cart')
269 ];
270 }
271
272 if (!isset($response['id'])) {
273 return [
274 'status' => 'failed',
275 'message' => $response['error']['message'] ? $response['error']['message'] : __('Invalid credentials!', 'fluent-cart')
276 ];
277 }
278
279 return [
280 'status' => 'success',
281 'message' => __('Stripe account verified!', 'fluent-cart')
282 ];
283 }
284
285 public function fields(): array
286 {
287 $disabled = apply_filters_deprecated('fluent_cart_form_disable_stripe_connect', [false, []], '1.3.16', 'fluent_cart/form_disable_stripe_connect', 'Use fluent_cart/form_disable_stripe_connect instead of fluent_cart_form_disable_stripe_connect. It will be removed in v1.4.3.');
288 $providerValue = apply_filters('fluent_cart/form_disable_stripe_connect', $disabled, []) ? 'api_keys' : 'connect';
289
290 return array(
291 'notice' => [
292 'value' => $this->renderStoreModeNotice(),
293 'label' => __('Store Mode notice', 'fluent-cart'),
294 'type' => 'notice'
295 ],
296 'payment_mode' => [
297 'type' => 'tabs',
298 'schema' => [
299 [
300 'type' => 'tab',
301 'label' => __('Live credentials', 'fluent-cart'),
302 'value' => 'live',
303 'schema' => []
304 ],
305 [
306 'type' => 'tab',
307 'label' => __('Test credentials', 'fluent-cart'),
308 'value' => 'test',
309 'schema' => [],
310 ]
311 ]
312 ],
313 'provider' => array(
314 'value' => $providerValue,
315 'label' => __('Provider', 'fluent-cart'),
316 'type' => 'provider'
317 ),
318 'setup_guide' => array(
319 'value' => '<h4>' . __('Or Setup keys manually.', 'fluent-cart') . '</h4><hr/>',
320 'label' => __('Or Setup keys manually', 'fluent-cart'),
321 'type' => 'html_attr',
322 'visible' => 'no'
323 ),
324 'checkout_mode' => array(
325 'value' => 'onsite',
326 'label' => __('Checkout Mode', 'fluent-cart'),
327 'type' => 'radio',
328 'options' => [
329 'onsite' => [
330 'label' => __('Embedded checkout (Recommended)', 'fluent-cart'),
331 'text' => __('Renders inside your checkout page. Supports all standard card payments with a seamless branded experience.', 'fluent-cart'),
332 'icon' => Vite::getAssetUrl('images/bill-line.svg')
333 ],
334 'hosted' => [
335 'label' => __('Stripe Hosted Checkout', 'fluent-cart'),
336 'text' => __("Redirects to Stripe's hosted page. Required for Bank Transfers and methods not supported in embedded mode.", 'fluent-cart'),
337 'icon' => Vite::getAssetUrl('images/external-link-line.svg')
338 ]
339 ],
340 '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'),
341 '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')
342 ),
343 'webhook_desc' => array(
344 'value' => Webhook::webhookInstruction(),
345 'label' => __('Webhook URL', 'fluent-cart'),
346 'type' => 'html_attr'
347 ),
348 'test_active_methods' => [
349 'value' => (new API())->getActivatedPaymentMethodsConfigs('test'),
350 'label' => __('Activated Methods', 'fluent-cart'),
351 'type' => 'active_methods'
352 ],
353 'live_active_methods' => [
354 'value' => (new API())->getActivatedPaymentMethodsConfigs('live'),
355 'label' => __('Activated Methods', 'fluent-cart'),
356 'type' => 'active_methods'
357 ],
358 );
359
360 }
361
362 public function getPublicKey($pre = '')
363 {
364 return $this->settings->getPublicKey();
365 }
366
367 public function getTransactionUrl($url, $data): string
368 {
369 $transaction = Arr::get($data, 'transaction', null);
370 if (!$transaction) {
371 return $url;
372 }
373
374 if ($transaction->transaction_type === 'refund') {
375 return 'https://dashboard.stripe.com/refunds/' . $transaction->vendor_charge_id;
376 }
377
378 return 'https://dashboard.stripe.com/payments/' . $transaction->vendor_charge_id;
379 }
380
381 public function getSubscriptionUrl($url, $data): string
382 {
383 return 'https://dashboard.stripe.com/subscriptions/' . Arr::get($data, 'vendor_subscription_id');
384 }
385
386 public function getOrderInfo($data)
387 {
388 // For hosted mode, we don't need to return intent data as checkout session is created on order placement
389
390 /*
391 * Filter the Stripe Elements appearance configuration
392 *
393 * This filter allows developers to customize the appearance of Stripe Elements.
394 * For example:
395 *
396 * function stripe_appearance($appearance) {
397 * return array(
398 * 'theme' => 'night',
399 * 'labels' => 'floating',
400 * 'variables' => array(
401 * 'colorPrimary' => '#0570de',
402 * 'colorBackground' => '#ffffff',
403 * 'colorText' => '#30313d',
404 * 'colorDanger' => '#df1b41',
405 * 'fontFamily' => 'Ideal Sans, system-ui, sans-serif',
406 * 'spacingUnit' => '2px',
407 * 'borderRadius' => '4px',
408 * )
409 * );
410 * }
411 * add_filter('fluent_cart/stripe_appearance', 'stripe_appearance', 10, 1);
412 *
413 * @see https://docs.stripe.com/elements/appearance-api for all available options
414 * @param array $appearance The appearance configuration
415 * @return array The modified appearance configuration
416 */
417 if (($this->settings->get('checkout_mode') ?? 'onsite') == 'hosted') {
418 wp_send_json(
419 [
420 'status' => 'success',
421 'message' => __('Order info retrieved!', 'fluent-cart'),
422 'data' => [],
423 'payment_args' => [
424 'checkout_mode' => 'hosted'
425 ],
426 ],
427 200
428 );
429 }
430
431 $cart = CartHelper::getCart();
432 $checkOutHelper = CartCheckoutHelper::make();
433 $shippingChargeData = (new WebCheckoutHandler())->getShippingChargeData($cart);
434 $shippingCharge = Arr::get($shippingChargeData, 'charge');
435 $totalPrice = $checkOutHelper->getItemsAmountTotal(false) + $shippingCharge;
436
437 $tax = $checkOutHelper->getCart()->checkout_data['tax_data'] ?? [];
438 $taxBehavior = (int) Arr::get($tax, 'tax_behavior', 0);
439 $storeTaxBehavior = (int) Arr::get($tax, 'store_tax_behavior', $taxBehavior);
440
441 if ($taxBehavior === 1) {
442 // Pure exclusive — add all tax including fee tax (tax_total contains both).
443 $totalPrice = $totalPrice + (int) Arr::get($tax, 'tax_total', 0)
444 + (int) Arr::get($tax, 'shipping_tax', 0);
445 } elseif ($taxBehavior === 3) {
446 // Mixed — add only exclusive product tax + fee/shipping if store is exclusive.
447 $totalPrice = $totalPrice + (int) Arr::get($tax, 'exclusive_tax_total', 0);
448 if ($storeTaxBehavior === 1) {
449 $totalPrice = $totalPrice + (int) Arr::get($tax, 'fee_tax', 0)
450 + (int) Arr::get($tax, 'shipping_tax', 0);
451 }
452 }
453
454 $items = $this->getCheckoutItems();
455
456 $hasSubscription = $this->validateSubscriptions($items);
457
458 $stripeSettings = new StripeSettingsBase();
459 $publicKey = $stripeSettings->getPublicKey();
460
461 if (empty($publicKey)) {
462 fluent_cart_add_log(
463 'Stripe Credential Validation',
464 sprintf('Stripe %s keys are missing or invalid.', $stripeSettings->getMode()),
465 'error',
466 ['log_type' => 'payment']
467 );
468 wp_send_json([
469 'status' => 'failed',
470 'message' => __('No valid public key found! Please contact the site administrator.', 'fluent-cart')
471 ], 422);
472 }
473
474 $paymentArgs['public_key'] = $publicKey;
475
476 // Allow filtering the appearance configuration for Stripe Elements
477 $appearance = apply_filters_deprecated('fluent_cart_stripe_appearance', [
478 ['theme' => 'stripe']
479 ], '1.3.16', 'fluent_cart/stripe_appearance', 'Use fluent_cart/stripe_appearance instead of fluent_cart_stripe_appearance. It will be removed in v1.4.3.');
480 $appearance = apply_filters('fluent_cart/stripe_appearance', $appearance);
481
482 $storeCurrency = CurrencySettings::get('currency');
483 $intentAmount = (int)$totalPrice;
484
485 if ($storeCurrency && CurrenciesHelper::isZeroDecimal($storeCurrency)) {
486 $intentAmount = (int)($intentAmount / 100);
487 }
488
489 $intentData = [
490 'mode' => 'payment',
491 'amount' => $intentAmount,
492 'currency' => strtolower($storeCurrency),
493 'automatic_payment_methods' => ['enabled' => true]
494 ];
495
496 if ($hasSubscription) {
497 $intentData['mode'] = 'subscription';
498 $intentData['setup_future_usage'] = 'off_session';
499 } elseif (Arr::get($data, 'save_payment_method') === 'yes') {
500 $intentData['setup_future_usage'] = 'on_session';
501 }
502
503 wp_send_json(
504 [
505 'status' => 'success',
506 'message' => __('Order info retrieved!', 'fluent-cart'),
507 'data' => [],
508 'payment_args' => $paymentArgs,
509 'intent' => $intentData,
510 'appearance' => $appearance,
511 ],
512 200
513 );
514 }
515
516 public function getConnectInfo(): array
517 {
518 return ConnectConfig::getConnectConfig();
519 }
520
521 public function disconnect($mode): bool
522 {
523 return ConnectConfig::disconnect($mode);
524 }
525
526 public function getCounterDisputeUrl($transaction)
527 {
528 if (!$transaction->meta['dispute_id']) {
529 return '';
530 }
531
532 return 'https://dashboard.stripe.com/disputes/' . $transaction->meta['dispute_id'];
533 }
534
535 public function acceptRemoteDispute($transaction, $args = [])
536 {
537 $disputeId = Arr::get($transaction->meta, 'dispute_id');
538 if (!$disputeId) {
539 $charge = (new API())->getStripeObject('payment_intents/' . $transaction->vendor_charge_id, ['expand' => ['latest_charge']]);
540
541 if (is_wp_error($charge) || empty($charge['dispute'])) {
542 new \WP_Error('No dispute ID found!', __('Please check stripe if the dispute is already accepted or not!', 'fluent-cart'));
543 }
544
545 $disputeId = Arr::get($charge, 'dispute', '');
546 }
547
548 $closeDispute = (new API())->createStripeObject('disputes/' . $disputeId . '/close');
549
550 if (is_wp_error($closeDispute)) {
551 return $closeDispute;
552 }
553
554 return $closeDispute;
555 }
556
557 public function isCurrencySupported(): bool
558 {
559 // stripe support all listed currencies except for IRR (Iranian Rial)
560 return strtoupper(CurrencySettings::get('currency')) !== 'IRR';
561 }
562
563 }
564