chore(store): App Store Connect API client and listing push script
Some checks failed
Build & Release APK / build (push) Has been cancelled
Some checks failed
Build & Release APK / build (push) Has been cancelled
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WontRuPqKcod9XK4ZN9MXv
This commit is contained in:
152
store/asc-fill.js
Normal file
152
store/asc-fill.js
Normal file
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env node
|
||||
// Push the App Store listing from store/listing.md into App Store Connect.
|
||||
// Idempotent: re-running overwrites text fields and replaces the screenshot set.
|
||||
//
|
||||
// node store/asc-fill.js
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const crypto = require("crypto");
|
||||
const { api, APP_ID } = require("./asc");
|
||||
|
||||
const NAME = "Him: Kegel Exercises for Men";
|
||||
const SUBTITLE = "Pelvic Floor Trainer & Timer";
|
||||
const PRIVACY_URL = "https://rehbock.xyz/kegel/privacy.html";
|
||||
const SUPPORT_URL = "https://rehbock.xyz/kegel/privacy.html";
|
||||
const KEYWORDS = "kegels,reverse,workout,stamina,control,bladder,prostate,strength,daily,free,squeeze,hold,guided";
|
||||
const PROMO = "Free, no account, no ads, works offline. Guided kegel sessions that get harder as you get stronger.";
|
||||
const DESCRIPTION = `A free pelvic floor trainer for men. No account, no ads, no subscription, no data collection. Everything runs on your phone.
|
||||
|
||||
Each session is a guided, four-block routine driven by a step timer with haptic cues so you can train with the screen off or your eyes closed:
|
||||
|
||||
• Holds — 8 slow squeeze-and-hold reps. The core of the workout. Holds start at 5 seconds and automatically grow by 1 second every 3 completed sessions, up to 30 seconds.
|
||||
• Pushes — 5 reverse kegels (gently bear down) to balance tension and relaxation.
|
||||
• Quick squeezes — 10 one-second burst contractions.
|
||||
• Quick pushes — 10 one-second burst releases.
|
||||
|
||||
Progressive overload is built in. Finish sessions and the holds get longer on their own. Want to go faster or slower? Adjust hold time manually in Settings.
|
||||
|
||||
Stay consistent with a day streak, your best streak, and a two-week activity strip on the home screen.
|
||||
|
||||
Why kegels?
|
||||
Pelvic floor exercises strengthen the muscles that support the bladder and bowel and are widely recommended for bladder control, post-prostate-surgery recovery, and general pelvic health. Like any muscle, they respond to regular, progressively harder training.
|
||||
|
||||
What this app is not
|
||||
It is a timer and progression tracker, not a medical device, and gives no medical advice. If you have a medical condition or recent surgery, talk to a clinician first.
|
||||
|
||||
Privacy
|
||||
Zero network access. Your session count, difficulty and history are stored only on your device. Delete the app and the data is gone.`;
|
||||
|
||||
const SHOTS_DIR = path.join(__dirname, "screenshots", "out", "ios");
|
||||
const DISPLAY_TYPE = "APP_IPHONE_67"; // 6.9" / 6.7" iPhones (1320×2868 accepted)
|
||||
|
||||
async function first(p) {
|
||||
const r = await api("GET", p);
|
||||
return r.data[0];
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const appInfo = await first(`/v1/apps/${APP_ID}/appInfos`);
|
||||
const infoLoc = await first(`/v1/appInfos/${appInfo.id}/appInfoLocalizations`);
|
||||
const version = await first(`/v1/apps/${APP_ID}/appStoreVersions?filter[platform]=IOS`);
|
||||
const verLoc = await first(`/v1/appStoreVersions/${version.id}/appStoreVersionLocalizations`);
|
||||
console.log(`appInfo ${appInfo.id} / version ${version.attributes.versionString} (${version.attributes.appStoreState})`);
|
||||
|
||||
// 1. Name, subtitle, privacy policy
|
||||
await api("PATCH", `/v1/appInfoLocalizations/${infoLoc.id}`, {
|
||||
data: { type: "appInfoLocalizations", id: infoLoc.id, attributes: { name: NAME, subtitle: SUBTITLE, privacyPolicyUrl: PRIVACY_URL } },
|
||||
});
|
||||
console.log("✔ name / subtitle / privacy URL");
|
||||
|
||||
// 2. Description, keywords, promo, support URL
|
||||
await api("PATCH", `/v1/appStoreVersionLocalizations/${verLoc.id}`, {
|
||||
data: {
|
||||
type: "appStoreVersionLocalizations",
|
||||
id: verLoc.id,
|
||||
attributes: { description: DESCRIPTION, keywords: KEYWORDS, promotionalText: PROMO, supportUrl: SUPPORT_URL },
|
||||
},
|
||||
});
|
||||
console.log("✔ description / keywords / promo / support URL");
|
||||
|
||||
// 3. Categories
|
||||
await api("PATCH", `/v1/appInfos/${appInfo.id}`, {
|
||||
data: {
|
||||
type: "appInfos",
|
||||
id: appInfo.id,
|
||||
relationships: {
|
||||
primaryCategory: { data: { type: "appCategories", id: "HEALTH_AND_FITNESS" } },
|
||||
secondaryCategory: { data: { type: "appCategories", id: "MEDICAL" } },
|
||||
},
|
||||
},
|
||||
});
|
||||
console.log("✔ categories");
|
||||
|
||||
// 4. Age rating: fitness timer, health topic yes, mild medical info, nothing else
|
||||
const none = "NONE";
|
||||
await api("PATCH", `/v1/ageRatingDeclarations/${appInfo.id}`, {
|
||||
data: {
|
||||
type: "ageRatingDeclarations",
|
||||
id: appInfo.id,
|
||||
attributes: {
|
||||
alcoholTobaccoOrDrugUseOrReferences: none,
|
||||
contests: none,
|
||||
gamblingSimulated: none,
|
||||
gunsOrOtherWeapons: none,
|
||||
horrorOrFearThemes: none,
|
||||
matureOrSuggestiveThemes: none,
|
||||
medicalOrTreatmentInformation: "INFREQUENT_OR_MILD",
|
||||
profanityOrCrudeHumor: none,
|
||||
sexualContentGraphicAndNudity: none,
|
||||
sexualContentOrNudity: none,
|
||||
violenceCartoonOrFantasy: none,
|
||||
violenceRealistic: none,
|
||||
violenceRealisticProlongedGraphicOrSadistic: none,
|
||||
advertising: false,
|
||||
gambling: false,
|
||||
healthOrWellnessTopics: true,
|
||||
lootBox: false,
|
||||
messagingAndChat: false,
|
||||
parentalControls: false,
|
||||
socialMedia: false,
|
||||
unrestrictedWebAccess: false,
|
||||
userGeneratedContent: false,
|
||||
ageAssurance: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
console.log("✔ age rating declaration");
|
||||
|
||||
// 5. Screenshots: replace the iPhone 6.9" set
|
||||
const sets = await api("GET", `/v1/appStoreVersionLocalizations/${verLoc.id}/appScreenshotSets?filter[screenshotDisplayType]=${DISPLAY_TYPE}`);
|
||||
for (const s of sets.data) await api("DELETE", `/v1/appScreenshotSets/${s.id}`);
|
||||
const set = await api("POST", "/v1/appScreenshotSets", {
|
||||
data: {
|
||||
type: "appScreenshotSets",
|
||||
attributes: { screenshotDisplayType: DISPLAY_TYPE },
|
||||
relationships: { appStoreVersionLocalization: { data: { type: "appStoreVersionLocalizations", id: verLoc.id } } },
|
||||
},
|
||||
});
|
||||
const files = fs.readdirSync(SHOTS_DIR).filter((f) => /^\d+\.png$/.test(f)).sort((a, b) => parseInt(a) - parseInt(b));
|
||||
for (const f of files) {
|
||||
const buf = fs.readFileSync(path.join(SHOTS_DIR, f));
|
||||
const shot = await api("POST", "/v1/appScreenshots", {
|
||||
data: {
|
||||
type: "appScreenshots",
|
||||
attributes: { fileName: f, fileSize: buf.length },
|
||||
relationships: { appScreenshotSet: { data: { type: "appScreenshotSets", id: set.data.id } } },
|
||||
},
|
||||
});
|
||||
for (const op of shot.data.attributes.uploadOperations) {
|
||||
const headers = Object.fromEntries(op.requestHeaders.map((h) => [h.name, h.value]));
|
||||
const res = await fetch(op.url, { method: op.method, headers, body: buf.subarray(op.offset, op.offset + op.length) });
|
||||
if (!res.ok) throw new Error(`upload chunk failed ${res.status} for ${f}`);
|
||||
}
|
||||
await api("PATCH", `/v1/appScreenshots/${shot.data.id}`, {
|
||||
data: { type: "appScreenshots", id: shot.data.id, attributes: { uploaded: true, sourceFileChecksum: crypto.createHash("md5").update(buf).digest("hex") } },
|
||||
});
|
||||
console.log(`✔ screenshot ${f}`);
|
||||
}
|
||||
console.log("done");
|
||||
}
|
||||
|
||||
main().catch((e) => { console.error(e.message); process.exit(1); });
|
||||
Reference in New Issue
Block a user