group_settings_fields( $fields ); // Sort fields by priority (groups & fields). $fields = $this->sort_settings_fields( $fields ); return $fields; } /** * Group settings fields. * * @param array $fields Settings fields. * @return array Processed settings fields. */ public function group_settings_fields( $fields ) { $tmp_fields = []; foreach ( $fields as $field ) { $field_group = $field['group'] ?? 'main'; if ( $field['type'] === 'heading' ) { $group_priority = $field['priority'] ?? 0; $tmp_fields[ $field_group ]['priority'] = $group_priority; $field['priority'] = -1; } $tmp_fields[ $field_group ]['fields'][] = $field; } return $tmp_fields; } /** * Sort settings fields. * * @param array $fields Settings fields. * @return array Processed settings fields. */ public function sort_settings_fields( $fields ) { // Sort groups by priority. uasort( $fields, function ( $a, $b ) { $priority_a = $a['priority'] ?? 0; $priority_b = $b['priority'] ?? 0; return $priority_a <=> $priority_b; } ); // Sort fields within each group by priority. foreach ( $fields as $group_key => $group_data ) { uasort( $group_data['fields'], function ( $a, $b ) { $priority_a = $a['priority'] ?? 0; $priority_b = $b['priority'] ?? 0; return $priority_a <=> $priority_b; } ); $fields[ $group_key ]['fields'] = $group_data['fields']; } return $fields; } /** * Label a settings group for its tab. * * The label is the group's `heading` field, which is also what the panel * shows, so a tab and its panel can never disagree. An add-on that adds a * group without a heading still gets a usable tab rather than a blank one: * `live_qr_settings` reads as "Live Qr Settings", which is wrong-ish but * findable, and the fix is for that add-on to add a heading. * * `main` is the exception. It is what a field with no `group` falls back to, * so it holds whatever nobody placed rather than anything named "Main". No * field ships in it — this plugin has no settings that are merely general — * and it only becomes a tab when something lands there uninvited. * * @param string $group_id Group key. * @param array $group Group data: `fields`, `priority`. * @return string Unescaped label. */ public static function group_label( $group_id, array $group ) { foreach ( $group['fields'] ?? array() as $field ) { if ( 'heading' === ( $field['type'] ?? '' ) && ! empty( $field['field_data']['title'] ) ) { return $field['field_data']['title']; } } if ( 'main' === $group_id ) { return __( 'General', 'subscription' ); } return ucwords( str_replace( array( '_', '-' ), ' ', (string) $group_id ) ); } /** * Whether every field in a group is locked behind Pro. * * Drives the "Pro" marker on the tab, so the whole panel does not have to be * opened to find out that none of it can be changed yet. * * @param array $group Group data. * @return bool */ public static function group_is_pro_locked( array $group ) { $has_field = false; foreach ( $group['fields'] ?? array() as $field ) { if ( 'heading' === ( $field['type'] ?? '' ) ) { continue; } $has_field = true; if ( empty( $field['field_data']['pro_locked'] ) ) { return false; } } return $has_field; } /** * The settings sections, in display order. * * One level, named for the job a merchant came to do rather than for the * plugin's internals. Each section is one rail item and one panel; the * groups inside it stack, so nothing is ever two clicks deep. * * There is deliberately no "All settings" entry. It duplicated every panel * on one page, which made the rail beside it look like decoration and gave * every setting two addresses. * * @return array Section key => label. */ public static function categories() { return array( 'renewals' => __( 'Renewals', 'subscription' ), 'payments' => __( 'Payments', 'subscription' ), 'switching' => __( 'Switching & Upgrades', 'subscription' ), 'customers' => __( 'Customers', 'subscription' ), 'advanced' => __( 'Advanced', 'subscription' ), ); } /** * Which section a settings group belongs to. * * Groups are merged rather than mapped one-to-one: a section holding a * single option is a wasted click, so `health_queue` sits with the other * plumbing in Advanced, and everything the customer meets — the role they * are given, checking out as a guest, what their subscription's quick view * shows — is in Customers. * * Unmapped groups, including any an add-on registers without knowing * sections exist, fall into `advanced`, so a new group is always reachable. * * @param string $group_id Group key. * @return string Section key. */ public static function group_category( $group_id ) { $map = array( 'renewals' => 'renewals', 'payment_gateways' => 'payments', 'payment_failure' => 'payments', 'grace_period' => 'payments', 'switching' => 'switching', 'role_based_settings' => 'customers', 'guest_checkout' => 'customers', 'live_qr_settings' => 'customers', 'api_settings' => 'advanced', 'health_queue' => 'advanced', ); return $map[ $group_id ] ?? 'advanced'; } /** * Group keys bucketed by section, each list in the order the groups already * sort in. * * Empty sections are dropped. Most of them are filled by Pro, and free * alone would otherwise show rail items that open onto nothing. * * @param array $settings_fields Grouped, sorted settings fields. * @return array Section key => ordered group keys. */ public static function category_groups( array $settings_fields ) { $out = array(); foreach ( array_keys( self::categories() ) as $cat ) { $out[ $cat ] = array(); } foreach ( array_keys( $settings_fields ) as $group_id ) { $cat = self::group_category( $group_id ); $out[ $cat ][] = $group_id; } return array_filter( $out, function ( $group_ids ) { return ! empty( $group_ids ); } ); } /** * Whether every group in a section is locked behind Pro. * * Drives the "Pro" marker on the rail item, so a section none of which can * be changed yet says so before it is opened. * * @param string[] $group_ids Group keys in the section. * @param array $settings_fields Grouped settings fields. * @return bool */ public static function category_is_pro_locked( array $group_ids, array $settings_fields ) { if ( empty( $group_ids ) ) { return false; } foreach ( $group_ids as $group_id ) { if ( ! self::group_is_pro_locked( $settings_fields[ $group_id ] ?? array() ) ) { return false; } } return true; } /** * Render specified settings field. * * @param string $field Field type. * @param array $args Field arguments. * @param bool $should_print Whether to print the field or return as HTML string. */ public static function render_settings_field( $field, $args, $should_print = true ) { if ( empty( $field ) ) { $field = 'input'; // Default field type. subscrpt_write_debug_log( "[SettingsHelper] Field type not specified. Defaulting to 'input'." ); } switch ( $field ) { case 'heading': return self::render_heading( $args, $should_print ); case 'switch': case 'toggle': return self::render_switch_field( $args, $should_print ); case 'select': return self::render_select_field( $args, $should_print ); case 'multi_select': return self::render_multiselect_field( $args, $should_print ); case 'join': return self::render_joined_field( $args, $should_print ); case 'editlist': return self::render_editlist_field( $args, $should_print ); case 'input': default: return self::render_input_field( $args, $should_print ); } } /** * Pro badge markup, shown beside settings that require WPSubscription Pro. * * @return string Pre-escaped badge HTML. */ public static function pro_badge_html() { return '' . esc_html__( 'Pro', 'subscription' ) . ''; } /** * Text Element HTML. * * @param array $args Same as 'render_text_field'. * @param bool $join_item Whether to return element for 'join' container or not. */ public static function inp_element( $args = [], $join_item = false ) { $id = $args['id']; $value = $args['value'] ?? ''; $placeholder = $args['placeholder'] ?? ''; $type = $args['type'] ?? 'text'; $disabled_attr = isset( $args['disabled'] ) && $args['disabled'] ? 'disabled' : ''; $style_attr = ''; if ( isset( $args['style'] ) ) { $style_attr = $args['style']; } $other_attrs_html = ''; foreach ( ( $args['attributes'] ?? [] ) as $attr_key => $attr_value ) { $other_attrs_html .= sprintf( ' %s="%s" ', esc_attr( $attr_key ), esc_attr( $attr_value ) ); } ob_start(); ?> style="" type="" placeholder="" value="" /> $opt_label ) { $adv_options[] = array( 'value' => (string) $opt_value, 'label' => $opt_label, ); } ob_start(); wpsubs_render_tag_select( array( 'name' => $id, 'value' => $args['selected'] ?? ( $multiple ? array() : '' ), 'options' => $adv_options, 'multiple' => $multiple, ) ); return ob_get_clean(); } // Regular select → wpsubs-adv-select (button-based custom dropdown). $selected = (string) ( $args['selected'] ?? '' ); $adv_options = array(); foreach ( ( $args['options'] ?? [] ) as $opt_value => $opt_label ) { $adv_options[] = array( 'value' => (string) $opt_value, 'label' => $opt_label, 'disabled' => isset( $args['disabled'] ) && ( is_array( $args['disabled'] ) ? in_array( $opt_value, $args['disabled'], true ) : $args['disabled'] === $opt_value ), ); } ob_start(); wpsubs_render_adv_select( array( 'name' => $id, 'value' => $selected, 'options' => $adv_options, 'align' => 'left', 'class' => $args['class'] ?? '', ) ); return ob_get_clean(); } /** * Render Field Heading. * * - Args: * - title (string) - Field title. * - description (string) - Field description (optional). * * @param array $args Field arguments. * @param bool $should_print Whether to print the field or return as HTML string. */ public static function render_heading( $args = [], $should_print = true ) { $title = $args['title'] ?? ''; $description = $args['description'] ?? ''; ob_start(); ?>

' . $field_hint . ': ' . __( 'Field ID is required.', 'subscription' ) . '

'; return $should_print ? print wp_kses_post( $no_id_msg ) : $no_id_msg; } // Input HTML. $text_el_html = self::inp_element( $args ); ob_start(); ?>

' . $field_hint . ': ' . __( 'Field ID is required.', 'subscription' ) . '

'; return $should_print ? print wp_kses_post( $no_id_msg ) : $no_id_msg; } $description_html = ''; if ( ! empty( $description ) ) { $description_html = sprintf( '

%s

', wp_kses_post( $description ) ); } $style_attr = ''; if ( isset( $args['style'] ) ) { $style_attr .= ' ' . $args['style']; } $other_attrs_html = ''; foreach ( ( $args['attributes'] ?? [] ) as $attr_key => $attr_value ) { $other_attrs_html .= sprintf( ' %s="%s" ', esc_attr( $attr_key ), esc_attr( $attr_value ) ); } $checked_attr = isset( $args['checked'] ) && (bool) $args['checked'] ? 'checked' : ''; $disabled_attr = isset( $args['disabled'] ) && (bool) $args['disabled'] ? 'disabled' : ''; ob_start(); ?>
label]. * - selected (string) - Selected option value. * - disabled (string|array) - Disabled option value(s). * * @param array $args Field arguments. * @param bool $should_print Whether to print the field or return as HTML string. */ public static function render_select_field( $args = [], $should_print = true ) { $title = $args['title'] ?? ''; $description = $args['description'] ?? ''; // Return error if ID is not provided. if ( empty( $args['id'] ?? '' ) ) { $field_hint = empty( $title ) ? 'Error' : $title; $no_id_msg = '

' . $field_hint . ': ' . __( 'Field ID is required.', 'subscription' ) . '

'; return $should_print ? print wp_kses_post( $no_id_msg ) : $no_id_msg; } // Select HTML. $select_el_html = self::select_element( $args ); ob_start(); ?>

[ 'multiple' => 'multiple', ], 'enhanced' => true, ]; $args = wp_parse_args( $args, $default_multiselect_args ); return self::render_select_field( $args, $should_print ); } /** * Render Joined field with multiple elements. * * - Args: * - title (string) - Field title. * - description (string) - Field description (optional). * - vertical (bool) - Whether to show items vertically or not. * - elements ([...string]) - Array of HTML elements to join. * * @param array $args Field arguments. * @param bool $should_print Whether to print the field or return as HTML string. */ public static function render_joined_field( $args = [], $should_print = true ) { $title = $args['title'] ?? ''; $description = $args['description'] ?? ''; $vertical_style = ( $args['vertical'] ?? false ) ? 'flex-direction:column;' : ''; ob_start(); ?>
style="">

' . $field_hint . ': ' . __( 'Field ID is required.', 'subscription' ) . '

'; return $should_print ? print wp_kses_post( $no_id_msg ) : $no_id_msg; } $body = self::editlist_body_html( $items, $add_placeholder, $add_label, $empty_text, $locked ); ob_start(); ?>
$id . '_modal', 'title' => $modal_title, 'body' => $body, 'class' => 'wpsubs-modal--editlist', 'footer' => '', ] ); ?>

>

/>