Post-submission Thank-you Page
Overview
The thank-you page is what users see after successfully submitting a form. It also serves as the destination for one-submission-only forms when a user who has already submitted opens the form again.
Admins can choose between the system's standard copy or a fully customized version, controlling the icon, image, heading, rich-text body, destination button, and an automatic close countdown.
An important design decision concerns how unset values are interpreted: a null value, an empty object, and an object whose mode is explicitly default are all treated as "use the standard copy". As a result, every pre-existing form immediately gets a proper Thai thank-you page with a close button, without any data migration.
Business Flow
- After a successful submission, the container sends the user to
/:hash/form/:id/thank-you. - The page re-fetches the form definition to obtain the thank-you configuration and theme, and fetches the OA data to obtain
botBasicIdfor building a chat link. - This page calls
useLiffInit()rather thanuseLiffAuth(), because forms that do not require login — or that were opened in a plain browser — must never be bounced to a login screen. LIFF still has to be initialized so thatliff.isInClient()andliff.closeWindow()are available. - The thank-you configuration is resolved into concrete render values covering the icon, image, heading, body, button, and auto-close behaviour.
- Rendering follows these rules:
- When both an image and an icon are configured, the image takes precedence.
- Custom HTML body content is rendered exclusively through the Tiptap viewer.
dangerouslySetInnerHTMLis never used. - Standard body content is plain text.
- The destination button behaves according to the type the admin selected, as shown below.
- When auto-close is configured (accepting 3 to 60 seconds, and only valid when the button is not a URL button and not absent), the screen shows a countdown ring and presses the button automatically when the time elapses.
- The "you can close this window" hint is shown only when there is no button or the button opens a URL — in the other cases the button itself is already the exit.
Destination button types
| Type | Behaviour |
|---|---|
| Close window | Calls closeLiff() to close the LIFF window |
| Open OA chat | Inside the LINE app, closes the window so the user returns to the chat themselves; outside the app, opens that OA's chat/add-friend link |
| Switch rich menu | Behaves the same as close, because the rich menu switch already happened server-side when the response was saved |
| Open URL | Sends the user to the URL the admin configured |
| No button | Renders no button at all |
Key Screens & Components
Page
- The thank-you page (
src/app/[hash]/form/[id]/thank-you/page.tsx) handles data loading and LIFF initialization.
Container and components (all under src/components/form-builder/thank-you/)
- The container (
thank-you.container.tsx) holds the logic that resolves configuration into render values and assembles the full screen. - The action component (
thank-you-action.tsx) implements the behaviour of each button type, including normalizing the@prefix on the bot id before building a chat URL. - The default copy module (
default-copy.ts) holds the text used when the admin has not customized anything.
Shared helpers
useLiffInit()(src/hooks/use-liff-init.ts) initializes LIFF without forcing a login.closeLiff()(src/lib/liff-close.ts) closes the LIFF window with a fallback.- Types for the thank-you configuration and button types live in the form builder type definitions.
Unit tests cover both screen rendering and OA chat link construction under src/components/form-builder/thank-you/__tests__/.
Dependencies
- Shares the Tiptap viewer with the Article Viewer. A repository-wide guard test forbids using raw HTML to render user-authored content.
- Requires
botBasicIdfrom The hash Route & LINE OA Resolution in order to build the OA chat button. - Uses the same theme CSS variables as Form Filling for visual continuity.
- Respects the user's
prefers-reduced-motionsetting: the countdown ring only animates when animation has not been disabled.
Backend Details (Client API)
The thank-you page has no endpoint of its own — both its configuration and its real side effects happen elsewhere. This section covers what the backend actually does behind this page.
Why the "switch rich menu" button calls no API at all
The key design decision: the rich menu switch is triggered server-side when the submission is saved, not by pressing a button on the thank-you page. The security reasoning is straightforward — if the web app triggered it, anyone who knows a form's URL could POST to change their own rich menu without ever filling the form in. On the web side, that button type therefore behaves exactly like "close window", because the real work finished at submit time.
Two independent menu-switching mechanisms
The backend has two mechanisms that can both fire on a single submission:
1. The form-level "convert to member" flag
- Publishes a job to switch the user's rich menu to the member menu.
- If publishing that job fails, the whole request fails — unlike mechanism 2, which is purely best-effort. In that case the user sees an error even though the answers may already have been stored.
- It then updates the user's type to member on a best-effort basis. This update is necessary because although the queued payload carries a user-type field, no worker has ever read it (neither in the legacy system nor the current one). Without this step, the rich menu changes but the user stays a guest — a defect fixed at exactly this point.
2. A "switch rich menu" action configured on the thank-you page
- The backend reads the thank-you configuration very defensively: the mode must be custom, the action type must be rich menu, and the menu id must be a valid positive integer. If any of that is missing it is skipped silently with a warning log rather than failing the request — the column holds raw JSON that may have been written before the CMS had a validator.
- Menu ownership is re-checked before publishing — the rich menu must belong to the form's own OA, taken from the form itself, never from the request. This is the only gate on this path and it is genuinely needed, because the worker looks rich menus up without filtering by OA; without this check, one OA's form could target another OA's menu.
- Every failure here is only a warning, because the answers are already saved, the user did nothing wrong, and the menu switch is a bonus.
- This mechanism does not touch the user's type, and must not: its job is to change the menu, not membership status.
A limitation worth knowing: switch ordering is not guaranteed
If a form both sets the "convert to member" flag and configures a rich-menu action, two jobs land in the same queue and whichever is processed last wins. The ordering is not guaranteed — the CMS documents this limitation. For a deterministic result, use one mechanism or the other.
Where this page's data comes from
- The entire thank-you configuration is already returned both in the submission response and by
GET /form-builder/:hash, so the web app can render the buttons and countdown without any extra call. - The
botBasicIdused to build the OA chat link comes from the hash-resolution endpoint (see The hash Route & LINE OA Resolution). - The backend does not resolve thank-you defaults — it returns the raw stored value, which may be null or an empty object. Interpreting "empty means use the standard copy" is entirely the web app's job.
Edge cases worth knowing
- A form configured with a rich-menu action but without required LINE login silently skips the job, because there is no user to switch the menu for. (The CMS enforces the login requirement at save time; this is a runtime safety belt.)
- Reopening or refreshing the thank-you page never re-triggers a menu switch, since the page issues no API calls.
- The switch is asynchronous work on a queue — the user may see the menu change some time after the thank-you page appears, or not at all if the worker is unhealthy, with no way for the web app to know.