fix(billing): open Shopify's plan approval page instead of showing 401

billing.request throws a 401 with the approval URL in X-Shopify-API-Request-Failure-Reauthorize-Url for App Bridge to follow, but a React Router form submission rendered it as an error page. The action now returns the URL and the page opens it at the top level. Invalid-session retries are still rethrown for App Bridge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
metatroncubeswdev 2026-10-16 09:14:37 -04:00
parent 6061877ca1
commit 3f1a07382b

View File

@ -1,4 +1,5 @@
import type { ActionFunctionArgs, LoaderFunctionArgs } from "react-router";
import { useEffect } from "react";
import { Form, useActionData, useLoaderData, useNavigation } from "react-router";
import prisma from "../db.server";
import type { ReminderPolicy } from "../engine/types";
@ -61,8 +62,17 @@ export const action = async ({ request }: ActionFunctionArgs) => {
await prisma.shop.update({ where: { id: cfg.shop.id }, data: { plan: "free" } });
return { ok: true, message: "Switched to Starter" };
}
// Redirects the merchant to Shopify's approval page.
await billing.request({ plan: plan as typeof PLAN_GROWTH, isTest, returnUrl: `https://${cfg.shop.domain}/admin/apps/${process.env.SHOPIFY_API_KEY}/app/settings` });
// billing.request throws a 401 carrying Shopify's approval URL for App Bridge to follow. A React Router
// form submission surfaces that 401 as an error page instead, so hand the URL to the page to open.
try {
await billing.request({ plan: plan as typeof PLAN_GROWTH, isTest, returnUrl: `https://${cfg.shop.domain}/admin/apps/${process.env.SHOPIFY_API_KEY}/app/settings` });
} catch (e) {
if (!(e instanceof Response) || e.headers.has("X-Shopify-Retry-Invalid-Session-Request")) throw e;
const confirmationUrl = e.headers.get("X-Shopify-API-Request-Failure-Reauthorize-Url");
if (confirmationUrl) return { ok: true, message: "Opening Shopify to approve the plan…", confirmationUrl };
console.error(`[datewise] billing request failed: ${e.status} ${[...e.headers.keys()].join(",")}`);
return { ok: false, message: "Shopify couldn't start the plan change. Reload the page and try again." };
}
}
if (intent === "guard") {
@ -170,6 +180,10 @@ export default function Settings() {
const { plan, s, guard, custom } = useLoaderData<typeof loader>();
const result = useActionData<typeof action>();
const busy = useNavigation().state !== "idle";
const confirmationUrl = result && "confirmationUrl" in result ? result.confirmationUrl : null;
useEffect(() => {
if (confirmationUrl) window.open(confirmationUrl, "_top");
}, [confirmationUrl]);
return (
<s-page heading="Settings">
{result && <s-banner tone={result.ok ? "success" : "critical"}>{result.message}</s-banner>}