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
153 lines
6.9 KiB
JavaScript
153 lines
6.9 KiB
JavaScript
#!/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); });
|