Is my Paypal checkout flow Angular-Node secure? - node.js

I'm developing a paypal checkout using the 'basic Smart Payment Buttons integration' and integrating it with server Node installing the 'checkout-server-sdk'.
I followed the documentations:
https://developer.paypal.com/docs/checkout/reference/server-integration/set-up-transaction/
https://github.com/paypal/Checkout-NodeJS-SDK
where they suggest to:
'createOrder' starting from the client and calling the server
generating on the server an orderID and return it to the client
'onApprove' send to the server the orderID and approve it on the server
return back to the client the response
I don't think it is a good flow.
Someone could:
start the payment
so the app create the order on the server taking the shoppingcart from db and elaborate a totalPrice of 100euros.
generete the orderID and send it back to the client
instead of approve this order, a 'bad user' could, in some way, send to the server another orderID that could correspond to a lower price (2euros)
so he could approve the payment of 2 euros
So I don't understand why we need to make the checkout jumping more times from client to server.
Or maybe am i doing something wrong on my checkoutflow ?
unfortunately I feel the Paypal documentation so unclear.
checkout.component.html
<!-- * here there is a form where i get shipment info, invoice info and so on ->
<!-- * PAYPAL SMART BUTTONS -->
<div>
<div #paypal></div>
</div>
checkout.component.ts
onFormSubmit() {
this.isFormSubmitted = true;
// set paypal settings and show the paypal buttons
this.paypalSetting(this.shippmentInfo, this.invoiceRequired, this.invoice, this.addressInvoice);
}
async paypalSetting(shipment, invoiceRequired, invoice, addressInvoice) {
await paypal
.Buttons({
style: {
size: 'responsive',
label: 'pay',
},
experience: {
input_fields: {
no_shipping: 1,
},
},
createOrder: async (data, actions) => {
console.log('CREATE ORDER -->');
var paypalOrderId;
//generate new order
await this.apiService.newOrder().toPromise().then(
(res) => {
console.log('ON CREATE: SUCCESSFULLY CREATED')
paypalOrderId = res.order.paypalOrderId;
// ????? someone here could change 'paypalOrderId' with another value !!!!
//I also would like to return the 'paypalOrderId' only here !!
},
(err) => {
console.log('ON CREATE: ERROR: ' + err);
// how should i manage this error ? i should skip the flow to onError but HOW ?
}
);
return paypalOrderId;
},
onApprove: async (data, actions) => {
console.log('APPROVE ORDER -->');
var paypalOrderId = data.orderID;
console.log('ON APPROVE: save the order on server/DB')
await this.apiService.saveOrder(shipment, invoiceRequired, invoice, addressInvoice, paypalOrderId).toPromise().then(
(res) => {
console.log('ON APPROVE: ORDER APPROVED')
this.isPaid = true;
//if isPaid i can show a 'success page'
},
(err) => {
console.log('ON APPROVE: ERROR: ' + err);
this.isPaid = false;
}
);
},
onError: (err) => {
console.log('ON ERROR: ' + err);
},
})
.render(this.paypalElement.nativeElement);
}
Node api.js
//* paypal
const paypal = require('#paypal/checkout-server-sdk');
const payPalClient = require('../paypalManager');
router.post('/newOrder', tokenManager.verifyAccessToken, async function (req, res, next) {
const idUser = req.userId;
// I get the shoppingcart of the user 'idUser'
// i calculate the total price
var totalPrice;
//* Call PayPal to set up a transaction
let order;
const request = new paypal.orders.OrdersCreateRequest();
request.prefer("return=representation");
request.requestBody({
intent: 'CAPTURE',
purchase_units: [{
description: 'payment ecc..', /
amount: {
currency_code: 'EUR',
value: totalPrice
}
}],
application_context: {
brand_name: "brand",
shipping_preference: 'NO_SHIPPING',
},
});
let response = await payPalClient.client().execute(request);
order = response;
const paypalOrderId = order.result.id;
// return a successful response to the client with the order ID
return res.json({
status: 200,
order: {
paypalOrderId: paypalOrderId,
},
message: "Paypal order sucessfully created",
});
});
router.post('/saveOrder', tokenManager.verifyAccessToken, async function (req, res, next) {
const idUser = req.userId;
var paypalOrderId = req.body.paypalOrderId;
try {
connection.beginTransaction(async () => {
try {
// here i insert all the checkout infos in DB
// confirm the queries executions
connection.commit(async function (err) {
if (err) {
//return connection.rollback(function () {
connection.rollback(function () {
return next(createError.Unauthorized("Sql query error: " + err)); //! or error.message
});
}
//* here i send the Emails to confirm the checkout
//* capture/approve the order
console.log('CAPTURING THE ORDER')
var request = new paypal.orders.OrdersCaptureRequest(paypalOrderId);
request.requestBody({});
// Call API with your client and get a response for your call
let response = await payPalClient.client().execute(request);
//*response
return res.json({
status: 200,
message: "Paypal sucessfully approved",
});
});// end commit
} catch (error) {
connection.rollback(function () {
return next(createError.Unauthorized("Sql query error " + error)); //! or error.message
});
}
});// end transaction
} catch (error) {
return next(error);
}
});
Node paypalManager.js
'use strict';
/**
* PayPal Node JS SDK dependency
*/
const checkoutNodeJssdk = require('#paypal/checkout-server-sdk');
/**
* Returns PayPal HTTP client instance with environment that has access
* credentials context. Use this instance to invoke PayPal APIs, provided the
* credentials have access.
*/
function client() {
return new checkoutNodeJssdk.core.PayPalHttpClient(environment());
}
/**
* Set up and return PayPal JavaScript SDK environment with PayPal access credentials.
* This sample uses SandboxEnvironment. In production, use LiveEnvironment.
*/
function environment() {
let clientId = process.env.PAYPAL_CLIENT_ID;
let clientSecret = process.env.PAYPAL_CLIENT_SECRET;
return new checkoutNodeJssdk.core.SandboxEnvironment(
clientId, clientSecret
);
}
module.exports = {
client: client,
prettyPrint: prettyPrint
};

The reason you are "jumping" between the client and the server, is the approval by the payer has to happen on the client. The payer cannot give their approval on your server, they are not sitting on your server. They are using a client browser.
Regarding:
a 'bad user' could, in some way, send to the server another orderID that could correspond to a lower price (2euros)
If this happens, your server should reject the undesired transaction, and not proceed with it. That's the point of having a server. Nothing happens unless your server OKs it.

Related

Sequelize not retrieving all data after insert

I have noticed that my backend is not retrieving the expected data after an insert.
In my React application, I have one function which inserts data into the database and after getting a response, a new request is sent to update the current component state with the newly fetched data.
All my functions are using await/async and in the backend, all transactions are correctly used and committed in order.
My client is calling the following endpoints:
-POST: api/ticket ( INSERT AN ITEM)
-GET: api/ticket (GET ALL ITEMS)
Here is what the backend is showing which looks correct to me, the problem is that in the 'SELECT' statement, the inserted item is not retrieved.
The transactions are started from two different routes but I don't see why it should be an issue.
In addition, I tried to change the AddItem function to output the same findAll statement which is called when using the GET method and the data returned are correct.
So why if I separate these two flows I do not get all the items? I always need to refresh the page to get the added item.
START TRANSACTION;
Executing (a9d14d5c-c0ac-4821-9b88-293b086debaa): INSERT INTO `messages` (`id`,`message`,`createdAt`,`updatedAt`,`ticketId`,`userId`) VALUES (DEFAULT,?,?,?,?,?);
Executing (a9d14d5c-c0ac-4821-9b88-293b086debaa): COMMIT;
Executing (9ee9ddaa-294e-41d1-9e03-9f02a2737030): START TRANSACTION;
Executing (9ee9ddaa-294e-41d1-9e03-9f02a2737030): SELECT `ticket`.`id`, `ticket`.`subject`, `ticket`.`status`, `ticket`.`createdAt`, `ticket`.`updatedAt`, `ticket`.`deletedAt`, `ticket`.`userId`, `messages`.`id` AS `messages.id`, `messages`.`message` AS `messages.message`, `messages`.`sender` AS `messages.sender`, `messages`.`createdAt` AS `messages.createdAt`, `messages`.`updatedAt` AS `messages.updatedAt`, `messages`.`deletedAt` AS `messages.deletedAt`, `messages`.`ticketId` AS `messages.ticketId`, `messages`.`userId` AS `messages.userId`, `messages->user`.`id` AS `messages.user.id`, `messages->user`.`firstname` AS `messages.user.firstname`, `messages->user`.`surname` AS `messages.user.surname`, `messages->user`.`email` AS `messages.user.email`, `messages->user`.`password` AS `messages.user.password`, `messages->user`.`stripeId` AS `messages.user.stripeId`, `messages->user`.`token` AS `messages.user.token`, `messages->user`.`birthDate` AS `messages.user.birthDate`, `messages->user`.`status` AS `messages.user.status`, `messages->user`.`confirmationCode` AS `messages.user.confirmationCode`, `messages->user`.`createdAt` AS `messages.user.createdAt`, `messages->user`.`updatedAt` AS `messages.user.updatedAt`, `messages->user`.`deletedAt` AS `messages.user.deletedAt` FROM `tickets` AS `ticket` LEFT OUTER JOIN `messages` AS `messages` ON `ticket`.`id` = `messages`.`ticketId` AND (`messages`.`deletedAt` IS NULL) LEFT OUTER JOIN `users` AS `messages->user` ON `messages`.`userId` = `messages->user`.`id` AND (`messages->user`.`deletedAt` IS NULL) WHERE (`ticket`.`deletedAt` IS NULL);
Executing (9ee9ddaa-294e-41d1-9e03-9f02a2737030): COMMIT;
-- POST '/api/ticket
exports.addMessage = async (req, res) => {
try {
const result = await sequelize.transaction(async (t) => {
var ticketId = req.body.ticketId;
const userId = req.body.userId;
const message = req.body.message;
const subject = req.body.subject;
// Validate input - If new ticket, a subject must be provided
if (!ticketId && !subject) {
return res
.status(400)
.send({ message: "New ticket must have a subject" });
}
// Validate input - If ticket exists, userId and message must be provided
if (!userId && !message && ticketId) {
return res
.status(400)
.send({ message: "UserID and message are required" });
}
// Create ticket is no ticketID was provided
if (!ticketId) {
const [ticket, created] = await Ticket.findOrCreate({
where: {
subject: subject,
userId: userId,
},
transaction: t,
});
ticketId = ticket.id;
}
// Create a new message object
const messageObject = await db.message.create(
{
message: message,
userId: userId,
ticketId: ticketId,
},
{ transaction: t }
);
// Output message object
return res.send(messageObject);
});
} catch (err) {
console.log(err);
return res.status(500).send({
message:
err.message || "Some error occurred while creating the ticket message.",
});
}
};
-- GET: api/ticket
exports.findAll = async (req, res) => {
try {
const result = await sequelize.transaction(async (t) => {
const tickets = await db.ticket.findAll(
{
include: [{ model: db.message, include: [db.user] }],
},
{ transaction: t }
);
tickets.forEach((ticket) => {
console.log(JSON.stringify(ticket.messages.length));
});
return res.send(tickets);
});
} catch (err) {
console.log(err);
res.status(500).send({
message: err.message || "Some error occurred while retrieving Tickets.",
});
}
};
You sent a response to a client before the transaction actually was committed. You just need to move res.send(messageObject); outside the transaction call.
You can try to look what's going on in the current version of your code if you add several console.log with messages to see what the actual order of actions is (I mean a couple of messages in POST (the last statement inside transaction and after transaction before res.send) and at least one at the beginning of GET).
Actually if the transaction was rolled back you'd send an uncommited and already removed object/record that I suppose is not your goal.

