0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-02-13 02:28:18 -05:00

Merge branch 'us/newsletter_subscription' into staging

This commit is contained in:
Andrey Antukh 2022-04-04 23:12:03 +02:00
commit 7105255212
19 changed files with 402 additions and 114 deletions

View file

@ -8,6 +8,7 @@
### :arrow_up: Deps updates
### :heart: Community contributions by (Thank you!)
## 1.13.0-beta
### :boom: Breaking changes

View file

@ -209,6 +209,9 @@
{:cron #app/cron "0 0 0 * * ?" ;; daily
:task :tasks-gc}
{:cron #app/cron "0 30 */3,23 * * ?"
:task :telemetry}
(when (cf/get :fdata-storage-backed)
{:cron #app/cron "0 0 * * * ?" ;; hourly
:task :file-offload})
@ -219,12 +222,7 @@
(when (contains? cf/flags :audit-log-gc)
{:cron #app/cron "0 0 0 * * ?" ;; daily
:task :audit-log-gc})
(when (or (contains? cf/flags :telemetry)
(cf/get :telemetry-enabled))
{:cron #app/cron "0 30 */3,23 * * ?"
:task :telemetry})]}
:task :audit-log-gc})]}
:app.worker/registry
{:metrics (ig/ref :app.metrics/metrics)

View file

@ -6,6 +6,7 @@
(ns app.rpc.mutations.profile
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.common.uuid :as uuid]
@ -30,7 +31,7 @@
(s/def ::email ::us/email)
(s/def ::fullname ::us/not-empty-string)
(s/def ::lang (s/nilable ::us/not-empty-string))
(s/def ::lang ::us/string)
(s/def ::path ::us/string)
(s/def ::profile-id ::us/uuid)
(s/def ::password ::us/not-empty-string)
@ -342,27 +343,41 @@
;; --- MUTATION: Update Profile (own)
(defn- update-profile
[conn {:keys [id fullname lang theme] :as params}]
(let [profile (db/update! conn :profile
{:fullname fullname
:lang lang
:theme theme}
{:id id})]
(-> profile
(profile/decode-profile-row)
(profile/strip-private-attrs))))
(s/def ::newsletter-subscribed ::us/boolean)
(s/def ::update-profile
(s/keys :req-un [::id ::fullname]
:opt-un [::lang ::theme]))
(s/keys :req-un [::fullname ::profile-id]
:opt-un [::lang ::theme ::newsletter-subscribed]))
(sv/defmethod ::update-profile
[{:keys [pool] :as cfg} params]
[{:keys [pool] :as cfg} {:keys [profile-id fullname lang theme newsletter-subscribed] :as params}]
(db/with-atomic [conn pool]
(let [profile (update-profile conn params)]
(with-meta profile
;; NOTE: we need to retrieve the profile independently if we use
;; it or not for explicit locking and avoid concurrent updates of
;; the same row/object.
(let [profile (-> (db/get-by-id conn :profile profile-id {:for-update true})
(profile/decode-profile-row))
;; Update the profile map with direct params
profile (-> profile
(assoc :fullname fullname)
(assoc :lang lang)
(assoc :theme theme))
;; Update profile props if the indirect prop is coming in
;; the params map and update the profile props data
;; acordingly.
profile (cond-> profile
(some? newsletter-subscribed)
(update :props assoc :newsletter-subscribed newsletter-subscribed))]
(db/update! conn :profile
{:fullname fullname
:lang lang
:theme theme
:props (db/tjson (:props profile))}
{:id profile-id})
(with-meta (-> profile profile/strip-private-attrs d/without-nils)
{::audit/props (audit/profile->props profile)}))))
;; --- MUTATION: Update Password

View file

@ -12,7 +12,7 @@
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.config :as cfg]
[app.config :as cf]
[app.db :as db]
[app.util.async :refer [thread-sleep]]
[app.util.json :as json]
@ -25,6 +25,7 @@
(declare get-stats)
(declare send!)
(declare get-subscriptions)
(s/def ::http-client fn?)
(s/def ::version ::us/string)
@ -38,18 +39,39 @@
(defmethod ig/init-key ::handler
[_ {:keys [pool sprops version] :as cfg}]
(fn [{:keys [send?] :or {send? true}}]
;; Sleep randomly between 0 to 10s
(when send?
(thread-sleep (rand-int 10000)))
(fn [{:keys [send? enabled?] :or {send? true enabled? false}}]
(let [subs (get-subscriptions pool)
enabled? (or enabled?
(contains? cf/flags :telemetry)
(cf/get :telemetry-enabled))
(let [instance-id (:instance-id sprops)
stats (-> (get-stats pool version)
(assoc :instance-id instance-id))]
(when send?
(send! cfg stats))
data {:subscriptions subs
:version version
:instance-id (:instance-id sprops)}]
(cond
;; If we have telemetry enabled, then proceed the normal
;; operation.
enabled?
(let [data (merge data (get-stats pool))]
(when send?
(thread-sleep (rand-int 10000))
(send! cfg data))
data)
stats)))
;; If we have telemetry disabled, but there are users that are
;; explicitly checked the newsletter subscription on the
;; onboarding dialog or the profile section, then proceed to
;; send a limited telemetry data, that consists in the list of
;; subscribed emails and the running penpot version.
(seq subs)
(do
(when send?
(thread-sleep (rand-int 10000))
(send! cfg data))
data)
:else
data))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; IMPL
@ -68,6 +90,12 @@
:response-status (:status response)
:response-body (:body response)))))
(defn- get-subscriptions
[conn]
(let [sql "select email from profile where props->>'~:newsletter-subscribed' = 'true'"]
(->> (db/exec! conn [sql])
(mapv :email))))
(defn- retrieve-num-teams
[conn]
(-> (db/exec-one! conn ["select count(*) as count from team;"]) :count))
@ -166,12 +194,11 @@
:user-tz (System/getProperty "user.timezone")}))
(defn get-stats
[conn version]
(let [referer (if (cfg/get :telemetry-with-taiga)
[conn]
(let [referer (if (cf/get :telemetry-with-taiga)
"taiga"
(cfg/get :telemetry-referer))]
(-> {:version version
:referer referer
(cf/get :telemetry-referer))]
(-> {:referer referer
:total-teams (retrieve-num-teams conn)
:total-projects (retrieve-num-projects conn)
:total-files (retrieve-num-files conn)

View file

@ -21,13 +21,16 @@
(with-mocks [mock {:target 'app.tasks.telemetry/send!
:return nil}]
(let [task-fn (-> th/*system* :app.worker/registry :telemetry)
prof (th/create-profile* 1 {:is-active true})]
prof (th/create-profile* 1 {:is-active true
:props {:newsletter-subscribed true}})]
;; run the task
(task-fn nil)
(task-fn {:send? true :enabled? true})
(t/is (:called? @mock))
(let [[_ data] (-> @mock :call-args)]
(t/is (contains? data :subscriptions))
(t/is (= [(:email prof)] (get data :subscriptions)))
(t/is (contains? data :total-fonts))
(t/is (contains? data :total-users))
(t/is (contains? data :total-projects))

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View file

@ -109,6 +109,38 @@
flex-direction: column;
max-width: 368px;
width: 100%;
.newsletter-subs {
border-bottom: 1px solid $color-gray-20;
border-top: 1px solid $color-gray-20;
padding: 30px 0;
margin-bottom: 31px;
.newsletter-title {
font-family: "worksans", sans-serif;
color: $color-gray-30;
font-size: $fs14;
}
label {
font-family: "worksans", sans-serif;
color: $color-gray-60;
font-size: $fs12;
margin-right: -17px;
margin-bottom: 13px;
}
.info {
font-family: "worksans", sans-serif;
color: $color-gray-30;
font-size: $fs12;
margin-bottom: 8px;
}
.input-checkbox label {
align-items: flex-start;
}
}
}
.options-form,

View file

@ -996,6 +996,57 @@
}
}
}
&.newsletter {
padding: $size-5 0 0 0;
flex-direction: column;
min-width: 555px;
.modal-top {
padding: 87px 40px 0 40px;
color: $color-gray-60;
display: flex;
flex-direction: column;
h1 {
font-family: sourcesanspro;
font-weight: bold;
font-size: $fs36;
margin-bottom: 0.75rem;
}
p {
font-family: sourcesanspro;
font-weight: 500;
font-size: $fs16;
margin-bottom: 1.5rem;
}
}
.modal-bottom {
margin: 0 32px;
padding: 32px 0;
color: $color-gray-60;
display: flex;
flex-direction: column;
border-top: 1px solid $color-gray-10;
p {
font-family: "worksans", sans-serif;
text-align: left;
color: $color-gray-30;
}
}
.modal-footer {
padding: 17px;
display: flex;
justify-content: flex-end;
.btn-secondary {
margin-right: 16px;
}
}
}
}
.deco {
@ -1004,6 +1055,23 @@
top: -18px;
width: 60px;
&.top {
width: 183px;
top: -106px;
left: 161px;
}
&.newsletter-right {
left: 515px;
top: 50px;
}
&.newsletter-left {
width: 26px;
left: -15px;
top: -15px;
}
&.right {
left: 590px;
top: 0;

View file

@ -54,11 +54,14 @@
:browser
:webworker))
(def default-flags
[:enable-newsletter-subscription])
(defn- parse-flags
[global]
(let [flags (obj/get global "penpotFlags" "")
flags (sequence (map keyword) (str/words flags))]
(flags/parse flags/default flags)))
(flags/parse flags/default default-flags flags)))
(defn- parse-version
[global]

View file

@ -303,8 +303,8 @@
(watch [_ _ stream]
(let [mdata (meta data)
on-success (:on-success mdata identity)
on-error (:on-error mdata #(rx/throw %))]
(->> (rp/mutation :update-profile data)
on-error (:on-error mdata rx/throw)]
(->> (rp/mutation :update-profile (dissoc data :props))
(rx/catch on-error)
(rx/mapcat
(fn [_]
@ -392,7 +392,6 @@
(->> (rp/mutation :update-profile-props {:props props})
(rx/map (constantly (fetch-profile)))))))))
(defn mark-questions-as-answered
[]
(ptk/reify ::mark-questions-as-answered

View file

@ -59,15 +59,15 @@
klass (str more-classes " "
(dom/classnames
:focus @focus?
:valid (and touched? (not error))
:invalid (and touched? error)
:disabled disabled
:empty (and is-text? (str/empty? value))
:with-icon (not (nil? help-icon'))
:custom-input is-text?
:input-radio is-radio?
:input-checkbox is-checkbox?))
:focus @focus?
:valid (and touched? (not error))
:invalid (and touched? error)
:disabled disabled
:empty (and is-text? (str/empty? value))
:with-icon (not (nil? help-icon'))
:custom-input is-text?
:input-radio is-radio?
:input-checkbox is-checkbox?))
swap-text-password
(fn []
@ -78,7 +78,7 @@
on-focus #(reset! focus? true)
on-change (fn [event]
(let [value (-> event dom/get-target dom/get-input-value)]
(let [value (-> event dom/get-target dom/get-input-value)]
(fm/on-input-change form input-name value trim)))
on-blur
@ -87,16 +87,23 @@
(when-not (get-in @form [:touched input-name])
(swap! form assoc-in [:touched input-name] true)))
on-click
(fn [_]
(when-not (get-in @form [:touched input-name])
(swap! form assoc-in [:touched input-name] true)))
props (-> props
(dissoc :help-icon :form :trim :children)
(assoc :id (name input-name)
:value value
:auto-focus auto-focus?
:on-click (when (or is-radio? is-checkbox?) on-click)
:on-focus on-focus
:on-blur on-blur
:placeholder label
:on-change on-change
:type @type')
(cond-> (and value is-checkbox?) (assoc :default-checked value))
(obj/clj->props))]
[:div
@ -210,7 +217,7 @@
(let [form (or form (mf/use-ctx form-ctx))]
[:input.btn-primary.btn-large
{:name "submit"
:class (when-not (:valid @form) "btn-disabled")
:class (when (or (not (:valid @form)) (true? disabled)) "btn-disabled")
:disabled (or (not (:valid @form)) (true? disabled))
:on-click on-click
:value label

View file

@ -10,6 +10,7 @@
[app.main.data.modal :as modal]
[app.main.data.users :as du]
[app.main.store :as st]
[app.main.ui.onboarding.newsletter]
[app.main.ui.onboarding.questions]
[app.main.ui.onboarding.team-choice]
[app.main.ui.onboarding.templates]
@ -134,8 +135,10 @@
[:p (tr "onboarding.slide.3.desc1")]
[:p (tr "onboarding.slide.3.desc2")]]
[:div.modal-navigation
[:button.btn-secondary {:on-click skip
:data-test "slide-3-btn"} (tr "labels.start")]
[:button.btn-secondary
{:on-click skip
:data-test "slide-3-btn"}
(tr "labels.start")]
[:& rc/navigation-bullets
{:slide slide
:navigate navigate
@ -149,23 +152,23 @@
klass (mf/use-state "fadeInDown")
navigate
(mf/use-callback #(reset! slide %))
(mf/use-fn #(reset! slide %))
skip
(mf/use-callback
(st/emitf (modal/hide)
(modal/show {:type :onboarding-choice})
(du/mark-onboarding-as-viewed)))]
(mf/use-fn
#(st/emit! (modal/hide)
(if (contains? @cf/flags :newsletter-subscription)
(modal/show {:type :onboarding-newsletter-modal})
(modal/show {:type :onboarding-choice}))
(du/mark-onboarding-as-viewed)))]
(mf/use-layout-effect
(mf/deps @slide)
(fn []
(when (not= :start @slide)
(reset! klass "fadeIn"))
(let [sem (tm/schedule 300 #(reset! klass nil))]
(fn []
(reset! klass nil)
(tm/dispose! sem)))))
(mf/with-effect [@slide]
(when (not= :start @slide)
(reset! klass "fadeIn"))
(let [sem (tm/schedule 300 #(reset! klass nil))]
(fn []
(reset! klass nil)
(tm/dispose! sem))))
[:div.modal-overlay
[:div.animated {:class @klass}

View file

@ -0,0 +1,47 @@
;; 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) UXBOX Labs SL
(ns app.main.ui.onboarding.newsletter
(:require
[app.main.data.messages :as dm]
[app.main.data.modal :as modal]
[app.main.data.users :as du]
[app.main.store :as st]
[app.util.i18n :as i18n :refer [tr]]
[rumext.alpha :as mf]))
(mf/defc onboarding-newsletter-modal
{::mf/register modal/components
::mf/register-as :onboarding-newsletter-modal}
[]
(let [message (tr "onboarding.newsletter.acceptance-message")
accept
(mf/use-callback
(fn []
(st/emit! (dm/success message)
(modal/show {:type :onboarding-choice})
(du/update-profile-props {:newsletter-subscribed true}))))
decline
(mf/use-callback
(fn []
(st/emit! (modal/show {:type :onboarding-choice})
(du/update-profile-props {:newsletter-subscribed false}))))]
[:div.modal-overlay
[:div.modal-container.onboarding.newsletter.animated.fadeInUp
[:div.modal-top
[:h1.newsletter-title {:data-test "onboarding-newsletter-title"} (tr "onboarding.newsletter.title")]
[:p (tr "onboarding.newsletter.desc")]]
[:div.modal-bottom
[:p (tr "onboarding.newsletter.privacy1") [:a {:target "_blank" :href "https://penpot.app/privacy.html"} (tr "onboarding.newsletter.policy")]]
[:p (tr "onboarding.newsletter.privacy2")]]
[:div.modal-footer
[:button.btn-secondary {:on-click decline} (tr "onboarding.newsletter.decline")]
[:button.btn-primary {:on-click accept} (tr "onboarding.newsletter.accept")]]
[:img.deco.top {:src "images/deco-newsletter.png" :border "0"}]
[:img.deco.newsletter-left {:src "images/deco-news-left.png" :border "0"}]
[:img.deco.newsletter-right {:src "images/deco-news-right.png" :border "0"}]]]))

View file

@ -13,7 +13,7 @@
[app.main.store :as st]
[app.main.ui.components.forms :as fm]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [t tr]]
[app.util.i18n :as i18n :refer [tr]]
[cljs.spec.alpha :as s]
[rumext.alpha :as mf]))
@ -30,51 +30,51 @@
(defn- on-submit
[form _event]
(let [data (:clean-data @form)
data (cond-> data
(empty? (:lang data))
(assoc :lang nil))
mdata {:on-success (partial on-success form)}]
(st/emit! (du/update-profile (with-meta data mdata)))))
(mf/defc options-form
[{:keys [locale] :as props}]
[]
(let [profile (mf/deref refs/profile)
initial (mf/with-memo [profile]
(update profile :lang #(or % "")))
form (fm/use-form :spec ::options-form
:initial profile)]
:initial initial)]
[:& fm/form {:class "options-form"
:on-submit on-submit
:form form}
[:h2 (t locale "labels.language")]
[:h2 (tr "labels.language")]
[:div.fields-row
[:& fm/select {:options (into [{:label "Auto (browser)" :value "default"}]
[:& fm/select {:options (into [{:label "Auto (browser)" :value ""}]
i18n/supported-locales)
:label (t locale "dashboard.select-ui-language")
:label (tr "dashboard.select-ui-language")
:default ""
:name :lang
:data-test "setting-lang"}]]
;; TODO: Do not show as long as we only have one theme
#_[:h2 (t locale "dashboard.theme-change")]
#_[:h2 (tr "dashboard.theme-change")]
#_[:div.fields-row
[:& fm/select {:label (t locale "dashboard.select-ui-theme")
[:& fm/select {:label (tr "dashboard.select-ui-theme")
:name :theme
:default "default"
:options [{:label "Default" :value "default"}]
:data-test "theme-lang"}]]
[:& fm/submit-button
{:label (t locale "dashboard.update-settings")
{:label (tr "dashboard.update-settings")
:data-test "submit-lang-change"}]]))
;; --- Password Page
(mf/defc options-page
[{:keys [locale]}]
[]
(mf/use-effect
#(dom/set-html-title (tr "title.settings.options")))
[:div.dashboard-settings
[:div.form-container
{:data-test "settings-form"}
[:& options-form {:locale locale}]]])
[:& options-form {}]]])

View file

@ -7,7 +7,7 @@
(ns app.main.ui.settings.profile
(:require
[app.common.spec :as us]
[app.config :as cfg]
[app.config :as cf]
[app.main.data.messages :as dm]
[app.main.data.modal :as modal]
[app.main.data.users :as du]
@ -17,7 +17,7 @@
[app.main.ui.components.forms :as fm]
[app.main.ui.icons :as i]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr t]]
[app.util.i18n :as i18n :refer [tr]]
[cljs.spec.alpha :as s]
[rumext.alpha :as mf]))
@ -40,10 +40,13 @@
;; --- Profile Form
(mf/defc profile-form
[{:keys [locale] :as props}]
[]
(let [profile (mf/deref refs/profile)
form (fm/use-form :spec ::profile-form
:initial profile)]
initial (mf/with-memo [profile]
(let [subscribed? (-> profile :props :newsletter-subscribed)]
(assoc profile :newsletter-subscribed subscribed?)))
form (fm/use-form :spec ::profile-form :initial initial)]
[:& fm/form {:on-submit on-submit
:form form
:class "profile-form"}
@ -51,7 +54,7 @@
[:& fm/input
{:type "text"
:name :fullname
:label (t locale "dashboard.your-name")}]]
:label (tr "dashboard.your-name")}]]
[:div.fields-row
[:& fm/input
@ -59,29 +62,40 @@
:name :email
:disabled true
:help-icon i/at
:label (t locale "dashboard.your-email")}]
:label (tr "dashboard.your-email")}]
[:div.options
[:div.change-email
[:a {:on-click #(modal/show! :change-email {})}
(t locale "dashboard.change-email")]]]]
(tr "dashboard.change-email")]]]]
(when (contains? @cf/flags :newsletter-subscription)
[:div.newsletter-subs
[:p.newsletter-title (tr "dashboard.newsletter-title")]
[:& fm/input {:name :newsletter-subscribed
:class "check-primary"
:type "checkbox"
:label (tr "dashboard.newsletter-msg")}]
[:p.info (tr "onboarding.newsletter.privacy1")
[:a {:target "_blank" :href "https://penpot.app/privacy.html"} (tr "onboarding.newsletter.policy")]]
[:p.info (tr "onboarding.newsletter.privacy2")]])
[:& fm/submit-button
{:label (t locale "dashboard.update-settings")}]
{:label (tr "dashboard.save-settings")
:disabled (empty? (:touched @form))}]
[:div.links
[:div.link-item
[:a {:on-click #(modal/show! :delete-account {})
:data-test "remove-acount-btn"}
(t locale "dashboard.remove-account")]]]]))
(tr "dashboard.remove-account")]]]]))
;; --- Profile Photo Form
(mf/defc profile-photo-form
[{:keys [locale] :as props}]
(let [file-input (mf/use-ref nil)
profile (mf/deref refs/profile)
photo (cfg/resolve-profile-photo-url profile)
(mf/defc profile-photo-form []
(let [file-input (mf/use-ref nil)
profile (mf/deref refs/profile)
photo (cf/resolve-profile-photo-url profile)
on-image-click #(dom/click (mf/ref-val file-input))
on-file-selected
@ -90,7 +104,7 @@
[:form.avatar-form
[:div.image-change-field
[:span.update-overlay {:on-click on-image-click} (t locale "labels.update")]
[:span.update-overlay {:on-click on-image-click} (tr "labels.update")]
[:img {:src photo}]
[:& file-uploader {:accept "image/jpeg,image/png"
:multi false
@ -100,14 +114,11 @@
;; --- Profile Page
(mf/defc profile-page
[{:keys [locale]}]
(mf/use-effect
#(dom/set-html-title (tr "title.settings.profile")))
(mf/defc profile-page []
(mf/with-effect []
(dom/set-html-title (tr "title.settings.profile")))
[:div.dashboard-settings
[:div.form-container.two-columns
[:& profile-photo-form {:locale locale}]
[:& profile-form {:locale locale}]]])
[:& profile-photo-form]
[:& profile-form]]])

View file

@ -581,7 +581,19 @@ msgstr "Search results"
msgid "dashboard.type-something"
msgstr "Type to search results"
#: src/app/main/ui/settings/profile.cljs, src/app/main/ui/settings/password.cljs, src/app/main/ui/settings/options.cljs
#: src/app/main/ui/settings/profile.cljs
msgid "dashboard.save-settings"
msgstr "Save settings"
#: src/app/main/ui/settings/profile.cljs
msgid "dashboard.newsletter-title"
msgstr "Newsletter subscription"
#: src/app/main/ui/settings/profile.cljs
msgid "dashboard.newsletter-msg"
msgstr "Send me news, product updates and recommendations about Penpot."
#: src/app/main/ui/settings/password.cljs, src/app/main/ui/settings/options.cljs
msgid "dashboard.update-settings"
msgstr "Update settings"
@ -1855,6 +1867,30 @@ msgstr ""
msgid "onboarding.welcome.title"
msgstr "Welcome to Penpot"
msgid "onboarding.newsletter.title"
msgstr "Want to receive Penpot news?"
msgid "onboarding.newsletter.desc"
msgstr "Subscribe to our newsletter to stay up to date with product development progress and news."
msgid "onboarding.newsletter.privacy1"
msgstr "Because we care about privacy, here's our "
msgid "onboarding.newsletter.policy"
msgstr "Privacy Policy."
msgid "onboarding.newsletter.privacy2"
msgstr "We will only send relevant emails to you. You can unsubscribe at any time in your user profile or via the unsubscribe link in any of our newsletters."
msgid "onboarding.newsletter.accept"
msgstr "Yes, subscribe"
msgid "onboarding.newsletter.decline"
msgstr "No, thanks"
msgid "onboarding.newsletter.acceptance-message"
msgstr "Your subscription request has been sent, we will send you an email to confirm it."
#: src/app/main/ui/auth/recovery.cljs
msgid "profile.recovery.go-to-login"
msgstr "Go to login"

View file

@ -587,7 +587,20 @@ msgstr "Resultados de búsqueda"
msgid "dashboard.type-something"
msgstr "Escribe algo para buscar"
#: src/app/main/ui/settings/profile.cljs, src/app/main/ui/settings/password.cljs, src/app/main/ui/settings/options.cljs
#: src/app/main/ui/settings/profile.cljs
msgid "dashboard.save-settings"
msgstr "Guardar opciones"
#: src/app/main/ui/settings/profile.cljs
msgid "dashboard.newsletter-title"
msgstr "Suscripción a newsletter"
#: src/app/main/ui/settings/profile.cljs
msgid "dashboard.newsletter-msg"
msgstr "Envíame noticias, actualizaciones de producto y recomendaciones sobre Penpot."
#: src/app/main/ui/settings/password.cljs, src/app/main/ui/settings/options.cljs
msgid "dashboard.update-settings"
msgstr "Actualizar opciones"
@ -1876,6 +1889,31 @@ msgstr ""
msgid "onboarding.welcome.title"
msgstr "Te damos la bienvenida a Penpot"
msgid "onboarding.newsletter.title"
msgstr "¿Quieres recibir noticias sobre Penpot?"
msgid "onboarding.newsletter.desc"
msgstr "Suscríbete a nuestra newsletter para estar al día de los progresos del producto y noticias."
msgid "onboarding.newsletter.privacy1"
msgstr "Porque nos importa la privacidad, aquí puedes ver nuestra "
msgid "onboarding.newsletter.policy"
msgstr "Política de Privacidad."
msgid "onboarding.newsletter.privacy2"
msgstr "Sólo te enviaremos emails relevantes para ti. Puedes desuscribirte en cualquier momento desde tu perfil o usando el vínculo de desuscripción en cualquiera de nuestras newsletters."
msgid "onboarding.newsletter.accept"
msgstr "Si, suscribirme"
msgid "onboarding.newsletter.decline"
msgstr "No, gracias"
msgid "onboarding.newsletter.acceptance-message"
msgstr "Tu solicitud de suscripción ha sido enviada, te haremos una confirmación a tu email"
#: src/app/main/ui/auth/recovery.cljs
msgid "profile.recovery.go-to-login"
msgstr "Ir al login"