pnzr
pnzr

Reputation: 225

Meteor.wrapasync when used with Stripe API won't return error properly

I'm integrating the Stripe API onto my app. I'm using the following method on the server

stripeRegister: function(token) {    
    // Create the secret key on the server
    var Stripe = StripeAPI(Meteor.settings.Stripe.secretKey);
    var syncFunction = Meteor.wrapAsync(Stripe.charges.create, Stripe.charges);

    var stripeToken = token.stripeToken;

    try {
      console.log("try")

      var charge = syncFunction({
        amount: 1000,
        currency: "usd",
        card: stripeToken,
        description: "[email protected]"
      });

      console.log(charge);
      console.log("after charge")

      return charge;
    }
    catch(e) {
      console.log("error")
      console.log(charge);
      console.log(e);
      throw new Meteor.Error(402, e);
    }
}

Now this works fine for a good charge. If you feed it a credit card that is good, its happy. However, if you feed it one that isn't and you get an error for any number of reasons you might get an error, it gives you a Exception while invoking method 'stripeRegister' undefined error.

If you try to then give it the callback in the syncFunction call it will print your error, but then you aren't doing things in the right manner anymore and can't properly throw an error because you're not "synchronous" anymore. For example,

var charge = syncFunction({
  amount: 1000,
  currency: "usd",
  card: stripeToken,
  description: "[email protected]"
  }, function(err, charge) {
  if (err && err.type === 'StripeCardError') {
    // The card has been declined
  }
});

I assumed that I was passing in the correct this context (the fact it returns good on a good charge makes me think this is right), but why is there an undefined call at this point? Shouldn't it be calling the callback of the Stripe.charges.create function and putting that into the catch block?

I was referencing this question Meteor.WrapAsync don't return value a lot in creating the solution. Thanks for the help

Upvotes: 1

Views: 1123

Answers (2)

pnzr
pnzr

Reputation: 225

So found a temporary fix. I say temporary because its not as elegant as I'd like. Anyways, I got around the error issue the following way. Hope this helps someone else later (and saves you a ton of time).

First things first, make sure to add the following packages to your project

copleykj:stripe-sync
grove:stripe-checkout
grove:stripe-npm
grove:stripe.js

There are other packages that would work for this, but these happen to be the ones I choose. The copleykj:stripe-sync package is great because it makes the whole stripe package async, which saves you a lot of time on any other usage of it.

Second, using the API. I found it easier to just put in a code block with lots of comments. The main issue with the error return is you can't access the top level error object because the format of the return is unique. For that reason, access the items within the object. Look at the switch statement to see what I mean. The method below is only on the server.

For a full list of all the methods wrapped, check out the stripe sync package page

// Use the access token of the account you are going to pay // If you are accepting the payments directly to yourself, this is your API token // if you are accepting payments on behalf of someone, then its their token that was gotten from Stipe Connect path

var Stripe = StripeSync(access_token);

try{
  // Just to show you its working
  var account = Stripe.account.retrieve()
  console.log(account);

  // An example charge
  // includes a application fee and the access token of the account you are charging
  var charge = Stripe.charges.create({
    amount: 1299,
    currency: "usd",
    card: 'some_card_token',
    description: "Test charge",
    application_fee: 299
  },
  access_token);

  console.log(charge);

}catch(error){
  // You can't do console.log(error) because it throws a server error
  // you can access the error.type and error.message though
  console.log(error.type);
  console.log(error.message);

  // Customize the return 
  switch (error.type) {
    case 'StripeCardError':
      // A declined card error
      // error.message; // => e.g. "Your card's expiration year is invalid."
      throw new Meteor.Error(1001, error.message);
      break;
    case 'StripeInvalidRequest':
      // Invalid parameters were supplied to Stripe's API
      throw new Meteor.Error(1001, error.message);
      break;
    case 'StripeAPIError':
      // An error occurred internally with Stripe's API
      throw new Meteor.Error(1001, error.message);
      break;
    case 'StripeConnectionError':
      // Some kind of error occurred during the HTTPS communication
      throw new Meteor.Error(1001, error.message);
      break;
    case 'StripeAuthenticationError':
      // You probably used an incorrect API key
      throw new Meteor.Error(1001, error.message);
      break;
    default:
      throw new Meteor.Error(1001, error.message);
  }
}

Third, the entire pattern.

On client: You have the form that is submitted. You can use either the custom form implementation or Stripe Checkout one. I used Stripe Checkout because it looks prettier and they do a lot of validation for me (lazy!). From here you use a Meteor.call to send the call to the server and wait for the response.

On server: You use that method I show above to interact on the server with the API. You return either a success or an error to the client.

So it kind of looks like this in total...

  • User submits some form. Stripe client-side API gives you a token.
  • You use a Meteor.call to call the server and pass along the token along with any other relevant info.
  • On the server, you use the StripeSync package and a try/catch block to interact with the API.
  • Based on the response from the API, return success or error.
  • In the error handler function on the client, display success or failure.

An example of the client side code, in case you were wondering. A copy paste won't work because its somewhat specific code to me, but you can get the general idea from it. I'm using StripeCheckout, so if you are using a custom form, almost everything you do will be in the onSubmit function. And you'll do a basic configure in the rendered function. I have to do more as part of the callback for the checkout. Also, I'm using Autoform to deal with forms.

Template.someTemplate.rendered = function() {

  // Need to interval the initialize to make sure its executing faster than it should
  var newInterval = Meteor.setInterval(function() {
    if (StripeCheckout) {
      Meteor.clearInterval(newInterval);
      var stripePubKey = stripe_publishable_key;

      handler = StripeCheckout.configure({
        key: stripePubKey,
        image: '[email protected]',
        token: function(token) {
          // Use the token to create the charge with a server-side script.
          // You can access the token ID with `token.id`

          var thisForm = AutoForm.getFormValues('registerForEventForm');

          Meteor.call('someServerCall', thisForm.insertDoc, thisForm.updateDoc, currentDoc, token, function(error, id) {
            console.log("\nMeteor call has returned");
            if (error) {
              // display error to the user
              throwError(error.reason);
              $("#btn").prop('disabled', false);
            }else{
              throwNotification("Successful registration and card payment.", "success");
            }
          });
        }
      });

      // Allow the button to be used to submit form
      $("#btn").prop('disabled', false);
    }
  }, 50);
};



AutoForm.hooks({
  registerForEventForm: {
    // Called when form does not have a `type` attribute
    onSubmit: function(insertDoc, updateDoc, currentDoc) {

      // Open Checkout with further options
      handler.open({
        name: 'Some App',
        description: 'Registration Cost for '+currentDoc.title,
        amount: 1299,
        email: Meteor.user().emails[0].address,
        closed: function() {
          $("#btn").prop('disabled', false);
        }
      });

      return false;

    },
  }
});

Good luck.

Upvotes: 2

Ben Wong
Ben Wong

Reputation: 31

I think this is currently a bug. See here: https://github.com/meteor/meteor/issues/2774

Upvotes: 1

Related Questions