Skip to content

Underline sign-in Licenses link in high-contrast themes (WCAG 1.4.1)#96618

Open
MelvinBot wants to merge 6 commits into
mainfrom
claude-licensesLinkUnderlineHighContrast
Open

Underline sign-in Licenses link in high-contrast themes (WCAG 1.4.1)#96618
MelvinBot wants to merge 6 commits into
mainfrom
claude-licensesLinkUnderlineHighContrast

Conversation

@MelvinBot

Copy link
Copy Markdown
Contributor

Explanation of Change

On the sign-in page, the Licenses link in the footer was distinguished from the surrounding muted text only by color, which fails WCAG 1.4.1 (Use of Color). Per the discussion on the issue, we only want to address this in high-contrast themes (light-contrast / dark-contrast), and only for the Licenses link.

This PR:

  • Adds a reusable underline style (textDecorationLine: 'underline') to src/styles/utils/textDecorationLine.ts.
  • Teaches AnchorRenderer to honor a link-underline class on anchors and apply the underline, following the same class-based pattern already used for no-style-link. This keeps the underline opt-in — no other links are affected.
  • In Licenses.tsx, detects high-contrast mode (mirroring the sibling HighContrastModeSwitcher, which uses SIGN_IN_HIGH_CONTRAST_INTENT for the logged-out flow) and, only when high contrast is active, injects the link-underline class onto the Licenses anchor.

The shared termsOfUse.license translation string and the global link style are intentionally left untouched, so the About page and all other links are unaffected.

Fixed Issues

$ #76919
PROPOSAL: #76919 (comment)

Tests

  • Verify that no errors appear in the JS console

Offline tests

QA Steps

// TODO: These must be filled out, or the issue title must include "[No QA]."

  • Verify that no errors appear in the JS console

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I followed proper code patterns (see Reviewing the code)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari

Co-authored-by: Pujan Shah <Pujan92@users.noreply.github.com>
@MelvinBot
MelvinBot requested a review from a team July 21, 2026 10:36
@MelvinBot

Copy link
Copy Markdown
Contributor Author

I created this PR from an upstream branch since I don't have push access to your fork.

To take ownership of this branch and be able to push updates, run:

git remote add upstream https://github.com/Expensify/App.git
git fetch upstream claude-licensesLinkUnderlineHighContrast
git checkout -b claude-licensesLinkUnderlineHighContrast upstream/claude-licensesLinkUnderlineHighContrast
git push -u origin claude-licensesLinkUnderlineHighContrast

Then you can close this PR and open a new one from your fork.

@codecov

codecov Bot commented Jul 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Looks like you've decreased code coverage for some files. Please write tests to increase, or at least maintain, the existing level of code coverage. See our documentation here for how to interpret this table.

Files with missing lines Coverage Δ
src/hooks/useIsHighContrast.ts 100.00% <100.00%> (ø)
...TMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx 73.13% <66.66%> (-0.31%) ⬇️
... and 248 files with indirect coverage changes

Comment thread src/pages/signin/Licenses.tsx Outdated
<Text style={[styles.textExtraSmallSupporting, styles.mb4]}>{`© ${currentYear} Expensify`}</Text>
<View style={[styles.renderHTML, styles.flexRow]}>
<RenderHTML html={`<muted-text-xs>${translate('termsOfUse.license')}</muted-text-xs>`} />
<RenderHTML html={`<muted-text-xs>${licenseHTML}</muted-text-xs>`} />

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MelvinBot instead of doing this, can you separate translation for licenseLink and conditionally apply the underline style based on high contrast. Need to revert all unnecessary current changes

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MelvinBot something like

<RenderHTML html={`<muted-text-xs>${translate('termsOfUse.license')} <a class="isHighContrast ? link-underline : ''" href="${CONST.OLD_DOT_PUBLIC_URLS.LICENSES_URL}">${licenseLink}</a>.</muted-text-xs>`} />

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — reworked as requested and pushed to this branch.

What changed

  • Split the link out into its own translation. termsOfUse.license is now a function that interpolates a separate termsOfUse.licenseLink, applied across all 10 locales.
  • Underline is applied conditionally, only in high contrast: Licenses.tsx:30 passes underline: isHighContrast into licenseLink, which wraps the link text in <u> when set.
  • Reverted the unnecessary changes: the generic link-underline handling in AnchorRenderer and the new underline style in textDecorationLine.ts are both gone.
  • AboutPage (the only other consumer of license) now passes the plain link (underline: false), so it's unaffected.

One note on the mechanism: I applied the underline via a <u> wrapper inside the anchor rather than a style/class on the link. The HTML engine is configured with enableCSSInlineProcessing={false} and AnchorRenderer rebuilds link styles from scratch, so an inline style or a class wouldn't take effect without editing that shared renderer — which is exactly what we're reverting. <u> keeps the change fully contained to the translation + the sign-in page.

Typecheck and lint pass. Let me know if you'd prefer a different shape for the split.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pushed code already does what you're after — separate licenseLink translation, underline applied conditionally on high contrast, and the shared-renderer changes reverted. It's rendered through <muted-text-xs> exactly as in your snippet. The one deliberate difference is how the underline is applied, and I'd push back on the class="link-underline" part for two concrete reasons:

