diff --git a/frontend/src/app/main/data/nitrate.cljs b/frontend/src/app/main/data/nitrate.cljs index cd4328191e..9f89218084 100644 --- a/frontend/src/app/main/data/nitrate.cljs +++ b/frontend/src/app/main/data/nitrate.cljs @@ -13,6 +13,7 @@ [app.util.i18n :refer [tr]] [app.util.storage :as storage] [beicon.v2.core :as rx] + [cuerdas.core :as str] [potok.v2.core :as ptk])) (def ^:private nitrate-entry-active-key ::nitrate-entry-active) @@ -74,14 +75,48 @@ (let [href (dm/str "/control-center/licenses/billing?callback=" (js/encodeURIComponent go-to-subscription-url))] (st/emit! (rt/nav-raw :href href)))) +(def nitrate-checkout-error-token "nitrate-checkout-error") +(def nitrate-checkout-finish-error-token "nitrate-checkout-finish-error") +(def nitrate-checkout-cancelled-token "nitrate-checkout-cancelled") + +(defn- append-query-param + [url key value] + (let [assoc-q (fn [u] + (update u :query + (fn [q] + (-> (u/query-string->map (or q "")) + (assoc (name key) value) + u/map->query-string)))) + parsed (u/uri url) + fragment (:fragment parsed)] + (if (str/blank? fragment) + (str (assoc-q parsed)) + (-> parsed + (assoc :fragment (str (assoc-q (u/parse fragment)))) + str)))) + +(defn build-nitrate-callback-urls + "Build the success/error/cancel callback URLs from a base URL by appending + a `subscription` query param identifying the outcome." + [base-url] + (let [build (fn [token] + (append-query-param base-url :subscription token))] + {:success-callback (build "subscribed-to-penpot-nitrate") + :error-callback (build nitrate-checkout-error-token) + :finish-error-callback (build nitrate-checkout-finish-error-token) + :cancel-callback (build nitrate-checkout-cancelled-token)})) + (defn go-to-buy-nitrate-license - ([subscription] - (go-to-buy-nitrate-license subscription nil)) - ([subscription callback] - (let [params (cond-> {:subscription subscription} - callback (assoc :callback callback)) - href (dm/str "/control-center/licenses/start?" (u/map->query-string params))] - (st/emit! (rt/nav-raw :href href))))) + [subscription base-url] + (let [{:keys [success-callback error-callback finish-error-callback cancel-callback]} + (build-nitrate-callback-urls base-url) + params {:subscription subscription + :callback success-callback + :error_callback error-callback + :finish_error_callback finish-error-callback + :cancel_callback cancel-callback} + href (dm/str "/control-center/licenses/start?" (u/map->query-string params))] + (st/emit! (rt/nav-raw :href href)))) (defn fetch-connectivity [] diff --git a/frontend/src/app/main/ui/nitrate/nitrate_form.cljs b/frontend/src/app/main/ui/nitrate/nitrate_form.cljs index 13143d5337..bb2bfecadd 100644 --- a/frontend/src/app/main/ui/nitrate/nitrate_form.cljs +++ b/frontend/src/app/main/ui/nitrate/nitrate_form.cljs @@ -40,7 +40,8 @@ (mf/use-fn (mf/deps form) (fn [] - (dnt/go-to-buy-nitrate-license (-> @form :clean-data :subscription name)))) + (let [subscription (-> @form :clean-data :subscription name)] + (dnt/go-to-buy-nitrate-license subscription dnt/go-to-subscription-url)))) on-activate-click (mf/use-fn diff --git a/frontend/src/app/main/ui/settings/subscription.cljs b/frontend/src/app/main/ui/settings/subscription.cljs index 51d5df31ba..972189e6b8 100644 --- a/frontend/src/app/main/ui/settings/subscription.cljs +++ b/frontend/src/app/main/ui/settings/subscription.cljs @@ -4,12 +4,12 @@ [app.common.data.macros :as dm] [app.common.schema :as sm] [app.common.time :as ct] - [app.common.uri :as u] [app.config :as cf] [app.main.data.auth :as da] [app.main.data.event :as ev] [app.main.data.modal :as modal] [app.main.data.nitrate :as dnt] + [app.main.data.notifications :as ntf] [app.main.refs :as refs] [app.main.router :as rt] [app.main.store :as st] @@ -39,7 +39,8 @@ code-action editors recommended - show-button-cta]}] + show-button-cta + inline-error]}] (let [has-trial? (and cta-link-trial cta-text-trial) has-cta-with-icon? (and cta-link-with-icon cta-text-with-icon) @@ -100,7 +101,9 @@ #(st/emit! (modal/show {:type :nitrate-code-activation})))} (if (= code-action :activate) (tr "subscription.settings.activate-by-code") - (tr "nitrate.subscription.settings.renew-with-code"))])])) + (tr "nitrate.subscription.settings.renew-with-code"))]) + (when inline-error + [:p {:class (stl/css :inline-error)} inline-error])])) (defn- get-subscription-name [subscription-type subscribe-to-trial?] (if subscribe-to-trial? @@ -399,6 +402,30 @@ (= params-subscription "subscribed-to-penpot-enterprise") (= params-subscription "subscribed-to-penpot-nitrate")) + nitrate-toast-message + (condp = params-subscription + dnt/nitrate-checkout-finish-error-token (tr "subscription.error.nitrate.checkout-finish-failed") + dnt/nitrate-checkout-cancelled-token (tr "subscription.error.nitrate.checkout-cancelled") + nil) + + nitrate-toast-level + (cond + (= params-subscription dnt/nitrate-checkout-cancelled-token) :info + (some? nitrate-toast-message) :error) + + show-nitrate-start-error? + (= params-subscription dnt/nitrate-checkout-error-token) + + nitrate-start-error* + (mf/use-state false) + + nitrate-start-error? + (deref nitrate-start-error*) + + nitrate-start-error-message + (when nitrate-start-error? + (tr "subscription.error.nitrate.checkout-failed")) + success-modal-is-trial? (-> route :params :query :trial) @@ -485,10 +512,26 @@ (mf/with-effect [authenticated? show-subscription-success-modal? show-trial-subscription-modal? + show-nitrate-start-error? success-modal-is-trial? + nitrate-toast-message + nitrate-toast-level subscription] (when ^boolean authenticated? + (when ^boolean show-nitrate-start-error? + (reset! nitrate-start-error* true)) (cond + (some? nitrate-toast-message) + (st/emit! + (ntf/show {:content nitrate-toast-message + :type :toast + :level nitrate-toast-level + :timeout 7000}) + (rt/nav :settings-subscription {} {::rt/replace true})) + + ^boolean show-nitrate-start-error? + (st/emit! (rt/nav :settings-subscription {} {::rt/replace true})) + ^boolean show-trial-subscription-modal? (st/emit! @@ -676,7 +719,8 @@ :cta-text-with-icon (tr "subscription.settings.more-information") :cta-link-with-icon go-to-pricing-page :code-action :activate - :show-button-cta (not nitrate-license)}])]]])) + :show-button-cta (not nitrate-license) + :inline-error nitrate-start-error-message}])]]])) (def ^:private schema:nitrate-form @@ -703,13 +747,8 @@ (mf/use-fn (mf/deps form) (fn [] - (let [subscription (-> @form :clean-data :subscription name) - return-url (dm/str - (rt/get-current-href) - "?" - (u/map->query-string - {:subscription "subscribed-to-penpot-nitrate"}))] - (dnt/go-to-buy-nitrate-license subscription return-url))))] + (let [subscription (-> @form :clean-data :subscription name)] + (dnt/go-to-buy-nitrate-license subscription (rt/get-current-href)))))] [:div {:class (stl/css :modal-overlay)} [:div {:class (stl/css :modal-dialog)} diff --git a/frontend/src/app/main/ui/settings/subscription.scss b/frontend/src/app/main/ui/settings/subscription.scss index 09f1f8e370..436a512042 100644 --- a/frontend/src/app/main/ui/settings/subscription.scss +++ b/frontend/src/app/main/ui/settings/subscription.scss @@ -194,6 +194,13 @@ margin-block-start: var(--sp-xl); } +.inline-error { + @include t.use-typography("body-small"); + + color: var(--color-foreground-error); + margin-block: var(--sp-s) 0; +} + .modal-overlay { @extend %modal-overlay-base; } diff --git a/frontend/test/frontend_tests/data/nitrate_test.cljs b/frontend/test/frontend_tests/data/nitrate_test.cljs new file mode 100644 index 0000000000..cfe179b0ea --- /dev/null +++ b/frontend/test/frontend_tests/data/nitrate_test.cljs @@ -0,0 +1,45 @@ +;; 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 frontend-tests.data.nitrate-test + (:require + [app.common.uri :as u] + [app.main.data.nitrate :as dnt] + [cljs.test :as t :include-macros true])) + +(t/deftest build-nitrate-callback-urls-preserves-hash-query + (t/testing "appends subscription to an existing query inside the hash route" + (let [callbacks (dnt/build-nitrate-callback-urls + "https://localhost:3449/#/dashboard/recent?team-id=e6666530-0216-81c8-8007-f17d6087b74f")] + (t/is (= "https://localhost:3449/#/dashboard/recent?team-id=e6666530-0216-81c8-8007-f17d6087b74f&subscription=subscribed-to-penpot-nitrate" + (:success-callback callbacks))) + (t/is (= "https://localhost:3449/#/dashboard/recent?team-id=e6666530-0216-81c8-8007-f17d6087b74f&subscription=nitrate-checkout-error" + (:error-callback callbacks))) + (t/is (= "https://localhost:3449/#/dashboard/recent?team-id=e6666530-0216-81c8-8007-f17d6087b74f&subscription=nitrate-checkout-finish-error" + (:finish-error-callback callbacks))) + (t/is (= "https://localhost:3449/#/dashboard/recent?team-id=e6666530-0216-81c8-8007-f17d6087b74f&subscription=nitrate-checkout-cancelled" + (:cancel-callback callbacks)))))) + +(t/deftest build-nitrate-callback-urls-adds-hash-query-when-missing + (t/testing "adds a hash query when the route has no query string yet" + (let [callbacks (dnt/build-nitrate-callback-urls + "https://localhost:3449/#/settings/subscriptions")] + (t/is (= "https://localhost:3449/#/settings/subscriptions?subscription=subscribed-to-penpot-nitrate" + (:success-callback callbacks)))))) + +(t/deftest build-nitrate-callback-urls-adds-regular-query-without-hash + (t/testing "falls back to the regular URL query when there is no hash route" + (let [callbacks (dnt/build-nitrate-callback-urls + "https://localhost:3449/control-center/licenses/billing?foo=bar")] + (t/is (= "https://localhost:3449/control-center/licenses/billing?foo=bar&subscription=subscribed-to-penpot-nitrate" + (:success-callback callbacks)))))) + +(t/deftest build-nitrate-callback-urls-accepts-uri-object + (t/testing "accepts a URI object as base url (used by the nitrate-form modal)" + (let [callbacks (dnt/build-nitrate-callback-urls + (u/uri "https://localhost:3449/#/settings/subscriptions"))] + (t/is (= "https://localhost:3449/#/settings/subscriptions?subscription=nitrate-checkout-error" + (:error-callback callbacks)))))) diff --git a/frontend/test/frontend_tests/runner.cljs b/frontend/test/frontend_tests/runner.cljs index 5f9078f910..065ed3bf80 100644 --- a/frontend/test/frontend_tests/runner.cljs +++ b/frontend/test/frontend_tests/runner.cljs @@ -3,6 +3,7 @@ [cljs.test :as t] [frontend-tests.basic-shapes-test] [frontend-tests.copy-as-svg-test] + [frontend-tests.data.nitrate-test] [frontend-tests.data.repo-test] [frontend-tests.data.uploads-test] [frontend-tests.data.viewer-test] @@ -49,6 +50,7 @@ (t/run-tests 'frontend-tests.basic-shapes-test 'frontend-tests.copy-as-svg-test + 'frontend-tests.data.nitrate-test 'frontend-tests.data.repo-test 'frontend-tests.errors-test 'frontend-tests.main-errors-test diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 0c524f9777..6b14173adc 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -5166,6 +5166,15 @@ msgstr "Inviting people while on the Unlimited plan" msgid "subscription.dashboard.upgrade-plan.power-up" msgstr "Power up" +msgid "subscription.error.nitrate.checkout-cancelled" +msgstr "Payment was not completed. Try again whenever you're ready." + +msgid "subscription.error.nitrate.checkout-failed" +msgstr "We couldn't start the checkout. Please try again. If the problem persists, contact us: support@penpot.app." + +msgid "subscription.error.nitrate.checkout-finish-failed" +msgstr "We’re having trouble activating your subscription. Please try again. If the problem persists, contact us: support@penpot.app." + #: src/app/main/ui/settings/sidebar.cljs:116, src/app/main/ui/settings/subscription.cljs:425, src/app/main/ui/settings/subscription.cljs:462 msgid "subscription.labels" msgstr "Subscription" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 4bbf92126e..f81279149f 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -5084,6 +5084,15 @@ msgstr "Invita a personas mientras estás en el plan Unlimited" msgid "subscription.dashboard.upgrade-plan.power-up" msgstr "Mejora" +msgid "subscription.error.nitrate.checkout-cancelled" +msgstr "El pago no se completó. Inténtalo de nuevo cuando quieras." + +msgid "subscription.error.nitrate.checkout-failed" +msgstr "No hemos podido iniciar el proceso de pago. Inténtalo de nuevo. Si el problema persiste, contáctanos: support@penpot.app." + +msgid "subscription.error.nitrate.checkout-finish-failed" +msgstr "Estamos teniendo problemas para activar tu suscripción. Inténtalo de nuevo. Si el problema persiste, contáctanos: support@penpot.app." + #: src/app/main/ui/settings/sidebar.cljs:116, src/app/main/ui/settings/subscription.cljs:425, src/app/main/ui/settings/subscription.cljs:462 msgid "subscription.labels" msgstr "Suscripción"