- 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
61 lines
4.5 KiB
TypeScript
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');
|
|
}
|