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

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