Subscribe to nitrate with an activation code

*  Subscribe to nitrate with an activation code

* 📎 Code review
This commit is contained in:
María Valderrama 2026-04-29 12:42:25 +02:00 committed by GitHub
parent 3f40be6b4d
commit e22a03e7e8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 671 additions and 70 deletions

View File

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

View File

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

View File

@ -39,3 +39,4 @@
(assert (contains? raw-svg-list id) "invalid raw svg id")
[:> "svg" props
[:use {:href (dm/str "#asset-" id)}]])

View File

@ -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")]]]]]))

View File

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

View File

@ -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"]]]]]))

View File

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

View File

@ -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")]]]])]]]]))

View File

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

View File

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

View File

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

View File

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