Conversation
25e77e7 to
5b9f030
Compare
|
You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool. What Enabling Code Scanning Means:
For more information about GitHub Code Scanning, check out the documentation. |
|
Merged: engine hardening for the native scaffold. The legacy token is adopted only when the previous shell actually stored one; the account modal gained its Done item with a router-level test that the modal closes; PlistBuddy failures in the iOS privacy script surface through the script's own exit path and the aps-environment key follows the build configuration; the expo-crypto digest shim has its own test. Verified with root pnpm check, native jest on both platforms, and a full CI run. |
|
Merged: the deterministic checks. Biome runs with warnings as errors and a cognitive complexity gate at 15, every suppression carrying a written reason. size-limit budgets cover the web initial load, all web JS and CSS, and both Hermes bundles. The release lanes assert the APK and the exported IPA against download ceilings out of the built artifacts. CodeQL scans source and workflows and gates the mobile release. The footprint job measures base and head in one run and recreates its comment on every push. Every gate was shown to fail under a planted violation before it was kept. |
|
Merged: the sync contract. Read failures are typed rather than collapsed to one flag. The read pool owns rate limits, honouring Retry-After and publishing one observable pause that every screen reads; the query layer owns transport and 5xx retries, so each failure kind has exactly one bounded ladder. The strip's states, in precedence: offline, rate limited with a live countdown, retrying, unreachable with the cause named and a Retry action, pending. A mark is green for the 5 second undo window, the same number the snackbar uses, then advancing, checked and disabled with a quiet dot while the write is queued, until Trakt confirms and the next episode takes the row. The fake Trakt gained fault modes (429 with Retry-After, 5xx, delay, hold, drop) and a reset control, and the mock lane drives both defects end to end. Merge-scoped footprint against the previous tip: product +1173, tests +1789, web initial load +1.2 kB, Hermes bundles +47.8 kB iOS and +32.2 kB Android, complexity profile unchanged. |
|
Merged: the native design foundation. packages/native/src/ui holds the tokens (dynamic colour pairs on iOS, scheme-resolved on Android, gated token for token against the web stylesheet's two theme blocks), eleven type roles per platform bound to Dynamic Type styles and Material 3 tokens with a test anchored to Apple's Large sizes and the Material scale, the snackbar hosts for the root, the sheet and the account modal, the app-idle marker, the accessibility id vocabulary, and the first primitives (row, check control, section header, empty state, sync strip). The splash stays up until the stores and fonts settle. Screen readers get the longer snackbar window. |
|
Two fixed behaviours, recorded on the web app running against the repository's fake Trakt ( Marking an episodecue-marking-an-episode.mp4First take: the check on the queue row is tapped and turns green, and the snackbar offers Undo ("Midnight Cartography S2 E3 marked"). The row advances to S2 E4 in the same frame, on the clock rather than on a round trip. The write is held by the fake for 7 seconds, so the undo window ( The sync strip under faultscue-sync-strip-under-faults.mp4Healthy is silence: no strip. One 429 with Two things in the recordings are worth a look before the native screens copy them:
|
|
Merged: size and quality budgets anchored to published norms. A committed quality baseline ratchets the count of cognitive-complexity suppressions (21 today) and the worst measured complexity (71 today) downward only, with zero suppressions allowed under the native app, and the check refuses a baseline that outruns the measurement. TypeScript suppressions are rejected across every package's source, tests, app routes and modules. Android release builds now ship with R8 and resource shrinking; CI builds the App Bundle, estimates the Play download for an arm64 xxhdpi device with bundletool (17.4 MB today, gated at 20 MB), gates the bundle itself, and derives the universal APK for the permission gate. The old 200 MB universal-APK ceiling is gone. The footprint comment reports comment density per package. Two gates written for the deleted web app (Vite size budgets, Lighthouse against vite preview) were dropped before merge, since the web UI is retired with the shells. |
|
Merged: the Maestro launch harness and the fake Trakt's missing controls. The fake Trakt gains six selectable seed states behind its reset control and the fault modes the parity flows need (a one-shot 401, a refused refresh, a 429 mid fan-out, a held and a dropped write, a failing history page, a pre-seeded op-log), each proven observable from a client. A Maestro launch flow asserts the onboarding screen, the connect button, the device code and the arrival on Up Next, with a test that every id a flow names exists in the app's id vocabulary. CI's iOS job now uploads the simulator app and a new native-e2e job on macOS installs it, boots a simulator, starts the fake and runs the flow. Getting that job green found and fixed a real launch defect: an unsigned simulator binary carries no entitlements, so every Keychain call failed and the auth store never left loading; the build is signed for the simulator now, the packaging step asserts the entitlements section is present, a token store that cannot answer drops to onboarding with a message instead of hanging, and the boot gates render a one-point frame so a hierarchy dump names whichever gate is holding. |
The browser reclassification is about a response Cloudflare sent and the browser refused to show. A socket this client aborted itself is the connection failing, so on the web it was telling a reader with no network that Trakt was having trouble.
An advancing row with no episode was let into the queue past the rules a projected one has always obeyed, so finishing an ended show left it on the list. The advance is one flag on the show, the exclusions apply to both shapes of it, and the zero-id sentinel stops being a second answer to the same question. On the web the marquee keeps its slot as a row rather than headlining an episode nobody can name, and an absent episode line no longer leaves an empty one behind.
The Capacitor line is the one that ships today, and its own manifest really does strip the permission its notifications plugin merges, which the privacy claims test pins; the README sentence saying so was true and had to come back. The APK gate's count of what app.config.ts drops moved with the list. The config test that was left asserting the absence of a string nothing declares is gone: the built APK is the only place that set can be checked honestly, and the gate already says so.
On a dark simulator the bar drew the light page fill: cream behind the large title, a light status bar over a dark screen, and the app's own theme starting one band down. The navigators had no theme, so they kept the library's, which is fixed light. They now take a theme built from the palette for the scheme, which is what the bar's fill, its title, its back chevron and the hairline under a scrolled bar all read. A bar is a UIKit surface configured through props rather than styles, so it resolves a color once when it is set and against the default traits: it has to be handed a resolved one and a new theme when the scheme changes, which is the opposite of every surface React Native draws. The screen's own header background goes with it. It existed to make the bar opaque over poster artwork, and the theme's card color already is.
Three things the card only gets wrong once it stops being artwork, and all three show at the largest text size, where every card is this shape. Its fill is #ffffff on a #fbfaf7 page, 1.04:1, so without an edge the card is not a card. It now carries the same hairline every other surface with that problem carries. Its stack sat on the bottom, which is where a stack belongs on a scrim whose strongest end is down there. On a plain surface it starts at the top, beside a poster that no longer has artwork the height of the card to sit in the middle of. Its eyebrow was amber on both compositions. Over artwork it takes the quiet ink the rest of the text over artwork takes; on the plain surface it takes the accent ink, because amber reads 1.97:1 against that fill and this is the one line on the screen that says an episode is new.
At the accessibility sizes a queue row is several lines tall and its poster is still 48 by 72, so centring the two left the poster floating against the middle of a paragraph with the title beginning well above it. The row already changes shape at this threshold to put its controls below the text; the artwork goes to the top of the text with it, which is the composition the row is drawn as.
Keep the pre-push aggregate aligned with CI so the size-limit regression guard covers both Hermes bundles before changes leave a workstation.
Record each measurement and web.dev reduction target beside its regression ceiling. Preserve the 20 MB Play decision and reject any later increase across committed history.
Apply Chromium binary size policy to both Hermes bundles and the Play download estimate. Growth above 64 kB requires a Binary-Size rationale in the pull request body.
Measure Android at Play reference density and across every bundletool dimension instead of gating AAB bytes. Gate the largest compressed variant from Apple App Thinning reports at 40 MB while retaining the IPA smoke ceiling.
Use Expo Router drawable tab icons so Android no longer loads the Material Symbols font. Gate Expo export metadata against the committed asset inventory so transitive additions require an explicit decision.
Pin Expo Atlas and preserve its production bundle graph as a pull request artifact. Add per-platform package totals to the footprint comment so Chromium-style size deltas have actionable attribution.
Seven behaviour changes landed on assertions written for the old behaviour, and the lanes that would have caught it never ran. Scoping the mark means a mark no longer re-reads the aggregate, so a 429 armed on that read never fires and a failed confirming read no longer reaches the strip: what carries it is the advancing row, which is what these now assert. The honest projection means the queue's lead show, one episode from its finale, names none rather than inventing S2 E11. And in a browser an unreadable answer from Trakt's own origin now says so instead of naming the reader's connection.
The per-flow budget asserted exact counts and one mark at 2; a mark costs 3, because the home screen's Previously section reads its own history page, so the gate could not have passed. It is a ceiling now, set from a measurement plus room for one retry, with the write counted separately so a mark that stopped reaching Trakt fails it too. MOCK_TRAKT_PORT moves the fake off its fixed port for the same reason E2E_PREVIEW_PORT exists, and the account reset now lets go of anything a hold fault left waiting, which its own docblock already promised.
Splitting the exclusions and the air test out of groupUpNext took it under the cognitive-complexity limit, so its biome-ignore is gone and the ratchet wants the lower number.
js-yaml and @xmldom/xmldom picked up high-severity advisories after this branch was cut, both through Expo's build tooling and neither reachable from anything Cue ships. The lockfile is unchanged by the rest of this branch, so the gate is red on the base too. Three overrides in the shape the file already uses, each a patch release inside its own major: xmldom 0.9.10 to 0.9.12, js-yaml 3.15.1 to 3.15.2 and 4.3.1 to 4.3.2.
Xcode documents the thinning export option as applying to non App Store exports only, and the release lane exports for the App Store, so the report the assertion reads is never produced and every iOS build would fail on a missing file. The IPA smoke ceiling stays; the App Store download figure has to come from App Store Connect until an ad hoc export lane exists.
Expo CLI ships EXPO_ROUTER_DISABLE_NATIVE_TABS_MD, which swaps the Android material icon converter for a stub and tree-shakes expo-symbols out of the bundle. Setting it in the Metro config covers every bundle the project builds, including the one Gradle embeds, and removes both the hand written resolver override and the 111 kB glyph table the override left behind. The Android bundle drops from 3896638 to 3803699 bytes.
The fake omitted movie fanart and related media, so simulator checks could not exercise the detail hero or recommendations. Add contract-backed fixture variants and flow assertions to keep those surfaces visible.
Android needs markers above system navigation, while iOS timing markers must remain outside scroll content to stay in the accessibility hierarchy.
The first iOS CI pass exposed a ghost tap on the visible season action after the related rail loaded. Let Maestro retry an ignored tap so the flow reaches its existing confirmation assertion reliably.
|
Merged: the Search screen, 1279 lines added and 106 deleted (net +1173). Product-Growth: about 450 product lines are the screen (native header search field, session recent terms, browse grids per enabled medium with zero-hit grids dropped, results with the Watchlist pill in three faces and its optimistic add, searching, error, no-results, media-hidden and offline states); the Library tile is reused for browse; the rest is tests, a flow and fake Trakt routes. Binary-Size: JavaScript only; no native change (numbers in the footprint comment on PR #59). Also in this merge: the shared sign-in flow now waits for the app's own idle marker instead of asserting the skeleton gone on a fixed timeout, which removes the intermittent failure several PRs hit; the coming-soon placeholder and its test are deleted because no screen uses them any more. The search tab stays a plain tab: the iOS 26 search-tab role swallows the tab bar into its own field and never presents the screen's search controller. Stills in matched light and dark pairs and a clip are on PR #59; a strict visual review found them clean apart from the system-drawn search field clipping at the largest text size, recorded for the Dynamic Type pass. |
Related content length changed the scroll position enough for the native header to cover the season action during the bulk-mark flow. Center that action before tapping so the visual path exercises both bulk marking and the related rail.
The base branch advanced with search fixtures and CI flow coverage while this detail work was in progress. Combine both fixture sets and both iOS flows so the PR remains mergeable without dropping either feature.
Maestro considered the Season 2 action visible while the native header still covered its tap target. Center the known episode row so the season action is safely hittable before opening the bulk-mark sheet.
A form sheet presented while the account full screen modal is already up never registers in the iOS accessibility tree: the sheet draws, but its container holds no elements, so VoiceOver and the Maestro flow cannot reach a month. The same happens to the episode sheet opened from a History row, so this is the nesting rather than the sheet's contents. The jump now draws inside the History screen as a modal panel over a dimmed backdrop, with an explicit Close where the sheet had a grabber. Picking sets the search params in place, which retires the route, its parameter round trip and the medium it had to carry back.
The centered episode lookup still settles at the sticky Continue boundary on iOS, leaving the Season 2 action covered. Drag the detail content down before tapping so the confirmation control is safely hittable.
The CI artifact proved the original tap could land under sticky content. Keep the corrected scroll position and retry an ignored iOS tap so the confirmation sheet opens reliably.
At the largest accessibility text the six column grids broke year labels
across lines ("202" over "6") and the panel grew past the top of the
screen, taking its Close control with it. The grids now fall to three
columns above a 1.3 text scale, and the panel caps at four fifths of the
window with its heading pinned and its body scrolling.
The leading time column was fixed at 62 pt, which broke "4:14 PM" inside
the meridiem; it now scales with the text. The panel draws on the
overlay token so it reads as raised against the dimmed page on dark.
Account navigation needs an opaque surface so sticky day labels remain visible while scrolling. A default same-day repeat makes the collapse badge testable in the device flow.
Season 2's header sat under the native nav bar once the flow scrolled past it: the scroll centered on an episode row further down the list, and a blind corrective swipe added after that did not bring it back. On load the header is already visible below the continue bar, so tap season-check-2 there instead of scrolling to it, and drop the swipe and tap-retry that were compensating for the wrong scroll target.
The preceding episode flow mutates the fake account even after Undo, which can move a single play ahead of the seeded repeat. Resetting before authentication makes the History assertion independent of suite order.
|
Merged: the Movie detail screen, 966 lines added and 39 deleted (net +927). Product-Growth: about 360 product lines are the screen, built from show detail's hero, check control, overflow menu and related row; the rest is tests, a flow and fake Trakt fixtures. Binary-Size: JavaScript only; no native change (numbers in the footprint comment on PR #58). The screen has the opaque bar, hero with backdrop, facts, the 56 pt check with snackbar and undo, the overflow (watchlist, Open on Trakt), More like this capped at six, the refusal to unmark a rewatched film with its Open history action, and loading, error and movies-off states. The fake Trakt now serves movie backdrops and related titles for movies and shows (it served neither, which hid both from every check), parsed through the app's contracts; both detail flows assert the related row and its cap. A strict visual review in light, dark and the largest text size found the rendered UI clean; stills are on PR #58. |
iOS exposes the repeated-play count in the row label while correctly omitting the decorative badge as a separate accessibility node. The device flow now checks the user-facing count on that row.
Install the standalone update client and bind each self-built binary to a fingerprint runtime and release channel. Keep publication manual and require green CI for the exact commit so updates cannot bypass the native release safeguards.
The confirm alert's title and its button both carry the words of the danger row, and the row stays in the hierarchy under the alert, so an unanchored text tap matched the title and the confirm was never pressed. Name the confirm by the Cancel button beside it and wait for the alert before aiming. Drop the queued write rather than holding it. A held socket only refuses after five fifteen second request timeouts; a dropped one refuses in seconds, and an unreachable Trakt is what the refusal copy describes. Then let writes through, refuse the revoke, and assert the sign out still lands on onboarding: revoking is best effort and must not strand a session. The flow runs last in the iOS suite now, because it ends on onboarding and the app idle measurement before it needs a signed in session.
|
Merged: the History screen, 1130 lines added and 71 deleted (net +1059). Product-Growth: about 570 product lines are the screen (day-grouped sections with sticky opaque headers, the native title filter, paging that stops while a page load has failed, same-item same-day collapse with a count in the row's accessible label, the row menu, removal with an honest remainder and an undo that awaits the removal, the month jump); the rest is tests, a flow and fake Trakt history. Binary-Size: JavaScript only; no native change (numbers in the footprint comment on PR #60). The month jump draws inside the screen as a panel with an explicit Close, because a second system presentation on top of the account modal never registers in the iOS accessibility tree; every month is a button reachable by name. The account stack's header is opaque so day headers do not pin behind it, sharing the tab stacks' options. Four layout breaks at the largest text size were found on the simulator and fixed. Fourteen matched light and dark stills are on PR #60. |
|
Merged: deliberate JavaScript updates with EAS Update, 341 lines added and 86 deleted (net +255). Binary-Size: the expo-updates native module adds 564.1 kB to the tester APK and 1.09 MB to the Play download estimate; JavaScript bundles are effectively unchanged. Nothing publishes automatically. An update goes out only through the manually run Publish update workflow, which requires green CI for the exact commit (the release workflow and this one now share one gate script), checks that the robot account can reach the project, and publishes to the chosen channel. The runtime version follows the native fingerprint, so an update only reaches builds with the same native code. No analytics package is added; a test keeps a deny list. The Expo project is @arunkris/cue. A fresh tester build is needed once before updates can reach a phone. |
The error block centres its headline and body, but the action sets its own alignSelf, so a retry button landed hard left under centred text. Centre it on the main axis, which is the one alignSelf does not own. The lapsed order value wrapped to a second line inside a 190 pt cap while its label was already wrapping, which read as three ragged columns. The cap now clears the longest value on one line and the label still wraps to two. Account.yaml signs back in at the end instead of moving down the suite: the app-idle measurement that follows it is gated to run last and needs the returning user the other flows leave behind.
|
Merged #61 Profile and Settings as e6ecdf9. Diffstat of this landing: +1,481 / -85 (net +1,396): product +711, tests +522, the rest flows and fixtures. PR #24 is now +37,381 / -38,476 (net -1,095). Sizes from the PR's footprint: iOS JavaScript bundle +55.3 kB (5.19 to 5.25 MB), Android JavaScript bundle +55.4 kB, tester APK 39.62 to 41.23 MB (+1.61 MB), Play estimate 18.15 to 19.67 MB (+1.51 MB). The change is JavaScript plus one 2.9 kB image and adds no dependency, so the APK delta is larger than this change explains; the base it was measured against predates the update client merged in #62. The next footprint on this pull request is the number to trust and I will correct this comment against it. What landed: Profile (stats tiles, avatar, links) and Settings (theme, sort, data section, sign out) on the account stack; sign out refuses while a queued write cannot reach Trakt and completes even when token revocation fails. The iOS flow proves both. 16 matched light and dark stills: #61 (comment) Known and owed: two different avatar fallback glyphs (tab bar and Profile); the word EPISODES breaks mid-word in the Profile tiles at the largest text size; no deletions were available in this landing, stated in its Product-Growth line. Screens merged: 10 of 10 built (Onboarding's visual layer is in progress separately). |






Cue becomes a native app. iOS and Android ship as one React Native app built with Expo,
with platform navigation, gestures, haptics and notifications, over a shared TypeScript core.
The web app is retired with the shells; a PWA, if kept, is Expo's web export of the same screens. When this merges the repository reads as an
Expo project: the Capacitor shells, the web UI and their plumbing are gone,
packages/coreholds the domainand the Trakt data layer once, and
packages/nativeis the app.Status
Every user-visible change lands with its screenshots and recordings attached to its merge
comment below, and each screen ships to TestFlight and Firebase App Distribution from
release/expoas it lands, so the app is tested on a phone, not from clips.Landed, each part verified before merge (root
pnpm check, native jest on both platforms,Playwright, the fake Trakt lane twice, the native launch flow on a signed simulator build):
@cue/coreextracted with zero cache busts for shipping users; the corecannot import an app, and it typechecks and tests itself without one.
the platform adapters, a local Swift and Kotlin haptics module, device-code OAuth with
PKCE, migration of the token and the pending write queue from the previous shell.
Retry-After, an observable rate-limit pause, a
retryingstate, the mark control's threestates, one episode mark costing three requests instead of nine on the seeded account.
downward-only suppression baseline (core at zero), zero type suppressions, zero clones,
size ceilings that can only be lowered plus a per-PR size delta gate, the Play download
estimate at Play's reference density, the asset allowlist, CodeQL, and a footprint comment
recreated on every push with line, bundle, complexity and attribution deltas.
and Material's scales, snackbar hosts, the accessibility id vocabulary gated both ways.
fault endpoints, a deterministic native build with its framework linkage asserted.
view-status shape, one write-outstanding state, the write lock keyed per show.
"On the way", the History footer, the lapsed drawer, every state, light, dark and the
largest text size; media in its merge comment.
release/expo, version 2.0.0.movie detail, History, Profile, Settings, Onboarding.
Type passes; per-episode local notifications for the shows being watched.
How to read the footprint comment
The merge base has no
packages/tree, so the bundle and complexity base columns read n/a onthis PR. Merge-scoped runs (base pinned to the previous tip) are posted in the comments below
as each part lands.
The workspace split
packages/webis the Vite app.packages/coreis@cue/core: the domain, theTrakt data layer, the durable write queue, the runtime and composition root, the
auth store, the hooks, the stores, the preferences and the URL parsers, plus the
ports each app fills (key-value storage, preference storage, token storage,
haptics, reminders, connectivity, app visibility, the OAuth redirect handoff).
It is TypeScript source with no build step, reached through one wildcard subpath
(
@cue/core/domain/up-next), and it contains no.tsxand no.css.The rules are enforced rather than agreed. dependency-cruiser grew from 8 rules
to 16 over
packages, every one of them exercised against a planted violation:the core cannot import an app, the domain cannot reach the data layer, a port
cannot grow an implementation, the web app owns the DOM and the native app owns
Expo, and neither app may import the other.
pnpm check:core-portableassertsthe core's file types out of git rather than out of a config, and biome bans
eight browser and node globals inside it.
Behaviour-preserving for the web app, proven
The persisted query cache is keyed on a buster that used to be a Vite
definehashing three source trees by path, so the workspace move alone would have
dropped every shipping user's cache, and the extraction would have dropped it
again.
scripts/write-buster.mjsreplaces it with a path-independent shapewitness: it hashes the trees that define every persisted shape, in source order,
with each import statement's specifier collapsed to the digest of the module it
resolves to, so a file that moves or an import that is respelled produces the
same witness, while a field added to a type does not.
pnpm buster:checkfailsthe build while the committed witness and the computed one disagree.
The buster literal is seeded with the value main's own build already produces, so
this branch ships zero cache busts:
PERSIST_BUSTERvite build --mode testd0c3d97c58b8vite build --mode testd0c3d97c58b8Both were read out of the built
dist/assets/index-*.js, and main's value alsorecomputes from source with the old function's own logic.
buster:checkat thetip reads
shape b76d12c06a3e, buster d0c3d97c58b8: the two differ, which iswhat a mechanism change with no shape change looks like.
packages/nativeAn Expo app on expo-router, running the shared core through its own composition
root. What is in it:
expo-sqlitekey-value store,
expo-secure-storefor the token, a synchronous preferencestorage over the same database in its own
pref.namespace,expo-networkfor connectivity,
AppStatefor visibility,expo-notificationsfor thereminders planner (inexact triggers, since the exact-alarm permission is
blocked), and
expo-applicationfor the version.seven-verb haptics port with each platform's own system feedback, plus the
read side of the previous shell's stored preferences.
expo-cryptoratherthan a hand-rolled encoder, because a phone has no page to redirect.
pending write queue is read once and removed, so an upgrade in place lands
signed in with its undelivered writes intact and can never replay them twice.
the account area as a full-screen modal.
app.config.ts, which blocks 25 permissions thedependency tree would otherwise ask for and adds none of its own; the release
APK asks for 7.
ios/andandroid/are generated rather than committed, so both projects arerebuilt from
app.config.tson every build.What is deliberately not here
The screens. Everything under
packages/native/src/screensis a placeholderthat renders real data from the shared hooks with no styling: rows of text, no
artwork, no theme. They land next, on this same branch, as it is worked. The
scaffolding is complete: routing, the composition root, the ports, auth,
migration and the gates are all in place and exercised on a device, so the
screens are the remaining work rather than the risky part.
Also not here, and tracked as the next steps after the screens: the art pipeline
(the shared hook still hands
Elementto its consumers), the theme port, asnackbar host on the native side, and the Maestro launch flow.
Verification
pnpm checkat the root: biome, dprint, cspell, three tsc programs,dependency-cruiser over 507 modules and 1,741 dependencies, knip, jscpd,
buster:check,verify-bundle, and vitest with coverage: 106 files, 875tests. Then jest-expo: 8 suites, 70 tests across the ios and android projects.
packages/web: chromium 239 passed, mobile-chromium 40 passed,and the mock-mode equivalence lane 16 passed, which drives 15 scripted flows
against the local fake Trakt with no interception and fails if any of the six
write paths stops being sent.
assembleDebugplusverify-apk.sh(9.9.9 (42), 5 permissions, backup off, every storage domain excluded from both
channels), and the native app's
assembleReleaseplus the same check on theexpo line (7 permissions, same privacy assertions).
xcodebuildand run on an iPhone 17 Prosimulator against the local fake Trakt. It signed in through the device-code
grant (the request log carries
POST /oauth/device/codewith an S256challenge, then
POST /oauth/device/tokenwith the matching 43-characterverifier) and painted Up Next from the shared
useUpNexthook with the fournative tabs beneath it.
The numbers
507 files changed, 13,502 insertions, 1,946 deletions. Of those, 338 are renames
(153 byte-identical, 185 carrying an edit), 133 files are new, 23 are modified in
place and 12 are deleted: most of the diff is the move and the import rewrite,
not new code.
The web app's 24,211 product lines become 11,212 in
@cue/coreand 13,548 inpackages/web, so 45 percent of the product code now runs on both targets, at acost of about 550 lines for the ports and seams.
packages/nativeis 2,621tracked lines: 1,016 of composition root and adapters, 378 of route tree, 323 of
local module (236 of them Swift and Kotlin), 441 of tests and 223 of config
plugins.