How to send a NODE.JS post request from an Angular Project?

I have a NODE.JS api using expressjs that connects to an SQL Server, and I want to use it in an angular project. I make use two files, a route file and a controllers file. My route file is as follows:
module.exports = (app) => {
const UsrContrllr = require('../Controllers/users.controllers');
//1. GET ALL USERS
app.get('/api/users', UsrContrllr.func1);
//2. POST NEW USER
app.post('/api/user/new', UsrContrllr.func2);
};
And my controllers file is given below:
const mssql = require('mssql');
exports.func1 = (req, res) =>
{
// Validate request
console.log(`Fetching RESPONSE`);
// create Request object
var request = new mssql.Request();
// query to the database and get the records
const queryStr = `SELECT * FROM USERS`;
request.query(queryStr, function (err, recordset) {
if (err) console.log(err)
else {
if (recordset.recordset.toString() === '') {
res.send('Oops!!! Required data not found...');
}
else {
// send records as a response
res.send(recordset);
}
};
});
};
exports.func2 = (req, res) =>
{
// Validate request
console.log(`INSERTING RECORD ${req}`);
// create Request object
var request = new mssql.Request();
// query to the database and get the records
const queryStr = `INSERT INTO GDUSERS (USERCODE, PASSWORD, LANGUAGE, USERCLASS, FIRSTNAME, LASTNAME, CONTACTNO) VALUES ('${req.body.usercode}', '${req.body.password}', 'EN', '0', '${req.body.firstname}', '${req.body.lastname}', '${req.body.contactno}');`;
request.query(queryStr, function (err, recordset) {
if (err) console.log(err)
else {
if (recordset.recordset.toString() == '') {
res.send('Oops!!! Required data not found...');
}
else {
// Send records as response
res.send(recordset);
}
};
});
};
The GET request works well, but when I try to run the POST request directly from the angular application, I get an error stating
Cannot GET URL/api/user/new
The angular code in my angular project is:
signup() {
let headers = new Headers({ 'Content-Type': 'application/json' });
let options = new RequestOptions({ headers: headers });
console.log(this.user); //User details come from a form
this.http.post(“URL", this.user, options)
.subscribe(
(err) => {
if(err) console.log(err);
console.log("Success");
});
}
I’m not sure whether the angular code I’m using, is right or not, and I don’t know where I’m going wrong. How does one exactly send a http POST request from an Angular project?
this i the way i handled my user signup with http.post calls. my approach is slightly different when signing up user because i am using a promise instead of observable (which i normally use for my servicecalls). but i will show you both ways.
createUser(user: User): Promise < string > {
const promise = new Promise < string > ((resolve, reject) => {
const userForPost = this.createUserForPost(user);
this.http.post(environment.backendUrl + '/api/user/signup', userForPost, this.config).toPromise < HttpConfig > ()
.then(createdUser => {
}).catch(error => {
console.log(error);
});
});
return promise;
}
here another example with an observable
createForumPost(forumPost: ForumPost) {
this.http.post < { message: string, forumPostId: string } > (environment.backendUrl + '/api/forumPosts', forumPost).subscribe((responseData) => {
const id = responseData.forumPostId;
forumPost.id = id;
});
}
i defined my URL somewhere else and then just use the environment.backedUrl + 'path' to define my path (the same as the path in your backend controller)
this is one of my first answers here on SO. i am sry if it is a bit messy
i hope i was able to help with my examples :)

Stripe - Update default card

I am trying to allow the user to update their default payment method after they add it. I am getting this in Firebase Functions: Error: No such source: card_1EhmibFZW9pBNLO2aveVfEm6.
This leads me to believe that I need to pass default_source a src_XXX... id rather than a card_XXX... id. Anyone have an idea on this?
Firebase Function:
// Update Stripe default card based on user choice
exports.updateDefaultSource = functions.firestore
.document("users/{userId}")
.onUpdate(async (change, context) => {
const newValue = change.after.data();
const previousValue = change.before.data();
console.log("previousValue.default_source: "+previousValue.default_source)
console.log("newValue.default_source: "+newValue.default_source)
if (
previousValue.default_source &&
newValue.default_source !== previousValue.default_source
) {
// this triggers on every update to profile (more overhead), can we reduce this?
try {
console.log("newValue.default_source: "+newValue.default_source)
const response = await stripe.customers.update(
previousValue.customer_id,
{ default_source: newValue.default_source },
(err, customer) => {
console.log(err);
}
);
return console.log("Response from Stripe update: " + response);
} catch (error) {
console.log(error);
await change.ref.set(
{ error: userFacingMessage(error) },
{ merge: true }
);
return reportError(error, { user: context.params.userId });
}
}
});
Firebase Function logs after I add the second Card to account:
Looks like this error solved itself, not 100% sure on how, but my guess is it had to do with Redux and/or Redux Persist not having everything loaded into the store.
My main question was answered by #hmunoz on whether or not the default_source accepted the card_123 type, which it does.

Accessing Firestore via Cloud Function

