apple iap-submit: DEVELOPER_REJECTED IAP / subscription / group versions (left by a canceled submission) are submitted as they are, plan says 'add (developer rejected, re-submitted as is)', --new-version only for APPROVED / REJECTED, empty draft reused; fake ASC models cancel and re-add. play build / check: warn before Gradle when defaultConfig does not read -PversionCode / -PversionName, with the exact Groovy / Kotlin line (built with AGP 8.13); android.injected.version.code does not override bundleRelease; post-build versionCode mismatch is a hard stop. play-production-release plan: 'Do you want to restart your review?' dialog, Restart review is never-press. Pitfalls 31-33
This commit is contained in:
parent
41d2ffcc24
commit
0b1451800c
18 changed files with 399 additions and 26 deletions
|
|
@ -7,7 +7,7 @@
|
|||
// (PATCH reviewSubmissionItems resolved: true), and an empty draft cannot be canceled ("not in cancellable state").
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from 'bun:test';
|
||||
import type { ShipConfig } from '../cli/lib/config';
|
||||
import { chooseSubmission, formatRows, type OpenSub } from '../cli/lib/review-submit';
|
||||
import { chooseSubmission, formatRows, type OpenSub, planIapSubmission, resubmitRows, type SubmitState } from '../cli/lib/review-submit';
|
||||
import { isConflict, upsertTiming } from '../cli/lib/upsert';
|
||||
import { notReadyYet, otherSubmissionOf, submitTiming } from '../cli/apple/submission';
|
||||
import { run as appleIapSubmit } from '../cli/apple/iap-submit';
|
||||
|
|
@ -248,6 +248,77 @@ describe('apple iap-submit with a submission back from App Review', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// Seen live (2026-10-12): the owner canceled a WAITING_FOR_REVIEW submission; the app version went DEVELOPER_REJECTED,
|
||||
// then PREPARE_FOR_SUBMISSION, every in-app purchase version stayed DEVELOPER_REJECTED. iap-submit skipped them all
|
||||
// ("change it in App Store Connect (or --new-version)"), but Apple takes the same versions again as items (201
|
||||
// READY_FOR_REVIEW) and the submit (200 WAITING_FOR_REVIEW); an empty draft that was open next to it is reused.
|
||||
describe('after the owner canceled a submission (DEVELOPER_REJECTED versions)', () => {
|
||||
/** rs-sub WAITING_FOR_REVIEW with the version and both IAPs, an empty draft next to it, then canceled in the fake. */
|
||||
function canceled(): void {
|
||||
s.asc.put({ type: 'appStoreVersions', id: 'ver-1', attributes: { versionString: '1.0.0', appVersionState: 'WAITING_FOR_REVIEW', platform: 'IOS' }, rels: { app: APP } });
|
||||
for (const [k, t] of [['gems', 'CONSUMABLE'], ['pro', 'NON_CONSUMABLE']] as const) {
|
||||
s.asc.put({ type: 'inAppPurchases', id: `iap-${k}`, attributes: { productId: `com.example.notes.${k}`, inAppPurchaseType: t, state: 'WAITING_FOR_REVIEW' }, rels: { app: APP } });
|
||||
s.asc.put({ type: 'inAppPurchaseVersions', id: `iapv-${k}`, attributes: { version: 1, state: 'WAITING_FOR_REVIEW' }, rels: { inAppPurchase: `iap-${k}` } });
|
||||
}
|
||||
s.asc.put({ type: 'reviewSubmissions', id: 'rs-sub', attributes: { platform: 'IOS', state: 'WAITING_FOR_REVIEW' }, rels: { app: APP } });
|
||||
s.asc.put({ type: 'reviewSubmissionItems', id: 'it-ver', attributes: { state: 'READY_FOR_REVIEW' }, rels: { reviewSubmission: 'rs-sub', appStoreVersion: 'ver-1' } });
|
||||
for (const k of ['gems', 'pro']) s.asc.put({ type: 'reviewSubmissionItems', id: `it-${k}`, attributes: { state: 'READY_FOR_REVIEW' }, rels: { reviewSubmission: 'rs-sub', inAppPurchaseVersion: `iapv-${k}` } });
|
||||
s.asc.put({ type: 'reviewSubmissions', id: 'rs-empty', attributes: { platform: 'IOS', state: 'READY_FOR_REVIEW' }, rels: { app: APP } });
|
||||
expect(s.asc.handle('PATCH', '/v1/reviewSubmissions/rs-sub', { data: { attributes: { canceled: true } } }).status).toBe(200);
|
||||
}
|
||||
|
||||
test('the fake: a canceled submission leaves DEVELOPER_REJECTED versions, which are accepted again as items; APPROVED ones are not', () => {
|
||||
canceled();
|
||||
expect(s.asc.res.get('iapv-gems')!.attributes.state).toBe('DEVELOPER_REJECTED');
|
||||
expect(s.asc.res.get('ver-1')!.attributes.appVersionState).toBe('PREPARE_FOR_SUBMISSION');
|
||||
const add = (vid: string): Response => s.asc.handle('POST', '/v1/reviewSubmissionItems', { data: { type: 'reviewSubmissionItems',
|
||||
relationships: { reviewSubmission: { data: { type: 'reviewSubmissions', id: 'rs-empty' } }, inAppPurchaseVersion: { data: { type: 'inAppPurchaseVersions', id: vid } } } } });
|
||||
expect(add('iapv-gems').status).toBe(201);
|
||||
s.asc.res.get('iapv-pro')!.attributes.state = 'APPROVED';
|
||||
expect(add('iapv-pro').status).toBe(409);
|
||||
});
|
||||
|
||||
test('plan: DEVELOPER_REJECTED IAP, subscription and group versions go in as they are; --new-version stays for APPROVED / REJECTED', () => {
|
||||
const st: SubmitState = {
|
||||
appVersions: [{ id: 'v-1', versionString: '1.0.0', state: 'DEVELOPER_REJECTED' }],
|
||||
iaps: [{ id: 'i-1', productId: 'com.example.game.gems', type: 'CONSUMABLE', state: 'READY_TO_SUBMIT', versions: [{ id: 'iv-1', version: 1, state: 'DEVELOPER_REJECTED' }] },
|
||||
{ id: 'i-2', productId: 'com.example.game.old', type: 'CONSUMABLE', state: 'APPROVED', versions: [{ id: 'iv-2', version: 1, state: 'APPROVED' }] }],
|
||||
subs: [{ id: 's-1', productId: 'com.example.game.plus', groupId: 'g-1', state: 'READY_TO_SUBMIT', versions: [{ id: 'sv-1', version: 1, state: 'DEVELOPER_REJECTED' }] }],
|
||||
groups: [{ id: 'g-1', name: 'Plus', versions: [{ id: 'gv-1', version: 1, state: 'DEVELOPER_REJECTED' }] }],
|
||||
draft: { id: 'rs-empty', itemVersionIds: [], state: 'READY_FOR_REVIEW' },
|
||||
};
|
||||
const plan = planIapSubmission(st, ['com.example.game.gems', 'com.example.game.plus', 'com.example.game.old'], { withVersion: true });
|
||||
expect(plan.blockers).toEqual([]);
|
||||
expect(plan.items.map(i => [i.kind, i.versionId, !!i.devRejected])).toEqual([['appStoreVersion', 'v-1', true], ['inAppPurchaseVersion', 'iv-1', true],
|
||||
['subscriptionVersion', 'sv-1', true], ['subscriptionGroupVersion', 'gv-1', true]]);
|
||||
expect(plan.skipped).toEqual([{ id: 'com.example.game.old', why: 'latest version is APPROVED: add --new-version to send a new version' }]);
|
||||
expect(plan.prepare.map(r => r.path)).not.toContain('/v1/reviewSubmissions');
|
||||
const rows = resubmitRows(st, plan, { number: '8', id: 'b-8', current: 'b-8', action: 'same', reason: 'attached already' });
|
||||
expect(rows.filter(r => r.step === 'item').map(r => r.action)).toEqual(Array(4).fill('add (developer rejected, re-submitted as is)'));
|
||||
});
|
||||
|
||||
test('iap-submit --with-version: the dry plan adds them as they are; a real run fills the empty draft and submits it, no new version, no new draft', async () => {
|
||||
canceled();
|
||||
const p = project(CONFIG);
|
||||
const a = s.calls.length;
|
||||
const dry = await capture(() => appleIapSubmit(p.ctx(['--with-version', '--new-version', '--dry'])));
|
||||
expect([dry.exit ?? 0, dry.err]).toEqual([0, '']);
|
||||
expect(shown(a)).toEqual([]);
|
||||
expect(dry.out).toContain('draft review submission rs-empty reused');
|
||||
expect(dry.out).toContain('add (developer rejected, re-submitted as is): com.example.notes.gems (consumable, version 1)');
|
||||
expect(dry.out).toContain('add (developer rejected, re-submitted as is): com.example.notes.pro (non-consumable, version 1)');
|
||||
expect(dry.out).not.toContain('skip');
|
||||
expect(dry.out).not.toContain('nothing to submit');
|
||||
expect(dry.out).not.toContain('/v1/inAppPurchaseVersions');
|
||||
const b = s.calls.length;
|
||||
const r = await capture(() => appleIapSubmit(p.ctx(['--with-version', '--yes'])));
|
||||
expect([r.exit ?? 0, r.err]).toEqual([0, '']);
|
||||
expect(shown(b)).toEqual(['POST /v1/reviewSubmissionItems', 'POST /v1/reviewSubmissionItems', 'POST /v1/reviewSubmissionItems', 'PATCH /v1/reviewSubmissions/rs-empty']);
|
||||
expect(s.asc.all('reviewSubmissions').map(x => [x.id, x.attributes.state])).toEqual([['rs-sub', 'CANCELING'], ['rs-empty', 'WAITING_FOR_REVIEW']]);
|
||||
expect(s.asc.children('rs-empty', 'reviewSubmissionItems').map(i => Object.values(i.rels).find(v => v !== 'rs-empty'))).toEqual(['ver-1', 'iapv-gems', 'iapv-pro']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('apple status', () => {
|
||||
test('an UNRESOLVED_ISSUES submission with its items and the resubmit command', async () => {
|
||||
rejected();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue