Skip to content

Commit c3a4c99

Browse files
Wire PuzzleIframe to the new per-provider URL builder strategy
PuzzleIframe.island.tsx now receives the full PuzzleConfig (replacing a previously pre-resolved src: string prop), since resolving the iframe URL now needs the reactive userId/darkMode context this component itself computes client-side. buildPuzzleIframeSrc's responsibility is now cleanly split in two: resolvePuzzleIframeUrl (src/lib/puzzleIframeUrl.ts) builds the provider-specific base URL (including AmuseLabs' uid/darkMode=0|1), then this function layers DCR's own guardian-puzzle-context JSON blob on top, unchanged, still applied uniformly to every provider regardless of config.iframe.provider. PuzzlePageLayout.tsx now passes puzzleConfig straight through to PuzzleIframe instead of pre-resolving a src via the old resolveIframeUrl. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 732d64e commit c3a4c99

2 files changed

Lines changed: 51 additions & 49 deletions

File tree

dotcom-rendering/src/components/PuzzleIframe.island.tsx

Lines changed: 49 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
import { css } from '@emotion/react';
22
import { useEffect, useState } from 'react';
33
import { getAuthStatus, subscribeToAuthStateChange } from '../lib/identity';
4+
import { resolvePuzzleIframeUrl } from '../lib/puzzleIframeUrl';
45
import { useMatchMedia } from '../lib/useMatchMedia';
6+
import type { PuzzleConfig } from '../model/puzzles/puzzleConfigs';
57

