Users can subscribe to my Rails app through Stripe:
class Subscription::CreditCardController < Subscription::PaymentController
def new
stripe_customer = Customers::FindOrCreate.call(current_account)
if stripe_customer
#stripe_subscription = Subscriptions::Create.call(current_account, #price, :payment_behavior => "default_incomplete")
#client_secret = #stripe_subscription.latest_invoice.payment_intent.client_secret
else
flash[:notice] = "An error occurred."
redirect_to plans_path
end
end
def create
flash[:success] = "Subscription created."
redirect_to plans_path
end
end
I am relying on Stripe.js to catch any errors that may occur:
form.addEventListener('submit', function(e) {
e.preventDefault();
handlePayment();
});
function handlePayment() {
stripe.confirmCardPayment(clientSecret.value, {
payment_method: {
card: card,
billing_details: {
name: name.value,
email: email.value
}
}
}).then(function(result) {
if (result.error) {
displayError(result.error);
} else {
form.submit();
}
});
}
This works in 99 % of all test cases so far. Stripe.js catches pretty much every error that can possibly occur. But on some rare occasions it does NOT, so that the error occurs on the server when Stripe is trying to create the Subscription, leaving it in an incomplete state.
This is a major problem because I am not handling these type of errors in my create controller action right now.
So why is it that Stripe catches most of these errors but not all? As a developer, am I supposed to validate user input with Stripe.js AND on the server? Or will one of the two suffice? Reading through Stripe's documentation I was under the impression that Stripe.js will handle everything for me and that I won't have to add additional checks on the server level.
Is this not correct?
Related
I had earlier been provided permission for the feature Manage_pages. However, Facebook has changed it policies and this feature has been depreciated, moreover, my application wasn't using much of the Facebook feature. For these 2 reasons, my app has been disabled.
SO, i submitted a review for the same. The feedback is as such:
The main purpose of my app is to take payments through the client's Facebook page , for which earlier used Manage_pages feature. Now, even if i hit the connect button, it says "The app is in dev mode" . Am stuck here, how can i get the button to work and display a screen cast if I keep getting the same error. this seems like an endless while loop :(
Now, when i am trying to re-submit an app review, it seems to be getting auto rejected.
Any insight would be appreciated!
Here is my Facebook controller code
class FacebookController < ApplicationController
skip_before_action :verify_authenticity_token
skip_before_action :authorize
def connect
# get array of facebook pages added
page_ids = params[:tabs_added].keys
# create or update connection for each facebook page added
page_ids.each do |page_id|
page = FacebookPage.where(page_id: page_id).first_or_initialize
page.update!(slug_id: params[:slug_id], form_type: params[:form_type])
end
head :created
end
def show
request = parse_signed_request(params[:signed_request]).try(:[], 'page')
facebook_page = FacebookPage.find_by(page_id: request.try(:[], 'id'))
facebook_admin = request.try(:[], 'admin')
if facebook_page
redirect_to contribution_form_url(
subdomain: facebook_page.account_type,
id: facebook_page.slug.name,
form_type: facebook_page.form_type,
iframe: true,
facebook: true,
facebook_admin: facebook_admin,
facebook_page: facebook_page
)
else
render nothing: true
end
end
def update
facebook_page = FacebookPage.find(params[:id])
slug = Slug.find_by!(name: page_params[:form_slug], account_type: facebook_page.account_type)
if facebook_page.update!(slug: slug, form_type: page_params[:form_type])
redirect_to contribution_form_url(
subdomain: facebook_page.account_type,
id: facebook_page.slug.name,
form_type: facebook_page.form_type,
iframe: true,
facebook: true,
facebook_admin: true,
facebook_page: facebook_page
)
end
end
private
def parse_signed_request(signed_request)
# We only care about the data after the '.'
payload = signed_request.split('.')[1]
# Facebook gives us a base64URL encoded string.
# Ruby only supports base64 out of the box, so we have to add padding to make it work
payload += '=' * (4 - payload.length.modulo(4))
decoded_json = Base64.decode64(payload)
JSON.parse(decoded_json)
end
def page_params
params.require(:facebook_page).permit(:form_type, :form_slug)
end
end
and i am also attaching the the script which opens the facebook tab
<script>
function move_to_top( value )
{
$(".fb_dialog").each(function(index) {
if($(this).css("top")!='-10000px') {
$(this).css("top", value + 'px' );
}
});
setTimeout( ('move_to_top("'+value+'");'), 1250);
}
window.addToFacebook = function() {
// make sure logged in as user; not page
FB.login(function(loginResponse) {
// if they are correctly logged in as user
if (loginResponse.authResponse) {
// popup modal to add form to page
FB.ui({
method: 'pagetab',
display: 'popup'
}, function(response) {
// if tabs were added, make the connection
if (Object.keys(response.tabs_added).length > 0) {
console.log("inside")
response.form_type = $('[name=form_type]:checked').val().slice(0,1);
response.slug_id = '<%= Slug.find_by(name: slug, account_type: candidate.account_type).try(:id) || candidate.account_slug.id %>';
$.post('/facebook/connect', response);
$.magnificPopup.open({
items: {
src: '<%=j render "form_settings/facebook_success" %>',
type: 'inline'
}
});
}
});
$(".fbProfileBrowserResult").ready( function(){
t = setTimeout ( ('move_to_top("'+50+'")'), 1250 );
});
}
});
};
</script>
<p><%= link_to 'Connect with Facebook', '#', onclick: 'addToFacebook()', class: 'button' %></p>
I am trying to validate User inputs on server side in a Rails Application with React as view. Basically I make axios calls to the Rails API like this:
const Script = props => {
const [script, setScript] = useState({})
const [scene, setScene] = useState({})
const [loaded, setLoaded] = useState(false)
useEffect(() => {
const scriptID = props.match.params.id
const url = `/api/v1/scripts/${scriptID}`
axios.get(url)
.then( resp => {
setScript(resp.data)
setLoaded(true)
})
.catch(resp => console.log(resp))
}, [])
const handleChange = (e) => {
e.preventDefault()
setScene(Object.assign({}, scene, {[e.target.name]: e.target.value}))
}
const handleSubmit = (e) => {
e.preventDefault()
const csrfToken = document.querySelector('[name=csrf-token]').content
axios.defaults.headers.common['X-CSRF-TOKEN'] = csrfToken
const script_id = script.data.id
axios.post('/api/v1/scenes', {scene, script_id})
.then(resp => {
const included = [...script.included, resp.data.data]
setScript({...script, included})
})
.catch(err => {
console.log(err.response.data.error)
})
.finally(() => {
setScene({name: '', description: ''})
})
}
All data gets passed into a react component with a form.
return (
<div className="wrapper">
{
loaded &&
<Fragment>
.
.
.
<SceneForm
handleChange={handleChange}
handleSubmit={handleSubmit}
attributes={script.data.attributes}
scene={scene}
/>
</Fragment>
}
</div>
)
In this form I have a name field and the corresponding name in Rails has a validation uniqueness: true. everything works fine if I enter a valid (unique) name.
I tried to implement a validation but I am not happy with the outcome. (It works in general: my no_errors? method does what is is supposed to do and I get a 403 status) This is the controller part:
def create
scene = script.scenes.new(scene_params)
if no_error?(scene)
if scene.save
render json: SceneSerializer.new(scene).serialized_json
else
render json: { error: scene.errors.messages }, status: 422
# render json: { error: scene.errors.messages[:name] }, status: 423
end
else
render json: { error: "name must be unique" }, status: 403
end
end
.
.
.
private
def no_error?(scene)
Scene.where(name: scene.name, script_id: scene.script_id).empty?
end
If I enter an existing name I get a console.log like this:
screenshot
Here is my concern: I am not happy with my approach of error handling in general. I do not want to get the 403 message logged to the console (I do want to avoid this message in the first place).
My idea is to take the "simple form" approach: Make the border of my field red and post an error message under the field, without any console output...
And on a side note: Is 403 the correct status? I was thinking about 422 but wasn't sure...
Thank you for your ideas in advance!
403 is the wrong status code. What you need is to return a 422 (unprocessable entity). 403 is more about policy and what you are authorized to do.
Then when you deal with http request it's a standard to have a request and status code printed in browser console. Not sur to get your issue here.
If it's about to display the error you could just have a function that colorize (or whatever fireworks you want) your input if the status code response is a 422.
Wandering into uncharted waters, I've been having some trouble figuring out how to access an error message I made in my Rails API thru my React Frontend. I've been reading through a good amount of sites and so far I haven't really been able to figure out where i'm going wrong. Is it on the Rails side or the React side?
TLDR; I want my response error to be: "Username or Password does not match.", but I am getting: "Request failed with status code 422"
Rails Controller
class Api::V1::SessionsController < ApplicationController
def create
user = User.find_by(email: params["user"]["email"]).try(:authenticate, params["user"]["password"])
if user
session[:user_id] = user.id
render json: {
status: 200,
logged_in: true,
user: user
}
else
// how can I reach this error message?
render json: { status: "error", message: "Username or Password does not match." }, status: :unprocessable_entity
end
end
...
React Component
handleLogin = (e) => {
e.preventDefault();
axios
.post(
'http://localhost:3001/api/v1/sessions',
{
user: { email: this.state.email, password: this.state.password }
},
{ withCredentials: true }
)
.then((response) => {
if (response.data.logged_in) {
this.handleSuccessfulAuth(response.data);
}
})
.catch((error) => {
// returns login error Request failed with status code 422
console.log('login error', error.message);
});
};
Your message property could be accessible like this:
.catch(error => {
console.log('login error', error.response.data.message);
});
The problem is that I can't show stripe errors on signup modal dialog.
Javascript:
$.externalScript('https://js.stripe.com/v1/').done(function(script, textStatus) {
Stripe.setPublishableKey($('meta[name="stripe-key"]').attr('content'));
var subscription = {
setupForm: function() {
return $('form').submit(function() {
$('input[type=submit]').prop('disabled', true);
if ($('#card_number').length) {
subscription.processCard();
return false;
} else {
return true;
}
});
},
processCard: function() {
var card;
card = {
name: $('#user_name').val(),
number: $('#card_number').val(),
cvc: $('#card_code').val(),
expMonth: $('#card_month').val(),
expYear: $('#card_year').val()
};
return Stripe.createToken(card, subscription.handleStripeResponse);
},
handleStripeResponse: function(status, response) {
if (status === 200) {
$('#stripe_token').val(response.id)
$('form')[0].submit()
} else {
$('#stripe_error').text(response.error.message).show();
return $('input[type=submit]').prop('disabled', false);
}
}
};
return subscription.setupForm();
});
In the same js file, I have this ajax call for devise's create method but I get null value for error all the time. So I guess this part below and the one above can't go together. I tried to merge them and do ajax call inside handleStripeResponse function above but then some other problems appeared.
$('.signup').click(function(e){
e.preventDefault();
$.ajax ({
url: '/users',
method: 'POST',
dataType: "JSON",
success: function(data) {
if (data.status == 'failed') {
$('#stripe_error').show().text(data.error_message);
return false;
}
}
});
});
Devise Registrations Controller:
def create
build_resource
user = User.new params[:user]
if user.save_with_payment
if user.active_for_authentication?
set_flash_message :notice, :signed_up if is_navigational_format?
sign_in(resource_name, user)
# respond_with resource, :location => after_sign_up_path_for(user)
render json: { status: 'success', resource: user }
else
set_flash_message :notice, :"signed_up_but_#{resource.inactive_message}" if is_navigational_format?
expire_session_data_after_sign_in!
respond_with user, :location => after_inactive_sign_up_path_for(user)
end
else
clean_up_passwords user
render json: { status: 'failed', error_message: user.save_with_payment }
end
end
User model:
def save_with_payment
if valid?
stripe_customer = Stripe::Customer.create(
:email => email,
:source => stripe_token
)
self.customer_id = stripe_customer.id
update_stripe_user_details
save!
end
rescue Stripe::InvalidRequestError => e
logger.error "Stripe error while creating customer: #{e.message}"
errors.add :base, "There was a problem with your credit card."
rescue Stripe::CardError => e
stripe_error e, e.message
errors.add :base, e.message
rescue Stripe::InvalidRequestError => e
stripe_error e, e.message
errors.add :base, e.message
rescue Stripe::AuthenticationError => e
stripe_error e, e.message
errors.add :base, e.message
rescue Stripe::APIConnectionError => e
stripe_error e, e.message
errors.add :base, e.message
rescue Stripe::StripeError => e
stripe_error e, e.message
errors.add :base, e.message
rescue => e
stripe_error e, e.message
errors.add :base, e.message
end
Please point me in the right direction, I am getting out of ideas.
The main problem you have is that you are trying to do way to much in a single controller action.
Your registrations controller is not just responsible for dealing with the users form input but also with dealing with a 3:rd party - if you start to think about the potential number of code paths here you'll realize that this is a crazy amount of responsibilities.
What you might want to do is split the actions up:
POST /registrations # create a user profile
POST /registrations/:registration_id/payments # pay
This gives you a clear set of responsibilities and makes it much easier to allow the user to retry failed payments without starting from square one or having an overly complex setup of passing params back and forth.
If what you are building is a members only type site where you want to restrict access to paying users you should handle this on the authorization layer - not the authentication layer.
What this means in practice is that you let any user with has completed the signup authorize but then check a flag on the record or count the number of payments on the record when you check if the user is authorized to view any content.
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # guest user (not logged in)
if user.payed_in_full?
can :read, :all
end
end
end
edit
The problem is that at the same form which has one button, I need to sign > up user and enter stripe card details.
Yeah, thats exactly the point. You need to change your approach.
If an ajax solution is acceptable you would do something like this:
function sendForm($form){
return $.ajax($form.attr('action'), {
data: $form.serializeArray(),
method: 'POST',
dataType: "JSON",
});
}
$(document).on('.signup', 'submit', function(){
var $form = $(this);
var promise = sendForm($form);
promise.done(function(data, textStatus, jqXHR ){
if (jqXHR.statusText === "OK") {
console.log("User signup was successful");
$form.toggleClass('signup payup');
$form.attr('action', '/payments');
$form.submit(); // this will cause the '.payup', 'submit' event
else {
console.log("User signup failed");
// todo - display errors
}
});
return false; // prevent default action
});
$(document).on('.payup', 'submit', function(){
var $form = $(this);
// should post to something like `/payments`
var promise = sendForm($form);
promise.done(function(data, textStatus, jqXHR ){
if (jqXHR.statusText === "OK") {
console.log("Payment was created successfully");
else {
console.log("Payment failed");
// todo - display errors
}
});
return false; // prevent default action
});
This gives you two different HTTP calls and is far simpler from an API standpoint, while the user experience is that he only submits the form once.
this is my controller method
def purchase
if #item_point > current_user.points
flash.keep[:notice] = "You dont have enough points to buy this item"
else
flash.keep[:notice] = "You have successfully redeemed this items, Thanks for the purchase."
end
render :json => flash
p flash[:notice]
end
and in my javascript ajax function's success i have written
$(".purchaseBtm button").click(function() {
var button_id = this.id.split("-")[1];
$.ajax({
url: '/hhr/purchase',
data : {
button_id : button_id
},
success: function(response) {
console.log('message: <%= flash[:notice]%>');
$('.flash-msg').html('');
$('.flash-'+button_id).text('<%= flash[:notice]%>')
}
});
});
This ajax call goes on a button click which passes some values, with which #item_point is calculated.
Problem is in my terminal i'm getting the correct Flash message every time.
but in my Browser console sometimes i get the first flash message as correct and then the same flash message persists. I wont get the second flash message. But the terminal shows corectly(the p message.)
If this action is to only to render feedback message from server, then I would just render the hash containing the message, as there is no point to store it in special flash container.
if #item_point > current_user.points
message = "You dont have enough points to buy this item"
else
message = "You have successfully redeemed this items, Thanks for the purchase." }
end
render json: { notice: message }
Edit:
Then in your json use
success: function(response) {
console.log('message: ' + response.notice);
$('.flash-msg').html('');
$('.flash-'+button_id).text(response.notice)
}
Also remember to set type of ajax request to dataType: 'json'