// 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 } export function profileIssues(config: ShipConfig, o: { demoAccount: boolean }): 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', 'deletionRequest'] 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)'}`); } if (!config.dataSafety.deletionRequest) err('accounts.enabled but dataSafety.deletionRequest is false: people must be able to delete their account and data'); } } 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'); 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; }