Multichannel Notifications Using Twilio Notify, Ruby Sinatra, and Swift

When you’ve got something important and time-sensitive to coordinate -- say, a flash mob for a birthday surprise! -- it’s critical to capture attention quickly. One quick way to ensure that they’re paying attention? Allow your users to receive notifications on their preferred device.

 

Multichannel notifications are a breeze with Twilio Notify -- we’ll build a simple flash mob coordinator app with Ruby and Sinatra to get you started on Notify quickly, then take things beyond SMS with Swift. In less than an hour, your users will be able to subscribe to SMS and/or native push notifications (their choice!) so you can be sure everyone shows up on time. Whether their dance moves are on point… you might need another tutorial for that.

 

birthday-flashmob

 

Get Set Up

We’re going to use the following tools to build our flash mob app:

  • Twilio Phone Numbers
  • Twilio Messaging Service
  • Twilio Notify
  • Ruby and the Sinatra Framework
  • Swift for iOS native notifications

 

If you need a brush-up on using Twilio with Ruby/Sinatra, check out the Twilio Ruby Quickstart here and our dev environment setup recommendations here. Want an iOS-specific Quickstart for Notify? We’ve got you covered here (we’ll be basing most of the iOS portion of this tutorial on that Quickstart).

 

Note: Make sure you have consent from users to receive notifications

It is best practice, and also potentially required by law in certain jurisdictions, for you to have consent from your end users before sending messages to them, and you should respect your end users' choice to not receive messages from you. It is also important to make sure your database is up to date. This is particularly important for number-based communications like SMS because over time phone numbers may be reassigned to different individuals. If your database is out of date, you could inadvertently send a message to someone who did not consent but were reassigned a phone number that was previously subscribed to your service by another person. Check out the Twilio Marketplace for Add-ons from our partners that can help you keep your database up to date.

Twilio recommends that you consult with your legal counsel to make sure that you are complying with all applicable laws in connection with communications you transmit using Twilio.

 

Go ahead and clone the Sinatra app and the iOS app from Github and follow the instructions in the README files to get up and running in your local dev environment. Let’s get started!

 

Create a Messaging Service

First, we’re going to need to set up an SMS Messaging Service via the Twilio Console.  Navigate to the Twilio console and find Messaging Services under Programmable SMS. Tap the + icon to create a new Messaging Service, and give it a friendly name (how about “Flash Mob” for this case?). Set this service as “mixed” and attach it to the Twilio number you’d like to connect to the flash mob app. (Need a phone number? You can purchase one via the Twilio console.)

messaging-services

 

 

Configure Push Credentials

Let’s add another layer -- native push notifications for our iOS users! First, clone the iOS app and find the unique identifier:

bundle-identifier

Feel free to change this identifier to something else if you prefer.


Then, you’ll need to generate a Twilio credential for Apple Push Notifications (APN). There’s a handy guide here -- you’ll need your unique ID from the previous step, and an Apple developer account.

 

Create a Notify Service Instance

Once we’ve got a Messaging Service instance and push credentials, we can set up a corresponding Notify Service. Just tap the + icon on the Notify Services page and select a Friendly Name. In this case, we’ll call ours “Flash Mob.”

We’ll then be able to associate the service with our Messaging Service and other push credentials. Once the service instance is created, you can select your existing messaging and push configuration from drop-downs.

configure-notify-service

 

Set Up the Server App and Run in Ngrok

Once we’ve got a Notify Service, we can add that to the .env file in our Sinatra app along with our Twilio account SID and Auth Token. If you haven’t yet copied the .env.example file to .env, do that now.

These environment variables are used to set up our Twilio REST client and connect our Notify service to the app.

Run bundle install to install gems locally, then bundle exec rackup to start the server. The app should run locally on port 9292.


Since we’d like to test the app on a mobile device, we can use ngrok to create a public tunnel to our locally-running application. Download and install ngrok, then run ngrok http 9292 from the command line to start ngrok for port 9292 and create a public URL. 

 

Loading Code Samples...
Language
require 'sinatra/base'
require 'sinatra/config_file'
require 'sinatra/flash'
require 'tilt/haml'
require 'dotenv/load'

require_relative 'lib/binding_creator'

ENV['RACK_ENV'] ||= 'development'

require 'bundler'
Bundler.require :default, ENV['RACK_ENV'].to_sym

module FlashMob
  class App < Sinatra::Base
    register Sinatra::ConfigFile
    config_file 'config/app.yml'

    enable :sessions
    register Sinatra::Flash
    set :root, File.dirname(__FILE__)

    # Connect to your Notify service
    before do
      account_sid = ENV['TWILIO_ACCOUNT_SID']
      auth_token = ENV['TWILIO_AUTH_TOKEN']
      client = Twilio::REST::Client.new(account_sid, auth_token)
      @service = client.notify.v1.services(ENV['TWILIO_NOTIFY_SERVICE'])
    end

    # User-facing notification sign-up
    get '/' do
      haml :'/signup'
    end

    # Create a binding when the user successfully registers via the web or 
    # mobile app
    post '/register' do
      begin 
        BindingCreator.create_binding(
          params[:type], params[:address], params[:tag], params[:identity])
        if params[:type] == 'sms'
          redirect to('/welcome')
        end
      rescue Twilio::REST::RestError => error
        @error = error
        flash[:error] = 'Please enter a valid phone number.'
        redirect to('/')
      end
    end


    # Organizer-specific message composition 
    # (you may wish to hide this behind an auth wall to prevent users from 
    # sending messages to the group)
    get '/compose' do
      haml :'/compose'
    end

    # Send a notification to participants who registered (use the tag param to
    # determine full or segmented audience)
    post '/send_message' do
      notification = @service.notifications.create(
        body: params[:message],
        tag: params[:tag])
      redirect to('/confirmation')
    end

    # Registration confirmation page
    get '/welcome' do
      haml :'/register_confirmation'
    end

    # Message confirmation page
    get '/confirmation' do
      haml :'/send_confirmation'
    end
  end
end
Declare environment variables so you can connect to your Notify service
Set up Twilio Notify in Sinatra

Declare environment variables so you can connect to your Notify service

Create SMS Binding

Next, we’ll need to create our first Notification Binding. This binding is our means of registering users for notifications based on their identity, address, and, optionally, tags. First, we’ll send a request to the /register endpoint.

 

The create_binding method will be called, which includes the binding create action from our Notify service.

 

Let’s break down the four args that we need to create a binding and how they’re used here:

Identity: Since this lightweight sample does not have a persistent User model, we’re generating random identities for each new binding. If you’re bringing these snippets into an existing application, we recommend using whichever form of UUID you’re already comfortable with. You can send notifications to individuals based on this identity, as well as to all registered users or by segment.

Binding type: The binding type represents the transport technology used for the notification. Here, we’ll see apn for Apple Push Notifications and sms for SMS notifications. As you may recall from the Notify Service configuration options, other binding types include fcm, gcm, and facebook-messenger.

Address: The address is the unique address for the recipient, based on the delivery channel. For SMS, this is a phone number (we wrote a quick formatting function to make sure inputs are in the correct E.164 format), and for APN this is the device token. See our bindings reference for appropriate address formats for other channels.

Tag: We’ll use tags later to send notifications to specific segments of our registered users. In this sample, web-based POST requests will come with a tag of “sms-recipient,” and app-based requests will have a tag of “ios-recipient.” This way, we can easily send SMS or push notifications separately, but you can still set any tags that you’d like.

Loading Code Samples...
Language
module BindingCreator
  def self.create_binding(binding_type, address, tag, identity)
    account_sid = ENV['TWILIO_ACCOUNT_SID']
    auth_token = ENV['TWILIO_AUTH_TOKEN']

    client = Twilio::REST::Client.new(account_sid, auth_token)

    service = client.notify.v1.services(ENV['TWILIO_NOTIFY_SERVICE'])

    # Set a random identity (if you have a model representing participants, 
    # their UUID is a good substitute here -- this is random for illustration 
    # purposes only, as this sample code does not have a User model)
    if identity.nil?
      identity = ('a'..'z').to_a.shuffle[0,8].join
    end

    # Format phone number type addresses with country code (for this sample, 
    # we'll use US only) and remove punctuation
    if binding_type == 'sms' && address[0..1] != "+1"
      address = "+1#{address.gsub(/[^0-9]/, '')}"
    end

    # Create a binding with your Notify service
    binding = service.bindings.create(
      identity: identity,
      binding_type: binding_type,
      address: address,
      tag: tag
    )

    # Send a transactional notification to let your users know that they have
    # successfully been registered for notifications
    notification = service.notifications.create(
      body: "Great, you're signed up! I'll let you know when and where to go when it's time for the surprise.",
      identity: identity
    )
  end
end
Create a Notify Binding in Sinatra

Send Transactional Notification

The first kind of notification we’ll send is one that goes to a single recipient upon sign-up. Just after the binding is created, we’ll use this transactional notification to confirm that they’ve opted in.

This is going to be our marker for a successful binding. Let’s run the server app and access it from a mobile device via ngrok, then enter a phone number to create an SMS binding and receive this notification.

SMS transactional notification

Configure the iOS Client

Now that our server app is good to go and we’ve set up push credentials, it’s time to open up the Swift app and get ready to receive push notifications.

In the ViewController.swift file, on this line:

var serverURL : String = "https://YOUR_SERVER_URL/register"

replace YOUR_SERVER_URL with the address of your server - if you’re using ngrok locally, it should be your ngrok url.

 

The app passes your device's device token as the address param in its request to the /register endpoint in the server app. This unique identifier is then used by Twilio Notify, along with a generated identity, a tag of our choosing, and the binding type apn, to create a binding.

 

Once you've entered your URL, you can compile and run the app on a device. When you tap register, the app will register your device with APNS and return a JSON response object if successful. The push response should match our default transactional notification.

register for notification on iOS

You can then close or background the app and await further notifications. 

Loading Code Samples...
Language
//
//  ViewController.swift
//  notifications
//
//  Created by Twilio Developer Education on 10/14/17.
//  Copyright © 2017 Twilio. All rights reserved.
//

import UIKit

class ViewController: UIViewController {

// Replace this with the NGROK or permanent URL serving your application
  var serverURL = "https://YOUR_SERVER_URL/register"
  var message:String = "tap the button to receive push alerts when there are updates"
  @IBOutlet var registerButton: UIButton!
  @IBOutlet weak var messageLabel: UILabel!
    

  override func viewDidLoad() {
    super.viewDidLoad()
  }

  override func viewWillAppear(_ animated: Bool) {
        // set the message label text to either the default text or, if applicable, push alert text
        setMessageOnPush()
    
        // hide the button if the view is appearing after a push alert has been received
        if message == "tap the button to receive push alerts when there are updates" {
            registerButton.isHidden = false
        }
        else {
            registerButton.isHidden = true
        }
    }

  override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    // Dispose of any resources that can be recreated.
  }

  @IBAction func didTapRegister(_ sender: UIButton) {
    // fetch the device token and send it as the address param in the POST request to /register
      let appDelegate = UIApplication.shared.delegate as! AppDelegate
      let deviceToken : String! = appDelegate.devToken
      registerDevice(deviceToken: deviceToken)
      resignFirstResponder()
  }

  
  func registerDevice(deviceToken: String) {
    // pass query params to our endpoint, including the deviceToken, which will be used to register push notifications from our app
    var components = URLComponents(string: serverURL)
    components?.queryItems = [
        URLQueryItem(name: "type", value: "apn"),
        URLQueryItem(name: "address", value: deviceToken),
        URLQueryItem(name: "tag", value: "ios-participant"),
    ]
    guard let url = components?.url else { return }
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    let task = URLSession.shared.dataTask(with: request) { data, response, error in
        guard let data = data, error == nil else {  // check for fundamental networking error
            print("error=\(String(describing: error))")
            return
        }
        if let httpStatus = response as? HTTPURLResponse, httpStatus.statusCode != 200 {  // check for http errors
            print("statusCode should be 200, but is \(httpStatus.statusCode)")
            print("response = \(String(describing: response))")
        }
        let responseString = String(data: data, encoding: .utf8)
        print("responseString = \(String(describing: responseString))")
    }
    task.resume()
  }
    
    
    func setMessageOnPush(){
        // update the label to reflect notification text, hide button since the user has already registered
        messageLabel.text = message
        registerButton.isHidden = true
    }



}
Register users for notifications with a request
Create a Notify Binding in Swift

Register users for notifications with a request

Send a Bulk Notification

Now the fun part: we’re going to get the whole group together and send a notification to everyone. Not only will everyone receive a notification, but it will be on their platform of choice -- in this case, SMS or iOS push.


Navigate to /compose in your browser to draft a message to send via Notify. Tapping the send button will POST to the /send_message endpoint, sending notifications to all recipients with the message of your choice as the body. When we send to all users, we’re using the default tag of all to encompass all registered tags.

 

 

Loading Code Samples...
Language
require 'sinatra/base'
require 'sinatra/config_file'
require 'sinatra/flash'
require 'tilt/haml'
require 'dotenv/load'

require_relative 'lib/binding_creator'

ENV['RACK_ENV'] ||= 'development'

require 'bundler'
Bundler.require :default, ENV['RACK_ENV'].to_sym

module FlashMob
  class App < Sinatra::Base
    register Sinatra::ConfigFile
    config_file 'config/app.yml'

    enable :sessions
    register Sinatra::Flash
    set :root, File.dirname(__FILE__)

    # Connect to your Notify service
    before do
      account_sid = ENV['TWILIO_ACCOUNT_SID']
      auth_token = ENV['TWILIO_AUTH_TOKEN']
      client = Twilio::REST::Client.new(account_sid, auth_token)
      @service = client.notify.v1.services(ENV['TWILIO_NOTIFY_SERVICE'])
    end

    # User-facing notification sign-up
    get '/' do
      haml :'/signup'
    end

    # Create a binding when the user successfully registers via the web or 
    # mobile app
    post '/register' do
      begin 
        BindingCreator.create_binding(
          params[:type], params[:address], params[:tag], params[:identity])
        if params[:type] == 'sms'
          redirect to('/welcome')
        end
      rescue Twilio::REST::RestError => error
        @error = error
        flash[:error] = 'Please enter a valid phone number.'
        redirect to('/')
      end
    end


    # Organizer-specific message composition 
    # (you may wish to hide this behind an auth wall to prevent users from 
    # sending messages to the group)
    get '/compose' do
      haml :'/compose'
    end

    # Send a notification to participants who registered (use the tag param to
    # determine full or segmented audience)
    post '/send_message' do
      notification = @service.notifications.create(
        body: params[:message],
        tag: params[:tag])
      redirect to('/confirmation')
    end

    # Registration confirmation page
    get '/welcome' do
      haml :'/register_confirmation'
    end

    # Message confirmation page
    get '/confirmation' do
      haml :'/send_confirmation'
    end
  end
end
Send notifications to multiple registered users
Send a Bulk Notification

Send notifications to multiple registered users

Send Notifications to User Segments

Finally, we’re going to take a look at how Notify tags can be used to segment notification recipients. Here we’ll group recipients with tags based on the type of notification, SMS or push, but in your apps, you can set tags based on any factors that are important to you.


If you noticed the two additional buttons in the compose view -- send to sms users only and send to iOS users only -- those are wired up to specific tags. Just as when we sent bulk notifications to all users with the all tag, we can pass a specific tag to the create action to limit our recipient group. You can set custom tags when bindings are created, based on any factors you like, such as fields on your User model, to craft meaningful segments for notifications.

 

compose a message for your notifications

Loading Code Samples...
Language
%h4 Tell everyone about the surprise!

%p What would you like to tell the group? Keep it short and sweet so it can fit into a text or a push notification!

%form.form-horizontal{ action: '/send_message', method: 'POST' }
  .form-group
    .col-sm-12
      %input.form-control{ name: :message }
  .form-group
    .col-sm-4
      %button.btn.btn-success{ name: :tag, value: 'all' } Send to all users
    .col-sm-4
      %button.btn.btn-success{ name: :tag, value: 'sms-participant' } Send to SMS users only    
    .col-sm-4
      %button.btn.btn-success{ name: :tag, value: 'ios-participant' } Send to iOS users only
Pass Tag Params to Send Segmented Notifications

Next Steps

Enjoyed setting up a surprise flash mob? Twilio Notify can be used for many more things and across even more channels. With specialized tags, you can segment recipients, so the right set of customers receive your targeted SMS or push notifications. And with additional channel integrations, you can set up a Notify app that reaches your customers via Facebook Messenger, send native Android push notifications, and even use Alexa via the Amazon Echo.  We can’t wait to see what you build!

Jennifer Aprahamian
Kat King

Need some help?

We all do sometimes; code is hard. Get help now from our support team, or lean on the wisdom of the crowd browsing the Twilio tag on Stack Overflow.

1 / 1
Loading Code Samples...
require 'sinatra/base'
require 'sinatra/config_file'
require 'sinatra/flash'
require 'tilt/haml'
require 'dotenv/load'

require_relative 'lib/binding_creator'

ENV['RACK_ENV'] ||= 'development'

require 'bundler'
Bundler.require :default, ENV['RACK_ENV'].to_sym

module FlashMob
  class App < Sinatra::Base
    register Sinatra::ConfigFile
    config_file 'config/app.yml'

    enable :sessions
    register Sinatra::Flash
    set :root, File.dirname(__FILE__)

    # Connect to your Notify service
    before do
      account_sid = ENV['TWILIO_ACCOUNT_SID']
      auth_token = ENV['TWILIO_AUTH_TOKEN']
      client = Twilio::REST::Client.new(account_sid, auth_token)
      @service = client.notify.v1.services(ENV['TWILIO_NOTIFY_SERVICE'])
    end

    # User-facing notification sign-up
    get '/' do
      haml :'/signup'
    end

    # Create a binding when the user successfully registers via the web or 
    # mobile app
    post '/register' do
      begin 
        BindingCreator.create_binding(
          params[:type], params[:address], params[:tag], params[:identity])
        if params[:type] == 'sms'
          redirect to('/welcome')
        end
      rescue Twilio::REST::RestError => error
        @error = error
        flash[:error] = 'Please enter a valid phone number.'
        redirect to('/')
      end
    end


    # Organizer-specific message composition 
    # (you may wish to hide this behind an auth wall to prevent users from 
    # sending messages to the group)
    get '/compose' do
      haml :'/compose'
    end

    # Send a notification to participants who registered (use the tag param to
    # determine full or segmented audience)
    post '/send_message' do
      notification = @service.notifications.create(
        body: params[:message],
        tag: params[:tag])
      redirect to('/confirmation')
    end

    # Registration confirmation page
    get '/welcome' do
      haml :'/register_confirmation'
    end

    # Message confirmation page
    get '/confirmation' do
      haml :'/send_confirmation'
    end
  end
end
module BindingCreator
  def self.create_binding(binding_type, address, tag, identity)
    account_sid = ENV['TWILIO_ACCOUNT_SID']
    auth_token = ENV['TWILIO_AUTH_TOKEN']

    client = Twilio::REST::Client.new(account_sid, auth_token)

    service = client.notify.v1.services(ENV['TWILIO_NOTIFY_SERVICE'])

    # Set a random identity (if you have a model representing participants, 
    # their UUID is a good substitute here -- this is random for illustration 
    # purposes only, as this sample code does not have a User model)
    if identity.nil?
      identity = ('a'..'z').to_a.shuffle[0,8].join
    end

    # Format phone number type addresses with country code (for this sample, 
    # we'll use US only) and remove punctuation
    if binding_type == 'sms' && address[0..1] != "+1"
      address = "+1#{address.gsub(/[^0-9]/, '')}"
    end

    # Create a binding with your Notify service
    binding = service.bindings.create(
      identity: identity,
      binding_type: binding_type,
      address: address,
      tag: tag
    )

    # Send a transactional notification to let your users know that they have
    # successfully been registered for notifications
    notification = service.notifications.create(
      body: "Great, you're signed up! I'll let you know when and where to go when it's time for the surprise.",
      identity: identity
    )
  end
end
//
//  ViewController.swift
//  notifications
//
//  Created by Twilio Developer Education on 10/14/17.
//  Copyright © 2017 Twilio. All rights reserved.
//

import UIKit

class ViewController: UIViewController {

// Replace this with the NGROK or permanent URL serving your application
  var serverURL = "https://YOUR_SERVER_URL/register"
  var message:String = "tap the button to receive push alerts when there are updates"
  @IBOutlet var registerButton: UIButton!
  @IBOutlet weak var messageLabel: UILabel!
    

  override func viewDidLoad() {
    super.viewDidLoad()
  }

  override func viewWillAppear(_ animated: Bool) {
        // set the message label text to either the default text or, if applicable, push alert text
        setMessageOnPush()
    
        // hide the button if the view is appearing after a push alert has been received
        if message == "tap the button to receive push alerts when there are updates" {
            registerButton.isHidden = false
        }
        else {
            registerButton.isHidden = true
        }
    }

