// App Review information as pure parts: the appStoreReviewDetails attributes from the config, the review contact and // the demo account, plus the notes (review notes, sign-in instructions, where account deletion is). Apple: the notes // field takes up to 4000 bytes; a demo account must keep working during the whole review (Guideline 2.1). // https://developer.apple.com/help/app-store-connect/reference/app-information/platform-version-information // https://developer.apple.com/documentation/appstoreconnectapi/appstorereviewdetail/attributes-data.dictionary import type { ShipConfig } from './config'; import { type DemoAccount, maskSecret, maskUser } from './credentials'; export const NOTES_MAX_BYTES = 4000; /** Notes for the reviewer: apple.info.reviewNotes, then reviewAccess.instructions, then where account deletion is. */ export function reviewNotes(config: Pick): string { const parts = [config.apple?.info?.reviewNotes?.trim() ?? '']; const ins = config.reviewAccess?.instructions?.trim(); if (ins && !parts[0]!.includes(ins)) parts.push(`Sign-in: ${ins}`); const del = config.accounts?.enabled ? config.accounts.inApp?.trim() : ''; if (del && !parts.join('\n').includes(del)) parts.push(`Account deletion: ${del}`); return parts.filter(Boolean).join('\n\n'); } export interface ReviewDetailPlan { attributes: Record; /** Log line without secrets. */ summary: string; } /** * Attributes for POST / PATCH appStoreReviewDetails. With reviewAccess.signInRequired the demo account is required * (throws when it is missing, so a submission never goes out with "sign-in required" and no account); without it the * demo fields are cleared. */ export function reviewDetailPlan(config: Pick, contact: Record, demo: DemoAccount | null): ReviewDetailPlan { const notes = reviewNotes(config); const bytes = new TextEncoder().encode(notes).length; if (bytes > NOTES_MAX_BYTES) throw new Error(`review notes are ${bytes} bytes; App Store Connect takes ${NOTES_MAX_BYTES}`); const required = !!config.reviewAccess?.signInRequired; if (required && !demo) throw new Error('reviewAccess.signInRequired is true but there is no demo account: add "reviewDemoAccount": { "username", "password" } to .storebound/credentials.json (or set REVIEW_DEMO_USER / REVIEW_DEMO_PASSWORD); see the review-access skill'); const attributes = { ...contact, demoAccountRequired: required, demoAccountName: required ? demo!.username : null, demoAccountPassword: required ? demo!.password : null, notes }; const summary = `${required ? `demo account ${maskUser(demo!.username)} / ${maskSecret(demo!.password)}` : 'no demo account (no sign-in needed)'}, notes ${bytes} bytes`; return { attributes, summary }; }