custom Bitcoin Stripe Rails payments - ruby-on-rails

I received an email from a customer saying that they never received their product after paying on Bitcoin/Stripe. the trouble is, I couldn't find a way to thoroughly test Bitcoin in a sandbox, so are not sure if my implementation is working as it should be. The sandbox automatically fills the receiver so I get the notification of payment, and all is ok. however when running live, im not sure if my polling is working correctly. I posted all my relevant code below, can anyone see any flaws in the code?
*my Store is running Ruby(2.3.1p112) on Rails (4.2.6)
my payment_bitcoin.html.erb
<%= render :partial => "offsite_checkout_summary" %>
<p>Your license key, along with your purchase receipt, will be sent to <strong><%= #order.licensee_name %></strong> at <strong><%= #order.email %></strong> once payment has been confirmed.</p>
<div id="extra_purchase_notes">
<em><strong>Note:</strong> The bitcoin link and price provided is only valid for <span id="countdown_time">10:00</span> minutes.</em>
</div>
<div class="d cl"></div>
<%= render :partial => "items_checkout_summary", :locals => { order: #order } %>
<div id="bitcoin_payment_address">
<script src="https://js.stripe.com/v2/stripe.js"></script>
<script type="text/javascript">
Stripe.setPublishableKey('<%= $STRIPE_PREFS['stripe_publishable_key'] %>');
function populateBitcoinCheckout(status, response) {
if (status === 200) {
document.getElementById("bitcoin_total").innerHTML = " (" + response.bitcoin_amount/100000000 + " BTC)";
document.getElementById("bitcoin_payment_string").innerHTML = 'Please send: <strong>' + response.bitcoin_amount/100000000 + ' BTC</strong> to <strong>' + '' + response.inbound_address + '</strong>';
document.getElementById("btc-button").href = response.bitcoin_uri + '&label=Software+Purchase';
//poll reciever
Stripe.bitcoinReceiver.pollReceiver(response.id, filledReceiverHandler(response));
//configure timer
function startTimer(duration, countdown) {
var timer = duration,minutes,seconds;
var t = setInterval(function () {
minutes = parseInt(timer / 60, 10)
seconds = parseInt(timer % 60, 10);
minutes = minutes < 10 ? "0" + minutes : minutes;
seconds = seconds < 10 ? "0" + seconds : seconds;
countdown.textContent = minutes + ":" + seconds;
if (--timer < 0) {
clearInterval(t);
document.getElementById("bitcoin_total").innerHTML = "";
document.getElementById("bitcoin_payment_string").innerHTML = "";
document.getElementById("bitcoin_button_text").innerHTML = "Refresh Order"
document.getElementById("btc-button").href = "javascript:history.back()"
document.getElementById("extra_purchase_notes").innerHTML = "<em><strong>Oops...</strong> This order has expired, use the Refresh Order button to retry.</em>"
Stripe.bitcoinReceiver.cancelReceiverPoll(response.id);
}
}, 1000);
}
//start timer
var countdown = document.getElementById('countdown_time');
startTimer(600, countdown);
} else {
document.getElementById("bitcoin_uri_string").innerHTML = JSON.stringify(response);
Stripe.bitcoinReceiver.cancelReceiverPoll(response.id);
}
}
Stripe.bitcoinReceiver.createReceiver({
amount: "<%= (#order.total * 100).round %>",
currency: 'usd',
description: 'Software purchase',
email: "<%= #order.email %>"
}, populateBitcoinCheckout);
function filledReceiverHandler(response)
{
if (response.filled === true) {
function post(path, parameters) {
var form = $('<form></form>');
form.attr("method", "post");
form.attr("action", path);
$.each(parameters, function(key, value) {
if ( typeof value == 'object' || typeof value == 'array' ){
$.each(value, function(subkey, subvalue) {
var field = $('<input />');
field.attr("type", "hidden");
field.attr("name", key+'[]');
field.attr("value", subvalue);
form.append(field);
});
} else {
var field = $('<input />');
field.attr("type", "hidden");
field.attr("name", key);
field.attr("value", value);
form.append(field);
}
});
$(document.body).append(form);
form.submit();
}
post('purchase_bitcoin', response);
}
}
</script>
</div>
</div>
<p id="bitcoin_payment_string"></p>
<div class="d"></div>
<p style="text-align: right;">
<a id="btc-button"><button class="bitcoin-button" style="visibility: visible;"><span id="bitcoin_button_text">Pay with Bitcoin</span></button></a>
</p>
and the relevant controller method:
#bitcoin purchase
def purchase_bitcoin
require 'stripe'
if check_if_order_exists() == false
if session[:failure_reason] != nil
render :action => 'failed'
return
end
redirect_to :action => 'index'
return
end
if session[:item_number] == nil
flash[:notice] = 'Nothing to purchase'
redirect_to :action => 'index'
return
end
#create the order
generateNewOrder("Bitcoin")
#total in cents
the_total = #order.total.to_f
the_total_cents = (the_total*100).to_i
Stripe.api_key = $STRIPE_PREFS['stripe_secret']
#add order details
#order.transaction_number = params[:id]
# Create the charge on Stripe's servers - this will charge the user's card
session[:coin_total] = (params[:bitcoin_amount].to_f/100000000).to_s
charge = Stripe::Charge.create(
:amount => params[:amount],
:currency => params[:currency],
:source => params[:id],
:description => 'Software purchase'
)
#re-add completed order details
the_id = charge["id"]
#order.transaction_number = the_id
#order.comment = 'Total = ' + (params[:bitcoin_amount].to_f/100000000).to_s + 'BTC; payment address = ' + params[:inbound_address]
#order.status = 'C'
#order.finish_and_save()
session[:order_id] = #order.id
#send license
Thread.new do
OrderMailer.thankyou(#order).deliver
end
#logger.info session.inspect
render :action => 'thankyou_bitcoin'
end

I ended up adding the polling logic into the timer, and this seems to work.... although it was implied in the docs that i didn't need to manually poll? at least thats how i read it...
here is my complete amended payment_bitcoin.html.erb for anyone else experiencing issues getting this working
<%= render :partial => "offsite_checkout_summary" %>
<p>Your license key, along with your purchase receipt, will be sent to <strong><%= #order.licensee_name %></strong> at <strong><%= #order.email %></strong> once payment has been confirmed.</p>
<div id="extra_purchase_notes">
<em><strong>Note:</strong> The bitcoin link and price provided is only valid for <span id="countdown_time">10:00</span> minutes.</em>
</div>
<div class="d cl"></div>
<%= render :partial => "items_checkout_summary", :locals => { order: #order } %>
<div id="bitcoin_payment_address">
<script src="https://js.stripe.com/v2/stripe.js"></script>
<script type="text/javascript">
Stripe.setPublishableKey('<%= $STRIPE_PREFS['stripe_publishable_key'] %>');
function populateBitcoinCheckout(status, response) {
if (status === 200) {
document.getElementById("bitcoin_total").innerHTML = " (" + response.bitcoin_amount/100000000 + " BTC)";
document.getElementById("bitcoin_payment_string").innerHTML = 'Please send: <strong>' + response.bitcoin_amount/100000000 + ' BTC</strong> to <strong>' + '' + response.inbound_address + '</strong>';
document.getElementById("btc-button").href = response.bitcoin_uri + '&label=Software+Purchase';
//configure timer
function startTimer(duration, countdown) {
var timer = duration,minutes,seconds;
var t = setInterval(function () {
minutes = parseInt(timer / 60, 10)
seconds = parseInt(timer % 60, 10);
minutes = minutes < 10 ? "0" + minutes : minutes;
seconds = seconds < 10 ? "0" + seconds : seconds;
//poll reciever
Stripe.bitcoinReceiver.pollReceiver(response.id, filledReceiverHandler(response));
countdown.textContent = minutes + ":" + seconds;
if (--timer < 0) {
clearInterval(t);
document.getElementById("bitcoin_total").innerHTML = "";
document.getElementById("bitcoin_payment_string").innerHTML = "";
document.getElementById("bitcoin_button_text").innerHTML = "Refresh Order"
document.getElementById("btc-button").href = "javascript:history.back()"
document.getElementById("extra_purchase_notes").innerHTML = "<em><strong>Oops...</strong> This order has expired, use the Refresh Order button to retry.</em>"
Stripe.bitcoinReceiver.cancelReceiverPoll(response.id);
}
}, 1000);
}
//start timer
var countdown = document.getElementById('countdown_time');
startTimer(600, countdown);
} else {
document.getElementById("bitcoin_uri_string").innerHTML = JSON.stringify(response);
}
}
Stripe.bitcoinReceiver.createReceiver({
amount: "<%= (#order.total * 100).round %>",
currency: 'usd',
description: 'Software purchase',
email: "<%= #order.email %>"
}, populateBitcoinCheckout);
function filledReceiverHandler(response)
{
if (response.filled === true) {
function post(path, parameters) {
var form = $('<form></form>');
form.attr("method", "post");
form.attr("action", path);
$.each(parameters, function(key, value) {
if ( typeof value == 'object' || typeof value == 'array' ){
$.each(value, function(subkey, subvalue) {
var field = $('<input />');
field.attr("type", "hidden");
field.attr("name", key+'[]');
field.attr("value", subvalue);
form.append(field);
});
} else {
var field = $('<input />');
field.attr("type", "hidden");
field.attr("name", key);
field.attr("value", value);
form.append(field);
}
});
$(document.body).append(form);
form.submit();
}
post('purchase_bitcoin', response);
}
}
</script>
</div>
</div>
<p id="bitcoin_payment_string"></p>
<div class="d"></div>
<p style="text-align: right;">
<a id="btc-button"><button class="bitcoin-button" style="visibility: visible;"><span id="bitcoin_button_text">Pay with Bitcoin</span></button></a>
</p>

I made more revisions, and actually removed all polling logic from my .erb, electing to use a web hook instead.
https://stripe.com/docs/webhooks
require 'json'
require 'stripe'
class Store::BitcoinController < ApplicationController
def payment_recieved
type = params[:type]
data = params[:data]
if (type.blank? || type != "bitcoin.receiver.filled" || data.blank?)
logger.warn("Got request to Bitcoin IPN with invalid receiver email from #{request.remote_addr || request.remote_ip}")
render :nothing => true, :status => 200
return
end
..process order here...
hopefully this will help some others with the same issues.
:)

Related

Undefined method `user_search_admin_path' for #<ActionView::Base:0x0000000010f748>

Getting this error when clicking on my Admin page:
"undefined method `user_search_admin_path' for #ActionView::Base:0x0000000010f748" on line 47 url: '<%= user_search_admin_path(:format=>:json) %>'.
I have a route for the "user_search" so not sure what is causing this error. Any ideas how to solve this error?
This is the admin route:
resources :admin, :as => :admin, :only => [:index, :create, :destroy] do
collection {
get "user_search";
get "group_search";
post "toggle_logging";
post "toggle_privs";
get "export_permissions";
get "export_roles";
};
Below are my code files:
index.html.erb
<% content_for :crumbs do %>
<li class="last"><%= link_to("Administrators", admin_index_path) %></li>
<% end %>
<% content_for :javascripts do %>
<script type="text/javascript">
function toggleGroup(group, that){
var el = "." + group;
$(el).parent().toggle(0,"swing",function(){
});
}
function highlightGroup(group, role){
var even = $('tr td.' + group).parent('.even').children("."+role).css("background-color");
var odd = $('tr td.' + group).parent('.odd').children("."+role).css("background-color");
// $('tr td.' + group).parent('.even').children("."+role).animate({backgroundColor:"red", opacity:0.5},800, function(){
// $('tr td.' + group).parent('.even').children("."+role).css({"background-color": even, opacity:1.0});
// });
// $('tr td.' + group).parent('.odd').children("."+role).animate({backgroundColor:"red", opacity:0.5},800, function(){
// $('tr td.' + group).parent('.odd').children("."+role).css({"background-color": odd, opacity:1.0});
// });
$('tr td.' + group).parent('.odd').children('.indented_description').animate({backgroundColor:"red", opacity:0.5},800, function(){
$('tr td.' + group).parent('.odd').children('.indented_description').css({"background-color": odd, opacity:1.0});
});
$('tr td.' + group).parent('.even').children('.indented_description').animate({backgroundColor:"red", opacity:0.5},800, function(){
$('tr td.' + group).parent('.even').children('.indented_description').css({"background-color": even, opacity:1.0});
});
}
$(document).ready(autocomplete_users);
function autocomplete_users() {
$(".add_usernames").autocomplete({
minLength: 3,
source: function(request, response) {
var copy = this.element;
$.ajax({
beforeSend: function(){
$(copy).parent().siblings(".spinner_td").css("display", "block");
},
complete: function() {
$(copy).parent().siblings(".spinner_td").css("display", "none");
},
url: '<%= user_search_admin_path(:format=>:json) %>',
data: {
q: request.term,
},
dataType: "json",
success: function(data) {
response(data);
},
})
},
parse: function(data) {
var parsed = [];
for ( var i = 0; i < data.length; ++i ) {
var row = data[i];
parsed[parsed.length] = {
data: row,
value: row,
result: row
};
}
return parsed;
}
});
}
$(document).ready(autocomplete_groups);
function autocomplete_groups() {
$(".add_groups").autocomplete({
minLength: 3,
source: function(request, response) {
var copy = this.element;
$.ajax({
beforeSend: function(){
$(copy).parent().siblings(".spinner_td").css("display", "block");
},
complete: function() {
$(copy).parent().siblings(".spinner_td").css("display", "none");
},
url: '<%= group_search_admin_path(:format=>:json) %>',
data: {
q: request.term,
},
dataType: "json",
success: function(data) {
response(data);
},
})
},
parse: function(data) {
var parsed = [];
for ( var i = 0; i < data.length; ++i ) {
var row = data[i];
parsed[parsed.length] = {
data: row,
value: row,
result: row
};
}
return parsed;
}
});
}
function add_new_group() {
$.ajax({
url:'auth_role/auth_group_add',
success: function(result) {
//$(result).insertBefore('#add_new_group_button')
$('.group_table').append(result);
autocomplete_groups();
}
});
};
function add_user_to_role() {
$.ajax({
url:'auth_role/auth_user_add',
success: function(result) {
//$(result).insertBefore('#add_new_user_button')
$('.user_table').append(result);
autocomplete_users();
}
});
};
function render_auth_role_partial () {
$.ajax({
url: 'auth_role/' + $("#roles_select").val()+ '/auth_role_partial',
success: function(result) {
$("#edit_role_div").replaceWith(result);
$("#roles_select").value = "<%= #role.id %>"
}
});
};
function show_new_role_form() {
$("#selector_div").hide();
$.ajax({
url: 'auth_role/new',
success: function(result) {
$("#edit_role_div").replaceWith(result);
}
});
};
// so users cannot lock themselves out of managing users
$(document).ready(function() {
$('#perms_checkbox_form').submit(function () {
if ($("input[id^='perm_role_task:manage_user_group']:checked").length == 0) {
alert("There must be at least one role with permission to manage users.");
return false;
}
});
});
function check_duplicate_role() {
var new_name = $("input[id='name']").val();
$.ajax({
url: 'auth_role/check_duplicate_role',
dataType: "json",
contentType: "application/json; charset=utf-8",
data: {new_name: new_name},
success: function(data) {
if (data == true) {
validate_role_form();
} else {
alert("Role already exists or is empty. Please choose another name.");
}
}
});
}
function validate_role_form() {
// Check that the role name is alphanumeric
var role_name = $('#edit_role_div input[name="name"]').val();
if (/[^\w\s]+/.test(role_name)) {
alert("Role name can only contain letters, numbers, _, or whitespace");
$('#edit_role_div input[name="name"]').css("background-color", "#FFB2B2");
return;
}
if ($.trim(role_name).length < 1) {
alert("Role name cannot be blank");
$('#edit_role_div input[name="name"]').css("background-color", "#FFB2B2");
return;
}
var groups = ($("input[id='auth_groups_name']"));
var users = ($("input[id='users_']"));
var group_names = [];
var user_names= [];
var submit_flag = 1;
$.each(groups, (function(index, elem) {
group_names.push($(elem).val());
}))
$.each(users, (function(index, elem){
user_names.push($(elem).val());
}))
if (group_names.length > 0 || user_names.length > 0) {
$.ajax({
url: 'auth_role/check_groups_and_users',
dataType: "json",
contentType: "application/json; charset=utf-8",
data: {group_names: group_names, user_names: user_names},
success:function(data) {
$("input[id='auth_groups_name']").css("background-color", "white");
$("input[id='users_']").css("background-color", "white");
if (data.invalid_groups.length > 0) {
submit_flag = 0;
// mark invalid groups
alert("At least one group is not a valid group.");
$.each(data.invalid_groups, function(index, elem) {
var ind = group_names.indexOf(elem);
$("input[id='auth_groups_name']").eq(ind).css("background-color", "#FFB2B2");
//$("input[id='auth_groups_name'][value='nen_project']").parent().parent().append('<td>Not a valid group.</td>');
});
}
if (data.invalid_users.length >0 ) {
submit_flag = 0;
alert("At least one user is not a valid user.");
$.each(data.invalid_users, function(index, elem) {
var ind = user_names.indexOf(elem);
$("input[id='users_']").eq(ind).css("background-color", "#FFB2B2");
});
}
if (submit_flag == 1) {
$("#update_role_form").submit();
}
}
});
} else {
$("#update_role_form").submit();
}
}
function export_perms() {
window.location.href = "<%= export_permissions_admin_path(:format => 'tsv') %>";
};
function export_roles() {
window.location.href = "<%= export_roles_admin_path(:format => 'tsv') %>";
};
</script>
<% end %>
<% content_for :title do %>
Administrator Panel
<% end %>
<h1><%= yield :title %></h1>
<h2>Build Logging</h2>
<%= form_tag("/admin/toggle_logging", :method =>"post", :id => "toggle_logging") do %>
The build detailed logging is currently <%= "#{#system_settings.detailed_logging ? 'Enabled': 'Disabled'}" %>
<br/><br/>
<%= submit_tag "#{#system_settings.detailed_logging ? 'Disable' : 'Enable'} Logging" ,:class=>"submit_auth_button"%>
<% end %>
<hr>
<% row_class = "even"
groups = []
def replaceSpaces(str)
if str.to_s.strip.length == 0
return str
else
copy = str
copy = copy.gsub " ", "_"
copy = copy.gsub "-", "_"
copy = copy.gsub ":", "_"
copy = copy.gsub "__", "_"
copy = copy.downcase
return copy
end
end
def cleanDescription(str)
if str.to_s.strip.length == 0
return str
else
copy = str
copy = copy.gsub "Edit CR Field: ", ""
copy = copy.gsub "Task: ", ""
copy = copy.gsub "Element: ", ""
return copy
end
end
def cleanDependsOn(str)
if str.to_s.strip.length == 0
return str
else
copy = replaceSpaces(str)
copy = copy.gsub "|", " "
return copy
end
end
%>
<h2>Permissions For Roles </h2>
<%= link_to_function raw("#{image_tag('export.png')} Export Permissions as TSV"), "export_perms()" %>
</br>
</br>
<%= form_tag("/auth_permission/update", :method =>"put", :id => "perms_checkbox_form") do %>
<table>
<tr>
<th class='permissionname'> Permission </th>
<% #roles.each do |role| %>
<th class='rolename'><%=role.name %></th>
<% end %>
</tr>
<% #permissions.each do |perm| %>
<%= fields_for "perm_role[]", perm do |perm_fields| %>
<% if !groups.include? perm.group %>
<% groups << perm.group %>
<tr class='group_header'>
<td onclick='toggleGroup("<%="#{replaceSpaces(perm.group)}"%>", this)'><a href='javascript:void(0);' id='toggler'><%=perm.group%><span id='info_helper'> (click to toggle)</span></a></td>
<% #roles.each do |perm_role| %>
<td class='permission_chkbox all_<%=replaceSpaces(perm.group) %> all_<%=replaceSpaces(perm_role.name) %>'> <%= check_box_tag("group_#{replaceSpaces(perm.group)}_#{replaceSpaces(perm_role.name)}", "unchecked", false ) %>
<% end %>
</tr>
<% end %>
<tr class='<%= row_class %>'>
<td class='indented_description'><%="#{cleanDescription(perm.description)}" %>
<% if perm.depends_desc %>
<br/>
<span class='depends_description <%="#{cleanDependsOn(perm.depends_on)}" %>'>
<%="#{perm.depends_desc}" %>
</span>
<% end %>
</td>
<% #roles.each do |perm_role| %>
<td class='permission_chkbox <%=replaceSpaces(perm.group) %> <%=replaceSpaces(perm_role.name)%>'> <%= check_box_tag("perm_role[#{perm.name},#{perm_role.name}]", "", perm.authroles.include?(perm_role) ) %></td>
<% end %>
<%
if row_class == 'even'
row_class = 'odd'
else
row_class = 'even'
end
%>
<% end %>
</tr>
<%end%>
</table>
<br/>
<%= submit_tag "Update Permissions" ,:class=>"submit_auth_button"%>
<% end %>
<script>
<% usednames = [] %>
<% jsgroups = [] %>
<% #permissions.each do |perm| %>
<% #roles.each do |perm_role| %>
var elementId = '#<%="group_#{replaceSpaces(perm.group)}_#{replaceSpaces(perm_role.name)}"%>';
<% if !jsgroups.include? "#{perm_role.name} #{perm.group}" %>
<% jsgroups << "#{perm_role.name} #{perm.group}" %>
$(elementId).click(function(){
// Check all the children
if(this.checked){
$(elementId).prop("checked", true);
//console.log($('.<%="#{replaceSpaces(perm.group)}.#{replaceSpaces(perm_role.name)}"%> input'))
$('.<%="#{replaceSpaces(perm.group)}.#{replaceSpaces(perm_role.name)}"%> input').each(function() {
$(this).prop("checked", true)
});
}
else{
$(elementId).prop("checked", false);
$('.<%="#{replaceSpaces(perm.group)}.#{replaceSpaces(perm_role.name)}"%> input').each(function() {
$(this).prop("checked", false)
});
}
});
<% end %>
// Need to clear out this variable otherwise it'll get confused
elementId = ""
// Check if everything under that group/role is already checked
// If it is then check the Group Header
// If it's not don't do anything
var is_all_checked = true;
$('.<%="#{replaceSpaces(perm.group)}.#{replaceSpaces(perm_role.name)}"%> input').each(function(){
if(!this.checked || typeof(this.checked) == "undefined") is_all_checked = false
});
if(is_all_checked){
$('#<%="group_#{replaceSpaces(perm.group)}_#{replaceSpaces(perm_role.name)}"%>').prop("checked", true);
}
// If any of the children permission roles get change, let's figure out if we need to check/uncheck
// the parent
$('.<%="#{replaceSpaces(perm.group)}.#{replaceSpaces(perm_role.name)}"%> input').change(function(){
// Check if everythin in that gorup is already filled out
var is_all_checked = true;
$('.<%="#{replaceSpaces(perm.group)}.#{replaceSpaces(perm_role.name)}"%> input').each(function(){
if(!this.checked || typeof(this.checked) == "undefined") is_all_checked = false
});
if(is_all_checked){
$('#<%="group_#{replaceSpaces(perm.group)}_#{replaceSpaces(perm_role.name)}"%>').prop("checked", true);
}
if(this.checked){
}
else{
// uncheck the parent if any of its children are unchecked
$('#<%="group_#{replaceSpaces(perm.group)}_#{replaceSpaces(perm_role.name)}"%>').prop("checked", false);
//console.log("uncheck parent");
}
});
<% t = ".#{replaceSpaces(perm.group)}.#{replaceSpaces(perm_role.name)} input" %>
<% if !usednames.include? t %>
<% usednames << t %>
$('.<%="#{replaceSpaces(perm.group)}.#{replaceSpaces(perm_role.name)}"%> input').change(function(){
var rolename = '<%="#{replaceSpaces(perm_role.name)}"%>';
var that = this;
var old_color = $(that).parent().parent().children().first().children('.depends_description').css('background-color')
$(that).parent().parent().children().first().children('.depends_description').animate({backgroundColor:'red', opacity:0.75}, 500);
$(that).parent().parent().children().first().children('.depends_description').animate({backgroundColor:old_color, opacity:1}, 100);
var classes = $(that).parent().parent().children().first().children('.depends_description').attr("class");
if(classes && classes != "") {
classes = classes.split(" ")
for(var k in classes){
if(classes[k] != "depends_description"){
// console.log("toggle", classes[k])
highlightGroup(classes[k], rolename);
}
}
}
});
<% end %>
<% end %>
<% end %>
</script>
<hr/>
<h2> Roles </h2>
<%= link_to_function raw("#{image_tag('export.png')} Export Roles as TSV"), "export_roles()" %>
</br>
</br>
<% if #role %>
<div id="selector_div">
<table>
<tr>
<td> Select Role: </td>
<td>
<%= select_tag "roles_select", options_for_select(#roles.collect {|role_option| [role_option.name, role_option.id]}, :selected => #role.id), :onchange => "render_auth_role_partial()", :class=>"role_selection" %>
</td>
<td>
<%= button_to_function "Create New Role", "show_new_role_form()", :class=>"small_auth_button" %>
</td>
<td>
</td>
</tr>
</table>
</div>
<% end %>
<hr>
<%= render :partial => 'auth_role_form', :locals => {:role => #role, :roles => #roles}%>
admin_controller.rb
class AdminController < ApplicationController
before_action :check_permission
def index
puts "stepped in index"
#system_settings = AuthSettings.first
#permissions = AuthPermission.sort(:group.asc, :name.asc)
#roles = AuthRole.all(:order => :name)
#role = #roles.first
#users = User.administrators
respond_to do |format|
format.html # index.html.erb
format.xml { render :xml => #users, :status => :ok }
format.json { render :json => #users, :status => :ok }
end
end
def create
usernames = params[:admin][:usernames].split(',')
#users = []
usernames.each do |username|
username.strip!
next if username.empty?
begin
#users << User.find_or_create_admin(username)
rescue
logger.error("Couldn't save user with name: #{username}")
end
end
respond_to do |format|
format.html { redirect_to(:action=>:index) }
format.xml { render :xml => #users, :status=>:created }
format.json { render :json => #users, :status=>:created }
format.js { render :layout=>false }
end
end
def destroy
#user = User.find_by_name(params[:id])
if #user
#user.admin = false
if #user.save
respond_to do |format|
format.html
format.xml { render :xml => #user }
format.js { render :layout => false }
end
end
else
redirect404
end
end
def user_search
username = params.delete(:q)
if username
#users = User.search_for_usernames(username)
respond_to do |format|
format.json { render :json => #users }
end
else
# TODO: this is erroneous because this particular request is always going to be a JSON request via the view
# If the request was made from the outside, and it was an HTML request, it's not going to render anything...
# regardless of whether or not the q parameter was supplied
flash[:error] = "You need to supply a 'q' parameter in order to search"
respond_to do |format|
format.html { render :action=>:index }
end
end
end
def group_search
groupname = params.delete(:q)
if groupname
#groupnames = User.search_for_groups(groupname)
respond_to do |format|
format.json { render :json => #groupnames }
end
else
# TODO: this is erroneous because this particular request is always going to be a JSON request via the view
# If the request was made from the outside, and it was an HTML request, it's not going to render anything...
# regardless of whether or not the q parameter was supplied
flash[:error] = "You need to supply a 'q' parameter in order to search"
respond_to do |format|
format.html { render :action=>:index }
end
end
end
def toggle_logging
Rails.logger.info params
setting = AuthSettings.first
setting.detailed_logging = !setting.detailed_logging
Rails.logger.info "Logging is #{setting.detailed_logging}"
if setting.save
flash[:notice] = "Detailed logging #{setting.detailed_logging ? 'enabled' : 'disabled'}."
else
flash[:error] = "Could not change logging"
end
redirect_to admin_index_path
end
def toggle_privs
current_user.admin_enabled = !current_user.admin_enabled
if current_user.save
flash[:notice] = "Administrative privileges #{current_user.admin_enabled ? 'enabled' : 'disabled'}."
else
flash[:error] = "Could not change your privilege level."
end
redirect_to admin_index_path
end
def export_permissions
respond_to do |format|
format.tsv do
tsv = []
column_headers = [:description, :depends_desc, :authroles]
# Filter records.
records = AuthPermission.sort(:group.asc, :name.asc)
# Render TSV lines.
records.each do |record|
element_line = []
column_headers.each do |column|
begin
if column == :authroles
value_arr = []
value = ""
record.authroles.each do |role|
value_arr << role.name
end unless !record.authroles
value << value_arr.join(",")
else
value = eval("record.#{column}")
end
element_line << value
rescue
element_line << nil
end
end
tsv << element_line.join("\t")
end
# Add column headers to the top of the matrix.
tsv.unshift([column_headers].flatten.join("\t"))
render :text => tsv.join("\r\n")
end
end
end
def export_roles
respond_to do |format|
format.tsv do
tsv = []
column_headers = [:name, :description, :groups, :users]
# Filter records.
records = AuthRole.all(:order => :name)
# Render TSV lines.
records.each do |record|
element_line = []
column_headers.each do |column|
begin
if column == :groups
value_arr = []
value = ""
record.groups.each do |group|
value_arr << group.name
end unless !record.groups
value << value_arr.join(",")
elsif column == :users
value = record.users.join(",")
else
value = eval("record.#{column}")
end
element_line << value
rescue
element_line << nil
end
end
tsv << element_line.join("\t")
end
# Add column headers to the top of the matrix.
tsv.unshift([column_headers].flatten.join("\t"))
render :text => tsv.join("\r\n")
end
end
end
private
def check_permission
ensure_task_permission('task:manage_user_group')
end
end
As per your routes definition, if the URI needed for a GET request is is /admin/user_search then you should be using user_search_admin_index_path - notice the _index suffix.

Ruby on Rails 6 Turbolinks Stripe form values not persisting on back

I am trying to build a form that allows a user to make a booking for a martial arts class they wish to attend. I have created a form that dynamically changes based on the selections the user makes, when I change any of the select options the form updates and when I submit the form it redirects to a Stripe checkout. The problem I have is after submitting the the form and I click the browsers back button or the back button provided on the Stripe checkout page the select options that have been updated have reverted back to the default options rather than the updated options. Can anyone help me correct this behaviour and get the correct form elements to persist?
Here is the code I am using to do this:
The view I am rendering the form in:
<% content_for :banner_title, #page_data['bannerTitle'] %>
<% content_for :head do %>
<meta name="turbolinks-cache-control" content="no-cache">
<% end %>
<div class="content container py-5">
<div class="row">
<div class="col-12 col-md-7 mx-auto">
<%= render "forms/booking", options: #options %>
</div>
</div>
</div>
The form I am using:
<%= form_for #booking, class: 'booking clearfix' do |f| %>
<div class="form-group">
<%= f.label(:class_name, "Select a class you wish to attend: ") %>
<%= f.select(:class_name, options_for_select(options[:class_names], #booking.class_name), {}, class: 'form-control' ) %>
</div>
<div class="form-group">
<%= f.label(:date, "Select a date:") %>
<%= f.select(:date, options_for_select( options[:dates], #booking.date ), {}, class: 'form-control' ) %>
</div>
<div class="form-group">
<%= f.label(:time, "Select a time: ")%>
<%= f.select(:time, options_for_select(options[:times], #booking.time), {}, class: 'form-control') %>
</div>
<div class="form-group">
<%= f.label(:attendees, "How many attending: ") %>
<%= f.select(:attendees, options_for_select(options[:attendees], #booking.attendees), {}, class: 'form-control' )%>
</div>
<%= f.submit 'Place Booking', class: 'btn btn-primary btn-lg text-light float-right', id: 'create-booking' %>
<% end %>
<%= javascript_pack_tag 'booking_form' %>
<script src="https://js.stripe.com/v3/"></script>
The model for the form (I'm not using ActiveRecord, i dont know if this makes any difference?):
class Booking
include ActiveModel::Model
MAX_ATTENDEES = 10
attr_accessor :time, :class_data, :attendees, :date, :class_name
def initialize(args={})
#time = args['time']
#class_name = args['class_name']
#class_data = args['class_data']
#date = args['date']
#attendees = args['attendees']
end
def day
#date.split(',').first
end
def available_dates
days_index_array = class_data['times'].keys.map {|k| day_index(k) }
days_within( days_index_array )
end
def available_times
if !date
class_data['times'][class_data['times'].keys.first.downcase]
else
class_data['times'][day.downcase]
end
end
def total_cost
#class_data['cost'].to_i * #attendees.to_i
end
def attending_string
ActionController::Base.helpers.pluralize(attendees, 'person')
end
private
def days_within(days, timeframe=1.month)
start_date = Date.tomorrow
end_date = start_date + timeframe
(start_date..end_date).to_a.select {|k| days.include?(k.wday) }
end
def day_index(day)
DateTime::DAYNAMES.index(day.to_s.capitalize)
end
end
And the controller I am calling the new action in:
class BookingsController < ApplicationController
include BookingsHelper
before_action :set_class_data
skip_before_action :set_page_data, except: :new
def new
set_booking
# store values to be passed to the form helper method options_for_select. Each value must be an array populated with arrays with the format [value, text]
#options = {
class_names: #class_data.map {|c| [ c['name'], c['name'] ]},
dates: #booking.available_dates.map {|d| [d.strftime('%A, %d %B'), d.strftime('%A, %d %B')] },
times: #booking.available_times.map {|t| [t,t]},
attendees: Booking::MAX_ATTENDEES.times.map {|i| [i+1, i+1]}
}
end
def create
end
def booking_form_data
booking_form_data = set_booking_form_data(params)
update_session_booking(booking_form_data)
render json: booking_form_data
end
private
def set_booking
if session[:current_booking]
pp "session exists"
#booking = Booking.new(session[:current_booking])
else
pp "session does not exist"
#booking = Booking.new
session[:current_booking] = #booking.instance_values
end
set_booking_class_data
end
def set_booking_class_data
!#booking.class_name ? #booking.class_data = #class_data.first.except('information') : #booking.class_data = #class_data.find {|cd| cd['name'] == #booking.class_name}.except('information')
end
def booking_params
params.permit(:class_name, :date, :time, :attendees, :update_type)
end
def update_session_booking(booking_form_data)
if params[:update_type] == 'class_name'
session[:current_booking]['class_name'] = params[:class_name]
session[:current_booking]['date'] = booking_form_data[:date_options].first
session[:current_booking]['time'] = booking_form_data[:time_options].first
elsif params[:update_type] == 'date'
session[:current_booking]['date'] = params[:date]
session[:current_booking]['time'] = booking_form_data[:time_options].first
elsif params[:update_type] == 'time'
session[:current_booking]['time'] = params['time']
elsif params[:update_type] == 'attendees'
session[:current_booking]['attendees'] = params[:attendees]
elsif params[:update_type] == 'load'
session[:current_booking] = booking_params.except(:update_type)
end
pp "Session Booking: #{session[:current_booking]}"
end
def set_booking_form_data(params)
booking_form_data = {}
selected_class = #class_data.find {|cd| cd['name'] == params[:class_name] }
# when the class_name select is changed
if params[:update_type] == 'class_name'
booking_form_data[:date_options] = days_within( selected_class['times'].keys.map {|k| day_index(k) } ).map {|d| d.strftime('%A, %d %B') }
booking_form_data[:time_options] = selected_class['times'][booking_form_data[:date_options].first.split(',')[0].downcase]
# when date select is changed
elsif params[:update_type] == 'date'
booking_form_data[:time_options] = selected_class['times'][params[:date].split(',')[0].downcase]
end
booking_form_data
end
end
And the javascript I am using to update the form:
getBookingFormData = (bodyData={}, successCallback=()=>{}) => {
$.ajax({
url: '/booking_form_data',
method: 'POST',
beforeSend: function(xhr) {xhr.setRequestHeader('X-CSRF-Token', $('meta[name="csrf-token"]').attr('content'))},
data: bodyData,
success: successCallback
})
}
createOptions = (values) => {
let newOptions = [];
$.each(values, (index, value) => {
let newOption = $('<option></option>');
newOption.attr('value', value);
newOption.text(value);
newOptions.push(newOption)
})
return newOptions
}
appendOptions = (options, element) => {
$(element).empty();
$(element).append(options)
}
currentFormValues = () => {
return {
class_name: $('#booking_class_name').val(),
date: $('#booking_date').val(),
time: $('#booking_time').val(),
attendees: $('#booking_attendees').val()
}
}
$('select#booking_class_name').on('change', () => {
let bodyData = {
class_name: $('select#booking_class_name').val(),
update_type: 'class_name'
}
let successCallback = (res) => {
let dateOptions = createOptions(res.date_options);
let dateSelect = $('select#booking_date');
let timeOptions = createOptions(res.time_options);
let timeSelect = $('select#booking_time');
appendOptions(dateOptions, dateSelect);
appendOptions(timeOptions, timeSelect);
}
getBookingFormData(bodyData, successCallback)
});
$('select#booking_date').on('change', () => {
let bodyData = {
class_name: $('select#booking_class_name').val(),
date: $('select#booking_date').val(),
update_type: 'date'
};
let successCallback = (res) => {
let timeOptions = createOptions(res.time_options);
let timeSelect = $('select#booking_time');
appendOptions(timeOptions, timeSelect);
}
getBookingFormData(bodyData, successCallback)
});
$('select#booking_time').on('change', () => {
let bodyData = {
time: $('select#booking_time').val(),
update_type: 'time'
};
getBookingFormData(bodyData);
});
$('select#booking_attendees').on('change', () => {
let bodyData = {
attendees: $('select#booking_attendees').val(),
update_type: 'attendees'
};
getBookingFormData(bodyData);
});
$('#create-booking').on('click',(e) => {
e.preventDefault();
bookingDefault = false
const stripe = Stripe(process.env.STRIPE_PUBLIC);
let requestHeaders = new Headers({
'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content'),
'Content-Type': 'application/json'
})
fetch('/create_checkout_session', {
method: 'POST',
headers: requestHeaders,
body: JSON.stringify(currentFormValues())
})
.then((res) => { return res.json() })
.then((session) => { return stripe.redirectToCheckout({ sessionId: session.id }) })
.then((result) => {
if (result.error) { alert(result.error.message) }
})
.catch((error) => { console.error('Error: ', error) })
})
From what Ive read i think it may be a problem related to caching which makes me think this is an issue with turbolinks but I could be completely wrong. Ive tried adding meta tags that disable turbolinks or force it to reload the page but they did not seem to work.
Any input at all would be really appreciated as Ive been stuck on this for days. Let me know if you need any more information
This isn't so much Stripe-related as it is related you your form value management. If you want to keep these values around, you'll need to build that into your front application, somehow. There are lots of options for this:
Using local storage
Using query parameters, if not sensitive info
Using a cookie and a server session you can re-retrieve and hydrate the f.select options with a default value.

Stripe SCA one-time payments Rails

I'm integrating Stripe SCA with payment intents into my rails 5.2.3 (ruby 2.5.1) app. I successfully had one-time payments working properly, however after I integrated subscriptions (successfully working), the one-time payments are receiving an incomplete status on Stripe "The customer has not entered their payment method". Looking at the JSON I can see my payment intent with ID successfully being created, however my charges, data is showing up as null. I can't figure out why the data is not being passed to stripe. Here are the corresponding files:
purchases.show.html.erb
<div class="container">
<h1>Purchasing <%= #recipe.title %> for <%= number_to_currency(#recipe.price) %></h1>
<%= form_with url: recipe_purchase_path(#recipe.id), local: true, id: "payment-form", data: { payment_intent_id: #payment_intent.client_secret } do |form| %>
<div class="form-group">
<label for="card-element">
Credit or debit card
</label>
<div id="card-element" class="form-control">
</div>
<div id="card-errors" role="alert">
</div>
</div>
<div class="form-group">
<label>Name on Card</label>
<%= form.text_field :name_on_card, placeholder: "Full name", class: "form-control" %>
</div>
<div class="form-group">
<%= form.hidden_field :payment_intent_id, value: #payment_intent.id %>
<button class="btn btn-outline-primary buy-recipe">Submit Payment</button>
</div>
<% end %>
</div>
purchases_controller.rb
class PurchasesController < ApplicationController
before_action :authenticate_user!
before_action :set_recipe, only:[:show, :create]
def receipt
#purchase = Purchase.find_by_uuid(params[:id])
#recipe = Recipe.find(#purchase.recipe_id)
end
def show
#payment_intent = Stripe::PaymentIntent.create(
amount: #recipe.price_in_cents,
currency: 'usd',
payment_method_types: params['card'],
metadata: {integration_check: 'accept_a_payment'},
)
end
def create
#payment_intent = Stripe::PaymentIntent.retrieve(params[:payment_intent_id])
if #payment_intent.status == "succeeded"
charge = #payment_intent.charges.data.first
card = charge.payment_method_details.card
purchase = Purchase.create(
customer_id: charge.id,
user_id: current_user.id,
recipe_id: #recipe.id,
uuid: SecureRandom.uuid,
amount: #recipe.price
)
current_user.favorites << #recipe
redirect_to recipe_path(#recipe.slug), notice: "#{#recipe.title} has been added to your Cookbook, thanks for purchasing!"
else
flash[:alert] = "Your order was unsuccessful. Please try again."
redirect_to recipe_purchase_path(#recipe.id)
end
end
private
def set_recipe
#recipe = Recipe.find(params[:recipe_id])
end
end
purchases.index.js
document.addEventListener("turbolinks:load", () => {
const form = document.querySelector("#payment-form")
if (form == null) { return }
const public_key = document.querySelector("meta[name='stripe-key']").getAttribute("content")
const stripe = Stripe(public_key)
const elements = stripe.elements()
const card = elements.create('card')
card.mount('#card-element')
card.addEventListener("change", (event) => {
var displayError = document.getElementById('card-errors')
if (event.error) {
displayError.textContent = event.error.message
} else {
displayError.textContent = ''
}
})
form.addEventListener("submit", (event) => {
event.preventDefault()
let data = {
payment_method: {
card: card,
billing_details: {
name: form.querySelector("#name_on_card").value
}
}
}
stripe.confirmCardPayment(form.dataset.paymentIntentId, data).then((result) => {
if (result.error) {
var errorElement = document.getElementById('card-errors')
errorElement.textContent = result.error.message
} else {
//
//
form.submit()
}
})
})
})
and a screenshot of JSON
here is my subscriptions.js file
document.addEventListener("turbolinks:load", () => {
let cardElement = document.querySelector("#card-element")
if (cardElement !== null) { setupStripe() }
})
function setupStripe() {
const stripe_key = document.querySelector("meta[name='stripe-key']").getAttribute("content")
const stripe = Stripe(stripe_key)
const elements = stripe.elements()
const card = elements.create('card')
card.mount('#card-element')
var displayError = document.getElementById('card-errors')
card.addEventListener('change', (event) => {
if (event.error) {
displayError.textContent = event.error.message
} else {
displayError.textContent = ''
}
})
const form = document.querySelector("#payment-form")
let paymentIntentId = form.dataset.paymentIntent
let setupIntentId = form.dataset.setupIntent
if (paymentIntentId) {
if (form.dataset.status == "requires_action") {
stripe.confirmCardPayment(paymentIntentId, { setup_future_usage: 'off_session' }).then((result) => {
if (result.error) {
displayError.textContent = result.error.message
form.querySelector("#card-details").classList.remove("d-none")
} else {
form.submit()
}
})
}
}
form.addEventListener('submit', (event) => {
event.preventDefault()
let name = form.querySelector("#name_on_card").value
let data = {
payment_method_data: {
card: card,
billing_details: {
name: name,
}
}
}
// Complete a payment intent
if (paymentIntentId) {
stripe.confirmCardPayment(paymentIntentId, {
payment_method: data.payment_method_data,
setup_future_usage: 'off_session',
save_payment_method: true,
}).then((result) => {
if (result.error) {
displayError.textContent = result.error.message
form.querySelector("#card-details").classList.remove("d-none")
} else {
form.submit()
}
})
// Updating a card or subscribing with a trial (using a SetupIntent)
} else if (setupIntentId) {
stripe.confirmCardSetup(setupIntentId, {
payment_method: data.payment_method_data
}).then((result) => {
if (result.error) {
displayError.textContent = result.error.message
} else {
addHiddenField(form, "payment_method_id", result.setupIntent.payment_method)
form.submit()
}
})
} else {
//subscribing w no trial
data.payment_method_data.type = 'card'
stripe.createPaymentMethod(data.payment_method_data).then((result) => {
if (result.error) {
displayError.textContent = result.error.message
} else {
addHiddenField(form, "payment_method_id", result.paymentMethod.id)
form.submit()
}
})
}
})
}
function addHiddenField(form, name, value) {
let input = document.createElement("input")
input.setAttribute("type", "hidden")
input.setAttribute("name", name)
input.setAttribute("value", value)
form.appendChild(input)
}
So special thanks to Chris Oliver on this...but what needed to be done was on the show.html.erb I had to change the payment_intent_id to :payment_intent in the form data.
<%= form_with url: recipe_purchase_path(#recipe.id), local: true, id: "payment-form", data: { payment_intent: #payment_intent.client_secret } do |form| %>
then in my show action in purchases_controller.rb I needed to add customer
def show
#payment_intent = Stripe::PaymentIntent.create(
amount: #recipe.price_in_cents,
currency: 'usd',
payment_method_types: ['card'],
customer: current_user.stripe_id || current_user.stripe_customer.id
)
end
Then I completely removed my purchases.js since the one-time payments are being handled in the subscription.js

Stripe from sandbox to live, question about source?

I have stripe connect working in testing mode but am curious about "source"..
The following code pre-authorizes payments in my controller:
token = params[:stripeToken]
payment_form = params[:payment_form]
customer = Stripe::Customer.create({
:source => 'tok_visa',
:email => params[:stripeEmail],
})
charge = Stripe::Charge.create({
amount: #amount,
currency: 'usd',
customer: customer.id,
capture: false,
})
I want to go into live mode, but I need to update the source to something other than 'tok_visa'...
What is supposed to go there?
So far, I collect the customer token and stripe token, i tried using both in that spot and neither work.
When i use
:source => #order.order_token,
in either the custom create or charge create, it tells me:
Stripe::InvalidRequestError (No such token: KFU8PmscYRDsXs1234560jG4sn8J-S8TMi123456BUBkWr9aOvw):
This is while using test number :4242424242424242
It works with the test token, but not number? Is this normal?
UPDATE:
When i use the following:
token = params[:stripeToken]
payment_form = params[:payment_form]
customer = Stripe::Customer.create({
# :source => 'tok_visa',
:source => token,
:email => order_params[:email],
})
charge = Stripe::Charge.create({
amount: #amount,
currency: 'usd',
customer: customer.id,
capture: false,
})
No charge is made and no order is saved (as it should if card is bad, etc.)
JSON in stripe
{
"error": {
"code": "missing",
"doc_url": "https://stripe.com/docs/error-codes/missing",
"message": "Cannot charge a customer that has no active card",
"param": "card",
"type": "card_error"
}
}
JS for Elements:
var stripe = Stripe('pk_test_xxxxxxxxxxxxxxxxxxxxx');
var elements = stripe.elements();
var style = {
base: {
color: '#32325d',
lineHeight: '24px',
fontFamily: '"Helvetica Neue", Helvetica, sans-serif',
fontSmoothing: 'antialiased',
fontSize: '16px',
'::placeholder': {
color: '#aab7c4'
}
},
invalid: {
color: '#fa755a',
iconColor: '#fa755a'
}
};
var card = elements.create('card', {style: style});
card.mount('#card-element');
card.addEventListener('change', function(event) {
var displayError = document.getElementById('card-errors');
if (event.error) {
displayError.textContent = event.error.message;
} else {
displayError.textContent = '';
}
});
var form = document.getElementById('payment_form');
form.addEventListener('submit', function(event) {
event.preventDefault();
stripe.createToken(card).then(function(result) {
if (result.error) {
var errorElement = document.getElementById('card-errors');
errorElement.textContent = result.error.message;
} else {
stripeTokenHandler(result.token);
}
});
});
function stripeTokenHandler(token) {
var form = document.getElementById('payment_form');
var hiddenInput = document.createElement('input');
hiddenInput.setAttribute('type', 'hidden');
hiddenInput.setAttribute('name', 'stripeToken');
hiddenInput.setAttribute('value', token.id);
form.appendChild(hiddenInput);
form.submit();
}
FOrm:
<%= form_for(#order, url: listing_orders_path([#listing, #listing_video]), remote: true ) do |form| %>
<div class="form-group">
<%= form.label :name, "Their Name" %>
<%= form.text_field :name, class: "form-control", required: true, placeholder: "Steph" %>
</div>
...
<script src="https://js.stripe.com/v3/"></script>
<div class="form-row">
<label for="card-element">
Credit or debit card
</label>
<div id="card-element" class="form-control">
</div>
<div id="card-errors" role="alert"></div>
</div>
<br>
<div class="form-group">
<%= form.submit id:"button-element" %>
</div>
<span class="token"></span>
<% end %>
With Stripe's frontend utilities such as checkout or charge, you can create either a token, which is an authorization for a single charge not attached to a customer, or a source, which is a tokenized form of a credit card that can be attached to a customer.
When you create a customer, you create them with a default source attached that all future charges go to automatically. On your frontend you'll need to change createToken to createSource - everything after that is roughly the same except the variable you are referring to in the response is source instead of token.
stripe.createSource(card).then(function(result) {
if (result.error) {
var errorElement = document.getElementById('card-errors');
errorElement.textContent = result.error.message;
} else {
stripeTokenHandler(result.source);
}
});
Solved it by adding the html post to the form.
it wasn't creating tokens due to this. Without the token, it can't do anyything

Why isn't this "ajax:success" event firing?

I have a view with several "Invite" buttons like this:
<div class = "fbinvite_form" id = "<%= friend[:identifier] %>" name = "fb">
<div class = "btn btn-small">
Invite
</div>
</div>
When one of these buttons are clicked an AJAX function is called (invite_friend) :
$('.fbinvite_form').click(function() {
invite_friend();
});
Here's invite_friend (some values are hard-coded as I debug):
function invite_friend() {
$.post('/groups/7/invitations',
{
"recipient_email": "facebook#meetcody.com",
"commit" : "fb",
"fb" : "fb"
},
function(response) {
});
}
Here's the relevant line that is returned from the controller:
render :json => {
:url => signup_with_token_url(#invitation.token),
:id => #invitation.id
},
:status => :created
I can confirm that this json is being rendered correctly. At this point I'm expecting an ajax:success event to fire. I have the following code at the top of my page:
$('.fbinvite_form').bind("ajax:success", function(evt, data, status, xhr) {
...
});
But it's not firing. Any clue what might be going wrong or how to better troubleshoot (I'm a bit of a noob)?
Additional Context
I wanted to add a little bit more as it might help. I had originally built this to work with a form and it worked fine. For some performance reasons I decided to switch to buttons with AJAX. Here's the original form:
<%= form_for([#group, #invitation], :remote => true, :html => { :'data-type' => 'html', :class => 'fbinvite_form', :id => friend[:identifier]}) do |f| %>
<%= f.hidden_field :recipient_email, :value => "facebook#meetcody.com" %>
<div class = "fbinvite btn_list_right" id = "<%= friend[:identifier] %>">
<%= f.submit "Invite", :class => "btn btn-medium btn-primary", :name => "fb" %>
</div>
<% end %>
This has since been replace with all the code you see above the controller snippet.
UPDATE 1
Per Vince's suggestion I have moved the "ajax:success" code into the success function. Here is the original "ajax:success" function:
$('.fbinvite_form').bind("ajax:success", function(evt, data, status, xhr){
var fb_id = $(this).attr('id');
var response = eval("(" + xhr.responseText + ")");
var link_url = response.url;
var id = response.id;
var inv_url = <%= raw('"' + group_url(#group) + '/invitations/"') %> + id;
var picture_url = "https://www.example.com.com/assets/cody_130by130.png";
var desc = <%= raw('"' + first_name(current_user) + " is working with Cody on fitness. Join " + genderizer(current_user, "his") + " group to start tracking your workouts. Cody and the other group members will keep you motivated!" + '"') %>;
send_invite(fb_id, link_url, picture_url, desc, inv_url); return false;
});
And here is what I've done to move the code into the success function. The issue is that I don't seem to have access to "xhr"
$.ajax({
type: "POST",
url: "/groups/7/invitations",
data: {recipient_email: "facebook#meetcody.com", commit : "fb", fb : "fb" },
dataType: "json",
success: function(evt, data, status, xhr) {
var fb_id = $(this).attr('id');
var response = eval("(" + xhr.responseText + ")");
var link_url = response.url;
var id = response.id;
var inv_url = <%= raw('"' + group_url(#group) + '/invitations/"') %> + id;
var picture_url = "https://www.meetcody.com/assets/cody_130by130.png";
var desc = <%= raw('"' + first_name(current_user) + " is working with Cody on fitness. Join " + genderizer(current_user, "his") + " group to start tracking your workouts. Cody and the other group members will keep you motivated!" + '"') %>;
send_invite(fb_id, link_url, picture_url, desc, inv_url); return false;
}
});
Add error handler like this and log the error, this should help diagnose the issue.
error: function(xhr, status, error) {
console.log(error);
}
EDIT
Sorry you need to use .ajax instead of .post.
$.ajax({
type: "POST",
url: "/groups/7/invitations",
data: "name=John&location=Boston",
success: function(msg){
alert( "Data Saved: " + msg );
},
error(xhr, status, error) {
console.log(error);
}
});

Resources