diff --git a/CHANGES.md b/CHANGES.md index 9680ad9dcc..9d2e6234c5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -26,6 +26,7 @@ - Improved text layer resizing: Allow double-click on text bounding box to set auto-width/auto-height [Taiga #11577](https://tree.taiga.io/project/penpot/issue/11577) - Improve text layer auto-resize: auto-width switches to auto-height on horizontal resize, and only switches to fixed on vertical resize [Taiga #11578](https://tree.taiga.io/project/penpot/issue/11578) - Highlight first font in font selector search. Apply only on Enter or click. [Taiga #11579](https://tree.taiga.io/project/penpot/issue/11579) +- Add the ability to show login dialog on profile settings [Github #6871](https://github.com/penpot/penpot/pull/6871) ### :bug: Bugs fixed diff --git a/frontend/src/app/main/errors.cljs b/frontend/src/app/main/errors.cljs index e9d987d238..e206c79258 100644 --- a/frontend/src/app/main/errors.cljs +++ b/frontend/src/app/main/errors.cljs @@ -102,23 +102,27 @@ (print-trace! error) (print-data! error)))) -;; We receive a explicit authentication error; -;; If the uri is for workspace, dashboard or view assign the -;; exception for the 'Oops' page. Otherwise this explicitly clears -;; all profile data and redirect the user to the login page. This is -;; here and not in app.main.errors because of circular dependency. +;; We receive a explicit authentication error; If the uri is for +;; workspace, dashboard, viewer or settings, then assign the exception +;; for show the error page. Otherwise this explicitly clears all +;; profile data and redirect the user to the login page. This is here +;; and not in app.main.errors because of circular dependency. (defmethod ptk/handle-error :authentication - [e] - (let [msg (tr "errors.auth.unable-to-login") - uri (.-href glob/location) - show-oops? (or (str/includes? uri "workspace") - (str/includes? uri "dashboard") - (str/includes? uri "view"))] - (if show-oops? - (st/async-emit! (rt/assign-exception e)) + [error] + (let [message (tr "errors.auth.unable-to-login") + uri (rt/get-current-href) + + show-error? + (or (str/includes? uri "workspace") + (str/includes? uri "dashboard") + (str/includes? uri "view") + (str/includes? uri "settings"))] + + (if show-error? + (st/async-emit! (rt/assign-exception error)) (do (st/emit! (da/logout)) - (ts/schedule 500 #(st/emit! (ntf/warn msg))))))) + (ts/schedule 500 #(st/emit! (ntf/warn message))))))) ;; Error that happens on an active business model validation does not ;; passes an validation (example: profile can't leave a team). From diff --git a/frontend/src/app/main/ui/routes.cljs b/frontend/src/app/main/ui/routes.cljs index dac362aca9..2cde2b60df 100644 --- a/frontend/src/app/main/ui/routes.cljs +++ b/frontend/src/app/main/ui/routes.cljs @@ -11,6 +11,7 @@ [app.common.uuid :as uuid] [app.config :as cf] [app.main.data.team :as dtm] + [app.main.errors :as errors] [app.main.repo :as rp] [app.main.router :as rt] [app.main.store :as st] @@ -130,7 +131,10 @@ (assoc query-params :team-id (:default-team-id profile)))))) :else - (st/emit! (rt/assign-exception {:type :not-found}))))))))) + (st/emit! (rt/assign-exception {:type :not-found})))) + + (fn [cause] + (errors/on-error cause))))))) (defn init-routes [] diff --git a/frontend/src/app/main/ui/settings.cljs b/frontend/src/app/main/ui/settings.cljs index 88929d928a..36cd8da7ba 100644 --- a/frontend/src/app/main/ui/settings.cljs +++ b/frontend/src/app/main/ui/settings.cljs @@ -42,7 +42,7 @@ (mf/with-effect [profile] (when (nil? profile) - (st/emit! (rt/nav :auth-login)))) + (st/emit! (rt/assign-exception {:type :authentication})))) [:* [:> modal-container*] diff --git a/frontend/src/app/main/ui/settings/subscription.cljs b/frontend/src/app/main/ui/settings/subscription.cljs index f6a9218169..72bed491a2 100644 --- a/frontend/src/app/main/ui/settings/subscription.cljs +++ b/frontend/src/app/main/ui/settings/subscription.cljs @@ -3,6 +3,7 @@ (:require [app.common.data.macros :as dm] [app.common.schema :as sm] + [app.main.data.auth :as da] [app.main.data.event :as ev] [app.main.data.modal :as modal] [app.main.data.profile :as du] @@ -96,7 +97,7 @@ handle-accept-dialog (mf/use-fn (fn [] (st/emit! (ptk/event ::ev/event {::ev/name "open-subscription-management" - ::ev/origin "profile" + ::ev/origin "settings" :section "subscription-management-modal"})) (let [current-href (rt/get-current-href) returnUrl (js/encodeURIComponent current-href) @@ -212,43 +213,69 @@ (mf/defc subscription-page* [{:keys [profile]}] - (let [route (mf/deref refs/route) - params (:params route) - params-subscription (:subscription (:query params)) - show-trial-subscription-modal (or (= params-subscription "subscription-to-penpot-unlimited") - (= params-subscription "subscription-to-penpot-enterprise")) - show-subscription-success-modal (or (= params-subscription "subscribed-to-penpot-unlimited") - (= params-subscription "subscribed-to-penpot-enterprise")) - subscription (:subscription (:props profile)) - subscription-type (get-subscription-type subscription) - subscription-is-trial (= (:status subscription) "trialing") - teams* (mf/use-state nil) - teams (deref teams*) - locale (mf/deref i18n/locale) - penpot-member (dt/format-date-locale-short (:created-at profile) {:locale locale}) - subscription-member (dt/format-date-locale-short (:start-date subscription) {:locale locale}) - go-to-pricing-page (mf/use-fn - (fn [] - (st/emit! (ptk/event ::ev/event {::ev/name "explore-pricing-click" ::ev/origin "settings" :section "subscription"})) - (dom/open-new-window "https://penpot.app/pricing"))) - go-to-payments (mf/use-fn - (fn [] - (st/emit! (ptk/event ::ev/event {::ev/name "open-subscription-management" - ::ev/origin "profile" - :section "subscription"})) - (let [current-href (rt/get-current-href) - returnUrl (js/encodeURIComponent current-href) - href (dm/str "payments/subscriptions/show?returnUrl=" returnUrl)] - (st/emit! (rt/nav-raw :href href))))) - open-subscription-modal (mf/use-fn - (mf/deps teams) - (fn [subscription-type] - (st/emit! (ptk/event ::ev/event {::ev/name "open-subscription-modal" - ::ev/origin "settings:in-app"})) - (st/emit! - (modal/show :management-dialog - {:subscription-type subscription-type - :teams teams :subscribe-to-trial (not subscription)}))))] + (let [route (mf/deref refs/route) + authenticated? (da/is-authenticated? profile) + + teams* (mf/use-state nil) + teams (deref teams*) + + locale (mf/deref i18n/locale) + + params-subscription + (-> route :params :query :subscription) + + show-trial-subscription-modal? + (or (= params-subscription "subscription-to-penpot-unlimited") + (= params-subscription "subscription-to-penpot-enterprise")) + + show-subscription-success-modal? + (or (= params-subscription "subscribed-to-penpot-unlimited") + (= params-subscription "subscribed-to-penpot-enterprise")) + + subscription + (-> profile :props :subscription) + + subscription-type + (get-subscription-type subscription) + + subscription-is-trial? + (= (:status subscription) "trialing") + + member-since + (dt/format-date-locale-short (:created-at profile) {:locale locale}) + + subscribed-since + (dt/format-date-locale-short (:start-date subscription) {:locale locale}) + + go-to-pricing-page + (mf/use-fn + (fn [] + (st/emit! (ev/event {::ev/name "explore-pricing-click" + ::ev/origin "settings" + :section "subscription"})) + (dom/open-new-window "https://penpot.app/pricing"))) + + go-to-payments + (mf/use-fn + (fn [] + (st/emit! (ev/event {::ev/name "open-subscription-management" + ::ev/origin "settings" + :section "subscription"})) + (let [current-href (rt/get-current-href) + returnUrl (js/encodeURIComponent current-href) + href (dm/str "payments/subscriptions/show?returnUrl=" returnUrl)] + (st/emit! (rt/nav-raw :href href))))) + + open-subscription-modal + (mf/use-fn + (mf/deps teams) + (fn [subscription-type] + (st/emit! (ev/event {::ev/name "open-subscription-modal" + ::ev/origin "settings:in-app"})) + (st/emit! + (modal/show :management-dialog + {:subscription-type subscription-type + :teams teams :subscribe-to-trial (not subscription)}))))] (mf/with-effect [] (->> (rp/cmd! :get-owned-teams) @@ -258,33 +285,35 @@ (mf/with-effect [] (dom/set-html-title (tr "subscription.labels"))) - (mf/with-effect [show-trial-subscription-modal subscription] - (when show-trial-subscription-modal - (st/emit! - (ptk/event ::ev/event {::ev/name "open-subscription-modal" - ::ev/origin "settings:from-pricing-page"}) - (modal/show :management-dialog - {:subscription-type (if (= params-subscription "subscription-to-penpot-unlimited") - "unlimited" - "enterprise") - :teams teams - :subscribe-to-trial (not subscription)}) - (rt/nav :settings-subscription {} {::rt/replace true})))) + (mf/with-effect [authenticated? show-subscription-success-modal? show-trial-subscription-modal? subscription] + (when ^boolean authenticated? + (cond + ^boolean show-trial-subscription-modal? - (mf/with-effect [show-subscription-success-modal subscription] - (when show-subscription-success-modal - (st/emit! - (modal/show :subscription-success - {:subscription-name (if (= params-subscription "subscribed-to-penpot-unlimited") - (tr "subscription.settings.unlimited-trial") - (tr "subscription.settings.enterprise-trial"))}) - (du/update-profile-props {:subscription - (-> subscription - (assoc :type (if (= params-subscription "subscribed-to-penpot-unlimited") - "unlimited" - "enterprise")) - (assoc :status "trialing"))}) - (rt/nav :settings-subscription {} {::rt/replace true})))) + (st/emit! + (ptk/event ::ev/event {::ev/name "open-subscription-modal" + ::ev/origin "settings:from-pricing-page"}) + (modal/show :management-dialog + {:subscription-type (if (= params-subscription "subscription-to-penpot-unlimited") + "unlimited" + "enterprise") + :teams teams + :subscribe-to-trial (not subscription)}) + (rt/nav :settings-subscription {} {::rt/replace true})) + + ^boolean show-subscription-success-modal? + (st/emit! + (modal/show :subscription-success + {:subscription-name (if (= params-subscription "subscribed-to-penpot-unlimited") + (tr "subscription.settings.unlimited-trial") + (tr "subscription.settings.enterprise-trial"))}) + (du/update-profile-props {:subscription + (-> subscription + (assoc :type (if (= params-subscription "subscribed-to-penpot-unlimited") + "unlimited" + "enterprise")) + (assoc :status "trialing"))}) + (rt/nav :settings-subscription {} {::rt/replace true}))))) [:section {:class (stl/css :dashboard-section)} [:div {:class (stl/css :dashboard-content)} @@ -301,7 +330,7 @@ (tr "subscription.settings.professional.storage")]}] "unlimited" - (if subscription-is-trial + (if subscription-is-trial? [:> plan-card* {:card-title (tr "subscription.settings.unlimited-trial") :card-title-icon i/character-u :benefits-title (tr "subscription.settings.benefits.all-professional-benefits") @@ -325,7 +354,7 @@ :editors (-> profile :props :subscription :quantity)}]) "enterprise" - (if subscription-is-trial + (if subscription-is-trial? [:> plan-card* {:card-title (tr "subscription.settings.enterprise-trial") :card-title-icon i/character-e :benefits-title (tr "subscription.settings.benefits.all-professional-benefits") @@ -344,13 +373,16 @@ :cta-link go-to-payments}])) [:div {:class (stl/css :membership-container)} - (when subscription-member [:div {:class (stl/css :membership)} - [:span {:class (stl/css :subscription-member)} i/crown] - [:span {:class (stl/css :membership-date)} (tr "subscription.settings.support-us-since" subscription-member)]]) + (when subscribed-since + [:div {:class (stl/css :membership)} + [:span {:class (stl/css :subscription-member)} i/crown] + [:span {:class (stl/css :membership-date)} + (tr "subscription.settings.support-us-since" subscribed-since)]]) [:div {:class (stl/css :membership)} [:span {:class (stl/css :penpot-member)} i/user] - [:span {:class (stl/css :membership-date)} (tr "subscription.settings.member-since" penpot-member)]]]] + [:span {:class (stl/css :membership-date)} + (tr "subscription.settings.member-since" member-since)]]]] [:div {:class (stl/css :other-subscriptions)} [:h3 {:class (stl/css :plan-section-title)} (tr "subscription.settings.other-plans")] diff --git a/frontend/src/app/main/ui/static.cljs b/frontend/src/app/main/ui/static.cljs index 6773b12d3e..789242d046 100644 --- a/frontend/src/app/main/ui/static.cljs +++ b/frontend/src/app/main/ui/static.cljs @@ -69,9 +69,8 @@ [:div {:class (stl/css :main-message)} (tr "errors.invite-invalid")] [:div {:class (stl/css :desc-message)} (tr "errors.invite-invalid.info")]]) -(mf/defc login-dialog - {::mf/props :obj} - [{:keys [show-dialog]}] +(mf/defc login-dialog* + [] (let [current-section (mf/use-state :login) user-email (mf/use-state "") register-token (mf/use-state "") @@ -94,9 +93,7 @@ success-login (mf/use-fn - (fn [] - (reset! show-dialog false) - (st/emit! (rt/reload true)))) + #(st/emit! (rt/reload true))) success-register (mf/use-fn @@ -117,7 +114,7 @@ (reset! current-section :recovery-email-sent))) on-nav-root - (mf/use-fn #(st/emit! (rt/nav-root)))] + (mf/use-fn #(st/emit! (rt/nav :auth-login {})))] [:div {:class (stl/css :overlay)} [:div {:class (stl/css :dialog-login)} @@ -203,11 +200,9 @@ [:button {:on-click on-click} button-text]]]])) (mf/defc request-access* - [{:keys [file-id team-id is-default is-workspace]}] - (let [profile (mf/deref refs/profile) - requested* (mf/use-state {:sent false :already-requested false}) + [{:keys [file-id team-id is-default is-workspace profile]}] + (let [requested* (mf/use-state {:sent false :already-requested false}) requested (deref requested*) - show-dialog (mf/use-state true) on-close (mf/use-fn @@ -237,90 +232,47 @@ (st/emit! (dcm/create-team-access-request (with-meta params mdata))))))] - [:* - (if (some? file-id) - (if is-workspace - [:div {:class (stl/css :workspace)} - [:div {:class (stl/css :workspace-left)} - i/logo-icon - [:div - [:div {:class (stl/css :project-name)} (tr "not-found.no-permission.project-name")] - [:div {:class (stl/css :file-name)} (tr "not-found.no-permission.penpot-file")]]] - [:div {:class (stl/css :workspace-right)}]] + (cond + is-default + [:& request-dialog {:title (tr "not-found.no-permission.project") + :button-text (tr "not-found.no-permission.go-dashboard") + :on-close on-close}] - [:div {:class (stl/css :viewer)} - ;; FIXME: the viewer header was never designed to be reused - ;; from other parts of the application, and this code looks - ;; like a fast workaround reusing it as-is without a proper - ;; component adaptation for be able to use it easily it on - ;; viewer context or static error page context - [:& viewer.header/header {:project - {:name (tr "not-found.no-permission.project-name")} - :index 0 - :file {:name (tr "not-found.no-permission.penpot-file")} - :page nil - :frame nil - :permissions {:is-logged true} - :zoom 1 - :section :interactions - :shown-thumbnails false - :interactions-mode nil}]]) + (and (some? file-id) (:already-requested requested)) + [:& request-dialog {:title (tr "not-found.no-permission.already-requested.file") + :content [(tr "not-found.no-permission.already-requested.or-others.file")] + :button-text (tr "not-found.no-permission.go-dashboard") + :on-close on-close}] - [:div {:class (stl/css :dashboard)} - [:div {:class (stl/css :dashboard-sidebar)} - [:> sidebar* - {:team nil - :projects [] - :project (:default-project-id profile) - :profile profile - :section :dashboard-projects - :search-term ""}]]]) + (:already-requested requested) + [:& request-dialog {:title (tr "not-found.no-permission.already-requested.project") + :content [(tr "not-found.no-permission.already-requested.or-others.project")] + :button-text (tr "not-found.no-permission.go-dashboard") + :on-close on-close}] - (when @show-dialog - (cond - (nil? profile) - [:& login-dialog {:show-dialog show-dialog}] + (:sent requested) + [:& request-dialog {:title (tr "not-found.no-permission.done.success") + :content [(tr "not-found.no-permission.done.remember")] + :button-text (tr "not-found.no-permission.go-dashboard") + :on-close on-close}] - is-default - [:& request-dialog {:title (tr "not-found.no-permission.project") - :button-text (tr "not-found.no-permission.go-dashboard") - :on-close on-close}] + (some? file-id) + [:& request-dialog {:title (tr "not-found.no-permission.file") + :content [(tr "not-found.no-permission.you-can-ask.file") + (tr "not-found.no-permission.if-approves")] + :button-text (tr "not-found.no-permission.ask") + :on-button-click on-request-access + :cancel-text (tr "not-found.no-permission.go-dashboard") + :on-close on-close}] - (and (some? file-id) (:already-requested requested)) - [:& request-dialog {:title (tr "not-found.no-permission.already-requested.file") - :content [(tr "not-found.no-permission.already-requested.or-others.file")] - :button-text (tr "not-found.no-permission.go-dashboard") - :on-close on-close}] - - (:already-requested requested) - [:& request-dialog {:title (tr "not-found.no-permission.already-requested.project") - :content [(tr "not-found.no-permission.already-requested.or-others.project")] - :button-text (tr "not-found.no-permission.go-dashboard") - :on-close on-close}] - - (:sent requested) - [:& request-dialog {:title (tr "not-found.no-permission.done.success") - :content [(tr "not-found.no-permission.done.remember")] - :button-text (tr "not-found.no-permission.go-dashboard") - :on-close on-close}] - - (some? file-id) - [:& request-dialog {:title (tr "not-found.no-permission.file") - :content [(tr "not-found.no-permission.you-can-ask.file") - (tr "not-found.no-permission.if-approves")] - :button-text (tr "not-found.no-permission.ask") - :on-button-click on-request-access - :cancel-text (tr "not-found.no-permission.go-dashboard") - :on-close on-close}] - - (some? team-id) - [:& request-dialog {:title (tr "not-found.no-permission.project") - :content [(tr "not-found.no-permission.you-can-ask.project") - (tr "not-found.no-permission.if-approves")] - :button-text (tr "not-found.no-permission.ask") - :on-button-click on-request-access - :cancel-text (tr "not-found.no-permission.go-dashboard") - :on-close on-close}]))])) + (some? team-id) + [:& request-dialog {:title (tr "not-found.no-permission.project") + :content [(tr "not-found.no-permission.you-can-ask.project") + (tr "not-found.no-permission.if-approves")] + :button-text (tr "not-found.no-permission.ask") + :on-button-click on-request-access + :cancel-text (tr "not-found.no-permission.go-dashboard") + :on-close on-close}]))) (mf/defc not-found* [] @@ -484,29 +436,77 @@ [:> internal-error* props]))) +(mf/defc context-wrapper* + [{:keys [is-workspace is-dashboard is-viewer profile children]}] + [:* + (cond + is-workspace + [:div {:class (stl/css :workspace)} + [:div {:class (stl/css :workspace-left)} + i/logo-icon + [:div + [:div {:class (stl/css :project-name)} (tr "not-found.no-permission.project-name")] + [:div {:class (stl/css :file-name)} (tr "not-found.no-permission.penpot-file")]]] + [:div {:class (stl/css :workspace-right)}]] + + is-viewer + [:div {:class (stl/css :viewer)} + ;; FIXME: the viewer header was never designed to be reused + ;; from other parts of the application, and this code looks + ;; like a fast workaround reusing it as-is without a proper + ;; component adaptation for be able to use it easily it on + ;; viewer context or static error page context + [:& viewer.header/header {:project + {:name (tr "not-found.no-permission.project-name")} + :index 0 + :file {:name (tr "not-found.no-permission.penpot-file")} + :page nil + :frame nil + :permissions {:is-logged true} + :zoom 1 + :section :interactions + :shown-thumbnails false + :interactions-mode nil}]] + + is-dashboard + [:div {:class (stl/css :dashboard)} + [:div {:class (stl/css :dashboard-sidebar)} + [:> sidebar* + {:team nil + :projects [] + :project (:default-project-id profile) + :profile profile + :section :dashboard-projects + :search-term ""}]]]) + + children]) + (mf/defc exception-page* {::mf/props :obj} [{:keys [data route] :as props}] - (let [type (:type data) - path (:path route) + (let [type (:type data) + path (:path route) - params (:query-params route) + params (:query-params route) - workspace? (str/includes? path "workspace") - dashboard? (str/includes? path "dashboard") - view? (str/includes? path "view") + workspace? (str/includes? path "workspace") + dashboard? (str/includes? path "dashboard") + view? (str/includes? path "view") ;; We store the request access info int this state - info* (mf/use-state nil) - info (deref info*) + info* (mf/use-state nil) + info (deref info*) - loaded? (get info :loaded false) + loaded? (get info :loaded false) + profile (mf/deref refs/profile) + + auth-error? + (= type :authentication) request-access? (and - (or (= type :not-found) - (= type :authentication)) + (or (= type :not-found) auth-error?) (or workspace? dashboard? view?) (or (:file-id info) (:team-id info)))] @@ -517,11 +517,25 @@ (rx/subs! (partial reset! info*) (partial reset! info* {:loaded true}))))) - (when loaded? - (if request-access? - [:> request-access* {:file-id (:file-id info) - :team-id (:team-id info) - :is-default (:team-default info) - :is-workspace workspace?}] - [:> exception-section* props])))) + + (if auth-error? + [:> context-wrapper* + {:is-workspace workspace? + :is-dashboard dashboard? + :is-viewer view? + :profile profile} + [:> login-dialog* {}]] + + (when loaded? + (if request-access? + [:> context-wrapper* {:is-workspace workspace? + :is-dashboard dashboard? + :is-viewer view? + :profile profile} + [:> request-access* {:file-id (:file-id info) + :team-id (:team-id info) + :is-default (:team-default info) + :is-workspace workspace?}]] + + [:> exception-section* props])))))