Remix redirect isn't isn't triggering redirect, just returns 200 code

Hi all,

I’m trying to set up automatic rediection from app.tsx when a user does not have an active payment. I’m using managed pricing. I’ve following the instructions outlined here:

export const loader = async ({ request }: LoaderFunctionArgs) => {
  const { billing, redirect, session } = await authenticate.admin(request);
  const appHandle = process.env.SHOPIFY_APP_HANDLE;

  const { hasActivePayment } = await billing.check();

  const shop = session.shop;
  const storeHandle = shop.replace('.myshopify.com', '');

  // If there's no active subscription, redirect to the plan selection page...
  if (!hasActivePayment) {
    throw redirect(`https://admin.shopify.com/store/${storeHandle}/charges/${appHandle}/pricing_plans`, {
      target: "_top", // required since the URL is outside the embedded app scope
    });
  }

  return { apiKey: process.env.SHOPIFY_API_KEY || "" };
};

When I hit any route on my app (in development - I’ve not tested production for obvious reasons), it just returns a 200 and doesn’t attempt any redirect:

I have no console errors. In my development console the following is printed:

10:14:51 │                     remix │ 10:14:51 [vite] (client) hmr update /app/routes/app.tsx
10:14:51 │                     remix │ 10:14:51 [vite] (client) hmr update /app/routes/app.tsx
10:14:51 │                     remix │ 10:14:51 [vite] (client) hmr update /app/routes/app.tsx
10:14:51 │                     remix │ 10:14:51 [vite] (ssr) page reload app/routes/app.tsx
10:14:55 │                     remix │ [shopify-app/INFO] Authenticating admin request | {shop: 21orange.myshopify.com}
10:14:55 │                     remix │ [shopify-app/INFO] Authenticating admin request | {shop: 21orange.myshopify.com}
10:14:56 │                     remix │ ErrorResponseImpl {
10:14:56 │                     remix │   status: 200,
10:14:56 │                     remix │   statusText: '',
10:14:56 │                     remix │   internal: false,
10:14:56 │                     remix │   data: '\n' +
10:14:56 │                     remix │     '      <script data-api-key="fcfb21a0f1d13c8588fc8c379a913087" src="https://cdn.shopify.com/shopifycloud/app-bridge.js"></script>\n' +
10:14:56 │                     remix │     '      <script>window.open("https://admin.shopify.com/store/21orange/charges/customize-pos-products-dev/pricing_plans", "_top")</script>\n' +
10:14:56 │                     remix │     '    '
10:14:56 │                     remix │ }

I’ve also tried redirecting to routes other than the pricing plan route. Nothing works… I’ve looked around these forums and github issues, and thought this was related, however I’ve updated to the latest versions of the following with no success:

"@shopify/app-bridge-react": "^4.2.4",
"@shopify/shopify-app-remix": "^4.0.1",

The following works as expeceted in the frontend, however I need to get the Remix redirect working in the loader:

  useEffect(() => {
    open(
      'https://admin.shopify.com/store/${storeHandle}/charges/${appHandle}/pricing_plans',
      "_top",
    );
  }, []);

Liam-Shopify do you or anybody else have any ideas? This is currently blocking me from making the subscription part of my app live. Thanks!

I’ve had to do the rediect client side instead:


export const loader = async ({ request }: LoaderFunctionArgs) => {
  const { billing } = await authenticate.admin(request);
  const { hasActivePayment } = await billing.check();

  const url = new URL(request.url);
  const pathname = url.pathname;

  const isRedirectingToShopifyBilling =
    url.hostname === 'admin.shopify.com' ||
    pathname.includes('/app/billing');

  return { needsRedirect: !hasActivePayment && !isRedirectingToShopifyBilling, apiKey: process.env.SHOPIFY_API_KEY || "" };
};

export default function App() {
  const { apiKey, needsRedirect } = useLoaderData<typeof loader>();
  const navigate = useNavigate()

  useEffect(() => {
    if (needsRedirect) {
      navigate('/app/billing');
    }
  }, [needsRedirect]);

...

This seems to work. However I would expect code in the docs to work as expected.