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

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