1. The link-underline class would be a no-op here. AnchorRenderer rebuilds the link style from scratch for sign-in links and only honors one class — no-style-link. Inline styles/classes don't flow through either, because enableCSSInlineProcessing={false}. So link-underline only works if I re-add handling to that shared renderer — which is exactly the change you asked me to revert.

2. The link can't be appended in JSX — it has to be interpolated inside the translation. Your snippet puts <a>…</a>. after translate('termsOfUse.license'), which assumes the link is always last. It isn't: in ja.ts:3050 the link sits mid-sentence (…その${licenseLink}に基づき…), and word order differs across the other locales too. Keeping license as a function that interpolates licenseLink is what keeps all 10 locales correct.

So instead I kept everything contained to the translation + sign-in page: Licenses.tsx:30 passes underline: isHighContrast into termsOfUse.licenseLink, which wraps just the link text in <u> when high contrast is on. Since styles.link sets no textDecorationLine, this is the only thing that draws the underline, and only in high contrast.

If you'd rather go the class route, I'm happy to — but that means re-introducing link-underline support in AnchorRenderer, so just confirm you're OK with touching that shared renderer and I'll switch it over.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — switched to the AnchorRenderer class approach and pushed to this branch.

What changed

  • Reverted the licenseLink translation split. termsOfUse.license is a plain string again with the inline <a>, across all 10 locales, and the LicenseLinkParams / LicenseUnderlineParams types are gone.
  • Re-added class handling in AnchorRenderer: links with the link-underline class get styles.underline appended (needed because styles.link sets textDecorationLine: 'none'), following the same pattern as no-style-link.
  • Licenses.tsx injects class="link-underline" onto the link only when high contrast is active, so the HTML-rendered link gets the underline — exactly as you described.
  • Re-added a reusable underline style in textDecorationLine.ts.
  • AboutPage (the only other consumer) is back to the plain translate('termsOfUse.license'), so it's unaffected.

npm run fmt, typecheck, and lint pass on the changed files.

One heads-up: react-compiler-compliance-check flags AboutPage.tsx (refs accessed during render / missing memoization deps around popoverAnchor and overlayContent). I confirmed this is pre-existing — the committed version before my change fails with the identical errors, and it's in code I didn't touch. My edit doesn't regress it.

@MelvinBot

Copy link
Copy Markdown
Contributor Author

I looked into the failing checks — no code fix is needed. Every code/CI check is green (typecheck, ESLint, Jest, Bun, Storybook, knip, unused-styles, oxfmt, React Compiler, etc.).

The only two red checks are:

  • PR Author Checklist / checklist
  • PR Reviewer Checklist / checklist

Both fail for the same reason — not a code problem:

⚠️ PR Author Checklist is not completely filled out. Please check every box to verify you've thought about the item.

These are resolved by a human ticking the checklist boxes in the PR description / review, not by pushing a commit. Once the author checklist is completed (and a reviewer completes theirs), those checks will pass. Pushing code would only re-run them without resolving them, so I haven't made any changes.

(The Reassure performance tests were still running when I checked; a static-underline style change can't affect them, and they're non-blocking regardless.)

@OSBotify

Copy link
Copy Markdown
Contributor

🦜 Polyglot Parrot! 🦜

Squawk! Looks like you added some shiny new English strings. Allow me to parrot them back to you in other tongues:

View the translation diff
diff --git a/src/languages/de.ts b/src/languages/de.ts
index 9eb95575e3e..ca32932404a 100644
--- a/src/languages/de.ts
+++ b/src/languages/de.ts
@@ -3084,7 +3084,7 @@ ${amount} für ${merchant} – ${date}`,
     termsOfUse: {
         terms: `<muted-text-xs>Durch das Einloggen stimmst du den <a href="${CONST.OLD_DOT_PUBLIC_URLS.TERMS_URL}">Nutzungsbedingungen</a> und der <a href="${CONST.OLD_DOT_PUBLIC_URLS.PRIVACY_URL}">Datenschutzerklärung</a> zu.</muted-text-xs>`,
         license: ({licenseLink}: LicenseLinkParams) =>
-            `Die Geldübermittlung wird von ${CONST.WALLET.PROGRAM_ISSUERS.EXPENSIFY_PAYMENTS} (NMLS-ID:2017010) gemäß seinen ${licenseLink} bereitgestellt.`,
+            `Die Geldübermittlung wird von ${CONST.WALLET.PROGRAM_ISSUERS.EXPENSIFY_PAYMENTS} (NMLS-ID:2017010) gemäß ihrer ${licenseLink} bereitgestellt.`,
         licenseLink: ({underline}: LicenseUnderlineParams) => `<a href="${CONST.OLD_DOT_PUBLIC_URLS.LICENSES_URL}">${underline ? '<u>Lizenzen</u>' : 'Lizenzen'}</a>`,
     },
     validateCodeForm: {
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 84dc6e8471b..a178ac26936 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -2950,7 +2950,7 @@ ${amount} para ${merchant} - ${date}`,
     termsOfUse: {
         terms: `<muted-text-xs>Al iniciar sesión, estás accediendo a los <a href="${CONST.OLD_DOT_PUBLIC_URLS.TERMS_URL}">Términos de Servicio</a> y <a href="${CONST.OLD_DOT_PUBLIC_URLS.PRIVACY_URL}">Privacidad</a>.</muted-text-xs>`,
         license: ({licenseLink}: LicenseLinkParams) =>
-            `El envío de dinero es brindado por ${CONST.WALLET.PROGRAM_ISSUERS.EXPENSIFY_PAYMENTS} (NMLS ID:2017010) de conformidad con sus ${licenseLink}.`,
+            `La transmisión de dinero es proporcionada por ${CONST.WALLET.PROGRAM_ISSUERS.EXPENSIFY_PAYMENTS} (NMLS ID:2017010) de acuerdo con su ${licenseLink}.`,
         licenseLink: ({underline}: LicenseUnderlineParams) => `<a href="${CONST.OLD_DOT_PUBLIC_URLS.LICENSES_URL}">${underline ? '<u>licencias</u>' : 'licencias'}</a>`,
     },
     validateCodeForm: {
diff --git a/src/languages/fr.ts b/src/languages/fr.ts
index 93837d28a22..b929fbe2146 100644
--- a/src/languages/fr.ts
+++ b/src/languages/fr.ts
@@ -3090,7 +3090,7 @@ ${amount} pour ${merchant} - ${date}`,
     termsOfUse: {
         terms: `<muted-text-xs>En vous connectant, vous acceptez les <a href="${CONST.OLD_DOT_PUBLIC_URLS.TERMS_URL}">Conditions d'utilisation</a> et la <a href="${CONST.OLD_DOT_PUBLIC_URLS.PRIVACY_URL}">Politique de confidentialité</a>.</muted-text-xs>`,
         license: ({licenseLink}: LicenseLinkParams) =>
-            `Les services de transfert d’argent sont fournis par ${CONST.WALLET.PROGRAM_ISSUERS.EXPENSIFY_PAYMENTS} (ID NMLS :2017010) conformément à ses ${licenseLink}.`,
+            `La transmission de fonds est fournie par ${CONST.WALLET.PROGRAM_ISSUERS.EXPENSIFY_PAYMENTS} (ID NMLS : 2017010) conformément à sa ${licenseLink}.`,
         licenseLink: ({underline}: LicenseUnderlineParams) => `<a href="${CONST.OLD_DOT_PUBLIC_URLS.LICENSES_URL}">${underline ? '<u>licences</u>' : 'licences'}</a>`,
     },
     validateCodeForm: {
diff --git a/src/languages/it.ts b/src/languages/it.ts
index 9ed2c235070..ac1a705bdfd 100644
--- a/src/languages/it.ts
+++ b/src/languages/it.ts
@@ -3077,7 +3077,7 @@ ${amount} per ${merchant} - ${date}`,
     termsOfUse: {
         terms: `<muted-text-xs>Accedendo, accetti i <a href="${CONST.OLD_DOT_PUBLIC_URLS.TERMS_URL}">Termini di servizio</a> e l’<a href="${CONST.OLD_DOT_PUBLIC_URLS.PRIVACY_URL}">Informativa sulla privacy</a>.</muted-text-xs>`,
         license: ({licenseLink}: LicenseLinkParams) =>
-            `L’attività di trasmissione di denaro è fornita da ${CONST.WALLET.PROGRAM_ISSUERS.EXPENSIFY_PAYMENTS} (NMLS ID:2017010) in conformità alle sue ${licenseLink}.`,
+            `Il servizio di trasferimento di denaro è fornito da ${CONST.WALLET.PROGRAM_ISSUERS.EXPENSIFY_PAYMENTS} (NMLS ID:2017010) in conformità alla sua ${licenseLink}.`,
         licenseLink: ({underline}: LicenseUnderlineParams) => `<a href="${CONST.OLD_DOT_PUBLIC_URLS.LICENSES_URL}">${underline ? '<u>licenze</u>' : 'licenze'}</a>`,
     },
     validateCodeForm: {
diff --git a/src/languages/ja.ts b/src/languages/ja.ts
index 506b05cab9e..1ad814a80af 100644
--- a/src/languages/ja.ts
+++ b/src/languages/ja.ts
@@ -3047,8 +3047,8 @@ ${date} の ${merchant} への ${amount}`,
     termsOfUse: {
         terms: `<muted-text-xs>ログインすると、<a href="${CONST.OLD_DOT_PUBLIC_URLS.TERMS_URL}">利用規約</a>および<a href="${CONST.OLD_DOT_PUBLIC_URLS.PRIVACY_URL}">プライバシーポリシー</a>に同意したものとみなされます。</muted-text-xs>`,
         license: ({licenseLink}: LicenseLinkParams) =>
-            `資金移動サービスは、その${licenseLink}に基づき、${CONST.WALLET.PROGRAM_ISSUERS.EXPENSIFY_PAYMENTS}(NMLS ID:2017010)によって提供されています。`,
-        licenseLink: ({underline}: LicenseUnderlineParams) => `<a href="${CONST.OLD_DOT_PUBLIC_URLS.LICENSES_URL}">${underline ? '<u>認可</u>' : '認可'}</a>`,
+            `送金サービスは、その${licenseLink}に基づき、${CONST.WALLET.PROGRAM_ISSUERS.EXPENSIFY_PAYMENTS}(NMLS ID:2017010)によって提供されます。`,
+        licenseLink: ({underline}: LicenseUnderlineParams) => `<a href="${CONST.OLD_DOT_PUBLIC_URLS.LICENSES_URL}">${underline ? '<u>ライセンス</u>' : 'ライセンス'}</a>`,
     },
     validateCodeForm: {
         magicCodeNotReceived: 'マジックコードを受け取っていませんか?',
diff --git a/src/languages/nl.ts b/src/languages/nl.ts
index 8fdeb5fa398..cbdfafae50f 100644
--- a/src/languages/nl.ts
+++ b/src/languages/nl.ts
@@ -3076,7 +3076,7 @@ ${amount} voor ${merchant} - ${date}`,
     termsOfUse: {
         terms: `<muted-text-xs>Door in te loggen ga je akkoord met de <a href="${CONST.OLD_DOT_PUBLIC_URLS.TERMS_URL}">Servicevoorwaarden</a> en het <a href="${CONST.OLD_DOT_PUBLIC_URLS.PRIVACY_URL}">Privacybeleid</a>.</muted-text-xs>`,
         license: ({licenseLink}: LicenseLinkParams) =>
-            `Geldtransfers worden verzorgd door ${CONST.WALLET.PROGRAM_ISSUERS.EXPENSIFY_PAYMENTS} (NMLS ID:2017010) volgens diens ${licenseLink}.`,
+            `Geldtransmissie wordt aangeboden door ${CONST.WALLET.PROGRAM_ISSUERS.EXPENSIFY_PAYMENTS} (NMLS-ID:2017010) overeenkomstig de voorwaarden van de ${licenseLink}.`,
         licenseLink: ({underline}: LicenseUnderlineParams) => `<a href="${CONST.OLD_DOT_PUBLIC_URLS.LICENSES_URL}">${underline ? '<u>licenties</u>' : 'licenties'}</a>`,
     },
     validateCodeForm: {
diff --git a/src/languages/pl.ts b/src/languages/pl.ts
index 7d9e4fd8145..e3dc9041113 100644
--- a/src/languages/pl.ts
+++ b/src/languages/pl.ts
@@ -3062,9 +3062,8 @@ ${amount} dla ${merchant} - ${date}`,
     },
     termsOfUse: {
         terms: `<muted-text-xs>Logując się, wyrażasz zgodę na <a href="${CONST.OLD_DOT_PUBLIC_URLS.TERMS_URL}">Warunki korzystania z usługi</a> oraz <a href="${CONST.OLD_DOT_PUBLIC_URLS.PRIVACY_URL}">Prywatność</a>.</muted-text-xs>`,
-        license: ({licenseLink}: LicenseLinkParams) =>
-            `Usługę przekazu pieniężnego świadczy ${CONST.WALLET.PROGRAM_ISSUERS.EXPENSIFY_PAYMENTS} (NMLS ID:2017010) na podstawie swoich ${licenseLink}.`,
-        licenseLink: ({underline}: LicenseUnderlineParams) => `<a href="${CONST.OLD_DOT_PUBLIC_URLS.LICENSES_URL}">${underline ? '<u>licencji</u>' : 'licencji'}</a>`,
+        license: ({licenseLink}: LicenseLinkParams) => `Przekaz pieniężny jest świadczony przez ${CONST.WALLET.PROGRAM_ISSUERS.EXPENSIFY_PAYMENTS} (NMLS ID:2017010) na mocy ${licenseLink}.`,
+        licenseLink: ({underline}: LicenseUnderlineParams) => `<a href="${CONST.OLD_DOT_PUBLIC_URLS.LICENSES_URL}">${underline ? '<u>licencje</u>' : 'licencje'}</a>`,
     },
     validateCodeForm: {
         magicCodeNotReceived: 'Nie otrzymano magicznego kodu?',
diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts
index a42248110a8..21cdec0eb34 100644
--- a/src/languages/pt-BR.ts
+++ b/src/languages/pt-BR.ts
@@ -3066,7 +3066,7 @@ ${amount} para ${merchant} - ${date}`,
     termsOfUse: {
         terms: `<muted-text-xs>Ao entrar, você concorda com os <a href="${CONST.OLD_DOT_PUBLIC_URLS.TERMS_URL}">Termos de Serviço</a> e a <a href="${CONST.OLD_DOT_PUBLIC_URLS.PRIVACY_URL}">Privacidade</a>.</muted-text-xs>`,
         license: ({licenseLink}: LicenseLinkParams) =>
-            `A transmissão de dinheiro é fornecida por ${CONST.WALLET.PROGRAM_ISSUERS.EXPENSIFY_PAYMENTS} (ID NMLS:2017010) de acordo com suas ${licenseLink}.`,
+            `A transmissão de dinheiro é fornecida por ${CONST.WALLET.PROGRAM_ISSUERS.EXPENSIFY_PAYMENTS} (ID NMLS:2017010) de acordo com sua ${licenseLink}.`,
         licenseLink: ({underline}: LicenseUnderlineParams) => `<a href="${CONST.OLD_DOT_PUBLIC_URLS.LICENSES_URL}">${underline ? '<u>licenças</u>' : 'licenças'}</a>`,
     },
     validateCodeForm: {
diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts
index 427b1c6b7f7..db595dca3ac 100644
--- a/src/languages/zh-hans.ts
+++ b/src/languages/zh-hans.ts
@@ -2976,7 +2976,7 @@ ${amount},商户:${merchant} - 日期:${date}`,
     },
     termsOfUse: {
         terms: `<muted-text-xs>登录即表示你同意<a href="${CONST.OLD_DOT_PUBLIC_URLS.TERMS_URL}">服务条款</a>和<a href="${CONST.OLD_DOT_PUBLIC_URLS.PRIVACY_URL}">隐私政策</a>。</muted-text-xs>`,
-        license: ({licenseLink}: LicenseLinkParams) => `资金汇款服务由 ${CONST.WALLET.PROGRAM_ISSUERS.EXPENSIFY_PAYMENTS}(NMLS ID:2017010)根据其${licenseLink}提供。`,
+        license: ({licenseLink}: LicenseLinkParams) => `资金传输服务由 ${CONST.WALLET.PROGRAM_ISSUERS.EXPENSIFY_PAYMENTS}(NMLS 编号:2017010)根据其 ${licenseLink} 提供。`,
         licenseLink: ({underline}: LicenseUnderlineParams) => `<a href="${CONST.OLD_DOT_PUBLIC_URLS.LICENSES_URL}">${underline ? '<u>许可证</u>' : '许可证'}</a>`,
     },
     validateCodeForm: {

Note

You can apply these changes to your branch by copying the patch to your clipboard, then running pbpaste | git apply 😉

View workflow run

@MelvinBot

Copy link
Copy Markdown
Contributor Author

I re-checked after your latest push (e0158bc) — no code fix is needed. All code/CI checks are green: typecheck's build compilation, all 4 builds, all 8 Jest jobs, Storybook, Bun, ESLint, Oxfmt, knip, spellcheck, unused-styles, and generateTranslations all pass. (typecheck and the Reassure perf tests were still finishing when I looked, but the full app builds already compiled cleanly, and a translation/underline change can't regress perf.)

The only two red checks are:

  • PR Author Checklist / checklist
  • PR Reviewer Checklist / checklist

Both fail for the same non-code reason:

⚠️ PR Author Checklist is not completely filled out. Please check every box to verify you've thought about the item.

These clear when a human ticks the checklist boxes in the PR description / review — pushing a commit only re-runs them without resolving them, so I haven't made any changes.

@MelvinBot

Copy link
Copy Markdown
Contributor Author

Still nothing to fix in code here — all code/CI checks are green (typecheck, ESLint, Jest, Bun, Storybook, knip, etc.).

The two red checks are the human checklists, which no commit can resolve:

  • PR Author Checklist / checklist — fails with "PR Author Checklist is not completely filled out. Please check every box…"
  • PR Reviewer Checklist / checklist

These pass once the author ticks every box in the checklist (and a reviewer completes theirs). There's no code change to push.

@MelvinBot

Copy link
Copy Markdown
Contributor Author

Re-checked after your latest commit (e0158bc) — all code/CI checks are green (typecheck, all 8 test jobs, ESLint, Bun, Storybook, knip, Oxfmt, and Reassure perf tests).

The only two red checks are the human-only checklists, which no code change can resolve:

  • PR Author Checklist / checklist"PR Author Checklist is not completely filled out. Please check every box…"
  • PR Reviewer Checklist / checklist

These pass once the boxes in the PR description are ticked (and a reviewer completes theirs). Nothing to fix in code, so I haven't pushed anything.

@Pujan92

Pujan92 commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

@MelvinBot mark this ready for review

@melvin-bot

melvin-bot Bot commented Jul 21, 2026

Copy link
Copy Markdown

I can't run change requests for your access level. I can investigate, or file an issue in Expensify/Expensify instead.


Failed after 0s · 0 tools used · view debug log

@rushatgabhane rushatgabhane self-assigned this Jul 21, 2026
@rushatgabhane

Copy link
Copy Markdown
Member

@MelvinBot please mark this ready for review

@MelvinBot
MelvinBot marked this pull request as ready for review July 21, 2026 15:58
@MelvinBot
MelvinBot requested a review from a team as a code owner July 21, 2026 15:58
@melvin-bot
melvin-bot Bot requested review from Pujan92, shawnborton and trjExpensify and removed request for a team and trjExpensify July 21, 2026 15:58
@melvin-bot

melvin-bot Bot commented Jul 21, 2026

Copy link
Copy Markdown

@shawnborton @Pujan92 One of you needs to copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

@melvin-bot
melvin-bot Bot removed the request for review from a team July 21, 2026 15:58
@MelvinBot

Copy link
Copy Markdown
Contributor Author

🤖 Done — PR #96618 is now marked Ready for review.

Comment thread src/pages/signin/Licenses.tsx Outdated
// Detection mirrors the sibling HighContrastModeSwitcher, since the logged-out sign-in flow tracks contrast via SIGN_IN_HIGH_CONTRAST_INTENT.
const currentTheme = preferredTheme ?? CONST.THEME.DEFAULT;
const isHighContrast = highContrastIntent ?? isHighContrastTheme(currentTheme);
const licenseHTML = translate('termsOfUse.license', {licenseLink: translate('termsOfUse.licenseLink', {underline: isHighContrast})});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ CONSISTENCY-3 (docs)

This high-contrast detection block — the two useOnyx subscriptions to PREFERRED_THEME / SIGN_IN_HIGH_CONTRAST_INTENT plus const currentTheme = preferredTheme ?? CONST.THEME.DEFAULT; and const isHighContrast = highContrastIntent ?? isHighContrastTheme(currentTheme); — is copied verbatim from the sibling HighContrastModeSwitcher.tsx (lines 25-30). The code comment even acknowledges "Detection mirrors the sibling HighContrastModeSwitcher." Duplicating the subscription keys and the SIGN_IN_HIGH_CONTRAST_INTENT fallback nuance in two places means the two call sites can drift apart if the contrast logic ever changes.

Extract the shared logic into a hook so both consumers stay in sync, e.g.:

// src/hooks/useIsHighContrast.ts
function useIsHighContrast() {
    const [preferredTheme] = useOnyx(ONYXKEYS.PREFERRED_THEME);
    const [highContrastIntent] = useOnyx(ONYXKEYS.SIGN_IN_HIGH_CONTRAST_INTENT);
    const currentTheme = preferredTheme ?? CONST.THEME.DEFAULT;
    return {currentTheme, isHighContrast: highContrastIntent ?? isHighContrastTheme(currentTheme)};
}

Then in Licenses.tsx: const {isHighContrast} = useIsHighContrast(); and consume it the same way in HighContrastModeSwitcher.tsx.


Reviewed at: e0158bc | Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MelvinBot see other pages and use existing pattern

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — extracted the duplicated detection into a shared hook and pushed to this branch.

Both call sites now share one source of truth, so the contrast logic can't drift.

One heads-up: I couldn't run the local checks (fmt/lint/typecheck) in this environment, so I'm relying on CI to validate. The change is a straight extraction with import cleanup, so I don't expect issues — but flagging it so it isn't assumed verified.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MelvinBot why did you create new hook. use existing

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right — reverted the new hook and pushed. There isn't a pre-existing hook that returns isHighContrast (the only theme hook, useThemePreference, reads PREFERRED_THEME directly and so misses the logged-out SIGN_IN_HIGH_CONTRAST_INTENT this page depends on), so I went back to the same inline detection every other sign-in page already uses:

const currentTheme = preferredTheme ?? CONST.THEME.DEFAULT;
const isHighContrast = highContrastIntent ?? isHighContrastTheme(currentTheme);

This leaves the two-line detection duplicated (which is what the bot's CONSISTENCY-3 flagged), but that matches the existing convention across the other pages, so I'm going with your call over the bot's.

One heads-up: I couldn't run fmt/lint/typecheck in this environment (out of memory), so I'm relying on CI. The change is a straight revert to the branch's earlier state, so I don't expect issues — flagging it so it isn't assumed verified.

@Pujan92

Pujan92 commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

@rushatgabhane do we need to apply the same for all the links in app? In that case, we can apply a conditional style in anchorRenderer

@rushatgabhane

Copy link
Copy Markdown
Member

do we need to apply the same for all the links in app? In that case, we can apply a conditional style in anchorRenderer

@Pujan92 is it easy to apply the same for all links in app?

@Pujan92

Pujan92 commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

single place can apply to all links that render with RenderHTML, we can give it a try if you want or else in the next PR

@rushatgabhane

Copy link
Copy Markdown
Member

@Pujan92 if it is single place, then let's give it a go here :)

(please ask for price bump)

@shawnborton

Copy link
Copy Markdown
Contributor

Let us know when there are screenshots/videos to review.

@MelvinBot

Copy link
Copy Markdown
Contributor Author

I looked into the failing checks, and there's nothing to fix in the code — every real CI check is passing.

The only two red checks are:

  • PR Author Checklist / checklist — fails because the checklist boxes in the PR description are still unchecked (No ... checklist).
  • PR Reviewer Checklist / checklist — fails because no reviewer has posted a completed reviewer checklist yet (No PR Reviewer Checklist was found).

Both are human-action checks, not code issues, so I haven't pushed anything. They'll go green once the author checklist is completed and a reviewer (e.g. rushatgabhane) runs their checklist.

All other checks (green)

ESLint, typecheck, all 8 test jobs, Bun tests, Storybook tests, knip comparison, Oxfmt, React Compiler Compliance, generateTranslations, spellcheck, and codecov all pass. Build and perf-test jobs are still running but unrelated to the failures.

@MelvinBot

Copy link
Copy Markdown
Contributor Author

The only failing checks are PR Author Checklist and PR Reviewer Checklist, and neither is a code problem — there's nothing for me to push a fix for.

Both fail because the checklist boxes in the PR description haven't been ticked. The author checklist log says it plainly:

You completed 0 out of 38 checklist items with 38 unfinished items
##[error]PR Author Checklist is not completely filled out. Please check every box to verify you've thought about the item.

To clear them:

  • Pujan92 — tick every box in the PR Author Checklist section of the PR description.
  • rushatgabhane — complete the PR Reviewer Checklist.

All other checks pass; the two build (…) jobs are still pending, not failed. Once the checklists are filled out those two checks will go green on their own.

@Pujan92

Pujan92 commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

With that change in AnchorRenderer, links rendered by RenderHTML will be underlined—though not everywhere.

if (HTMLEngineUtils.isChildOfAlertText(tnode)) {
linkStyle = [styles.formError, styles.mb0, styles.link];
}
if (tnode.classes.includes('no-style-link')) {
// If the link has a class of a no-style-link, we don't apply any styles
linkStyle = {...(style as TextStyle)};
delete linkStyle.color;
delete linkStyle.textDecorationLine;
delete linkStyle.textDecorationColor;
}

Screen.Recording.2026-07-21.at.22.29.17.mov

@shawnborton

Copy link
Copy Markdown
Contributor

Also wanted to cc our accessibility expert @dubielzyk-expensify for review when he gets a chance.

@dubielzyk-expensify

Copy link
Copy Markdown
Contributor

Would love some more comprehensive videos/screenshots or maybe when you're ready we can adhoc test it?

I think in general it looks right to me. I don't love it, but I'm aware that we need it for passing a11y guidelines. It's also not used heaps of places so I think it doesn't hugely make them overly visible or anything. The only other way to solve this would be to append an icon or character to the text, but I think this is a safer place to start since it's also just for the high contrast mode

@rushatgabhane

Copy link
Copy Markdown
Member

#96618 (comment)

@Pujan92 that looks good to me. I like that the changes are well scoped.

The links that are not underlined, we can work on them in a follow up

@Pujan92

Pujan92 commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

@MelvinBot we can revert the changes and apply the logic inside AnchorRenderer. with that, links that render with RenderHTML will have the underline.

if (HTMLEngineUtils.isChildOfAlertText(tnode)) {
linkStyle = [styles.formError, styles.mb0, styles.link];
}
if (tnode.classes.includes('no-style-link')) {

Revert the per-locale licenseLink translation split and re-add class-based
underline handling in AnchorRenderer. Licenses.tsx injects the link-underline
class only in high contrast, so the HTML-rendered link gets the underline.

Co-authored-by: Pujan Shah <Pujan92@users.noreply.github.com>
@MelvinBot
MelvinBot requested a review from a team as a code owner July 22, 2026 10:19
@MelvinBot

Copy link
Copy Markdown
Contributor Author

⚠️ MelvinBot CI Fix Loop Detected

MelvinBot has made 5 consecutive commits trying to fix CI failures. Automatic CI fix dispatches are now paused for this PR.

Next steps: A human should review the recent commits and resolve the conflicting CI errors manually.


// Links opt into an underline via the link-underline class so they are distinguishable by more than color (WCAG 1.4.1).
// styles.link sets textDecorationLine: 'none', so the underline is appended last to override it.
if (tnode.classes.includes('link-underline')) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (tnode.classes.includes('link-underline')) {
if (isHIghContrast) {

@MelvinBot we need to apply for all links when isHIghContrast is true.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — pushed to this branch. I handled both of your comments together since they're one change.

What changed

  • Re-added the useIsHighContrast hook (as per the earlier commit) — subscribes to PREFERRED_THEME / SIGN_IN_HIGH_CONTRAST_INTENT and returns {currentTheme, isHighContrast}.
  • AnchorRenderer.tsx now consumes the hook and applies styles.underline when if (isHighContrast) — so all links (not just Licenses) get underlined in high contrast, exactly as you asked.
  • Licenses.tsx is back to plain translate('termsOfUse.license') — no more link-underline class injection or inline detection there, since the underline is now centralized in the renderer.

Two things worth confirming before merge:

  1. Scope is now app-wide. The if (isHighContrast) block lives in the non-chat branch of AnchorRenderer (guarded by !isChildOfComment && !isChildOfTaskTitle), so this underlines every HTML-rendered link outside of chat/task-title (sign-in, settings, RBR, labels, etc.) whenever a high-contrast theme is active — not just the Licenses link the issue was scoped to. That's a reasonable WCAG 1.4.1 improvement, but it's a deliberate expansion beyond issue [HOLD][$125] Color: Many Pages: Various links are identified only by use of color #76919, so flagging it for a design/QA look.

  2. The hook is a reversal of earlier feedback. rushatgabhane earlier asked me to remove this hook and use inline detection. Re-introducing it here is justified because AnchorRenderer is a shared renderer (inline useOnyx duplication is worse there) — but since you two are both assignees and gave opposite guidance on the hook, please align on it.

One heads-up: I couldn't run fmt/lint/typecheck locally in this environment, so I'm relying on CI. The edits follow the surrounding style, but flagging so it isn't assumed verified.

Comment thread src/pages/signin/Licenses.tsx Outdated
// Only underline the Licenses link in high-contrast themes, so it is distinguishable by more than color (WCAG 1.4.1).
// Detection mirrors the sibling HighContrastModeSwitcher, since the logged-out sign-in flow tracks contrast via SIGN_IN_HIGH_CONTRAST_INTENT.
const currentTheme = preferredTheme ?? CONST.THEME.DEFAULT;
const isHighContrast = highContrastIntent ?? isHighContrastTheme(currentTheme);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MelvinBot we can move this to new hook as per the earlier commit, and use it inside AnchorRenderer not here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — this is already handled in f79347f2 (pushed alongside your AnchorRenderer comment, since they're one change):

  • Re-added the useIsHighContrast hook, as per the earlier commit.
  • The detection now lives inside AnchorRenderer, which calls the hook at AnchorRenderer.tsx:35 and underlines every link when it's true (AnchorRenderer.tsx:113).
  • The inline useOnyx/isHighContrast block is gone from Licenses.tsx — it's back to a plain component that just renders termsOfUse.license.

@Pujan92

Pujan92 commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

someone plz run the adhoc build

@github-actions

Copy link
Copy Markdown
Contributor

🚧 dylanexpensify has triggered a test Expensify/App build. You can view the workflow run here.

@Pujan92

Pujan92 commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Reviewer Checklist

  • I have verified the author checklist is complete (all boxes are checked off).
  • I verified the correct issue is linked in the ### Fixed Issues section above
  • I verified testing steps are clear and they cover the changes made in this PR
    • I verified the steps for local testing are in the Tests section
    • I verified the steps for Staging and/or Production testing are in the QA steps section
    • I verified the steps cover any possible failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
  • I checked that screenshots or videos are included for tests on all platforms
  • I included screenshots or videos for tests on all platforms
  • I verified that the composer does not automatically focus or open the keyboard on mobile unless explicitly intended. This includes checking that returning the app from the background does not unexpectedly open the keyboard.
  • I verified tests pass on all platforms & I tested again on:
    • Android: HybridApp
    • Android: mWeb Chrome
    • iOS: HybridApp
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • If there are any errors in the console that are unrelated to this PR, I either fixed them (preferred) or linked to where I reported them in Slack
  • I verified proper code patterns were followed (see Reviewing the code)
    • I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick).
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I verified that this PR follows the guidelines as stated in the Review Guidelines
  • I verified other components that can be impacted by these changes have been tested, and I retested again (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar have been tested & I retested again)
  • If a new component is created I verified that:
    • A similar component doesn't exist in the codebase
    • All props are defined accurately and each prop has a /** comment above it */
    • The file is named correctly
    • The component has a clear name that is non-ambiguous and the purpose of the component can be inferred from the name alone
    • The only data being stored in the state is data necessary for rendering and nothing else
    • For Class Components, any internal methods passed to components event handlers are bound to this properly so there are no scoping issues (i.e. for onClick={this.submit} the method this.submit should be bound to this in the constructor)
    • Any internal methods bound to this are necessary to be bound (i.e. avoid this.submit = this.submit.bind(this); if this.submit is never passed to a component event handler like onClick)
    • All JSX used for rendering exists in the render method
    • The component has the minimum amount of code necessary for its purpose, and it is broken down into smaller components in order to separate concerns and functions
  • If any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG)
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • For any bug fix or new feature in this PR, I verified that sufficient unit tests are included to prevent regressions in this flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.
  • I have checked off every checkbox in the PR reviewer checklist, including those that don't apply to this PR.

Screenshots/Videos

Android: HybridApp
Android: mWeb Chrome
iOS: HybridApp
Simulator.Screen.Recording.-.iPhone.15.Pro.-.2026-07-22.at.18.18.30.mov
iOS: mWeb Safari
MacOS: Chrome / Safari
Screen.Recording.2026-07-22.at.18.10.54.mov

@Pujan92 Pujan92 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! It shows underline for high contrast mode which renders with RenderHTML component.

@melvin-bot

melvin-bot Bot commented Jul 22, 2026

Copy link
Copy Markdown

We did not find an internal engineer to review this PR, trying to assign a random engineer to #76919 as well as to this PR... Please reach out for help on Slack if no one gets assigned!

@melvin-bot
melvin-bot Bot requested a review from jasperhuangg July 22, 2026 12:49

@rushatgabhane rushatgabhane left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shawnborton all yours!

@shawnborton

Copy link
Copy Markdown
Contributor

Looks like the "Go back" link doesn't get the underline treatment?
CleanShot 2026-07-22 at 09 56 48@2x

@shawnborton

Copy link
Copy Markdown
Contributor

Same with "submit a bug" here:
CleanShot 2026-07-22 at 09 57 21@2x

@Pujan92

Pujan92 commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Yes, those are rendered separately. we need to consider it in the next pr #96618 (comment)

@shawnborton

Copy link
Copy Markdown
Contributor

Got it, okay. Nothing else on my end then, feel free to continue with final review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants