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

535 lines 21.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 $data[$mode . '_secret_key'] = Helper::encryptKey(Arr::get($data, $mode . '_secret_key'));
200 }
201
202 if (Arr::get($data, 'provider') === 'api_keys') {
203 $data['test_publishable_key'] = '';
204 $data['live_publishable_key'] = '';
205 $data['test_secret_key'] = '';
206 $data['live_secret_key'] = '';
207 }
208
209 return $data;
210 }
211
212 public static function validateSettings($data): array
213 {
214 $mode = Arr::get($data, 'payment_mode', 'test');
215 $provider = Arr::get($data, 'provider', 'connect');
216
217 if ($provider === 'api_keys') {
218 if ($mode === 'live') {
219 $sk = defined('FCT_STRIPE_LIVE_SECRET_KEY') ? FCT_STRIPE_LIVE_SECRET_KEY : Arr::get($data, 'live_secret_key');
220 } else {
221 $sk = defined('FCT_STRIPE_TEST_SECRET_KEY') ? FCT_STRIPE_TEST_SECRET_KEY : Arr::get($data, 'test_secret_key');
222 }
223 } else {
224 $sk = $mode === 'live' ? Arr::get($data, 'live_secret_key') : Arr::get($data, 'test_secret_key');
225 if (empty($sk)) {
226 $errorMessage = $mode === 'live' ? __('Stripe not connected in live mode!', 'fluent-cart') : __('Stripe not connected in test mode!', 'fluent-cart');
227 return [
228 'status' => 'failed',
229 'message' => $errorMessage
230 ];
231 } else {
232 return [
233 'status' => 'success',
234 'message' => __('Stripe account already verified!', 'fluent-cart')
235 ];
236 }
237 }
238
239 if (empty($sk)) {
240 return [
241 'status' => 'failed',
242 'message' => __('Please provide a valid secret key!', 'fluent-cart')
243 ];
244 }
245
246 if ($mode === 'live' && !str_contains($sk, 'sk_live')) {
247 return [
248 'status' => 'failed',
249 'message' => __('Please provide a valid LIVE secret key!', 'fluent-cart')
250 ];
251 } else if ($mode === 'test' && !str_contains($sk, 'sk_test')) {
252 return [
253 'status' => 'failed',
254 'message' => __('Please provide a valid TEST secret key!', 'fluent-cart')
255 ];
256 }
257
258 $response = (new API)->remoteRequest('account', [], $sk, 'GET');
259
260 if (isset($response['error'])) {
261 return [
262 'status' => 'failed',
263 'message' => $response['error']['message'] ? $response['error']['message'] : __('Invalid credentials!', 'fluent-cart')
264 ];
265 }
266
267 if (!isset($response['id'])) {
268 return [
269 'status' => 'failed',
270 'message' => $response['error']['message'] ? $response['error']['message'] : __('Invalid credentials!', 'fluent-cart')
271 ];
272 }
273
274 return [
275 'status' => 'success',
276 'message' => __('Stripe account verified!', 'fluent-cart')
277 ];
278 }
279
280 public function fields(): array
281 {
282 $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.');
283 $providerValue = apply_filters('fluent_cart/form_disable_stripe_connect', $disabled, []) ? 'api_keys' : 'connect';
284
285 return array(
286 'notice' => [
287 'value' => $this->renderStoreModeNotice(),
288 'label' => __('Store Mode notice', 'fluent-cart'),
289 'type' => 'notice'
290 ],
291 'payment_mode' => [
292 'type' => 'tabs',
293 'schema' => [
294 [
295 'type' => 'tab',
296 'label' => __('Live credentials', 'fluent-cart'),
297 'value' => 'live',
298 'schema' => []
299 ],
300 [
301 'type' => 'tab',
302 'label' => __('Test credentials', 'fluent-cart'),
303 'value' => 'test',
304 'schema' => [],
305 ]
306 ]
307 ],
308 'provider' => array(
309 'value' => $providerValue,
310 'label' => __('Provider', 'fluent-cart'),
311 'type' => 'provider'
312 ),
313 'setup_guide' => array(
314 'value' => '<h4>' . __('Or Setup keys manually.', 'fluent-cart') . '</h4><hr/>',
315 'label' => __('Or Setup keys manually', 'fluent-cart'),
316 'type' => 'html_attr',
317 'visible' => 'no'
318 ),
319 'checkout_mode' => array(
320 'value' => 'onsite',
321 'label' => __('Checkout Mode', 'fluent-cart'),
322 'type' => 'radio',
323 'options' => [
324 'onsite' => __('Embedded checkout (Recommended)', 'fluent-cart'),
325 'hosted' => __('Stripe Hosted checkout', 'fluent-cart')
326 ],
327 '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'),
328 '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')
329 ),
330 'webhook_desc' => array(
331 'value' => Webhook::webhookInstruction(),
332 'label' => __('Webhook URL', 'fluent-cart'),
333 'type' => 'html_attr'
334 ),
335 'test_active_methods' => [
336 'value' => (new API())->getActivatedPaymentMethodsConfigs('test'),
337 'label' => __('Activated Methods', 'fluent-cart'),
338 'type' => 'active_methods'
339 ],
340 'live_active_methods' => [
341 'value' => (new API())->getActivatedPaymentMethodsConfigs('live'),
342 'label' => __('Activated Methods', 'fluent-cart'),
343 'type' => 'active_methods'
344 ],
345 );
346
347 }
348
349 public function getPublicKey($pre = '')
350 {
351 return $this->settings->getPublicKey();
352 }
353
354 public function getTransactionUrl($url, $data): string
355 {
356 $transaction = Arr::get($data, 'transaction', null);
357 if (!$transaction) {
358 return $url;
359 }
360
361 if ($transaction->transaction_type === 'refund') {
362 return 'https://dashboard.stripe.com/refunds/' . $transaction->vendor_charge_id;
363 }
364
365 return 'https://dashboard.stripe.com/payments/' . $transaction->vendor_charge_id;
366 }
367
368 public function getSubscriptionUrl($url, $data): string
369 {
370 return 'https://dashboard.stripe.com/subscriptions/' . Arr::get($data, 'vendor_subscription_id');
371 }
372
373 public function getOrderInfo($data)
374 {
375 // For hosted mode, we don't need to return intent data as checkout session is created on order placement
376
377 /*
378 * Filter the Stripe Elements appearance configuration
379 *
380 * This filter allows developers to customize the appearance of Stripe Elements.
381 * For example:
382 *
383 * function stripe_appearance($appearance) {
384 * return array(
385 * 'theme' => 'night',
386 * 'labels' => 'floating',
387 * 'variables' => array(
388 * 'colorPrimary' => '#0570de',
389 * 'colorBackground' => '#ffffff',
390 * 'colorText' => '#30313d',
391 * 'colorDanger' => '#df1b41',
392 * 'fontFamily' => 'Ideal Sans, system-ui, sans-serif',
393 * 'spacingUnit' => '2px',
394 * 'borderRadius' => '4px',
395 * )
396 * );
397 * }
398 * add_filter('fluent_cart/stripe_appearance', 'stripe_appearance', 10, 1);
399 *
400 * @see https://docs.stripe.com/elements/appearance-api for all available options
401 * @param array $appearance The appearance configuration
402 * @return array The modified appearance configuration
403 */
404 if (($this->settings->get('checkout_mode') ?? 'onsite') == 'hosted') {
405 wp_send_json(
406 [
407 'status' => 'success',
408 'message' => __('Order info retrieved!', 'fluent-cart'),
409 'data' => [],
410 'payment_args' => [
411 'checkout_mode' => 'hosted'
412 ],
413 ],
414 200
415 );
416 }
417
418 $cart = CartHelper::getCart();
419 $checkOutHelper = CartCheckoutHelper::make();
420 $shippingChargeData = (new WebCheckoutHandler())->getShippingChargeData($cart);
421 $shippingCharge = Arr::get($shippingChargeData, 'charge');
422 $totalPrice = $checkOutHelper->getItemsAmountTotal(false) + $shippingCharge;
423
424 $tax = $checkOutHelper->getCart()->checkout_data['tax_data'] ?? [];
425 if (Arr::get($tax, 'tax_behavior', 0) == 1) {
426 $totalPrice = $totalPrice + Arr::get($tax, 'tax_total', 0) + Arr::get($tax, 'shipping_tax', 0);
427 }
428
429 $items = $this->getCheckoutItems();
430
431 $hasSubscription = $this->validateSubscriptions($items);
432
433 $stripeSettings = new StripeSettingsBase();
434 $publicKey = $stripeSettings->getPublicKey();
435
436 if (empty($publicKey)) {
437 $message = __('No valid public key found!', 'fluent-cart');
438 fluent_cart_add_log('Stripe Credential Validation', $message, 'error', ['log_type' => 'payment']);
439 wp_send_json([
440 'status' => 'failed',
441 'message' => $message
442 ], 423);
443 }
444
445 $paymentArgs['public_key'] = $publicKey;
446
447 // Allow filtering the appearance configuration for Stripe Elements
448 $appearance = apply_filters_deprecated('fluent_cart_stripe_appearance', [
449 ['theme' => 'stripe']
450 ], '1.3.16', 'fluent_cart/stripe_appearance', 'Use fluent_cart/stripe_appearance instead of fluent_cart_stripe_appearance.');
451 $appearance = apply_filters('fluent_cart/stripe_appearance', $appearance);
452
453 $storeCurrency = CurrencySettings::get('currency');
454 $intentAmount = (int)$totalPrice;
455
456 if ($storeCurrency && CurrenciesHelper::isZeroDecimal($storeCurrency)) {
457 $intentAmount = (int)($intentAmount / 100);
458 }
459
460 $intentData = [
461 'mode' => 'payment',
462 'amount' => $intentAmount,
463 'currency' => strtolower($storeCurrency),
464 'automatic_payment_methods' => ['enabled' => true]
465 ];
466
467 if ($hasSubscription) {
468 $intentData['mode'] = 'subscription';
469 $intentData['setup_future_usage'] = 'off_session';
470 } elseif (Arr::get($data, 'save_payment_method') === 'yes') {
471 $intentData['setup_future_usage'] = 'on_session';
472 }
473
474 wp_send_json(
475 [
476 'status' => 'success',
477 'message' => __('Order info retrieved!', 'fluent-cart'),
478 'data' => [],
479 'payment_args' => $paymentArgs,
480 'intent' => $intentData,
481 'appearance' => $appearance,
482 ],
483 200
484 );
485 }
486
487 public function getConnectInfo(): array
488 {
489 return ConnectConfig::getConnectConfig();
490 }
491
492 public function disconnect($mode): bool
493 {
494 return ConnectConfig::disconnect($mode);
495 }
496
497 public function getCounterDisputeUrl($transaction)
498 {
499 if (!$transaction->meta['dispute_id']) {
500 return '';
501 }
502
503 return 'https://dashboard.stripe.com/disputes/' . $transaction->meta['dispute_id'];
504 }
505
506 public function acceptRemoteDispute($transaction, $args = [])
507 {
508 $disputeId = Arr::get($transaction->meta, 'dispute_id');
509 if (!$disputeId) {
510 $charge = (new API())->getStripeObject('payment_intents/' . $transaction->vendor_charge_id, ['expand' => ['latest_charge']]);
511
512 if (is_wp_error($charge) || empty($charge['dispute'])) {
513 new \WP_Error('No dispute ID found!', __('Please check stripe if the dispute is already accepted or not!', 'fluent-cart'));
514 }
515
516 $disputeId = Arr::get($charge, 'dispute', '');
517 }
518
519 $closeDispute = (new API())->createStripeObject('disputes/' . $disputeId . '/close');
520
521 if (is_wp_error($closeDispute)) {
522 return $closeDispute;
523 }
524
525 return $closeDispute;
526 }
527
528 public function isCurrencySupported(): bool
529 {
530 // stripe support all listed currencies except for IRR (Iranian Rial)
531 return strtoupper(CurrencySettings::get('currency')) !== 'IRR';
532 }
533
534 }
535