A little polishing, make upgrade banner work when not logged in
This commit is contained in:
parent
7cff44b647
commit
f945fb4cdd
15 changed files with 98 additions and 121 deletions
|
@ -201,7 +201,6 @@ func execServe(c *cli.Context) error {
|
||||||
|
|
||||||
webRootIsApp := webRoot == "app"
|
webRootIsApp := webRoot == "app"
|
||||||
enableWeb := webRoot != "disable"
|
enableWeb := webRoot != "disable"
|
||||||
enablePayments := stripeSecretKey != ""
|
|
||||||
|
|
||||||
// Default auth permissions
|
// Default auth permissions
|
||||||
authDefault, err := user.ParsePermission(authDefaultAccess)
|
authDefault, err := user.ParsePermission(authDefaultAccess)
|
||||||
|
@ -298,7 +297,6 @@ func execServe(c *cli.Context) error {
|
||||||
conf.EnableWeb = enableWeb
|
conf.EnableWeb = enableWeb
|
||||||
conf.EnableSignup = enableSignup
|
conf.EnableSignup = enableSignup
|
||||||
conf.EnableLogin = enableLogin
|
conf.EnableLogin = enableLogin
|
||||||
conf.EnablePayments = enablePayments
|
|
||||||
conf.EnableReservations = enableReservations
|
conf.EnableReservations = enableReservations
|
||||||
conf.Version = c.App.Version
|
conf.Version = c.App.Version
|
||||||
|
|
||||||
|
|
|
@ -1037,8 +1037,8 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`).
|
||||||
| `enable-signup` | `NTFY_SIGNUP` | *boolean* (`true` or `false`) | `false` | Allows users to sign up via the web app, or API |
|
| `enable-signup` | `NTFY_SIGNUP` | *boolean* (`true` or `false`) | `false` | Allows users to sign up via the web app, or API |
|
||||||
| `enable-login` | `NTFY_LOGIN` | *boolean* (`true` or `false`) | `false` | Allows users to log in via the web app, or API |
|
| `enable-login` | `NTFY_LOGIN` | *boolean* (`true` or `false`) | `false` | Allows users to log in via the web app, or API |
|
||||||
| `enable-reservations` | `NTFY_RESERVATIONS` | *boolean* (`true` or `false`) | `false` | Allows users to reserve topics (if their tier allows it) |
|
| `enable-reservations` | `NTFY_RESERVATIONS` | *boolean* (`true` or `false`) | `false` | Allows users to reserve topics (if their tier allows it) |
|
||||||
| `enable-payments` | `NTFY_PAYMENTS` | *boolean* (`true` or `false`) | `false` | Enables payments integration (_preliminary option, may change_) |
|
| `stripe-secret-key` | `NTFY_STRIPE_SECRET_KEY` | *string* | - | Payments: Key used for the Stripe API communication, this enables payments |
|
||||||
|
| `stripe-webhook-key` | `NTFY_STRIPE_WEBHOOK_KEY` | *string* | - | Payments: Key required to validate the authenticity of incoming webhooks from Stripe |
|
||||||
|
|
||||||
The format for a *duration* is: `<number>(smh)`, e.g. 30s, 20m or 1h.
|
The format for a *duration* is: `<number>(smh)`, e.g. 30s, 20m or 1h.
|
||||||
The format for a *size* is: `<number>(GMK)`, e.g. 1G, 200M or 4000k.
|
The format for a *size* is: `<number>(GMK)`, e.g. 1G, 200M or 4000k.
|
||||||
|
|
|
@ -115,7 +115,6 @@ type Config struct {
|
||||||
EnableWeb bool
|
EnableWeb bool
|
||||||
EnableSignup bool // Enable creation of accounts via API and UI
|
EnableSignup bool // Enable creation of accounts via API and UI
|
||||||
EnableLogin bool
|
EnableLogin bool
|
||||||
EnablePayments bool
|
|
||||||
EnableReservations bool // Allow users with role "user" to own/reserve topics
|
EnableReservations bool // Allow users with role "user" to own/reserve topics
|
||||||
Version string // injected by App
|
Version string // injected by App
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,7 +59,8 @@ var (
|
||||||
errHTTPBadRequestPermissionInvalid = &errHTTP{40025, http.StatusBadRequest, "invalid request: incorrect permission string", ""}
|
errHTTPBadRequestPermissionInvalid = &errHTTP{40025, http.StatusBadRequest, "invalid request: incorrect permission string", ""}
|
||||||
errHTTPBadRequestMakesNoSenseForAdmin = &errHTTP{40026, http.StatusBadRequest, "invalid request: this makes no sense for admins", ""}
|
errHTTPBadRequestMakesNoSenseForAdmin = &errHTTP{40026, http.StatusBadRequest, "invalid request: this makes no sense for admins", ""}
|
||||||
errHTTPBadRequestNotAPaidUser = &errHTTP{40027, http.StatusBadRequest, "invalid request: not a paid user", ""}
|
errHTTPBadRequestNotAPaidUser = &errHTTP{40027, http.StatusBadRequest, "invalid request: not a paid user", ""}
|
||||||
errHTTPBadRequestInvalidStripeRequest = &errHTTP{40028, http.StatusBadRequest, "invalid request: not a valid Stripe request", ""}
|
errHTTPBadRequestBillingRequestInvalid = &errHTTP{40028, http.StatusBadRequest, "invalid request: not a valid billing request", ""}
|
||||||
|
errHTTPBadRequestBillingSubscriptionExists = &errHTTP{40029, http.StatusBadRequest, "invalid request: billing subscription already exists", ""}
|
||||||
errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", ""}
|
errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", ""}
|
||||||
errHTTPUnauthorized = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication"}
|
errHTTPUnauthorized = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication"}
|
||||||
errHTTPForbidden = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication"}
|
errHTTPForbidden = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication"}
|
||||||
|
|
|
@ -89,12 +89,7 @@ const (
|
||||||
WHERE time <= ? AND published = 0
|
WHERE time <= ? AND published = 0
|
||||||
ORDER BY time, id
|
ORDER BY time, id
|
||||||
`
|
`
|
||||||
selectMessagesExpiredQuery = `
|
selectMessagesExpiredQuery = `SELECT mid FROM messages WHERE expires <= ? AND published = 1`
|
||||||
SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, encoding
|
|
||||||
FROM messages
|
|
||||||
WHERE expires <= ? AND published = 1
|
|
||||||
ORDER BY time, id
|
|
||||||
`
|
|
||||||
updateMessagePublishedQuery = `UPDATE messages SET published = 1 WHERE mid = ?`
|
updateMessagePublishedQuery = `UPDATE messages SET published = 1 WHERE mid = ?`
|
||||||
selectMessagesCountQuery = `SELECT COUNT(*) FROM messages`
|
selectMessagesCountQuery = `SELECT COUNT(*) FROM messages`
|
||||||
selectMessageCountPerTopicQuery = `SELECT topic, COUNT(*) FROM messages GROUP BY topic`
|
selectMessageCountPerTopicQuery = `SELECT topic, COUNT(*) FROM messages GROUP BY topic`
|
||||||
|
@ -431,12 +426,25 @@ func (c *messageCache) MessagesDue() ([]*message, error) {
|
||||||
return readMessages(rows)
|
return readMessages(rows)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *messageCache) MessagesExpired() ([]*message, error) {
|
// MessagesExpired returns a list of IDs for messages that have expires (should be deleted)
|
||||||
|
func (c *messageCache) MessagesExpired() ([]string, error) {
|
||||||
rows, err := c.db.Query(selectMessagesExpiredQuery, time.Now().Unix())
|
rows, err := c.db.Query(selectMessagesExpiredQuery, time.Now().Unix())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return readMessages(rows)
|
defer rows.Close()
|
||||||
|
ids := make([]string, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
var id string
|
||||||
|
if err := rows.Scan(&id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ids = append(ids, id)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ids, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *messageCache) MarkPublished(m *message) error {
|
func (c *messageCache) MarkPublished(m *message) error {
|
||||||
|
|
|
@ -270,13 +270,9 @@ func testCachePrune(t *testing.T, c *messageCache) {
|
||||||
require.Equal(t, 2, counts["mytopic"])
|
require.Equal(t, 2, counts["mytopic"])
|
||||||
require.Equal(t, 1, counts["another_topic"])
|
require.Equal(t, 1, counts["another_topic"])
|
||||||
|
|
||||||
expiredMessages, err := c.MessagesExpired()
|
expiredMessageIDs, err := c.MessagesExpired()
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
ids := make([]string, 0)
|
require.Nil(t, c.DeleteMessages(expiredMessageIDs...))
|
||||||
for _, m := range expiredMessages {
|
|
||||||
ids = append(ids, m.ID)
|
|
||||||
}
|
|
||||||
require.Nil(t, c.DeleteMessages(ids...))
|
|
||||||
|
|
||||||
counts, err = c.MessageCounts()
|
counts, err = c.MessageCounts()
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
|
|
|
@ -43,10 +43,13 @@ import (
|
||||||
- delete subscription when account deleted
|
- delete subscription when account deleted
|
||||||
- delete messages + reserved topics on ResetTier
|
- delete messages + reserved topics on ResetTier
|
||||||
|
|
||||||
|
- move v1/account/tiers to v1/tiers
|
||||||
|
|
||||||
Limits & rate limiting:
|
Limits & rate limiting:
|
||||||
users without tier: should the stats be persisted? are they meaningful?
|
users without tier: should the stats be persisted? are they meaningful?
|
||||||
-> test that the visitor is based on the IP address!
|
-> test that the visitor is based on the IP address!
|
||||||
login/account endpoints
|
login/account endpoints
|
||||||
|
when ResetStats() is run, reset messagesLimiter (and others)?
|
||||||
update last_seen when API is accessed
|
update last_seen when API is accessed
|
||||||
Make sure account endpoints make sense for admins
|
Make sure account endpoints make sense for admins
|
||||||
|
|
||||||
|
@ -54,11 +57,10 @@ import (
|
||||||
- flicker of upgrade banner
|
- flicker of upgrade banner
|
||||||
- JS constants
|
- JS constants
|
||||||
Sync:
|
Sync:
|
||||||
- "mute" setting
|
|
||||||
- figure out what settings are "web" or "phone"
|
|
||||||
- sync problems with "deleteAfter=0" and "displayName="
|
- sync problems with "deleteAfter=0" and "displayName="
|
||||||
Delete visitor when tier is changed to refresh rate limiters
|
Delete visitor when tier is changed to refresh rate limiters
|
||||||
Tests:
|
Tests:
|
||||||
|
- Payment endpoints (make mocks)
|
||||||
- Change tier from higher to lower tier (delete reservations)
|
- Change tier from higher to lower tier (delete reservations)
|
||||||
- Message rate limiting and reset tests
|
- Message rate limiting and reset tests
|
||||||
- test that the visitor is based on the IP address when a user has no tier
|
- test that the visitor is based on the IP address when a user has no tier
|
||||||
|
@ -104,13 +106,13 @@ var (
|
||||||
accountPath = "/account"
|
accountPath = "/account"
|
||||||
matrixPushPath = "/_matrix/push/v1/notify"
|
matrixPushPath = "/_matrix/push/v1/notify"
|
||||||
apiHealthPath = "/v1/health"
|
apiHealthPath = "/v1/health"
|
||||||
|
apiTiers = "/v1/tiers"
|
||||||
apiAccountPath = "/v1/account"
|
apiAccountPath = "/v1/account"
|
||||||
apiAccountTokenPath = "/v1/account/token"
|
apiAccountTokenPath = "/v1/account/token"
|
||||||
apiAccountPasswordPath = "/v1/account/password"
|
apiAccountPasswordPath = "/v1/account/password"
|
||||||
apiAccountSettingsPath = "/v1/account/settings"
|
apiAccountSettingsPath = "/v1/account/settings"
|
||||||
apiAccountSubscriptionPath = "/v1/account/subscription"
|
apiAccountSubscriptionPath = "/v1/account/subscription"
|
||||||
apiAccountReservationPath = "/v1/account/reservation"
|
apiAccountReservationPath = "/v1/account/reservation"
|
||||||
apiAccountBillingTiersPath = "/v1/account/billing/tiers"
|
|
||||||
apiAccountBillingPortalPath = "/v1/account/billing/portal"
|
apiAccountBillingPortalPath = "/v1/account/billing/portal"
|
||||||
apiAccountBillingWebhookPath = "/v1/account/billing/webhook"
|
apiAccountBillingWebhookPath = "/v1/account/billing/webhook"
|
||||||
apiAccountBillingSubscriptionPath = "/v1/account/billing/subscription"
|
apiAccountBillingSubscriptionPath = "/v1/account/billing/subscription"
|
||||||
|
@ -378,20 +380,20 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit
|
||||||
return s.ensureUser(s.withAccountSync(s.handleAccountReservationAdd))(w, r, v)
|
return s.ensureUser(s.withAccountSync(s.handleAccountReservationAdd))(w, r, v)
|
||||||
} else if r.Method == http.MethodDelete && apiAccountReservationSingleRegex.MatchString(r.URL.Path) {
|
} else if r.Method == http.MethodDelete && apiAccountReservationSingleRegex.MatchString(r.URL.Path) {
|
||||||
return s.ensureUser(s.withAccountSync(s.handleAccountReservationDelete))(w, r, v)
|
return s.ensureUser(s.withAccountSync(s.handleAccountReservationDelete))(w, r, v)
|
||||||
} else if r.Method == http.MethodGet && r.URL.Path == apiAccountBillingTiersPath {
|
|
||||||
return s.ensurePaymentsEnabled(s.handleAccountBillingTiersGet)(w, r, v)
|
|
||||||
} else if r.Method == http.MethodPost && r.URL.Path == apiAccountBillingSubscriptionPath {
|
} else if r.Method == http.MethodPost && r.URL.Path == apiAccountBillingSubscriptionPath {
|
||||||
return s.ensurePaymentsEnabled(s.ensureUser(s.handleAccountBillingSubscriptionCreate))(w, r, v) // Account sync via incoming Stripe webhook
|
return s.ensurePaymentsEnabled(s.ensureUser(s.handleAccountBillingSubscriptionCreate))(w, r, v) // Account sync via incoming Stripe webhook
|
||||||
} else if r.Method == http.MethodGet && apiAccountBillingSubscriptionCheckoutSuccessRegex.MatchString(r.URL.Path) {
|
} else if r.Method == http.MethodGet && apiAccountBillingSubscriptionCheckoutSuccessRegex.MatchString(r.URL.Path) {
|
||||||
return s.ensurePaymentsEnabled(s.ensureUserManager(s.handleAccountBillingSubscriptionCreateSuccess))(w, r, v) // No user context!
|
return s.ensurePaymentsEnabled(s.ensureUserManager(s.handleAccountBillingSubscriptionCreateSuccess))(w, r, v) // No user context!
|
||||||
} else if r.Method == http.MethodPut && r.URL.Path == apiAccountBillingSubscriptionPath {
|
} else if r.Method == http.MethodPut && r.URL.Path == apiAccountBillingSubscriptionPath {
|
||||||
return s.ensurePaymentsEnabled(s.ensureUser(s.handleAccountBillingSubscriptionUpdate))(w, r, v) // Account sync via incoming Stripe webhook
|
return s.ensurePaymentsEnabled(s.ensureStripeCustomer(s.handleAccountBillingSubscriptionUpdate))(w, r, v) // Account sync via incoming Stripe webhook
|
||||||
} else if r.Method == http.MethodDelete && r.URL.Path == apiAccountBillingSubscriptionPath {
|
} else if r.Method == http.MethodDelete && r.URL.Path == apiAccountBillingSubscriptionPath {
|
||||||
return s.ensurePaymentsEnabled(s.ensureStripeCustomer(s.handleAccountBillingSubscriptionDelete))(w, r, v) // Account sync via incoming Stripe webhook
|
return s.ensurePaymentsEnabled(s.ensureStripeCustomer(s.handleAccountBillingSubscriptionDelete))(w, r, v) // Account sync via incoming Stripe webhook
|
||||||
} else if r.Method == http.MethodPost && r.URL.Path == apiAccountBillingPortalPath {
|
} else if r.Method == http.MethodPost && r.URL.Path == apiAccountBillingPortalPath {
|
||||||
return s.ensurePaymentsEnabled(s.ensureStripeCustomer(s.handleAccountBillingPortalSessionCreate))(w, r, v)
|
return s.ensurePaymentsEnabled(s.ensureStripeCustomer(s.handleAccountBillingPortalSessionCreate))(w, r, v)
|
||||||
} else if r.Method == http.MethodPost && r.URL.Path == apiAccountBillingWebhookPath {
|
} else if r.Method == http.MethodPost && r.URL.Path == apiAccountBillingWebhookPath {
|
||||||
return s.ensurePaymentsEnabled(s.ensureUserManager(s.handleAccountBillingWebhook))(w, r, v)
|
return s.ensurePaymentsEnabled(s.ensureUserManager(s.handleAccountBillingWebhook))(w, r, v) // This request comes from Stripe!
|
||||||
|
} else if r.Method == http.MethodGet && r.URL.Path == apiTiers {
|
||||||
|
return s.ensurePaymentsEnabled(s.handleBillingTiersGet)(w, r, v)
|
||||||
} else if r.Method == http.MethodGet && r.URL.Path == matrixPushPath {
|
} else if r.Method == http.MethodGet && r.URL.Path == matrixPushPath {
|
||||||
return s.handleMatrixDiscovery(w)
|
return s.handleMatrixDiscovery(w)
|
||||||
} else if r.Method == http.MethodGet && staticRegex.MatchString(r.URL.Path) {
|
} else if r.Method == http.MethodGet && staticRegex.MatchString(r.URL.Path) {
|
||||||
|
@ -480,7 +482,7 @@ func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, _ *visi
|
||||||
AppRoot: appRoot,
|
AppRoot: appRoot,
|
||||||
EnableLogin: s.config.EnableLogin,
|
EnableLogin: s.config.EnableLogin,
|
||||||
EnableSignup: s.config.EnableSignup,
|
EnableSignup: s.config.EnableSignup,
|
||||||
EnablePayments: s.config.EnablePayments,
|
EnablePayments: s.config.StripeSecretKey != "",
|
||||||
EnableReservations: s.config.EnableReservations,
|
EnableReservations: s.config.EnableReservations,
|
||||||
DisallowedTopics: disallowedTopics,
|
DisallowedTopics: disallowedTopics,
|
||||||
}
|
}
|
||||||
|
@ -1271,18 +1273,14 @@ func (s *Server) execManager() {
|
||||||
|
|
||||||
// DeleteMessages message cache
|
// DeleteMessages message cache
|
||||||
log.Debug("Manager: Pruning messages")
|
log.Debug("Manager: Pruning messages")
|
||||||
expiredMessages, err := s.messageCache.MessagesExpired()
|
expiredMessageIDs, err := s.messageCache.MessagesExpired()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("Manager: Error retrieving expired messages: %s", err.Error())
|
log.Warn("Manager: Error retrieving expired messages: %s", err.Error())
|
||||||
} else if len(expiredMessages) > 0 {
|
} else if len(expiredMessageIDs) > 0 {
|
||||||
ids := make([]string, 0)
|
if err := s.fileCache.Remove(expiredMessageIDs...); err != nil {
|
||||||
for _, m := range expiredMessages {
|
|
||||||
ids = append(ids, m.ID)
|
|
||||||
}
|
|
||||||
if err := s.fileCache.Remove(ids...); err != nil {
|
|
||||||
log.Warn("Manager: Error deleting attachments for expired messages: %s", err.Error())
|
log.Warn("Manager: Error deleting attachments for expired messages: %s", err.Error())
|
||||||
}
|
}
|
||||||
if err := s.messageCache.DeleteMessages(ids...); err != nil {
|
if err := s.messageCache.DeleteMessages(expiredMessageIDs...); err != nil {
|
||||||
log.Warn("Manager: Error marking attachments deleted: %s", err.Error())
|
log.Warn("Manager: Error marking attachments deleted: %s", err.Error())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -1359,6 +1357,8 @@ func (s *Server) runManager() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// runStatsResetter runs once a day (usually midnight UTC) to reset all the visitor's message and
|
||||||
|
// email counters. The stats are used to display the counters in the web app, as well as for rate limiting.
|
||||||
func (s *Server) runStatsResetter() {
|
func (s *Server) runStatsResetter() {
|
||||||
for {
|
for {
|
||||||
runAt := util.NextOccurrenceUTC(s.config.VisitorStatsResetTime, time.Now())
|
runAt := util.NextOccurrenceUTC(s.config.VisitorStatsResetTime, time.Now())
|
||||||
|
|
|
@ -33,7 +33,7 @@ func (s *Server) ensureUser(next handleFunc) handleFunc {
|
||||||
|
|
||||||
func (s *Server) ensurePaymentsEnabled(next handleFunc) handleFunc {
|
func (s *Server) ensurePaymentsEnabled(next handleFunc) handleFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
return func(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
||||||
if !s.config.EnablePayments {
|
if s.config.StripeSecretKey == "" {
|
||||||
return errHTTPNotFound
|
return errHTTPNotFound
|
||||||
}
|
}
|
||||||
return next(w, r, v)
|
return next(w, r, v)
|
||||||
|
|
|
@ -25,11 +25,15 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
errNotAPaidTier = errors.New("tier does not have Stripe price identifier")
|
errNotAPaidTier = errors.New("tier does not have billing price identifier")
|
||||||
|
errMultipleBillingSubscriptions = errors.New("cannot have multiple billing subscriptions")
|
||||||
|
errNoBillingSubscription = errors.New("user does not have an active billing subscription")
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Server) handleAccountBillingTiersGet(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
// handleBillingTiersGet returns all available paid tiers, and the free tier. This is to populate the upgrade dialog
|
||||||
tiers, err := v.userManager.Tiers()
|
// in the UI. Note that this endpoint does NOT have a user context (no v.user!).
|
||||||
|
func (s *Server) handleBillingTiersGet(w http.ResponseWriter, _ *http.Request, _ *visitor) error {
|
||||||
|
tiers, err := s.userManager.Tiers()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -92,7 +96,7 @@ func (s *Server) handleAccountBillingTiersGet(w http.ResponseWriter, r *http.Req
|
||||||
// will be updated by a subsequent webhook from Stripe, once the subscription becomes active.
|
// will be updated by a subsequent webhook from Stripe, once the subscription becomes active.
|
||||||
func (s *Server) handleAccountBillingSubscriptionCreate(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
func (s *Server) handleAccountBillingSubscriptionCreate(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
||||||
if v.user.Billing.StripeSubscriptionID != "" {
|
if v.user.Billing.StripeSubscriptionID != "" {
|
||||||
return errors.New("subscription already exists") //FIXME
|
return errHTTPBadRequestBillingSubscriptionExists
|
||||||
}
|
}
|
||||||
req, err := readJSONWithLimit[apiAccountBillingSubscriptionChangeRequest](r.Body, jsonBodyBytesLimit)
|
req, err := readJSONWithLimit[apiAccountBillingSubscriptionChangeRequest](r.Body, jsonBodyBytesLimit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -112,7 +116,7 @@ func (s *Server) handleAccountBillingSubscriptionCreate(w http.ResponseWriter, r
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
} else if stripeCustomer.Subscriptions != nil && len(stripeCustomer.Subscriptions.Data) > 0 {
|
} else if stripeCustomer.Subscriptions != nil && len(stripeCustomer.Subscriptions.Data) > 0 {
|
||||||
return errors.New("customer cannot have more than one subscription") //FIXME
|
return errMultipleBillingSubscriptions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
successURL := s.config.BaseURL + apiAccountBillingSubscriptionCheckoutSuccessTemplate
|
successURL := s.config.BaseURL + apiAccountBillingSubscriptionCheckoutSuccessTemplate
|
||||||
|
@ -157,15 +161,15 @@ func (s *Server) handleAccountBillingSubscriptionCreateSuccess(w http.ResponseWr
|
||||||
sess, err := session.Get(sessionID, nil) // FIXME how do I rate limit this?
|
sess, err := session.Get(sessionID, nil) // FIXME how do I rate limit this?
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("Stripe: %s", err)
|
log.Warn("Stripe: %s", err)
|
||||||
return errHTTPBadRequestInvalidStripeRequest
|
return errHTTPBadRequestBillingRequestInvalid
|
||||||
} else if sess.Customer == nil || sess.Subscription == nil || sess.ClientReferenceID == "" {
|
} else if sess.Customer == nil || sess.Subscription == nil || sess.ClientReferenceID == "" {
|
||||||
return wrapErrHTTP(errHTTPBadRequestInvalidStripeRequest, "customer or subscription not found")
|
return wrapErrHTTP(errHTTPBadRequestBillingRequestInvalid, "customer or subscription not found")
|
||||||
}
|
}
|
||||||
sub, err := subscription.Get(sess.Subscription.ID, nil)
|
sub, err := subscription.Get(sess.Subscription.ID, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
} else if sub.Items == nil || len(sub.Items.Data) != 1 || sub.Items.Data[0].Price == nil {
|
} else if sub.Items == nil || len(sub.Items.Data) != 1 || sub.Items.Data[0].Price == nil {
|
||||||
return wrapErrHTTP(errHTTPBadRequestInvalidStripeRequest, "more than one line item in existing subscription")
|
return wrapErrHTTP(errHTTPBadRequestBillingRequestInvalid, "more than one line item in existing subscription")
|
||||||
}
|
}
|
||||||
tier, err := s.userManager.TierByStripePrice(sub.Items.Data[0].Price.ID)
|
tier, err := s.userManager.TierByStripePrice(sub.Items.Data[0].Price.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -186,7 +190,7 @@ func (s *Server) handleAccountBillingSubscriptionCreateSuccess(w http.ResponseWr
|
||||||
// a user's tier accordingly. This endpoint only works if there is an existing subscription.
|
// a user's tier accordingly. This endpoint only works if there is an existing subscription.
|
||||||
func (s *Server) handleAccountBillingSubscriptionUpdate(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
func (s *Server) handleAccountBillingSubscriptionUpdate(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
||||||
if v.user.Billing.StripeSubscriptionID == "" {
|
if v.user.Billing.StripeSubscriptionID == "" {
|
||||||
return errors.New("no existing subscription for user")
|
return errNoBillingSubscription
|
||||||
}
|
}
|
||||||
req, err := readJSONWithLimit[apiAccountBillingSubscriptionChangeRequest](r.Body, jsonBodyBytesLimit)
|
req, err := readJSONWithLimit[apiAccountBillingSubscriptionChangeRequest](r.Body, jsonBodyBytesLimit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -226,9 +230,6 @@ func (s *Server) handleAccountBillingSubscriptionUpdate(w http.ResponseWriter, r
|
||||||
// handleAccountBillingSubscriptionDelete facilitates downgrading a paid user to a tier-less user,
|
// handleAccountBillingSubscriptionDelete facilitates downgrading a paid user to a tier-less user,
|
||||||
// and cancelling the Stripe subscription entirely
|
// and cancelling the Stripe subscription entirely
|
||||||
func (s *Server) handleAccountBillingSubscriptionDelete(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
func (s *Server) handleAccountBillingSubscriptionDelete(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
||||||
if v.user.Billing.StripeCustomerID == "" {
|
|
||||||
return errHTTPBadRequestNotAPaidUser
|
|
||||||
}
|
|
||||||
if v.user.Billing.StripeSubscriptionID != "" {
|
if v.user.Billing.StripeSubscriptionID != "" {
|
||||||
params := &stripe.SubscriptionParams{
|
params := &stripe.SubscriptionParams{
|
||||||
CancelAtPeriodEnd: stripe.Bool(true),
|
CancelAtPeriodEnd: stripe.Bool(true),
|
||||||
|
@ -269,11 +270,13 @@ func (s *Server) handleAccountBillingPortalSessionCreate(w http.ResponseWriter,
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleAccountBillingWebhook handles incoming Stripe webhooks. It mainly keeps the local user database in sync
|
||||||
|
// with the Stripe view of the world. This endpoint is authorized via the Stripe webhook secret. Note that the
|
||||||
|
// visitor (v) in this endpoint is the Stripe API, so we don't have v.user available.
|
||||||
func (s *Server) handleAccountBillingWebhook(w http.ResponseWriter, r *http.Request, _ *visitor) error {
|
func (s *Server) handleAccountBillingWebhook(w http.ResponseWriter, r *http.Request, _ *visitor) error {
|
||||||
// Note that the visitor (v) in this endpoint is the Stripe API, so we don't have v.user available
|
|
||||||
stripeSignature := r.Header.Get("Stripe-Signature")
|
stripeSignature := r.Header.Get("Stripe-Signature")
|
||||||
if stripeSignature == "" {
|
if stripeSignature == "" {
|
||||||
return errHTTPBadRequestInvalidStripeRequest
|
return errHTTPBadRequestBillingRequestInvalid
|
||||||
}
|
}
|
||||||
body, err := util.Peek(r.Body, stripeBodyBytesLimit)
|
body, err := util.Peek(r.Body, stripeBodyBytesLimit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -283,9 +286,9 @@ func (s *Server) handleAccountBillingWebhook(w http.ResponseWriter, r *http.Requ
|
||||||
}
|
}
|
||||||
event, err := webhook.ConstructEvent(body.PeekedBytes, stripeSignature, s.config.StripeWebhookKey)
|
event, err := webhook.ConstructEvent(body.PeekedBytes, stripeSignature, s.config.StripeWebhookKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errHTTPBadRequestInvalidStripeRequest
|
return errHTTPBadRequestBillingRequestInvalid
|
||||||
} else if event.Data == nil || event.Data.Raw == nil {
|
} else if event.Data == nil || event.Data.Raw == nil {
|
||||||
return errHTTPBadRequestInvalidStripeRequest
|
return errHTTPBadRequestBillingRequestInvalid
|
||||||
}
|
}
|
||||||
log.Info("Stripe: webhook event %s received", event.Type)
|
log.Info("Stripe: webhook event %s received", event.Type)
|
||||||
switch event.Type {
|
switch event.Type {
|
||||||
|
@ -306,7 +309,7 @@ func (s *Server) handleAccountBillingWebhookSubscriptionUpdated(event json.RawMe
|
||||||
cancelAt := gjson.GetBytes(event, "cancel_at")
|
cancelAt := gjson.GetBytes(event, "cancel_at")
|
||||||
priceID := gjson.GetBytes(event, "items.data.0.price.id")
|
priceID := gjson.GetBytes(event, "items.data.0.price.id")
|
||||||
if !subscriptionID.Exists() || !status.Exists() || !currentPeriodEnd.Exists() || !cancelAt.Exists() || !priceID.Exists() {
|
if !subscriptionID.Exists() || !status.Exists() || !currentPeriodEnd.Exists() || !cancelAt.Exists() || !priceID.Exists() {
|
||||||
return errHTTPBadRequestInvalidStripeRequest
|
return errHTTPBadRequestBillingRequestInvalid
|
||||||
}
|
}
|
||||||
log.Info("Stripe: customer %s: Updating subscription to status %s, with price %s", customerID.String(), status, priceID)
|
log.Info("Stripe: customer %s: Updating subscription to status %s, with price %s", customerID.String(), status, priceID)
|
||||||
u, err := s.userManager.UserByStripeCustomer(customerID.String())
|
u, err := s.userManager.UserByStripeCustomer(customerID.String())
|
||||||
|
@ -327,7 +330,7 @@ func (s *Server) handleAccountBillingWebhookSubscriptionUpdated(event json.RawMe
|
||||||
func (s *Server) handleAccountBillingWebhookSubscriptionDeleted(event json.RawMessage) error {
|
func (s *Server) handleAccountBillingWebhookSubscriptionDeleted(event json.RawMessage) error {
|
||||||
customerID := gjson.GetBytes(event, "customer")
|
customerID := gjson.GetBytes(event, "customer")
|
||||||
if !customerID.Exists() {
|
if !customerID.Exists() {
|
||||||
return errHTTPBadRequestInvalidStripeRequest
|
return errHTTPBadRequestBillingRequestInvalid
|
||||||
}
|
}
|
||||||
log.Info("Stripe: customer %s: subscription deleted, downgrading to unpaid tier", customerID.String())
|
log.Info("Stripe: customer %s: subscription deleted, downgrading to unpaid tier", customerID.String())
|
||||||
u, err := s.userManager.UserByStripeCustomer(customerID.String())
|
u, err := s.userManager.UserByStripeCustomer(customerID.String())
|
||||||
|
|
|
@ -745,7 +745,7 @@ func TestServer_Auth_ViaQuery(t *testing.T) {
|
||||||
func TestServer_StatsResetter(t *testing.T) {
|
func TestServer_StatsResetter(t *testing.T) {
|
||||||
c := newTestConfigWithAuthFile(t)
|
c := newTestConfigWithAuthFile(t)
|
||||||
c.AuthDefault = user.PermissionDenyAll
|
c.AuthDefault = user.PermissionDenyAll
|
||||||
c.VisitorStatsResetTime = time.Now().Add(time.Second)
|
c.VisitorStatsResetTime = time.Now().Add(2 * time.Second)
|
||||||
s := newTestServer(t, c)
|
s := newTestServer(t, c)
|
||||||
go s.runStatsResetter()
|
go s.runStatsResetter()
|
||||||
|
|
||||||
|
@ -773,8 +773,8 @@ func TestServer_StatsResetter(t *testing.T) {
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
require.Equal(t, int64(5), account.Stats.Messages)
|
require.Equal(t, int64(5), account.Stats.Messages)
|
||||||
|
|
||||||
// Start stats resetter
|
// Wait for stats resetter to run
|
||||||
time.Sleep(1200 * time.Millisecond)
|
time.Sleep(2200 * time.Millisecond)
|
||||||
|
|
||||||
// User stats show 0 messages now!
|
// User stats show 0 messages now!
|
||||||
response = request(t, s, "GET", "/v1/account", "", nil)
|
response = request(t, s, "GET", "/v1/account", "", nil)
|
||||||
|
@ -1325,7 +1325,7 @@ func TestServer_PublishAttachmentTooLargeBodyVisitorAttachmentTotalSizeLimit(t *
|
||||||
require.Equal(t, 41301, err.Code)
|
require.Equal(t, 41301, err.Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestServer_PublishAttachmentAndPrune(t *testing.T) {
|
func TestServer_PublishAttachmentAndExpire(t *testing.T) {
|
||||||
content := util.RandomString(5000) // > 4096
|
content := util.RandomString(5000) // > 4096
|
||||||
|
|
||||||
c := newTestConfig(t)
|
c := newTestConfig(t)
|
||||||
|
|
|
@ -208,6 +208,7 @@ func (v *visitor) ResetStats() {
|
||||||
if v.user != nil {
|
if v.user != nil {
|
||||||
v.user.Stats.Messages = 0
|
v.user.Stats.Messages = 0
|
||||||
v.user.Stats.Emails = 0
|
v.user.Stats.Emails = 0
|
||||||
|
// v.messagesLimiter = ... // FIXME
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -211,6 +211,7 @@
|
||||||
"account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} total storage",
|
"account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} total storage",
|
||||||
"account_upgrade_dialog_tier_selected_label": "Selected",
|
"account_upgrade_dialog_tier_selected_label": "Selected",
|
||||||
"account_upgrade_dialog_button_cancel": "Cancel",
|
"account_upgrade_dialog_button_cancel": "Cancel",
|
||||||
|
"account_upgrade_dialog_button_redirect_signup": "Sign up now",
|
||||||
"account_upgrade_dialog_button_pay_now": "Pay now and subscribe",
|
"account_upgrade_dialog_button_pay_now": "Pay now and subscribe",
|
||||||
"account_upgrade_dialog_button_cancel_subscription": "Cancel subscription",
|
"account_upgrade_dialog_button_cancel_subscription": "Cancel subscription",
|
||||||
"account_upgrade_dialog_button_update_subscription": "Update subscription",
|
"account_upgrade_dialog_button_update_subscription": "Update subscription",
|
||||||
|
|
|
@ -8,7 +8,7 @@ import {
|
||||||
accountTokenUrl,
|
accountTokenUrl,
|
||||||
accountUrl, maybeWithAuth, topicUrl,
|
accountUrl, maybeWithAuth, topicUrl,
|
||||||
withBasicAuth,
|
withBasicAuth,
|
||||||
withBearerAuth, accountBillingSubscriptionUrl, accountBillingPortalUrl, accountBillingTiersUrl
|
withBearerAuth, accountBillingSubscriptionUrl, accountBillingPortalUrl, tiersUrl
|
||||||
} from "./utils";
|
} from "./utils";
|
||||||
import session from "./Session";
|
import session from "./Session";
|
||||||
import subscriptionManager from "./SubscriptionManager";
|
import subscriptionManager from "./SubscriptionManager";
|
||||||
|
@ -170,7 +170,6 @@ class AccountApi {
|
||||||
} else if (response.status !== 200) {
|
} else if (response.status !== 200) {
|
||||||
throw new Error(`Unexpected server response ${response.status}`);
|
throw new Error(`Unexpected server response ${response.status}`);
|
||||||
}
|
}
|
||||||
this.triggerChange(); // Dangle!
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async addSubscription(payload) {
|
async addSubscription(payload) {
|
||||||
|
@ -189,7 +188,6 @@ class AccountApi {
|
||||||
}
|
}
|
||||||
const subscription = await response.json();
|
const subscription = await response.json();
|
||||||
console.log(`[AccountApi] Subscription`, subscription);
|
console.log(`[AccountApi] Subscription`, subscription);
|
||||||
this.triggerChange(); // Dangle!
|
|
||||||
return subscription;
|
return subscription;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -209,7 +207,6 @@ class AccountApi {
|
||||||
}
|
}
|
||||||
const subscription = await response.json();
|
const subscription = await response.json();
|
||||||
console.log(`[AccountApi] Subscription`, subscription);
|
console.log(`[AccountApi] Subscription`, subscription);
|
||||||
this.triggerChange(); // Dangle!
|
|
||||||
return subscription;
|
return subscription;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -225,7 +222,6 @@ class AccountApi {
|
||||||
} else if (response.status !== 200) {
|
} else if (response.status !== 200) {
|
||||||
throw new Error(`Unexpected server response ${response.status}`);
|
throw new Error(`Unexpected server response ${response.status}`);
|
||||||
}
|
}
|
||||||
this.triggerChange(); // Dangle!
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async upsertReservation(topic, everyone) {
|
async upsertReservation(topic, everyone) {
|
||||||
|
@ -246,7 +242,6 @@ class AccountApi {
|
||||||
} else if (response.status !== 200) {
|
} else if (response.status !== 200) {
|
||||||
throw new Error(`Unexpected server response ${response.status}`);
|
throw new Error(`Unexpected server response ${response.status}`);
|
||||||
}
|
}
|
||||||
this.triggerChange(); // Dangle!
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteReservation(topic) {
|
async deleteReservation(topic) {
|
||||||
|
@ -261,18 +256,13 @@ class AccountApi {
|
||||||
} else if (response.status !== 200) {
|
} else if (response.status !== 200) {
|
||||||
throw new Error(`Unexpected server response ${response.status}`);
|
throw new Error(`Unexpected server response ${response.status}`);
|
||||||
}
|
}
|
||||||
this.triggerChange(); // Dangle!
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async billingTiers() {
|
async billingTiers() {
|
||||||
const url = accountBillingTiersUrl(config.base_url);
|
const url = tiersUrl(config.base_url);
|
||||||
console.log(`[AccountApi] Fetching billing tiers`);
|
console.log(`[AccountApi] Fetching billing tiers`);
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url); // No auth needed!
|
||||||
headers: withBearerAuth({}, session.token())
|
if (response.status !== 200) {
|
||||||
});
|
|
||||||
if (response.status === 401 || response.status === 403) {
|
|
||||||
throw new UnauthorizedError();
|
|
||||||
} else if (response.status !== 200) {
|
|
||||||
throw new Error(`Unexpected server response ${response.status}`);
|
throw new Error(`Unexpected server response ${response.status}`);
|
||||||
}
|
}
|
||||||
return await response.json();
|
return await response.json();
|
||||||
|
@ -367,35 +357,6 @@ class AccountApi {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async triggerChange() {
|
|
||||||
return null;
|
|
||||||
const account = await this.get();
|
|
||||||
if (!account.sync_topic) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const url = topicUrl(config.base_url, account.sync_topic);
|
|
||||||
console.log(`[AccountApi] Triggering account change to ${url}`);
|
|
||||||
const user = await userManager.get(config.base_url);
|
|
||||||
const headers = {
|
|
||||||
Cache: "no" // We really don't need to store this!
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: 'PUT',
|
|
||||||
body: JSON.stringify({
|
|
||||||
event: "sync",
|
|
||||||
source: this.identity
|
|
||||||
}),
|
|
||||||
headers: maybeWithAuth(headers, user)
|
|
||||||
});
|
|
||||||
if (response.status < 200 || response.status > 299) {
|
|
||||||
throw new Error(`Unexpected response: ${response.status}`);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log(`[AccountApi] Publishing to sync topic failed`, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
startWorker() {
|
startWorker() {
|
||||||
if (this.timer !== null) {
|
if (this.timer !== null) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -28,7 +28,7 @@ export const accountReservationUrl = (baseUrl) => `${baseUrl}/v1/account/reserva
|
||||||
export const accountReservationSingleUrl = (baseUrl, topic) => `${baseUrl}/v1/account/reservation/${topic}`;
|
export const accountReservationSingleUrl = (baseUrl, topic) => `${baseUrl}/v1/account/reservation/${topic}`;
|
||||||
export const accountBillingSubscriptionUrl = (baseUrl) => `${baseUrl}/v1/account/billing/subscription`;
|
export const accountBillingSubscriptionUrl = (baseUrl) => `${baseUrl}/v1/account/billing/subscription`;
|
||||||
export const accountBillingPortalUrl = (baseUrl) => `${baseUrl}/v1/account/billing/portal`;
|
export const accountBillingPortalUrl = (baseUrl) => `${baseUrl}/v1/account/billing/portal`;
|
||||||
export const accountBillingTiersUrl = (baseUrl) => `${baseUrl}/v1/account/billing/tiers`;
|
export const tiersUrl = (baseUrl) => `${baseUrl}/v1/tiers`;
|
||||||
export const shortUrl = (url) => url.replaceAll(/https?:\/\//g, "");
|
export const shortUrl = (url) => url.replaceAll(/https?:\/\//g, "");
|
||||||
export const expandUrl = (url) => [`https://${url}`, `http://${url}`];
|
export const expandUrl = (url) => [`https://${url}`, `http://${url}`];
|
||||||
export const expandSecureUrl = (url) => `https://${url}`;
|
export const expandSecureUrl = (url) => `https://${url}`;
|
||||||
|
|
|
@ -24,7 +24,7 @@ import Box from "@mui/material/Box";
|
||||||
|
|
||||||
const UpgradeDialog = (props) => {
|
const UpgradeDialog = (props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { account } = useContext(AccountContext);
|
const { account } = useContext(AccountContext); // May be undefined!
|
||||||
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
||||||
const [tiers, setTiers] = useState(null);
|
const [tiers, setTiers] = useState(null);
|
||||||
const [newTier, setNewTier] = useState(account?.tier?.code); // May be undefined
|
const [newTier, setNewTier] = useState(account?.tier?.code); // May be undefined
|
||||||
|
@ -37,28 +37,32 @@ const UpgradeDialog = (props) => {
|
||||||
})();
|
})();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!account || !tiers) {
|
if (!tiers) {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentTier = account.tier?.code; // May be undefined
|
const currentTier = account?.tier?.code; // May be undefined
|
||||||
let action, submitButtonLabel, submitButtonEnabled;
|
let action, submitButtonLabel, submitButtonEnabled;
|
||||||
if (currentTier === newTier) {
|
if (!account) {
|
||||||
|
submitButtonLabel = t("account_upgrade_dialog_button_redirect_signup");
|
||||||
|
submitButtonEnabled = true;
|
||||||
|
action = Action.REDIRECT_SIGNUP;
|
||||||
|
} else if (currentTier === newTier) {
|
||||||
submitButtonLabel = t("account_upgrade_dialog_button_update_subscription");
|
submitButtonLabel = t("account_upgrade_dialog_button_update_subscription");
|
||||||
submitButtonEnabled = false;
|
submitButtonEnabled = false;
|
||||||
action = null;
|
action = null;
|
||||||
} else if (!currentTier) {
|
} else if (!currentTier) {
|
||||||
submitButtonLabel = t("account_upgrade_dialog_button_pay_now");
|
submitButtonLabel = t("account_upgrade_dialog_button_pay_now");
|
||||||
submitButtonEnabled = true;
|
submitButtonEnabled = true;
|
||||||
action = Action.CREATE;
|
action = Action.CREATE_SUBSCRIPTION;
|
||||||
} else if (!newTier) {
|
} else if (!newTier) {
|
||||||
submitButtonLabel = t("account_upgrade_dialog_button_cancel_subscription");
|
submitButtonLabel = t("account_upgrade_dialog_button_cancel_subscription");
|
||||||
submitButtonEnabled = true;
|
submitButtonEnabled = true;
|
||||||
action = Action.CANCEL;
|
action = Action.CANCEL_SUBSCRIPTION;
|
||||||
} else {
|
} else {
|
||||||
submitButtonLabel = t("account_upgrade_dialog_button_update_subscription");
|
submitButtonLabel = t("account_upgrade_dialog_button_update_subscription");
|
||||||
submitButtonEnabled = true;
|
submitButtonEnabled = true;
|
||||||
action = Action.UPDATE;
|
action = Action.UPDATE_SUBSCRIPTION;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
|
@ -66,14 +70,18 @@ const UpgradeDialog = (props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
|
if (action === Action.REDIRECT_SIGNUP) {
|
||||||
|
window.location.href = routes.signup;
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
if (action === Action.CREATE) {
|
if (action === Action.CREATE_SUBSCRIPTION) {
|
||||||
const response = await accountApi.createBillingSubscription(newTier);
|
const response = await accountApi.createBillingSubscription(newTier);
|
||||||
window.location.href = response.redirect_url;
|
window.location.href = response.redirect_url;
|
||||||
} else if (action === Action.UPDATE) {
|
} else if (action === Action.UPDATE_SUBSCRIPTION) {
|
||||||
await accountApi.updateBillingSubscription(newTier);
|
await accountApi.updateBillingSubscription(newTier);
|
||||||
} else if (action === Action.CANCEL) {
|
} else if (action === Action.CANCEL_SUBSCRIPTION) {
|
||||||
await accountApi.deleteBillingSubscription();
|
await accountApi.deleteBillingSubscription();
|
||||||
}
|
}
|
||||||
props.onCancel();
|
props.onCancel();
|
||||||
|
@ -113,14 +121,14 @@ const UpgradeDialog = (props) => {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{action === Action.CANCEL &&
|
{action === Action.CANCEL_SUBSCRIPTION &&
|
||||||
<Alert severity="warning">
|
<Alert severity="warning">
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="account_upgrade_dialog_cancel_warning"
|
i18nKey="account_upgrade_dialog_cancel_warning"
|
||||||
values={{ date: formatShortDate(account.billing.paid_until) }} />
|
values={{ date: formatShortDate(account?.billing?.paid_until || 0) }} />
|
||||||
</Alert>
|
</Alert>
|
||||||
}
|
}
|
||||||
{currentTier && (!action || action === Action.UPDATE) &&
|
{currentTier && (!action || action === Action.UPDATE_SUBSCRIPTION) &&
|
||||||
<Alert severity="info">
|
<Alert severity="info">
|
||||||
<Trans i18nKey="account_upgrade_dialog_proration_info" />
|
<Trans i18nKey="account_upgrade_dialog_proration_info" />
|
||||||
</Alert>
|
</Alert>
|
||||||
|
@ -148,8 +156,8 @@ const TierCard = (props) => {
|
||||||
flexShrink: 1,
|
flexShrink: 1,
|
||||||
flexBasis: 0,
|
flexBasis: 0,
|
||||||
borderRadius: "3px",
|
borderRadius: "3px",
|
||||||
"&:first-child": { ml: 0 },
|
"&:first-of-type": { ml: 0 },
|
||||||
"&:last-child": { mr: 0 },
|
"&:last-of-type": { mr: 0 },
|
||||||
...cardStyle
|
...cardStyle
|
||||||
}}>
|
}}>
|
||||||
<Card sx={{ height: "100%" }}>
|
<Card sx={{ height: "100%" }}>
|
||||||
|
@ -209,9 +217,10 @@ const FeatureItem = (props) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const Action = {
|
const Action = {
|
||||||
CREATE: 1,
|
REDIRECT_SIGNUP: 0,
|
||||||
UPDATE: 2,
|
CREATE_SUBSCRIPTION: 1,
|
||||||
CANCEL: 3
|
UPDATE_SUBSCRIPTION: 2,
|
||||||
|
CANCEL_SUBSCRIPTION: 3
|
||||||
};
|
};
|
||||||
|
|
||||||
export default UpgradeDialog;
|
export default UpgradeDialog;
|
||||||
|
|
Loading…
Reference in a new issue