How to Let Users Invite Friends to Their Groups

Say you want to build a service where people can invite others to a specific group or team or project. Not an entirely uncommon idea. What do you use to build that invitation system? There’s devise_invitable, but unless you want to spend hours breaking apart the code because it’s never going to work to the exact specifications you have in mind, you might as well build your own from scratch.

So you have a bunch of users already and they’re trying to invite people to their groups/teams/projects. There are a few possible scenarios for what will happen and some fairly common edgecases to watch out for. The most straightforward use-case is when the user sends out a group invite to a new recipient, the recipient receives it, and signs up on the site. The recipient then becomes a user and all that’s needed on the backend is for their data to be added to the users table and for the association between them and the group to be created.

Sweet and easy. I think I’ll go pat myself on the back and have some pie.

But wait.

What happens if they’re a user already? Well, ok maybe we’ll just check for that first that way if they are we can take note. Then we can send them an invite letting them know and they’ll see the group they’re invited to when they log in. Maybe something like this.

1
2
3
4
5
6
7
8
  def user_already_exists?(recipient_email)
    member =  User.find_by_email(recipient_email)
    if member
      self.user_id = member.id
    else
      send_invite
    end
  end

No harm, no foul! Now onto pie.

Hmm, but what if the invitation wasn’t sent to the email that their account is under?

Ahh, and now things are getting a bit more complicated. How will we know that the recipient of the invitation is the same as the user who eventually signs up if they use a different email? Maybe email isn’t the best identifier then. We’ll need something that can be passed from the invitation back to our application that way we’ll have an identifier that is unlikely to change. Let’s go with a randomly generated token that we’ll use in the urls we send. That way if they do sign up or log in under a different email we can still tell what group the user was invited to.

1
2
3
def generate_token
    self.invitation_token = Digest::SHA1::hexdigest([Time.now, rand].join)
end

Good job. Pie now?

Nope, because there’s still the possibility that a person may be invited to multiple groups before they’ve accepted any invitations. We want to be able to display all the groups they’re invited to so how do we make sure the invitations can be grouped together? In reality, this isn’t something we can guarantee, but we can try to catch as many situations as possible by checking to see if the email the invitation is being sent to is already on the invitation table.

1
2
def check_invitation_table_for_email(recipient_email)
    invitations = Invitation.where({:email => recipient_email})

Hey! This email is so lets just take the token already generated and use it for the second group invitation. Now when the recipient visits the website to either log in or sign up we’ll be able to find all groups they were invited to either by their email or the invitation token and we can display all those groups for them.

While we’re at it why don’t we deal with the possibility of someone being invited to the same group multiple times. Maybe her friends really want her to join their group. Well she still only needs one invitation in our table and only one token. So when we check to see if the email is in the invitation table already let’s check to see if the group id is the same as well. If it is, we’ll just ignore and if not then we’ll add the new group invite.

1
2
3
4
5
6
7
8
9
10
11
if invitations != []
      invitations.each do |invitation|
        if invitation.group_id == self.group_id
          self.group_id = nil
        else
          self.invitation_token = invitation.invitation_token
        end
      end
    else
      new_invitation(recipient_email)
    end

Once we put everything together we should have a fairly solid system that deals with a some of the most common possibilities. It’ll look something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
def user_already_exists?(recipient_email)
    member =  User.find_by_email(recipient_email)
    if member
      self.user_id = member.id
    else
      check_invitation_table_for_email(recipient_email)
    end
  end

  private
  def check_invitation_table_for_email(recipient_email)
    invitations = Invitation.where({:email => recipient_email})
    if invitations != []
      invitations.each do |invitation|
        if invitation.group_id == self.group_id
          self.group_id = nil
        else
          self.invitation_token = invitation.invitation_token
        end
      end
    else
      new_invitation(recipient_email)
    end
  end

  def new_invitation(recipient_email)
    generate_token
    self.email = recipient_email
  end

  def generate_token
    self.invitation_token = Digest::SHA1::hexdigest([Time.now, rand].join)
  end

Refactoring and pie to follow.