// Upload the App Review screenshot of every in-app purchase (idempotent: a screenshot whose MD5 matches is kept) and // print each product's state. Nothing is submitted. // storebound apple iap-review [--dry] // Screenshot per product: products[].reviewScreenshot, else apple.reviewShots/.(png|jpg). // Size: a real device screenshot size (e.g. 1290 x 2796); other sizes fail with IMAGE_INCORRECT_DIMENSIONS. import { existsSync, readFileSync } from 'node:fs'; import { join } from 'node:path'; import type { Ctx } from '../lib/ctx'; import { need } from '../lib/ctx'; import { done, log, warn } from '../lib/log'; import { ascClient, md5 } from './asc'; export async function run(ctx: Ctx): Promise { const { config, root, resolve } = ctx.loaded; const c = await ascClient(root, need(config.apple?.bundleId, 'apple.bundleId'), config.apple?.appId); const appId = await c.app(); const defs = new Map((config.products ?? []).map(p => [p.id, p])); for (const p of await c.all(`/v1/apps/${appId}/inAppPurchasesV2?limit=200`)) { const sku: string = p.attributes.productId; const def = defs.get(sku); const file = def?.reviewScreenshot ? resolve(def.reviewScreenshot) : ['png', 'jpg', 'jpeg'].map(e => resolve(join(config.apple?.reviewShots ?? 'store/iap-review', `${sku}.${e}`))).find(existsSync); if (!file || !existsSync(file)) { warn('apple', `${sku}: no review screenshot (set products[].reviewScreenshot)`); continue; } const buf = readFileSync(file); const cur = (await c.maybe(`/v2/inAppPurchases/${p.id}/appStoreReviewScreenshot`))?.data; if (cur && cur.attributes.sourceFileChecksum === md5(buf) && cur.attributes.assetDeliveryState?.state !== 'FAILED') { log('apple', `${sku}: screenshot up to date`); continue; } if (ctx.args.has('dry')) { log('apple', `${sku}: would upload ${file}`); continue; } if (cur) await c.asc('DELETE', `/v1/inAppPurchaseAppStoreReviewScreenshots/${cur.id}`); await c.uploadAsset('inAppPurchaseAppStoreReviewScreenshots', { inAppPurchaseV2: { data: { type: 'inAppPurchases', id: p.id } } }, file); log('apple', `${sku}: screenshot uploaded`); } await Bun.sleep(3000); for (const p of await c.all(`/v1/apps/${appId}/inAppPurchasesV2?limit=200`)) log('apple', `${p.attributes.productId}: ${p.attributes.state}`); done('apple', 'iap-review done (READY_TO_SUBMIT = complete; first IAPs go to review with a version, from the IAP page)'); }