vydanne 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/GETTING_STARTED.md +69 -40
- package/README.md +91 -16
- package/SKILL.md +129 -24
- package/bin/vydanne.mjs +24 -1
- package/package.json +3 -2
- package/src/client.mjs +42 -6
- package/src/commands/accessibility.mjs +13 -0
- package/src/commands/ageRating.mjs +94 -21
- package/src/commands/bridge.mjs +328 -0
- package/src/commands/compliance.mjs +78 -9
- package/src/commands/diff.mjs +63 -19
- package/src/commands/fill.mjs +74 -13
- package/src/commands/inspect.mjs +4 -2
- package/src/commands/preflight.mjs +59 -6
- package/src/commands/prepare.mjs +220 -0
- package/src/commands/prerelease.mjs +17 -5
- package/src/commands/previews.mjs +33 -2
- package/src/commands/privacy.mjs +75 -5
- package/src/commands/push.mjs +118 -0
- package/src/commands/reviewContact.mjs +63 -13
- package/src/config.mjs +53 -4
- package/src/index.mjs +60 -3
- package/src/locales.mjs +27 -5
- package/src/play/aab.mjs +133 -0
- package/src/play/commands/diff.mjs +40 -0
- package/src/play/commands/fill.mjs +20 -19
- package/src/play/commands/prerelease.mjs +94 -20
- package/src/play/images.mjs +67 -0
- package/src/registry.mjs +11 -0
- package/src/screenshots.mjs +112 -0
- package/src/upload.mjs +3 -1
- package/types/index.d.ts +225 -10
- package/vydanne.config.example.mjs +54 -1
package/bin/vydanne.mjs
CHANGED
|
@@ -15,6 +15,14 @@ const i = argv.indexOf("--config");
|
|
|
15
15
|
const cfgPath = i >= 0 ? argv[i + 1] : undefined;
|
|
16
16
|
const si = argv.indexOf("--store");
|
|
17
17
|
const store = si >= 0 ? argv[si + 1] : "apple";
|
|
18
|
+
// Validated, because the dispatch below only tests `store === "google"`: a typo (`--store goole`) or a
|
|
19
|
+
// missing value (`--store --apply`) would otherwise fall through to the APPLE branch and aim the command
|
|
20
|
+
// at the other store — the worst possible reading of a mistyped argument, and a silent one, since every
|
|
21
|
+
// Apple command runs happily from a repo configured for both.
|
|
22
|
+
if (store !== "apple" && store !== "google") {
|
|
23
|
+
console.error(`\x1b[31mvydanne: unknown --store '${store}' (expected: apple, google)\x1b[0m`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
18
26
|
|
|
19
27
|
// SAFE BY DEFAULT. Every store-mutating command is a dry run unless `--apply` is passed. The Play half
|
|
20
28
|
// always worked this way (VYDANNE_COMMIT=1, enforceable because an Edit can be discarded); the Apple half
|
|
@@ -57,6 +65,9 @@ try {
|
|
|
57
65
|
console.log(`supported (${Object.keys(r.supported).length}):`);
|
|
58
66
|
for (const [ui, asc] of Object.entries(r.supported)) console.log(` ${ui} -> ${asc}`);
|
|
59
67
|
console.log(`unsupported (${r.unsupported.length}) [no App Store language -> fall back to primary]: ${r.unsupported.join(", ")}`);
|
|
68
|
+
// A localeMap entry pointing at a code Apple does not have is a config mistake, not a missing
|
|
69
|
+
// language — it would otherwise be indistinguishable from the line above.
|
|
70
|
+
if (r.invalid?.length) console.log(`\x1b[31minvalid localeMap (${r.invalid.length}) [not an App Store code]: ${r.invalid.join(", ")}\x1b[0m`);
|
|
60
71
|
} else if (store === "google") {
|
|
61
72
|
const cfg = await loadConfig(cfgPath);
|
|
62
73
|
if (!cfg.google) throw new Error("vydanne: no `google` block in config — add packageName + a service-account key");
|
|
@@ -107,6 +118,15 @@ usage: vydanne <command> [--apply] [--config vydanne.config.mjs]
|
|
|
107
118
|
--apply PERFORM the writes. Without it every store-mutating command below (marked ✎) runs
|
|
108
119
|
as a DRY RUN: it reads the store, reports exactly what it would change, and sends
|
|
109
120
|
nothing. Read-only commands ignore the flag.
|
|
121
|
+
✎ prepare create/reuse the editable App Store version and attach the uploaded build.
|
|
122
|
+
Run this FIRST on an app that already has a version on sale — until it has,
|
|
123
|
+
there is no draft for \`fill\` to write into. Never submits; VYDANNE_VERSION=<x>
|
|
124
|
+
to name the version instead of reading it off the newest build.
|
|
125
|
+
✎ push the whole pipeline, in order: prepare → fill → previews → age-rating →
|
|
126
|
+
review-contact → accessibility → preflight. Stops at the first failure.
|
|
127
|
+
--skip <step>[,<step>] drops steps that don't apply (prepare/preflight can't be
|
|
128
|
+
skipped); every skip is reported again at the end, so green still means green.
|
|
129
|
+
Ends at a green preflight — Add to Review + Submit stay yours, in the web UI.
|
|
110
130
|
✎ fill metadata + screenshots + previews (native; iOS & macOS separate)
|
|
111
131
|
✎ age-rating set the age rating (AppInfo declaration)
|
|
112
132
|
✎ review-contact App Review contact from the gitignored files
|
|
@@ -115,6 +135,8 @@ usage: vydanne <command> [--apply] [--config vydanne.config.mjs]
|
|
|
115
135
|
✎ previews upload App Preview videos (native chunked upload)
|
|
116
136
|
iap validate IAP fields; VYDANNE_FLATTEN=<png> flattens a screenshot to RGB
|
|
117
137
|
compliance generate the US encryption self-classification PDF
|
|
138
|
+
bridge map zdymak's store-assets output onto the folders \`fill\` reads (locale codes,
|
|
139
|
+
device prefixes, Play paths). Local files only; --dry-run previews.
|
|
118
140
|
inspect read-only ASC state
|
|
119
141
|
diff show what differs between local (metadata/screenshots/previews) and ASC
|
|
120
142
|
preflight verify submission-completeness (the gotcha checker)
|
|
@@ -125,7 +147,8 @@ usage: vydanne <command> [--apply] [--config vydanne.config.mjs]
|
|
|
125
147
|
credentials: env > .env cascade (.env, .env.<mode>, .env.local, .env.<mode>.local) > user config
|
|
126
148
|
(\$VYDANNE_CONFIG_HOME, %APPDATA%\\vydanne or \$XDG_CONFIG_HOME/vydanne, ~/.appstoreconnect).
|
|
127
149
|
NEVER the committed vydanne.config.mjs — run \`vydanne auth\` to see what resolved.
|
|
128
|
-
toggles: VYDANNE_SKIP_METADATA / VYDANNE_SKIP_SCREENSHOTS (fill),
|
|
150
|
+
toggles: VYDANNE_SKIP_METADATA / VYDANNE_SKIP_SCREENSHOTS (fill), VYDANNE_REPLACE=1 (fill/previews:
|
|
151
|
+
replace populated slots), VYDANNE_VERSION (prepare), VYDANNE_A11Y_PUBLISH (accessibility),
|
|
129
152
|
VYDANNE_PROFILE (named profile), VYDANNE_ENV (.env mode),
|
|
130
153
|
VYDANNE_COMMIT=1 (legacy alias for --apply — prefer the flag)`;
|
|
131
154
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vydanne",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "App Store Connect + Google Play submission prep — the companion to zdymak (media). Native Node (no fastlane/Ruby/Python): localized listings, screenshot/preview/icon upload, ratings, review contact, accessibility & privacy labels, IAP, export docs, build upload to TestFlight / a Play closed track, a diff of local-vs-store, and a preflight verifier that encodes the store gotchas. Never submits for review. iOS/macOS via the ASC REST API; Android via the Play Developer Edits API (--store google).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"app-store-connect",
|
|
@@ -51,7 +51,8 @@
|
|
|
51
51
|
"scripts": {
|
|
52
52
|
"check:docs": "node scripts/check-docs.mjs",
|
|
53
53
|
"check:types": "tsc --noEmit -p tsconfig.json && node scripts/check-types.mjs",
|
|
54
|
-
"
|
|
54
|
+
"check:config": "node scripts/check-config.mjs",
|
|
55
|
+
"prepublishOnly": "npm run check:docs && npm run check:types && npm run check:config",
|
|
55
56
|
"release:patch": "node scripts/release.mjs patch",
|
|
56
57
|
"release:minor": "node scripts/release.mjs minor",
|
|
57
58
|
"release:major": "node scripts/release.mjs major"
|
package/src/client.mjs
CHANGED
|
@@ -3,13 +3,15 @@ import { yellow } from "./util.mjs";
|
|
|
3
3
|
|
|
4
4
|
const API = "https://api.appstoreconnect.apple.com";
|
|
5
5
|
const IRIS = "https://appstoreconnect.apple.com/iris";
|
|
6
|
-
|
|
6
|
+
// DEVELOPER_REMOVED_FROM_SALE is the state a version reaches when the DEVELOPER pulls it, as opposed to
|
|
7
|
+
// Apple; both are shipped versions that will never become editable again, so both are dead here.
|
|
8
|
+
const DEAD_VERSION = ["READY_FOR_SALE", "REMOVED_FROM_SALE", "DEVELOPER_REMOVED_FROM_SALE", "REPLACED_WITH_NEW_VERSION"];
|
|
7
9
|
// READY_FOR_DISTRIBUTION is Apple's newer name for the LIVE app-info (they're migrating the
|
|
8
10
|
// state vocabulary; versions still report appStoreState=READY_FOR_SALE). Without it here,
|
|
9
11
|
// appInfo() treats the live record as editable and every name/subtitle PATCH comes back
|
|
10
12
|
// ENTITY_ERROR.ATTRIBUTE.INVALID.INVALID_STATE — which fails the whole locale in `fill`,
|
|
11
13
|
// release notes included, on any app that already has a version on sale.
|
|
12
|
-
const DEAD_INFO = ["READY_FOR_SALE", "READY_FOR_DISTRIBUTION", "REPLACED_WITH_NEW_VERSION", "REMOVED_FROM_SALE"];
|
|
14
|
+
const DEAD_INFO = ["READY_FOR_SALE", "READY_FOR_DISTRIBUTION", "REPLACED_WITH_NEW_VERSION", "REMOVED_FROM_SALE", "DEVELOPER_REMOVED_FROM_SALE"];
|
|
13
15
|
|
|
14
16
|
// Anything that is not a read. ASC has no transaction to roll back — unlike Play, where an edit can be
|
|
15
17
|
// discarded — so for Apple the only safe place to stand between a command and a live listing is here.
|
|
@@ -79,16 +81,50 @@ export class Client {
|
|
|
79
81
|
return app;
|
|
80
82
|
}
|
|
81
83
|
|
|
82
|
-
|
|
84
|
+
/**
|
|
85
|
+
* The version being PREPARED — or null when there isn't one.
|
|
86
|
+
*
|
|
87
|
+
* This used to fall back to `data[0]`, the LIVE version, whenever nothing editable existed. The
|
|
88
|
+
* fallback was silent and it defeated its own callers: `fill`, `preflight`, `reviewContact` and
|
|
89
|
+
* `previews` each test `if (!v)` and report "no editable version", and not one of those branches could
|
|
90
|
+
* be reached on an app that had shipped once. What happened instead was worse than an error — `fill`
|
|
91
|
+
* aimed its description/whatsNew PATCHes at the listing customers were reading, and `preflight`
|
|
92
|
+
* validated that same live listing and printed "no blockers", calling a release submittable when
|
|
93
|
+
* there was nothing to submit.
|
|
94
|
+
*
|
|
95
|
+
* So a write is never handed the live version by default. Read-only commands (`inspect`, `diff`) pass
|
|
96
|
+
* `allowLive: true`, because "how does local compare with what is on sale" is a real question and that
|
|
97
|
+
* is the only version they could ask it about. Everything else gets null and says so — and now has
|
|
98
|
+
* `prepare` to point at, which is the command that makes an editable version exist.
|
|
99
|
+
*/
|
|
100
|
+
async editVersion(platform, { allowLive = false } = {}) {
|
|
83
101
|
const { json } = await this.get(`/v1/apps/${this.appId}/appStoreVersions?filter[platform]=${platform}&limit=10`);
|
|
84
102
|
const data = json.data || [];
|
|
85
|
-
|
|
103
|
+
const editable = data.find((v) => !DEAD_VERSION.includes(v.attributes.appStoreState));
|
|
104
|
+
if (editable) return editable;
|
|
105
|
+
return allowLive ? data[0] || null : null;
|
|
86
106
|
}
|
|
87
107
|
|
|
88
|
-
|
|
108
|
+
/**
|
|
109
|
+
* The app-info being PREPARED (name/subtitle, age rating live on it) — or null when there isn't one.
|
|
110
|
+
*
|
|
111
|
+
* Same disease, same cure as editVersion() above: this fell back to `data[0]` — the LIVE app-info —
|
|
112
|
+
* whenever nothing editable existed, and the fallback aimed writes at the record customers see. It is
|
|
113
|
+
* how the DEAD_INFO bug was found in the first place (every name/subtitle PATCH against the live
|
|
114
|
+
* record comes back INVALID_STATE and fails the whole locale in `fill`), and adding
|
|
115
|
+
* READY_FOR_DISTRIBUTION to DEAD_INFO only fixed the case where an editable sibling EXISTS to be
|
|
116
|
+
* found; the moment there is none, `|| data[0]` reintroduced exactly the state that comment warns
|
|
117
|
+
* about. Now a write gets null and a clear skip instead of twenty locales of ENTITY_ERROR.
|
|
118
|
+
*
|
|
119
|
+
* `allowLive` is for reads (`diff`), where "how does local compare with what is on sale" is the
|
|
120
|
+
* question being asked.
|
|
121
|
+
*/
|
|
122
|
+
async appInfo({ allowLive = false } = {}) {
|
|
89
123
|
const { json } = await this.get(`/v1/apps/${this.appId}/appInfos?limit=10`);
|
|
90
124
|
const data = json.data || [];
|
|
91
|
-
|
|
125
|
+
const editable = data.find((i) => !DEAD_INFO.includes(i.attributes.state));
|
|
126
|
+
if (editable) return editable;
|
|
127
|
+
return allowLive ? data[0] || null : null;
|
|
92
128
|
}
|
|
93
129
|
|
|
94
130
|
async versionLocalizations(versionId) {
|
|
@@ -107,8 +107,14 @@ export async function run(config, client) {
|
|
|
107
107
|
console.log(` declaring: ${claimed.length ? claimed.join(", ") : "(nothing)"}`);
|
|
108
108
|
|
|
109
109
|
let gated = false;
|
|
110
|
+
// A PATCH that Apple refuses is a claim that never reached the store. Both failure paths below used
|
|
111
|
+
// to print red and `continue`, and the command still returned true — so `push` treated a family whose
|
|
112
|
+
// declaration never saved as a completed step. The `continue`s stay (one refused family must not hide
|
|
113
|
+
// the other three); the verdict now travels out with the return.
|
|
114
|
+
const failures = [];
|
|
110
115
|
for (const family of Object.keys(UNAVAILABLE)) {
|
|
111
116
|
const id = decls[family];
|
|
117
|
+
// Not a failure: Apple only holds declarations for the families the app actually ships on.
|
|
112
118
|
if (!id) {
|
|
113
119
|
console.error(yellow(` no ${family} declaration`));
|
|
114
120
|
continue;
|
|
@@ -119,6 +125,7 @@ export async function run(config, client) {
|
|
|
119
125
|
});
|
|
120
126
|
if (r.status >= 300) {
|
|
121
127
|
console.error(red(` ${family} draft ${r.status}`));
|
|
128
|
+
failures.push(`${family}: draft not saved (${r.status})`);
|
|
122
129
|
continue;
|
|
123
130
|
}
|
|
124
131
|
if (publish) {
|
|
@@ -132,11 +139,17 @@ export async function run(config, client) {
|
|
|
132
139
|
console.log(yellow(` ${family}: draft saved — publish deferred (app not live yet)`));
|
|
133
140
|
} else {
|
|
134
141
|
console.error(red(` ${family} publish ${p.status}`));
|
|
142
|
+
failures.push(`${family}: publish refused (${p.status})`);
|
|
135
143
|
}
|
|
136
144
|
} else {
|
|
137
145
|
console.log(green(` ${family}: draft saved`));
|
|
138
146
|
}
|
|
139
147
|
}
|
|
148
|
+
if (failures.length) {
|
|
149
|
+
console.error(red(`accessibility: ${failures.length} declaration(s) did not save:`));
|
|
150
|
+
for (const f of failures) console.error(` ${red("x")} ${f}`);
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
140
153
|
console.log(
|
|
141
154
|
gated
|
|
142
155
|
? yellow("accessibility staged (DRAFT); re-run with VYDANNE_A11Y_PUBLISH=1 once the app is live")
|
|
@@ -1,33 +1,106 @@
|
|
|
1
1
|
import { green, yellow, red } from "../util.mjs";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Set the age rating via the AppInfo age-rating declaration.
|
|
5
|
+
*
|
|
6
|
+
* `rating: "4+"` stays the shorthand it always was: every content descriptor NONE, every capability
|
|
7
|
+
* question false, which is what Apple recomputes 4+ from. Anything else is declared feature by feature
|
|
8
|
+
* in `ageRating`, merged over that all-NONE base — so an app with fantasy violence sets one key rather
|
|
9
|
+
* than restating twenty-seven.
|
|
10
|
+
*
|
|
11
|
+
* This used to refuse outright for any rating but 4+, which made the whole command — and `push`, which
|
|
12
|
+
* runs it as a step — unusable for any app with violence, gambling, chat or user-generated content.
|
|
13
|
+
* The base was always the general thing; only the door was missing.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const N = "NONE";
|
|
17
|
+
|
|
18
|
+
// Apple's 2025 age-rating schema. A PATCH must include ALL required fields (a partial set 409s), and
|
|
19
|
+
// `ageRatingOverride` (deprecated) cannot be sent alongside `ageRatingOverrideV2` — so we send only V2.
|
|
20
|
+
// Content descriptors are enums (NONE/INFREQUENT_OR_MILD/FREQUENT_OR_INTENSE); capability questions are
|
|
21
|
+
// booleans. All benign here → 4+.
|
|
22
|
+
const BASE = {
|
|
23
|
+
advertising: false, alcoholTobaccoOrDrugUseOrReferences: N, contests: N, gambling: false,
|
|
24
|
+
gamblingSimulated: N, gunsOrOtherWeapons: N, healthOrWellnessTopics: false, kidsAgeBand: null,
|
|
25
|
+
lootBox: false, medicalOrTreatmentInformation: N, messagingAndChat: false, parentalControls: false,
|
|
26
|
+
profanityOrCrudeHumor: N, ageAssurance: false, sexualContentGraphicAndNudity: N, sexualContentOrNudity: N,
|
|
27
|
+
socialMedia: false, socialMediaAgeRestricted: false, horrorOrFearThemes: N, matureOrSuggestiveThemes: N,
|
|
28
|
+
unrestrictedWebAccess: false, userGeneratedContent: false, violenceCartoonOrFantasy: N,
|
|
29
|
+
violenceRealisticProlongedGraphicOrSadistic: N, violenceRealistic: N, ageRatingOverrideV2: N,
|
|
30
|
+
koreaAgeRatingOverride: N,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** Values Apple accepts for a content descriptor. `kidsAgeBand` and the overrides are checked separately. */
|
|
34
|
+
const DESCRIPTOR_VALUES = new Set([N, "INFREQUENT_OR_MILD", "FREQUENT_OR_INTENSE"]);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The attributes to send, or a human-readable problem.
|
|
38
|
+
*
|
|
39
|
+
* A declaration is validated against the schema BEFORE it reaches Apple, because the failure otherwise
|
|
40
|
+
* is a 409 naming a field the operator did not know existed. An unknown key is a typo — and a typo in
|
|
41
|
+
* this table means a descriptor silently stayed NONE, which is a rating that understates the app.
|
|
42
|
+
*/
|
|
43
|
+
export function resolveAttributes(config) {
|
|
44
|
+
const declared = config.ageRating;
|
|
45
|
+
if (!declared) {
|
|
46
|
+
if (config.rating === "4+") return { attributes: { ...BASE } };
|
|
47
|
+
return {
|
|
48
|
+
problem: [
|
|
49
|
+
`age-rating: rating is '${config.rating}', but nothing describes what makes it that.`,
|
|
50
|
+
"'4+' is the only rating that needs no detail (every descriptor NONE). For anything else,",
|
|
51
|
+
"declare the content Apple asks about — it computes the rating from these, you don't set it:",
|
|
52
|
+
"",
|
|
53
|
+
" ageRating: {",
|
|
54
|
+
" violenceCartoonOrFantasy: 'INFREQUENT_OR_MILD',",
|
|
55
|
+
" userGeneratedContent: false,",
|
|
56
|
+
" },",
|
|
57
|
+
"",
|
|
58
|
+
`Known keys: ${Object.keys(BASE).join(", ")}`,
|
|
59
|
+
].join("\n"),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const unknown = Object.keys(declared).filter((k) => !(k in BASE));
|
|
63
|
+
if (unknown.length) {
|
|
64
|
+
return { problem: `age-rating: unknown key(s) ${unknown.join(", ")}. Known: ${Object.keys(BASE).join(", ")}` };
|
|
65
|
+
}
|
|
66
|
+
const bad = [];
|
|
67
|
+
for (const [k, v] of Object.entries(declared)) {
|
|
68
|
+
if (k === "kidsAgeBand") continue; // null | FIVE_AND_UNDER | SIX_TO_EIGHT | NINE_TO_ELEVEN
|
|
69
|
+
if (typeof BASE[k] === "boolean" && typeof v !== "boolean") bad.push(`${k} must be true or false`);
|
|
70
|
+
else if (typeof BASE[k] === "string" && !DESCRIPTOR_VALUES.has(v)) bad.push(`${k} must be one of ${[...DESCRIPTOR_VALUES].join(" / ")}`);
|
|
71
|
+
}
|
|
72
|
+
if (bad.length) return { problem: `age-rating: ${bad.join("; ")}` };
|
|
73
|
+
return { attributes: { ...BASE, ...declared } };
|
|
74
|
+
}
|
|
75
|
+
|
|
6
76
|
export async function run(config, client) {
|
|
7
|
-
|
|
77
|
+
const { attributes, problem } = resolveAttributes(config);
|
|
78
|
+
if (problem) { console.error(red(problem)); return false; }
|
|
8
79
|
await client.findApp(config.bundleId);
|
|
80
|
+
// No allowLive: this PATCHes the age-rating declaration, and the live app-info's declaration is not
|
|
81
|
+
// ours to aim a write at. The refusal below used to be unreachable — appInfo() handed back the live
|
|
82
|
+
// record instead of null, so the write was planned against it and Apple's INVALID_STATE was the
|
|
83
|
+
// first anyone heard of it.
|
|
9
84
|
const info = await client.appInfo();
|
|
10
|
-
if (!info) {
|
|
85
|
+
if (!info) {
|
|
86
|
+
console.error(red("age-rating: no editable app info — refusing to write to the live record."));
|
|
87
|
+
console.error(" `vydanne prepare --apply` starts the next version, which makes app info editable again.");
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
11
90
|
const { json } = await client.get(`/v1/appInfos/${info.id}/ageRatingDeclaration`);
|
|
12
91
|
const id = json.data?.id;
|
|
13
92
|
if (!id) { console.error(red("age-rating: no declaration")); return false; }
|
|
14
|
-
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
advertising: false, alcoholTobaccoOrDrugUseOrReferences: N, contests: N, gambling: false,
|
|
21
|
-
gamblingSimulated: N, gunsOrOtherWeapons: N, healthOrWellnessTopics: false, kidsAgeBand: null,
|
|
22
|
-
lootBox: false, medicalOrTreatmentInformation: N, messagingAndChat: false, parentalControls: false,
|
|
23
|
-
profanityOrCrudeHumor: N, ageAssurance: false, sexualContentGraphicAndNudity: N, sexualContentOrNudity: N,
|
|
24
|
-
socialMedia: false, socialMediaAgeRestricted: false, horrorOrFearThemes: N, matureOrSuggestiveThemes: N,
|
|
25
|
-
unrestrictedWebAccess: false, userGeneratedContent: false, violenceCartoonOrFantasy: N,
|
|
26
|
-
violenceRealisticProlongedGraphicOrSadistic: N, violenceRealistic: N, ageRatingOverrideV2: N,
|
|
27
|
-
koreaAgeRatingOverride: N,
|
|
28
|
-
};
|
|
93
|
+
|
|
94
|
+
// What is actually being asserted, printed before it is sent. Apple computes the rating from these,
|
|
95
|
+
// so the declared non-defaults ARE the rating — worth seeing in the log of the run that set them.
|
|
96
|
+
const declared = Object.entries(attributes).filter(([k, v]) => v !== BASE[k] || (v !== false && v !== N && v !== null));
|
|
97
|
+
console.log(` declaring: ${declared.length ? declared.map(([k, v]) => `${k}=${v}`).join(", ") : "everything NONE (4+)"}`);
|
|
98
|
+
|
|
29
99
|
const r = await client.patch(`/v1/ageRatingDeclarations/${id}`, { data: { type: "ageRatingDeclarations", id, attributes } });
|
|
30
100
|
if (r.status >= 300) { console.error(red(`age-rating: ${r.status}: ${JSON.stringify(r.json).slice(0, 200)}`)); return false; }
|
|
31
|
-
|
|
101
|
+
// Apple decides the band from the descriptors; naming config.rating here reports what was ASKED for,
|
|
102
|
+
// which is the only thing this command controls.
|
|
103
|
+
const label = config.ageRating ? `${config.rating} (from the declared descriptors)` : "4+";
|
|
104
|
+
console.log(client.dryRun ? yellow(`age rating WOULD be set -> ${label}`) : green(`age rating set -> ${label}`));
|
|
32
105
|
return true;
|
|
33
106
|
}
|