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

540 lines 21.3 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 if (Arr::get($tax, 'tax_behavior', 0) == 1) {
431 $totalPrice = $totalPrice + Arr::get($tax, 'tax_total', 0) + Arr::get($tax, 'shipping_tax', 0);
432 }
433
434 $items = $this->getCheckoutItems();
435
436 $hasSubscription = $this->validateSubscriptions($items);
437
438 $stripeSettings = new StripeSettingsBase();
439 $publicKey = $stripeSettings->getPublicKey();
440
441 if (empty($publicKey)) {
442 $message = __('No valid public key found!', 'fluent-cart');
443 fluent_cart_add_log('Stripe Credential Validation', $message, 'error', ['log_type' => 'payment']);
444 wp_send_json([
445 'status' => 'failed',
446 'message' => $message
447 ], 423);
448 }
449
450 $paymentArgs['public_key'] = $publicKey;
451
452 // Allow filtering the appearance configuration for Stripe Elements
453 $appearance = apply_filters_deprecated('fluent_cart_stripe_appearance', [
454 ['theme' => 'stripe']
455 ], '1.3.16', 'fluent_cart/stripe_appearance', 'Use fluent_cart/stripe_appearance instead of fluent_cart_stripe_appearance.');
456 $appearance = apply_filters('fluent_cart/stripe_appearance', $appearance);
457
458 $storeCurrency = CurrencySettings::get('currency');
459 $intentAmount = (int)$totalPrice;
460
461 if ($storeCurrency && CurrenciesHelper::isZeroDecimal($storeCurrency)) {
462 $intentAmount = (int)($intentAmount / 100);
463 }
464
465 $intentData = [
466 'mode' => 'payment',
467 'amount' => $intentAmount,
468 'currency' => strtolower($storeCurrency),
469 'automatic_payment_methods' => ['enabled' => true]
470 ];
471
472 if ($hasSubscription) {
473 $intentData['mode'] = 'subscription';
474 $intentData['setup_future_usage'] = 'off_session';
475 } elseif (Arr::get($data, 'save_payment_method') === 'yes') {
476 $intentData['setup_future_usage'] = 'on_session';
477 }
478
479 wp_send_json(
480 [
481 'status' => 'success',
482 'message' => __('Order info retrieved!', 'fluent-cart'),
483 'data' => [],
484 'payment_args' => $paymentArgs,
485 'intent' => $intentData,
486 'appearance' => $appearance,
487 ],
488 200
489 );
490 }
491
492 public function getConnectInfo(): array
493 {
494 return ConnectConfig::getConnectConfig();
495 }
496
497 public function disconnect($mode): bool
498 {
499 return ConnectConfig::disconnect($mode);
500 }
501
502 public function getCounterDisputeUrl($transaction)
503 {
504 if (!$transaction->meta['dispute_id']) {
505 return '';
506 }
507
508 return 'https://dashboard.stripe.com/disputes/' . $transaction->meta['dispute_id'];
509 }
510
511 public function acceptRemoteDispute($transaction, $args = [])
512 {
513 $disputeId = Arr::get($transaction->meta, 'dispute_id');
514 if (!$disputeId) {
515 $charge = (new API())->getStripeObject('payment_intents/' . $transaction->vendor_charge_id, ['expand' => ['latest_charge']]);
516
517 if (is_wp_error($charge) || empty($charge['dispute'])) {
518 new \WP_Error('No dispute ID found!', __('Please check stripe if the dispute is already accepted or not!', 'fluent-cart'));
519 }
520
521 $disputeId = Arr::get($charge, 'dispute', '');
522 }
523
524 $closeDispute = (new API())->createStripeObject('disputes/' . $disputeId . '/close');
525
526 if (is_wp_error($closeDispute)) {
527 return $closeDispute;
528 }
529
530 return $closeDispute;
531 }
532
533 public function isCurrencySupported(): bool
534 {
535 // stripe support all listed currencies except for IRR (Iranian Rial)
536 return strtoupper(CurrencySettings::get('currency')) !== 'IRR';
537 }
538
539 }
540