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

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

556 lines 22.2 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' => '#136196',
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.');
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' => __('Embedded checkout (Recommended)', 'fluent-cart'),
330 'hosted' => __('Stripe Hosted checkout', 'fluent-cart')
331 ],
332 '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'),
333 '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')
334 ),
335 'webhook_desc' => array(
336 'value' => Webhook::webhookInstruction(),
337 'label' => __('Webhook URL', 'fluent-cart'),
338 'type' => 'html_attr'
339 ),
340 'test_active_methods' => [
341 'value' => (new API())->getActivatedPaymentMethodsConfigs('test'),
342 'label' => __('Activated Methods', 'fluent-cart'),
343 'type' => 'active_methods'
344 ],
345 'live_active_methods' => [
346 'value' => (new API())->getActivatedPaymentMethodsConfigs('live'),
347 'label' => __('Activated Methods', 'fluent-cart'),
348 'type' => 'active_methods'
349 ],
350 );
351
352 }
353
354 public function getPublicKey($pre = '')
355 {
356 return $this->settings->getPublicKey();
357 }
358
359 public function getTransactionUrl($url, $data): string
360 {
361 $transaction = Arr::get($data, 'transaction', null);
362 if (!$transaction) {
363 return $url;
364 }
365
366 if ($transaction->transaction_type === 'refund') {
367 return 'https://dashboard.stripe.com/refunds/' . $transaction->vendor_charge_id;
368 }
369
370 return 'https://dashboard.stripe.com/payments/' . $transaction->vendor_charge_id;
371 }
372
373 public function getSubscriptionUrl($url, $data): string
374 {
375 return 'https://dashboard.stripe.com/subscriptions/' . Arr::get($data, 'vendor_subscription_id');
376 }
377
378 public function getOrderInfo($data)
379 {
380 // For hosted mode, we don't need to return intent data as checkout session is created on order placement
381
382 /*
383 * Filter the Stripe Elements appearance configuration
384 *
385 * This filter allows developers to customize the appearance of Stripe Elements.
386 * For example:
387 *
388 * function stripe_appearance($appearance) {
389 * return array(
390 * 'theme' => 'night',
391 * 'labels' => 'floating',
392 * 'variables' => array(
393 * 'colorPrimary' => '#0570de',
394 * 'colorBackground' => '#ffffff',
395 * 'colorText' => '#30313d',
396 * 'colorDanger' => '#df1b41',
397 * 'fontFamily' => 'Ideal Sans, system-ui, sans-serif',
398 * 'spacingUnit' => '2px',
399 * 'borderRadius' => '4px',
400 * )
401 * );
402 * }
403 * add_filter('fluent_cart/stripe_appearance', 'stripe_appearance', 10, 1);
404 *
405 * @see https://docs.stripe.com/elements/appearance-api for all available options
406 * @param array $appearance The appearance configuration
407 * @return array The modified appearance configuration
408 */
409 if (($this->settings->get('checkout_mode') ?? 'onsite') == 'hosted') {
410 wp_send_json(
411 [
412 'status' => 'success',
413 'message' => __('Order info retrieved!', 'fluent-cart'),
414 'data' => [],
415 'payment_args' => [
416 'checkout_mode' => 'hosted'
417 ],
418 ],
419 200
420 );
421 }
422
423 $cart = CartHelper::getCart();
424 $checkOutHelper = CartCheckoutHelper::make();
425 $shippingChargeData = (new WebCheckoutHandler())->getShippingChargeData($cart);
426 $shippingCharge = Arr::get($shippingChargeData, 'charge');
427 $totalPrice = $checkOutHelper->getItemsAmountTotal(false) + $shippingCharge;
428
429 $tax = $checkOutHelper->getCart()->checkout_data['tax_data'] ?? [];
430 $taxBehavior = (int) Arr::get($tax, 'tax_behavior', 0);
431 $storeTaxBehavior = (int) Arr::get($tax, 'store_tax_behavior', $taxBehavior);
432
433 if ($taxBehavior === 1) {
434 // Pure exclusive — add all tax including fee tax (tax_total contains both).
435 $totalPrice = $totalPrice + (int) Arr::get($tax, 'tax_total', 0)
436 + (int) Arr::get($tax, 'shipping_tax', 0);
437 } elseif ($taxBehavior === 3) {
438 // Mixed — add only exclusive product tax + fee/shipping if store is exclusive.
439 $totalPrice = $totalPrice + (int) Arr::get($tax, 'exclusive_tax_total', 0);
440 if ($storeTaxBehavior === 1) {
441 $totalPrice = $totalPrice + (int) Arr::get($tax, 'fee_tax', 0)
442 + (int) Arr::get($tax, 'shipping_tax', 0);
443 }
444 }
445
446 $items = $this->getCheckoutItems();
447
448 $hasSubscription = $this->validateSubscriptions($items);
449
450 $stripeSettings = new StripeSettingsBase();
451 $publicKey = $stripeSettings->getPublicKey();
452
453 if (empty($publicKey)) {
454 fluent_cart_add_log(
455 'Stripe Credential Validation',
456 sprintf('Stripe %s keys are missing or invalid.', $stripeSettings->getMode()),
457 'error',
458 ['log_type' => 'payment']
459 );
460 wp_send_json([
461 'status' => 'failed',
462 'message' => __('No valid public key found! Please contact the site administrator.', 'fluent-cart')
463 ], 422);
464 }
465
466 $paymentArgs['public_key'] = $publicKey;
467
468 // Allow filtering the appearance configuration for Stripe Elements
469 $appearance = apply_filters_deprecated('fluent_cart_stripe_appearance', [
470 ['theme' => 'stripe']
471 ], '1.3.16', 'fluent_cart/stripe_appearance', 'Use fluent_cart/stripe_appearance instead of fluent_cart_stripe_appearance.');
472 $appearance = apply_filters('fluent_cart/stripe_appearance', $appearance);
473
474 $storeCurrency = CurrencySettings::get('currency');
475 $intentAmount = (int)$totalPrice;
476
477 if ($storeCurrency && CurrenciesHelper::isZeroDecimal($storeCurrency)) {
478 $intentAmount = (int)($intentAmount / 100);
479 }
480
481 $intentData = [
482 'mode' => 'payment',
483 'amount' => $intentAmount,
484 'currency' => strtolower($storeCurrency),
485 'automatic_payment_methods' => ['enabled' => true]
486 ];
487
488 if ($hasSubscription) {
489 $intentData['mode'] = 'subscription';
490 $intentData['setup_future_usage'] = 'off_session';
491 } elseif (Arr::get($data, 'save_payment_method') === 'yes') {
492 $intentData['setup_future_usage'] = 'on_session';
493 }
494
495 wp_send_json(
496 [
497 'status' => 'success',
498 'message' => __('Order info retrieved!', 'fluent-cart'),
499 'data' => [],
500 'payment_args' => $paymentArgs,
501 'intent' => $intentData,
502 'appearance' => $appearance,
503 ],
504 200
505 );
506 }
507
508 public function getConnectInfo(): array
509 {
510 return ConnectConfig::getConnectConfig();
511 }
512
513 public function disconnect($mode): bool
514 {
515 return ConnectConfig::disconnect($mode);
516 }
517
518 public function getCounterDisputeUrl($transaction)
519 {
520 if (!$transaction->meta['dispute_id']) {
521 return '';
522 }
523
524 return 'https://dashboard.stripe.com/disputes/' . $transaction->meta['dispute_id'];
525 }
526
527 public function acceptRemoteDispute($transaction, $args = [])
528 {
529 $disputeId = Arr::get($transaction->meta, 'dispute_id');
530 if (!$disputeId) {
531 $charge = (new API())->getStripeObject('payment_intents/' . $transaction->vendor_charge_id, ['expand' => ['latest_charge']]);
532
533 if (is_wp_error($charge) || empty($charge['dispute'])) {
534 new \WP_Error('No dispute ID found!', __('Please check stripe if the dispute is already accepted or not!', 'fluent-cart'));
535 }
536
537 $disputeId = Arr::get($charge, 'dispute', '');
538 }
539
540 $closeDispute = (new API())->createStripeObject('disputes/' . $disputeId . '/close');
541
542 if (is_wp_error($closeDispute)) {
543 return $closeDispute;
544 }
545
546 return $closeDispute;
547 }
548
549 public function isCurrencySupported(): bool
550 {
551 // stripe support all listed currencies except for IRR (Iranian Rial)
552 return strtoupper(CurrencySettings::get('currency')) !== 'IRR';
553 }
554
555 }
556