Visual indicators subscription for teams and project settings (#6546)

*  Visual indicators subscription for teams and project settings

* 📎 Fixes PR feedback

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
This commit is contained in:
Marina López 2025-05-26 12:56:40 +02:00 committed by GitHub
parent 5e8929e504
commit e5bc369e56
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 1116 additions and 155 deletions

View file

@ -46,6 +46,10 @@
(update [_ state]
(assoc state :router (create routes)))))
(defn encode-url
[url]
(js/encodeURIComponent url))
(defn match
"Given routing tree and current path, return match with possibly
coerced parameters. Return nil if no match found."

View file

@ -21,12 +21,13 @@
[app.main.refs :as refs]
[app.main.router :as rt]
[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.components.link :refer [link]]
[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 :as subscription]
[app.main.ui.dashboard.subscription :refer [subscription-sidebar* menu-team-icon*]]
[app.main.ui.dashboard.team-form]
[app.main.ui.icons :as i :refer [icon-xref]]
[app.util.dom :as dom]
@ -330,8 +331,14 @@
[:img {:src (cf/resolve-team-photo-url team-item)
:class (stl/css :team-picture)
:alt (:name team-item)}]
[:span {:class (stl/css :team-text)
:title (:name team-item)} (:name team-item)]
(if (and (contains? cf/flags :subscriptions)
(or (= "unlimited" (:type (:subscription team-item))) (= "enterprise" (: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))}]]
[:span {:class (stl/css :team-text)
:title (:name team-item)} (:name team-item)])
(when (= (:id team-item) (:id team))
tick-icon)])
@ -645,19 +652,35 @@
handle-close-team
(fn []
(reset! show-teams-ddwn? false))]
(reset! show-teams-ddwn? false))
subscription (:subscription team)
subscription-name (:type subscription)]
[:div {:class (stl/css :sidebar-team-switch)}
[:div {:class (stl/css :switch-content)}
[:button {:class (stl/css :current-team)
:on-click handle-show-team-click
:on-key-down handle-show-team-keydown}
(if (:is-default team)
(cond
(:is-default team)
[:div {:class (stl/css :team-name)}
[:span {:class (stl/css :penpot-icon)} i/logo-icon]
[:span {:class (stl/css :team-text)} (tr "dashboard.default-team-name")]]
(and (contains? cf/flags :subscriptions)
(not (:is-default team))
(or (= "unlimited" subscription-name) (= "enterprise" subscription-name)))
[: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}]]]
(and (not (:is-default team))
(not (contains? cf/flags :subscriptions)))
[:div {:class (stl/css :team-name)}
[:img {:src (cf/resolve-team-photo-url team)
:class (stl/css :team-picture)
@ -964,7 +987,7 @@
[:*
(when (contains? cf/flags :subscriptions)
[:> subscription/sidebar*])
[:> subscription-sidebar* {:profile profile}])
;; TODO remove this block when subscriptions is full implemented
(when (contains? cf/flags :subscriptions-old)
@ -974,7 +997,7 @@
[:span (tr "dashboard.upgrade-plan.penpot-free")]
[:span {:class (stl/css :no-limits)} (tr "dashboard.upgrade-plan.no-limits")]]
[:div {:class (stl/css :power-up)}
(tr "dashboard.upgrade-plan.power-up")]])
(tr "subscription.dashboard.upgrade-plan.power-up")]])
(when (and team profile)
[:& comments-section

View file

@ -84,11 +84,18 @@
.team-text {
@include textEllipsis;
@include smallTitleTipography;
width: $s-144;
width: auto;
text-align: left;
color: var(--menu-foreground-color-hover);
}
.team-text-with-icon {
display: flex;
gap: $s-8;
max-width: 100%;
overflow: hidden;
}
// This icon still use the old svg
.penpot-icon {
@include flexCenter;

View file

@ -3,15 +3,21 @@
(ns app.main.ui.dashboard.subscription
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.config :as cf]
[app.main.router :as rt]
[app.main.store :as st]
[app.main.ui.components.dropdown-menu :refer [dropdown-menu-item*]]
[app.main.ui.ds.product.cta :refer [cta*]]
[app.main.ui.icons :as i]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as kbd]
[lambdaisland.uri :as u]
[rumext.v2 :as mf]))
(mf/defc cta-power-up*
[{:keys [top-title top-description bottom-description cta-text cta-link has-dropdown]}]
[{:keys [top-title top-description bottom-description has-dropdown]}]
(let [show-data* (mf/use-state false)
show-data (deref show-data*)
handle-click
@ -24,55 +30,166 @@
:on-click handle-click}
[:button {:class (stl/css :cta-top-section)}
[:div {:class (stl/css :content)}
[:span {:class (stl/css :cta-title)} top-title]
[:span {:class (stl/css :cta-text)} top-description]]
[:span {:class (stl/css :cta-title :cta-text)} top-title]
[:span {:class (stl/css :cta-text-m)} top-description]]
(when has-dropdown [:span {:class (stl/css :icon-dropdown)} i/arrow])]
(when (and has-dropdown show-data)
[:div {:class (stl/css :cta-bottom-section)}
[:> i18n/tr-html* {:content bottom-description
:class (stl/css :content)
:tag-name "button"}]
[:button {:class (stl/css :cta-highlight :cta-link) :on-click cta-link}
cta-text]])]))
:tag-name "span"}]])]))
(mf/defc sidebar*
[]
(let [;; TODO subscription cases professional/unlimited/enterprise
subscription-name :unlimited
subscription-is-trial false
go-to-subscription
(mf/use-fn #(st/emit! (rt/nav :settings-subscription)))]
(mf/defc subscription-sidebar*
[{:keys [profile]}]
(let [subscription (:subscription (:props profile))
subscription-name (if subscription
(:type subscription)
"professional")
subscription-is-trial (= (:status subscription) "trialing")
subscription-href (dm/str (u/join cf/public-uri "#/settings/subscriptions"))]
(case subscription-name
:professional
"professional"
[:> cta-power-up*
{:top-title (tr "subscription.dashboard.power-up.professional.top-title")
:top-description (tr "dashboard.upgrade-plan.no-limits")
:bottom-description (tr "subscription.dashboard.power-up.professional.bottom-description")
:cta-text (tr "dashboard.upgrade-plan.power-up")
:cta-link go-to-subscription
{:top-title (tr "subscription.dashboard.power-up.your-subscription")
:top-description (tr "subscription.dashboard.power-up.professional.top-title")
:bottom-description (tr "subscription.dashboard.power-up.professional.bottom-description", subscription-href)
:has-dropdown true}]
:unlimited
"unlimited"
(if subscription-is-trial
[:> cta-power-up*
{:top-title (tr "subscription.dashboard.power-up.trial.top-title")
:top-description (tr "subscription.dashboard.power-up.trial.top-description")
:bottom-description (tr "subscription.dashboard.power-up.trial.bottom-description")
:cta-text (tr "subscription.dashboard.power-up.subscribe")}]
{:top-title (tr "subscription.dashboard.power-up.your-subscription")
:top-description (tr "subscription.dashboard.power-up.trial.top-title")
:bottom-description (tr "subscription.dashboard.power-up.trial.bottom-description", subscription-href)
:has-dropdown true}]
[:> cta-power-up*
{:top-title (tr "subscription.dashboard.power-up.unlimited-plan")
:top-description (tr "subscription.dashboard.power-up.unlimited.top-description")
:bottom-description (tr "subscription.dashboard.power-up.unlimited.bottom-description")
:cta-text (tr "subscription.dashboard.power-up.unlimited.cta")
:cta-link go-to-subscription
{:top-title (tr "subscription.dashboard.power-up.your-subscription")
:top-description (tr "subscription.dashboard.power-up.unlimited-plan")
:bottom-description (tr "subscription.dashboard.power-up.unlimited.bottom-description", subscription-href)
:has-dropdown true}])
:enterprise
"enterprise"
[:> cta-power-up*
{:top-title (tr "subscription.dashboard.power-up.enterprise-plan")
:top-description (tr "subscription.dashboard.power-up.enterprise.description")
{:top-title (tr "subscription.dashboard.power-up.your-subscription")
:top-description (tr "subscription.dashboard.power-up.enterprise-plan")
:has-dropdown false}])))
(mf/defc team*
[{:keys [is-owner team]}]
(let [subscription (:subscription team)
subscription-name (:type subscription)
subscription-is-trial (= "trialing" (:status subscription))
go-to-manage-subscription
(mf/use-fn
(fn []
;; TODO add event tracking
(let [href (-> (rt/get-current-href)
(rt/encode-url))
href (str "payments/subscriptions/show?returnUrl=" href)]
(st/emit! (rt/nav-raw :href href)))))]
[:div {:class (stl/css :team)}
[:div {:class (stl/css :team-label)}
(tr "subscription.dashboard.team-plan")]
[:span {:class (stl/css :team-text)}
(case subscription-name
"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"))
[:button {:class (stl/css :manage-subscription-link)
:on-click go-to-manage-subscription}
(tr "subscription.settings.manage-your-subscription")])]))
(mf/defc menu-team-icon*
[{:keys [subscription-name]}]
[:span {:class (stl/css :subscription-icon)}
(case subscription-name
"unlimited" i/character-u
"enterprise" i/character-e)])
(mf/defc main-menu-power-up*
[{:keys [close-sub-menu]}]
(let [go-to-subscription (mf/use-fn #(st/emit! (rt/nav :settings-subscription)))]
[:> dropdown-menu-item* {:class (stl/css-case :menu-item true)
:on-click go-to-subscription
:on-key-down (fn [event]
(when (kbd/enter? event)
(go-to-subscription)))
:on-pointer-enter close-sub-menu
:id "file-menu-power-up"}
[:span {:class (stl/css :item-name)} (tr "subscription.workspace.header.menu.option.power-up")]]))
(mf/defc members-cta*
[{:keys [banner-is-expanded team profile]}]
(let [subscription (:subscription team)
subscription-name (:type subscription)
subscription-is-trial (= "trialing" (:status subscription))
is-owner (:is-owner (:permissions team))
email-owner (:email (some #(when (:is-admin %) %) (:members team)))
mail-to-owner (str "<a href=\"" "mailto:" email-owner "\">" email-owner "</a>")
go-to-subscription (dm/str (u/join cf/public-uri "#/settings/subscriptions"))
link
(if is-owner
go-to-subscription
mail-to-owner)
cta-title
(cond
(= "professional" subscription-name)
(tr "subscription.dashboard.cta.professional-plan-designed")
subscription-is-trial
(tr "subscription.dashboard.cta.trial-plan-designed")
(= "unlimited" subscription-name)
(tr "subscription.dashboard.cta.unlimited-many-editors" (:quantity (:subscription (:props profile)))))
cta-message
(cond
(and (= "professional" subscription-name) is-owner)
(tr "subscription.dashboard.cta.upgrade-to-unlimited-enterprise-owner" link)
(and (= "professional" subscription-name) (not is-owner))
(tr "subscription.dashboard.cta.upgrade-to-unlimited-enterprise-member" link)
(and subscription-is-trial is-owner)
(tr "subscription.dashboard.cta.upgrade-to-full-access-owner" link)
(and subscription-is-trial (not is-owner))
(tr "subscription.dashboard.cta.upgrade-to-full-access-member" link)
(and (= "unlimited" subscription-name) (not subscription-is-trial))
(tr "subscription.dashboard.cta.upgrade-to-unlimited-enterprise-owner-more-seats" link))]
[:> cta* {:class (stl/css-case ::members-cta-full-width banner-is-expanded :members-cta (not banner-is-expanded)) :title cta-title}
[:> i18n/tr-html*
{:tag-name "span"
:class (stl/css :cta-message)
:content cta-message}]]))
(defn show-subscription-members-main-banner?
[team profile]
(or
(and (= (:type (:subscription team)) "professional") (>= (count (:members team)) 8))
(and
(= (:type (:subscription team)) "unlimited")
(not (= (:status (:subscription team)) "trialing"))
(>= (count (:members team)) (:quantity (:subscription (:props profile))))
(:is-owner (:permissions team)))
(= (:status (:subscription team)) "paused")))
(defn show-subscription-invitations-main-banner?
[team]
(or
(and (= (:type (:subscription team)) "professional")
(>= (count (:members team)) 8))
(= (:status (:subscription team)) "paused")))

View file

@ -2,6 +2,7 @@
@use "common/refactor/common-dashboard";
@use "../ds/typography.scss" as t;
@use "../ds/_borders.scss" as *;
@use "../ds/spacing.scss" as *;
.cta-power-up {
display: flex;
@ -27,7 +28,7 @@
.icon-dropdown {
@include flexCenter;
height: 100%;
width: $s-16;
width: var(--sp-l);
}
.icon-dropdown svg {
@ -37,42 +38,114 @@
}
.cta-bottom-section {
border-block-start: $s-1 solid var(--color-background-quaternary);
display: grid;
border-block-start: $b-1 solid var(--color-background-quaternary);
color: var(--color-foreground-secondary);
grid-template-columns: 1fr auto;
margin-block-start: $s-12;
padding-block-start: $s-12;
margin-block-start: var(--sp-m);
padding-block-start: var(--sp-m);
}
.cta-bottom-section .content {
@include t.use-typography("body-small");
@include buttonStyle;
color: var(--color-foreground-secondary);
display: inline;
display: inline-block;
text-align: left;
}
.cta-text,
.cta-text-m,
.cta-title {
text-align: left;
}
.cta-title {
margin-block-end: var(--sp-xs);
}
.cta-text {
@include t.use-typography("body-small");
}
.cta-title {
.cta-text-m {
@include t.use-typography("body-medium");
}
.cta-bottom-section .content strong,
.cta-highlight {
.cta-bottom-section .content a {
@include t.use-typography("body-small");
color: var(--color-accent-tertiary);
margin-inline-start: var(--sp-xs);
}
.cta-link {
@include buttonStyle;
align-self: end;
margin-inline-start: $s-4;
margin-inline-start: var(--sp-xs);
}
.team {
display: grid;
grid-auto-rows: min-content;
gap: var(--sp-s);
max-width: $s-1000;
width: 100%;
}
.team-label {
@include t.use-typography("headline-small");
color: var(--title-foreground-color);
}
.team-text {
@include t.use-typography("body-large");
color: var(--color-foreground-primary);
}
.manage-subscription-link {
@include buttonStyle;
@include t.use-typography("body-small");
color: var(--color-accent-tertiary);
display: flex;
padding: 0;
}
.subscription-icon {
@extend .button-icon;
background: var(--color-background-primary);
stroke: var(--color-foreground-secondary);
border-radius: var(--sp-xs);
border: $b-1 solid var(--color-foreground-secondary);
}
.menu-item {
@extend .menu-item-base;
cursor: pointer;
&:hover {
color: var(--menu-foreground-color-hover);
.open-arrow {
svg {
stroke: var(--menu-foreground-color-hover);
}
}
}
}
.members-cta {
height: fit-content;
margin-block-start: var(--sp-s);
margin-inline-start: $s-68;
max-width: $s-200;
}
.members-cta-full-width {
max-width: $s-1000;
}
.cta-message {
@include t.use-typography("body-small");
color: var(--color-foreground-secondary);
a {
color: var(--color-accent-primary);
}
}

View file

@ -22,6 +22,10 @@
[app.main.ui.components.file-uploader :refer [file-uploader]]
[app.main.ui.components.forms :as fm]
[app.main.ui.dashboard.change-owner]
[app.main.ui.dashboard.subscription :refer [team*
members-cta*
show-subscription-members-main-banner?
show-subscription-invitations-main-banner?]]
[app.main.ui.dashboard.team-form]
[app.main.ui.ds.foundations.assets.icon :refer [icon*]]
[app.main.ui.icons :as i]
@ -537,10 +541,22 @@
[:*
[:& header {:section :dashboard-team-members :team team}]
[:section {:class (stl/css :dashboard-container :dashboard-team-members)}
[:section {:class (stl/css-case
:dashboard-container true
:dashboard-team-members true
:dashboard-top-cta (show-subscription-members-main-banner? team profile))}
(when (and (contains? cfg/flags :subscriptions)
(show-subscription-members-main-banner? team profile))
[:> members-cta* {:banner-is-expanded true :team team :profile profile}])
[:> team-members*
{:profile profile
:team team}]]])
:team team}]
(when (and
(contains? cfg/flags :subscriptions)
(or
(and (= (:type (:subscription team)) "professional") (< (count (:members team)) 8))
(= (:status (:subscription team)) "trialing")))
[:> members-cta* {:banner-is-expanded false :team team}])]])
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; INVITATIONS SECTION
@ -803,8 +819,18 @@
[:*
[:& header {:section :dashboard-team-invitations
:team team}]
[:section {:class (stl/css :dashboard-team-invitations)}
[:> invitation-section* {:team team}]]])
[:section {:class (stl/css-case
:dashboard-team-invitations true
:dashboard-top-cta (show-subscription-invitations-main-banner? team))}
(when (and (contains? cfg/flags :subscriptions)
(show-subscription-invitations-main-banner? team))
[:> members-cta* {:banner-is-expanded true :team team}])
[:> invitation-section* {:team team}]
(when (and (contains? cfg/flags :subscriptions)
(or
(and (= (:type (:subscription team)) "professional") (< (count (:members team)) 8))
(= (:status (:subscription team)) "trialing")))
[:> members-cta* {:banner-is-expanded false :team team}])]])
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; WEBHOOKS SECTION
@ -1159,5 +1185,8 @@
[:div {:class (stl/css :block-content)}
document-icon
[:span {:class (stl/css :block-text)}
(tr "labels.num-of-files" (i18n/c (:files stats)))]]]]]))
(tr "labels.num-of-files" (i18n/c (:files stats)))]]]
(when (contains? cfg/flags :subscriptions)
[:> team* {:is-owner (:is-owner permissions) :team team}])]]))

View file

@ -9,13 +9,14 @@
// Dashboard team settings
.dashboard-team-settings {
display: grid;
grid-template-rows: auto auto 1fr;
justify-items: center;
display: flex;
flex-direction: column;
align-items: center;
gap: $s-24;
width: 100%;
border-top: $s-1 solid var(--panel-border-color);
overflow-y: auto;
padding-inline: $s-24;
}
.block {
@ -23,7 +24,7 @@
grid-auto-rows: min-content;
gap: $s-8;
max-width: $s-1000;
width: $s-1000;
width: 100%;
}
.info-block {
@ -105,22 +106,28 @@
// TEAM MEMBERS PAGE
.dashboard-team-members {
display: grid;
justify-items: center;
display: flex;
justify-content: center;
width: 100%;
height: 100%;
padding-top: $s-20;
padding-inline-start: $s-20;
padding-block-start: $s-20;
border-top: $s-1 solid var(--panel-border-color);
overflow-y: auto;
scrollbar-gutter: stable;
}
.dashboard-team-members.dashboard-top-cta {
flex-direction: column;
justify-content: flex-start;
}
.team-members {
display: grid;
grid-template-rows: auto 1fr;
height: fit-content;
max-width: $s-1000;
width: $s-1000;
width: 100%;
}
.table-header {
@ -275,22 +282,28 @@
// TEAM INVITATION PAGE
.dashboard-team-invitations {
display: grid;
justify-items: center;
display: flex;
justify-content: center;
width: 100%;
height: 100%;
padding-top: $s-20;
padding-inline-start: $s-20;
padding-block-start: $s-20;
border-top: $s-1 solid var(--panel-border-color);
overflow-y: auto;
scrollbar-gutter: stable;
}
.dashboard-team-invitations .dashboard-top-cta {
flex-direction: flex;
justify-content: flex-start;
}
.invitations {
display: grid;
grid-template-rows: auto 1fr;
height: fit-content;
max-width: $s-1000;
width: $s-1000;
width: 100%;
}
.table-row-invitations {

View file

@ -19,6 +19,7 @@
(def ^:svg-id loader "loader")
(def ^:svg-id logo-error-screen "logo-error-screen")
(def ^:svg-id login-illustration "login-illustration")
(def ^:svg-id logo-subscription "logo-subscription")
(def ^:svg-id marketing-arrows "marketing-arrows")
(def ^:svg-id marketing-exchange "marketing-exchange")
(def ^:svg-id marketing-file "marketing-file")

View file

@ -18,6 +18,7 @@
(def ^:icon logo-icon (icon-xref :penpot-logo-icon))
(def ^:icon logo-error-screen (icon-xref :logo-error-screen))
(def ^:icon login-illustration (icon-xref :login-illustration))
(def ^:icon logo-subscription (icon-xref :logo-subscription))
(def ^:icon brand-openid (icon-xref :brand-openid))
(def ^:icon brand-github (icon-xref :brand-github))

View file

@ -69,7 +69,7 @@
[:& options-page]
:settings-subscription
[:> subscription-page*]
[:> subscription-page* {:profile profile}]
:settings-access-tokens
[:& access-tokens-page]

View file

@ -1,16 +1,24 @@
(ns app.main.ui.settings.subscription
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.main.data.event :as ev]
[app.main.data.modal :as modal]
[app.main.refs :as refs]
[app.main.repo :as rp]
[app.main.router :as rt]
[app.main.store :as st]
[app.main.ui.icons :as i]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[app.util.time :as dt]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]
[rumext.v2 :as mf]))
(mf/defc plan-card*
{::mf/props :obj}
[{:keys [card-title card-title-icon price-value price-period benefits-title benefits cta-text cta-link]}]
[{:keys [card-title card-title-icon price-value price-period benefits-title benefits cta-text cta-link cta-text-trial cta-link-trial cta-text-with-icon cta-link-with-icon]}]
[:div {:class (stl/css :plan-card)}
[:div {:class (stl/css :plan-card-header)}
[:div {:class (stl/css :plan-card-title-container)}
@ -23,25 +31,195 @@
(when benefits-title [:h5 {:class (stl/css :benefits-title)} benefits-title])
[:ul {:class (stl/css :benefits-list)}
(for [benefit benefits]
[:li {:key (str benefit) :class (stl/css :benefit)} "- " benefit])]
(when (and cta-link cta-text) [:a {:class (stl/css :cta-button)
:href cta-link} cta-text])])
[:li {:key (dm/str benefit) :class (stl/css :benefit)} "- " benefit])]
(when (and cta-link-with-icon cta-text-with-icon) [:button {:class (stl/css :cta-button :more-info)
:on-click cta-link-with-icon} cta-text-with-icon i/open-link])
(when (and cta-link cta-text) [:button {:class (stl/css-case :cta-button true
:bottom-link (not (and cta-link-trial cta-text-trial)))
: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])])
(mf/defc subscribe-management-dialog
{::mf/register modal/components
::mf/register-as :management-dialog}
[{:keys [subscription-name teams subscribe-to-trial]}]
(let [min-members* (mf/use-state (or (some->> teams (map :total-members) (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 "unlimited"
(mf/use-fn
(mf/deps min-members)
(fn []
;; TODO add event tracking subscribe trial unlimited
(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)))))
(mf/use-fn
(fn []
;; TODO add event tracking subscribe trial 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-callback
(fn []
;; TODO add event subscribe to another 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)))
(modal/hide!)))
handle-close-dialog (mf/use-callback
(fn []
;; TODO add event tracking close modal/cancel subscription
(modal/hide!)))]
[:div {:class (stl/css :modal-overlay)}
[: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)]
[:div {:class (stl/css :modal-content)}
(if (seq teams)
[* [: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)]
[:li {:key (dm/str (:id team)) :class (stl/css :team-name)}
(:name team) (tr "subscription.settings.management.dialog.members" (:total-members 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
:on-change #(let [new-value (js/parseInt (.. % -target -value))]
(reset! min-members* (if (or (js/isNaN new-value) (zero? new-value)) 1 (max 1 new-value))))}]]
[: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"))
(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}]
[: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)}]]]]]]))
(mf/defc subscription-success-dialog
{::mf/register modal/components
::mf/register-as :subscription-success}
[{:keys [subscription-name]}]
(let [handle-close-dialog (mf/use-callback
(fn []
;; TODO add event tracking close modal
(modal/hide!)))]
[:div {:class (stl/css :modal-overlay)}
[:div {:class (stl/css :modal-dialog :subscription-success)}
[:button {:class (stl/css :close-btn) :on-click handle-close-dialog} i/close]
[:div {:class (stl/css :modal-success-content)}
[:div {:class (stl/css :modal-start)}
i/logo-subscription]
[:div {:class (stl/css :modal-end)}
[:div {:class (stl/css :modal-title)} (tr "subscription.settings.sucess.dialog.title" subscription-name)]
[:p {:class (stl/css :modal-text-large)} (tr "subscription.settings.success.dialog.description")]
[:p {:class (stl/css :modal-text-large)} (tr "subscription.settings.sucess.dialog.footer")]
[:div {:class (stl/css :success-action-buttons)}
[:input
{:class (stl/css :primary-button)
:type "button"
:value (tr "labels.close")
:on-click handle-close-dialog}]]]]]]))
(mf/defc subscription-page*
[]
(let [;; TODO subscription cases professional/unlimited/enterprise
subscription-name :unlimited
subscription-is-trial false
locale (mf/deref i18n/locale)
profile (mf/deref refs/profile)
penpot-member (dt/format-date-locale-short (:created-at profile) {:locale locale})
;; TODO get subscription member date
subscription-member "January 17, 2024"
;; TODO update url to penpot payments
go-to-payments "https://penpot.app/pricing"]
[{:keys [profile]}]
(let [route (mf/deref refs/route)
params (:params route)
show-subscription-success-modal (and (:query params)
(or (= (:subscription (:query params)) "subscribed-to-penpot-unlimited")
(= (:subscription (:query params)) "subscribed-to-penpot-enterprise")))
subscription (:subscription (:props profile))
subscription-name (if subscription
(:type subscription)
"professional")
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 []
;; TODO add event tracking manage subscription in stripe
(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-name]
;; TODO add event tracking open modal to try trial
(st/emit!
(modal/show :management-dialog
{:subscription-name subscription-name
:teams teams :subscribe-to-trial (not subscription)}))))]
(mf/with-effect []
(->> (rp/cmd! :get-owned-teams)
(rx/subs! (fn [teams]
(reset! teams* teams)))))
(mf/with-effect []
(dom/set-html-title (tr "subscription.labels")))
(when show-subscription-success-modal
;; add name subscription from params
(st/emit! (modal/show :subscription-success
{:subscription-name (if (= (:subscription (:query params)) "subscribed-to-penpot-unlimited")
(tr "subscription.settings.unlimited-trial-modal")
(tr "subscription.settings.enterprise-trial-modal"))})))
[:section {:class (stl/css :dashboard-section)}
[:div {:class (stl/css :dashboard-content)}
[:h2 {:class (stl/css :title-section)} (tr "subscription.labels")]
@ -50,13 +228,13 @@
[:div {:class (stl/css :your-subscription)}
[:h3 {:class (stl/css :plan-section-title)} (tr "subscription.settings.section-plan")]
(case subscription-name
:professional
"professional"
[:> plan-card* {:card-title (tr "subscription.settings.professional")
:benefits [(tr "subscription.settings.professional.projects-files"),
(tr "subscription.settings.professional.teams-editors"),
(tr "subscription.settings.professional.storage")]}]
:unlimited
"unlimited"
(if subscription-is-trial
[:> plan-card* {:card-title (tr "subscription.settings.unlimited-trial")
:card-title-icon i/character-u
@ -65,7 +243,9 @@
(tr "subscription.settings.unlimited.bill"),
(tr "subscription.settings.unlimited.storage")]
:cta-text (tr "subscription.settings.manage-your-subscription")
:cta-link go-to-payments}]
:cta-link go-to-payments
:cta-text-trial (tr "subscription.settings.add-payment-to-continue")
:cta-link-trial go-to-payments}]
[:> plan-card* {:card-title (tr "subscription.settings.unlimited")
:card-title-icon i/character-u
@ -76,10 +256,10 @@
:cta-text (tr "subscription.settings.manage-your-subscription")
:cta-link go-to-payments}])
:enterprise
"enterprise"
[:> plan-card* {:card-title (tr "subscription.settings.enterprise")
:card-title-icon i/character-e
:benefits-title (tr "subscription.settings.benefits.all-professiona-benefits")
:benefits-title (tr "subscription.settings.benefits.all-professional-benefits")
:benefits [(tr "subscription.settings.enterprise.support"),
(tr "subscription.settings.enterprise.security"),
(tr "subscription.settings.enterprise.logs")]
@ -97,36 +277,42 @@
[: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-name "professional")
[:> plan-card* {:card-title (tr "subscription.settings.professional")
:price-value "$0"
:price-period (tr "subscription.settings.price-editor-month")
:benefits [(tr "subscription.settings.professional.projects-files"),
(tr "subscription.settings.professional.teams-editors"),
(tr "subscription.settings.professional.storage")]
:cta-text (tr "subscription.dashboard.power-up.subscribe")
:cta-link go-to-payments}])
:cta-text (tr "subscription.settings.subscribe")
:cta-link #(open-subscription-modal "professional")
: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-name "unlimited")
[:> plan-card* {:card-title (tr "subscription.settings.unlimited")
:card-title-icon i/character-u
:price-value "$7"
:price-period (tr "subscription.settings.price-editor-month")
:benefits-title (tr "subscription.settings.benefits.all-professiona-benefits")
:benefits-title (tr "subscription.settings.benefits.all-professional-benefits")
:benefits [(tr "subscription.settings.unlimited.teams"),
(tr "subscription.settings.unlimited.bill"),
(tr "subscription.settings.unlimited.storage")]
:cta-text (tr "subscription.settings.ulimited.try-it-free")
:cta-link go-to-payments}])
:cta-text (if subscription (tr "subscription.settings.subscribe") (tr "subscription.settings.try-it-free"))
:cta-link #(open-subscription-modal "unlimited")
: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-name "enterprise")
[:> plan-card* {:card-title (tr "subscription.settings.enterprise")
:card-title-icon i/character-e
:price-value "$950"
:price-period (tr "subscription.settings.price-organization-month")
:benefits-title (tr "subscription.settings.benefits.all-professiona-benefits")
:benefits-title (tr "subscription.settings.benefits.all-professional-benefits")
:benefits [(tr "subscription.settings.enterprise.support"),
(tr "subscription.settings.enterprise.security"),
(tr "subscription.settings.enterprise.logs")]
:cta-text (tr "subscription.dashboard.power-up.subscribe")
:cta-link go-to-payments}])]]]))
:cta-text (if subscription (tr "subscription.settings.subscribe") (tr "subscription.settings.try-it-free"))
:cta-link #(open-subscription-modal "enterprise")
:cta-text-with-icon (tr "subscription.settings.more-information")
:cta-link-with-icon go-to-pricing-page}])]]]))

