diff --git a/backend/src/app/rpc/commands/teams.clj b/backend/src/app/rpc/commands/teams.clj index b3454cb90d..bb23c6997c 100644 --- a/backend/src/app/rpc/commands/teams.clj +++ b/backend/src/app/rpc/commands/teams.clj @@ -140,7 +140,7 @@ WHEN 'professional' THEN 'active' ELSE COALESCE(p.props->'~:subscription'->>'~:status', 'incomplete') END, - '~:seats', p.props->'~:quantity' + '~:seats', p.props->'~:subscription'->'~:quantity' ) AS subscription FROM team_profile_rel AS tp JOIN team AS t ON (t.id = tp.team_id) diff --git a/frontend/playwright/data/subscription/get-profile-enterprise-canceled-subscription.json b/frontend/playwright/data/subscription/get-profile-enterprise-canceled-subscription.json new file mode 100644 index 0000000000..4273ee2012 --- /dev/null +++ b/frontend/playwright/data/subscription/get-profile-enterprise-canceled-subscription.json @@ -0,0 +1,25 @@ +{ + "~:email": "foo@example.com", + "~:is-demo": false, + "~:auth-backend": "penpot", + "~:fullname": "Princesa Leia", + "~:modified-at": "~m1713533116365", + "~:is-active": true, + "~:default-project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b", + "~:id": "~uc7ce0794-0992-8105-8004-38e630f29a9b", + "~:is-muted": false, + "~:default-team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d", + "~:created-at": "~m1713533116365", + "~:is-blocked": false, + "~:props": { + "~:subscription": { + "~:quantity": 2, + "~:status": "canceled", + "~:type": "enterprise", + "~start-date": "~m1746444667" + }, + "~:v2-info-shown": true, + "~:viewed-tutorial?": false, + "~:viewed-walkthrough?": false + } +} diff --git a/frontend/playwright/data/subscription/get-profile-unlimited-unpaid-subscription.json b/frontend/playwright/data/subscription/get-profile-unlimited-unpaid-subscription.json new file mode 100644 index 0000000000..34178320d9 --- /dev/null +++ b/frontend/playwright/data/subscription/get-profile-unlimited-unpaid-subscription.json @@ -0,0 +1,25 @@ +{ + "~:email": "foo@example.com", + "~:is-demo": false, + "~:auth-backend": "penpot", + "~:fullname": "Princesa Leia", + "~:modified-at": "~m1713533116365", + "~:is-active": true, + "~:default-project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b", + "~:id": "~uc7ce0794-0992-8105-8004-38e630f29a9b", + "~:is-muted": false, + "~:default-team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d", + "~:created-at": "~m1713533116365", + "~:is-blocked": false, + "~:props": { + "~:subscription": { + "~:quantity": 2, + "~:status": "unpaid", + "~:type": "unlimited", + "~start-date": "~m1746444667" + }, + "~:v2-info-shown": true, + "~:viewed-tutorial?": false, + "~:viewed-walkthrough?": false + } +} diff --git a/frontend/playwright/data/subscription/get-teams-enterprise-one-team.json b/frontend/playwright/data/subscription/get-teams-enterprise-one-team.json index 84e860bf51..e920218115 100644 --- a/frontend/playwright/data/subscription/get-teams-enterprise-one-team.json +++ b/frontend/playwright/data/subscription/get-teams-enterprise-one-team.json @@ -17,7 +17,8 @@ }, "~:subscription": { "~:type": "enterprise", - "~:status": "trialing" + "~:status": "trialing", + "~:seats": null }, "~:name": "Default", "~:modified-at": "~m1713533116375", diff --git a/frontend/playwright/data/subscription/get-teams-professional-subscription-owner.json b/frontend/playwright/data/subscription/get-teams-professional-subscription-owner.json index 450a06b2f6..ff228ab2f3 100644 --- a/frontend/playwright/data/subscription/get-teams-professional-subscription-owner.json +++ b/frontend/playwright/data/subscription/get-teams-professional-subscription-owner.json @@ -41,7 +41,8 @@ }, "~:subscription": { "~:type": "professional", - "~:status": "active" + "~:status": "active", + "~:seats": null }, "~:name": "Second team", "~:modified-at": "~m1701164272671", diff --git a/frontend/playwright/data/subscription/get-teams-unlimited-one-team.json b/frontend/playwright/data/subscription/get-teams-unlimited-one-team.json index 3f5d3ca43a..a43392b9ee 100644 --- a/frontend/playwright/data/subscription/get-teams-unlimited-one-team.json +++ b/frontend/playwright/data/subscription/get-teams-unlimited-one-team.json @@ -17,7 +17,8 @@ }, "~:subscription": { "~:type": "unlimited", - "~:status": "trialing" + "~:status": "trialing", + "~:seats": 2 }, "~:name": "Default", "~:modified-at": "~m1713533116375", diff --git a/frontend/playwright/data/subscription/get-teams-unlimited-subscription-member.json b/frontend/playwright/data/subscription/get-teams-unlimited-subscription-member.json index 3f05a8bf01..59dc7071b8 100644 --- a/frontend/playwright/data/subscription/get-teams-unlimited-subscription-member.json +++ b/frontend/playwright/data/subscription/get-teams-unlimited-subscription-member.json @@ -41,7 +41,8 @@ }, "~:subscription": { "~:type": "unlimited", - "~:status": "trialing" + "~:status": "trialing", + "~:seats": 2 }, "~:name": "Second team", "~:modified-at": "~m1701164272671", diff --git a/frontend/playwright/data/subscription/get-teams-unlimited-subscription-owner.json b/frontend/playwright/data/subscription/get-teams-unlimited-subscription-owner.json index aab433c348..b211cd6ddc 100644 --- a/frontend/playwright/data/subscription/get-teams-unlimited-subscription-owner.json +++ b/frontend/playwright/data/subscription/get-teams-unlimited-subscription-owner.json @@ -41,7 +41,8 @@ }, "~:subscription": { "~:type": "unlimited", - "~:status": "trialing" + "~:status": "trialing", + "~:seats": 2 }, "~:name": "Second team", "~:modified-at": "~m1701164272671", diff --git a/frontend/playwright/ui/specs/subscriptions-dashboard.spec.js b/frontend/playwright/ui/specs/subscriptions-dashboard.spec.js index b14abffd48..62cb1a718e 100644 --- a/frontend/playwright/ui/specs/subscriptions-dashboard.spec.js +++ b/frontend/playwright/ui/specs/subscriptions-dashboard.spec.js @@ -79,6 +79,74 @@ test.describe("Subscriptions: dashboard", () => { "Unlimited plan (trial)", ); }); + + test("When the subscription status is unpaid, the sidebar dropdown displays the name Professional for the Unlimited subscription", async ({ + page, + }) => { + await DashboardPage.mockRPC( + page, + "get-profile", + "subscription/get-profile-unlimited-unpaid-subscription.json", + ); + + await DashboardPage.mockRPC( + page, + "get-team-info", + "subscription/get-team-info-subscriptions.json", + ); + + const dashboardPage = new DashboardPage(page); + await dashboardPage.setupDashboardFull(); + await DashboardPage.mockRPC( + page, + "get-teams", + "subscription/get-teams-unlimited-subscription-owner.json", + ); + + await dashboardPage.mockRPC( + "push-audit-events", + "workspace/audit-event-empty.json", + ); + await dashboardPage.goToDashboard(); + + await expect(page.getByTestId("subscription-name")).toHaveText( + "Professional plan", + ); + }); + + test("When the subscription status is canceled, the sidebar dropdown displays the name Professional for the Enterprise subscription", async ({ + page, + }) => { + await DashboardPage.mockRPC( + page, + "get-profile", + "subscription/get-profile-enterprise-canceled-subscription.json", + ); + + await DashboardPage.mockRPC( + page, + "get-team-info", + "subscription/get-team-info-subscriptions.json", + ); + + const dashboardPage = new DashboardPage(page); + await dashboardPage.setupDashboardFull(); + await DashboardPage.mockRPC( + page, + "get-teams", + "subscription/get-teams-unlimited-subscription-owner.json", + ); + + await dashboardPage.mockRPC( + "push-audit-events", + "workspace/audit-event-empty.json", + ); + await dashboardPage.goToDashboard(); + + await expect(page.getByTestId("subscription-name")).toHaveText( + "Professional plan", + ); + }); }); test.describe("Subscriptions: team members and invitations", () => { diff --git a/frontend/src/app/main/ui/components/forms.scss b/frontend/src/app/main/ui/components/forms.scss index 7d09baa4e8..61960d2609 100644 --- a/frontend/src/app/main/ui/components/forms.scss +++ b/frontend/src/app/main/ui/components/forms.scss @@ -8,6 +8,7 @@ // INPUT .input-wrapper { + --input-icon-padding: var(--sp-l); display: flex; flex-direction: column; align-items: center; @@ -108,7 +109,7 @@ align-items: center; justify-content: center; position: absolute; - right: $s-16; + right: var(--input-icon-padding); top: calc(50% - $s-8); svg { width: $s-12; diff --git a/frontend/src/app/main/ui/dashboard.cljs b/frontend/src/app/main/ui/dashboard.cljs index 7ab47029fb..05c523fa29 100644 --- a/frontend/src/app/main/ui/dashboard.cljs +++ b/frontend/src/app/main/ui/dashboard.cljs @@ -132,7 +132,7 @@ [:> team-members-page* {:team team :profile profile}] :dashboard-invitations - [:> team-invitations-page* {:team team :profile profile}] + [:> team-invitations-page* {:team team}] :dashboard-webhooks [:> webhooks-page* {:team team}] diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs index 799c47c2c4..4c6a3c6191 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs @@ -27,7 +27,7 @@ [app.main.ui.dashboard.comments :refer [comments-icon* comments-section]] [app.main.ui.dashboard.inline-edition :refer [inline-edition]] [app.main.ui.dashboard.project-menu :refer [project-menu*]] - [app.main.ui.dashboard.subscription :refer [subscription-sidebar* menu-team-icon*]] + [app.main.ui.dashboard.subscription :refer [subscription-sidebar* menu-team-icon* get-subscription-type]] [app.main.ui.dashboard.team-form] [app.main.ui.icons :as i :refer [icon-xref]] [app.util.dom :as dom] @@ -333,10 +333,10 @@ :alt (:name team-item)}] (if (and (contains? cf/flags :subscriptions) - (or (= "unlimited" (:type (:subscription team-item))) (= "enterprise" (:type (:subscription team-item))))) + (#{"unlimited" "enterprise"} (get-subscription-type (:subscription team-item)))) [:div {:class (stl/css :team-text-with-icon)} [:span {:class (stl/css :team-text) :title (:name team-item)} (:name team-item)] - [:> menu-team-icon* {:subscription-name (:type (:subscription team-item))}]] + [:> menu-team-icon* {:subscription-type (get-subscription-type (:subscription team-item))}]] [:span {:class (stl/css :team-text) :title (:name team-item)} (:name team-item)]) (when (= (:id team-item) (:id team)) @@ -654,7 +654,7 @@ (fn [] (reset! show-teams-ddwn? false)) subscription (:subscription team) - subscription-name (:type subscription)] + subscription-type (get-subscription-type subscription)] [:div {:class (stl/css :sidebar-team-switch)} [:div {:class (stl/css :switch-content)} @@ -669,18 +669,18 @@ (and (contains? cf/flags :subscriptions) (not (:is-default team)) - (or (= "unlimited" subscription-name) (= "enterprise" subscription-name))) + (or (= "unlimited" subscription-type) (= "enterprise" subscription-type))) [:div {:class (stl/css :team-name)} [:img {:src (cf/resolve-team-photo-url team) :class (stl/css :team-picture) :alt (:name team)}] [:div {:class (stl/css :team-text-with-icon)} [:span {:class (stl/css :team-text) :title (:name team)} (:name team)] - [:> menu-team-icon* {:subscription-name subscription-name}]]] + [:> menu-team-icon* {:subscription-type subscription-type}]]] (and (not (:is-default team)) - (or (not= "unlimited" subscription-name) (not= "enterprise" subscription-name))) + (or (not= "unlimited" subscription-type) (not= "enterprise" subscription-type))) [:div {:class (stl/css :team-name)} [:img {:src (cf/resolve-team-photo-url team) :class (stl/css :team-picture) diff --git a/frontend/src/app/main/ui/dashboard/subscription.cljs b/frontend/src/app/main/ui/dashboard/subscription.cljs index 69f8402b04..6f784cda28 100644 --- a/frontend/src/app/main/ui/dashboard/subscription.cljs +++ b/frontend/src/app/main/ui/dashboard/subscription.cljs @@ -18,6 +18,12 @@ [potok.v2.core :as ptk] [rumext.v2 :as mf])) +(defn get-subscription-type + [{:keys [type status] :as subscription}] + (if (and subscription (not (contains? #{"unpaid" "canceled"} status))) + type + "professional")) + (mf/defc cta-power-up* [{:keys [top-title top-description bottom-description has-dropdown]}] (let [show-data* (mf/use-state false) @@ -46,13 +52,11 @@ (mf/defc subscription-sidebar* [{:keys [profile]}] (let [subscription (:subscription (:props profile)) - subscription-name (if subscription - (:type subscription) - "professional") + subscription-type (get-subscription-type subscription) subscription-is-trial (= (:status subscription) "trialing") subscription-href (dm/str (u/join cf/public-uri "#/settings/subscriptions"))] - (case subscription-name + (case subscription-type "professional" [:> cta-power-up* {:top-title (tr "subscription.dashboard.power-up.your-subscription") @@ -88,7 +92,7 @@ (mf/defc team* [{:keys [is-owner team]}] (let [subscription (:subscription team) - subscription-name (:type subscription) + subscription-type (get-subscription-type subscription) subscription-is-trial (= "trialing" (:status subscription)) go-to-manage-subscription @@ -106,23 +110,23 @@ [:div {:class (stl/css :team-label)} (tr "subscription.dashboard.team-plan")] [:span {:class (stl/css :team-text)} - (case subscription-name + (case subscription-type "professional" (tr "subscription.settings.professional") "unlimited" (if subscription-is-trial (tr "subscription.settings.unlimited-trial") (tr "subscription.settings.unlimited")) "enterprise" (tr "subscription.settings.enterprise"))] - (when (and is-owner (not= subscription-name "professional")) + (when (and is-owner (not= subscription-type "professional")) [:button {:class (stl/css :manage-subscription-link) :on-click go-to-manage-subscription :data-testid "manage-subscription-link"} (tr "subscription.settings.manage-your-subscription")])])) (mf/defc menu-team-icon* - [{:keys [subscription-name]}] + [{:keys [subscription-type]}] [:span {:class (stl/css :subscription-icon) :data-testid "subscription-icon"} - (case subscription-name + (case subscription-type "unlimited" i/character-u "enterprise" i/character-e)]) @@ -139,15 +143,15 @@ [:span {:class (stl/css :item-name)} (tr "subscription.workspace.header.menu.option.power-up")]])) (mf/defc members-cta* - [{:keys [banner-is-expanded team profile]}] + [{:keys [banner-is-expanded team]}] (let [subscription (:subscription team) - subscription-name (:type subscription) - is-owner (:is-owner (:permissions team)) + subscription-type (get-subscription-type subscription) + is-owner (-> team :permissions :is-owner) email-owner (:email (some #(when (:is-owner %) %) (:members team))) mail-to-owner (str "" email-owner "") go-to-subscription (dm/str (u/join cf/public-uri "#/settings/subscriptions")) - seats (-> profile :props :subscription :quantity) + seats (or (:seats subscription) 0) editors (count (filterv :can-edit (:members team))) link @@ -157,24 +161,24 @@ cta-title (cond - (and (= "professional" subscription-name) (>= editors 8)) + (and (= "professional" subscription-type) (>= editors 8)) (tr "subscription.dashboard.cta.professional-plan-designed") - (and (= "unlimited" subscription-name) (< seats editors)) + (and (= "unlimited" subscription-type) (< seats editors)) (tr "subscription.dashboard.cta.unlimited-many-editors" seats editors)) cta-message (cond - (and (= "professional" subscription-name) (>= editors 8) is-owner) + (and (= "professional" subscription-type) (>= editors 8) is-owner) (tr "subscription.dashboard.cta.upgrade-to-unlimited-enterprise-owner" link) - (and (= "professional" subscription-name) (>= editors 8) (not is-owner)) + (and (= "professional" subscription-type) (>= editors 8) (not is-owner)) (tr "subscription.dashboard.cta.upgrade-to-unlimited-enterprise-member" link) - (and (= "unlimited" subscription-name) (< seats editors) is-owner) + (and (= "unlimited" subscription-type) (< seats editors) is-owner) (tr "subscription.dashboard.cta.upgrade-more-seats-owner" link) - (and (= "unlimited" subscription-name) (< seats editors) (not is-owner)) + (and (= "unlimited" subscription-type) (< seats editors) (not is-owner)) (tr "subscription.dashboard.cta.upgrade-more-seats-member" link))] [:> cta* {:class (stl/css-case ::members-cta-full-width banner-is-expanded :members-cta (not banner-is-expanded)) :title cta-title} @@ -184,25 +188,26 @@ :content cta-message}]])) (defn show-subscription-members-main-banner? - [team profile] - (let [seats (-> profile :props :subscription :quantity) - editors (count (filter :can-edit (:members team)))] + [team] + (let [subscription-type (get-subscription-type (:subscription team)) + seats (-> team :subscription :seats) + editors (count (filter :can-edit (:members team)))] (or - (and (= (:type (:subscription team)) "professional") + (and (= subscription-type "professional") (> editors 8)) (and - (= (:type (:subscription team)) "unlimited") + (= subscription-type "unlimited") (>= editors 8) (< seats editors))))) (defn show-subscription-members-small-banner? - [team profile] - (let [seats (-> profile :props :subscription :quantity) + [team] + (let [subscription-type (get-subscription-type (:subscription team)) + seats (-> team :subscription :seats) editors (count (filterv :can-edit (:members team)))] (or - (and (= (:type (:subscription team)) "professional") + (and (= subscription-type "professional") (= editors 8)) - (and (= (:type (:subscription team)) "unlimited") + (and (= subscription-type "unlimited") (< editors 8) (< seats editors))))) - diff --git a/frontend/src/app/main/ui/dashboard/team.cljs b/frontend/src/app/main/ui/dashboard/team.cljs index b6f0b50ae4..e4594c7bad 100644 --- a/frontend/src/app/main/ui/dashboard/team.cljs +++ b/frontend/src/app/main/ui/dashboard/team.cljs @@ -544,18 +544,18 @@ [:section {:class (stl/css-case :dashboard-container true :dashboard-team-members true - :dashboard-top-cta (show-subscription-members-main-banner? team profile))} + :dashboard-top-cta (show-subscription-members-main-banner? team))} (when (and (contains? cfg/flags :subscriptions) - (show-subscription-members-main-banner? team profile)) - [:> members-cta* {:banner-is-expanded true :team team :profile profile}]) + (show-subscription-members-main-banner? team)) + [:> members-cta* {:banner-is-expanded true :team team}]) [:> team-members* {:profile profile :team team}] (when (and (contains? cfg/flags :subscriptions) - (show-subscription-members-small-banner? team profile)) - [:> members-cta* {:banner-is-expanded false :team team :profile profile}])]]) + (show-subscription-members-small-banner? team)) + [:> members-cta* {:banner-is-expanded false :team team}])]]) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; INVITATIONS SECTION @@ -803,7 +803,7 @@ (mf/defc team-invitations-page* {::mf/props :obj} - [{:keys [team profile]}] + [{:keys [team]}] (mf/with-effect [team] (dom/set-html-title @@ -820,14 +820,14 @@ :team team}] [:section {:class (stl/css-case :dashboard-team-invitations true - :dashboard-top-cta (show-subscription-members-main-banner? team profile))} + :dashboard-top-cta (show-subscription-members-main-banner? team))} (when (and (contains? cfg/flags :subscriptions) - (show-subscription-members-main-banner? team profile)) - [:> members-cta* {:banner-is-expanded true :team team :profile profile}]) + (show-subscription-members-main-banner? team)) + [:> members-cta* {:banner-is-expanded true :team team}]) [:> invitation-section* {:team team}] (when (and (contains? cfg/flags :subscriptions) - (show-subscription-members-small-banner? team profile)) - [:> members-cta* {:banner-is-expanded false :team team :profile profile}])]]) + (show-subscription-members-small-banner? team)) + [:> members-cta* {:banner-is-expanded false :team team}])]]) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; WEBHOOKS SECTION diff --git a/frontend/src/app/main/ui/settings/subscription.cljs b/frontend/src/app/main/ui/settings/subscription.cljs index 21ac9383d6..f6a9218169 100644 --- a/frontend/src/app/main/ui/settings/subscription.cljs +++ b/frontend/src/app/main/ui/settings/subscription.cljs @@ -2,7 +2,7 @@ (:require-macros [app.main.style :as stl]) (:require [app.common.data.macros :as dm] - [app.common.math :as mth] + [app.common.schema :as sm] [app.main.data.event :as ev] [app.main.data.modal :as modal] [app.main.data.profile :as du] @@ -10,6 +10,8 @@ [app.main.repo :as rp] [app.main.router :as rt] [app.main.store :as st] + [app.main.ui.components.forms :as fm] + [app.main.ui.dashboard.subscription :refer [get-subscription-type]] [app.main.ui.icons :as i] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] @@ -52,42 +54,45 @@ :on-click cta-link} cta-text]) (when (and cta-link-trial cta-text-trial) [:button {:class (stl/css :cta-button :bottom-link) :on-click cta-link-trial} cta-text-trial])]) +(def ^:private schema:seats-form + [:map {:title "SeatsForm"} + [:min-members [::sm/number {:min 1 :max 9999}]]]) (mf/defc subscribe-management-dialog {::mf/register modal/components ::mf/register-as :management-dialog} - [{:keys [subscription-name teams subscribe-to-trial]}] + [{:keys [subscription-type teams subscribe-to-trial]}] - (let [min-members* (mf/use-state (or (some->> teams (map :total-editors) (apply max)) 1)) - min-members (deref min-members*) - formatted-subscription-name (if subscribe-to-trial - (if (= subscription-name "unlimited") - (tr "subscription.settings.unlimited-trial") - (tr "subscription.settings.enterprise-trial")) - (case subscription-name - "professional" (tr "subscription.settings.professional") - "unlimited" (tr "subscription.settings.unlimited") - "enterprise" (tr "subscription.settings.enterprise"))) - handle-subscription-trial (if (= subscription-name "unlimited") - (mf/use-fn - (mf/deps min-members) - (fn [] - (st/emit! (ptk/event ::ev/event {::ev/name "create-trial-subscription" - :type "unlimited" - :quantity min-members})) - (let [current-href (rt/get-current-href) - returnUrl (js/encodeURIComponent current-href) - href (dm/str "payments/subscriptions/create?type=unlimited&quantity=" min-members "&returnUrl=" returnUrl)] - (st/emit! (rt/nav-raw :href href))))) + (let [subscription-name (if subscribe-to-trial + (if (= subscription-type "unlimited") + (tr "subscription.settings.unlimited-trial") + (tr "subscription.settings.enterprise-trial")) + (case subscription-type + "professional" (tr "subscription.settings.professional") + "unlimited" (tr "subscription.settings.unlimited") + "enterprise" (tr "subscription.settings.enterprise"))) + initial (mf/with-memo [] + {:min-members (or (some->> teams (map :total-editors) (apply max)) 1)}) + form (fm/use-form :schema schema:seats-form + :initial initial) + subscribe-to-unlimited (mf/use-fn + (fn [form] + (let [data (:clean-data @form) + return-url (-> (rt/get-current-href) (rt/encode-url)) + href (dm/str "payments/subscriptions/create?type=unlimited&quantity=" (:min-members data) "&returnUrl=" return-url)] + (st/emit! (ptk/event ::ev/event {::ev/name "create-trial-subscription" + :type "unlimited" + :quantity (:min-members data)}) + (rt/nav-raw :href href))))) + + subscribe-to-enterprise (mf/use-fn + (fn [] + (st/emit! (ptk/event ::ev/event {::ev/name "create-trial-subscription" + :type "enterprise"})) + (let [return-url (-> (rt/get-current-href) (rt/encode-url)) + href (dm/str "payments/subscriptions/create?type=enterprise&returnUrl=" return-url)] + (st/emit! (rt/nav-raw :href href))))) - (mf/use-fn - (fn [] - (st/emit! (ptk/event ::ev/event {::ev/name "create-trial-subscription" - :type "enterprise"})) - (let [current-href (rt/get-current-href) - returnUrl (js/encodeURIComponent current-href) - href (dm/str "payments/subscriptions/create?type=enterprise&returnUrl=" returnUrl)] - (st/emit! (rt/nav-raw :href href)))))) handle-accept-dialog (mf/use-fn (fn [] (st/emit! (ptk/event ::ev/event {::ev/name "open-subscription-management" @@ -107,62 +112,71 @@ [:div {:class (stl/css :modal-dialog)} [:button {:class (stl/css :close-btn) :on-click handle-close-dialog} i/close] [:div {:class (stl/css :modal-title :subscription-title)} - (tr "subscription.settings.management.dialog.title" formatted-subscription-name)] + (tr "subscription.settings.management.dialog.title" subscription-name)] [:div {:class (stl/css :modal-content)} (if (seq teams) - [* [:div {:class (stl/css :modal-text)} - (tr "subscription.settings.management.dialog.choose-this-plan")] + [:* [:div {:class (stl/css :modal-text)} + (tr "subscription.settings.management.dialog.choose-this-plan")] [:ul {:class (stl/css :teams-list)} - (for [team (js->clj teams :keywordize-keys true)] + (for [team teams] [:li {:key (dm/str (:id team)) :class (stl/css :team-name)} (:name team) (tr "subscription.settings.management.dialog.members" (:total-editors team))])]] [:div {:class (stl/css :modal-text)} (tr "subscription.settings.management.dialog.no-teams")]) - (when (and (= subscription-name "unlimited") subscribe-to-trial) - [[:label {:for "editors-subscription" :class (stl/css :modal-text :editors-label)} - (tr "subscription.settings.management.dialog.select-editors")] - [:div {:class (stl/css :editors-wrapper)} - [:div {:class (stl/css :input-wrapper)} - [:input {:id "editors-subscription" - :class (stl/css :input-field) - :type "number" - :value min-members - :min 1 - :max 1000 - :on-change #(let [new-value (js/parseInt (.. % -target -value))] - (reset! min-members* - (let [v (cond - (or (mth/nan? new-value) (zero? new-value)) 1 - (> new-value 1000) 1000 - :else (max 1 new-value))] - v)))}]] - [:div {:class (stl/css :editors-cost)} - [:span {:class (stl/css :modal-text-small)} - (tr "subscription.settings.management.dialog.price-month" min-members)] - [:span {:class (stl/css :modal-text-small)} - (tr "subscription.settings.management.dialog.payment-explanation")]]]]) - (when (and - (or (= subscription-name "professional") (= subscription-name "unlimited")) + (or (= subscription-type "professional") (= subscription-type "unlimited")) (not subscribe-to-trial)) [:div {:class (stl/css :modal-text)} (tr "subscription.settings.management.dialog.downgrade")]) - [:div {:class (stl/css :modal-footer)} - [:div {:class (stl/css :action-buttons)} - [:input - {:class (stl/css :cancel-button) - :type "button" - :value (tr "ds.confirm-cancel") - :on-click handle-close-dialog}] + (if (and (= subscription-type "unlimited") subscribe-to-trial) + [:& fm/form {:on-submit subscribe-to-unlimited + :class (stl/css :seats-form) + :form form} + [:label {:for "editors-subscription" :class (stl/css :modal-text :editors-label)} + (tr "subscription.settings.management.dialog.select-editors")] - [:input - {:class (stl/css :primary-button) - :type "button" - :value (if subscribe-to-trial (tr "subscription.settings.start-trial") (tr "labels.continue")) - :on-click (if subscribe-to-trial handle-subscription-trial handle-accept-dialog)}]]]]]])) + [:div {:class (stl/css :editors-wrapper)} + [:div {:class (stl/css :fields-row)} + [:& fm/input {:type "number" + :name :min-members + :show-error false + :label "" + :class (stl/css :input-field)}]] + [:div {:class (stl/css :editors-cost)} + [:span {:class (stl/css :modal-text-small)} + (tr "subscription.settings.management.dialog.price-month" (or (get-in @form [:clean-data :min-members]) 0))] + [:span {:class (stl/css :modal-text-small)} + (tr "subscription.settings.management.dialog.payment-explanation")]]] + + [:div {:class (stl/css :modal-footer)} + [:div {:class (stl/css :action-buttons)} + [:input + {:class (stl/css :cancel-button) + :type "button" + :value (tr "ds.confirm-cancel") + :on-click handle-close-dialog}] + + [:> fm/submit-button* + {:label (tr "subscription.settings.start-trial") + :class (stl/css :primary-button)}]]]] + + [:div {:class (stl/css :modal-footer)} + [:div {:class (stl/css :action-buttons)} + [:input + {:class (stl/css :cancel-button) + :type "button" + :value (tr "ds.confirm-cancel") + :on-click handle-close-dialog}] + + [:input + {:class (stl/css :primary-button) + + :type "button" + :value (if subscribe-to-trial (tr "subscription.settings.start-trial") (tr "labels.continue")) + :on-click (if subscribe-to-trial subscribe-to-enterprise handle-accept-dialog)}]]])]]])) (mf/defc subscription-success-dialog {::mf/register modal/components @@ -201,13 +215,12 @@ (let [route (mf/deref refs/route) params (:params route) params-subscription (:subscription (:query params)) - show-subscription-success-modal (and (:query params) - (or (= (:subscription (:query params)) "subscribed-to-penpot-unlimited") - (= (:subscription (:query params)) "subscribed-to-penpot-enterprise"))) + 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-name (if subscription - (:type subscription) - "professional") + subscription-type (get-subscription-type subscription) subscription-is-trial (= (:status subscription) "trialing") teams* (mf/use-state nil) teams (deref teams*) @@ -229,11 +242,12 @@ (st/emit! (rt/nav-raw :href href))))) open-subscription-modal (mf/use-fn (mf/deps teams) - (fn [subscription-name] - (st/emit! (ptk/event ::ev/event {::ev/name "open-subscription-modal"})) + (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-name subscription-name + {:subscription-type subscription-type :teams teams :subscribe-to-trial (not subscription)}))))] (mf/with-effect [] @@ -244,6 +258,19 @@ (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 [show-subscription-success-modal subscription] (when show-subscription-success-modal (st/emit! @@ -266,7 +293,7 @@ [:div {:class (stl/css :your-subscription)} [:h3 {:class (stl/css :plan-section-title)} (tr "subscription.settings.section-plan")] - (case subscription-name + (case subscription-type "professional" [:> plan-card* {:card-title (tr "subscription.settings.professional") :benefits [(tr "subscription.settings.professional.projects-files"), @@ -327,7 +354,7 @@ [:div {:class (stl/css :other-subscriptions)} [:h3 {:class (stl/css :plan-section-title)} (tr "subscription.settings.other-plans")] - (when (not= subscription-name "professional") + (when (not= subscription-type "professional") [:> plan-card* {:card-title (tr "subscription.settings.professional") :price-value "$0" :price-period (tr "subscription.settings.price-editor-month") @@ -339,7 +366,7 @@ :cta-text-with-icon (tr "subscription.settings.more-information") :cta-link-with-icon go-to-pricing-page}]) - (when (not= subscription-name "unlimited") + (when (not= subscription-type "unlimited") [:> plan-card* {:card-title (tr "subscription.settings.unlimited") :card-title-icon i/character-u :price-value "$7" @@ -353,7 +380,7 @@ :cta-text-with-icon (tr "subscription.settings.more-information") :cta-link-with-icon go-to-pricing-page}]) - (when (not= subscription-name "enterprise") + (when (not= subscription-type "enterprise") [:> plan-card* {:card-title (tr "subscription.settings.enterprise") :card-title-icon i/character-e :price-value "$950" diff --git a/frontend/src/app/main/ui/settings/subscription.scss b/frontend/src/app/main/ui/settings/subscription.scss index ffc7547682..f7602758d7 100644 --- a/frontend/src/app/main/ui/settings/subscription.scss +++ b/frontend/src/app/main/ui/settings/subscription.scss @@ -253,8 +253,8 @@ margin-block: var(--sp-xxl); } -.input-wrapper { - @extend .input-element; +.input-field { + --input-icon-padding: var(--sp-s); width: $s-80; } diff --git a/frontend/src/app/main/ui/workspace/main_menu.cljs b/frontend/src/app/main/ui/workspace/main_menu.cljs index e92cfbb4c6..2c9be80e5e 100644 --- a/frontend/src/app/main/ui/workspace/main_menu.cljs +++ b/frontend/src/app/main/ui/workspace/main_menu.cljs @@ -30,7 +30,7 @@ [app.main.store :as st] [app.main.ui.components.dropdown-menu :refer [dropdown-menu dropdown-menu-item*]] [app.main.ui.context :as ctx] - [app.main.ui.dashboard.subscription :refer [main-menu-power-up*]] + [app.main.ui.dashboard.subscription :refer [main-menu-power-up* get-subscription-type]] [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] [app.main.ui.hooks.resize :as r] [app.main.ui.icons :as i] @@ -825,9 +825,7 @@ (modal/show :plugin-management {})))) subscription (:subscription (:props profile)) - subscription-name (if subscription - (:type subscription) - "professional")] + subscription-type (get-subscription-type subscription)] (mf/with-effect [] (let [disposable (->> st/stream @@ -913,7 +911,7 @@ [:span {:class (stl/css :item-name)} (tr "workspace.header.menu.option.help-info")] [:span {:class (stl/css :open-arrow)} i/arrow]] - (when (and (contains? cf/flags :subscriptions) (not= "enterprise" subscription-name)) + (when (and (contains? cf/flags :subscriptions) (not= "enterprise" subscription-type)) [:> main-menu-power-up* {:close-sub-menu close-sub-menu}]) ;; TODO remove this block when subscriptions is full implemented diff --git a/frontend/src/app/main/ui/workspace/sidebar/versions.cljs b/frontend/src/app/main/ui/workspace/sidebar/versions.cljs index 73902900e2..edd74d3a63 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/versions.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/versions.cljs @@ -17,6 +17,7 @@ [app.main.store :as st] [app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.components.select :refer [select]] + [app.main.ui.dashboard.subscription :refer [get-subscription-type]] [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] [app.main.ui.ds.foundations.assets.icon :as i] [app.main.ui.ds.product.autosaved-milestone :refer [autosaved-milestone*]] @@ -36,26 +37,27 @@ (defn get-versions-stored-days [team] - (let [subscription-name (-> team :subscription :type)] + (let [subscription-type (get-subscription-type (:subscription team))] (cond - (= subscription-name "unlimited") 30 - (= subscription-name "enterprise") 90 + (= subscription-type "unlimited") 30 + (= subscription-type "enterprise") 90 :else 7))) (defn get-versions-warning-subtext [team] - (let [subscription-name (-> team :subscription :type) + (let [subscription-type (get-subscription-type (:subscription team)) is-owner? (-> team :permissions :is-owner) email-owner (:email (some #(when (:is-owner %) %) (:members team))) + support-email "support@penpot.app" go-to-subscription (dm/str (u/join cfg/public-uri "#/settings/subscriptions"))] (if (contains? cfg/flags :subscriptions) (if is-owner? - (if (= "enterprise" subscription-name) - (tr "subscription.workspace.versions.warning.enterprise.subtext-owner" "support@penpot.app") + (if (= "enterprise" subscription-type) + (tr "subscription.workspace.versions.warning.enterprise.subtext-owner" support-email support-email) (tr "subscription.workspace.versions.warning.subtext-owner" go-to-subscription)) (tr "subscription.workspace.versions.warning.subtext-member" email-owner email-owner)) - (tr "workspace.versions.warning.subtext" "support@penpot.app")))) + (tr "workspace.versions.warning.subtext" support-email)))) (defn group-snapshots [data] diff --git a/frontend/translations/en.po b/frontend/translations/en.po index f8a6bdb7bb..41d976df70 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -7883,6 +7883,7 @@ msgstr "" "If you'd like to increase this limit, " "[upgrade your plan|target:self](%s)" +#, markdown msgid "subscription.workspace.versions.warning.enterprise.subtext-owner" msgstr "" "If you'd like to increase this limit, write to us at " diff --git a/frontend/translations/es.po b/frontend/translations/es.po index fa018fea8e..cf1fa0b593 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -7816,6 +7816,7 @@ msgstr "" "Si quieres aumentar este límite, " "[mejora tu plan|target:self](%s)" +#, markdown msgid "subscription.workspace.versions.warning.enterprise.subtext-owner" msgstr "" "Si quieres aumentar este límite, escríbenos a "