11import { css } from '@emotion/react' ;
22import { useEffect , useState } from 'react' ;
33import { getAuthStatus , subscribeToAuthStateChange } from '../lib/identity' ;
4+ import { resolvePuzzleIframeUrl } from '../lib/puzzleIframeUrl' ;
45import { useMatchMedia } from '../lib/useMatchMedia' ;
6+ import type { PuzzleConfig } from '../model/puzzles/puzzleConfigs' ;
57
68interface 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 */
4157export 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 */
166174export 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 */
219224export 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
0 commit comments