diff --git a/scripts/generate-brand-assets.mjs b/scripts/generate-brand-assets.mjs index 65833defdc..1e9cea18a0 100644 --- a/scripts/generate-brand-assets.mjs +++ b/scripts/generate-brand-assets.mjs @@ -54,17 +54,17 @@ const LEGACY_APPLICATION_ASSETS = [ 'OpenBitFun-Installer/src-tauri/icons/icon.ico', 'OpenBitFun-Installer/src-tauri/icons/icon.icns', 'src/apps/mobile/harmonyos/AppScope/resources/base/media/openbitfun_icon.png', + 'src/apps/mobile/harmonyos/AppScope/resources/base/media/openbitfun-app-icon.png', 'src/apps/mobile/harmonyos/AppScope/resources/base/media/background.png', 'src/apps/mobile/harmonyos/AppScope/resources/base/media/foreground.png', 'src/apps/mobile/harmonyos/AppScope/resources/base/media/layered_image.json', - 'src/apps/mobile/harmonyos/AppScope/resources/base/media/openbitfun-app-icon.png', 'src/apps/mobile/harmonyos/entry/src/main/resources/base/media/openbitfun_icon.png', + 'src/apps/mobile/harmonyos/entry/src/main/resources/base/media/openbitfun-app-icon.png', + 'src/apps/mobile/harmonyos/entry/src/main/resources/base/media/openbitfun-start-window.png', 'src/apps/mobile/harmonyos/entry/src/main/resources/base/media/background.png', 'src/apps/mobile/harmonyos/entry/src/main/resources/base/media/foreground.png', 'src/apps/mobile/harmonyos/entry/src/main/resources/base/media/layered_image.json', 'src/apps/mobile/harmonyos/entry/src/main/resources/base/media/startIcon.png', - 'src/apps/mobile/harmonyos/entry/src/main/resources/base/media/openbitfun-app-icon.png', - 'src/apps/mobile/harmonyos/entry/src/main/resources/base/media/openbitfun-start-window.png', 'src/apps/mobile/ios/OpenBitFun/Resources.xcassets/AppIcon.appiconset/openbitfun_icon.png', 'src/apps/mobile/ios/OpenBitFun/Resources.xcassets/OpenBitFunLogo.imageset', 'src/apps/relay-server/static/assets/Logo-ICON-BOaKcXgO.png', diff --git a/scripts/generate-brand-assets.test.mjs b/scripts/generate-brand-assets.test.mjs index 8d705a8a45..733fa7aa7d 100644 --- a/scripts/generate-brand-assets.test.mjs +++ b/scripts/generate-brand-assets.test.mjs @@ -1,5 +1,5 @@ import assert from 'node:assert/strict'; -import { readFileSync } from 'node:fs'; +import { readFileSync, readdirSync } from 'node:fs'; import test from 'node:test'; import { canonicalizeIcns } from './icns-container.mjs'; @@ -8,6 +8,11 @@ const GENERATED_ICNS_FILES = [ 'OpenBitFun-Installer/src-tauri/icons/openbitfun-app-icon.icns', ]; +const HARMONY_MEDIA_DIRS = [ + 'src/apps/mobile/harmonyos/AppScope/resources/base/media', + 'src/apps/mobile/harmonyos/entry/src/main/resources/base/media', +]; + function createChunk(type, payload) { const chunk = Buffer.alloc(8 + payload.length); chunk.write(type, 0, 4, 'ascii'); @@ -45,6 +50,25 @@ test('generated macOS icons use the canonical ICNS layout', () => { assert.ok(desktop.equals(installer), 'desktop and installer ICNS files differ'); }); +test('HarmonyOS generated media use valid resource identifiers', () => { + for (const directory of HARMONY_MEDIA_DIRS) { + for (const fileName of readdirSync(directory)) { + const resourceName = fileName.replace(/\.[^.]+$/, ''); + assert.match( + resourceName, + /^[a-zA-Z0-9_]+$/, + `${directory}/${fileName} is not a valid HarmonyOS resource name`, + ); + } + } + + const appConfig = readFileSync('src/apps/mobile/harmonyos/AppScope/app.json5', 'utf8'); + const moduleConfig = readFileSync('src/apps/mobile/harmonyos/entry/src/main/module.json5', 'utf8'); + assert.match(appConfig, /\$media:openbitfun_app_icon/); + assert.match(moduleConfig, /\$media:openbitfun_app_icon/); + assert.match(moduleConfig, /\$media:openbitfun_start_window/); +}); + test('ICNS canonicalization rejects malformed containers', () => { assert.throws( () => canonicalizeIcns(Buffer.from('not-an-icns')), diff --git a/src/apps/mobile/harmonyos/entry/src/main/ets/services/ConnectScanDecisionPolicy.ets b/src/apps/mobile/harmonyos/entry/src/main/ets/services/ConnectScanDecisionPolicy.ets index 241801fc26..a47846f55a 100644 --- a/src/apps/mobile/harmonyos/entry/src/main/ets/services/ConnectScanDecisionPolicy.ets +++ b/src/apps/mobile/harmonyos/entry/src/main/ets/services/ConnectScanDecisionPolicy.ets @@ -20,16 +20,20 @@ export class DetectedRemoteUrlResult { * What a scanned pair URL should do next. Applying the URL and starting a * connect are separate steps: the detector never connects by itself. * - * A OpenBitFun cloud session on this phone is already the account proof. An - * account-auth QR for that same user must not ask for the password again; - * it continues through the same-account device path instead of room pairing. + * An OpenBitFun cloud session on this phone is already the account proof. The + * account device directory is the authoritative membership check for a QR + * target; display usernames are not, because aliases and restored legacy + * records can legitimately differ. Reuse is still limited to the same relay, + * then device selection verifies the scanned desktop id through the + * authenticated directory instead of asking for the password based on a + * string mismatch. */ export class ConnectScanDecisionPolicy { static decide( accountAuth: boolean, hasCloudAccountSession: boolean, - cloudUsername: string, - qrUsername: string, + _cloudUsername: string, + _qrUsername: string, desktopId: string, cloudRelayUrl: string = '', qrRelayUrl: string = '' @@ -38,7 +42,6 @@ export class ConnectScanDecisionPolicy { return DetectedUrlAction.PAIR_NOW; } if (hasCloudAccountSession && - ConnectScanDecisionPolicy.usernamesCompatible(cloudUsername, qrUsername) && ConnectScanDecisionPolicy.relayEndpointsCompatible(cloudRelayUrl, qrRelayUrl)) { return desktopId.trim().length > 0 ? DetectedUrlAction.USE_CLOUD_DEVICE : @@ -47,14 +50,6 @@ export class ConnectScanDecisionPolicy { return DetectedUrlAction.PROMPT_ACCOUNT_PASSWORD; } - static usernamesCompatible(cloudUsername: string, qrUsername: string): boolean { - const qr = qrUsername.trim(); - if (qr.length === 0) { - return true; - } - return cloudUsername.trim() === qr; - } - static relayEndpointsCompatible(cloudRelayUrl: string, qrRelayUrl: string): boolean { const cloud = ConnectScanDecisionPolicy.normalizedRelayEndpoint(cloudRelayUrl); const qr = ConnectScanDecisionPolicy.normalizedRelayEndpoint(qrRelayUrl); diff --git a/src/apps/mobile/harmonyos/entry/src/test/ConversationPresentationUnit.test.ets b/src/apps/mobile/harmonyos/entry/src/test/ConversationPresentationUnit.test.ets index 90f59c0b3d..d628e4fee8 100644 --- a/src/apps/mobile/harmonyos/entry/src/test/ConversationPresentationUnit.test.ets +++ b/src/apps/mobile/harmonyos/entry/src/test/ConversationPresentationUnit.test.ets @@ -775,9 +775,17 @@ export default function conversationPresentationUnitTest() { )).assertEqual(DetectedUrlAction.PROMPT_ACCOUNT_PASSWORD); }); - it('still asks for a password when the QR belongs to a different account', 0, () => { - expect(ConnectScanDecisionPolicy.decide(true, true, 'alice', 'bob', 'desktop-1')) - .assertEqual(DetectedUrlAction.PROMPT_ACCOUNT_PASSWORD); + it('uses the authenticated device directory instead of display-name equality', 0, () => { + expect(ConnectScanDecisionPolicy.decide( + true, true, 'alice', 'bob', 'desktop-1', + 'https://relay.example.com', 'wss://relay.example.com/ws' + )) + .assertEqual(DetectedUrlAction.USE_CLOUD_DEVICE); + expect(ConnectScanDecisionPolicy.decide( + true, true, '', 'renamed-account', 'desktop-1', + 'https://relay.example.com', 'https://relay.example.com/' + )) + .assertEqual(DetectedUrlAction.USE_CLOUD_DEVICE); }); }); diff --git a/src/apps/mobile/harmonyos/entry/src/test/TransportAndGeneralChatUnit.test.ets b/src/apps/mobile/harmonyos/entry/src/test/TransportAndGeneralChatUnit.test.ets index e084103637..0d21063cd7 100644 --- a/src/apps/mobile/harmonyos/entry/src/test/TransportAndGeneralChatUnit.test.ets +++ b/src/apps/mobile/harmonyos/entry/src/test/TransportAndGeneralChatUnit.test.ets @@ -881,7 +881,7 @@ export default function transportAndGeneralChatUnitTest() { expect(policy.shouldPromptForAccount(descriptor)).assertTrue(); expect(policy.shouldPromptForAccount(descriptor, true, 'alice', descriptor.relayUrl)).assertFalse(); - expect(policy.shouldPromptForAccount(descriptor, true, 'bob', descriptor.relayUrl)).assertTrue(); + expect(policy.shouldPromptForAccount(descriptor, true, 'bob', descriptor.relayUrl)).assertFalse(); }); });