Reputation: 1895
I have a form_tag using Ajax to save a new user card with Stripe. It renders a Stripe card form, passes the input to a controller method, and then is supposed to serve a js file. It works without the Stripe stuff, but with it, I'm getting authentication issues.
Here's the basic code:
<%= form_tag(save_card_path, id:'payment-form', remote: true) do %>
<div id="card-element">
<!-- A Stripe Element will be inserted here. -->
</div>
<button id="submit-card" class="submit-btn">Save Card</button>
<% end %>
<script type="text/javascript">
var stripe = Stripe('<%= @stripe_public %>');
var elements = stripe.elements();
// Custom styling can be passed to options when creating an Element.
var style = {
base: {
// Add your base input styles here. For example:
fontSize: '20px',
color: "#32325d",
}
};
// Create an instance of the card Element.
var card = elements.create('card', {style: style});
// Add an instance of the card Element into the `card-element` <div>.
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 = '';
}
});
// Create a token or display an error when the form is submitted.
var form = document.getElementById('payment-form');
form.addEventListener('submit', function(event) {
event.preventDefault();
stripe.createToken(card).then(function(result) {
if (result.error) {
// Inform the customer that there was an error.
var errorElement = document.getElementById('card-errors');
errorElement.textContent = result.error.message;
} else {
// Send the token to your server.
stripeTokenHandler(result.token);
}
});
});
function stripeTokenHandler(token) {
// Insert the token ID into the form so it gets submitted to the server
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);
// Submit the form
form.submit();
}
</script>
def save_card
respond_to do |format|
format.js
end
end
$("html").hide();
So like I said, without the Stripe code everything works fine, and an authenticity_token
is present in the params, but the code as I wrote it above gives the following error:
def handle_unverified_request
raise ActionController::InvalidAuthenticityToken
end
with only the params:
{"utf8"=>"✓", "stripeToken"=>"<token>"}
When I add the option authenticity_token: true
to the form, the params once again contain an authenticity_token
, but now when it gets to the format.js
line, I get the error
ActionController::UnknownFormat
I've run into a similar problem before while trying to upload files in forms via Ajax, but I discovered the remotipart gem, and that solved it. But it doesn't appear to help in this case.
Does anyone know why including a Stripe field would get rid of my authenticity_token, and why even with an authenticity_token, the js format isn't recognized?
Jquery-ujs is included via the line //= require jquery_ujs
in my application.js
, the line <%= csrf_meta_tags %>
is included in application.html.erb
, and my source code includes the lines
<meta name="csrf-param" content="authenticity_token" />
<meta name="csrf-token" content="<TOKEN>" />
This is the case whether my ajax call works or not.
This is my full log when I add the option authenticity_token: true
:
Started POST "/save_card" for 127.0.0.1 at 2020-02-20 00:07:04 +0100
Processing by UsersController#save_card as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"[LONG TOKEN]", "post"=>"47", "transaction"=>"bid", "stripeToken"=>"[STRIPE TOKEN]"}
User Load (0.9ms)
SELECT "users".* FROM "users" WHERE "users"."id" = $1 ORDER BY "users"."id" ASC LIMIT $2 [["id", 2], ["LIMIT", 1]]
(0.3ms) BEGIN SQL
(2.3ms) INSERT INTO "cards" ("stripe_customer_id", "brand", "last4", "exp_month", "exp_year", "user_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING "id" [["stripe_customer_id", "[STRIPE TOKEN]"], ["brand", "Visa"], ["last4", "4242"], ["exp_month", "4"], ["exp_year", "2024"], ["user_id", 2], ["created_at", "2020-02-19 23:07:05.246789"], ["updated_at", "2020-02-19 23:07:05.246789"]]
(2.8ms) COMMIT
Completed 406 Not Acceptable in 672ms (ActiveRecord: 6.3ms)
ActionController::UnknownFormat (ActionController::UnknownFormat): app/controllers/users_controller.rb:485:in `save_card' Started GET "/serviceworker.js" for 127.0.0.1 at 2020-02-20 00:07:05 +0100 Started GET "/serviceworker.js" for ::1 at 2020-02-20 00:07:21 +0100
and this is my log when I remove authenticity_token: true
:
Started POST "/save_card" for 127.0.0.1 at 2020-02-20 00:33:17 +0100
Processing by UsersController#save_card as HTML
Parameters: {"utf8"=>"✓", "post"=>"47", "transaction"=>"bid", "stripeToken"=>"[STRIPE TOKEN]"} Can't verify CSRF token authenticity. Completed 422 Unprocessable Entity in 1ms (ActiveRecord: 0.0ms)
ActionController::InvalidAuthenticityToken (ActionController::InvalidAuthenticityToken): actionpack (5.1.7) lib/action_controller/metal/request_forgery_protection.rb:195:in
handle_unverified_request' actionpack (5.1.7) lib/action_controller/metal/request_forgery_protection.rb:227:in
handle_unverified_request' devise (4.7.0) lib/devise/controllers/helpers.rb:255:inhandle_unverified_request' actionpack (5.1.7) lib/action_controller/metal/request_forgery_protection.rb:222:in
verify_authenticity_token' activesupport (5.1.7) lib/active_support/callbacks.rb:413:inblock in make_lambda' activesupport (5.1.7) lib/active_support/callbacks.rb:197:in
block (2 levels) in halting' actionpack (5.1.7) lib/abstract_controller/callbacks.rb:12:inblock (2 levels) in <module:Callbacks>' activesupport (5.1.7) lib/active_support/callbacks.rb:198:in
block in halting' activesupport (5.1.7) lib/active_support/callbacks.rb:507:inblock in invoke_before' activesupport (5.1.7) lib/active_support/callbacks.rb:507:in
each' activesupport (5.1.7) lib/active_support/callbacks.rb:507:ininvoke_before' activesupport (5.1.7) lib/active_support/callbacks.rb:130:in
run_callbacks' actionpack (5.1.7) lib/abstract_controller/callbacks.rb:19:inprocess_action' actionpack (5.1.7) lib/action_controller/metal/rescue.rb:20:in
process_action' actionpack (5.1.7) lib/action_controller/metal/instrumentation.rb:32:inblock in process_action' activesupport (5.1.7) lib/active_support/notifications.rb:166:in
block in instrument' activesupport (5.1.7) lib/active_support/notifications/instrumenter.rb:21:ininstrument' activesupport (5.1.7) lib/active_support/notifications.rb:166:in
instrument' actionpack (5.1.7) lib/action_controller/metal/instrumentation.rb:30:inprocess_action' actionpack (5.1.7) lib/action_controller/metal/params_wrapper.rb:252:in
process_action' activerecord (5.1.7) lib/active_record/railties/controller_runtime.rb:22:inprocess_action' actionpack (5.1.7) lib/abstract_controller/base.rb:124:in
process' actionview (5.1.7) lib/action_view/rendering.rb:30:inprocess' actionpack (5.1.7) lib/action_controller/metal.rb:189:in
dispatch' actionpack (5.1.7) lib/action_controller/metal.rb:253:indispatch' actionpack (5.1.7) lib/action_dispatch/routing/route_set.rb:49:in
dispatch' actionpack (5.1.7) lib/action_dispatch/routing/route_set.rb:31:inserve' actionpack (5.1.7) lib/action_dispatch/journey/router.rb:50:in
block in serve' actionpack (5.1.7) lib/action_dispatch/journey/router.rb:33:ineach' actionpack (5.1.7) lib/action_dispatch/journey/router.rb:33:in
serve' actionpack (5.1.7) lib/action_dispatch/routing/route_set.rb:844:incall' serviceworker-rails (0.6.0) lib/serviceworker/middleware.rb:35:in
call' remotipart (1.4.3) lib/remotipart/middleware.rb:32:incall' warden (1.2.8) lib/warden/manager.rb:36:in
block in call' warden (1.2.8) lib/warden/manager.rb:34:incatch' warden (1.2.8) lib/warden/manager.rb:34:in
call' rack (2.0.7) lib/rack/etag.rb:25:incall' rack (2.0.7) lib/rack/conditional_get.rb:38:in
call' rack (2.0.7) lib/rack/head.rb:12:incall' rack (2.0.7) lib/rack/session/abstract/id.rb:232:in
context' rack (2.0.7) lib/rack/session/abstract/id.rb:226:incall' actionpack (5.1.7) lib/action_dispatch/middleware/cookies.rb:613:in
call' activerecord (5.1.7) lib/active_record/migration.rb:556:incall' actionpack (5.1.7) lib/action_dispatch/middleware/callbacks.rb:26:in
block in call' activesupport (5.1.7) lib/active_support/callbacks.rb:97:inrun_callbacks' actionpack (5.1.7) lib/action_dispatch/middleware/callbacks.rb:24:in
call' actionpack (5.1.7) lib/action_dispatch/middleware/executor.rb:12:incall' actionpack (5.1.7) lib/action_dispatch/middleware/debug_exceptions.rb:59:in
call' web-console (3.7.0) lib/web_console/middleware.rb:135:incall_app' web-console (3.7.0) lib/web_console/middleware.rb:30:in
block in call' web-console (3.7.0) lib/web_console/middleware.rb:20:incatch' web-console (3.7.0) lib/web_console/middleware.rb:20:in
call' actionpack (5.1.7) lib/action_dispatch/middleware/show_exceptions.rb:31:incall' railties (5.1.7) lib/rails/rack/logger.rb:36:in
call_app' railties (5.1.7) lib/rails/rack/logger.rb:24:inblock in call' activesupport (5.1.7) lib/active_support/tagged_logging.rb:69:in
block in tagged' activesupport (5.1.7) lib/active_support/tagged_logging.rb:26:intagged' activesupport (5.1.7) lib/active_support/tagged_logging.rb:69:in
tagged' railties (5.1.7) lib/rails/rack/logger.rb:24:incall' sprockets-rails (3.2.1) lib/sprockets/rails/quiet_assets.rb:13:in
call' actionpack (5.1.7) lib/action_dispatch/middleware/remote_ip.rb:79:incall' actionpack (5.1.7) lib/action_dispatch/middleware/request_id.rb:25:in
call' rack (2.0.7) lib/rack/method_override.rb:22:incall' rack (2.0.7) lib/rack/runtime.rb:22:in
call' activesupport (5.1.7) lib/active_support/cache/strategy/local_cache_middleware.rb:27:incall' actionpack (5.1.7) lib/action_dispatch/middleware/executor.rb:12:in
call' actionpack (5.1.7) lib/action_dispatch/middleware/static.rb:125:incall' rack (2.0.7) lib/rack/sendfile.rb:111:in
call' railties (5.1.7) lib/rails/engine.rb:522:incall' puma (3.12.1) lib/puma/configuration.rb:227:in
call' puma (3.12.1) lib/puma/server.rb:660:inhandle_request' puma (3.12.1) lib/puma/server.rb:474:in
process_client' puma (3.12.1) lib/puma/server.rb:334:inblock in run' puma (3.12.1) lib/puma/thread_pool.rb:135:in
block in spawn_thread' Started GET "/serviceworker.js" for 127.0.0.1 at 2020-02-20 00:33:18 +0100
Upvotes: 0
Views: 499
Reputation: 6321
Keep everything as it was, and just update .submit()
The actual issue is sending as HTML
instead of as JS
on form.submit()
.
https://github.com/rails/rails/issues/29546
//from
form.submit();
//to
form.dispatchEvent(new Event('submit', {bubbles: true}));
Previously, You were submitting, then asking to submit it again. OnClick button, get stripe data then submit with my answer. Sample below.
Also, you need to add disable/enable
for submit button on click, to ignore multiple clicks.
...
// Create a token or display an error when the form is submitted.
let submitCardBtn = document.getElementById('submit-card');
submitCardBtn.addEventListener('click', function(event) {
event.preventDefault();
// disable button
submitCardBtn.disabled = true;
stripe.createToken(card).then(function(result) {
if (result.error) {
// Inform the customer that there was an error.
var errorElement = document.getElementById('card-errors');
errorElement.textContent = result.error.message;
// enable button on false
submitCardBtn.disabled = false;
} else {
// Send the token to your server.
stripeTokenHandler(result.token);
}
});
});
function stripeTokenHandler(token) {
// Insert the token ID into the form so it gets submitted to the server
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);
// Submit the form
form.dispatchEvent(new Event('submit', {bubbles: true}));
// enable button
submitCardBtn.disabled = false;
}
Upvotes: 1
Reputation: 610
Assuming you are on Rails 5.X and you have //= require rails-ujs
in your application.js
file
Instead of form.submit()
, using Rails.fire(form, 'submit')
will allow you the submit the form via Ajax
Upvotes: 1
Reputation: 446
Could you try the following,
Add the option authenticity_token: true
to the form, then the params once again contain an authenticity_token. And rename your save_card.js
to save_card.js.erb
.
Upvotes: 1