desktop: Releases > Send for review (App Store submission state, builds by number, Submit / Resubmit / in-app purchases; Play versionCodes by number, Send to production, managed publishing, console plans for countries and Send changes for review), plan panel before every owner dialog with streamed progress; App info console steps for every console plan form with verify; Catalog price-change owner actions and the declined state; Data safety deletion answer; TestFlight create missing groups; Android SDK in Dependencies; upload progress in job toasts; boot check send-for-review cases on D1 and D3 state
This commit is contained in:
parent
33eb24a3dc
commit
2b101f0fd8
22 changed files with 1527 additions and 57 deletions
|
|
@ -7,6 +7,7 @@ import { type Account, type AccountCheck, type ConsoleFillResult, type ConsoleFo
|
|||
type PortfolioResult, type Project, type ProjectSummary, type Settings, type StoreApp, type StoreRelease, type Studio, type ToolResult } from '../api';
|
||||
import { buildApps, type Overrides } from '../lib/identity';
|
||||
import type { CatalogDiff, HealthSnapshot, MetricRow, Review, TestFlightStatus, ValidateResult, ScanResult } from '../lib/model';
|
||||
import { APPLE_STATUS_READY, APPLE_STATUS_UNRESOLVED, consolePlan, consoleVerify, ownerDry, PLAY_STATUS } from './submit-fixtures';
|
||||
import { ANALYTICS, ASO, DECLARATIONS, LISTING_TEXT, PPO, PPO_STORE, PRICE_DIFF, PRICES, renderArt, RENDERS } from './fixtures-more';
|
||||
|
||||
export const FIXTURE_MARKER = 'storebound-dev-fixtures';
|
||||
|
|
@ -113,6 +114,7 @@ const SUMMARY: Record<string, ProjectSummary> = {
|
|||
products: 4, productIds: ['coins_small', 'coins_large', 'no_ads', 'starter_pack'], subscriptions: 1, subscriptionIds: ['vip_pass_monthly'],
|
||||
catalog: { ios: ['coins_small', 'coins_large', 'no_ads', 'starter_pack', 'vip_pass_monthly'], android: ['coins_small', 'coins_large', 'no_ads', 'starter_pack', 'vip_pass_monthly'] },
|
||||
shots: true, testflightGroups: 3, profile: 'game', icon: `${HOME}/apps/prism-puddle/store/icons/app-store-1024.png`,
|
||||
playRelease: { managedPublishing: null, countries: null, category: 'Puzzle' },
|
||||
},
|
||||
'p-tidy': {
|
||||
dir: `${HOME}/apps/tidy-lists`, config: `${HOME}/apps/tidy-lists/storebound.config.ts`, name: 'Tidy Lists', languages: ['en', 'tr'],
|
||||
|
|
@ -120,6 +122,7 @@ const SUMMARY: Record<string, ProjectSummary> = {
|
|||
products: 1, productIds: ['tip_jar'], subscriptions: 2, subscriptionIds: ['pro_monthly', 'pro_yearly'],
|
||||
catalog: { ios: ['tip_jar', 'pro_monthly', 'pro_yearly'], android: ['tip_jar', 'pro_monthly', 'pro_yearly'] },
|
||||
shots: true, testflightGroups: 2, profile: 'app', icon: `${HOME}/apps/tidy-lists/store/icons/app-store-1024.png`,
|
||||
playRelease: { managedPublishing: true, countries: 12, category: 'Productivity' },
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -440,7 +443,13 @@ function read(method: string, id: string, args: Record<string, unknown> = {}): T
|
|||
case 'analytics.compare': return ANALYTICS[id] ? ok(method, ANALYTICS[id]) : { method, ok: false, exitCode: 1, dryRun: false, command: 'storebound analytics compare --json', output: `error: no snapshots in ${HOME}/apps/tidy-lists/.storebound/analytics (storebound analytics snapshot)` };
|
||||
case 'aso.audit': return ASO[id] ? ok(method, ASO[id]) : { method, ok: false, exitCode: 1, dryRun: false, command: 'storebound aso audit --json', output: 'fixture: no data for aso.audit' };
|
||||
case 'check': return { method, ok: true, exitCode: 0, dryRun: false, command: 'storebound check', output: '12:00:01 [check] config ok: storebound.config.ts\n12:00:01 [check] listing: 4 languages\n12:00:01 [check] 0 problems, 2 warnings' };
|
||||
default: return { method, ok: false, exitCode: 1, dryRun: false, command: `storebound ${method}`, output: `fixture: no data for ${method}` };
|
||||
// send for review: Prism Puddle is ready to submit, Tidy Lists is back from App Review (UNRESOLVED_ISSUES)
|
||||
case 'apple.status': return ok(method, id === 'p-tidy' ? APPLE_STATUS_UNRESOLVED : APPLE_STATUS_READY);
|
||||
case 'play.status': return ok(method, PLAY_STATUS);
|
||||
case 'console.plan': return ok(method, consolePlan(String(args.form)));
|
||||
case 'console.verify': return ok(method, consoleVerify(String(args.form)));
|
||||
default:
|
||||
if (method.startsWith('owner.')) return ownerDry(method, args); return { method, ok: false, exitCode: 1, dryRun: false, command: `storebound ${method}`, output: `fixture: no data for ${method}` };
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -466,7 +475,7 @@ const CONSOLE_FILL: Record<ConsoleForm, ConsoleFillResult> = {
|
|||
};
|
||||
|
||||
const CACHED_AGO: Record<string, number> = { health: 60 * 3, 'reviews.list': 45, 'apple.testflight_status': 60 * 5, 'catalog.diff': 60 * 26, validate: 60 * 2, 'prices.current': 60 * 4, 'apple.ppo_status': 60 * 6 };
|
||||
const NETWORK = new Set(['health', 'reviews.list', 'apple.testflight_status', 'catalog.diff', 'validate', 'prices.current', 'prices.diff', 'apple.ppo_status']);
|
||||
const NETWORK = new Set(['health', 'reviews.list', 'apple.testflight_status', 'catalog.diff', 'validate', 'prices.current', 'prices.diff', 'apple.ppo_status', 'apple.status', 'play.status', 'console.verify']);
|
||||
|
||||
async function progressLines(job: string | null, lines: string[]) {
|
||||
for (const line of lines) { await sleep(260); if (job) emit('sidecar-progress', { params: { requestId: 1, line, stream: 'stderr' }, job }); }
|
||||
|
|
@ -535,7 +544,7 @@ async function answer(cmd: string, a: Record<string, unknown>): Promise<unknown>
|
|||
case 'cache_clear': return null;
|
||||
case 'sidecar_call': {
|
||||
const method = a.method as string;
|
||||
if (method === 'system.tools') return { tools: (await answer('sidecar_status', {}) as { tools: { tools: unknown[] } }).tools.tools };
|
||||
if (method === 'system.tools') return { tools: (await answer('sidecar_status', {}) as { tools: { tools: unknown[] } }).tools.tools, androidSdk: { path: `${HOME}/Library/Android/sdk`, source: 'default', checked: ['ANDROID_HOME: not set', 'ANDROID_SDK_ROOT: not set', `Android Studio default folder: ${HOME}/Library/Android/sdk`] } };
|
||||
const id = a.projectId as string;
|
||||
if (NETWORK.has(method)) await progressLines(job, [`[${method}] reading App Store Connect`, `[${method}] reading Google Play`, `[${method}] done`]);
|
||||
else await sleep(150);
|
||||
|
|
@ -554,7 +563,9 @@ async function answer(cmd: string, a: Record<string, unknown>): Promise<unknown>
|
|||
const res: ToolResult = { method, ok: true, exitCode: 0, dryRun: false, command: `storebound console fill ${String(args.form)} --json`, output: '[console] browser closed', data: { ...r, filled: r.answers.length - r.uncertain, highlighted: r.uncertain, fallback: false, stoppedAt: null } };
|
||||
return { confirmed: true, stage: 'done', dryRun: { ...res, dryRun: true, data: r }, result: res };
|
||||
}
|
||||
await progressLines(job, [`[${method}] dry run`, `[${method}] confirmed in the dialog`, `[${method}] applying`]);
|
||||
await progressLines(job, method === 'owner.apple_submit' || method === 'owner.apple_resubmit'
|
||||
? [`[${method}] dry run`, `[${method}] confirmed in the dialog`, '[submit] build attached', '[submit] review submission rs-new-1: not ready to be submitted yet, no item left REJECTED (try 1, next in 15s)', '[submit] accepted on try 2']
|
||||
: [`[${method}] dry run`, `[${method}] confirmed in the dialog`, `[${method}] applying`]);
|
||||
if (job && cancelled.has(job)) throw { code: -32800, message: 'Request cancelled' };
|
||||
const d = DRAFTS[a.projectId as string]?.drafts.find(x => x.file.endsWith(`/${String(args.file)}`));
|
||||
if (method === 'reviews.approve' && d) d.approved = !args.undo;
|
||||
|
|
|
|||
105
apps/desktop/src/dev/submit-fixtures.ts
Normal file
105
apps/desktop/src/dev/submit-fixtures.ts
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
// Made-up store answers for the send-for-review cards (Releases tab), in the shapes `apple status --json` and
|
||||
// `play status --json` print: one App Store app ready to submit (an editable version, VALID and processing builds, a
|
||||
// harmless empty draft) and one back from App Review (UNRESOLVED_ISSUES with a REJECTED version item), plus Play
|
||||
// tracks. Used by the dev fixture mode (fixtures.ts), tests/submit.test.ts and the boot check's live case
|
||||
// (tools/desktop-boot-check.ts). Placeholder ids only; nothing here is real store data.
|
||||
|
||||
const version = (id: string, versionString: string, state: string, build: string | null) =>
|
||||
({ id, type: 'appStoreVersions', attributes: { versionString, appVersionState: state, platform: 'IOS' }, relationships: { build: { data: build ? { type: 'builds', id: build } : null } } });
|
||||
const build = (id: string, number: string, state: string, uploaded: string, pre: string) =>
|
||||
({ id, type: 'builds', attributes: { version: number, processingState: state, uploadedDate: uploaded }, relationships: { preReleaseVersion: { data: { type: 'preReleaseVersions', id: pre } } } });
|
||||
|
||||
const BUILDS = [build('b-53', '53', 'PROCESSING', '2026-10-12T08:10:00Z', 'pre-240'), build('b-52', '52', 'VALID', '2026-10-11T16:40:00Z', 'pre-240'), build('b-48', '48', 'VALID', '2026-09-30T09:00:00Z', 'pre-230')];
|
||||
|
||||
export const APPLE_STATUS_READY = {
|
||||
app: { id: '1000000001', attributes: { name: 'Prism Puddle', bundleId: 'com.example.prismpuddle' } },
|
||||
versions: [version('ver-240', '2.4.0', 'PREPARE_FOR_SUBMISSION', null), version('ver-230', '2.3.0', 'READY_FOR_SALE', 'b-48')],
|
||||
builds: BUILDS, attachedBuilds: [], buildVersions: { 'b-53': '2.4.0', 'b-52': '2.4.0', 'b-48': '2.3.0' },
|
||||
iaps: [], phased: {},
|
||||
subs: [{ id: 'rs-empty-1', attributes: { state: 'READY_FOR_REVIEW', submittedDate: null } }, { id: 'rs-old-1', attributes: { state: 'COMPLETE', submittedDate: '2026-09-30T12:00:00Z' } }],
|
||||
unresolved: [], emptyDrafts: ['rs-empty-1'],
|
||||
};
|
||||
|
||||
export const APPLE_STATUS_UNRESOLVED = {
|
||||
...APPLE_STATUS_READY,
|
||||
versions: [version('ver-240', '2.4.0', 'REJECTED', 'b-52'), version('ver-230', '2.3.0', 'READY_FOR_SALE', 'b-48')],
|
||||
subs: [{ id: 'rs-back-1', attributes: { state: 'UNRESOLVED_ISSUES', submittedDate: '2026-10-11T18:00:00Z' } }],
|
||||
unresolved: [{ id: 'rs-back-1', items: [
|
||||
{ id: 'it-ver-1', kind: 'appStoreVersion', versionId: 'ver-240', state: 'REJECTED' },
|
||||
{ id: 'it-coins-1', kind: 'inAppPurchaseVersion', versionId: 'iapv-coins', state: 'READY_FOR_REVIEW' },
|
||||
] }],
|
||||
emptyDrafts: [],
|
||||
};
|
||||
|
||||
const res = (method: string, command: string, output: string, data?: unknown, ok = true) =>
|
||||
({ method, ok, exitCode: ok ? 0 : 1, dryRun: true, command, output, ...(data === undefined ? {} : { data }) });
|
||||
|
||||
/** Dry runs of the owner actions, with the plan tables the CLI prints (cli/lib/review-submit.ts formatRows). */
|
||||
export function ownerDry(method: string, args: Record<string, unknown>) {
|
||||
const b = String(args.build ?? '');
|
||||
if (method === 'owner.apple_submit' || method === 'owner.apple_resubmit') {
|
||||
const resub = method === 'owner.apple_resubmit';
|
||||
const table = resub ? [
|
||||
'STEP WHAT NOW ACTION',
|
||||
`build build ${b} attached: 52 attach build ${b} instead of 52`,
|
||||
'submission rs-back-1 UNRESOLVED_ISSUES reuse (resubmit, no new draft)',
|
||||
'item app version 2.4.0 REJECTED in it already; mark resolved',
|
||||
'item coins_small READY_FOR_REVIEW in it already',
|
||||
'submit PATCH submitted: true - owner step (--yes); retried while Apple says "not ready yet" and no item is left REJECTED',
|
||||
] : [
|
||||
'STEP WHAT NOW ACTION',
|
||||
`build build ${b} none attached attach build ${b}`,
|
||||
'submission new draft - create (no open submission)',
|
||||
'item app version 2.4.0 - add',
|
||||
'item coins_small (consumable, version 1) - add',
|
||||
'submit PATCH submitted: true - owner step (--yes); retried while Apple says "not ready yet" and no item is left REJECTED',
|
||||
];
|
||||
const tag = resub ? 'resubmit' : 'submit';
|
||||
return res(method, `storebound apple ${tag} --build ${b} --dry --json`, [...table, `12:00:02 [${tag}] dry run: nothing sent`].join('\n'),
|
||||
{ dryRun: true, version: { versionString: '2.4.0', state: resub ? 'REJECTED' : 'PREPARE_FOR_SUBMISSION' }, build: { action: 'attach', number: b }, blockers: [] });
|
||||
}
|
||||
if (method === 'owner.play_production') {
|
||||
return res(method, `storebound play release --version-code ${String(args.version_code)} --production ${String(args.status ?? 'completed')} --dry`,
|
||||
[`12:00:01 [play] production: versionCode ${String(args.version_code)} as ${String(args.status ?? 'completed')}`, '12:00:01 [play] release notes: en-US, tr-TR', '12:00:01 [play] WARN managed publishing is off by default (play.managedPublishing not set): the release goes live automatically once Google approves it', '12:00:01 [play] dry run: edit not committed'].join('\n'));
|
||||
}
|
||||
if (method.startsWith('owner.prices_') && method !== 'owner.prices_import') {
|
||||
return res(method, `storebound ${method.replace('owner.prices_', '').replace('_', ' ')} --dry`, ['ID REGION NOW WANT ACTION', 'coins_small US USD 0.99 USD 1.49 owner (price change: needs --allow-price-changes)', 'coins_small DE EUR 0.99 EUR 1.49 owner (price change: needs --allow-price-changes)', '12:00:01 [play] dry run: nothing written'].join('\n'));
|
||||
}
|
||||
return res(method, `storebound ${method.replace('owner.', '').replace('_', ' ')} --dry`, `12:00:01 [${method}] dry run (fixture)`);
|
||||
}
|
||||
|
||||
/** `console plan <form> --json`, shortened (made up from the demo configs). */
|
||||
export function consolePlan(form: string) {
|
||||
const play = form.startsWith('play');
|
||||
return {
|
||||
form, store: play ? 'play' : 'apple', title: form === 'play-production-release' ? 'Play Console: send the first production release for review' : `Console plan: ${form}`,
|
||||
start: { url: play ? 'https://play.google.com/console' : 'https://appstoreconnect.apple.com/apps', deepLink: false, reach: play ? ['the app in the app list', 'Test and release > Production'] : ['the app', 'General > App Information'] },
|
||||
steps: [
|
||||
{ n: 1, text: 'Open the page above (sign in yourself the first time).' },
|
||||
{ n: 2, text: 'Press Edit release, then Next: the review page lists errors and warnings.', click: ['Edit release', 'Next'] },
|
||||
{ n: 3, text: 'Press Save (nothing is sent yet).', click: ['Save'] },
|
||||
{ n: 4, text: 'Stop. The owner presses "Send changes for review" in Publishing overview.' },
|
||||
],
|
||||
answers: [
|
||||
{ id: 'managedPublishing', question: 'Managed publishing on?', text: 'No', status: 'owner-check', note: 'Managed publishing is off by default: the release goes live once Google approves it.' },
|
||||
{ id: 'countries', question: 'Countries / regions', text: 'All countries / regions', status: 'certain' },
|
||||
],
|
||||
askOwner: ['Managed publishing on?: No (owner check)'],
|
||||
buttons: { neverPress: ['Send changes for review', 'Publish', 'Submit', 'Create', 'Start rollout to Production'], final: 'Send changes for review' },
|
||||
};
|
||||
}
|
||||
|
||||
export function consoleVerify(form: string) {
|
||||
return { form, source: 'page-text', rows: [{ item: 'Production release', expected: '2.4.0 (24)', seen: '2.4.0 (24)', status: 'match' }, { item: 'Countries', expected: 'all', seen: '177 countries / regions', status: 'match' }], mismatches: 0, matched: 2, ok: true, notes: [] };
|
||||
}
|
||||
|
||||
export const PLAY_STATUS = {
|
||||
details: { defaultLanguage: 'en-US' },
|
||||
category: { config: 'Puzzle', source: 'play.category', api: false, plan: 'storebound console plan play-store-settings' },
|
||||
tracks: [
|
||||
{ track: 'production', releases: [{ name: '2.3.0 (23)', versionCodes: ['23'], status: 'completed' }] },
|
||||
{ track: 'beta', releases: [{ name: '2.4.0 (24)', versionCodes: ['24'], status: 'completed' }] },
|
||||
{ track: 'internal', releases: [{ name: '2.4.0 (24)', versionCodes: ['24'], status: 'completed' }, { name: '2.3.1 (22)', versionCodes: ['22'], status: 'completed' }] },
|
||||
],
|
||||
listings: [], products: [],
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue