This Rails web application shows how you can use Twilio Client to make browser-to-phone and browser-to-browser calls.
This application powers the support site for the Birchwood Bicycle Polo Co., which sells equipment to those who play "the sport of kings." It has three main features:
In this tutorial, we'll point out the key bits of code that make this application work. Check out the project README on GitHub to see how to run the code yourself.
Learn how Zendesk uses Twilio Client to make phone support available in 40+ countries.
The home page of our app displays a form for customers to submit support tickets. We use Rails layouts to power the page.
The Ticket
model itself has just a few fields:
class Ticket < ActiveRecord::Base validates :name, :phone_number, :description, presence: true end
class TicketsController < ApplicationController
def create
support_ticket = Ticket.new(ticket_params)
if support_ticket.save
redirect_to root_path, notice:
'Your ticket was submitted! An agent will call you soon.'
else
redirect_to root_path, flash:
{ error: support_ticket.errors.full_messages }
end
end
def ticket_params
params.require(:ticket).permit(:name, :phone_number, :description)
end
end
Now we can create a ticket. Next up, let's work on the dashboard for the support agent.
When a support agent visits /dashboard
, they see all the support tickets which have been submitted.
Each ticket also has a "Call Customer" button which invokes a JavaScript function we wrote named callCustomer()
. That function kicks off a Twilio Client call to the phone number passed as its sole parameter.
<h2>Support Tickets</h2>
<p class="lead">
This is the list of most recent support tickets. Click the "Call customer" button to start a phone call from your browser.
</p>
<div class="row">
<div class="col-md-4 col-md-push-8">
<div class="panel panel-primary client-controls">
<div class="panel-heading">
<h3 class="panel-title">Make a call</h3>
</div>
<div class="panel-body">
<p><strong>Status</strong></p>
<div class="well well-sm" id="call-status">
Connecting to Twilio...
</div>
<button class="btn btn-lg btn-success answer-button" disabled>Answer call</button>
<button class="btn btn-lg btn-danger hangup-button" disabled onclick="hangUp()">Hang up</button>
</div>
</div>
</div>
<div class="col-md-8 col-md-pull-4">
<% @tickets.each do |ticket| %>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Ticket #<%= ticket.id %> <small class="pull-right"><%= ticket.created_at %></small></h3>
</div>
<div class="panel-body">
<div class="pull-right">
<button onclick="callCustomer('<%= ticket.phone_number %>')" type="button" class="btn btn-primary btn-lg call-customer-button">
<span class="glyphicon glyphicon-earphone" aria-hidden="true"></span>
Call customer
</button>
</div>
<p><strong>Name:</strong> <%= ticket.name %></p>
<p><strong>Phone number:</strong> <%= ticket.phone_number %></p>
<p><strong>Description:</strong></p>
<%= ticket.description %>
</div>
</div>
<% end %>
</div>
</div>
Great, now we have an interface good enough for our support agents. Next up, in order to let our agents make calls to their customers through the browser, we need to provide them a capability token.
We use the Twilio Ruby helper library to generate and configure our capability tokens. To allow our support agents to call the phone numbers on our tickets, we use the OutgoingClientScope
class.
That method requires an identifier for a TwiML Application. Twilio will send a POST request to our backend every time a user makes a Twilio Client call — the TwiML Application tells Twilio which URL to send that request to.
require 'pry'
class TwilioCapability
def self.generate(role)
# To find TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN visit
# https://www.twilio.com/user/account
account_sid = ENV['TWILIO_ACCOUNT_SID']
auth_token = ENV['TWILIO_AUTH_TOKEN']
# binding.pry
capability = Twilio::JWT::ClientCapability.new(account_sid, auth_token)
outgoing_scope = Twilio::JWT::ClientCapability::OutgoingClientScope.new(account_sid, role)
capability.add_scope outgoing_scope
incoming_scope = Twilio::JWT::ClientCapability::IncomingClientScope.new(role)
capability.add_scope incoming_scope
application_sid = ENV['TWIML_APPLICATION_SID']
capability.to_s
end
end
Once we are equipped with our Capability Token, the next step is to set up the Twilio Device Client in the browser.
To use the Twilio Device Client in a web browser we use the twilio.js library.
We start by retrieving a capability token from the view we defined in the previous step with a POST request through AJAX. Then we enable the Twilio Device Client for this page by passing our token to Twilio.Device.setup()
.
After that the Twilio.Device.ready()
callback will let us know when the browser is ready to make and receive calls.
/**
* Twilio Client configuration for the browser-calls-rails
* example application.
*/
// Store some selectors for elements we'll reuse
var callStatus = $("#call-status");
var answerButton = $(".answer-button");
var callSupportButton = $(".call-support-button");
var hangUpButton = $(".hangup-button");
var callCustomerButtons = $(".call-customer-button");
/* Helper function to update the call status bar */
function updateCallStatus(status) {
callStatus.text(status);
}
/* Get a Twilio Client token with an AJAX request */
$(document).ready(function() {
$.post("/token/generate", {page: window.location.pathname}, function(data) {
// Set up the Twilio Client Device with the token
Twilio.Device.setup(data.token);
});
});
/* Callback to let us know Twilio Client is ready */
Twilio.Device.ready(function (device) {
updateCallStatus("Ready");
});
/* Report any errors to the call status display */
Twilio.Device.error(function (error) {
updateCallStatus("ERROR: " + error.message);
});
/* Callback for when Twilio Client initiates a new connection */
Twilio.Device.connect(function (connection) {
// Enable the hang up button and disable the call buttons
hangUpButton.prop("disabled", false);
callCustomerButtons.prop("disabled", true);
callSupportButton.prop("disabled", true);
answerButton.prop("disabled", true);
// If phoneNumber is part of the connection, this is a call from a
// support agent to a customer's phone
if ("phoneNumber" in connection.message) {
updateCallStatus("In call with " + connection.message.phoneNumber);
} else {
// This is a call from a website user to a support agent
updateCallStatus("In call with support");
}
});
/* Callback for when a call ends */
Twilio.Device.disconnect(function(connection) {
// Disable the hangup button and enable the call buttons
hangUpButton.prop("disabled", true);
callCustomerButtons.prop("disabled", false);
callSupportButton.prop("disabled", false);
updateCallStatus("Ready");
});
/* Callback for when Twilio Client receives a new incoming call */
Twilio.Device.incoming(function(connection) {
updateCallStatus("Incoming support call");
// Set a callback to be executed when the connection is accepted
connection.accept(function() {
updateCallStatus("In call with customer");
});
// Set a callback on the answer button and enable it
answerButton.click(function() {
connection.accept();
});
answerButton.prop("disabled", false);
});
/* Call a customer from a support ticket */
function callCustomer(phoneNumber) {
updateCallStatus("Calling " + phoneNumber + "...");
var params = {"phoneNumber": phoneNumber};
Twilio.Device.connect(params);
}
/* Call the support_agent from the home page */
function callSupport() {
updateCallStatus("Calling support...");
// Our backend will assume that no params means a call to support_agent
Twilio.Device.connect();
}
/* End a call */
function hangUp() {
Twilio.Device.disconnectAll();
}
Now that we have almost everything in place, it's time to see the core of this tutorial. Let's look at how we can let our agents start a call from their browsers.
When our support agents click on the Call Customer button on a support ticket, the function highlighted on the code snippet will initiate the call.
We use Twilio.Device.connect()
to begin a new outgoing call. Our backend will tell Twilio how to handle this call, so we include a phoneNumber
parameter that we'll use on our call
view.
/**
* Twilio Client configuration for the browser-calls-rails
* example application.
*/
// Store some selectors for elements we'll reuse
var callStatus = $("#call-status");
var answerButton = $(".answer-button");
var callSupportButton = $(".call-support-button");
var hangUpButton = $(".hangup-button");
var callCustomerButtons = $(".call-customer-button");
/* Helper function to update the call status bar */
function updateCallStatus(status) {
callStatus.text(status);
}
/* Get a Twilio Client token with an AJAX request */
$(document).ready(function() {
$.post("/token/generate", {page: window.location.pathname}, function(data) {
// Set up the Twilio Client Device with the token
Twilio.Device.setup(data.token);
});
});
/* Callback to let us know Twilio Client is ready */
Twilio.Device.ready(function (device) {
updateCallStatus("Ready");
});
/* Report any errors to the call status display */
Twilio.Device.error(function (error) {
updateCallStatus("ERROR: " + error.message);
});
/* Callback for when Twilio Client initiates a new connection */
Twilio.Device.connect(function (connection) {
// Enable the hang up button and disable the call buttons
hangUpButton.prop("disabled", false);
callCustomerButtons.prop("disabled", true);
callSupportButton.prop("disabled", true);
answerButton.prop("disabled", true);
// If phoneNumber is part of the connection, this is a call from a
// support agent to a customer's phone
if ("phoneNumber" in connection.message) {
updateCallStatus("In call with " + connection.message.phoneNumber);
} else {
// This is a call from a website user to a support agent
updateCallStatus("In call with support");
}
});
/* Callback for when a call ends */
Twilio.Device.disconnect(function(connection) {
// Disable the hangup button and enable the call buttons
hangUpButton.prop("disabled", true);
callCustomerButtons.prop("disabled", false);
callSupportButton.prop("disabled", false);
updateCallStatus("Ready");
});
/* Callback for when Twilio Client receives a new incoming call */
Twilio.Device.incoming(function(connection) {
updateCallStatus("Incoming support call");
// Set a callback to be executed when the connection is accepted
connection.accept(function() {
updateCallStatus("In call with customer");
});
// Set a callback on the answer button and enable it
answerButton.click(function() {
connection.accept();
});
answerButton.prop("disabled", false);
});
/* Call a customer from a support ticket */
function callCustomer(phoneNumber) {
updateCallStatus("Calling " + phoneNumber + "...");
var params = {"phoneNumber": phoneNumber};
Twilio.Device.connect(params);
}
/* Call the support_agent from the home page */
function callSupport() {
updateCallStatus("Calling support...");
// Our backend will assume that no params means a call to support_agent
Twilio.Device.connect();
}
/* End a call */
function hangUp() {
Twilio.Device.disconnectAll();
}
Great! Now our agents are able to make calls to their customers. Next up, let's look at how to connect that call to a phone number.
Whenever one of our users makes a call, Twilio will send a POST request to the URL we set on our TwiML Application - in this case, /call/connect
.
We use TwiML to respond to the request and tell Twilio how to handle the call. Twilio will pass along the phoneNumber
parameter from the previous step in its request, which we will then Dial in our TwiML.
class CallController < ApplicationController
skip_before_filter :verify_authenticity_token
def connect
render xml: twilio_reponse
end
private
def twilio_reponse
twilio_number = ENV['TWILIO_PHONE_NUMBER']
res = Twilio::TwiML::VoiceResponse.new do |response|
dial = Twilio::TwiML::Dial.new caller_id: twilio_number
if params.include?(:phoneNumber)
dial.number params[:phoneNumber]
else
dial.client('support_agent')
end
response.append(dial)
end
return res.to_s
end
end
After our call
view responds, Twilio completes the connection between our support agent's browser and the customer's phone. Let's go back to the browser and see how this is handled.
We use the Twilio.Device.connect()
callback to update some UI elements to notify our users that they are in a call. This function receives a Connection object, which offers some additional details about the call.
/**
* Twilio Client configuration for the browser-calls-rails
* example application.
*/
// Store some selectors for elements we'll reuse
var callStatus = $("#call-status");
var answerButton = $(".answer-button");
var callSupportButton = $(".call-support-button");
var hangUpButton = $(".hangup-button");
var callCustomerButtons = $(".call-customer-button");
/* Helper function to update the call status bar */
function updateCallStatus(status) {
callStatus.text(status);
}
/* Get a Twilio Client token with an AJAX request */
$(document).ready(function() {
$.post("/token/generate", {page: window.location.pathname}, function(data) {
// Set up the Twilio Client Device with the token
Twilio.Device.setup(data.token);
});
});
/* Callback to let us know Twilio Client is ready */
Twilio.Device.ready(function (device) {
updateCallStatus("Ready");
});
/* Report any errors to the call status display */
Twilio.Device.error(function (error) {
updateCallStatus("ERROR: " + error.message);
});
/* Callback for when Twilio Client initiates a new connection */
Twilio.Device.connect(function (connection) {
// Enable the hang up button and disable the call buttons
hangUpButton.prop("disabled", false);
callCustomerButtons.prop("disabled", true);
callSupportButton.prop("disabled", true);
answerButton.prop("disabled", true);
// If phoneNumber is part of the connection, this is a call from a
// support agent to a customer's phone
if ("phoneNumber" in connection.message) {
updateCallStatus("In call with " + connection.message.phoneNumber);
} else {
// This is a call from a website user to a support agent
updateCallStatus("In call with support");
}
});
/* Callback for when a call ends */
Twilio.Device.disconnect(function(connection) {
// Disable the hangup button and enable the call buttons
hangUpButton.prop("disabled", true);
callCustomerButtons.prop("disabled", false);
callSupportButton.prop("disabled", false);
updateCallStatus("Ready");
});
/* Callback for when Twilio Client receives a new incoming call */
Twilio.Device.incoming(function(connection) {
updateCallStatus("Incoming support call");
// Set a callback to be executed when the connection is accepted
connection.accept(function() {
updateCallStatus("In call with customer");
});
// Set a callback on the answer button and enable it
answerButton.click(function() {
connection.accept();
});
answerButton.prop("disabled", false);
});
/* Call a customer from a support ticket */
function callCustomer(phoneNumber) {
updateCallStatus("Calling " + phoneNumber + "...");
var params = {"phoneNumber": phoneNumber};
Twilio.Device.connect(params);
}
/* Call the support_agent from the home page */
function callSupport() {
updateCallStatus("Calling support...");
// Our backend will assume that no params means a call to support_agent
Twilio.Device.connect();
}
/* End a call */
function hangUp() {
Twilio.Device.disconnectAll();
}
And that's all for our browser-to-phone example. Next up, we will go even further and show a browser-to-browser example.
Support tickets are useful, but sometimes a customer needs help right now. With just a little more work we let customers speak with a support agent live via a browser-to-browser call.
When a customer clicks the Call support button on the home page we again use Twilio.Device.connect()
to initiate the call. This time we don't pass any additional parameters — our backend will route this call to our support agent.
/**
* Twilio Client configuration for the browser-calls-rails
* example application.
*/
// Store some selectors for elements we'll reuse
var callStatus = $("#call-status");
var answerButton = $(".answer-button");
var callSupportButton = $(".call-support-button");
var hangUpButton = $(".hangup-button");
var callCustomerButtons = $(".call-customer-button");
/* Helper function to update the call status bar */
function updateCallStatus(status) {
callStatus.text(status);
}
/* Get a Twilio Client token with an AJAX request */
$(document).ready(function() {
$.post("/token/generate", {page: window.location.pathname}, function(data) {
// Set up the Twilio Client Device with the token
Twilio.Device.setup(data.token);
});
});
/* Callback to let us know Twilio Client is ready */
Twilio.Device.ready(function (device) {
updateCallStatus("Ready");
});
/* Report any errors to the call status display */
Twilio.Device.error(function (error) {
updateCallStatus("ERROR: " + error.message);
});
/* Callback for when Twilio Client initiates a new connection */
Twilio.Device.connect(function (connection) {
// Enable the hang up button and disable the call buttons
hangUpButton.prop("disabled", false);
callCustomerButtons.prop("disabled", true);
callSupportButton.prop("disabled", true);
answerButton.prop("disabled", true);
// If phoneNumber is part of the connection, this is a call from a
// support agent to a customer's phone
if ("phoneNumber" in connection.message) {
updateCallStatus("In call with " + connection.message.phoneNumber);
} else {
// This is a call from a website user to a support agent
updateCallStatus("In call with support");
}
});
/* Callback for when a call ends */
Twilio.Device.disconnect(function(connection) {
// Disable the hangup button and enable the call buttons
hangUpButton.prop("disabled", true);
callCustomerButtons.prop("disabled", false);
callSupportButton.prop("disabled", false);
updateCallStatus("Ready");
});
/* Callback for when Twilio Client receives a new incoming call */
Twilio.Device.incoming(function(connection) {
updateCallStatus("Incoming support call");
// Set a callback to be executed when the connection is accepted
connection.accept(function() {
updateCallStatus("In call with customer");
});
// Set a callback on the answer button and enable it
answerButton.click(function() {
connection.accept();
});
answerButton.prop("disabled", false);
});
/* Call a customer from a support ticket */
function callCustomer(phoneNumber) {
updateCallStatus("Calling " + phoneNumber + "...");
var params = {"phoneNumber": phoneNumber};
Twilio.Device.connect(params);
}
/* Call the support_agent from the home page */
function callSupport() {
updateCallStatus("Calling support...");
// Our backend will assume that no params means a call to support_agent
Twilio.Device.connect();
}
/* End a call */
function hangUp() {
Twilio.Device.disconnectAll();
}
Setting up the browser-to-browser call was rather simple right? Now let's look at how our backend will route this call to our support agent.
To allow our support agents to accept incoming calls we use the allow_client_incoming() method when generating their capability token, passing support_agent as the client's name:
capability.allow_client_incoming 'support_agent'
Then, when Twilio sends a POST request to our /call/connect
end point, we can connect the call to our support agent by responding with a <Client> TwiML noun and the support_agent
name.
class CallController < ApplicationController
skip_before_filter :verify_authenticity_token
def connect
render xml: twilio_reponse
end
private
def twilio_reponse
twilio_number = ENV['TWILIO_PHONE_NUMBER']
res = Twilio::TwiML::VoiceResponse.new do |response|
dial = Twilio::TwiML::Dial.new caller_id: twilio_number
if params.include?(:phoneNumber)
dial.number params[:phoneNumber]
else
dial.client('support_agent')
end
response.append(dial)
end
return res.to_s
end
end
That's how we prepare everything for accepting incoming calls. Now we should go back in the browser and see how to handle the connection this time.
When our support agent's client receives an incoming call, it will trigger the function we defined on the Twilio.Device.incoming()
callback.
The incoming connection
will be in a pending state until we invoke its .accept()
method, which we do in a function bound to the Answer Call button.
We also set a .accept()
callback to update the UI once the call is live.
/**
* Twilio Client configuration for the browser-calls-rails
* example application.
*/
// Store some selectors for elements we'll reuse
var callStatus = $("#call-status");
var answerButton = $(".answer-button");
var callSupportButton = $(".call-support-button");
var hangUpButton = $(".hangup-button");
var callCustomerButtons = $(".call-customer-button");
/* Helper function to update the call status bar */
function updateCallStatus(status) {
callStatus.text(status);
}
/* Get a Twilio Client token with an AJAX request */
$(document).ready(function() {
$.post("/token/generate", {page: window.location.pathname}, function(data) {
// Set up the Twilio Client Device with the token
Twilio.Device.setup(data.token);
});
});
/* Callback to let us know Twilio Client is ready */
Twilio.Device.ready(function (device) {
updateCallStatus("Ready");
});
/* Report any errors to the call status display */
Twilio.Device.error(function (error) {
updateCallStatus("ERROR: " + error.message);
});
/* Callback for when Twilio Client initiates a new connection */
Twilio.Device.connect(function (connection) {
// Enable the hang up button and disable the call buttons
hangUpButton.prop("disabled", false);
callCustomerButtons.prop("disabled", true);
callSupportButton.prop("disabled", true);
answerButton.prop("disabled", true);
// If phoneNumber is part of the connection, this is a call from a
// support agent to a customer's phone
if ("phoneNumber" in connection.message) {
updateCallStatus("In call with " + connection.message.phoneNumber);
} else {
// This is a call from a website user to a support agent
updateCallStatus("In call with support");
}
});
/* Callback for when a call ends */
Twilio.Device.disconnect(function(connection) {
// Disable the hangup button and enable the call buttons
hangUpButton.prop("disabled", true);
callCustomerButtons.prop("disabled", false);
callSupportButton.prop("disabled", false);
updateCallStatus("Ready");
});
/* Callback for when Twilio Client receives a new incoming call */
Twilio.Device.incoming(function(connection) {
updateCallStatus("Incoming support call");
// Set a callback to be executed when the connection is accepted
connection.accept(function() {
updateCallStatus("In call with customer");
});
// Set a callback on the answer button and enable it
answerButton.click(function() {
connection.accept();
});
answerButton.prop("disabled", false);
});
/* Call a customer from a support ticket */
function callCustomer(phoneNumber) {
updateCallStatus("Calling " + phoneNumber + "...");
var params = {"phoneNumber": phoneNumber};
Twilio.Device.connect(params);
}
/* Call the support_agent from the home page */
function callSupport() {
updateCallStatus("Calling support...");
// Our backend will assume that no params means a call to support_agent
Twilio.Device.connect();
}
/* End a call */
function hangUp() {
Twilio.Device.disconnectAll();
}
Great! Now we know how to work on both cases: browser-to-phone and browser-to-browser calls! Next up, we will see what happens when they decide to hang up the call.
In order to finish a call we invoke Twilio.Device.disconnectAll(),
which we wired to the Hang up button in our UI.
We also pass a callback function to Twilio.Device.disconnect()
above, to reset some UI elements.
/**
* Twilio Client configuration for the browser-calls-rails
* example application.
*/
// Store some selectors for elements we'll reuse
var callStatus = $("#call-status");
var answerButton = $(".answer-button");
var callSupportButton = $(".call-support-button");
var hangUpButton = $(".hangup-button");
var callCustomerButtons = $(".call-customer-button");
/* Helper function to update the call status bar */
function updateCallStatus(status) {
callStatus.text(status);
}
/* Get a Twilio Client token with an AJAX request */
$(document).ready(function() {
$.post("/token/generate", {page: window.location.pathname}, function(data) {
// Set up the Twilio Client Device with the token
Twilio.Device.setup(data.token);
});
});
/* Callback to let us know Twilio Client is ready */
Twilio.Device.ready(function (device) {
updateCallStatus("Ready");
});
/* Report any errors to the call status display */
Twilio.Device.error(function (error) {
updateCallStatus("ERROR: " + error.message);
});
/* Callback for when Twilio Client initiates a new connection */
Twilio.Device.connect(function (connection) {
// Enable the hang up button and disable the call buttons
hangUpButton.prop("disabled", false);
callCustomerButtons.prop("disabled", true);
callSupportButton.prop("disabled", true);
answerButton.prop("disabled", true);
// If phoneNumber is part of the connection, this is a call from a
// support agent to a customer's phone
if ("phoneNumber" in connection.message) {
updateCallStatus("In call with " + connection.message.phoneNumber);
} else {
// This is a call from a website user to a support agent
updateCallStatus("In call with support");
}
});
/* Callback for when a call ends */
Twilio.Device.disconnect(function(connection) {
// Disable the hangup button and enable the call buttons
hangUpButton.prop("disabled", true);
callCustomerButtons.prop("disabled", false);
callSupportButton.prop("disabled", false);
updateCallStatus("Ready");
});
/* Callback for when Twilio Client receives a new incoming call */
Twilio.Device.incoming(function(connection) {
updateCallStatus("Incoming support call");
// Set a callback to be executed when the connection is accepted
connection.accept(function() {
updateCallStatus("In call with customer");
});
// Set a callback on the answer button and enable it
answerButton.click(function() {
connection.accept();
});
answerButton.prop("disabled", false);
});
/* Call a customer from a support ticket */
function callCustomer(phoneNumber) {
updateCallStatus("Calling " + phoneNumber + "...");
var params = {"phoneNumber": phoneNumber};
Twilio.Device.connect(params);
}
/* Call the support_agent from the home page */
function callSupport() {
updateCallStatus("Calling support...");
// Our backend will assume that no params means a call to support_agent
Twilio.Device.connect();
}
/* End a call */
function hangUp() {
Twilio.Device.disconnectAll();
}
That's it! Our Rails application now powers browser-to-phone and browser-to-browser calls using Twilio Client.
If you're a Ruby developer working with Twilio, you might also enjoy these tutorials:
Click To Call with Ruby and Rails
Put a button on your web page that connects visitors to live support or sales people via telephone.
Automated Survey with Ruby and Rails
Instantly collect structured data from your users with a survey conducted over a voice call or SMS text messages.
Thanks for checking this tutorial out! If you have any feedback to share with us please contact us on Twitter, we'd love to hear it.
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.
class TicketsController < ApplicationController
def create
support_ticket = Ticket.new(ticket_params)
if support_ticket.save
redirect_to root_path, notice:
'Your ticket was submitted! An agent will call you soon.'
else
redirect_to root_path, flash:
{ error: support_ticket.errors.full_messages }
end
end
def ticket_params
params.require(:ticket).permit(:name, :phone_number, :description)
end
end
<h2>Support Tickets</h2>
<p class="lead">
This is the list of most recent support tickets. Click the "Call customer" button to start a phone call from your browser.
</p>
<div class="row">
<div class="col-md-4 col-md-push-8">
<div class="panel panel-primary client-controls">
<div class="panel-heading">
<h3 class="panel-title">Make a call</h3>
</div>
<div class="panel-body">
<p><strong>Status</strong></p>
<div class="well well-sm" id="call-status">
Connecting to Twilio...
</div>
<button class="btn btn-lg btn-success answer-button" disabled>Answer call</button>
<button class="btn btn-lg btn-danger hangup-button" disabled onclick="hangUp()">Hang up</button>
</div>
</div>
</div>
<div class="col-md-8 col-md-pull-4">
<% @tickets.each do |ticket| %>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Ticket #<%= ticket.id %> <small class="pull-right"><%= ticket.created_at %></small></h3>
</div>
<div class="panel-body">
<div class="pull-right">
<button onclick="callCustomer('<%= ticket.phone_number %>')" type="button" class="btn btn-primary btn-lg call-customer-button">
<span class="glyphicon glyphicon-earphone" aria-hidden="true"></span>
Call customer
</button>
</div>
<p><strong>Name:</strong> <%= ticket.name %></p>
<p><strong>Phone number:</strong> <%= ticket.phone_number %></p>
<p><strong>Description:</strong></p>
<%= ticket.description %>
</div>
</div>
<% end %>
</div>
</div>
require 'pry'
class TwilioCapability
def self.generate(role)
# To find TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN visit
# https://www.twilio.com/user/account
account_sid = ENV['TWILIO_ACCOUNT_SID']
auth_token = ENV['TWILIO_AUTH_TOKEN']
# binding.pry
capability = Twilio::JWT::ClientCapability.new(account_sid, auth_token)
outgoing_scope = Twilio::JWT::ClientCapability::OutgoingClientScope.new(account_sid, role)
capability.add_scope outgoing_scope
incoming_scope = Twilio::JWT::ClientCapability::IncomingClientScope.new(role)
capability.add_scope incoming_scope
application_sid = ENV['TWIML_APPLICATION_SID']
capability.to_s
end
end
/**
* Twilio Client configuration for the browser-calls-rails
* example application.
*/
// Store some selectors for elements we'll reuse
var callStatus = $("#call-status");
var answerButton = $(".answer-button");
var callSupportButton = $(".call-support-button");
var hangUpButton = $(".hangup-button");
var callCustomerButtons = $(".call-customer-button");
/* Helper function to update the call status bar */
function updateCallStatus(status) {
callStatus.text(status);
}
/* Get a Twilio Client token with an AJAX request */
$(document).ready(function() {
$.post("/token/generate", {page: window.location.pathname}, function(data) {
// Set up the Twilio Client Device with the token
Twilio.Device.setup(data.token);
});
});
/* Callback to let us know Twilio Client is ready */
Twilio.Device.ready(function (device) {
updateCallStatus("Ready");
});
/* Report any errors to the call status display */
Twilio.Device.error(function (error) {
updateCallStatus("ERROR: " + error.message);
});
/* Callback for when Twilio Client initiates a new connection */
Twilio.Device.connect(function (connection) {
// Enable the hang up button and disable the call buttons
hangUpButton.prop("disabled", false);
callCustomerButtons.prop("disabled", true);
callSupportButton.prop("disabled", true);
answerButton.prop("disabled", true);
// If phoneNumber is part of the connection, this is a call from a
// support agent to a customer's phone
if ("phoneNumber" in connection.message) {
updateCallStatus("In call with " + connection.message.phoneNumber);
} else {
// This is a call from a website user to a support agent
updateCallStatus("In call with support");
}
});
/* Callback for when a call ends */
Twilio.Device.disconnect(function(connection) {
// Disable the hangup button and enable the call buttons
hangUpButton.prop("disabled", true);
callCustomerButtons.prop("disabled", false);
callSupportButton.prop("disabled", false);
updateCallStatus("Ready");
});
/* Callback for when Twilio Client receives a new incoming call */
Twilio.Device.incoming(function(connection) {
updateCallStatus("Incoming support call");
// Set a callback to be executed when the connection is accepted
connection.accept(function() {
updateCallStatus("In call with customer");
});
// Set a callback on the answer button and enable it
answerButton.click(function() {
connection.accept();
});
answerButton.prop("disabled", false);
});
/* Call a customer from a support ticket */
function callCustomer(phoneNumber) {
updateCallStatus("Calling " + phoneNumber + "...");
var params = {"phoneNumber": phoneNumber};
Twilio.Device.connect(params);
}
/* Call the support_agent from the home page */
function callSupport() {
updateCallStatus("Calling support...");
// Our backend will assume that no params means a call to support_agent
Twilio.Device.connect();
}
/* End a call */
function hangUp() {
Twilio.Device.disconnectAll();
}
/**
* Twilio Client configuration for the browser-calls-rails
* example application.
*/
// Store some selectors for elements we'll reuse
var callStatus = $("#call-status");
var answerButton = $(".answer-button");
var callSupportButton = $(".call-support-button");
var hangUpButton = $(".hangup-button");
var callCustomerButtons = $(".call-customer-button");
/* Helper function to update the call status bar */
function updateCallStatus(status) {
callStatus.text(status);
}
/* Get a Twilio Client token with an AJAX request */
$(document).ready(function() {
$.post("/token/generate", {page: window.location.pathname}, function(data) {
// Set up the Twilio Client Device with the token
Twilio.Device.setup(data.token);
});
});
/* Callback to let us know Twilio Client is ready */
Twilio.Device.ready(function (device) {
updateCallStatus("Ready");
});
/* Report any errors to the call status display */
Twilio.Device.error(function (error) {
updateCallStatus("ERROR: " + error.message);
});
/* Callback for when Twilio Client initiates a new connection */
Twilio.Device.connect(function (connection) {
// Enable the hang up button and disable the call buttons
hangUpButton.prop("disabled", false);
callCustomerButtons.prop("disabled", true);
callSupportButton.prop("disabled", true);
answerButton.prop("disabled", true);
// If phoneNumber is part of the connection, this is a call from a
// support agent to a customer's phone
if ("phoneNumber" in connection.message) {
updateCallStatus("In call with " + connection.message.phoneNumber);
} else {
// This is a call from a website user to a support agent
updateCallStatus("In call with support");
}
});
/* Callback for when a call ends */
Twilio.Device.disconnect(function(connection) {
// Disable the hangup button and enable the call buttons
hangUpButton.prop("disabled", true);
callCustomerButtons.prop("disabled", false);
callSupportButton.prop("disabled", false);
updateCallStatus("Ready");
});
/* Callback for when Twilio Client receives a new incoming call */
Twilio.Device.incoming(function(connection) {
updateCallStatus("Incoming support call");
// Set a callback to be executed when the connection is accepted
connection.accept(function() {
updateCallStatus("In call with customer");
});
// Set a callback on the answer button and enable it
answerButton.click(function() {
connection.accept();
});
answerButton.prop("disabled", false);
});
/* Call a customer from a support ticket */
function callCustomer(phoneNumber) {
updateCallStatus("Calling " + phoneNumber + "...");
var params = {"phoneNumber": phoneNumber};
Twilio.Device.connect(params);
}
/* Call the support_agent from the home page */
function callSupport() {
updateCallStatus("Calling support...");
// Our backend will assume that no params means a call to support_agent
Twilio.Device.connect();
}
/* End a call */
function hangUp() {
Twilio.Device.disconnectAll();
}
class CallController < ApplicationController
skip_before_filter :verify_authenticity_token
def connect
render xml: twilio_reponse
end
private
def twilio_reponse
twilio_number = ENV['TWILIO_PHONE_NUMBER']
res = Twilio::TwiML::VoiceResponse.new do |response|
dial = Twilio::TwiML::Dial.new caller_id: twilio_number
if params.include?(:phoneNumber)
dial.number params[:phoneNumber]
else
dial.client('support_agent')
end
response.append(dial)
end
return res.to_s
end
end
/**
* Twilio Client configuration for the browser-calls-rails
* example application.
*/
// Store some selectors for elements we'll reuse
var callStatus = $("#call-status");
var answerButton = $(".answer-button");
var callSupportButton = $(".call-support-button");
var hangUpButton = $(".hangup-button");
var callCustomerButtons = $(".call-customer-button");
/* Helper function to update the call status bar */
function updateCallStatus(status) {
callStatus.text(status);
}
/* Get a Twilio Client token with an AJAX request */
$(document).ready(function() {
$.post("/token/generate", {page: window.location.pathname}, function(data) {
// Set up the Twilio Client Device with the token
Twilio.Device.setup(data.token);
});
});
/* Callback to let us know Twilio Client is ready */
Twilio.Device.ready(function (device) {
updateCallStatus("Ready");
});
/* Report any errors to the call status display */
Twilio.Device.error(function (error) {
updateCallStatus("ERROR: " + error.message);
});
/* Callback for when Twilio Client initiates a new connection */
Twilio.Device.connect(function (connection) {
// Enable the hang up button and disable the call buttons
hangUpButton.prop("disabled", false);
callCustomerButtons.prop("disabled", true);
callSupportButton.prop("disabled", true);
answerButton.prop("disabled", true);
// If phoneNumber is part of the connection, this is a call from a
// support agent to a customer's phone
if ("phoneNumber" in connection.message) {
updateCallStatus("In call with " + connection.message.phoneNumber);
} else {
// This is a call from a website user to a support agent
updateCallStatus("In call with support");
}
});
/* Callback for when a call ends */
Twilio.Device.disconnect(function(connection) {
// Disable the hangup button and enable the call buttons
hangUpButton.prop("disabled", true);
callCustomerButtons.prop("disabled", false);
callSupportButton.prop("disabled", false);
updateCallStatus("Ready");
});
/* Callback for when Twilio Client receives a new incoming call */
Twilio.Device.incoming(function(connection) {
updateCallStatus("Incoming support call");
// Set a callback to be executed when the connection is accepted
connection.accept(function() {
updateCallStatus("In call with customer");
});
// Set a callback on the answer button and enable it
answerButton.click(function() {
connection.accept();
});
answerButton.prop("disabled", false);
});
/* Call a customer from a support ticket */
function callCustomer(phoneNumber) {
updateCallStatus("Calling " + phoneNumber + "...");
var params = {"phoneNumber": phoneNumber};
Twilio.Device.connect(params);
}
/* Call the support_agent from the home page */
function callSupport() {
updateCallStatus("Calling support...");
// Our backend will assume that no params means a call to support_agent
Twilio.Device.connect();
}
/* End a call */
function hangUp() {
Twilio.Device.disconnectAll();
}
/**
* Twilio Client configuration for the browser-calls-rails
* example application.
*/
// Store some selectors for elements we'll reuse
var callStatus = $("#call-status");
var answerButton = $(".answer-button");
var callSupportButton = $(".call-support-button");
var hangUpButton = $(".hangup-button");
var callCustomerButtons = $(".call-customer-button");
/* Helper function to update the call status bar */
function updateCallStatus(status) {
callStatus.text(status);
}
/* Get a Twilio Client token with an AJAX request */
$(document).ready(function() {
$.post("/token/generate", {page: window.location.pathname}, function(data) {
// Set up the Twilio Client Device with the token
Twilio.Device.setup(data.token);
});
});
/* Callback to let us know Twilio Client is ready */
Twilio.Device.ready(function (device) {
updateCallStatus("Ready");
});
/* Report any errors to the call status display */
Twilio.Device.error(function (error) {
updateCallStatus("ERROR: " + error.message);
});
/* Callback for when Twilio Client initiates a new connection */
Twilio.Device.connect(function (connection) {
// Enable the hang up button and disable the call buttons
hangUpButton.prop("disabled", false);
callCustomerButtons.prop("disabled", true);
callSupportButton.prop("disabled", true);
answerButton.prop("disabled", true);
// If phoneNumber is part of the connection, this is a call from a
// support agent to a customer's phone
if ("phoneNumber" in connection.message) {
updateCallStatus("In call with " + connection.message.phoneNumber);
} else {
// This is a call from a website user to a support agent
updateCallStatus("In call with support");
}
});
/* Callback for when a call ends */
Twilio.Device.disconnect(function(connection) {
// Disable the hangup button and enable the call buttons
hangUpButton.prop("disabled", true);
callCustomerButtons.prop("disabled", false);
callSupportButton.prop("disabled", false);
updateCallStatus("Ready");
});
/* Callback for when Twilio Client receives a new incoming call */
Twilio.Device.incoming(function(connection) {
updateCallStatus("Incoming support call");
// Set a callback to be executed when the connection is accepted
connection.accept(function() {
updateCallStatus("In call with customer");
});
// Set a callback on the answer button and enable it
answerButton.click(function() {
connection.accept();
});
answerButton.prop("disabled", false);
});
/* Call a customer from a support ticket */
function callCustomer(phoneNumber) {
updateCallStatus("Calling " + phoneNumber + "...");
var params = {"phoneNumber": phoneNumber};
Twilio.Device.connect(params);
}
/* Call the support_agent from the home page */
function callSupport() {
updateCallStatus("Calling support...");
// Our backend will assume that no params means a call to support_agent
Twilio.Device.connect();
}
/* End a call */
function hangUp() {
Twilio.Device.disconnectAll();
}
class CallController < ApplicationController
skip_before_filter :verify_authenticity_token
def connect
render xml: twilio_reponse
end
private
def twilio_reponse
twilio_number = ENV['TWILIO_PHONE_NUMBER']
res = Twilio::TwiML::VoiceResponse.new do |response|
dial = Twilio::TwiML::Dial.new caller_id: twilio_number
if params.include?(:phoneNumber)
dial.number params[:phoneNumber]
else
dial.client('support_agent')
end
response.append(dial)
end
return res.to_s
end
end
/**
* Twilio Client configuration for the browser-calls-rails
* example application.
*/
// Store some selectors for elements we'll reuse
var callStatus = $("#call-status");
var answerButton = $(".answer-button");
var callSupportButton = $(".call-support-button");
var hangUpButton = $(".hangup-button");
var callCustomerButtons = $(".call-customer-button");
/* Helper function to update the call status bar */
function updateCallStatus(status) {
callStatus.text(status);
}
/* Get a Twilio Client token with an AJAX request */
$(document).ready(function() {
$.post("/token/generate", {page: window.location.pathname}, function(data) {
// Set up the Twilio Client Device with the token
Twilio.Device.setup(data.token);
});
});
/* Callback to let us know Twilio Client is ready */
Twilio.Device.ready(function (device) {
updateCallStatus("Ready");
});
/* Report any errors to the call status display */
Twilio.Device.error(function (error) {
updateCallStatus("ERROR: " + error.message);
});
/* Callback for when Twilio Client initiates a new connection */
Twilio.Device.connect(function (connection) {
// Enable the hang up button and disable the call buttons
hangUpButton.prop("disabled", false);
callCustomerButtons.prop("disabled", true);
callSupportButton.prop("disabled", true);
answerButton.prop("disabled", true);
// If phoneNumber is part of the connection, this is a call from a
// support agent to a customer's phone
if ("phoneNumber" in connection.message) {
updateCallStatus("In call with " + connection.message.phoneNumber);
} else {
// This is a call from a website user to a support agent
updateCallStatus("In call with support");
}
});
/* Callback for when a call ends */
Twilio.Device.disconnect(function(connection) {
// Disable the hangup button and enable the call buttons
hangUpButton.prop("disabled", true);
callCustomerButtons.prop("disabled", false);
callSupportButton.prop("disabled", false);
updateCallStatus("Ready");
});
/* Callback for when Twilio Client receives a new incoming call */
Twilio.Device.incoming(function(connection) {
updateCallStatus("Incoming support call");
// Set a callback to be executed when the connection is accepted
connection.accept(function() {
updateCallStatus("In call with customer");
});
// Set a callback on the answer button and enable it
answerButton.click(function() {
connection.accept();
});
answerButton.prop("disabled", false);
});
/* Call a customer from a support ticket */
function callCustomer(phoneNumber) {
updateCallStatus("Calling " + phoneNumber + "...");
var params = {"phoneNumber": phoneNumber};
Twilio.Device.connect(params);
}
/* Call the support_agent from the home page */
function callSupport() {
updateCallStatus("Calling support...");
// Our backend will assume that no params means a call to support_agent
Twilio.Device.connect();
}
/* End a call */
function hangUp() {
Twilio.Device.disconnectAll();
}
/**
* Twilio Client configuration for the browser-calls-rails
* example application.
*/
// Store some selectors for elements we'll reuse
var callStatus = $("#call-status");
var answerButton = $(".answer-button");
var callSupportButton = $(".call-support-button");
var hangUpButton = $(".hangup-button");
var callCustomerButtons = $(".call-customer-button");
/* Helper function to update the call status bar */
function updateCallStatus(status) {
callStatus.text(status);
}
/* Get a Twilio Client token with an AJAX request */
$(document).ready(function() {
$.post("/token/generate", {page: window.location.pathname}, function(data) {
// Set up the Twilio Client Device with the token
Twilio.Device.setup(data.token);
});
});
/* Callback to let us know Twilio Client is ready */
Twilio.Device.ready(function (device) {
updateCallStatus("Ready");
});
/* Report any errors to the call status display */
Twilio.Device.error(function (error) {
updateCallStatus("ERROR: " + error.message);
});
/* Callback for when Twilio Client initiates a new connection */
Twilio.Device.connect(function (connection) {
// Enable the hang up button and disable the call buttons
hangUpButton.prop("disabled", false);
callCustomerButtons.prop("disabled", true);
callSupportButton.prop("disabled", true);
answerButton.prop("disabled", true);
// If phoneNumber is part of the connection, this is a call from a
// support agent to a customer's phone
if ("phoneNumber" in connection.message) {
updateCallStatus("In call with " + connection.message.phoneNumber);
} else {
// This is a call from a website user to a support agent
updateCallStatus("In call with support");
}
});
/* Callback for when a call ends */
Twilio.Device.disconnect(function(connection) {
// Disable the hangup button and enable the call buttons
hangUpButton.prop("disabled", true);
callCustomerButtons.prop("disabled", false);
callSupportButton.prop("disabled", false);
updateCallStatus("Ready");
});
/* Callback for when Twilio Client receives a new incoming call */
Twilio.Device.incoming(function(connection) {
updateCallStatus("Incoming support call");
// Set a callback to be executed when the connection is accepted
connection.accept(function() {
updateCallStatus("In call with customer");
});
// Set a callback on the answer button and enable it
answerButton.click(function() {
connection.accept();
});
answerButton.prop("disabled", false);
});
/* Call a customer from a support ticket */
function callCustomer(phoneNumber) {
updateCallStatus("Calling " + phoneNumber + "...");
var params = {"phoneNumber": phoneNumber};
Twilio.Device.connect(params);
}
/* Call the support_agent from the home page */
function callSupport() {
updateCallStatus("Calling support...");
// Our backend will assume that no params means a call to support_agent
Twilio.Device.connect();
}
/* End a call */
function hangUp() {
Twilio.Device.disconnectAll();
}