[test store]Using the managed price, after selecting free, the subscription update was received immediately, and the plan changed to free

I am testing the managed price using a test store, and my app has not been published yet.
I have set up a Pro plan and a Free plan.

When I switched from the Free plan to the Pro plan, I received the webhook, and everything worked correctly.

However, when I switched from Pro back to Free, a confirmation page popped up stating that the plan would change to Free only after the Pro plan expires.

This is also the behavior I expected — switching to Free after the Pro plan expires.
However, I immediately received a subscription update webhook, and my plan changed to Free right away.

This is very strange, it means my test account directly switched to Free.

Is this behavior only in the test store, and official merchants won’t encounter this issue?
Or do I need to do some additional work to ensure it runs correctly?

My understanding is that I have integrated managed pricing, and then by using webhooks and providing refreshes, I can handle it properly, isn’t that right?

here is my code:

import type { ActionFunctionArgs } from "react-router";
import { authenticate } from "../shopify.server";

import * as SubscriptionDb from "../db/subscriptions.server";
import { refreshSubscription } from "app/services/subscription-service.server";

export const action = async ({ request }: ActionFunctionArgs) => {
  const { shop, session, topic, admin } = await authenticate.webhook(request);

  console.log(
    `[SUB_UPDATE] Received ${topic} webhook for ${shop}. session exists:${session !== undefined}`,
  );

  if (!session) {
    console.log(`[SUB_UPDATE] Deleting subscription for ${shop}`);
    await SubscriptionDb.deleteSubscriptionsByShop(shop);
  } else if (admin) {
    console.log(`[SUB_UPDATE] Refreshing subscription for ${shop}`);
    await refreshSubscription(session.shop, admin);
  }
  return new Response();
};

//  app/services/subscription-service.server
export async function refreshSubscription(shop: string, admin?: AdminApiType) {
  try {
    const adminToUse = admin || (await unauthenticated.admin(shop)).admin;
    // Get the latest active subscription from Shopify
    const { currentAppInstallation } = await getActiveSubscriptions(adminToUse);
    const activeSubscription = currentAppInstallation.activeSubscriptions[0];

    console.log("activeSubscription", shop, activeSubscription);

    // Delete all existing subscriptions for the shop
    await SubscriptionDb.deleteSubscriptionsByShop(shop);

    // If there's an active subscription, create it in our database
    if (activeSubscription) {
      // upsert
      await SubscriptionDb.createSubscription({
        shop,
        subscriptionId: activeSubscription.id,
        status: activeSubscription.status,
        planName: activeSubscription.name,
        currentPeriodEnd: new Date(activeSubscription.currentPeriodEnd),
      });
      return activeSubscription;
    }
  } catch (exception) {
    console.error(
      `refreshSubscription - failed. shop:${shop}. exception: ${exception}`,
    );
  }

  return null;
}
1 Like

Hey @Zwei_Black, what you’re seeing is the expected behavior. When a merchant downgrades to a Free plan, the subscription object updates immediately (which is why you receive the webhook right away and see it change to “Free”), but the billing stays on the Pro plan until currentPeriodEnd. The UI message about the plan changing “in the next billing cycle” refers to when the Free billing begins, not when the subscription object updates.

You may need to add logic that checks currentPeriodEnd to determine feature access. Even though the subscription name is “Free,” the merchant has paid through currentPeriodEnd.

How can I find out about the previous subscriptions? In this case, checking the active subscriptions is actually meaningless, because they will be updated immediately anyway. So how can I track which plan the user is currently on?

Great question. You can query allSubscriptions to see the details of previous subscriptions.

Using the currentPeriodEnd timestamp can help you determine when to downgrade the available features in those months where a merchant has downgraded mid cycle.

I also tried this: Billing

But still return ‘Free’ after swithing to ‘Free’ plan.

  const { hasActivePayment, appSubscriptions } = await billing.check();
  console.log("hasActivePayment", JSON.stringify(hasActivePayment));
  console.log("appSubscriptions", JSON.stringify(appSubscriptions));

Do you have any examples of a fully correct implementation? I feel that the promises in the documentation are far from the actual situation. When I use Stripe’s subscription API in other systems, it returns the current status of the user, but Shopify doesn’t seem to provide an intuitive way to get this status.

Thanks for sharing that. I agree, the language used in the docs can be confusing.

In these cases, it’s useful to consider the billing period separate from the app subscription.

Its currently only possible for a merchant to have a single app subscription at a time, so when a merchant downgrades and accepts the change, their plan is changed immediately and that is what is reflected in the API, but their billing cycle remains the same.

The merchant docs do a better job of explaining this: https://help.shopify.com/en/manual/your-account/manage-billing/billing-charges/types-of-charges/third-party-charges/app-charges#app-prorating-upgrades-downgrades

Is a scenario with Shopify managed pricing and a downgrade mid cycle, the subscription changes immediately, but their billing change is defered until the start of the next cycle. You would need to account for this within your app logic.

Alternatively, there is the billing API where you have a little more control over the replacement behaviour. You can specify immediately in instances of downgrades and the merchant would then be granted credits for the remaining balance. AppSubscriptionReplacementBehavior - GraphQL Admin

Yes, this is the confusing part of Shopify’s billing API. I feel that it actually only cares about the future subscription status, rather than what the current status should be. So when canceling a subscription, upgrading or downgrading (regardless of the replacement behavior), it will change immediately.

The current membership status needs to be handled by the application itself. For example, if a user uninstalls the app during the subscription period, their subscription will be immediately cleared, but they should actually still be considered a member. The billing API should not really be used to obtain the current membership status.

I can now save a copy of the last valid subscription in the database, and when the subscription from api is undefinited, determine whether the user has a subscription based on the database. However, for deferred downgrades and similar cases (using immediate effect works fine), it is not possible to determine the current status through the billing API.

I hope that in the future there will be an API to get the user’s current membership status, just like Stripe. However, Shopify’s subscription model is quite different, so it might be difficult to implement.

Thanks for that context and feedback, @Zwei_Black. I’ll be passing that on to our product teams for sure.

Currently managed billing doesn’t have all of the features that the Billing API does, so we may run in to some of these scenarios where we can’t deal with them as elegently as when using the billing API. As more functionality is added to managed billing I imagine this will make it much better and clearer in the future.

2 Likes