- apple testflight wait --build N (progress, timeout, exit 0/1/7) and expire --keep N | --older-than D (dry run, asks) - apple iap-submit: IAP / subscription / group versions into a review submission; first of a type only with the app version (--with-version), --no-submit keeps the draft; read-only MCP plan - apple listing pull, play listing pull (diff, --write), catalog pull (config proposal, never rewritten) - whats-new --languages / check / apply: one file per language, agent or hook translation, per-store limits - apple offer-codes, win-back, promoted, preorder; Play pre-registration as a guide (no API) - apple cpp and apple events: list, create, upload, submit (asks) - reports: Apple sales / finance, Play earnings / sales into .storebound/reports with summary and CSV - triage: TestFlight crash / feedback and Play error reports grouped by signature, device, OS - aso rank: iTunes Search API ranks and competitors, rate limited - ads scan: AdAttributionKit / SKAdNetwork Info.plist and source checks - tests against recorded, sanitized answers; skills, README, CHANGELOG
62 lines
4.8 KiB
TypeScript
62 lines
4.8 KiB
TypeScript
// 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<void> {
|
|
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<string, { apiId: string; kind: 'iap' | 'sub' }>();
|
|
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)'}`);
|
|
}
|