Skip to content

Test licence code, a working local dev loop, and an RSVP funnel that works end to end - #2

Merged
Fre0Grella merged 4 commits into
mainfrom
claude/festive-babbage-7mm4d3
Sep 28, 2026
Merged

Fre0Grella merged 4 commits into
mainfrom
claude/festive-babbage-7mm4d3

Conversation

@Fre0Grella

Copy link
Copy Markdown
Owner

Summary

There was no way to test the paid features, and when I tested them anyway the RSVP funnel was broken at its first step. This PR fixes both.

1. Testing the paid features (fc38763)

Test licence BC-TEST-TEST-TEST

  • Grants pro on the local and preview Workers. The code comes from a TEST_LICENCE_CODE var.
  • Any number of accounts can redeem it, and it never writes a licence_keys row.
  • It is refused whenever ENVIRONMENT is production, even if the var is set there, because the code is in a public repo.

Local dev now reaches the Worker

  • astro dev runs no Pages Functions, so every relative /api/* call returned 404. The app silently stayed on the anonymous free tier.
  • astro.config.mjs now proxies /api, /invite and /auth/{dev,google,logout} to :8787 (override with BACKEND_DEV_URL).
  • It also rewrites /i/* and /join/* onto their pages, mirroring functions/.
  • /auth/callback is left alone, since it is a page.

Sign in from the browser without Google

  • /api/session now reports devSignIn wherever POST /auth/dev is open. Both sides use one shared predicate.
  • When it's set, the header shows an email sign-in instead of the Google link, which 500s without a client.
  • This also lets self-hosters without a Google client sign in from the browser.

local now runs SELF_HOSTED=false

  • The paywall is real locally, so the upgrade can be tested.
  • Setting SELF_HOSTED="true" in .dev.vars brings back the self-hosted behaviour.

2. RSVP funnel fixes (ec32e34)

I found these by driving the whole flow in Chromium: host publishes, guests open, answer and forward, host watches.

Bug Effect Fix
documentOf() called structuredClone on Vue reactive proxies Publishing always threw, so no invite link was ever created. Co-organiser sync goes through the same function. Read through toRaw
addInvite never gave the local row the server's id A hand-typed guest on a published party appeared twice, permanently New adoptRemoteGuest stamps the id, or drops the local row if a poll already brought the server's copy in
mergeFunnel carried reactive rows into the new list persist() failed with DataCloneError, so the party stopped saving Copy the rows
Share sheet channels were placeholders WhatsApp, Email, Messages, Instagram and QR only set a "sent" flag wa.me / mailto: / sms: links with the invite text; system share sheet (or copy) for Instagram; downloadable QR
https:// hard-coded when copying links Copied links broke on http://localhost Use the page's own origin, for the host's link and the guest's forward link
Guest who opened the link but never answered Blank row, empty avatar, and an Accept button that would issue a ticket to nobody Row reads "Opened the link", avatar shows ?, only Decline is offered
Two margin-left: autos in the funnel header "Live" badge floated mid-header Badge now sits beside Send invite

The READMEs (root and backend/) describe the new local flow and the test licence.

Testing

  • npm run lint, npm run typecheck, npm run build and npm run build:docs all pass. The docs build contains no app/auth/i/join routes.
  • npm test: 49 frontend tests and 113 backend tests pass. New tests cover:
    • the test licence rules (accepted, reusable across accounts, case-insensitive, refused on production, absent when the var is unset)
    • the devSignIn flag
    • documentOf given a reactive party
    • mergeFunnel output surviving structuredClone
    • adoptRemoteGuest, including the race where a poll lands first
  • End to end in Chromium, against wrangler dev with a local D1:
    • sign in → redeem the test code → publish
    • five guests, including a forward chain and one who opens without answering
    • funnel shows 5 reached / 3 confirmed / 1 maybe / 1 declined, with the friend-of-friend at depth 1
    • turning forwarding off reaches the server
    • host Reset and Accept survive the 20 s poll
    • a hand-typed guest appears once across polls and reloads
    • the sign-in popover fits at 390 px

Notes

  • Both commits were made with --no-verify. The husky hook's npx lint-staged fails in the cloud sandbox (undefined@lint-staged), so I ran npx lint-staged and npm run lint by hand first.
  • Preview only accepts the test code after the Worker is redeployed with this wrangler.jsonc.
  • Out of scope, noted for follow-up: modals don't close on Escape, and the Worker's CORS policy reflects any origin.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TivKBtoj8GvfK4yeS44Vnr


Generated by Claude Code

Every paid feature sits behind a redeemed licence, and there was no way to
test any of them:

- `astro dev` ran no Pages Functions, so every relative /api/* call 404ed,
  the session resolved to anonymous and the app stayed on the free tier
  with the Worker running beside it. /i/<slug> and /join/<token> 404ed too.
- The local Worker had SELF_HOSTED=true, so a signed-in local user was
  already pro and the upgrade path could not be exercised.
- Sign in linked to /auth/google, which 500s without a Google client, so
  a browser could not sign in locally at all (curl's cookie is not the
  browser's).
- Minted codes are single-use, so each test needed a fresh one.

Now:

- astro.config.mjs proxies /api, /invite and the three /auth routes to
  the Worker (BACKEND_DEV_URL, default :8787) and rewrites /i/* and
  /join/* onto their pages, mirroring functions/. /auth/callback is left
  alone for the same reason functions/auth/ has no catchall.
- BC-TEST-TEST-TEST, from TEST_LICENCE_CODE on the local and preview
  environments, grants pro without a licence_keys row and can be redeemed
  by any number of accounts. It is refused whenever ENVIRONMENT is
  production, since the code is in a public repository.
- local now runs SELF_HOSTED=false so the paywall is real; .dev.vars can
  set it back to "true" to see the self-hosted behaviour.
- The session reports devSignIn wherever POST /auth/dev is open (local
  or self-hosted; one shared predicate), and the header then offers an
  email sign-in instead of the Google link. Self-hosters without a
  Google client can now sign in from the browser too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TivKBtoj8GvfK4yeS44Vnr
Driving the funnel in a browser — host publishes, guests open, answer and
forward, host watches — found it broken at the first step and in several
places after.

- Publishing always threw. documentOf() structuredClone'd each field of
  the store's reactive party, and every object reached through a Vue
  proxy is a proxy, which structuredClone refuses. No invite link was
  ever created; co-organiser sync went through the same function. It now
  reads through toRaw.
- Hand-typed guests on a published party appeared twice, permanently.
  addInvite posted the guest but never gave the local row the server's
  id, so mergeFunnel kept it as hand-typed and added the server's copy
  beside it. adoptRemoteGuest stamps the id (or drops the local row if a
  poll already brought the copy in).
- The party then stopped saving: mergeFunnel carried reactive rows into
  the new list, putting proxies inside the raw party, and persist()
  failed with DataCloneError. It copies them now.
- Share sheet: WhatsApp, Email, Messages, Instagram and QR code only set
  a "sent" flag. They now open wa.me, mailto: and sms: with the invite
  text, use the system share sheet (or copy) for Instagram, and render a
  downloadable QR. Copied links used a hard-coded https://, which broke
  on http://localhost; both the host's and a guest's forward link now
  use the page's own origin.
- Guests tab: someone who opened the link without answering was a blank
  row with an empty avatar and an Accept button that would issue a
  ticket to nobody. They read "Opened the link", show "?", and offer
  Decline only. The Live badge sits beside Send invite rather than
  floating mid-header.

Verified in Chromium against wrangler dev + local D1: 5 reached,
3 confirmed, 1 maybe, 1 declined; a friend-of-friend at depth 1;
disabling forwarding reaches the server; host Reset/Accept survive the
20s poll; a hand-typed guest appears once across polls and reloads.
Unit tests cover the three data bugs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TivKBtoj8GvfK4yeS44Vnr
The Pages project could not get bottlecount.pages.dev — the name was taken —
and Cloudflare assigned bottlecount-epj.pages.dev. Every place that names the
app's origin is updated: the Worker's FRONTEND_URL (production and preview;
Google's redirect URI is built from it), the app build's SITE, the docs
build's PUBLIC_APP_ORIGIN, the canonical-link check in CI, and the README.

Also fills in the production D1 database_id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TivKBtoj8GvfK4yeS44Vnr
Not a secret — it is sent to the browser in the OAuth redirect. The client
secret and JWT secret are Worker secrets set with `wrangler secret put`.
The client's authorised redirect URI is
https://bottlecount-epj.pages.dev/auth/google.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TivKBtoj8GvfK4yeS44Vnr
@Fre0Grella
Fre0Grella merged commit e032998 into main Sep 28, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants