* @since 0.0.1 */ protected $subscription_plan; /** * Amount type. * * @var string * @since 0.0.1 */ protected $amount_type; /** * Fixed amount. * * @var float * @since 0.0.1 */ protected $fixed_amount; /** * Minimum amount. * * @var float * @since 0.0.1 */ protected $minimum_amount; /** * Customer name field slug. * * @var string * @since 0.0.1 */ protected $customer_name_field; /** * Customer email field slug. * * @var string * @since 0.0.1 */ protected $customer_email_field; /** * Variable amount field slug. * * @var string * @since 0.0.1 */ protected $variable_amount_field; /** * Payment methods enabled for this block. * * @var array * @since 1.0.0 */ protected $payment_methods = []; /** * Aria-describedby attribute value. * * @var string * @since 0.0.1 */ protected $aria_described_by = ''; /** * The payment type as configured in the editor, before the default-choice and * Pro-availability rewrites below. * * $payment_type holds the *active* type being rendered; this holds the admin's * intent, so 'both' can be distinguished from a plain single-mode block. * * @var string * @since 1.5.1 */ protected $original_payment_type = 'one-time'; /** * Which choice is pre-selected in 'both' mode ('one-time' or 'subscription'). * * @var string * @since 1.5.1 */ protected $default_payment_choice = 'one-time'; /** * Admin-configured label for the "make it recurring" opt-in checkbox, shown only * in 'both' mode. * * @var string * @since 1.5.1 */ protected $recurring_toggle_label = ''; /** * One-time amount configuration. Used only in 'both' mode. * * @var array * @since 1.5.1 */ protected $one_time_config = []; /** * Subscription amount configuration. Used only in 'both' mode. * * @var array * @since 1.5.1 */ protected $subscription_config = []; /** * Constructor for the Payment Markup class. * * @param array $attributes Block attributes. * @since 0.0.1 */ public function __construct( $attributes ) { // Get payment settings from Stripe Helper. $this->stripe_connected = Stripe_Helper::is_stripe_connected(); $this->payment_mode = Payment_Helper::get_payment_mode(); $this->slug = 'payment'; $this->set_properties( $attributes ); $this->set_unique_slug(); $this->set_markup_properties(); // Build aria-describedby attribute. $this->aria_described_by = $this->get_aria_describedby(); // Set payment-specific properties. $this->amount = $attributes['amount'] ?? 10; $this->currency = $attributes['currency'] ?? 'USD'; // Use currency from settings if not specified in block. if ( empty( $this->currency ) || 'USD' === $this->currency ) { $this->currency = Payment_Helper::get_currency(); } // Get the publishable key for this form's selected account (mode-aware). $stripe_account_id = Stripe_Helper::resolve_account_for_form( $this->form_id ); $this->stripe_publishable_key = Stripe_Helper::get_stripe_publishable_key( '', $stripe_account_id ); // Stripe told us at connect (or on account.updated) that this account // cannot charge cards in this mode. Rendering the card box anyway is what // gave donors an empty "Credit / Debit Card" accordion and lost every // attempt in silence. Only a positively-known-bad state counts — an // account we have never checked stays enabled, because a stale flag // hiding a working gateway is a worse failure than the one being fixed. $this->stripe_card_blocked = Stripe_Helper::is_card_capability_blocked( $stripe_account_id ); // Fall back to one-time if a recurring path is configured but Pro is not // active or too old. 'both' collapses too, which is what hides the donor's // recurring choice: the chooser only renders while the type is still 'both'. // // The collapse lives in Payment_Helper::effective_payment_type(), shared // with validate_payment_type(). Both have to reach the same answer: if the // validator disagrees with what this markup rendered, it rejects the very // request its own form invited. // // $configured_type stays the RAW attribute — the 'both' branch further down // keys on it deliberately, because a collapsed form still has 'both' stored // and validate_payment_amount() reads that stored config. $configured_type = $attributes['paymentType'] ?? 'one-time'; $this->payment_type = Payment_Helper::effective_payment_type( $configured_type ); $this->subscription_plan = $attributes['subscriptionPlan'] ?? []; $this->amount_type = $attributes['amountType'] ?? 'fixed'; $this->fixed_amount = $attributes['fixedAmount'] ?? 10; $this->minimum_amount = $attributes['minimumAmount'] ?? 0; // Set customer field mappings. $this->customer_name_field = $attributes['customerNameField'] ?? ''; $this->customer_email_field = $attributes['customerEmailField'] ?? ''; // Set variable amount field mapping. $this->variable_amount_field = $attributes['variableAmountField'] ?? ''; // Dual-mode ('both') configuration. $payment_type is rewritten below to the // active choice, so keep the admin's intent separately. $this->original_payment_type = $this->payment_type; $this->default_payment_choice = $attributes['defaultPaymentChoice'] ?? 'one-time'; if ( ! in_array( $this->default_payment_choice, [ 'one-time', 'subscription' ], true ) ) { $this->default_payment_choice = 'one-time'; } $this->recurring_toggle_label = $attributes['recurringToggleLabel'] ?? __( 'Make this a recurring donation', 'suredonation' ); $this->one_time_config = [ 'amount_type' => $attributes['oneTimeAmountType'] ?? 'fixed', 'fixed_amount' => isset( $attributes['oneTimeFixedAmount'] ) ? (float) $attributes['oneTimeFixedAmount'] : 10.0, 'minimum_amount' => isset( $attributes['oneTimeMinimumAmount'] ) ? (float) $attributes['oneTimeMinimumAmount'] : 0.0, 'variable_field' => $attributes['oneTimeVariableAmountField'] ?? '', ]; $this->subscription_config = [ 'amount_type' => $attributes['subscriptionAmountType'] ?? 'fixed', 'fixed_amount' => isset( $attributes['subscriptionFixedAmount'] ) ? (float) $attributes['subscriptionFixedAmount'] : 10.0, 'minimum_amount' => isset( $attributes['subscriptionMinimumAmount'] ) ? (float) $attributes['subscriptionMinimumAmount'] : 0.0, 'variable_field' => $attributes['subscriptionVariableAmountField'] ?? '', ]; // In 'both' mode the initially visible amount — and the mode Stripe Elements // initialises in — is the default choice. Rewrite the scalar properties so all // the rendering below keeps working unchanged; the chooser's JS re-syncs them // client-side when the donor switches. // // Keyed on the CONFIGURED type, not the (possibly collapsed) active type: when // the recurring add-on is inactive a 'both' form collapses to one-time and the // chooser never renders, but validate_payment_amount() still reads the stored // 'both' config and checks the submitted amount against the one_time sub-config. // Seed the active scalars from that sub-config rather than the shared // attributes, so the price the donor sees matches the price validation enforces // — otherwise a form whose shared and one-time amounts diverge renders one // price and then rejects it with no admin signal. if ( 'both' === $configured_type ) { // The chooser now defaults to one-time and opts into recurring via a // checkbox, so a 'both' block always initialises in one-time regardless of // the (legacy) defaultPaymentChoice — the JS reveals the subscription panel // and re-syncs these scalars when the box is ticked. This is also what // validate_payment_amount() demands when Pro is inactive and the form has // collapsed to one-time, so the two cases converge. $this->default_payment_choice = 'one-time'; $this->payment_type = 'one-time'; $this->amount_type = $this->one_time_config['amount_type']; $this->fixed_amount = $this->one_time_config['fixed_amount']; $this->minimum_amount = $this->one_time_config['minimum_amount']; $this->variable_amount_field = $this->one_time_config['variable_field']; } // Parse payment methods from block attributes (with backward compat fallback). $block_payment_methods = $attributes['paymentMethods'] ?? null; if ( ! is_array( $block_payment_methods ) || empty( $block_payment_methods ) ) { $gateway = $attributes['gateway'] ?? 'stripe'; $block_payment_methods = [ is_string( $gateway ) ? $gateway : 'stripe' ]; } // Filter to only globally-available gateways. $available = []; foreach ( $block_payment_methods as $method ) { if ( 'stripe' === $method && $this->stripe_connected && ! empty( $this->stripe_publishable_key ) && ! $this->stripe_card_blocked ) { $available[] = 'stripe'; } elseif ( 'offline' === $method && Offline_Helper::is_offline_enabled() ) { $available[] = 'offline'; } } /** * Filter the list of available payment methods for this block. * * Allows extensions (e.g., PayPal) to add themselves when connected. * * @param array $available Currently available method IDs. * @param array $block_payment_methods Methods selected in the block. * @param array $attributes Block attributes. * @since 1.0.0 */ $available = apply_filters( 'suredonation_available_payment_methods', $available, $block_payment_methods, $attributes ); // The fallback exists so a form still advertises something when nothing // resolved as available, but it must not resurrect a gateway we // positively know cannot charge: `payment_methods` is what // `data-payment-methods` advertises to the frontend script, and listing // a gateway whose container was never rendered makes the script select // it, try to mount into nothing, and never initialise the gateway that // does work — an empty box again, by a different route. $fallback = $this->stripe_card_blocked ? array_values( array_diff( $block_payment_methods, [ 'stripe' ] ) ) : $block_payment_methods; $this->payment_methods = ! empty( $available ) ? $available : $fallback; // BACKWARD COMPATIBILITY: Migrate customer fields from subscriptionPlan. if ( empty( $this->customer_name_field ) && ! empty( $this->subscription_plan['customer_name'] ) ) { $this->customer_name_field = $this->subscription_plan['customer_name']; } if ( empty( $this->customer_email_field ) && ! empty( $this->subscription_plan['customer_email'] ) ) { $this->customer_email_field = $this->subscription_plan['customer_email']; } } /** * Render the payment field markup. * * @return string * @since 0.0.1 */ public function markup() { $has_stripe = in_array( 'stripe', $this->payment_methods, true ); // Determine which gateways are actually connected/available for this form. // get_registered_payment_methods() returns only enabled methods (Stripe when // connected, Offline when enabled, plus any added by extensions such as // PayPal), so it is the single gateway-agnostic source of truth. $methods = $this->get_registered_payment_methods(); // No payment gateway is connected/available. Show a clear message instead of // returning an empty string, which previously left donors with a silently // broken form and no way to donate. See issue #219. if ( empty( $methods ) ) { return $this->render_gateway_unavailable_notice(); } // Validate payment field requirements. if ( ! $this->validate_payment_requirements() ) { return ''; } $field_classes = $this->get_field_classes(); $payment_methods_csv = implode( ',', $this->payment_methods ); $default_gateway = $this->payment_methods[0]; ob_start(); ?>
stripe_publishable_key ) ) { ?> data-stripe-key="stripe_publishable_key ); ?>" data-currency="currency ) ); ?>" data-currency-symbol="currency ) ); ?>" data-payment-mode="payment_mode ); ?>" data-amount-type="amount_type ); ?>" data-fixed-amount="fixed_amount ); ?>" data-payment-type="payment_type ); ?>" data-customer-name-field="customer_name_field ); ?>" data-customer-email-field="customer_email_field ); ?>" data-nonce="" amount_type ) { ?> data-variable-amount-field="variable_amount_field ); ?>" get_inactive_variable_field() ) { ?> data-variable-amount-field="get_inactive_variable_field() ); ?>" minimum_amount > 0 ) { ?> data-minimum-amount="minimum_amount ); ?>" original_payment_type ) { ?> data-original-payment-type="both" data-default-payment-choice="default_payment_choice ); ?>" data-one-time-amount-type="one_time_config['amount_type'] ) ); ?>" data-one-time-fixed-amount="one_time_config['fixed_amount'] ) ); ?>" data-one-time-minimum-amount="one_time_config['minimum_amount'] ) ); ?>" data-one-time-variable-amount-field="one_time_config['variable_field'] ) ); ?>" data-subscription-amount-type="subscription_config['amount_type'] ) ); ?>" data-subscription-fixed-amount="subscription_config['fixed_amount'] ) ); ?>" data-subscription-minimum-amount="subscription_config['minimum_amount'] ) ); ?>" data-subscription-variable-amount-field="subscription_config['variable_field'] ) ); ?>" has_subscription_path() && ! empty( $this->subscription_plan ) ) { ?> data-subscription-plan-name="subscription_plan['name'] ) ? Helper::get_string_value( $this->subscription_plan['name'] ) : __( 'Subscription Plan', 'suredonation' ) ); ?>" data-subscription-interval="subscription_plan['interval'] ) ? Helper::get_string_value( $this->subscription_plan['interval'] ) : 'month' ); ?>" data-subscription-billing-cycles="subscription_plan['billingCycles'] ) ? Helper::get_string_value( $this->subscription_plan['billingCycles'] ) : '0' ); ?>" > label_markup ); ?> help_markup ); ?>
original_payment_type ) { // Donor picks the type; both amount panels are rendered and the // chooser's JS toggles which one is visible. echo wp_kses( $this->render_payment_type_chooser(), Helper::get_allowed_form_html() ); echo wp_kses( $this->render_dual_amount_displays(), Helper::get_allowed_form_html() ); } else { echo wp_kses( $this->render_amount_display(), Helper::get_allowed_form_html() ); } // Test mode notice. if ( 'test' === $this->payment_mode && $has_stripe ) { echo wp_kses_post( $this->get_test_mode_notice() ); } // Admin-only: the form renders (Offline is enabled) but no real // gateway is connected. echo wp_kses_post( $this->get_gateway_setup_notice() ); // Admin-only: the form renders on its other gateways, but the // card form was dropped because Stripe cannot charge. echo wp_kses_post( $this->get_capability_notice() ); // Payment methods accordion ( $methods computed above ). echo $this->render_payment_methods_accordion( $methods ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Method builds markup with esc_attr/esc_html on all dynamic values and wp_kses_post on content. ?>
Array of payment method configurations. * @since 1.0.0 */ private function get_registered_payment_methods() { $available_methods = [ 'stripe' => [ 'id' => 'stripe', 'label' => __( 'Credit / Debit Card', 'suredonation' ), // The capability check belongs here rather than in the // `suredonation_available_payment_methods` loop above: that loop // falls back to the raw block selection when it ends up empty, so // dropping Stripe there would put it straight back on a form that // offers nothing else. 'enabled' => $this->stripe_connected && ! empty( $this->stripe_publishable_key ) && ! $this->stripe_card_blocked, 'container_class' => 'sd-payment-element', 'container_id' => 'sd-payment-element-' . $this->block_id, ], 'offline' => [ 'id' => 'offline', 'label' => __( 'Offline Donation', 'suredonation' ), 'enabled' => Offline_Helper::is_offline_enabled(), 'container_class' => 'sd-offline-instructions', 'content' => Offline_Helper::get_offline_instructions( $this->get_campaign_name() ), ], ]; $methods = []; foreach ( $this->payment_methods as $method_id ) { if ( isset( $available_methods[ $method_id ] ) && $available_methods[ $method_id ]['enabled'] ) { $methods[ $method_id ] = $available_methods[ $method_id ]; } } /** * Filter the registered payment methods and their display config. * * Allows extensions to add payment methods (e.g., PayPal) with their * label, container class, and enabled state for frontend rendering. * * @param array> $methods Registered methods keyed by ID. * @param array $payment_methods Methods selected in this block. * @param string $block_id The block ID. * @since 1.0.0 */ return apply_filters( 'suredonation_registered_payment_methods', $methods, $this->payment_methods, $this->block_id ); } /** * Get the campaign name for this form. * * @return string Campaign name or empty string. * @since 1.0.0 */ private function get_campaign_name() { if ( empty( $this->form_id ) ) { return ''; } $campaign_id = Donation_Form::get_form_campaign_id( absint( $this->form_id ) ); if ( empty( $campaign_id ) ) { return ''; } return get_the_title( $campaign_id ); } /** * Render payment methods as accordion. * Each payment method is an accordion item with header and collapsible content. * * @param array $methods Array of payment methods. * @return string Payment methods accordion markup. * @since 1.0.0 */ private function render_payment_methods_accordion( $methods ) { if ( empty( $methods ) || ! is_array( $methods ) ) { return ''; } $is_single_method = count( $methods ) === 1; $is_first = true; ob_start(); ?>
aria-label="" />
id="" class="" >
payment_type || 'both' === $this->original_payment_type; } /** * Variable-amount field slug of the choice that is *not* active on page load. * * Returns '' outside 'both' mode, or when the inactive choice is not variable. * * @return string * @since 1.5.1 */ private function get_inactive_variable_field() { if ( 'both' !== $this->original_payment_type ) { return ''; } $inactive = 'subscription' === $this->default_payment_choice ? $this->one_time_config : $this->subscription_config; return 'variable' === $inactive['amount_type'] ? Helper::get_string_value( $inactive['variable_field'] ) : ''; } /** * Render the "make it recurring" toggle, shown only in 'both' mode. * * The block always initialises as one-time; ticking this checkbox opts the donor * into the recurring choice (the JS reveals the subscription amount panel and * re-points the block at that config). A single opt-in checkbox is friendlier than * a two-button chooser for a form that is one-time by default. * * @return string Chooser markup. * @since 1.5.1 */ private function render_payment_type_chooser() { $checkbox_id = 'sd-payment-recurring-' . $this->block_id; $one_time_panel = 'sd-payment-amount-' . $this->block_id . '-one-time'; $subscription_panel = 'sd-payment-amount-' . $this->block_id . '-subscription'; /** * Filter the label on the "make it recurring" opt-in checkbox. Defaults to the * admin-configured `recurringToggleLabel` block attribute. * * @since 1.5.1 * @param string $label Checkbox label. * @param string $block_id The payment block id. */ $label = apply_filters( 'suredonation_payment_recurring_toggle_label', $this->recurring_toggle_label, $this->block_id ); // Mirror the checkbox-field markup (sd-checkbox-wrap / sd-checkbox-label / // sd-input-checkbox / sd-checkbox-text) so this opt-in inherits the exact same // styling as every other SureDonation checkbox instead of a bespoke look. ob_start(); ?>
$this->one_time_config, 'subscription' => $this->subscription_config, ]; ob_start(); foreach ( $panels as $type => $config ) { $panel_id = 'sd-payment-amount-' . $this->block_id . '-' . $type; ?>
default_payment_choice ? '' : 'hidden'; ?> > render_amount_display( $type, Helper::get_string_value( $config['amount_type'] ), floatval( Helper::get_string_value( $config['fixed_amount'] ) ), floatval( Helper::get_string_value( $config['minimum_amount'] ) ) ), Helper::get_allowed_form_html() ); ?>
payment_type : $payment_type; $amount_type = null === $amount_type ? $this->amount_type : $amount_type; $fixed_amount = null === $fixed_amount ? $this->fixed_amount : $fixed_amount; $minimum_amount = null === $minimum_amount ? $this->minimum_amount : $minimum_amount; ob_start(); if ( 'fixed' === $amount_type ) { ?>
subscription_plan ) ) { $interval = isset( $this->subscription_plan['interval'] ) ? Helper::get_string_value( $this->subscription_plan['interval'] ) : 'month'; $billing_cycles = isset( $this->subscription_plan['billingCycles'] ) ? Helper::get_string_value( $this->subscription_plan['billingCycles'] ) : '0'; $interval_label = $this->get_interval_label( $interval ); // Build subscription text. if ( 'ongoing' === $billing_cycles ) { echo esc_html( sprintf( /* translators: 1: Amount with currency, 2: Interval (day/week/month/quarter/year) */ __( '%1$s per %2$s (until cancelled)', 'suredonation' ), $this->format_currency( $fixed_amount, $this->currency ), $interval_label ) ); } elseif ( (int) $billing_cycles > 0 ) { echo esc_html( sprintf( /* translators: 1: Amount with currency, 2: Interval (day/week/month/quarter/year), 3: Number of billing cycles */ __( '%1$s per %2$s (%3$s payments)', 'suredonation' ), $this->format_currency( $fixed_amount, $this->currency ), $interval_label, $billing_cycles ) ); } else { echo esc_html( sprintf( /* translators: 1: Amount with currency, 2: Interval (day/week/month/quarter/year) */ __( '%1$s per %2$s', 'suredonation' ), $this->format_currency( $fixed_amount, $this->currency ), $interval_label ) ); } } else { echo esc_html( $this->format_currency( $fixed_amount, $this->currency ) ); } ?>
subscription_plan ) ) { $interval = isset( $this->subscription_plan['interval'] ) ? Helper::get_string_value( $this->subscription_plan['interval'] ) : 'month'; $billing_cycles = isset( $this->subscription_plan['billingCycles'] ) ? Helper::get_string_value( $this->subscription_plan['billingCycles'] ) : '0'; $interval_label = $this->get_interval_label( $interval ); // Build message format based on billing cycles. if ( 'ongoing' === $billing_cycles ) { /* translators: 1: Amount with currency placeholder, 2: Interval (day/week/month/quarter/year) */ $message_format = sprintf( __( '{amount} per %s (until cancelled)', 'suredonation' ), $interval_label ); } elseif ( (int) $billing_cycles > 0 ) { /* translators: 1: Amount with currency placeholder, 2: Interval (day/week/month/quarter/year), 3: Number of billing cycles */ $message_format = sprintf( __( '{amount} per %1$s (%2$s payments)', 'suredonation' ), $interval_label, $billing_cycles ); } else { /* translators: 1: Amount with currency placeholder, 2: Interval (day/week/month/quarter/year) */ $message_format = sprintf( __( '{amount} per %s', 'suredonation' ), $interval_label ); } } ?>
0 ) { ?> format_currency( $minimum_amount, $this->currency ) ) ); ?>
customer_email_field ) ) { return false; } // Check subscription-specific requirements. Keyed on whether a subscription // is reachable at all, not on the active type: in 'both' mode the donor can // switch to recurring even when one-time is the default, so the name field // and plan must be configured either way. if ( $this->has_subscription_path() ) { if ( empty( $this->customer_name_field ) ) { return false; } if ( empty( $this->subscription_plan ) || empty( $this->subscription_plan['name'] ) ) { return false; } } return true; } /** * Format currency for display. * * @param float $amount Amount to format. * @param string $currency Currency code. * @return string * @since 0.0.1 */ private function format_currency( $amount, $currency ) { // Delegate to the single source of truth so decimal handling and the // currency sign position stay consistent with every other surface. return Payment_Helper::format_amount( $amount, $currency ); } /** * Get the human-readable label for a payment interval slug. * * @param string $interval_slug The slug (e.g., 'day', 'week', 'month', 'quarter', 'yearly'). * @return string The translated interval label, or the slug itself if not found. * @since 0.0.1 */ private function get_interval_label( $interval_slug ) { $interval_labels = [ 'day' => __( 'day', 'suredonation' ), 'week' => __( 'week', 'suredonation' ), 'month' => __( 'month', 'suredonation' ), 'quarter' => __( 'quarter', 'suredonation' ), 'yearly' => __( 'year', 'suredonation' ), ]; return $interval_labels[ $interval_slug ] ?? $interval_slug; } /** * Render test mode notice for admin users. * Only shows if user has manage_options capability. * * @return string Test mode notice markup or empty string. * @since 0.0.1 */ private function get_test_mode_notice() { // Only show to users with manage_options capability. if ( ! current_user_can( 'manage_options' ) ) { return ''; } $this->prevent_page_cache(); // Build dynamic link to payment settings (shared, hash-routed URL). // Tagged so the settings screen can record that this notice is what brought // the admin over. No subpage: the payments screen falls back to the Stripe // panel, which is where the arrival is read. $settings_url = Payment_Helper::get_settings_url( '', [ 'sd_notice' => 'frontend_test_mode' ] ); ob_start(); ?> get_field_classes( [ 'sd-payment-unavailable' ] ); // Both audiences are told the gateways are not configured, but only donors // are told to contact the administrator — an admin *is* that person and // gets the actionable button below, so the sentence would send them in a // circle. Mirrors the capability + settings-URL pattern used by // get_test_mode_notice(). $is_admin = current_user_can( 'manage_options' ); $configure_url = ''; $configure_text = ''; // The two audiences get different copy here, so either variant being // cached and replayed to the other is wrong. if ( $is_admin ) { $this->prevent_page_cache(); } // "Not configured" is wrong when Stripe is connected and simply cannot // charge — the admin would go looking for a setup step they already // completed, and reconnecting does not fix an account-side restriction. // Say what is actually true instead. Donors are never told which // gateway or why: it is the site's problem, not theirs. $blocked_only = $this->stripe_card_blocked; if ( $blocked_only ) { $notice_text = $is_admin ? __( 'Stripe is connected but cannot accept card payments for this account.', 'suredonation' ) : __( 'Card payments are currently unavailable. Please contact the site administrator.', 'suredonation' ); } else { $notice_text = $is_admin ? __( 'Payment gateways are not configured.', 'suredonation' ) : __( 'Payment gateways are not configured. Please contact the site administrator.', 'suredonation' ); } if ( $is_admin && $blocked_only ) { // The fix is in the gateway's own settings, where the capability // warning and the Stripe dashboard link live. $configure_url = Payment_Helper::get_settings_url( 'stripe', [ 'sd_notice' => 'stripe_capability' ] ); $configure_text = __( 'Review Stripe account status', 'suredonation' ); } elseif ( $is_admin ) { // Route the admin to the right place. If a gateway is already usable // on the site, the block simply hasn't selected it — send them to // this form's editor to fix the payment block. Otherwise no gateway // is set up at all — send them to global payment settings. $edit_link = $this->form_id > 0 ? get_edit_post_link( (int) $this->form_id ) : ''; if ( Payment_Helper::has_usable_gateway() && $edit_link ) { // Deliberately untagged: this branch goes to the block editor, not // the settings screen, so there is no arrival for it to record. // Tracking it would need a hook in the editor, which is not worth // a tracker on a public page — the reason this whole mechanism // records arrivals rather than clicks. $configure_url = $edit_link; $configure_text = __( 'Edit this form’s payment settings', 'suredonation' ); } else { // No gateway connected — send the admin straight to the gateway // connect screen (Stripe) rather than the currency/mode page. $configure_url = Payment_Helper::get_settings_url( 'stripe', [ 'sd_notice' => 'frontend_gateway_unavailable' ] ); $configure_text = __( 'Configure payment gateway', 'suredonation' ); } } ob_start(); ?>
label_markup ); ?>

render_configure_link( $configure_url, $configure_text ) : '' ); ?>
prevent_page_cache(); ob_start(); ?>

render_configure_link( Payment_Helper::get_settings_url( 'stripe', [ 'sd_notice' => 'frontend_gateway_setup' ] ), __( 'Configure payment gateway', 'suredonation' ) ) ); ?>
stripe_card_blocked || ! current_user_can( 'manage_options' ) ) { return ''; } $this->prevent_page_cache(); ob_start(); ?>

render_configure_link( // The marker rides the query string so the settings screen // can record that this notice is what brought the admin // there. Tracking the arrival rather than the click keeps // every byte of analytics off the public page the notice // renders on. Payment_Helper::get_settings_url( 'stripe', [ 'sd_notice' => 'stripe_capability' ] ), __( 'Review Stripe account status', 'suredonation' ) ) ); ?>