View file

@ -6,6 +6,8 @@
@use "common/refactor/common-refactor.scss" as *;
@use "../ds/typography.scss" as t;
@use "../ds/_borders.scss" as *;
@use "../ds/spacing.scss" as *;
.dashboard-section {
display: flex;
@ -19,30 +21,30 @@
justify-content: center;
flex-direction: column;
max-width: $s-500;
margin-bottom: $s-32;
margin-block-end: var(--sp-xxxl);
width: $s-580;
margin: $s-92 auto $s-120 auto;
justify-content: center;
}
.membership-container {
margin-block-start: $s-16;
margin-block-start: var(--sp-l);
}
.membership {
align-items: center;
display: flex;
margin-block-start: $s-8;
margin-block-start: var(--sp-s);
}
.membership.first {
margin-block-start: $s-16;
margin-block-start: var(--sp-l);
}
.membership-date {
@include t.use-typography("body-small");
color: var(--color-foreground-secondary);
margin-inline-start: $s-8;
margin-inline-start: var(--sp-s);
}
.subscription-member,
@ -61,7 +63,7 @@
.title-section {
@include t.use-typography("title-large");
color: var(--color-foreground-primary);
margin-block-end: $s-16;
margin-block-end: var(--sp-l);
}
.plan-section-title {
@ -70,29 +72,32 @@
}
.plan-card {
border: $s-1 solid var(--color-foreground-secondary);
border-radius: $s-8;
margin-block-start: $s-16;
padding: $s-16;
border: $b-1 solid var(--color-foreground-secondary);
border-radius: var(--sp-s);
margin-block-start: var(--sp-l);
padding: var(--sp-l);
}
.plan-card-header {
display: flex;
justify-content: space-between;
margin-block-end: $s-8;
margin-block-end: var(--sp-s);
}
.plan-card-title-container {
display: flex;
align-items: center;
gap: $s-8;
gap: var(--sp-s);
}
.plan-title-icon {
@extend .button-icon;
stroke: var(--color-foreground-primary);
border-radius: $s-4;
border: $s-1 solid var(--color-foreground-primary);
height: var(--sp-xl);
width: var(--sp-xl);
border-radius: var(--sp-xs);
border: $b-1 solid var(--color-foreground-primary);
padding: $s-1;
}
.plan-card-title,
@ -122,5 +127,134 @@
.cta-button {
@include t.use-typography("body-small");
@include buttonStyle;
color: var(--color-accent-tertiary);
display: flex;
margin-block-start: var(--sp-m);
}
.cta-button svg {
@extend .button-icon;
height: var(--sp-l);
width: var(--sp-l);
stroke: var(--color-accent-tertiary);
margin-inline-start: var(--sp-xs);
}
.bottom-link {
margin-block-start: var(--sp-xs);
}
.modal-overlay {
@extend .modal-overlay-base;
}
.modal-dialog {
@extend .modal-container-base;
display: grid;
grid-template-rows: auto 1fr auto;
max-height: initial;
min-width: $s-520;
}
.modal-dialog.subscription-success {
min-width: $s-612;
}
.close-btn {
@extend .modal-close-btn-base;
}
.modal-title {
@include t.use-typography("title-large");
margin-block-end: var(--sp-xxxl);
color: var(--modal-title-foreground-color);
display: flex;
gap: var(--sp-m);
}
.subscription-title {
margin-block-end: var(--sp-l);
}
.modal-text-lage {
@include t.use-typography("body-large");
}
.modal-text-small {
@include t.use-typography("body-small");
}
.modal-content,
.modal-end {
color: var(--color-foreground-secondary);
display: flex;
flex-direction: column;
}
.modal-success-content {
display: flex;
gap: $s-40;
}
.modal-footer {
margin-block-start: $s-40;
}
.action-buttons {
@extend .modal-action-btns;
}
.success-action-buttons {
margin-block-start: var(--sp-l);
}
.primary-button {
@extend .modal-accept-btn;
}
.cancel-button {
@extend .modal-cancel-btn;
}
.modal-start {
display: flex;
justify-content: center;
max-width: $s-220;
svg {
width: 100%;
height: auto;
}
@media (max-width: 992px) {
display: none;
}
}
.teams-list {
list-style-position: inside;
list-style-type: disc;
margin-inline-start: var(--sp-xl);
margin-block: var(--sp-xxl);
}
.input-wrapper {
@extend .input-element;
width: $s-80;
}
.editors-label {
margin-block-start: var(--sp-xxl);
}
.editors-wrapper {
display: flex;
gap: var(--sp-xl);
margin-block-start: var(--sp-l);
}
.editors-cost {
display: flex;
flex-direction: column;
}

View file

@ -30,6 +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.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.hooks.resize :as r]
[app.main.ui.icons :as i]
@ -819,7 +820,12 @@
(reset! sub-menu* nil)
(st/emit!
(ptk/event ::ev/event {::ev/name "open-plugins-manager" ::ev/origin "workspace:menu"})
(modal/show :plugin-management {}))))]
(modal/show :plugin-management {}))))
subscription (:subscription (:props profile))
subscription-name (if subscription
(:type subscription)
"professional")]
(mf/with-effect []
(let [disposable (->> st/stream
@ -904,6 +910,10 @@
:id "file-menu-help-info"}
[: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))
[:> main-menu-power-up* {:close-sub-menu close-sub-menu}])
;; TODO remove this block when subscriptions is full implemented
(when (contains? cf/flags :subscriptions-old)
[:> dropdown-menu-item* {:class (stl/css-case :menu-item true)
@ -913,7 +923,7 @@
(on-power-up-click)))
:on-pointer-enter close-sub-menu
:id "file-menu-power-up"}
[:span {:class (stl/css :item-name)} (tr "workspace.header.menu.option.power-up")]])]
[:span {:class (stl/css :item-name)} (tr "subscription.workspace.header.menu.option.power-up")]])]
(case sub-menu
:file