fix(android): use login consent instead of native privacy prompt
This commit is contained in:
@@ -1,35 +1,4 @@
|
|||||||
{
|
{
|
||||||
"version": "20260728",
|
"version": "20260729",
|
||||||
"prompt": "template",
|
"prompt": "none"
|
||||||
"title": "服务协议和隐私政策",
|
|
||||||
"message": " 请你审慎阅读并充分理解《服务协议》和《隐私政策》。为了提供登录、录音转写、图片/视频/文件上传和故障排查服务,本应用会在你使用相应功能时请求必要的设备和文件访问权限。你可阅读<a href=\"__TERMS_URL__\">《服务协议》</a>和<a href=\"__PRIVACY_URL__\">《隐私政策》</a>了解详细信息。点击“同意并继续”即表示你同意上述协议。",
|
|
||||||
"buttonAccept": "同意并继续",
|
|
||||||
"buttonRefuse": "暂不同意",
|
|
||||||
"hrefLoader": "system",
|
|
||||||
"backToExit": "false",
|
|
||||||
"second": {
|
|
||||||
"title": "确认提示",
|
|
||||||
"message": " 进入应用前,请先阅读并同意<a href=\"__TERMS_URL__\">《服务协议》</a>和<a href=\"__PRIVACY_URL__\">《隐私政策》</a>。",
|
|
||||||
"buttonAccept": "同意并继续",
|
|
||||||
"buttonRefuse": "退出应用"
|
|
||||||
},
|
|
||||||
"disagreeMode": {
|
|
||||||
"support": false,
|
|
||||||
"loadNativePlugins": false,
|
|
||||||
"visitorEntry": false,
|
|
||||||
"showAlways": false
|
|
||||||
},
|
|
||||||
"styles": {
|
|
||||||
"backgroundColor": "#FFFFFF",
|
|
||||||
"borderRadius": "12px",
|
|
||||||
"title": {
|
|
||||||
"color": "#1F2329"
|
|
||||||
},
|
|
||||||
"buttonAccept": {
|
|
||||||
"color": "#E60012"
|
|
||||||
},
|
|
||||||
"buttonRefuse": {
|
|
||||||
"color": "#6B7280"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "aihr-mobile-uni",
|
"name": "aihr-mobile-uni",
|
||||||
"version": "0.1.10",
|
"version": "0.1.11",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "aihr-mobile-uni",
|
"name": "aihr-mobile-uni",
|
||||||
"version": "0.1.10",
|
"version": "0.1.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dcloudio/uni-app": "3.0.0-alpha-5020220260725001",
|
"@dcloudio/uni-app": "3.0.0-alpha-5020220260725001",
|
||||||
"@dcloudio/uni-app-plus": "3.0.0-alpha-5020220260725001",
|
"@dcloudio/uni-app-plus": "3.0.0-alpha-5020220260725001",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package",
|
"$schema": "https://json.schemastore.org/package",
|
||||||
"name": "aihr-mobile-uni",
|
"name": "aihr-mobile-uni",
|
||||||
"version": "0.1.10",
|
"version": "0.1.11",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -10,10 +10,11 @@
|
|||||||
"build:h5": "uni build -p h5",
|
"build:h5": "uni build -p h5",
|
||||||
"build:app": "uni build -p app && node scripts/sync-app-native-resources.mjs",
|
"build:app": "uni build -p app && node scripts/sync-app-native-resources.mjs",
|
||||||
"generate:app-assets": "sh scripts/generate-app-assets.sh",
|
"generate:app-assets": "sh scripts/generate-app-assets.sh",
|
||||||
"configure:app-privacy": "node scripts/configure-android-privacy.mjs",
|
"configure:app-legal": "node scripts/configure-android-privacy.mjs",
|
||||||
|
"configure:app-privacy": "npm run configure:app-legal",
|
||||||
"verify:app-assets": "node scripts/verify-app-assets.mjs",
|
"verify:app-assets": "node scripts/verify-app-assets.mjs",
|
||||||
"verify:android-apk": "powershell -NoProfile -ExecutionPolicy Bypass -File scripts/verify-android-apk.ps1",
|
"verify:android-apk": "powershell -NoProfile -ExecutionPolicy Bypass -File scripts/verify-android-apk.ps1",
|
||||||
"verify:app-release": "node scripts/verify-app-assets.mjs --require-privacy --check-legal-urls",
|
"verify:app-release": "node scripts/verify-app-assets.mjs --require-legal-consent --check-legal-urls",
|
||||||
"test:unit": "node --test tests/*.test.mjs",
|
"test:unit": "node --test tests/*.test.mjs",
|
||||||
"typecheck": "vue-tsc --noEmit"
|
"typecheck": "vue-tsc --noEmit"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -74,43 +74,39 @@ export const validateLegalUrlPair = (termsValue, privacyValue) => {
|
|||||||
return { termsUrl, privacyUrl };
|
return { termsUrl, privacyUrl };
|
||||||
};
|
};
|
||||||
|
|
||||||
const extractLinks = (message) =>
|
|
||||||
[...String(message || '').matchAll(/href="([^"]+)"/g)].map((match) => match[1]);
|
|
||||||
|
|
||||||
export const validateAndroidPrivacyDocument = (privacy) => {
|
export const validateAndroidPrivacyDocument = (privacy) => {
|
||||||
if (!privacy || typeof privacy !== 'object') {
|
if (!privacy || typeof privacy !== 'object') {
|
||||||
throw new Error('androidPrivacy.json must contain a JSON object');
|
throw new Error('androidPrivacy.json must contain a JSON object');
|
||||||
}
|
}
|
||||||
if (privacy.prompt !== 'template') {
|
if (privacy.prompt !== 'none') {
|
||||||
throw new Error('androidPrivacy.json must use prompt=template');
|
throw new Error('androidPrivacy.json must use prompt=none for the controlled internal App');
|
||||||
}
|
}
|
||||||
if (!/^[1-9]\d*$/.test(String(privacy.version || ''))) {
|
if (!/^[1-9]\d*$/.test(String(privacy.version || ''))) {
|
||||||
throw new Error('androidPrivacy.json version must be a positive numeric policy version');
|
throw new Error('androidPrivacy.json version must be a positive numeric configuration version');
|
||||||
}
|
}
|
||||||
if (privacy.hrefLoader !== 'system') {
|
for (const templateOnlyField of [
|
||||||
throw new Error('androidPrivacy.json must open legal links with hrefLoader=system');
|
'title',
|
||||||
|
'message',
|
||||||
|
'buttonAccept',
|
||||||
|
'buttonRefuse',
|
||||||
|
'hrefLoader',
|
||||||
|
'second',
|
||||||
|
'disagreeMode',
|
||||||
|
'styles'
|
||||||
|
]) {
|
||||||
|
if (Object.hasOwn(privacy, templateOnlyField)) {
|
||||||
|
throw new Error(`androidPrivacy.json prompt=none must not contain ${templateOnlyField}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return {
|
||||||
const primaryLinks = extractLinks(privacy.message);
|
prompt: privacy.prompt,
|
||||||
const secondaryLinks = extractLinks(privacy.second?.message);
|
version: String(privacy.version)
|
||||||
if (primaryLinks.length !== 2 || secondaryLinks.length !== 2) {
|
};
|
||||||
throw new Error('androidPrivacy.json must contain two legal links in both prompts');
|
|
||||||
}
|
|
||||||
|
|
||||||
const primary = validateLegalUrlPair(primaryLinks[0], primaryLinks[1]);
|
|
||||||
const secondary = validateLegalUrlPair(secondaryLinks[0], secondaryLinks[1]);
|
|
||||||
if (primary.termsUrl !== secondary.termsUrl || primary.privacyUrl !== secondary.privacyUrl) {
|
|
||||||
throw new Error('androidPrivacy.json primary and secondary prompts must use the same legal links');
|
|
||||||
}
|
|
||||||
return primary;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const renderAndroidPrivacy = (templateText, termsValue, privacyValue) => {
|
export const renderAndroidPrivacy = (templateText, termsValue, privacyValue) => {
|
||||||
const { termsUrl, privacyUrl } = validateLegalUrlPair(termsValue, privacyValue);
|
validateLegalUrlPair(termsValue, privacyValue);
|
||||||
const rendered = String(templateText)
|
const privacy = JSON.parse(String(templateText));
|
||||||
.replaceAll('__TERMS_URL__', termsUrl)
|
|
||||||
.replaceAll('__PRIVACY_URL__', privacyUrl);
|
|
||||||
const privacy = JSON.parse(rendered);
|
|
||||||
validateAndroidPrivacyDocument(privacy);
|
validateAndroidPrivacyDocument(privacy);
|
||||||
return `${JSON.stringify(privacy, null, 2)}\n`;
|
return `${JSON.stringify(privacy, null, 2)}\n`;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ try {
|
|||||||
privacyUrl
|
privacyUrl
|
||||||
);
|
);
|
||||||
writeFileSync(outputPath, rendered, 'utf8');
|
writeFileSync(outputPath, rendered, 'utf8');
|
||||||
console.log('src/androidPrivacy.json configured from final public legal URLs.');
|
console.log('src/androidPrivacy.json configured with prompt=none; final public legal URLs validated for the login page.');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Android privacy configuration failed: ${error instanceof Error ? error.message : String(error)}`);
|
console.error(`Android privacy configuration failed: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
@@ -107,37 +107,71 @@ try {
|
|||||||
throw "APK versionName mismatch: got $versionName, expected $($sourceManifest.versionName)"
|
throw "APK versionName mismatch: got $versionName, expected $($sourceManifest.versionName)"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (-not [string]::IsNullOrWhiteSpace($ExpectedTermsUrl)) {
|
if ($BuildKind -eq 'custom-base') {
|
||||||
if ($BuildKind -eq 'custom-base') {
|
# A DCloud custom base APK contains only the native debug runtime. HBuilderX
|
||||||
# A DCloud custom base APK contains only the native debug runtime. HBuilderX
|
# syncs the compiled www resources separately when it runs the app on a
|
||||||
# syncs the compiled www resources separately when it runs the app on a
|
# device, so both no-prompt and login-link checks use the compiled App files.
|
||||||
# device, so the legal links cannot truthfully be asserted from the APK.
|
$compiledPrivacyPath = Join-Path $projectRoot 'dist\build\app\androidPrivacy.json'
|
||||||
$compiledPrivacyPath = Join-Path $projectRoot 'dist\build\app\androidPrivacy.json'
|
if (-not (Test-Path -LiteralPath $compiledPrivacyPath)) {
|
||||||
if (-not (Test-Path -LiteralPath $compiledPrivacyPath)) {
|
throw 'Compiled App resources are missing androidPrivacy.json; run build:app first.'
|
||||||
throw 'Compiled App resources are missing androidPrivacy.json; run build:app first.'
|
|
||||||
}
|
|
||||||
$privacyJson = Get-Content -Raw -Encoding UTF8 -LiteralPath $compiledPrivacyPath
|
|
||||||
}
|
}
|
||||||
else {
|
$privacyJson = Get-Content -Raw -Encoding UTF8 -LiteralPath $compiledPrivacyPath
|
||||||
$bundledPrivacyFiles = @(
|
$legalAppServiceFiles = @(
|
||||||
Get-ChildItem -LiteralPath (Join-Path $decodePath 'assets\apps') `
|
@(
|
||||||
-Recurse -File -Filter 'androidPrivacy.json'
|
(Join-Path $projectRoot 'dist\build\app\app-service.js'),
|
||||||
)
|
(Join-Path $projectRoot 'dist\build\app-plus\app-service.js')
|
||||||
if ($bundledPrivacyFiles.Count -ne 1) {
|
) |
|
||||||
throw "Expected exactly one bundled androidPrivacy.json, found $($bundledPrivacyFiles.Count)."
|
Where-Object { Test-Path -LiteralPath $_ } |
|
||||||
}
|
Select-Object -First 1
|
||||||
$privacyJson = Get-Content -Raw -Encoding UTF8 -LiteralPath $bundledPrivacyFiles[0].FullName
|
)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$bundledPrivacyFiles = @(
|
||||||
|
Get-ChildItem -LiteralPath (Join-Path $decodePath 'assets\apps') `
|
||||||
|
-Recurse -File -Filter 'androidPrivacy.json'
|
||||||
|
)
|
||||||
|
if ($bundledPrivacyFiles.Count -ne 1) {
|
||||||
|
throw "Expected exactly one bundled androidPrivacy.json, found $($bundledPrivacyFiles.Count)."
|
||||||
}
|
}
|
||||||
|
$privacyJson = Get-Content -Raw -Encoding UTF8 -LiteralPath $bundledPrivacyFiles[0].FullName
|
||||||
|
$legalAppServiceFiles = @(
|
||||||
|
Get-ChildItem -LiteralPath (Join-Path $decodePath 'assets\apps') `
|
||||||
|
-Recurse -File -Filter 'app-service.js'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (-not $privacyJson.Contains($ExpectedTermsUrl)) {
|
$privacyConfig = $privacyJson | ConvertFrom-Json
|
||||||
throw 'Packaged privacy configuration is missing the expected service agreement URL.'
|
if ($privacyConfig.prompt -ne 'none') {
|
||||||
|
throw 'Packaged privacy configuration must disable the DCloud native prompt with prompt=none.'
|
||||||
|
}
|
||||||
|
foreach ($templateOnlyField in @(
|
||||||
|
'title',
|
||||||
|
'message',
|
||||||
|
'buttonAccept',
|
||||||
|
'buttonRefuse',
|
||||||
|
'hrefLoader',
|
||||||
|
'second',
|
||||||
|
'disagreeMode',
|
||||||
|
'styles'
|
||||||
|
)) {
|
||||||
|
if ($privacyConfig.PSObject.Properties.Name -contains $templateOnlyField) {
|
||||||
|
throw "Packaged prompt=none privacy configuration must not contain $templateOnlyField."
|
||||||
}
|
}
|
||||||
if (-not $privacyJson.Contains($ExpectedPrivacyUrl)) {
|
}
|
||||||
throw 'Packaged privacy configuration is missing the expected privacy policy URL.'
|
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($ExpectedTermsUrl)) {
|
||||||
|
if ($legalAppServiceFiles.Count -ne 1) {
|
||||||
|
throw "Expected exactly one App service for login legal-link verification, found $($legalAppServiceFiles.Count)."
|
||||||
}
|
}
|
||||||
$privacyConfig = $privacyJson | ConvertFrom-Json
|
$legalAppService = Get-Content -Raw -Encoding UTF8 -LiteralPath $legalAppServiceFiles[0]
|
||||||
if ($privacyConfig.prompt -ne 'template') {
|
if (-not $legalAppService.Contains($ExpectedTermsUrl)) {
|
||||||
throw 'Packaged privacy configuration must enable the DCloud native template prompt.'
|
throw 'Packaged login flow is missing the expected service agreement URL.'
|
||||||
|
}
|
||||||
|
if (-not $legalAppService.Contains($ExpectedPrivacyUrl)) {
|
||||||
|
throw 'Packaged login flow is missing the expected privacy policy URL.'
|
||||||
|
}
|
||||||
|
if (-not $legalAppService.Contains('请先阅读并勾选同意')) {
|
||||||
|
throw 'Packaged login flow is missing the unchecked-consent failure message.'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,13 +8,16 @@ import {
|
|||||||
const projectRoot = resolve(import.meta.dirname, '..');
|
const projectRoot = resolve(import.meta.dirname, '..');
|
||||||
const manifestPath = resolve(projectRoot, 'src/manifest.json');
|
const manifestPath = resolve(projectRoot, 'src/manifest.json');
|
||||||
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
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 checkLegalUrls = process.argv.includes('--check-legal-urls');
|
||||||
const failures = [];
|
const failures = [];
|
||||||
let releaseLegalUrls;
|
let releaseLegalUrls;
|
||||||
|
|
||||||
const fail = (message) => failures.push(message);
|
const fail = (message) => failures.push(message);
|
||||||
const compiledAppRoot = resolve(projectRoot, 'dist/build/app');
|
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 sourceAndroidManifestPath = resolve(projectRoot, 'AndroidManifest.xml');
|
||||||
const compiledAndroidManifestPath = resolve(compiledAppRoot, 'AndroidManifest.xml');
|
const compiledAndroidManifestPath = resolve(compiledAppRoot, 'AndroidManifest.xml');
|
||||||
const sourceNetworkSecurityPath = resolve(
|
const sourceNetworkSecurityPath = resolve(
|
||||||
@@ -129,8 +132,8 @@ const modules = app?.modules;
|
|||||||
if (!Array.isArray(app?.screenOrientation) || !app.screenOrientation.includes('portrait-primary')) {
|
if (!Array.isArray(app?.screenOrientation) || !app.screenOrientation.includes('portrait-primary')) {
|
||||||
fail('app-plus.screenOrientation must include portrait-primary');
|
fail('app-plus.screenOrientation must include portrait-primary');
|
||||||
}
|
}
|
||||||
if (splashscreen?.useOriginalMsgbox !== true) {
|
if (splashscreen?.useOriginalMsgbox !== false) {
|
||||||
fail('app-plus.distribute.splashscreen.useOriginalMsgbox must enable the native privacy prompt');
|
fail('app-plus.distribute.splashscreen.useOriginalMsgbox must disable the native privacy prompt');
|
||||||
}
|
}
|
||||||
if (app?.ssl?.untrustedca !== 'refuse') {
|
if (app?.ssl?.untrustedca !== 'refuse') {
|
||||||
fail('app-plus.ssl.untrustedca must remain 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 privacyPath = resolve(projectRoot, 'src/androidPrivacy.json');
|
||||||
const compiledPrivacyPath = resolve(projectRoot, 'dist/build/app/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') {
|
if (String(process.env.VITE_DEV_SMS_HINT_ENABLED || '').toLowerCase() === 'true') {
|
||||||
fail('VITE_DEV_SMS_HINT_ENABLED must be disabled for release builds');
|
fail('VITE_DEV_SMS_HINT_ENABLED must be disabled for release builds');
|
||||||
}
|
}
|
||||||
@@ -265,45 +268,78 @@ if (requirePrivacy) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!existsSync(privacyPath)) {
|
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 {
|
} else {
|
||||||
try {
|
try {
|
||||||
const privacy = JSON.parse(readFileSync(privacyPath, 'utf8'));
|
const privacy = JSON.parse(readFileSync(privacyPath, 'utf8'));
|
||||||
const privacyLegalUrls = validateAndroidPrivacyDocument(privacy);
|
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;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
fail(`androidPrivacy.json is invalid: ${error instanceof Error ? error.message : String(error)}`);
|
fail(`androidPrivacy.json is invalid: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!existsSync(compiledPrivacyPath)) {
|
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 {
|
} else {
|
||||||
try {
|
try {
|
||||||
const compiledPrivacy = JSON.parse(readFileSync(compiledPrivacyPath, 'utf8'));
|
const compiledPrivacy = JSON.parse(readFileSync(compiledPrivacyPath, 'utf8'));
|
||||||
const compiledLegalUrls = validateAndroidPrivacyDocument(compiledPrivacy);
|
validateAndroidPrivacyDocument(compiledPrivacy);
|
||||||
if (
|
if (
|
||||||
releaseLegalUrls
|
existsSync(privacyPath)
|
||||||
&& (
|
&& JSON.stringify(compiledPrivacy) !== JSON.stringify(JSON.parse(readFileSync(privacyPath, 'utf8')))
|
||||||
compiledLegalUrls.termsUrl !== releaseLegalUrls.termsUrl
|
|
||||||
|| compiledLegalUrls.privacyUrl !== releaseLegalUrls.privacyUrl
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
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) {
|
} catch (error) {
|
||||||
fail(`compiled App androidPrivacy.json is invalid: ${error instanceof Error ? error.message : String(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(
|
verifyAndroidNetworkSecurity(
|
||||||
compiledAndroidManifestPath,
|
compiledAndroidManifestPath,
|
||||||
compiledNetworkSecurityPath,
|
compiledNetworkSecurityPath,
|
||||||
@@ -342,7 +378,7 @@ const checkRemoteLegalPage = async (url, label, expectedText) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (requirePrivacy && checkLegalUrls && releaseLegalUrls) {
|
if (requireLegalConsent && checkLegalUrls && releaseLegalUrls) {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
checkRemoteLegalPage(releaseLegalUrls.termsUrl, 'service agreement URL', /服务协议|用户协议/),
|
checkRemoteLegalPage(releaseLegalUrls.termsUrl, 'service agreement URL', /服务协议|用户协议/),
|
||||||
checkRemoteLegalPage(releaseLegalUrls.privacyUrl, 'privacy policy URL', /隐私政策|个人信息保护/)
|
checkRemoteLegalPage(releaseLegalUrls.privacyUrl, 'privacy policy URL', /隐私政策|个人信息保护/)
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
"appid" : "__UNI__B36D8BE",
|
"appid" : "__UNI__B36D8BE",
|
||||||
"description" : "帮道员工端",
|
"description" : "帮道员工端",
|
||||||
"vueVersion" : "3",
|
"vueVersion" : "3",
|
||||||
"versionName" : "0.1.10",
|
"versionName" : "0.1.11",
|
||||||
"versionCode" : "110",
|
"versionCode" : "111",
|
||||||
"uniStatistics" : {
|
"uniStatistics" : {
|
||||||
"enable" : false
|
"enable" : false
|
||||||
},
|
},
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
},
|
},
|
||||||
"splashscreen" : {
|
"splashscreen" : {
|
||||||
"iosStyle" : "default",
|
"iosStyle" : "default",
|
||||||
"useOriginalMsgbox" : true,
|
"useOriginalMsgbox" : false,
|
||||||
"ios" : {
|
"ios" : {
|
||||||
"iphone" : {
|
"iphone" : {
|
||||||
"retina40" : "src/static/app/splash/ios-iphone-retina40.png",
|
"retina40" : "src/static/app/splash/ios-iphone-retina40.png",
|
||||||
|
|||||||
@@ -94,7 +94,11 @@ test('Android 启动阶段不主动索取设备信息或外部存储权限', asy
|
|||||||
const android = manifest['app-plus']?.distribute?.android;
|
const android = manifest['app-plus']?.distribute?.android;
|
||||||
const modules = manifest['app-plus']?.modules;
|
const modules = manifest['app-plus']?.modules;
|
||||||
|
|
||||||
assert.equal(manifest['app-plus']?.distribute?.splashscreen?.useOriginalMsgbox, true);
|
assert.equal(
|
||||||
|
manifest['app-plus']?.distribute?.splashscreen?.useOriginalMsgbox,
|
||||||
|
false,
|
||||||
|
'受控企业内测不展示 DCloud 原生隐私弹窗,协议同意统一放在登录页'
|
||||||
|
);
|
||||||
assert.equal(android?.permissionPhoneState?.request, 'none');
|
assert.equal(android?.permissionPhoneState?.request, 'none');
|
||||||
assert.equal(android?.permissionExternalStorage?.request, 'none');
|
assert.equal(android?.permissionExternalStorage?.request, 'none');
|
||||||
assert.ok(Object.hasOwn(modules || {}, 'Camera'), '媒体功能仍需按用户操作动态申请相机权限');
|
assert.ok(Object.hasOwn(modules || {}, 'Camera'), '媒体功能仍需按用户操作动态申请相机权限');
|
||||||
@@ -133,7 +137,7 @@ test('正式法务地址必须使用不同的公网 HTTPS 页面', () => {
|
|||||||
));
|
));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Android 隐私模板的首次与二次弹窗使用同一组最终链接', async () => {
|
test('Android 原生隐私配置明确关闭弹窗,法务地址仍须通过登录页构建校验', async () => {
|
||||||
const template = await readFile(new URL('../androidPrivacy.json.example', import.meta.url), 'utf8');
|
const template = await readFile(new URL('../androidPrivacy.json.example', import.meta.url), 'utf8');
|
||||||
const rendered = renderAndroidPrivacy(
|
const rendered = renderAndroidPrivacy(
|
||||||
template,
|
template,
|
||||||
@@ -143,13 +147,18 @@ test('Android 隐私模板的首次与二次弹窗使用同一组最终链接',
|
|||||||
const privacy = JSON.parse(rendered);
|
const privacy = JSON.parse(rendered);
|
||||||
|
|
||||||
assert.deepEqual(validateAndroidPrivacyDocument(privacy), {
|
assert.deepEqual(validateAndroidPrivacyDocument(privacy), {
|
||||||
termsUrl: 'https://legal.company.cn/bangdao/terms',
|
prompt: 'none',
|
||||||
privacyUrl: 'https://legal.company.cn/bangdao/privacy'
|
version: '20260729'
|
||||||
});
|
});
|
||||||
assert.doesNotMatch(rendered, /__TERMS_URL__|__PRIVACY_URL__/);
|
assert.doesNotMatch(rendered, /服务协议|隐私政策|buttonAccept|buttonRefuse|second/);
|
||||||
|
assert.throws(
|
||||||
privacy.second.message = privacy.second.message.replace('/privacy', '/other-privacy');
|
() => validateAndroidPrivacyDocument({ ...privacy, prompt: 'template' }),
|
||||||
assert.throws(() => validateAndroidPrivacyDocument(privacy), /must use the same legal links/);
|
/prompt=none/
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() => validateAndroidPrivacyDocument({ ...privacy, message: 'unexpected native prompt' }),
|
||||||
|
/must not contain message/
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Android 隐私配置写入 uni-app CLI 输入目录并校验编译产物', async () => {
|
test('Android 隐私配置写入 uni-app CLI 输入目录并校验编译产物', async () => {
|
||||||
@@ -164,6 +173,7 @@ test('Android 隐私配置写入 uni-app CLI 输入目录并校验编译产物',
|
|||||||
|
|
||||||
assert.match(configureScript, /resolve\(projectRoot, 'src', 'androidPrivacy\.json'\)/);
|
assert.match(configureScript, /resolve\(projectRoot, 'src', 'androidPrivacy\.json'\)/);
|
||||||
assert.match(verifier, /dist\/build\/app\/androidPrivacy\.json/);
|
assert.match(verifier, /dist\/build\/app\/androidPrivacy\.json/);
|
||||||
|
assert.match(verifier, /compiledManifest\.plus\?\.distribute\?\.splashscreen\?\.useOriginalMsgbox !== false/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('App 登录页法务地址校验不依赖浏览器 URL 构造器', async () => {
|
test('App 登录页法务地址校验不依赖浏览器 URL 构造器', async () => {
|
||||||
@@ -187,6 +197,10 @@ test('APK 门禁区分自定义调试基座与内置业务资源的测试包', a
|
|||||||
assert.match(verifier, /assets\\apps/);
|
assert.match(verifier, /assets\\apps/);
|
||||||
assert.match(verifier, /bundled androidPrivacy\.json/);
|
assert.match(verifier, /bundled androidPrivacy\.json/);
|
||||||
assert.match(verifier, /DCloud custom base APK contains only the native debug runtime/);
|
assert.match(verifier, /DCloud custom base APK contains only the native debug runtime/);
|
||||||
|
assert.match(verifier, /prompt=none/);
|
||||||
|
assert.match(verifier, /Packaged login flow is missing the expected service agreement URL/);
|
||||||
|
assert.match(verifier, /Packaged login flow is missing the expected privacy policy URL/);
|
||||||
|
assert.match(verifier, /Packaged login flow is missing the unchecked-consent failure message/);
|
||||||
assert.match(verifier, /dist\\build\\app\\app-service\.js/);
|
assert.match(verifier, /dist\\build\\app\\app-service\.js/);
|
||||||
assert.match(verifier, /dist\\build\\app-plus\\app-service\.js/);
|
assert.match(verifier, /dist\\build\\app-plus\\app-service\.js/);
|
||||||
assert.match(verifier, /fixed test SMS code/);
|
assert.match(verifier, /fixed test SMS code/);
|
||||||
@@ -209,5 +223,5 @@ test('8 月 1 日 RC 脚本默认拒绝脏工作区且记录完整构建证据',
|
|||||||
assert.match(script, /AllowDirtyRehearsal/);
|
assert.match(script, /AllowDirtyRehearsal/);
|
||||||
assert.match(script, /RC build changed the previously clean worktree/);
|
assert.match(script, /RC build changed the previously clean worktree/);
|
||||||
assert.match(script, /releaseEligible = \$releaseEligible/);
|
assert.match(script, /releaseEligible = \$releaseEligible/);
|
||||||
assert.match(script, /APK metadata, legal links, signature, content match and sensitive-value scan/);
|
assert.match(script, /APK metadata, login legal links, no native privacy prompt, signature, content match and sensitive-value scan/);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ Remove-Item Env:VITE_DEV_SMS_HINT_VALUE -ErrorAction SilentlyContinue
|
|||||||
if (-not $SkipBuild) {
|
if (-not $SkipBuild) {
|
||||||
Invoke-Checked -FilePath 'npm' -Arguments @('--prefix', 'mobile-uni', 'run', 'test:unit')
|
Invoke-Checked -FilePath 'npm' -Arguments @('--prefix', 'mobile-uni', 'run', 'test:unit')
|
||||||
Invoke-Checked -FilePath 'npm' -Arguments @('--prefix', 'mobile-uni', 'run', 'typecheck')
|
Invoke-Checked -FilePath 'npm' -Arguments @('--prefix', 'mobile-uni', 'run', 'typecheck')
|
||||||
Invoke-Checked -FilePath 'npm' -Arguments @('--prefix', 'mobile-uni', 'run', 'configure:app-privacy')
|
Invoke-Checked -FilePath 'npm' -Arguments @('--prefix', 'mobile-uni', 'run', 'configure:app-legal')
|
||||||
Invoke-Checked -FilePath 'npm' -Arguments @('--prefix', 'mobile-uni', 'run', 'build:h5')
|
Invoke-Checked -FilePath 'npm' -Arguments @('--prefix', 'mobile-uni', 'run', 'build:h5')
|
||||||
Invoke-Checked -FilePath 'npm' -Arguments @('--prefix', 'mobile-uni', 'run', 'build:app')
|
Invoke-Checked -FilePath 'npm' -Arguments @('--prefix', 'mobile-uni', 'run', 'build:app')
|
||||||
Invoke-Checked -FilePath 'npm' -Arguments @('--prefix', 'mobile-uni', 'run', 'verify:app-release')
|
Invoke-Checked -FilePath 'npm' -Arguments @('--prefix', 'mobile-uni', 'run', 'verify:app-release')
|
||||||
@@ -212,11 +212,11 @@ $evidence = [ordered]@{
|
|||||||
'frontend production build',
|
'frontend production build',
|
||||||
'ruoyi-aihr full test suite',
|
'ruoyi-aihr full test suite',
|
||||||
'backend package',
|
'backend package',
|
||||||
'APK metadata, legal links, signature, content match and sensitive-value scan'
|
'APK metadata, login legal links, no native privacy prompt, signature, content match and sensitive-value scan'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
manualGatesRemaining = @(
|
manualGatesRemaining = @(
|
||||||
'privacy reject and consent choices',
|
'login legal links and unchecked-consent failure on Android',
|
||||||
'login error and re-login',
|
'login error and re-login',
|
||||||
'microphone allow and deny',
|
'microphone allow and deny',
|
||||||
'media cancellation',
|
'media cancellation',
|
||||||
|
|||||||
Reference in New Issue
Block a user