fix(android): use login consent instead of native privacy prompt

This commit is contained in:
key
2026-07-29 09:59:41 +08:00
parent f322f2b21b
commit 7f3357c7a1
10 changed files with 181 additions and 131 deletions
+61 -25
View File
@@ -8,13 +8,16 @@ import {
const projectRoot = resolve(import.meta.dirname, '..');
const manifestPath = resolve(projectRoot, 'src/manifest.json');
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
const requirePrivacy = process.argv.includes('--require-privacy');
const requireLegalConsent = process.argv.includes('--require-legal-consent');
const checkLegalUrls = process.argv.includes('--check-legal-urls');
const failures = [];
let releaseLegalUrls;
const fail = (message) => failures.push(message);
const compiledAppRoot = resolve(projectRoot, 'dist/build/app');
const compiledManifestPath = resolve(compiledAppRoot, 'manifest.json');
const compiledAppServicePath = resolve(compiledAppRoot, 'app-service.js');
const loginPagePath = resolve(projectRoot, 'src/pages/auth/login/index.vue');
const sourceAndroidManifestPath = resolve(projectRoot, 'AndroidManifest.xml');
const compiledAndroidManifestPath = resolve(compiledAppRoot, 'AndroidManifest.xml');
const sourceNetworkSecurityPath = resolve(
@@ -129,8 +132,8 @@ const modules = app?.modules;
if (!Array.isArray(app?.screenOrientation) || !app.screenOrientation.includes('portrait-primary')) {
fail('app-plus.screenOrientation must include portrait-primary');
}
if (splashscreen?.useOriginalMsgbox !== true) {
fail('app-plus.distribute.splashscreen.useOriginalMsgbox must enable the native privacy prompt');
if (splashscreen?.useOriginalMsgbox !== false) {
fail('app-plus.distribute.splashscreen.useOriginalMsgbox must disable the native privacy prompt');
}
if (app?.ssl?.untrustedca !== 'refuse') {
fail('app-plus.ssl.untrustedca must remain refuse');
@@ -248,7 +251,7 @@ for (const [relativePath, width, height] of splashPaths) expectPng(relativePath,
const privacyPath = resolve(projectRoot, 'src/androidPrivacy.json');
const compiledPrivacyPath = resolve(projectRoot, 'dist/build/app/androidPrivacy.json');
if (requirePrivacy) {
if (requireLegalConsent) {
if (String(process.env.VITE_DEV_SMS_HINT_ENABLED || '').toLowerCase() === 'true') {
fail('VITE_DEV_SMS_HINT_ENABLED must be disabled for release builds');
}
@@ -265,45 +268,78 @@ if (requirePrivacy) {
}
if (!existsSync(privacyPath)) {
fail('src/androidPrivacy.json is required for release; run configure:app-privacy with final legal URLs');
fail('src/androidPrivacy.json is required for release; run configure:app-legal with final legal URLs');
} else {
try {
const privacy = JSON.parse(readFileSync(privacyPath, 'utf8'));
const privacyLegalUrls = validateAndroidPrivacyDocument(privacy);
if (
releaseLegalUrls
&& (
privacyLegalUrls.termsUrl !== releaseLegalUrls.termsUrl
|| privacyLegalUrls.privacyUrl !== releaseLegalUrls.privacyUrl
)
) {
fail('androidPrivacy.json legal links must match VITE_APP_TERMS_URL and VITE_APP_PRIVACY_URL');
}
releaseLegalUrls = privacyLegalUrls;
validateAndroidPrivacyDocument(privacy);
} catch (error) {
fail(`androidPrivacy.json is invalid: ${error instanceof Error ? error.message : String(error)}`);
}
}
if (!existsSync(compiledPrivacyPath)) {
fail('compiled App resource is missing androidPrivacy.json; run build:app after configure:app-privacy');
fail('compiled App resource is missing androidPrivacy.json; run build:app after configure:app-legal');
} else {
try {
const compiledPrivacy = JSON.parse(readFileSync(compiledPrivacyPath, 'utf8'));
const compiledLegalUrls = validateAndroidPrivacyDocument(compiledPrivacy);
validateAndroidPrivacyDocument(compiledPrivacy);
if (
releaseLegalUrls
&& (
compiledLegalUrls.termsUrl !== releaseLegalUrls.termsUrl
|| compiledLegalUrls.privacyUrl !== releaseLegalUrls.privacyUrl
)
existsSync(privacyPath)
&& JSON.stringify(compiledPrivacy) !== JSON.stringify(JSON.parse(readFileSync(privacyPath, 'utf8')))
) {
fail('compiled App androidPrivacy.json must match the source privacy configuration');
fail('compiled App androidPrivacy.json must match the source prompt=none configuration');
}
} catch (error) {
fail(`compiled App androidPrivacy.json is invalid: ${error instanceof Error ? error.message : String(error)}`);
}
}
if (!existsSync(compiledManifestPath)) {
fail('compiled App resource is missing manifest.json; run build:app');
} else {
try {
const compiledManifest = JSON.parse(readFileSync(compiledManifestPath, 'utf8'));
if (compiledManifest.plus?.distribute?.splashscreen?.useOriginalMsgbox !== false) {
fail('compiled App manifest must disable the DCloud native privacy prompt');
}
} catch (error) {
fail(`compiled App manifest is invalid: ${error instanceof Error ? error.message : String(error)}`);
}
}
if (!existsSync(loginPagePath)) {
fail('login page is missing');
} else {
const loginSource = readFileSync(loginPagePath, 'utf8');
for (const [label, pattern] of [
['default unchecked consent', /const agreed = ref\(false\)/],
['shared legal consent gate', /const ensureLegalConsent = \(\) =>/],
['terms link', /openLegalDocument\('terms'\)/],
['privacy link', /openLegalDocument\('privacy'\)/],
['SMS legal gate', /const sendCode = async \(\) => \{[\s\S]*?if \(!ensureLegalConsent\(\)\) return;/],
['login legal gate', /const login = async \(\) => \{[\s\S]*?if \(!ensureLegalConsent\(\)\) return;/]
]) {
if (!pattern.test(loginSource)) {
fail(`login page is missing ${label}`);
}
}
}
if (!existsSync(compiledAppServicePath)) {
fail('compiled App resource is missing app-service.js; run build:app');
} else if (releaseLegalUrls) {
const appService = readFileSync(compiledAppServicePath, 'utf8');
if (!appService.includes(releaseLegalUrls.termsUrl)) {
fail('compiled App login flow is missing VITE_APP_TERMS_URL');
}
if (!appService.includes(releaseLegalUrls.privacyUrl)) {
fail('compiled App login flow is missing VITE_APP_PRIVACY_URL');
}
if (!appService.includes('请先阅读并勾选同意')) {
fail('compiled App login flow is missing the unchecked-consent failure message');
}
}
verifyAndroidNetworkSecurity(
compiledAndroidManifestPath,
compiledNetworkSecurityPath,
@@ -342,7 +378,7 @@ const checkRemoteLegalPage = async (url, label, expectedText) => {
}
};
if (requirePrivacy && checkLegalUrls && releaseLegalUrls) {
if (requireLegalConsent && checkLegalUrls && releaseLegalUrls) {
await Promise.all([
checkRemoteLegalPage(releaseLegalUrls.termsUrl, 'service agreement URL', /服务协议|用户协议/),
checkRemoteLegalPage(releaseLegalUrls.privacyUrl, 'privacy policy URL', /隐私政策|个人信息保护/)