🐛 Add fixes from subscription design review (#6870)

* 🐛 Fixes from subscription design review

* 🐛 Fix to consider professional plan the unpaid and canceled status

* 📎 Fixes PR feedback
This commit is contained in:
Marina López 2025-07-10 11:55:16 +02:00 committed by GitHub
parent 66c5841d48
commit ba6a02d1d9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 312 additions and 154 deletions

View file

@ -140,7 +140,7 @@
WHEN 'professional' THEN 'active' WHEN 'professional' THEN 'active'
ELSE COALESCE(p.props->'~:subscription'->>'~:status', 'incomplete') ELSE COALESCE(p.props->'~:subscription'->>'~:status', 'incomplete')
END, END,
'~:seats', p.props->'~:quantity' '~:seats', p.props->'~:subscription'->'~:quantity'
) AS subscription ) AS subscription
FROM team_profile_rel AS tp FROM team_profile_rel AS tp
JOIN team AS t ON (t.id = tp.team_id) JOIN team AS t ON (t.id = tp.team_id)

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -17,7 +17,8 @@
}, },
"~:subscription": { "~:subscription": {
"~:type": "enterprise", "~:type": "enterprise",
"~:status": "trialing" "~:status": "trialing",
"~:seats": null
}, },
"~:name": "Default", "~:name": "Default",
"~:modified-at": "~m1713533116375", "~:modified-at": "~m1713533116375",

View file

@ -41,7 +41,8 @@
}, },
"~:subscription": { "~:subscription": {
"~:type": "professional", "~:type": "professional",
"~:status": "active" "~:status": "active",
"~:seats": null
}, },
"~:name": "Second team", "~:name": "Second team",
"~:modified-at": "~m1701164272671", "~:modified-at": "~m1701164272671",

View file

@ -17,7 +17,8 @@
}, },
"~:subscription": { "~:subscription": {
"~:type": "unlimited", "~:type": "unlimited",
"~:status": "trialing" "~:status": "trialing",
"~:seats": 2
}, },
"~:name": "Default", "~:name": "Default",
"~:modified-at": "~m1713533116375", "~:modified-at": "~m1713533116375",

View file

@ -41,7 +41,8 @@
}, },
"~:subscription": { "~:subscription": {
"~:type": "unlimited", "~:type": "unlimited",
"~:status": "trialing" "~:status": "trialing",
"~:seats": 2
}, },
"~:name": "Second team", "~:name": "Second team",
"~:modified-at": "~m1701164272671", "~:modified-at": "~m1701164272671",

View file

@ -41,7 +41,8 @@
}, },
"~:subscription": { "~:subscription": {
"~:type": "unlimited", "~:type": "unlimited",
"~:status": "trialing" "~:status": "trialing",
"~:seats": 2
}, },
"~:name": "Second team", "~:name": "Second team",
"~:modified-at": "~m1701164272671", "~:modified-at": "~m1701164272671",

View file