  override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    // Dispose of any resources that can be recreated.
  }

  @IBAction func didTapRegister(_ sender: UIButton) {
    // fetch the device token and send it as the address param in the POST request to /register
      let appDelegate = UIApplication.shared.delegate as! AppDelegate
      let deviceToken : String! = appDelegate.devToken
      registerDevice(deviceToken: deviceToken)
      resignFirstResponder()
  }

  
  func registerDevice(deviceToken: String) {
    // pass query params to our endpoint, including the deviceToken, which will be used to register push notifications from our app
    var components = URLComponents(string: serverURL)
    components?.queryItems = [
        URLQueryItem(name: "type", value: "apn"),
        URLQueryItem(name: "address", value: deviceToken),
        URLQueryItem(name: "tag", value: "ios-participant"),
    ]
    guard let url = components?.url else { return }
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    let task = URLSession.shared.dataTask(with: request) { data, response, error in
        guard let data = data, error == nil else {  // check for fundamental networking error
            print("error=\(String(describing: error))")
            return
        }
        if let httpStatus = response as? HTTPURLResponse, httpStatus.statusCode != 200 {  // check for http errors
            print("statusCode should be 200, but is \(httpStatus.statusCode)")
            print("response = \(String(describing: response))")
        }
        let responseString = String(data: data, encoding: .utf8)
        print("responseString = \(String(describing: responseString))")
    }
    task.resume()
  }
    
    
    func setMessageOnPush(){
        // update the label to reflect notification text, hide button since the user has already registered
        messageLabel.text = message
        registerButton.isHidden = true
    }



}
require 'sinatra/base'
require 'sinatra/config_file'
require 'sinatra/flash'
require 'tilt/haml'
require 'dotenv/load'

require_relative 'lib/binding_creator'

ENV['RACK_ENV'] ||= 'development'

require 'bundler'
Bundler.require :default, ENV['RACK_ENV'].to_sym

module FlashMob
  class App < Sinatra::Base
    register Sinatra::ConfigFile
    config_file 'config/app.yml'

    enable :sessions
    register Sinatra::Flash
    set :root, File.dirname(__FILE__)

    # Connect to your Notify service
    before do
      account_sid = ENV['TWILIO_ACCOUNT_SID']
      auth_token = ENV['TWILIO_AUTH_TOKEN']
      client = Twilio::REST::Client.new(account_sid, auth_token)
      @service = client.notify.v1.services(ENV['TWILIO_NOTIFY_SERVICE'])
    end

    # User-facing notification sign-up
    get '/' do
      haml :'/signup'
    end

    # Create a binding when the user successfully registers via the web or 
    # mobile app
    post '/register' do
      begin 
        BindingCreator.create_binding(
          params[:type], params[:address], params[:tag], params[:identity])
        if params[:type] == 'sms'
          redirect to('/welcome')
        end
      rescue Twilio::REST::RestError => error
        @error = error
        flash[:error] = 'Please enter a valid phone number.'
        redirect to('/')
      end
    end


    # Organizer-specific message composition 
    # (you may wish to hide this behind an auth wall to prevent users from 
    # sending messages to the group)
    get '/compose' do
      haml :'/compose'
    end

    # Send a notification to participants who registered (use the tag param to
    # determine full or segmented audience)
    post '/send_message' do
      notification = @service.notifications.create(
        body: params[:message],
        tag: params[:tag])
      redirect to('/confirmation')
    end

    # Registration confirmation page
    get '/welcome' do
      haml :'/register_confirmation'
    end

    # Message confirmation page
    get '/confirmation' do
      haml :'/send_confirmation'
    end
  end
end
%h4 Tell everyone about the surprise!

%p What would you like to tell the group? Keep it short and sweet so it can fit into a text or a push notification!

%form.form-horizontal{ action: '/send_message', method: 'POST' }
  .form-group
    .col-sm-12
      %input.form-control{ name: :message }
  .form-group
    .col-sm-4
      %button.btn.btn-success{ name: :tag, value: 'all' } Send to all users
    .col-sm-4
      %button.btn.btn-success{ name: :tag, value: 'sms-participant' } Send to SMS users only    
    .col-sm-4
      %button.btn.btn-success{ name: :tag, value: 'ios-participant' } Send to iOS users only