Ask for plan approval again when the subscription was cancelled (uninstall, then reinstall)
Shopify App Pricing keeps a prepaid subscription active with cancelAtEndOfCycle after an uninstall, so activeSubscription isn't null on reinstall. Treat a cancelled subscription like no plan (review 1.2.2). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
d723f41de5
commit
d814ba62bb
@ -5,7 +5,7 @@
|
||||
const PARTNER_API_VERSION = "2026-07";
|
||||
|
||||
const QUERY = `query OfferlystActiveSubscription($appId: ID!, $shopId: ID!) {
|
||||
activeSubscription(appId: $appId, shopId: $shopId) { items { handle } currentBillingCycle { endTime } pendingUpdate { items { handle } } }
|
||||
activeSubscription(appId: $appId, shopId: $shopId) { cancelAtEndOfCycle items { handle } currentBillingCycle { endTime } pendingUpdate { items { handle } } }
|
||||
}`;
|
||||
|
||||
export interface ActiveSubscription {
|
||||
@ -14,6 +14,8 @@ export interface ActiveSubscription {
|
||||
/** a scheduled change (e.g. a downgrade from a yearly plan): its plan handles and when it applies */
|
||||
pendingHandles: string[];
|
||||
changesAt: string | null;
|
||||
/** the subscription was cancelled (app uninstalled, or cancelled in the admin) and only runs out its paid period */
|
||||
cancelled: boolean;
|
||||
}
|
||||
|
||||
// Only confirmed subscriptions are cached, briefly: a merchant who just approved a plan is checked at once,
|
||||
@ -39,6 +41,7 @@ export async function fetchActiveSubscription(shopGid: string, { fresh = false }
|
||||
const json = (await res.json().catch(() => ({}))) as {
|
||||
data?: {
|
||||
activeSubscription: {
|
||||
cancelAtEndOfCycle: boolean;
|
||||
items: { handle: string | null }[];
|
||||
currentBillingCycle: { endTime: string } | null;
|
||||
pendingUpdate: { items: { handle: string | null }[] } | null;
|
||||
@ -59,7 +62,8 @@ export async function fetchActiveSubscription(shopGid: string, { fresh = false }
|
||||
const sub = {
|
||||
handles: handlesOf(active.items),
|
||||
pendingHandles,
|
||||
changesAt: pendingHandles.length ? active.currentBillingCycle?.endTime || null : null,
|
||||
changesAt: pendingHandles.length || active.cancelAtEndOfCycle ? active.currentBillingCycle?.endTime || null : null,
|
||||
cancelled: !!active.cancelAtEndOfCycle,
|
||||
};
|
||||
cache.set(shopGid, { at: Date.now(), sub });
|
||||
return sub;
|
||||
|
||||
@ -10,8 +10,9 @@ import { fetchActiveSubscription, partnerApiConfigured } from "../lib/partner.se
|
||||
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
const { admin, redirect, session } = await authenticate.admin(request);
|
||||
|
||||
// Shopify App Pricing: a shop without a plan (new install, or reinstall after the old one was cancelled)
|
||||
// goes to Shopify's plan selection page. A Partner API failure never redirects a paying merchant.
|
||||
// Shopify App Pricing: a shop without a plan (new install), or whose subscription was cancelled (uninstall,
|
||||
// then reinstall: Shopify keeps a prepaid period running with cancelAtEndOfCycle) goes to Shopify's plan
|
||||
// selection page to approve a plan again. A Partner API failure never redirects a paying merchant.
|
||||
if (partnerApiConfigured()) {
|
||||
// the welcome link adds plan_handle after a plan is approved: skip the cache so the new plan shows at once
|
||||
const fresh = new URL(request.url).searchParams.has("plan_handle");
|
||||
@ -23,7 +24,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
} catch (e) {
|
||||
console.error("subscription check failed", e);
|
||||
}
|
||||
if (sub === null) {
|
||||
if (sub === null || sub?.cancelled) {
|
||||
const store = session.shop.replace(".myshopify.com", "");
|
||||
const handle = process.env.SHOPIFY_APP_HANDLE || "offerlyst";
|
||||
throw redirect(`https://admin.shopify.com/store/${store}/charges/${handle}/pricing_plans`, { target: "_top" });
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user