68
interface Props {
7-
/** The already-resolved iframe src URL for this puzzle. */
8-
src: string;
9+
/**
10+
* The puzzle's own `PuzzleConfig`, used to resolve the provider-specific
11+
* iframe URL (`set`/`baseUrl`, etc., see `src/lib/puzzleIframeUrl.ts`)
12+
* client-side, since that resolution needs the reactive `userId`/
13+
* `darkMode` context this component itself computes. Replaces a
14+
* previous, pre-resolved `src: string` prop.
15+
*/
16+
puzzleConfig: PuzzleConfig;
917
title: string;
1018
/**
1119
* Whether dark mode is available for this page/request at all (the
@@ -37,6 +45,14 @@ const frameStyles = css`
3745
* (AmuseLabs, Wordiply) to personalise/save progress against a real
3846
* account and render consistently with the reader's colour scheme, rather
3947
* than guessing at either.
48+
*
49+
* Structurally a superset of `PuzzleUrlContext`
50+
* (`src/lib/puzzleIframeUrl.ts`, `{ userId, darkMode }`, what the
51+
* per-provider URL-building strategy needs) plus `puzzleDate`, which no
52+
* provider's URL strategy currently uses. Deliberately not split into two
53+
* separate types: a `PuzzleContext` value can be passed anywhere a
54+
* `PuzzleUrlContext` is expected as-is (TypeScript's structural typing
55+
* allows the extra `puzzleDate` field), so there is nothing to duplicate.
4056
*/
4157
export interface PuzzleContext {
4258
/**
@@ -136,49 +152,39 @@ const buildPuzzleContext = (
136152
});
137153

138154
/**
139-
* Encodes `context` as JSON into a `guardian-puzzle-context` query
140-
* parameter on `src` (preserving any existing query parameters, e.g.
141-
* AmuseLabs' `?set=...&embed=1&idx=1`), and, separately, appends a plain
142-
* `uid=<userId>` query parameter when the reader is signed in.
155+
* Resolves the final iframe `src` for a puzzle in two steps, with a
156+
* deliberately clean split of responsibility:
143157
*
144-
* `uid` is a genuinely different, independently-confirmed mechanism from
145-
* `guardian-puzzle-context`: the native (Android/iOS) apps' own real,
146-
* working AmuseLabs integration appends `&uid=<value>` as a plain query
147-
* parameter when the user is authenticated, and omits it entirely when
148-
* signed out (their own words: "If the user is authenticated, we add
149-
* &uid=<puzzleId>"). It is added here *alongside*, not instead of,
150-
* `guardian-puzzle-context` (which still carries dark mode and puzzle
151-
* date, for which there is no separately-confirmed mechanism yet). `uid`'s
152-
* value is sourced identically to `context.userId`
153-
* (`idToken.claims.legacy_identity_id`), the native apps call their
154-
* equivalent value a "puzzleId", but there is no independent confirmation
155-
* that identifier format matches ours, only that this exact query
156-
* parameter name/pattern is what they use for their own equivalent value,
157-
* see the "Open questions" section of docs/puzzle-page.md.
158+
* 1. `resolvePuzzleIframeUrl` (`src/lib/puzzleIframeUrl.ts`) builds the
159+
* provider-specific base URL, including whichever query params that
160+
* specific provider actually supports (e.g. AmuseLabs' `uid`/
161+
* `darkMode=0|1`, confirmed per-provider, not applied to every
162+
* provider generically).
163+
* 2. This function then layers DCR's own `guardian-puzzle-context` JSON
164+
* blob on top, as a query parameter, applied uniformly to every
165+
* provider regardless of `puzzleConfig.iframe.provider`. This is our
166+
* own generic, additional channel, not a provider-specific mechanism
167+
* (providers that don't understand it simply ignore it), so it
168+
* deliberately stays outside the per-provider strategy in step 1.
158169
*
159-
* Always includes `guardian-puzzle-context` - unlike the previous
160-
* `userId`-only mechanism, the context shape itself always carries all
161-
* three fields, so there's no "nothing to add" case to omit it for. `uid`
162-
* is the only conditional part, present only when `context.userId` is
163-
* non-null, and never sent as `uid=null` or empty. Returns `src` unchanged
164-
* if it cannot be parsed as an absolute URL.
170+
* Returns the provider URL unchanged if it cannot be parsed as an absolute
171+
* URL (steps 2's `guardian-puzzle-context` is simply not added in that
172+
* case).
165173
*/
166174
export const buildPuzzleIframeSrc = (
167-
src: string,
175+
puzzleConfig: PuzzleConfig,
168176
context: PuzzleContext,
169177
): string => {
178+
const providerUrl = resolvePuzzleIframeUrl(puzzleConfig, context);
170179
try {
171-
const url = new URL(src);
180+
const url = new URL(providerUrl);
172181
url.searchParams.set(
173182
'guardian-puzzle-context',
174183
JSON.stringify(context),
175184
);
176-
if (context.userId !== null) {
177-
url.searchParams.set('uid', context.userId);
178-
}
179185
return url.toString();
180186
} catch {
181-
return src;
187+
return providerUrl;
182188
}
183189
};
184190

@@ -205,27 +211,26 @@ const postContextMessage = (
205211
* iframe `src` (so it is present from the very first request the iframe
206212
* makes), and via `postMessage` once the iframe has loaded (`{ type:
207213
* 'guardian-puzzle-context', context }` - see `PuzzleContextMessage`).
208-
* When the reader is signed in, also appends a plain `uid=<userId>` query
209-
* parameter alongside `guardian-puzzle-context` (see
210-
* `buildPuzzleIframeSrc`'s doc comment), a separately-confirmed mechanism
211-
* from the native apps' real AmuseLabs integration. Because `src` is
212-
* derived from the reactive `usePuzzleUserId()`/`usePuzzleDarkMode()`
213-
* results, the iframe is automatically reloaded by the browser (a fresh
214-
* `src` triggers a new navigation) whenever the reader's sign-in state or
215-
* OS colour-scheme preference changes while on the page - no manual
216-
* reload fallback is needed for that case, though the `onLoad`
217-
* `postMessage` still fires again after each such reload too.
214+
* The provider-specific portion of the URL (e.g. AmuseLabs' `uid`/
215+
* `darkMode=0|1` query params) is resolved separately per provider, see
216+
* `buildPuzzleIframeSrc`'s doc comment. Because `src` is derived from the
217+
* reactive `usePuzzleUserId()`/`usePuzzleDarkMode()` results, the iframe is
218+
* automatically reloaded by the browser (a fresh `src` triggers a new
219+
* navigation) whenever the reader's sign-in state or OS colour-scheme
220+
* preference changes while on the page - no manual reload fallback is
221+
* needed for that case, though the `onLoad` `postMessage` still fires
222+
* again after each such reload too.
218223
*/
219224
export const PuzzleIframe = ({
220-
src,
225+
puzzleConfig,
221226
title,
222227
darkModeAvailable,
223228
puzzleDate,
224229
}: Props) => {
225230
const userId = usePuzzleUserId();
226231
const darkMode = usePuzzleDarkMode(darkModeAvailable);
227232
const context = buildPuzzleContext(userId, darkMode, puzzleDate);
228-
const iframeSrc = buildPuzzleIframeSrc(src, context);
233+
const iframeSrc = buildPuzzleIframeSrc(puzzleConfig, context);
229234

230235
return (
231236
<iframe

dotcom-rendering/src/layouts/PuzzlePageLayout.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,7 @@ import { ArticleDesign, ArticleDisplay, Pillar } from '../lib/articleFormat';
1919
import { formatPuzzleDate } from '../lib/puzzleDate';
2020
import { isPuzzlesHubV1Enabled } from '../lib/puzzlesHubVersionExperiment';
2121
import type { NavType } from '../model/extract-nav';
22-
import {
23-
type PuzzleConfig,
24-
resolveIframeUrl,
25-
} from '../model/puzzles/puzzleConfigs';
22+
import type { PuzzleConfig } from '../model/puzzles/puzzleConfigs';
2623
import { palette as themePalette } from '../palette';
2724
import type { FEPuzzlePageType } from '../types/puzzlePage';
2825

@@ -164,7 +161,7 @@ const PuzzlePageContent = ({
164161
return (
165162
<Island priority="critical" defer={{ until: 'visible' }}>
166163
<PuzzleIframe
167-
src={resolveIframeUrl(puzzleConfig)}
164+
puzzleConfig={puzzleConfig}
168165
title={instance.title}
169166
darkModeAvailable={darkModeAvailable}
170167
puzzleDate={instance.puzzleDate ?? null}

0 commit comments

Comments
 (0)