review access: guest-play apps rejected by Play as a login wall (empty name field, disabled Continue, a code link on the first screen): check warns for a Play app with a first-run gate or IAP and no reviewAccess.instructions; with guest steps the play-sign-in-details plan declares one entry without credentials, the dialog's required full-access box as the owner's step; store-pitfalls 73

This commit is contained in:
CRTkafa 2026-10-17 12:50:06 +08:00
parent d6070cc185
commit 824758179b
11 changed files with 202 additions and 15 deletions

View file

@ -209,13 +209,34 @@ function adsDeclaration(config: ShipConfig, p: PlanIds): Part {
};
}
/** Play's Add instructions dialog: "Any other information required to access your app" holds at most 500 characters. */
export const PLAY_ACCESS_INFO_MAX = 500;
/** The name of the one entry a guest-play app declares (no account, no credentials). */
export const GUEST_ENTRY_NAME = 'Guest play (no account needed)';
/** The Add instructions dialog's required box: an agreement, ticked by the owner only. */
export const PLAY_FULL_ACCESS_BOX = 'Sign in details in this declaration provide full access to all the features and content within this app, including premium or paid content';
function signInDetails(config: ShipConfig, p: PlanIds): Part {
const ra = config.reviewAccess;
const all = 'All functionality in my app is available without any access restrictions', some = 'All or some functionality in my app is restricted';
const out: PlanAnswer[] = [ra ? ans('access', 'Is any functionality restricted (sign-in, membership, location)?', ra.signInRequired, 'certain', 'reviewAccess.signInRequired', { control: 'radio', action: 'pick', label: ra.signInRequired ? some : all, where: 'Sign in details (App access)' })
: ans('access', 'Is any functionality restricted (sign-in, membership, location)?', null, 'ask-owner', 'reviewAccess.signInRequired', { control: 'radio', where: 'Sign in details', note: 'Set reviewAccess { signInRequired } (review-access skill).' })];
if (ra?.signInRequired) {
out.push(ra.instructions ? ans('instructions', 'Instructions for the reviewer', ra.instructions, 'certain', 'reviewAccess.instructions', { control: 'text', action: 'type', label: 'Any other information required to access your app', where: 'Add instructions' })
const info = 'Any other information required to access your app';
// a guest-play app with reviewer steps (signInRequired false + instructions): Play rejected such a game as a login
// wall ("provide valid login credentials") while it was declared unrestricted, so it is declared restricted with
// one entry that has no credentials and says how to get in as a guest
const guest = !!ra && !ra.signInRequired && !!ra.instructions?.trim();
const restricted = !!ra && (ra.signInRequired || guest);
const q = 'Is any functionality restricted (sign-in, membership, location)?';
const out: PlanAnswer[] = [ra ? ans('access', q, restricted, 'certain', guest ? 'reviewAccess.instructions (guest steps, no sign-in)' : 'reviewAccess.signInRequired', { control: 'radio', action: 'pick', label: restricted ? some : all, where: 'Sign in details (App access)',
...(guest ? { note: 'No sign-in, but guest steps: answered restricted so the reviewer reads them (Play rejected a guest-play game declared unrestricted as a login wall).' } : {}) })
: ans('access', q, null, 'ask-owner', 'reviewAccess.signInRequired', { control: 'radio', where: 'Sign in details', note: 'Set reviewAccess { signInRequired } (review-access skill).' })];
const long = (t: string) => t.length > PLAY_ACCESS_INFO_MAX ? { note: `${t.length} characters: Play takes at most ${PLAY_ACCESS_INFO_MAX}; shorten reviewAccess.instructions.` } : {};
if (guest) {
const steps = ra!.instructions!.trim();
out.push(ans('entryName', 'Name of the instructions entry', GUEST_ENTRY_NAME, 'certain', 'reviewAccess.signInRequired false', { control: 'text', action: 'type', label: GUEST_ENTRY_NAME, where: 'Add instructions: Name' }));
out.push(ans('credentials', 'Username and password', null, 'certain', 'reviewAccess.signInRequired false', { control: 'text', where: 'Add instructions: Username, Password', text: 'leave both empty (no account)', note: 'Guest play: no credentials exist; type nothing into Username or Password.' }));
out.push(ans('instructions', 'Instructions for the reviewer', steps, steps.length > PLAY_ACCESS_INFO_MAX ? 'owner-check' : 'certain', 'reviewAccess.instructions', { control: 'text', action: 'type', label: info, where: 'Add instructions: Any other information', ...long(steps) }));
} else if (ra?.signInRequired) {
out.push(ra.instructions ? ans('instructions', 'Instructions for the reviewer', ra.instructions, 'certain', 'reviewAccess.instructions', { control: 'text', action: 'type', label: info, where: 'Add instructions', ...long(ra.instructions) })
: ans('instructions', 'Instructions for the reviewer', null, 'ask-owner', 'reviewAccess.instructions', { note: 'Set reviewAccess.instructions (how to sign in and reach the main features).' }));
// with reviewDemoAccount in .storebound/credentials.json the agent types it (never printed: not in chat, logs, the
// plan or a commit); without it the owner types the username and password (or adds it there first)
@ -228,22 +249,30 @@ function signInDetails(config: ShipConfig, p: PlanIds): Part {
control: 'text', where: 'Add instructions: Username, Password',
note: `reviewDemoAccount: ${p.demoAccount === undefined ? 'not checked' : 'MISSING'}. The owner adds it to .storebound/credentials.json before the run (then the agent types it, never printed), or types the username and password into the card themselves. storebound never prints them.` }));
}
// the dialog's required declaration box: an agreement, so the owner ticks it (agents never tick declarations)
if (restricted) out.push(ans('fullAccess', 'Do these sign in details give full access to every feature and content, including premium or paid content?', null, 'agreement', 'Play Add instructions dialog (required)', { control: 'checkbox', label: PLAY_FULL_ACCESS_BOX, where: 'Add instructions dialog, required box' }));
const add = 'Add instructions';
return {
title: 'Play Console: Sign in details (App access)',
start: start(config, p, 'app-content/testing-credentials', appContentReach('Sign in details')),
steps: steps([
{ text: `Open App content > Sign in details: ${start(config, p, 'app-content/testing-credentials', []).url}` },
{ text: 'Pick whether any functionality is restricted (material radio: JS click on the inner [role=radio]).', answers: ['access'] },
guest && { text: `Guest steps (no sign-in): press "${add}", type the Name "${GUEST_ENTRY_NAME}", leave Username and Password empty, and type reviewAccess.instructions into "${info}" (at most ${PLAY_ACCESS_INFO_MAX} characters).`, click: [add], answers: ['entryName', 'credentials', 'instructions'] },
ra?.signInRequired && { text: p.demoAccount
? 'Restricted: press "Add instructions", type the instructions, then type the Username and Password from .storebound/credentials.json (reviewDemoAccount; click each field and type: never echo them in chat or logs).'
: 'Restricted: press "Add instructions", type the instructions; the Username and Password are the owner\'s (no reviewDemoAccount in .storebound/credentials.json: in the owner batch).', click: ['Add instructions'], answers: ['instructions', 'credentials'] },
? `Restricted: press "${add}", type the instructions, then type the Username and Password from .storebound/credentials.json (reviewDemoAccount; click each field and type: never echo them in chat or logs).`
: `Restricted: press "${add}", type the instructions; the Username and Password are the owner's (no reviewDemoAccount in .storebound/credentials.json: in the owner batch).`, click: [add], answers: ['instructions', 'credentials'] },
restricted && { text: `Leave the required box "${PLAY_FULL_ACCESS_BOX}" unticked and stop: the owner ticks it (a declaration). Then press "Add" (closes the dialog; the entry is listed).`, click: ['Add'], answers: ['fullAccess'] },
{ text: DECLARATION_SAVE },
{ text: 'Verify: read the page text and pipe it to `storebound console verify play-sign-in-details --from-text -`.' },
]),
answers: out,
...declarationButtons('Sign in details', [{ button: 'Add instructions', class: 'navigation', where: 'Sign in details: opens the instructions dialog' }]),
uiFacts: ['Verified 2026-10-12: the Sign in details card (App access) is /app-content/testing-credentials.', 'Credentials are never in the config, the plan or chat: the agent types them from .storebound/credentials.json (reviewDemoAccount) without printing them; without that entry the owner types them.', PLAY_MATERIAL_FACT],
verify: pageVerify('play-sign-in-details', ['The selected radio matches reviewAccess.signInRequired; with a sign-in, one set of instructions is listed.']),
...declarationButtons('Sign in details', [{ button: add, class: 'navigation', where: 'Sign in details: opens the instructions dialog' },
...(restricted ? [{ button: 'Add', class: 'navigation', where: 'Add instructions dialog: lists the entry on the card (nothing is stored before Save)' }] : [])]),
uiFacts: ['Verified 2026-10-12: the Sign in details card (App access) is /app-content/testing-credentials.', 'Credentials are never in the config, the plan or chat: the agent types them from .storebound/credentials.json (reviewDemoAccount) without printing them; without that entry the owner types them.',
`The Add instructions dialog: Name, Username, Password, "${info}" (at most ${PLAY_ACCESS_INFO_MAX} characters) and the required box "${PLAY_FULL_ACCESS_BOX}" (the owner's).`,
'A guest-play game declared "no restrictions" was rejected as a login wall (empty name field, Continue disabled, an "I have a code" link on the first screen): with guest steps the card says restricted with one entry without credentials.', PLAY_MATERIAL_FACT],
verify: pageVerify('play-sign-in-details', [guest ? `Restricted is selected; one entry "${GUEST_ENTRY_NAME}" with no username or password is listed.` : 'The selected radio matches reviewAccess.signInRequired; with a sign-in, one set of instructions is listed.']),
guidance: [`Open: Policy and programs > App content > Sign in details (App access): ${start(config, p, 'app-content/testing-credentials', []).url}`],
};
}