[Solved] [React Native / Async API Call] Method callback executes in client before API call resolves


#1

If I log the code execution to the server, I see that the callback fires immediately and logs [2.1] and [2.2] to the console, while the API [SUCCESS] response is logged only later, despite my assumption that it should awaited. What am I missing? Thank you very much!

React Native Client

try {
          // 2.1 Set subscription status
          Meteor.call('users.setSub', {
            receipt: transactionReceipt,
...
          }, (error, result) => {
            if (error) {
              ...
            } else {
              // 2.2 Subscribe to plan from db
              const planHandle = Meteor.subscribe('plan.sub', 'subscribed', async () => {
                const subscription = await Meteor.user().sub;
                const paid = await (subscription.expires > Date.now());
const plan = paid && await Meteor.collection('Plans').findOne({});
                const kw = plan && plan.kw;
const trial = !paid;
                const trialStage = subscription.trialStage;
                const trialPlan = Meteor.user().tagesplan;
                const { breakfast, lunch, dinner } = (trial && trialPlan) || (plan && plan[week][day]) || { breakfast: '', lunch: '', dinner: '' };
                // 2.3 Update state with new plan and subscription status
                this.setState({
                  plan,
                  breakfast,
                  lunch,
                  dinner,
                  loading: !breakfast || !lunch || !dinner,
                  trial,
                  trialStage,
                  transactionReceipt,
                  purchaseToken,
                  productId,
                  kw,
                });
              });
            }
          }); 
...

Server

'users.setSubscription': async function usersSetSubscription(params) {
...
await updatePlan({ owner: this.userId });
...
try {
...
// use JWT to authenticate with service account
        await google.options({ auth: jwtClient });

        // get subscription status by purchaseToken
        await googlePlay.purchases.subscriptions.get({
          packageName,
          subscriptionId,
          token: purchaseToken,
        }, async (error, response) => {
          if (error) {
            console.log('[ERROR - GOOGLE PLAY: SUBSCRIPTIONS.GET] ', error);
          } else {
            console.log('[SUCCESS - GOOGLE PLAY: SUBSCRIPTIONS.GET] ', response);
            // check if expired
            const { data } = response; // JSON.parse(response);
            const expiration = data.expiryTimeMillis;
            subscription.active = await (expiration > Date.now());
            subscription.expires = expiration;
            if (!subscription.starts) subscription.starts = data.startTimeMillis;

            // update user
            return setSubscription({ userId: this.userId, subscription })
              .then(response => response)
              .catch((exception) => {
                console.log('[ERROR][user.setSubscription] ', exception);
                throw new Meteor.Error('500', exception);
              });
          }
}
...
}

#2

You are using await here, but then also passing in a callback, which is almost certainly incorrect. You probably either want to use a try/catch with await assigned to value, or use the callback - but not both.

If that API returns a promise, you’d do something like this:

try {
        // get subscription status by purchaseToken
        // This assumes this will return a promise - if not, you just need to 
        // get rid of "await" and use the callback
        const result = await googlePlay.purchases.subscriptions.get({
          packageName,
          subscriptionId,
          token: purchaseToken,
        })
        console.log('[SUCCESS - GOOGLE PLAY: SUBSCRIPTIONS.GET] ', response);
} catch (error) {
     console.log('[ERROR - GOOGLE PLAY: SUBSCRIPTIONS.GET] ', error);
     return Meteor.Error('some-error-code', 'Some sanitized message')
}

            //continue - check if expired

If not, you can just use the callback, or find some tool to wrap the callback in something which will return a promise.

THis also doesn’t look quite right:

subscription.active = await (expiration > Date.now());

await only works when the function you are calling returns a promise. A promise is the object that let’s you chain .then and .catch on to - like the last method you called setSubscription which does return a promise, but on which you did not await.

try {
    return await setSubscription({ userId: this.userId, subscription })
} catch (error) {
    console.error('[ERROR][user.setSubscription] ', error);
    throw new Meteor.Error('500', error);
}

#3

Meteor specific stuff, and that Google API (which I assume does not actually support Promises) - the server code needs to run in Fibers, which does not require the use of async/await. You probably want to remove all the async/await stuff from your code, and use Meteor.wrapAsync instead.

Here’s a quick example:

// get a Fiber wrapped func
const getGooglePlaySubscriptions = Meteor.wrapAsync(googlePlay.purchases.subscriptions.get)
// now we can call that with synchronous style:
try {
    var response = getGooglePlaySubscriptions({
          packageName,
          subscriptionId,
          token: purchaseToken,
    })
    console.log('[SUCCESS - GOOGLE PLAY: SUBSCRIPTIONS.GET] ', response)
} catch (error) {
    console.error('[ERROR - GOOGLE PLAY: SUBSCRIPTIONS.GET] ', error)
}

#4

Thank you @captainn! Yes, that wasn’t my original code, in the end I was just sticking awaits to everything, like “You get an await, and you…”. :slight_smile:


#5

No way, it works! That was the last piece to make the app work! Thanks again, @captainn!
So, I just removed the async / await stuff and wrapped the API call which contained a callback in an Meteor.wrapAsync() to make it run sync-style within the Fiber, as suggested.