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:
parent
6061877ca1
commit
3f1a07382b
@ -1,4 +1,5 @@
|
|||||||
import type { ActionFunctionArgs, LoaderFunctionArgs } from "react-router";
|
import type { ActionFunctionArgs, LoaderFunctionArgs } from "react-router";
|
||||||
|
import { useEffect } from "react";
|
||||||
import { Form, useActionData, useLoaderData, useNavigation } from "react-router";
|
import { Form, useActionData, useLoaderData, useNavigation } from "react-router";
|
||||||
import prisma from "../db.server";
|
import prisma from "../db.server";
|
||||||
import type { ReminderPolicy } from "../engine/types";
|
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" } });
|
await prisma.shop.update({ where: { id: cfg.shop.id }, data: { plan: "free" } });
|
||||||
return { ok: true, message: "Switched to Starter" };
|
return { ok: true, message: "Switched to Starter" };
|
||||||
}
|
}
|
||||||
// Redirects the merchant to Shopify's approval page.
|
// 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` });
|
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") {
|
if (intent === "guard") {
|
||||||
@ -170,6 +180,10 @@ export default function Settings() {
|
|||||||
const { plan, s, guard, custom } = useLoaderData<typeof loader>();
|
const { plan, s, guard, custom } = useLoaderData<typeof loader>();
|
||||||
const result = useActionData<typeof action>();
|
const result = useActionData<typeof action>();
|
||||||
const busy = useNavigation().state !== "idle";
|
const busy = useNavigation().state !== "idle";
|
||||||
|
const confirmationUrl = result && "confirmationUrl" in result ? result.confirmationUrl : null;
|
||||||
|
useEffect(() => {
|
||||||
|
if (confirmationUrl) window.open(confirmationUrl, "_top");
|
||||||
|
}, [confirmationUrl]);
|
||||||
return (
|
return (
|
||||||
<s-page heading="Settings">
|
<s-page heading="Settings">
|
||||||
{result && <s-banner tone={result.ok ? "success" : "critical"}>{result.message}</s-banner>}
|
{result && <s-banner tone={result.ok ? "success" : "critical"}>{result.message}</s-banner>}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user