So i have 2 Cloud Functions within the same file:
exports.Auth = functions.region('europe-west1').https.onRequest((req, res) =>
and
exports.IPN = functions.region('europe-west1').https.onRequest((req, res) =>
When adding the following code right at the start of my Auth function it adds a new document to the Firestore as expected, however, when i add the same code at the start of my IPN function, which is currently being called via Paypal's IPN Simulator, it does nothing, no errors.
let pin = RandomPIN(10, '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ');
var userRef = db.collection('Users').doc(pin);
var setWithOptions = userRef.set({ Activated: false }, { merge: true });
console.log("PIN: "+pin);
What on earth is going on, i must be missing something?
Thanks in advance.
Update:
Here are the logs, first with the 2 middle lines commented and then uncommented It seems to be silently failing, i'm just not sure what is causing it.
Update with Complete function:
exports.IPN = functions.region('europe-west1').https.onRequest((req, res) =>
{
console.log("IPN Notification Event Received");
let pin = RandomPIN(10, '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ');
var userRef = db.collection('Users').doc(pin);
var setWithOptions = userRef.set({ Activated: false }, { merge: true });
console.log("PIN: "+pin);
if (req.method !== "POST")
{
console.error("Request method not allowed.");
res.status(405).send("Method Not Allowed");
}
else
{
console.log("IPN Notification Event received successfully.");
res.status(200).end();
}
let ipnTransactionMessage = req.body;
// Convert JSON ipn data to a query string since Google Cloud Function does not expose raw request data.
let formUrlEncodedBody = querystring.stringify(ipnTransactionMessage);
// Build the body of the verification post message by prefixing 'cmd=_notify-validate'.
let verificationBody = `cmd=_notify-validate&${formUrlEncodedBody}`;
console.log(`Verifying IPN: ${verificationBody}`);
let options = {
method: "POST",
uri: getPaypalURI(),
body: verificationBody,
};
// POST verification IPN data to paypal to validate.
request(options, function callback(error, response, body)
{
if(!error && response.statusCode === 200)
{
if(body === "VERIFIED")
{
console.log(`Verified IPN: IPN message for Transaction ID: ${ipnTransactionMessage.txn_id} is verified.`);
SendPIN(ipnTransactionMessage.payer_email, pin);
}
else if(body === "INVALID")
console.error(`Invalid IPN: IPN message for Transaction ID: ${ipnTransactionMessage.txn_id} is invalid.`);
else
console.error("Unexpected reponse body.");
}
else
{
console.error(error);
console.log(body);
}
});
});
Indeed it is a problem of Promises chaining and also a problem due to the request library: request supports callback interfaces natively but does not return a promise, which is what you must do within a Cloud Function.
I would suggest that you watch these official Firebase videos from Doug : https://www.youtube.com/watch?v=7IkUgCLr5oA&t=28s and https://www.youtube.com/watch?v=652XeeKNHSk which explain this key concept.
You can use request-promise (https://github.com/request/request-promise) and the rp() method which "returns a regular Promises/A+ compliant promise".
It is not clear what SendPIN() is doing. Let's make the assumption it returns a Promise. If this is true, you could adapt your code along the following lines:
//....
const rp = require('request-promise');
//....
exports.IPN = functions.region('europe-west1').https.onRequest((req, res) => {
console.log('IPN Notification Event Received');
let pin = RandomPIN(
10,
'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
);
var userRef = db.collection('Users').doc(pin);
if (req.method !== 'POST') {
console.error('Request method not allowed.');
res.status(405).send('Method Not Allowed');
} else {
let ipnTransactionMessage;
userRef
.set({ Activated: false }, { merge: true })
.then(() => {
console.log('PIN: ' + pin);
ipnTransactionMessage = req.body;
// Convert JSON ipn data to a query string since Google Cloud Function does not expose raw request data.
let formUrlEncodedBody = querystring.stringify(ipnTransactionMessage);
// Build the body of the verification post message by prefixing 'cmd=_notify-validate'.
let verificationBody = `cmd=_notify-validate&${formUrlEncodedBody}`;
console.log(`Verifying IPN: ${verificationBody}`);
let options = {
method: 'POST',
uri: getPaypalURI(),
body: verificationBody
};
// POST verification IPN data to paypal to validate.
return rp(options);
})
.then(response => {
//Not sure what you will get within the response object...
console.log(
`Verified IPN: IPN message for Transaction ID: ${
ipnTransactionMessage.txn_id
} is verified.`
);
return SendPIN(ipnTransactionMessage.payer_email, pin); //It is not clear what SendPIN is doing, let's make the assumption it returns a Promise...
})
.then(() => {
res.send('Success');
return null;
})
.catch(err => {
console.error(
`Invalid IPN: IPN message for Transaction ID: ${
ipnTransactionMessage.txn_id
} is invalid.`
);
res
.status(500)
.send(
'Error: ' +
err +
` - Invalid IPN: IPN message for Transaction ID: ${
ipnTransactionMessage.txn_id
} is invalid.`
);
return null;
});
}
});

Getting meteor call to return stripe payments response

I've been working with Meteor and the stripe package to try and make a customer. So First I have my client side code which calls a method on the server so when clicked i have in the client.js:
Meteor.call('usersignup', function (error, result) {
console.log (result);
});
So this calls the Method on the server.js:
var Future = Npm.require('fibers/future');
var stripe = StripeAPI('my key'); // secret stripe API key
Meteor.methods({
usersignup: function(cusEmail){
var fut = new Future();
stripe.customers.create(
{ email: cusEmail },
function(err, customer) {
if (err) {
console.log(err);
fut.ret;
}
fut.ret(customer);
}
);
return fut.wait();
},
userfail: function(cusid){
var fut = new Future();
stripe.customers.retrieve(cusid, function(err, result) {
if(err){
console.log(err);
fut.ret;
}
fut.ret(err, result);
});
return fut.wait();
}
});
Now this works and creates a customer when I log onto the stripe.com dashboard but I'm trying to get the response returned to the client well at least the customer id for now and print it in the console. This is where I can't seem to get it to work. It'll log undefined when I do console.log(result). Any ideas?
EDIT: So I put the fiber and the stripe key as global variables now and don't get an error but the returns don't seem to be returning any values. so on the client side I have:
'click #signupsubmit': function (event) {
console.log("hello");
var whatis = getVal(); // function gets value of forms and returns object
var testid;
var cusid = Meteor.call('usersignup', whatis.email, function (error, result) {
if (error) {
console.log(err.message);
return;
}
console.log(result);
console.log("meteor call");
testid = result;
return (result);
});
console.log("outside call");
console.log(testid);
console.log(cusid);
},
});
So i've been running some console.log tests and it seems it executes the meteor.call and keeps going down the line. Console.log of both testid and cusid return undefined but a couple seconds later I receive the console.log of result and the string "meteor call" from inside the meteor.call. Is there a way to wait for the meteor call to finish then run the rest of what is in my click function? so console output will go like:
"hello"
"outside call"
test id undefined
cusid undefined
"meteor call"
"result"
Keep in mind that the stripe API doesn't use Fibers. You need to put it in manually. The callback doesn't reach the client because by then it would have already got a response (its async)
You can use something like this to wait for a result from the stripe callback before a result is returned to the client:
var stripe = StripeAPI('mykeygoeshere'); // secret stripe API key
var Future = Npm.require('fibers/future');
var fut = new Future();
stripe.customers.create(
{ email: 'hello#example.org' },
function(err, customer) {
if (err) {
console.log(err.message);
fut.ret;
}
fut.ret("customer id", customer.id);
}
);
return fut.wait();
Here a Future is used and it waits for a result to be received from the stripe callback before a result is returned to the client.
More info can be found on Fibers/Futures & Synchronous Callbacks incuding how to go about them & when to use them:
Meteor: Calling an asynchronous function inside a Meteor.method and returning the result
https://github.com/laverdet/node-fibers
https://gist.github.com/possibilities/3443021
Here's something simpler. Meteor now has Meteor.wrapAsync() for this kind of situation:
var stripe = StripeAPI("key");
Meteor.methods({
yourMethod: function(callArg) {
var charge = Meteor.wrapAsync(stripe.charges.create, stripe.charges);
charge({
amount: amount,
currency: "usd",
//I passed the stripe token in callArg
card: callArg.stripeToken,
}, function(err, charge) {
if (err && err.type === 'StripeCardError') {
// The card has been declined
throw new Meteor.Error("stripe-charge-error", err.message);
}
//Insert your 'on success' code here
});
}
});
I found this post really helpful:
Meteor: Proper use of Meteor.wrapAsync on server

Resources