From 19b403f98f88d5a1587102f18160647780a02dec Mon Sep 17 00:00:00 2001 From: metatroncubeswdev Date: Fri, 30 Oct 2026 20:31:33 -0400 Subject: [PATCH] fix(billing): register plans approved as test charges; reset plan on uninstall App Store review 1.2.2: the reviewer's store got a test charge, but billing.check ran with isTest:false and ignored it, so the approved plan never registered. Checks now count test subscriptions too, and cancel uses each subscription's own test flag. Uninstall resets the stored plan to free (Shopify cancels the subscription), so a reinstall asks for approval again. Co-Authored-By: Claude Opus 5.5 --- app/routes/app.settings.tsx | 8 +++++--- app/routes/webhooks.app.tsx | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/routes/app.settings.tsx b/app/routes/app.settings.tsx index 64e9e85..5ba42f1 100644 --- a/app/routes/app.settings.tsx +++ b/app/routes/app.settings.tsx @@ -16,7 +16,9 @@ import { type CustomizationKind, findCustomizations, findGuard, gql, setCustomiz export const loader = async ({ request }: LoaderFunctionArgs) => { const { cfg, admin, billing } = await adminContext(request); // Keep the stored plan in sync with Shopify billing. - const check = await billing.check({ plans: [PLAN_GROWTH, PLAN_PRO], isTest: await billingIsTest(admin) }); + // isTest: true counts test subscriptions too. Shopify makes the charge a test on stores that can't be billed + // (including App Store review stores), and those must still unlock the plan. + const check = await billing.check({ plans: [PLAN_GROWTH, PLAN_PRO], isTest: true }); const plan = check.appSubscriptions.find((s) => s.status === "ACTIVE")?.name ?? "free"; if (plan !== cfg.shop.plan) await prisma.shop.update({ where: { id: cfg.shop.id }, data: { plan } }); const guard = await findGuard(admin).catch(() => null); @@ -57,8 +59,8 @@ export const action = async ({ request }: ActionFunctionArgs) => { const plan = intent.slice(5); const isTest = await billingIsTest(admin); if (plan === "free") { - const check = await billing.check({ plans: [PLAN_GROWTH, PLAN_PRO], isTest }); - for (const sub of check.appSubscriptions) await billing.cancel({ subscriptionId: sub.id, isTest, prorate: true }); + const check = await billing.check({ plans: [PLAN_GROWTH, PLAN_PRO], isTest: true }); + for (const sub of check.appSubscriptions) await billing.cancel({ subscriptionId: sub.id, isTest: sub.test, prorate: true }); await prisma.shop.update({ where: { id: cfg.shop.id }, data: { plan: "free" } }); return { ok: true, message: "Switched to Starter" }; } diff --git a/app/routes/webhooks.app.tsx b/app/routes/webhooks.app.tsx index 66c55a3..aba53ed 100644 --- a/app/routes/webhooks.app.tsx +++ b/app/routes/webhooks.app.tsx @@ -11,7 +11,7 @@ export const action = async ({ request }: ActionFunctionArgs) => { const { shop, topic, session, payload } = await authenticate.webhook(request); if (topic === "APP_UNINSTALLED") { if (session) await prisma.session.deleteMany({ where: { shop } }); - await prisma.shop.updateMany({ where: { domain: shop }, data: { uninstalledAt: new Date() } }); + await prisma.shop.updateMany({ where: { domain: shop }, data: { uninstalledAt: new Date(), plan: "free" } }); // Shopify cancels app subscriptions on uninstall await prisma.hold.deleteMany({ where: { shop: { domain: shop } } }); await prisma.reminderJob.updateMany({ where: { shop: { domain: shop }, status: "pending" }, data: { status: "cancelled" } }); }