@ -79,6 +79,74 @@ test.describe("Subscriptions: dashboard", () => {
"Unlimited plan (trial)", "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", () => { test.describe("Subscriptions: team members and invitations", () => {

View file

@ -8,6 +8,7 @@
// INPUT // INPUT
.input-wrapper { .input-wrapper {
--input-icon-padding: var(--sp-l);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
@ -108,7 +109,7 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
position: absolute; position: absolute;
right: $s-16; right: var(--input-icon-padding);
top: calc(50% - $s-8); top: calc(50% - $s-8);
svg { svg {
width: $s-12; width: $s-12;

View file

@ -132,7 +132,7 @@
[:> team-members-page* {:team team :profile profile}] [:> team-members-page* {:team team :profile profile}]
:dashboard-invitations :dashboard-invitations
[:> team-invitations-page* {:team team :profile profile}] [:> team-invitations-page* {:team team}]
:dashboard-webhooks :dashboard-webhooks
[:> webhooks-page* {:team team}] [:> webhooks-page* {:team team}]

View file

@ -27,7 +27,7 @@
[app.main.ui.dashboard.comments :refer [comments-icon* comments-section]] [app.main.ui.dashboard.comments :refer [comments-icon* comments-section]]
[app.main.ui.dashboard.inline-edition :refer [inline-edition]] [app.main.ui.dashboard.inline-edition :refer [inline-edition]]
[app.main.ui.dashboard.project-menu :refer [project-menu*]] [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.dashboard.team-form]
[app.main.ui.icons :as i :refer [icon-xref]] [app.main.ui.icons :as i :refer [icon-xref]]
[app.util.dom :as dom] [app.util.dom :as dom]
@ -333,10 +333,10 @@
:alt (:name team-item)}] :alt (:name team-item)}]
(if (and (contains? cf/flags :subscriptions) (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)} [:div {:class (stl/css :team-text-with-icon)}
[:span {:class (stl/css :team-text) :title (:name team-item)} (:name team-item)] [: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) [:span {:class (stl/css :team-text)
:title (:name team-item)} (:name team-item)]) :title (:name team-item)} (:name team-item)])
(when (= (:id team-item) (:id team)) (when (= (:id team-item) (:id team))
@ -654,7 +654,7 @@
(fn [] (fn []
(reset! show-teams-ddwn? false)) (reset! show-teams-ddwn? false))
subscription (:subscription team) subscription (:subscription team)
subscription-name (:type subscription)] subscription-type (get-subscription-type subscription)]
[:div {:class (stl/css :sidebar-team-switch)} [:div {:class (stl/css :sidebar-team-switch)}
[:div {:class (stl/css :switch-content)} [:div {:class (stl/css :switch-content)}
@ -669,18 +669,18 @@
(and (contains? cf/flags :subscriptions) (and (contains? cf/flags :subscriptions)
(not (:is-default team)) (not (:is-default team))
(or (= "unlimited" subscription-name) (= "enterprise" subscription-name))) (or (= "unlimited" subscription-type) (= "enterprise" subscription-type)))
[:div {:class (stl/css :team-name)} [:div {:class (stl/css :team-name)}
[:img {:src (cf/resolve-team-photo-url team) [:img {:src (cf/resolve-team-photo-url team)
:class (stl/css :team-picture) :class (stl/css :team-picture)
:alt (:name team)}] :alt (:name team)}]
[:div {:class (stl/css :team-text-with-icon)} [:div {:class (stl/css :team-text-with-icon)}
[:span {:class (stl/css :team-text) :title (:name team)} (:name team)] [: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)) (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)} [:div {:class (stl/css :team-name)}
[:img {:src (cf/resolve-team-photo-url team) [:img {:src (cf/resolve-team-photo-url team)
:class (stl/css :team-picture) :class (stl/css :team-picture)

View file

@ -18,6 +18,12 @@
[potok.v2.core :as ptk] [potok.v2.core :as ptk]
[rumext.v2 :as mf])) [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* (mf/defc cta-power-up*
[{:keys [top-title top-description bottom-description has-dropdown]}] [{:keys [top-title top-description bottom-description has-dropdown]}]
(let [show-data* (mf/use-state false) (let [show-data* (mf/use-state false)
@ -46,13 +52,11 @@
(mf/defc subscription-sidebar* (mf/defc subscription-sidebar*
[{:keys [profile]}] [{:keys [profile]}]
(let [subscription (:subscription (:props profile)) (let [subscription (:subscription (:props profile))
subscription-name (if subscription subscription-type (get-subscription-type subscription)
(:type subscription)
"professional")
subscription-is-trial (= (:status subscription) "trialing") subscription-is-trial (= (:status subscription) "trialing")
subscription-href (dm/str (u/join cf/public-uri "#/settings/subscriptions"))] subscription-href (dm/str (u/join cf/public-uri "#/settings/subscriptions"))]
(case subscription-name (case subscription-type
"professional" "professional"
[:> cta-power-up* [:> cta-power-up*
{:top-title (tr "subscription.dashboard.power-up.your-subscription") {:top-title (tr "subscription.dashboard.power-up.your-subscription")
@ -88,7 +92,7 @@
(mf/defc team* (mf/defc team*
[{:keys [is-owner team]}] [{:keys [is-owner team]}]
(let [subscription (:subscription team) (let [subscription (:subscription team)
subscription-name (:type subscription) subscription-type (get-subscription-type subscription)
subscription-is-trial (= "trialing" (:status subscription)) subscription-is-trial (= "trialing" (:status subscription))
go-to-manage-subscription go-to-manage-subscription
@ -106,23 +110,23 @@
[:div {:class (stl/css :team-label)} [:div {:class (stl/css :team-label)}
(tr "subscription.dashboard.team-plan")] (tr "subscription.dashboard.team-plan")]
[:span {:class (stl/css :team-text)} [:span {:class (stl/css :team-text)}
(case subscription-name (case subscription-type
"professional" (tr "subscription.settings.professional") "professional" (tr "subscription.settings.professional")
"unlimited" (if subscription-is-trial "unlimited" (if subscription-is-trial
(tr "subscription.settings.unlimited-trial") (tr "subscription.settings.unlimited-trial")
(tr "subscription.settings.unlimited")) (tr "subscription.settings.unlimited"))
"enterprise" (tr "subscription.settings.enterprise"))] "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) [:button {:class (stl/css :manage-subscription-link)
:on-click go-to-manage-subscription :on-click go-to-manage-subscription
:data-testid "manage-subscription-link"} :data-testid "manage-subscription-link"}
(tr "subscription.settings.manage-your-subscription")])])) (tr "subscription.settings.manage-your-subscription")])]))
(mf/defc menu-team-icon* (mf/defc menu-team-icon*
[{:keys [subscription-name]}] [{:keys [subscription-type]}]
[:span {:class (stl/css :subscription-icon) :data-testid "subscription-icon"} [:span {:class (stl/css :subscription-icon) :data-testid "subscription-icon"}
(case subscription-name (case subscription-type
"unlimited" i/character-u "unlimited" i/character-u
"enterprise" i/character-e)]) "enterprise" i/character-e)])
@ -139,15 +143,15 @@
[:span {:class (stl/css :item-name)} (tr "subscription.workspace.header.menu.option.power-up")]])) [:span {:class (stl/css :item-name)} (tr "subscription.workspace.header.menu.option.power-up")]]))
(mf/defc members-cta* (mf/defc members-cta*
[{:keys [banner-is-expanded team profile]}] [{:keys [banner-is-expanded team]}]
(let [subscription (:subscription team) (let [subscription (:subscription team)
subscription-name (:type subscription) subscription-type (get-subscription-type subscription)
is-owner (:is-owner (:permissions team)) is-owner (-> team :permissions :is-owner)
email-owner (:email (some #(when (:is-owner %) %) (:members team))) email-owner (:email (some #(when (:is-owner %) %) (:members team)))
mail-to-owner (str "<a href=\"" "mailto:" email-owner "\">" email-owner "</a>") mail-to-owner (str "<a href=\"" "mailto:" email-owner "\">" email-owner "</a>")
go-to-subscription (dm/str (u/join cf/public-uri "#/settings/subscriptions")) 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))) editors (count (filterv :can-edit (:members team)))
link link
@ -157,24 +161,24 @@
cta-title cta-title
(cond (cond
(and (= "professional" subscription-name) (>= editors 8)) (and (= "professional" subscription-type) (>= editors 8))
(tr "subscription.dashboard.cta.professional-plan-designed") (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)) (tr "subscription.dashboard.cta.unlimited-many-editors" seats editors))
cta-message cta-message
(cond (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) (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) (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) (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))] (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} [:> 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}]])) :content cta-message}]]))
(defn show-subscription-members-main-banner? (defn show-subscription-members-main-banner?
[team profile] [team]
(let [seats (-> profile :props :subscription :quantity) (let [subscription-type (get-subscription-type (:subscription team))
editors (count (filter :can-edit (:members team)))] seats (-> team :subscription :seats)
editors (count (filter :can-edit (:members team)))]
(or (or
(and (= (:type (:subscription team)) "professional") (and (= subscription-type "professional")
(> editors 8)) (> editors 8))
(and (and
(= (:type (:subscription team)) "unlimited") (= subscription-type "unlimited")
(>= editors 8) (>= editors 8)
(< seats editors))))) (< seats editors)))))
(defn show-subscription-members-small-banner? (defn show-subscription-members-small-banner?
[team profile] [team]
(let [seats (-> profile :props :subscription :quantity) (let [subscription-type (get-subscription-type (:subscription team))
seats (-> team :subscription :seats)
editors (count (filterv :can-edit (:members team)))] editors (count (filterv :can-edit (:members team)))]
(or (or
(and (= (:type (:subscription team)) "professional") (and (= subscription-type "professional")
(= editors 8)) (= editors 8))
(and (= (:type (:subscription team)) "unlimited") (and (= subscription-type "unlimited")
(< editors 8) (< editors 8)
(< seats editors))))) (< seats editors)))))

View file

@ -544,18 +544,18 @@
[:section {:class (stl/css-case [:section {:class (stl/css-case
:dashboard-container true :dashboard-container true
:dashboard-team-members 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) (when (and (contains? cfg/flags :subscriptions)
(show-subscription-members-main-banner? team profile)) (show-subscription-members-main-banner? team))
[:> members-cta* {:banner-is-expanded true :team team :profile profile}]) [:> members-cta* {:banner-is-expanded true :team team}])
[:> team-members* [:> team-members*
{:profile profile {:profile profile
:team team}] :team team}]
(when (and (when (and
(contains? cfg/flags :subscriptions) (contains? cfg/flags :subscriptions)
(show-subscription-members-small-banner? team profile)) (show-subscription-members-small-banner? team))
[:> members-cta* {:banner-is-expanded false :team team :profile profile}])]]) [:> members-cta* {:banner-is-expanded false :team team}])]])
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; INVITATIONS SECTION ;; INVITATIONS SECTION
@ -803,7 +803,7 @@
(mf/defc team-invitations-page* (mf/defc team-invitations-page*
{::mf/props :obj} {::mf/props :obj}
[{:keys [team profile]}] [{:keys [team]}]
(mf/with-effect [team] (mf/with-effect [team]
(dom/set-html-title (dom/set-html-title
@ -820,14 +820,14 @@
:team team}] :team team}]
[:section {:class (stl/css-case [:section {:class (stl/css-case
:dashboard-team-invitations true :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) (when (and (contains? cfg/flags :subscriptions)
(show-subscription-members-main-banner? team profile)) (show-subscription-members-main-banner? team))
[:> members-cta* {:banner-is-expanded true :team team :profile profile}]) [:> members-cta* {:banner-is-expanded true :team team}])
[:> invitation-section* {:team team}] [:> invitation-section* {:team team}]
(when (and (contains? cfg/flags :subscriptions) (when (and (contains? cfg/flags :subscriptions)
(show-subscription-members-small-banner? team profile)) (show-subscription-members-small-banner? team))
[:> members-cta* {:banner-is-expanded false :team team :profile profile}])]]) [:> members-cta* {:banner-is-expanded false :team team}])]])
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; WEBHOOKS SECTION ;; WEBHOOKS SECTION

View file

@ -2,7 +2,7 @@
(:require-macros [app.main.style :as stl]) (:require-macros [app.main.style :as stl])
(:require (:require
[app.common.data.macros :as dm] [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.event :as ev]
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
[app.main.data.profile :as du] [app.main.data.profile :as du]
@ -10,6 +10,8 @@
[app.main.repo :as rp] [app.main.repo :as rp]
[app.main.router :as rt] [app.main.router :as rt]
[app.main.store :as st] [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.main.ui.icons :as i]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
@ -52,42 +54,45 @@
:on-click cta-link} cta-text]) :on-click cta-link} cta-text])
(when (and cta-link-trial cta-text-trial) [:button {:class (stl/css :cta-button :bottom-link) (when (and cta-link-trial cta-text-trial) [:button {:class (stl/css :cta-button :bottom-link)
:on-click cta-link-trial} cta-text-trial])]) :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/defc subscribe-management-dialog
{::mf/register modal/components {::mf/register modal/components
::mf/register-as :management-dialog} ::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)) (let [subscription-name (if subscribe-to-trial
min-members (deref min-members*) (if (= subscription-type "unlimited")
formatted-subscription-name (if subscribe-to-trial (tr "subscription.settings.unlimited-trial")
(if (= subscription-name "unlimited") (tr "subscription.settings.enterprise-trial"))
(tr "subscription.settings.unlimited-trial") (case subscription-type
(tr "subscription.settings.enterprise-trial")) "professional" (tr "subscription.settings.professional")
(case subscription-name "unlimited" (tr "subscription.settings.unlimited")
"professional" (tr "subscription.settings.professional") "enterprise" (tr "subscription.settings.enterprise")))
"unlimited" (tr "subscription.settings.unlimited") initial (mf/with-memo []
"enterprise" (tr "subscription.settings.enterprise"))) {:min-members (or (some->> teams (map :total-editors) (apply max)) 1)})
handle-subscription-trial (if (= subscription-name "unlimited") form (fm/use-form :schema schema:seats-form
(mf/use-fn :initial initial)
(mf/deps min-members) subscribe-to-unlimited (mf/use-fn
(fn [] (fn [form]
(st/emit! (ptk/event ::ev/event {::ev/name "create-trial-subscription" (let [data (:clean-data @form)
:type "unlimited" return-url (-> (rt/get-current-href) (rt/encode-url))
:quantity min-members})) href (dm/str "payments/subscriptions/create?type=unlimited&quantity=" (:min-members data) "&returnUrl=" return-url)]
(let [current-href (rt/get-current-href) (st/emit! (ptk/event ::ev/event {::ev/name "create-trial-subscription"
returnUrl (js/encodeURIComponent current-href) :type "unlimited"
href (dm/str "payments/subscriptions/create?type=unlimited&quantity=" min-members "&returnUrl=" returnUrl)] :quantity (:min-members data)})
(st/emit! (rt/nav-raw :href href))))) (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 handle-accept-dialog (mf/use-fn
(fn [] (fn []
(st/emit! (ptk/event ::ev/event {::ev/name "open-subscription-management" (st/emit! (ptk/event ::ev/event {::ev/name "open-subscription-management"
@ -107,62 +112,71 @@
[:div {:class (stl/css :modal-dialog)} [:div {:class (stl/css :modal-dialog)}
[:button {:class (stl/css :close-btn) :on-click handle-close-dialog} i/close] [:button {:class (stl/css :close-btn) :on-click handle-close-dialog} i/close]
[:div {:class (stl/css :modal-title :subscription-title)} [: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)} [:div {:class (stl/css :modal-content)}
(if (seq teams) (if (seq teams)
[* [:div {:class (stl/css :modal-text)} [:* [:div {:class (stl/css :modal-text)}
(tr "subscription.settings.management.dialog.choose-this-plan")] (tr "subscription.settings.management.dialog.choose-this-plan")]
[:ul {:class (stl/css :teams-list)} [: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)} [:li {:key (dm/str (:id team)) :class (stl/css :team-name)}
(:name team) (tr "subscription.settings.management.dialog.members" (:total-editors team))])]] (:name team) (tr "subscription.settings.management.dialog.members" (:total-editors team))])]]
[:div {:class (stl/css :modal-text)} [:div {:class (stl/css :modal-text)}
(tr "subscription.settings.management.dialog.no-teams")]) (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 (when (and
(or (= subscription-name "professional") (= subscription-name "unlimited")) (or (= subscription-type "professional") (= subscription-type "unlimited"))
(not subscribe-to-trial)) (not subscribe-to-trial))
[:div {:class (stl/css :modal-text)} [:div {:class (stl/css :modal-text)}
(tr "subscription.settings.management.dialog.downgrade")]) (tr "subscription.settings.management.dialog.downgrade")])
[:div {:class (stl/css :modal-footer)} (if (and (= subscription-type "unlimited") subscribe-to-trial)
[:div {:class (stl/css :action-buttons)} [:& fm/form {:on-submit subscribe-to-unlimited
[:input :class (stl/css :seats-form)
{:class (stl/css :cancel-button) :form form}
:type "button" [:label {:for "editors-subscription" :class (stl/css :modal-text :editors-label)}
:value (tr "ds.confirm-cancel") (tr "subscription.settings.management.dialog.select-editors")]
:on-click handle-close-dialog}]
[:input [:div {:class (stl/css :editors-wrapper)}
{:class (stl/css :primary-button) [:div {:class (stl/css :fields-row)}
:type "button" [:& fm/input {:type "number"
:value (if subscribe-to-trial (tr "subscription.settings.start-trial") (tr "labels.continue")) :name :min-members
:on-click (if subscribe-to-trial handle-subscription-trial handle-accept-dialog)}]]]]]])) :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/defc subscription-success-dialog
{::mf/register modal/components {::mf/register modal/components
@ -201,13 +215,12 @@
(let [route (mf/deref refs/route) (let [route (mf/deref refs/route)
params (:params route) params (:params route)
params-subscription (:subscription (:query params)) params-subscription (:subscription (:query params))
show-subscription-success-modal (and (:query params) show-trial-subscription-modal (or (= params-subscription "subscription-to-penpot-unlimited")
(or (= (:subscription (:query params)) "subscribed-to-penpot-unlimited") (= params-subscription "subscription-to-penpot-enterprise"))
(= (:subscription (:query params)) "subscribed-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 (:subscription (:props profile))
subscription-name (if subscription subscription-type (get-subscription-type subscription)
(:type subscription)
"professional")
subscription-is-trial (= (:status subscription) "trialing") subscription-is-trial (= (:status subscription) "trialing")
teams* (mf/use-state nil) teams* (mf/use-state nil)
teams (deref teams*) teams (deref teams*)
@ -229,11 +242,12 @@
(st/emit! (rt/nav-raw :href href))))) (st/emit! (rt/nav-raw :href href)))))
open-subscription-modal (mf/use-fn open-subscription-modal (mf/use-fn
(mf/deps teams) (mf/deps teams)
(fn [subscription-name] (fn [subscription-type]
(st/emit! (ptk/event ::ev/event {::ev/name "open-subscription-modal"})) (st/emit! (ptk/event ::ev/event {::ev/name "open-subscription-modal"
::ev/origin "settings:in-app"}))
(st/emit! (st/emit!
(modal/show :management-dialog (modal/show :management-dialog
{:subscription-name subscription-name {:subscription-type subscription-type
:teams teams :subscribe-to-trial (not subscription)}))))] :teams teams :subscribe-to-trial (not subscription)}))))]
(mf/with-effect [] (mf/with-effect []
@ -244,6 +258,19 @@
(mf/with-effect [] (mf/with-effect []
(dom/set-html-title (tr "subscription.labels"))) (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] (mf/with-effect [show-subscription-success-modal subscription]
(when show-subscription-success-modal (when show-subscription-success-modal
(st/emit! (st/emit!
@ -266,7 +293,7 @@
[:div {:class (stl/css :your-subscription)} [:div {:class (stl/css :your-subscription)}
[:h3 {:class (stl/css :plan-section-title)} (tr "subscription.settings.section-plan")] [:h3 {:class (stl/css :plan-section-title)} (tr "subscription.settings.section-plan")]
(case subscription-name (case subscription-type
"professional" "professional"
[:> plan-card* {:card-title (tr "subscription.settings.professional") [:> plan-card* {:card-title (tr "subscription.settings.professional")
:benefits [(tr "subscription.settings.professional.projects-files"), :benefits [(tr "subscription.settings.professional.projects-files"),
@ -327,7 +354,7 @@
[:div {:class (stl/css :other-subscriptions)} [:div {:class (stl/css :other-subscriptions)}
[:h3 {:class (stl/css :plan-section-title)} (tr "subscription.settings.other-plans")] [: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") [:> plan-card* {:card-title (tr "subscription.settings.professional")
:price-value "$0" :price-value "$0"
:price-period (tr "subscription.settings.price-editor-month") :price-period (tr "subscription.settings.price-editor-month")
@ -339,7 +366,7 @@
:cta-text-with-icon (tr "subscription.settings.more-information") :cta-text-with-icon (tr "subscription.settings.more-information")
:cta-link-with-icon go-to-pricing-page}]) :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") [:> plan-card* {:card-title (tr "subscription.settings.unlimited")
:card-title-icon i/character-u :card-title-icon i/character-u
:price-value "$7" :price-value "$7"
@ -353,7 +380,7 @@
:cta-text-with-icon (tr "subscription.settings.more-information") :cta-text-with-icon (tr "subscription.settings.more-information")
:cta-link-with-icon go-to-pricing-page}]) :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") [:> plan-card* {:card-title (tr "subscription.settings.enterprise")
:card-title-icon i/character-e :card-title-icon i/character-e
:price-value "$950" :price-value "$950"

View file

@ -253,8 +253,8 @@
margin-block: var(--sp-xxl); margin-block: var(--sp-xxl);
} }
.input-wrapper { .input-field {
@extend .input-element; --input-icon-padding: var(--sp-s);
width: $s-80; width: $s-80;
} }

View file

@ -30,7 +30,7 @@
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.dropdown-menu :refer [dropdown-menu dropdown-menu-item*]] [app.main.ui.components.dropdown-menu :refer [dropdown-menu dropdown-menu-item*]]
[app.main.ui.context :as ctx] [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.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.hooks.resize :as r] [app.main.ui.hooks.resize :as r]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
@ -825,9 +825,7 @@
(modal/show :plugin-management {})))) (modal/show :plugin-management {}))))
subscription (:subscription (:props profile)) subscription (:subscription (:props profile))
subscription-name (if subscription subscription-type (get-subscription-type subscription)]
(:type subscription)
"professional")]
(mf/with-effect [] (mf/with-effect []
(let [disposable (->> st/stream (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 :item-name)} (tr "workspace.header.menu.option.help-info")]
[:span {:class (stl/css :open-arrow)} i/arrow]] [: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}]) [:> main-menu-power-up* {:close-sub-menu close-sub-menu}])
;; TODO remove this block when subscriptions is full implemented ;; TODO remove this block when subscriptions is full implemented

View file

@ -17,6 +17,7 @@
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.components.select :refer [select]] [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.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :as i] [app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.ds.product.autosaved-milestone :refer [autosaved-milestone*]] [app.main.ui.ds.product.autosaved-milestone :refer [autosaved-milestone*]]
@ -36,26 +37,27 @@
(defn get-versions-stored-days (defn get-versions-stored-days
[team] [team]
(let [subscription-name (-> team :subscription :type)] (let [subscription-type (get-subscription-type (:subscription team))]
(cond (cond
(= subscription-name "unlimited") 30 (= subscription-type "unlimited") 30
(= subscription-name "enterprise") 90 (= subscription-type "enterprise") 90
:else 7))) :else 7)))
(defn get-versions-warning-subtext (defn get-versions-warning-subtext
[team] [team]
(let [subscription-name (-> team :subscription :type) (let [subscription-type (get-subscription-type (:subscription team))
is-owner? (-> team :permissions :is-owner) is-owner? (-> team :permissions :is-owner)
email-owner (:email (some #(when (:is-owner %) %) (:members team))) 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"))] go-to-subscription (dm/str (u/join cfg/public-uri "#/settings/subscriptions"))]
(if (contains? cfg/flags :subscriptions) (if (contains? cfg/flags :subscriptions)
(if is-owner? (if is-owner?
(if (= "enterprise" subscription-name) (if (= "enterprise" subscription-type)
(tr "subscription.workspace.versions.warning.enterprise.subtext-owner" "support@penpot.app") (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-owner" go-to-subscription))
(tr "subscription.workspace.versions.warning.subtext-member" email-owner email-owner)) (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 (defn group-snapshots
[data] [data]

View file

@ -7883,6 +7883,7 @@ msgstr ""
"If you'd like to increase this limit, " "If you'd like to increase this limit, "
"[upgrade your plan|target:self](%s)" "[upgrade your plan|target:self](%s)"
#, markdown
msgid "subscription.workspace.versions.warning.enterprise.subtext-owner" msgid "subscription.workspace.versions.warning.enterprise.subtext-owner"
msgstr "" msgstr ""
"If you'd like to increase this limit, write to us at " "If you'd like to increase this limit, write to us at "

View file

@ -7816,6 +7816,7 @@ msgstr ""
"Si quieres aumentar este límite, " "Si quieres aumentar este límite, "
"[mejora tu plan|target:self](%s)" "[mejora tu plan|target:self](%s)"
#, markdown
msgid "subscription.workspace.versions.warning.enterprise.subtext-owner" msgid "subscription.workspace.versions.warning.enterprise.subtext-owner"
msgstr "" msgstr ""
"Si quieres aumentar este límite, escríbenos a " "Si quieres aumentar este límite, escríbenos a "