Shopify App Pricing (managed recurring plans)
AppiFire uses Shopify App Pricing for recurring plans configured in the Partner Dashboard. One-time AI credit packs still use the Billing API (appPurchaseOneTimeCreate) — no Partner Dashboard packages are required for credit packs.
Related: Credits wallet · Billing & usage · Shopify billing & payments
Partner Dashboard plans
Section titled “Partner Dashboard plans”| Plan handle | Price | Internal shops.plan |
|---|---|---|
free | $0/mo | free |
appifire-ai-chat-plan-1 | $20/mo | paid |
Constants: SHOPIFY_PLAN_HANDLES in app/lib/plans.server.js. Mapping: mapPlanHandleToInternal(planHandle).
Do not create new recurring charges with appSubscriptionCreate after opting into Shopify App Pricing — Shopify blocks or duplicates billing.
Install and upgrade flows
Section titled “Install and upgrade flows”Install (choice B)
Section titled “Install (choice B)”- OAuth /
afterAuthcreates the shop withplan: "free". - No forced plan picker on install.
- Merchant can use the app on Free until they choose to upgrade.
Upgrade / downgrade (choice A)
Section titled “Upgrade / downgrade (choice A)”-
Upgrade and Change plan on Billing use
window.top.location.hrefto open Shopify’s plan picker (embedded apps cannot loadadmin.shopify.cominside the iframe — a plainredirect()inside the iframe shows “refused to connect”).https://admin.shopify.com/store/{storeHandle}/charges/{appHandle}/pricing_plansImplemented in
app/routes/app.billing.jsx(client-side top navigation, not iframeredirect). -
SHOPIFY_APP_HANDLE(defaultappifire-ai-chat) and store handle from*.myshopify.com— seeapp/lib/billing-plans.server.js. -
Downgrade: merchant picks Free on the same page (not a cancel-only path in the app UI).
After plan selection (redirect)
Section titled “After plan selection (redirect)”Shopify redirects back with query params (e.g. plan_handle=free or plan_handle=appifire-ai-chat-plan-1).
| Route | Role |
|---|---|
app/routes/app.billing.confirm.jsx | Sync plan when plan_handle or legacy charge_id is present → redirect /app/billing?plan_selected=1 |
app/routes/app.billing.jsx loader | Also reads plan_handle on load; runs resolveSubscriptionState + syncShopPlanFromSubscriptionState |
Configure welcome / redirect URLs per plan in Partner Dashboard to hit /app/billing/confirm or /app/billing.
Paid vs free detection (critical)
Section titled “Paid vs free detection (critical)”Wrong (pre-migration):
effectivePlan = activeSubscriptions.length > 0 ? "paid" : "free";With managed pricing, Free can appear as an active $0 subscription. That would incorrectly show buy-credits UI and skip free reply caps.
Correct: Paid only when subscription maps to appifire-ai-chat-plan-1 (or recurring line-item price > $0), via:
- URL
plan_handle(highest priority when present) - Partner API
activeSubscription(appId, shopId)whenSHOPIFY_PARTNER_*env vars are set (app/lib/partner-api.server.js) - Admin API
activeSubscriptions+classifyAdminSubscription()(app/lib/billing-plans.server.js)
Webhook app_subscriptions/update uses classifySubscriptionByNameAndPrice() so active $0 subs do not set plan = paid.
One-time credit packs (unchanged API)
Section titled “One-time credit packs (unchanged API)”| Step | Detail |
|---|---|
| Gate | merchantHasPaidPlan() — paid plan only, not “any active subscription” |
| Create | POST /app/billing/buy-credits → appPurchaseOneTimeCreate (dynamic $10–$200 from ADD_ON_CREDIT_AMOUNTS) |
| Confirm | /app/billing/buy-credits/confirm?charge_id=… |
| Balance | shops.credit_balance (USD decimal) |
Environment variables
Section titled “Environment variables”SHOPIFY_APP_HANDLE=appifire-ai-chat# Optional Partner API (recommended before legacy Billing API cutoff ~April 2026):SHOPIFY_PARTNER_ORG_ID=SHOPIFY_PARTNER_CLIENT_ID=SHOPIFY_PARTNER_CLIENT_SECRET=SHOPIFY_PARTNER_APP_ID=# Optional: gid://shopify/Shop/... if Partner shop lookup failsSHOPIFY_PARTNER_SHOP_GID=Also: PAID_PLAN_MONTHLY_PRICE, FREE_PLAN_REPLIES_CAP, PAID_PLAN_INCLUDED_CREDITS_USD — see .env.sample.
Files index
Section titled “Files index”| Concern | File |
|---|---|
| Plan handles | app/lib/plans.server.js |
| Plan picker URL, resolve/sync | app/lib/billing-plans.server.js |
| Partner API (optional) | app/lib/partner-api.server.js |
| Billing UI + loader | app/routes/app.billing.jsx |
| Post-picker confirm | app/routes/app.billing.confirm.jsx |
| Credit packs | app/routes/app.billing.buy-credits.jsx, app.billing.buy-credits.confirm.jsx |
| Subscription webhook | app/routes/webhooks.app_subscriptions.update.jsx |
Test checklist
Section titled “Test checklist”| Scenario | Expected |
|---|---|
| Fresh install | shops.plan = free, no charge |
| Upgrade | Plan picker → $20 plan → plan = paid, included credits granted |
| Downgrade | Plan picker → Free → plan = free, free reply cap |
| Free + $0 managed sub | Not paid; no buy-credits |
| Buy credits (paid) | One-time charge → balance increases |
| Buy credits (free) | 403 with clear message |
| Cancel / lapse | Leftover balance usable; suppress_included_credits_after_subscription_lapse |