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 <noreply@anthropic.com>
This commit is contained in:
metatroncubeswdev 2026-10-30 20:31:33 -04:00
parent 799789ca28
commit 19b403f98f
2 changed files with 6 additions and 4 deletions

View File

@ -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" };
}

View File

@ -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" } });
}