query_vars, get_the_permalink() . '?better_payment_paypal_status=success&better_payment_widget_id=' . $widget_id ); $cancel_url = add_query_arg( $wp->query_vars, get_the_permalink() . '?better_payment_error_status=error&better_payment_widget_id=' . $widget_id ); ob_start(); $error_info = [ 'business_email' => !empty($settings['better_payment_paypal_business_email']) ]; ?> bp_allowed_tags() ); } /** * Paystack button * * @since 0.0.1 */ public static function paystack_button( $widget_id = '' , $settings = [], $args = [] ) { $args['extra_classes'] = ! empty( $args['extra_classes'] ) ? $args['extra_classes'] : ''; $args['extra_classes'] = is_array($args['extra_classes']) ? implode(' ', $args['extra_classes']) : $args['extra_classes']; $args['button_text_default'] = ! empty( $args['button_text_default'] ) ? sanitize_text_field( $args['button_text_default'] ) : 'Proceed to Payment'; $paystack_button_text = ! empty( $settings["better_payment_form_form_buttons_paystack_button_text"] ) ? sanitize_text_field( $settings["better_payment_form_form_buttons_paystack_button_text"] ) : __( $args['button_text_default'], 'better-payment' ); ob_start(); ?> bp_allowed_tags() ); } public static function hide_show_payment_button_class( $settings, $type = 'paypal' ){ $button_hidden_class = 'is-hidden'; $types = ['paypal', 'stripe', 'paystack']; if( ! in_array($type, $types) ){ return $button_hidden_class; } $is_type_enable = ! empty( $settings[ "better_payment_form_{$type}_enable" ] ) && 'yes' === $settings[ "better_payment_form_{$type}_enable" ]; if( ! $is_type_enable ){ return $button_hidden_class; } $is_paypal_enable = ! empty( $settings[ "better_payment_form_paypal_enable" ] ) && 'yes' === $settings[ "better_payment_form_paypal_enable" ]; $is_stripe_enable = ! empty( $settings[ "better_payment_form_stripe_enable" ] ) && 'yes' === $settings[ "better_payment_form_stripe_enable" ]; $is_paystack_enable = ! empty( $settings[ "better_payment_form_paystack_enable" ] ) && 'yes' === $settings[ "better_payment_form_paystack_enable" ]; switch( $type ){ case 'paypal': $button_hidden_class = ''; break; case 'stripe': if( ( ! $is_paypal_enable ) ){ $button_hidden_class = ''; } break; case 'paystack': if( ( ! $is_paypal_enable ) && ( ! $is_stripe_enable ) ){ $button_hidden_class = ''; } break; default: break; } return $button_hidden_class; } /** * Payment create in db * * @since 0.0.1 */ public static function payment_create( $data ) { global $wpdb; $table = "{$wpdb->prefix}better_payment"; $wpdb->insert( $table, self::normalize_pending_stripe_status( $data ) ); if ( $wpdb->insert_id ) { return $wpdb->insert_id; } else { return false; } } /** * A Stripe Checkout Session order is always created pending. * * Every Stripe surface stores the session's `payment_status` at creation. For a session with * nothing to charge right now — a subscription trial, a 100%-off promotion code, setup mode — * Stripe already reports `no_payment_required` before anyone has completed the checkout. Stored * as is, the order could never be confirmed (confirmation claims it from `unpaid`), so the trial * or free donation never reached the thank-you page, its receipts or the campaign's donors. * Only a Checkout Session row (`obj_id` `cs_…`) is touched: a renewal PaymentIntent row is * written after its charge and keeps its status. * * @since 2.4.0 * * @param mixed $data Row data for payment_create(). * @return mixed */ public static function normalize_pending_stripe_status( $data ) { if ( ! is_array( $data ) || ! isset( $data['status'], $data['obj_id'] ) || ! is_string( $data['obj_id'] ) ) { return $data; } if ( 'no_payment_required' === $data['status'] && 0 === strpos( $data['obj_id'], 'cs_' ) ) { $data['status'] = self::STRIPE_PENDING_STATUS; } return $data; } /** * A new, unguessable order id: `{gateway}_{24 hex chars}`. * * The order id is the only key the public return pages and the PayPal status poll look * an order up by. uniqid() is the current time in microseconds, so ids created around a * known moment could be enumerated to read other visitors' order status and receipts. * 96 random bits cannot be. Fits the 50-char `order_id` column with room to spare. * * @since 2.3.4 * * @param string $gateway 'paypal' | 'stripe' | 'paystack'. * @return string */ public static function new_order_id( $gateway ) { try { $random = bin2hex( random_bytes( 12 ) ); } catch ( \Exception $e ) { $random = strtolower( wp_generate_password( 24, false ) ); } return sanitize_key( $gateway ) . '_' . $random; } /** * Mamage response * * @since 0.0.1 */ public static function manage_response( $settings = [], $widget_id = '' ) { $status = false; //if widget id is different then return false if ( !empty( $_REQUEST[ 'better_payment_widget_id' ] ) && $_REQUEST[ 'better_payment_widget_id' ] !== $widget_id ) { return $status; } if ( !empty( $_REQUEST[ 'better_payment_error_status' ] ) ) { // Determine order_id based on payment method $order_id = ''; if ( !empty( $_REQUEST['better_payment_stripe_id'] ) ) { $order_id = sanitize_text_field( $_REQUEST['better_payment_stripe_id'] ); } elseif ( !empty( $_REQUEST['better_payment_paystack_id'] ) ) { $order_id = sanitize_text_field( $_REQUEST['better_payment_paystack_id'] ); } elseif ( !empty( $_REQUEST['better_payment_paypal_id'] ) ) { $order_id = sanitize_text_field( $_REQUEST['better_payment_paypal_id'] ); } self::render_failed_response( $settings, $order_id ); return $status; } if ( !empty( $_REQUEST[ 'better_payment_paypal_status' ] ) && $_REQUEST[ 'better_payment_paypal_status' ] == 'success' ) { $status = self::paypal_payment_success( $settings ); } elseif ( !empty( $_REQUEST[ 'better_payment_stripe_status' ] ) && $_REQUEST[ 'better_payment_stripe_status' ] == 'success' ) { $status = self::stripe_payment_success( $settings ); } elseif ( !empty( $_REQUEST[ 'better_payment_paystack_status' ] ) && $_REQUEST[ 'better_payment_paystack_status' ] == 'success' ) { $status = self::paystack_payment_success( $settings ); } if ( is_wp_error( $status ) ) { // A verification that confirmed no payment never renders the success page or // redirects to the success URL (a WP_Error is truthy, and used to do both). if ( 'bp_stripe_payment_processing' === $status->get_error_code() ) { self::stripe_processing_message_template(); // The return URL keeps its query args, so reloading it verifies the order again. return $status; } if ( 'bp_stripe_payment_incomplete' === $status->get_error_code() ) { $stripe_order_id = ! empty( $_REQUEST['better_payment_stripe_id'] ) && is_scalar( $_REQUEST['better_payment_stripe_id'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['better_payment_stripe_id'] ) ) : ''; self::render_failed_response( $settings, $stripe_order_id ); // Like a cancelled checkout: the form renders again under the notice. $status = false; } } elseif ( ! empty( $status['pending'] ) ) { // Use home_url() instead of HTTP_HOST to avoid open-redirect via forged Host headers. $return_url = home_url( $_SERVER['REQUEST_URI'] ); // PayPal uses rm=2 (POST redirect), so item_number arrives in $_POST and is absent from the URL. // Add it explicitly so the poll-complete redirect (a plain GET) still carries item_number. if ( ! empty( $status['order_id'] ) && strpos( $return_url, 'item_number=' ) === false ) { $return_url = add_query_arg( 'item_number', $status['order_id'], $return_url ); } ?>

prefix}better_payment"; $row = $wpdb->get_row( $wpdb->prepare( "SELECT transaction_id, status, email, amount, currency FROM $table WHERE order_id = %s LIMIT 1", $item_number ) ); // No matching order — reject (prevents fake success display with forged item_number). if ( empty( $row ) ) { return false; } // IPN confirmed — return verified DB data. if ( $row->status === 'Completed' ) { return [ 'amount' => floatval( $row->amount ), 'email' => sanitize_email( $row->email ), 'transaction_id' => sanitize_text_field( $row->transaction_id ), 'currency' => sanitize_text_field( $row->currency ), 'method' => 'paypal', ]; } // Order exists but IPN hasn't arrived yet — signal pending so JS can poll. return [ 'pending' => true, 'order_id' => $item_number ]; } /** * AJAX: check whether a PayPal order has been confirmed by IPN. * * Called by the frontend polling loop after the user lands on the return URL * before the IPN fires. Returns {status:'pending'} or {status:'completed'}. * * Lives on Handler (not Actions) so it can be registered unconditionally — * PayPal payments flow through the block/campaign path without Elementor. */ public static function check_paypal_status() { check_ajax_referer( 'better-payment', 'security' ); $order_id = ! empty( $_POST['order_id'] ) ? sanitize_text_field( $_POST['order_id'] ) : ''; if ( empty( $order_id ) ) { wp_send_json_error( [ 'message' => 'Missing order_id' ] ); } global $wpdb; $table = "{$wpdb->prefix}better_payment"; $row = $wpdb->get_row( $wpdb->prepare( "SELECT status FROM $table WHERE order_id = %s LIMIT 1", $order_id ) ); if ( empty( $row ) ) { wp_send_json_error( [ 'message' => 'Order not found' ] ); } if ( $row->status === 'Completed' ) { wp_send_json_success( [ 'status' => 'completed' ] ); } wp_send_json_success( [ 'status' => 'pending' ] ); } /** * PayPal IPN listener. * * PayPal POSTs the raw IPN body here server-to-server. We echo it back with * cmd=_notify-validate; only a VERIFIED response from PayPal's servers reaches * apply_paypal_ipn(), the only writer. The browser return URL * (paypal_payment_success) never writes. * * @since 2.1.3 */ public static function handle_paypal_ipn() { $raw_post = file_get_contents( 'php://input' ); if ( empty( $raw_post ) ) { status_header( 400 ); exit; } $data = []; wp_parse_str( $raw_post, $data ); // PayPal includes test_ipn=1 for sandbox notifications. $verify_url = ! empty( $data['test_ipn'] ) ? 'https://ipnpb.sandbox.paypal.com/cgi-bin/webscr' : 'https://ipnpb.paypal.com/cgi-bin/webscr'; $response = wp_remote_post( $verify_url, [ 'body' => 'cmd=_notify-validate&' . $raw_post, 'timeout' => 30, 'sslverify' => true, 'headers' => [ 'Content-Type' => 'application/x-www-form-urlencoded' ], ] ); // Always respond 200 to PayPal to prevent retries, but only act on VERIFIED. if ( is_wp_error( $response ) || wp_remote_retrieve_body( $response ) !== 'VERIFIED' ) { status_header( 200 ); exit; } self::apply_paypal_ipn( $data ); status_header( 200 ); exit; } /** * Apply a PayPal IPN to the order it names. * * Call ONLY with a notification handle_paypal_ipn() posted back and PayPal answered VERIFIED — * nothing here authenticates the data. Changes nothing unless the notification is a * `Completed` payment for a pending order (`status IS NULL`) paid to the merchant's address, * for at least the order's amount, in its currency, under a transaction id no order holds. * Then the row is claimed with one update (`WHERE id = … AND status IS NULL`): PayPal redelivers * a notification it saw no 200 for and can deliver one twice at once, and only the delivery that * wins the claim sends the receipts. * * @since 2.4.0 * * @param array $data The notification's fields. * @return string 'confirmed' | 'ignored' | 'unmatched' | 'refused' | 'duplicate' */ public static function apply_paypal_ipn( array $data ) { $data = array_map( 'sanitize_text_field', $data ); if ( empty( $data['payment_status'] ) || empty( $data['item_number'] ) || $data['payment_status'] !== 'Completed' ) { return 'ignored'; } global $wpdb; $table = "{$wpdb->prefix}better_payment"; $results = $wpdb->get_row( $wpdb->prepare( "SELECT id, amount, currency, form_fields_info, referer FROM $table WHERE order_id = %s AND status IS NULL LIMIT 1", $data['item_number'] ) ); if ( empty( $results->id ) ) { return 'unmatched'; } // Validate receiver_email matches the merchant's configured PayPal email. $form_info = maybe_unserialize( $results->form_fields_info ); $expected_email = ! empty( $form_info['paypal_business_email'] ) ? strtolower( $form_info['paypal_business_email'] ) : ''; $receiver = ! empty( $data['receiver_email'] ) ? strtolower( $data['receiver_email'] ) : ( ! empty( $data['business'] ) ? strtolower( $data['business'] ) : '' ); if ( $expected_email && $receiver && $expected_email !== $receiver ) { return 'refused'; } // Validate the paid amount is not less than the expected order amount. $paid_amount = ! empty( $data['mc_gross'] ) ? floatval( $data['mc_gross'] ) : 0; if ( $paid_amount < floatval( $results->amount ) ) { return 'refused'; } // Validate currency matches. $paid_currency = ! empty( $data['mc_currency'] ) ? strtoupper( $data['mc_currency'] ) : ''; if ( $paid_currency && $results->currency && strtoupper( $results->currency ) !== $paid_currency ) { return 'refused'; } $txn_id = ! empty( $data['txn_id'] ) ? $data['txn_id'] : ''; // Prevent txn_id replay: reject if this transaction ID was already recorded. if ( $txn_id ) { $existing = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM $table WHERE transaction_id = %s LIMIT 1", $txn_id ) ); if ( $existing ) { return 'duplicate'; } } $payer_email = ! empty( $data['payer_email'] ) ? sanitize_email( $data['payer_email'] ) : ''; $claimed = $wpdb->update( $table, [ 'transaction_id' => $txn_id, 'status' => $data['payment_status'], 'email' => $payer_email, 'customer_info' => maybe_serialize( $data ), ], [ 'id' => (int) $results->id, 'status' => null, ] ); if ( 1 !== (int) $claimed ) { // A concurrent delivery of this notification confirmed the order first. return 'duplicate'; } $is_elementor_form = ! empty( $results->referer ) && $results->referer === 'elementor-form' ? 1 : 0; // Rebuild the e-mail settings persisted at payment-create time. IPN runs // server-to-server with no live widget context, so without this the body // would collapse to the bare transaction id (every section flag defaults // to 0). Legacy / pending rows created before this fix fall back to []. $email_settings = ! empty( $form_info['email_settings'] ) && is_array( $form_info['email_settings'] ) ? $form_info['email_settings'] : []; // Mirror the Stripe / Paystack gate: honour the form's "Send Email" toggle, // and always e-mail for Elementor Pro Form submissions. if ( ( isset( $email_settings['better_payment_form_email_enable'] ) && 'yes' === $email_settings['better_payment_form_email_enable'] ) || $is_elementor_form ) { self::better_email_notification( $txn_id, $payer_email, $email_settings, 'PayPal', $results->form_fields_info, $is_elementor_form ); } return 'confirmed'; } /** * Stripe payment success — the return page. * * Reads the transaction id from the request, EXCEPT when the caller * supplies `better_payment_stripe_id` in $settings. A caller that has * already authorized the id against server-side state (the WooCommerce * return handler matches it to the order's own `_bp_payment_id`) must * pass it explicitly: a caller-side guard that reads $_GET cannot bind * this lookup, because PHP's default request_order=GP lets a POST body * overwrite the query string and the two would then see different ids. * * A pending order is verified against Stripe and confirmed through * confirm_stripe_session(). An order an earlier return (a reload, a second * tab) already confirmed renders the success page again without asking * Stripe or re-running anything. See stripe_settled_return(). * * @since 0.0.1 * * @param array $settings Form settings; `better_payment_stripe_secret_key` is required to verify. * @return array|false|\WP_Error Success data, false for no order to show, or why nothing was confirmed. */ public static function stripe_payment_success( $settings = [] ) { $data = $_REQUEST; if ( ! empty( $settings[ 'better_payment_stripe_id' ] ) ) { $data[ 'better_payment_stripe_id' ] = $settings[ 'better_payment_stripe_id' ]; } if ( empty( $data['better_payment_stripe_id'] ) || ! is_scalar( $data['better_payment_stripe_id'] ) ) { return false; } $order_id = sanitize_text_field( (string) $data['better_payment_stripe_id'] ); $row = self::get_stripe_row( 'order_id', $order_id ); if ( empty( $row->id ) ) { return false; } if ( self::STRIPE_PENDING_STATUS !== $row->status ) { return self::stripe_settled_return( $row, $order_id ); } if ( empty( $row->obj_id ) || empty( $settings[ 'better_payment_stripe_secret_key' ] ) ) { return false; } // Subscription and split-payment orders are Pro's: Pro (or, for an older Pro, Lite's // transitional copy) adds the subscription expansion and the recurring / split flags. $verification = self::stripe_verification_for( $row ); $response = wp_safe_remote_post( 'https://api.stripe.com/v1/checkout/sessions/' . rawurlencode( (string) $row->obj_id ), array( 'method' => 'GET', 'headers' => array( 'Authorization' => 'Basic ' . base64_encode( sanitize_text_field( $settings[ 'better_payment_stripe_secret_key' ] ) . ':' ), 'Stripe-Version' => '2019-05-16', ), 'timeout' => 70, 'body' => array( 'expand' => $verification['expand'], ), ) ); if ( is_wp_error( $response ) || empty( $response[ 'body' ] ) ) { return new \WP_Error( 'stripe_error', __( 'There was a problem connecting to the Stripe API endpoint.', 'better-payment' ) ); } $session = json_decode( $response[ 'body' ] ); if ( ! is_object( $session ) || isset( $session->error ) ) { return new \WP_Error( 'stripe_error', __( 'There was a problem connecting to the Stripe API endpoint.', 'better-payment' ) ); } return self::confirm_stripe_session( $row, $session, $settings, $verification ); } /** * Confirm a pending Stripe order from its verified Checkout Session. * * The one place a Stripe payment is confirmed: the return page. In order: * * 1. The session must be this row's (when it carries an id), must not be a setup-mode session * (the WooCommerce free-trial checkout, which OrderHandler confirms by its SetupIntent), and * must be paid (stripe_session_is_paid()). Nothing below runs for a session that is not: an * abandoned checkout, or a completed one whose delayed payment has not cleared (a later * return confirms it once it has). * 2. Pro's verified-session step, the customer e-mail and the charged amount are computed. The * amount is `amount_total / 100` whenever Stripe reports one — 0 for a 100%-off promotion * code or a trial — and the form's amount is kept as `original_amount` when they differ. * Pro's step must stay free of side effects: a request that loses the claim has asked it too. * 3. The row is claimed with ONE update, `WHERE id = … AND status = 'unpaid'`, which writes the * confirmed status, amount, e-mail, session and Pro's details together. However many returns * race for a session (a reload, a second tab), exactly one update matches. * 4. Only that winner fires `better_payment/stripe_payment/success` and * `better_payment/payment_confirmed` and sends the receipts. A caller that lost the claim to a * confirmation gets the same success data back, so the visitor still sees the thank-you page. * * The confirmed status is always `paid`, including for a session Stripe reports as * `no_payment_required`: campaign totals and donor counts, Pro's Donors Wall and the WooCommerce * bridge count `paid`, and a $0 donation is still a donation. Stripe's own value stays in * `customer_info`. * * @since 2.4.0 * * @param object|null $row Pending row from get_stripe_row() (null is refused). * @param mixed $session Decoded Checkout Session, with $verification['expand'] expanded where Pro needs it (anything but an object is refused). * @param array|null $settings Form settings for the receipts; null or [] for none. * @param array|null $verification stripe_verification_for( $row ); computed when null. * @return array|\WP_Error Success data for the thank-you page, or why nothing was confirmed. */ public static function confirm_stripe_session( $row, $session, $settings = null, $verification = null ) { global $wpdb; $table = "{$wpdb->prefix}better_payment"; $settings = is_array( $settings ) ? $settings : []; if ( ! is_object( $row ) || empty( $row->id ) || ! is_object( $session ) ) { return new \WP_Error( 'bp_stripe_confirm_invalid', __( 'There is no Stripe payment to confirm.', 'better-payment' ) ); } if ( isset( $session->id ) && ( empty( $row->obj_id ) || (string) $session->id !== (string) $row->obj_id ) ) { return new \WP_Error( 'bp_stripe_session_mismatch', __( 'Stripe did not confirm a payment for this checkout.', 'better-payment' ) ); } if ( isset( $session->mode ) && 'setup' === $session->mode ) { return new \WP_Error( 'bp_stripe_setup_session', __( 'This checkout saves a payment method and is confirmed by its order.', 'better-payment' ) ); } $stored_fields = isset( $row->form_fields_info ) ? maybe_unserialize( $row->form_fields_info ) : []; $form_fields_info = is_array( $stored_fields ) ? $stored_fields : []; if ( ! is_array( $verification ) || ! isset( $verification['expand'] ) ) { $verification = self::stripe_verification_for( $row ); } // Stripe returns the session whether or not anyone paid for it. Nothing below — // Pro's verified-session step, the row update, the confirmation hooks, the e-mails, // the success page — may run for a session that is not complete and paid. The row // stays 'unpaid', so a later return can still confirm it. if ( ! self::stripe_session_is_paid( $session ) ) { return self::stripe_unpaid_session_error( $session ); } $action_data = [ 'form_fields_info' => $form_fields_info, 'is_payment_recurring' => $verification['is_payment_recurring'], 'is_payment_split_payment' => $verification['is_payment_split_payment'], 'checkout_session_id' => $row->obj_id, ]; // A subscription's id, customer e-mail and details, and the thank-you page's // recurring / split flags, are Pro's to add. $verified = PaymentEntitlement::stripe_verified_session( [ 'form_fields_info' => $form_fields_info, 'transaction_id' => ! empty( $row->transaction_id ) ? sanitize_text_field( $row->transaction_id ) : '', 'email' => self::stripe_session_customer_email( $session ), 'frontend_data' => [], ], $session, $verification ); $confirmed_fields = is_array( $verified['form_fields_info'] ) ? $verified['form_fields_info'] : $form_fields_info; $transaction_id = $verified['transaction_id']; $customer_email = $verified['email']; $amounts = self::stripe_paid_amounts( $session, isset( $row->amount ) ? $row->amount : 0, $confirmed_fields ); $paid_amount = $amounts['amount']; $confirmed_fields = $amounts['form_fields_info']; $claimed = $wpdb->update( $table, array( 'amount' => $paid_amount, 'status' => self::STRIPE_CONFIRMED_STATUS, 'email' => $customer_email, 'customer_info' => maybe_serialize( $session ), 'form_fields_info' => maybe_serialize( $confirmed_fields ), 'transaction_id' => $transaction_id, ), array( 'id' => (int) $row->id, 'status' => self::STRIPE_PENDING_STATUS, ) ); if ( 1 !== (int) $claimed ) { // Another return (a reload, a second tab) got here first (or the write failed). // Whatever it recorded is the outcome; nothing is fired or sent twice. $current = self::get_stripe_row( 'id', (int) $row->id ); if ( ! empty( $current->id ) && self::STRIPE_CONFIRMED_STATUS === $current->status ) { return self::stripe_frontend_data( $current, $verified['frontend_data'] ); } return new \WP_Error( 'bp_stripe_confirm_failed', __( 'The payment could not be recorded. Please reload this page.', 'better-payment' ) ); } do_action('better_payment/stripe_payment/success', $action_data); do_action( 'better_payment/payment_confirmed', (int) $row->id ); $frontend_data = [ 'amount' => $paid_amount, 'email' => $customer_email, 'transaction_id' => $transaction_id, 'currency' => ! empty( $row->currency ) ? $row->currency : __( 'USD', 'better-payment' ), 'method' => 'stripe', ] + $verified['frontend_data']; //Send email notification if ( ( isset( $settings[ 'better_payment_form_email_enable' ] ) && 'yes' === $settings[ 'better_payment_form_email_enable' ] ) || ( isset( $row->referer ) && 'elementor-form' === $row->referer ) ) { $is_elementor_form = isset( $row->referer ) && 'elementor-form' === $row->referer ? 1 : 0; self::better_email_notification( $transaction_id, $customer_email, $settings, 'Stripe', $row->form_fields_info, $is_elementor_form ); } return $frontend_data; } /** * A Stripe transaction row, looked up by `id`, `order_id` or `obj_id` (the Checkout Session id). * * @since 2.4.0 * * @param string $field 'id' | 'order_id' | 'obj_id'. * @param mixed $value Value to match; anything but a non-empty scalar matches nothing. * @return object|null */ public static function get_stripe_row( $field, $value ) { global $wpdb; if ( ! in_array( $field, [ 'id', 'order_id', 'obj_id' ], true ) || ! is_scalar( $value ) || '' === (string) $value ) { return null; } $table = "{$wpdb->prefix}better_payment"; $format = 'id' === $field ? '%d' : '%s'; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table from $wpdb->prefix, column and format from the whitelist above. $row = $wpdb->get_row( $wpdb->prepare( "SELECT id, order_id, obj_id, amount, transaction_id, currency, email, status, source, referer, form_fields_info, payment_date FROM $table WHERE $field = $format LIMIT 1", $value ) ); return is_object( $row ) ? $row : null; } /** * How a row's Checkout Session is retrieved and whether it is a subscription — Pro's answer. * * @since 2.4.0 * * @param object|null $row Transaction row; null reads as a row with no stored fields. * @return array `expand`, `is_payment_recurring`, `is_payment_split_payment`. */ public static function stripe_verification_for( $row ) { $form_fields_info = is_object( $row ) && isset( $row->form_fields_info ) ? maybe_unserialize( $row->form_fields_info ) : []; return PaymentEntitlement::stripe_verification_args( [ 'expand' => [ 'payment_intent' ], 'is_payment_recurring' => false, 'is_payment_split_payment' => false, ], is_array( $form_fields_info ) ? $form_fields_info : [] ); } /** * What a verified Checkout Session charged, and what the transaction's fields record about it. * * The charged amount is `amount_total / 100` whenever Stripe reports a numeric total — 0 * included: a 100%-off promotion code or a trial charges nothing, and a row claiming otherwise * would add money nobody paid to a campaign's total. It used to fall back to the form's amount * whenever the total was 0. Only a session with no total at all keeps the row's amount. * * `exact_paid_amount` is always recorded. When the charge differs from the amount the order was * created for, that amount is kept as `original_amount`; `is_coupon_applied` and * `paid_amount_diff` (read by the transaction details and the receipt) are set only when a * discount explains the difference — a trial is not a coupon. * * @since 2.4.0 * * @param mixed $session Decoded Checkout Session. * @param mixed $row_amount The row's amount (what the order was created for). * @param array $form_fields_info The fields to record into. * @return array `amount` (float) and `form_fields_info`. */ public static function stripe_paid_amounts( $session, $row_amount, array $form_fields_info ) { $original = is_numeric( $row_amount ) ? (float) $row_amount : 0.0; $total = is_object( $session ) && isset( $session->amount_total ) && is_numeric( $session->amount_total ) ? (float) $session->amount_total : null; $paid = null === $total ? $original : $total / 100; $form_fields_info['is_coupon_applied'] = 0; $form_fields_info['exact_paid_amount'] = $paid; unset( $form_fields_info['original_amount'], $form_fields_info['paid_amount_diff'] ); if ( (int) round( $paid * 100 ) !== (int) round( $original * 100 ) ) { $form_fields_info['original_amount'] = $original; $discounted = ( is_object( $session ) && isset( $session->total_details->amount_discount ) && is_numeric( $session->total_details->amount_discount ) && (float) $session->total_details->amount_discount > 0 ) || ( isset( $form_fields_info['stripe_coupon_code'] ) && is_scalar( $form_fields_info['stripe_coupon_code'] ) && '' !== trim( (string) $form_fields_info['stripe_coupon_code'] ) ); if ( $discounted ) { $form_fields_info['paid_amount_diff'] = $original - $paid; $form_fields_info['is_coupon_applied'] = 1; } } return [ 'amount' => $paid, 'form_fields_info' => $form_fields_info, ]; } /** * The customer's e-mail on a Checkout Session: the charge's billing e-mail when the PaymentIntent * is expanded, else the session's `customer_details` / `customer_email`. * * @since 2.4.0 * * @param mixed $session Decoded Checkout Session. * @return string */ public static function stripe_session_customer_email( $session ) { if ( ! is_object( $session ) ) { return ''; } if ( isset( $session->payment_intent->charges->data ) && is_array( $session->payment_intent->charges->data ) && ! empty( $session->payment_intent->charges->data ) ) { $charge = current( $session->payment_intent->charges->data ); if ( isset( $charge->billing_details->email ) && is_string( $charge->billing_details->email ) && '' !== $charge->billing_details->email ) { return sanitize_email( $charge->billing_details->email ); } } if ( isset( $session->customer_details->email ) && is_string( $session->customer_details->email ) && '' !== $session->customer_details->email ) { return sanitize_email( $session->customer_details->email ); } if ( isset( $session->customer_email ) && is_string( $session->customer_email ) ) { return sanitize_email( $session->customer_email ); } return ''; } /** * Whether an order id is one Handler::new_order_id() minted for Stripe. * * A return for an order that is already confirmed shows its receipt from the row, by id alone. * Orders from before 2.3.4 carry a time-based id that could be enumerated, so only an * unguessable one gets that page. * * @since 2.4.0 * * @param mixed $order_id Order id. * @return bool */ public static function stripe_order_id_is_unguessable( $order_id ) { return is_string( $order_id ) && 1 === preg_match( '/^stripe_[0-9a-f]{24}$/', $order_id ); } /** * The return page for an order that is no longer pending. * * - Confirmed (`paid`): the success data, rebuilt from the row — no Stripe call, no hook, no * e-mail — with Pro's thank-you page flags. Only for an unguessable order id. * - Anything else (refunded, an older release's statuses): nothing, as before. * * @param object $row The row. * @param string $order_id The order id the return names. * @return array|false */ private static function stripe_settled_return( $row, $order_id ) { if ( self::STRIPE_CONFIRMED_STATUS !== $row->status || ! self::stripe_order_id_is_unguessable( $order_id ) ) { return false; } $form_fields_info = isset( $row->form_fields_info ) ? maybe_unserialize( $row->form_fields_info ) : []; // Only the thank-you page flags are used, and Pro derives them from the stored fields. $details = PaymentEntitlement::stripe_verified_session( [ 'form_fields_info' => is_array( $form_fields_info ) ? $form_fields_info : [], 'transaction_id' => (string) $row->transaction_id, 'email' => (string) $row->email, 'frontend_data' => [], ], new \stdClass(), self::stripe_verification_for( $row ) ); return self::stripe_frontend_data( $row, $details['frontend_data'] ); } /** * Success data for a confirmed Stripe row. * * @param object $row Confirmed row. * @param array $extra Pro's thank-you page flags. * @return array */ private static function stripe_frontend_data( $row, array $extra = [] ) { return [ 'amount' => (float) $row->amount, 'email' => sanitize_email( (string) $row->email ), 'transaction_id' => sanitize_text_field( (string) $row->transaction_id ), 'currency' => ! empty( $row->currency ) ? sanitize_text_field( $row->currency ) : __( 'USD', 'better-payment' ), 'method' => 'stripe', ] + $extra; } /** * Whether a retrieved Stripe Checkout Session has really been paid for. * * Retrieving a session proves only that it exists. A visitor can come back to the return * URL from a checkout they abandoned (`status` is `open`, or `expired`), and a completed * checkout paid with a delayed method (bank debits, vouchers) is `complete` with * `payment_status: unpaid` until the money arrives — sometimes days later, sometimes never. * Both used to confirm the order: the row, `better_payment/payment_confirmed`, Pro's * payment types, the receipts and the success page. * * Paid means `payment_status` is `paid` (the funds are there), or `no_payment_required` * (a 100%-off promotion code, a subscription trial) on a `complete` session: Stripe also * reports `no_payment_required` for such a session before it is completed. A present * `status` other than `complete` is never paid. A missing `status` is tolerated only for * `paid` — requests pin `Stripe-Version: 2019-05-16`, and `paid` is only ever reported for * a completed checkout — so a response without the field cannot stall every payment. * Anything else missing or unrecognised is unpaid. * * @since 2.4.0 * * @param mixed $session Decoded Checkout Session. * @return bool */ public static function stripe_session_is_paid( $session ) { if ( ! is_object( $session ) ) { return false; } $status = isset( $session->status ) && is_string( $session->status ) ? $session->status : ''; $payment_status = isset( $session->payment_status ) && is_string( $session->payment_status ) ? $session->payment_status : ''; if ( 'paid' === $payment_status ) { return '' === $status || 'complete' === $status; } return 'no_payment_required' === $payment_status && 'complete' === $status; } /** * Why a Stripe return did not confirm its order. * * `bp_stripe_payment_processing`: the checkout was completed but Stripe has not been paid * yet (a delayed payment method). `bp_stripe_payment_incomplete`: anything else — the * checkout was never completed. The message is shown in WooCommerce order notes. * * @since 2.4.0 * * @param mixed $session Decoded Checkout Session. * @return \WP_Error */ public static function stripe_unpaid_session_error( $session ) { $status = is_object( $session ) && isset( $session->status ) && is_string( $session->status ) ? $session->status : ''; $payment_status = is_object( $session ) && isset( $session->payment_status ) && is_string( $session->payment_status ) ? $session->payment_status : ''; $data = [ 'status' => sanitize_text_field( $status ), 'payment_status' => sanitize_text_field( $payment_status ), ]; if ( 'complete' === $status && 'unpaid' === $payment_status ) { return new \WP_Error( 'bp_stripe_payment_processing', __( 'Stripe has not confirmed this payment yet. It is confirmed once the payment clears.', 'better-payment' ), $data ); } return new \WP_Error( 'bp_stripe_payment_incomplete', __( 'Stripe did not confirm a payment for this checkout.', 'better-payment' ), $data ); } /** * The return page for a completed Stripe checkout whose payment has not cleared yet. * * Deliberately neither the success page (nothing has been paid) nor the failure page (the * payment may still succeed). * * @since 2.4.0 * * @return void */ private static function stripe_processing_message_template() { ?>

prefix}better_payment"; $order_id = sanitize_text_field( (string) $data[ 'better_payment_paystack_id' ] ); $reference = sanitize_text_field( (string) $data['reference'] ); $results = $wpdb->get_row( $wpdb->prepare( "SELECT id,obj_id,transaction_id,amount,currency,email,status,form_fields_info,referer FROM $table WHERE order_id=%s limit 1", $order_id ) ); if ( empty( $results->id ) ) { return false; } if ( 'unpaid' !== $results->status ) { return self::paystack_settled_return( $results, $reference ); } $header_info = array( 'Authorization' => 'Bearer ' . sanitize_text_field( isset( $settings[ 'better_payment_paystack_secret_key' ] ) ? $settings[ 'better_payment_paystack_secret_key' ] : '' ), "Cache-Control: no-cache", ); $response = wp_safe_remote_get( 'https://api.paystack.co/transaction/verify/'. $reference, array( 'headers' => $header_info, 'timeout' => 70, ) ); $response = json_decode(wp_remote_retrieve_body($response)); if ( empty( $response->status ) ) { return false; } $verified = isset( $response->data ) ? $response->data : null; if ( ! self::paystack_verification_matches( $results, $verified ) || self::paystack_reference_claimed_elsewhere( $verified->reference, (int) $results->id ) ) { return false; } $claimed = $wpdb->update( $table, array( 'status' => ! empty( $response->data->status ) ? sanitize_text_field( $response->data->status ) : '', 'amount' => ! empty( $response->data->amount ) ? floatval( ( $response->data->amount ) / 100 ) : 0, 'obj_id' => ! empty( $response->data->id ) ? sanitize_text_field( $response->data->id ) : '', 'transaction_id' => ! empty( $response->data->reference ) ? sanitize_text_field( $response->data->reference ) : '', 'customer_info' => ! empty( $response->data->customer ) ? maybe_serialize( $response->data->customer ) : '', 'email' => ! empty( $response->data->customer->email ) ? sanitize_email( $response->data->customer->email ) : '', ), array( 'id' => (int) $results->id, 'status' => 'unpaid', ) ); if ( 1 !== (int) $claimed ) { // Another return confirmed this order first (or the write failed): nothing is fired or sent twice. $current = $wpdb->get_row( $wpdb->prepare( "SELECT id,transaction_id,amount,currency,email,status FROM $table WHERE id=%d limit 1", (int) $results->id ) ); return ! empty( $current->id ) ? self::paystack_settled_return( $current, $reference ) : false; } do_action( 'better_payment/payment_confirmed', (int) $results->id ); $better_customer_email = ! empty( $response->data->customer->email ) ? maybe_serialize( $response->data->customer->email ) : ''; $transaction_id = ! empty( $response->data->reference ) ? sanitize_text_field( $response->data->reference ) : ''; $frontend_data = [ 'amount' => ! empty( $response->data->amount ) ? floatval( ( $response->data->amount ) / 100 ) : 0, 'email' => ! empty( $better_customer_email ) ? $better_customer_email : '', 'transaction_id' => $transaction_id, 'currency' => ! empty( $response->data->currency ) ? sanitize_text_field( $response->data->currency ) : __( 'USD', 'better-payment' ), 'method' => 'paystack', ]; //Send email notification if ( $better_customer_email && ( ( isset( $settings[ 'better_payment_form_email_enable' ] ) && $settings[ 'better_payment_form_email_enable' ] == 'yes' ) || ( $results->referer === 'elementor-form' ) ) ) { $is_elementor_form = ! empty( $results->referer ) && $results->referer === 'elementor-form' ? 1 : 0; self::better_email_notification($transaction_id, $better_customer_email, $settings, 'Paystack', $results->form_fields_info, $is_elementor_form); } do_action( 'better_payment/payment/success', $order_id, (int) $results->id, 'paystack' ); return $frontend_data; } /** * The return page for a Paystack order that is no longer pending. * * A confirmed order (`success`) shows its success page again, from the row, when the return * carries the reference the order holds — the order id alone is not enough to see a receipt. * Anything else shows nothing, as before. * * @param object $row The row. * @param string $reference The return's `reference`. * @return array|false */ private static function paystack_settled_return( $row, $reference ) { $stored = isset( $row->transaction_id ) ? (string) $row->transaction_id : ''; if ( ! isset( $row->status ) || 'success' !== $row->status || '' === $stored || ! hash_equals( $stored, (string) $reference ) ) { return false; } return [ 'amount' => (float) $row->amount, 'email' => sanitize_email( (string) $row->email ), 'transaction_id' => sanitize_text_field( $stored ), 'currency' => ! empty( $row->currency ) ? sanitize_text_field( $row->currency ) : __( 'USD', 'better-payment' ), 'method' => 'paystack', ]; } /** * Whether a verified Paystack transaction really pays for the pending order. * * The verify call looks up whatever `reference` the return URL carries, and the return * URL is the visitor's to edit. Without this, a cheap paid transaction could be replayed * against an expensive pending order and mark it paid. Checked, in order: * - the transaction itself succeeded. Paystack's top-level `status: true` only says the * reference resolved — it is just as true for an abandoned or failed checkout, which is * how a visitor who paid nothing used to reach the success page; * - the reference is the one Paystack issued for this order at initialize (rows created * before references were stored have none, and fall through to the amount check — * paystack_reference_claimed_elsewhere() keeps that fallback from being replayable); * - the amount paid covers the amount the order was created for — which * PaymentRequestGuard has already held to what the form allows; * - the currency is the order's. * * @param object $row Pending row: transaction_id, amount, currency. * @param mixed $verified `data` object from Paystack's /transaction/verify response. * @return bool */ private static function paystack_verification_matches( $row, $verified ) { if ( ! is_object( $verified ) ) { return false; } if ( ! isset( $verified->status ) || 'success' !== $verified->status ) { return false; } $expected_reference = isset( $row->transaction_id ) ? (string) $row->transaction_id : ''; $verified_reference = isset( $verified->reference ) ? (string) $verified->reference : ''; if ( '' !== $expected_reference && $verified_reference !== $expected_reference ) { return false; } $paid = isset( $verified->amount ) && is_numeric( $verified->amount ) ? floatval( $verified->amount ) / 100 : 0.0; if ( $paid + 0.005 < floatval( isset( $row->amount ) ? $row->amount : 0 ) ) { return false; } $paid_currency = isset( $verified->currency ) ? strtoupper( (string) $verified->currency ) : ''; $order_currency = isset( $row->currency ) ? strtoupper( (string) $row->currency ) : ''; return '' === $paid_currency || '' === $order_currency || $paid_currency === $order_currency; } /** * Whether a Paystack reference already belongs to a different order. * * Every order created since references were stored holds its own reference from * initialize, so this only ever decides the legacy fallback — an order with no stored * reference. Without it, one genuine payment could be replayed to mark every such * order paid. A reference no order has claimed can still settle exactly one of them. * * @param mixed $reference Verified reference. * @param int $row_id The order being verified. * @return bool True when the reference must not settle this order. */ private static function paystack_reference_claimed_elsewhere( $reference, $row_id ) { global $wpdb; $reference = is_scalar( $reference ) ? sanitize_text_field( (string) $reference ) : ''; if ( '' === $reference ) { return true; } $table = "{$wpdb->prefix}better_payment"; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from $wpdb->prefix. $other = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM $table WHERE transaction_id = %s AND id <> %d LIMIT 1", $reference, (int) $row_id ) ); return ! empty( $other ); } /** * Success message template * * @since 0.0.1 */ public static function success_message_template( $settings = [], $tr_id = '' ) { if ( empty( $tr_id ) || is_wp_error($tr_id)) { return false; } wp_enqueue_script('better-payment-admin-script'); $image_url = BETTER_PAYMENT_ASSETS . '/img/success.svg'; $show_icon = 0; $default_icon = 1; $helper_obj = new Helper(); $store_name = ( isset( $settings['better_payment_form_payment_source'] ) && $settings['better_payment_form_payment_source'] === 'manual' && !empty( $settings['better_payment_form_title'] ) ) ? esc_html($settings['better_payment_form_title']) : esc_html( get_bloginfo('name') ); $payment_desc_text = !empty( $settings['better_payment_form_success_message_heading'] ) ? esc_html($settings['better_payment_form_success_message_heading']) : esc_html( __('You paid', 'better-payment') . ' [currency_symbol][amount] ' . __('to', 'better-payment') . ' [store_name]'); $bp__currency_symbol = esc_html( ! empty( $tr_id['currency'] ) ? $helper_obj->get_currency_symbol( $tr_id['currency'] ) : '' ); $payment_desc_text = str_replace( '[currency_symbol]', '' . $bp__currency_symbol . '', $payment_desc_text ); $payment_desc_text = str_replace( '[amount]', '' . esc_html( ! empty( $tr_id['amount'] ) ? $tr_id['amount'] : 0 ) . '', $payment_desc_text ); $payment_desc_text = str_replace( '[store_name]', '' . esc_html( $store_name ) . '', $payment_desc_text ); $payment_mail_text = !empty( $settings['better_payment_form_success_message_sub_heading'] ) ? esc_html($settings['better_payment_form_success_message_sub_heading']) : esc_html( __('Payment Confirmation email will be sent to ', 'better-payment') . '[customer_email]'); $payment_mail_text = str_replace( '[customer_email]', '' . esc_html( ! empty( $tr_id['email'] ) ? $tr_id['email'] : '' ) . '', $payment_mail_text ); if ( !empty( $settings[ 'better_payment_form_success_message_icon' ][ 'library' ] ) ) { if ( $settings[ 'better_payment_form_success_message_icon' ][ 'library' ] == 'svg' ) { $image_url = $settings[ 'better_payment_form_success_message_icon' ][ 'value' ][ 'url' ]; $default_icon = 0; } else { $show_icon = 1; $image_url = $settings[ 'better_payment_form_success_message_icon' ][ 'value' ]; $default_icon = 0; } } $allowed_payment_methods = [ 'paypal' => BETTER_PAYMENT_ASSETS . '/img/paypal-2.svg', 'stripe' => BETTER_PAYMENT_ASSETS . '/img/stripe-2.svg', 'paystack' => BETTER_PAYMENT_ASSETS . '/img/paystack-2.svg', ]; $payment_method_logo = isset( $tr_id['method'] ) && isset( $allowed_payment_methods[ $tr_id['method'] ] ) ? $allowed_payment_methods[ $tr_id['method'] ] : ''; $better_payment_form_success_message_thanks = !empty( $settings['better_payment_form_success_message_thanks'] ) ? esc_html( $settings['better_payment_form_success_message_thanks'] ) : esc_html__('Thank You!', 'better-payment'); $better_payment_form_success_message_transaction = !empty( $settings['better_payment_form_success_message_transaction'] ) ? esc_html__( $settings['better_payment_form_success_message_transaction'], 'better-payment' ) : esc_html__('Transaction ID', 'better-payment'); $better_payment_form_success_message_amount_text = !empty( $settings['better_payment_form_success_message_amount_text'] ) ? esc_html( $settings['better_payment_form_success_message_amount_text'] ) : esc_html__( 'Amount', 'better-payment' ); $better_payment_form_success_message_currency_text = !empty( $settings['better_payment_form_success_message_currency_text'] ) ? esc_html( $settings['better_payment_form_success_message_currency_text'] ) : esc_html__( 'Currency', 'better-payment' ); $better_payment_form_success_message_pay_method_text = !empty( $settings['better_payment_form_success_message_pay_method_text'] ) ? esc_html( $settings['better_payment_form_success_message_pay_method_text'] ) : esc_html__( 'Payment Method', 'better-payment' ); // Rows only Pro's payment types add: Payment Type, and a split payment's "Paid Amount … out of …". $pro_rows_after_payment_method = PaymentEntitlement::success_rows( 'after_payment_method', $settings, $tr_id, [ 'currency_symbol' => $bp__currency_symbol ] ); $pro_rows_after_merchant_details = PaymentEntitlement::success_rows( 'after_merchant_details', $settings, $tr_id, [ 'currency_symbol' => $bp__currency_symbol ] ); $better_payment_form_success_message_merchant_details_text = !empty( $settings['better_payment_form_success_message_merchant_details_text'] ) ? esc_html( $settings['better_payment_form_success_message_merchant_details_text'] ) : esc_html__( 'Merchant Details', 'better-payment' ); $better_payment_form_success_message_merchant_details_value = isset( $settings['better_payment_form_payment_source'] ) && $settings['better_payment_form_payment_source'] === 'manual' && ! empty( $settings['better_payment_form_title'] ) ? esc_html($settings['better_payment_form_title']) : esc_html( get_bloginfo( 'name' ) ); $better_payment_form_success_message_purchase_details_text = !empty( $settings['better_payment_form_success_message_purchase_details_text'] ) ? esc_html( $settings['better_payment_form_success_message_purchase_details_text'] ) : esc_html__( 'Purchase Details', 'better-payment' ); $better_payment_form_success_message_print_text = !empty( $settings['better_payment_form_success_message_print_text'] ) ? esc_html( $settings['better_payment_form_success_message_print_text'] ) : esc_html__( 'Print', 'better-payment' ); $better_payment_form_success_message_view_details_btn_text = !empty( $settings['better_payment_form_success_message_view_details_btn_text'] ) ? esc_html( $settings['better_payment_form_success_message_view_details_btn_text'] ) : esc_html__( 'View Details', 'better-payment' ); $better_payment_form_success_page_view_details_url = !empty( $settings['better_payment_form_success_page_view_details_url']['url'] ) ? esc_url( $settings['better_payment_form_success_page_view_details_url']['url'] ) : 'javascript:void(0)'; ?>

🎉

Better Payment wallet :

  • find($settings['better_payment_form_fluentcart_product_id']); if ($fluentcart_product) { $product_names[] = sanitize_text_field( $fluentcart_product->post_title ); } } catch ( \Exception $e ) { // } endif; if( !empty( $settings['better_payment_form_fluentcart_product_ids'] ) && function_exists('fluentCart') && class_exists( '\FluentCart\App\Models\Product' ) ): $product_ids = $settings['better_payment_form_fluentcart_product_ids']; foreach( $product_ids as $product_id ): try { $fluentcart_product = \FluentCart\App\Models\Product::query()->find($product_id); if ($fluentcart_product) { $product_names[] = sanitize_text_field( $fluentcart_product->post_title ); } } catch ( \Exception $e ) { // } endforeach; endif; if( !empty($product_names) ): echo '' . implode( ', ', $product_names ) . ''; endif; ?>

Payment of to failed.

:

prefix}better_payment"; $result = $wpdb->get_row( $wpdb->prepare( "SELECT amount, currency, transaction_id, order_id, source FROM $table WHERE order_id=%s LIMIT 1", $order_id ) ); if ( !empty( $result ) ) { $helper_obj = new Helper(); return [ 'amount' => floatval( $result->amount ), 'currency' => sanitize_text_field( $result->currency ), 'currency_symbol' => $helper_obj->get_currency_symbol( $result->currency ), 'transaction_id' => !empty( $result->transaction_id ) ? sanitize_text_field( $result->transaction_id ) : 'N/A', 'method' => sanitize_text_field( $result->source ), ]; } return false; } /** * Whitelist the e-mail related settings that better_email_notification() reads. * * PayPal confirms payments asynchronously through the IPN listener * (handle_paypal_ipn), where the live Elementor widget / block settings are NOT * available. Stripe and Paystack build their e-mail synchronously on the return * page, so they still have the full settings array in hand. To reach parity we * persist this subset into the transaction's form_fields_info at payment-create * time, and the IPN handler rebuilds the same $settings to produce an identical * e-mail body (heading, From / To / Transaction Summary / footer sections, * subjects, headers) and to honour the "Send Email" on/off toggle. * * @param mixed $settings Full widget / block / form settings (guarded: non-array input returns []). * * @return array Whitelisted e-mail settings (safe to serialize into form_fields_info). * @since 2.2.2 */ public static function extract_email_settings( $settings = [] ) { $email_settings = []; if ( ! is_array( $settings ) ) { return $email_settings; } foreach ( $settings as $key => $value ) { if ( 0 === strpos( $key, 'better_payment_email_' ) || 0 === strpos( $key, 'better_payment_form_email_' ) || 'better_payment_form_title' === $key ) { $email_settings[ $key ] = $value; } } return $email_settings; } /** * Email template * * @since 0.0.1 */ public static function better_email_notification($transaction_id, $customer_email, $settings, $referrer='Stripe', $form_fields_info='', $is_elementor_form = 0){ //Send Email: customer and admin $better_txn_id = sanitize_text_field( $transaction_id ); $better_cus_email = sanitize_email( $customer_email ); $default_subject = sprintf(__('Better Payment transaction on %s', 'better-payment'), esc_html(get_option('blogname'))); $better_payment_global_settings = DB::get_settings(); $better_payment_email_to = !empty($better_payment_global_settings['better_payment_settings_general_email_to']) ? $better_payment_global_settings['better_payment_settings_general_email_to'] : get_option( 'admin_email' ); $better_payment_email_subject = !empty($better_payment_global_settings['better_payment_settings_general_email_subject']) ? $better_payment_global_settings['better_payment_settings_general_email_subject'] : $default_subject; $better_payment_email_content = !empty($better_payment_global_settings['better_payment_settings_general_email_message_admin']) ? $better_payment_global_settings['better_payment_settings_general_email_message_admin'] : 'Email Content'; $better_payment_email_from = !empty($better_payment_global_settings['better_payment_settings_general_email_from_email']) ? $better_payment_global_settings['better_payment_settings_general_email_from_email'] : ''; $better_payment_email_from_name = !empty($better_payment_global_settings['better_payment_settings_general_email_from_name']) ? $better_payment_global_settings['better_payment_settings_general_email_from_name'] : ''; $better_payment_email_reply_to = !empty($better_payment_global_settings['better_payment_settings_general_email_reply_to']) ? $better_payment_global_settings['better_payment_settings_general_email_reply_to'] : ''; $better_payment_email_cc = !empty($better_payment_global_settings['better_payment_settings_general_email_cc']) ? $better_payment_global_settings['better_payment_settings_general_email_cc'] : ''; $better_payment_email_bcc = !empty($better_payment_global_settings['better_payment_settings_general_email_bcc']) ? $better_payment_global_settings['better_payment_settings_general_email_bcc'] : ''; $better_payment_email_content_type = !empty($better_payment_global_settings['better_payment_settings_general_email_send_as']) ? $better_payment_global_settings['better_payment_settings_general_email_send_as'] : 'html'; $better_payment_email_subject_customer = !empty($better_payment_global_settings['better_payment_settings_general_email_subject_customer']) ? $better_payment_global_settings['better_payment_settings_general_email_subject_customer'] : $default_subject; $better_payment_email_content_customer = !empty($better_payment_global_settings['better_payment_settings_general_email_message_customer']) ? $better_payment_global_settings['better_payment_settings_general_email_message_customer'] : 'Email Content'; $better_payment_email_from_customer = !empty($better_payment_global_settings['better_payment_settings_general_email_from_email_customer']) ? $better_payment_global_settings['better_payment_settings_general_email_from_email_customer'] : ''; $better_payment_email_from_name_customer = !empty($better_payment_global_settings['better_payment_settings_general_email_from_name_customer']) ? $better_payment_global_settings['better_payment_settings_general_email_from_name_customer'] : ''; $better_payment_email_reply_to_customer = !empty($better_payment_global_settings['better_payment_settings_general_email_reply_to_customer']) ? $better_payment_global_settings['better_payment_settings_general_email_reply_to_customer'] : ''; $better_payment_email_cc_customer = !empty($better_payment_global_settings['better_payment_settings_general_email_cc_customer']) ? $better_payment_global_settings['better_payment_settings_general_email_cc_customer'] : ''; $better_payment_email_bcc_customer = !empty($better_payment_global_settings['better_payment_settings_general_email_bcc_customer']) ? $better_payment_global_settings['better_payment_settings_general_email_bcc_customer'] : ''; $better_payment_email_content_type_customer = !empty($better_payment_global_settings['better_payment_settings_general_email_send_as_customer']) ? $better_payment_global_settings['better_payment_settings_general_email_send_as_customer'] : 'html'; $args = []; $args['email_form_name'] = ! empty( $settings['better_payment_form_title'] ) ? sanitize_text_field( $settings['better_payment_form_title'] ) : 'N/A'; $args['email_content_heading_show'] = ! empty( $settings['better_payment_email_content_heading'] ) && 'yes' === $settings['better_payment_email_content_heading'] ? 1 : 0; $args['email_content_from_section_show'] = ! empty( $settings['better_payment_email_content_from_section'] ) && 'yes' === $settings['better_payment_email_content_from_section'] ? 1 : 0; $args['email_content_to_section_show'] = ! empty( $settings['better_payment_email_content_to_section'] ) && 'yes' === $settings['better_payment_email_content_to_section'] ? 1 : 0; $args['email_content_transaction_summary_show'] = ! empty( $settings['better_payment_email_content_transaction_summary'] ) && 'yes' === $settings['better_payment_email_content_transaction_summary'] ? 1 : 0; $args['email_content_footer_text_show'] = ! empty( $settings['better_payment_email_content_footer_text'] ) && 'yes' === $settings['better_payment_email_content_footer_text'] ? 1 : 0; $args['email_content_customer'] = ! empty( $settings['better_payment_email_content_customer'] ) ? $settings['better_payment_email_content_customer'] : ''; $args['email_content_admin'] = ! empty( $settings['better_payment_email_content'] ) ? $settings['better_payment_email_content'] : ''; $args['is_elementor_form'] = $is_elementor_form; if ( ! empty( $settings['better_payment_email_content_greeting'] ) ) { $args['email_content_greeting'] = $settings['better_payment_email_content_greeting']; } if ( ! empty( $settings['better_payment_form_email_logo']['url'] ) ) { $args['email_logo_url'] = $settings['better_payment_form_email_logo']['url']; } if ( ! empty( $settings['better_payment_form_email_attachment']['id'] ) ) { $args['email_attachment_id'] = intval( $settings['better_payment_form_email_attachment']['id'] ); $args['email_attachment_path'] = get_attached_file( $args['email_attachment_id'] ); } if ( ! empty( $settings['better_payment_form_email_attachment_pdf_show'] ) && ! empty( $settings['better_payment_form_email_attachment_pdf']['id'] ) ) { $args['email_attachment_id'] = intval( $settings['better_payment_form_email_attachment_pdf']['id'] ); $args['email_attachment_path'] = get_attached_file( $args['email_attachment_id'] ); } if(isset($settings['better_payment_email_to'])){ $better_payment_email_to = sanitize_email($settings['better_payment_email_to']); } if(isset($settings['better_payment_email_subject'])){ $better_payment_email_subject = sanitize_text_field($settings['better_payment_email_subject']); } if(isset($settings['better_payment_email_subject_customer'])){ $better_payment_email_subject_customer = sanitize_text_field($settings['better_payment_email_subject_customer']); } if(isset($settings['better_payment_email_content_type'])){ $better_payment_email_content_type = sanitize_text_field($settings['better_payment_email_content_type']); } if(isset($settings['better_payment_email_content_type_customer'])){ $better_payment_email_content_type_customer = sanitize_text_field($settings['better_payment_email_content_type_customer']); } $args['email_content_type'] = $better_payment_email_content_type; $args['email_content_type_customer'] = $better_payment_email_content_type_customer; $line_break = $better_payment_email_content_type == 'html' ? '
' : "\n"; $line_break_customer = $better_payment_email_content_type_customer == 'html' ? '
' : "\n"; global $wpdb; $table = "{$wpdb->prefix}better_payment"; $transaction_obj = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table WHERE transaction_id=%s limit 1", sanitize_text_field( $transaction_id ) ) ); if(isset($settings['better_payment_email_content'])){ $better_payment_email_content = sanitize_text_field($settings['better_payment_email_content']); $better_payment_email_content = self::better_render_email_body( $better_payment_email_content, $form_fields_info, $line_break, 'admin', $transaction_obj, $args ); } else if ( !empty($better_payment_email_content) ){ $better_payment_email_content = self::better_render_email_body( $better_payment_email_content, $form_fields_info, $line_break, 'admin', $transaction_obj, $args ); } if(isset($settings['better_payment_email_content_customer'])){ $better_payment_email_content_customer = sanitize_text_field($settings['better_payment_email_content_customer']); $better_payment_email_content_customer = self::better_render_email_body( $better_payment_email_content_customer, $form_fields_info, $line_break_customer, 'customer', $transaction_obj, $args ); } else if ( !empty($better_payment_email_content_customer) ){ $better_payment_email_content_customer = self::better_render_email_body( $better_payment_email_content_customer, $form_fields_info, $line_break, 'customer', $transaction_obj, $args ); //$line_break_customer not used : need to check type on parent method and add headers accordingly. } if(isset($settings['better_payment_email_from'])){ $better_payment_email_from = sanitize_email($settings['better_payment_email_from']); } if(isset($settings['better_payment_email_from_name'])){ $better_payment_email_from_name = sanitize_text_field($settings['better_payment_email_from_name']); } if(isset($settings['better_payment_email_reply_to'])){ $better_payment_email_reply_to = sanitize_email($settings['better_payment_email_reply_to']); } if(isset($settings['better_payment_email_cc'])){ $better_payment_email_cc = sanitize_email($settings['better_payment_email_cc']); } if(isset($settings['better_payment_email_bcc'])){ $better_payment_email_bcc = sanitize_email($settings['better_payment_email_bcc']); } //Customer Email if(isset($settings['better_payment_email_from_customer'])){ $better_payment_email_from_customer = sanitize_email($settings['better_payment_email_from_customer']); } if(isset($settings['better_payment_email_from_name_customer'])){ $better_payment_email_from_name_customer = sanitize_text_field($settings['better_payment_email_from_name_customer']); } if(isset($settings['better_payment_email_reply_to_customer'])){ $better_payment_email_reply_to_customer = sanitize_email($settings['better_payment_email_reply_to_customer']); } if(isset($settings['better_payment_email_cc_customer'])){ $better_payment_email_cc_customer = sanitize_email($settings['better_payment_email_cc_customer']); } if(isset($settings['better_payment_email_bcc_customer'])){ $better_payment_email_bcc_customer = sanitize_email($settings['better_payment_email_bcc_customer']); } if($better_payment_email_content == ''){ $better_payment_email_content = __('New better payment transaction! ', 'better-payment'); } if($better_payment_email_content_customer == ''){ $better_payment_email_content_customer = __('New better payment transaction! ', 'better-payment'); } $better_payment_email_headers = []; $better_payment_email_headers_customer = []; if($better_payment_email_content_type == 'html'){ $better_payment_email_headers[] = 'Content-Type: text/html; charset=UTF-8'; } if($better_payment_email_content_type_customer == 'html'){ $better_payment_email_headers_customer[] = 'Content-Type: text/html; charset=UTF-8'; } if($better_payment_email_from != ''){ $better_payment_email_headers[] = "From: $better_payment_email_from_name <$better_payment_email_from>"; } if($better_payment_email_cc != ''){ $better_payment_email_headers[] = "Cc: <$better_payment_email_cc>"; } if($better_payment_email_bcc != ''){ $better_payment_email_headers[] = "BCc: $better_payment_email_bcc"; } if($better_payment_email_reply_to != ''){ $better_payment_email_headers[] = "Reply-To: $better_payment_email_reply_to"; } //Customer Email if($better_payment_email_from_customer != ''){ $better_payment_email_headers_customer[] = "From: $better_payment_email_from_name_customer <$better_payment_email_from_customer>"; } $form_fields_info_array = []; if( ! empty( $form_fields_info ) ){ $form_fields_info_array = maybe_unserialize( $form_fields_info ); } $form_fields_info_cus_email = ! empty( $form_fields_info_array['primary_email'] ) ? sanitize_email( $form_fields_info_array['primary_email'] ) : ''; if($better_payment_email_cc_customer != '' || ! empty( $form_fields_info_cus_email ) ){ $better_payment_email_cc_customer_multiple = implode(',', [$form_fields_info_cus_email, $better_payment_email_cc_customer]); $better_payment_email_headers_customer[] = "Cc: $better_payment_email_cc_customer_multiple"; } if($better_payment_email_bcc_customer != ''){ $better_payment_email_headers_customer[] = "BCc: $better_payment_email_bcc_customer"; } if($better_payment_email_reply_to_customer != ''){ $better_payment_email_headers_customer[] = "Reply-To: $better_payment_email_reply_to_customer"; } $email_attachments = ! empty( $args['email_attachment_path'] ) ? [ $args['email_attachment_path'] ] : []; $allowed_extensions = array('jpg', 'jpeg', 'png', 'pdf'); $file_info = count( $email_attachments ) ? pathinfo($email_attachments[0]) : []; $file_extension = count( $file_info ) ? strtolower($file_info['extension']) : ''; if ( ! in_array( $file_extension, $allowed_extensions ) ){ $email_attachments = []; } self::send_better_email($better_cus_email, $better_payment_email_subject_customer, $better_payment_email_content_customer, $better_payment_email_headers_customer, $email_attachments); //customer mail self::send_better_email($better_payment_email_to, $better_payment_email_subject, $better_payment_email_content, $better_payment_email_headers); //admin mail } /** * Sends an HTML email. * * @since 0.0.2 * * @param string $to * @param string $subject * @param array $message * * @return bool false indicates mail sending failed while true doesn't gurantee that mail sent. * true just indicate script processed successfully without any errors. * Ref: https://developer.wordpress.org/reference/functions/wp_mail/ */ public static function send_better_email( $to, $subject, $message, $headers=[], $attachments = [] ) { $response = false; $mailValidated = filter_var($to, FILTER_VALIDATE_EMAIL); if ($mailValidated) { $response = wp_mail( $to, $subject, $message, $headers, $attachments ); } return $response; } /** * Email body * * * @return string * @since 0.0.1 */ public static function better_render_email_body( $email_body, $form_fields_info, $line_break, $type = 'admin', $transaction_obj = null, $args = [] ) { $transaction_id = $currency = $payment_date = $customer_name = $customer_first_name = $customer_last_name = $amount = ''; $helper_obj = new Helper(); if(null !== $transaction_obj && is_object($transaction_obj)){ $transaction_id = $transaction_obj->transaction_id; $amount = $transaction_obj->amount; $status = $transaction_obj->status; $source = $transaction_obj->source; $currency = $transaction_obj->currency; $currency_symbol = $helper_obj->get_currency_symbol( esc_html($currency) ); $payment_date = wp_date(get_option('date_format').' '.get_option('time_format'), strtotime($transaction_obj->payment_date)); } $referer_content_page_link = '#'; $form_fields_info_arr = maybe_unserialize($form_fields_info); $form_fields_info_arr = is_array( $form_fields_info_arr ) ? $form_fields_info_arr : []; $email_logo_url = ! empty( $args['email_logo_url'] ) ? $args['email_logo_url'] : ''; $email_content_greeting = ! empty( $args['email_content_greeting'] ) ? 1 : 0; $email_form_name = ! empty( $args['email_form_name'] ) ? $args['email_form_name'] : 'N/A'; $form_field_rows = []; foreach ( $form_fields_info_arr as $key => $field ) { //Hide few fields: storage keys, not fields the visitor filled in (the From section hides the same ones) if( $key === 'referer_page_id' || $key === 'referer_widget_id' || $key === 'source' || $key === 'el_form_fields' || $key === 'amount_quantity' || $key === 'woo_product_id' || $key === 'woo_product_ids' || $key === 'fluentcart_product_id' || $key === 'fluentcart_product_ids' || $key === 'detailed_product_info' || $key === 'email_settings' ) { if($key === 'referer_page_id'){ $referer_content_page_link = !empty($field) ? get_permalink( $field ) : $referer_content_page_link; } continue; } $label_key = $key; if($key === 'primary_first_name'){ $label_key = 'first_name'; $customer_first_name = $field; } if($key === 'primary_last_name'){ $label_key = 'last_name'; $customer_last_name = $field; } if($key === 'primary_email'){ $label_key = 'email'; } if($key === 'primary_payment_amount'){ $label_key = 'payment_amount'; } $key_formatted = self::better_title_case($label_key); if($key_formatted === 'Amount'){ $key_formatted = __('Paid', 'better-payment'); } $form_field_rows[] = [ 'key' => $key, 'label' => $key_formatted, 'value' => $field, ]; } // A Pro payment's own fields (split payments, layout 6) are hidden or relabelled by Pro. $all_fields_rows = PaymentEntitlement::email_rows( $form_field_rows, 'all_fields', $form_fields_info_arr ); if($customer_first_name) { $customer_name = $customer_first_name . ' '; } if($customer_last_name) { $customer_name .= $customer_last_name . ' '; } $allowed_sources = ['paypal', 'stripe', 'paystack']; $source_image_url = BETTER_PAYMENT_ASSETS . '/img/stripe.png'; $source_image_alt = 'Stripe'; if( in_array( strtolower( $transaction_obj->source ), $allowed_sources ) ){ $source_image_url = strtolower( $transaction_obj->source ) == 'paypal' ? BETTER_PAYMENT_ASSETS . '/img/paypal.png' : BETTER_PAYMENT_ASSETS . "/img/{$transaction_obj->source}.png"; $source_image_alt = strtolower( $transaction_obj->source ) == 'paypal' ? 'PayPal' : ucfirst( strtolower( $transaction_obj->source ) ); } $amount_quantity = ! empty( $form_fields_info_arr['amount_quantity'] ) ? intval( $form_fields_info_arr['amount_quantity'] ) : 1; // #ToDo Product id or ids with comma $woo_product_id = ! empty( $form_fields_info_arr['woo_product_id'] ) ? intval( $form_fields_info_arr['woo_product_id'] ) : 0; $fluentcart_product_id = ! empty( $form_fields_info_arr['fluentcart_product_id'] ) ? intval( $form_fields_info_arr['fluentcart_product_id'] ) : 0; $woo_product_ids = ! empty( $form_fields_info_arr['woo_product_ids'] ) ? maybe_unserialize( $form_fields_info_arr['woo_product_ids'] ) : [0]; $fluentcart_product_ids = ! empty( $form_fields_info_arr['fluentcart_product_ids'] ) ? maybe_unserialize( $form_fields_info_arr['fluentcart_product_ids'] ) : [0]; $product_name = ''; $product_permalink = ''; $product_image_src = ''; // Handle WooCommerce products if (function_exists('wc_get_product') && $woo_product_id ) { $bp_woocommerce_product = wc_get_product($woo_product_id); $product = $bp_woocommerce_product; if ($product) { $product_name = $product->get_name(); $product_permalink = get_permalink($product->get_id()); $product_price = $product->get_price(); $product_image_src_array = wp_get_attachment_image_src( get_post_thumbnail_id( $product->get_id() ), 'single-post-thumbnail' ); $product_image_src = is_array( $product_image_src_array ) && count( $product_image_src_array ) ? $product_image_src_array[0] : ''; } } if (function_exists('fluentCart') && $fluentcart_product_id ) { try { $fluentcart_product = \FluentCart\App\Models\Product::query() ->with(['detail', 'variants']) ->find($fluentcart_product_id); if ($fluentcart_product) { $product_name = $fluentcart_product->post_title; $product_permalink = get_permalink($fluentcart_product->ID); // Get price from the first variant or detail $product_price = 0; if (!empty($fluentcart_product->variants) && count($fluentcart_product->variants) > 0) { $product_price = $fluentcart_product->variants[0]->item_price; } elseif (!empty($fluentcart_product->detail)) { $product_price = $fluentcart_product->detail->min_price; } $product_image_src_array = wp_get_attachment_image_src( get_post_thumbnail_id( $fluentcart_product->ID ), 'single-post-thumbnail' ); $product_image_src = is_array( $product_image_src_array ) && count( $product_image_src_array ) ? $product_image_src_array[0] : ''; } } catch ( \Exception $e ) { // Handle error silently } } $detailed_product_info = isset( $form_fields_info_arr['detailed_product_info'] ) ? maybe_unserialize( $form_fields_info_arr['detailed_product_info'] ) : []; $all_data = [ 'amount' => $amount, 'amount_quantity' => $amount_quantity, 'woo_product_id' => $woo_product_id, 'woo_product_ids' => $woo_product_ids, 'fluentcart_product_id' => $fluentcart_product_id, 'fluentcart_product_ids' => $fluentcart_product_ids, 'product_name' => $product_name, 'product_permalink' => $product_permalink, 'product_image_src' => $product_image_src, 'detailed_product_info' => $detailed_product_info, 'amount_single' => $amount_quantity > 0 ? floatval( $amount / $amount_quantity ) : $amount, 'currency' => ! empty( $currency ) ? $currency : '', 'currency_symbol' => ! empty( $currency_symbol ) ? $currency_symbol : '', 'payment_date_time' => ! empty( $payment_date ) ? $payment_date : '', 'transaction_id' => ! empty( $transaction_id ) ? $transaction_id : '', 'payment_date_only' => wp_date(get_option('date_format'), strtotime($transaction_obj->payment_date)), 'status' => ucfirst( $status ), 'form_fields_info_arr' => $form_fields_info_arr, // 'admin_name' => $admin_name, 'customer_name' => $customer_name, 'site_title' => get_bloginfo( 'name' ), 'site_admin_email' => get_bloginfo( 'admin_email' ), 'form_name' => $email_form_name, 'payment_method' => ucfirst( $source ), 'email_logo_url' => $email_logo_url, 'source_image_url' => $source_image_url, 'source_image_alt' => $source_image_alt, 'view_transaction_link' => admin_url("admin.php?page=better-payment-transactions&action=view&id={$transaction_obj->id}"), 'email_content_heading_show' => intval( $args['email_content_heading_show'] ), 'email_content_from_section_show' => intval( $args['email_content_from_section_show'] ), 'email_content_to_section_show' => intval( $args['email_content_to_section_show'] ), 'email_content_transaction_summary_show' => intval( $args['email_content_transaction_summary_show'] ), 'email_content_footer_text_show' => intval( $args['email_content_footer_text_show'] ), // The form's messages, with `[bp-all-fields]` replaced where the author placed it. 'email_content_customer' => self::render_email_message( $args['email_content_customer'] ?? '', $all_fields_rows, 'plain' === ( $args['email_content_type_customer'] ?? 'html' ) ), 'email_content_admin' => self::render_email_message( $args['email_content_admin'] ?? '', $all_fields_rows, 'plain' === ( $args['email_content_type'] ?? 'html' ) ), 'is_elementor_form' => intval( $args['is_elementor_form'] ), 'email_content_type' => $args['email_content_type'] ?? 'html', 'email_content_type_customer' => $args['email_content_type_customer'] ?? 'html', ]; // Email V2: the template is the whole body. ob_start(); include BETTER_PAYMENT_ADMIN_VIEWS_PATH . "/template-email-notification.php"; $email_body = ob_get_contents(); ob_end_clean(); return $email_body; } /** * A form's e-mail message, with `[bp-all-fields]` replaced by the submitted fields. * * The template prints the message; the fields used to be built into a body the template's * output then overwrote, so the token reached every e-mail as literal text. The message is * the form's own setting and is kses-filtered as it always was. The fields are the * visitor's, so they are escaped here and inserted after the filter — never passed through * it or do_shortcode(), where a submitted value could add markup or run a shortcode. * * @since 2.4.0 * * @param mixed $message The form's message setting. * @param array[] $rows `label` and `value` of each field to list, in order. * @param bool $is_plain Whether the e-mail is sent as plain text. * @return string */ public static function render_email_message( $message, array $rows, $is_plain = false ) { $message = wp_kses_post( is_scalar( $message ) ? (string) $message : '' ); if ( false === strpos( $message, '[bp-all-fields]' ) ) { return $message; } return str_replace( '[bp-all-fields]', self::email_all_fields_markup( $rows, $is_plain ), $message ); } /** * The `[bp-all-fields]` block: a two-column table, or `Label: value` lines in a plain-text e-mail. * * @param array[] $rows `label` and `value` of each field. * @param bool $is_plain Plain text instead of HTML. * @return string */ private static function email_all_fields_markup( array $rows, $is_plain ) { $lines = []; $cells = ''; foreach ( $rows as $row ) { if ( ! isset( $row['label'] ) || ! is_scalar( $row['label'] ) || ! array_key_exists( 'value', $row ) ) { continue; } // A nested value (stored settings, a product list) is not something a visitor typed. if ( ! is_scalar( $row['value'] ) && null !== $row['value'] ) { continue; } $label = (string) $row['label']; $value = (string) $row['value']; if ( $is_plain ) { $lines[] = rtrim( wp_strip_all_tags( $label ), ': ' ) . ': ' . wp_strip_all_tags( $value ); continue; } $cells .= '' . '' . esc_html( $label ) . '' . '' . esc_html( $value ) . '' . ''; } if ( $is_plain ) { return implode( "\n", $lines ); } return '' . $cells . '
'; } /** * String helper method * helps to convert string to title case * * @return string * @since 0.0.1 */ public static function better_title_case($string){ $string = str_replace('_',' ', $string); $string = ucwords($string); return $string; } /** * Safe icon rendering for payment form messages * * Handles multiple icon types: FontAwesome, Dashicons, SVG from media library * Works with or without Elementor being active * * @param array $icon_settings Icon settings from block/widget controls * @param array $html_attributes Optional HTML attributes for the icon element * * @return void (outputs HTML directly) * @since 2.0.4 */ public static function render_icon( $icon_settings = [], $html_attributes = [] ) { // Bail if no settings provided if ( empty( $icon_settings ) || ! is_array( $icon_settings ) ) { return; } // If value is empty, nothing to render if ( empty( $icon_settings['value'] ) ) { return; } $library = ! empty( $icon_settings['library'] ) ? $icon_settings['library'] : ''; $value = $icon_settings['value']; // Try to use Elementor's Icons_Manager if it's available and loaded if ( did_action( 'elementor/loaded' ) && class_exists( '\Elementor\Icons_Manager' ) ) { try { \Elementor\Icons_Manager::render_icon( $icon_settings, $html_attributes ); return; } catch ( \Exception $e ) { // If Elementor rendering fails, fall back to manual rendering } } // Handle SVG (uploaded via media library) if ( $library === 'svg' ) { if ( is_array( $value ) && ! empty( $value['url'] ) ) { $svg_url = esc_url( $value['url'] ); echo 'Icon'; return; } } // Handle FontAwesome icons (all variants: fa, fas, far, fab, fal, fad, etc.) if ( in_array( $library, [ 'fa', 'fas', 'far', 'fab', 'fal', 'fad', 'fontawesome', 'font-awesome' ], true ) ) { if ( is_string( $value ) && ! empty( $value ) ) { // Build complete FontAwesome class $icon_class = 'fa'; // Add variant if specified (fas, far, fab, fal) if ( ! empty( $library ) && $library !== 'fa' && $library !== 'fontawesome' && $library !== 'font-awesome' ) { $icon_class = esc_attr( $library ); } else { // Default to 'fas' if no variant specified $icon_class = 'fas'; } // Add the specific icon class $icon_class .= ' ' . esc_attr( $value ); $html_class = ! empty( $html_attributes['class'] ) ? ' ' . $html_attributes['class'] : ''; $aria_hidden = isset( $html_attributes['aria-hidden'] ) ? ' aria-hidden="' . esc_attr( $html_attributes['aria-hidden'] ) . '"' : ' aria-hidden="true"'; echo ''; return; } } // Handle Dashicons if ( in_array( $library, [ 'dashicons' ], true ) ) { if ( is_string( $value ) && ! empty( $value ) ) { $icon_class = 'dashicons dashicons-' . esc_attr( $value ); $html_class = ! empty( $html_attributes['class'] ) ? ' ' . $html_attributes['class'] : ''; $aria_hidden = isset( $html_attributes['aria-hidden'] ) ? ' aria-hidden="' . esc_attr( $html_attributes['aria-hidden'] ) . '"' : ' aria-hidden="true"'; echo ''; return; } } // Handle custom CSS classes (like 'bp-icon bp-check-circle') // These are custom icon fonts or CSS-based icons defined by the theme/plugin if ( 'custom' === $library ) { if ( is_string( $value ) && ! empty( $value ) ) { $html_class = ! empty( $html_attributes['class'] ) ? ' ' . $html_attributes['class'] : ''; $aria_hidden = isset( $html_attributes['aria-hidden'] ) ? ' aria-hidden="' . esc_attr( $html_attributes['aria-hidden'] ) . '"' : ' aria-hidden="true"'; echo ''; return; } } // If value is a plain string (might be a FontAwesome class without library specified) if ( is_string( $value ) && ! empty( $value ) && empty( $library ) ) { // Check if it looks like a FontAwesome class if ( strpos( $value, 'fa' ) !== false || strpos( $value, 'fa-' ) !== false ) { $icon_class = 'fas ' . esc_attr( $value ); $html_class = ! empty( $html_attributes['class'] ) ? ' ' . $html_attributes['class'] : ''; $aria_hidden = isset( $html_attributes['aria-hidden'] ) ? ' aria-hidden="' . esc_attr( $html_attributes['aria-hidden'] ) . '"' : ' aria-hidden="true"'; echo ''; return; } // Check if it looks like a dashicon if ( strpos( $value, 'dashicon' ) !== false || strpos( $value, 'dashicons-' ) !== false ) { $icon_class = 'dashicons ' . esc_attr( $value ); $html_class = ! empty( $html_attributes['class'] ) ? ' ' . $html_attributes['class'] : ''; $aria_hidden = isset( $html_attributes['aria-hidden'] ) ? ' aria-hidden="' . esc_attr( $html_attributes['aria-hidden'] ) . '"' : ' aria-hidden="true"'; echo ''; return; } } } }