Using onCreateUser async


#1

When saving a user for the first time I need to make an external call to a service and only after the service returns correctly can I complete the creation of the user as I need to have an id that is returned from the service appended to the user. It appears I can’t make an async call from onCreateUser() since it will return before the external call is completed. Is there another option to make onCreateUser() async or is there another hook I should be looking at instead. This is a chicken vs egg issue since I need a userid to make the call to the external service but I need the external service to successfully complete in order to create the user.

Any recommendations or pointers on how I can approach this?


#2

Reviving an old but important thread I think.

To the OP - the onCreateUser function’s ‘user’ argument has an ‘_id’ property which you can use to make that call. Works well.

What doesn’t work so well is if the (async) call in question then requires you to update the user in question. When that happens, trying to update the users db causes an error as the user is not yet in the Meteor.users collection.

The alternative I’ve been struggling with over the last day and a half is doing so using wrapAsync/bindEnvironment/async-await.

I’ve ultimately failed because the onCreateUser function MUST return the user object and not a promise.

Has anybody figured out a workaround?


#3

Can you provide example code on what you are trying to accomplish?


#4

Cheers for jumping in!

Sure…

Use case

When a new user signs up, add them as a customer to Stripe as well as create a subscription for them on Stripe.

Stripe functions

(All tested and working correctly from the ‘billing and plans’ page I have set up for already signed up users. These are all functions getting called via a validated method from the client when they select a plan to change to/subscribe to.

// Create a customer on stripe. Pass in email (required) and description (practice name)
export const createCustomerOnStripe = ({ email, clientName }) =>
	stripe.customers.create({ email, description: clientName });

// Once a customer is created, add a subscription for them (include token if it is a paid plan)
export const addSubscriptionToCustomer = ({ stripeId, planId, token }) => {
	if (token) {
		return stripe.subscriptions.create({ customer: stripeId, plan: planId, source: token });
	}
	return stripe.subscriptions.create({ customer: stripeId, plan: planId });
};

// Update payment card for customer
export const updatePaymentCard = ({ stripeId, token }) => stripe.customers.update(stripeId, { source: token });

// Change from an existing subscription to a different one
export const changeSubscription = ({ subscriptionId, planId, token }) => {
	if (token) {
		return stripe.subscriptions.update(subscriptionId, { plan: planId, source: token });
	}
	return stripe.subscriptions.update(subscriptionId, { plan: planId });
};

Now, the validated methods that call the functions above take care of the users I already have, but new users, I wish them added on Stripe to a free plan when they sign up.

So…

Working fine before any modifications:

Accounts.onCreateUser(({ email, planId, clientName }, user) => {
	// Add some data to the new user so they don't have an empty dashboard
	seedNewAccount(user._id, clientName, email);
	return user;
});

Not working:

1 Turning it into an async function

Note - This does actually create the customer on Stripe no problem, but the overall onCreateUser ends up returning a promise that resolves into the user, and that throws the account creation process off (it does not expect a promise).

Accounts.onCreateUser(async ({ email, planId, clientName }, user) => {
	// Add some data to the new user so they don't have an empty dashboard
	seedNewAccount(user._id, clientName, email);
	// Once all the above is successfully done, add the client to Stripe with a free subscription
	const { id: stripeId } = await createCustomerOnStripe({ email, clientName });
	const { id, status, current_period_end } = await addSubscriptionToCustomer({
		stripeId,
		planId,
  })
  const planInfo = { stripeId, status, renewalDate: current_period_end, planId }
  user.planInfo = planInfo;

  return user;
});

2 Using Meteors wrapasync

Accounts.onCreateUser(({ email, planId, clientName }, user) => {
	// Add some data to the new user so they don't have an empty dashboard
	seedNewAccount(user._id, clientName, email);
  // Once all the above is successfully done, add the client to Stripe with a free subscription
  function addToStripe() {
    const { id: stripeId } = await createCustomerOnStripe({ email, clientName });
    const { id, status, current_period_end } = await addSubscriptionToCustomer({
      stripeId,
      planId,
    })
    const planInfo = { stripeId, status, renewalDate: current_period_end, planId }
    user.planInfo = planInfo;
  }
  const syncAddToStripe = Meteor.wrapAsync(addToStripe);
  syncAddToStripe();

	return user;
});

I’ve tried so many variations it’s starting to get muddled, but I think the above snipped ended up not working at all.

I think it is because the wrapAsync expects a callback while this is promise based?

3 Using an async function within onCreateUser

Kind of a noobish approach this, but summary is it ends up with the same issue as number 1. The moment you add an async function, the whole thing becomes promise based and you can’t directly return the user


Am I on the right track with any of the approaches above or wildly off? I’m hoping that I am on the right track and just a tweak or two will be sufficient to get this working.


#5

Here you can find an answer to your question: https://stackoverflow.com/questions/47017782/meteor-promises-in-accounts-oncreateuser/47033150#47033150

As you can see, I was also on the wrong track but @coagmano got it right. I will copy the relevant part of his answer here for completeness. Enjoy!

Accounts.onCreateUser((options, user) => {
  // ...

  user.stripe = {};
  const createStripeCustomer = Meteor.wrapAsync(stripe.customers.create,stripe.customers);
  const response = createStripeCustomer({
      description: user.profile.first_name + ' ' + user.profile.last_name
  });
  user.stripe.id = response.id
  return user;
});

#6

Cheers @tomsp !

I’ll give that a go and report back :+1:


#7

Success! cheers again @tomsp

I don’t actually fully understand what I have done (need to read more into the binding of wrapAsync), but here is the working code for anybody who needs it:

export const processNewSignup = ({ clientName, email, planId }) => {
	const createStripeCustomer = Meteor.wrapAsync(stripe.customers.create, stripe.customers);
	const createStripeSub = Meteor.wrapAsync(stripe.subscriptions.create, stripe.subscriptions);
	const { id: stripeId } = createStripeCustomer({
		description: clientName,
		email,
	});
	const { id: subscriptionId, status: subscriptionStatus, current_period_end: renewalDate } = createStripeSub({
		customer: stripeId,
		plan: planId,
	});
	return { stripeId, subscriptionId, subscriptionStatus, renewalDate };
};

And then within accounts.js:

Accounts.onCreateUser(({ email, planId, clientName }, user) => {
	// Add some data to the new user so they don't have an empty dashboard
	seedNewAccount(user._id, clientName, email);
        const billingInfo = processNewSignup({clientName, email, planId});
        user.billing = {planId, ...billingInfo};
	return user;
});

#8

If you want to understand wrapAsync, read about fibers:
https://benjamn.github.io/goto2015-talk/#/

I also found this course. I haven’t checked it out but talks about fibers and wrapAsync
link: https://www.eventedmind.com/classes/meteor-fibers-and-dynamics-9ba17f98/introduction-852d7a76


#9

Nice! I hadn’t seen that before. Thanks for sharing :slight_smile:


#10

When I started my current job (First time working with Meteor), they told me to: 1. do the tutorial 2. learn what fibers are