Bind one email to another?

Greetings, I’m creating a membership site with S2, incorporating both Paypal and Mailchimp. A paid member will obviously have an email tied to their account, but I’d like to be able to also include their spouse’s email, who would receive same benefits but not have to pay for a separate membership. This spouse would become part of the email list of Mailchimp.

In other words, I’m asking if there’s a way to bind the spouse’s membership to the paying member. So, for example, if the paying member’s membership expires, so also does the spouse’s, and when the paying member renews their membership, then automatically the spouse’s is renewed, too. Also, when the paying member signs up for the first time, there must be a custom field or something that then gets translated into its own membership somehow.

It can be done manually, of course, by making a custom field for spouse’s email, then when a new member signs up I can manually make the spouse a “courtesy” member (same rights, but non-paying), and then manually update this courtesy member whenever status of paying member changes. But, I was wondering if there’s a way to automate such a thing?

Thank-you for any insights you may have, I really appreciate it.

It seems like I need to use “hooks” to achieve this, but do I use S2 hooks or WP hooks?

S2 allows for custom registration fields, and so I’ll create a new field “spouse email”. Then, when a new user registers, I’ll want to have a php function contained in mu-plugins that is called when the new user registers, using an appropriate hook. The WP hook seems to be “user_register”, but I’m not sure if that will have the S2 custom field “spouse email” available? The php function I write will want to check if the spouse email field is filled for the recently created user, and if it is then will create a new user who will have same membership privileges, but maybe a different membership level to indicate that membership is conditioned upon the paying spouse.

Specifically, if the paying user’s membership is renewed or expires, then the spouse’s membership must track that, in a “master/slave” manner. There must be another hook related to renewal/expiration that the mu-plugin function accesses.

I don’t know, maybe this is just too advanced for my current level of understanding WP, php, and S2? If there are any experts out there, maybe you feel it’s not a big deal and could be implemented easily? Please share how you’d go about it. Thank-you!

Hi Eric,
This is an interesting dilemma.

On my site, where I needed to do something semi-tricky, but not in the same area as your problem, I wrote a custom set of functions to control protected content. In my case, I was wanting S2Member to protect content across various sites of a multisite installation while only being active on one (the main) site. To accomplish this, I had to write a function that essentially looks up the user’s access from the main site that I made available network wide.

In your situation, the solution I would envision would be using custom capabilities, and having the add-on members (spouse members) be given a special custom capability (i.e. “spouse”) that your custom functions to protect content could then use to look up the membership level of the primary/paying member. Instead of hooking heavily into the user creation process, your site would check access based on when the add-on member tries to access the content.

In this scenario, you would have the primary member have a special field in their account that they would add their spouse email to (like what you describe), and then you can either automatically create a new user based on that email address, watch new users as they sign up and then give them access as their credentials match, or create a special page where spouses could create add-on memberships that would give them this custom capability (third option seems easiest to implement). Depending on your level of coding ability, you could hide the add-on user member signup form behind a separate form where they need to add the email address of the main user.

While not a perfect system in some regards, if the paying member upgraded, downgraded, cancelled, or stopped paying, then your custom function could block access to the add-on member the next time they logged in to access their account (or perhaps you could have a custom script that would run periodically that would clean up old users in the case of the paying member cancelling or leaving). Or, you could have the functionality built in to offer a spouse account of a cancelled member to purchase their own membership and convert their add-on member to a full member.

In short, while the paying member could have their account based on membership levels, I would suggest looking at custom capabilities for the add-on membership accounts.

Hopefully these ideas makes sense. I know what you are describing can be done. However, I don’t have any code examples right now (I’m writing this while at work). Hopefully the conceptual ideas help point you in a productive direction. :slight_smile:

~Cam

1 Like

Wow, thank-you for the thoughtful response, Cam. Yes, I’m definitely going to classify the spouse as either a separate membership level or a unique custom capability. The tricky part is creating the spouse user in the first place. I’ve been playing around with the WP “user_register” hook, and it’s unsatisfactory, as it seems like it’s called after the primary user is created but before the primary meta-data is saved, with the result being that the spouse user I create during this time receives the meta data of the primary user and the primary user receives nothing.

