06 — Public Submission Page (Screen 13, `/t/:token`)
Cursor-ready plan for the customer-facing testimonial capture flow: token validation, media upload, consent, and submission APIs.
06 — Public Submission Page (Screen 13)
Section titled “06 — Public Submission Page (Screen 13)”Source: 02-Implementation-Blueprint.md — Screen 13 - Public Submission Page (/t/:token), with the minimum companion public routes from §6 (t.$token.jsx, api.testimonial-upload-url, api.testimonial-submit; optional api.testimonial-media-callback).
Product alignment: 01-Post-Purchase-Video-Testimonial-Collector-Plan.md — §6 MVP (tokenized links, mobile-friendly upload/record, video + photo + optional text + rating, consent).
This document is a build spec only. No code changes are implied until an implementation task references this file.
0) Goal (one sentence)
Section titled “0) Goal (one sentence)”A buyer opens a unique link (/t/:token) tied to one TestimonialRequest; the app validates the token, shows locked product context, lets them record or upload video/photo, optionally add text + rating, require consent, then create a TestimonialSubmission (+ TestimonialMediaAsset) in pending moderation state and mark the request as submitted.
1) Prerequisites (must exist before this screen works end-to-end)
Section titled “1) Prerequisites (must exist before this screen works end-to-end)”| Dependency | Why |
|---|---|
TestimonialRequest with unique submissionToken (blueprint §5.5) | Loader resolves :token → request row; binds shopifyProductId, shopId, optional requestId on submission. |
TestimonialSubmission + TestimonialMediaAsset (§5.7 / §5.8) | Final POST creates rows; media row tracks upload/processing. |
Shop + testimonialConsentVersion (or equivalent on §5.1) | Persist consent version string on submit. |
Optional: Product mirror or Shopify Admin API | To show product title + image on the page without extra Shopify calls from an unauthenticated browser (prefer denormalized snapshot on TestimonialRequest or join Product by shopifyProductId). |
Out of scope for this plan: creating requests from webhooks (§4), email templates (Screen 4), moderation UI (Screens 5–6). For local dev, seed a TestimonialRequest with a known token.
2) URL and routing
Section titled “2) URL and routing”| Artifact | Responsibility |
|---|---|
app/routes/t.$token.jsx | Public page route. :token = TestimonialRequest.submissionToken (opaque string; use high-entropy tokens, e.g. UUID v4 or 32+ byte hex). |
app/routes/api.testimonial-upload-url.jsx | POST (or GET if you prefer) — returns short-lived presigned upload target + storageKey / assetId the client must send on final submit. |
app/routes/api.testimonial-submit.jsx | POST JSON — validates token again, creates TestimonialSubmission + links media, updates request status. |
Do not nest under /app/* — page is customer-facing, no authenticate.admin.
React Router: ensure t.$token is excluded from embedded app layout if the boilerplate wraps /app only (follow existing _index vs app split).
3) Token validation rules (loader for t.$token.jsx)
Section titled “3) Token validation rules (loader for t.$token.jsx)”On every load:
- Look up
TestimonialRequestwheresubmissionToken === params.token(constant-time comparison if comparing strings in application code). - If not found → render a simple 404 / invalid link page (no stack traces, no hints).
- If
statusis alreadysubmitted→ show “Already submitted” (idempotent messaging; optional link to store). - If
expiredorfailed(if you use these) → friendly message; no form. - If
scheduledbut not yetsent(optional policy): either allow early access or show “Link not active yet” — pick one and document in env (recommend: allow onlysentorclickedfor MVP to match email timing). - Resolve shop by
shopId; if shopstatusnot active / app uninstalled → invalid link.
Open decision (record in ticket): blueprint does not mandate tokenExpiresAt. Recommend adding optional tokenExpiresAt on TestimonialRequest for security; loader rejects if now > expires.
Analytics: set clickedAt on first successful page load (once per token) to avoid double-counting — use a small idempotent update (e.g. only if clickedAt is null).
4) Page UI (Screen 13 field checklist)
Section titled “4) Page UI (Screen 13 field checklist)”Build a mobile-first layout (single column, large tap targets). No Shopify Polaris requirement on public page unless you reuse it; plain CSS or lightweight components is fine.
4.1 Header
Section titled “4.1 Header”- Merchant-configurable header + trust copy (source:
Shoptestimonial branding JSON, hardcoded v1 defaults, or static copy until Screen 8 ships — document source of truth).
4.2 Product context (locked)
Section titled “4.2 Product context (locked)”- Product title, thumbnail, optional variant title from request snapshot or
Productjoin. - Short note: “Your testimonial will be linked to this product.” (blueprint Screen 13 product binding note).
4.3 Media mode
Section titled “4.3 Media mode”- Record video —
MediaRecorderwhere supported; fallback message to upload if not. - Upload video — file input
acceptvideo types; max size from shop/moderation settings (Screen 9 — if not ready, use safe defaults, e.g. 100MB). - Upload photo — image
accept; same size limits.
Client flow: user picks mode → file/blob → call upload-url API → PUT to storage → receive final playbackUrl / storageKey / assetId for submit payload.
4.4 Text and rating
Section titled “4.4 Text and rating”- Headline (optional), review text (optional) — maxlength enforced client + server.
- Star rating — optional or required per
Shop/ Screen 9Require minimum rating/ global flagratingRequired(add to shop testimonial settings if missing).
4.5 Consent
Section titled “4.5 Consent”- Checkbox required; label text must mention media usage; store
consentAccepted,consentAcceptedAt,consentVersionon submission (§5.7).
4.6 Submit
Section titled “4.6 Submit”- Primary button disabled until: media uploaded (or recorded and uploaded), consent checked, rating valid if required.
- Loading and error states; no full page reload required if using
fetch.
4.7 Success
Section titled “4.7 Success”- Thank-you message; optional incentive copy from
TestimonialCampaign/ request (ifincentiveEnabled— read-only display; fulfillment of discount is a later plan).
5) API: api.testimonial-upload-url
Section titled “5) API: api.testimonial-upload-url”5.1 Purpose
Section titled “5.1 Purpose”Return credentials for direct upload to object storage (S3-compatible, Supabase, Mux direct upload, etc.) without streaming large files through the app server.
5.2 Request
Section titled “5.2 Request”- Body:
{ "token": "<same as URL token>", "mimeType": "video/mp4", "fileName": "optional", "bytes": 12345678 }
or query params if GET — POST preferred to avoid logging tokens.
5.3 Validation
Section titled “5.3 Validation”- Resolve request by
tokenwith same rules as §3 (must be eligible to submit). - Enforce max file size and allowed MIME list (
video/*,image/*subset). - Rate limit per token + IP (prevent abuse).
5.4 Response
Section titled “5.4 Response”{ "uploadUrl": "https://...", "method": "PUT", "headers": { ... }, "assetId": "uuid", "storageKey": "..." }
Shape must match whatapi.testimonial-submitexpects to attach toTestimonialMediaAsset.
5.5 Security
Section titled “5.5 Security”- Presigned URL short TTL (e.g. 15 minutes).
- Do not return shop
accessTokenor admin keys to the browser.
6) API: api.testimonial-submit
Section titled “6) API: api.testimonial-submit”6.1 Request body (example contract)
Section titled “6.1 Request body (example contract)”{ "token": "opaque-token", "mediaType": "video", "assetId": "uuid", "storageKey": "shop/.../file.mp4", "mimeType": "video/mp4", "headline": "optional", "reviewText": "optional", "rating": 5, "displayName": "optional public name", "consentAccepted": true}6.2 Server steps (transactional where possible)
Section titled “6.2 Server steps (transactional where possible)”- Re-validate token and eligibility (including no duplicate submission for same request — unique constraint or check
submittedAt). - Create
TestimonialSubmission:shopId,requestId,shopifyProductId/ variant / customer from request (customer must not override product).mediaType,status: pending,consentAccepted: true, timestamps,consentVersionfrom shop.- Optional
displayName/displayEmailif collected (minimize PII; blueprint allows display name).
- Create
TestimonialMediaAsset:submissionId,storageProvider,storageKey,mimeType,processingStatus: uploaded(orprocessingif async pipeline).playbackUrl/thumbnailUrlnullable until worker fills (§7 integration step 7).
- Update
TestimonialRequest:status = submitted,submittedAt = now(). - Return
{ "ok": true, "submissionId": "..." }.
6.3 Errors
Section titled “6.3 Errors”- 400 validation, 404 invalid token, 409 already submitted, 413 payload too large (if checking server-side), 429 rate limit.
7) Media processing (boundary of this plan)
Section titled “7) Media processing (boundary of this plan)”This plan ends at intake (uploaded / processing). Implementing transcoding, thumbnails, and processingStatus = ready belongs to a separate media-worker plan (blueprint §7 steps 6–7).
- Submission must still appear in moderation inbox with a “processing” badge if video not ready.
- Optional route
api.testimonial-media-callback: provider webhook → updateTestimonialMediaAsset— out of scope here; stub if needed.
8) Security and compliance (minimum)
Section titled “8) Security and compliance (minimum)”- HTTPS only in production.
- CORS: upload and submit APIs: allow storefront origins only if calls originate from theme; for
/t/*on app domain, CORS may be same-origin — configure explicitly. - GDPR: submission stores customer-related data; ensure customers/redact compliance path (blueprint §8) later scrubs by email/customer id — document fields to redact.
- Abuse: rate limits, max uploads per token, block oversized files.
9) Testing checklist (acceptance criteria)
Section titled “9) Testing checklist (acceptance criteria)”- Invalid token → generic error page.
- Valid token → product context correct; cannot change product client-side (tamper POST
shopifyProductId— server ignores body product id and uses request). - Upload URL returns presigned fields; client can complete PUT.
- Submit creates
TestimonialSubmission+TestimonialMediaAsset; requeststatus=submitted. - Second submit for same token → 409 or “already submitted” UX.
- Consent unchecked → 400.
- Mobile: record and upload paths smoke-tested on iOS Safari + Chrome Android.
10) Implementation order (for Cursor)
Section titled “10) Implementation order (for Cursor)”- Prisma models §5.5–5.8 (if not already migrated) + any
tokenExpiresAt/ product snapshot columns you add. t.$token.jsxloader + static layout + error states.api.testimonial-upload-url+ storage provider wiring (env vars).- Client-side upload/record integration on the page.
api.testimonial-submit+ DB transaction + request status update.- Success page + basic logging for debugging (no PII in logs).
11) References
Section titled “11) References”02-Implementation-Blueprint.md— Screen 13, §5.5–5.8, §6 public routes, §7 steps 5–6, §8 security.01-Post-Purchase-Video-Testimonial-Collector-Plan.md— §6 MVP Collection & Requests + Media Processing (intake only here).
12) Note on numbering
Section titled “12) Note on numbering”05-storefront-widgets-and-read-api.md already covers Screen 8 / 8.1. This file is 06-… so plans stay ordered.