How I contributed notifications to an open-source product

Aniket PatidarAniket Patidar
5 min read

This post details my contribution to the notification system for Moneygun, an open-source, white-label SaaS boilerplate. It provides a practical and actionable guide on integrating notifications into a Rails application using the Noticed gem.

https://github.com/yshmarov/moneygun/pull/286
First, I want to share that I learned about Yaroslav Shmarov (@yarotheslav) looking for contributors for their open-source product, Moneygun, through a Twitter post he made. I also wanted to explore the new version of the Noticed gem, so I decided to give it a try. That's how I got the opportunity to work on this issue.

Do check out Moneygun if you want to build your next B2B SaaS app (software as a service): https://github.com/yshmarov/moneygun

There are two things that need to be implemented.

  • In-app notifications

  • Email

First, I explored the gem to understand how to implement basic boilerplate code for notifications, then began by adding the gem to the Rails application:

# notifications
gem "noticed"

Or

Run the following command to add Noticed to your Gemfile:

bundle add "noticed"

Generate and then run the migrations:

rails noticed:install:migrations
rails db:migrate

To start, create a Notifier:

We have two requirements here:

  1. The user should be notified when they are invited to an organization.

  2. The user should be notified when their membership request is accepted.

rails generate noticed:notifier MembershipInvitationNotifier
rails generate noticed:notifier MembershipRequestAcceptedNotifier

In this post, we'll deliver notifications by email, and in the next post, we'll cover turbo_stream, which provides real-time UI updates to users' browsers.

First, let's take a look at how the generated notifier appears.

Three files are created under the notifiers:

# app/notifiers/application_notifier.rb
class ApplicationNotifier < Noticed::Event
end
# app/notifiers/membership_request_accepted_notifier.rb
# To deliver this notification:
#
# MembershipRequestAcceptedNotifier.with(record: @post, message: "New post").deliver(User.all)
class MembershipRequestAcceptedNotifier < ApplicationNotifier
 # Add your delivery methods
 #
 # deliver_by :email do |config|
 #   config.mailer = "UserMailer"
 #   config.method = "new_post"
 # end
 #
 # bulk_deliver_by :slack do |config|
 #   config.url = -> { Rails.application.credentials.slack_webhook_url }
 # end
 #
 # deliver_by :custom do |config|
 #   config.class = "MyDeliveryMethod"
 # end
 #
 # Add required params
 #
 # required_param :message
end
# app/notifiers/membership_invitation_notifier.rb
# To deliver this notification:
#
# MembershipInvitationNotifier.with(record: @post, message: "New post").deliver(User.all)
class MembershipInvitationNotifier < ApplicationNotifier
 # Add your delivery methods
 #
 # deliver_by :email do |config|
 #   config.mailer = "UserMailer"
 #   config.method = "new_post"
 # end
 #
 # bulk_deliver_by :slack do |config|
 #   config.url = -> { Rails.application.credentials.slack_webhook_url }
 # end
 #
 # deliver_by :custom do |config|
 #   config.class = "MyDeliveryMethod"
 # end
 #
 # Add required params
 #
 # required_param :message
end

In the boilerplate code, you can see there's⁣deliver_by: email, so we will implement the mailer delivery method first.

We won't complicate things for now; we'll start with one notifier to give you an idea, and then you can implement another notifier on your own.

So we need to notify the user when he is invited to an organization. First, clean up the commented lines and keep only the ones that are needed.

class MembershipInvitationNotifier < ApplicationNotifier
 # Add your delivery methods
 #
 # deliver_by :email do |config|
 #   config.mailer = "UserMailer"
 #   config.method = "new_post"
 # end
 #
 # Add required params
 #
 # required_param :message
end

Notifiers can use different helper methods. Inside a notification_methods block, we also set up the message and URL helpers.

notification_methods do
   # I18n helpers
   def message
     t(".message")
   end

   # URL helpers are accessible in notifications
   # Don't forget to set your default_url_options so Rails knows how to generate urls
   def url
     user_invitations_url
   end
 end

These helpers can be helpful when rendering a user’s notifications on the web.

<div>
  <% @user.notifications.each do |notification| %>
    <%= link_to notification.message, notification.url %>
  <% end %>
</div>

Calling the message helper in the ERB view will look for the following translation path:

# config/locales/en.yml
en:
 notifiers:
   membership_invitation_notifier:
     notification:
       message: You've been invited to join %{organization_name}

Notifiers can choose required parameters using the required_paramsmethod. We need to declare :organization as a required parameter to ensure params[:organization] is available in our notifier:

 required_params :organization
 def message
   t(".message", organization_name: params[:organization].name)
 end

In this case, we need the mailer, so generate it by running

rails generate mailer MembershipMailer invitation_email
# app/mailers/membership_mailer.rb
class MembershipMailer < ApplicationMailer
  # Subject can be set in your I18n file at config/locales/en.yml
  # with the following lookup:
  #
  #   en.membership_mailer.invitation_email.subject
  #
  def invitation_email
    @greeting = "Hi"
    mail to: "to@example.org"
  end
end

app/views/membership_mailer/invitation_email.text.erb

Membership#invitation_email

<%= @greeting %>, find me in app/views/membership_mailer/invitation_email.text.erb

Instead of using this default greeting, we want to send our message and include the action_url. To do this, we need to have the notification object available, so we should pass it from our notifier like this:

deliver_by :email do |config|
  config.mailer = "MembershipMailer"
  config.method = :invitation_email
  config.args   = -> { [ self ] }
end

Now we need to make a small change to our mailer method.

# app/mailers/membership_mailer.rb
class MembershipMailer < ApplicationMailer
  def invitation_email(notification)
    setup(notification)
    mail(to: @recipient.email, subject: t(".subject", organization_name: @organization.name))
  end

  private

  def setup(notification)
    @organization = notification.params[:organization]
    @message = notification.message
    @recipient = notification.recipient
    @action_url = notification.url
  end
end
# app/views/membership_mailer/invitation_email.text.erb
<%= @message %>

View your invitations: <%= @action_url %>

Now our notifier code looks like this:

class MembershipInvitationNotifier < ApplicationNotifier
  # Add your delivery methods
  deliver_by :email do |config|
    config.mailer = "MembershipMailer"
    config.method = :invitation_email
    config.args   = -> { [ self ] }
  end

  # Add required params
  required_params :organization

  notification_methods do
    # I18n helpers
    def message
      t(".message", organization_name: params[:organization].name)
    end

    # URL helpers are accessible in notifications
    # Don't forget to set your default_url_options so Rails knows how to generate urls
    def url
      user_invitations_url
    end
  end
end

Our notifier setup is now complete. Next, we need to trigger this notifier:

# app/models/membership.rb
class Membership < ApplicationRecord
  after_create :send_invitation_notification

  private

  def send_invitation_notification
    MembershipInvitationNotifier.with(organization: organization).deliver(user)
  end
end

With this setup in place, we have everything necessary to notify a user when they are invited to join an organization. Thank you for taking the time to read.

0
Subscribe to my newsletter

Read articles from Aniket Patidar directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Aniket Patidar
Aniket Patidar

An enthusiastic Full Stack Ruby on Rails Engineer from India, who enjoys writing from time to time. I started this blog as a way of motivating myself into making meaningful contributions. Most of these writings are Ruby posts with a few smatterings of opinion with regard to developer life.