From 4fc448521dc03a75610b7ccb2960f67281de2468 Mon Sep 17 00:00:00 2001
From: David Parker <dlparker1005@gmail.com>
Date: Tue, 29 Oct 2024 15:45:33 -0400
Subject: [PATCH 1/6] Adding class PMPro_Field_Group to abstract global user
 field vars

---
 classes/class-pmpro-field-group.php | 187 ++++++++
 includes/fields.php                 | 648 ++++++++++++----------------
 paid-memberships-pro.php            |   1 +
 3 files changed, 469 insertions(+), 367 deletions(-)
 create mode 100644 classes/class-pmpro-field-group.php

diff --git a/classes/class-pmpro-field-group.php b/classes/class-pmpro-field-group.php
new file mode 100644
index 000000000..338a24b22
--- /dev/null
+++ b/classes/class-pmpro-field-group.php
@@ -0,0 +1,187 @@
+<?php
+class PMPro_Field_Group {
+	/**
+	 * The name of the field group.
+	 *
+	 * We want this to be read only via a magic getter.
+	 *
+	 * @var string
+	 */
+	protected $name;
+
+	/**
+	 * The label for the field group.
+	 *
+	 * @var string
+	 */
+	public $label;
+
+	/**
+	 * The description for the field group.
+	 *
+	 * @var string
+	 */
+	public $description;
+
+	/**
+	 * Constructor.
+	 */
+	public function __construct( $name, $label, $description = '' ) {
+		$this->name        = $name;
+		$this->label       = $label;
+		$this->description = $description;
+	}
+
+	/**
+	 * Magic getter for read-only properties.
+	 *
+	 * @param string $name The property name.
+	 * @return mixed The property value.
+	 */
+	public function __get( $name ) {
+		if ( isset( $this->$name ) ) {
+			return $this->$name;
+		}
+
+		return null;
+	}
+
+	/**
+	 * Add a field group.
+	 *
+	 * @since TBD
+	 *
+	 * @param string $name        The name of the field group.
+	 * @param string|null $label  The label for the field group. If NULL, a cleaned version of the name will be used.
+	 * @param string $description The description for the field group.
+	 *
+	 * @return PMPro_Field_Group The field group object.
+	 */
+	public static function add( $name, $label = NULL, $description = '' ) {
+		global $pmpro_field_groups;
+
+		// If the field group already exists, update the label and description.
+		if ( ! empty( $pmpro_field_groups[ $name ] ) ) { // Looking at global to avoid infinite loop when a group doesn't exist.
+			$existing_field_group = self::get( $name );
+			$existing_field_group->label       = $label;
+			$existing_field_group->description = $description;
+
+			return $existing_field_group;
+		}
+
+		// If no label is provided, use the name.
+		if ( empty( $label ) ) {
+			if ( $name === 'checkout_boxes' ) {
+				apply_filters( 'pmpro_default_field_group_label', __( 'More Information','paid-memberships-pro' ) );
+			} else {
+				$label = ucwords( str_replace( '_', ' ', $name ) );
+			}
+		}
+
+		// Create a new field group object.
+		$field_group = new PMPro_Field_Group( $name, $label, $description );
+
+		// Add the field group to the global array.
+		$pmpro_field_groups[ $name ] = $field_group;
+
+		return $field_group;
+	}
+
+	/**
+	 * Get all added field groups.
+	 *
+	 * @since TBD
+	 *
+	 * @return array An array of PMPro_Field_Group objects.
+	 */
+	public static function get_all() {
+		global $pmpro_field_groups;
+
+		if ( empty( $pmpro_field_groups ) ) {
+			$pmpro_field_groups = array();
+		}
+
+		return $pmpro_field_groups;
+	}
+
+	/**
+	 * Get an added field group by name.
+	 *
+	 * @since TBD
+	 *
+	 * @param string $name The name of the field group.
+	 * @return PMPro_Field_Group The field group object.
+	 */
+	public static function get( $name ) {
+		// Get all field groups.
+		$field_groups = self::get_all();
+
+		// If we don't yet have the field group, create it.
+		if ( empty( $field_groups[ $name ] ) ) {
+			return self::add( $name );
+		}
+
+		// Return the field group.
+		return $field_groups[ $name ];
+	}
+
+	/**
+	 * Add a field to this field group.
+	 *
+	 * @since TBD
+	 *
+	 * @param PMPro_Field $field The field object to add.
+	 * @return bool True if the field was added, otherwise false.
+	 */
+	public function add_field( $field ) {
+		global $pmpro_user_fields;
+		if ( empty( $pmpro_user_fields ) ) {
+			$pmpro_user_fields = array();
+		}
+
+		/**
+		 * Filter the field to add.
+		 * 
+		 * @since 2.9.3
+		 *             
+		 * @param PMProField $field The field being added.
+		 * @param string $group_name The name of the group to add the field to.
+		 */
+		$field = apply_filters( 'pmpro_add_user_field', $field, $this->name );
+
+		// Make sure that we have a valid field.
+		if ( empty( $field ) || ! pmpro_is_field( $field ) ) {
+			return false;
+		}
+
+		// Make sure the group is in the global array of fields.
+		if ( empty( $pmpro_user_fields[ $this->name ] ) ) {
+			$pmpro_user_fields[ $this->name ] = array();
+		}
+
+		// Add the field to the group.
+		$pmpro_user_fields[ $this->name ][] = $field;
+
+		return true;
+	}
+
+	/**
+	 * Get all fields in this field group.
+	 *
+	 * @since TBD
+	 *
+	 * @return array An array of PMPro_Field objects.
+	 */
+	public function get_fields() {
+		global $pmpro_user_fields;
+		if ( empty( $pmpro_user_fields ) ) {
+			$pmpro_user_fields = array();
+		}
+
+		if ( empty( $pmpro_user_fields[ $this->name ] ) ) {
+			$pmpro_user_fields[ $this->name ] = array();
+		}
+
+		return $pmpro_user_fields[ $this->name ];
+	}
+}
\ No newline at end of file
diff --git a/includes/fields.php b/includes/fields.php
index ad947ff28..312e8324c 100644
--- a/includes/fields.php
+++ b/includes/fields.php
@@ -1,15 +1,4 @@
 <?php 
-// Global to store field groups and user fields.
-global $pmpro_field_groups, $pmpro_user_fields;
-$pmpro_user_fields = array();
-
-// Add default group.
-$cb = new stdClass();
-$cb->name = 'checkout_boxes';
-$cb->label = apply_filters( 'pmpro_default_field_group_label', __( 'More Information','paid-memberships-pro' ) );
-$cb->order = 0;
-$pmpro_field_groups = array( 'checkout_boxes' => $cb );
-
 /**
  * Check if a variable is a PMPro_Field.
  * Also checks for PMProRH_Field.
@@ -35,38 +24,23 @@ function pmpro_is_field( $var ) {
  *	- before_submit_button
  *	- just_profile (make sure you set the profile attr of the field to true or admins)
  */
