Reviews
Reviews are product data. They are read on the server, and they are written through a small browser flow that proves the shopper bought the product.
A store can turn reviews off with config.reviewsEnabled. When it is off, do
not render the review section or call the review writes.
Read reviews
Code
rows is a page of reviews, total is the full count. Both come from a single
envelope; do not derive the total from the page length.
A failed read is { rows: [], total: 0 }. Render the empty state, not an error
page, when a product has no reviews or the read is briefly degraded.
A review has:
Code
verified: true means the review rests on a delivered order. Show it as a
verified purchase. Do not use the customer's full name as an identifier.
media is empty when the review has no photo or voice note. A media item is:
Code
url and thumbUrl are already public URLs. Do not build a media URL from an
object key.
The review flow
Writing a review has four steps:
- Find the purchase. The shopper enters the order number and the phone used for the order.
- Choose a product. The eligible order lists its products, each marked already reviewed or not.
- Attach media (optional). Photos and a voice note are uploaded before the review is submitted.
- Submit the review. The server decides whether it is published now or waits for the store to check it.
The server owns the rules: the order must be delivered, the phone must match, a product can be reviewed once, and the store's moderation decides visibility.
Use the review session
createReviewSession runs all four steps for you, with
upload progress, retries and photo shrinking, and turns every refusal into a
code. The steps below show the routes it calls, for a theme that needs its own
flow.
The routes answer with a status first. On success the body is { data } (and
{ success: true, data } for the submit); on a refusal it is
{ error, code, domainError, … } with a 4xx or 5xx status. Branch on
response.ok, then read code.
1. Prove the purchase
Code
media: null means text only. Never show a photo picker or a record button when
the grant is absent.
grant is short-lived. Send it with every upload and with the review. If it
expires, ask the shopper to prove the purchase again; do not reuse an old grant.
If the store enables Turnstile, pass the widget token in turnstileToken. The
server checks it with the rest of the request.
2. Show what can be reviewed
Code
An already-reviewed product stays visible but cannot be reviewed again. Do not silently remove it; the shopper needs to see that the review exists.
3. Upload media
Media is uploaded before submitting the review. Each upload answers with an
uploadId; the review carries all of them.
Photos
prepareReviewPhoto is browser-only and lives on its own entry point:
Code
It draws the photo in the correct orientation, limits its longest edge to 1,600 px, and re-encodes it as WebP (or JPEG). If the browser cannot do that, it returns the original file; the server still converts it and removes EXIF.
Upload the prepared file as it is:
Code
Do not send a base64 string or a data URL. The route forwards the file as a stream, and the server reads its kind from its bytes.
Voice notes
Code
The recorder records mono audio at about 32 kb/s and stops at the store's limit. The upload sends both the file and its duration:
Code
canRecordVoice() returns false on browsers without microphone recording;
hide the record button rather than showing one that cannot work.
A photo is kept by the server as WebP; that conversion is also what removes its EXIF and GPS data. Do not add a second client-side “strip metadata” step.
4. Submit
Code
The route is the same-origin one shown below; the browser never imports the
server client. visibility is always present: published when the review
shows at once, after_review when the merchant approves it first. Word the
confirmation accordingly; never promise a review is public when it is
after_review.
Do not clear the form on a failure. The shopper should be able to fix the body, rating or attachments and try again.
The review session
Steps 1 to 4 are one object in the browser. createReviewSession, from
@wocart/storefront/review-media, proves the purchase, sends each photo or
voice note the moment it is added — with its progress, retries on a dropped
connection, and a photo shrunk first — and submits. Every refusal comes back as
a code, never a thrown error:
Code
find refuses a malformed order number before asking (VALIDATION_FAILED), and
reset() forgets the order and cancels what is still sending when the shopper
changes order. Word every code in REVIEW_REFUSAL_CODES — a Record over it
fails your type check when the platform adds one — and read VALIDATION_FAILED
at submit as a review too short. REVIEW_TEXT_LIMITS gives the title and body
lengths for your fields and your hints.
The same-origin routes
The browser does not hold the store key. Bind the SDK's review routes once:
Code
Then export the handlers you need:
Code
Code
Code
And for the list:
Code
The routes add the cross-origin check, input bounds, grant header and stream handling. Do not reimplement those in a component.
Review media is browser code
@wocart/storefront/review-media touches canvas, the microphone and the DOM.
Never import it from page frontmatter, middleware, an action or an API route.
Empty and unavailable states
reviewsEnabled: false— render no review UI and call no review endpoint.rows: []— “No reviews yet”; the form may still be available.media: null— text-only review form.maxPhotos: 0— no photo picker.voiceMaxSeconds: null— no voice recorder.reviewed: true— show the product, disable the action.- A refused proof (
!response.ok, orstatus: "refused"from the session) — keep the form and word thecode.
Do not turn a wrong order number into “no reviews”. It is a refusal of the proof, not an empty review list.