## What changed - adds production iOS identity, localized storefront metadata, native privacy declarations, App Store-safe artwork, and account-deletion UX - mirrors the live Danish Google Play title, short description, and long description in the App Store metadata source - generates Android launcher/store icons from the opaque iOS marketing master so both platforms use the same white background - adds guarded GitHub Actions workflows for storefront readiness, credential health, signed TestFlight uploads, and App Store candidate preparation - adds pinned Fastlane configuration with a committed dependency lock, release manifest tooling, and an operational App Store runbook - preserves the upstream iOS safe-area implementation while retaining opaque App Store icon assets ## Why The repository previously supported development-signed device bundles but had no production App Store identity, reproducible storefront source of truth, or protected signed-release pipeline. Apple also requires in-app account deletion for apps that support account creation. The Android icon master was transparent, which rendered as black on dark store/device surfaces. ## Impact Automation remains fail-closed behind `APP_STORE_AUTOMATION_ENABLED=false`. No build can upload to TestFlight or change App Store metadata until the switch is deliberately enabled after merge and the remaining release gates are satisfied. ## Validation - focused App Store, iOS icon, and cross-platform icon-background tests pass - every generated Android store/launcher icon is opaque with pure-white corners; iOS marketing artwork is checked the same way - Android icon drift check passes for all 19 generated files - production Vite build and the broader focused release checks completed successfully - storefront metadata is valid; only the two expected screenshot-set warnings remain - App Store Readiness is green at head `4445fecc` - Apple Distribution certificate and App Store profile were independently verified for `HP3FJ4GVL7.io.truckwash.app` - live App Store Connect API authentication succeeded for app `6792777794` - App Store record, free Denmark-only availability, and automatic `Internal QA` TestFlight group are configured - EU trader status, Content Rights, 4+ age rating, and the published App Privacy label are completed in App Store Connect - iPhone and iPad accessibility declarations are configured honestly as pre-release drafts ## Remaining external gates - reviewed iPhone and iPad screenshot sets are still required - an App Review login must be supplied without creating or exposing customer credentials - the first signed TestFlight candidate must run after merge and deliberate automation enablement
137 lines
3.6 KiB
JavaScript
137 lines
3.6 KiB
JavaScript
#!/usr/bin/env node
|
|
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import Jimp from "jimp";
|
|
|
|
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
const sourcePath = path.join(
|
|
projectRoot,
|
|
"ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-1024.png",
|
|
);
|
|
const checkOnly = process.argv.includes("--check");
|
|
const launcherBackground = "#FFFFFF";
|
|
|
|
const densityScale = {
|
|
mdpi: 1,
|
|
hdpi: 1.5,
|
|
xhdpi: 2,
|
|
xxhdpi: 3,
|
|
xxxhdpi: 4,
|
|
};
|
|
|
|
const targets = [
|
|
{ relativePath: "public/icons/icon-512x512.png", size: 512 },
|
|
{ relativePath: "public/icons/icon-192x192.png", size: 192 },
|
|
{ relativePath: "store_icon.png", size: 512 },
|
|
];
|
|
|
|
for (const [density, scale] of Object.entries(densityScale)) {
|
|
const legacySize = Math.round(48 * scale);
|
|
const foregroundSize = Math.round(108 * scale);
|
|
targets.push(
|
|
{
|
|
relativePath: `android/app/src/main/res/mipmap-${density}/ic_launcher.png`,
|
|
size: legacySize,
|
|
},
|
|
{
|
|
relativePath: `android/app/src/main/res/mipmap-${density}/ic_launcher_round.png`,
|
|
size: legacySize,
|
|
},
|
|
{
|
|
relativePath: `android/app/src/main/res/mipmap-${density}/ic_launcher_foreground.png`,
|
|
size: foregroundSize,
|
|
},
|
|
);
|
|
}
|
|
|
|
const xmlTargets = [
|
|
{
|
|
relativePath: "android/app/src/main/res/values/ic_launcher_background.xml",
|
|
content: `<?xml version="1.0" encoding="utf-8"?>\n<resources>\n <color name="ic_launcher_background">${launcherBackground}</color>\n</resources>\n`,
|
|
},
|
|
];
|
|
|
|
function targetPath(relativePath) {
|
|
return path.join(projectRoot, relativePath);
|
|
}
|
|
|
|
async function readIfExists(filePath) {
|
|
try {
|
|
return await fs.readFile(filePath);
|
|
} catch (error) {
|
|
if (error.code === "ENOENT") {
|
|
return null;
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async function writeIfChanged(relativePath, content) {
|
|
const filePath = targetPath(relativePath);
|
|
const current = await readIfExists(filePath);
|
|
|
|
if (current?.equals(content)) {
|
|
return false;
|
|
}
|
|
|
|
if (checkOnly) {
|
|
return true;
|
|
}
|
|
|
|
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
await fs.writeFile(filePath, content);
|
|
return true;
|
|
}
|
|
|
|
async function renderPng(sourceImage, size) {
|
|
const image = sourceImage.clone().resize(size, size, Jimp.RESIZE_BICUBIC);
|
|
return image.getBufferAsync(Jimp.MIME_PNG);
|
|
}
|
|
|
|
async function main() {
|
|
const sourceBuffer = await fs.readFile(sourcePath);
|
|
const sourceImage = await Jimp.read(sourceBuffer);
|
|
|
|
if (sourceImage.bitmap.width !== 1024 || sourceImage.bitmap.height !== 1024) {
|
|
throw new Error(`Expected ${path.relative(projectRoot, sourcePath)} to be a 1024x1024 PNG.`);
|
|
}
|
|
|
|
const changed = [];
|
|
|
|
for (const target of targets) {
|
|
const buffer = await renderPng(sourceImage, target.size);
|
|
if (await writeIfChanged(target.relativePath, buffer)) {
|
|
changed.push(target.relativePath);
|
|
}
|
|
}
|
|
|
|
for (const target of xmlTargets) {
|
|
if (await writeIfChanged(target.relativePath, Buffer.from(target.content))) {
|
|
changed.push(target.relativePath);
|
|
}
|
|
}
|
|
|
|
if (changed.length === 0) {
|
|
console.log(`Android icon assets are current (${targets.length + xmlTargets.length} files).`);
|
|
return;
|
|
}
|
|
|
|
if (checkOnly) {
|
|
console.error("Android icon assets are out of date:");
|
|
for (const relativePath of changed) {
|
|
console.error(`- ${relativePath}`);
|
|
}
|
|
console.error("Run `npm run mobile:android:icons`.");
|
|
process.exitCode = 1;
|
|
return;
|
|
}
|
|
|
|
console.log(`Updated ${changed.length} Android icon asset${changed.length === 1 ? "" : "s"}.`);
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(error.message);
|
|
process.exitCode = 1;
|
|
});
|