diff --git a/CHANGES.md b/CHANGES.md index 072b424ba8..14f1699c0b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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 diff --git a/backend/src/app/rpc/commands/verify_token.clj b/backend/src/app/rpc/commands/verify_token.clj index cc270b3ded..e25be628ad 100644 --- a/backend/src/app/rpc/commands/verify_token.clj +++ b/backend/src/app/rpc/commands/verify_token.clj @@ -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) diff --git a/frontend/src/app/main/ui/auth/verify_token.cljs b/frontend/src/app/main/ui/auth/verify_token.cljs index 6ac2a1b60f..a93954ace3 100644 --- a/frontend/src/app/main/ui/auth/verify_token.cljs +++ b/frontend/src/app/main/ui/auth/verify_token.cljs @@ -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}]))) diff --git a/frontend/src/app/main/ui/static.cljs b/frontend/src/app/main/ui/static.cljs index a13d6f76d8..f426ae0874 100644 --- a/frontend/src/app/main/ui/static.cljs +++ b/frontend/src/app/main/ui/static.cljs @@ -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} diff --git a/frontend/translations/en.po b/frontend/translations/en.po index e04a501a56..f3675e7c0e 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -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."