-function pmpro_add_user_field( $where, $field ) {
-	global $pmpro_user_fields;
-	
+function pmpro_add_user_field( $where, $field ) {	
     /**
      * Filter the group to add the field to.
      * 
      * @since 2.9.3
+	 * @deprecated TBD
      * 
      * @param string $where The name of the group to add the field to.
      * @param PMProField $field The field being added.
      */
-    $where = apply_filters( 'pmpro_add_user_field_where', $where, $field );
-    
-    /**
-     * Filter the field to add.
-     * 
-     * @since 2.9.3
-     *             
-     * @param PMProField $field The field being added.
-     * @param string $where The name of the group to add the field to.
-     */
-    $field = apply_filters( 'pmpro_add_user_field', $field, $where );
-    
-    if(empty($pmpro_user_fields[$where])) {
-		$pmpro_user_fields[$where] = array();
-	}
-	if ( ! empty( $field ) && pmpro_is_field( $field ) ) {
-		$pmpro_user_fields[$where][] = $field;
-		return true;
-	}
+    $where = apply_filters_deprecated( 'pmpro_add_user_field_where', array( $where, $field ), 'TBD', 'pmpro_add_user_field' );
 
-	return false;
+	// Get the field group.
+	$field_group = PMPro_Field_Group::get( $where );
+
+	// Add the field to the group.
+	$field_group->add_field( $field );
 }
 
 /**
@@ -77,34 +51,7 @@ function pmpro_add_user_field( $where, $field ) {
  * Name must contain no spaces or special characters.
  */
 function pmpro_add_field_group( $name, $label = NULL, $description = '', $order = NULL ) {
-	global $pmpro_field_groups;
-	// Bail if the group already exists.
-	foreach ( $pmpro_field_groups as $group ) {
-		if ( $group->name === $name ) {
-			// Group already exists.
-			return false;
-		}
-	}
-
-	$temp = new stdClass();
-	$temp->name = $name;
-	$temp->label = $label;
-	$temp->description = $description;
-	$temp->order = $order;
-
-	//defaults
-	if( empty( $temp->label ) ) {
-        $temp->label = ucwords($temp->name);
-    }
-	if( ! isset( $order ) ) {
-		$lastbox = pmpro_array_end( $pmpro_field_groups );
-		$temp->order = $lastbox->order + 1;
-	}
-
-	$pmpro_field_groups[$name] = $temp;
-	usort( $pmpro_field_groups, 'pmpro_sort_by_order' );
-
-	return true;
+	return PMPro_Field_Group::add( $name, $label, $description );
 }
 
 /**
@@ -210,15 +157,7 @@ function pmpro_add_user_taxonomy( $name, $name_plural ) {
  * Get a field group by name.
  */
 function pmpro_get_field_group_by_name( $name ) {
-	global $pmpro_field_groups;
-	if( ! empty( $pmpro_field_groups ) ) {
-		foreach( $pmpro_field_groups as $group ) {
-			if( $group->name == $name ) {
-                return $group;
-            }
-		}
-	}
-	return false;
+	return PMPro_Field_Group::get( $name );
 }
 
 /**
@@ -258,23 +197,22 @@ function pmpro_check_field_for_level( $field, $scope = 'default', $args = NULL )
  * Find fields in a group and display them at checkout.
  */
 function pmpro_display_fields_in_group( $group, $scope = 'checkout' ) {
-    global $pmpro_user_fields;
-
-	if( ! empty( $pmpro_user_fields[$group] ) ) {
-		foreach( $pmpro_user_fields[$group] as $field ) {
-			if ( ! pmpro_is_field( $field ) ) {
-                continue;
-            }
-            
-            if ( ! pmpro_check_field_for_level( $field ) ) {
-                continue;
-            }
-            
-            if ( $scope == 'checkout' ) {
-                if( ! isset( $field->profile ) || $field->profile !== 'only' && $field->profile !== 'only_admin' ) {
-    				$field->displayAtCheckout();
-    			}
-            }
+    // Get the field group.
+	$field_group = pmpro_get_field_group_by_name( $group );
+	$fields = $field_group->get_fields();
+	foreach( $fields as $field ) {
+		if ( ! pmpro_is_field( $field ) ) {
+			continue;
+		}
+		
+		if ( ! pmpro_check_field_for_level( $field ) ) {
+			continue;
+		}
+		
+		if ( $scope == 'checkout' ) {
+			if( ! isset( $field->profile ) || $field->profile !== 'only' && $field->profile !== 'only_admin' ) {
+				$field->displayAtCheckout();
+			}
 		}
 	}
 }
@@ -308,33 +246,42 @@ function pmpro_checkout_after_captcha_fields() {
 
 //checkout boxes
 function pmpro_checkout_boxes_fields() {
-	global $pmpro_user_fields, $pmpro_field_groups;
+	// Get all field groups.
+	$field_groups = PMPro_Field_Group::get_all();
+
+	// Cycle through the field groups.
+	foreach( $field_groups as $field_group_name => $field_group ) {
+		// If this is not a checkout box, skip it.
+		if ( in_array( $field_group_name, array( 'after_username', 'after_password', 'after_email', 'after_captcha', 'after_pricing_fields', 'after_billing_fields', 'before_submit_button', 'after_tos_fields' ) ) ) {
+			continue;
+		}
+
+		// Get all the fields for this group.
+		$fields = $field_group->get_fields();
 
-	foreach($pmpro_field_groups as $cb)
-	{
 		//how many fields to show at checkout?
 		$n = 0;
-		if(!empty($pmpro_user_fields[$cb->name]))
-			foreach($pmpro_user_fields[$cb->name] as $field)
+		if(!empty( $fields ))
+			foreach( $fields as $field)
 				if(pmpro_is_field($field) && pmpro_check_field_for_level($field) && (!isset($field->profile) || (isset($field->profile) && $field->profile !== "only" && $field->profile !== "only_admin")))		$n++;
 
 		if($n > 0) {
 			?>
-			<fieldset id="pmpro_form_fieldset-<?php echo esc_attr( sanitize_title( $cb->name ) ); ?>" class="<?php echo esc_attr( pmpro_get_element_class( 'pmpro_form_fieldset', 'pmpro_form_fieldset-' . sanitize_title( $cb->name ) ) ); ?>">
+			<fieldset id="pmpro_form_fieldset-<?php echo esc_attr( sanitize_title( $field_group_name ) ); ?>" class="<?php echo esc_attr( pmpro_get_element_class( 'pmpro_form_fieldset', 'pmpro_form_fieldset-' . sanitize_title( $field_group_name ) ) ); ?>">
 				<div class="<?php echo esc_attr( pmpro_get_element_class( 'pmpro_card' ) ); ?>">
 					<div class="<?php echo esc_attr( pmpro_get_element_class( 'pmpro_card_content' ) ); ?>">
 						<?php if ( ! empty( $cb->label ) ) { ?>
 							<legend class="<?php echo esc_attr( pmpro_get_element_class( 'pmpro_form_legend' ) ); ?>">
-								<h2 class="<?php echo esc_attr( pmpro_get_element_class( 'pmpro_form_heading pmpro_font-large' ) ); ?>"><?php echo wp_kses_post( $cb->label ); ?></h2>
+								<h2 class="<?php echo esc_attr( pmpro_get_element_class( 'pmpro_form_heading pmpro_font-large' ) ); ?>"><?php echo wp_kses_post( $field_group->label ); ?></h2>
 							</legend>
 						<?php } ?>
 						<div class="<?php echo esc_attr( pmpro_get_element_class( 'pmpro_form_fields' ) ); ?>">
 							<?php if ( ! empty( $cb->description ) ) { ?>
-								<div class="<?php echo esc_attr( pmpro_get_element_class( 'pmpro_form_fields-description' ) ); ?>"><?php echo wp_kses_post( $cb->description ); ?></div>
+								<div class="<?php echo esc_attr( pmpro_get_element_class( 'pmpro_form_fields-description' ) ); ?>"><?php echo wp_kses_post( $field_group->description ); ?></div>
 							<?php } ?>
 
 							<?php
-								foreach($pmpro_user_fields[$cb->name] as $field) {
+								foreach($fields  as $field) {
 									if( pmpro_is_field($field) && pmpro_check_field_for_level($field) && (!isset($field->profile) || (isset($field->profile) && $field->profile !== "only" && $field->profile !== "only_admin"))) {
 										$field->displayAtCheckout();
 									}
@@ -378,89 +325,83 @@ function pmpro_checkout_after_tos_fields() {
  * Update the fields at checkout.
  */
 function pmpro_after_checkout_save_fields( $user_id, $order ) {
-	global $pmpro_user_fields;
-
-	//any fields?
-	if(!empty($pmpro_user_fields))
-	{
-		//cycle through groups
-		foreach($pmpro_user_fields as $where => $fields)
-		{
-			//cycle through fields
-			foreach($fields as $field)
-			{
-                if( ! pmpro_is_field( $field ) ) {
-                    continue;
-                }
-                
-                if ( ! pmpro_check_field_for_level( $field, "profile", $user_id ) ) {
-                    continue;
-                }
+	// Loop through all the field groups.
+	$field_groups = PMPro_Field_Group::get_all();
+	foreach($field_groups as $group_name => $group) {
+		// Loop through all the fields in the group.
+		$fields = $group->get_fields();
+		foreach($fields as $field) {
+			if( ! pmpro_is_field( $field ) ) {
+				continue;
+			}
+			
+			if ( ! pmpro_check_field_for_level( $field, "profile", $user_id ) ) {
+				continue;
+			}
 
-				if(!empty($field->profile) && ($field->profile === "only" || $field->profile === "only_admin")) {
-                    continue;	//wasn't shown at checkout
-                }
+			if(!empty($field->profile) && ($field->profile === "only" || $field->profile === "only_admin")) {
+				continue;	//wasn't shown at checkout
+			}
 
-				//assume no value
-				$value = NULL;
+			//assume no value
+			$value = NULL;
 
-				// Where are we getting the value from? We sanitize $value right after this.
-				// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
-				if(isset($_REQUEST[$field->name]))
-				{
-					//request
-					$value = $_REQUEST[$field->name];
-				}
-				elseif(isset($_REQUEST[$field->name . '_checkbox']) && $field->type == 'checkbox')
-				{
-					//unchecked checkbox
-					$value = 0;
-				}
-				elseif(!empty($_POST[$field->name . "_checkbox"]) && in_array( $field->type, array( 'checkbox', 'checkbox_grouped', 'select2' ) ) )	//handle unchecked checkboxes
-				{
-					//unchecked checkbox
-					$value = array();
-				}
-				elseif(isset($_SESSION[$field->name]))
+			// Where are we getting the value from? We sanitize $value right after this.
+			// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+			if(isset($_REQUEST[$field->name]))
+			{
+				//request
+				$value = $_REQUEST[$field->name];
+			}
+			elseif(isset($_REQUEST[$field->name . '_checkbox']) && $field->type == 'checkbox')
+			{
+				//unchecked checkbox
+				$value = 0;
+			}
+			elseif(!empty($_POST[$field->name . "_checkbox"]) && in_array( $field->type, array( 'checkbox', 'checkbox_grouped', 'select2' ) ) )	//handle unchecked checkboxes
+			{
+				//unchecked checkbox
+				$value = array();
+			}
+			elseif(isset($_SESSION[$field->name]))
+			{
+				//file or value?
+				if(is_array($_SESSION[$field->name]) && isset($_SESSION[$field->name]['name']))
 				{
-					//file or value?
-					if(is_array($_SESSION[$field->name]) && isset($_SESSION[$field->name]['name']))
-					{
-						//add to files global
-						$_FILES[$field->name] = $_SESSION[$field->name];
-
-						//set value to name
-						$value = $_SESSION[$field->name]['name'];
-					}
-					else
-					{
-						//session
-						$value = $_SESSION[$field->name];
-					}
+					//add to files global
+					$_FILES[$field->name] = $_SESSION[$field->name];
 
-					//unset
-					unset($_SESSION[$field->name]);
+					//set value to name
+					$value = $_SESSION[$field->name]['name'];
 				}
-				elseif(isset($_FILES[$field->name]))
+				else
 				{
-					//file
-					$value = $_FILES[$field->name]['name'];
+					//session
+					$value = $_SESSION[$field->name];
 				}
-				// phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
 
-				//update user meta
-				if(isset($value))
-				{
-					if ( ! empty( $field->sanitize ) ) {
-						$value = pmpro_sanitize( $value, $field );
-                    }
+				//unset
+				unset($_SESSION[$field->name]);
+			}
+			elseif(isset($_FILES[$field->name]))
+			{
+				//file
+				$value = $_FILES[$field->name]['name'];
+			}
+			// phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
 
-					//callback?
-					if(!empty($field->save_function))
-						call_user_func( $field->save_function, $user_id, $field->name, $value, $order );
-					else
-						update_user_meta($user_id, $field->meta_key, $value);
+			//update user meta
+			if(isset($value))
+			{
+				if ( ! empty( $field->sanitize ) ) {
+					$value = pmpro_sanitize( $value, $field );
 				}
+
+				//callback?
+				if(!empty($field->save_function))
+					call_user_func( $field->save_function, $user_id, $field->name, $value, $order );
+				else
+					update_user_meta($user_id, $field->meta_key, $value);
 			}
 		}
 	}
@@ -475,51 +416,44 @@ function pmpro_after_checkout_save_fields( $user_id, $order ) {
  * Require required fields.
  */
 function pmpro_registration_checks_for_user_fields( $okay ) {
-	global $current_user;
-
-	//arrays to store fields that were required and missed
+	// Arrays to store fields that were required and missed.
 	$required = array();
     $required_labels = array();
 
-	//any fields?
-	global $pmpro_user_fields;
-	if(!empty($pmpro_user_fields))
-	{
-		//cycle through groups
-		foreach($pmpro_user_fields as $where => $fields)
-		{
-			//cycle through fields
-			foreach($fields as $field)
-			{
-                //handle arrays
-                $field->name = preg_replace('/\[\]$/', '', $field->name);
-
-				//if the field is not for this level, skip it
-                if( ! pmpro_is_field( $field ) ) {
-                    continue;
-                }
-                
-                if ( ! pmpro_check_field_for_level( $field ) ) {
-                    continue;
-                }
+	// Loop through all the field groups.
+	$field_groups = PMPro_Field_Group::get_all();
+	foreach($field_groups as $group_name => $group) {
+		// Loop through all the fields in the group.
+		$fields = $group->get_fields();
+		foreach($fields as $field) {
+			//handle arrays
+			$field->name = preg_replace('/\[\]$/', '', $field->name);
+
+			//if the field is not for this level, skip it
+			if( ! pmpro_is_field( $field ) ) {
+				continue;
+			}
+			
+			if ( ! pmpro_check_field_for_level( $field ) ) {
+				continue;
+			}
 
-				if(!empty($field->profile) && ($field->profile === "only" || $field->profile === "only_admin")) {
-                    continue;	//wasn't shown at checkout
-                }
+			if(!empty($field->profile) && ($field->profile === "only" || $field->profile === "only_admin")) {
+				continue;	//wasn't shown at checkout
+			}
 
-				// If this is a file upload, check whether the file is allowed.
-				if ( isset( $_FILES[ $field->name ] ) && ! empty( $_FILES[$field->name]['name'] ) ) {
-					$upload_check = pmpro_check_upload( $field->name );
-					if ( is_wp_error( $upload_check ) ) {
-						pmpro_setMessage( $upload_check->get_error_message(), 'pmpro_error' );
-						return false;
-					}
+			// If this is a file upload, check whether the file is allowed.
+			if ( isset( $_FILES[ $field->name ] ) && ! empty( $_FILES[$field->name]['name'] ) ) {
+				$upload_check = pmpro_check_upload( $field->name );
+				if ( is_wp_error( $upload_check ) ) {
+					pmpro_setMessage( $upload_check->get_error_message(), 'pmpro_error' );
+					return false;
 				}
+			}
 
-				if( ! $field->was_filled_if_needed() ) {
-					$required[] = $field->name;
-                    $required_labels[] = $field->label;
-				}
+			if( ! $field->was_filled_if_needed() ) {
+				$required[] = $field->name;
+				$required_labels[] = $field->label;
 			}
 		}
 	}
@@ -557,68 +491,63 @@ function pmpro_registration_checks_for_user_fields( $okay ) {
  * @deprecated 2.12.4 Use pmpro_after_checkout_save_fields instead to save fields immediately or pmpro_save_checkout_data_to_order for delayed checkouts.
  */
 function pmpro_paypalexpress_session_vars_for_user_fields() {
-	global $pmpro_user_fields;
-
 	_deprecated_function( __FUNCTION__, '2.12.4', 'pmpro_after_checkout_save_fields' );
 
-	//save our added fields in session while the user goes off to PayPal
-	if(!empty($pmpro_user_fields))
-	{
-		//cycle through groups
-		foreach($pmpro_user_fields as $where => $fields)
+	// Loop through all the field groups.
+	$field_groups = PMPro_Field_Group::get_all();
+	foreach($field_groups as $group_name => $group) {
+		// Loop through all the fields in the group.
+		$fields = $group->get_fields();
+		foreach($fields as $field)
 		{
-			//cycle through fields
-			foreach($fields as $field)
-			{
-                if( ! pmpro_is_field( $field ) ) {
-                    continue;
-                }
-                
-                if ( ! pmpro_check_field_for_level( $field ) ) {
-                    continue;
-                }
+			if( ! pmpro_is_field( $field ) ) {
+				continue;
+			}
+			
+			if ( ! pmpro_check_field_for_level( $field ) ) {
+				continue;
+			}
 
-                if( isset( $_REQUEST[$field->name] ) ) {
-					$_SESSION[$field->name] = pmpro_sanitize( $_REQUEST[$field->name], $field ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
-				} elseif ( isset( $_FILES[$field->name] ) ) {
-					/*
-						We need to save the file somewhere and save values in $_SESSION
-					*/
-					// Make sure the file is allowed.
-					$upload_check = pmpro_check_upload( $field->name );
-					if ( is_wp_error( $upload_check ) ) {
-						continue;
-					}
+			if( isset( $_REQUEST[$field->name] ) ) {
+				$_SESSION[$field->name] = pmpro_sanitize( $_REQUEST[$field->name], $field ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+			} elseif ( isset( $_FILES[$field->name] ) ) {
+				/*
+					We need to save the file somewhere and save values in $_SESSION
+				*/
+				// Make sure the file is allowed.
+				$upload_check = pmpro_check_upload( $field->name );
+				if ( is_wp_error( $upload_check ) ) {
+					continue;
+				}
 
-					// Get $file and $filetype.
-					$file = array_map( 'sanitize_text_field', $_FILES[ $field->name ] );
-					$filetype = wp_check_filetype_and_ext( $file['tmp_name'], $file['name'] );
+				// Get $file and $filetype.
+				$file = array_map( 'sanitize_text_field', $_FILES[ $field->name ] );
+				$filetype = wp_check_filetype_and_ext( $file['tmp_name'], $file['name'] );
 
-					// Make sure file was uploaded during this page load.
-					if ( ! is_uploaded_file( sanitize_text_field( $file['tmp_name'] ) ) ) {						
-						continue;
-					}
+				// Make sure file was uploaded during this page load.
+				if ( ! is_uploaded_file( sanitize_text_field( $file['tmp_name'] ) ) ) {						
+					continue;
+				}
 
-					//check for a register helper directory in wp-content
-					$upload_dir = wp_upload_dir();
-					$pmprorh_dir = $upload_dir['basedir'] . "/pmpro-register-helper/tmp/";
+				//check for a register helper directory in wp-content
+				$upload_dir = wp_upload_dir();
+				$pmprorh_dir = $upload_dir['basedir'] . "/pmpro-register-helper/tmp/";
 
-					//create the dir and subdir if needed
-					if(!is_dir($pmprorh_dir))
-					{
-						wp_mkdir_p($pmprorh_dir);
-					}
+				//create the dir and subdir if needed
+				if(!is_dir($pmprorh_dir))
+				{
+					wp_mkdir_p($pmprorh_dir);
+				}
 
-					//move file
-					$new_filename = $pmprorh_dir . basename( sanitize_file_name( $file['name'] ) );
-					move_uploaded_file( sanitize_text_field( $$file['tmp_name'] ), $new_filename );
+				//move file
+				$new_filename = $pmprorh_dir . basename( sanitize_file_name( $file['name'] ) );
+				move_uploaded_file( sanitize_text_field( $$file['tmp_name'] ), $new_filename );
 
-					//update location of file
-					$_FILES[$field->name]['tmp_name'] = $new_filename;
+				//update location of file
+				$_FILES[$field->name]['tmp_name'] = $new_filename;
 
-					//save file info in session
-					$_SESSION[$field->name] = array_map( 'sanitize_text_field', $file );
-				}
+				//save file info in session
+				$_SESSION[$field->name] = array_map( 'sanitize_text_field', $file );
 			}
 		}
 	}
@@ -628,8 +557,6 @@ function pmpro_paypalexpress_session_vars_for_user_fields() {
  * Show user fields in profile.
  */
 function pmpro_show_user_fields_in_profile( $user, $withlocations = false ) {
-	global $pmpro_user_fields;
-
 	//which fields are marked for the profile
 	$profile_fields = pmpro_get_user_fields_for_profile($user->ID, $withlocations);
 
@@ -764,25 +691,20 @@ function pmpro_show_user_fields_in_frontend_profile_with_locations( $user ) {
  * when using the Add Member Admin Add On.
  */
 // Add fields to form.
-function pmpro_add_member_admin_fields( $user = null, $user_id = null)
-{
-    global $pmpro_user_fields;
-
-    $addmember_fields = array();
-    if(!empty($pmpro_user_fields))
-    {
-        //cycle through groups
-        foreach($pmpro_user_fields as $where => $fields)
-        {
-            //cycle through fields
-            foreach($fields as $field)
-            {
-	            if(pmpro_is_field($field) && isset($field->addmember) && !empty($field->addmember) && ( in_array( strtolower( $field->addmember ), array( 'true', 'yes' ) ) || true == $field->addmember ) )
-                {
-                        $addmember_fields[] = $field;
-                }
-            }
-        }
+function pmpro_add_member_admin_fields( $user = null, $user_id = null) {
+	$addmember_fields = array();
+	// Loop through all the field groups.
+	$field_groups = PMPro_Field_Group::get_all();
+	foreach($field_groups as $group_name => $group) {
+		// Loop through all the fields in the group.
+		$fields = $group->get_fields();
+		foreach($fields as $field)
+		{
+			if(pmpro_is_field($field) && isset($field->addmember) && !empty($field->addmember) && ( in_array( strtolower( $field->addmember ), array( 'true', 'yes' ) ) || true == $field->addmember ) )
+			{
+				$addmember_fields[] = $field;
+			}
+		}
     }
 
 
@@ -818,7 +740,6 @@ function pmpro_add_member_admin_fields( $user = null, $user_id = null)
  * @return void
  */
 function pmpro_add_member_admin_save_user_fields( $uid = null, $user = null ) {
-	global $pmpro_user_fields;
 	
 	// Use the ID from the $user object if passed in.
 	if ( ! empty( $user ) && is_object( $user ) ) {
@@ -838,21 +759,19 @@ function pmpro_add_member_admin_save_user_fields( $uid = null, $user = null ) {
 		return false;
 	}
 
-    $addmember_fields = array();
-    if(!empty($pmpro_user_fields))
-    {
-        //cycle through groups
-        foreach($pmpro_user_fields as $where => $fields)
-        {
-            //cycle through fields
-            foreach($fields as $field)
-            {
-	            if(pmpro_is_field($field) && isset($field->addmember) && !empty($field->addmember) && ( in_array( strtolower( $field->addmember ), array( 'true', 'yes' ) ) || true == $field->addmember ) )
-                {
-                        $addmember_fields[] = $field;
-                }
-            }
-        }
+	$addmember_fields = array();
+	// Loop through all the field groups.
+	$field_groups = PMPro_Field_Group::get_all();
+	foreach($field_groups as $group_name => $group) {
+		// Loop through all the fields in the group.
+		$fields = $group->get_fields();
+		foreach($fields as $field)
+		{
+			if(pmpro_is_field($field) && isset($field->addmember) && !empty($field->addmember) && ( in_array( strtolower( $field->addmember ), array( 'true', 'yes' ) ) || true == $field->addmember ) )
+			{
+					$addmember_fields[] = $field;
+			}
+		}
     }
 
     //save our added fields in session while the user goes off to PayPal
@@ -905,22 +824,17 @@ function pmpro_add_member_admin_save_user_fields( $uid = null, $user = null ) {
  * Get user fields which are set to show up in the Members List CSV Export.
  */
 function pmpro_get_user_fields_for_csv() {
-	global $pmpro_user_fields;
-
 	$csv_fields = array();
-	if(!empty($pmpro_user_fields))
-	{
-		//cycle through groups
-		foreach($pmpro_user_fields as $where => $fields)
+	// Loop through all the field groups.
+	$field_groups = PMPro_Field_Group::get_all();
+	foreach($field_groups as $group_name => $group) {
+		// Loop through all the fields in the group.
+		$fields = $group->get_fields();
+		foreach($fields as $field)
 		{
-			//cycle through fields
-			foreach($fields as $field)
+			if(pmpro_is_field($field) && !empty($field->memberslistcsv) && ($field->memberslistcsv == "true"))
 			{
-				if(pmpro_is_field($field) && !empty($field->memberslistcsv) && ($field->memberslistcsv == "true"))
-				{
-					$csv_fields[] = $field;
-				}
-
+				$csv_fields[] = $field;
 			}
 		}
 	}
@@ -933,43 +847,39 @@ function pmpro_get_user_fields_for_csv() {
  * If a $user_id is passed in, get fields based on the user's level.
  */
 function pmpro_get_user_fields_for_profile( $user_id, $withlocations = false ) {
-	global $pmpro_user_fields;
-
 	$profile_fields = array();
-	if(!empty($pmpro_user_fields))
-	{
-		//cycle through groups
-		foreach($pmpro_user_fields as $where => $fields)
+	// Loop through all the field groups.
+	$field_groups = PMPro_Field_Group::get_all();
+	foreach($field_groups as $group_name => $group) {
+		// Loop through all the fields in the group.
+		$fields = $group->get_fields();
+		foreach($fields as $field)
 		{
-			//cycle through fields
-			foreach($fields as $field)
-			{
-				if( ! pmpro_is_field( $field ) ) {
-                    continue;
-                }
-                
-                if ( ! pmpro_check_field_for_level( $field, "profile", $user_id ) ) {
-                    continue;
-                }
+			if( ! pmpro_is_field( $field ) ) {
+				continue;
+			}
+			
+			if ( ! pmpro_check_field_for_level( $field, "profile", $user_id ) ) {
+				continue;
+			}
 
-				if(!empty($field->profile) && ($field->profile === "admins" || $field->profile === "admin" || $field->profile === "only_admin"))
-				{
-					if( current_user_can( 'manage_options' ) || current_user_can( 'pmpro_membership_manager' ) )
-					{
-						if($withlocations)
-							$profile_fields[$where][] = $field;
-						else
-							$profile_fields[] = $field;
-					}
-				}
-				elseif(!empty($field->profile))
+			if(!empty($field->profile) && ($field->profile === "admins" || $field->profile === "admin" || $field->profile === "only_admin"))
+			{
+				if( current_user_can( 'manage_options' ) || current_user_can( 'pmpro_membership_manager' ) )
 				{
 					if($withlocations)
-						$profile_fields[$where][] = $field;
+						$profile_fields[$group_name][] = $field;
 					else
 						$profile_fields[] = $field;
 				}
 			}
+			elseif(!empty($field->profile))
+			{
+				if($withlocations)
+					$profile_fields[$group_name][] = $field;
+				else
+					$profile_fields[] = $field;
+			}
 		}
 	}
 
@@ -1050,7 +960,7 @@ function pmpro_save_user_fields_in_profile( $user_id )
  * Add user fields to confirmation email.
  */
 function pmpro_add_user_fields_to_email( $email ) {
-	global $wpdb, $pmpro_user_fields, $pmpro_field_groups;
+	global $wpdb;
 
 	//only update admin confirmation emails
 	if ( ! empty( $email ) && strpos( $email->template, "checkout" ) !== false && strpos( $email->template, "admin" ) !== false ) {
@@ -1062,22 +972,15 @@ function pmpro_add_user_fields_to_email( $email ) {
 
 
 			//add to bottom of email
-			if ( ! empty( $pmpro_field_groups ) ) {
+			$field_groups = PMPro_Field_Group::get_all();
+			if ( ! empty( $field_groups ) ) {
 				$fields_content = "<p>" . __( 'Extra Fields:', 'paid-memberships-pro' ) . "<br />";
 				$added_field = false;
-				//cycle through groups
-				foreach( $pmpro_field_groups as $group ) {
-
-					// Get the groups name so we can grab it from the associative array.
-					$group_name = $group->name;
-
-					// Skip if there are no fields in this group.
-					if ( empty( $pmpro_user_fields[$group_name] ) ) {
-						continue;
-					}
-					
-					//cycle through groups and fields associated with that group.
-					foreach( $pmpro_user_fields[$group_name] as $field ) {
+				// Loop through all the field groups.
+				foreach( $field_groups as $group_name => $group ) {
+					// Loop through all the fields in the group.
+					$fields = $group->get_fields();
+					foreach( $fields as $field ) {
 
 						if ( ! pmpro_is_field( $field ) ) {
 							continue;
@@ -1186,8 +1089,11 @@ function pmpro_cron_delete_tmp() {
 /**
  * Get user fields from global.
  * @since 2.9.3
+ * @deprecated TBD
  */
 function pmpro_get_user_fields() {
+	_deprecated_function( __FUNCTION__, 'TBD' );
+
     global $pmpro_user_fields;
         
     return (array)$pmpro_user_fields;
@@ -1304,8 +1210,8 @@ function pmpro_get_field_group_html( $group = null ) {
 			
 			<div class="pmpro_userfield-group-fields">
 				<?php
-					if ( ! empty( $group->fields ) ) {
-						foreach ( $group->fields as $field ) {
+					if ( ! empty( $group_fields ) ) {
+						foreach ( $group_fields as $field ) {
 							pmpro_get_field_html( $field );
 						}
 					}
@@ -1596,7 +1502,6 @@ function pmpro_get_user_fields_settings() {
  * Load user field settings into the fields global var.
  */
 function pmpro_load_user_fields_from_settings() {
-    global $pmpro_user_fields, $pmpro_field_groups;
     $settings_groups = pmpro_get_user_fields_settings();
 
     foreach ( $settings_groups as $group ) {
@@ -1716,9 +1621,12 @@ function pmpro_has_coded_user_fields() {
  * @return string|array The label(s) for the passed value. Will be same type as $field_value.
  */
 function pmpro_get_label_for_user_field_value( $field_name, $field_value ) {
-	global $pmpro_user_fields;
-	foreach ( $pmpro_user_fields as $user_field_group ) { // Loop through each user field group.
-		foreach ( $user_field_group as $user_field ) { // Loop through each user field in the group.
+	// Loop through all the field groups.
+	$field_groups = PMPro_Field_Group::get_all();
+	foreach($field_groups as $group_name => $group) {
+		// Loop through all the fields in the group.
+		$fields = $group->get_fields();
+		foreach( $fields as $user_field ) {
 			// Check if this is the user field that we are displaying.
 			if ( $user_field->name !== $field_name ) {
 				continue;
@@ -1757,23 +1665,29 @@ function pmpro_get_label_for_user_field_value( $field_name, $field_value ) {
 }
 
 /**
- * Get a single field from the global $pmpro_user_fields array.
+ * Get a single user field.
  * @since 3.0
  * @param string $field_name The name of the field to get.
  * @return bool|object The field object if found, false otherwise.
  */
 function pmpro_get_user_field( $field_name ) {
-	global $pmpro_user_fields;
-	
-	if ( empty( $pmpro_user_fields ) ) {
-		return false;
-	}
+	// Loop through all the field groups.
+	$field_groups = PMPro_Field_Group::get_all();
+	foreach($field_groups as $group_name => $group) {
+		// Loop through all the fields in the group.
+		$fields = $group->get_fields();
+		foreach( $fields as $user_field ) {
+			// Check if this is the user field that we are displaying.
+			if ( $user_field->name !== $field_name ) {
+				continue;
+			}
 
-	foreach ( $pmpro_user_fields as $group ) {
-		foreach ( $group as $field ) {
-			if ( $field->name === $field_name ) {
-				return $field;
+			// Make sure that we have a valid user field.
+			if ( ! pmpro_is_field( $user_field ) ) {
+				continue;
 			}
+
+			return $user_field;
 		}
 	}
 	
diff --git a/paid-memberships-pro.php b/paid-memberships-pro.php
index 556652d7d..2677ff0b9 100644
--- a/paid-memberships-pro.php
+++ b/paid-memberships-pro.php
@@ -46,6 +46,7 @@
 require_once( PMPRO_DIR . '/classes/class.memberorder.php' );       // class to process and save orders
 require_once( PMPRO_DIR . '/classes/class.pmproemail.php' );        // setup and filter emails sent by PMPro
 require_once( PMPRO_DIR . '/classes/class-pmpro-field.php' );
+require_once( PMPRO_DIR . '/classes/class-pmpro-field-group.php' );
 require_once( PMPRO_DIR . '/classes/class-pmpro-levels.php' );
 require_once( PMPRO_DIR . '/classes/class-pmpro-subscription.php' );
 require_once( PMPRO_DIR . '/classes/class-pmpro-admin-activity-email.php' );        // setup the admin activity email

From 14fe973d78d88aa3d4206c6970a2655622a0a2a9 Mon Sep 17 00:00:00 2001
From: David Parker <dlparker1005@gmail.com>
Date: Wed, 30 Oct 2024 09:00:50 -0400
Subject: [PATCH 2/6] Adding disclaimer about user field globals

---
 classes/class-pmpro-field-group.php | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/classes/class-pmpro-field-group.php b/classes/class-pmpro-field-group.php
index 338a24b22..3fc3469d6 100644
--- a/classes/class-pmpro-field-group.php
+++ b/classes/class-pmpro-field-group.php
@@ -1,4 +1,9 @@
 <?php
+/**
+ * Note: One goal of this class is to abstract all uses of the global $pmpro_field_groups and $pmpro_user_fields arrays.
+ *       In the next major release, we will likely remove these globals and store field groups and fields in this class
+ *       instead to prevent conflicts with other plugins and themes.
+ */
 class PMPro_Field_Group {
 	/**
 	 * The name of the field group.

From 59ebed46ed9528df7bfc95e10ddf3676bf34a48a Mon Sep 17 00:00:00 2001
From: David Parker <dlparker1005@gmail.com>
Date: Fri, 1 Nov 2024 13:44:14 -0400
Subject: [PATCH 3/6] Overhauling user fields logic with abstraction,
 simplifications, and deprecations

---
 adminpages/member-edit.php                    |   16 +-
 ...ro-class-member-edit-panel-user-fields.php |   37 +-
 adminpages/user-fields/field-settings.php     |  207 +++
 adminpages/user-fields/group-settings.php     |  132 ++
 adminpages/userfields.php                     |    4 +-
 classes/class-pmpro-field-group.php           |  361 +++++-
 classes/class-pmpro-field.php                 |  576 ++++++---
 includes/fields.php                           | 1150 +++++------------
 includes/functions.php                        |    7 +-
 scheduled/crons.php                           |   26 +
 shortcodes/pmpro_member.php                   |   17 +-
 11 files changed, 1531 insertions(+), 1002 deletions(-)
 create mode 100644 adminpages/user-fields/field-settings.php
 create mode 100644 adminpages/user-fields/group-settings.php

diff --git a/adminpages/member-edit.php b/adminpages/member-edit.php
index 00f27ca5c..74e996cce 100644
--- a/adminpages/member-edit.php
+++ b/adminpages/member-edit.php
@@ -25,11 +25,19 @@ function pmpro_member_edit_get_panels() {
 	// Add user fields panels.
 	$user_id = PMPro_Member_Edit_Panel::get_user()->ID;
 	if ( $user_id ) {
-		$profile_user_fields = pmpro_get_user_fields_for_profile( $user_id, true );
-		if ( ! empty( $profile_user_fields ) ) {
-			foreach ( $profile_user_fields as $group_name => $user_fields ) {
-				$panels[] = new PMPro_Member_Edit_Panel_User_Fields( $group_name );
+		foreach( PMPro_Field_Group::get_all() as $group ) {
+			$fields_to_display = $group->get_fields_to_display(
+				array(
+					'scope' => 'profile',
+					'user_id' => $user_id,
+				)
+			);
+	
+			if ( empty( $fields_to_display ) ) {
+				continue;
 			}
+
+			$panels[] = new PMPro_Member_Edit_Panel_User_Fields( $group->name );
 		}
 	}
 
diff --git a/adminpages/member-edit/pmpro-class-member-edit-panel-user-fields.php b/adminpages/member-edit/pmpro-class-member-edit-panel-user-fields.php
index 30d0aff86..4155d4e8d 100644
--- a/adminpages/member-edit/pmpro-class-member-edit-panel-user-fields.php
+++ b/adminpages/member-edit/pmpro-class-member-edit-panel-user-fields.php
@@ -1,12 +1,19 @@
 <?php
 
 class PMPro_Member_Edit_Panel_User_Fields extends PMPro_Member_Edit_Panel {
+	/**
+	 * @var PMPro_Field_Group The field group .
+	 * @since TBD
+	 */
+	private $field_group;
+
 	/**
 	 * Set up the panel.
 	 */
 	public function __construct( $field_group_name ) {
+		$this->field_group = PMPro_Field_Group::get( $field_group_name );
 		$this->slug = 'user-fields-' . sanitize_title( $field_group_name );
-		$this->title = $field_group_name;
+		$this->title = $this->field_group->label;
 		$this->submit_text = current_user_can( 'edit_users' ) ? __( 'Update Member', 'paid-memberships-pro' ) : '';
 	}
 
@@ -15,9 +22,8 @@ public function __construct( $field_group_name ) {
 	 */
 	protected function display_panel_contents() {
 		// Print the group description.
-		$field_group = pmpro_get_field_group_by_name( $this->title );
-		if ( ! empty( $field_group->description ) ) {
-			echo wp_kses_post( $field_group->description );
+		if ( ! empty( $this->field_group->description ) ) {
+			echo wp_kses_post( $this->field_group->description );
 		}
 
 		// Check if this is a checkout field location and show a message about custom code.
@@ -31,21 +37,22 @@ protected function display_panel_contents() {
 			'before_submit_button',
 			'just_profile'
 		);
-		if ( in_array( $this->title, $checkout_field_locations ) ) {
+		if ( in_array( $this->field_group->name, $checkout_field_locations ) ) {
 			esc_html_e( 'These user fields were added via custom code to hook into the following location:', 'paid-memberships-pro' );
-			echo ' <code>' . esc_html( $this->title ) . '</code>';
+			echo ' <code>' . esc_html( $this->field_group->name ) . '</code>';
 		}
 
 		// Print the fields.
-		$profile_user_fields = pmpro_get_user_fields_for_profile( self::get_user()->ID, true );
 		?>
 		<table class="form-table">
 			<?php
-			foreach( $profile_user_fields[$this->title] as $field ) {
-				if ( pmpro_is_field( $field ) ) {
-					$field->displayInProfile( self::get_user()->ID ); // Field will be readonly if cannot edit users.
-				}
-			}
+			$this->field_group->display(
+				array(
+					'markup' => 'table',
+					'show_group_label' => false,
+					'user_id' => self::get_user()->ID,
+				)
+			);
 			?>
 		</table>
 		<?php
@@ -55,7 +62,11 @@ protected function display_panel_contents() {
 	 * Save panel data.
 	 */
 	public function save() {
-		$saved = ( pmpro_save_user_fields_in_profile( self::get_user()->ID ) !== false ); // Function returns false on failed, null on saved. Will check edit_users cap in function.
+		$saved = $this->field_group->save_fields(
+			array(
+				'user_id' => self::get_user()->ID,
+			)
+		);
 
 		// Show success message.
 		if ( $saved ) {
diff --git a/adminpages/user-fields/field-settings.php b/adminpages/user-fields/field-settings.php
new file mode 100644
index 000000000..8662216ca
--- /dev/null
+++ b/adminpages/user-fields/field-settings.php
@@ -0,0 +1,207 @@
+<?php
+
+if ( ! empty( $field ) ) {
+	// Assume field stdClass in format we save to settings.
+	$field_label = $field->label;
+	$field_name = $field->name;
+	$field_type = $field->type;
+	$field_required = $field->required;
+	$field_readonly = $field->readonly;     	
+	$field_profile = $field->profile;
+	$field_wrapper_class = $field->wrapper_class;
+	$field_element_class = $field->element_class;
+	$field_hint = $field->hint;
+	$field_options = $field->options;
+	$field_allowed_file_types = $field->allowed_file_types;
+	$field_max_file_size = $field->max_file_size;
+	$field_default = $field->default;
+} else {
+	// Default field values
+	$field_label = '';
+	$field_name = '';
+	$field_type = '';
+	$field_required = false;
+	$field_readonly = false;
+	$field_profile = '';
+	$field_wrapper_class = '';
+	$field_element_class = '';
+	$field_hint = '';
+	$field_options = '';
+	$field_allowed_file_types = '';
+	$field_max_file_size = '';
+	$field_default = '';
+}
+
+// Other vars
+$levels = pmpro_sort_levels_by_order( pmpro_getAllLevels( true, true ) );
+?>
+<div class="pmpro_userfield-group-field pmpro_userfield-group-field-collapse">
+	<ul class="pmpro_userfield-group-tbody">
+		<li class="pmpro_userfield-group-column-order">
+			<div class="pmpro_userfield-group-buttons">
+				<button type="button" aria-disabled="false" class="pmpro_userfield-group-buttons-button pmpro_userfield-field-buttons-button-move-up" aria-label="<?php esc_attr_e( 'Move up', 'paid-memberships-pro' ); ?>">
+					<span class="dashicons dashicons-arrow-up-alt2"></span>
+				</button>
+				<span class="pmpro_userfield-group-buttons-description"><?php esc_html_e( 'Move Field Up', 'paid-memberships-pro' ); ?></span>
+
+				<button type="button" aria-disabled="false" class="pmpro_userfield-group-buttons-button pmpro_userfield-field-buttons-button-move-down" aria-label="<?php esc_attr_e( 'Move down', 'paid-memberships-pro' ); ?>">
+					<span class="dashicons dashicons-arrow-down-alt2"></span>
+				</button>
+				<span class="pmpro_userfield-group-buttons-description"><?php esc_html_e( 'Move Field Down', 'paid-memberships-pro' ); ?></span>
+			</div> <!-- end pmpro_userfield-group-buttons -->
+		</li>
+		<li class="pmpro_userfield-group-column-label">
+			<span class="pmpro_userfield-label"><?php echo strip_tags( wp_kses_post( $field_label ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span>
+			<div class="pmpro_userfield-field-options">
+				<a class="edit-field" title="<?php esc_attr_e( 'Edit field', 'paid-memberships-pro' ); ?>" href="javascript:void(0);"><?php esc_html_e( 'Edit', 'paid-memberships-pro' ); ?></a> |
+				<a class="duplicate-field" title="<?php esc_attr_e( 'Duplicate field', 'paid-memberships-pro' ); ?>" href="javascript:void(0);"><?php esc_html_e( 'Duplicate', 'paid-memberships-pro' ); ?></a> |
+				<a class="delete-field" title="<?php esc_attr_e( 'Delete field', 'paid-memberships-pro' ); ?>" href="javascript:void(0);"><?php esc_html_e( 'Delete', 'paid-memberships-pro' ); ?></a>
+			</div> <!-- end pmpro_userfield-group-options -->
+		</li>
+		<li class="pmpro_userfield-group-column-name"><?php echo esc_html( $field_name); ?></li>
+		<li class="pmpro_userfield-group-column-type"><?php echo esc_html( $field_type); ?></li>
+	</ul>
+
+	<div class="pmpro_userfield-field-settings" style="display: none;">
+
+		<div class="pmpro_userfield-field-setting">
+			<label>
+				<?php esc_html_e( 'Label', 'paid-memberships-pro' ); ?><br />
+				<input type="text" name="pmpro_userfields_field_label" value="<?php echo esc_attr( $field_label );?>" />                    
+			</label>                
+			<span class="description"><?php esc_html_e( 'Brief descriptive text for the field. Shown on user forms.', 'paid-memberships-pro' ); ?></span>
+		</div> <!-- end pmpro_userfield-field-setting -->
+
+		<div class="pmpro_userfield-field-setting">
+			<label>
+				<?php esc_html_e( 'Name', 'paid-memberships-pro' ); ?><br />
+				<input type="text" name="pmpro_userfields_field_name" value="<?php echo esc_attr( $field_name );?>" />
+			</label>                
+			<span class="description"><?php esc_html_e( 'Single word with no spaces. Underscores are allowed.', 'paid-memberships-pro' ); ?></span>
+		</div> <!-- end pmpro_userfield-field-setting -->
+
+		<div class="pmpro_userfield-field-setting">
+			<label>
+				<?php esc_html_e( 'Type', 'paid-memberships-pro' ); ?><br />
+				<select name="pmpro_userfields_field_type" />
+					<option value="text" <?php selected( $field_type, 'text' ); ?>><?php esc_html_e( 'Text', 'paid-memberships-pro' ); ?></option>
+					<option value="textarea" <?php selected( $field_type, 'textarea' ); ?>><?php esc_html_e( 'Text Area', 'paid-memberships-pro' ); ?></option>
+					<option value="checkbox" <?php selected( $field_type, 'checkbox' ); ?>><?php esc_html_e( 'Checkbox', 'paid-memberships-pro' ); ?></option>
+		<option value="checkbox_grouped" <?php selected( $field_type, 'checkbox_grouped' ); ?>><?php esc_html_e( 'Checkbox Group', 'paid-memberships-pro' ); ?></option>
+					<option value="radio" <?php selected( $field_type, 'radio' ); ?>><?php esc_html_e( 'Radio', 'paid-memberships-pro' ); ?></option>
+					<option value="select" <?php selected( $field_type, 'select' ); ?>><?php esc_html_e( 'Select / Dropdown', 'paid-memberships-pro' ); ?></option>
+					<option value="select2" <?php selected( $field_type, 'select2' ); ?>><?php esc_html_e( 'Select2 / Autocomplete', 'paid-memberships-pro' ); ?></option>
+					<option value="multiselect" <?php selected( $field_type, 'multiselect' ); ?>><?php esc_html_e( 'Multi Select', 'paid-memberships-pro' ); ?></option>
+					<option value="file" <?php selected( $field_type, 'file' ); ?>><?php esc_html_e( 'File', 'paid-memberships-pro' ); ?></option>
+					<option value="number" <?php selected( $field_type, 'number' ); ?>><?php esc_html_e( 'Number', 'paid-memberships-pro' ); ?></option>
+					<option value="date" <?php selected( $field_type, 'date' ); ?>><?php esc_html_e( 'Date', 'paid-memberships-pro' ); ?></option>
+					<option value="readonly" <?php selected( $field_type, 'readonly' ); ?>><?php esc_html_e( 'Read-Only', 'paid-memberships-pro' ); ?></option>
+					<option value="hidden" <?php selected( $field_type, 'hidden' ); ?>><?php esc_html_e( 'Hidden', 'paid-memberships-pro' ); ?></option>
+				</select>
+			</label>                
+		</div> <!-- end pmpro_userfield-field-setting -->
+
+		<div class="pmpro_userfield-field-setting pmpro_userfield-field-setting-dual">
+			<div class="pmpro_userfield-field-setting">
+				<label>
+					<?php esc_html_e( 'Required at Checkout?', 'paid-memberships-pro' ); ?><br />
+					<select name="pmpro_userfields_field_required">
+						<option value="no" <?php selected( $field_required, 'no' );?>><?php esc_html_e( 'No', 'paid-memberships-pro' ); ?></option>
+						<option value="yes" <?php selected( $field_required, 'yes' );?>><?php esc_html_e( 'Yes', 'paid-memberships-pro' ); ?></option>
+					</select>
+				</label>                    
+			</div> <!-- end pmpro_userfield-field-setting -->
+
+			<div class="pmpro_userfield-field-setting">
+				<label>
+					<?php esc_html_e( 'Read Only?', 'paid-memberships-pro' ); ?><br />
+					<select name="pmpro_userfields_field_readonly">
+						<option value="no" <?php selected( $field_readonly, 'no' );?>><?php esc_html_e( 'No', 'paid-memberships-pro' ); ?></option>
+						<option value="yes" <?php selected( $field_readonly, 'yes' );?>><?php esc_html_e( 'Yes', 'paid-memberships-pro' ); ?></option>
+					</select>
+				</label>                    
+			</div> <!-- end pmpro_userfield-field-setting -->
+		</div> <!-- end pmpro_userfield-field-setting-dual -->
+
+		<div class="pmpro_userfield-field-setting">
+			<label>
+				<?php esc_html_e( 'Show field on user profile?', 'paid-memberships-pro' ); ?><br />
+				<select name="pmpro_userfields_field_profile">
+					<option value="" <?php selected( empty( $field_profile ), 0);?>><?php esc_html_e( '[Inherit Group Setting]', 'paid-memberships-pro' ); ?></option>
+					<option value="yes" <?php selected( $field_profile, 'yes' );?>><?php esc_html_e( 'Yes', 'paid-memberships-pro' ); ?></option>
+					<option value="admins" <?php selected( $field_profile, 'admins' );?>><?php esc_html_e( 'Yes (only admins)', 'paid-memberships-pro' ); ?></option>
+					<option value="no" <?php selected( $field_profile, 'no' );?>><?php esc_html_e( 'No', 'paid-memberships-pro' ); ?></option>
+				</select>
+			</label>                
+		</div> <!-- end pmpro_userfield-field-setting -->
+
+		<div class="pmpro_userfield-field-setting pmpro_userfield-field-setting-dual">
+			<div class="pmpro_userfield-field-setting">
+				<label>
+					<?php esc_html_e( 'Field Wrapper Class (optional)', 'paid-memberships-pro' ); ?><br />
+					<input type="text" name="pmpro_userfields_field_class" value="<?php echo esc_attr( $field_wrapper_class );?>" />
+				</label>
+				<span class="description"><?php esc_html_e( 'Assign a custom CSS selector to the field\'s wrapping div', 'paid-memberships-pro' ); ?>.</span>
+			</div> <!-- end pmpro_userfield-field-setting -->
+
+			<div class="pmpro_userfield-field-setting">
+				<label>
+					<?php esc_html_e( 'Field Element Class (optional)', 'paid-memberships-pro' ); ?><br />
+					<input type="text" name="pmpro_userfields_field_divclass" value="<?php echo esc_attr( $field_element_class );?>" />
+				</label>                
+				<span class="description"><?php esc_html_e( 'Assign a custom CSS selector to the field', 'paid-memberships-pro' ); ?></span>
+			</div> <!-- end pmpro_userfield-field-setting -->
+		</div> <!-- end pmpro_userfield-field-setting-dual -->
+
+		<div class="pmpro_userfield-field-setting">
+			<label>
+				<?php esc_html_e( 'Hint (optional)', 'paid-memberships-pro' ); ?><br />
+				<textarea name="pmpro_userfields_field_hint" /><?php echo esc_textarea( $field_hint );?></textarea>
+			</label>                
+			<span class="description"><?php esc_html_e( 'Descriptive text for users or admins submitting the field.', 'paid-memberships-pro' ); ?></span>
+		</div> <!-- end pmpro_userfield-field-setting -->
+
+		<div class="pmpro_userfield-field-setting">
+			<div class="pmpro_userfield-field-setting pmpro_userfield-field-setting-dual">
+				<div class="pmpro_userfield-field-setting">
+					<label>
+						<?php esc_html_e( 'Allowed File Types', 'paid-memberships-pro' ); ?><br />
+						<input type="text" name="pmpro_userfields_field_allowed_file_types" value="<?php echo esc_attr( trim( $field_allowed_file_types ) ); ?>" />
+					</label>
+					<span class="description"><?php esc_html_e( 'Restrict the file type that is allowed to be uploaded. Separate the file types using a comma ",". For example: png,pdf,jpg.', 'paid-memberships-pro' ); ?></span>
+				</div> <!-- end pmpro_userfield-field-setting -->
+				<div class="pmpro_userfield-field-setting">
+					<?php $server_max_upload = wp_max_upload_size() / 1024 / 1024; ?>
+					<label>
+						<?php esc_html_e( 'Max File Size Upload', 'paid-memberships-pro' ); ?><br />
+						<input type="number" name="pmpro_userfields_field_max_file_size" value="<?php echo intval( $field_max_file_size ); ?>" max="<?php echo esc_attr( $server_max_upload ); ?>"/>
+					</label>
+					<span class="description"><?php printf( esc_html__( 'Enter an upload size limit for files in Megabytes (MB) or set it to 0 to use your default server upload limit. Your server upload limit is %s.', 'paid-memberships-pro' ), $server_max_upload . 'MB' ); ?></span>
+				</div> <!-- end pmpro_userfield-field-setting -->
+			</div>
+			<div class="pmpro_userfield-field-setting">
+				<label>
+					<?php esc_html_e( 'Options', 'paid-memberships-pro' ); ?><br />
+					<textarea name="pmpro_userfields_field_options" /><?php echo esc_textarea( $field_options );?></textarea>
+				</label>
+				<span class="description"><?php esc_html_e( 'One option per line. To set separate values and labels, use value:label.', 'paid-memberships-pro' ); ?></span>
+			</div> <!-- end pmpro_userfield-field-setting -->
+			
+			<div class="pmpro_userfield-field-setting">
+				<label>
+					<?php esc_html_e( 'Default Value (optional)', 'paid-memberships-pro' ); ?><br />
+					<input type="text" name="pmpro_userfields_field_default" value="<?php echo esc_attr( $field_default ); ?>" />
+				</label>
+			</div> <!-- end pmpro_userfield-field-setting -->
+		</div>
+
+		<div class="pmpro_userfield-field-actions">            
+			<button name="pmpro_userfields_close_field" class="button button-secondary pmpro_userfields_close_field">
+				<?php esc_html_e( 'Close Field', 'paid-memberships-pro' ); ?>
+			</button> 
+			<button name="pmpro_userfields_delete_field" class="button button-secondary is-destructive">
+				<?php esc_html_e( 'Delete Field', 'paid-memberships-pro' ); ?>
+			</button>           
+		</div> <!-- end pmpro_userfield-field-actions -->
+	</div> <!-- end pmpro_userfield-field-settings -->        
+</div> <!-- end pmpro_userfield-group-field -->
diff --git a/adminpages/user-fields/group-settings.php b/adminpages/user-fields/group-settings.php
new file mode 100644
index 000000000..2c42a89ed
--- /dev/null
+++ b/adminpages/user-fields/group-settings.php
@@ -0,0 +1,132 @@
+<?php
+
+if ( ! empty( $group ) ) {
+	// Assume group stdClass in format we save to settings.
+	$group_name = $group->name;
+	$group_show_checkout = $group->checkout;
+	$group_show_profile = $group->profile;
+	$group_description = $group->description;    	
+	$group_levels = $group->levels;
+	$group_fields = $group->fields;
+} else {
+	// Default group settings.
+	$group_name = '';
+	$group_show_checkout = 'yes';
+	$group_show_profile = 'yes';
+	$group_description = '';    	
+	$group_levels = array();
+	$group_fields = array();
+}
+
+// Other vars
+$levels = pmpro_sort_levels_by_order( pmpro_getAllLevels( true, true ) );
+
+// Render field group HTML.
+?>
+<div class="pmpro_userfield-group">
+	<div class="pmpro_userfield-group-header">
+		<div class="pmpro_userfield-group-buttons">
+			<button type="button" aria-disabled="false" class="pmpro_userfield-group-buttons-button pmpro_userfield-group-buttons-button-move-up" aria-label="<?php esc_attr_e( 'Move up', 'paid-memberships-pro' ); ?>">
+				<span class="dashicons dashicons-arrow-up-alt2"></span>
+			</button>
+			<span class="pmpro_userfield-group-buttons-description"><?php esc_html_e( 'Move Group Up', 'paid-memberships-pro' ); ?></span>
+
+			<button type="button" aria-disabled="false" class="pmpro_userfield-group-buttons-button pmpro_userfield-group-buttons-button-move-down" aria-label="<?php esc_attr_e( 'Move down', 'paid-memberships-pro' ); ?>">
+				<span class="dashicons dashicons-arrow-down-alt2"></span>
+			</button>
+			<span id="pmpro_userfield-group-buttons-description-2" class="pmpro_userfield-group-buttons-description"><?php esc_html_e( 'Move Group Down', 'paid-memberships-pro' ); ?></span>
+		</div> <!-- end pmpro_userfield-group-buttons -->
+		<h3>
+			<label>                    
+				<?php esc_html_e( 'Group Name', 'paid-memberships-pro' ); ?>
+				<input type="text" name="pmpro_userfields_group_name" placeholder="<?php esc_attr_e( 'Group Name', 'paid-memberships-pro' ); ?>" value="<?php echo esc_attr( $group_name ); ?>" />
+			</label>                
+		</h3>
+		<button type="button" aria-disabled="false" class="pmpro_userfield-group-buttons-button pmpro_userfield-group-buttons-button-toggle-group" aria-label="<?php esc_attr_e( 'Expand and Edit Group', 'paid-memberships-pro' ); ?>">
+			<span class="dashicons dashicons-arrow-up"></span>
+		</button>
+		<span class="pmpro_userfield-group-buttons-description"><?php esc_html_e( 'Expand and Edit Group', 'paid-memberships-pro' ); ?></span>
+	</div> <!-- end pmpro_userfield-group-header -->
+
+	<div class="pmpro_userfield-inside">
+		<div class="pmpro_userfield-field-settings">
+			
+			<div class="pmpro_userfield-field-setting">
+				<label>
+					<?php esc_html_e( 'Show fields at checkout?', 'paid-memberships-pro' ); ?><br />
+					<select name="pmpro_userfields_group_checkout">
+						<option value="yes" <?php selected( $group_show_checkout, 'yes' ); ?>><?php esc_html_e( 'Yes', 'paid-memberships-pro' ); ?></option>
+						<option value="no" <?php selected( $group_show_checkout, 'no' ); ?>><?php esc_html_e( 'No', 'paid-memberships-pro' ); ?></option>
+					</select>
+				</label>
+			</div> <!-- end pmpro_userfield-field-setting -->
+			
+			<div class="pmpro_userfield-field-setting">
+				<label>
+					<?php esc_html_e( 'Show fields on user profile?', 'paid-memberships-pro' ); ?><br />
+					<select name="pmpro_userfields_group_profile">
+						<option value="yes" <?php selected( $group_show_profile, 'yes' ); ?>><?php esc_html_e( 'Yes', 'paid-memberships-pro' ); ?></option>
+						<option value="admins" <?php selected( $group_show_profile, 'admins' ); ?>><?php esc_html_e( 'Yes (only admins)', 'paid-memberships-pro' ); ?></option>
+						<option value="no" <?php selected( $group_show_profile, 'no' ); ?>><?php esc_html_e( 'No', 'paid-memberships-pro' ); ?></option>
+					</select>
+				</label>
+			</div> <!-- end pmpro_userfield-field-setting -->
+			
+			<div class="pmpro_userfield-field-setting">
+				<label>
+					<?php esc_html_e( 'Description (optional, visible to users)', 'paid-memberships-pro' ); ?><br />
+					<textarea name="pmpro_userfields_group_description"><?php echo esc_textarea( $group_description );?></textarea>
+				</label>
+			</div> <!-- end pmpro_userfield-field-setting -->
+			
+			<div class="pmpro_userfield-field-setting">
+				<?php esc_html_e( 'Restrict Fields for Membership Levels', 'paid-memberships-pro' ); ?><br />
+				<div class="pmpro_checkbox_box" <?php if ( count( $levels ) > 3 ) { ?>style="height: 90px; overflow: auto;"<?php } ?>>
+					<?php foreach( $levels as $level ) { ?>
+						<div class="pmpro_clickable">
+							<label>
+								<input type="checkbox" id="pmpro_userfields_group_membership_<?php echo esc_attr( $level->id); ?>" name="pmpro_userfields_group_membership[]" <?php checked( true, in_array( $level->id, $group_levels ) );?>>
+								<?php echo esc_html( $level->name ); ?>
+							</label>
+						</div>
+					<?php } ?>
+				</div>
+			</div> <!-- end pmpro_userfield-field-setting -->
+		
+		</div> <!-- end pmpro_userfield-field-settings -->
+		
+		<h3><?php esc_html_e( 'Manage Fields in This Group', 'paid-memberships-pro' ); ?></h3>
+		
+		<ul class="pmpro_userfield-group-thead">
+			<li class="pmpro_userfield-group-column-order"><?php esc_html_e( 'Order', 'paid-memberships-pro'); ?></li>
+			<li class="pmpro_userfield-group-column-label"><?php esc_html_e( 'Label', 'paid-memberships-pro'); ?></li>
+			<li class="pmpro_userfield-group-column-name"><?php esc_html_e( 'Name', 'paid-memberships-pro'); ?></li>
+			<li class="pmpro_userfield-group-column-type"><?php esc_html_e( 'Type', 'paid-memberships-pro'); ?></li>
+		</ul>
+		
+		<div class="pmpro_userfield-group-fields">
+			<?php
+				if ( ! empty( $group_fields ) ) {
+					foreach ( $group_fields as $field ) {
+						pmpro_get_field_html( $field );
+					}
+				}
+			?>
+			
+			<!-- end pmpro_userfield-group-fields -->
+		
+		</div> <!-- end pmpro_userfield-inside -->
+
+		<div class="pmpro_userfield-group-actions">
+			<button name="pmpro_userfields_add_field" class="button button-secondary button-hero">
+				<?php
+					/* translators: a plus sign dashicon */
+					printf( esc_html__( '%s Add Field', 'paid-memberships-pro' ), '<span class="dashicons dashicons-plus"></span>' ); ?>
+			</button>
+			<button name="pmpro_userfields_delete_group" class="button button-secondary is-destructive">
+				<?php esc_html_e( 'Delete Group', 'paid-memberships-pro' ); ?>
+			</button>
+		</div> <!-- end pmpro_userfield-group-actions -->
+
+	</div> <!-- end pmpro_userfield-group -->
+</div> <!-- end inside -->
diff --git a/adminpages/userfields.php b/adminpages/userfields.php
index 7bb2f5505..ad6db4ef4 100644
--- a/adminpages/userfields.php
+++ b/adminpages/userfields.php
@@ -60,7 +60,9 @@
 	require_once( dirname(__FILE__) . '/admin_header.php' );
 
 	// Show warning if there are additional fields that are coded.
-	if ( pmpro_has_coded_user_fields() ) {
+	$num_fields_from_settings = array_sum( array_map( function ($group) { return count( $group->fields ); }, pmpro_get_user_fields_settings() ) ); // Fields from UI settings page.
+	$total_registered_fields = array_sum( array_map( function ($group) { return count( $group->get_fields() ); }, PMPro_Field_Group::get_all() ) ); // All registered fields.
+	if ( $num_fields_from_settings < $total_registered_fields ) {
 		?>
 		<div class="notice notice-warning">
 			<p><?php esc_html_e( 'This website has additional user fields that are set up with code. Coded fields cannot be edited here and will show in addition to the fields set up on this page.', 'paid-memberships-pro' ); ?></p>
diff --git a/classes/class-pmpro-field-group.php b/classes/class-pmpro-field-group.php
index 3fc3469d6..7a94f2185 100644
--- a/classes/class-pmpro-field-group.php
+++ b/classes/class-pmpro-field-group.php
@@ -31,7 +31,7 @@ class PMPro_Field_Group {
 	/**
 	 * Constructor.
 	 */
-	public function __construct( $name, $label, $description = '' ) {
+	private function __construct( $name, $label, $description = '' ) {
 		$this->name        = $name;
 		$this->label       = $label;
 		$this->description = $description;
@@ -130,6 +130,59 @@ public static function get( $name ) {
 		return $field_groups[ $name ];
 	}
 
+	/**
+	 * Get the field group for a field.
+	 *
+	 * @since TBD
+	 *
+	 * @param PMPro_Field $field The field object.
+	 * @return PMPro_Field_Group|null The field group object, or NULL if the field is not in a group.
+	 */
+	public static function get_group_for_field( $field ) {
+		global $pmpro_field_groups;
+
+		if ( empty( $pmpro_field_groups ) ) {
+			$pmpro_field_groups = array();
+		}
+
+		foreach ( $pmpro_field_groups as $field_group ) {
+			$group_fields = $field_group->get_fields();
+			foreach( $group_fields as $group_field ) {
+				if ( $group_field->name === $field->name ) {
+					return $field_group;
+				}
+			}
+		}
+
+		return null;
+	}
+
+	/**
+	 * Get a field by name.
+	 *
+	 * @since TBD
+	 *
+	 * @param string $name The name of the field.
+	 * @return PMPro_Field|null The field object, or NULL if the field is not in a group.
+	 */
+	public static function get_field( $name ) {
+		global $pmpro_user_fields;
+
+		if ( empty( $pmpro_user_fields ) ) {
+			$pmpro_user_fields = array();
+		}
+
+		foreach ( $pmpro_user_fields as $group_name => $fields ) {
+			foreach ( $fields as $field ) {
+				if ( $field->name === $name ) {
+					return $field;
+				}
+			}
+		}
+
+		return null;
+	}
+
 	/**
 	 * Add a field to this field group.
 	 *
@@ -189,4 +242,310 @@ public function get_fields() {
 
 		return $pmpro_user_fields[ $this->name ];
 	}
+
+	/**
+	 * Get all fields to display in a specific context.
+	 *
+	 * @since TBD
+	 *
+	 * @param array $args The arguments for getting the fields.
+	 */
+	public function get_fields_to_display( $args = array() ) {
+		$default_args = array(
+			'scope' => 'profile', // The scope of the fields to show. Can be 'profile' or 'checkout'.
+			'user_id' => NULL, // The user ID to show the users for. If null, we are showing fields for the current user.
+		);
+		$args = wp_parse_args( $args, $default_args );
+
+		// Get all fields in this group.
+		$fields = $this->get_fields();
+
+		// Get the user ID.
+		$user_id = empty( $args['user_id'] ) ? get_current_user_id() : $args['user_id'];
+
+		// Get a list of the fields that should be displayed.
+		$fields_to_display = array();
+		foreach ( $fields as $field ) {
+			// Validate the field for scope.
+			if ( 'checkout' === $args['scope'] ) {
+				// At checkout.
+				// Check if this field should only be shown in the profile.
+				if ( in_array( $field->profile, array( 'only', 'only_admin' ), true ) ) {
+					continue;
+				}
+
+				// Check if this field is for the level being purchased.
+				// Get the checkout level.
+				$checkout_level = pmpro_getLevelAtCheckout();
+				$chekcout_level_id = ! empty( $checkout_level->id ) ? (int)$checkout_level->id : NULL;
+				if ( empty( $chekcout_level_id ) ) {
+					continue;
+				}
+				if ( ! empty( $field->levels ) && ! in_array( (int) $chekcout_level_id, $field->levels, true ) ) {
+					continue;
+				}
+			} else {
+				// In profile.
+				// Check if this field should ever be shown in the profile.
+				if ( empty( $field->profile ) ) {
+					continue;
+				}
+
+				// Check if this field should only be shown to admins.
+				if ( ( ! current_user_can( 'manage_options' ) && ! current_user_can( 'pmpro_membership_manager' ) ) && in_array( $field->profile, array( 'admins', 'admin', 'only_admin' ), true ) ) {
+					continue;
+				}
+
+				// Check if the user has a level required for this field.
+				if ( ! empty( $field->levels ) && ! pmpro_hasMembershipLevel( $field->levels, $user_id ) ) {
+					continue;
+				}
+			}
+
+			// Add the field to the list of fields to display.
+			$fields_to_display[] = $field;
+		}
+
+		return $fields_to_display;
+	}
+
+	/**
+	 * Display the field group.
+	 *
+	 * @since TBD
+	 *
+	 * @param array $args The arguments for displaying the fields.
+	 */
+	public function display( $args = array() ) {
+		$default_args = array(
+			'markup' => 'card', // The markup to use for the field group. Can be 'card', 'div' or 'table'.
+			'scope' => 'profile', // The scope of the fields to show. Can be 'profile' or 'checkout'.
+			'show_group_label' => true, // Whether or not to show the field group.
+			'prefill_from_request' => false, // Whether or not to prefill the field values from the $_REQUEST array.
+			'show_required' => false, // Whether or not to show required fields.
+			'user_id' => NULL, // The user ID to show the users for. If null, we are showing fields for the current user.
+		);
+		$args = wp_parse_args( $args, $default_args );
+
+		// Get the user ID.
+		$user_id = empty( $args['user_id'] ) ? get_current_user_id() : $args['user_id'];
+
+		// Get the fields to display.
+		$fields_to_display = $this->get_fields_to_display( $args );
+
+		// If we don't have any fields to display, don't display the group.
+		if ( empty( $fields_to_display ) ) {
+			return;
+		}
+
+		// Display the field group.
+		if ( empty( $args['show_group_label'] ) ) {
+			$group_header = '';
+			$group_footer = '';
+		} elseif ( $args['markup'] === 'card' ) {
+			// Get the "header" for the field group.
+			ob_start();
+			?>
+			<fieldset id="pmpro_form_fieldset-<?php echo esc_attr( sanitize_title( $this->name ) ); ?>" class="<?php echo esc_attr( pmpro_get_element_class( 'pmpro_form_fieldset', 'pmpro_form_fieldset-' . sanitize_title( $this->name ) ) ); ?>">
+				<div class="<?php echo esc_attr( pmpro_get_element_class( 'pmpro_card' ) ); ?>">
+					<div class="<?php echo esc_attr( pmpro_get_element_class( 'pmpro_card_content' ) ); ?>">
+						<?php if ( ! empty( $this->label ) ) { ?>
+							<legend class="<?php echo esc_attr( pmpro_get_element_class( 'pmpro_form_legend' ) ); ?>">
+								<h2 class="<?php echo esc_attr( pmpro_get_element_class( 'pmpro_form_heading pmpro_font-large' ) ); ?>"><?php echo wp_kses_post( $this->label ); ?></h2>
+							</legend>
+						<?php } ?>
+						<div class="<?php echo esc_attr( pmpro_get_element_class( 'pmpro_form_fields' ) ); ?>">
+							<?php if ( ! empty( $this->description ) ) { ?>
+								<div class="<?php echo esc_attr( pmpro_get_element_class( 'pmpro_form_fields-description' ) ); ?>"><?php echo wp_kses_post( $this->description ); ?></div>
+							<?php } ?>
+			<?php
+			$group_header = ob_get_clean();
+
+			// Get the "footer" for the field group.
+			ob_start();
+			?>
+						</div> <!-- end pmpro_form_fields -->
+					</div> <!-- end pmpro_card_content -->
+				</div> <!-- end pmpro_card -->
+			</fieldset> <!-- end pmpro_form_fieldset -->
+			<?php
+			$group_footer = ob_get_clean();
+		} elseif( $args['markup'] === 'div' ) {
+			// Get the "header" for the field group.
+			ob_start();
+			?>
+			<div class="<?php echo esc_attr( pmpro_get_element_class( 'pmpro_spacer' ) ); ?>"></div>
+			<fieldset id="pmpro_form_fieldset-<?php echo esc_attr( sanitize_title( $this->name ) ); ?>" class="<?php echo esc_attr( pmpro_get_element_class( 'pmpro_form_fieldset', 'pmpro_form_fieldset-' . sanitize_title( $this->name ) ) ); ?>">
+				<?php if ( ! empty( $this->label ) ) { ?>
+					<legend class="<?php echo esc_attr( pmpro_get_element_class( 'pmpro_form_legend' ) ); ?>">
+						<h2 class="<?php echo esc_attr( pmpro_get_element_class( 'pmpro_form_heading pmpro_font-large' ) ); ?>"><?php echo wp_kses_post( $this->label ); ?></h2>
+					</legend>
+				<?php } ?>
+				<div class="<?php echo esc_attr( pmpro_get_element_class( 'pmpro_form_fields' ) ); ?>">
+					<?php if ( ! empty( $this->description ) ) { ?>
+						<div class="<?php echo esc_attr( pmpro_get_element_class( 'pmpro_form_fields-description' ) ); ?>"><?php echo wp_kses_post( $this->description ); ?></div>
+					<?php } ?>
+			<?php
+			$group_header = ob_get_clean();
+
+			// Get the "footer" for the field group.
+			ob_start();
+			?>
+				</div> <!-- end pmpro_form_fields -->
+			</fieldset> <!-- end pmpro_form_fieldset -->
+			<?php
+			$group_footer = ob_get_clean();
+		}else {
+			// Get the "header" for the field group.
+			ob_start();
+			?>
+			<h2><?php echo wp_kses_post( $this->label ); ?></h2>
+			<?php
+			if ( ! empty( $box->description ) ) {
+				?>
+				<p><?php echo wp_kses_post( $this->description ); ?></p>
+				<?php
+			}
+			?>
+			<table class="form-table">
+			<?php
+			$group_header = ob_get_clean();
+
+			// Get the "footer" for the field group.
+			ob_start();
+			?>
+			</table>
+			<?php
+			$group_footer = ob_get_clean();
+		}
+
+		// Output the group header.
+		echo $group_header;
+
+		// Display the fields.
+		foreach ( $fields_to_display as $field ) {
+			// Get the value for this field.
+			$value = '';
+			if( ! empty( $args['prefill_from_request'] ) && null !== $field->get_value_from_request() ) {
+				$value = $field->get_value_from_request();
+			} elseif ( ! empty( $user_id ) && metadata_exists( 'user', $user_id, $field->meta_key ) ) {
+				$value = get_user_meta( $user_id, $field->meta_key, true );
+			} elseif ( ! empty( $user_id ) ) {
+				$userdata = get_userdata( $user_id );
+				if ( ! empty( $userdata->{$field->name} ) ) {
+					$value = $userdata->{$field->name};
+				} elseif(isset($field->value)) {
+					$value = $field->value;
+				}
+			} elseif(isset($field->value)) {
+				$value = $field->value;
+			}
+
+			if ( $args['markup'] === 'div' || $args['markup'] === 'card' ) {
+				// Fix divclass.
+				if ( ! empty( $field->divclass ) ) {
+					$field->divclass .= " ";
+				}
+
+				// Add a class to the field based on the type.
+				$field->divclass .= "pmpro_form_field pmpro_form_field-" . $field->type;
+				$field->class .= " pmpro_form_input-" . $field->type;
+
+				// Add the required class to field.
+				if ( ! empty( $args['show_required'] ) && ! empty( $field->required ) ) {
+					$field->divclass .= " pmpro_form_field-required";
+					$field->class .= " pmpro_form_input-required";
+				}
+
+				// Add the class to not show a field is required if set.
+				if ( ! empty( $args['show_required'] ) && ( empty( $field->showrequired ) || is_string( $field->showrequired ) ) ) {
+					$field->divclass .= " pmpro_form_field-hide-required";
+				}
+
+				// Run the class through the filter.
+				$field->divclass = pmpro_get_element_class( $field->divclass );
+				$field->class = pmpro_get_element_class( $field->class );
+
+				?>
+				<div id="<?php echo esc_attr( $field->id );?>_div" <?php if ( ! empty( $field->divclass ) ) { echo 'class="' . esc_attr( $field->divclass ) . '"'; } ?>>
+					<?php if(!empty($field->showmainlabel)) { ?>
+						<label class="<?php echo esc_attr( pmpro_get_element_class( 'pmpro_form_label' ) ); ?>" for="<?php echo esc_attr( $field->name );?>">
+							<?php echo wp_kses_post( $field->label );?>
+							<?php 
+								if ( ! empty( $field->required ) && ! empty( $field->showrequired ) && $field->showrequired === 'label' ) {
+								?><span class="<?php echo esc_attr( pmpro_get_element_class( 'pmpro_asterisk' ) ); ?>"> <abbr title="<?php esc_attr_e( 'Required Field' ,'paid-memberships-pro' ); ?>">*</abbr></span><?php
+								}
+							?>
+						</label>
+						<?php $field->display( $value ); ?>
+					<?php } else { ?>
+						<?php $field->display( $value ); ?>
+					<?php } ?>
+
+					<?php if(!empty($field->hint)) { ?>
+						<p class="<?php echo esc_attr( pmpro_get_element_class( 'pmpro_form_hint' ) ); ?>"><?php echo wp_kses_post( $field->hint );?></p>
+					<?php } ?>
+				</div>
+				<?php
+			} else {
+				?>
+				<tr id="<?php echo esc_attr( $field->id );?>_tr">
+					<th>
+						<?php if ( ! empty( $field->showmainlabel ) ) { ?>
+							<label for="<?php echo esc_attr($field->name);?>"><?php echo wp_kses_post( $field->label );?></label>
+						<?php } ?>
+					</th>
+					<td>
+						<?php
+							if(current_user_can("edit_user", $user_id))
+								$field->display($value);
+							else
+								echo "<div>" . wp_kses_post( $field->displayValue($value) ) . "</div>";
+						?>
+						<?php if(!empty($field->hint)) { ?>
+							<p class="description"><?php echo wp_kses_post( $field->hint );?></p>
+						<?php } ?>
+					</td>
+				</tr>
+				<?php
+			}
+		}
+
+		// Output the group footer.
+		echo $group_footer;
+	}
+
+	/**
+	 * Save fields in a specific context.
+	 *
+	 * @since TBD
+	 *
+	 * @param array $args The arguments for saving the fields.
+	 * @return bool True if the fields were saved, otherwise false.
+	 */
+	public function save_fields( $args = array() ) {
+		$default_args = array(
+			'scope' => 'profile', // The scope of the fields to save. Can be 'profile' or 'checkout'.
+			'user_id' => NULL, // The user ID to save the users for. If null, we are saving fields for the current user.
+		);
+		$args = wp_parse_args( $args, $default_args );
+
+		// Get the user ID if needed.
+		$user_id = empty( $args['user_id'] ) ? get_current_user_id() : $args['user_id'];
+
+		// Make sure the current user can edit this user.
+		if ( 'scope' == 'profile' && ! current_user_can( 'edit_user', $user_id ) ) {
+			return false;
+		}
+
+		// Get the fields to display.
+		$fields_to_display = $this->get_fields_to_display( $args );
+
+		// Save the fields.
+		foreach ( $fields_to_display as $field ) {
+			$field->save_field_for_user( $user_id );
+		}
+
+		return true;
+	}
 }
\ No newline at end of file
diff --git a/classes/class-pmpro-field.php b/classes/class-pmpro-field.php
index 9b8092096..dbf0603ac 100755
--- a/classes/class-pmpro-field.php
+++ b/classes/class-pmpro-field.php
@@ -10,7 +10,7 @@ class PMPro_Field {
 	 *
 	 * @var string
 	 */
-	public $name = '';
+	private $name = '';
 
 	/**
 	 * The type of field that this is.
@@ -19,7 +19,7 @@ class PMPro_Field {
 	 *
 	 * @var string
 	 */
-	public $type = '';
+	private $type = '';
 
 	/**
 	 * The meta key for this field.
@@ -30,7 +30,7 @@ class PMPro_Field {
 	 *
 	 * @var string
 	 */
-	public $meta_key = '';
+	private $meta_key = '';
 
 	/**
 	 * The label of the field.
@@ -41,7 +41,7 @@ class PMPro_Field {
 	 *
 	 * @var string
 	 */
-	public $label = '';
+	private $label = '';
 
 	/**
 	 * Whether the label should be shown.
@@ -50,7 +50,7 @@ class PMPro_Field {
 	 *
 	 * @var bool
 	 */
-	public $showmainlabel = true;
+	private $showmainlabel = true;
 
 	/**
 	 * A hint to be displayed with the field.
@@ -59,7 +59,7 @@ class PMPro_Field {
 	 *
 	 * @var string
 	 */
-	public $hint = '';
+	private $hint = '';
 
 	/**
 	 * The membership levels that this field should be displayed for.
@@ -68,7 +68,7 @@ class PMPro_Field {
 	 *
 	 * @var array
 	 */
-	public $levels = array();
+	private $levels = array();
 
 	/**
 	 * Whether the field is required.
@@ -77,7 +77,7 @@ class PMPro_Field {
 	 *
 	 * @var bool
 	 */
-	public $required = false;
+	private $required = false;
 
 	/**
 	 * Whether the field should be shown as required if $required is set to true.
@@ -86,7 +86,7 @@ class PMPro_Field {
 	 *
 	 * @var bool
 	 */
-	public $showrequired = true;
+	private $showrequired = true;
 
 	/**
 	 * Where this field should be shown.
@@ -97,7 +97,7 @@ class PMPro_Field {
 	 *
 	 * @var mixed
 	 */
-	public $profile = true;
+	private $profile = true;
 
 	/**
 	 * Whether the field is readonly.
@@ -106,7 +106,7 @@ class PMPro_Field {
 	 *
 	 * @var bool
 	 */
-	public $readonly = false;
+	private $readonly = false;
 
 	/**
 	 * Array to define conditions when a field should be shown or hidden.
@@ -115,7 +115,7 @@ class PMPro_Field {
 	 *
 	 * @var array
 	 */
-	public $depends = array();
+	private $depends = array();
 
 	/**
 	 * Flag to determine if depends conditions should be ANDed or ORed together.
@@ -124,7 +124,7 @@ class PMPro_Field {
 	 *
 	 * @var bool
 	 */
-	public $depends_or = false;
+	private $depends_or = false;
 
 	/**
 	 * Whether the field value should be sanitized before saving.
@@ -133,7 +133,7 @@ class PMPro_Field {
 	 *
 	 * @var bool
 	 */
-	public $sanitize = true;
+	private $sanitize = true;
 
 	/**
 	 * The ID to show for the field.
@@ -142,7 +142,7 @@ class PMPro_Field {
 	 *
 	 * @var string
 	 */
-	public $id = '';
+	private $id = '';
 
 	/**
 	 * Class for the input field.
@@ -151,7 +151,7 @@ class PMPro_Field {
 	 *
 	 * @var string
 	 */
-	public $class = '';
+	private $class = '';
 
 	/**
 	 * Class for the div wrapper for the input field.
@@ -160,7 +160,7 @@ class PMPro_Field {
 	 *
 	 * @var string
 	 */
-	public $divclass = '';
+	private $divclass = '';
 
 	/**
 	 * Whether this field should be included in a members list CSV export.
@@ -169,7 +169,7 @@ class PMPro_Field {
 	 *
 	 * @var bool
 	 */
-	public $memberslistcsv = false;
+	private $memberslistcsv = false;
 
 	/**
 	 * The save function that should be used for this field.
@@ -180,7 +180,7 @@ class PMPro_Field {
 	 *
 	 * @var callable
 	 */
-	public $save_function = null;
+	private $save_function = null;
 
 	/**
 	 * Whether this field should be shown when adding a member using
@@ -190,7 +190,7 @@ class PMPro_Field {
 	 *
 	 * @var bool
 	 */
-	public $addmember = false;
+	private $addmember = false;
 
 	/**
 	 * The size attribute when using a text input field type.
@@ -199,7 +199,7 @@ class PMPro_Field {
 	 *
 	 * @var int
 	 */
-	public $size = 30;
+	private $size = 30;
 
 	/**
 	 * The number of rows to show when using a textarea field type.
@@ -208,7 +208,7 @@ class PMPro_Field {
 	 *
 	 * @var int
 	 */
-	public $rows = 5;
+	private $rows = 5;
 
 	/**
 	 * The number of columns to show when using a textarea field type.
@@ -217,7 +217,7 @@ class PMPro_Field {
 	 *
 	 * @var int
 	 */
-	public $cols = 80;
+	private $cols = 80;
 
 	/**
 	 * The options for a select, select2, multiselect, checkbox_grouped, or radio field type.
@@ -226,7 +226,7 @@ class PMPro_Field {
 	 *
 	 * @var array
 	 */
-	public $options = array();
+	private $options = array();
 
 	/**
 	 * Whether multiple options should be selectable when using a select, select2, or multiselect field type.
@@ -235,7 +235,7 @@ class PMPro_Field {
 	 *
 	 * @var bool
 	 */
-	public $multiple = false;
+	private $multiple = false;
 
 	/**
 	 * The text to show next to a checkbox when using a checkbox field type.
@@ -244,7 +244,7 @@ class PMPro_Field {
 	 *
 	 * @var string
 	 */
-	public $text = '';
+	private $text = '';
 
 	/**
 	 * The HTML to show for an HTML field type.
@@ -253,7 +253,7 @@ class PMPro_Field {
 	 *
 	 * @var string
 	 */
-	public $html = '';
+	private $html = '';
 
 	/**
 	 * The default value for a field.
@@ -262,7 +262,7 @@ class PMPro_Field {
 	 *
 	 * @var string
 	 */
-	public $default = '';
+	private $default = '';
 
 	/**
 	 * File upload types.
@@ -272,7 +272,7 @@ class PMPro_Field {
 	 * @var string
 	 *
 	 */
-	public $allowed_file_types = '';
+	private $allowed_file_types = '';
 
 	/**
 	 * File upload limit
@@ -281,7 +281,7 @@ class PMPro_Field {
 	 * 
 	 * @var int
 	 */
-	public $max_file_size = '';
+	private $max_file_size = '';
 
 	function __construct($name = NULL, $type = NULL, $attr = NULL) {
 		if ( ! empty( $name ) )
@@ -290,6 +290,137 @@ function __construct($name = NULL, $type = NULL, $attr = NULL) {
 			return true;
 	}
 
+	/**
+	 * Magic getter to allow reading private class properties.
+	 *
+	 * @param string $name The property name.
+	 * @return mixed The property value.
+	 */
+	function __get( $name ) {
+		if ( isset( $this->$name ) ) {
+			if ( ! $this->is_valid_property( $name ) ) {
+				_doing_it_wrong( __FUNCTION__, sprintf( esc_html__( 'The property %s is not valid for the field type %s.', 'paid-memberships-pro' ), $name, $this->type ), 'TBD' );
+			}
+			return $this->$name;
+		} else {
+			_doing_it_wrong( __FUNCTION__, sprintf( esc_html__( 'The property %s does not exist.', 'paid-memberships-pro' ), $name ), 'TBD' );
+		}
+
+		return null;
+	}
+
+	/**
+	 * Magic setter to allow setting private class properties and
+	 * throwing warnings when we want to phase out a property.
+	 *
+	 * @param string $name The property name.
+	 * @param mixed $value The property value.
+	 */
+	function __set( $name, $value ) {
+		if ( 'type' === $name ) {
+			_doing_it_wrong( __FUNCTION__, esc_html__( 'PMPro_Field properties should not be modified directly and may break in a future version. Instead, create a new PMPro_Field object.', 'paid-memberships-pro' ), 'TBD' );
+		}
+
+		$this->$name = $value;
+	}
+	
+	/**
+	 * Magic isset to check if a private class property is set.
+	 *
+	 * @param string $name The property name.
+	 * @return bool Whether the property is set.
+	 */
+	function __isset( $name ) {
+		return isset( $this->$name );
+	}
+
+	/**
+	 * Magic __call to allow calling private class methods and throwing warnings
+	 * when we want to phase out a method.
+	 */
+	function __call( $name, $arguments ) {
+		switch( $name ) {
+			case 'set':
+				_doing_it_wrong( __FUNCTION__, sprintf( esc_html__( 'The method %s of PMPro_Field has become private and will not be available in a future version. Instead, use the $args property of the constructor when creating a new PMPro_Field object.', 'paid-memberships-pro' ), $name ), 'TBD' );
+				break;
+			case 'saveUsersTable':
+			case 'saveTermRelationshipsTable':
+			case 'saveFile':
+				_doing_it_wrong( __FUNCTION__, sprintf( esc_html__( 'The method %s of PMPro_Field has become private and will not be available in a future version. Instead, use the save_field_for_user method of the PMPro_Field object.', 'paid-memberships-pro' ), $name ), 'TBD' );
+				break;
+			case 'getHTML':
+			case 'getDependenciesJS':
+				_doing_it_wrong( __FUNCTION__, sprintf( esc_html__( 'The method %s of PMPro_Field has become private and will not be available in a future version. Instead, use the display() method of the PMPro_Field object.', 'paid-memberships-pro' ), $name ), 'TBD' );
+				break;
+			default:
+				_doing_it_wrong( __FUNCTION__, sprintf( esc_html__( 'The method %s of PMPro_Field has become private and will not be available in a future version.', 'paid-memberships-pro' ), $name ), 'TBD' );
+				break;
+		}
+		return call_user_func( array( $this, $name ), $arguments );
+	}
+
+	/**
+	 * Check if a property should be present for the current field type.
+	 *
+	 * @since TBD
+	 *
+	 * @param string $property The property to check.
+	 * return bool Whether the property is valid for the field type.
+	 */
+	private function is_valid_property( $property ) {
+		switch ( $property ) {
+			case 'name':
+			case 'type':
+			case 'meta_key':
+			case 'label':
+			case 'showmainlabel':
+			case 'hint':
+			case 'levels':
+			case 'required':
+			case 'showrequired':
+			case 'profile':
+			case 'readonly':
+			case 'depends':
+			case 'depends_or':
+			case 'sanitize':
+			case 'id':
+			case 'class':
+			case 'divclass':
+			case 'memberslistcsv':
+			case 'save_function':
+			case 'addmember':
+			case 'default':
+				return true;
+				break;
+			case 'size':
+				return in_array( $this->type, array( 'text', 'number' ) );
+				break;
+			case 'rows':
+			case 'cols':
+				return 'textarea' === $this->type;
+				break;
+			case 'options':
+				return in_array( $this->type, array( 'select', 'multiselect', 'select2', 'radio', 'checkbox_grouped' ) );
+				break;
+			case 'multiple':
+				return in_array( $this->type, array( 'select', 'select2', 'multiselect' ) );
+				break;
+			case 'text':
+				return 'checkbox' === $this->type;
+				break;
+			case 'html':
+				return 'html' === $this->type;
+				break;
+			case 'allowed_file_types':
+			case 'max_file_size':
+				return 'file' === $this->type;
+				break;
+			default:
+				return false;
+				break;
+		}
+	}
+
 	/*
 		setup field based on passed values
 		attr is array of one or more of the following:
@@ -300,7 +431,7 @@ function __construct($name = NULL, $type = NULL, $attr = NULL) {
 		- just_profile = bool (not required. true means only show field in profile)
 		- class = string (class to add to html element)
 	*/
-	function set($name, $type, $attr = array())
+	private function set($name, $type, $attr = array())
 	{
 		$this->name = $name;
 		$this->type = $type;
@@ -468,8 +599,86 @@ function set($name, $type, $attr = array())
 		return true;
 	}
 
+	/**
+	 * Get the field value from $_REQEUST or $_SESSION.
+	 * The value will be sanitized if the field has the sanitize property set to true.
+	 *
+	 * @since TBD
+	 *
+	 * @return mixed The value of the field or null if not found.
+	 */
+	function get_value_from_request() {
+		if ( isset( $_REQUEST[ $this->name ] ) ) {
+			$value = $_REQUEST[$this->name];
+		} elseif ( isset( $_REQUEST[ $this->name . '_checkbox' ] ) && $this->type == 'checkbox' ) {
+			// Empty checkbox.
+			$value = 0;
+		} elseif ( ! empty( $_REQUEST[ $this->name . '_checkbox' ] ) && in_array( $this->type, array( 'checkbox_grouped', 'select2' ) ) )	{
+			// Empty group checkboxes or select2.
+			$value = array();
+		} elseif ( isset( $_FILES[$this->name] ) && $this->type == 'file' ) {
+			// File field.
+			$value = $_FILES[$this->name]['name'];
+		}  elseif ( isset( $_SESSION[$this->name] ) ) {
+			// Value stored in session.
+			if ( is_array( $_SESSION[$this->name] ) && isset( $_SESSION[$this->name]['name'] ) ) {
+				// File field in session.
+				$_FILES[$this->name] = $_SESSION[$this->name];
+				$value = $_SESSION[$this->name]['name'];
+			} else {
+				// Other field in session.
+				$value = $_SESSION[$this->name];
+			}
+
+			// Clean up session.
+			unset($_SESSION[$this->name]);
+		} else {
+			// No value found.
+			return null;
+		}
+
+		// Sanitize the value if needed.
+		if ( ! empty( $field->sanitize ) ) {
+			if ( $this->type == 'textarea' ) {
+				$value = sanitize_textarea_field( $value );
+			} elseif ( is_array( $value ) ) {
+				$value = array_map( 'sanitize_text_field', $value );
+			} else {
+				$value = sanitize_text_field( $value );
+			}
+		}
+
+		return $value;
+	}
+
+	/**
+	 * Save the field for a user.
+	 *
+	 * @since TBD
+	 *
+	 * @param int $user_id The user ID to save the field for.
+	 */
+	function save_field_for_user( int $user_id ) {
+		// Get the value of the field.
+		$value = $this->get_value_from_request();
+
+		// If field was not submitted, bail.
+		if ( null === $value ) {
+			return;
+		}
+
+		// Check if we have a save function.
+		if ( ! empty( $this->save_function ) ) {
+			// Call the save function.
+			call_user_func( $this->save_function, $user_id, $this->name, $value, $this );
+		} else {
+			// Save the value to usermeta.
+			update_user_meta($user_id, $this->meta_key, $value);
+		}
+	}
+
 	// Save function for users table field.
-	function saveUsersTable( $user_id, $name, $value ) {
+	private function saveUsersTable( $user_id, $name, $value ) {
 		// Special sanitization needed for certain user fields.
 		if ( $name === 'user_url' ) {
 			$value = esc_url_raw( $value );
@@ -483,7 +692,7 @@ function saveUsersTable( $user_id, $name, $value ) {
 	}
 
 	// Save function for user taxonomy field.
-	function saveTermRelationshipsTable( $user_id, $name, $value ) {
+	private function saveTermRelationshipsTable( $user_id, $name, $value ) {
 		// Get the taxonomy to save for.
 		if ( isset( $this->taxonomy ) ) {
 			$taxonomy = $this->taxonomy;
@@ -512,7 +721,7 @@ function saveTermRelationshipsTable( $user_id, $name, $value ) {
 	}
 
 	//save function for files
-	function saveFile($user_id, $name, $value)
+	private function saveFile($user_id, $name, $value)
 	{
 		//setup some vars
 		$user = get_userdata($user_id);
@@ -649,15 +858,17 @@ function saveFile($user_id, $name, $value)
 		update_user_meta($user_id, $meta_key, $file_meta_value_array );
 	}
 
-	//echo the HTML for the field
-	function display($value = NULL)
-	{
-		echo $this->getHTML($value); //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
+	/**
+	 * Display the field.
+	 */
+	function display( $value = NULL ) {
+		echo $this->getHTML( $value ); //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
+		$this->getDependenciesJS();
 		return;
 	}
 
 	//get HTML for the field
-	function getHTML($value = "")
+	private function getHTML($value = "")
 	{
 		// Vars to store HTML to be added to the beginning or end.
 		$r_beginning = '';
@@ -934,35 +1145,32 @@ function getHTML($value = "")
 		{
 			$r = '';
 
-			//old value
-			if(is_user_logged_in())
-			{
-				global $current_user;
-				$old_value = get_user_meta($current_user->ID, $this->meta_key, true);
-				if(!empty($old_value))
-					$r .= '<input type="hidden" name="' . $this->name . '_old" value="' . esc_attr($old_value['filename']) . '" />';
-			}
-
 			// Show the existing file with a preview and allow user to delete or replace.
-			if ( ! empty( $value ) ) {
+			if ( ! empty( $value ) && ( is_array( $value ) || ! empty( $this->file ) ) ) {
+				if ( is_array( $value ) ) {
+					$file = $value;
+				} elseif ( ! empty( $this->file ) ) {
+					// Legacy support for $this->file.
+					$file = $this->file;
+				}
 
 				// Show a preview of existing file if image type.
-				if ( ( ! empty( $this->preview ) ) && ! empty( $this->file['previewurl'] ) ) {
-					$filetype = wp_check_filetype( basename( $this->file['previewurl'] ), null );
+				if ( ( ! empty( $this->preview ) ) && ! empty( $file['previewurl'] ) ) {
+					$filetype = wp_check_filetype( basename( $file['previewurl'] ), null );
 					if ( $filetype && 0 === strpos( $filetype['type'], 'image/' ) ) {
-						$r_beginning .= '<div class="' . esc_attr( pmpro_get_element_class( 'pmpro_form_field-file-preview' ) ) . '"><img src="' . esc_url( $this->file['previewurl'] ) . '" alt="' . esc_attr( basename($value) ) . '" /></div>';
+						$r_beginning .= '<div class="' . esc_attr( pmpro_get_element_class( 'pmpro_form_field-file-preview' ) ) . '"><img src="' . esc_url( $file['previewurl'] ) . '" alt="' . esc_attr( basename($file['filename']) ) . '" /></div>';
 					}
 				}
 
-				if( ! empty( $this->file['fullurl'] ) ) {
-					$r_beginning .= '<div class="pmpro_form_field-file-name pmpro_file_' . esc_attr( $this->name ) . '_name">' . sprintf(__('Current File: %s', 'paid-memberships-pro' ), '<a target="_blank" href="' . esc_url( $this->file['fullurl'] ) . '">' . esc_html( basename($value) ) . '</a>' ) . '</div>';
-				} else {
+				if( ! empty( $file['fullurl'] ) ) {
+					$r_beginning .= '<div class="pmpro_form_field-file-name pmpro_file_' . esc_attr( $this->name ) . '_name">' . sprintf(__('Current File: %s', 'paid-memberships-pro' ), '<a target="_blank" href="' . esc_url( $file['fullurl'] ) . '">' . esc_html( basename($file['filename']) ) . '</a>' ) . '</div>';
+				} elseif( is_string( $value ) ) {
 					$r_beginning .= sprintf(__('Current File: %s', 'paid-memberships-pro' ), basename($value) );
 				}
 
 				$r_beginning .= '<div class="pmpro_form_field-file-actions">';
 				// Allow user to delete the uploaded file if we know the full location. 
-				if ( ( ! empty( $this->allow_delete ) ) && ! empty( $this->file['fullurl'] ) ) {
+				if ( ( ! empty( $this->allow_delete ) ) && ! empty( $file['fullurl'] ) ) {
 					// Check whether the current user can delete the uploaded file based on the field attribute 'allow_delete'.
 					if ( $this->allow_delete === true || 
 						( $this->allow_delete === 'admins' || $this->allow_delete === 'only_admin' && current_user_can( 'manage_options' ) )
@@ -977,48 +1185,52 @@ function getHTML($value = "")
 					$r_beginning .= '<input id="pmpro_delete_file_' . esc_attr( $this->name ) . '_field" name="pmpro_delete_file_' . esc_attr( $this->name ) . '_field" type="hidden" value="0" />';
 				}
 				$r_beginning .= '</div>';
-			}
+				//include script to change enctype of the form and allow deletion
+				$r .= '
+				<script>
+					jQuery(document).ready(function() {
+						jQuery("#pmpro_delete_file_' . esc_attr( $this->name ) . '_button").on("click",function(){
+							jQuery("#pmpro_delete_file_' . esc_attr( $this->name ) . '_field").val("' . esc_attr( basename( $file['filename'] ) ) . '");
+							jQuery(".pmpro_file_' . esc_attr( $this->name ) . '_name").css("text-decoration", "line-through");
+							jQuery("#pmpro_cancel_change_file_' . esc_attr( $this->name ) . '_button").show();
+							jQuery("#pmpro_delete_file_' . esc_attr( $this->name ) . '_button").hide();
+							jQuery("#pmpro_replace_file_' . esc_attr( $this->name ) . '_button").hide();
+							jQuery("#pmpro_file_' . esc_attr( $this->id ) . '_upload").hide();
+						});
 
-			//include script to change enctype of the form and allow deletion
-			$r .= '
-			<script>
-				jQuery(document).ready(function() {
-					jQuery("#' . esc_attr( $this->id ) . '").closest("form").attr("enctype", "multipart/form-data");
-
-					jQuery("#pmpro_delete_file_' . esc_attr( $this->name ) . '_button").on("click",function(){
-						jQuery("#pmpro_delete_file_' . esc_attr( $this->name ) . '_field").val("' . esc_attr( basename($value) ) . '");
-						jQuery(".pmpro_file_' . esc_attr( $this->name ) . '_name").css("text-decoration", "line-through");
-						jQuery("#pmpro_cancel_change_file_' . esc_attr( $this->name ) . '_button").show();
-						jQuery("#pmpro_delete_file_' . esc_attr( $this->name ) . '_button").hide();
-						jQuery("#pmpro_replace_file_' . esc_attr( $this->name ) . '_button").hide();
-						jQuery("#pmpro_file_' . esc_attr( $this->id ) . '_upload").hide();
-					});
+						jQuery("#pmpro_replace_file_' . esc_attr( $this->name ) . '_button").on("click",function(){
+							jQuery("#pmpro_delete_file_' . esc_attr( $this->name ) . '_field").val("' . esc_attr( basename( $file['filename'] ) ) . '");
+							jQuery(".pmpro_file_' . esc_attr( $this->name ) . '_name").css("text-decoration", "line-through");
+							jQuery("#pmpro_cancel_change_file_' . esc_attr( $this->name ) . '_button").show();
+							jQuery("#pmpro_delete_file_' . esc_attr( $this->name ) . '_button").hide();
+							jQuery("#pmpro_replace_file_' . esc_attr( $this->name ) . '_button").hide();
+							jQuery("#pmpro_file_' . esc_attr( $this->id ) . '_upload").show();
+						});
 
-					jQuery("#pmpro_replace_file_' . esc_attr( $this->name ) . '_button").on("click",function(){
-						jQuery("#pmpro_delete_file_' . esc_attr( $this->name ) . '_field").val("' . esc_attr( basename($value) ) . '");
-						jQuery(".pmpro_file_' . esc_attr( $this->name ) . '_name").css("text-decoration", "line-through");
-						jQuery("#pmpro_cancel_change_file_' . esc_attr( $this->name ) . '_button").show();
-						jQuery("#pmpro_delete_file_' . esc_attr( $this->name ) . '_button").hide();
-						jQuery("#pmpro_replace_file_' . esc_attr( $this->name ) . '_button").hide();
-						jQuery("#pmpro_file_' . esc_attr( $this->id ) . '_upload").show();
-					});
+						jQuery("#pmpro_cancel_change_file_' . esc_attr( $this->name ) . '_button").on("click",function(){
+							jQuery("#pmpro_delete_file_' . esc_attr( $this->name ) . '_field").val(0);
+							// set file input field to empty.
+							jQuery("#' . esc_attr( $this->id ) . '").val("");
+							jQuery(".pmpro_file_' . esc_attr( $this->name ) . '_name").css("text-decoration", "none");
+							jQuery("#pmpro_delete_file_' . esc_attr( $this->name ) . '_button").show();
+							jQuery("#pmpro_replace_file_' . esc_attr( $this->name ) . '_button").show();
+							jQuery("#pmpro_cancel_change_file_' . esc_attr( $this->name ) . '_button").hide();
+							jQuery("#pmpro_file_' . esc_attr( $this->id ) . '_upload").hide();
+						});
 
-					jQuery("#pmpro_cancel_change_file_' . esc_attr( $this->name ) . '_button").on("click",function(){
-						jQuery("#pmpro_delete_file_' . esc_attr( $this->name ) . '_field").val(0);
-						// set file input field to empty.
-						jQuery("#' . esc_attr( $this->id ) . '").val("");
-						jQuery(".pmpro_file_' . esc_attr( $this->name ) . '_name").css("text-decoration", "none");
-						jQuery("#pmpro_delete_file_' . esc_attr( $this->name ) . '_button").show();
-						jQuery("#pmpro_replace_file_' . esc_attr( $this->name ) . '_button").show();
-						jQuery("#pmpro_cancel_change_file_' . esc_attr( $this->name ) . '_button").hide();
-						jQuery("#pmpro_file_' . esc_attr( $this->id ) . '_upload").hide();
 					});
+				</script>
+				';
+			}
 
-				});
-			</script>
-			';
-
-			$r .= '<div id="pmpro_file_' . esc_attr( $this->id ) . '_upload" class="' . esc_attr( pmpro_get_element_class( 'pmpro_form_field-file-upload' ) ) . '" ' . (empty($value) ? '' : 'style="display: none;"') . '>';
+			$r .= '
+				<script>
+					jQuery(document).ready(function() {
+						jQuery("#' . esc_attr( $this->id ) . '").closest("form").attr("enctype", "multipart/form-data");
+					});
+				</script>
+				';
+			$r .= '<div id="pmpro_file_' . esc_attr( $this->id ) . '_upload" class="' . esc_attr( pmpro_get_element_class( 'pmpro_form_field-file-upload' ) ) . '" ' . ((empty($value) || is_string($value)) ? '' : 'style="display: none;"') . '>';
 			$r .= '<input type="file" id="' . esc_attr( $this->id ) . '" ';
 			
 			if ( ! empty( $this->allowed_file_types ) ) {
@@ -1164,7 +1376,7 @@ function getHTMLAttributes() {
 		return $html;
 	}
 
-	function getDependenciesJS()
+	private function getDependenciesJS()
 	{
 		global $pmpro_user_fields;
 		//dependencies
@@ -1172,27 +1384,44 @@ function getDependenciesJS()
 		{
 			//build the checks
 			$checks_escaped = array();
+			$binds = array();
 			foreach($this->depends as $check)
 			{
 				if(!empty($check['id']))
 				{
 					// If checking checkbox_grouped, need to update the $check['id'] with index of option.
 					$field_id = $check['id'];
-					$depends_checkout_box = PMPro_Field::get_checkout_box_name_for_field( $field_id );
-					if ( empty( $depends_checkout_box ) ) {
+					$depends_field = PMPro_Field_Group::get_field( $field_id );
+					if ( empty( $depends_field ) ) {
 						continue;
 					}
-					foreach ( $pmpro_user_fields[ $depends_checkout_box ] as $field ) {
-						if ( $field->type === 'checkbox_grouped' && $field->name === $field_id && ! empty( $field->options ) ) {
-							$field_id = $field_id . '_' . intval( array_search( $check['value'], array_keys( $field->options ) )+1 );
-						}
+				
+					$depends_field_group = PMPro_Field_Group::get_group_for_field( $depends_field );
+					if ( empty( $depends_field_group ) ) {
+						continue;
 					}
 
-					$checks_escaped[] = "((jQuery('#" . esc_html( $field_id ) ."')".".is(':checkbox')) "
-					 ."? jQuery('#" . esc_html( $field_id ) . ":checked').length > 0"
-					 .":(jQuery('#" . esc_html( $field_id ) . "').val() == " . json_encode($check['value']) . " || jQuery.inArray( jQuery('#" . esc_html( $field_id ) . "').val(), " . json_encode($check['value']) . ") > -1)) ||"."(jQuery(\"input:radio[name='". esc_html( $check['id'] ) ."']:checked\").val() == ".json_encode($check['value'])." || jQuery.inArray(".json_encode($check['value']).", jQuery(\"input:radio[name='". esc_html( $field_id ) ."']:checked\").val()) > -1)";
-
-					$binds[] = "#" . esc_html( $field_id ) .",input:radio[name=". esc_html( $field_id ) ."]";
+					// Let's simplify.
+					switch ( $depends_field->type ) {
+						case 'checkbox_grouped':
+							// Find an input with the name of the field and the value of the option, then check if it is selected.
+							// Don't use the ID.
+							$checks_escaped[] = "jQuery('input[name=\"" . esc_js( $field_id ) . "[]\"][value=" . esc_js( $check['value'] ) . "]:checked').length > 0";
+							$binds[] = "input[name=\"" . esc_js( $field_id ) . "[]\"]";
+							break;
+						case 'checkbox':
+							$checks_escaped[] = "jQuery('#" . esc_html( $field_id ) . "').is(':checked') == " . ( empty( $check['value'] ) ? 'false' : 'true' );
+							$binds[] = "#" . esc_html( $field_id );
+							break;
+						case 'radio':
+							$checks_escaped[] = "jQuery('#" . esc_html( $field_id ) . " input[type=radio][value=" . esc_js( $check['value'] ) . "]:checked').length > 0";
+							$binds[] = "#" . esc_html( $field_id ) . " input[type=radio][name=" . esc_js( $field_id ) . "]";
+							break;
+						default:
+							$checks_escaped[] = "jQuery('#" . esc_html( $field_id ) . "').val() == " . json_encode( $check['value'] ) . " || jQuery.inArray( jQuery('#" . esc_html( $field_id ) . "').val(), " . json_encode( $check['value'] ) . ") > -1";
+							$binds[] = "#" . esc_html( $field_id );
+							break;
+					}
 				}
 			}
 
@@ -1245,33 +1474,20 @@ function pmpro_<?php echo esc_html( $this->id );?>_hideshow() {
 		}
 	}
 
+	/**
+	 * Display the field at checkout.
+	 *
+	 * @deprecated TBD Use PMPro_Field_Group::display() instead.
+	 */
 	function displayAtCheckout()
 	{
+		_deprecated_function( __METHOD__, 'TBD', 'PMPro_Field_Group::display()' );
 		global $current_user;
 
-		if(isset($_REQUEST[$this->name])) {
-			$value = pmpro_sanitize( $_REQUEST[$this->name], $this );
-		} elseif(isset($_SESSION[$this->name])) {
-			//file or value?
-			if(is_array($_SESSION[$this->name]) && !empty($_SESSION[$this->name]['name']))
-			{
-				$_FILES[$this->name] = $_SESSION[$this->name];
-				$this->file = pmpro_sanitize( $_SESSION[$this->name]['name'], $this );
-				$value = pmpro_sanitize( $_SESSION[$this->name]['name'], $this );
-			} else {
-				$value = pmpro_sanitize( $_SESSION[$this->name], $this );
-			}
-		}
-		elseif(!empty($current_user->ID) && metadata_exists("user", $current_user->ID, $this->meta_key))
-		{
-			$meta = get_user_meta($current_user->ID, $this->meta_key, true);
-			if(is_array($meta) && !empty($meta['filename']))
-			{
-				$this->file = get_user_meta($current_user->ID, $this->meta_key, true);
-				$value = $this->file['filename'];
-			} else {
-				$value = $meta;
-			}
+		if( null !== $this->get_value_from_request() ) {
+			$value = $this->get_value_from_request();
+		} elseif(!empty($current_user->ID) && metadata_exists("user", $current_user->ID, $this->meta_key)) {
+			$value = get_user_meta($current_user->ID, $this->meta_key, true);
 		} elseif ( ! empty( $current_user->ID ) ) {
 			$userdata = get_userdata( $current_user->ID );
 			if ( ! empty( $userdata->{$this->name} ) ) {
@@ -1333,23 +1549,18 @@ function displayAtCheckout()
 			<?php } ?>
 		</div>
 		<?php
-
-		$this->getDependenciesJS();
 	}
 
+	/**
+	 * @deprecated TBD Use PMPro_Field_Group::display() instead.
+	 */
 	function displayInProfile($user_id, $edit = NULL)
 	{
+		_deprecated_function( __METHOD__, 'TBD', 'PMPro_Field_Group::display()' );
 		global $current_user;
 		if(metadata_exists("user", $user_id, $this->meta_key))
 		{
-			$meta = get_user_meta($user_id, $this->meta_key, true);
-			if(is_array($meta) && !empty($meta['filename']))
-			{
-				$this->file = get_user_meta($user_id, $this->meta_key, true);
-				$value = $this->file['filename'];
-			}
-			else
-				$value = $meta;
+			$value = get_user_meta($user_id, $this->meta_key, true);
 		}
 		elseif(!empty($this->value))
 			$value = $this->value;
@@ -1375,8 +1586,6 @@ function displayInProfile($user_id, $edit = NULL)
 			</td>
 		</tr>
 		<?php
-
-		$this->getDependenciesJS();
 	}
 
 	/**
@@ -1456,28 +1665,34 @@ function displayValue( $value, $echo = true ) {
 	 * @param array $array The array to check if it is associative.
 	 * @return bool True if the array is associative, false otherwise.
 	 */
-	function is_assoc( $array ) {
+	private function is_assoc( $array ) {
 		if ( empty( $array ) ) {
 			return false;
 		}
 		return array_keys( $array ) !== range( 0, count( $array ) - 1) ;
 	}
 
+	/**
+	 * @deprecated TBD Use PMPro_Field_Group::get_group_for_field() instead.
+	 */
 	static function get_checkout_box_name_for_field( $field_name ) {
-		global $pmpro_user_fields;
-		foreach( $pmpro_user_fields as $checkout_box_name => $fields ) {
-			foreach($fields as $field) {
-				if( $field->name == $field_name ) {
-					return $checkout_box_name;
-				}
-			}
+		_deprecated_function( __METHOD__, 'TBD', 'PMPro_Field_Group::get_group_for_field()' );
+		$field = PMPro_Field_Group::get_field( $field_name );
+		if ( empty( $field ) ) {
+			return '';
 		}
-		return '';
+	
+		$field_group = PMPro_Field_Group::get_group_for_field( $field );
+		return $field_group ? $field_group->name : '';
 	}
 
+	/**
+	 * @deprecated TBD
+	 */
 	function was_present_on_checkout_page() {
+		_deprecated_function( __METHOD__, 'TBD' );
 		// Check if checkout box that field is in is on page.
-		$checkout_box = PMPro_Field::get_checkout_box_name_for_field( $this->name );
+		$checkout_box = PMPro_Field_Group::get_group_for_field( $this );
 		if ( empty( $checkout_box ) ) {
 			// Checkout box does not exist.
 			return false;
@@ -1489,7 +1704,7 @@ function was_present_on_checkout_page() {
 			'after_email',
 			'after_captcha',
 		);
-		if ( is_user_logged_in() && in_array( $checkout_box, $user_fields_locations ) ) {
+		if ( is_user_logged_in() && in_array( $checkout_box->name, $user_fields_locations ) ) {
 			// User is logged in and field is only for new users.
 			return false;
 		}
@@ -1532,23 +1747,70 @@ function was_present_on_checkout_page() {
 		return true;
 	}
 
+	/**
+	 * Check if the field was filled if needed.
+	 */
 	function was_filled_if_needed() {
-		// If field is never required or is not present on checkout page, return true.
-		if ( ! $this->required || ! $this->was_present_on_checkout_page() ) {
+		// If the field is not required, skip it.
+		if ( empty( $field->required ) ) {
 			return true;
 		}
 
-		// Return whether the field is filled.
+		// If this field has a 'depends` attribute, check if the field was actually shown.
+		if ( ! empty( $this->depends ) ) {
+			foreach ( $this->depends as $check ) {
+				// If the $check object isn't valid, skip it.
+				if ( ! isset( $check->id ) || ! isset( $check->value ) ) {
+					return true;
+				}
+
+				// Get the field to check.
+				$check_field = PMPro_Field_Group::get_field( $check->id );
+				if ( empty( $check_field ) ) {
+					// The check field doesn't exist, so this field wasn't shown.
+					return true;
+				}
+
+				// Get the value of the field.
+				$check_field_value = $check_field->get_value_from_request();
+				if ( null === $check_field_value ) {
+					// The check field wasn't submitted, so this field wasn't shown.
+					return true;
+				}
+
+				// If $check['value'] doesn't match the value of the field, skip this field.
+				if ( is_array( $check_field_value ) ) {
+					if ( ! in_array( $check->value, $check_field_value ) ) {
+						return true;
+					}
+				} else {
+					if ( $check->value !== $check_field_value ) {
+						return true;
+					}
+				}
+			}
+		}
+
+		// At this point, we know that the field needs to be filled.
+		$value = $this->get_value_from_request();
 		switch ( $this->type ) {
 			case 'text':
 			case 'textarea':
 			case 'number':
-				$filled = ( isset( $_REQUEST[$this->name] ) && '' !== trim( $_REQUEST[$this->name] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+				$filled = ( null !== $value && '' !== trim( $value ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+				break;
+			case 'file':
+				if ( ! empty( $_FILES[ $this->name ]['name'] ) ) {
+					$filled = true;
+				} elseif ( ! empty( get_user_meta( get_current_user_id(), $this->name, true ) ) && empty( $_REQUEST['pmpro_delete_file_' . $this->name . '_field'] ) ) {
+					$filled = true;
+				} else {
+					$filled = false;
+				}
 				break;
 			default:
-				$filled = ! ( empty( $_REQUEST[$this->name] ) && empty( $_FILES[$this->name]['name'] ) && empty( $_REQUEST[$this->name.'_old'] ) );
+				$filled = ! empty( $value );
 		}
-
 		return $filled;
 	}
 }
\ No newline at end of file
diff --git a/includes/fields.php b/includes/fields.php
index 312e8324c..d6d953ec4 100644
--- a/includes/fields.php
+++ b/includes/fields.php
@@ -24,7 +24,7 @@ function pmpro_is_field( $var ) {
  *	- before_submit_button
  *	- just_profile (make sure you set the profile attr of the field to true or admins)
  */
-function pmpro_add_user_field( $where, $field ) {	
+function pmpro_add_user_field( $where, $field ) {
     /**
      * Filter the group to add the field to.
      * 
@@ -155,8 +155,11 @@ function pmpro_add_user_taxonomy( $name, $name_plural ) {
 
 /**
  * Get a field group by name.
+ *
+ * @deprecated TBD Use PMPro_Field_Group::get instead.
  */
 function pmpro_get_field_group_by_name( $name ) {
+	_deprecated_function( __FUNCTION__, 'TBD', 'PMPro_Field_Group::get' );
 	return PMPro_Field_Group::get( $name );
 }
 
@@ -193,28 +196,58 @@ function pmpro_check_field_for_level( $field, $scope = 'default', $args = NULL )
 	return true;
 }
 
+/**
+ * Get a list of all fields that are only shown when creating a user at checkout.
+ */
+function pmpro_get_user_creation_field_groups() {
+	return array(
+		'after_username',
+		'after_password',
+		'after_email',
+	);
+}
+
 /**
  * Find fields in a group and display them at checkout.
+ * This function is only used for the following fields at checkout:
+ * - after_username
+ * - after_password
+ * - after_email
+ * - after_captcha
+ * - checkout_boxes
+ * - after_billing_fields
+ * - before_submit_button
+ * - after_tos_fields
  */
 function pmpro_display_fields_in_group( $group, $scope = 'checkout' ) {
-    // Get the field group.
-	$field_group = pmpro_get_field_group_by_name( $group );
-	$fields = $field_group->get_fields();
-	foreach( $fields as $field ) {
-		if ( ! pmpro_is_field( $field ) ) {
-			continue;
-		}
-		
-		if ( ! pmpro_check_field_for_level( $field ) ) {
-			continue;
-		}
-		
-		if ( $scope == 'checkout' ) {
-			if( ! isset( $field->profile ) || $field->profile !== 'only' && $field->profile !== 'only_admin' ) {
-				$field->displayAtCheckout();
-			}
-		}
+	$valid_groups = array(
+		'after_username',
+		'after_password',
+		'after_pricing_fields',
+		'after_email',
+		'after_captcha',
+		'after_billing_fields',
+		'before_submit_button',
+		'after_tos_fields',
+	);
+	if ( ! in_array( $group, $valid_groups ) ) {
+		_doing_it_wrong( __FUNCTION__, sprintf( __( 'The group %s should not be passed into %s. Use PMPro_Field_Group::display() instead.', 'paid-memberships-pro' ), $group, __FUNCTION__ ), '2.9.3' );
+	}
+	if ( $scope !== 'checkout' ) {
+		_doing_it_wrong( __FUNCTION__, sprintf( __( 'The scope %s should not be passed into %s. Use PMPro_Field_Group::display() instead.', 'paid-memberships-pro' ), $scope, __FUNCTION__ ), '2.9.3' );
 	}
+
+    // Get the field group.
+	$field_group = PMPro_Field_Group::get( $group );
+	$field_group->display(
+		array(
+			'markup' => 'div',
+			'scope' => 'checkout',
+			'show_group_label' => false,
+			'prefill_from_request' => true,
+			'show_required' => true,	
+		)
+	);
 }
 
 /**
@@ -249,6 +282,12 @@ function pmpro_checkout_boxes_fields() {
 	// Get all field groups.
 	$field_groups = PMPro_Field_Group::get_all();
 
+	$checkout_level = pmpro_getLevelAtCheckout();
+	$chekcout_level_id = ! empty( $checkout_level->id ) ? (int)$checkout_level->id : NULL;
+	if ( empty( $chekcout_level_id ) ) {
+		return;
+	}
+
 	// Cycle through the field groups.
 	foreach( $field_groups as $field_group_name => $field_group ) {
 		// If this is not a checkout box, skip it.
@@ -256,43 +295,14 @@ function pmpro_checkout_boxes_fields() {
 			continue;
 		}
 
-		// Get all the fields for this group.
-		$fields = $field_group->get_fields();
-
-		//how many fields to show at checkout?
-		$n = 0;
-		if(!empty( $fields ))
-			foreach( $fields as $field)
-				if(pmpro_is_field($field) && pmpro_check_field_for_level($field) && (!isset($field->profile) || (isset($field->profile) && $field->profile !== "only" && $field->profile !== "only_admin")))		$n++;
-
-		if($n > 0) {
-			?>
-			<fieldset id="pmpro_form_fieldset-<?php echo esc_attr( sanitize_title( $field_group_name ) ); ?>" class="<?php echo esc_attr( pmpro_get_element_class( 'pmpro_form_fieldset', 'pmpro_form_fieldset-' . sanitize_title( $field_group_name ) ) ); ?>">
-				<div class="<?php echo esc_attr( pmpro_get_element_class( 'pmpro_card' ) ); ?>">
-					<div class="<?php echo esc_attr( pmpro_get_element_class( 'pmpro_card_content' ) ); ?>">
-						<?php if ( ! empty( $cb->label ) ) { ?>
-							<legend class="<?php echo esc_attr( pmpro_get_element_class( 'pmpro_form_legend' ) ); ?>">
-								<h2 class="<?php echo esc_attr( pmpro_get_element_class( 'pmpro_form_heading pmpro_font-large' ) ); ?>"><?php echo wp_kses_post( $field_group->label ); ?></h2>
-							</legend>
-						<?php } ?>
-						<div class="<?php echo esc_attr( pmpro_get_element_class( 'pmpro_form_fields' ) ); ?>">
-							<?php if ( ! empty( $cb->description ) ) { ?>
-								<div class="<?php echo esc_attr( pmpro_get_element_class( 'pmpro_form_fields-description' ) ); ?>"><?php echo wp_kses_post( $field_group->description ); ?></div>
-							<?php } ?>
-
-							<?php
-								foreach($fields  as $field) {
-									if( pmpro_is_field($field) && pmpro_check_field_for_level($field) && (!isset($field->profile) || (isset($field->profile) && $field->profile !== "only" && $field->profile !== "only_admin"))) {
-										$field->displayAtCheckout();
-									}
-								}
-							?>
-						</div> <!-- end pmpro_form_fields -->
-					</div> <!-- end pmpro_card_content -->
-				</div> <!-- end pmpro_card -->
-			</fieldset> <!-- end pmpro_form_fieldset -->
-			<?php
-		}
+		$field_group->display(
+			array(
+				'markup' => 'card',
+				'scope' => 'checkout',
+				'prefill_from_request' => true,
+				'show_required' => true,
+			)
+		);
 	}
 }
 add_action( 'pmpro_checkout_boxes', 'pmpro_checkout_boxes_fields' );
@@ -322,88 +332,124 @@ function pmpro_checkout_after_tos_fields() {
 add_action( 'pmpro_checkout_before_submit_button', 'pmpro_checkout_after_tos_fields', 6 );
 
 /**
- * Update the fields at checkout.
+ * Update user creation fields at checkout after a user is created.
+ *
+ * @since TBD
+ *
+ * @param int $user_id The ID of the user that was created.
  */
-function pmpro_after_checkout_save_fields( $user_id, $order ) {
+function pmpro_checkout_before_user_auth_save_fields( $user_id ) {
+	// Loop through all the field groups.
+	$field_groups = PMPro_Field_Group::get_all();
+	$user_creation_field_groups = pmpro_get_user_creation_field_groups();
+	foreach($field_groups as $group_name => $group) {
+		if ( ! in_array( $group_name, $user_creation_field_groups ) ) {
+			continue;
+		}
+
+		// Save the fields.
+		$group->save_fields(
+			array(
+				'user_id' => $user_id,
+				'scope' => 'checkout',
+			)
+		);
+	}
+}
+add_action( 'pmpro_checkout_before_user_auth', 'pmpro_checkout_before_user_auth_save_fields' );
+
+/**
+ * Require required fields before creating a user at checkout.
+ */
+function pmpro_checkout_user_creation_checks_user_fields( $okay ) {
+	// Arrays to store fields that were required and missed.
+	$required = array();
+    $required_labels = array();
+
 	// Loop through all the field groups.
 	$field_groups = PMPro_Field_Group::get_all();
+	$user_creation_field_groups = pmpro_get_user_creation_field_groups();
 	foreach($field_groups as $group_name => $group) {
+		if ( ! in_array( $group_name, $user_creation_field_groups ) ) {
+			continue;
+		}
+
 		// Loop through all the fields in the group.
-		$fields = $group->get_fields();
+		$fields = $group->get_fields_to_display(
+			array(
+				'scope' => 'checkout',
+			)
+		);
 		foreach($fields as $field) {
-			if( ! pmpro_is_field( $field ) ) {
-				continue;
+			// If this is a file upload, check whether the file is allowed.
+			if ( isset( $_FILES[ $field->name ] ) && ! empty( $_FILES[$field->name]['name'] ) ) {
+				$upload_check = pmpro_check_upload( $field->name );
+				if ( is_wp_error( $upload_check ) ) {
+					pmpro_setMessage( $upload_check->get_error_message(), 'pmpro_error' );
+					return false;
+				}
 			}
-			
-			if ( ! pmpro_check_field_for_level( $field, "profile", $user_id ) ) {
+
+			// If the field was filled if needed, skip it.
+			if ( empty( $field->was_filled_if_needed() ) ) {
 				continue;
 			}
 
-			if(!empty($field->profile) && ($field->profile === "only" || $field->profile === "only_admin")) {
-				continue;	//wasn't shown at checkout
-			}
+			// The field was not filled.
+			$required[] = $field->name;
+			$required_labels[] = $field->label;
+		}
+	}
 
-			//assume no value
-			$value = NULL;
+	if(!empty($required))
+	{
+		$required = array_unique($required);
 
-			// Where are we getting the value from? We sanitize $value right after this.
-			// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
-			if(isset($_REQUEST[$field->name]))
-			{
-				//request
-				$value = $_REQUEST[$field->name];
-			}
-			elseif(isset($_REQUEST[$field->name . '_checkbox']) && $field->type == 'checkbox')
-			{
-				//unchecked checkbox
-				$value = 0;
-			}
-			elseif(!empty($_POST[$field->name . "_checkbox"]) && in_array( $field->type, array( 'checkbox', 'checkbox_grouped', 'select2' ) ) )	//handle unchecked checkboxes
-			{
-				//unchecked checkbox
-				$value = array();
-			}
-			elseif(isset($_SESSION[$field->name]))
-			{
-				//file or value?
-				if(is_array($_SESSION[$field->name]) && isset($_SESSION[$field->name]['name']))
-				{
-					//add to files global
-					$_FILES[$field->name] = $_SESSION[$field->name];
+		//add them to error fields
+		global $pmpro_error_fields;
+		$pmpro_error_fields = array_merge((array)$pmpro_error_fields, $required);
 
-					//set value to name
-					$value = $_SESSION[$field->name]['name'];
-				}
-				else
-				{
-					//session
-					$value = $_SESSION[$field->name];
-				}
+		if( count( $required ) == 1 ) {
+			$pmpro_msg = sprintf( __( 'The %s field is required.', 'paid-memberships-pro' ),  implode(", ", $required_labels) );
+			$pmpro_msgt = 'pmpro_error';
+		} else {
+			$pmpro_msg = sprintf( __( 'The %s fields are required.', 'paid-memberships-pro' ),  implode(", ", $required_labels) );
+			$pmpro_msgt = 'pmpro_error';
+		}
 
-				//unset
-				unset($_SESSION[$field->name]);
-			}
-			elseif(isset($_FILES[$field->name]))
-			{
-				//file
-				$value = $_FILES[$field->name]['name'];
-			}
-			// phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+		if($okay)
+			pmpro_setMessage($pmpro_msg, $pmpro_msgt);
 
-			//update user meta
-			if(isset($value))
-			{
-				if ( ! empty( $field->sanitize ) ) {
-					$value = pmpro_sanitize( $value, $field );
-				}
+		return false;
+	}
 
-				//callback?
-				if(!empty($field->save_function))
-					call_user_func( $field->save_function, $user_id, $field->name, $value, $order );
-				else
-					update_user_meta($user_id, $field->meta_key, $value);
-			}
+	//return whatever status was before
+	return $okay;
+}
+add_filter( 'pmpro_checkout_user_creation_checks', 'pmpro_checkout_user_creation_checks_user_fields' );
+
+/**
+ * Update the fields after a checkout is completed.
+ *
+ * @param int $user_id The ID of the user that was created.
+ * @param object $order The order object.
+ */
+function pmpro_after_checkout_save_fields( $user_id, $order ) {
+	// Loop through all the field groups.
+	$field_groups = PMPro_Field_Group::get_all();
+	$user_creation_field_groups = pmpro_get_user_creation_field_groups();
+	foreach($field_groups as $group_name => $group) {
+		if ( in_array( $group_name, $user_creation_field_groups ) ) {
+			continue;
 		}
+
+		// Save the fields.
+		$group->save_fields(
+			array(
+				'user_id' => $user_id,
+				'scope' => 'checkout',
+			)
+		);
 	}
 }
 add_action( 'pmpro_after_checkout', 'pmpro_after_checkout_save_fields', 10, 2 );
@@ -413,7 +459,7 @@ function pmpro_after_checkout_save_fields( $user_id, $order ) {
 add_action( 'pmpro_before_send_to_payfast', 'pmpro_after_checkout_save_fields', 20, 2 );	//for the Payfast Gateway Add On
 
 /**
- * Require required fields.
+ * Require required fields before creating an order at checkout.
  */
 function pmpro_registration_checks_for_user_fields( $okay ) {
 	// Arrays to store fields that were required and missed.
@@ -422,26 +468,19 @@ function pmpro_registration_checks_for_user_fields( $okay ) {
 
 	// Loop through all the field groups.
 	$field_groups = PMPro_Field_Group::get_all();
+	$user_creation_field_groups = pmpro_get_user_creation_field_groups();
 	foreach($field_groups as $group_name => $group) {
+		if ( in_array( $group_name, $user_creation_field_groups ) ) {
+			continue;
+		}
+
 		// Loop through all the fields in the group.
-		$fields = $group->get_fields();
+		$fields = $group->get_fields_to_display(
+			array(
+				'scope' => 'checkout',
+			)
+		);
 		foreach($fields as $field) {
-			//handle arrays
-			$field->name = preg_replace('/\[\]$/', '', $field->name);
-
-			//if the field is not for this level, skip it
-			if( ! pmpro_is_field( $field ) ) {
-				continue;
-			}
-			
-			if ( ! pmpro_check_field_for_level( $field ) ) {
-				continue;
-			}
-
-			if(!empty($field->profile) && ($field->profile === "only" || $field->profile === "only_admin")) {
-				continue;	//wasn't shown at checkout
-			}
-
 			// If this is a file upload, check whether the file is allowed.
 			if ( isset( $_FILES[ $field->name ] ) && ! empty( $_FILES[$field->name]['name'] ) ) {
 				$upload_check = pmpro_check_upload( $field->name );
@@ -451,10 +490,14 @@ function pmpro_registration_checks_for_user_fields( $okay ) {
 				}
 			}
 
-			if( ! $field->was_filled_if_needed() ) {
-				$required[] = $field->name;
-				$required_labels[] = $field->label;
+			// If the field was filled if needed, skip it.
+			if ( empty( $field->was_filled_if_needed() ) ) {
+				continue;
 			}
+
+			// The field was not filled.
+			$required[] = $field->name;
+			$required_labels[] = $field->label;
 		}
 	}
 
@@ -555,63 +598,41 @@ function pmpro_paypalexpress_session_vars_for_user_fields() {
 
 /**
  * Show user fields in profile.
+ *
+ * @deprecated TBD
  */
 function pmpro_show_user_fields_in_profile( $user, $withlocations = false ) {
-	//which fields are marked for the profile
-	$profile_fields = pmpro_get_user_fields_for_profile($user->ID, $withlocations);
-
-	//show the fields
-	if(!empty($profile_fields) && $withlocations)
-	{
-		foreach($profile_fields as $where => $fields)
-		{
-			$box = pmpro_get_field_group_by_name($where);
-
-			if ( !empty($box->label) ) {
-				?>
-				<h2><?php echo wp_kses_post( $box->label ); ?></h2>
-				<?php
-				if ( ! empty( $box->description ) ) {
-					?>
-					<p><?php echo wp_kses_post( $box->description ); ?></p>
-					<?php
-				}
-			}
-			?>
-			
-
-			<table class="form-table">
-			<?php
-			//cycle through groups
-			foreach($fields as $field)
-			{
-				if ( pmpro_is_field( $field ) )
-					$field->displayInProfile($user->ID);
-			}
-			?>
-			</table>
-			<?php
-		}
-	}
-	elseif(!empty($profile_fields))
-	{
-		?>
-		<table class="form-table">
-		<?php
-		//cycle through groups
-		foreach($profile_fields as $field)
-		{
-			if ( pmpro_is_field( $field ) ) {
-                $field->displayInProfile($user->ID);
-            }
-		}
-		?>
-		</table>
-		<?php
+	_deprecated_function( __FUNCTION__, 'TBD', 'pmpro_show_user_fields_in_profile_with_locations' );
+	if ( $withlocations ) {
+		return pmpro_show_user_fields_in_profile_with_locations( $user );
 	}
+	$groups = PMPro_Field_Group::get_all();
+	foreach( $groups as $group ) {
+		$group->display(
+			array(
+				'markup' => 'table',
+				'scope' => 'profile',
+				'show_group_label' => $withlocations,
+				'user_id' => $user->ID,
+			)
+		);
+	}	
 }
+
+/**
+ * Show user fields in the backend profile.
+ */
 function pmpro_show_user_fields_in_profile_with_locations( $user ) {
-	pmpro_show_user_fields_in_profile($user, true);
+	$groups = PMPro_Field_Group::get_all();
+	foreach( $groups as $group ) {
+		$group->display(
+			array(
+				'markup' => 'table',
+				'scope' => 'profile',
+				'user_id' => $user->ID,
+			)
+		);
+	}	
 }
 add_action( 'show_user_profile', 'pmpro_show_user_fields_in_profile_with_locations' );
 add_action( 'edit_user_profile', 'pmpro_show_user_fields_in_profile_with_locations' );
@@ -620,69 +641,43 @@ function pmpro_show_user_fields_in_profile_with_locations( $user ) {
  * Show Profile fields on the frontend "Member Profile Edit" page.
  *
  * @since 2.3
+ * @deprecated TBD
  */
 function pmpro_show_user_fields_in_frontend_profile( $user, $withlocations = false ) {
-	//which fields are marked for the profile
-	$profile_fields = pmpro_get_user_fields_for_profile($user->ID, $withlocations);
-
-	//show the fields
-	if ( ! empty( $profile_fields ) && $withlocations ) {
-		foreach( $profile_fields as $where => $fields ) {
-			$box = pmpro_get_field_group_by_name( $where );
-
-			// Only show on front-end if there are fields to be shown.
-			$show_fields = array();
-			foreach( $fields as $key => $field ) {
-				if ( pmpro_is_field( $field ) && $field->profile !== 'only_admin' && $field->profile !== 'admin' && $field->profile !== 'admins' ) {
-					$show_fields[] = $field;
-				}
-			}
-
-			// Bail if there are no fields to show on the front-end profile.
-			if ( empty( $show_fields ) ) {
-				continue;
-			}
-			?>
-			<div class="<?php echo esc_attr( pmpro_get_element_class( 'pmpro_spacer' ) ); ?>"></div>
-			<fieldset id="pmpro_form_fieldset-<?php echo esc_attr( sanitize_title( $where ) ); ?>" class="<?php echo esc_attr( pmpro_get_element_class( 'pmpro_form_fieldset', 'pmpro_form_fieldset-' . sanitize_title( $where ) ) ); ?>">
-				<?php if ( ! empty( $box->label ) ) { ?>
-					<legend class="<?php echo esc_attr( pmpro_get_element_class( 'pmpro_form_legend' ) ); ?>">
-						<h2 class="<?php echo esc_attr( pmpro_get_element_class( 'pmpro_form_heading pmpro_font-large' ) ); ?>"><?php echo wp_kses_post( $box->label ); ?></h2>
-					</legend>
-				<?php } ?>
-				<div class="<?php echo esc_attr( pmpro_get_element_class( 'pmpro_form_fields' ) ); ?>">
-					<?php if ( ! empty( $box->description ) ) { ?>
-						<div class="<?php echo esc_attr( pmpro_get_element_class( 'pmpro_form_fields-description' ) ); ?>"><?php echo wp_kses_post( $box->description ); ?></div>
-					<?php } ?>
-
-					<?php
-						 // Show fields.
-						foreach( $show_fields as $field ) {
-							$field->displayAtCheckout( $user->ID );
-						}
-					?>
-				</div> <!-- end pmpro_form_fields -->
-			</fieldset> <!-- end pmpro_form_fieldset -->
-			<?php
-		}
-	} elseif ( ! empty( $profile_fields ) ) { ?>
-		<fieldset class="<?php echo esc_attr( pmpro_get_element_class( 'pmpro_form_fieldset' ) ); ?>">
-			<div class="<?php echo esc_attr( pmpro_get_element_class( 'pmpro_form_fields' ) ); ?>">
-				<?php
-					// Cycle through groups.
-					foreach( $profile_fields as $field ) {
-						if ( pmpro_is_field( $field ) && $field->profile !== 'only_admin' ) {
-							$field->displayAtCheckout( $user->ID );
-						}
-					}
-				?>
-			</div> <!-- end pmpro_form_fields -->
-		</fieldset> <!-- end pmpro_form_fieldset -->
-		<?php
+	_deprecated_function( __FUNCTION__, 'TBD', 'pmpro_show_user_fields_in_frontend_profile_with_locations' );
+	if ( $withlocations ) {
+		return pmpro_show_user_fields_in_frontend_profile_with_locations( $user );
 	}
+
+	$groups = PMPro_Field_Group::get_all();
+	foreach( $groups as $group ) {
+		$group->display(
+			array(
+				'markup' => 'div',
+				'scope' => 'profile',
+				'show_group_label' => $withlocations,
+				'user_id' => $user->ID,
+			)
+		);
+	}	
 }
+
+/**
+ * Show Profile fields on the frontend "Member Profile Edit" page.
+ *
+ * @since 2.3
+ */
 function pmpro_show_user_fields_in_frontend_profile_with_locations( $user ) {
-	pmpro_show_user_fields_in_frontend_profile( $user, true );
+	$groups = PMPro_Field_Group::get_all();
+	foreach( $groups as $group ) {
+		$group->display(
+			array(
+				'markup' => 'div',
+				'scope' => 'profile',
+				'user_id' => $user->ID,
+			)
+		);
+	}	
 }
 add_action( 'pmpro_show_user_profile', 'pmpro_show_user_fields_in_frontend_profile_with_locations' );
 
@@ -709,24 +704,45 @@ function pmpro_add_member_admin_fields( $user = null, $user_id = null) {
 
 
     //show the fields
-    if(!empty($addmember_fields))
-    {
-        ?>
-            <?php
-            //cycle through groups
-            foreach($addmember_fields as $field)
-            {
-				if(empty($user_id) && !empty($user) && !empty($user->ID)) {
-					$user_id = $user->ID;
-				}
+    if(!empty($addmember_fields)) {
+		//cycle through groups
+		foreach($addmember_fields as $field)
+		{
+			if(empty($user_id) && !empty($user) && !empty($user->ID)) {
+				$user_id = $user->ID;
+			}
 
-		    		if( pmpro_is_field( $field ) ) {
-                        $field->displayInProfile($user_id);
-                    }
-					
-            }
-            ?>
-    <?php
+			if( ! pmpro_is_field( $field ) ) {
+				continue;
+			}
+
+			if(metadata_exists("user", $user_id, $field->meta_key))
+			{
+				$value = get_user_meta($user_id, $field->meta_key, true);
+			} else {
+				$value = "";
+			}
+			?>
+			<tr id="<?php echo esc_attr( $field->id );?>_tr">
+				<th>
+					<?php if ( ! empty( $field->showmainlabel ) ) { ?>
+						<label for="<?php echo esc_attr($field->name);?>"><?php echo wp_kses_post( $field->label );?></label>
+					<?php } ?>
+				</th>
+				<td>
+					<?php
+						if(current_user_can("edit_user", $user_id) && $field !== false)
+							$field->display($value);
+						else
+							echo "<div>" . wp_kses_post( $field->displayValue($value) ) . "</div>";
+					?>
+					<?php if(!empty($field->hint)) { ?>
+						<p class="description"><?php echo wp_kses_post( $field->hint );?></p>
+					<?php } ?>
+				</td>
+			</tr>
+			<?php	
+		}
     }
 }
 add_action( 'pmpro_add_member_fields', 'pmpro_add_member_admin_fields', 10, 2 );
@@ -780,41 +796,7 @@ function pmpro_add_member_admin_save_user_fields( $uid = null, $user = null ) {
         //cycle through fields
         foreach($addmember_fields as $field)
         {
-            if(pmpro_is_field($field) && isset($_POST[$field->name]) || isset($_FILES[$field->name]))
-            {
-	            // Sanitize by default, or not. Some fields may have custom save functions/etc.
-				// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
-				if ( ! empty( $field->sanitize ) && isset( $_POST[ $field->name ] ) ) {
-		            $value = pmpro_sanitize( $_POST[ $field->name ], $field );
-	            } elseif( isset($_POST[$field->name]) ) {
-	                $value = $_POST[ $field->name ];
-                } else {
-                	$value = $_FILES[$field->name];
-                }
-				// phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
-
-                //callback?
-                if(!empty($field->save_function))
-                    call_user_func($field->save_function, $user_id, $field->name, $value );
-                else
-                    update_user_meta($user_id, $field->meta_key, $value );
-            }
-            elseif(pmpro_is_field($field) && !empty($_POST[$field->name . "_checkbox"]) && $field->type == 'checkbox')	//handle unchecked checkboxes
-            {
-                //callback?
-                if(!empty($field->save_function))
-                    call_user_func($field->save_function, $user_id, $field->name, 0);
-                else
-                    update_user_meta($user_id, $field->meta_key, 0);
-			}
-			elseif(!empty($_POST[$field->name . "_checkbox"]) && in_array( $field->type, array( 'checkbox', 'checkbox_grouped', 'select2' ) ) )	//handle unchecked checkboxes
-			{
-				//callback?
-				if(!empty($field->save_function))
-					call_user_func($field->save_function, $user_id, $field->name, array());
-				else
-					update_user_meta($user_id, $field->meta_key, array());
-			}
+            $field->save_field_for_user( $user_id );
         }
     }
 }
@@ -845,41 +827,31 @@ function pmpro_get_user_fields_for_csv() {
 /**
  * Get user fields which are marked to show in the profile.
  * If a $user_id is passed in, get fields based on the user's level.
+ *
+ * @deprecated TBD Use PMPro_Field_Group::get_fields_to_display instead.
  */
 function pmpro_get_user_fields_for_profile( $user_id, $withlocations = false ) {
+	_deprecated_function( __FUNCTION__, 'TBD', 'PMPro_Field_Group::get_fields_to_display' );
 	$profile_fields = array();
 	// Loop through all the field groups.
 	$field_groups = PMPro_Field_Group::get_all();
 	foreach($field_groups as $group_name => $group) {
-		// Loop through all the fields in the group.
-		$fields = $group->get_fields();
-		foreach($fields as $field)
-		{
-			if( ! pmpro_is_field( $field ) ) {
-				continue;
-			}
-			
-			if ( ! pmpro_check_field_for_level( $field, "profile", $user_id ) ) {
-				continue;
-			}
+		// Get the fields to display.
+		$fields_to_display = $group->get_fields_to_display(
+			array(
+				'scope' => 'profile',
+				'user_id' => $user_id,
+			)
+		);
 
-			if(!empty($field->profile) && ($field->profile === "admins" || $field->profile === "admin" || $field->profile === "only_admin"))
-			{
-				if( current_user_can( 'manage_options' ) || current_user_can( 'pmpro_membership_manager' ) )
-				{
-					if($withlocations)
-						$profile_fields[$group_name][] = $field;
-					else
-						$profile_fields[] = $field;
-				}
-			}
-			elseif(!empty($field->profile))
-			{
-				if($withlocations)
-					$profile_fields[$group_name][] = $field;
-				else
-					$profile_fields[] = $field;
-			}
+		if ( empty( $fields_to_display ) ) {
+			continue;
+		}
+
+		if ( $withlocations ) {
+			$profile_fields[ $group_name ] = $fields_to_display;
+		} else {
+			$profile_fields = array_merge( $profile_fields, $fields_to_display );
 		}
 	}
 
@@ -902,54 +874,16 @@ function pmpro_save_user_fields_in_profile( $user_id )
 	if ( !current_user_can( 'edit_user', $user_id ) )
 		return false;
 
-	$profile_fields = pmpro_get_user_fields_for_profile($user_id);
-
-	//save our added fields in session while the user goes off to PayPal
-	if(!empty($profile_fields))
-	{
-		//cycle through fields
-		foreach($profile_fields as $field)
-		{
-            if( ! pmpro_is_field( $field ) ) {
-                continue;
-            }
-
-			if(isset($_POST[$field->name]) || isset($_FILES[$field->name]))
-			{
-				// Sanitize by default, or not. Some fields may have custom save functions/etc.
-				// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
-				if ( ! empty( $field->sanitize ) && isset( $_POST[ $field->name ] ) ) {
-					$value = pmpro_sanitize( $_POST[ $field->name ], $field );
-				} elseif( isset($_POST[$field->name]) ) {
-				    $value = $_POST[ $field->name ];
-                } else {
-                	$value = $_FILES[$field->name];
-                }
-				// phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
-
-				//callback?
-				if(!empty($field->save_function))
-					call_user_func($field->save_function, $user_id, $field->name, $value);
-				else
-					update_user_meta($user_id, $field->meta_key, $value);
-			}
-			elseif(!empty($_POST[$field->name . "_checkbox"]) && $field->type == 'checkbox')	//handle unchecked checkboxes
-			{
-				//callback?
-				if(!empty($field->save_function))
-					call_user_func($field->save_function, $user_id, $field->name, 0);
-				else
-					update_user_meta($user_id, $field->meta_key, 0);
-			}
-			elseif(!empty($_POST[$field->name . "_checkbox"]) && in_array( $field->type, array( 'checkbox', 'checkbox_grouped', 'select2' ) ) )	//handle unchecked checkboxes
-			{
-				//callback?
-				if(!empty($field->save_function))
-					call_user_func($field->save_function, $user_id, $field->name, array());
-				else
-					update_user_meta($user_id, $field->meta_key, array());
-			}
-		}
+	// Loop through all the field groups.
+	$field_groups = PMPro_Field_Group::get_all();
+	foreach($field_groups as $group_name => $group) {
+		// Save the fields.
+		$group->save_fields(
+			array(
+				'scope' => 'profile',
+				'user_id' => $user_id,
+			)
+		);
 	}
 }
 add_action( 'personal_options_update', 'pmpro_save_user_fields_in_profile' );
@@ -966,11 +900,8 @@ function pmpro_add_user_fields_to_email( $email ) {
 	if ( ! empty( $email ) && strpos( $email->template, "checkout" ) !== false && strpos( $email->template, "admin" ) !== false ) {
 		//get the user_id from the email
 		$user_id = $wpdb->get_var( "SELECT ID FROM $wpdb->users WHERE user_email = '" . esc_sql( $email->data['user_email'] ) . "' LIMIT 1" );
-		$level_id = empty( $email->data['membership_id'] ) ? null : intval( $email->data['membership_id'] );
 
 		if ( ! empty( $user_id ) ) {
-
-
 			//add to bottom of email
 			$field_groups = PMPro_Field_Group::get_all();
 			if ( ! empty( $field_groups ) ) {
@@ -979,37 +910,15 @@ function pmpro_add_user_fields_to_email( $email ) {
 				// Loop through all the field groups.
 				foreach( $field_groups as $group_name => $group ) {
 					// Loop through all the fields in the group.
-					$fields = $group->get_fields();
+					$fields = $group->get_fields_to_display(
+						array(
+							'scope' => 'checkout',
+							'user_id' => $user_id,
+						)
+					);
 					foreach( $fields as $field ) {
-
-						if ( ! pmpro_is_field( $field ) ) {
-							continue;
-						}
-
-						// If the field is showing only in the profile or to admins we can skip it.
-						if ( ! empty( $field->profile ) && ( $field->profile === "only" || $field->profile === "only_admin" ) ) {
-							continue;
-						}
-
-						// Let's make sure the field level ID's are the same as the one they checked out for.
-						if ( ! empty( $field->levels ) &&  ( empty( $level_id) || ! in_array( $level_id, $field->levels ) ) ) {
-							continue;
-						}
-
 						$fields_content .= "- " . esc_html( $field->label ) . ": ";
-						$value = get_user_meta( $user_id, $field->name, true);
-
-						// Get the label value for field types that have labels.
-						$value = pmpro_get_label_for_user_field_value( $field->name, $value );
-
-						if ( $field->type == "file" && is_array( $value ) && ! empty( $value['fullurl'] ) ) {
-							$fields_content .= pmpro_sanitize( $value['fullurl'], $field );
-						} elseif( is_array( $value  ) ) {
-							$fields_content .= implode(", ", pmpro_sanitize( $value, $field ) );
-						} else {
-							$fields_content .= pmpro_sanitize( $value, $field );
-						}
-
+						$fields_content .= $field->displayValue( get_user_meta( $user_id, $field->name, true), false );
 						$fields_content .= "<br />";
 						$added_field = true;
 					}
@@ -1060,32 +969,6 @@ function pmpro_csv_columns_for_user_fields( $user, $column ) {
 	}
 }
 
-/**
- * Delete old files in wp-content/uploads/pmpro-register-helper/tmp every day.
- */
-function pmpro_cron_delete_tmp() {
-	$upload_dir = wp_upload_dir();
-	$pmprorh_dir = $upload_dir['basedir'] . "/paid-memberships-pro/tmp/";
-
-	if(file_exists($pmprorh_dir) && $handle = opendir($pmprorh_dir))
-	{
-		while(false !== ($file = readdir($handle)))
-		{
-			$file = $pmprorh_dir . $file;
-			$filelastmodified = filemtime($file);
-			if(is_file($file) && (time() - $filelastmodified) > 3600)
-			{
-				unlink($file);
-			}
-		}
-
-		closedir($handle);
-	}
-
-	exit;
-}
-add_action( 'pmpro_cron_delete_tmp', 'pmpro_cron_delete_tmp' );
-
 /**
  * Get user fields from global.
  * @since 2.9.3
@@ -1104,349 +987,14 @@ function pmpro_get_user_fields() {
  * Get field group HTML for settings.
  */
 function pmpro_get_field_group_html( $group = null ) {
-    if ( ! empty( $group ) ) {
-        // Assume group stdClass in format we save to settings.
-        $group_name = $group->name;
-    	$group_show_checkout = $group->checkout;
-    	$group_show_profile = $group->profile;
-    	$group_description = $group->description;    	
-    	$group_levels = $group->levels;
-        $group_fields = $group->fields;
-    } else {
-        // Default group settings.
-        $group_name = '';
-    	$group_show_checkout = 'yes';
-    	$group_show_profile = 'yes';
-    	$group_description = '';    	
-    	$group_levels = array();
-        $group_fields = array();
-    }
-    
-    // Other vars
-	$levels = pmpro_sort_levels_by_order( pmpro_getAllLevels( true, true ) );
-
-    // Render field group HTML.
-    ?>
-    <div class="pmpro_userfield-group">
-        <div class="pmpro_userfield-group-header">
-            <div class="pmpro_userfield-group-buttons">
-                <button type="button" aria-disabled="false" class="pmpro_userfield-group-buttons-button pmpro_userfield-group-buttons-button-move-up" aria-label="<?php esc_attr_e( 'Move up', 'paid-memberships-pro' ); ?>">
-                    <span class="dashicons dashicons-arrow-up-alt2"></span>
-                </button>
-                <span class="pmpro_userfield-group-buttons-description"><?php esc_html_e( 'Move Group Up', 'paid-memberships-pro' ); ?></span>
-
-                <button type="button" aria-disabled="false" class="pmpro_userfield-group-buttons-button pmpro_userfield-group-buttons-button-move-down" aria-label="<?php esc_attr_e( 'Move down', 'paid-memberships-pro' ); ?>">
-                    <span class="dashicons dashicons-arrow-down-alt2"></span>
-                </button>
-                <span id="pmpro_userfield-group-buttons-description-2" class="pmpro_userfield-group-buttons-description"><?php esc_html_e( 'Move Group Down', 'paid-memberships-pro' ); ?></span>
-            </div> <!-- end pmpro_userfield-group-buttons -->
-            <h3>
-                <label>                    
-                    <?php esc_html_e( 'Group Name', 'paid-memberships-pro' ); ?>
-                    <input type="text" name="pmpro_userfields_group_name" placeholder="<?php esc_attr_e( 'Group Name', 'paid-memberships-pro' ); ?>" value="<?php echo esc_attr( $group_name ); ?>" />
-                </label>                
-            </h3>
-            <button type="button" aria-disabled="false" class="pmpro_userfield-group-buttons-button pmpro_userfield-group-buttons-button-toggle-group" aria-label="<?php esc_attr_e( 'Expand and Edit Group', 'paid-memberships-pro' ); ?>">
-                <span class="dashicons dashicons-arrow-up"></span>
-            </button>
-            <span class="pmpro_userfield-group-buttons-description"><?php esc_html_e( 'Expand and Edit Group', 'paid-memberships-pro' ); ?></span>
-        </div> <!-- end pmpro_userfield-group-header -->
-
-        <div class="pmpro_userfield-inside">
-			<div class="pmpro_userfield-field-settings">
-				
-				<div class="pmpro_userfield-field-setting">
-					<label>
-                        <?php esc_html_e( 'Show fields at checkout?', 'paid-memberships-pro' ); ?><br />
-    					<select name="pmpro_userfields_group_checkout">
-    						<option value="yes" <?php selected( $group_show_checkout, 'yes' ); ?>><?php esc_html_e( 'Yes', 'paid-memberships-pro' ); ?></option>
-    						<option value="no" <?php selected( $group_show_checkout, 'no' ); ?>><?php esc_html_e( 'No', 'paid-memberships-pro' ); ?></option>
-    					</select>
-                    </label>
-				</div> <!-- end pmpro_userfield-field-setting -->
-				
-				<div class="pmpro_userfield-field-setting">
-					<label>
-                        <?php esc_html_e( 'Show fields on user profile?', 'paid-memberships-pro' ); ?><br />
-                        <select name="pmpro_userfields_group_profile">
-    						<option value="yes" <?php selected( $group_show_profile, 'yes' ); ?>><?php esc_html_e( 'Yes', 'paid-memberships-pro' ); ?></option>
-    						<option value="admins" <?php selected( $group_show_profile, 'admins' ); ?>><?php esc_html_e( 'Yes (only admins)', 'paid-memberships-pro' ); ?></option>
-    						<option value="no" <?php selected( $group_show_profile, 'no' ); ?>><?php esc_html_e( 'No', 'paid-memberships-pro' ); ?></option>
-    					</select>
-                    </label>
-				</div> <!-- end pmpro_userfield-field-setting -->
-				
-				<div class="pmpro_userfield-field-setting">
-					<label>
-                        <?php esc_html_e( 'Description (optional, visible to users)', 'paid-memberships-pro' ); ?><br />
-					    <textarea name="pmpro_userfields_group_description"><?php echo esc_textarea( $group_description );?></textarea>
-                    </label>
-				</div> <!-- end pmpro_userfield-field-setting -->
-				
-				<div class="pmpro_userfield-field-setting">
-                    <?php esc_html_e( 'Restrict Fields for Membership Levels', 'paid-memberships-pro' ); ?><br />
-                    <div class="pmpro_checkbox_box" <?php if ( count( $levels ) > 3 ) { ?>style="height: 90px; overflow: auto;"<?php } ?>>
-						<?php foreach( $levels as $level ) { ?>
-							<div class="pmpro_clickable">
-                                <label>
-                                    <input type="checkbox" id="pmpro_userfields_group_membership_<?php echo esc_attr( $level->id); ?>" name="pmpro_userfields_group_membership[]" <?php checked( true, in_array( $level->id, $group_levels ) );?>>
-                                    <?php echo esc_html( $level->name ); ?>
-                                </label>
-                            </div>
-						<?php } ?>
-					</div>
-				</div> <!-- end pmpro_userfield-field-setting -->
-			
-			</div> <!-- end pmpro_userfield-field-settings -->
-			
-			<h3><?php esc_html_e( 'Manage Fields in This Group', 'paid-memberships-pro' ); ?></h3>
-			
-			<ul class="pmpro_userfield-group-thead">
-				<li class="pmpro_userfield-group-column-order"><?php esc_html_e( 'Order', 'paid-memberships-pro'); ?></li>
-				<li class="pmpro_userfield-group-column-label"><?php esc_html_e( 'Label', 'paid-memberships-pro'); ?></li>
-				<li class="pmpro_userfield-group-column-name"><?php esc_html_e( 'Name', 'paid-memberships-pro'); ?></li>
-				<li class="pmpro_userfield-group-column-type"><?php esc_html_e( 'Type', 'paid-memberships-pro'); ?></li>
-			</ul>
-			
-			<div class="pmpro_userfield-group-fields">
-				<?php
-					if ( ! empty( $group_fields ) ) {
-						foreach ( $group_fields as $field ) {
-							pmpro_get_field_html( $field );
-						}
-					}
-                ?>
-                
-                <!-- end pmpro_userfield-group-fields -->
-            
-            </div> <!-- end pmpro_userfield-inside -->
-
-			<div class="pmpro_userfield-group-actions">
-				<button name="pmpro_userfields_add_field" class="button button-secondary button-hero">
-					<?php
-						/* translators: a plus sign dashicon */
-						printf( esc_html__( '%s Add Field', 'paid-memberships-pro' ), '<span class="dashicons dashicons-plus"></span>' ); ?>
-				</button>
-                <button name="pmpro_userfields_delete_group" class="button button-secondary is-destructive">
-                    <?php esc_html_e( 'Delete Group', 'paid-memberships-pro' ); ?>
-                </button>
-			</div> <!-- end pmpro_userfield-group-actions -->
-
-		</div> <!-- end pmpro_userfield-group -->
-    </div> <!-- end inside -->
-    <?php
+    include( PMPRO_DIR . '/adminpages/user-fields/group-settings.php' );
 }
  
 /**
  * Get field HTML for settings.
  */
 function pmpro_get_field_html( $field = null ) {
-    if ( ! empty( $field ) ) {
-        // Assume field stdClass in format we save to settings.
-        $field_label = $field->label;
-        $field_name = $field->name;
-        $field_type = $field->type;
-        $field_required = $field->required;
-        $field_readonly = $field->readonly;     	
-        $field_profile = $field->profile;
-        $field_wrapper_class = $field->wrapper_class;
-        $field_element_class = $field->element_class;
-        $field_hint = $field->hint;
-        $field_options = $field->options;
-        $field_allowed_file_types = $field->allowed_file_types;
-        $field_max_file_size = $field->max_file_size;
-        $field_default = $field->default;
-    } else {
-        // Default field values
-        $field_label = '';
-        $field_name = '';
-        $field_type = '';
-        $field_required = false;
-        $field_readonly = false;
-        $field_profile = '';
-        $field_wrapper_class = '';
-        $field_element_class = '';
-        $field_hint = '';
-        $field_options = '';
-        $field_allowed_file_types = '';
-        $field_max_file_size = '';
-        $field_default = '';
-    }
-    
-	// Other vars
-	$levels = pmpro_sort_levels_by_order( pmpro_getAllLevels( true, true ) );
-	?>
-    <div class="pmpro_userfield-group-field pmpro_userfield-group-field-collapse">
-        <ul class="pmpro_userfield-group-tbody">
-            <li class="pmpro_userfield-group-column-order">
-                <div class="pmpro_userfield-group-buttons">
-                    <button type="button" aria-disabled="false" class="pmpro_userfield-group-buttons-button pmpro_userfield-field-buttons-button-move-up" aria-label="<?php esc_attr_e( 'Move up', 'paid-memberships-pro' ); ?>">
-                        <span class="dashicons dashicons-arrow-up-alt2"></span>
-                    </button>
-                    <span class="pmpro_userfield-group-buttons-description"><?php esc_html_e( 'Move Field Up', 'paid-memberships-pro' ); ?></span>
-
-                    <button type="button" aria-disabled="false" class="pmpro_userfield-group-buttons-button pmpro_userfield-field-buttons-button-move-down" aria-label="<?php esc_attr_e( 'Move down', 'paid-memberships-pro' ); ?>">
-                        <span class="dashicons dashicons-arrow-down-alt2"></span>
-                    </button>
-                    <span class="pmpro_userfield-group-buttons-description"><?php esc_html_e( 'Move Field Down', 'paid-memberships-pro' ); ?></span>
-                </div> <!-- end pmpro_userfield-group-buttons -->
-            </li>
-            <li class="pmpro_userfield-group-column-label">
-                <span class="pmpro_userfield-label"><?php echo strip_tags( wp_kses_post( $field_label ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span>
-                <div class="pmpro_userfield-field-options">
-                    <a class="edit-field" title="<?php esc_attr_e( 'Edit field', 'paid-memberships-pro' ); ?>" href="javascript:void(0);"><?php esc_html_e( 'Edit', 'paid-memberships-pro' ); ?></a> |
-                    <a class="duplicate-field" title="<?php esc_attr_e( 'Duplicate field', 'paid-memberships-pro' ); ?>" href="javascript:void(0);"><?php esc_html_e( 'Duplicate', 'paid-memberships-pro' ); ?></a> |
-                    <a class="delete-field" title="<?php esc_attr_e( 'Delete field', 'paid-memberships-pro' ); ?>" href="javascript:void(0);"><?php esc_html_e( 'Delete', 'paid-memberships-pro' ); ?></a>
-                </div> <!-- end pmpro_userfield-group-options -->
-            </li>
-            <li class="pmpro_userfield-group-column-name"><?php echo esc_html( $field_name); ?></li>
-            <li class="pmpro_userfield-group-column-type"><?php echo esc_html( $field_type); ?></li>
-        </ul>
-
-        <div class="pmpro_userfield-field-settings" style="display: none;">
-
-            <div class="pmpro_userfield-field-setting">
-                <label>
-                    <?php esc_html_e( 'Label', 'paid-memberships-pro' ); ?><br />
-                    <input type="text" name="pmpro_userfields_field_label" value="<?php echo esc_attr( $field_label );?>" />                    
-                </label>                
-                <span class="description"><?php esc_html_e( 'Brief descriptive text for the field. Shown on user forms.', 'paid-memberships-pro' ); ?></span>
-            </div> <!-- end pmpro_userfield-field-setting -->
-
-            <div class="pmpro_userfield-field-setting">
-                <label>
-                    <?php esc_html_e( 'Name', 'paid-memberships-pro' ); ?><br />
-                    <input type="text" name="pmpro_userfields_field_name" value="<?php echo esc_attr( $field_name );?>" />
-                </label>                
-                <span class="description"><?php esc_html_e( 'Single word with no spaces. Underscores are allowed.', 'paid-memberships-pro' ); ?></span>
-            </div> <!-- end pmpro_userfield-field-setting -->
-
-            <div class="pmpro_userfield-field-setting">
-                <label>
-                    <?php esc_html_e( 'Type', 'paid-memberships-pro' ); ?><br />
-                    <select name="pmpro_userfields_field_type" />
-                        <option value="text" <?php selected( $field_type, 'text' ); ?>><?php esc_html_e( 'Text', 'paid-memberships-pro' ); ?></option>
-                        <option value="textarea" <?php selected( $field_type, 'textarea' ); ?>><?php esc_html_e( 'Text Area', 'paid-memberships-pro' ); ?></option>
-                        <option value="checkbox" <?php selected( $field_type, 'checkbox' ); ?>><?php esc_html_e( 'Checkbox', 'paid-memberships-pro' ); ?></option>
-			<option value="checkbox_grouped" <?php selected( $field_type, 'checkbox_grouped' ); ?>><?php esc_html_e( 'Checkbox Group', 'paid-memberships-pro' ); ?></option>
-                        <option value="radio" <?php selected( $field_type, 'radio' ); ?>><?php esc_html_e( 'Radio', 'paid-memberships-pro' ); ?></option>
-                        <option value="select" <?php selected( $field_type, 'select' ); ?>><?php esc_html_e( 'Select / Dropdown', 'paid-memberships-pro' ); ?></option>
-                        <option value="select2" <?php selected( $field_type, 'select2' ); ?>><?php esc_html_e( 'Select2 / Autocomplete', 'paid-memberships-pro' ); ?></option>
-                        <option value="multiselect" <?php selected( $field_type, 'multiselect' ); ?>><?php esc_html_e( 'Multi Select', 'paid-memberships-pro' ); ?></option>
-                        <option value="file" <?php selected( $field_type, 'file' ); ?>><?php esc_html_e( 'File', 'paid-memberships-pro' ); ?></option>
-                        <option value="number" <?php selected( $field_type, 'number' ); ?>><?php esc_html_e( 'Number', 'paid-memberships-pro' ); ?></option>
-                        <option value="date" <?php selected( $field_type, 'date' ); ?>><?php esc_html_e( 'Date', 'paid-memberships-pro' ); ?></option>
-                        <option value="readonly" <?php selected( $field_type, 'readonly' ); ?>><?php esc_html_e( 'Read-Only', 'paid-memberships-pro' ); ?></option>
-                        <option value="hidden" <?php selected( $field_type, 'hidden' ); ?>><?php esc_html_e( 'Hidden', 'paid-memberships-pro' ); ?></option>
-                    </select>
-                </label>                
-            </div> <!-- end pmpro_userfield-field-setting -->
-
-            <div class="pmpro_userfield-field-setting pmpro_userfield-field-setting-dual">
-                <div class="pmpro_userfield-field-setting">
-                    <label>
-                        <?php esc_html_e( 'Required at Checkout?', 'paid-memberships-pro' ); ?><br />
-                        <select name="pmpro_userfields_field_required">
-                            <option value="no" <?php selected( $field_required, 'no' );?>><?php esc_html_e( 'No', 'paid-memberships-pro' ); ?></option>
-                            <option value="yes" <?php selected( $field_required, 'yes' );?>><?php esc_html_e( 'Yes', 'paid-memberships-pro' ); ?></option>
-                        </select>
-                    </label>                    
-                </div> <!-- end pmpro_userfield-field-setting -->
-
-                <div class="pmpro_userfield-field-setting">
-                    <label>
-                        <?php esc_html_e( 'Read Only?', 'paid-memberships-pro' ); ?><br />
-                        <select name="pmpro_userfields_field_readonly">
-                            <option value="no" <?php selected( $field_readonly, 'no' );?>><?php esc_html_e( 'No', 'paid-memberships-pro' ); ?></option>
-                            <option value="yes" <?php selected( $field_readonly, 'yes' );?>><?php esc_html_e( 'Yes', 'paid-memberships-pro' ); ?></option>
-                        </select>
-                    </label>                    
-                </div> <!-- end pmpro_userfield-field-setting -->
-            </div> <!-- end pmpro_userfield-field-setting-dual -->
-
-            <div class="pmpro_userfield-field-setting">
-                <label>
-                    <?php esc_html_e( 'Show field on user profile?', 'paid-memberships-pro' ); ?><br />
-                    <select name="pmpro_userfields_field_profile">
-                        <option value="" <?php selected( empty( $field_profile ), 0);?>><?php esc_html_e( '[Inherit Group Setting]', 'paid-memberships-pro' ); ?></option>
-                        <option value="yes" <?php selected( $field_profile, 'yes' );?>><?php esc_html_e( 'Yes', 'paid-memberships-pro' ); ?></option>
-                        <option value="admins" <?php selected( $field_profile, 'admins' );?>><?php esc_html_e( 'Yes (only admins)', 'paid-memberships-pro' ); ?></option>
-                        <option value="no" <?php selected( $field_profile, 'no' );?>><?php esc_html_e( 'No', 'paid-memberships-pro' ); ?></option>
-                    </select>
-                </label>                
-            </div> <!-- end pmpro_userfield-field-setting -->
-
-            <div class="pmpro_userfield-field-setting pmpro_userfield-field-setting-dual">
-                <div class="pmpro_userfield-field-setting">
-                    <label>
-                        <?php esc_html_e( 'Field Wrapper Class (optional)', 'paid-memberships-pro' ); ?><br />
-                        <input type="text" name="pmpro_userfields_field_class" value="<?php echo esc_attr( $field_wrapper_class );?>" />
-                    </label>
-                    <span class="description"><?php esc_html_e( 'Assign a custom CSS selector to the field\'s wrapping div', 'paid-memberships-pro' ); ?>.</span>
-                </div> <!-- end pmpro_userfield-field-setting -->
-
-                <div class="pmpro_userfield-field-setting">
-                    <label>
-                        <?php esc_html_e( 'Field Element Class (optional)', 'paid-memberships-pro' ); ?><br />
-                        <input type="text" name="pmpro_userfields_field_divclass" value="<?php echo esc_attr( $field_element_class );?>" />
-                    </label>                
-                    <span class="description"><?php esc_html_e( 'Assign a custom CSS selector to the field', 'paid-memberships-pro' ); ?></span>
-                </div> <!-- end pmpro_userfield-field-setting -->
-            </div> <!-- end pmpro_userfield-field-setting-dual -->
-
-            <div class="pmpro_userfield-field-setting">
-                <label>
-                    <?php esc_html_e( 'Hint (optional)', 'paid-memberships-pro' ); ?><br />
-                    <textarea name="pmpro_userfields_field_hint" /><?php echo esc_textarea( $field_hint );?></textarea>
-                </label>                
-                <span class="description"><?php esc_html_e( 'Descriptive text for users or admins submitting the field.', 'paid-memberships-pro' ); ?></span>
-            </div> <!-- end pmpro_userfield-field-setting -->
-
-			<div class="pmpro_userfield-field-setting">
-				<div class="pmpro_userfield-field-setting pmpro_userfield-field-setting-dual">
-					<div class="pmpro_userfield-field-setting">
-						<label>
-							<?php esc_html_e( 'Allowed File Types', 'paid-memberships-pro' ); ?><br />
-							<input type="text" name="pmpro_userfields_field_allowed_file_types" value="<?php echo esc_attr( trim( $field_allowed_file_types ) ); ?>" />
-						</label>
-						<span class="description"><?php esc_html_e( 'Restrict the file type that is allowed to be uploaded. Separate the file types using a comma ",". For example: png,pdf,jpg.', 'paid-memberships-pro' ); ?></span>
-					</div> <!-- end pmpro_userfield-field-setting -->
-					<div class="pmpro_userfield-field-setting">
-						<?php $server_max_upload = wp_max_upload_size() / 1024 / 1024; ?>
-						<label>
-							<?php esc_html_e( 'Max File Size Upload', 'paid-memberships-pro' ); ?><br />
-							<input type="number" name="pmpro_userfields_field_max_file_size" value="<?php echo intval( $field_max_file_size ); ?>" max="<?php echo esc_attr( $server_max_upload ); ?>"/>
-						</label>
-						<span class="description"><?php printf( esc_html__( 'Enter an upload size limit for files in Megabytes (MB) or set it to 0 to use your default server upload limit. Your server upload limit is %s.', 'paid-memberships-pro' ), $server_max_upload . 'MB' ); ?></span>
-					</div> <!-- end pmpro_userfield-field-setting -->
-				</div>
-				<div class="pmpro_userfield-field-setting">
-					<label>
-						<?php esc_html_e( 'Options', 'paid-memberships-pro' ); ?><br />
-						<textarea name="pmpro_userfields_field_options" /><?php echo esc_textarea( $field_options );?></textarea>
-					</label>
-					<span class="description"><?php esc_html_e( 'One option per line. To set separate values and labels, use value:label.', 'paid-memberships-pro' ); ?></span>
-				</div> <!-- end pmpro_userfield-field-setting -->
-				
-				<div class="pmpro_userfield-field-setting">
-					<label>
-						<?php esc_html_e( 'Default Value (optional)', 'paid-memberships-pro' ); ?><br />
-						<input type="text" name="pmpro_userfields_field_default" value="<?php echo esc_attr( $field_default ); ?>" />
-					</label>
-				</div> <!-- end pmpro_userfield-field-setting -->
-			</div>
-
-            <div class="pmpro_userfield-field-actions">            
-                <button name="pmpro_userfields_close_field" class="button button-secondary pmpro_userfields_close_field">
-                    <?php esc_html_e( 'Close Field', 'paid-memberships-pro' ); ?>
-                </button> 
-				<button name="pmpro_userfields_delete_field" class="button button-secondary is-destructive">
-                    <?php esc_html_e( 'Delete Field', 'paid-memberships-pro' ); ?>
-                </button>           
-            </div> <!-- end pmpro_userfield-field-actions -->
-        </div> <!-- end pmpro_userfield-field-settings -->        
-    </div> <!-- end pmpro_userfield-group-field -->
-    <?php
+	include( PMPRO_DIR . '/adminpages/user-fields/field-settings.php' );
 }
 
 /**
@@ -1505,7 +1053,7 @@ function pmpro_load_user_fields_from_settings() {
     $settings_groups = pmpro_get_user_fields_settings();
 
     foreach ( $settings_groups as $group ) {
-        pmpro_add_field_group( $group->name, $group->name, $group->description );
+        $group_obj = PMPro_field_Group::add( $group->name, $group->name, $group->description );
         
         // Figure out profile value. Change 2 settings values into 1 field value.
         if ( $group->checkout === 'yes' ) {
@@ -1583,7 +1131,7 @@ function pmpro_load_user_fields_from_settings() {
                     'default' => $settings_field->default,
                 )
             );
-            pmpro_add_user_field( $group->name, $field );
+            $group_obj->add_field( $field );
         }
     }        
 }
@@ -1593,11 +1141,13 @@ function pmpro_load_user_fields_from_settings() {
  * Check if user is adding custom user fields with code.
  *
  * @since 2.9
+ * @deprecated TBD
  *
  * @return bool True if user is adding custom user fields with code.
  */
 function pmpro_has_coded_user_fields() {
-	global $pmpro_user_fields, $pmprorh_registration_fields;
+	_deprecated_function( __FUNCTION__, 'TBD' );
+	global $pmprorh_registration_fields;
 
 	// Check if coded fields are being added using the PMPro Register Helper Add On active.
 	if ( ! empty( $pmprorh_registration_fields ) ) {
@@ -1605,15 +1155,16 @@ function pmpro_has_coded_user_fields() {
 	}
 
 	// Check if coded fields are being added using the PMPro Register Helper Add On inactive.
-	$num_db_fields = array_sum( array_map( function ($group) { return count( $group->fields ); }, pmpro_get_user_fields_settings() ) ); // Fields from UI settings page.
-	$num_global_fields = array_sum( array_map( 'count', $pmpro_user_fields ) ); // Total loaded fields.
-	return $num_global_fields > $num_db_fields;
+	$num_fields_from_settings = array_sum( array_map( function ($group) { return count( $group->fields ); }, pmpro_get_user_fields_settings() ) ); // Fields from UI settings page.
+	$total_registered_fields = array_sum( array_map( function ($group) { return count( $group->get_fields() ); }, PMPro_Field_Group::get_all() ) ); // All registered fields.
+	return $total_registered_fields > $num_fields_from_settings;
 }
 
 /**
  * Gets the label(s) for a passed user field value.
  *
  * @since 2.11
+ * @deprecated TBD Use PMProField::displayValue instead.
  *
  * @param string $field_name  The name of the field that the value belongs to.
  * @param string|array $field_value The value to get the label for.
@@ -1621,6 +1172,8 @@ function pmpro_has_coded_user_fields() {
  * @return string|array The label(s) for the passed value. Will be same type as $field_value.
  */
 function pmpro_get_label_for_user_field_value( $field_name, $field_value ) {
+	_deprecated_function( __FUNCTION__, 'TBD', 'PMProField::displayValue' );
+
 	// Loop through all the field groups.
 	$field_groups = PMPro_Field_Group::get_all();
 	foreach($field_groups as $group_name => $group) {
@@ -1648,17 +1201,7 @@ function pmpro_get_label_for_user_field_value( $field_name, $field_value ) {
             }
             
 			// Replace meta values with their corresponding labels.
-			if ( is_array( $field_value ) ) {
-				foreach ( $field_value as $key => $value ) {
-					if ( isset( $user_field->options[ $value ] ) ) {
-						$field_value[ $key ] = $user_field->options[ $value ];
-					}
-				}
-			} else {
-				if ( isset( $user_field->options[ $field_value ] ) ) {
-					$field_value = $user_field->options[ $field_value ];
-				}
-			}
+			$field_value = $user_field->displayValue( $field_value, false );
 		}
 	}
 	return $field_value;
@@ -1667,29 +1210,12 @@ function pmpro_get_label_for_user_field_value( $field_name, $field_value ) {
 /**
  * Get a single user field.
  * @since 3.0
+ * @deprecated TBD
  * @param string $field_name The name of the field to get.
  * @return bool|object The field object if found, false otherwise.
  */
 function pmpro_get_user_field( $field_name ) {
-	// Loop through all the field groups.
-	$field_groups = PMPro_Field_Group::get_all();
-	foreach($field_groups as $group_name => $group) {
-		// Loop through all the fields in the group.
-		$fields = $group->get_fields();
-		foreach( $fields as $user_field ) {
-			// Check if this is the user field that we are displaying.
-			if ( $user_field->name !== $field_name ) {
-				continue;
-			}
-
-			// Make sure that we have a valid user field.
-			if ( ! pmpro_is_field( $user_field ) ) {
-				continue;
-			}
-
-			return $user_field;
-		}
-	}
-	
-	return false;
+	_deprecated_function( __FUNCTION__, 'TBD', 'PMPro_Field_Group::get_field' );
+	$field = PMPro_Field_Group::get_field( $field_name );
+	return empty( $field ) ? false : $field;
 }
diff --git a/includes/functions.php b/includes/functions.php
index cae94b1bc..3c53878f1 100644
--- a/includes/functions.php
+++ b/includes/functions.php
@@ -3809,12 +3809,17 @@ function pmpro_sanitize_with_safelist( $needle, $safelist ) {
  * Sanitizes the passed value.
  * Default sanitizing for things like user fields.
  *
+ * @since TBD Marking the $field argument as deprecated.
+ *
  * @param array|int|null|string|stdClass $value The value to sanitize
- * @param PMPro_Field $field (optional) Field to check type.
  *
  * @return array|int|string|object     Sanitized value
  */
 function pmpro_sanitize( $value, $field = null ) {
+	if ( null !== $field ) {
+		// This argument is deprecated. User fields now have sanitization logic in the field class.
+		_deprecated_argument( __FUNCTION__, 'TBD', __( 'The $field argument is deprecated. The sanitization logic is now built into the PMPro_Field class.', 'paid-memberships-pro' ) );
+	}
 
 	if ( is_array( $value ) ) {
 
diff --git a/scheduled/crons.php b/scheduled/crons.php
index ff3720787..6457927c6 100644
--- a/scheduled/crons.php
+++ b/scheduled/crons.php
@@ -377,3 +377,29 @@ function pmpro_cron_recurring_payment_reminders() {
 		$previous_days = $days;
 	}
 }
+
+/**
+ * Delete old files in wp-content/uploads/pmpro-register-helper/tmp every day.
+ */
+function pmpro_cron_delete_tmp() {
+	$upload_dir = wp_upload_dir();
+	$pmprorh_dir = $upload_dir['basedir'] . "/paid-memberships-pro/tmp/";
+
+	if(file_exists($pmprorh_dir) && $handle = opendir($pmprorh_dir))
+	{
+		while(false !== ($file = readdir($handle)))
+		{
+			$file = $pmprorh_dir . $file;
+			$filelastmodified = filemtime($file);
+			if(is_file($file) && (time() - $filelastmodified) > 3600)
+			{
+				unlink($file);
+			}
+		}
+
+		closedir($handle);
+	}
+
+	exit;
+}
+add_action( 'pmpro_cron_delete_tmp', 'pmpro_cron_delete_tmp' );
diff --git a/shortcodes/pmpro_member.php b/shortcodes/pmpro_member.php
index 07ce3be76..a9c9ea3f7 100644
--- a/shortcodes/pmpro_member.php
+++ b/shortcodes/pmpro_member.php
@@ -185,21 +185,12 @@ function pmpro_member_shortcode( $atts, $content = null, $shortcode_tag = '' ) {
 		}
 	}
 
-	// Check for files to reformat them.
-	if ( is_array( $r ) && ! empty( $r['fullurl'] ) ) {
-		$file_field = pmpro_get_user_field( $field );
-		if ( ! empty( $file_field ) ) {
-			$file_field->file = $r;
-			$file_field->readonly = true;
-			$r = $file_field->displayValue( $r['fullurl'], false ); // False to not echo.
-		} else {
-			$r = '<a href="' . esc_url( $r['fullurl'] ) . '">' . esc_html( basename($r['fullurl'] ) ) . '</a>';
-		}
+	// If this is a user field, get the display value.
+	$user_field = PMPro_Field_Group::get_field( $field );
+	if ( ! empty( $user_field ) ) {
+		$r = $user_field->displayValue( $r, false );
 	}
 
-	// If this is a user field with an associative array of options, get the label(s) for the value(s).
-	$r = pmpro_get_label_for_user_field_value( $field, $r );
-
 	// Check for arrays to reformat them.
 	if ( is_array( $r ) ) {
 		$r = implode( ', ', $r );

From aff9c338c2235c18e6b0d2361a27aca0463a00f1 Mon Sep 17 00:00:00 2001
From: David Parker <dlparker1005@gmail.com>
Date: Mon, 4 Nov 2024 11:11:02 -0500
Subject: [PATCH 4/6] Remove reference to global in pmpro_check_upload()

---
 includes/functions.php | 69 +++++++++++++++++-------------------------
 1 file changed, 28 insertions(+), 41 deletions(-)

diff --git a/includes/functions.php b/includes/functions.php
index 3c53878f1..4be0c8aad 100644
--- a/includes/functions.php
+++ b/includes/functions.php
@@ -4765,8 +4765,6 @@ function pmpro_set_expiration_date( $user_id, $level_id, $enddate ) {
  * @return true|WP_Error True if the file is allowed, otherwise a WP_Error object.
  */
 function pmpro_check_upload( $file_index ) {
-	global $pmpro_user_fields;
-
 	// Check if the file was uploaded.
 	if ( empty( $_FILES[ $file_index ] ) ) {
 		return new WP_Error( 'pmpro_upload_error', __( 'No file was uploaded.', 'paid-memberships-pro' ) );
@@ -4787,51 +4785,40 @@ function pmpro_check_upload( $file_index ) {
 	}
 
 	// If this is an upload for a user field, we need to perform additional checks.
-	$is_user_field = false;
-	if ( ! empty( $pmpro_user_fields ) && is_array( $pmpro_user_fields ) ) {
-		foreach ( $pmpro_user_fields as $checkout_box ) {
-			foreach ( $checkout_box as $field ) {
-				if ( $field->name == $file_index ) {
-					// This file is being uploaded for a user field.
-					$is_user_field = true;
-
-					// First, make sure that this is a 'file' field.
-					if ( $field->type !== 'file' ) {
-						return new WP_Error( 'pmpro_upload_error', __( 'Invalid field input.', 'paid-memberships-pro' ) );
-					}
+	$field = PMPro_Field_Group::get_field( $file_index );
+	if ( ! empty( $field) ) {
+		// First, make sure that this is a 'file' field.
+		if ( $field->type !== 'file' ) {
+			return new WP_Error( 'pmpro_upload_error', __( 'Invalid field input.', 'paid-memberships-pro' ) );
+		}
 
-					// If there are allowed file types, check if the file is an allowed file type.
-					// It does not look like the ext property is documented anywhere, but keeping it in case sites are using it.
-					if ( ! empty( $field->ext ) && is_array( $field->ext ) && ! in_array( $filetype['ext'], $field->ext ) ) {
-						return new WP_Error( 'pmpro_upload_error', __( 'Invalid file type.', 'paid-memberships-pro' ) );
-					}
+		// If there are allowed file types, check if the file is an allowed file type.
+		// It does not look like the ext property is documented anywhere, but keeping it in case sites are using it.
+		if ( ! empty( $field->ext ) && is_array( $field->ext ) && ! in_array( $filetype['ext'], $field->ext ) ) {
+			return new WP_Error( 'pmpro_upload_error', __( 'Invalid file type.', 'paid-memberships-pro' ) );
+		}
 
-					// Check the file type against the allowed types.
-					$allowed_mime_types = ! empty( $field->allowed_file_types ) ? array_map( 'sanitize_text_field', explode( ',', $field->allowed_file_types ) ) : array();
+		// Check the file type against the allowed types.
+		$allowed_mime_types = ! empty( $field->allowed_file_types ) ? array_map( 'sanitize_text_field', explode( ',', $field->allowed_file_types ) ) : array();
 
-					//Remove fullstops from the beginning of the allowed file types.
-					$allowed_mime_types = array_map( function( $type ) {
-						return ltrim( $type, '.' );
-					}, $allowed_mime_types );
+		//Remove fullstops from the beginning of the allowed file types.
+		$allowed_mime_types = array_map( function( $type ) {
+			return ltrim( $type, '.' );
+		}, $allowed_mime_types );
 
-					// Check the file type against the allowed types. If empty allowed mimes, assume any file upload is okay.
-					if ( ! empty( $allowed_mime_types ) && ! in_array( $filetype['ext'], $allowed_mime_types ) ) {
-						return new WP_Error( 'pmpro_upload_file_type_error', sprintf( esc_html__( 'Invalid file type. Please try uploading the file type(s): %s', 'paid-memberships-pro' ), implode( ',' ,$allowed_mime_types ) ) );
-					}
-					
-					// Check if the file upload is too big to upload.
-					if ( $field->max_file_size > 0 ) {
-						$upload_max_file_size_in_bytes = $field->max_file_size * 1024 * 1024;
-						if ( $file['size'] > $upload_max_file_size_in_bytes ) {
-							return new WP_Error( 'pmpro_upload_file_size_error', sprintf( esc_html__( 'File size is too large for %s. Please upload files smaller than %dMB.', 'paid-memberships-pro' ), $field->label, $field->max_file_size ) );
-						}
-					}
-				}
+		// Check the file type against the allowed types. If empty allowed mimes, assume any file upload is okay.
+		if ( ! empty( $allowed_mime_types ) && ! in_array( $filetype['ext'], $allowed_mime_types ) ) {
+			return new WP_Error( 'pmpro_upload_file_type_error', sprintf( esc_html__( 'Invalid file type. Please try uploading the file type(s): %s', 'paid-memberships-pro' ), implode( ',' ,$allowed_mime_types ) ) );
+		}
+		
+		// Check if the file upload is too big to upload.
+		if ( $field->max_file_size > 0 ) {
+			$upload_max_file_size_in_bytes = $field->max_file_size * 1024 * 1024;
+			if ( $file['size'] > $upload_max_file_size_in_bytes ) {
+				return new WP_Error( 'pmpro_upload_file_size_error', sprintf( esc_html__( 'File size is too large for %s. Please upload files smaller than %dMB.', 'paid-memberships-pro' ), $field->label, $field->max_file_size ) );
 			}
 		}
-	}
-
-	if ( ! $is_user_field ) {
+	} else {
 		/**
 		 * Filter whether a file not associated with a user field can be uploaded.
 		 *

From a845f0f3ef2f5267bebd110dcd0f6f18caa27834 Mon Sep 17 00:00:00 2001
From: David Parker <dlparker1005@gmail.com>
Date: Wed, 6 Nov 2024 12:24:29 -0500
Subject: [PATCH 5/6] Rewrite user fields UI built on PHP

---
 adminpages/user-fields/delete-field.php   |  43 ++
 adminpages/user-fields/delete-group.php   |  38 ++
 adminpages/user-fields/edit-field.php     | 281 ++++++++++
 adminpages/user-fields/edit-group.php     | 159 ++++++
 adminpages/user-fields/field-settings.php | 207 -------
 adminpages/user-fields/group-settings.php | 132 -----
 adminpages/user-fields/save-field.php     | 107 ++++
 adminpages/user-fields/save-group.php     |  70 +++
 adminpages/userfields.php                 | 630 +++++++++++++++++-----
 classes/class-pmpro-field-group.php       |   8 +-
 includes/fields.php                       |  14 +-
 includes/scripts.php                      |  10 -
 includes/services.php                     | 115 +++-
 js/pmpro-admin.js                         | 322 +----------
 14 files changed, 1339 insertions(+), 797 deletions(-)
 create mode 100644 adminpages/user-fields/delete-field.php
 create mode 100644 adminpages/user-fields/delete-group.php
 create mode 100644 adminpages/user-fields/edit-field.php
 create mode 100644 adminpages/user-fields/edit-group.php
 delete mode 100644 adminpages/user-fields/field-settings.php
 delete mode 100644 adminpages/user-fields/group-settings.php
 create mode 100644 adminpages/user-fields/save-field.php
 create mode 100644 adminpages/user-fields/save-group.php

diff --git a/adminpages/user-fields/delete-field.php b/adminpages/user-fields/delete-field.php
new file mode 100644
index 000000000..684443186
--- /dev/null
+++ b/adminpages/user-fields/delete-field.php
@@ -0,0 +1,43 @@
+<?php
+
+// Get the field to delete
+if ( isset( $_REQUEST['delete_name'] ) ) {
+	$delete_name = sanitize_text_field( $_REQUEST['delete_name'] );
+
+	// Get the current settings.
+	$current_settings = pmpro_get_user_fields_settings();
+
+	// Remove the field from the settings.
+	$new_settings = array();
+	$deleted = false;
+	foreach ( $current_settings as $group_setting ) {
+		$new_fields = array();
+		foreach ( $group_setting->fields as $field_setting ) {
+			if ( $field_setting->name === $delete_name ) {
+				$deleted = true;
+			} else {
+				$new_fields[] = $field_setting;
+			}
+		}
+		$group_setting->fields = $new_fields;
+		$new_settings[] = $group_setting;
+	}
+
+	if ( $deleted ) {
+		// Save the new settings.
+		update_option( 'pmpro_user_fields_settings', $new_settings );
+
+		// Show a success message.
+		pmpro_setMessage( __( 'Field deleted.', 'paid-memberships-pro' ), 'success' );
+
+		// Redirect with javascript.
+		?>
+		<script>
+			 window.location.href = '?page=pmpro-userfields&success_message=<?php echo urlencode( __( 'Field deleted.', 'paid-memberships-pro' ) ); ?>';
+		</script>
+		<?php
+		exit;
+	} else {
+		pmpro_setMessage( __( 'Field not found.', 'paid-memberships-pro' ), -1 );
+	}
+}
\ No newline at end of file
diff --git a/adminpages/user-fields/delete-group.php b/adminpages/user-fields/delete-group.php
new file mode 100644
index 000000000..84c43bba0
--- /dev/null
+++ b/adminpages/user-fields/delete-group.php
@@ -0,0 +1,38 @@
+<?php
+
+// Get the group to delete
+if ( isset( $_REQUEST['delete_name'] ) ) {
+	$delete_name = sanitize_text_field( $_REQUEST['delete_name'] );
+
+	// Get the current settings.
+	$current_settings = pmpro_get_user_fields_settings();
+
+	// Remove the group from the settings.
+	$new_settings = array();
+	$deleted = false;
+	foreach ( $current_settings as $group_setting ) {
+		if ( $group_setting->name === $delete_name ) {
+			$deleted = true;
+		} else {
+			$new_settings[] = $group_setting;
+		}
+	}
+
+	if ( $deleted ) {
+		// Save the new settings.
+		update_option( 'pmpro_user_fields_settings', $new_settings );
+
+		// Show a success message.
+		pmpro_setMessage( __( 'Group deleted.', 'paid-memberships-pro' ), 'success' );
+
+		// Redirect with javascript.
+		?>
+		<script>
+			window.location.href = '?page=pmpro-userfields&success_message=<?php echo urlencode( __( 'Group deleted.', 'paid-memberships-pro' ) ); ?>';
+		</script>
+		<?php
+		exit;
+	} else {
+		pmpro_setMessage( __( 'Group not found.', 'paid-memberships-pro' ), -1 );
+	}
+}
\ No newline at end of file
diff --git a/adminpages/user-fields/edit-field.php b/adminpages/user-fields/edit-field.php
new file mode 100644
index 000000000..c73092826
--- /dev/null
+++ b/adminpages/user-fields/edit-field.php
@@ -0,0 +1,281 @@
+<?php
+/**
+ * @var string $edit The field being edited or empty if adding a new field.
+ */
+
+global $wpdb, $pmpro_msg, $pmpro_msgt;
+
+$field = null;
+if ( ! empty( $edit ) ) {
+	// Get the current user fields settings.
+	$current_settings = pmpro_get_user_fields_settings();
+
+	// Find the field.
+	foreach( $current_settings as $group ) {
+		foreach( $group->fields as $f ) {
+			if ( $f->name === $edit ) {
+				$field = $f;
+				$field->group = $group->name;
+				break 2;
+			}
+		}
+	}
+}
+
+// If we still don't have a field, get default settings.
+if ( empty( $field ) ) {
+	$field = new stdClass();
+	$field->name = '';
+	$field->label = '';
+	$field->type = '';
+	$field->required = false;
+	$field->readonly = false;
+	$field->profile = '';
+	$field->wrapper_class = '';
+	$field->element_class = '';
+	$field->hint = '';
+	$field->default = '';
+	$field->options = '';
+	$field->allowed_file_types = '';
+	$field->max_file_size = '';
+	$field->group = empty( $_REQUEST['group'] ) ? '' : sanitize_text_field( $_REQUEST['group'] );
+}
+
+?>
+<hr class="wp-header-end">
+<?php if ( ! empty( $field->name ) ) { ?>
+	<h1 class="wp-heading-inline">
+		<?php
+		echo sprintf(
+			// translators: %s is the Level ID.
+			esc_html__('Edit Field : %s', 'paid-memberships-pro'),
+			esc_attr( $field->name )
+		);
+		?>
+	</h1>
+<?php } else { ?>
+	<h1 class="wp-heading-inline"><?php esc_html_e('Add New Field', 'paid-memberships-pro'); ?></h1>
+<?php } ?>
+
+<?php
+// Show the settings page message.
+if (!empty($pmpro_msg)) { ?>
+	<div class="inline notice notice-large <?php echo $pmpro_msgt > 0 ? 'notice-success' : 'notice-error'; ?>">
+		<p><?php echo wp_kses_post( $pmpro_msg ); ?></p>
+	</div>
+<?php }
+?>
+<form action="" method="post" enctype="multipart/form-data">
+	<input type="hidden" name="action" value="save_field" />
+	<?php wp_nonce_field('save_field', 'pmpro_userfields_nonce'); ?>
+
+	<div id="general-information" class="pmpro_section" data-visibility="shown" data-activated="true">
+		<div class="pmpro_section_toggle">
+			<button class="pmpro_section-toggle-button" type="button" aria-expanded="true">
+				<span class="dashicons dashicons-arrow-up-alt2"></span>
+				<?php esc_html_e('General Information', 'paid-memberships-pro'); ?>
+			</button>
+		</div>
+		<div class="pmpro_section_inside">
+			<table class="form-table">
+				<tbody>
+					<tr>
+						<th scope="row" valign="top"><label for="name"><?php esc_html_e('Label', 'paid-memberships-pro'); ?></label></th>
+						<td>
+							<input id="label" name="label" type="text" value="<?php echo esc_attr( $field->label ) ?>" class="regular-text" required />
+							<p class="description"><?php esc_html_e('Brief descriptive text for the field. Shown on user forms.', 'paid-memberships-pro'); ?></p>
+						</td>
+					</tr>
+					<?php
+					if ( empty( $field->name ) ) {
+						?>
+						<tr>
+							<th scope="row" valign="top"><label for="name"><?php esc_html_e('Name', 'paid-memberships-pro'); ?></label></th>
+							<td>
+								<input id="name" name="name" type="text" value="<?php echo esc_attr( $field->name ) ?>" class="regular-text" required />
+								<p class="description"><?php esc_html_e('Single word with no spaces. Underscores are allowed. This is the field name used in the database.', 'paid-memberships-pro'); ?></p>
+							</td>
+						</tr>
+						<?php
+					} else {
+						?>
+						<input type="hidden" name="name" value="<?php echo esc_attr( $field->name ); ?>" />
+						<?php
+					}
+					?>
+					<tr>
+						<th scope="row" valign="top"><label for="group"><?php esc_html_e('Group', 'paid-memberships-pro'); ?></label></th>
+						<td>
+							<select id="group" name="group" required>
+								<?php
+								$groups = PMPro_Field_Group::get_all();
+								foreach ( $groups as $group ) {
+									?>
+									<option value="<?php echo esc_attr( $group->name ); ?>" <?php selected( $field->group, $group->name ); ?>><?php echo esc_html( $group->label ); ?></option>
+									<?php
+								}
+								?>
+							</select>
+							<p class="description"><?php esc_html_e('The group this field belongs to.', 'paid-memberships-pro'); ?></p>
+						</td>
+					</tr>
+				</tbody>
+			</table>
+		</div> <!-- end pmpro_section_inside -->
+	</div> <!-- end pmpro_section -->
+
+	<div id="field-attributes" class="pmpro_section" data-visibility="shown" data-activated="true">
+		<div class="pmpro_section_toggle">
+			<button class="pmpro_section-toggle-button" type="button" aria-expanded="true">
+				<span class="dashicons dashicons-arrow-up-alt2"></span>
+				<?php esc_html_e('Field Attributes', 'paid-memberships-pro'); ?>
+			</button>
+		</div>
+		<div class="pmpro_section_inside">
+			<table class="form-table">
+				<tbody>
+					<tr>
+						<th scope="row" valign="top"><label for="type"><?php esc_html_e('Type', 'paid-memberships-pro'); ?></label></th>
+						<td>
+							<select id="type" name="type" required>
+								<?php
+								$types = array(
+									'text' => 'Text',
+									'textarea' => 'Text Area',
+									'checkbox' => 'Checkbox',
+									'checkbox_grouped' => 'Checkbox Group',
+									'radio' => 'Radio',
+									'select' => 'Select / Dropdown',
+									'select2' => 'Select2 / Autocomplete',
+									'multiselect' => 'Multi Select',
+									'file' => 'File',
+									'number' => 'Number',
+									'date' => 'Date',
+									'readonly' => 'Read-Only',
+									'hidden' => 'Hidden',
+								);
+								foreach ( $types as $type => $label ) {
+									?>
+									<option value="<?php echo esc_attr( $type ); ?>" <?php selected( $field->type, $type ); ?>><?php echo esc_html( $label ); ?></option>
+									<?php
+								}
+								?>
+							</select>
+						</td>
+					</tr>
+					<tr>
+						<th scope="row" valign="top"><label for="required"><?php esc_html_e('Required At Checkout?', 'paid-memberships-pro'); ?></label></th>
+						<td>
+							<select id="required" name="required">
+								<option value="no" <?php selected( $field->required, 'no' ); ?>><?php esc_html_e('No', 'paid-memberships-pro'); ?></option>
+								<option value="yes" <?php selected( $field->required, 'yes' ); ?>><?php esc_html_e('Yes', 'paid-memberships-pro'); ?></option>
+							</select>
+						</td>
+					</tr>
+					<tr>
+						<th scope="row" valign="top"><label for="hint"><?php esc_html_e('Hint', 'paid-memberships-pro'); ?></label></th>
+						<td>
+							<input id="hint" name="hint" type="text" value="<?php echo esc_attr( $field->hint ); ?>" class="regular-text" />
+							<p class="description"><?php esc_html_e('Descriptive text for users or admins submitting the field.', 'paid-memberships-pro'); ?></p>
+						</td>
+					</tr>
+					<tr class="field_type field_type_checkbox_grouped field_type_radio field_type_select field_type_select2 field_type_multiselect">
+						<th scope="row" valign="top"><label for="options"><?php esc_html_e('Options', 'paid-memberships-pro'); ?></label></th>
+						<td>
+							<textarea id="options" name="options" class="large-text" rows="5"><?php echo esc_textarea( $field->options ); ?></textarea>
+							<p class="description"><?php esc_html_e( 'One option per line. To set separate values and labels, use value:label.', 'paid-memberships-pro'); ?></p>
+						</td>
+					</tr>
+					<tr class="field_type field_type_text field_type_textarea field_type_checkbox field_type_radio field_type_select field_type_date field_type_readonly field_type_hidden field_type_number">
+						<th scope="row" valign="top"><label for="default"><?php esc_html_e('Default Value', 'paid-memberships-pro'); ?></label></th>
+						<td><input id="default" name="default" type="text" value="<?php echo esc_attr( $field->default ); ?>" class="regular-text" /></td>
+					</tr>
+					<tr class="field_type field_type_file">
+						<th scope="row" valign="top"><label for="allowed_file_types"><?php esc_html_e('Allowed File Types', 'paid-memberships-pro'); ?></label></th>
+						<td>
+							<input id="allowed_file_types" name="allowed_file_types" type="text" value="<?php echo esc_attr( $field->allowed_file_types ); ?>" class="regular-text" />
+							<p class="description"><?php esc_html_e( 'Restrict the file type that is allowed to be uploaded. Separate the file types using a comma ",". For example: png,pdf,jpg.', 'paid-memberships-pro' ); ?></p>
+						</td>
+					</tr>
+					<tr class="field_type field_type_file">
+						<th scope="row" valign="top"><label for="max_file_size"><?php esc_html_e('Max File Size', 'paid-memberships-pro'); ?></label></th>
+						<td>
+							<input id="max_file_size" name="max_file_size" type="text" value="<?php echo esc_attr( $field->max_file_size ); ?>" class="regular-text" />
+							<?php $server_max_upload = wp_max_upload_size() / 1024 / 1024; ?>
+							<p class="description"><?php printf( esc_html__( 'Enter an upload size limit for files in Megabytes (MB) or set it to 0 to use your default server upload limit. Your server upload limit is %s.', 'paid-memberships-pro' ), $server_max_upload . 'MB' ); ?></p>
+						</td>
+					</tr>
+				</tbody>
+			</table>
+		</div> <!-- end pmpro_section_inside -->
+	</div> <!-- end pmpro_section -->
+
+	<div id="visibility-settings" class="pmpro_section" data-visibility="shown" data-activated="true">
+		<div class="pmpro_section_toggle">
+			<button class="pmpro_section-toggle-button" type="button" aria-expanded="true">
+				<span class="dashicons dashicons-arrow-up-alt2"></span>
+				<?php esc_html_e('Visibility Settings', 'paid-memberships-pro'); ?>
+			</button>
+		</div>
+		<div class="pmpro_section_inside">
+			<table class="form-table">
+				<tbody>
+					<tr>
+						<th scope="row" valign="top"><label for="profile"><?php esc_html_e('Show on User Profile?', 'paid-memberships-pro'); ?></label></th>
+						<td>
+							<select id="profile" name="profile">
+								<option value="" <?php selected( empty( $field ) ? 0 : $field->profile, 0 ); ?>><?php esc_html_e('[Inherit Group Setting]', 'paid-memberships-pro'); ?></option>
+								<option value="yes" <?php selected( empty( $field ) ? 0 : $field->profile, 'yes' ); ?>><?php esc_html_e('Yes', 'paid-memberships-pro'); ?></option>
+								<option value="admins" <?php selected( empty( $field ) ? 0 : $field->profile, 'admins' ); ?>><?php esc_html_e('Yes (only admins)', 'paid-memberships-pro'); ?></option>
+								<option value="no" <?php selected( empty( $field ) ? 0 : $field->profile, 'no' ); ?>><?php esc_html_e('No', 'paid-memberships-pro'); ?></option>
+							</select>
+						</td>
+					</tr>
+				</tbody>
+			</table>
+		</div> <!-- end pmpro_section_inside -->
+	</div> <!-- end pmpro_section -->
+
+	<div id="additional-styles" class="pmpro_section" data-visibility="shown" data-activated="true">
+		<div class="pmpro_section_toggle">
+			<button class="pmpro_section-toggle-button" type="button" aria-expanded="true">
+				<span class="dashicons dashicons-arrow-up-alt2"></span>
+				<?php esc_html_e('Additional Styling', 'paid-memberships-pro'); ?>
+			</button>
+		</div>
+		<div class="pmpro_section_inside">
+			<table class="form-table">
+				<tbody>
+					<tr>
+						<th scope="row" valign="top"><label for="profile"><?php esc_html_e('Read Only?', 'paid-memberships-pro'); ?></label></th>
+						<td>
+							<select id="readonly" name="readonly">
+								<option value="no" <?php selected( $field->readonly, 'no' ); ?>><?php esc_html_e('No', 'paid-memberships-pro'); ?></option>
+								<option value="yes" <?php selected( $field->readonly, 'yes' ); ?>><?php esc_html_e('Yes', 'paid-memberships-pro'); ?></option>
+							</select>
+						</td>
+					</tr>
+					<tr>
+						<th scope="row" valign="top"><label for="profile"><?php esc_html_e('Field Wrapper Class', 'paid-memberships-pro'); ?></label></th>
+						<td>
+							<input id="wrapper_class" name="wrapper_class" type="text" value="<?php echo esc_attr( $field->wrapper_class ); ?>" />
+							<p class="description"><?php esc_html_e('Assign a custom CSS selector to the field\'s wrapping div.', 'paid-memberships-pro'); ?></p>
+						</td>
+					</tr>
+					<tr>
+						<th scope="row" valign="top"><label for="profile"><?php esc_html_e('Field Element Class', 'paid-memberships-pro'); ?></label></th>
+						<td>
+							<input id="element_class" name="element_class" type="text" value="<?php echo esc_attr( $field->element_class ); ?>"/>
+							<p class="description"><?php esc_html_e('Assign a custom CSS selector to the field.', 'paid-memberships-pro'); ?></p>
+						</td>
+					</tr>
+				</tbody>
+			</table>
+		</div> <!-- end pmpro_section_inside -->
+	</div> <!-- end pmpro_section -->
+
+	<p class="submit">
+		<input name="save" type="submit" class="button button-primary" value="<?php esc_attr_e('Save Field', 'paid-memberships-pro'); ?>" />
+		<input name="cancel" type="button" class="button" value="<?php esc_attr_e('Cancel', 'paid-memberships-pro'); ?>" onclick="location.href='<?php echo esc_url(add_query_arg('page', 'pmpro-userfields', admin_url('admin.php'))); ?>';" />
+	</p>
+</form>
\ No newline at end of file
diff --git a/adminpages/user-fields/edit-group.php b/adminpages/user-fields/edit-group.php
new file mode 100644
index 000000000..c3d14c34c
--- /dev/null
+++ b/adminpages/user-fields/edit-group.php
@@ -0,0 +1,159 @@
+<?php
+/**
+ * @var string $edit_group The group being edited or empty if adding a new group.
+ */
+
+global $wpdb, $pmpro_msg, $pmpro_msgt;
+
+$group = null;
+if ( ! empty( $edit_group ) ) {
+	// Get the current user fields settings.
+	$current_settings = pmpro_get_user_fields_settings();
+
+	// Find the group.
+	foreach( $current_settings as $group_settings ) {
+		if ( $group_settings->name === $edit_group ) {
+			$group = $group_settings;
+			break;
+		}
+	}
+}
+
+// If we still don't have a group, get default settings.
+if ( empty( $group ) ) {
+	$group = new stdClass();
+	$group->name = '';
+	$group->label = '';
+	$group->checkout = 'yes';
+	$group->profile = 'yes';
+	$group->description = '';
+	$group->levels = array();
+}
+
+// If the name is set but not the label, set the label to the name.
+if ( ! empty( $group->name ) && empty( $group->label ) ) {
+	$group->label = $group->name;
+}
+
+// Get all membership levels.
+$levels = pmpro_sort_levels_by_order( pmpro_getAllLevels( true, true ) );
+
+?>
+<hr class="wp-header-end">
+<?php if ( ! empty( $group->name ) ) { ?>
+	<h1 class="wp-heading-inline">
+		<?php
+		echo sprintf(
+			// translators: %s is the Level ID.
+			esc_html__('Edit Field Group : %s', 'paid-memberships-pro'),
+			esc_attr( $group->name )
+		);
+		?>
+	</h1>
+<?php } else { ?>
+	<h1 class="wp-heading-inline"><?php esc_html_e('Add New Field Group', 'paid-memberships-pro'); ?></h1>
+<?php } ?>
+
+<?php
+// Show the settings page message.
+if (!empty($pmpro_msg)) { ?>
+	<div class="inline notice notice-large <?php echo $pmpro_msgt > 0 ? 'notice-success' : 'notice-error'; ?>">
+		<p><?php echo wp_kses_post( $pmpro_msg ); ?></p>
+	</div>
+<?php }
+?>
+<form action="" method="post" enctype="multipart/form-data">
+	<input type="hidden" name="action" value="save_group" />
+	<?php wp_nonce_field('save_group', 'pmpro_userfields_nonce'); ?>
+
+	<div id="general-information" class="pmpro_section" data-visibility="shown" data-activated="true">
+		<div class="pmpro_section_toggle">
+			<button class="pmpro_section-toggle-button" type="button" aria-expanded="true">
+				<span class="dashicons dashicons-arrow-up-alt2"></span>
+				<?php esc_html_e('General Information', 'paid-memberships-pro'); ?>
+			</button>
+		</div>
+		<div class="pmpro_section_inside">
+			<table class="form-table">
+				<tbody>
+					<tr>
+						<th scope="row" valign="top"><label for="name"><?php esc_html_e('Label', 'paid-memberships-pro'); ?></label></th>
+						<td>
+							<input id="label" name="label" type="text" value="<?php echo esc_attr( $group->label ) ?>" class="regular-text" required />
+							<p class="description"><?php esc_html_e('Brief descriptive text for the field group. Shown as a header on user forms.', 'paid-memberships-pro'); ?></p>
+						</td>
+					</tr>
+					<tr>
+						<th scope="row" valign="top"><label for="name"><?php esc_html_e('Name', 'paid-memberships-pro'); ?></label></th>
+						<td>
+							<input type="hidden" name="original_name" value="<?php echo esc_attr( $group->name ); ?>" />
+							<input id="name" name="name" type="text" value="<?php echo esc_attr( $group->name ) ?>" class="regular-text" required />
+							<p class="description"><?php esc_html_e('Single word with no spaces. Underscores are allowed. This is the name to represent the group in code.', 'paid-memberships-pro'); ?></p>
+						</td>
+					</tr>
+					<tr>
+						<th scope="row" valign="top"><label for="description"><?php esc_html_e('Description', 'paid-memberships-pro'); ?></label></th>
+						<td>
+							<textarea id="description" name="description" class="large-text" rows="5"><?php echo esc_textarea( $group->description ); ?></textarea>
+							<p class="description"><?php esc_html_e('Descriptive text for users or admins viewing the field group.', 'paid-memberships-pro'); ?></p>
+						</td>
+					</tr>
+				</tbody>
+			</table>
+		</div> <!-- end pmpro_section_inside -->
+	</div> <!-- end pmpro_section -->
+
+	<div id="visibility-settings" class="pmpro_section" data-visibility="shown" data-activated="true">
+		<div class="pmpro_section_toggle">
+			<button class="pmpro_section-toggle-button" type="button" aria-expanded="true">
+				<span class="dashicons dashicons-arrow-up-alt2"></span>
+				<?php esc_html_e('Visibility Settings', 'paid-memberships-pro'); ?>
+			</button>
+		</div>
+		<div class="pmpro_section_inside">
+			<table class="form-table">
+				<tbody>
+					<tr>
+						<th scope="row" valign="top"><label for="checkout"><?php esc_html_e('Show at Checkout?', 'paid-memberships-pro'); ?></label></th>
+						<td>
+							<select id="checkout" name="checkout">
+								<option value="yes" <?php selected( $group->checkout, 'yes' ); ?>><?php esc_html_e('Yes', 'paid-memberships-pro'); ?></option>
+								<option value="no" <?php selected( $group->checkout, 'no' ); ?>><?php esc_html_e('No', 'paid-memberships-pro'); ?></option>
+							</select>
+						</td>
+					</tr>
+					<tr>
+						<th scope="row" valign="top"><label for="profile"><?php esc_html_e('Show on User Profile?', 'paid-memberships-pro'); ?></label></th>
+						<td>
+							<select id="profile" name="profile">
+								<option value="yes" <?php selected( $group->profile, 'yes' ); ?>><?php esc_html_e('Yes', 'paid-memberships-pro'); ?></option>
+								<option value="admins" <?php selected( $group->profile, 'admins' ); ?>><?php esc_html_e('Yes (only admins)', 'paid-memberships-pro'); ?></option>
+								<option value="no" <?php selected( $group->profile, 'no' ); ?>><?php esc_html_e('No', 'paid-memberships-pro'); ?></option>
+							</select>
+						</td>
+					</tr>
+					<tr>
+						<th scope="row" valign="top"><label for="levels"><?php esc_html_e('Restrict Fields for Membership Levels', 'paid-memberships-pro'); ?></label></th>
+						<td>
+							<div class="pmpro_checkbox_box" <?php if ( count( $levels ) > 3 ) { ?>style="height: 90px; overflow: auto;"<?php } ?>>
+								<?php foreach( $levels as $level ) { ?>
+									<div class="pmpro_clickable">
+										<label>
+											<input type="checkbox" name="levels[]" <?php checked( true, in_array( $level->id, $group->levels ) );?> value="<?php echo esc_attr( $level->id); ?>">
+											<?php echo esc_html( $level->name ); ?>
+										</label>
+									</div>
+								<?php } ?>
+							</div>
+						</td>
+					</tr>
+				</tbody>
+			</table>
+		</div> <!-- end pmpro_section_inside -->
+	</div> <!-- end pmpro_section -->
+
+	<p class="submit">
+		<input name="save" type="submit" class="button button-primary" value="<?php esc_attr_e('Save Group', 'paid-memberships-pro'); ?>" />
+		<input name="cancel" type="button" class="button" value="<?php esc_attr_e('Cancel', 'paid-memberships-pro'); ?>" onclick="location.href='<?php echo esc_url(add_query_arg('page', 'pmpro-userfields', admin_url('admin.php'))); ?>';" />
+	</p>
+</form>
\ No newline at end of file
diff --git a/adminpages/user-fields/field-settings.php b/adminpages/user-fields/field-settings.php
deleted file mode 100644
index 8662216ca..000000000
--- a/adminpages/user-fields/field-settings.php
+++ /dev/null
@@ -1,207 +0,0 @@
-<?php
-
-if ( ! empty( $field ) ) {
-	// Assume field stdClass in format we save to settings.
-	$field_label = $field->label;
-	$field_name = $field->name;
-	$field_type = $field->type;
-	$field_required = $field->required;
-	$field_readonly = $field->readonly;     	
-	$field_profile = $field->profile;
-	$field_wrapper_class = $field->wrapper_class;
-	$field_element_class = $field->element_class;
-	$field_hint = $field->hint;
-	$field_options = $field->options;
-	$field_allowed_file_types = $field->allowed_file_types;
-	$field_max_file_size = $field->max_file_size;
-	$field_default = $field->default;
-} else {
-	// Default field values
-	$field_label = '';
-	$field_name = '';
-	$field_type = '';
-	$field_required = false;
-	$field_readonly = false;
-	$field_profile = '';
-	$field_wrapper_class = '';
-	$field_element_class = '';
-	$field_hint = '';
-	$field_options = '';
-	$field_allowed_file_types = '';
-	$field_max_file_size = '';
-	$field_default = '';
-}
-
-// Other vars
-$levels = pmpro_sort_levels_by_order( pmpro_getAllLevels( true, true ) );
-?>
-<div class="pmpro_userfield-group-field pmpro_userfield-group-field-collapse">
-	<ul class="pmpro_userfield-group-tbody">
-		<li class="pmpro_userfield-group-column-order">
-			<div class="pmpro_userfield-group-buttons">
-				<button type="button" aria-disabled="false" class="pmpro_userfield-group-buttons-button pmpro_userfield-field-buttons-button-move-up" aria-label="<?php esc_attr_e( 'Move up', 'paid-memberships-pro' ); ?>">
-					<span class="dashicons dashicons-arrow-up-alt2"></span>
-				</button>
-				<span class="pmpro_userfield-group-buttons-description"><?php esc_html_e( 'Move Field Up', 'paid-memberships-pro' ); ?></span>
-
-				<button type="button" aria-disabled="false" class="pmpro_userfield-group-buttons-button pmpro_userfield-field-buttons-button-move-down" aria-label="<?php esc_attr_e( 'Move down', 'paid-memberships-pro' ); ?>">
-					<span class="dashicons dashicons-arrow-down-alt2"></span>
-				</button>
-				<span class="pmpro_userfield-group-buttons-description"><?php esc_html_e( 'Move Field Down', 'paid-memberships-pro' ); ?></span>
-			</div> <!-- end pmpro_userfield-group-buttons -->
-		</li>
-		<li class="pmpro_userfield-group-column-label">
-			<span class="pmpro_userfield-label"><?php echo strip_tags( wp_kses_post( $field_label ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span>
-			<div class="pmpro_userfield-field-options">
-				<a class="edit-field" title="<?php esc_attr_e( 'Edit field', 'paid-memberships-pro' ); ?>" href="javascript:void(0);"><?php esc_html_e( 'Edit', 'paid-memberships-pro' ); ?></a> |
-				<a class="duplicate-field" title="<?php esc_attr_e( 'Duplicate field', 'paid-memberships-pro' ); ?>" href="javascript:void(0);"><?php esc_html_e( 'Duplicate', 'paid-memberships-pro' ); ?></a> |
-				<a class="delete-field" title="<?php esc_attr_e( 'Delete field', 'paid-memberships-pro' ); ?>" href="javascript:void(0);"><?php esc_html_e( 'Delete', 'paid-memberships-pro' ); ?></a>
-			</div> <!-- end pmpro_userfield-group-options -->
-		</li>
-		<li class="pmpro_userfield-group-column-name"><?php echo esc_html( $field_name); ?></li>
-		<li class="pmpro_userfield-group-column-type"><?php echo esc_html( $field_type); ?></li>
-	</ul>
-
-	<div class="pmpro_userfield-field-settings" style="display: none;">
-
-		<div class="pmpro_userfield-field-setting">
-			<label>
-				<?php esc_html_e( 'Label', 'paid-memberships-pro' ); ?><br />
-				<input type="text" name="pmpro_userfields_field_label" value="<?php echo esc_attr( $field_label );?>" />                    
-			</label>                
-			<span class="description"><?php esc_html_e( 'Brief descriptive text for the field. Shown on user forms.', 'paid-memberships-pro' ); ?></span>
-		</div> <!-- end pmpro_userfield-field-setting -->
-
-		<div class="pmpro_userfield-field-setting">
-			<label>
-				<?php esc_html_e( 'Name', 'paid-memberships-pro' ); ?><br />
-				<input type="text" name="pmpro_userfields_field_name" value="<?php echo esc_attr( $field_name );?>" />
-			</label>                
-			<span class="description"><?php esc_html_e( 'Single word with no spaces. Underscores are allowed.', 'paid-memberships-pro' ); ?></span>
-		</div> <!-- end pmpro_userfield-field-setting -->
-
-		<div class="pmpro_userfield-field-setting">
-			<label>
-				<?php esc_html_e( 'Type', 'paid-memberships-pro' ); ?><br />
-				<select name="pmpro_userfields_field_type" />
-					<option value="text" <?php selected( $field_type, 'text' ); ?>><?php esc_html_e( 'Text', 'paid-memberships-pro' ); ?></option>
-					<option value="textarea" <?php selected( $field_type, 'textarea' ); ?>><?php esc_html_e( 'Text Area', 'paid-memberships-pro' ); ?></option>
-					<option value="checkbox" <?php selected( $field_type, 'checkbox' ); ?>><?php esc_html_e( 'Checkbox', 'paid-memberships-pro' ); ?></option>
-		<option value="checkbox_grouped" <?php selected( $field_type, 'checkbox_grouped' ); ?>><?php esc_html_e( 'Checkbox Group', 'paid-memberships-pro' ); ?></option>
-					<option value="radio" <?php selected( $field_type, 'radio' ); ?>><?php esc_html_e( 'Radio', 'paid-memberships-pro' ); ?></option>
-					<option value="select" <?php selected( $field_type, 'select' ); ?>><?php esc_html_e( 'Select / Dropdown', 'paid-memberships-pro' ); ?></option>
-					<option value="select2" <?php selected( $field_type, 'select2' ); ?>><?php esc_html_e( 'Select2 / Autocomplete', 'paid-memberships-pro' ); ?></option>
-					<option value="multiselect" <?php selected( $field_type, 'multiselect' ); ?>><?php esc_html_e( 'Multi Select', 'paid-memberships-pro' ); ?></option>
-					<option value="file" <?php selected( $field_type, 'file' ); ?>><?php esc_html_e( 'File', 'paid-memberships-pro' ); ?></option>
-					<option value="number" <?php selected( $field_type, 'number' ); ?>><?php esc_html_e( 'Number', 'paid-memberships-pro' ); ?></option>
-					<option value="date" <?php selected( $field_type, 'date' ); ?>><?php esc_html_e( 'Date', 'paid-memberships-pro' ); ?></option>
-					<option value="readonly" <?php selected( $field_type, 'readonly' ); ?>><?php esc_html_e( 'Read-Only', 'paid-memberships-pro' ); ?></option>
-					<option value="hidden" <?php selected( $field_type, 'hidden' ); ?>><?php esc_html_e( 'Hidden', 'paid-memberships-pro' ); ?></option>
-				</select>
-			</label>                
-		</div> <!-- end pmpro_userfield-field-setting -->
-
-		<div class="pmpro_userfield-field-setting pmpro_userfield-field-setting-dual">
-			<div class="pmpro_userfield-field-setting">
-				<label>
-					<?php esc_html_e( 'Required at Checkout?', 'paid-memberships-pro' ); ?><br />
-					<select name="pmpro_userfields_field_required">
-						<option value="no" <?php selected( $field_required, 'no' );?>><?php esc_html_e( 'No', 'paid-memberships-pro' ); ?></option>
-						<option value="yes" <?php selected( $field_required, 'yes' );?>><?php esc_html_e( 'Yes', 'paid-memberships-pro' ); ?></option>
-					</select>
-				</label>                    
-			</div> <!-- end pmpro_userfield-field-setting -->
-
-			<div class="pmpro_userfield-field-setting">
-				<label>
-					<?php esc_html_e( 'Read Only?', 'paid-memberships-pro' ); ?><br />
-					<select name="pmpro_userfields_field_readonly">
-						<option value="no" <?php selected( $field_readonly, 'no' );?>><?php esc_html_e( 'No', 'paid-memberships-pro' ); ?></option>
-						<option value="yes" <?php selected( $field_readonly, 'yes' );?>><?php esc_html_e( 'Yes', 'paid-memberships-pro' ); ?></option>
-					</select>
-				</label>                    
-			</div> <!-- end pmpro_userfield-field-setting -->
-		</div> <!-- end pmpro_userfield-field-setting-dual -->
-
-		<div class="pmpro_userfield-field-setting">
-			<label>
-				<?php esc_html_e( 'Show field on user profile?', 'paid-memberships-pro' ); ?><br />
-				<select name="pmpro_userfields_field_profile">
-					<option value="" <?php selected( empty( $field_profile ), 0);?>><?php esc_html_e( '[Inherit Group Setting]', 'paid-memberships-pro' ); ?></option>
-					<option value="yes" <?php selected( $field_profile, 'yes' );?>><?php esc_html_e( 'Yes', 'paid-memberships-pro' ); ?></option>
-					<option value="admins" <?php selected( $field_profile, 'admins' );?>><?php esc_html_e( 'Yes (only admins)', 'paid-memberships-pro' ); ?></option>
-					<option value="no" <?php selected( $field_profile, 'no' );?>><?php esc_html_e( 'No', 'paid-memberships-pro' ); ?></option>
-				</select>
-			</label>                
-		</div> <!-- end pmpro_userfield-field-setting -->
-
-		<div class="pmpro_userfield-field-setting pmpro_userfield-field-setting-dual">
-			<div class="pmpro_userfield-field-setting">
-				<label>
-					<?php esc_html_e( 'Field Wrapper Class (optional)', 'paid-memberships-pro' ); ?><br />
-					<input type="text" name="pmpro_userfields_field_class" value="<?php echo esc_attr( $field_wrapper_class );?>" />
-				</label>
-				<span class="description"><?php esc_html_e( 'Assign a custom CSS selector to the field\'s wrapping div', 'paid-memberships-pro' ); ?>.</span>
-			</div> <!-- end pmpro_userfield-field-setting -->
-
-			<div class="pmpro_userfield-field-setting">
-				<label>
-					<?php esc_html_e( 'Field Element Class (optional)', 'paid-memberships-pro' ); ?><br />
-					<input type="text" name="pmpro_userfields_field_divclass" value="<?php echo esc_attr( $field_element_class );?>" />
-				</label>                
-				<span class="description"><?php esc_html_e( 'Assign a custom CSS selector to the field', 'paid-memberships-pro' ); ?></span>
-			</div> <!-- end pmpro_userfield-field-setting -->
-		</div> <!-- end pmpro_userfield-field-setting-dual -->
-
-		<div class="pmpro_userfield-field-setting">
-			<label>
-				<?php esc_html_e( 'Hint (optional)', 'paid-memberships-pro' ); ?><br />
-				<textarea name="pmpro_userfields_field_hint" /><?php echo esc_textarea( $field_hint );?></textarea>
-			</label>                
-			<span class="description"><?php esc_html_e( 'Descriptive text for users or admins submitting the field.', 'paid-memberships-pro' ); ?></span>
-		</div> <!-- end pmpro_userfield-field-setting -->
-
-		<div class="pmpro_userfield-field-setting">
-			<div class="pmpro_userfield-field-setting pmpro_userfield-field-setting-dual">
-				<div class="pmpro_userfield-field-setting">
-					<label>
-						<?php esc_html_e( 'Allowed File Types', 'paid-memberships-pro' ); ?><br />
-						<input type="text" name="pmpro_userfields_field_allowed_file_types" value="<?php echo esc_attr( trim( $field_allowed_file_types ) ); ?>" />
-					</label>
-					<span class="description"><?php esc_html_e( 'Restrict the file type that is allowed to be uploaded. Separate the file types using a comma ",". For example: png,pdf,jpg.', 'paid-memberships-pro' ); ?></span>
-				</div> <!-- end pmpro_userfield-field-setting -->
-				<div class="pmpro_userfield-field-setting">
-					<?php $server_max_upload = wp_max_upload_size() / 1024 / 1024; ?>
-					<label>
-						<?php esc_html_e( 'Max File Size Upload', 'paid-memberships-pro' ); ?><br />
-						<input type="number" name="pmpro_userfields_field_max_file_size" value="<?php echo intval( $field_max_file_size ); ?>" max="<?php echo esc_attr( $server_max_upload ); ?>"/>
-					</label>
-					<span class="description"><?php printf( esc_html__( 'Enter an upload size limit for files in Megabytes (MB) or set it to 0 to use your default server upload limit. Your server upload limit is %s.', 'paid-memberships-pro' ), $server_max_upload . 'MB' ); ?></span>
-				</div> <!-- end pmpro_userfield-field-setting -->
-			</div>
-			<div class="pmpro_userfield-field-setting">
-				<label>
-					<?php esc_html_e( 'Options', 'paid-memberships-pro' ); ?><br />
-					<textarea name="pmpro_userfields_field_options" /><?php echo esc_textarea( $field_options );?></textarea>
-				</label>
-				<span class="description"><?php esc_html_e( 'One option per line. To set separate values and labels, use value:label.', 'paid-memberships-pro' ); ?></span>
-			</div> <!-- end pmpro_userfield-field-setting -->
-			
-			<div class="pmpro_userfield-field-setting">
-				<label>
-					<?php esc_html_e( 'Default Value (optional)', 'paid-memberships-pro' ); ?><br />
-					<input type="text" name="pmpro_userfields_field_default" value="<?php echo esc_attr( $field_default ); ?>" />
-				</label>
-			</div> <!-- end pmpro_userfield-field-setting -->
-		</div>
-
-		<div class="pmpro_userfield-field-actions">            
-			<button name="pmpro_userfields_close_field" class="button button-secondary pmpro_userfields_close_field">
-				<?php esc_html_e( 'Close Field', 'paid-memberships-pro' ); ?>
-			</button> 
-			<button name="pmpro_userfields_delete_field" class="button button-secondary is-destructive">
-				<?php esc_html_e( 'Delete Field', 'paid-memberships-pro' ); ?>
-			</button>           
-		</div> <!-- end pmpro_userfield-field-actions -->
-	</div> <!-- end pmpro_userfield-field-settings -->        
-</div> <!-- end pmpro_userfield-group-field -->
diff --git a/adminpages/user-fields/group-settings.php b/adminpages/user-fields/group-settings.php
deleted file mode 100644
index 2c42a89ed..000000000
--- a/adminpages/user-fields/group-settings.php
+++ /dev/null
@@ -1,132 +0,0 @@
-<?php
-
-if ( ! empty( $group ) ) {
-	// Assume group stdClass in format we save to settings.
-	$group_name = $group->name;
-	$group_show_checkout = $group->checkout;
-	$group_show_profile = $group->profile;
-	$group_description = $group->description;    	
-	$group_levels = $group->levels;
-	$group_fields = $group->fields;
-} else {
-	// Default group settings.
-	$group_name = '';
-	$group_show_checkout = 'yes';
-	$group_show_profile = 'yes';
-	$group_description = '';    	
-	$group_levels = array();
-	$group_fields = array();
-}
-
-// Other vars
-$levels = pmpro_sort_levels_by_order( pmpro_getAllLevels( true, true ) );
-
-// Render field group HTML.
-?>
-<div class="pmpro_userfield-group">
-	<div class="pmpro_userfield-group-header">
-		<div class="pmpro_userfield-group-buttons">
-			<button type="button" aria-disabled="false" class="pmpro_userfield-group-buttons-button pmpro_userfield-group-buttons-button-move-up" aria-label="<?php esc_attr_e( 'Move up', 'paid-memberships-pro' ); ?>">
-				<span class="dashicons dashicons-arrow-up-alt2"></span>
-			</button>
-			<span class="pmpro_userfield-group-buttons-description"><?php esc_html_e( 'Move Group Up', 'paid-memberships-pro' ); ?></span>
-
-			<button type="button" aria-disabled="false" class="pmpro_userfield-group-buttons-button pmpro_userfield-group-buttons-button-move-down" aria-label="<?php esc_attr_e( 'Move down', 'paid-memberships-pro' ); ?>">
-				<span class="dashicons dashicons-arrow-down-alt2"></span>
-			</button>
-			<span id="pmpro_userfield-group-buttons-description-2" class="pmpro_userfield-group-buttons-description"><?php esc_html_e( 'Move Group Down', 'paid-memberships-pro' ); ?></span>
-		</div> <!-- end pmpro_userfield-group-buttons -->
-		<h3>
-			<label>                    
-				<?php esc_html_e( 'Group Name', 'paid-memberships-pro' ); ?>
-				<input type="text" name="pmpro_userfields_group_name" placeholder="<?php esc_attr_e( 'Group Name', 'paid-memberships-pro' ); ?>" value="<?php echo esc_attr( $group_name ); ?>" />
-			</label>                
-		</h3>
-		<button type="button" aria-disabled="false" class="pmpro_userfield-group-buttons-button pmpro_userfield-group-buttons-button-toggle-group" aria-label="<?php esc_attr_e( 'Expand and Edit Group', 'paid-memberships-pro' ); ?>">
-			<span class="dashicons dashicons-arrow-up"></span>
-		</button>
-		<span class="pmpro_userfield-group-buttons-description"><?php esc_html_e( 'Expand and Edit Group', 'paid-memberships-pro' ); ?></span>
-	</div> <!-- end pmpro_userfield-group-header -->
-
-	<div class="pmpro_userfield-inside">
-		<div class="pmpro_userfield-field-settings">
-			
-			<div class="pmpro_userfield-field-setting">
-				<label>
-					<?php esc_html_e( 'Show fields at checkout?', 'paid-memberships-pro' ); ?><br />
-					<select name="pmpro_userfields_group_checkout">
-						<option value="yes" <?php selected( $group_show_checkout, 'yes' ); ?>><?php esc_html_e( 'Yes', 'paid-memberships-pro' ); ?></option>
-						<option value="no" <?php selected( $group_show_checkout, 'no' ); ?>><?php esc_html_e( 'No', 'paid-memberships-pro' ); ?></option>
-					</select>
-				</label>
-			</div> <!-- end pmpro_userfield-field-setting -->
-			
-			<div class="pmpro_userfield-field-setting">
-				<label>
-					<?php esc_html_e( 'Show fields on user profile?', 'paid-memberships-pro' ); ?><br />
-					<select name="pmpro_userfields_group_profile">
-						<option value="yes" <?php selected( $group_show_profile, 'yes' ); ?>><?php esc_html_e( 'Yes', 'paid-memberships-pro' ); ?></option>
-						<option value="admins" <?php selected( $group_show_profile, 'admins' ); ?>><?php esc_html_e( 'Yes (only admins)', 'paid-memberships-pro' ); ?></option>
-						<option value="no" <?php selected( $group_show_profile, 'no' ); ?>><?php esc_html_e( 'No', 'paid-memberships-pro' ); ?></option>
-					</select>
-				</label>
-			</div> <!-- end pmpro_userfield-field-setting -->
-			
-			<div class="pmpro_userfield-field-setting">
-				<label>
-					<?php esc_html_e( 'Description (optional, visible to users)', 'paid-memberships-pro' ); ?><br />
-					<textarea name="pmpro_userfields_group_description"><?php echo esc_textarea( $group_description );?></textarea>
-				</label>
-			</div> <!-- end pmpro_userfield-field-setting -->
-			
-			<div class="pmpro_userfield-field-setting">
-				<?php esc_html_e( 'Restrict Fields for Membership Levels', 'paid-memberships-pro' ); ?><br />
-				<div class="pmpro_checkbox_box" <?php if ( count( $levels ) > 3 ) { ?>style="height: 90px; overflow: auto;"<?php } ?>>
-					<?php foreach( $levels as $level ) { ?>
-						<div class="pmpro_clickable">
-							<label>
-								<input type="checkbox" id="pmpro_userfields_group_membership_<?php echo esc_attr( $level->id); ?>" name="pmpro_userfields_group_membership[]" <?php checked( true, in_array( $level->id, $group_levels ) );?>>
-								<?php echo esc_html( $level->name ); ?>
-							</label>
-						</div>
-					<?php } ?>
-				</div>
-			</div> <!-- end pmpro_userfield-field-setting -->
-		
-		</div> <!-- end pmpro_userfield-field-settings -->
-		
-		<h3><?php esc_html_e( 'Manage Fields in This Group', 'paid-memberships-pro' ); ?></h3>
-		
-		<ul class="pmpro_userfield-group-thead">
-			<li class="pmpro_userfield-group-column-order"><?php esc_html_e( 'Order', 'paid-memberships-pro'); ?></li>
-			<li class="pmpro_userfield-group-column-label"><?php esc_html_e( 'Label', 'paid-memberships-pro'); ?></li>
-			<li class="pmpro_userfield-group-column-name"><?php esc_html_e( 'Name', 'paid-memberships-pro'); ?></li>
-			<li class="pmpro_userfield-group-column-type"><?php esc_html_e( 'Type', 'paid-memberships-pro'); ?></li>
-		</ul>
-		
-		<div class="pmpro_userfield-group-fields">
-			<?php
-				if ( ! empty( $group_fields ) ) {
-					foreach ( $group_fields as $field ) {
-						pmpro_get_field_html( $field );
-					}
-				}
-			?>
-			
-			<!-- end pmpro_userfield-group-fields -->
-		
-		</div> <!-- end pmpro_userfield-inside -->
-
-		<div class="pmpro_userfield-group-actions">
-			<button name="pmpro_userfields_add_field" class="button button-secondary button-hero">
-				<?php
-					/* translators: a plus sign dashicon */
-					printf( esc_html__( '%s Add Field', 'paid-memberships-pro' ), '<span class="dashicons dashicons-plus"></span>' ); ?>
-			</button>
-			<button name="pmpro_userfields_delete_group" class="button button-secondary is-destructive">
-				<?php esc_html_e( 'Delete Group', 'paid-memberships-pro' ); ?>
-			</button>
-		</div> <!-- end pmpro_userfield-group-actions -->
-
-	</div> <!-- end pmpro_userfield-group -->
-</div> <!-- end inside -->
diff --git a/adminpages/user-fields/save-field.php b/adminpages/user-fields/save-field.php
new file mode 100644
index 000000000..e08d0eb89
--- /dev/null
+++ b/adminpages/user-fields/save-field.php
@@ -0,0 +1,107 @@
+<?php
+
+global $pmpro_msg, $pmpro_msgt;
+
+// Make sure that we have a field name being saved.
+if ( empty( $_POST['name' ] ) ) {
+	pmpro_setMessage( __( 'Name is required.', 'paid-memberships-pro' ), -1 );
+}
+
+// Get field attributes and make sure there's a type.
+if ( -1 !== $pmpro_msgt ) {
+	$field = new stdClass();
+	$field->name = pmpro_getParam( 'name', 'POST' );
+	$field->label = pmpro_getParam( 'label', 'POST' );
+	$field->type = pmpro_getParam( 'type', 'POST' );
+	$field->required = pmpro_getParam( 'required', 'POST' );
+	$field->readonly = pmpro_getParam( 'readonly', 'POST' );
+	$field->profile = pmpro_getParam( 'profile', 'POST' );
+	$field->wrapper_class = pmpro_getParam( 'wrapper_class', 'POST' );
+	$field->element_class = pmpro_getParam( 'element_class', 'POST' );
+	$field->hint = pmpro_getParam( 'hint', 'POST', '', 'wp_kses_post' );
+	$field->options = pmpro_getParam( 'options', 'POST', '', 'sanitize_textarea_field' );
+	$field->allowed_file_types = pmpro_getParam( 'allowed_file_types', 'POST' );
+	$field->max_file_size = pmpro_getParam( 'max_file_size', 'POST' );
+	$field->default = pmpro_getParam( 'default', 'POST' );
+
+	if ( empty( $field->type ) ) {
+		pmpro_setMessage( __( 'Type is required.', 'paid-memberships-pro' ), -1 );
+	}
+}
+
+// Make sure that there was a group passed.
+if ( -1 !== $pmpro_msgt ) {
+	$group = pmpro_getParam( 'group', 'POST' );
+	if ( empty( $group ) ) {
+		pmpro_setMessage( __( 'Group is required.', 'paid-memberships-pro' ), -1 );
+	}
+}
+
+// Make sure that the group is valid.
+if ( -1 !== $pmpro_msgt ) {
+	$current_settings = pmpro_get_user_fields_settings();
+	$group_obj = null;
+	foreach ( $current_settings as $group_setting ) {
+		if ( $group_setting->name === $group ) {
+			$group_obj = $group_setting;
+			break;
+		}
+	}
+	if ( empty( $group_obj ) ) {
+		pmpro_setMessage( __( 'Invalid group.', 'paid-memberships-pro' ), -1 );
+	}
+}
+
+// If there are no errors, save the field.
+if ( -1 !== $pmpro_msgt ) {
+	// If the field is already in the group, update it.
+	$found = false;
+	$new_fields = array();
+	foreach ( $group_obj->fields as $key => $field_setting ) {
+		if ( $field_setting->name === $field->name ) {
+			$new_fields[] = $field;
+			$found = true;
+		} else {
+			$new_fields[] = $field_setting;
+		}
+	}
+
+	// If the field was not found, add it.
+	if ( ! $found ) {
+		$new_fields[] = $field;
+	}
+	$group_obj->fields = $new_fields;
+
+	// Delete fields with this name from other groups and update the current group.
+	$new_settings = array();
+	foreach ( $current_settings as $group_setting ) {
+		if ( $group_setting->name === $group ) {
+			$new_settings[] = $group_obj;
+		} else {
+			$new_fields = array();
+			foreach ( $group_setting->fields as $field_setting ) {
+				if ( $field_setting->name !== $field->name ) {
+					$new_fields[] = $field_setting;
+				}
+			}
+			$group_setting->fields = $new_fields;
+			$new_settings[] = $group_setting;
+		}
+	}
+
+	// Save the new settings.
+	update_option( 'pmpro_user_fields_settings', $new_settings );
+
+	// Set the field being edited.
+	$_REQUEST['edit'] = $field->name;
+
+	// Show a success message.
+	pmpro_setMessage( __( 'Field saved.', 'paid-memberships-pro' ) . ' <a href="' . admin_url( 'admin.php?page=pmpro-userfields' ) . '">' . __( 'View All Fields.', 'paid-memberships-pro' ) . '</a>', 'pmpro_success' );
+
+	// Redirect with javascript.
+	?>
+	<script>
+		window.location.href = '?page=pmpro-userfields&success_message=<?php echo urlencode( __( 'Field saved.', 'paid-memberships-pro' ) ); ?>';
+	</script>
+	<?php
+}
diff --git a/adminpages/user-fields/save-group.php b/adminpages/user-fields/save-group.php
new file mode 100644
index 000000000..94713994a
--- /dev/null
+++ b/adminpages/user-fields/save-group.php
@@ -0,0 +1,70 @@
+<?php
+
+global $pmpro_msg, $pmpro_msgt;
+
+// Make sure that we have a group name being saved.
+if ( empty( $_POST['name' ] ) ) {
+	pmpro_setMessage( __( 'Name is required.', 'paid-memberships-pro' ), -1 );
+}
+
+// Get group attributes.
+if ( -1 !== $pmpro_msgt ) {
+	$group = new stdClass();
+	$group->name = pmpro_getParam( 'name', 'POST' );
+	$group->label = pmpro_getParam( 'label', 'POST' );
+	$group->checkout = pmpro_getParam( 'checkout', 'POST' );
+	$group->profile = pmpro_getParam( 'profile', 'POST' );
+	$group->description = pmpro_getParam( 'description', 'POST', '', 'wp_kses_post' );
+	$group->levels = array_map( 'intval', empty( $_POST['levels'] ) ? array() : $_POST['levels'] );
+}
+
+// If the name has changed but there is already another group with that name, show an error.
+if ( -1 !== $pmpro_msgt && $group->name !== $_REQUEST['original_name'] ) {
+	$all_groups = PMPro_Field_Group::get_all();
+	if ( isset( $all_groups[ $group->name ] ) ) {
+		pmpro_setMessage( __( 'A group with that name already exists.', 'paid-memberships-pro' ), -1 );
+	}
+}
+
+// If the group is hidden from both the checkout and profile pages, show an error.
+// This will prevent any fields from being loaded in pmpro_load_user_fields_from_settings().
+if ( -1 !== $pmpro_msgt && 'no' === $group->checkout && 'no' === $group->profile ) {
+	pmpro_setMessage( __( 'Group cannot be hidden from both the checkout and profile pages.', 'paid-memberships-pro' ), -1 );
+}
+
+// If there are no errors, save the group.
+if ( -1 !== $pmpro_msgt ) {
+	$current_settings = pmpro_get_user_fields_settings();
+	$new_settings = array();
+	$added = false;
+	foreach ( $current_settings as $group_setting ) {
+		if ( $group_setting->name === $_REQUEST['original_name'] ) {
+			$group->fields = $group_setting->fields;
+			$new_settings[] = $group;
+			$added = true;
+		} else {
+			$new_settings[] = $group_setting;
+		}
+	}
+
+	// If the group is new, add it to the settings.
+	if ( ! $added ) {
+		$new_settings[] = $group;
+	}
+
+	// Save the new settings.
+	update_option( 'pmpro_user_fields_settings', $new_settings );
+
+	// Set the field being edited.
+	$_REQUEST['edit_group'] = $group->name;
+
+	// Show a success message.
+	pmpro_setMessage( __( 'Group saved.', 'paid-memberships-pro' ) . ' <a href="' . admin_url( 'admin.php?page=pmpro-userfields' ) . '">' . __( 'View All Fields.', 'paid-memberships-pro' ) . '</a>', 'pmpro_success' );
+
+	// Redirect with javascript.
+	?>
+	<script>
+		window.location.href = '?page=pmpro-userfields&success_message=<?php echo urlencode( __( 'Group saved.', 'paid-memberships-pro' ) ); ?>';
+	</script>
+	<?php
+}
diff --git a/adminpages/userfields.php b/adminpages/userfields.php
index ad6db4ef4..e1ce5975a 100644
--- a/adminpages/userfields.php
+++ b/adminpages/userfields.php
@@ -1,165 +1,521 @@
 <?php
-	global $msg, $msgt;
+	global $pmpro_msg, $pmpro_msgt;
 	
 	// Only admins can get this.
 	if ( ! function_exists( 'current_user_can' ) || ( ! current_user_can( 'manage_options' ) && ! current_user_can( 'pmpro_userfields' ) ) ) {
 		die( esc_html__( 'You do not have permissions to perform this action.', 'paid-memberships-pro' ) );
 	}
 
-	// Get all levels regardless of visibility.
-	$levels = pmpro_sort_levels_by_order( pmpro_getAllLevels( true, true ) );
+	// Process form submissions.
+
+	$action = isset( $_REQUEST['action'] ) ? sanitize_text_field( $_REQUEST['action'] ) : false;
+	if ( ! empty( $action ) && ( empty( sanitize_key( $_REQUEST['pmpro_userfields_nonce'] ) ) || ! check_admin_referer( $action, 'pmpro_userfields_nonce' ) ) ) {
+		pmpro_setMessage( __( 'Are you sure you want to do that? Try again.', 'paid-memberships-pro' ), -1 );
+		$action = false;
+	} elseif ( ! empty( $_REQUEST['success_message'] ) ) {
+		pmpro_setMessage( sanitize_text_field( $_REQUEST['success_message'] ), 1 );
+	}
+	switch ( $action ) {
+		case 'save_field':
+			include_once( PMPRO_DIR . '/adminpages/user-fields/save-field.php' );
+			break;
+		case 'delete_field':
+			include_once( PMPRO_DIR . '/adminpages/user-fields/delete-field.php' );
+			break;
+		case 'save_group':
+			include_once( PMPRO_DIR . '/adminpages/user-fields/save-group.php' );
+			break;
+		case 'delete_group':
+			include_once( PMPRO_DIR . '/adminpages/user-fields/delete-group.php' );
+			break;
+	}
 
-	/**
-	 * Save fields if form was submitted.
-	 */
-	if ( ! empty( $_REQUEST['savesettings'] ) ) {
-		// Check nonce.
-		check_admin_referer( 'savesettings', 'pmpro_userfields_nonce' );
 
-		// Note: We sanitize the data below.
-		$groups = json_decode( stripslashes( $_REQUEST['pmpro_user_fields_settings'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+	// Show header.
+	require_once(dirname(__FILE__) . "/admin_header.php");
+	
+	// Show page contents.
+	if ( isset( $_REQUEST['edit'] ) ) {
+		// Editing a field.
+		$edit = sanitize_text_field( $_REQUEST['edit'] );
+		require_once( PMPRO_DIR . '/adminpages/user-fields/edit-field.php' );
+	} elseif ( isset( $_REQUEST['edit_group'] ) ) {
+		// Editing a group.
+		$edit_group = sanitize_text_field( $_REQUEST['edit_group'] );
+		require_once( PMPRO_DIR . '/adminpages/user-fields/edit-group.php' );
+	} else {
+		// Showing the fields list.
+		$groups = PMPro_Field_Group::get_all();
+
+		// Order the groups based on how they would show at checkout.
+		$ordered_groups = array();
+		$pre_checkout_field_locations = array(
+			'after_username',
+			'after_password',
+			'after_pricing_fields',
+			'after_email',
+		);
+		$post_checkout_field_locations = array(
+			'after_billing_fields',
+			'after_captcha',
+			'before_submit_button',
+			'after_tos_fields',
+		);
 
-		// Sanitize everything.
+		// Add all of the "pre-checkout" field groups to the ordered groups array.
+		foreach ( $pre_checkout_field_locations as $location ) {
+			if ( array_key_exists( $location, $groups ) ) {
+				$ordered_groups[] = $groups[ $location ];
+			}
+		}
+
+		// Add all of the "checkout" field groups to the ordered groups array.
 		foreach ( $groups as $group ) {
-			$group->name        = sanitize_text_field( $group->name );
-			$group->checkout    = 'yes' === $group->checkout ? 'yes' : 'no';
-			$group->profile     = sanitize_text_field( $group->profile );
-			$group->description = wp_kses_post( $group->description );
-			$group->levels      = array_map( 'intval', $group->levels );
-			foreach ( $group->fields as $field ) {
-				$field_name 		  = pmpro_format_field_name( $field->name ); //Replace spaces and dashes with underscores.
-				$field->name          = sanitize_text_field( $field_name );
-				$field->label         = wp_kses_post( $field->label );
-				$field->type          = sanitize_text_field( $field->type );
-				$field->required      = 'yes' === $field->required ? 'yes' : 'no';
-				$field->readonly      = 'yes' === $field->readonly ? 'yes' : 'no';
-				$field->profile       = sanitize_text_field( $field->profile );
-				$field->wrapper_class = sanitize_text_field( $field->wrapper_class );
-				$field->element_class = sanitize_text_field( $field->element_class );
-				$field->hint          = wp_kses_post( $field->hint );
-				$field->options       = sanitize_textarea_field( $field->options );
+			if ( ! in_array( $group->name, $pre_checkout_field_locations ) && ! in_array( $group->name, $post_checkout_field_locations ) ) {
+				$ordered_groups[] = $group;
 			}
 		}
 
-		update_option( 'pmpro_user_fields_settings', $groups, false );
-		
-		// Assume success.
-		$msg = true;
-		$msgt = __( 'Your user field settings have been updated.', 'paid-memberships-pro' );
-	}
+		// Add all of the "post-checkout" field groups to the ordered groups array.
+		foreach ( $post_checkout_field_locations as $location ) {
+			if ( array_key_exists( $location, $groups ) ) {
+				$ordered_groups[] = $groups[ $location ];
+			}
+		}
+
+		// Get lists of all groups and fields that were created via UI.
+		$ui_settings = pmpro_get_user_fields_settings();
+		$ui_groups   = array();
+		$ui_fields   = array();
+		if ( is_array( $ui_settings ) ) {
+			foreach ( $ui_settings as $group_setting ) {
+				$ui_groups[] = $group_setting->name;
+				foreach ( $group_setting->fields as $field ) {
+					$ui_fields[] = $field->name;
+				}
+			}
+		}
 
-	/**
-	 * Get the user fields from options.
-	 */
-	$user_fields_settings = pmpro_get_user_fields_settings();
-
-	/**
-	 * Load the common header for admin pages.
-	 *
-	 */
-	require_once( dirname(__FILE__) . '/admin_header.php' );
-
-	// Show warning if there are additional fields that are coded.
-	$num_fields_from_settings = array_sum( array_map( function ($group) { return count( $group->fields ); }, pmpro_get_user_fields_settings() ) ); // Fields from UI settings page.
-	$total_registered_fields = array_sum( array_map( function ($group) { return count( $group->get_fields() ); }, PMPro_Field_Group::get_all() ) ); // All registered fields.
-	if ( $num_fields_from_settings < $total_registered_fields ) {
 		?>
-		<div class="notice notice-warning">
-			<p><?php esc_html_e( 'This website has additional user fields that are set up with code. Coded fields cannot be edited here and will show in addition to the fields set up on this page.', 'paid-memberships-pro' ); ?></p>
-		</div>
-		<?php
-	}
+		<script>
+			jQuery(document).ready(function($) {
 
-	/**
-	 * Meta boxes for User Fields admin page.
-	 *
-	 */
-	add_meta_box(
-		'pmpro_userfields_save',
-		esc_html__( 'Save', 'paid-memberships-pro' ),
-		'pmpro_userfields_save_widget',
-		'memberships_page_pmpro-userfields',
-		'side'
-	);
-	add_meta_box(
-		'pmpro_userfields_help',
-		esc_html__( 'User Fields Help', 'paid-memberships-pro' ),
-		'pmpro_userfields_help_widget',
-		'memberships_page_pmpro-userfields',
-		'side'
-	);
-
-	/**
-	 * Meta box to show a save button and other data.
-	 *
-	 */
-	function pmpro_userfields_save_widget() { ?>
-		<p class="submit">
-			<input id="pmpro_userfields_savesettings" name="savesettings" type="submit" class="button-primary" value="<?php esc_attr_e( 'Save All Changes', 'paid-memberships-pro' ); ?>" disabled/>
-		</p>
-		<?php
-	}
+				// Return a helper with preserved width of cells
+				// from http://www.foliotek.com/devblog/make-table-rows-sortable-using-jquery-ui-sortable/
+				var fixHelper = function(e, ui) {
+					ui.children().each(function() {
+						$(this).width($(this).width());
+					});
+					return ui;
+				};
+
+				$("table.has-sortable-fields tbody").sortable({
+					axis: "y",
+					helper: fixHelper,
+					placeholder: 'testclass',
+					forcePlaceholderSize: true,
+					update: update_level_order,
+					items: "tr.sortable-field"
+				});
+
+				function update_level_order(event, ui) {
+					// Create an array of the field names in the new order.
+					field_order = [];
+					ui.item.parent().find('tr.sortable-field').each(function() {
+						field_order.push( $("td:first", this).text());
+					});
+
+					data = {
+						action: 'pmpro_update_field_order',
+						group: ui.item.closest('.pmpro_section').find('.pmpro-userfields-settings-group-name').val(),
+						ordered_fields: field_order,
+						nonce: '<?php echo esc_attr( wp_create_nonce( 'pmpro_update_field_order' ) ); ?>'
+					};
 
-	/**
-	 * Meta box to show help information.
-	 *
-	 */
-	function pmpro_userfields_help_widget() { ?>
-		<p><?php esc_html_e( 'User fields can be added to the membership checkout form, the frontend user profile edit page, and for admins only on the Edit Member and Edit User screens.', 'paid-memberships-pro' ); ?></p>
-		<p><?php esc_html_e( 'Groups are used to define a collection of fields that should be displayed together under a common heading. Group settings control field locations and membership level visibility.', 'paid-memberships-pro' ); ?></p>
-		<p><a target="_blank" href="https://www.paidmembershipspro.com/documentation/user-fields/create-field-group/?utm_source=plugin&utm_medium=pmpro-userfields&utm_campaign=documentation&utm_content=user-fields"><?php esc_html_e( 'Documentation: User Fields', 'paid-memberships-pro' ); ?></a></p>
+					$.post(ajaxurl, data, function(response) {
+					});
+				}
+
+				$('.pmpro_section-sort-button-move-up').on('click',function(){
+					var current = $(this).closest('.pmpro_section');
+					// Check if the group before this one is sortable.
+					if ( current.prev().hasClass('pmpro_section_sortable') ) {
+						current.prev().before(current);
+						update_level_group_order();
+					}
+				});
+				$('.pmpro_section-sort-button-move-down').on('click',function(){
+					var current = $(this).closest('.pmpro_section');
+					// Check if the group after this one is sortable.
+					if ( current.next().hasClass('pmpro_section_sortable') ) {
+						current.next().after(current);
+						update_level_group_order();
+					}
+				});
+
+				function update_level_group_order(event, ui) {
+					group_order = [];
+					$(".pmpro_section_sortable").each(function() {
+						group_order.push( $(this).find('.pmpro-userfields-settings-group-name').val());
+					});
+					console.log(group_order);
+
+					data = {
+						action: 'pmpro_update_field_group_order',
+						ordered_groups: group_order,
+						nonce: '<?php echo esc_attr( wp_create_nonce( 'pmpro_update_field_group_order' ) ); ?>'
+					};
+
+					$.post(ajaxurl, data, function(response) {
+					});
+				}
+			});
+		</script>
 		<?php
-	}
 
-	?>		
-	<hr class="wp-header-end">
-	<h1><?php esc_html_e( 'User Fields', 'paid-memberships-pro' ); ?></h1>
 
-	<div id="poststuff">
-		<div id="post-body" class="metabox-holder columns-2">
+		// Check if there are multiple fields with the same name. If so, show an error.
+		// TODO
 
-			<div id="post-body-content">
-				<div class="inside">
+		?>
+		<hr class="wp-header-end">
+		<?php if( count( $ordered_groups ) === 0 ) { ?>
+			<div class="pmpro-new-install">
+				<h2><?php esc_html_e( 'No Field Groups Found', 'paid-memberships-pro' ); ?></h2>
+				<a href="#" class="button-primary"><?php esc_html_e( 'Create a Group', 'paid-memberships-pro' ); ?></a>
+			</div> <!-- end pmpro-new-install -->
+		<?php } else { ?>
+			<h1 class="wp-heading-inline"><?php esc_html_e( 'User Fields', 'paid-memberships-pro' ); ?></h1>
 
-					<?php						
-						foreach( $user_fields_settings as $group ) {
-							pmpro_get_field_group_html( $group );
-						}
+			<?php
+				// Build the page action links to return.
+				$pmpro_membershiplevels_page_action_links = array();
+
+				// Add New Group link
+				$pmpro_membershiplevels_page_action_links['add-new-group'] = array(
+					'url' => add_query_arg( array( 'edit_group' => '-1' ), admin_url( 'admin.php?page=pmpro-userfields' ) ),
+					'name' => __( 'Add New Group', 'paid-memberships-pro' ),
+					'icon' => 'plus'
+				);
+
+				// Display the links.
+				foreach ( $pmpro_membershiplevels_page_action_links as $pmpro_membershiplevels_page_action_link ) {
+					
+					// If the value is not an array, it is not in the correct format. Continue.
+					if ( ! is_array( $pmpro_membershiplevels_page_action_link ) ) {
+						continue;
+					}
+
+					// Figure out CSS classes for the links.
+					$classes = array();
+					$classes[] = 'page-title-action';
+					if ( ! empty( $pmpro_membershiplevels_page_action_link['icon'] ) ) {
+						$classes[] = 'pmpro-has-icon';
+						$classes[] = 'pmpro-has-icon-' . esc_attr( $pmpro_membershiplevels_page_action_link['icon'] );
+					}
+					if ( ! empty( $pmpro_membershiplevels_page_action_link['classes'] ) ) {
+						$classes[] = $pmpro_membershiplevels_page_action_link['classes'];
+					}
+					$class = implode( ' ', array_unique( $classes ) );
 					?>
+					<a class="<?php echo esc_attr( $class ); ?>" href="<?php echo $pmpro_membershiplevels_page_action_link['url']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>"><?php echo esc_html( $pmpro_membershiplevels_page_action_link['name'] ); ?></a>
+					<?php
+				}
+			?>
+			<p><?php esc_html_e('Drag and drop fields within the group to reorder them. Reorder groups using the up/down arrows.', 'paid-memberships-pro' ); ?></p>
+			<?php
+			// Show the settings page message.
+			if (!empty($pmpro_msg)) { ?>
+				<div class="inline notice notice-large <?php echo $pmpro_msgt > 0 ? 'notice-success' : 'notice-error'; ?>">
+					<p><?php echo wp_kses_post( $pmpro_msg ); ?></p>
+				</div>
+			<?php }
+			?>
+			<div id="pmpro-userfields-groups">
+				<?php
+				foreach ( $ordered_groups as $group ) {
+					$group_can_be_moved = ! in_array( $group->name, $pre_checkout_field_locations ) && ! in_array( $group->name, $post_checkout_field_locations ) && in_array( $group->name, $ui_groups );
+					?>
+					<div id="pmpro-userfields-settings-group-div-<?php echo esc_attr( $group->name ); ?>" class="pmpro_section <?php echo $group_can_be_moved ? 'pmpro_section_sortable' : ''; ?>" data-visibility="shown" data-activated="true">
+						<div class="pmpro_section_toggle">
+							<?php
+							// Enable moving groups for those that allow it.
+							if ( $group_can_be_moved ) {
+								?>
+									<div class="pmpro_section-sort">
+									<button type="button" aria-disabled="false" class="pmpro_section-sort-button pmpro_section-sort-button-move-up" aria-label="<?php esc_attr_e( 'Move up', 'paid-memberships-pro' ); ?>">
+										<span class="dashicons dashicons-arrow-up-alt2"></span>
+									</button>
+									<span class="pmpro_section-sort-button-description"><?php esc_html_e( 'Move Group Up', 'paid-memberships-pro' ); ?></span>
 
-					<p class="text-center">
-						<button id="pmpro_userfields_add_group" name="pmpro_userfields_add_group" class="button button-primary button-hero">
+									<button type="button" aria-disabled="false" class="pmpro_section-sort-button pmpro_section-sort-button-move-down" aria-label="<?php esc_attr_e( 'Move down', 'paid-memberships-pro' ); ?>">
+										<span class="dashicons dashicons-arrow-down-alt2"></span>
+									</button>
+									<span id="pmpro_section-sort-button-description-2" class="pmpro_section-sort-button-description"><?php esc_html_e( 'Move Group Down', 'paid-memberships-pro' ); ?></span>
+								</div> <!-- end pmpro_section-sort -->
+								<?php
+							} else {
+								// Show a lock icon for groups that are not sortable.
+								?>
+								<div class="pmpro_section-sort">
+									<span class="dashicons dashicons-lock"></span>
+								</div>
+								<?php
+							}
+							?>
+							<button class="pmpro_section-toggle-button" type="button" aria-expanded="true">
+								<span class="dashicons dashicons-arrow-up-alt2"></span>
+								<input type="hidden" class="pmpro-userfields-settings-group-name" value="<?php echo esc_attr( $group->name ); ?>" />
+								<?php echo esc_html( $group->label ) . '<small>'. sprintf( esc_html__( 'Name: %s', 'paid-memberships-pro' ), esc_html( $group->name ) ) . '</small>'; ?>
+							</button>
+						</div>
+						<div class="pmpro_section_inside">
 							<?php
-								/* translators: a plus sign dashicon */
-								printf( esc_html__( '%s Add Field Group', 'paid-memberships-pro' ), '<span class="dashicons dashicons-plus"></span>' ); ?>
-						</button>
-					</p>
-
-				</div> <!-- end inside -->
-			</div> <!-- end post-body-content -->
-
-			<div id="postbox-container-1" class="postbox-container">
-				<form action="" method="post" enctype="multipart/form-data">
-					<?php wp_nonce_field('savesettings', 'pmpro_userfields_nonce');?>
-					<?php do_meta_boxes( 'memberships_page_pmpro-userfields', 'side', '' ); ?>
-					<?php wp_nonce_field('closedpostboxes', 'closedpostboxesnonce', false ); ?>
-					<?php wp_nonce_field('meta-box-order', 'meta-box-order-nonce', false ); ?>
-					<input type="hidden" id="pmpro_user_fields_settings" name="pmpro_user_fields_settings" value="<?php echo esc_attr( json_encode( $user_fields_settings ) );?>" />
-				</form>
-			</div> <!-- end postbox-container-1 -->
-
-		</div> <!-- end post-body -->
-	</div> <!-- end poststuff -->		
-	<script type="text/javascript">
-	  //<![CDATA[
-	  jQuery(document).ready( function($) {
-		  // close postboxes that should be closed
-		  $('.if-js-closed').removeClass('if-js-closed').addClass('closed');
-		  // postboxes setup
-		  postboxes.add_postbox_toggles('admin_page_pmpro-userfields');
-	  });
-	  //]]>
-	</script>
+							// Check if we have settings for this group.
+							if ( ! empty( $group->description ) ) {
+								?>
+								<p><?php echo wp_kses_post( $group->description ); ?></p>
+								<?php
+							}
+							if ( in_array( $group->name, $ui_groups ) ) {
+								?>
+								<p class="description"><?php esc_html_e( 'You can edit if fields in this group are shown at checkout and their level restrictions by clicking the "Edit Group" button.', 'paid-memberships-pro' ); ?></p>
+								<?php
+							} else {
+								?>
+								<p class="description"><?php esc_html_e( 'This group was added via custom code.', 'paid-memberships-pro' ); ?></p>
+								<?php
+							}
 
-<?php
+							// Get the fields for this group.
+							$group_fields = $group->get_fields();
+							$has_sortable_fields = count( array_intersect( $ui_fields, wp_list_pluck( $group_fields, 'name' ) ) ) > 1;
+							?>
+							<table class="widefat <?php if ( $has_sortable_fields  ) { ?> has-sortable-fields<?php } ?>">
+								<thead>
+									<tr>
+										<th><?php esc_html_e('Name', 'paid-memberships-pro' );?></th>
+										<th><?php esc_html_e('Label', 'paid-memberships-pro' );?></th>
+										<th><?php esc_html_e('Show in Profile?', 'paid-memberships-pro' );?></th>
+										<th><?php esc_html_e('Show at Checkout?', 'paid-memberships-pro' );?></th>
+										<th><?php esc_html_e('Required at Checkout?', 'paid-memberships-pro' );?></th>
+										<th><?php esc_html_e('Level Restrictions', 'paid-memberships-pro' );?></th>
+										<th><?php esc_html_e('Preview', 'paid-memberships-pro' );?></th>
+									</tr>
+								</thead>
+								<tbody>
+									<?php
+									if ( empty( $group_fields ) ) {
+										?>
+										<tr>
+											<td colspan="3">
+												<?php
+												esc_html_e( 'No user fields found.', 'paid-memberships-pro' );
+												?>
+											</td>
+										</tr>
+										<?php
+									}
+									foreach ( $group_fields as $field ) {
+									?>
+									<tr class="<?php if ( in_array( $field->name, $ui_fields ) ) { ?>sortable-field<?php } ?>">
+										<td><?php echo esc_html( $field->name );?></td>
+										<?php
+										if ( in_array( $field->name, $ui_fields ) ) {
+											?>
+											<td class="has-row-actions">
+												<span class="field-label"><a href="<?php echo esc_url( add_query_arg( array( 'page' => 'pmpro-userfields', 'edit' => $field->name ), admin_url( 'admin.php' ) ) ); ?>"><?php echo esc_html( $field->label ); ?></a></span>
+												<div class="row-actions">
+													<?php
+													$delete_text = esc_html(
+														sprintf(
+															// translators: %s is the Level Name.
+															__( "Are you sure you want to the %s field?", 'paid-memberships-pro' ),
+															$field->label
+														)
+													);
+
+													$delete_nonce_url = wp_nonce_url(
+														add_query_arg(
+															[
+																'page'   => 'pmpro-userfields',
+																'action' => 'delete_field',
+																'delete_name' => $field->name,
+															],
+															admin_url( 'admin.php' )
+														),
+														'delete_field',
+														'pmpro_userfields_nonce'
+													);
+
+													$actions = [
+														'edit'   => sprintf(
+															'<a title="%1$s" href="%2$s">%3$s</a>',
+															esc_attr__( 'Edit', 'paid-memberships-pro' ),
+															esc_url(
+																add_query_arg(
+																	[
+																		'page' => 'pmpro-userfields',
+																		'edit' => $field->name,
+																	],
+																	admin_url( 'admin.php' )
+																)
+															),
+															esc_html__( 'Edit', 'paid-memberships-pro' )
+														),
+														'delete' => sprintf(
+															'<a title="%1$s" href="%2$s">%3$s</a>',
+															esc_attr__( 'Delete', 'paid-memberships-pro' ),
+															'javascript:pmpro_askfirst(\'' . esc_js( $delete_text ) . '\', \'' . esc_js( $delete_nonce_url ) . '\'); void(0);',
+															esc_html__( 'Delete', 'paid-memberships-pro' )
+														),
+													];
+
+													$actions_html = [];
+
+													foreach ( $actions as $action => $link ) {
+														$actions_html[] = sprintf(
+															'<span class="%1$s">%2$s</span>',
+															esc_attr( $action ),
+															$link
+														);
+													}
+
+													if ( ! empty( $actions_html ) ) {
+														echo implode( ' | ', $actions_html ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
+													}
+													?>
+												</div>
+											</td>
+											<?php
+										} else {
+											?>
+											<td><?php echo esc_html( $field->label ); ?></td>
+											<?php
+										}
+										?>
+										<td>
+											<?php
+											if ( in_array( $field->profile, array( true, 'only' ), true ) ) {
+												esc_html_e( 'Yes', 'paid-memberships-pro' );
+											} elseif ( in_array( $field->profile, array( 'admin', 'only_admin' ), true ) ) {
+												esc_html_e( 'Yes (only admins)', 'paid-memberships-pro' );
+											} else {
+												esc_html_e( 'No', 'paid-memberships-pro' );
+											}
+											?>
+										</td>
+										<td>
+											<?php
+											if ( in_array( $field->profile, array( 'only', 'only_admin' ), true ) ) {
+												esc_html_e( 'No', 'paid-memberships-pro' );
+											} else {
+												esc_html_e( 'Yes', 'paid-memberships-pro' );
+											}
+											?>
+										</td>
+										<td><?php echo $field->required ? 'Yes' : 'No';?></td>
+										<td>
+											<?php
+											if ( empty( $field->levels ) ) {
+												esc_html_e( 'No Level Restrictions', 'paid-memberships-pro' );
+											} elseif ( 3 >= count( $field->levels ) ) {
+												$level_names = array();
+												foreach ( $field->levels as $level_id ) {
+													$level = pmpro_getLevel( $level_id );
+													if ( ! empty( $level ) ) {
+														$level_names[] = $level->name;
+													}
+												}
+												echo esc_html( implode( ', ', $level_names ) );
+											} else {
+												// Show a preview with a button to expand and show all levels.
+												$level_names = array();
+												foreach ( $field->levels as $level_id ) {
+													$level = pmpro_getLevel( $level_id );
+													if ( ! empty( $level ) ) {
+														$level_names[] = $level->name;
+													}
+												}
+												$preview_levels = array_slice( $level_names, 0, 3 );
+												echo esc_html( implode( ', ', $preview_levels ) ) . '...';
+												?>
+												<span class="pmpro-level-restrictions-preview">
+													<a href="#" class="pmpro-level-restrictions-preview-button"><?php esc_html_e( 'Show All', 'paid-memberships-pro' ); ?></a>
+													<span class="pmpro-level-restrictions-preview-list" style="display: none;">
+														<?php echo esc_html( implode( ', ', $level_names ) ); ?>
+													</span>
+												</span>
+												<?php
+											}
+											?>
+										</td>
+										<td>
+											<?php
+											$field->display( empty( $field->default ) ? null : $field->default );
+											if(!empty($field->hint)) {
+												?>
+												<p class="description"><?php echo wp_kses_post( $field->hint );?></p>
+												<?php
+											}
+											?>
+										</td>
+									</tr>
+									<?php
+										}
+									?>
+								</tbody>
+							</table>
+							<p class="text-center">
+								<a class="button button-primary button-hero" href="<?php echo esc_url( add_query_arg( array( 'edit' => '-1', 'group' => $group->name ), admin_url( 'admin.php?page=pmpro-userfields' ) ) ); ?>">
+									<?php
+										/* translators: a plus sign dashicon */
+										printf( esc_html__( '%s Add New Field', 'paid-memberships-pro' ), '<span class="dashicons dashicons-plus"></span>' ); ?>
+								</a>
+							</p>
+							<?php
+							// If the group was added via UI, show the group actions.
+							if ( in_array( $group->name, $ui_groups ) ) {
+								?>
+								<div class="pmpro_section_actions">
+									<a class="button-secondary pmpro-has-icon pmpro-has-icon-edit" href="<?php echo esc_url( add_query_arg( array( 'edit_group' => $group->name ), admin_url( 'admin.php?page=pmpro-userfields' ) ) ); ?>" ><?php esc_html_e( 'Edit Group', 'paid-memberships-pro' ) ?></a>
+									<?php
+										// Show a button to delete the group (disabled if there are fields in group).
+										$disabled_button = empty( $group_fields) ? '' : 'disabled=disabled';
+										$disabled_message = empty( $group_fields) ? '' : '<span class="description"><em>' . __( 'Delete fields to enable group deletion.', 'paid-memberships-pro' ) . '</em></span>';
+										$delete_link = '#';
+										if ( empty( $group_fields) ) {
+											$delete_url = empty( $group_fields) ? wp_nonce_url( add_query_arg( array( 'delete_name' => $group->name, 'action' => 'delete_group' ), admin_url( 'admin.php?page=pmpro-userfields' ) ), 'delete_group', 'pmpro_userfields_nonce' ) : '#';
+											$delete_text = esc_html(
+												sprintf(
+													// translators: %s is the Group Name.
+													__( "Are you sure you want to delete user field group: %s?", 'paid-memberships-pro' ),
+													$group->label
+												)
+											);
+											$delete_link = 'javascript:pmpro_askfirst(\'' . esc_js( $delete_text ) . '\', \'' . esc_js( esc_url( $delete_url ) ) . '\'); void(0);';
+										}
+									?>
+									<a <?php echo esc_attr( $disabled_button ); ?> class="button is-destructive pmpro-has-icon pmpro-has-icon-trash" href="<?php echo esc_attr( $delete_link ); ?>" ><?php esc_html_e( 'Delete Group', 'paid-memberships-pro' ) ?></a>
+									<?php echo wp_kses_post( $disabled_message ); ?>
+								</div>
+								<?php
+							}
+							?>
+						</div> <!-- end .pmpro_section_inside -->
+					</div> <!-- end .pmpro_section -->
+				<?php }  // Close group loop ?>
+			</div>
+			<p class="text-center">
+				<a class="button button-secondary button-hero" href="<?php echo esc_url( add_query_arg( array( 'edit_group' => '-1' ), admin_url( 'admin.php?page=pmpro-userfields' ) ) ); ?>">
+					<?php
+						/* translators: a plus sign dashicon */
+						printf( esc_html__( '%s Add New Group', 'paid-memberships-pro' ), '<span class="dashicons dashicons-plus"></span>' ); ?>
+				</a>
+			</p>
+			<?php 
+		}
+	}
+
+	// Show footer.
 	require_once(dirname(__FILE__) . "/admin_footer.php");
diff --git a/classes/class-pmpro-field-group.php b/classes/class-pmpro-field-group.php
index 7a94f2185..7f292bdab 100644
--- a/classes/class-pmpro-field-group.php
+++ b/classes/class-pmpro-field-group.php
@@ -173,10 +173,8 @@ public static function get_field( $name ) {
 		}
 
 		foreach ( $pmpro_user_fields as $group_name => $fields ) {
-			foreach ( $fields as $field ) {
-				if ( $field->name === $name ) {
-					return $field;
-				}
+			if ( isset( $fields[ $name ] ) ) {
+				return $fields[ $name ];
 			}
 		}
 
@@ -218,7 +216,7 @@ public function add_field( $field ) {
 		}
 
 		// Add the field to the group.
-		$pmpro_user_fields[ $this->name ][] = $field;
+		$pmpro_user_fields[ $this->name ][ $field->name ] = $field;
 
 		return true;
 	}
diff --git a/includes/fields.php b/includes/fields.php
index d6d953ec4..686690207 100644
--- a/includes/fields.php
+++ b/includes/fields.php
@@ -985,16 +985,20 @@ function pmpro_get_user_fields() {
 // Code for the user fields settings page.
 /**
  * Get field group HTML for settings.
+ *
+ * @deprecated TBD
  */
 function pmpro_get_field_group_html( $group = null ) {
-    include( PMPRO_DIR . '/adminpages/user-fields/group-settings.php' );
+	_deprecated_function( __FUNCTION__, 'TBD' );
 }
  
 /**
  * Get field HTML for settings.
+ *
+ * @deprecated TBD
  */
 function pmpro_get_field_html( $field = null ) {
-	include( PMPRO_DIR . '/adminpages/user-fields/field-settings.php' );
+	_deprecated_function( __FUNCTION__, 'TBD' );
 }
 
 /**
@@ -1005,7 +1009,8 @@ function pmpro_get_field_html( $field = null ) {
 function pmpro_get_user_fields_settings() {
     $default_user_fields_settings = array(
         (object) array(
-            'name' => __( 'More Information', 'paid-memberships-pro' ),
+			'name' => 'more_information',
+            'label' => __( 'More Information', 'paid-memberships-pro' ),
             'checkout' => 'yes',
             'profile' => 'yes',
             'description' => '',
@@ -1019,6 +1024,7 @@ function pmpro_get_user_fields_settings() {
     // Make sure all expected properties are set for each group.
 	foreach ( $settings as $group ) {
 		$group->name = ! empty( $group->name ) ? $group->name : '';
+		$group->label = ! empty( $group->label ) ? $group->label : '';
 		$group->checkout = ! empty( $group->checkout ) ? $group->checkout : 'yes';
 		$group->profile = ! empty( $group->profile ) ? $group->profile : 'yes';
 		$group->description = ! empty( $group->description ) ? $group->description : '';
@@ -1053,7 +1059,7 @@ function pmpro_load_user_fields_from_settings() {
     $settings_groups = pmpro_get_user_fields_settings();
 
     foreach ( $settings_groups as $group ) {
-        $group_obj = PMPro_field_Group::add( $group->name, $group->name, $group->description );
+        $group_obj = PMPro_field_Group::add( $group->name, $group->label, $group->description );
         
         // Figure out profile value. Change 2 settings values into 1 field value.
         if ( $group->checkout === 'yes' ) {
diff --git a/includes/scripts.php b/includes/scripts.php
index 050f88c4c..2098f2149 100644
--- a/includes/scripts.php
+++ b/includes/scripts.php
@@ -137,14 +137,6 @@ function pmpro_admin_enqueue_scripts() {
         $level->formatted_expiration = trim( pmpro_no_quotes( pmpro_getLevelExpiration( $level ) ) );
         $all_levels_formatted_text[$level->id] = $level;
     }
-    // Get HTML for empty field group.
-    ob_start();
-    pmpro_get_field_group_html();
-    $empty_field_group_html = ob_get_clean();
-    // Get HTML for empty field.
-    ob_start();
-    pmpro_get_field_html();
-    $empty_field_html = ob_get_clean();
 
 	wp_localize_script(
 		'pmpro_admin',
@@ -154,8 +146,6 @@ function pmpro_admin_enqueue_scripts() {
 			'all_levels_formatted_text' => $all_levels_formatted_text,
 			'all_level_values_and_labels' => $all_level_values_and_labels,
 			'checkout_url' => pmpro_url( 'checkout' ),
-			'user_fields_blank_group' => $empty_field_group_html,
-			'user_fields_blank_field' => $empty_field_html,
 			// We want the core WP translation so we can check for it in JS.
 			'plugin_updated_successfully_text' => __( 'Plugin updated successfully.' ),
 		)
diff --git a/includes/services.php b/includes/services.php
index d5045250a..8d9743c4d 100644
--- a/includes/services.php
+++ b/includes/services.php
@@ -197,18 +197,127 @@ function pmpro_update_level_group_order() {
 // User fields AJAX.
 /**
  * Callback to draw a field group.
+ *
+ * @deprecated TBD
  */
-function pmpro_userfields_get_group_ajax() {	
-	pmpro_get_field_group_html();
+function pmpro_userfields_get_group_ajax() {
+	_deprecated_function( __FUNCTION__, 'TBD' );
     exit;
 }
 add_action( 'wp_ajax_pmpro_userfields_get_group', 'pmpro_userfields_get_group_ajax' );
  
 /**
  * Callback to draw a field.
+ *
+ * @deprecated TBD
  */
 function pmpro_userfields_get_field_ajax() {
- 	pmpro_get_field_html();
+	_deprecated_function( __FUNCTION__, 'TBD' );
 	exit;
 }
 add_action( 'wp_ajax_pmpro_userfields_get_field', 'pmpro_userfields_get_field_ajax' );
+
+function pmpro_update_field_order() {
+	// only admins can get this
+	if ( ! function_exists( 'current_user_can' ) || ( ! current_user_can( 'manage_options' ) && ! current_user_can( 'pmpro_userfields' ) ) ) {
+		die( esc_html__( 'You do not have permissions to perform this action.', 'paid-memberships-pro' ) );
+	}
+
+	// Check the nonce.
+	if ( ! wp_verify_nonce( sanitize_key( $_REQUEST['nonce'] ), 'pmpro_update_field_order' ) ) {
+		die( esc_html__( 'You do not have permissions to perform this action.', 'paid-memberships-pro' ) );
+	}
+
+	// Get the field group that was reordered and the new order.
+	$field_group = sanitize_text_field( $_REQUEST['group'] );
+	$ordered_fields = array_map( 'sanitize_text_field', $_REQUEST['ordered_fields'] );
+	
+	// Get the current user fields settings.
+	$current_settings = pmpro_get_user_fields_settings();
+
+	// Find the group object that we are reordering.
+	$group = null;
+	foreach ( $current_settings as $group_settings ) {
+		if ( $group_settings->name === $field_group ) {
+			$group = $group_settings;
+			break;
+		}
+	}
+	if ( empty( $group ) ) {
+		die( esc_html__( 'Could not find the group to reorder.', 'paid-memberships-pro' ) );
+	}
+
+	// Create an associative version of $group->fields to make it easier to reorder.
+	$group_field_tmp = array();
+	foreach ( $group->fields as $field ) {
+		$group_field_tmp[ $field->name ] = $field;
+	}
+
+	// Create a reordered version of the fields.
+	$reordered_fields = array();
+	foreach ( $ordered_fields as $field_name ) {
+		if ( isset( $group_field_tmp[ $field_name ] ) ) {
+			$reordered_fields[] = $group_field_tmp[ $field_name ];
+			unset( $group_field_tmp[ $field_name ] );
+		}
+	}
+
+	// If there are any fields left in $group_field_tmp, add them to the end of $reordered_fields.
+	if ( ! empty( $group_field_tmp ) ) {
+		$reordered_fields = array_merge( $reordered_fields, $group_field_tmp );
+	}
+
+	// Update the group with the reordered fields.
+	$group->fields = $reordered_fields;
+
+	// Update the settings with the reordered group.
+	update_option( 'pmpro_user_fields_settings', $current_settings );
+
+    exit;
+}
+add_action('wp_ajax_pmpro_update_field_order', 'pmpro_update_field_order');
+
+
+function pmpro_update_field_group_order() {
+	// only admins can get this
+	if ( ! function_exists( 'current_user_can' ) || ( ! current_user_can( 'manage_options' ) && ! current_user_can( 'pmpro_userfields' ) ) ) {
+		die( esc_html__( 'You do not have permissions to perform this action.', 'paid-memberships-pro' ) );
+	}
+
+	// Check the nonce.
+	if ( ! wp_verify_nonce( sanitize_key( $_REQUEST['nonce'] ), 'pmpro_update_field_group_order' ) ) {
+		die( esc_html__( 'You do not have permissions to perform this action.', 'paid-memberships-pro' ) );
+	}
+
+	// Get the new order.
+	$ordered_groups = array_map( 'sanitize_text_field', $_REQUEST['ordered_groups'] );
+
+	// Get the current user fields settings.
+	$current_settings = pmpro_get_user_fields_settings();
+
+	// Create an associative version of $current_settings to make it easier to reorder.
+	$current_settings_tmp = array();
+	foreach ( $current_settings as $group_settings ) {
+		$current_settings_tmp[ $group_settings->name ] = $group_settings;
+	}
+
+	// Create a reordered version of the groups.
+	$reordered_groups = array();
+	foreach ( $ordered_groups as $group_name ) {
+		if ( isset( $current_settings_tmp[ $group_name ] ) ) {
+			$reordered_groups[] = $current_settings_tmp[ $group_name ];
+			unset( $current_settings_tmp[ $group_name ] );
+		}
+	}
+
+	// If there are any groups left in $current_settings_tmp, add them to the end of $reordered_groups.
+	if ( ! empty( $current_settings_tmp ) ) {
+		$reordered_groups = array_merge( $reordered_groups, $current_settings_tmp );
+	}
+
+	// Update the settings with the reordered groups.
+	update_option( 'pmpro_user_fields_settings', $reordered_groups );
+
+	exit;
+}
+add_action('wp_ajax_pmpro_update_field_group_order', 'pmpro_update_field_group_order');
diff --git a/js/pmpro-admin.js b/js/pmpro-admin.js
index 16467385d..49619ecb3 100644
--- a/js/pmpro-admin.js
+++ b/js/pmpro-admin.js
@@ -257,315 +257,39 @@ jQuery(document).ready(function () {
 
 // Function to prep click events.
 function pmpro_userfields_prep_click_events() {
-	// Whenever we make a change, warn the user if they try to navigate away.
-	function pmpro_userfields_made_a_change() {
-		window.onbeforeunload = function () {
-			return true;
-		};
-		jQuery('#pmpro_userfields_savesettings').prop("disabled", false);
-	}
-
-	// Add group button.
-	jQuery('#pmpro_userfields_add_group').unbind('click').on('click', function (event) {
-		jQuery('#pmpro_userfields_add_group').parent('p').before(pmpro.user_fields_blank_group);
-		pmpro_userfields_prep_click_events();
-		jQuery('#pmpro_userfields_add_group').parent('p').prev().find('input').focus().select();
-		pmpro_userfields_made_a_change();
-	});
-
-	// Delete group button.
-	jQuery('.pmpro_userfield-group-actions button[name=pmpro_userfields_delete_group]').unbind('click').on('click', function (event) {
-		var thegroup = jQuery(this).closest('.pmpro_userfield-group');
-		var thename = thegroup.find('input[name=pmpro_userfields_group_name]').val();
-		var answer;
-		if (thename.length > 0) {
-			answer = window.confirm('Delete the "' + thename + '" group?');
-		} else {
-			answer = window.confirm('Delete this group?');
-		}
-		if (answer) {
-			thegroup.remove();
-			pmpro_userfields_made_a_change();
-		}
-	});
+	function update_userfield_type_fields() {
+		// Hide all <tr> elements with the `field_type` class.
+		jQuery('.field_type').hide();
 
-	// Add field button.
-	jQuery('button[name="pmpro_userfields_add_field"]').unbind('click').on('click', function (event) {
-		var thefields = jQuery(event.target).closest('div.pmpro_userfield-group-actions').siblings('div.pmpro_userfield-group-fields');
-		thefields.append(pmpro.user_fields_blank_field);
-		pmpro_userfields_prep_click_events();
-		thefields.children().last().find('.edit-field').click();
-		thefields.children().last().find('input[name="pmpro_userfields_field_label"]').focus().select();
-		pmpro_userfields_made_a_change();
-	});
+		// Get the selected field type.
+		var field_type = jQuery('.pmpro_admin-pmpro-userfields select[name=type]').val();
 
-	// Delete field button.
-	jQuery('.pmpro_userfield-field-options a.delete-field, .pmpro_userfield-field-actions .is-destructive').unbind('click').on('click', function (event) {
-		var thefield = jQuery(this).closest('.pmpro_userfield-group-field');
-		var thelabel = thefield.find('input[name=pmpro_userfields_field_label]').val();
-		var answer;
-		if (thelabel.length > 0) {
-			answer = window.confirm('Delete the "' + thelabel + '" field?');
-		} else {
-			answer = window.confirm('Delete this unlabeled field?');
-		}
-		if (answer) {
-			thefield.remove();
-			pmpro_userfields_made_a_change();
-		}
-	});
-
-	// Toggle groups.    
-	jQuery('button.pmpro_userfield-group-buttons-button-toggle-group, div.pmpro_userfield-group-header h3').unbind('click').on('click', function (event) {
-		event.preventDefault();
-
-		// Ignore if the text field was clicked.        
-		if (jQuery(event.target).prop('nodeName') === 'INPUT') {
-			return;
-		}
-
-		// Find the toggle button and open or close.
-		let thebutton = jQuery(event.target).parents('.pmpro_userfield-group').find('button.pmpro_userfield-group-buttons-button-toggle-group');
-		let buttonicon = thebutton.children('.dashicons');
-		let groupheader = thebutton.closest('.pmpro_userfield-group-header');
-		let groupinside = groupheader.siblings('.pmpro_userfield-inside');
-
-		if (buttonicon.hasClass('dashicons-arrow-up')) {
-			// closing
-			buttonicon.removeClass('dashicons-arrow-up');
-			buttonicon.addClass('dashicons-arrow-down');
-			groupinside.slideUp();
-		} else {
-			// opening
-			buttonicon.removeClass('dashicons-arrow-down');
-			buttonicon.addClass('dashicons-arrow-up');
-			groupinside.slideDown();
-		}
-	});
-
-	// Move group up.
-	jQuery('.pmpro_userfield-group-buttons-button-move-up').unbind('click').on('click', function (event) {
-		var thegroup = jQuery(this).closest('.pmpro_userfield-group');
-		var thegroupprev = thegroup.prev('.pmpro_userfield-group');
-		if (thegroupprev.length > 0) {
-			thegroup.insertBefore(thegroupprev);
-			pmpro_userfields_made_a_change();
-		}
-	});
-
-	// Move group down.
-	jQuery('.pmpro_userfield-group-buttons-button-move-down').unbind('click').on('click', function (event) {
-		var thegroup = jQuery(this).closest('.pmpro_userfield-group');
-		var thegroupnext = thegroup.next('.pmpro_userfield-group');
-		if (thegroupnext.length > 0) {
-			thegroup.insertAfter(thegroupnext);
-			pmpro_userfields_made_a_change();
-		}
-	});
-
-	// Open field.
-	jQuery('a.edit-field').unbind('click').on('click', function (event) {
-		var fieldcontainer = jQuery(this).parents('.pmpro_userfield-group-field');
-		var fieldsettings = fieldcontainer.children('.pmpro_userfield-field-settings');
-
-		fieldcontainer.removeClass('pmpro_userfield-group-field-collapse');
-		fieldcontainer.addClass('pmpro_userfield-group-field-expand');
-		fieldsettings.find('select[name=pmpro_userfields_field_type]').change();
-		fieldsettings.show();
-	});
-
-	// Close field.
-	jQuery('button.pmpro_userfields_close_field').unbind('click').on('click', function (event) {
-		event.preventDefault();
-		var fieldcontainer = jQuery(this).parents('.pmpro_userfield-group-field');
-		var fieldsettings = fieldcontainer.children('.pmpro_userfield-field-settings');
-		var fieldheading = fieldsettings.prev();
-		// Update label, name, and type.
-		fieldheading.find('span.pmpro_userfield-label').html(fieldsettings.find('input[name=pmpro_userfields_field_label]').val().replace(/(<([^>]+)>)/gi, ''));
-		fieldheading.find('li.pmpro_userfield-group-column-name').html(fieldsettings.find('input[name=pmpro_userfields_field_name]').val());
-		fieldheading.find('li.pmpro_userfield-group-column-type').html(fieldsettings.find('select[name=pmpro_userfields_field_type]').val());
-
-		// Toggle
-		fieldcontainer.removeClass('pmpro_userfield-group-field-expand');
-		fieldcontainer.addClass('pmpro_userfield-group-field-collapse');
-		fieldsettings.hide();
-	});
-
-	// Move field up.
-	jQuery('.pmpro_userfield-field-buttons-button-move-up').unbind('click').on('click', function (event) {
-		var thefield = jQuery(this).closest('.pmpro_userfield-group-field');
-		var thefieldprev = thefield.prev('.pmpro_userfield-group-field');
-		if (thefieldprev.length > 0) {
-			thefield.insertBefore(thefieldprev);
-			pmpro_userfields_made_a_change();
-		}
-	});
-
-	// Move field down.
-	jQuery('.pmpro_userfield-field-buttons-button-move-down').unbind('click').on('click', function (event) {
-		var thefield = jQuery(this).closest('.pmpro_userfield-group-field');
-		var thefieldnext = thefield.next('.pmpro_userfield-group-field');
-		if (thefieldnext.length > 0) {
-			thefield.insertAfter(thefieldnext);
-			pmpro_userfields_made_a_change();
-		}
-	});
-
-	// Duplicate field.
-	jQuery('a.duplicate-field').unbind('click').on('click', function (event) {
-		var thefield = jQuery(this).closest('.pmpro_userfield-group-field');
-		thefield.clone(true).insertAfter(thefield); // clone( true ) to clone event handlers.
-		pmpro_userfields_made_a_change();
-	});
+		// Show al the <tr> elements with `field_type_{field_type}` class.
+		jQuery('.field_type_' + field_type).show();
+	}
+	update_userfield_type_fields();
 
 	// Toggle field settings based on type.
-	jQuery('select[name=pmpro_userfields_field_type]').on('change', function (event) {
-		var fieldcontainer = jQuery(this).parents('.pmpro_userfield-group-field');
-		var fieldsettings = fieldcontainer.children('.pmpro_userfield-field-settings');
-		var fieldtype = jQuery(this).val();
-
-		var fieldoptions = fieldsettings.find('textarea[name=pmpro_userfields_field_options]').parents('.pmpro_userfield-field-setting');
-		var fieldfiles = fieldsettings.find('input[name=pmpro_userfields_field_max_file_size]').parents('.pmpro_userfield-field-setting');
-		var fielddefault = fieldsettings.find('input[name=pmpro_userfields_field_default]').parents('.pmpro_userfield-field-setting');
-
-		// Hide all the field settings.
-		fieldoptions.hide();
-		fieldfiles.hide();
-		fielddefault.hide();
-
-		// Show the option field if needed.
-		var optiontypes = ['checkbox_grouped', 'radio', 'select', 'select2', 'multiselect'];
-		if (jQuery.inArray(fieldtype, optiontypes) > -1) {
-			fieldoptions.show();
-		}
-
-		// Show the file field options if needed.
-		if (fieldtype === 'file') {
-			fieldfiles.show();
-		}
-
-		// Show the default field if needed.
-		var defaulttypes = ['text', 'textarea', 'checkbox', 'radio', 'select', 'date', 'readonly', 'hidden', 'number'];
-		if (jQuery.inArray(fieldtype, defaulttypes) > -1) {
-			fielddefault.show();
-		}
+	jQuery('.pmpro_admin-pmpro-userfields select[name=type]').on('change', function (event) {
+		update_userfield_type_fields();
 	});
 
 	// Suggest name after leaving label field.
-	jQuery('input[name=pmpro_userfields_field_label]').on('focusout', function (event) {
-		var fieldcontainer = jQuery(this).parents('.pmpro_userfield-group-field');
-		var fieldsettings = fieldcontainer.children('.pmpro_userfield-field-settings');
-		var fieldname = fieldsettings.find('input[name=pmpro_userfields_field_name]');
-		if (!fieldname.val()) {
-			fieldname.val(jQuery(this).val().toLowerCase().replace(/[^a-z0-9]/gi, '_').replace(/(^\_+|\_+$)/mg, ''));
+	jQuery('.pmpro_admin-pmpro-userfields input[name=label]').on('focusout', function (event) {
+		// Check if the "name" field is empty and a text field.
+		var name = jQuery('.pmpro_admin-pmpro-userfields input[name=name]').val();
+		var label = jQuery('.pmpro_admin-pmpro-userfields input[name=label]').val();
+		if ( ! name && label ) {
+			// Generate a name based on the label.
+			name = label.toLowerCase().replace(/[^a-z0-9]/gi, '_').replace(/(^\_+|\_+$)/mg, '');
+			jQuery('.pmpro_admin-pmpro-userfields input[name=name]').val(name);
 		}
 	});
 
-	// If we change a field, mark it as changed.
-	jQuery('.pmpro_userfield-group input, .pmpro_userfield-group textarea, .pmpro_userfield-group select').on('change', function (event) {
-		pmpro_userfields_made_a_change();
-	});
-
-	// Save User Field Settings
-	jQuery('#pmpro_userfields_savesettings').unbind('click').on('click', function (event) {
-		///event.preventDefault();
-		// We have saved, so we no longer need to warn user if they try to navigate away.
-		window.onbeforeunload = null;
-
-		let field_groups = [];
-		let group_names = [];
-		let default_group_name = 'More Information';
-
-		jQuery('.pmpro_userfield-group').each(function (index, value) {
-			let group_name = jQuery(this).find('input[name=pmpro_userfields_group_name]').val();
-
-			// Make sure name is not blank.
-			if (group_name.length === 0) {
-				group_name = default_group_name;
-			}
-			// Make sure name is unique.
-			let count = 1;
-			while (group_names.includes(group_name)) {
-				count++;
-				group_name = group_name.replace(/\(0-9*\)/, '');
-				group_name = group_name + ' (' + String(count) + ')';
-			}
-			group_names.push(group_name);
-
-			let group_checkout = jQuery(this).find('select[name=pmpro_userfields_group_checkout]').val();
-			let group_profile = jQuery(this).find('select[name=pmpro_userfields_group_profile]').val();
-			let group_description = jQuery(this).find('textarea[name=pmpro_userfields_group_description]').val();
-
-			// Get level ids.            
-			let group_levels = [];
-			jQuery(this).find('input[name="pmpro_userfields_group_membership[]"]:checked').each(function () {
-				group_levels.push(parseInt(jQuery(this).attr('id').replace('pmpro_userfields_group_membership_', '')));
-			});
-
-			// Get fields.
-			let group_fields = [];
-			jQuery(this).find('div.pmpro_userfield-group-fields div.pmpro_userfield-field-settings').each(function () {
-				let field_label = jQuery(this).find('input[name=pmpro_userfields_field_label]').val();
-				let field_name = jQuery(this).find('input[name=pmpro_userfields_field_name]').val();
-				let field_type = jQuery(this).find('select[name=pmpro_userfields_field_type]').val();
-				let field_required = jQuery(this).find('select[name=pmpro_userfields_field_required]').val();
-				let field_readonly = jQuery(this).find('select[name=pmpro_userfields_field_readonly]').val();
-				let field_profile = jQuery(this).find('select[name=pmpro_userfields_field_profile]').val();
-				let field_wrapper_class = jQuery(this).find('input[name=pmpro_userfields_field_class]').val();
-				let field_element_class = jQuery(this).find('input[name=pmpro_userfields_field_divclass]').val();
-				let field_hint = jQuery(this).find('textarea[name=pmpro_userfields_field_hint]').val();
-				let field_options = jQuery(this).find('textarea[name=pmpro_userfields_field_options]').val();
-				let field_allowed_file_types = jQuery(this).find('input[name=pmpro_userfields_field_allowed_file_types]').val();
-				let field_max_file_size = jQuery(this).find('input[name=pmpro_userfields_field_max_file_size]').val();
-				let field_default = jQuery(this).find('input[name=pmpro_userfields_field_default]').val();
-
-				// Get level ids.            
-				let field_levels = [];
-				jQuery(this).find('input[name="pmpro_userfields_field_levels[]"]:checked').each(function () {
-					field_levels.push(parseInt(jQuery(this).attr('id').replace('pmpro_userfields_field_levels_', '')));
-				});
-
-				let field = {
-					'label': field_label,
-					'name': field_name,
-					'type': field_type,
-					'required': field_required,
-					'readonly': field_readonly,
-					'levels': field_levels,
-					'profile': field_profile,
-					'wrapper_class': field_wrapper_class,
-					'element_class': field_element_class,
-					'hint': field_hint,
-					'options': field_options,
-					'allowed_file_types': field_allowed_file_types,
-					'max_file_size': field_max_file_size,
-					'default': field_default
-				};
-
-				// Add to array. (Only if it has a label or name.)
-				if (field.label.length > 0 || field.name.length > 0) {
-					group_fields.push(field);
-				}
-			});
-
-			// Set up the field group object.
-			let field_group = {
-				'name': group_name,
-				'checkout': group_checkout,
-				'profile': group_profile,
-				'description': group_description,
-				'levels': group_levels,
-				'fields': group_fields
-			};
-
-			// Add to array.
-			field_groups.push(field_group);
-		});
-
-		// console.log( field_groups );
-		jQuery('#pmpro_user_fields_settings').val(JSON.stringify(field_groups));
-
-		return true;
+	jQuery('.pmpro-level-restrictions-preview-button').on('click', function(event) {
+		event.preventDefault();
+		jQuery(this).hide();
+		jQuery(this).next('.pmpro-level-restrictions-preview-list').show();
 	});
 }
 

From 3c73ae14cca16123f25c5673331f720c936efcd7 Mon Sep 17 00:00:00 2001
From: David Parker <dlparker1005@gmail.com>
Date: Thu, 7 Nov 2024 11:16:30 -0500
Subject: [PATCH 6/6] Fixing edge cases

---
 classes/class-pmpro-field.php | 6 +++---
 includes/fields.php           | 4 ++--
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/classes/class-pmpro-field.php b/classes/class-pmpro-field.php
index dbf0603ac..50b0984e7 100755
--- a/classes/class-pmpro-field.php
+++ b/classes/class-pmpro-field.php
@@ -880,7 +880,7 @@ private function getHTML($value = "")
 			$r_end .= "</div>";
 		}
 
-		if ( empty( $value ) && pmpro_is_checkout() ) {
+		if ( '' === $value && pmpro_is_checkout() ) {
 			/**
 			 * Filter to set the default value for a field. The default value will only load if no value is already found.
 			 * 
@@ -894,7 +894,7 @@ private function getHTML($value = "")
 
 		if($this->type == "text")
 		{
-			$r = '<input type="text" id="' . esc_attr( $this->id ) . '" name="' . esc_attr( $this->name ) . '" value="' . esc_attr(wp_unslash($value)) . '" ';
+			$r = '<input type="text" id="' . esc_attr( $this->id ) . '" name="' . esc_attr( $this->name ) . '" value="' . ( is_string( $value ) ? esc_attr(wp_unslash($value) ) : '' ) . '" ';
 			if(!empty($this->size))
 				$r .= 'size="' . esc_attr( $this->size ) . '" ';
 			if(!empty($this->class))
@@ -1120,7 +1120,7 @@ private function getHTML($value = "")
 				$r .= 'readonly="readonly" ';
 			if(!empty($this->html_attributes))
 				$r .= $this->getHTMLAttributes();
-			$r .= '>' . esc_textarea(wp_unslash($value)) . '</textarea>';
+				$r .= '>' . ( ( is_string( $value ) ) ? esc_textarea(wp_unslash($value) ) : '' ) . '</textarea>';
 		}
 		elseif($this->type == "hidden")
 		{
diff --git a/includes/fields.php b/includes/fields.php
index d6d953ec4..acd00238b 100644
--- a/includes/fields.php
+++ b/includes/fields.php
@@ -391,7 +391,7 @@ function pmpro_checkout_user_creation_checks_user_fields( $okay ) {
 			}
 
 			// If the field was filled if needed, skip it.
-			if ( empty( $field->was_filled_if_needed() ) ) {
+			if ( $field->was_filled_if_needed() ) {
 				continue;
 			}
 
@@ -491,7 +491,7 @@ function pmpro_registration_checks_for_user_fields( $okay ) {
 			}
 
 			// If the field was filled if needed, skip it.
-			if ( empty( $field->was_filled_if_needed() ) ) {
+			if ( $field->was_filled_if_needed() ) {
 				continue;
 			}