## What changed - add `NSLocationAlwaysAndWhenInUseUsageDescription` to the iOS app - localize the purpose string in Danish and English - enforce the key in mobile permission validation and App Store readiness tests ## Why App Store Connect accepted builds 1 and 2 but emitted `ITMS-90683`, stating that the final app references APIs requiring this purpose string. Shipping a corrected binary avoids submitting a candidate with a known Apple delivery warning. ## Validation - `node scripts/mobile/check-permissions.mjs` - `node scripts/mobile/validate-app-store.mjs --strict` - `vitest run tests/unit/app-store-product-readiness.spec.js` (5 tests) - Prettier check for changed JS files - `git diff --check`
94 lines
4.5 KiB
JavaScript
94 lines
4.5 KiB
JavaScript
import { createHash } from "node:crypto";
|
|
import { readFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
import { readI18nRuntimeMessages } from "./helpers/readI18nRuntimeMessages";
|
|
|
|
const root = process.cwd();
|
|
const read = (relativePath) => readFileSync(join(root, relativePath));
|
|
const readText = (relativePath) => read(relativePath).toString("utf8");
|
|
const sha256 = (relativePath) => createHash("sha256").update(read(relativePath)).digest("hex");
|
|
|
|
const pngMetadata = (relativePath) => {
|
|
const png = read(relativePath);
|
|
return {
|
|
width: png.readUInt32BE(16),
|
|
height: png.readUInt32BE(20),
|
|
colorType: png.readUInt8(25),
|
|
};
|
|
};
|
|
|
|
describe("App Store product readiness", () => {
|
|
it("ships Danish and English privacy and account-deletion disclosures", () => {
|
|
const da = readI18nRuntimeMessages("da");
|
|
const en = readI18nRuntimeMessages("en");
|
|
|
|
expect(da.privacy_policy.title).toBe("Privatlivspolitik");
|
|
expect(en.privacy_policy.title).toBe("Privacy policy");
|
|
expect(da.privacy_policy.deletion_body).toContain("Profil → Slet konto");
|
|
expect(en.privacy_policy.deletion_body).toContain("Profile → Delete account");
|
|
expect(da.user_dashboard.profile.deletion.title).toBe("Slet konto");
|
|
expect(en.user_dashboard.profile.deletion.title).toBe("Delete account");
|
|
expect(da.support.help_deletion).toContain("kontosletning");
|
|
expect(en.support.help_deletion).toContain("account deletion");
|
|
});
|
|
|
|
it("declares localized camera and location reasons plus exempt encryption use", () => {
|
|
const infoPlist = readText("ios/App/App/Info.plist");
|
|
const danishStrings = readText("ios/App/App/da.lproj/InfoPlist.strings");
|
|
const englishStrings = readText("ios/App/App/en.lproj/InfoPlist.strings");
|
|
|
|
expect(infoPlist).toContain("<key>ITSAppUsesNonExemptEncryption</key>");
|
|
expect(infoPlist).toMatch(/<key>ITSAppUsesNonExemptEncryption<\/key>\s*<false\/>/u);
|
|
expect(danishStrings).toContain('"NSCameraUsageDescription"');
|
|
expect(danishStrings).toContain('"NSLocationAlwaysAndWhenInUseUsageDescription"');
|
|
expect(danishStrings).toContain('"NSLocationWhenInUseUsageDescription"');
|
|
expect(englishStrings).toContain('"NSCameraUsageDescription"');
|
|
expect(englishStrings).toContain('"NSLocationAlwaysAndWhenInUseUsageDescription"');
|
|
expect(englishStrings).toContain('"NSLocationWhenInUseUsageDescription"');
|
|
});
|
|
|
|
it("declares linked app-functionality data without tracking", () => {
|
|
const privacyManifest = readText("ios/App/App/PrivacyInfo.xcprivacy");
|
|
|
|
for (const type of [
|
|
"NSPrivacyCollectedDataTypeName",
|
|
"NSPrivacyCollectedDataTypeEmailAddress",
|
|
"NSPrivacyCollectedDataTypePhoneNumber",
|
|
"NSPrivacyCollectedDataTypePhysicalAddress",
|
|
"NSPrivacyCollectedDataTypeUserID",
|
|
"NSPrivacyCollectedDataTypePurchaseHistory",
|
|
"NSPrivacyCollectedDataTypePaymentInfo",
|
|
"NSPrivacyCollectedDataTypePhotosorVideos",
|
|
"NSPrivacyCollectedDataTypeCustomerSupport",
|
|
"NSPrivacyCollectedDataTypeOtherUserContent",
|
|
"NSPrivacyCollectedDataTypeOtherDiagnosticData",
|
|
]) {
|
|
expect(privacyManifest).toContain(`<string>${type}</string>`);
|
|
}
|
|
|
|
expect(privacyManifest).not.toContain("NSPrivacyCollectedDataTypePreciseLocation");
|
|
expect(privacyManifest).toMatch(/<key>NSPrivacyTracking<\/key>\s*<false\/>/u);
|
|
expect(privacyManifest).not.toContain("NSPrivacyCollectedDataTypePurposeThirdPartyAdvertising");
|
|
});
|
|
|
|
it("replaces Capacitor artwork with opaque, correctly sized Truck Wash assets", () => {
|
|
const iconPath = "ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png";
|
|
const splashPath = "ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732.png";
|
|
|
|
expect(pngMetadata(iconPath)).toEqual({ width: 1024, height: 1024, colorType: 2 });
|
|
expect(pngMetadata(splashPath)).toEqual({ width: 2732, height: 2732, colorType: 2 });
|
|
expect(sha256(iconPath)).not.toBe("29e4777e319de3ee5a52c3a8004ec19d0568414004257e36d7c94a077d71c93b");
|
|
expect(sha256(splashPath)).not.toBe("1b5002b74a5500e697298ced06ca2811ac33f2771f236f3c720ff23243890530");
|
|
});
|
|
|
|
it("keeps the fixed mobile header below the native iOS status area", () => {
|
|
const mobileHeader = readText("src/components/viewport/page/headers/MobileHeader.vue");
|
|
|
|
expect(mobileHeader).toContain("padding-top: var(--pleno-mobile-safe-top)");
|
|
expect(mobileHeader).toContain("height: calc(80px + var(--pleno-mobile-safe-top))");
|
|
expect(mobileHeader).toContain("min-height: calc(80px + var(--pleno-mobile-safe-top))");
|
|
});
|
|
});
|