I hope that made sense! Anyway, now what I’m thinking of doing, is once a day (say in the middle of the night), have some code automatically run that loops through all users, putting all their info including meta-data into an array. The code will look for spouse email field and match it up with an existing spouse user, check membership level status of primary user to be sure all is well, and if spouse user doesn’t exist (because primary user joined earlier that day), then create spouse user. Doing this should avoid any timing hiccups I experienced with the hooks. What do you think?

This seems like a great idea.

The only hiccup I can see with it is if the spouse account wants to be used on the same day as a primary user is created (or before the script runs). To get around that flaw, you could include a notice that spouse accounts will be linked within 24 hours, allowing time for your script to run, or you could tap into the login routine of a user and manually run your script if it is a spouse account.

If you look closely at what S2 Member Pro changes in the main WP files, you can get the picture that there are ways member creation is lacking, since S2 adds in several filters and hooks within the user creation process.

Best wishes with your site and getting this configuration working. :slight_smile:
~Cam

1 Like

Hi Cam,

You seem like a long-time user of S2, and probably pretty good at coding. Could I trouble you to have a quick look at my code, which is not yet giving me the results I’m seeking? I’ve managed to get the cron event to work, and with the help of the plugin “Crontrol”, I can fire it immediately for testing purposes. I also figured out how to print out results, by setting DEBUG flag in wp-config.php file and creating a log file. I read all the users from WP database (just some fake ones for the moment), and I’m now trying to create two arrays of users, one array is filled with the users that have paid or are “courtesy” (free lifetime membership, i.e. no eot) and who have listed a spouse email in S2 custom field. The other array is the users who are the spouses, whom I call “conditional” members, and they have an invisible S2 custom field (for admin eyes only) called the master ID, which is the user ID of the paid or courtesy member they are tied to. It’s like a foreign key in a database. So, level0 = non members (but I have their email), level1 = conditional members, level2 = paid members (through paypal or by adding them by hand if paid by check), and level3 = courtesy members.

Anyway, the filling of the arrays seems to be going well, but now I want to start updating user info and creating new members programmatically. Specifically, if there’s a paid/courtesy member with a spouse email, then I’ll check that there’s a conditional member tied to them, and if not then create new conditional member with the appropriate masterID custom field added to their info. Updates can also occur, if the paid/courtesy member changes their profile and sticks in a new spouse email. In that case, I’ll find the conditional member with the masterID, and update their email.

But, the problem I’m having is that the updating of user isn’t working as expected. For example, as you’ll see in the code, I do a quick error check to see if there are any conditional members that have no masterID in their custom field, and if I find one, then demote them down to level0 (non member). To test it out, in Admin Panel I created a new user and chose their role to be conditional member (level1), but didn’t put in any masterID. The code does indeed recognize this and tries to demote them. The trouble is, when I go look at that user later in the Admin Panel, it has no role, and at the very bottom of user profile page, it has a new note saying there are “added capabilities”, this note not being present on any other users.

I found this S2 tutorial on this subject, here it is:
https://s2member.com/kb-article/rolescapabilities-via-php/

But, this tutorial is rather confusing, probably due to my limited php knowledge. For example, why do they instantiate the user with $user = new WP_User($user->ID); ? Isn’t $user already instantiated because of the foreach loop?

Okay, so here’s my code, I put in lots of comments so it should be apparent what I’m trying to do. The code is in s2-hacks.php, which resides in mu-plugins. I’d be very grateful if you could take a look at it, and you’ll probably find some errors I’m making. Thank-you!

Later edit: code appears in next comment


<?php
/*
Regularly do an inventory of users, adding spouse user when necessary.
Perform the action once a day, at 3 a.m. (add 7 hr to use UT time = 10:00:00), or if no one
is viewing website at this time, then run as soon as someone accesses the website.
Eventually use absolute cron table on HostGator, so it will definitely run at 3 a.m.
*/

//Schedule an action if it's not already scheduled
if ( ! wp_next_scheduled( 'uea_cron_hook' ) ) {
		wp_schedule_event( strtotime('10:00:00'), 'daily', 'uea_cron_hook' );
}

//Hook into that action that'll fire daily
add_action( 'uea_cron_hook', 'uea_cron_function' );

