I migrated most of Payola to Rails 5.2, and I was able to create subscriptions for users without issue up until this point. I'm puzzled at why my form is unable to generate stripeTokens and store them with the form. I receive the error stripeToken required for new customer with paid subscription. How can I fix my registration form to properly capture the stripeToken before committing the active job? I'm using the 2017 API.
UPDATE: referencing subscription_form_onestep.js. Stripe no longer allows you to use Stripe.card.createToken. I'm currently trying to see if using Stripe.createToken will work without issue.
UPDATE 2: the form is still not passing the stripeToken when the submission process begins.
UPDATE 3: It works after changing Stripe.card.createToken to Stripe.createToken and adding <%= yield(:head) %> to application.html.
registration/new.html.erb
{"guid":"74l524","status":"pending","error":null}
Server Log Error
[ActiveJob] [Payola::Worker::ActiveJob] [2597f80c-eb4c-4d51-a009-07e35668b07c] Plan Load (5.0ms) SELECT "plans".* FROM "plans" WHERE "plans"."id" = $1 LIMIT $2 [["id", 4], ["LIMIT", 1]]
[ActiveJob] [Payola::Worker::ActiveJob] [2597f80c-eb4c-4d51-a009-07e35668b07c] Payola::Subscription Exists (1.0ms) SELECT 1 AS one FROM "payola_subscriptions" WHERE "payola_subscriptions"."guid" = $1 AND "payola_subscriptions"."id" != $2 LIMIT $3 [["guid", "gjfsnd"], ["id", 49],
["LIMIT", 1]]
[ActiveJob] [Payola::Worker::ActiveJob] [2597f80c-eb4c-4d51-a009-07e35668b07c] Payola::Subscription Update (1.0ms) UPDATE "payola_subscriptions" SET "error" = $1, "updated_at" = $2 WHERE "payola_subscriptions"."id" = $3 [["error", "stripeToken required for new customer with paid subscription"], ["updated_at", "2019-02-24 15:10:20.714048"],
["id", 49]]
[ActiveJob] [Payola::Worker::ActiveJob] [2597f80c-eb4c-4d51-a009-07e35668b07c] PaperTrail::Version Create (1.0ms) INSERT INTO "versions" ("item_type", "item_id", "event", "object", "created_at")
subscription_form_onestep.js
var PayolaOnestepSubscriptionForm = {
initialize: function () {
$(document).off('submit.payola-onestep-subscription-form').on(
'submit.payola-onestep-subscription-form', '.payola-onestep-subscription-form',
function () {
return PayolaOnestepSubscriptionForm.handleSubmit($(this));
}
);
},
handleSubmit: function (form) {
if (!PayolaOnestepSubscriptionForm.validateForm(form)) {
return false;
}
$(form).find("input[type=submit]").prop("disabled", true);
$('.payola-spinner').css('visibility', 'visible');
Stripe.card.createToken(form, function (status, response) {
PayolaOnestepSubscriptionForm.stripeResponseHandler(form, status, response);
});
return false;
},
validateForm: function (form) {
var cardNumber = $("input[data-stripe='number']").val();
if (!Stripe.card.validateCardNumber(cardNumber)) {
PayolaOnestepSubscriptionForm.showError(form, 'The card number is not a valid credit card number.');
return false;
}
if ($("[data-stripe='exp']").length) {
var valid = !Stripe.card.validateExpiry($("[data-stripe='exp']").val());
} else {
var expMonth = $("[data-stripe='exp_month']").val();
var expYear = $("[data-stripe='exp_year']").val();
var valid = !Stripe.card.validateExpiry(expMonth, expYear);
}
if (valid) {
PayolaOnestepSubscriptionForm.showError(form, "Your card's expiration month/year is invalid.");
return false
}
var cvc = $("input[data-stripe='cvc']").val();
if (!Stripe.card.validateCVC(cvc)) {
PayolaOnestepSubscriptionForm.showError(form, "Your card's security code is invalid.");
return false;
}
return true;
},
stripeResponseHandler: function (form, status, response) {
if (response.error) {
PayolaOnestepSubscriptionForm.showError(form, response.error.message);
} else {
var email = form.find("[data-payola='email']").val();
var coupon = form.find("[data-payola='coupon']").val();
var quantity = form.find("[data-payola='quantity']").val();
var base_path = form.data('payola-base-path');
var plan_type = form.data('payola-plan-type');
var plan_id = form.data('payola-plan-id');
var action = $(form).attr('action');
form.append($('<input type="hidden" name="plan_type">').val(plan_type));
form.append($('<input type="hidden" name="plan_id">').val(plan_id));
console.log(response.id + "stripeToken Value DEBUG");
form.append($('<input type="hidden" name="stripeToken">').val(response.id));
form.append($('<input type="hidden" name="stripeEmail">').val(email));
form.append($('<input type="hidden" name="coupon">').val(coupon));
form.append($('<input type="hidden" name="quantity">').val(quantity));
form.append(PayolaOnestepSubscriptionForm.authenticityTokenInput());
$.ajax({
type: "POST",
url: action,
data: form.serialize(),
success: function (data) {
PayolaOnestepSubscriptionForm.poll(form, 60, data.guid, base_path);
},
error: function (data) {
PayolaOnestepSubscriptionForm.showError(form, jQuery.parseJSON(data.responseText).error);
}
});
}
},
poll: function (form, num_retries_left, guid, base_path) {
if (num_retries_left === 0) {
PayolaOnestepSubscriptionForm.showError(form, "This seems to be taking too long. Please contact support and give them transaction ID: " + guid);
}
var handler = function (data) {
if (data.status === "active") {
window.location = base_path + '/confirm_subscription/' + guid;
} else {
setTimeout(function () {
PayolaOnestepSubscriptionForm.poll(form, num_retries_left - 1, guid, base_path);
}, 500);
}
};
var errorHandler = function (jqXHR) {
PayolaOnestepSubscriptionForm.showError(form, jQuery.parseJSON(jqXHR.responseText).error);
};
if (typeof guid != 'undefined') {
$.ajax({
type: 'GET',
dataType: 'json',
url: base_path + '/subscription_status/' + guid,
success: handler,
error: errorHandler
});
}
},
showError: function (form, message) {
$('.payola-spinner').css("visibility", "hidden");
$(form).find("input[type=submit]").prop('disabled', false).trigger('error', message);
var error_selector = form.data('payola-error-selector');
if (error_selector) {
$(error_selector).text(message);
$(error_selector).show();
} else {
form.find('.payola-payment-error').text(message);
form.find('.payola-payment-error').show();
}
},
authenticityTokenInput: function () {
return $('<input type="hidden" name="authenticity_token"></input>').val($('meta[name="csrf-token"]').attr("content"));
}
};
PayolaOnestepSubscriptionForm.initialize();
routes.rb
scope module: 'payola' do
post 'payola/buy/:product_class/:permalink' => 'transactions#create', as: :buy
get 'payola/confirm/:guid' => 'transactions#show', as: :confirm
get 'payola/status/:guid' => 'transactions#status', as: :status
post 'payola/subscribe/:plan_class/:plan_id' => 'subscriptions#create', as: :subscribe
get 'payola/confirm_subscription/:guid' => 'subscriptions#show', as: :confirm_subscription
get 'payola/subscription_status/:guid' => 'subscriptions#status', as: :subscription_status
delete 'payola/cancel_subscription/:guid' => 'subscriptions#destroy', as: :cancel_subscription
post 'payola/change_plan/:guid' => 'subscriptions#change_plan', as: :change_subscription_plan
post 'payola/change_quantity/:guid' => 'subscriptions#change_quantity', as: :change_subscription_quantity
post 'payola/update_card/:guid' => 'subscriptions#update_card', as: :update_card
post 'payola/update_customer/:id' => 'customers#update', as: :update_customer
post 'payola/create_card/:customer_id' => 'cards#create', as: :create_card
delete 'payola/destroy_card/:id/:customer_id' => 'cards#destroy', as: :destroy_card
mount StripeEvent::Engine => '/events'
end
new.html.erb
<% content_for :head do %>
<%= render 'payola/transactions/stripe_header' %>
<% end %>
<div class="container">
<div class="form-padding-top">
<%= form_for(resource, :as => resource_name, :url => registration_path(resource_name), :html => {
:class => 'payola-onestep-subscription-form',
'data-payola-base-path' => payola_path,
'data-payola-plan-type' => resource.plan.plan_class,
'data-payola-plan-id' => resource.plan.id}) do |f| %>
<div class="form-group">
<div class="col-md-8">
<%= devise_error_messages! %>
<span id="error_explanation" class="payola-payment-error"></span>
</div>
</div>
<fieldset class="form-group">
<legend><strong>Sign up</legend>
</fieldset>
<div class="form-group">
<div class="col-md-4">
<label>Select a plan for your company</label>
<%= f.collection_select(:plan_id, Plan.user_plan, :id, :name, {}, {:class => 'form-control'}) %>
</div>
</div>
<div class="form-group">
<label for="fg-1" class="col-md-5 col-form-label">Email</label>
<div class="col-md-5">
<%= f.email_field :email, label: false, required: true, autofocus: true, placeholder: 'What is your email address?', class: 'form-control' %>
</div>
</div>
<div class="form-group">
<label class="col-md-5 col-form-label">Username</label>
<div class="col-md-5">
<span class="username-icon-styles">
<span id="valid-username" class="fa fa-check-circle"></span>
<span id="invalid-username" class="fa fa-times-circle"></span>
</span>
<%= f.text_field :username, label: false, required: true, placeholder: 'Create a username for your account', class: 'form-control' %>
</div>
</div>
<p class="status" style="display: block;">
<span class="status_icon"></span>
<span class="status_message"></span>
</p>
</div>
</div>
</div>
<div class="form-group">
<div class="col-md-2">
<label>CVC</label>
<%= text_field :card_code, nil, name: nil, class: 'form-control', data: {stripe: 'cvc'}, placeholder: '000', maxlength: 3 %>
</div>
</div>
<br/>
<div class="form-group">
<div class="col-md-2">
<label>Card Expiration</label>
<%= select_month nil, {use_two_digit_numbers: true}, {name: nil, data: {stripe: 'exp_month'}, class: 'form-control'} %>
<%= select_year nil, {start_year: Date.today.year, end_year: Date.today.year + 10}, {name: nil, data: {stripe: 'exp_year'}, class: 'form-control'} %>
</div>
</div>
<div class="form-group">
<div class="col-md-5">
<label>Password</label>
<%= f.password_field :password, required: true, hint: ("#{@minimum_password_length} characters minimum, include special characters @!%$& and capital letters" if @minimum_password_length), class: 'form-control', label: false %>
</div>
</div>
<div class="form-group">
<div class="col-md-5">
<label for="password-confirm">Password Confirm</label>
<%= f.password_field :password_confirmation, required: true, label: false, class: 'form-control' %>
</div>
</div>
<%= f.submit "Sign up", :class => 'bttn-material-flat bttn-md bttn-primary button', data: {disable_with: false} %>
<div class="text-center">
<div class="payola-spinner">Loading…</div>
</div>
<% end %>
registrations_controller.rb
class Users::RegistrationsController < Devise::RegistrationsController
include Payola::StatusBehavior
before_action :cancel_subscription, only: [:destroy]
def new
build_resource({})
unless params[:plan].nil?
@plan = Plan.find_by!(stripe_id: params[:plan])
resource.plan = @plan
end
yield resource if block_given?
respond_with resource
end
def create
build_resource(sign_up_params)
plan = Plan.find_by!(id: params[:user][:plan_id].to_i)
resource.role = User.roles[plan.stripe_id]
resource.save
yield resource if block_given?
if resource.persisted?
if resource.active_for_authentication?
set_flash_message :notice, :signed_up if is_flashing_format?
sign_up(resource_name, resource)
subscribe
else
set_flash_message :notice, :"signed_up_but_#{resource.inactive_message}" if is_flashing_format?
expire_data_after_sign_in!
subscribe
end
else
clean_up_passwords resource
resource.errors.full_messages.each {|i| flash[i] = i}
end
end
def user_change_plan
plan = Plan.find_by!(id: params[:user][:plan_id].to_i)
unless plan == current_user.plan
role = User.roles[plan.stripe_id]
if current_user.update_attributes!(plan: plan, role: role)
subscription = Payola::Subscription.find_by!(owner_id: current_user.id)
Payola::ChangeSubscriptionPlan.call(subscription, plan)
redirect_to edit_user_registration_path, notice: 'Plan changed.'
else
flash[:alert] = 'Unable to change plan.'
build_resource
render :edit
end
end
end
def user_change_credit_card
@subscription = Payola::Subscription.find_by!(owner_id: current_user.id)
if @subscription.save
Payola::UpdateCard.call(@subscription, params[:stripeToken])
redirect_to edit_user_registration_path, notice: 'Your Credit Card Was Successfully Updated.'
else
build_resource
redirect_to edit_user_registration_path, notice: "There was an error, please check for mistakes with your card information"
end
end
def cancel_user_subscription
@subscription = Payola::Subscription.find_by!(owner_id: current_user.id, state: 'active')
if @subscription.save
Payola::CancelSubscription.call(@subscription) if [email protected]?
current_user.plan_id = nil
current_user.deactivated = true
current_user.save
redirect_to edit_user_registration_path, notice: "Your Plan was Successfully Cancelled."
else
build_resource
redirect_to edit_user_registration_path, notice: "Something went wrong, please try again."
end
end
private
def after_update_path_for(resource)
authenticated_user_root_path
end
def after_sign_up_path_for(resource)
u_dashboard_index_path
end
def subscribe
params[:plan] = current_user.plan
params[:stripeEmail] = current_user.email
params[:trial_start] = DateTime.now
params[:trial_end] = DateTime.now + 6.days
subscription = Payola::CreateSubscription.call(params, current_user)
current_user.save
render_payola_status(subscription)
end
end