PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / 6.22
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More v6.22
6.35 6.34 6.33.1 6.33 6.32.1 6.32 6.31 6.25 6.25.1 6.26 6.26.1 6.27 6.28 6.29 6.3 6.3.1 6.3.2 6.30 6.4 6.4.1 6.4.2 6.5 6.5.1 6.5.2 6.5.3 All 141 releases
formidable / stripe / models / FrmStrpLiteAuth.php

FrmStrpLiteAuth.php in Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More 6.22, at stripe/models/FrmStrpLiteAuth.php

730 lines 21.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if ( ! defined( 'ABSPATH' ) ) {
3 die( 'You are not allowed to call this page directly.' );
4 }
5
6 class FrmStrpLiteAuth {
7
8 /**
9 * All of the form IDs with payment details in the URL params will be included in this array.
10 *
11 * @var array
12 */
13 private static $form_ids = array();
14
15 /**
16 * If returning from Stripe to authorize a payment, show the message.
17 * This is used for 3D secure and for Stripe link.
18 *
19 * @since 6.5, introduced in v2.0 of the Stripe add on.
20 *
21 * @param string $html Form HTML that gets filtered through frm_filter_final_form.
22 * @return string
23 */
24 public static function maybe_show_message( $html ) {
25 $link_error = FrmAppHelper::simple_get( 'frm_link_error' );
26 if ( $link_error ) {
27 $message = '<div class="frm_error_style">' . self::get_message_for_stripe_link_code( $link_error ) . '</div>';
28 self::insert_error_message( $message, $html );
29 return $html;
30 }
31
32 $form_id = self::check_html_for_form_id_match( $html );
33 if ( false === $form_id ) {
34 return $html;
35 }
36
37 $details = FrmStrpLiteUrlParamHelper::get_details_for_form( $form_id );
38 if ( ! is_array( $details ) ) {
39 return $html;
40 }
41
42 $atts = array(
43 'fields' => FrmFieldsHelper::get_form_fields( $form_id ),
44 'entry' => $details['entry'],
45 );
46 self::prepare_success_atts( $atts );
47
48 $intent = $details['intent'];
49 $payment = $details['payment'];
50
51 if ( self::intent_has_failed_status( $intent ) ) {
52 $message = '<div class="frm_error_style">' . $intent->last_payment_error->message . '</div>';
53 self::insert_error_message( $message, $html );
54 return $html;
55 }
56
57 $intent_is_processing = 'processing' === $intent->status;
58 if ( $intent_is_processing ) {
59 // Append an additional processing message to the end of the success message.
60 $filter = function ( $message ) {
61 $stripe_settings = FrmStrpLiteAppHelper::get_settings();
62 $message .= '<p>' . esc_html( $stripe_settings->settings->processing_message ) . '</p>';
63 return $message;
64 };
65 add_filter( 'frm_content', $filter );
66 }
67
68 ob_start();
69 FrmFormsController::run_on_submit_actions( $atts );
70 $message = ob_get_contents();
71 ob_end_clean();
72
73 // Clean up the filter we added above so no other success messages get altered if there are multiple forms.
74 if ( $intent_is_processing ) {
75 remove_filter( 'frm_content', $filter );
76 }
77
78 return $message;
79 }
80
81 /**
82 * @param int|string $form_id
83 * @return array|false
84 */
85 private static function check_request_params( $form_id ) {
86 if ( ! FrmStrpLiteAppHelper::stripe_is_configured() ) {
87 return false;
88 }
89
90 $details = FrmStrpLiteUrlParamHelper::get_details_for_form( $form_id );
91 if ( ! is_array( $details ) ) {
92 return false;
93 }
94
95 self::$form_ids[] = $form_id;
96
97 return $details;
98 }
99
100 /**
101 * The frm_filter_final_form filter only passes form HTML as a string.
102 * To determine which form is being filtered, this function checks for the
103 * hidden form_id input. If there is a match, it returns the matching form id.
104 *
105 * @since 6.5
106 *
107 * @param string $html
108 * @return false|int Matching form id or false if there is no match.
109 */
110 private static function check_html_for_form_id_match( $html ) {
111 foreach ( self::$form_ids as $form_id ) {
112 $substring = '<input type="hidden" name="form_id" value="' . $form_id . '"';
113 if ( strpos( $html, $substring ) ) {
114 return $form_id;
115 }
116 }
117
118 return false;
119 }
120
121 /**
122 * Translate an error code into a readable message for the front end.
123 * FrmStrpLiteLinkRedirectHelper uses these codes to redirect errors that are then handled in self::maybe_show_message.
124 *
125 * @since 6.5, introduced in v3.0 of the Stripe add on.
126 *
127 * @param string $code
128 * @return string
129 */
130 private static function get_message_for_stripe_link_code( $code ) {
131 switch ( $code ) {
132 case 'intent_does_not_exist':
133 return __( 'Payment intent does not exist.', 'formidable' );
134 case 'unable_to_verify':
135 return __( 'Unable to verify payment intent.', 'formidable' );
136 case 'did_not_complete':
137 return __( 'Payment did not complete.', 'formidable' );
138 case 'no_payment_record':
139 return __( 'Unable to find record of payment.', 'formidable' );
140 case 'no_entry_found':
141 return __( 'This form submission does not exist.', 'formidable' );
142 case 'no_stripe_link_action':
143 return __( 'This form is not configured for Stripe link payments.', 'formidable' );
144 case 'create_subscription_failed':
145 return __( 'Something went wrong when trying to create a subscription.', 'formidable' );
146 case 'payment_failed':
147 return __( 'Payment was not successfully processed.', 'formidable' );
148 }
149 return '';
150 }
151
152 /**
153 * Add the parameters the receiving functions are expecting.
154 *
155 * @since 6.5, introduced in v2.0 of the Stripe add on.
156 *
157 * @param array $atts
158 * @return void
159 */
160 private static function prepare_success_atts( &$atts ) {
161 $atts['form'] = FrmForm::getOne( $atts['entry']->form_id );
162 $atts['entry_id'] = $atts['entry']->id;
163 $opt = 'success_action';
164 $atts['conf_method'] = ! empty( $atts['form']->options[ $opt ] ) ? $atts['form']->options[ $opt ] : 'message';
165
166 $actions = FrmFormsController::get_met_on_submit_actions( $atts, 'create' );
167 if ( $actions ) {
168 $action = reset( $actions );
169 if ( ! empty( $action->post_content['success_action'] ) && 'message' === $action->post_content['success_action'] ) {
170 $atts['conf_method'] = $action->post_content['success_action'];
171 }
172 }
173 }
174
175 /**
176 * Insert a message/error where the form styling will be applied.
177 *
178 * @since 6.5, introduced in v2.0 of the Stripe add on.
179 */
180 private static function insert_error_message( $message, &$form ) {
181 $add_after = '<fieldset>';
182 $pos = strpos( $form, $add_after );
183 if ( $pos !== false ) {
184 $form = substr_replace( $form, $add_after . $message, $pos, strlen( $add_after ) );
185 }
186 }
187
188 /**
189 * Include the token if going between pages.
190 *
191 * @param object $form The form being submitted.
192 * @return void
193 */
194 public static function add_hidden_token_field( $form ) {
195 $posted_form = FrmAppHelper::get_param( 'form_id', 0, 'post', 'absint' );
196 if ( $posted_form != $form->id || FrmFormsController::just_created_entry( $form->id ) ) {
197 // Check to make sure the correct form was submitted.
198 // Was an entry already created and the form should be loaded fresh?
199
200 $intents = self::maybe_create_intents( $form->id );
201 self::include_intents_in_form( $intents, $form );
202
203 return;
204 }
205
206 $intents = self::get_payment_intents( 'frmintent' . $form->id );
207 if ( ! empty( $intents ) ) {
208 self::update_intent_pricing( $form->id, $intents );
209 } else {
210 $intents = self::maybe_create_intents( $form->id );
211 }
212
213 self::include_intents_in_form( $intents, $form );
214 }
215
216 /**
217 * Include hidden fields with payment intent IDs in the form.
218 *
219 * @since 6.5, introduced in v2.02 of the Stripe add on.
220 *
221 * @param array $intents
222 * @param stdClass $form
223 * @return void
224 */
225 private static function include_intents_in_form( $intents, $form ) {
226 foreach ( $intents as $intent ) {
227 if ( is_array( $intent ) ) {
228 $id = $intent['id'];
229 $action = $intent['action'];
230 } else {
231 $id = $intent;
232 $action = '';
233 }
234
235 echo '<input type="hidden" name="frmintent' . esc_attr( $form->id ) . '[]" value="' . esc_attr( $id ) . '" data-action="' . esc_attr( $action ) . '" />';
236 }
237 }
238
239 /**
240 * Check POST data for payment intents.
241 *
242 * @since 6.5, introduced in v2.0 of the Stripe add on.
243 *
244 * @param string $name
245 * @return mixed
246 */
247 public static function get_payment_intents( $name ) {
248 // phpcs:ignore WordPress.Security.NonceVerification.Missing
249 if ( ! isset( $_POST[ $name ] ) ) {
250 return array();
251 }
252 $intents = $_POST[ $name ]; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Missing
253 FrmAppHelper::sanitize_value( 'sanitize_text_field', $intents );
254 return $intents;
255 }
256
257 /**
258 * Update pricing before authorizing.
259 *
260 * @since 6.5, introduced in v2.0 of the Stripe add on.
261 *
262 * @return void
263 */
264 public static function update_intent_ajax() {
265 check_ajax_referer( 'frm_strp_ajax', 'nonce' );
266
267 if ( empty( $_POST['form'] ) ) {
268 wp_die();
269 }
270
271 $form = json_decode( stripslashes( $_POST['form'] ), true ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
272 if ( ! is_array( $form ) ) {
273 wp_die();
274 }
275
276 self::format_form_data( $form );
277
278 $form_id = absint( $form['form_id'] );
279 $intents = isset( $form[ 'frmintent' . $form_id ] ) ? $form[ 'frmintent' . $form_id ] : array();
280
281 if ( empty( $intents ) ) {
282 wp_die();
283 }
284
285 if ( ! is_array( $intents ) ) {
286 $intents = array( $intents );
287 } else {
288 foreach ( $intents as $k => $intent ) {
289 if ( is_array( $intent ) && isset( $intent[ $k ] ) ) {
290 $intents[ $k ] = $intent[ $k ];
291 }
292 }
293 }
294
295 $_POST = $form;
296 self::update_intent_pricing( $form_id, $intents );
297
298 wp_die();
299 }
300
301 /**
302 * Update pricing on page turn and non-ajax validation.
303 *
304 * @since 6.5, introduced in v2.0 of the Stripe add on.
305 * @param int $form_id
306 * @param array $intents
307 * @return void
308 */
309 private static function update_intent_pricing( $form_id, &$intents ) {
310 // phpcs:ignore WordPress.Security.NonceVerification.Missing
311 if ( ! isset( $_POST['form_id'] ) || absint( $_POST['form_id'] ) != $form_id ) {
312 return;
313 }
314
315 $actions = FrmStrpLiteActionsController::get_actions_before_submit( $form_id );
316 if ( empty( $actions ) || empty( $intents ) ) {
317 return;
318 }
319
320 $form = FrmForm::getOne( $form_id );
321
322 try {
323 if ( ! FrmStrpLiteAppHelper::call_stripe_helper_class( 'initialize_api' ) ) {
324 return;
325 }
326 } catch ( Exception $e ) {
327 // Intent was not created.
328 return;
329 }
330
331 foreach ( $intents as $k => $intent ) {
332 $intent_id = explode( '_secret_', $intent )[0];
333 $is_setup_intent = 0 === strpos( $intent_id, 'seti_' );
334 if ( $is_setup_intent ) {
335 continue;
336 }
337
338 $saved = FrmStrpLiteAppHelper::call_stripe_helper_class( 'get_intent', $intent_id );
339 if ( empty( $saved->metadata->action ) ) {
340 continue;
341 }
342
343 foreach ( $actions as $action ) {
344 if ( $saved->metadata->action != $action->ID ) {
345 continue;
346 }
347 $intents[ $k ] = array(
348 'id' => $intent,
349 'action' => $action->ID,
350 );
351
352 $amount = $action->post_content['amount'];
353 if ( strpos( $amount, '[' ) === false ) {
354 // The amount is static, so it doesn't need an update.
355 continue;
356 }
357
358 // Update amount based on field shortcodes.
359 $entry = self::generate_false_entry();
360 $amount = FrmStrpLiteActionsController::prepare_amount( $amount, compact( 'form', 'entry', 'action' ) );
361 if ( $saved->amount == $amount || $amount == '000' ) {
362 continue;
363 }
364
365 FrmStrpLiteAppHelper::call_stripe_helper_class( 'update_intent', $intent_id, array( 'amount' => $amount ) );
366 }//end foreach
367 }//end foreach
368 }
369
370 /**
371 * Create an entry object with posted values.
372 *
373 * @since 6.5, introduced in v2.0 of the Stripe add on.
374 * @return stdClass
375 */
376 private static function generate_false_entry() {
377 $entry = new stdClass();
378 $entry->post_id = 0;
379 $entry->id = 0;
380 $entry->metas = array();
381
382 // phpcs:ignore WordPress.Security.NonceVerification.Missing
383 foreach ( $_POST as $k => $v ) {
384 $k = sanitize_text_field( stripslashes( $k ) );
385 $v = wp_unslash( $v );
386
387 if ( $k === 'item_meta' ) {
388 foreach ( $v as $f => $value ) {
389 FrmAppHelper::sanitize_value( 'wp_kses_post', $value );
390 $entry->metas[ absint( $f ) ] = $value;
391 }
392 } else {
393 FrmAppHelper::sanitize_value( 'wp_kses_post', $v );
394 $entry->{$k} = $v;
395 }
396 }
397
398 return $entry;
399 }
400
401 /**
402 * Reformat the form data in name => value array.
403 *
404 * @since 6.5, introduced in v2.0 of the Stripe add on.
405 *
406 * @param array $form
407 * @return void
408 */
409 private static function format_form_data( &$form ) {
410 $formatted = array();
411
412 foreach ( $form as $input ) {
413 $key = $input['name'];
414 if ( isset( $formatted[ $key ] ) ) {
415 if ( is_array( $formatted[ $key ] ) ) {
416 $formatted[ $key ][] = $input['value'];
417 } else {
418 $formatted[ $key ] = array( $formatted[ $key ], $input['value'] );
419 }
420 } else {
421 $formatted[ $key ] = $input['value'];
422 }
423 }
424
425 parse_str( http_build_query( $formatted ), $form );
426 }
427
428 /**
429 * Create intents on form load when required.
430 * This only happens in two cases: For stripe link, and when processing a one-time payment before the entry is created.
431 *
432 * @since 6.5, introduced in v2.0 of the Stripe add on.
433 *
434 * @param int|string $form_id
435 * @return array
436 */
437 private static function maybe_create_intents( $form_id ) {
438 $intents = array();
439
440 $details = self::check_request_params( $form_id );
441 if ( is_array( $details ) ) {
442 $payment = $details['payment'];
443 $intent = $details['intent'];
444 $payment_failed = self::payment_failed( $payment, $intent );
445
446 // Exit early if the request params are set.
447 // This way an extra payment intent isn't created for Stripe Link.
448 if ( ! $payment_failed ) {
449 return $intents;
450 }
451 }
452
453 if ( ! FrmStrpLiteAppHelper::call_stripe_helper_class( 'initialize_api' ) ) {
454 // Stripe is not configured, so don't create intents.
455 return $intents;
456 }
457
458 $actions = FrmStrpLiteActionsController::get_actions_before_submit( $form_id );
459 self::add_amount_to_actions( $form_id, $actions );
460
461 foreach ( $actions as $action ) {
462 if ( is_array( $details ) && self::intent_has_failed_status( $details['intent'] ) ) {
463 $intents[] = array(
464 'id' => $details['intent']->client_secret,
465 'action' => $action->ID,
466 );
467 continue;
468 }
469
470 $intent = self::create_intent( $action );
471 if ( ! is_object( $intent ) ) {
472 // A non-object is a string error message.
473 // The error gets logged to results.log so we can just skip it.
474 // Reasons it could fail is because a payment method type was specified that will not work.
475 // A payment method type may not work because of a currency conflict, or because it isn't enabled.
476 // Or the payment method type could be an incorrect value.
477 // When using Stripe Connect, the error will just say "Unable to create intent".
478 // In this case, you can find the full error message in the Stripe dashboard.
479 continue;
480 }
481
482 $intents[] = array(
483 'id' => $intent->client_secret,
484 'action' => $action->ID,
485 );
486 }//end foreach
487
488 return $intents;
489 }
490
491 /**
492 * Create a payment intent for Stripe link or when processing a payment before the entry is created.
493 *
494 * @since 3.0 This code was moved out of self::maybe_create_intents into a new function.
495 *
496 * @param WP_Post $action
497 * @return mixed
498 */
499 private static function create_intent( $action ) {
500 $amount = $action->post_content['amount'];
501 $currency = $action->post_content['currency'];
502
503 if ( $amount == '000' ) {
504 // Create the intent when the form loads.
505 $amount = in_array( strtolower( $currency ), array( 'aud', 'cad', 'eur', 'gbp', 'usd' ), true ) ? 100 : 1000;
506 }
507
508 if ( 'recurring' === $action->post_content['type'] ) {
509 $payment_method_types = FrmStrpLitePaymentTypeHandler::get_payment_method_types( $action );
510 return self::create_setup_intent( $payment_method_types );
511 }
512
513 $new_charge = array(
514 'amount' => $amount,
515 'currency' => $currency,
516 'metadata' => array( 'action' => $action->ID ),
517 );
518
519 if ( FrmStrpLitePaymentTypeHandler::should_use_automatic_payment_methods( $action ) ) {
520 $new_charge['automatic_payment_methods'] = array( 'enabled' => true );
521 } else {
522 $payment_method_types = FrmStrpLitePaymentTypeHandler::get_payment_method_types( $action );
523 $new_charge['payment_method_types'] = $payment_method_types;
524 }
525
526 return FrmStrpLiteAppHelper::call_stripe_helper_class( 'create_intent', $new_charge );
527 }
528
529 /**
530 * Create a customer and an associated setup intent for a recurring Stripe link payment.
531 *
532 * @since 6.5, introduced in v3.0 of the Stripe add on.
533 *
534 * @param array $payment_method_types
535 * @return false|object
536 */
537 private static function create_setup_intent( $payment_method_types ) {
538 $payment_info = array(
539 'user_id' => FrmTransLiteAppHelper::get_user_id_for_current_payment(),
540 );
541
542 // We need to add a customer to support subscriptions with link.
543 $customer = FrmStrpLiteAppHelper::call_stripe_helper_class( 'get_customer', $payment_info );
544 if ( ! is_object( $customer ) ) {
545 return false;
546 }
547
548 return FrmStrpLiteAppHelper::call_stripe_helper_class( 'create_setup_intent', $customer->id, $payment_method_types );
549 }
550
551 /**
552 * @since 6.5, introduced in v2.0 of the Stripe add on.
553 *
554 * @param int|string $form_id
555 * @param array $actions
556 * @return void
557 */
558 private static function add_amount_to_actions( $form_id, &$actions ) {
559 if ( empty( $actions ) ) {
560 return;
561 }
562 $form = FrmForm::getOne( $form_id );
563
564 foreach ( $actions as $k => $action ) {
565 $amount = self::get_amount_before_submit( compact( 'action', 'form' ) );
566 $actions[ $k ]->post_content['amount'] = $amount;
567 }
568 }
569
570 /**
571 * @since 6.5, introduced in v2.0 of the Stripe add on.
572 *
573 * @param array $atts
574 * @return string
575 */
576 private static function get_amount_before_submit( $atts ) {
577 $amount = $atts['action']->post_content['amount'];
578 return FrmStrpLiteActionsController::prepare_amount( $atts['action']->post_content['amount'], $atts );
579 }
580
581 /**
582 * Get the URL to return to after a payment is complete.
583 * This may either use the success URL on redirect, or the message on success.
584 * It shouldn't be confused for the Stripe link return URL. It isn't used for that. That uses the frmstrplinkreturn AJAX action instead.
585 *
586 * @since 6.5, introduced in v2.0 of the Stripe add on.
587 *
588 * @param array $atts
589 * @return string
590 */
591 public static function return_url( $atts ) {
592 $atts = array(
593 'entry' => $atts['entry'],
594 );
595 self::prepare_success_atts( $atts );
596
597 if ( $atts['conf_method'] === 'redirect' ) {
598 $redirect = self::get_redirect_url( $atts );
599 } else {
600 $redirect = self::get_message_url( $atts );
601 }
602
603 return $redirect;
604 }
605
606 /**
607 * If the form should redirect, get the url to redirect to.
608 *
609 * @since 6.5, introduced in v2.0 of the Stripe add on.
610 *
611 * @param array $atts {
612 * The form and entry details.
613 *
614 * @type stdClass $form
615 * @type stdClass $entry
616 * }
617 * @return string
618 */
619 private static function get_redirect_url( $atts ) {
620 $actions = FrmFormsController::get_met_on_submit_actions( $atts );
621 if ( $actions ) {
622 $success_url = reset( $actions )->post_content['success_url'];
623 }
624
625 if ( empty( $success_url ) ) {
626 $success_url = $atts['form']->options['success_url'];
627 }
628
629 $success_url = trim( $atts['form']->options['success_url'] );
630 $success_url = apply_filters( 'frm_content', $success_url, $atts['form'], $atts['entry'] );
631 $success_url = do_shortcode( $success_url );
632 $atts['id'] = $atts['entry']->id;
633
634 add_filter( 'frm_redirect_url', 'FrmEntriesController::prepare_redirect_url' );
635 return apply_filters( 'frm_redirect_url', $success_url, $atts['form'], $atts );
636 }
637
638 /**
639 * If the form should should a message, append it to the success url.
640 *
641 * @since 6.5, introduced in v2.0 of the Stripe add on.
642 *
643 * @param array $atts
644 */
645 private static function get_message_url( $atts ) {
646 $url = self::get_referer_url( $atts['entry_id'], false );
647 if ( false === $url ) {
648 $url = FrmAppHelper::get_server_value( 'HTTP_REFERER' );
649 }
650 return add_query_arg( array( 'frmstrp' => $atts['entry_id'] ), $url );
651 }
652
653 /**
654 * @since 6.5
655 *
656 * @param int|string $entry_id
657 * @param bool $delete_meta
658 * @return false|string
659 */
660 public static function get_referer_url( $entry_id, $delete_meta = true ) {
661 $row = FrmDb::get_row(
662 'frm_item_metas',
663 array(
664 'field_id' => 0,
665 'item_id' => $entry_id,
666 'meta_value LIKE' => '{"referer":',
667 ),
668 'id, meta_value'
669 );
670 if ( ! $row ) {
671 return false;
672 }
673
674 $meta = $row->meta_value;
675 $meta = json_decode( $meta, true );
676
677 if ( ! is_array( $meta ) || empty( $meta['referer'] ) ) {
678 return false;
679 }
680
681 self::delete_temporary_referer_meta( (int) $row->id );
682 return $meta['referer'];
683 }
684
685 /**
686 * Delete the referer meta as we'll no longer need it.
687 *
688 * @param int $row_id
689 * @return void
690 */
691 private static function delete_temporary_referer_meta( $row_id ) {
692 global $wpdb;
693 $wpdb->delete( $wpdb->prefix . 'frm_item_metas', array( 'id' => $row_id ) );
694 }
695
696 /**
697 * Check if a payment or setup intent has failed.
698 *
699 * @since 6.5.1
700 *
701 * @param object $intent
702 * @return bool
703 */
704 private static function intent_has_failed_status( $intent ) {
705 return in_array( $intent->status, array( 'requires_source', 'requires_payment_method', 'canceled' ), true );
706 }
707
708 /**
709 * Check if a payment failed.
710 *
711 * @since 6.8
712 *
713 * @param object $payment
714 * @param object $intent
715 * @return bool
716 */
717 public static function payment_failed( $payment, $intent ) {
718 if ( self::intent_has_failed_status( $intent ) ) {
719 return true;
720 }
721
722 // The $intent will be "succeeded" with a failed payment when testing with the 4000000000000341 credit card.
723 if ( 'payment_failed' === FrmAppHelper::simple_get( 'frm_link_error' ) && 'failed' === $payment->status ) {
724 return true;
725 }
726
727 return false;
728 }
729 }
730