diff --git bp-groups/bp-groups-classes.php bp-groups/bp-groups-classes.php
index 64c3c3a..a542426 100644
--- bp-groups/bp-groups-classes.php
+++ bp-groups/bp-groups-classes.php
@@ -1367,188 +1367,668 @@ class BP_Groups_Member {
  * API for creating group extensions without having to hardcode the content into
  * the theme.
  *
- * This class must be extended for each group extension and the following methods overridden:
+ * To implement, extend this class. In your constructor, pass an optional array
+ * of arguments to parent::init() to configure your widget. The config array
+ * supports the following values:
+ *   - 'slug' A unique identifier for your extension. This value will be used
+ *     to build URLs, so make it URL-safe
+ *   - 'name' A translatable name for your extension. This value is used to
+       populate the navigation tab, as well as the default titles for admin/
+       edit/create tabs.
+ *   - 'visibility' Set to 'public' (default) for your extension (the main tab
+ *     as well as the widget) to be available to anyone who can access the
+ *     group, 'private' otherwise.
+ *   - 'nav_item_position' An integer explaining where the nav item should
+ *     appear in the tab list
+ *   - 'enable_nav_item' Set to true for your extension's main tab to be
+ *     available to anyone who can access the group.
+ *   - 'nav_item_name' The translatable text you want to appear in the nav tab.
+ *     Defaults to the value of 'name'.
+ *   - 'display_hook' The WordPress action that the widget_display() method is
+ *     hooked to
+ *   - 'template_file' The template file that will be used to load the content
+ *     of your main extension tab. Defaults to 'groups/single/plugins.php'.
+ *   - 'screens' A multi-dimensional array, described below
  *
- * BP_Group_Extension::widget_display(), BP_Group_Extension::display(),
- * BP_Group_Extension::edit_screen_save(), BP_Group_Extension::edit_screen(),
- * BP_Group_Extension::create_screen_save(), BP_Group_Extension::create_screen()
+ * BP_Group_Extension uses the concept of "settings screens". There are three
+ * contexts for settings screens:
+ *   - 'create', which inserts a new step into the group creation process
+ *   - 'edit', which adds a tab for your extension into the Admin section of
+ *     a group
+ *   - 'admin', which adds a metabox to the Groups administration panel in the
+ *     WordPress Dashboard
+ * Each of these settings screens is populated by a pair of methods: one that
+ * creates the markup for the screen, and one that processes form data
+ * submitted from the screen. If your plugin needs screens in all three
+ * contexts, and if the markup and form processing logic will be the same in
+ * each case, you can define two methods to handle all of the screens:
+ *   function settings_screen() {}
+ *   function settings_screen_save() {}
+ * If one or more of your settings screen needs separate logic, you may define
+ * context-specific methods, for example:
+ *   function edit_screen() {}
+ *   function edit_screen_save() {}
+ * BP_Group_Extension will use the more specific methods if they are available.
+ *
+ * You can further customize the settings screens (tab names, etc) by passing
+ * an optional 'screens' parameter to the init array. The format is as follows:
+ *   'screens' => array(
+ *       'create' => array(
+ *	     'slug' => 'foo',
+ *	     'name' => 'Foo',
+ *	     'position' => 55,
+ *	     'screen_callback' => 'my_create_screen_callback',
+ *	     'screen_save_callback' => 'my_create_screen_save_callback',
+ *	 ),
+ *	 'edit' => array( // ...
+ *   ),
+ * Only provide those arguments that you actually want to change from the
+ * default configuration. BP_Group_Extension will do the rest.
+ *
+ * Note that the 'edit' screen accepts an additional parameter: 'submit_text',
+ * which defines the text of the Submit button automatically added to the Edit
+ * screen of the extension (defaults to 'Save Changes'). Also, the 'admin'
+ * screen accepts two additional parameters: 'metabox_priority' and
+ * 'metabox_context'. See the docs for add_meta_box() for more details on these
+ * arguments.
+ *
+ * Prior to BuddyPress 1.7, group extension configurations were set slightly
+ * differently. The legacy method is still supported, though deprecated.
  *
  * @package BuddyPress
  * @subpackage Groups
  * @since BuddyPress (1.1)
  */
 class BP_Group_Extension {
-	var $name = false;
-	var $slug = false;
 
-	// The name/slug of the Group Admin tab for this extension
-	var $admin_name = '';
-	var $admin_slug = '';
+	/**
+	 * @var array Information about this extension's screens
+	 * @since BuddyPress (1.8)
+	 */
+	var $screens = array();
+
+	/**
+	 * @var string The name of the extending class
+	 * @since BuddyPress (1.8)
+	 */
+	var $class_name;
+
+	/**
+	 * @var object A ReflectionClass object of the current extension
+	 * @since BuddyPress (1.8)
+	 */
+	var $class_reflection;
 
-	// The name/slug of the Group Creation tab for this extension
-	var $create_name = '';
-	var $create_slug = '';
+	/**
+	 * @var array Parsed configuration paramaters for the extension
+	 * @since BuddyPress (1.8)
+	 */
+	var $params = array();
 
-	// Will this extension be visible to non-members of a group? Options: public/private
-	var $visibility = 'public';
+	/**
+	 * @var int The id of the current group
+	 * @since BuddyPress (1.8)
+	 */
+	var $group_id;
 
-	var $create_step_position = 81;
-	var $nav_item_position = 81;
+	/**
+	 * @var string The slug of the current extension
+	 */
+	var $slug;
 
 	/**
-	 * @var string Context for the optional admin metabox
-	 * @see https://codex.wordpress.org/Function_Reference/add_meta_box for
-	 *      possible values
-	 * @since BuddyPress (1.7)
+	 * @var string The translatable name of the current extension
 	 */
-	var $admin_metabox_context = 'normal';
+	var $name;
 
 	/**
-	 * @var string Priority for the optional admin menabox
-	 * @see https://codex.wordpress.org/Function_Reference/add_meta_box for
-	 *      possible values
-	 * @since BuddyPress (1.7)
+	 * @var string Whether the extension tab is visible. 'public'
+	 *   or 'private'
 	 */
-	var $admin_metabox_priority = 'core';
+	var $visibility;
 
-	var $enable_create_step = true;
-	var $enable_nav_item = true;
-	var $enable_edit_item = true;
-	var $enable_admin_item = true;
+	/**
+	 * @var int The numeric position of the main nav item
+	 */
+	var $nav_item_position;
 
-	var $nav_item_name = false;
+	/**
+	 * @var bool Whether to show the nav item
+	 */
+	var $enable_nav_item;
 
-	var $display_hook = 'groups_custom_group_boxes';
-	var $template_file = 'groups/single/plugins';
+	/**
+	 * @var string The text of the nav item. Defaults to self::name
+	 */
+	var $nav_item_name;
+
+	/**
+	 * @var string The WP action that self::widget_display() is attached
+	 *   to. Defaults to 'groups_custom_group_boxes'
+	 */
+	var $display_hook;
+
+	/**
+	 * @var string The template file used to load the plugin content.
+	 *   Defaults to 'groups/single/plugins'
+	 */
+	var $template_file;
 
-	// Methods you should override
+	/**
+	 * @var bool Has the extension been initialized?
+	 * @since BuddyPress (1.8)
+	 */
+	protected $initialized = false;
 
-	function display() {}
+	/**
+	 * @var array Extension properties as set by legacy extensions
+	 * @since BuddyPress (1.8)
+	 */
+	protected $legacy_properties = array();
 
-	function widget_display() {}
+	/**
+	 * @var array Extension properties as set by legacy extensions, but
+	 *   converted to match the new format for params
+	 * @since BuddyPress (1.8)
+	 */
+	protected $legacy_properties_converted = array();
 
-	function edit_screen( $group_id = null ) {}
+	/**
+	 * @var array Miscellaneous data as set by the __set() magic method
+	 * @since BuddyPress (1.8)
+	 */
+	protected $data = array();
 
-	function edit_screen_save( $group_id = null ) {}
+	/** Screen Overrides **************************************************/
 
-	function create_screen( $group_id = null ) {}
+	/**
+	 * Screen override methods are how your extension will display content
+	 * and handle form submits. Your extension should only override those
+	 * methods that it needs for its purposes.
+	 */
 
-	function create_screen_save( $group_id = null ) {}
+	// The content of the group tab
+	public function display() {}
 
-	// Private Methods
+	// Content displayed in a widget sidebar, if applicable
+	public function widget_display() {}
 
-	function _register() {
-		global $bp;
+	// *_screen() displays the settings form for the given context
+	// *_screen_save() processes data submitted via the settings form
+	// The settings_* methods are generic fallbacks, which can optionally
+	// be overridden by the more specific edit_*, create_*, and admin_*
+	// versions.
+	public function settings_screen( $group_id = null ) {}
+	public function settings_screen_save( $group_id = null ) {}
+	public function edit_screen( $group_id = null ) {}
+	public function edit_screen_save( $group_id = null ) {}
+	public function create_screen( $group_id = null ) {}
+	public function create_screen_save( $group_id = null ) {}
+	public function admin_screen( $group_id = null ) {}
+	public function admin_screen_save( $group_id = null ) {}
 
-		// If admin/create names and slugs are not provided, they fall back on the main
-		// name and slug for the extension
-		if ( ! $this->admin_name ) {
-			$this->admin_name = $this->name;
+	/** Setup *************************************************************/
+
+	/**
+	 * Initialize the extension, using your config settings
+	 *
+	 * Your plugin should call this method at the very end of its
+	 * constructor, like so:
+	 *
+	 *   public function __construct() {
+	 *       $args = array(
+	 *           'slug' => 'my-group-extension',
+	 *           'name' => 'My Group Extension',
+	 *           // ...
+	 *       );
+	 *
+	 *       parent::init( $args );
+	 *   }
+	 *
+	 * @since BuddyPress (1.8)
+	 * @param array $args See inline definition below for arguments
+	 */
+	public function init( $args = array() ) {
+
+		// Before this init() method was introduced, plugins were
+		// encouraged to set their config directly. For backward
+		// compatibility with these plugins, we detect whether this is
+		// one of those legacy plugins, and parse any legacy arguments
+		// with those passed to init()
+		$this->parse_legacy_properties();
+		$args = $this->parse_args_r( $args, $this->legacy_properties_converted );
+
+		// Parse with defaults
+		$this->params = $this->parse_args_r( $args, array(
+			'slug'                   => '',
+			'name'                   => '',
+			'visibility'             => 'public',
+			'nav_item_position'      => 81,
+			'enable_nav_item'        => true,
+			'nav_item_name'          => false,
+			'display_hook'           => 'groups_custom_group_boxes',
+			'template_file'          => 'groups/single/plugins',
+			'screens'                => $this->get_default_screens(),
+		) );
+
+		$this->initialized = true;
+	}
+
+	/**
+	 * The main setup routine for the extension
+	 *
+	 * This method contains the primary logic for setting up an extension's
+	 * configuration, setting up backward compatibility for legacy plugins,
+	 * and hooking the extension's screen functions into WP and BP.
+	 *
+	 * Marked 'public' because it must be accessible to add_action().
+	 * However, you should never need to invoke this method yourself - it
+	 * is called automatically at the right point in the load order by
+	 * bp_register_group_extension().
+	 *
+	 * @since BuddyPress (1.1)
+	 */
+	public function _register() {
+		// Detect and parse properties set by legacy extensions
+		$this->parse_legacy_properties();
+
+		// Initialize, if necessary. This should only happen for
+		// legacy extensions that don't call parent::init() themselves
+		if ( ! $this->initialized ) {
+			$this->init();
 		}
 
-		if ( ! $this->admin_slug ) {
-			$this->admin_slug = $this->slug;
+		// Set some config values, based on the parsed params
+		$this->group_id          = $this->get_group_id();
+		$this->slug              = $this->params['slug'];
+		$this->name              = $this->params['name'];
+		$this->visibility        = $this->params['visibility'];
+		$this->nav_item_position = $this->params['nav_item_position'];
+		$this->nav_item_name     = $this->params['nav_item_name'];
+		$this->display_hook      = $this->params['display_hook'];
+		$this->template_file     = $this->params['template_file'];
+
+		// Configure 'screens': create, admin, and edit contexts
+		$this->setup_screens();
+
+		// Mirror configuration data so it's accessible to plugins
+		// that look for it in its old locations
+		$this->setup_legacy_properties();
+
+		// Hook the extension into BuddyPress
+		$this->setup_display_hooks();
+		$this->setup_create_hooks();
+		$this->setup_edit_hooks();
+		$this->setup_admin_hooks();
+	}
+
+	/**
+	 * Set up some basic info about the Extension
+	 *
+	 * Here we collect the name of the extending class, as well as a
+	 * ReflectionClass that is used in get_screen_callback() to determine
+	 * whether your extension overrides certain callback methods.
+	 *
+	 * @since BuddyPress (1.8)
+	 */
+	protected function setup_class_info() {
+		if ( is_null( $this->class_name ) ) {
+			$this->class_name = get_class( $this );
 		}
 
-		if ( ! $this->create_name ) {
-			$this->create_name = $this->name;
+		if ( is_null( $this->class_reflection ) ) {
+			$this->class_reflection = new ReflectionClass( $this->class_name );
 		}
+	}
 
-		if ( ! $this->create_slug ) {
-			$this->create_slug = $this->slug;
+	/**
+	 * Get the current group id
+	 *
+	 * Check for:
+	 *   - current group
+	 *   - new group
+	 *   - group admin
+	 *
+	 * @since BuddyPress (1.8)
+	 */
+	public static function get_group_id() {
+		// Usually this will work
+		$group_id = bp_get_current_group_id();
+
+		// On the admin, get the group id out of the $_GET params
+		if ( ! $group_id && is_admin() && isset( $_GET['page'] ) && 'bp-groups' === $_GET['page'] && ! empty( $_GET['gid'] ) ) {
+			$group_id = (int) $_GET['gid'];
 		}
 
-		if ( ! empty( $this->enable_create_step ) ) {
-			// Insert the group creation step for the new group extension
-			$bp->groups->group_creation_steps[ $this->create_slug ] = array(
-				'name'     => $this->create_name,
-				'slug'     => $this->create_slug,
-				'position' => $this->create_step_position,
-			);
+		// This fallback will only be hit when the create step is
+		// very early
+		if ( ! $group_id && bp_get_new_group_id() ) {
+			$group_id = bp_get_new_group_id();
+		}
 
-			// The maybe_ methods check to see whether the create_*
-			// callbacks should be invoked (ie, are we on the
-			// correct group creation step). Hooked in separate
-			// methods because current creation step info not yet
-			// available at this point
-			add_action( 'groups_custom_create_steps', array( $this, 'maybe_create_screen' ) );
-			add_action( 'groups_create_group_step_save_' . $this->create_slug, array( $this, 'maybe_create_screen_save' ) );
-		}
-
-		// When we are viewing a single group, add the group extension nav item
-		if ( bp_is_group() ) {
-			if ( $this->visibility == 'public' || ( $this->visibility != 'public' && $bp->groups->current_group->user_has_access ) ) {
-				if ( $this->enable_nav_item ) {
-					bp_core_new_subnav_item( array(
-						'name' => !$this->nav_item_name ? $this->name : $this->nav_item_name,
-						'slug' => $this->slug,
-						'parent_slug' => $bp->groups->current_group->slug,
-						'parent_url' => bp_get_group_permalink( $bp->groups->current_group ),
-						'position' => $this->nav_item_position,
-						'item_css_id' => 'nav-' . $this->slug,
-						'screen_function' => array( &$this, '_display_hook' ),
-						'user_has_access' => $this->enable_nav_item
-					) );
-
-					// When we are viewing the extension display page, set the title and options title
-					if ( bp_is_current_action( $this->slug ) ) {
-						add_action( 'bp_template_content_header', create_function( '', 'echo "' . esc_attr( $this->name ) . '";' ) );
-						add_action( 'bp_template_title', create_function( '', 'echo "' . esc_attr( $this->name ) . '";' ) );
-					}
-				}
+		// On some setups, the group id has to be fetched out of the
+		// $_POST array
+		// @todo Figure out why this is happening during group creation
+		if ( ! $group_id && isset( $_POST['group_id'] ) ) {
+			$group_id = (int) $_POST['group_id'];
+		}
 
-				// Hook the group home widget
-				if ( ! bp_current_action() && bp_is_current_action( 'home' ) ) {
-					add_action( $this->display_hook, array( &$this, 'widget_display' ) );
-				}
+		return $group_id;
+	}
+
+	/**
+	 * Gather configuration data about your screens
+	 *
+	 * @since BuddyPress (1.8)
+	 */
+	protected function get_default_screens() {
+		$this->setup_class_info();
+
+		$screens = array(
+			'create' => array(),
+			'edit'   => array(),
+			'admin'  => array(
+				'metabox_context'  => 'normal',
+				'metabox_priority' => 'core',
+			),
+		);
+
+		foreach ( $screens as $context => &$screen ) {
+			$screen['enabled'] = true;
+
+			$screen['screen_callback']      = $this->get_screen_callback( $context, 'screen' );
+			$screen['screen_save_callback'] = $this->get_screen_callback( $context, 'screen_save' );
+
+			$screen['name']        = '';
+			$screen['slug']        = '';
+			$screen['position']    = 81;
+			$screen['submit_text'] = __( 'Save Changes', 'buddypress' );
+		}
+
+		return $screens;
+	}
+
+	/**
+	 * Set up screens array based on params
+	 *
+	 * @since BuddyPress (1.8)
+	 */
+	protected function setup_screens() {
+		foreach ( (array) $this->params['screens'] as $context => $screen ) {
+			if ( empty( $screen['slug'] ) ) {
+				$screen['slug'] = $this->slug;
+			}
+
+			if ( empty( $screen['name'] ) ) {
+				$screen['name'] = $this->name;
 			}
+
+			$this->screens[ $context ] = $screen;
 		}
+	}
 
-		// Construct the admin edit tab for the new group extension
-		if ( ! empty( $this->enable_edit_item ) && bp_is_item_admin() ) {
-			add_action( 'groups_admin_tabs', create_function( '$current, $group_slug',
-				'$selected = "";
-				if ( "' . esc_attr( $this->admin_slug ) . '" == $current )
-					$selected = " class=\"current\"";
-				echo "<li{$selected}><a href=\"' . trailingslashit( bp_get_root_domain() . '/' . bp_get_groups_root_slug() . '/{$group_slug}/admin/' . esc_attr( $this->admin_slug ) ) . '\">' . esc_attr( $this->admin_name ) . '</a></li>";'
-			), 10, 2 );
+	/** Display *************************************************************/
 
-			// Catch the edit screen and forward it to the plugin template
-			if ( bp_is_groups_component() && bp_is_current_action( 'admin' ) && bp_is_action_variable( $this->admin_slug, 0 ) ) {
-				$this->edit_screen_save( bp_get_current_group_id() );
+	/**
+	 * Hook this extension's group tab into BuddyPress, if necessary
+	 *
+	 * @since BuddyPress (1.8)
+	 */
+	protected function setup_display_hooks() {
+		if ( ! bp_is_group() ) {
+			return;
+		}
+
+		if ( 'public' != $this->visibility && ! buddypress()->groups->current_group->user_has_access ) {
+			false;
+		}
+
+		if ( $this->enable_nav_item ) {
+			bp_core_new_subnav_item( array(
+				'name'            => ! $this->nav_item_name ? $this->name : $this->nav_item_name,
+				'slug'            => $this->slug,
+				'parent_slug'     => bp_get_current_group_slug(),
+				'parent_url'      => bp_get_group_permalink( groups_get_current_group() ),
+				'position'        => $this->nav_item_position,
+				'item_css_id'     => 'nav-' . $this->slug,
+				'screen_function' => array( &$this, '_display_hook' ),
+				'user_has_access' => $this->enable_nav_item
+			) );
+
+			// When we are viewing the extension display page, set the title and options title
+			if ( bp_is_current_action( $this->slug ) ) {
+				add_action( 'bp_template_content_header', create_function( '', 'echo "' . esc_attr( $this->name ) . '";' ) );
+				add_action( 'bp_template_title', create_function( '', 'echo "' . esc_attr( $this->name ) . '";' ) );
+			}
+		}
 
-				add_action( 'groups_custom_edit_steps', array( &$this, 'call_edit_screen' ) );
+		// Hook the group home widget
+		if ( ! bp_current_action() && bp_is_current_action( 'home' ) ) {
+			add_action( $this->display_hook, array( &$this, 'widget_display' ) );
+		}
+	}
 
-				if ( '' != bp_locate_template( array( 'groups/single/home.php' ), false ) ) {
-					bp_core_load_template( apply_filters( 'groups_template_group_home', 'groups/single/home' ) );
-				} else {
-					add_action( 'bp_template_content_header', create_function( '', 'echo "<ul class=\"content-header-nav\">"; bp_group_admin_tabs(); echo "</ul>";' ) );
-					add_action( 'bp_template_content', array( &$this, 'call_edit_screen' ) );
-					bp_core_load_template( apply_filters( 'bp_core_template_plugin', '/groups/single/plugins' ) );
-				}
+	/**
+	 * Hooks the main display method, and loads the template file
+	 */
+	public function _display_hook() {
+		add_action( 'bp_template_content', array( &$this, 'display' ) );
+		bp_core_load_template( apply_filters( 'bp_core_template_plugin', $this->template_file ) );
+	}
+
+	/** Create *************************************************************/
+
+	/**
+	 * Hook this extension's Create step into BuddyPress, if necessary
+	 *
+	 * @since BuddyPress (1.8)
+	 */
+	protected function setup_create_hooks() {
+		if ( ! $this->is_screen_enabled( 'create' ) ) {
+			return;
+		}
+
+		$screen = $this->screens['create'];
+
+		// Insert the group creation step for the new group extension
+		buddypress()->groups->group_creation_steps[ $screen['slug'] ] = array(
+			'name'     => $screen['name'],
+			'slug'     => $screen['slug'],
+			'position' => $screen['position'],
+		);
+
+		// The maybe_ methods check to see whether the create_*
+		// callbacks should be invoked (ie, are we on the
+		// correct group creation step). Hooked in separate
+		// methods because current creation step info not yet
+		// available at this point
+		add_action( 'groups_custom_create_steps', array( $this, 'maybe_create_screen' ) );
+		add_action( 'groups_create_group_step_save_' . $screen['slug'], array( $this, 'maybe_create_screen_save' ) );
+	}
+
+	/**
+	 * Call the create_screen() method, if we're on the right page
+	 *
+	 * @since 1.8
+	 */
+	public function maybe_create_screen() {
+		if ( bp_is_group_creation_step( $this->screens['create']['slug'] ) ) {
+			call_user_func( $this->screens['create']['screen_callback'], $this->group_id );
+			$this->nonce_field( 'create' );
+
+			// The create screen requires an additional nonce field
+			// due to a quirk in the way the templates are built
+			wp_nonce_field( 'groups_create_save_' . bp_get_groups_current_create_step() );
+		}
+	}
+
+	/**
+	 * Call the create_screen_save() method, if we're on the right page
+	 *
+	 * @since 1.8
+	 */
+	public function maybe_create_screen_save() {
+		if ( bp_is_group_creation_step( $this->screens['create']['slug'] ) ) {
+			$this->check_nonce( 'create' );
+			call_user_func( $this->screens['create']['screen_save_callback'], $this->group_id );
+		}
+	}
+
+	/** Edit *************************************************************/
+
+	/**
+	 * Hook this extension's Edit panel into BuddyPress, if necessary
+	 *
+	 * @since BuddyPress (1.8)
+	 */
+	protected function setup_edit_hooks() {
+		if ( ! $this->is_screen_enabled( 'edit' ) || ! bp_is_item_admin() ) {
+			return;
+		}
+
+		$screen = $this->screens['edit'];
+
+		// Add the tab
+		// @todo BP should be using bp_core_new_subnav_item()
+		add_action( 'groups_admin_tabs', create_function( '$current, $group_slug',
+			'$selected = "";
+			if ( "' . esc_attr( $screen['slug'] ) . '" == $current )
+				$selected = " class=\"current\"";
+			echo "<li{$selected}><a href=\"' . trailingslashit( bp_get_root_domain() . '/' . bp_get_groups_root_slug() . '/{$group_slug}/admin/' . esc_attr( $screen['slug'] ) ) . '\">' . esc_attr( $screen['name'] ) . '</a></li>";'
+		), 10, 2 );
+
+		// Catch the edit screen and forward it to the plugin template
+		if ( bp_is_groups_component() && bp_is_current_action( 'admin' ) && bp_is_action_variable( $screen['slug'], 0 ) ) {
+			$this->call_edit_screen_save( $this->group_id );
+
+			add_action( 'groups_custom_edit_steps', array( &$this, 'call_edit_screen' ) );
+
+			if ( '' != bp_locate_template( array( 'groups/single/home.php' ), false ) ) {
+				bp_core_load_template( apply_filters( 'groups_template_group_home', 'groups/single/home' ) );
+			} else {
+				add_action( 'bp_template_content_header', create_function( '', 'echo "<ul class=\"content-header-nav\">"; bp_group_admin_tabs(); echo "</ul>";' ) );
+				add_action( 'bp_template_content', array( &$this, 'call_edit_screen' ) );
+				bp_core_load_template( apply_filters( 'bp_core_template_plugin', '/groups/single/plugins' ) );
 			}
 		}
+	}
+
+	/**
+	 * Call the edit_screen() method
+	 *
+	 * Previous versions of BP_Group_Extension required plugins to provide
+	 * their own Submit button and nonce fields when building markup. In
+	 * BP 1.8, this requirement was lifted - BP_Group_Extension now handles
+	 * all required submit buttons and nonces.
+	 *
+	 * We put the edit screen markup into an output buffer before echoing.
+	 * This is so that we can check for the presence of a hardcoded submit
+	 * button, as would be present in legacy plugins; if one is found, we
+	 * do not auto-add our own button.
+	 *
+	 * @since BuddyPress (1.8)
+	 */
+	public function call_edit_screen() {
+		ob_start();
+		call_user_func( $this->screens['edit']['screen_callback'], $this->group_id );
+		$screen = ob_get_contents();
+		ob_end_clean();
+
+		echo $this->maybe_add_submit_button( $screen );
+
+		$this->nonce_field( 'edit' );
+	}
+
+	/**
+	 * Check the nonce, and call the edit_screen_save() method
+	 *
+	 * @since BuddyPress (1.8)
+	 */
+	public function call_edit_screen_save() {
+		if ( ! empty( $_POST ) ) {
+			$this->check_nonce( 'edit' );
+			call_user_func( $this->screens['edit']['screen_save_callback'], $this->group_id );
+		}
+	}
+
+	/**
+	 * Add a submit button to the edit form, if it needs one
+	 *
+	 * There's an inconsistency in the way that the group Edit and Create
+	 * screens are rendered: the Create screen has a submit button built
+	 * in, but the Edit screen does not. This function allows plugin
+	 * authors to write markup that does not contain the submit button for
+	 * use on both the Create and Edit screens - BP will provide the button
+	 * if one is not found.
+	 *
+	 * @since BuddyPress (1.8)
+	 * @param string $screen The screen markup, captured in the output buffer
+	 * @param string $screen The same markup, with a submit button added
+	 */
+	protected function maybe_add_submit_button( $screen ) {
+		if ( ! $this->has_submit_button( $screen ) ) {
+			$screen .= sprintf(
+				'<div id="%s"><input type="submit" name="save" value="%s" id="%s"></div>',
+				'bp-group-edit-' . $this->slug . '-submit-wrapper',
+				$this->screens['edit']['submit_text'],
+				'bp-group-edit-' . $this->slug . '-submit'
+			);
+		}
+
+		return $screen;
+	}
+
+	/**
+	 * Does the given markup have a submit button?
+	 *
+	 * @since BuddyPress (1.8)
+	 * @param $screen The markup to check
+	 * @return bool
+	 */
+	public static function has_submit_button( $screen ) {
+		$pattern = "/<input[^>]+type=[\'\"]submit[\'\"]/";
+		preg_match( $pattern, $screen, $matches );
+		return ! empty( $matches[0] );
+	}
+
+	/** Admin *************************************************************/
 
-		// Construct the admin metabox
-		// Plugin authors: Note that $this->enable_admin_item must be
-		// set to true, and self::admin_screen() must be defined
-		if ( ! empty( $this->enable_admin_item ) && is_admin() && method_exists( get_class( $this ), 'admin_screen' ) ) {
+	/**
+	 * Hook this extension's Admin metabox into BuddyPress, if necessary
+	 *
+	 * @since BuddyPress (1.8)
+	 */
+	protected function setup_admin_hooks() {
+		if ( $this->is_screen_enabled( 'admin' ) && is_admin() ) {
 			// Hook the admin screen markup function to the content hook
-			add_action( 'bp_groups_admin_meta_box_content_' . $this->slug, array( $this, 'admin_screen' ) );
+			add_action( 'bp_groups_admin_meta_box_content_' . $this->slug, array( $this, 'call_admin_screen' ) );
 
 			// Initialize the metabox
 			add_action( 'bp_groups_admin_meta_boxes', array( $this, '_meta_box_display_callback' ) );
 
 			// Catch the metabox save
-			if ( method_exists( get_class( $this ), 'admin_screen_save' ) ) {
-				add_action( 'bp_group_admin_edit_after', array( $this, 'admin_screen_save' ), 10 );
-			}
+			add_action( 'bp_group_admin_edit_after', array( $this, 'call_admin_screen_save' ), 10 );
 		}
 	}
 
-	function _display_hook() {
-		add_action( 'bp_template_content', array( &$this, 'display' ) );
-		bp_core_load_template( apply_filters( 'bp_core_template_plugin', $this->template_file ) );
+	/**
+	 * Call the admin_screen() method, and add a nonce field
+	 *
+	 * @since BuddyPress (1.8)
+	 */
+	public function call_admin_screen( $group_id = null ) {
+		call_user_func( $this->screens['admin']['screen_callback'], $this->group_id );
+		$this->nonce_field( 'admin' );
+	}
+
+	/**
+	 * Check the nonce, and call the admin_screen_save() method
+	 *
+	 * @since BuddyPress (1.8)
+	 */
+	public function call_admin_screen_save( $group_id = null ) {
+		$this->check_nonce( 'admin' );
+		call_user_func( $this->screens['admin']['screen_save_callback'], $this->group_id );
 	}
 
 	/**
@@ -1556,53 +2036,462 @@ class BP_Group_Extension {
 	 *
 	 * @since BuddyPress (1.7)
 	 */
-	function _meta_box_display_callback() {
+	public function _meta_box_display_callback() {
 		$group_id = isset( $_GET['gid'] ) ? (int) $_GET['gid'] : 0;
 
+		$screen = $this->screens['admin'];
+
 		add_meta_box(
-			$this->slug,
-			$this->name,
+			$screen['slug'],
+			$screen['name'],
 			create_function( '', 'do_action( "bp_groups_admin_meta_box_content_' . $this->slug . '", ' . $group_id . ' );' ),
 			get_current_screen()->id,
-			$this->admin_metabox_context,
-			$this->admin_metabox_priority
+			$screen['metabox_context'],
+			$screen['metabox_priority']
 		);
 	}
 
+
+	/** Utilities *********************************************************/
+
 	/**
-	 * Call the edit_screen() method
+	 * Generate the nonce fields for a settings form
 	 *
-	 * Broken into a standalone method so we can pass the current group id
-	 * to edit_screen()
+	 * The nonce field name (the second param passed to wp_nonce_field)
+	 * contains this extension's slug and is thus unique to this extension.
+	 * This is necessary because in some cases (namely, the Dashboard),
+	 * more than one extension may generate nonces on the same page, and we
+	 * must avoid name clashes.
 	 *
-	 * @since 1.8
+	 * @since BuddyPress (1.8)
+	 * @uses wp_nonce_field()
+	 * @param string $context 'create', 'edit', 'admin'
 	 */
-	public function call_edit_screen() {
-		$this->edit_screen( bp_get_current_group_id() );
+	public function nonce_field( $context = '' ) {
+		wp_nonce_field( 'bp_group_extension_' . $this->slug . '_' . $context, '_bp_group_' . $context . '_nonce_' . $this->slug );
 	}
 
 	/**
-	 * Call the create_screen() method, if we're on the right page
+	 * Check the nonce on a submitted settings form
 	 *
-	 * @since 1.8
+	 * @since BuddyPress (1.8)
+	 * @uses check_admin_referer()
+	 * @param string $context 'create', 'edit', 'admin'
 	 */
-	public function maybe_create_screen() {
-		if ( bp_is_group_creation_step( $this->slug ) ) {
-			$this->create_screen( bp_get_new_group_id() );
+	public function check_nonce( $context = '' ) {
+		check_admin_referer( 'bp_group_extension_' . $this->slug . '_' . $context, '_bp_group_' . $context . '_nonce_' . $this->slug );
+	}
+
+	/**
+	 * Is the specified screen enabled?
+	 *
+	 * To be enabled, a screen must both have the 'enabled' key set to true
+	 * (legacy: $this->enable_create_step, etc), and its screen_callback
+	 * must also exist and be callable.
+	 *
+	 * @since BuddyPress (1.8)
+	 * @param string $context 'create', 'edit', 'admin'
+	 * @return bool
+	 */
+	public function is_screen_enabled( $context = '' ) {
+		$enabled = false;
+
+		if ( isset( $this->screens[ $context ] ) ) {
+			$enabled = $this->screens[ $context ]['enabled'] && is_callable( $this->screens[ $context ]['screen_callback'] );
 		}
+
+		return $enabled;
 	}
 
 	/**
-	 * Call the create_screen_save() method, if we're on the right page
+	 * Get the appropriate screen callback for the specified context/type
 	 *
-	 * @since 1.8
+	 * BP Group Extensions have three special "screen contexts": create,
+	 * admin, and edit. Each of these contexts has a corresponding
+	 * _screen() and _screen_save() method, which allow group extension
+	 * plugins to define different markup and logic for each context.
+	 *
+	 * BP also supports fallback settings_screen() and
+	 * settings_screen_save() methods, which can be used to define markup
+	 * and logic that is shared between context. For each context, you may
+	 * either provide context-specific methods, or you can let BP fall back
+	 * on the shared settings_* callbacks.
+	 *
+	 * For example, consider a BP_Group_Extension implementation that looks
+	 * like this:
+	 *
+	 *   // ...
+	 *   function create_screen( $group_id ) { ... }
+	 *   function create_screen_save( $group_id ) { ... }
+	 *   function settings_screen( $group_id ) { ... }
+	 *   function settings_screen_save( $group_id ) { ... }
+	 *   // ...
+	 *
+	 * BP_Group_Extension will use your create_* methods for the Create
+	 * steps, and will use your generic settings_* methods for the Edit
+	 * and Admin contexts. This schema allows plugin authors maximum
+	 * flexibility without having to repeat themselves.
+	 *
+	 * The get_screen_callback() method uses a ReflectionClass object to
+	 * determine whether your extension has provided a given callback.
+	 *
+	 * @since BuddyPress (1.8)
+	 * @param string $context 'create', 'edit', 'admin'
+	 * @param string $type 'screen', 'screen_save'
+	 * @return mixed A callable function handle
 	 */
-	public function maybe_create_screen_save() {
-		if ( bp_is_group_creation_step( $this->slug ) ) {
-			$this->create_screen_save( bp_get_new_group_id() );
+	public function get_screen_callback( $context = '', $type = 'screen' ) {
+		$callback = '';
+
+		// Try the context-specific callback first
+		$method  = $context . '_' . $type;
+		$rmethod = $this->class_reflection->getMethod( $method );
+		if ( isset( $rmethod->class ) && $this->class_name === $rmethod->class ) {
+			$callback = array( $this->class_name, $method );
+		}
+
+		if ( ! $callback ) {
+			$fallback_method  = 'settings_' . $type;
+			$rfallback_method = $this->class_reflection->getMethod( $fallback_method );
+			if ( isset( $rfallback_method->class ) && $this->class_name === $rfallback_method->class ) {
+				$callback = array( $this->class_name, $fallback_method );
+			}
+		}
+
+		return $callback;
+	}
+
+	/**
+	 * Recursive argument parsing
+	 *
+	 * This acts like a multi-dimensional version of wp_parse_args() (minus
+	 * the querystring parsing - you must pass arrays).
+	 *
+	 * Values from $a override those from $b; keys in $b that don't exist
+	 * in $a are passed through.
+	 *
+	 * This is different from array_merge_recursive(), both because of the
+	 * order of preference ($a overrides $b) and because of the fact that
+	 * array_merge_recursive() combines arrays deep in the tree, rather
+	 * than overwriting the b array with the a array.
+	 *
+	 * The implementation of this function is specific to the needs of
+	 * BP_Group_Extension, where we know that arrays will always be
+	 * associative, and that an argument under a given key in one array
+	 * will be matched by a value of identical depth in the other one. The
+	 * function is NOT designed for general use, and will probably result
+	 * in unexpected results when used with data in the wild. See, eg,
+	 * http://core.trac.wordpress.org/ticket/19888
+	 *
+	 * @since BuddyPress (1.8)
+	 * @arg array $a
+	 * @arg array $b
+	 * @return array
+	 */
+	public static function parse_args_r( &$a, $b ) {
+		$a = (array) $a;
+		$b = (array) $b;
+		$r = $b;
+
+		foreach ( $a as $k => &$v ) {
+			if ( is_array( $v ) && isset( $r[ $k ] ) ) {
+				$r[ $k ] = self::parse_args_r( $v, $r[ $k ] );
+			} else {
+				$r[ $k ] = $v;
+			}
+		}
+
+		return $r;
+	}
+
+	/** Legacy Support ****************************************************/
+
+	/**
+	 * In BuddyPress 1.8, the recommended technique for configuring
+	 * extensions changed from directly setting various object properties
+	 * in the class constructor, to passing a configuration array to
+	 * parent::init(). The following methods ensure that extensions created
+	 * in the old way continue to work, by converting legacy configuration
+	 * data to the new format.
+	 */
+
+	/**
+	 * Provide access to otherwise unavailable object properties
+	 *
+	 * This magic method is here for backward compatibility with plugins
+	 * that refer to config properties that have moved to a different
+	 * location (such as enable_create_step, which is now at
+	 * $this->screens['create']['enabled']
+	 *
+	 * The legacy_properties array is set up in
+	 * self::setup_legacy_properties().
+	 *
+	 * @since BuddyPress (1.8)
+	 * @param string $key
+	 * @return mixed
+	 */
+	public function __get( $key ) {
+		if ( isset( $this->legacy_properties[ $key ] ) ) {
+			return $this->legacy_properties[ $key ];
+		} else if ( isset( $this->data[ $key ] ) ) {
+			return $this->data[ $key ];
+		} else {
+			return null;
+		}
+	}
+
+	/**
+	 * Allow plugins to set otherwise unavailable object properties
+	 *
+	 * This magic method is here for backward compatibility with plugins
+	 * that may attempt to modify the group extension by manually assigning
+	 * a value to an object property that no longer exists, such as
+	 * $this->enable_create_step.
+	 *
+	 * @since BuddyPress (1.8)
+	 * @param string $key
+	 * @param mixed $value
+	 */
+	public function __set( $key, $value ) {
+
+		if ( ! $this->is_initialized ) {
+			$this->data[ $key ] = $value;
+		}
+
+		switch ( $key ) {
+			case 'enable_create_step' :
+				$this->screens['create']['enabled'] = $value;
+				break;
+
+			case 'enable_edit_item' :
+				$this->screens['edit']['enabled'] = $value;
+				break;
+
+			case 'enable_admin_item' :
+				$this->screens['admin']['enabled'] = $value;
+				break;
+
+			case 'create_step_position' :
+				$this->screens['create']['position'] = $value;
+				break;
+
+			// Note: 'admin' becomes 'edit' to distinguish from Dashboard 'admin'
+			case 'admin_name' :
+				$this->screens['edit']['name'] = $value;
+				break;
+
+			case 'admin_slug' :
+				$this->screens['edit']['slug'] = $value;
+				break;
+
+			case 'create_name' :
+				$this->screens['create']['name'] = $value;
+				break;
+
+			case 'create_slug' :
+				$this->screens['create']['slug'] = $value;
+				break;
+
+			case 'admin_metabox_context' :
+				$this->screens['admin']['metabox_context'] = $value;
+				break;
+
+			case 'admin_metabox_priority' :
+				$this->screens['admin']['metabox_priority'] = $value;
+				break;
+
+			default :
+				$this->data[ $key ] = $value;
+				break;
 		}
 	}
 
+	/**
+	 * Returns a list of legacy properties
+	 *
+	 * The legacy implementation of BP_Group_Extension used all of these
+	 * object properties for configuration. Some have been moved.
+	 *
+	 * @since BuddyPress (1.8)
+	 * @return array
+	 */
+	protected function get_legacy_property_list() {
+		return array(
+			'name',
+			'slug',
+			'admin_name',
+			'admin_slug',
+			'create_name',
+			'create_slug',
+			'visibility',
+			'create_step_position',
+			'nav_item_position',
+			'admin_metabox_context',
+			'admin_metabox_priority',
+			'enable_create_step',
+			'enable_nav_item',
+			'enable_edit_item',
+			'enable_admin_item',
+			'nav_item_name',
+			'display_hook',
+			'template_file',
+		);
+	}
+
+	/**
+	 * Parse legacy properties
+	 *
+	 * The old standard for BP_Group_Extension was for plugins to register
+	 * their settings as properties in their constructor. The new method is
+	 * to pass a config array to the init() method. In order to support
+	 * legacy plugins, we slurp up legacy properties, and later on we'll
+	 * parse them into the new init() array.
+	 *
+	 * @since BuddyPress (1.8)
+	 */
+	protected function parse_legacy_properties() {
+
+		// Only run this one time
+		if ( empty( $this->legacy_properties_converted ) ) {
+			$properties = $this->get_legacy_property_list();
+
+			// By-reference variable for convenience
+			$lpc =& $this->legacy_properties_converted;
+
+			foreach ( $properties as $property ) {
+				// No legacy config exists for this key
+				if ( ! isset( $this->{$property} ) ) {
+					continue;
+				}
+
+				// Grab the value and record it as appropriate
+				$value = $this->{$property};
+
+				switch ( $property ) {
+					case 'enable_create_step' :
+						$lpc['screens']['create']['enabled'] = (bool) $value;
+						break;
+
+					case 'enable_edit_item' :
+						$lpc['screens']['edit']['enabled'] = (bool) $value;
+						break;
+
+					case 'enable_admin_item' :
+						$lpc['screens']['admin']['enabled'] = (bool) $value;
+						break;
+
+					case 'create_step_position' :
+						$lpc['screens']['create']['position'] = $value;
+						break;
+
+					// Note: 'admin' becomes 'edit' to distinguish from Dashboard 'admin'
+					case 'admin_name' :
+						$lpc['screens']['edit']['name'] = $value;
+						break;
+
+					case 'admin_slug' :
+						$lpc['screens']['edit']['slug'] = $value;
+						break;
+
+					case 'create_name' :
+						$lpc['screens']['create']['name'] = $value;
+						break;
+
+					case 'create_slug' :
+						$lpc['screens']['create']['slug'] = $value;
+						break;
+
+					case 'admin_metabox_context' :
+						$lpc['screens']['admin']['metabox_context'] = $value;
+						break;
+
+					case 'admin_metabox_priority' :
+						$lpc['screens']['admin']['metabox_priority'] = $value;
+						break;
+
+					default :
+						$lpc[ $property ] = $value;
+						break;
+				}
+			}
+		}
+	}
+
+	/**
+	 * Set up legacy properties
+	 *
+	 * This method is responsible for ensuring that all legacy config
+	 * properties are stored in an array $this->legacy_properties, so that
+	 * they remain available to plugins that reference the variables at
+	 * their old locations.
+	 *
+	 * @see self::__get()
+	 *
+	 * @since BuddyPress (1.8)
+	 */
+	protected function setup_legacy_properties() {
+
+		// Only run this one time
+		if ( empty( $this->legacy_properties ) ) {
+			$properties = $this->get_legacy_property_list();
+			$params     = $this->params;
+			$lp         =& $this->legacy_properties;
+
+			foreach ( $properties as $property ) {
+				switch ( $property ) {
+					case 'enable_create_step' :
+						$lp['enable_create_step'] = $params['screens']['create']['enabled'];
+						break;
+
+					case 'enable_edit_item' :
+						$lp['enable_edit_item'] = $params['screens']['edit']['enabled'];
+						break;
+
+					case 'enable_admin_item' :
+						$lp['enable_admin_item'] = $params['screens']['admin']['enabled'];
+						break;
+
+					case 'create_step_position' :
+						$lp['create_step_position'] = $params['screens']['create']['position'];
+						break;
+
+					// Note: 'admin' becomes 'edit' to distinguish from Dashboard 'admin'
+					case 'admin_name' :
+						$lp['admin_name'] = $params['screens']['edit']['name'];
+						break;
+
+					case 'admin_slug' :
+						$lp['admin_slug'] = $params['screens']['edit']['slug'];
+						break;
+
+					case 'create_name' :
+						$lp['create_name'] = $params['screens']['create']['name'];
+						break;
+
+					case 'create_slug' :
+						$lp['create_slug'] = $params['screens']['create']['slug'];
+						break;
+
+					case 'admin_metabox_context' :
+						$lp['admin_metabox_context'] = $params['screens']['admin']['metabox_context'];
+						break;
+
+					case 'admin_metabox_priority' :
+						$lp['admin_metabox_priority'] = $params['screens']['admin']['metabox_priority'];
+						break;
+
+					default :
+						// All other items get moved over
+						$lp[ $property ] = $params[ $property ];
+
+						// Also reapply to the object, for backpat
+						$this->{$property} = $params[ $property ];
+
+						break;
+				}
+			}
+		}
+	}
 }
 
 function bp_register_group_extension( $group_extension_class ) {
diff --git tests/assets/group-extensions.php tests/assets/group-extensions.php
new file mode 100644
index 0000000..ca4a551
--- /dev/null
+++ tests/assets/group-extensions.php
@@ -0,0 +1,144 @@
+<?php
+
+/**
+ * The following implementations of BP_Group_Extension act as dummy plugins
+ * for our unit tests
+ */
+
+class BPTest_Group_Extension_Parse_Legacy_Properties extends BP_Group_Extension {
+	function __construct() {
+		$class_name = get_class( $this );
+		$this->name = $class_name;
+		$this->slug = sanitize_title( $class_name );
+		$this->admin_name = $this->name . ' Edit';
+		$this->admin_slug = $this->slug . '-edit';
+		$this->create_name = $this->name . ' Create';
+		$this->create_slug = $this->slug . '-create';
+		$this->visibility = 'private';
+		$this->create_step_position = 58;
+		$this->nav_item_position = 63;
+		$this->admin_metabox_context = 'high';
+		$this->admin_metabox_priority = 'side';
+		$this->enable_create_step = false;
+		$this->enable_nav_item = true;
+		$this->enable_edit_item = false;
+		$this->enable_admin_item = true;
+		$this->nav_item_name = $this->name . ' Nav';
+		$this->display_hook = 'foo_hook';
+		$this->template_file = 'foo_template';
+	}
+
+	/**
+	 * Provides access to protected method unneeded in BP
+	 */
+	function _parse_legacy_properties() {
+		return $this->parse_legacy_properties();
+	}
+
+	/**
+	 * Provides access to protected property unneeded in BP
+	 */
+	function _get_legacy_properties_converted() {
+		return $this->legacy_properties_converted;
+	}
+}
+
+class BPTest_Group_Extension_Setup_Screens_Use_Global_Fallbacks extends BP_Group_Extension {
+	function __construct() {
+		$class_name = get_class( $this );
+		$this->slug = sanitize_title( $class_name );
+		$this->name = $class_name;
+	}
+
+	/**
+	 * Provides access to protected method unneeded in BP
+	 */
+	function _get_default_screens() {
+		return $this->get_default_screens();
+	}
+
+	/**
+	 * Provides access to protected method unneeded in BP
+	 */
+	function _setup_class_info() {
+		return $this->setup_class_info();
+	}
+
+}
+
+class BPTest_Group_Extension_Setup_Screens_Define_Edit_Screens_Locally extends BP_Group_Extension {
+	function __construct() {
+		$class_name = get_class( $this );
+		$this->slug = sanitize_title( $class_name );
+		$this->name = $class_name;
+	}
+
+	function edit_screen() {}
+	function edit_screen_save() {}
+
+	/**
+	 * Provides access to protected method unneeded in BP
+	 */
+	function _get_default_screens() {
+		return $this->get_default_screens();
+	}
+
+	/**
+	 * Provides access to protected method unneeded in BP
+	 */
+	function _setup_class_info() {
+		return $this->setup_class_info();
+	}
+
+}
+
+class BPTest_Group_Extension_Access_Root_Property extends BP_Group_Extension {
+	function __construct() {
+		$class_name = get_class( $this );
+
+		$args = array(
+			'slug' => sanitize_title( $class_name ),
+			'name' => $class_name,
+			'nav_item_position' => 39,
+		);
+
+		parent::init( $args );
+	}
+}
+
+class BPTest_Group_Extension_Access_Init_Property_Using_Legacy_Location extends BP_Group_Extension {
+	function __construct() {
+		$class_name = get_class( $this );
+
+		$args = array(
+			'slug' => sanitize_title( $class_name ),
+			'name' => $class_name,
+			'screens' => array(
+				'create' => array(
+					'position' => 18,
+				),
+			),
+		);
+
+		parent::init( $args );
+	}
+}
+
+class BPTest_Group_Extension_Get_Screen_Callback_Fallbacks extends BP_Group_Extension {
+	function __construct() {
+		$class_name = get_class( $this );
+
+		$args = array(
+			'slug' => sanitize_title( $class_name ),
+			'name' => $class_name,
+		);
+
+		parent::init( $args );
+	}
+
+	function settings_screen() {}
+	function settings_screen_save() {}
+
+	function edit_screen() {}
+	function edit_screen_save() {}
+}
diff --git tests/bootstrap.php tests/bootstrap.php
index 5d6b734..5dc3f60 100644
--- tests/bootstrap.php
+++ tests/bootstrap.php
@@ -2,14 +2,18 @@
 
 define( 'BP_PLUGIN_DIR', dirname( dirname( __FILE__ ) ) . '/' );
 
+if ( ! defined( 'BP_TESTS_DIR' ) ) {
+	define( 'BP_TESTS_DIR', dirname( __FILE__ ) . '/' );
+}
+
 require_once getenv( 'WP_TESTS_DIR' ) . '/includes/functions.php';
 
 function _install_and_load_buddypress() {
-	require dirname( __FILE__ ) . '/includes/loader.php';
+	require BP_TESTS_DIR . '/includes/loader.php';
 }
 tests_add_filter( 'muplugins_loaded', '_install_and_load_buddypress' );
 
 require getenv( 'WP_TESTS_DIR' ) . '/includes/bootstrap.php';
 
 // Load the BP-specific testing tools
-require dirname( __FILE__ ) . '/includes/testcase.php';
+require BP_TESTS_DIR . '/includes/testcase.php';
diff --git tests/testcases/groups/class-bp-group-extension.php tests/testcases/groups/class-bp-group-extension.php
new file mode 100644
index 0000000..4d0a01c
--- /dev/null
+++ tests/testcases/groups/class-bp-group-extension.php
@@ -0,0 +1,224 @@
+<?php
+
+include_once BP_TESTS_DIR . '/assets/group-extensions.php';
+
+/**
+ * @group groups
+ */
+class BP_Tests_Group_Extension_TestCases extends BP_UnitTestCase {
+	public function setUp() {
+		parent::setUp();
+	}
+
+	public function tearDown() {
+		parent::tearDown();
+	}
+
+	public function test_parse_legacy_properties() {
+		$class_name = 'BPTest_Group_Extension_Parse_Legacy_Properties';
+		$class_slug = sanitize_title( $class_name );
+		$e = new $class_name();
+		$e->_register();
+
+		// Test most items separately so we can ignore irrelevant props
+		$l = $e->_get_legacy_properties_converted();
+		$this->assertEquals( $l['name'], $class_name );
+		$this->assertEquals( $l['slug'], $class_slug );
+		$this->assertEquals( $l['visibility'], 'private' );
+		$this->assertEquals( $l['nav_item_position'], 63 );
+		$this->assertEquals( $l['enable_nav_item'], true );
+		$this->assertEquals( $l['nav_item_name'], $class_name . ' Nav' );
+		$this->assertEquals( $l['display_hook'], 'foo_hook' );
+		$this->assertEquals( $l['template_file'], 'foo_template' );
+
+		// Build the screens array manually
+		$expected = array(
+			'create' => array(
+				'name' => $class_name . ' Create',
+				'slug' => $class_slug . '-create',
+				'position' => 58,
+				'enabled' => false,
+			),
+			'edit' => array(
+				'name' => $class_name . ' Edit',
+				'slug' => $class_slug . '-edit',
+				'enabled' => false,
+			),
+			'admin' => array(
+				'enabled' => true,
+				'metabox_context' => 'high',
+				'metabox_priority' => 'side',
+			),
+		);
+
+		$this->assertEquals( $expected, $l['screens'] );
+	}
+
+	public function test_setup_screens_use_global_fallbacks() {
+		$class_name = 'BPTest_Group_Extension_Setup_Screens_Use_Global_Fallbacks';
+		$e = new $class_name();
+		$e->_setup_class_info();
+		$screens = $e->_get_default_screens();
+
+		$fallback = array(
+			'screen_callback' => array( $class_name, 'settings_screen' ),
+			'screen_save_callback' => array( $class_name, 'settings_screen_save' ),
+		);
+		$fallbacks = array(
+			'create' => $fallback,
+			'edit' => $fallback,
+			'admin' => $fallback,
+		);
+
+		// strip everything from the screens array but what we we're
+		// testing
+		foreach ( $screens as &$screen ) {
+			foreach ( $screen as $k => $v ) {
+				if ( ! in_array( $k, array( 'screen_callback', 'screen_save_callback' ) ) ) {
+					unset( $screen[ $k ] );
+				}
+			}
+		}
+
+		$this->assertEquals( $screens, $fallbacks );
+	}
+
+	public function test_setup_screens_define_edit_screens_locally() {
+		$class_name = 'BPTest_Group_Extension_Setup_Screens_Define_Edit_Screens_Locally';
+		$e = new $class_name();
+		$e->_setup_class_info();
+		$screens = $e->_get_default_screens();
+
+		$fallback = array(
+			'screen_callback' => array( $class_name, 'settings_screen' ),
+			'screen_save_callback' => array( $class_name, 'settings_screen_save' ),
+		);
+		$expected = array(
+			'create' => $fallback,
+			'edit' => array(
+				'screen_callback' => array( $class_name, 'edit_screen' ),
+				'screen_save_callback' => array( $class_name, 'edit_screen_save' ),
+			),
+			'admin' => $fallback,
+		);
+
+		// strip everything from the screens array but what we we're
+		// testing
+		foreach ( $screens as &$screen ) {
+			foreach ( $screen as $k => $v ) {
+				if ( ! in_array( $k, array( 'screen_callback', 'screen_save_callback' ) ) ) {
+					unset( $screen[ $k ] );
+				}
+			}
+		}
+
+		$this->assertEquals( $screens, $expected );
+	}
+
+	public function test_parse_args_r() {
+		$a = array(
+			'veggies' => 'yes',
+			'ice_cream' => 'dope',
+			'fruit' => array(
+				'apple' => 'gross',
+				'berries' => array(
+					'blueberries' => array(
+						'in_season' => 'never',
+						'oh' => 'boy',
+					),
+					'cherries' => 'sometimes',
+				),
+			),
+		);
+
+		$b = array(
+			'veggies' => 'no',
+			'cheese' => array(
+				'cheddar' => 'good',
+			),
+			'fruit' => array(
+				'apple' => 'yum',
+				'berries' => array(
+					'strawberries' => 'awesome',
+					'blueberries' => array(
+						'in_season' => 'yes',
+						'out_of_season' => 'no',
+					),
+				),
+			),
+		);
+
+		$expected = array(
+			'veggies' => 'yes',
+			'ice_cream' => 'dope',
+			'cheese' => array(
+				'cheddar' => 'good',
+			),
+			'fruit' => array(
+				'apple' => 'gross',
+				'berries' => array(
+					'strawberries' => 'awesome',
+					'blueberries' => array(
+						'in_season' => 'never',
+						'out_of_season' => 'no',
+						'oh' => 'boy',
+					),
+					'cherries' => 'sometimes',
+				),
+			),
+		);
+
+		$this->assertEquals( $expected, BP_Group_Extension::parse_args_r( $a, $b ) );
+	}
+
+	/**
+	 * Config that gets intentionally stored as a direct property of object
+	 */
+	public function test_access_root_property() {
+		$class_name = 'BPTest_Group_Extension_Access_Root_Property';
+		$e = new $class_name();
+		$e->_register();
+
+		$this->assertEquals( 39, $e->nav_item_position );
+	}
+
+	/**
+	 * Config that gets registered using init(), but is then accessed via
+	 * the legacy location
+	 */
+	public function test_access_init_property_using_legacy_location() {
+		$class_name = 'BPTest_Group_Extension_Access_Init_Property_Using_Legacy_Location';
+		$e = new $class_name();
+		$e->_register();
+
+		$this->assertEquals( 18, $e->create_step_position );
+	}
+
+	/**
+	 * Provides settings_screen* and edit_screen*
+	 */
+	public function test_get_screen_callback_fallbacks() {
+		$class_name = 'BPTest_Group_Extension_Get_Screen_Callback_Fallbacks';
+		$e = new $class_name();
+		$e->_register();
+
+		$this->assertEquals( array( $class_name, 'settings_screen' ), $e->screens['create']['screen_callback'] );
+		$this->assertEquals( array( $class_name, 'settings_screen_save' ), $e->screens['create']['screen_save_callback'] );
+		$this->assertEquals( array( $class_name, 'settings_screen' ), $e->screens['admin']['screen_callback'] );
+		$this->assertEquals( array( $class_name, 'settings_screen_save' ), $e->screens['admin']['screen_save_callback'] );
+		$this->assertEquals( array( $class_name, 'edit_screen' ), $e->screens['edit']['screen_callback'] );
+		$this->assertEquals( array( $class_name, 'edit_screen_save' ), $e->screens['edit']['screen_save_callback'] );
+	}
+
+	public function test_has_submit_button() {
+		$a = '<p>Foo bar</p><input type="text" name="awesome" /><input name="save" type="submit" id="saverrrr" />sweet';
+		$this->assertTrue( BP_Group_Extension::has_submit_button( $a ) );
+
+		$b = '<p>Foo bar</p><input type="text" name="awesome" />sweet';
+		$this->assertFalse( BP_Group_Extension::has_submit_button( $b ) );
+
+		// switch the quotation marks
+		$c = "<p>Foo bar</p><input type='text' name='awesome' /><input name='save' type='submit' id='saverrrr' />sweet";
+		$this->assertTrue( BP_Group_Extension::has_submit_button( $c ) );
+	}
+}
