diff --git a/backend/src/app/rpc/mutations/profile.clj b/backend/src/app/rpc/mutations/profile.clj index 0b2d9c906..5ed8f7a94 100644 --- a/backend/src/app/rpc/mutations/profile.clj +++ b/backend/src/app/rpc/mutations/profile.clj @@ -49,8 +49,10 @@ (declare register-profile) (s/def ::invitation-token ::us/not-empty-string) +(s/def ::terms-privacy ::us/boolean) + (s/def ::register-profile - (s/keys :req-un [::email ::password ::fullname] + (s/keys :req-un [::email ::password ::fullname ::terms-privacy] :opt-un [::invitation-token])) (sv/defmethod ::register-profile {:auth false :rlimit :password} @@ -63,6 +65,10 @@ (ex/raise :type :validation :code :email-domain-is-not-allowed)) + (when-not (:terms-privacy params) + (ex/raise :type :validation + :code :invalid-terms-and-privacy)) + (db/with-atomic [conn pool] (let [cfg (assoc cfg :conn conn)] (register-profile cfg params)))) diff --git a/backend/tests/app/tests/test_services_profile.clj b/backend/tests/app/tests/test_services_profile.clj index f48fc8801..f527a3d31 100644 --- a/backend/tests/app/tests/test_services_profile.clj +++ b/backend/tests/app/tests/test_services_profile.clj @@ -190,6 +190,32 @@ (t/testing "not allowed email domain" (t/is (false? (profile/email-domain-in-whitelist? whitelist "username@somedomain.com")))))) +(t/deftest test-register-with-no-terms-and-privacy + (let [data {::th/type :register-profile + :email "user@example.com" + :password "foobar" + :fullname "foobar" + :terms-privacy nil} + out (th/mutation! data) + error (:error out) + edata (ex-data error)] + (t/is (th/ex-info? error)) + (t/is (= (:type edata) :validation)) + (t/is (= (:code edata) :spec-validation)))) + +(t/deftest test-register-with-bad-terms-and-privacy + (let [data {::th/type :register-profile + :email "user@example.com" + :password "foobar" + :fullname "foobar" + :terms-privacy false} + out (th/mutation! data) + error (:error out) + edata (ex-data error)] + (t/is (th/ex-info? error)) + (t/is (= (:type edata) :validation)) + (t/is (= (:code edata) :invalid-terms-and-privacy)))) + (t/deftest test-register-when-registration-disabled (with-mocks [mock {:target 'app.config/get :return (th/mock-config-get-with @@ -197,7 +223,8 @@ (let [data {::th/type :register-profile :email "user@example.com" :password "foobar" - :fullname "foobar"} + :fullname "foobar" + :terms-privacy true} out (th/mutation! data) error (:error out) edata (ex-data error)] @@ -210,7 +237,8 @@ data {::th/type :register-profile :email (:email profile) :password "foobar" - :fullname "foobar"} + :fullname "foobar" + :terms-privacy true} out (th/mutation! data) error (:error out) edata (ex-data error)] @@ -225,7 +253,8 @@ data {::th/type :register-profile :email "user@example.com" :password "foobar" - :fullname "foobar"} + :fullname "foobar" + :terms-privacy true} out (th/mutation! data)] ;; (th/print-result! out) (let [mock (deref mock) @@ -250,7 +279,8 @@ data {::th/type :register-profile :email "user@example.com" :password "foobar" - :fullname "foobar"} + :fullname "foobar" + :terms-privacy true} _ (th/create-global-complaint-for pool {:type :bounce :email "user@example.com"}) out (th/mutation! data)] ;; (th/print-result! out) @@ -270,7 +300,8 @@ data {::th/type :register-profile :email "user@example.com" :password "foobar" - :fullname "foobar"} + :fullname "foobar" + :terms-privacy true} _ (th/create-global-complaint-for pool {:type :complaint :email "user@example.com"}) out (th/mutation! data)] diff --git a/frontend/resources/locales.json b/frontend/resources/locales.json index b7e0d744a..0f9f6d0b5 100644 --- a/frontend/resources/locales.json +++ b/frontend/resources/locales.json @@ -369,6 +369,13 @@ }, "used-in" : [ "src/app/main/ui/auth.cljs" ] }, + "auth.terms-privacy-agreement" : { + "translations" : { + "en" : "When creating a new account, you agree to our terms of service and privacy policy.", + "es" : "Al crear una nueva cuenta, aceptas nuestros términos de servicio y política de privacidad." + }, + "used-in" : [ "src/app/main/ui/auth/register.cljs" ] + }, "auth.verification-email-sent" : { "translations" : { "ca" : "Em enviat un correu de verificació a", @@ -857,6 +864,13 @@ }, "used-in" : [ "src/app/main/ui/dashboard/grid.cljs" ] }, + "errors.terms-privacy-agreement-invalid" : { + "translations" : { + "en" : "You must accept our terms of service and privacy policy.", + "es" : "Debes aceptar nuestros términos de servicio y política de privacidad." + }, + "used-in" : [ "src/app/main/ui/auth/register.cljs" ] + }, "errors.clipboard-not-implemented" : { "translations" : { "ca" : "El teu navegador no pot realitzar aquesta operació", diff --git a/frontend/resources/styles/common/framework.scss b/frontend/resources/styles/common/framework.scss index 33116b33d..064541a06 100644 --- a/frontend/resources/styles/common/framework.scss +++ b/frontend/resources/styles/common/framework.scss @@ -561,27 +561,28 @@ input.element-name { .input-radio, .input-checkbox { align-items: center; + color: $color-gray-40; display: flex; margin-bottom: 10px; margin-top: 10px; padding-left: 0px; label{ - align-items: center; cursor: pointer; display: flex; margin-right: 15px; - font-size: $fs13; + font-size: $fs12; &:before{ content:""; width: 20px; height: 20px; margin-right: 10px; - background-color: $color-gray-50; - border: 1px solid $color-gray-60; - box-shadow: inset 0 0 0 0 $color-primary ; + background-color: $color-gray-10; + border: 1px solid $color-gray-30; + box-shadow: inset 0 0 0 0 $color-primary; box-sizing: border-box; + flex-shrink: 0; } } @@ -676,7 +677,6 @@ input[type=radio]:checked + label:before{ label { transition: border 0.2s linear 0s, color 0.2s linear 0s; - white-space: nowrap; position: relative; &:before { @@ -687,11 +687,11 @@ input[type=radio]:checked + label:before{ &::after { display: inline-block; - width: 16px; - height: 16px; + width: 20px; + height: 20px; position: absolute; left: 3.2px; - top: 0px; + top: 0; font-size: $fs11; transition: border 0.2s linear 0s, color 0.2s linear 0s; } @@ -732,7 +732,7 @@ input[type=radio]:checked + label:before{ &::after { content:"✓"; - color: #000000; + color: #ffffff; font-size: $fs16; } diff --git a/frontend/resources/styles/main/partials/dashboard-grid.scss b/frontend/resources/styles/main/partials/dashboard-grid.scss index e877491e3..df950b3d3 100644 --- a/frontend/resources/styles/main/partials/dashboard-grid.scss +++ b/frontend/resources/styles/main/partials/dashboard-grid.scss @@ -280,18 +280,6 @@ margin-top: 15px; } - .input-checkbox { - margin: 0; - position: absolute; - top: 10px; - right: 5px; - - label { - margin: 0; - } - - } - } // STYLES FOR LIBRARIES diff --git a/frontend/src/app/main/ui/auth/register.cljs b/frontend/src/app/main/ui/auth/register.cljs index 63b59b1b9..0f3745b5a 100644 --- a/frontend/src/app/main/ui/auth/register.cljs +++ b/frontend/src/app/main/ui/auth/register.cljs @@ -36,17 +36,23 @@ (defn- validate [data] - (let [password (:password data)] - (when (> 8 (count password)) - {:password {:message "errors.password-too-short"}}))) + (let [password (:password data) + terms-privacy (:terms-privacy data)] + (cond-> {} + (> 8 (count password)) + (assoc :password {:message "errors.password-too-short"}) + + (and (not terms-privacy) false) + (assoc :terms-privacy {:message "errors.terms-privacy-agreement-invalid"})))) (s/def ::fullname ::us/not-empty-string) (s/def ::password ::us/not-empty-string) (s/def ::email ::us/email) (s/def ::invitation-token ::us/not-empty-string) +(s/def ::terms-privacy ::us/boolean) (s/def ::register-form - (s/keys :req-un [::password ::fullname ::email] + (s/keys :req-un [::password ::fullname ::email ::terms-privacy] :opt-un [::invitation-token])) (mf/defc register-form @@ -113,10 +119,16 @@ :label (tr "auth.password") :type "password"}]] + [:div.fields-row + [:& fm/input {:name :terms-privacy + :class "check-primary" + :tab-index "4" + :label (tr "auth.terms-privacy-agreement") + :type "checkbox"}]] + [:& fm/submit-button {:label (tr "auth.register-submit") - :disabled @submitted? - }]])) + :disabled @submitted?}]])) ;; --- Register Page diff --git a/frontend/src/app/main/ui/components/forms.cljs b/frontend/src/app/main/ui/components/forms.cljs index 37b08f6c9..27bae23ba 100644 --- a/frontend/src/app/main/ui/components/forms.cljs +++ b/frontend/src/app/main/ui/components/forms.cljs @@ -23,56 +23,70 @@ (def use-form fm/use-form) (mf/defc input - [{:keys [type label help-icon disabled name form hint trim] :as props}] - (let [form (or form (mf/use-ctx form-ctx)) + [{:keys [label help-icon disabled form hint trim] :as props}] + (let [input-type (get props :type) + input-name (get props :name) + more-classes (get props :class) - type' (mf/use-state type) - focus? (mf/use-state false) + form (or form (mf/use-ctx form-ctx)) - touched? (get-in @form [:touched name]) - error (get-in @form [:errors name]) + type' (mf/use-state input-type) + focus? (mf/use-state false) + is-checkbox? (= @type' "checkbox") + is-radio? (= @type' "radio") + is-text? (or (= @type' "password") + (= @type' "text") + (= @type' "email")) - value (get-in @form [:data name] "") + touched? (get-in @form [:touched input-name]) + error (get-in @form [:errors input-name]) - help-icon' (cond - (and (= type "password") - (= @type' "password")) - i/eye + value (get-in @form [:data input-name] "") - (and (= type "password") - (= @type' "text")) - i/eye-closed + help-icon' (cond + (and (= input-type "password") + (= @type' "password")) + i/eye - :else - help-icon) + (and (= input-type "password") + (= @type' "text")) + i/eye-closed - klass (dom/classnames - :focus @focus? - :valid (and touched? (not error)) - :invalid (and touched? error) - :disabled disabled - :empty (str/empty? value) - :with-icon (not (nil? help-icon'))) + :else + help-icon) + + 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 (= @type' "radio") + :input-checkbox (= @type' "checkbox"))) swap-text-password (fn [] - (swap! type' (fn [type] - (if (= "password" type) + (swap! type' (fn [input-type] + (if (= "password" input-type) "text" "password")))) on-focus #(reset! focus? true) - on-change (fm/on-input-change form name trim) + on-change (fm/on-input-change form input-name trim) on-blur (fn [event] (reset! focus? false) - (when-not (get-in @form [:touched name]) - (swap! form assoc-in [:touched name] true))) + (when-not (get-in @form [:touched input-name]) + (swap! form assoc-in [:touched input-name] true))) props (-> props (dissoc :help-icon :form :trim) - (assoc :value value + (assoc :id (name input-name) + :value value :on-focus on-focus :on-blur on-blur :placeholder label @@ -80,15 +94,15 @@ :type @type') (obj/clj->props))] - [:div.custom-input + [:div {:class klass} [:* - [:label label] [:> :input props] + [:label {:for (name input-name)} label] (when help-icon' [:div.help-icon {:style {:cursor "pointer"} - :on-click (when (= "password" type) + :on-click (when (= "password" input-type) swap-text-password)} help-icon']) (cond @@ -100,16 +114,17 @@ (mf/defc textarea - [{:keys [label disabled name form hint trim] :as props}] - (let [form (or form (mf/use-ctx form-ctx)) + [{:keys [label disabled form hint trim] :as props}] + (let [input-name (get props :name) + + form (or form (mf/use-ctx form-ctx)) - type' (mf/use-state type) focus? (mf/use-state false) - touched? (get-in @form [:touched name]) - error (get-in @form [:errors name]) + touched? (get-in @form [:touched input-name]) + error (get-in @form [:errors input-name]) - value (get-in @form [:data name] "") + value (get-in @form [:data input-name] "") klass (dom/classnames :focus @focus? @@ -120,13 +135,13 @@ ) on-focus #(reset! focus? true) - on-change (fm/on-input-change form name trim) + on-change (fm/on-input-change form input-name trim) on-blur (fn [event] (reset! focus? false) - (when-not (get-in @form [:touched name]) - (swap! form assoc-in [:touched name] true))) + (when-not (get-in @form [:touched input-name]) + (swap! form assoc-in [:touched input-name] true))) props (-> props (dissoc :help-icon :form :trim) @@ -134,8 +149,7 @@ :on-focus on-focus :on-blur on-blur ;; :placeholder label - :on-change on-change - :type @type') + :on-change on-change) (obj/clj->props))] [:div.custom-input @@ -151,12 +165,14 @@ [:span.hint hint])]])) (mf/defc select - [{:keys [options label name form default] + [{:keys [options label form default] :as props :or {default ""}}] - (let [form (or form (mf/use-ctx form-ctx)) - value (get-in @form [:data name] default) + (let [input-name (get props :name) + + form (or form (mf/use-ctx form-ctx)) + value (get-in @form [:data input-name] default) cvalue (d/seek #(= value (:value %)) options) - on-change (fm/on-input-change form name)] + on-change (fm/on-input-change form input-name)] [:div.custom-select [:select {:value value diff --git a/frontend/src/app/util/forms.cljs b/frontend/src/app/util/forms.cljs index 94e1a385e..d7465bd48 100644 --- a/frontend/src/app/util/forms.cljs +++ b/frontend/src/app/util/forms.cljs @@ -89,7 +89,6 @@ (mf/set-ref-val! state-ref new-value) (render inc)) - ISwap (-swap! [self f] (let [f (wrap-update-fn f opts)] @@ -119,7 +118,10 @@ ([form field trim?] (fn [event] (let [target (dom/get-target event) - value (dom/get-value target)] + value (if (or (= (.-type target) "checkbox") + (= (.-type target) "radio")) + (.-checked target) + (dom/get-value target))] (swap! form (fn [state] (-> state (assoc-in [:data field] (if trim? (str/trim value) value))