mirror of
https://github.com/penpot/penpot.git
synced 2026-09-06 20:18:39 +00:00
✨ Show nitrate checkout error on subscription page
When the Stripe checkout fails to start, the subscription page now shows an inline error in the Business Nitrate card under the CTA instead of a toast. When the post-payment activation fails, the toast message is updated to point users to support@penpot.app. The nitrate-form modal also passed a URI object to build-nitrate-callback-urls while the underlying append-query-param relied on lambdaisland's u/parse, which only accepts strings. Switched to the local u/uri helper so both strings and URI records work, so failures opened from the modal land on the subscription page.
This commit is contained in:
parent
10a0e9e78c
commit
f79cfafae5
@ -13,6 +13,7 @@
|
|||||||
[app.util.i18n :refer [tr]]
|
[app.util.i18n :refer [tr]]
|
||||||
[app.util.storage :as storage]
|
[app.util.storage :as storage]
|
||||||
[beicon.v2.core :as rx]
|
[beicon.v2.core :as rx]
|
||||||
|
[cuerdas.core :as str]
|
||||||
[potok.v2.core :as ptk]))
|
[potok.v2.core :as ptk]))
|
||||||
|
|
||||||
(def ^:private nitrate-entry-active-key ::nitrate-entry-active)
|
(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))]
|
(let [href (dm/str "/control-center/licenses/billing?callback=" (js/encodeURIComponent go-to-subscription-url))]
|
||||||
(st/emit! (rt/nav-raw :href href))))
|
(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
|
(defn go-to-buy-nitrate-license
|
||||||
([subscription]
|
[subscription base-url]
|
||||||
(go-to-buy-nitrate-license subscription nil))
|
(let [{:keys [success-callback error-callback finish-error-callback cancel-callback]}
|
||||||
([subscription callback]
|
(build-nitrate-callback-urls base-url)
|
||||||
(let [params (cond-> {:subscription subscription}
|
params {:subscription subscription
|
||||||
callback (assoc :callback callback))
|
:callback success-callback
|
||||||
href (dm/str "/control-center/licenses/start?" (u/map->query-string params))]
|
:error_callback error-callback
|
||||||
(st/emit! (rt/nav-raw :href href)))))
|
: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
|
(defn fetch-connectivity
|
||||||
[]
|
[]
|
||||||
|
|||||||
@ -40,7 +40,8 @@
|
|||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps form)
|
(mf/deps form)
|
||||||
(fn []
|
(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
|
on-activate-click
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
|
|||||||
@ -4,12 +4,12 @@
|
|||||||
[app.common.data.macros :as dm]
|
[app.common.data.macros :as dm]
|
||||||
[app.common.schema :as sm]
|
[app.common.schema :as sm]
|
||||||
[app.common.time :as ct]
|
[app.common.time :as ct]
|
||||||
[app.common.uri :as u]
|
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.main.data.auth :as da]
|
[app.main.data.auth :as da]
|
||||||
[app.main.data.event :as ev]
|
[app.main.data.event :as ev]
|
||||||
[app.main.data.modal :as modal]
|
[app.main.data.modal :as modal]
|
||||||
[app.main.data.nitrate :as dnt]
|
[app.main.data.nitrate :as dnt]
|
||||||
|
[app.main.data.notifications :as ntf]
|
||||||
[app.main.refs :as refs]
|
[app.main.refs :as refs]
|
||||||
[app.main.router :as rt]
|
[app.main.router :as rt]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
@ -39,7 +39,8 @@
|
|||||||
code-action
|
code-action
|
||||||
editors
|
editors
|
||||||
recommended
|
recommended
|
||||||
show-button-cta]}]
|
show-button-cta
|
||||||
|
inline-error]}]
|
||||||
|
|
||||||
(let [has-trial? (and cta-link-trial cta-text-trial)
|
(let [has-trial? (and cta-link-trial cta-text-trial)
|
||||||
has-cta-with-icon? (and cta-link-with-icon cta-text-with-icon)
|
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})))}
|
#(st/emit! (modal/show {:type :nitrate-code-activation})))}
|
||||||
(if (= code-action :activate)
|
(if (= code-action :activate)
|
||||||
(tr "subscription.settings.activate-by-code")
|
(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?]
|
(defn- get-subscription-name [subscription-type subscribe-to-trial?]
|
||||||
(if subscribe-to-trial?
|
(if subscribe-to-trial?
|
||||||
@ -399,6 +402,30 @@
|
|||||||
(= params-subscription "subscribed-to-penpot-enterprise")
|
(= params-subscription "subscribed-to-penpot-enterprise")
|
||||||
(= params-subscription "subscribed-to-penpot-nitrate"))
|
(= 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?
|
success-modal-is-trial?
|
||||||
(-> route :params :query :trial)
|
(-> route :params :query :trial)
|
||||||
|
|
||||||
@ -485,10 +512,26 @@
|
|||||||
(mf/with-effect [authenticated?
|
(mf/with-effect [authenticated?
|
||||||
show-subscription-success-modal?
|
show-subscription-success-modal?
|
||||||
show-trial-subscription-modal?
|
show-trial-subscription-modal?
|
||||||
|
show-nitrate-start-error?
|
||||||
success-modal-is-trial?
|
success-modal-is-trial?
|
||||||
|
nitrate-toast-message
|
||||||
|
nitrate-toast-level
|
||||||
subscription]
|
subscription]
|
||||||
(when ^boolean authenticated?
|
(when ^boolean authenticated?
|
||||||
|
(when ^boolean show-nitrate-start-error?
|
||||||
|
(reset! nitrate-start-error* true))
|
||||||
(cond
|
(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?
|
^boolean show-trial-subscription-modal?
|
||||||
|
|
||||||
(st/emit!
|
(st/emit!
|
||||||
@ -676,7 +719,8 @@
|
|||||||
:cta-text-with-icon (tr "subscription.settings.more-information")
|
:cta-text-with-icon (tr "subscription.settings.more-information")
|
||||||
:cta-link-with-icon go-to-pricing-page
|
:cta-link-with-icon go-to-pricing-page
|
||||||
:code-action :activate
|
: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
|
(def ^:private schema:nitrate-form
|
||||||
@ -703,13 +747,8 @@
|
|||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps form)
|
(mf/deps form)
|
||||||
(fn []
|
(fn []
|
||||||
(let [subscription (-> @form :clean-data :subscription name)
|
(let [subscription (-> @form :clean-data :subscription name)]
|
||||||
return-url (dm/str
|
(dnt/go-to-buy-nitrate-license subscription (rt/get-current-href)))))]
|
||||||
(rt/get-current-href)
|
|
||||||
"?"
|
|
||||||
(u/map->query-string
|
|
||||||
{:subscription "subscribed-to-penpot-nitrate"}))]
|
|
||||||
(dnt/go-to-buy-nitrate-license subscription return-url))))]
|
|
||||||
|
|
||||||
[:div {:class (stl/css :modal-overlay)}
|
[:div {:class (stl/css :modal-overlay)}
|
||||||
[:div {:class (stl/css :modal-dialog)}
|
[:div {:class (stl/css :modal-dialog)}
|
||||||
|
|||||||
@ -194,6 +194,13 @@
|
|||||||
margin-block-start: var(--sp-xl);
|
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 {
|
.modal-overlay {
|
||||||
@extend %modal-overlay-base;
|
@extend %modal-overlay-base;
|
||||||
}
|
}
|
||||||
|
|||||||
45
frontend/test/frontend_tests/data/nitrate_test.cljs
Normal file
45
frontend/test/frontend_tests/data/nitrate_test.cljs
Normal file
@ -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))))))
|
||||||
@ -3,6 +3,7 @@
|
|||||||
[cljs.test :as t]
|
[cljs.test :as t]
|
||||||
[frontend-tests.basic-shapes-test]
|
[frontend-tests.basic-shapes-test]
|
||||||
[frontend-tests.copy-as-svg-test]
|
[frontend-tests.copy-as-svg-test]
|
||||||
|
[frontend-tests.data.nitrate-test]
|
||||||
[frontend-tests.data.repo-test]
|
[frontend-tests.data.repo-test]
|
||||||
[frontend-tests.data.uploads-test]
|
[frontend-tests.data.uploads-test]
|
||||||
[frontend-tests.data.viewer-test]
|
[frontend-tests.data.viewer-test]
|
||||||
@ -49,6 +50,7 @@
|
|||||||
(t/run-tests
|
(t/run-tests
|
||||||
'frontend-tests.basic-shapes-test
|
'frontend-tests.basic-shapes-test
|
||||||
'frontend-tests.copy-as-svg-test
|
'frontend-tests.copy-as-svg-test
|
||||||
|
'frontend-tests.data.nitrate-test
|
||||||
'frontend-tests.data.repo-test
|
'frontend-tests.data.repo-test
|
||||||
'frontend-tests.errors-test
|
'frontend-tests.errors-test
|
||||||
'frontend-tests.main-errors-test
|
'frontend-tests.main-errors-test
|
||||||
|
|||||||
@ -5166,6 +5166,15 @@ msgstr "Inviting people while on the Unlimited plan"
|
|||||||
msgid "subscription.dashboard.upgrade-plan.power-up"
|
msgid "subscription.dashboard.upgrade-plan.power-up"
|
||||||
msgstr "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
|
#: 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"
|
msgid "subscription.labels"
|
||||||
msgstr "Subscription"
|
msgstr "Subscription"
|
||||||
|
|||||||
@ -5084,6 +5084,15 @@ msgstr "Invita a personas mientras estás en el plan Unlimited"
|
|||||||
msgid "subscription.dashboard.upgrade-plan.power-up"
|
msgid "subscription.dashboard.upgrade-plan.power-up"
|
||||||
msgstr "Mejora"
|
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
|
#: 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"
|
msgid "subscription.labels"
|
||||||
msgstr "Suscripción"
|
msgstr "Suscripción"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user