| @@ -4,7 +4,10 @@ Self-hosted fonts: no `fonts.googleapis.com` or `fonts.gstatic.com`. Static asse | |||
| Adjust `report-uri` if your API base path or host differs (UAT example below; PROD uses `https://pnsps.gld.gov.hk/api/csp-report`). | |||
| See also: [csp-report-review-2026-08-03.md](./csp-report-review-2026-08-03.md) for the PROD report analysis that drove `frame-src` / `media-src` updates. | |||
| See also: | |||
| - [csp-report-review-2026-08-14.md](./csp-report-review-2026-08-14.md) — latest PROD + UAT report review and add/remove summary | |||
| - [csp-report-review-2026-08-03.md](./csp-report-review-2026-08-03.md) — earlier PROD report analysis that drove `frame-src` / `media-src` updates | |||
| ## Enforcing | |||
| @@ -18,7 +21,7 @@ Header always set Content-Security-Policy "default-src 'self'; \ | |||
| script-src 'self'; \ | |||
| style-src 'self' 'unsafe-inline'; \ | |||
| style-src-elem 'self' 'unsafe-inline'; \ | |||
| img-src 'self' data: https://www.w3.org https://w3.org; \ | |||
| img-src 'self' data:; \ | |||
| media-src 'self' blob: data:; \ | |||
| font-src 'self' data:; \ | |||
| connect-src 'self'; \ | |||
| @@ -39,7 +42,7 @@ Header always set Content-Security-Policy-Report-Only "default-src 'self'; \ | |||
| script-src 'self'; \ | |||
| style-src 'self' 'unsafe-inline'; \ | |||
| style-src-elem 'self' 'unsafe-inline'; \ | |||
| img-src 'self' data: https://www.w3.org https://w3.org; \ | |||
| img-src 'self' data:; \ | |||
| media-src 'self' blob: data:; \ | |||
| font-src 'self' data:; \ | |||
| connect-src 'self'; \ | |||
| @@ -51,7 +54,7 @@ Header always set Content-Security-Policy-Report-Only "default-src 'self'; \ | |||
| - **`frame-src`**: Required for proof file preview iframes that use `data:` / `blob:` URLs (`UploadFileTable.js`). Without this, framing falls back to `default-src 'self'` and browsers report `frame-src` violations (often with empty `blocked-uri`). | |||
| - **`style-src-elem`**: Explicit, alongside `style-src`, for `<link rel="stylesheet">` behaviour in modern browsers. | |||
| - **`img-src`**: Includes `https://www.w3.org` and `https://w3.org` so W3C WCAG badge URLs are allowed. | |||
| - **`img-src`**: `'self' data:` is enough. The WCAG 2 AA badge is bundled locally (`src/assets/images/wcag2AA.png`) and no longer loaded from `www.w3.org`. | |||
| - **`media-src`**: `blob:` for captcha audio object URLs; `data:` for data-URI media if used. | |||
| - **`font-src`**: `'self' data:` covers bundled fonts and `data:` URLs if used. | |||
| - **Report noise**: Browser extensions (Kaspersky, Perplexity, Youdao, Quark, Google Fonts injected by tooling, `wasm-eval` from chrome-extension) will still appear under Report-Only. Do **not** allowlist those origins. | |||
| @@ -12,6 +12,7 @@ Source logs (external): | |||
| Related app docs: | |||
| - [csp-apache.conf.md](./csp-apache.conf.md) — Apache header snippets to deploy | |||
| - [csp-report-review-2026-08-14.md](./csp-report-review-2026-08-14.md) — later PROD + UAT review (add/remove summary) | |||
| --- | |||
| @@ -0,0 +1,212 @@ | |||
| # CSP Report Review (PROD + UAT, 2026-08-14) | |||
| Review of Content-Security-Policy-Report-Only violations from PNSPS PROD | |||
| (`https://pnsps.gld.gov.hk`) and UAT (`https://pnspsuat.gld.gov.hk`), for | |||
| deciding Apache CSP updates. | |||
| Source logs (external): | |||
| - `P1 CSP.txt` — PROD node, 168 reports, 2026-07-30 to 2026-08-14 | |||
| - `P2 CSP.txt` — PROD node, 137 reports, 2026-07-30 to 2026-08-14 | |||
| - `TT CSP.txt` — UAT, 133 reports, 2026-05-29 to 2026-08-03 | |||
| Related app docs: | |||
| - [csp-apache.conf.md](./csp-apache.conf.md) — Apache header snippets to deploy | |||
| - [csp-report-review-2026-08-03.md](./csp-report-review-2026-08-03.md) — earlier PROD review | |||
| --- | |||
| ## Verdict | |||
| CSP is still **Report-Only** (`disposition: report`) — nothing is blocking users | |||
| yet. There is **one real PNSPS SPA problem** (proof PDF preview `frame-src`). | |||
| Everything else is browser-extension noise or a **different app** on the same | |||
| UAT host. | |||
| The 2026-08-03 recommendation to add `frame-src` is **still not on the live | |||
| header**. | |||
| --- | |||
| ## Add / remove summary | |||
| **PROD — add one line, remove nothing:** | |||
| ```apache | |||
| frame-src 'self' data: blob:; | |||
| ``` | |||
| Keep `script-src 'self'`. Do not add `'unsafe-inline'` or `'unsafe-eval'`. | |||
| **UAT — add the same line, remove nothing now:** | |||
| ```apache | |||
| frame-src 'self' data: blob:; | |||
| ``` | |||
| Keep `script-src 'self' 'unsafe-inline' 'unsafe-eval'` for now (JSF payment recon | |||
| on the same host). Do not copy this `script-src` to PROD. | |||
| **Do not add on either env:** `'wasm-unsafe-eval'`, Youdao, NetEase CDN, | |||
| `todesktop-internal`, Google Fonts. | |||
| **Optional later (not required for these reports):** UAT can drop | |||
| `'unsafe-inline' 'unsafe-eval'` from the SPA header only after | |||
| `/paymentrecon-dept_web/` has its own CSP. | |||
| --- | |||
| ## Confirmed current PROD policy (Report-Only) | |||
| This is the live header that generated the P1/P2 reports. It matches the | |||
| `original-policy` in every PROD violation. | |||
| ```apache | |||
| Header always set Content-Security-Policy-Report-Only "default-src 'self'; \ | |||
| base-uri 'self'; \ | |||
| object-src 'none'; \ | |||
| frame-ancestors 'none'; \ | |||
| form-action 'self'; \ | |||
| script-src 'self'; \ | |||
| style-src 'self' 'unsafe-inline'; \ | |||
| style-src-elem 'self' 'unsafe-inline'; \ | |||
| img-src 'self' data: https://www.w3.org https://w3.org; \ | |||
| media-src 'self' blob:; \ | |||
| font-src 'self' data:; \ | |||
| connect-src 'self'; \ | |||
| upgrade-insecure-requests; \ | |||
| report-uri https://pnsps.gld.gov.hk/api/csp-report" | |||
| ``` | |||
| Gap vs the reports: **no `frame-src`**. Framing therefore falls back to | |||
| `default-src 'self'`, which is why proof PDF preview reports `frame-src` with | |||
| an empty `blocked-uri`. | |||
| --- | |||
| ## Confirmed current UAT policy (Report-Only) | |||
| ```apache | |||
| Header always set Content-Security-Policy-Report-Only "default-src 'self'; \ | |||
| base-uri 'self'; \ | |||
| object-src 'none'; \ | |||
| frame-ancestors 'none'; \ | |||
| form-action 'self'; \ | |||
| script-src 'self' 'unsafe-inline' 'unsafe-eval'; \ | |||
| style-src 'self' 'unsafe-inline'; \ | |||
| style-src-elem 'self' 'unsafe-inline'; \ | |||
| img-src 'self' data: https://www.w3.org https://w3.org; \ | |||
| media-src 'self' blob:; \ | |||
| font-src 'self' data:; \ | |||
| connect-src 'self'; \ | |||
| upgrade-insecure-requests; \ | |||
| report-uri https://pnspsuat.gld.gov.hk/api/csp-report" | |||
| ``` | |||
| UAT vs PROD differences: | |||
| - UAT `script-src` is `'self' 'unsafe-inline' 'unsafe-eval'` — PROD is `'self'` only. | |||
| - Both still lack `frame-src`. | |||
| - TT report `original-policy` was older: `script-src 'self' 'unsafe-inline'` | |||
| **without** `'unsafe-eval'`. That is why TT logged 42 JSF `eval` hits. Those | |||
| should stop under the current UAT header. | |||
| Do **not** copy `'unsafe-inline'` or `'unsafe-eval'` onto PROD. The React SPA | |||
| does not need them. `'unsafe-eval'` is only there for the shared-host JSF | |||
| payment recon app. | |||
| --- | |||
| ## Real problem (PROD P1 + P2) | |||
| | Volume | Directive | Blocked | Pages | | |||
| |---:|---|---|---| | |||
| | 150 | `frame-src` | empty `""` | `/proof/create/{id}` | | |||
| Source is always `static/js/4608.4af455e6.chunk.js`. Cause is proof file preview | |||
| in `src/pages/Proof/Create_FromApp/UploadFileTable.js`: | |||
| `FileReader.readAsDataURL` then `document.write` an `<iframe src="data:...">`. | |||
| With no `frame-src`, CSP uses `default-src 'self'` and blocks `data:` frames. | |||
| Browsers strip the `data:` URI from reports (empty `blocked-uri`). | |||
| **If this policy is promoted to enforcing without `frame-src`, PDF preview on | |||
| proof create will break.** | |||
| Apache fix (already documented in [csp-apache.conf.md](./csp-apache.conf.md), | |||
| not deployed): | |||
| ```apache | |||
| frame-src 'self' data: blob:; | |||
| ``` | |||
| Optional frontend follow-up: switch preview to `URL.createObjectURL` + `blob:` | |||
| (still needs `frame-src ... blob:`). | |||
| Optional (low priority): `media-src 'self' blob: data:;` — current `blob:` | |||
| already covers captcha audio. | |||
| --- | |||
| ## Not a PNSPS SPA problem — do not allowlist | |||
| | Volume | Where | Directive | Blocked | Why ignore | | |||
| |---:|---|---|---|---| | |||
| | 109 | PROD | `script-src` | `wasm-eval` | `source-file: chrome-extension` | | |||
| | 18 | PROD | `media-src` | `dict.youdao.com` | Youdao translation plugin | | |||
| | 11 | PROD | `img-src` | `ydlunacommon-cdn.nosdn.127.net` | Youdao/NetEase CDN icons | | |||
| | 3 | PROD P2 | `script-src-elem` | `todesktop-internal` | Desktop wrapper | | |||
| | 105 | PROD 14 + UAT 91 | `img-src` | `www.w3.org/WAI/wcag2AA` | Already allowed in policy; mostly SPA `status-code: 404` noise. Badge is in `src/components/cards/AuthFooter.js`. | | |||
| Do **not** add `'unsafe-eval'`, `'wasm-unsafe-eval'`, Youdao, NetEase, or | |||
| `todesktop-internal` to the SPA policy. | |||
| --- | |||
| ## UAT-only: payment recon JSF (not the React app) | |||
| 42 TT reports: `script-src` / `eval` on: | |||
| - `/paymentrecon-dept_web/report/paymentReconRpt01Search.jsf` (38) | |||
| - `/paymentrecon-dept_web/report/paymentReconRpt02Search.jsf` (3) | |||
| - `/paymentrecon-dept_web/manualrecon/manualReconSearch.jsf` (1) | |||
| This is a **separate JSF app** on the same Apache host. The TT logs were | |||
| generated under the **old** UAT policy (`script-src 'self' 'unsafe-inline'`). | |||
| Current UAT already adds `'unsafe-eval'`, so those `eval` reports should | |||
| disappear. | |||
| That UAT looseness is a workaround for JSF, not a requirement of PNSPS. Before | |||
| enforcing CSP: | |||
| - Keep PROD at `script-src 'self'` (do not add `'unsafe-eval'`). | |||
| - Prefer a path-specific header for `/paymentrecon-dept_web/` on UAT so the SPA | |||
| can later drop `'unsafe-inline'` / `'unsafe-eval'`. | |||
| --- | |||
| ## Recommended actions (ops / Apache, not app code) | |||
| 1. Add `frame-src 'self' data: blob:;` to both PROD and UAT Report-Only headers. | |||
| Do not change PROD `script-src 'self'`. | |||
| 2. Confirm proof-create preview reports drop after that deploy. | |||
| 3. Keep Report-Only until `frame-src` volume is gone; remaining extension noise | |||
| is expected. | |||
| 4. Do not promote UAT's `'unsafe-inline' 'unsafe-eval'` to PROD. Those tokens | |||
| exist for JSF payment recon on the shared UAT host. | |||
| 5. Optional: give `/paymentrecon-dept_web/` its own Apache CSP so UAT SPA can | |||
| later match PROD (`script-src 'self'`). | |||
| 6. Optional later: host the WCAG badge locally to cut `img-src` noise; change | |||
| preview to `blob:` URLs. | |||
| No application code change is required for the reports to become clean after | |||
| the Apache `frame-src` deploy. | |||
| --- | |||
| ## Backend reference | |||
| - Report endpoint: `POST /csp-report` (context path → `/api/csp-report`) | |||
| - Spring Security also sets a short API CSP on API responses | |||
| (`default-src 'self'; script-src 'self'; frame-ancestors 'self'`). | |||
| Frontend/Apache CSP above is what browsers enforce for the SPA document. | |||
| @@ -22,7 +22,7 @@ let expiredAlertShownInMemory = false; | |||
| /** Login / public auth endpoints must not trigger token refresh or session-expiry reload. */ | |||
| const isPublicAuthRequest = (reqUrl) => | |||
| reqUrl.includes('/login') || reqUrl.includes('/ldap-login'); | |||
| reqUrl.includes('/login'); | |||
| /** Clear stale session-expiry flag so a new login attempt can show its own error dialog. */ | |||
| export const clearExpiredSessionAlert = () => { | |||
| @@ -46,7 +46,6 @@ export default class JwtService { | |||
| if (response && response.status === 401) { | |||
| const isPublicAuthRequest = | |||
| reqUrl.includes('/login') || | |||
| reqUrl.includes('/ldap-login') || | |||
| reqUrl.includes('/refresh-token') | |||
| if (isPublicAuthRequest) { | |||
| return Promise.reject(error) | |||
| @@ -1,6 +1,7 @@ | |||
| // material-ui | |||
| import { useMediaQuery, Container, Link, Typography, Stack } from '@mui/material'; | |||
| import bhkLogo from 'assets/images/BHK_logo_rgb_zh-hk.png'; | |||
| import wcag2AA from 'assets/images/wcag2AA.png'; | |||
| import {FormattedMessage} from "react-intl"; | |||
| import {useIntl} from "react-intl"; | |||
| import { | |||
| @@ -62,7 +63,7 @@ const AuthFooter = () => { | |||
| <img | |||
| height="32" | |||
| width="88" | |||
| src="https://www.w3.org/WAI/wcag2AA" | |||
| src={wcag2AA} | |||
| alt={wcagAlt} | |||
| /> | |||
| </a> | |||
| @@ -24,6 +24,7 @@ const LoadingComponent = Loadable(React.lazy(() => import('pages/extra-pages/Loa | |||
| import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png' | |||
| import {FormattedMessage} from "react-intl"; | |||
| import { ERROR_CONTAINED_BUTTON_SX } from "themes/colorConst"; | |||
| const BackgroundHead = { | |||
| backgroundImage: `url(${titleBackgroundImg})`, | |||
| width: '100%', | |||
| @@ -161,11 +162,10 @@ const Index = () => { | |||
| component="span" | |||
| variant="contained" | |||
| size="large" | |||
| color="error" | |||
| onClick={() => { | |||
| cancelPayment(); | |||
| }} | |||
| sx={{ m: 4 }} | |||
| sx={{ m: 4, ...ERROR_CONTAINED_BUTTON_SX }} | |||
| > | |||
| <FormattedMessage id="payCancel"/> | |||
| </Button> | |||
| @@ -4,6 +4,7 @@ import { | |||
| Typography, | |||
| Stack, | |||
| Button, | |||
| Box, | |||
| } from '@mui/material'; | |||
| import * as React from "react"; | |||
| import * as HttpUtils from "utils/HttpUtils"; | |||
| @@ -23,6 +24,7 @@ const LoadingComponent = Loadable(React.lazy(() => import('pages/extra-pages/Loa | |||
| import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png' | |||
| import {FormattedMessage, useIntl} from "react-intl"; | |||
| import { PRIMARY_CONTAINED_BUTTON_SX, ERROR_CONTAINED_BUTTON_SX } from "themes/colorConst"; | |||
| const BackgroundHead = { | |||
| backgroundImage: `url(${titleBackgroundImg})`, | |||
| width: '100%', | |||
| @@ -394,27 +396,48 @@ const Index = () => { | |||
| </div> | |||
| </Grid> | |||
| {/*row 1*/} | |||
| <Grid item xs={12} md={12} > | |||
| <Grid container justifyContent="flex-start" alignItems="center" > | |||
| <center> | |||
| <Grid item xs={12} md={12} width="100%"> | |||
| <Grid container justifyContent="flex-start" alignItems="center" width="100%"> | |||
| <center style={{ width: '100%', display: 'block' }}> | |||
| <Grid item xs={12} md={12} > | |||
| <br /><br /> | |||
| <Typography component="span" variant="body1" sx={{ width: '80%', margin: 'auto', mt: 4, textAlign: "left", fontSize: '20px' }}> | |||
| <FormattedMessage id="payAlert"/> | |||
| <br /><br /> | |||
| <Box sx={{ width: { xs: '90%', md: '40%' }, mx: 'auto', mt: 4, textAlign: 'left' }}> | |||
| {!isError && | |||
| <Typography component="div" variant="body1" color="error" sx={{ fontSize: '20px', fontWeight: 700, textAlign: 'center' }}> | |||
| <FormattedMessage id="payAlert"/> | |||
| </Typography> | |||
| } | |||
| {isError && | |||
| <Typography component="div" variant="body1" color="error" sx={{ fontSize: '20px', fontWeight: 700, textAlign: 'justify' }}> | |||
| <FormattedMessage id="fpsPaymentErrorMsg1"/> | |||
| <Box | |||
| component="ul" | |||
| sx={{ | |||
| m: 0, | |||
| mt: 1, | |||
| px: 0, | |||
| width: '100%', | |||
| boxSizing: 'border-box', | |||
| listStylePosition: 'outside', | |||
| paddingLeft: '1.5em', | |||
| textAlign: 'justify', | |||
| }} | |||
| > | |||
| <li style={{fontWeight:'bold', textAlign: 'justify'}}><FormattedMessage id="fpsPaymentErrorMsg2"/></li> | |||
| <li style={{fontWeight:'bold', textAlign: 'justify'}}><FormattedMessage id="fpsPaymentErrorMsg3"/></li> | |||
| </Box> | |||
| </Typography> | |||
| } | |||
| </Box> | |||
| <Typography component="span" variant="body1" sx={{ width: '80%', margin: 'auto', mt: 4, textAlign: "center", fontSize: '20px', display: 'block' }}> | |||
| <img src={FpsIcon} width="80" height="80" alt="FPS"></img> | |||
| <br /> | |||
| <FormattedMessage id="payTotalDeatail"/> | |||
| <br /> | |||
| {"HK$ " + currencyFormat(paymentData.amount)} | |||
| </Typography> | |||
| <br /><br /> | |||
| {isError ? | |||
| <Typography component="span" variant="body1" color="error" sx={{ width: '80%', margin: 'auto', mt: 4, textAlign: "left", fontSize: '20px' }}> | |||
| <FormattedMessage id="fpsPaymentErrorMsg"/> | |||
| </Typography> | |||
| : | |||
| browserType==mobileBrowser? | |||
| <br /> | |||
| {!isError && | |||
| (browserType==mobileBrowser? | |||
| <Typography variant="h3" sx={{ ml: 8, mt: 4, mr: 8, textAlign: "center" }}> | |||
| { | |||
| sysEnv=="prod"? | |||
| @@ -480,26 +503,32 @@ const Index = () => { | |||
| </> | |||
| } | |||
| </Typography> | |||
| } | |||
| )} | |||
| <Typography variant="h3" sx={{ ml: 8, mt: 4, mr: 8, textAlign: "center" }}> | |||
| <Button | |||
| component="span" | |||
| variant="contained" | |||
| size="large" | |||
| onClick={()=>{ | |||
| cancelPayment(); | |||
| }} | |||
| sx={{ | |||
| m: 4, | |||
| backgroundColor: (theme) => theme.palette.error.darker, | |||
| color: (theme) => theme.palette.error.contrastText, | |||
| '&:hover': { | |||
| backgroundColor: (theme) => theme.palette.error.dark, | |||
| }, | |||
| }} | |||
| > | |||
| <FormattedMessage id="payCancel"/> | |||
| </Button> | |||
| {isError ? | |||
| <Button | |||
| component="span" | |||
| variant="contained" | |||
| size="large" | |||
| color="primary" | |||
| onClick={() => navigate("/dashboard")} | |||
| sx={{ m: 4, ...PRIMARY_CONTAINED_BUTTON_SX }} | |||
| > | |||
| <FormattedMessage id="backToMainPage"/> | |||
| </Button> | |||
| : | |||
| <Button | |||
| component="span" | |||
| variant="contained" | |||
| size="large" | |||
| onClick={()=>{ | |||
| cancelPayment(); | |||
| }} | |||
| sx={{ m: 4, ...ERROR_CONTAINED_BUTTON_SX }} | |||
| > | |||
| <FormattedMessage id="payCancel"/> | |||
| </Button> | |||
| } | |||
| </Typography> | |||
| </Grid> | |||
| </center> | |||
| @@ -18,6 +18,7 @@ const LoadingComponent = Loadable(React.lazy(() => import('pages/extra-pages/Loa | |||
| import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png' | |||
| import {FormattedMessage} from "react-intl"; | |||
| import { ERROR_CONTAINED_BUTTON_SX } from "themes/colorConst"; | |||
| const BackgroundHead = { | |||
| backgroundImage: `url(${titleBackgroundImg})`, | |||
| width: '100%', | |||
| @@ -322,11 +323,10 @@ const Index = () => { | |||
| component="span" | |||
| variant="contained" | |||
| size="large" | |||
| color="error" | |||
| onClick={()=>{ | |||
| cancelPayment(); | |||
| }} | |||
| sx={{ m: 4 }} | |||
| sx={{ m: 4, ...ERROR_CONTAINED_BUTTON_SX }} | |||
| > | |||
| <FormattedMessage id="payCancel"/> | |||
| </Button> | |||
| @@ -277,23 +277,21 @@ const MultiPaymentWindow = (props) => { | |||
| maxWidth={'xl'} | |||
| fullScreen={props.isFullScreen} | |||
| > | |||
| <DialogTitle > | |||
| <Grid container> | |||
| <Grid item> | |||
| <Stack direction="column" justifyContent="flex-start" alignItems="center"> | |||
| <Typography variant="h4"> | |||
| {windowTitle} | |||
| </Typography> | |||
| </Stack> | |||
| </Grid> | |||
| </Grid> | |||
| <DialogTitle sx={{ pb: 1, pt: 2 }}> | |||
| <Typography variant="h4"> | |||
| {windowTitle} | |||
| </Typography> | |||
| </DialogTitle> | |||
| <FormikProvider value={formik}> | |||
| <form> | |||
| <DialogContent> | |||
| <DialogContentText> | |||
| <FormLabel sx={{ fontSize: "20px", color: "#000000", textAlign: "left", width: "100%", whiteSpace: "pre-line", paddingLeft: 0 }}> | |||
| <FormattedMessage id="paymentProcessLimited"/> | |||
| <DialogContent sx={{ pt: 1 }}> | |||
| <DialogContentText component="div" sx={{ m: 0 }}> | |||
| <FormLabel component="div" sx={{ fontSize: "18px", color: "#000000", textAlign: "left", width: "100%", paddingLeft: 0, mb: 0 }}> | |||
| <Box component="ul" sx={{ m: 0, pl: 2.5 }}> | |||
| <li><FormattedMessage id="paymentProcessLimited1"/></li> | |||
| <li><FormattedMessage id="paymentProcessLimited2"/></li> | |||
| <li><FormattedMessage id="paymentProcessLimited3"/></li> | |||
| </Box> | |||
| </FormLabel> | |||
| <Grid item xs={12} md={12} sx={{ pt: 2 }} style={{ height: '100%' }} width="100%"> | |||
| <Box xs={12} md={12} sx={{ p: 4, border: '3px solid #eee', borderRadius: '10px' }} > | |||
| @@ -493,7 +493,7 @@ const Index = () => { | |||
| onClick={() => paymentClick()} | |||
| sx={{ mt: 4, ...PAY_CONTAINED_BUTTON_SX }} | |||
| > | |||
| <FormattedMessage id="selectPaymentMethodBtn"/> | |||
| <FormattedMessage id="pay"/> | |||
| </Button> | |||
| <Button | |||
| component="span" | |||
| @@ -17,6 +17,7 @@ import { | |||
| } from '@mui/material'; | |||
| import CancelOutlinedIcon from '@mui/icons-material/CancelOutlined'; | |||
| import {FormattedMessage} from "react-intl"; | |||
| import { PRIMARY_CONTAINED_BUTTON_SX } from "themes/colorConst"; | |||
| const PasswordAlertDialog = (props) => { | |||
| return ( | |||
| @@ -63,7 +64,7 @@ const PasswordAlertDialog = (props) => { | |||
| </DialogContentText> | |||
| </DialogContent> | |||
| <DialogActions> | |||
| <Button variant="contained" color="error" onClick={props.handleClose} autoFocus> | |||
| <Button variant="contained" onClick={props.handleClose} autoFocus sx={PRIMARY_CONTAINED_BUTTON_SX}> | |||
| <FormattedMessage id="close"/> | |||
| </Button> | |||
| </DialogActions> | |||
| @@ -12,6 +12,7 @@ import { | |||
| import { isORGLoggedIn, } from "utils/Utils"; | |||
| import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png' | |||
| import { FormattedMessage, useIntl } from "react-intl"; | |||
| import { ERROR_CONTAINED_BUTTON_SX } from "themes/colorConst"; | |||
| import AdsClickRoundedIcon from '@mui/icons-material/AdsClickRounded'; | |||
| import * as React from "react"; | |||
| import Loadable from 'components/Loadable'; | |||
| @@ -300,7 +301,7 @@ const DashboardDefault = () => { | |||
| </Typography> | |||
| </DialogContent> | |||
| <DialogActions> | |||
| <Button variant="contained" color="error" onClick={() => setIsPopUp(false)}><Typography variant="h5"><FormattedMessage id="close" /></Typography></Button> | |||
| <Button variant="contained" onClick={() => setIsPopUp(false)} sx={ERROR_CONTAINED_BUTTON_SX}><Typography variant="h5"><FormattedMessage id="close" /></Typography></Button> | |||
| </DialogActions> | |||
| </Dialog> | |||
| </div> | |||
| @@ -58,6 +58,17 @@ export const CONTAINED_PRIMARY_BLUE = '#0c489e'; | |||
| /** Validation error text/border — matches FormHelperText in styles.css (e.g. "Please enter password"). WCAG AA on white. */ | |||
| export const VALIDATION_ERROR_COLOR = '#B00020'; | |||
| /** WCAG 2.0 AA contained error button red — lightest tone passing 4.5:1 with white text (~4.57:1). */ | |||
| export const CONTAINED_ERROR_RED = '#e61f2a'; | |||
| export const ERROR_CONTAINED_BUTTON_SX = { | |||
| backgroundColor: CONTAINED_ERROR_RED, | |||
| color: '#FFFFFF', | |||
| '&:hover': { | |||
| backgroundColor: '#d32f2f', | |||
| }, | |||
| }; | |||
| export const PRIMARY_CONTAINED_BUTTON_SX = { | |||
| backgroundColor: CONTAINED_PRIMARY_BLUE, | |||
| color: '#FFFFFF', | |||
| @@ -48,13 +48,14 @@ export const PNSPS_THEME = createTheme({ | |||
| }, | |||
| styleOverrides: { | |||
| root: ({ ownerState }) => ({ | |||
| ...(ownerState.color === "cancel" && { | |||
| borderColor: "#9E9E9E", | |||
| }), | |||
| '&:active': { | |||
| boxShadow: 'none', | |||
| transform: 'none', | |||
| }, | |||
| ...(ownerState.color === "cancel" && { | |||
| borderColor: "#9E9E9E", | |||
| }), | |||
| textTransform: 'none', | |||
| '&:active': { | |||
| boxShadow: 'none', | |||
| transform: 'none', | |||
| }, | |||
| }), | |||
| contained: ({ theme, ownerState }) => ({ | |||
| ...(ownerState.color === "cancel" && { | |||
| @@ -121,6 +121,7 @@ | |||
| "iAmSmartNoIdNoMsg": "Invalid information, please return to the creation of account page.", | |||
| "mainPage": "Main Page", | |||
| "backToMainPage": "Back to Main Page", | |||
| "myPublicNotice": "My Public Notices", | |||
| "publicNotice": "Public Notice", | |||
| "publicNoticeApp": "Public Notice Application", | |||
| @@ -417,8 +418,8 @@ | |||
| "payId": "Payment No.", | |||
| "payIdNRefer": "Payment No. / Payment Reference No.", | |||
| "payConfirm": "Confirm payment", | |||
| "payCancel": "Cancel payment", | |||
| "payAlert": "Please don’t close this window, you may either complete this payment or cancel this payment by the button at the bottom of this page.", | |||
| "payCancel": "Cancel Payment", | |||
| "payAlert": "Please do not refresh or close this page during payment to avoid payment failure. In case of cancellation, please use the “Cancel Payment” button at the bottom of this page.", | |||
| "payTotalDeatail": "Total Payment Amount", | |||
| "payDeatail": "Total Payment Amount", | |||
| "payTotal": "Total Payment Amount", | |||
| @@ -451,7 +452,9 @@ | |||
| "paymentLimitPrice2":" is only applicable when minimum amount is HK$0.10 and maximum amount is HK$9,999,999.99", | |||
| "paymentLimitPPS":" Payment could not be made via mobile device browsers, please use desktop computers to make payment.", | |||
| "paymentMethod": "Payment Method", | |||
| "paymentProcessLimited":"Please complete the payment process within 15 minutes. Note: For FPS payments, scanning, payment and all necessary approvals must be finished within 3 minutes due to security-related QR code expiry.", | |||
| "paymentProcessLimited1":"Please complete the payment process within 15 minutes.", | |||
| "paymentProcessLimited2":"Please do not refresh or close any page during payment to avoid payment failure.", | |||
| "paymentProcessLimited3":"Note: For FPS payments, scanning, payment and all necessary approvals must be finished within 3 minutes due to security-related QR code expiry.", | |||
| "publicNoticeDetailTitle": "Public Notice Application Information", | |||
| "applyPerson": "Applicant", | |||
| @@ -510,12 +513,14 @@ | |||
| "payNPGOMethod":"NPGO Collection Office", | |||
| "payOnlineBtn":"Pay Online", | |||
| "fpsQrcodeTitle1":"Please scan the following QR code", | |||
| "fpsQrcodeTitle2":"QR code is valid for 3 minutes", | |||
| "fpsQrcodeTitle2":"QR code is valid for 3 minutes only", | |||
| "fpsQrcodeTitle3":"Please complete the payment process within the specified time", | |||
| "fpsQrcodeTitle4":"Remaining time:", | |||
| "fpsQrcodeTitle5":"s", | |||
| "fpsQrcodeExpired":"QR code has expired.", | |||
| "fpsPaymentErrorMsg":"An error occurred while loading the payment QR code. Please do not refresh/reload this page manually during the payment. If the payment was not made successfully, please click 'Cancel payment' button and make the payment again. Sorry for the inconvenience caused.", | |||
| "fpsPaymentErrorMsg1":"The payment QR code is no longer valid after reloading the page. Please verify your banking transaction history:", | |||
| "fpsPaymentErrorMsg2":"If payment was deducted: Please contact Accounts Section (Tel.: 2231 5183/2231 5318) with your transaction details.", | |||
| "fpsPaymentErrorMsg3":"If payment was not deducted: Please click \"Back to Main Page\" and try again after 30 minutes.", | |||
| "fpsSelectPaymentApp":"Please Select Bank App", | |||
| "payDnRemark": "Payment proof (e.g. ATM receipt, internet banking record) to be sent to gld_acct@gld.gov.hk by {date} 12:30 p.m.", | |||
| @@ -90,7 +90,9 @@ | |||
| "fpsQrcodeTitle4":"剩余时间:", | |||
| "fpsQrcodeTitle5":"秒", | |||
| "fpsQrcodeExpired":"二维码已过期", | |||
| "fpsPaymentErrorMsg":"载入支付二维码时发生错误。请勿在付款过程中更新此页面。如果付款未完成,请点击「取消支付」按钮并重新支付。由此造成的不便,敬请谅解。", | |||
| "fpsPaymentErrorMsg1":"页面重新加载时发生错误,支付二维码已失效。请先确认银行交易纪录:", | |||
| "fpsPaymentErrorMsg2":"如已扣款:请联络会计组(电话:2231 5183 / 2231 5318)并提供交易信息。", | |||
| "fpsPaymentErrorMsg3":"如未扣款:请点击「返回主页」并等待30分钟后重新尝试。", | |||
| "fpsSelectPaymentApp":"请选择付款支付程序", | |||
| "payDnRemark": "在{date}下午12时30分前将付款证明(例如银行入数纸或网上银行付款记录)电邮至 gld_acct@gld.gov.hk", | |||
| @@ -159,6 +161,7 @@ | |||
| "iAmSmartNoIdNoMsg": "无效资料,请返回建立账户页面。", | |||
| "mainPage": "主页", | |||
| "backToMainPage": "返回主页", | |||
| "publicNotice": "公共启事", | |||
| "publicNoticeApp": "公共啟事申请", | |||
| "myPublicNotice": "我的公共启事", | |||
| @@ -452,7 +455,7 @@ | |||
| "payIdNRefer": "付款编号 / 付款参考编号", | |||
| "payConfirm": "确认付款", | |||
| "payCancel": "取消付款", | |||
| "payAlert": "请不要关闭此窗口,您可以通过此页面底部的按钮完成此付款或取消此付款。", | |||
| "payAlert": "付款过程中请勿重新整理或关闭此页面,以免支付失败。如需取消,请使用本页底部的「取消付款」按钮。", | |||
| "payTotalDeatail": "付款总额", | |||
| "payDeatail": "付款总额", | |||
| "payTotal": "付款总额", | |||
| @@ -486,7 +489,9 @@ | |||
| "paymentLimitPrice2":"只适用于最小金额为 0.10 港元及最高金额为 9,999,999.99港元", | |||
| "paymentLimitPPS":"付款不适用于流动装置的浏览器,请使用桌面电脑。", | |||
| "paymentMethod": "付款方式", | |||
| "paymentProcessLimited":"请于15分钟内完成付款程序。 注意:使用转数快(FPS)时,因二维码具安全时效限制,须于3分钟内完成扫码、付款及所有相关审核程序。", | |||
| "paymentProcessLimited1":"请于15分钟内完成付款程序。", | |||
| "paymentProcessLimited2":"付款过程中请勿重新整理或关闭任何页面,以免支付失败。", | |||
| "paymentProcessLimited3":"注意:使用转数快(FPS)时,因二维码具安全时效限制,须于3分钟内完成扫码、付款及所有相关审核程序。", | |||
| "publicNoticeDetailTitle": "公共启事申请资料", | |||
| "applyPerson": "申请人", | |||
| @@ -90,7 +90,9 @@ | |||
| "fpsQrcodeTitle4":"剩餘時間:", | |||
| "fpsQrcodeTitle5":"秒", | |||
| "fpsQrcodeExpired":"二維碼已過期", | |||
| "fpsPaymentErrorMsg":"載入支付二維碼時發生錯誤。請勿在付款過程中更新此頁面。如果付款未完成,請點擊「取消支付」按鈕並重新支付。由此造成的不便,敬請諒解。", | |||
| "fpsPaymentErrorMsg1":"頁面重新載入時發生錯誤,支付二維碼已失效。請先確認銀行交易紀錄:", | |||
| "fpsPaymentErrorMsg2":"如已扣款:請聯絡會計組(電話:2231 5183 / 2231 5318)並提供交易資料。", | |||
| "fpsPaymentErrorMsg3":"如未扣款:請點擊「返回主頁」並等待30分鐘後重新嘗試。", | |||
| "fpsSelectPaymentApp":"請選擇付款支付程式", | |||
| "payDnRemark": "在{date}下午12時30分前將付款證明(例如銀行入數紙或網上銀行付款記錄)電郵至 gld_acct@gld.gov.hk", | |||
| @@ -159,6 +161,7 @@ | |||
| "iAmSmartNoIdNoMsg": "無效資料,請返回建立賬戶頁面。", | |||
| "mainPage": "主頁", | |||
| "backToMainPage": "返回主頁", | |||
| "publicNotice": "公共啟事", | |||
| "publicNoticeApp": "公共啟事申請", | |||
| "myPublicNotice": "我的公共啟事", | |||
| @@ -453,7 +456,7 @@ | |||
| "payIdNRefer": "付款編號 / 付款參考編號", | |||
| "payConfirm": "確認付款", | |||
| "payCancel": "取消付款", | |||
| "payAlert": "請不要關閉此窗口,您可以透過本頁底部的按鈕完成此付款或取消本付款。", | |||
| "payAlert": "付款過程中請勿重新整理或關閉此頁面,以免支付失敗。如需取消,請使用本頁底部的「取消付款」按鈕。", | |||
| "payTotalDeatail": "付款總額", | |||
| "payDeatail": "付款總額", | |||
| "payTotal": "付款總額", | |||
| @@ -487,7 +490,9 @@ | |||
| "paymentLimitPrice2":"只適用於最小金額為 0.10 港元及最高金額為 9,999,999.99港元", | |||
| "paymentLimitPPS":"付款不適用於流動裝置的瀏覽器,請使用桌面電腦。", | |||
| "paymentMethod": "付款方法", | |||
| "paymentProcessLimited":"請於15分鐘內完成付款程序。 注意:使用轉數快(FPS)時,因二維碼具安全時效限制,須於3分鐘內完成掃碼、付款及所有相關審核程序。", | |||
| "paymentProcessLimited1":"請於15分鐘內完成付款程序。", | |||
| "paymentProcessLimited2":"付款過程中請勿重新整理或關閉任何頁面,以免支付失敗。", | |||
| "paymentProcessLimited3":"注意:使用轉數快(FPS)時,因二維碼具安全時效限制,須於3分鐘內完成掃碼、付款及所有相關審核程序。", | |||
| "publicNoticeDetailTitle": "公共啟事申請資料", | |||
| "applyPerson": "申請人", | |||