Show specific error messages for invitation token failures (#9223)

*  Show specific error messages for invitation token failures

Surface distinct error messages for the three invitation-token failure
modes that the backend already distinguishes: email mismatch, expired
token, and invalid/corrupted token. Replaces the single generic
could not accept invitation message with actionable text so the
user knows what went wrong and how to recover.

Signed-off-by: jack-stormentswe <crazycoder131@gmail.com>

* 💄 Update CHANGE.md

Signed-off-by: jack-stormentswe <crazycoder131@gmail.com>

* 💄 Address review feedback on invitation-error messages

Signed-off-by: jack-stormentswe <crazycoder131@gmail.com>

---------

Signed-off-by: jack-stormentswe <crazycoder131@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
This commit is contained in:
Jack Storment 2026-04-29 11:57:59 -04:00 committed by GitHub
parent 510a015424
commit 22b85f1a92
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 47 additions and 10 deletions

View File

@ -50,6 +50,7 @@
- Preserve vector content when pasting from external tools such as Inkscape: recognise SVG sent as text/plain (with optional XML declaration and HTML comments), skip the raster preview when an SVG sibling is on the clipboard, and ignore empty SVG blobs that some tools advertise alongside the real payload, so pasted graphics arrive editable without spurious "SVG is invalid" warnings [Github #546](https://github.com/penpot/penpot/issues/546)
- Add Shift+Numpad0/1/2 as aliases to Shift+0/1/2 for zoom shortcuts [Github #2457](https://github.com/penpot/penpot/issues/2457)
- Adds a **Pixel grid color** picker in the viewport settings, next to the existing canvas color control [Github #7750](https://github.com/penpot/penpot/issues/7750)
- Show specific invitation-link error messages instead of a single generic "Invite invalid" page: distinguish expired invitations, email-mismatch (signed in with the wrong account) and corrupted/invalid tokens, each with an actionable recovery hint [Github #9220](https://github.com/penpot/penpot/issues/9220)
### :bug: Bugs fixed

View File

@ -190,6 +190,7 @@
(= member-email (:email profile)))
(ex/raise :type :validation
:code :invalid-token
:reason :email-mismatch
:hint "logged-in user does not matches the invitation"))
(when (:is-member membership)

View File

@ -68,8 +68,15 @@
(mf/defc verify-token*
[{:keys [route]}]
(let [token (get-in route [:query-params :token])
bad-token (mf/use-state false)]
(let [token (get-in route [:query-params :token])
;; Holds the specific failure reason when the token fails, or
;; nil while still loading / on success. Any non-nil keyword is
;; truthy, so this single state replaces the previous pair of
;; (bad-token? + bad-token-reason) hooks. Reasons:
;; :token-expired -> JWT past its :exp
;; :email-mismatch -> invitation email != logged-in email
;; :invalid-token -> corrupted / unknown / fallback
bad-token-reason (mf/use-state nil)]
(mf/with-effect []
(dom/set-html-title (tr "title.default"))
@ -78,7 +85,7 @@
(fn [tdata]
(handle-token tdata))
(fn [cause]
(let [{:keys [type code team-id] :as error} (ex-data cause)]
(let [{:keys [type code team-id reason] :as error} (ex-data cause)]
(cond
(= :invalid-token-already-member code)
(st/emit!
@ -91,8 +98,12 @@
(or (= :validation type)
(= :invalid-token code)
(= :token-expired (:reason error)))
(reset! bad-token true)
(= :token-expired reason))
(reset! bad-token-reason
(cond
(= :token-expired reason) :token-expired
(= :email-mismatch reason) :email-mismatch
:else :invalid-token))
(= :email-already-exists code)
(let [msg (tr "errors.email-already-exists")]
@ -109,8 +120,8 @@
(ts/schedule 100 #(st/emit! (ntf/error msg)))
(st/emit! (rt/nav :auth-login)))))))))
(if @bad-token
[:> static/invalid-token {}]
(if @bad-token-reason
[:> static/invalid-token {:reason @bad-token-reason}]
[:> loader* {:title (tr "labels.loading")
:overlay true}])))

View File

@ -67,10 +67,26 @@
[:span (tr "not-found.made-with-love")]]]))
(mf/defc invalid-token
[]
[{:keys [reason]}]
;; Map the specific failure reason to actionable copy. Falls back to
;; the generic invitation-invalid message when the reason is missing
;; or unknown so the UX never regresses for unhandled cases.
;;
;; The branches use `tr` with literal keys (instead of `(tr key-var)`)
;; so the i18n usage scanner can statically track every key.
[:> error-container* {}
[:div {:class (stl/css :main-message)} (tr "errors.invite-invalid")]
[:div {:class (stl/css :desc-message)} (tr "errors.invite-invalid.info")]])
(case reason
:email-mismatch
[:*
[:div {:class (stl/css :main-message)} (tr "errors.invite-email-mismatch")]]
:token-expired
[:*
[:div {:class (stl/css :main-message)} (tr "errors.invite-expired")]]
[:*
[:div {:class (stl/css :main-message)} (tr "errors.invite-invalid")]
[:div {:class (stl/css :desc-message)} (tr "errors.invite-invalid.info")]])])
(mf/defc login-modal*
{::mf/private true}

View File

@ -1545,6 +1545,14 @@ msgstr "Invite invalid"
msgid "errors.invite-invalid.info"
msgstr "This invite might be canceled or may be expired."
#: src/app/main/ui/static.cljs
msgid "errors.invite-expired"
msgstr "This invitation has expired. Ask the team owner to send you a new one."
#: src/app/main/ui/static.cljs
msgid "errors.invite-email-mismatch"
msgstr "This invitation is for a different email. Log out and sign in with the invited account, or ask the team owner for a new invitation."
#: src/app/main/ui/auth/login.cljs:89
msgid "errors.ldap-disabled"
msgstr "LDAP authentication is disabled."