Browse Source

fix(frontend): improve dense QR readability (#6396)

Dense AmneziaWG configs crossed a QR version boundary at the fixed display size, and the generated symbol had no quiet zone. Use low error correction and a four-module margin to reduce module density while keeping the payload unchanged.
dawn 9 hours ago
parent
commit
04e8458054

+ 2 - 0
frontend/src/pages/inbounds/qr/QrPanel.tsx

@@ -140,6 +140,8 @@ export default function QrPanel({
               className="qr-code"
               value={value}
               size={size}
+              errorLevel="L"
+              marginSize={4}
               type="svg"
               bordered={false}
               color="#000000"

+ 89 - 0
frontend/src/test/qr-panel-readable.test.tsx

@@ -0,0 +1,89 @@
+import { render } from '@testing-library/react';
+import { describe, expect, it } from 'vitest';
+
+import { genAmneziaWGConfig } from '@/lib/xray/inbound-link';
+import QrPanel from '@/pages/inbounds/qr/QrPanel';
+import { AmneziawgInboundSettingsSchema } from '@/schemas/protocols/inbound/amneziawg';
+
+const KEY = `${'A'.repeat(43)}=`;
+
+function awgConfig(disableCookies: boolean): string {
+  const settings = AmneziawgInboundSettingsSchema.parse({
+    server: {
+      publicKey: KEY,
+      mtu: 1420,
+      primaryDns: '8.8.8.8',
+      secondaryDns: '8.8.4.4',
+      jc: 4,
+      jmin: 65,
+      jmax: 220,
+      s1: 87,
+      s2: 44,
+      s3: 21,
+      s4: 19,
+      h1: '462980921-463150218',
+      h2: '1177681572-1177787900',
+      h3: '1907413509-1907903969',
+      h4: '2029908558-2030313135',
+      i1: '<r 148>',
+      headerProtectionKey: KEY,
+      contentPaddingAddition: '17-49',
+      rekeyAfterTime: '111-139',
+      rekeyTimeout: '4-7',
+      rejectAfterTime: '187-251',
+      keepaliveTimeout: '9-14',
+      maxHandshakeAttempts: '19-36',
+      randomTrailers: true,
+      disableCookies,
+    },
+    clients: [
+      {
+        email: 'my-client',
+        privateKey: KEY,
+        preSharedKey: KEY,
+        allowedIPs: ['10.8.1.2/32'],
+        keepAlive: 25,
+      },
+    ],
+  });
+
+  return genAmneziaWGConfig({
+    settings,
+    address: 'your-server.example.com',
+    port: 443,
+    remark: 'my-client',
+    peerIndex: 0,
+  });
+}
+
+function qrGeometry(value: string): { viewBox: string; foreground: string } {
+  const { container } = render(<QrPanel value={value} />);
+  const svg = container.querySelector('.qr-code svg');
+  const paths = svg?.querySelectorAll('path');
+
+  expect(svg).not.toBeNull();
+  expect(paths).toHaveLength(2);
+
+  return {
+    viewBox: svg?.getAttribute('viewBox') ?? '',
+    foreground: paths?.item(1).getAttribute('d') ?? '',
+  };
+}
+
+describe('QrPanel dense AmneziaWG config', () => {
+  it('keeps the complete 3.1 config readable across the DisableCookies QR boundary', () => {
+    const complete = awgConfig(true);
+    const withoutDisableCookies = awgConfig(false);
+
+    expect(complete).toContain('DisableCookies = on\n');
+    expect(complete.length - withoutDisableCookies.length).toBe(20);
+
+    const completeQr = qrGeometry(complete);
+    const shorterQr = qrGeometry(withoutDisableCookies);
+
+    expect(completeQr.viewBox).toBe('0 0 105 105');
+    expect(shorterQr.viewBox).toBe('0 0 101 101');
+    expect(completeQr.foreground).toMatch(/^M4 4h7/);
+    expect(shorterQr.foreground).toMatch(/^M4 4h7/);
+  });
+});