The API guide imported react-syntax-highlighter's default entry, which is
the highlight.js build with every language compiled in. It also applied
Prism themes to it, and the two use different class names, so the samples
were largely uncoloured on top of being heavy. Switch to PrismLight and
register the six languages the guide actually renders.
GoogleOAuthProvider wrapped the whole app but only the login and register
pages use it, so the Google Identity SDK initialised on every dashboard
page. Move it inside LoginWithGoogle, which is where both consumers go
through.
Total client chunks drop from 3.1M to 2.3M.
Also fixes four e2e tests that were relying on page.goto returning a
fully loaded page. Adding loading.tsx introduced a short skeleton state
that did not exist before, and each test measured or interacted during
it. They now wait on real signals instead of timing:
- bulk send waits for the dropzone row cap, which comes from
useSubscription inside that page's own hook. The previous wait on the
devices response was wrong: the dashboard layout requests the same
query key, so it can resolve before the page hydrates.
- the mobile overflow sweep and the API guide replace networkidle, which
needs a 500ms window with zero connections that link prefetching keeps
pushing out of reach.
- the footer/tab-bar check re-scrolls as the page grows, instead of
scrolling once while the skeleton is still short.
- the overflow sweep measures the billing tab rather than
/dashboard/account, a redirect stub with no layout of its own whose
client-side redirect tore down the measurement.
A new test asserts the samples carry Prism token markup in all five
languages, so a revert to the highlight.js entry fails instead of
silently dropping the colours.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The messaging pages now stream behind a loading.tsx boundary, so their
HTML can paint before React hydrates. Tests that filled the To field or
dropped a CSV immediately after page.goto raced that window: the input
was there, the handlers were not, and the interaction was silently lost.
Under parallel workers the race lost often enough to fail runs at random.
Both specs now navigate through a helper that waits for the client-side
devices fetch, which only fires after hydration effects run, so the form
is proven interactive before the test touches it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The bulk send rebuild shipped several parsing bugs that silently
produced wrong results rather than failing loudly.
Columns were read from the first parsed row instead of the header.
papaparse only assigns keys for values actually present in a row, so a
short first row hid every column after it: the phone column could not
be selected and the file was unusable. Read results.meta.fields.
Row-level parse errors were discarded. Only fatal stream failures were
reported, so a mis-delimited file parsed into one mangled column and
looked fine. Surface them as a warning.
The plan row cap was checked once at drop time and skipped entirely
while the subscription was still loading, so an over-cap file passed on
a limited plan and nothing revalidated it. The cap is now derived, so
it applies whenever the limit resolves.
Rejected files returned silently, leaving a dropped .xlsx with no
feedback. Report the rejection.
The preview index was left pointing into the old recipient list when
the phone column changed, hiding the preview and disabling its own Next
button. It is now clamped and reset.
Also count segments from the rendered message rather than falling back
to the raw template, which counted the placeholder literals.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The old screen was one 425-line component with correctness bugs, not just a
weak layout.
Fixed defects:
- Empty recipients were sent. handleSendBulkSMS mapped every parsed row into
the payload, so a blank phone cell produced a message addressed to "".
There was no filtering, no phone validation and no dedupe, so the same
number listed twice was texted twice.
- User-facing copy printed raw bytes: "max 1048576 bytes".
- The row limit could be wrong. maxRows fell back to 50 while the
subscription query was still loading, so an unlimited-plan user could be
told their file was too big. The cap is now only enforced once the real
limit is known.
- Steps were gated with opacity-50 + pointer-events-none, which leaves
controls in the tab order and unannounced. Locked steps now use `inert`.
- Data came from raw useQuery(['devices']) + devices?.data instead of the
typed useDevices(), the same shared-cache-key shape hazard that caused the
devices?.filter crash in round 1.
The new flow: upload (with parse summary and a preview table of the first 5
rows), map (auto-detected phone column plus live valid/invalid/duplicate
counts), compose (clickable variable chips, SMS segment counter, preview that
cycles through real recipients) and review (explicit count, an itemised list
of skipped rows and why, nothing sent until confirmed).
bulk-csv.ts holds the rules that decide who receives a message, as pure
functions with 24 unit tests. Those tests caught a real bug during
development: the short "to" hint substring-matched a column named "total",
which would have silently auto-selected the wrong column. Short hints are now
exact-match only.
Sample CSV at /samples/bulk-sms-sample.csv using 555 reserved-for-fiction US
and Canada numbers, so an unedited upload cannot text a real person, with an
order-confirmation template that demonstrates multi-variable interpolation.
E2e asserts the actual request payload contains exactly the two valid
recipients from a file seeded with a blank, an invalid and a duplicate row,
and that the parsed-CSV table does not reintroduce sideways scroll at 375px.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>