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' ) . '