From e22a03e7e859e53c2f203f752bf754c35b839844 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mar=C3=ADa=20Valderrama?= Date: Wed, 29 Apr 2026 12:42:25 +0200 Subject: [PATCH] :sparkles: Subscribe to nitrate with an activation code * :sparkles: Subscribe to nitrate with an activation code * :paperclip: Code review --- backend/src/app/nitrate.clj | 27 ++++- backend/src/app/rpc/commands/nitrate.clj | 36 ++++++ .../ui/ds/foundations/assets/raw_svg.cljs | 1 + .../nitrate_activation_success_modal.cljs | 67 +++++++++++ .../nitrate_activation_success_modal.scss | 79 +++++++++++++ .../nitrate_code_activation_modal.cljs | 98 ++++++++++++++++ .../nitrate_code_activation_modal.scss | 107 ++++++++++++++++++ .../src/app/main/ui/nitrate/nitrate_form.cljs | 41 +++++-- .../src/app/main/ui/nitrate/nitrate_form.scss | 30 ++--- .../app/main/ui/settings/subscription.cljs | 58 +++------- frontend/translations/en.po | 97 ++++++++++++++++ frontend/translations/es.po | 100 ++++++++++++++++ 12 files changed, 671 insertions(+), 70 deletions(-) create mode 100644 frontend/src/app/main/ui/nitrate/nitrate_activation_success_modal.cljs create mode 100644 frontend/src/app/main/ui/nitrate/nitrate_activation_success_modal.scss create mode 100644 frontend/src/app/main/ui/nitrate/nitrate_code_activation_modal.cljs create mode 100644 frontend/src/app/main/ui/nitrate/nitrate_code_activation_modal.scss diff --git a/backend/src/app/nitrate.clj b/backend/src/app/nitrate.clj index b9f0949394..28f2cf1eb7 100644 --- a/backend/src/app/nitrate.clj +++ b/backend/src/app/nitrate.clj @@ -55,7 +55,7 @@ result))))) -(defn- with-validate [handler uri schema] +(defn- with-validate [handler uri schema & {:keys [throw-on-error?]}] (fn [] (let [response (handler) status (:status response)] @@ -73,7 +73,11 @@ :uri uri :status status :body (:body response))) - nil) + (if throw-on-error? + (ex/raise :type :nitrate-http-error + :status status + :hint (str "nitrate HTTP " status " at " uri)) + nil)) (= status 204) ;; 204 doesn't return any body nil :else ;; For success status codes, validate the response @@ -89,11 +93,11 @@ nil))))))) (defn- request-to-nitrate - [cfg method uri schema {:keys [::rpc/profile-id request-params] :as params}] + [cfg method uri schema {:keys [::rpc/profile-id request-params throw-on-error?] :as params}] (let [shared-key (-> cfg ::setup/shared-keys :nitrate) full-http-call (-> (request-builder cfg method uri shared-key profile-id request-params) (with-retries 3) - (with-validate uri schema))] + (with-validate uri schema :throw-on-error? throw-on-error?))] (full-http-call))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -340,6 +344,18 @@ "/api/connectivity") schema:connectivity params))) +(def ^:private schema:redeem-result + [:map + [:cancel-at [:maybe schema:timestamp]]]) + +(defn- redeem-activation-code-api + [cfg params] + (let [baseuri (cf/get :nitrate-backend-uri)] + (request-to-nitrate cfg :post + (str baseuri "/api/activation-codes/redeem") + schema:redeem-result + (assoc params :throw-on-error? true)))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; INITIALIZATION ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -359,7 +375,8 @@ :delete-team (partial delete-team-api cfg) :remove-team-from-org (partial remove-team-from-org-api cfg) :get-subscription (partial get-subscription-api cfg) - :connectivity (partial get-connectivity-api cfg)})) + :connectivity (partial get-connectivity-api cfg) + :redeem-activation-code (partial redeem-activation-code-api cfg)})) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; UTILS diff --git a/backend/src/app/rpc/commands/nitrate.clj b/backend/src/app/rpc/commands/nitrate.clj index 2f88361693..91f58f6448 100644 --- a/backend/src/app/rpc/commands/nitrate.clj +++ b/backend/src/app/rpc/commands/nitrate.clj @@ -11,6 +11,7 @@ [app.common.data :as d] [app.common.exceptions :as ex] [app.common.schema :as sm] + [app.common.time :as ct] [app.db :as db] [app.nitrate :as nitrate] [app.rpc :as-alias rpc] @@ -56,6 +57,41 @@ [cfg _params] (nitrate/call cfg :connectivity {})) +(def ^:private schema:redeem-activation-code-params + [:map {:title "RedeemActivationCodeParams"} + [:activation-code ::sm/text]]) + +(def ^:private schema:redeem-activation-code-result + [:map {:title "RedeemActivationCodeResult"} + [:cancel-at [:maybe ct/schema:inst]]]) + +(sv/defmethod ::redeem-nitrate-activation-code + {::rpc/auth true + ::doc/added "2.14" + ::sm/params schema:redeem-activation-code-params + ::sm/result schema:redeem-activation-code-result} + [cfg {:keys [::rpc/profile-id activation-code]}] + (let [profile (db/get cfg :profile {:id profile-id})] + (try + (let [result (nitrate/call cfg :redeem-activation-code + {:request-params {:code activation-code + :penpot-id profile-id + :email (:email profile)}})] + (when-not result + (ex/raise :type :validation + :code :invalid-activation-code + :hint "The activation code is invalid, expired or fully redeemed")) + result) + (catch Exception cause + (let [{:keys [type status]} (ex-data cause)] + (if (= type :nitrate-http-error) + (ex/raise :type :validation + :code (case status + 410 :expired-activation-code + :invalid-activation-code) + :cause cause) + (throw cause))))))) + (def ^:private sql:prefix-team-name-and-unset-default "UPDATE team SET name = ? || name, diff --git a/frontend/src/app/main/ui/ds/foundations/assets/raw_svg.cljs b/frontend/src/app/main/ui/ds/foundations/assets/raw_svg.cljs index e744cfe292..56f5b076a5 100644 --- a/frontend/src/app/main/ui/ds/foundations/assets/raw_svg.cljs +++ b/frontend/src/app/main/ui/ds/foundations/assets/raw_svg.cljs @@ -39,3 +39,4 @@ (assert (contains? raw-svg-list id) "invalid raw svg id") [:> "svg" props [:use {:href (dm/str "#asset-" id)}]]) + diff --git a/frontend/src/app/main/ui/nitrate/nitrate_activation_success_modal.cljs b/frontend/src/app/main/ui/nitrate/nitrate_activation_success_modal.cljs new file mode 100644 index 0000000000..0f68a5b5f7 --- /dev/null +++ b/frontend/src/app/main/ui/nitrate/nitrate_activation_success_modal.cljs @@ -0,0 +1,67 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.nitrate.nitrate-activation-success-modal + (:require-macros [app.main.style :as stl]) + (:require + [app.common.data.macros :as dm] + [app.common.time :as ct] + [app.main.data.modal :as modal] + [app.main.data.nitrate :as dnt] + [app.main.refs :as refs] + [app.main.ui.ds.buttons.button :refer [button*]] + [app.main.ui.ds.foundations.assets.icon :refer [icon*]] + [app.main.ui.ds.foundations.assets.raw-svg :refer [raw-svg*]] + [app.util.i18n :refer [tr]] + [rumext.v2 :as mf])) + +(mf/defc nitrate-activation-success-modal* + {::mf/register modal/components + ::mf/register-as :nitrate-activation-success + ::mf/wrap-props true} + [props] + + (let [profile (mf/deref refs/profile) + light? (= "light" (:theme profile)) + svg-id (if light? "logo-subscription-light" "logo-subscription") + + cancel-at (dm/get-in props [:subscription :cancel-at]) + date-str (when cancel-at + (ct/format-inst cancel-at "d MMMM, yyyy")) + + on-create-org + (mf/use-fn + (fn [] + (modal/hide!) + (dnt/go-to-nitrate-cc-create-org)))] + + [:div {:class (stl/css :modal-overlay)} + [:div {:class (stl/css :modal-dialog)} + [:button {:class (stl/css :close-btn) :on-click modal/hide!} + [:> icon* {:icon-id "close" + :size "m"}]] + + [:div {:class (stl/css :modal-content)} + [:div {:class (stl/css :modal-start)} + [:> raw-svg* {:id svg-id}]] + + [:div {:class (stl/css :modal-end)} + [:div {:class (stl/css :modal-title)} + (tr "nitrate.activation-success.title")] + + [:p {:class (stl/css :modal-text-primary)} + (tr "nitrate.activation-success.active-until" date-str)] + + [:p {:class (stl/css :modal-text)} + (tr "nitrate.activation-success.manage-info")] + + [:p {:class (stl/css :modal-text)} + (tr "nitrate.activation-success.enjoy")] + + [:> button* {:variant "primary" + :on-click on-create-org + :class (stl/css :modal-button)} + (tr "nitrate.activation-success.create-org")]]]]])) diff --git a/frontend/src/app/main/ui/nitrate/nitrate_activation_success_modal.scss b/frontend/src/app/main/ui/nitrate/nitrate_activation_success_modal.scss new file mode 100644 index 0000000000..5f1e8dd483 --- /dev/null +++ b/frontend/src/app/main/ui/nitrate/nitrate_activation_success_modal.scss @@ -0,0 +1,79 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "refactor/common-refactor.scss" as deprecated; +@use "ds/typography.scss" as t; +@use "ds/_borders.scss" as *; +@use "ds/spacing.scss" as *; +@use "ds/_sizes.scss" as *; +@use "ds/_utils.scss" as *; + +.modal-overlay { + @extend %modal-overlay-base; + + z-index: var(--z-index-notifications); +} + +.modal-dialog { + @extend %modal-container-base; + + max-block-size: initial; + min-inline-size: px2rem(608); + max-inline-size: px2rem(608); + padding: var(--sp-xxxl); +} + +.close-btn { + @extend %modal-close-btn-base; +} + +.modal-content { + display: flex; + gap: $sz-40; +} + +.modal-start { + display: flex; + justify-content: center; + min-inline-size: $sz-224; + + @media (width <= 640px) { + display: none; + } +} + +.modal-start svg { + inline-size: 100%; + block-size: auto; +} + +.modal-end { + color: var(--color-foreground-secondary); + display: flex; + flex-direction: column; + gap: var(--sp-m); +} + +.modal-title { + @include t.use-typography("title-large"); + + color: var(--modal-title-foreground-color); +} + +.modal-text-primary { + @include t.use-typography("body-large"); + + color: var(--color-foreground-primary); +} + +.modal-text { + @include t.use-typography("body-large"); +} + +.modal-button { + margin-block-start: var(--sp-s); + align-self: flex-start; +} diff --git a/frontend/src/app/main/ui/nitrate/nitrate_code_activation_modal.cljs b/frontend/src/app/main/ui/nitrate/nitrate_code_activation_modal.cljs new file mode 100644 index 0000000000..131dfc257c --- /dev/null +++ b/frontend/src/app/main/ui/nitrate/nitrate_code_activation_modal.cljs @@ -0,0 +1,98 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.nitrate.nitrate-code-activation-modal + (:require-macros [app.main.style :as stl]) + (:require + [app.main.data.modal :as modal] + [app.main.data.profile :as dprof] + [app.main.repo :as rp] + [app.main.store :as st] + [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] + [app.main.ui.ds.foundations.assets.icon :as i] + [app.main.ui.nitrate.nitrate-activation-success-modal] + [app.util.dom :as dom] + [app.util.i18n :refer [tr]] + [beicon.v2.core :as rx] + [cuerdas.core :as str] + [rumext.v2 :as mf])) + +(mf/defc nitrate-code-activation-modal* + {::mf/register modal/components + ::mf/register-as :nitrate-code-activation} + [_props] + (let [value* (mf/use-state "") + error* (mf/use-state nil) + + on-change + (mf/use-fn + (fn [event] + (reset! error* nil) + (reset! value* (dom/get-target-val event)))) + + on-accept + (mf/use-fn + (mf/deps value*) + (fn [_] + (let [code (str/trim @value*)] + (when (seq code) + (->> (rp/cmd! ::redeem-nitrate-activation-code {:activation-code code}) + (rx/subs! + (fn [result] + (modal/hide!) + (st/emit! + (modal/show {:type :nitrate-activation-success :subscription result}) + (dprof/refresh-profile))) + (fn [error] + ;; TODO: "Already used" is not yet detectable (CC upserts on reuse). + (let [code (-> error ex-data :code)] + (reset! error* (case code + :expired-activation-code (tr "nitrate.activation-code.expired-error") + (tr "nitrate.activation-code.invalid-error"))))))))))) + + on-key-down + (mf/use-fn + (mf/deps on-accept) + (fn [event] + (when (and (= "Enter" (.-key event)) (.-ctrlKey event)) + (on-accept event))))] + + [:div {:class (stl/css :modal-overlay)} + [:div {:class (stl/css :modal-dialog)} + [:> icon-button* {:variant "ghost" + :class (stl/css :close-btn) + :aria-label (tr "labels.close") + :on-click modal/hide! + :icon i/close}] + + [:div {:class (stl/css :modal-header)} + [:h2 {:class (stl/css :modal-title)} (tr "nitrate.code-activation.title")]] + + [:div {:class (stl/css :modal-content)} + [:div {:class (stl/css-case :code-field true :invalid (some? @error*))} + [:label {:class (stl/css :code-label)} + (tr "nitrate.code-activation.input-label")] + [:textarea {:class (stl/css :code-textarea) + :auto-focus true + :value @value* + :placeholder (tr "nitrate.code-activation.placeholder") + :on-change on-change + :on-key-down on-key-down}] + (when @error* + [:span {:class (stl/css :error-msg)} @error*])] + + [:input + {:type "button" + :class (stl/css-case :accept-btn true + :global/disabled (empty? (str/trim @value*))) + :disabled (empty? (str/trim @value*)) + :value (tr "nitrate.code-activation.submit") + :on-click on-accept}] + [:div {:class (stl/css :footer-text)} + (tr "nitrate.code-activation.footer") " " + [:a {:class (stl/css :link) + :href "mailto:sales@nitrate.com"} + "sales@nitrate.com"]]]]])) diff --git a/frontend/src/app/main/ui/nitrate/nitrate_code_activation_modal.scss b/frontend/src/app/main/ui/nitrate/nitrate_code_activation_modal.scss new file mode 100644 index 0000000000..d241c38332 --- /dev/null +++ b/frontend/src/app/main/ui/nitrate/nitrate_code_activation_modal.scss @@ -0,0 +1,107 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "refactor/common-refactor.scss" as deprecated; +@use "ds/typography.scss" as t; +@use "ds/spacing.scss" as *; +@use "ds/_sizes.scss" as *; +@use "ds/_borders.scss" as *; + +.close-btn { + @extend %modal-close-btn-base; +} + +.modal-overlay { + @extend %modal-overlay-base; + + z-index: var(--z-index-notifications); +} + +.modal-dialog { + @extend %modal-container-base; + + inline-size: $sz-480; + max-inline-size: $sz-480; + max-block-size: none; + max-height: none; + padding: var(--sp-xxxl); +} + +.modal-title { + @include t.use-typography("title-large"); + + color: var(--modal-title-foreground-color); + margin-block-end: var(--sp-xxxl); +} + +.modal-content { + display: flex; + flex-direction: column; + gap: var(--sp-m); + color: var(--color-foreground-secondary); +} + +.accept-btn { + @extend %modal-accept-btn; + + inline-size: 100%; +} + +.code-field { + display: flex; + flex-direction: column; + gap: var(--sp-xs); +} + +.code-label { + @include t.use-typography("body-medium"); + + color: var(--color-foreground-secondary); +} + +.code-textarea { + @include t.use-typography("body-medium"); + + block-size: $sz-200; + resize: vertical; + font-family: monospace; + word-break: break-all; + padding: var(--sp-s); + border-radius: $br-8; + border: $b-1 solid var(--input-border-color); + background-color: var(--input-background-color); + color: var(--color-foreground-primary); + outline: none; +} + +.code-textarea:focus { + border-color: var(--color-accent-primary); +} + +.invalid .code-textarea { + border-color: var(--input-border-color-error); +} + +.invalid .code-textarea:focus { + border-color: var(--input-border-color-error); +} + +.error-msg { + @include t.use-typography("body-small"); + + color: var(--element-foreground-error); +} + +.footer-text { + @include t.use-typography("body-medium"); + + color: var(--color-foreground-secondary); + margin-block-start: var(--sp-xxxl); +} + +.link { + color: var(--color-accent-primary); +} diff --git a/frontend/src/app/main/ui/nitrate/nitrate_form.cljs b/frontend/src/app/main/ui/nitrate/nitrate_form.cljs index ef3bc46d38..13143d5337 100644 --- a/frontend/src/app/main/ui/nitrate/nitrate_form.cljs +++ b/frontend/src/app/main/ui/nitrate/nitrate_form.cljs @@ -11,10 +11,13 @@ [app.main.data.modal :as modal] [app.main.data.nitrate :as dnt] [app.main.refs :as refs] + [app.main.store :as st] [app.main.ui.components.forms :as fm] [app.main.ui.ds.buttons.button :refer [button*]] [app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]] [app.main.ui.ds.foundations.assets.raw-svg :refer [raw-svg*]] + [app.main.ui.nitrate.nitrate-code-activation-modal] + [app.util.i18n :refer [tr]] [rumext.v2 :as mf])) (def ^:private schema:nitrate-form @@ -37,7 +40,12 @@ (mf/use-fn (mf/deps form) (fn [] - (dnt/go-to-buy-nitrate-license (-> @form :clean-data :subscription name))))] + (dnt/go-to-buy-nitrate-license (-> @form :clean-data :subscription name)))) + + on-activate-click + (mf/use-fn + (fn [] + (st/emit! (modal/show {:type :nitrate-code-activation}))))] [:div {:class (stl/css :modal-overlay)} [:div {:class (stl/css :modal-dialog :subscription-success)} @@ -51,7 +59,7 @@ [:div {:class (stl/css :modal-end)} [:div {:class (stl/css :modal-title)} - "Unlock Nitrate Features"] + (tr "nitrate.form.title")] [:p {:class (stl/css :modal-text-large)} "Prow scuttle parrel provost."] @@ -64,8 +72,8 @@ [:p {:class (stl/css :modal-text-large)} [:& fm/radio-buttons - {:options [{:label "Price Tag Montly" :value "monthly"} - {:label "Price Tag Yearly (Discount)" :value "yearly"}] + {:options [{:label (tr "nitrate.form.billing-monthly") :value "monthly"} + {:label (tr "nitrate.form.billing-yearly") :value "yearly"}] :name :subscription :class (stl/css :radio-btns)}]] @@ -75,23 +83,34 @@ :on-click on-click :class (stl/css :modal-button)} (if (:subscription profile) - "UPGRADE TO NITRATE" - "Try it free for 14 days")] + (tr "nitrate.form.upgrade") + (tr "nitrate.form.try-free"))] [:div {:class (stl/css :modal-text-small :modal-info)} - "Cancel anytime before your next billing cycle."]]] + (tr "nitrate.form.cancel-anytime")]]] + [:p {:class (stl/css :modal-text-medium)} + (tr "nitrate.form.have-code") " " [:a {:class (stl/css :link) + :on-click on-activate-click} + (tr "nitrate.form.enter-code")]] [:p {:class (stl/css :modal-text-medium)} [:a {:class (stl/css :link) :href dnt/go-to-subscription-url} - "See my current plan"]]] + (tr "nitrate.form.see-plan")]]] [:div {:class (stl/css :contact)} [:p {:class (stl/css :modal-text-large)} (if (:subscription profile) - "Contact us to upgrade to Nitrate:" - "Contact us to try Nitrate for 14 days:")] + (tr "nitrate.form.contact-upgrade") + (tr "nitrate.form.contact-trial"))] [:p {:class (stl/css :modal-text-large)} [:a {:class (stl/css :link) :href "mailto:sales@penpot.app"} - "sales@penpot.app"]]])]]]])) + "sales@penpot.app"]] + [:div {:class (stl/css :activation-code)} + [:p {:class (stl/css :modal-text-large)} + (tr "nitrate.form.have-code")] + [:p {:class (stl/css :modal-text-large)} + [:a {:class (stl/css :link) + :on-click on-activate-click} + (tr "nitrate.form.enter-code")]]]])]]]])) diff --git a/frontend/src/app/main/ui/nitrate/nitrate_form.scss b/frontend/src/app/main/ui/nitrate/nitrate_form.scss index d21a24c26c..76942a6f7a 100644 --- a/frontend/src/app/main/ui/nitrate/nitrate_form.scss +++ b/frontend/src/app/main/ui/nitrate/nitrate_form.scss @@ -77,36 +77,40 @@ justify-content: center; min-inline-size: $sz-284; - svg { - inline-size: 100%; - block-size: auto; - } - @media (width <= 992px) { display: none; } } +.modal-start svg { + inline-size: 100%; + block-size: auto; +} + .radio-btns { - label { - @include t.use-typography("body-large"); - - padding: 0; - display: flex; - align-items: center; - } - display: flex; flex-direction: column; padding: var(--sp-l) 0 0 0; gap: 0; } +.radio-btns label { + @include t.use-typography("body-large"); + + padding: 0; + display: flex; + align-items: center; +} + .contact { margin-block-start: $sz-96; color: var(--color-foreground-primary); } +.activation-code { + margin-block-start: var(--sp-xxxl); +} + .link { color: var(--color-accent-primary); } diff --git a/frontend/src/app/main/ui/settings/subscription.cljs b/frontend/src/app/main/ui/settings/subscription.cljs index 42d892f7d8..af7e9d9ddc 100644 --- a/frontend/src/app/main/ui/settings/subscription.cljs +++ b/frontend/src/app/main/ui/settings/subscription.cljs @@ -18,6 +18,7 @@ [app.main.ui.ds.buttons.button :refer [button*]] [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i] [app.main.ui.ds.foundations.assets.raw-svg :refer [raw-svg*]] + [app.main.ui.nitrate.nitrate-activation-success-modal] [app.main.ui.notifications.badge :refer [badge-notification]] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr c]] @@ -36,6 +37,7 @@ cta-link-trial cta-text-with-icon cta-link-with-icon + show-activation-by-code editors recommended show-button-cta]}] @@ -65,6 +67,14 @@ [:ul {:class (stl/css :benefits-list)} (for [benefit benefits] [:li {:key (dm/str benefit) :class (stl/css :benefit)} "- " benefit])] + (when (and cta-link cta-text show-button-cta) + [:> button* {:variant "primary" + :type "button" + :class (stl/css-case :bottom-button (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]) (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 @@ -74,14 +84,10 @@ [: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 cta-text show-button-cta) - [:> button* {:variant "primary" - :type "button" - :class (stl/css-case :bottom-button (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])]) + (when show-activation-by-code + [:button {:class (stl/css :cta-button :activate-by-code) + :on-click #(st/emit! (modal/show {:type :nitrate-code-activation}))} + (tr "subscription.settings.activate-by-code")])]) (defn- make-management-form-schema [min-editors] [:map {:title "SeatsForm"} @@ -359,37 +365,6 @@ :value (tr "labels.close") :on-click handle-close-dialog}]]]]]])) -(mf/defc nitrate-success-dialog - {::mf/register modal/components - ::mf/register-as :nitrate-success} - [] - ;; TODO add translations for this texts when we have the definitive ones - (let [profile (mf/deref refs/profile)] - - [:div {:class (stl/css :modal-overlay)} - [:div {:class (stl/css :modal-dialog :subscription-success)} - [:button {:class (stl/css :close-btn) :on-click modal/hide!} - [:> icon* {:icon-id "close" - :size "m"}]] - [:div {:class (stl/css :modal-success-content)} - [:div {:class (stl/css :modal-start)} - [:> raw-svg* {:id (if (= "light" (:theme profile)) "logo-subscription-light" "logo-subscription")}]] - - [:div {:class (stl/css :modal-end)} - [:div {:class (stl/css :modal-title)} - "You are Business Nitrate!"] - [:p {:class (stl/css :modal-text-large)} - (tr "subscription.settings.success.dialog.description")] - [:p {:class (stl/css :modal-text-large)} - (tr "subscription.settings.success.dialog.footer")] - - [:div {:class (stl/css :success-action-buttons)} - [:input - {:class (stl/css :primary-button) - :type "button" - :value "CREATE ORGANIZATION" - :on-click dnt/go-to-nitrate-cc-create-org}]]]]]])) - (mf/defc subscription-page* [{:keys [profile]}] (let [route (mf/deref refs/route) @@ -500,7 +475,7 @@ ^boolean show-subscription-success-modal? (st/emit! (if (= params-subscription "subscribed-to-penpot-nitrate") - (modal/show :nitrate-success {}) + (modal/show :nitrate-activation-success {}) (modal/show :subscription-success {:subscription-name (if (= params-subscription "subscribed-to-penpot-unlimited") (if (= success-modal-is-trial? "true") @@ -523,7 +498,7 @@ [:> plan-card* {:card-title "Business Nitrate" :card-title-icon i/character-b :cancel-at (when (:cancel-at nitrate-license) - (dm/str "Active until " (ct/format-inst (:cancel-at nitrate-license) "d MMMM, yyyy"))) + (tr "nitrate.subscription.active-until" (ct/format-inst (:cancel-at nitrate-license) "d MMMM, yyyy"))) :benefits-title "Loren ipsum", :benefits ["Loren ipsum", "Loren ipsum", @@ -660,6 +635,7 @@ :cta-link (if (= subscription-type "unlimited") #(open-contact-sales-modal subscription-type "Nitrate") #(open-subscription-modal "nitrate" subscription)) :cta-text-with-icon (tr "subscription.settings.more-information") :cta-link-with-icon go-to-pricing-page + :show-activation-by-code true :show-button-cta (not nitrate-license)}])]]])) diff --git a/frontend/translations/en.po b/frontend/translations/en.po index e4259e76fa..9426a51036 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -9252,3 +9252,100 @@ msgstr "Are you sure you want to delete this team that is part of %s org?" msgid "plugins.validation.message" msgstr "Field %s is invalid: %s" + +msgid "nitrate.code-activation.title" +msgstr "Activate Nitrate" + +msgid "nitrate.code-activation.input-label" +msgstr "Enter your activation code" + +msgid "nitrate.code-activation.submit" +msgstr "Activate" + +msgid "nitrate.code-activation.footer" +msgstr "Need a code? Contact us:" + +msgid "nitrate.code-activation.placeholder" +msgstr "Paste your activation code here" + + +msgid "nitrate.activation-success.title" +msgstr "You are Business Nitrate!" + +msgid "nitrate.activation-success.active-until" +msgstr "Your plan is active until %s." + +msgid "nitrate.activation-success.manage-info" +msgstr "You can manage your subscription anytime from the Subscription page in your account settings." + +msgid "nitrate.activation-success.enjoy" +msgstr "Enjoy your plan!" + +msgid "nitrate.activation-success.create-org" +msgstr "Create organization" + +msgid "nitrate.form.title" +msgstr "Unlock Nitrate Features" + +msgid "nitrate.form.billing-monthly" +msgstr "Price Tag Monthly" + +msgid "nitrate.form.billing-yearly" +msgstr "Price Tag Yearly (Discount)" + +msgid "nitrate.form.upgrade" +msgstr "Upgrade to Nitrate" + +msgid "nitrate.form.try-free" +msgstr "Try it free for 14 days" + +msgid "nitrate.form.cancel-anytime" +msgstr "Cancel anytime before your next billing cycle." + +msgid "nitrate.form.have-code" +msgstr "Have an activation code?" + +msgid "nitrate.form.enter-code" +msgstr "Enter activation code" + +msgid "nitrate.form.see-plan" +msgstr "See my current plan" + +msgid "nitrate.form.contact-upgrade" +msgstr "Contact us to upgrade to Nitrate:" + +msgid "nitrate.form.contact-trial" +msgstr "Contact us to try Nitrate for 14 days:" + +msgid "nitrate.activation-code.invalid-error" +msgstr "Invalid code." + +msgid "nitrate.activation-code.expired-error" +msgstr "This code has expired." + +msgid "nitrate.contact-sales.title" +msgstr "Switch to %s plan?" + +msgid "nitrate.contact-sales.downgrade-title" +msgstr "When you downgrade:" + +msgid "nitrate.contact-sales.downgrade-org-deleted" +msgstr "Your organization will be deleted." + +msgid "nitrate.contact-sales.downgrade-teams-available" +msgstr "The teams, projects and files will no longer be part of any organization but they will remain available." + +msgid "nitrate.contact-sales.downgrade-storage-limited" +msgstr "Your total storage, auto-version history, and file recovery period will be limited." + +msgid "nitrate.contact-sales.downgrade-contact-info" +msgstr "To switch to this plan, please contact our sales team. We'll help you update your subscription and ensure everything is set up correctly." + +msgid "nitrate.contact-sales.button" +msgstr "Contact sales" + +msgid "nitrate.subscription.active-until" +msgstr "Active until %s" + +msgid "subscription.settings.activate-by-code" +msgstr "Enter activation code" \ No newline at end of file diff --git a/frontend/translations/es.po b/frontend/translations/es.po index ecb1a4ecec..4d670f8716 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -8977,3 +8977,103 @@ msgstr "Pulsar para cerrar la ruta" msgid "modals.delete-org-team-confirm.message" msgstr "¿Estás seguro de que deseas eliminar este equipo que forma parte de la organización %s?" + +msgid "nitrate.code-activation.title" +msgstr "Activar Nitrate" + +msgid "nitrate.code-activation.input-label" +msgstr "Introduce tu código de activación" + +msgid "nitrate.code-activation.submit" +msgstr "Activar" + +msgid "nitrate.code-activation.footer" +msgstr "¿Necesitas un código? Contáctanos:" + +msgid "nitrate.code-activation.placeholder" +msgstr "Pega aquí tu código de activación" + + +msgid "nitrate.activation-success.title" +msgstr "¡Ya eres Business Nitrate!" + +msgid "nitrate.activation-success.active-until" +msgstr "Tu plan está activo hasta el %s." + +msgid "nitrate.activation-success.manage-info" +msgstr "Puedes gestionar tu suscripción en cualquier momento desde la página de Suscripción en la configuración de tu cuenta." + +msgid "nitrate.activation-success.enjoy" +msgstr "¡Disfruta de tu plan!" + +msgid "nitrate.activation-success.create-org" +msgstr "Crear organización" + +msgid "nitrate.form.title" +msgstr "Desbloquea las funciones de Nitrate" + +msgid "nitrate.form.billing-monthly" +msgstr "Precio mensual" + +msgid "nitrate.form.billing-yearly" +msgstr "Precio anual (descuento)" + +msgid "nitrate.form.upgrade" +msgstr "Actualizar a Nitrate" + +msgid "nitrate.form.try-free" +msgstr "Pruébalo gratis durante 14 días" + +msgid "nitrate.form.cancel-anytime" +msgstr "Cancela en cualquier momento antes de tu próximo ciclo de facturación." + +msgid "nitrate.form.have-code" +msgstr "¿Tienes un código de activación?" + +msgid "nitrate.form.enter-code" +msgstr "Introducir código de activación" + +msgid "nitrate.form.see-plan" +msgstr "Ver mi plan actual" + +msgid "nitrate.form.contact-upgrade" +msgstr "Contáctanos para actualizar a Nitrate:" + +msgid "nitrate.form.contact-trial" +msgstr "Contáctanos para probar Nitrate durante 14 días:" + +msgid "nitrate.activation-code.invalid-error" +msgstr "Código inválido." + +msgid "nitrate.activation-code.expired-error" +msgstr "Este código ha caducado." + +msgid "nitrate.contact-sales.title" +msgstr "¿Cambiar al plan %s?" + +msgid "nitrate.contact-sales.downgrade-title" +msgstr "Al bajar de plan:" + +msgid "nitrate.contact-sales.downgrade-org-deleted" +msgstr "Tu organización será eliminada." + +msgid "nitrate.contact-sales.downgrade-teams-available" +msgstr "Los equipos, proyectos y archivos dejarán de pertenecer a la organización pero seguirán disponibles." + +msgid "nitrate.contact-sales.downgrade-storage-limited" +msgstr "Tu almacenamiento total, el historial de versiones automático y el período de recuperación de archivos serán limitados." + +msgid "nitrate.contact-sales.downgrade-contact-info" +msgstr "Para cambiar a este plan, contacta con nuestro equipo de ventas. Te ayudaremos a actualizar tu suscripción y a asegurarnos de que todo esté configurado correctamente." + +msgid "nitrate.contact-sales.button" +msgstr "Contactar con ventas" + +msgid "nitrate.subscription.active-until" +msgstr "Activo hasta el %s" + +msgid "subscription.settings.more-information" +msgstr "Más información" + +msgid "subscription.settings.activate-by-code" +msgstr "Introducir código de activación" \ No newline at end of file