// Promoted in-app purchases on the App Store product page, from apple.promoted. // storebound apple promoted [--enable] [--dry] [--yes] [--json] // Status (what is promoted, enabled, its review state), then: creates the missing promoted purchases disabled, // updates visibleForAllUsers, sets the display order. --enable also switches them on, which shows them on the // product page once approved: that asks first and is not an MCP tool. Each promoted purchase needs its promotional // image (App Store Connect) and the app must handle purchases started on the App Store (StoreKit PurchaseIntent). // At most 20 at a time. Pure parts and sources: cli/lib/offers.ts. import type { Ctx } from '../lib/ctx'; import { confirm, need } from '../lib/ctx'; import { done, log, printJson, warn } from '../lib/log'; import { planPromoted, type PromotedNow, promotedOrderReq } from '../lib/offers'; import { textTable } from '../lib/table'; import { ascClient, type J } from './asc'; import { describeReq, execute } from './jsonapi'; const TAG = 'promoted'; export async function run(ctx: Ctx): Promise { const { config, root } = ctx.loaded; const a = ctx.args; const dry = a.has('dry'); const cfg = config.apple?.promoted; const c = await ascClient(root, need(config.apple?.bundleId, 'apple.bundleId'), config.apple?.appId); const appId = await c.app(); const products = new Map(); for (const p of await c.all(`/v1/apps/${appId}/inAppPurchasesV2?limit=200`)) products.set(p.attributes.productId, { apiId: p.id, kind: 'iap' }); for (const g of await c.all(`/v1/apps/${appId}/subscriptionGroups?limit=200`)) for (const s of await c.all(`/v1/subscriptionGroups/${g.id}/subscriptions?limit=200`)) products.set(s.attributes.productId, { apiId: s.id, kind: 'sub' }); const byApiId = new Map([...products.entries()].map(([pid, v]) => [v.apiId, pid] as const)); const raw = await c.asc('GET', `/v1/apps/${appId}/promotedPurchases?include=inAppPurchaseV2,subscription&limit=50`); const now: PromotedNow[] = (raw.data ?? []).map((p: J) => ({ id: p.id, enabled: !!p.attributes.enabled, visibleForAllUsers: !!p.attributes.visibleForAllUsers, state: p.attributes.state, productId: byApiId.get(p.relationships?.inAppPurchaseV2?.data?.id ?? p.relationships?.subscription?.data?.id ?? '') ?? '?' })); const status = now.map((p, i) => [i + 1, p.productId, p.enabled ? 'enabled' : 'off', p.visibleForAllUsers ? 'everyone' : 'app decides', p.state ?? '']); if (!cfg) { if (a.has('json')) printJson({ promoted: now }); else console.log(now.length ? textTable(['#', 'product', 'enabled', 'visible to', 'state'], status) : 'no promoted in-app purchases'); done(TAG, 'apple.promoted is not set: status only'); return; } const plan = planPromoted(appId, cfg, now, products, { enable: a.has('enable') }); const order = promotedOrderReq(appId, cfg, now); for (const u of plan.unknown) warn(TAG, `${u}: not on App Store Connect (storebound apple iap / apple subscriptions)`); if (plan.extra.length) log(TAG, `promoted on the store but not in apple.promoted (left as they are): ${plan.extra.join(', ')}`); if (now.length + plan.create.length > 20) throw new Error(`${now.length + plan.create.length} promoted purchases: at most 20`); const reqs = [...plan.create, ...plan.update]; if (a.has('json')) printJson({ dryRun: dry, promoted: now, requests: [...reqs, ...(order ? [order] : [])].map(r => describeReq(r)), unknown: plan.unknown, extra: plan.extra }); else if (now.length) console.log(textTable(['#', 'product', 'enabled', 'visible to', 'state'], status)); if (!reqs.length && !order) { done(TAG, 'promoted purchases up to date'); return; } if (dry) { await execute(c.asc, [...reqs, ...(order ? [order] : [])], { dry, tag: TAG }); done(TAG, 'dry run: nothing changed'); return; } const enabling = reqs.some(r => JSON.stringify(r.body).includes('"enabled":true')); if (enabling && !(await confirm(ctx, 'Switch promoted purchases on? Once approved they show on the App Store product page.'))) return; await execute(c.asc, reqs, { dry: false, tag: TAG }); // the order covers the purchases that exist now (new ones included) if (plan.create.length || order) { const fresh = await c.asc('GET', `/v1/apps/${appId}/promotedPurchases?include=inAppPurchaseV2,subscription&limit=50`); const after: PromotedNow[] = (fresh.data ?? []).map((p: J) => ({ id: p.id, enabled: !!p.attributes.enabled, visibleForAllUsers: !!p.attributes.visibleForAllUsers, productId: byApiId.get(p.relationships?.inAppPurchaseV2?.data?.id ?? p.relationships?.subscription?.data?.id ?? '') ?? '?' })); const o = promotedOrderReq(appId, cfg, after); if (o) await execute(c.asc, [o], { dry: false, tag: TAG }); } done(TAG, `promoted purchases updated${a.has('enable') ? '' : ' (not switched on: --enable when the images are approved)'}`); }