diff --git a/app/routes/app.settings.tsx b/app/routes/app.settings.tsx index 0fd2e4a..64e9e85 100644 --- a/app/routes/app.settings.tsx +++ b/app/routes/app.settings.tsx @@ -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(); const result = useActionData(); const busy = useNavigation().state !== "idle"; + const confirmationUrl = result && "confirmationUrl" in result ? result.confirmationUrl : null; + useEffect(() => { + if (confirmationUrl) window.open(confirmationUrl, "_top"); + }, [confirmationUrl]); return ( {result && {result.message}}