From f07cbc9053fc47a1318f110743aca7525a29d420 Mon Sep 17 00:00:00 2001 From: Alessandro Kreslin Date: Thu, 17 Sep 2026 07:39:12 -0400 Subject: [PATCH 1/7] seal feedback element --- messages/en.json | 2 + messages/fr.json | 2 + .../SealOfReliabilityDescriptionPage.tsx | 3 + .../components/HowItIsCalculatedPage.tsx | 3 + src/app/components/SealFeedbackButton.tsx | 100 ++++++++++++++++++ .../Feed/components/FeedReliabilityView.tsx | 2 + src/app/utils/seal-feedback-url.spec.ts | 48 +++++++++ src/app/utils/seal-feedback-url.ts | 56 ++++++++++ 8 files changed, 216 insertions(+) create mode 100644 src/app/components/SealFeedbackButton.tsx create mode 100644 src/app/utils/seal-feedback-url.spec.ts create mode 100644 src/app/utils/seal-feedback-url.ts diff --git a/messages/en.json b/messages/en.json index 09e888f0..c9ee7560 100644 --- a/messages/en.json +++ b/messages/en.json @@ -1,5 +1,7 @@ { "common": { + "sealFeedbackButtonLabel": "Got feedback?", + "sealFeedbackButtonAriaLabel": "Share feedback on the Seal of Reliability (opens in a new tab)", "copyToClipboard": "Copy to clipboard", "copied": "Copied!", "name": "Name", diff --git a/messages/fr.json b/messages/fr.json index f3a76031..914395bb 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -1,5 +1,7 @@ { "common": { + "sealFeedbackButtonLabel": "Des commentaires ?", + "sealFeedbackButtonAriaLabel": "Donnez votre avis sur le Sceau de fiabilité (ouvre un nouvel onglet)", "copyToClipboard": "Copier dans le presse-papiers", "copied": "Copié!", "name": "Nom", diff --git a/src/app/[locale]/seal-of-reliability/components/SealOfReliabilityDescriptionPage.tsx b/src/app/[locale]/seal-of-reliability/components/SealOfReliabilityDescriptionPage.tsx index ff904200..e8dcecc9 100644 --- a/src/app/[locale]/seal-of-reliability/components/SealOfReliabilityDescriptionPage.tsx +++ b/src/app/[locale]/seal-of-reliability/components/SealOfReliabilityDescriptionPage.tsx @@ -23,6 +23,7 @@ import { getTranslations } from 'next-intl/server'; import CardSectionTitle from '../../../components/CardSectionTitle'; import SectionContainer from '../../../components/SectionContainer'; import SealOfReliability from '../../../components/SealOfReliability'; +import SealFeedbackButton from '../../../components/SealFeedbackButton'; import { accordionStyle } from '../../../components/accordionStyle'; import { Link as LocaleLink } from '../../../../i18n/navigation'; import { @@ -49,6 +50,8 @@ export default async function SealOfReliabilityDescriptionPage(): Promise + + { return ( + + { + setIsMounted(true); + }, []); + + // Before mount the link still points at the form, just without the + // prefill, so it works for anyone who clicks during hydration. + const feedbackUrl = useMemo( + () => + isMounted ? buildSealFeedbackUrl({ name, email }) : SEAL_FEEDBACK_URL, + [isMounted, name, email], + ); + + return ( + <> + + theme.zIndex.appBar - 1, + }} + > + + + + ); +} diff --git a/src/app/screens/Feed/components/FeedReliabilityView.tsx b/src/app/screens/Feed/components/FeedReliabilityView.tsx index a5044641..686a3966 100644 --- a/src/app/screens/Feed/components/FeedReliabilityView.tsx +++ b/src/app/screens/Feed/components/FeedReliabilityView.tsx @@ -34,6 +34,7 @@ import { getCoverageWindowLength } from '../lib/continuous-coverage'; import { getLatestCoverageWindow } from '../lib/fresh-coverage'; import { displayFormattedDate } from '../../../utils/date'; import SectionContainer from '../../../components/SectionContainer'; +import SealFeedbackButton from '../../../components/SealFeedbackButton'; interface Props { feed: AllFeedType; @@ -102,6 +103,7 @@ export default async function FeedReliabilityView({ > + diff --git a/src/app/utils/seal-feedback-url.spec.ts b/src/app/utils/seal-feedback-url.spec.ts new file mode 100644 index 00000000..7caf04db --- /dev/null +++ b/src/app/utils/seal-feedback-url.spec.ts @@ -0,0 +1,48 @@ +import { buildSealFeedbackUrl, SEAL_FEEDBACK_URL } from './seal-feedback-url'; + +describe('buildSealFeedbackUrl', () => { + it('returns the bare form URL when nothing is known about the user', () => { + const url = buildSealFeedbackUrl(); + + expect(url).toContain('/viewform'); + expect(url).not.toContain('usp=pp_url'); + expect(url).not.toContain('entry.'); + }); + + it('exposes the same bare URL the server renders before hydration', () => { + expect(SEAL_FEEDBACK_URL).toBe(buildSealFeedbackUrl()); + }); + + it('prefills both name and email when both are available', () => { + const url = new URL( + buildSealFeedbackUrl({ + name: 'Ada Lovelace', + email: 'ada@example.org', + }), + ); + + expect(url.searchParams.get('usp')).toBe('pp_url'); + expect(url.searchParams.get('entry.1481076408')).toBe('Ada Lovelace'); + expect(url.searchParams.get('entry.1610387831')).toBe('ada@example.org'); + }); + + it('omits the fields it does not have', () => { + const url = new URL(buildSealFeedbackUrl({ email: 'ada@example.org' })); + + expect(url.searchParams.has('entry.1481076408')).toBe(false); + expect(url.searchParams.get('entry.1610387831')).toBe('ada@example.org'); + }); + + it('ignores blank and null values rather than prefilling empty fields', () => { + const url = buildSealFeedbackUrl({ name: ' ', email: null }); + + expect(url).not.toContain('usp=pp_url'); + expect(url).not.toContain('entry.'); + }); + + it('trims surrounding whitespace before prefilling', () => { + const url = new URL(buildSealFeedbackUrl({ name: ' Ada Lovelace ' })); + + expect(url.searchParams.get('entry.1481076408')).toBe('Ada Lovelace'); + }); +}); diff --git a/src/app/utils/seal-feedback-url.ts b/src/app/utils/seal-feedback-url.ts new file mode 100644 index 00000000..a56ef035 --- /dev/null +++ b/src/app/utils/seal-feedback-url.ts @@ -0,0 +1,56 @@ +/** + * Google Form collecting feedback on the Seal of Reliability. + * + * The canonical `viewform` URL rather than its `forms.gle` short link: + * prefill parameters only survive on the long form, the short link drops the + * query string when it redirects. + */ +const SEAL_FEEDBACK_FORM_URL = + 'https://docs.google.com/forms/d/e/1FAIpQLSdKlJEdxA8Jz8g6Z04h3PsWbQ9qPsjp93qONrWvQR60xJT3-Q/viewform'; + +/** + * Field ids taken from the form's own "Get pre-filled link" output. They are + * stable for the life of a question, but deleting and re-adding the "Name" or + * "Email" question in the form mints a new id and silently stops the prefill - + * regenerate them from the form if prefilled values stop showing up. + */ +const NAME_ENTRY_ID = 'entry.1481076408'; +const EMAIL_ENTRY_ID = 'entry.1610387831'; + +/** The form with nothing prefilled - what the server renders. */ +export const SEAL_FEEDBACK_URL = SEAL_FEEDBACK_FORM_URL; + +export interface SealFeedbackPrefill { + name?: string | null; + email?: string | null; +} + +/** + * Builds the feedback form URL, prefilling the respondent's name and email + * when the app knows them. Blank or missing values are left out entirely so + * the form renders an empty field rather than an empty prefill. + */ +export function buildSealFeedbackUrl( + prefill: SealFeedbackPrefill = {}, +): string { + const url = new URL(SEAL_FEEDBACK_FORM_URL); + const name = prefill.name?.trim() ?? ''; + const email = prefill.email?.trim() ?? ''; + + if (name === '' && email === '') { + return url.toString(); + } + + // Google's own marker for a prefilled link; without it the form ignores the + // entry parameters. + url.searchParams.set('usp', 'pp_url'); + + if (name !== '') { + url.searchParams.set(NAME_ENTRY_ID, name); + } + if (email !== '') { + url.searchParams.set(EMAIL_ENTRY_ID, email); + } + + return url.toString(); +} From e7403a569762cb1ca01a8a1f45ec77329c2ae842 Mon Sep 17 00:00:00 2001 From: Alessandro Kreslin Date: Thu, 17 Sep 2026 07:56:56 -0400 Subject: [PATCH 2/7] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/app/components/SealFeedbackButton.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/app/components/SealFeedbackButton.tsx b/src/app/components/SealFeedbackButton.tsx index ce444fab..f1b4d9ad 100644 --- a/src/app/components/SealFeedbackButton.tsx +++ b/src/app/components/SealFeedbackButton.tsx @@ -26,10 +26,18 @@ import { export default function SealFeedbackButton(): ReactElement { const t = useTranslations('common'); const user = useAppSelector(selectUserProfile); - const { email: sessionEmail, displayName } = useAuthSession(); + const { + email: sessionEmail, + displayName, + isAuthenticated, + } = useAuthSession(); - const name = user?.fullName ?? displayName; - const email = user?.email ?? sessionEmail; + const name = isAuthenticated + ? user?.fullName?.trim() || displayName + : null; + const email = isAuthenticated + ? user?.email?.trim() || sessionEmail + : null; /** * Who the visitor is exists only on the client, so prefilling during the From 7c771eff1a10de855e2163aaeae653f8937b5bf0 Mon Sep 17 00:00:00 2001 From: Alessandro Kreslin Date: Thu, 17 Sep 2026 07:57:12 -0400 Subject: [PATCH 3/7] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/app/components/SealFeedbackButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/SealFeedbackButton.tsx b/src/app/components/SealFeedbackButton.tsx index f1b4d9ad..586d03e8 100644 --- a/src/app/components/SealFeedbackButton.tsx +++ b/src/app/components/SealFeedbackButton.tsx @@ -80,7 +80,7 @@ export default function SealFeedbackButton(): ReactElement { marginTop: 'auto', marginBottom: 'auto', zIndex: (theme) => theme.zIndex.appBar - 1, - transform: 'rotate(270deg)' + transform: 'rotate(270deg)', }} startIcon={} > From 972d60b191e1d0df42a271b46ef272a99ed5ef79 Mon Sep 17 00:00:00 2001 From: Alessandro Kreslin Date: Thu, 17 Sep 2026 08:01:21 -0400 Subject: [PATCH 4/7] lint fix --- src/app/components/SealFeedbackButton.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/app/components/SealFeedbackButton.tsx b/src/app/components/SealFeedbackButton.tsx index 586d03e8..9575a873 100644 --- a/src/app/components/SealFeedbackButton.tsx +++ b/src/app/components/SealFeedbackButton.tsx @@ -32,12 +32,8 @@ export default function SealFeedbackButton(): ReactElement { isAuthenticated, } = useAuthSession(); - const name = isAuthenticated - ? user?.fullName?.trim() || displayName - : null; - const email = isAuthenticated - ? user?.email?.trim() || sessionEmail - : null; + const name = isAuthenticated ? user?.fullName?.trim() || displayName : null; + const email = isAuthenticated ? user?.email?.trim() || sessionEmail : null; /** * Who the visitor is exists only on the client, so prefilling during the From 100038802608876d14403263b54772b7714e0474 Mon Sep 17 00:00:00 2001 From: Alessandro Kreslin Date: Thu, 17 Sep 2026 10:01:55 -0400 Subject: [PATCH 5/7] better google form url --- src/app/utils/seal-feedback-url.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/app/utils/seal-feedback-url.ts b/src/app/utils/seal-feedback-url.ts index a56ef035..7db9be39 100644 --- a/src/app/utils/seal-feedback-url.ts +++ b/src/app/utils/seal-feedback-url.ts @@ -1,12 +1,8 @@ /** - * Google Form collecting feedback on the Seal of Reliability. - * - * The canonical `viewform` URL rather than its `forms.gle` short link: - * prefill parameters only survive on the long form, the short link drops the - * query string when it redirects. + * It's a redirect to a google form */ const SEAL_FEEDBACK_FORM_URL = - 'https://docs.google.com/forms/d/e/1FAIpQLSdKlJEdxA8Jz8g6Z04h3PsWbQ9qPsjp93qONrWvQR60xJT3-Q/viewform'; + 'https://share.mobilitydata.org/sealfeedback'; /** * Field ids taken from the form's own "Get pre-filled link" output. They are From f9cc379dbe9300172d7ee7a29da3a1ad41e66e19 Mon Sep 17 00:00:00 2001 From: Alessandro Kreslin Date: Thu, 17 Sep 2026 10:35:28 -0400 Subject: [PATCH 6/7] lint --- src/app/utils/seal-feedback-url.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/app/utils/seal-feedback-url.ts b/src/app/utils/seal-feedback-url.ts index 7db9be39..c56fce7b 100644 --- a/src/app/utils/seal-feedback-url.ts +++ b/src/app/utils/seal-feedback-url.ts @@ -1,8 +1,7 @@ /** * It's a redirect to a google form */ -const SEAL_FEEDBACK_FORM_URL = - 'https://share.mobilitydata.org/sealfeedback'; +const SEAL_FEEDBACK_FORM_URL = 'https://share.mobilitydata.org/sealfeedback'; /** * Field ids taken from the form's own "Get pre-filled link" output. They are From f8a2c578c72595a187488688dbbb36b1256e0ae8 Mon Sep 17 00:00:00 2001 From: Alessandro Kreslin Date: Thu, 17 Sep 2026 10:46:27 -0400 Subject: [PATCH 7/7] unit test fix --- src/app/utils/seal-feedback-url.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/utils/seal-feedback-url.spec.ts b/src/app/utils/seal-feedback-url.spec.ts index 7caf04db..be2737c0 100644 --- a/src/app/utils/seal-feedback-url.spec.ts +++ b/src/app/utils/seal-feedback-url.spec.ts @@ -4,7 +4,7 @@ describe('buildSealFeedbackUrl', () => { it('returns the bare form URL when nothing is known about the user', () => { const url = buildSealFeedbackUrl(); - expect(url).toContain('/viewform'); + expect(url).toBe('https://share.mobilitydata.org/sealfeedback'); expect(url).not.toContain('usp=pp_url'); expect(url).not.toContain('entry.'); });