52 lines
5 KiB
TypeScript
52 lines
5 KiB
TypeScript
// Offline profile checks for `storebound check`: what an app with accounts, a sign-in for review or subscriptions
|
|
// must have in the config before any store sees it. Pure. The online counterparts (URLs answer, the demo account is
|
|
// on App Review, subscription states) are in validate-rules.ts (profileRules, appleSubscriptionRules).
|
|
import { profileOf, type ShipConfig } from './config';
|
|
import { accountCreationOf } from './declarations';
|
|
import { allSubscriptions } from './subscriptions';
|
|
|
|
export interface ProfileIssue { level: 'error' | 'warn'; msg: string }
|
|
|
|
/** firstRunGate: where the code shows a first-run gate (name entry, code / transfer entry, account transfer UI), from
|
|
* scan's SIGNALS.firstRunGate; check fills it for a Play app without reviewer instructions. */
|
|
export function profileIssues(config: ShipConfig, o: { demoAccount: boolean; firstRunGate?: string[] }): ProfileIssue[] {
|
|
const out: ProfileIssue[] = [];
|
|
const err = (msg: string) => out.push({ level: 'error', msg });
|
|
const warn = (msg: string) => out.push({ level: 'warn', msg });
|
|
const accounts = !!config.accounts?.enabled;
|
|
if (config.dataSafety) for (const k of ['collectsData', 'encryptedInTransit'] as const)
|
|
if (typeof config.dataSafety[k] !== 'boolean') err(`dataSafety.${k}: not answered (true | false; ask the owner, store-declarations skill)`);
|
|
const acm = config.dataSafety?.accountCreation ?? ['PSL_ACM_NONE'];
|
|
if (accounts) {
|
|
if (config.play && !config.accounts!.deletionUrl) err('accounts.enabled: set accounts.deletionUrl (Play asks for a web page to request account deletion; account-deletion skill)');
|
|
if (config.apple && !config.accounts!.inApp) warn('accounts.enabled: set accounts.inApp to where deletion starts in the app (Apple 5.1.1(v); it goes into the review notes)');
|
|
if (config.dataSafety) {
|
|
if (acm.includes('PSL_ACM_NONE')) {
|
|
const s = accountCreationOf(config.accounts!.signUp);
|
|
err(`accounts.enabled but dataSafety.accountCreation says PSL_ACM_NONE: list the sign-up methods you offer${s ? ` (accounts.signUp means accountCreation: ${JSON.stringify(s)})` : ' (accounts.signUp, e.g. [\'username\'] for a username with no password, tells storebound scan which)'}`);
|
|
}
|
|
}
|
|
} else if (config.dataSafety && !acm.includes('PSL_ACM_NONE')) warn('dataSafety.accountCreation lists sign-up methods but accounts.enabled is not set: add accounts { enabled: true, deletionUrl, inApp }');
|
|
if (config.reviewAccess?.signInRequired) {
|
|
if (!o.demoAccount) err('App Review needs a demo account (sign-in required) · add reviewDemoAccount to .storebound/credentials.json · review-access skill');
|
|
if (!config.reviewAccess.instructions) warn('reviewAccess.instructions: tell the reviewer how to sign in and where the main features are');
|
|
} else if (accounts && config.reviewAccess === undefined) warn('the app has accounts: set reviewAccess { signInRequired } so App Review and Play App access get a demo account when needed');
|
|
// a guest-play app (no sign-in) rejected by Play as a login wall: an empty name field with Continue disabled and an
|
|
// "I have a code" link on the first screen read as a sign-in. Guest steps in reviewAccess.instructions turn Play's
|
|
// Sign in details into a "restricted" entry without credentials that tells the reviewer how to get in.
|
|
const gate = o.firstRunGate ?? [];
|
|
const iap = !!config.products?.length || allSubscriptions(config.subscriptions).length > 0;
|
|
if (config.play && !config.reviewAccess?.signInRequired && !config.reviewAccess?.instructions?.trim() && (gate.length || iap))
|
|
warn(`Play may reject as a login wall; add guest steps: ${[gate.length && `first-run gate in the code (${gate.slice(0, 2).join(', ')})`, iap && 'in-app purchases'].filter(Boolean).join(' and ')} with no reviewAccess.instructions. Set reviewAccess.instructions to English steps for a reviewer without an account (e.g. "No account needed: tap Continue, the name is pre-filled; the code link is only for moving progress"); the play-sign-in-details plan then declares a guest entry (store-pitfalls: guest-play apps rejected as a login wall)`);
|
|
const steps = config.reviewAccess?.instructions?.trim() ?? '';
|
|
if (config.play && steps.length > 500) warn(`reviewAccess.instructions: ${steps.length} characters; Play's Sign in details "Any other information" takes at most 500`);
|
|
const subs = allSubscriptions(config.subscriptions);
|
|
if (subs.length) {
|
|
if (!config.app.termsUrl) err('subscriptions: set app.termsUrl (Terms of Use / EULA, linked in the app and the metadata; Apple 3.1.2)');
|
|
if (config.apple && subs.some(x => !x.sub.reviewScreenshot)) warn(`subscriptions without reviewScreenshot: ${subs.filter(x => !x.sub.reviewScreenshot).map(x => x.sub.id).join(', ')} (App Store review needs one each)`);
|
|
}
|
|
const p = profileOf(config);
|
|
if (p === 'subscription-app' && !subs.length) warn('app.profile is subscription-app but there are no subscriptions in the config');
|
|
if (p === 'game' && config.shots?.style === 'app') warn('app.profile is game but shots.style is app (fine if intended)');
|
|
return out;
|
|
}
|