//Function that runs on cron
function uea_cron_function() {
	$users = get_users('orderby=ID'); //This retrieves all user data (including meta) from WP database

// Loop through all users, creating two new arrays
	$paidMembers = array();  //I'll fill this array with paid/courtesy members that have spouse emails
	$conditionalMembers = array();  //I'll fill this with conditional members
	foreach ($users as $user){
		$user_id = $user->ID;
		$user_role = $user->roles[0]; //roles is an array with one element
		write_log('$user dump before');
		write_log($user);
		$usermeta = get_user_meta($user_id);
		write_log('$usermeta dump before');
		write_log($usermeta);

// Find paid/courtesy members with spouse emails, create an array of them.
		if ($user_role == 's2member_level2' || $user_role == 's2member_level3') {
			$keymeta = 'wp_s2member_custom_fields';
			$valuemeta = get_user_meta($user_id, $keymeta, true);
			if ($valuemeta) { //checks that there is non-empty custom field data
				foreach ($valuemeta as $keycustom => $valuecustom) { //loop through associative array
					if ($keycustom == 'email_address_2' && $valuecustom) { //There is a non-empty spouse email
						array_push($paidMembers, $user);
					}
				}
			}
		}

// Find conditional members, create an array of them.
		if ($user_role == 's2member_level1') {
			$hasMaster = false;
			$keymeta = 'wp_s2member_custom_fields';
			$valuemeta = get_user_meta($user_id, $keymeta, true);
			if ($valuemeta) { //checks that there is non-empty custom field data
				foreach ($valuemeta as $keycustom => $valuecustom) { //loop through associative array
					if ($keycustom == 'master_ID' && $valuecustom) { //There is a non-empty master ID
						array_push($conditionalMembers, $user);
						$hasMaster = true;
					}
				}
			}
			if (!$hasMaster) { //for whatever reason, conditional member has no master, so make them a non member
				$user->remove_cap('access_s2member_level1');
				$user->add_cap('access_s2member_level0');
				$user->add_role('s2member_level0');
//				wp_update_user( array( 'ID' => $user_id, 'role[0]' => 's2member_level0' ) );
			}
		}

		write_log('$user dump after');
		write_log($user);
		$usermeta = get_user_meta($user_id);
		write_log('$usermeta dump after');
		write_log($usermeta);

	}

// First verify all existing conditional members are legit (i.e. track their masters)

/* 	$result = wp_create_user('johndoe2', 'passwordgoeshere2', 'john.doe2@example.com');
	if (is_wp_error($result)) {
		$error = $result->get_error_message();
		write_log($error);
	} else {
		//handle successful creation here
		$newuser = get_user_by('id', $result);
		write_log('New conditional member created, simple dump here:');
		write_log($newuser);
	}
 */
}

// Write to log file (need to set WP_DEBUG true in wp-config.php)
function write_log($log) {
	if (true === WP_DEBUG) {
		if (is_array($log) || is_object($log)) {
			error_log(print_r($log, true));
		} else {
			error_log($log);
		}
	}
}

?>

Looking at the tutorial and at your code, if you want to demote your users with the condition !$hasMaster then the way you are going about the change doesn’t look correct.

In WP, roles are typically understood as sets of capabilities. S2 member adds its 4 default roles (level 1-4) to the already standard set of user roles (admin, author, editor, subscriber, etc.), while also opening up the possibility of including custom capabilities that are separate from roles themselves.

$user->remove_cap('access_s2member_level1'); // Tries to remove a capability. Unless you set this as a ccap, it won't do much.
$user->add_cap('access_s2member_level0'); //Adds a capability to the user (independent of the users role)
$user->add_role('s2member_level0'); //Adds a role to a user, but not sure what this would do if the user already has a higher role. It might not do anything if there is no role by that name defined, which is likely if you haven't set it up somewhere else.

To demote a user away from level 1, I probably would simply replace your four lines of code updating the user with the single line -> $user->set_role("subscriber")

if (!$hasMaster) { //for whatever reason, conditional member has no master, so make them a non member
	$user->set_role("subscriber");
}

S2 member treats the subscriber role as a Level 0, or less than Level 1, so this should work. (Note: Untested code, but it should be a good starting point to work from.)

To answer your other question, I suspect that they initiate the WP_User to get access to the S2Member custom ccap capabilities. Since these are stored in the user_meta DB table, they might not be immediately available with the base get_users() function, or it may be easier to get the user info into an object using this method. (I’m writing this from memory, so it’s possible this last paragraph isn’t 100% correct.)

I hope I understood your problem correctly and that this has pointed you in the right direction.

~Cam

1 Like

Wow, that was an easy fix, it totally worked!
Earlier on my own I had tried the set_role function, but used “s2member_level0” instead of “subscriber”, and was getting poor results. But using subscriber makes things function as I’d expect.
You’ve been a great help, Cam!
(Stay tuned, I may have another problem with code as I continue writing it…) :slightly_smiling_face:

Glad it’s working as expected now. :slight_smile:

Hey Cam, everything’s going well, code is now written and working beautifully. But… there’s one little problem that I can’t seem to fix, maybe it’s a bug in S2Member? It is this:
In S2Member, I’ve created some custom fields, namely the “spouse email” and a “master ID”. The spouse email is what a paying member can enter, so that their spouse can become a “conditional member” and receive emails, as already explained. The master ID is something that gets assigned to conditional members programmatically when the new conditional member is created. It’s a reference to the user ID of the paying member who listed them as a spouse. I use the commands:


	$keymeta = 'wp_s2member_custom_fields';
	update_user_meta($user_id, $keymeta, array('master_id' => $master_id));

Anyway, it totally works, a new custom field is created for the conditional member, but the problem is when I then view things on the Admin Panel/All Users page. I go under screen options and make sure that Master ID is one of the columns to display, but for some reason the value does not appear!
But, when I then edit the profile of the conditional member, the field for Master ID is properly filled, and if I simply go down and hit “update user” (without even making any changes), then when I view All Users, the value appears in the Master ID column.

I know that’s a rather obscure bug, and the fix is to manually edit every conditional member and hit the update user button, but maybe you know why it’s not appearing in All Users page?

Thank-you for your insights as always.

-Eric

Hi Eric,
Sorry for missing your email until now. That is a fascinating bug.

Troubleshooting this is a touch past my familiarity since my WP/S2 skills are a little rusty, however one idea I have would be to call wp_update_user on the user data after you have updated the user meta. If I am not mistaken, the edit/save user info in the admin panel uses this function, and it might be possible to replicate it as a separate step at the end of your function. (The only caution I would have is making sure you don’t accidentally include the user password info in the update command, because that might trigger an email to the user which could cause confusion. To accomplish this, I would unset or remove any reference to the user password field, or simply update a relatively simple piece of the users data.)

Another idea that I stumbled on is a function called clean_user_cache. This might solve your problem as it is run during the user update function and sounds like it could work.

Hope these ideas help.
~Cam

Hi Cam, those were both great ideas, and I tried them out, but alas, no luck!
But… I have a new big clue, and I think you can help with this:

Apparently the S2 custom field data is stored in a “serialized associative array”. So, I can just print out the entire meta data object by doing this:
$usermeta = get_user_meta($user_id);
write_log($usermeta);

And what I get as a print-out for the custom field part of this meta data is an associative array in serialized form. The big clue is that I printed it out before and after hitting the “update user” button in admin panel. There is a subtle difference, behold:
a:1:{s:9:“master_id”;i:20;}
a:1:{s:9:“master_id”;s:2:“20”;}

You can see that i is replaced by s:2, and I think that may be why the second one displays in the admin panel, because that page of all users lists all the s2 custom fields, so maybe it wasn’t recognizing the first one.

However, when I use the command unserialize() to print them out as a normal associative array, they both look identical:
Array
(
[master_id] => 20
)

As I said, I use the following command to create the custom field and fill it with a value:
$keymeta = ‘wp_s2member_custom_fields’;
update_user_meta($user_id, $keymeta, array(‘master_id’ => $master_id));

But, maybe there’s a missing s2-related command I also need to call?
Or, maybe I need to edit the serialized array after creating the field? If you think that’s the answer, do you know how to go about that? Is it just string manipulation commands of php? I’m not looking forward to figuring that out :frowning_face:

Anyway, I think I’ve at least found the issue, but I’m just not sure how to resolve it?
I’m super-grateful to you for taking the time to help me and lending any more insights!
-Eric

Epiphany: I just realized s:2 refers to string, not s2member! When I change the update command to this, then all is well:
$keymeta = ‘wp_s2member_custom_fields’;
update_user_meta($user_id, $keymeta, array(‘master_id’ => strval($master_id)));

So, just ignore my previous post! I think I’m good to go, now :grinning:

Awesome. Happy to have helped you work towards getting a solution.

Best wishes with your site!

~Cam