storebound/cli/apple/preorder.ts
CRT b63f2fdfbc Store operations: TestFlight wait / expire, IAP review submissions, listing pulls, release notes per language, offers, custom product pages, in-app events, reports, triage, keyword ranks, AdAttributionKit scan
- 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
2026-10-11 23:58:17 +08:00

61 lines
4.5 KiB
TypeScript

// App Store pre-orders through the app availability API.
// storebound apple preorder status [--json]
// storebound apple preorder enable --release-date YYYY-MM-DD [--territories USA,DEU] [--dry] [--yes] [--json]
// storebound apple preorder end [--territories USA,DEU] [--dry] [--yes] [--json]
// status: per territory available, release date, pre-order on, publish date, content statuses. enable puts the
// territories (default: all the app is available in and not yet released) on pre-order with that release date; end
// stops the pre-order and releases the app there at once. Both are public, so they show the dry run and ask first;
// neither is an MCP tool. Apple requires an approved version first; the release date is 2-180 days ahead for a new
// app (up to 365 days for an existing app in a new region). Google Play pre-registration has no API: Play Console >
// Grow users > Pre-registration (see the app-store-promotion skill). Pure parts and sources: cli/lib/offers.ts.
import type { Ctx } from '../lib/ctx';
import { confirm, need } from '../lib/ctx';
import { done, printJson, warn } from '../lib/log';
import { endPreOrderReq, planPreOrder, territoryRows } from '../lib/offers';
import { textTable } from '../lib/table';
import { ascClient, type J } from './asc';
import { describeReq, execute } from './jsonapi';
const TAG = 'preorder';
const USAGE = 'usage: storebound apple preorder status | enable --release-date YYYY-MM-DD [--territories USA,DEU] | end [--territories USA]';
export async function run(ctx: Ctx): Promise<void> {
const { config, root } = ctx.loaded;
const a = ctx.args;
const sub = a._[0];
if (!sub || !['status', 'enable', 'end'].includes(sub)) throw new Error(USAGE);
const dry = a.has('dry');
const c = await ascClient(root, need(config.apple?.bundleId, 'apple.bundleId'), config.apple?.appId);
const appId = await c.app();
const avail = (await c.maybe(`/v1/apps/${appId}/appAvailabilityV2`))?.data;
if (!avail) throw new Error('the app has no availability yet: set territories first (storebound apple appinfo, or Pricing and Availability in App Store Connect)');
const pagesOut: J[] = [];
for (let url: string | null = `/v2/appAvailabilities/${avail.id}/territoryAvailabilities?include=territory&limit=200`; url;) { const p: J = await c.asc('GET', url); pagesOut.push(p); url = p.links?.next ?? null; }
const rows = territoryRows(pagesOut);
const territories = a.list('territories').length ? a.list('territories') : null;
if (sub === 'status') {
const pre = rows.filter(r => r.preOrderEnabled);
if (a.has('json')) { printJson({ availableInNewTerritories: avail.attributes?.availableInNewTerritories ?? null, territories: rows }); return; }
console.log(textTable(['territory', 'available', 'release date', 'pre-order', 'pre-order published', 'status'],
rows.filter(r => !territories || territories.includes(r.territory)).map(r => [r.territory, r.available ? 'yes' : 'no', r.releaseDate ?? '', r.preOrderEnabled ? 'on' : '', r.preOrderPublishDate ?? '', (r.contentStatuses ?? []).join(' ')])));
done(TAG, `${rows.length} territories, ${pre.length} on pre-order`);
return;
}
const plan = sub === 'enable'
? planPreOrder(rows, need(a.get('release-date'), '--release-date YYYY-MM-DD'), territories)
: { reqs: [endPreOrderReq(rows, territories)].filter(r => r !== null), problems: [] as string[], warnings: [] as string[] };
for (const w of plan.warnings) warn(TAG, w);
if (plan.problems.length) throw new Error(plan.problems.join('; '));
if (a.has('json') && dry) { printJson({ dryRun: true, requests: plan.reqs.map(r => describeReq(r)), warnings: plan.warnings }); return; }
if (!plan.reqs.length) { done(TAG, sub === 'enable' ? 'pre-order already set up like that' : 'no territory is on pre-order'); return; }
if (dry) { await execute(c.asc, plan.reqs, { dry, tag: TAG }); done(TAG, 'dry run: nothing changed'); return; }
const q = sub === 'enable'
? `Put the app on pre-order in ${plan.reqs.length} territor${plan.reqs.length === 1 ? 'y' : 'ies'} with release date ${a.get('release-date')}? The pre-order page goes public once you release an approved version.`
: `End the pre-order and release the app now (${plan.reqs[0]!.note})?`;
if (!(await confirm(ctx, q))) return;
await execute(c.asc, plan.reqs, { dry: false, tag: TAG });
if (a.has('json')) printJson({ dryRun: false, requests: plan.reqs.map(r => describeReq(r)) });
done(TAG, sub === 'enable' ? 'pre-order set (storebound apple preorder status)' : 'pre-order ended; the app is released in those territories');
}