From 7faed3ee1e2defdd68e9970aa6b69e574a323c02 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Mon, 16 Jan 2023 10:35:12 -0500 Subject: [PATCH] Add "Canceled" banner --- server/server.go | 2 ++ server/server_account.go | 1 + server/server_payments.go | 7 ++++++- server/types.go | 1 + user/manager.go | 18 ++++++++++-------- user/types.go | 1 + web/public/static/langs/en.json | 2 ++ web/src/app/utils.js | 5 +++++ web/src/components/Account.js | 14 +++++++++++--- 9 files changed, 39 insertions(+), 12 deletions(-) diff --git a/server/server.go b/server/server.go index 6489131..c8d9440 100644 --- a/server/server.go +++ b/server/server.go @@ -44,6 +44,8 @@ import ( - delete subscription when account deleted - remove tier.paid - add tier.visible + - fix tier selection boxes + - account sync after switching tiers Limits & rate limiting: users without tier: should the stats be persisted? are they meaningful? diff --git a/server/server_account.go b/server/server_account.go index fe7d4c1..66250db 100644 --- a/server/server_account.go +++ b/server/server_account.go @@ -97,6 +97,7 @@ func (s *Server) handleAccountGet(w http.ResponseWriter, _ *http.Request, v *vis Subscription: v.user.Billing.StripeSubscriptionID != "", Status: string(v.user.Billing.StripeSubscriptionStatus), PaidUntil: v.user.Billing.StripeSubscriptionPaidUntil.Unix(), + CancelAt: v.user.Billing.StripeSubscriptionCancelAt.Unix(), } } reservations, err := s.userManager.Reservations(v.user.Name) diff --git a/server/server_payments.go b/server/server_payments.go index 81e5221..b78a94a 100644 --- a/server/server_payments.go +++ b/server/server_payments.go @@ -62,6 +62,7 @@ func (s *Server) handleAccountBillingSubscriptionDelete(w http.ResponseWriter, r v.user.Billing.StripeSubscriptionID = "" v.user.Billing.StripeSubscriptionStatus = "" v.user.Billing.StripeSubscriptionPaidUntil = time.Unix(0, 0) + v.user.Billing.StripeSubscriptionCancelAt = time.Unix(0, 0) if err := s.userManager.ChangeBilling(v.user); err != nil { return err } @@ -170,6 +171,7 @@ func (s *Server) handleAccountCheckoutSessionSuccessGet(w http.ResponseWriter, r u.Billing.StripeSubscriptionID = sub.ID u.Billing.StripeSubscriptionStatus = sub.Status u.Billing.StripeSubscriptionPaidUntil = time.Unix(sub.CurrentPeriodEnd, 0) + u.Billing.StripeSubscriptionCancelAt = time.Unix(sub.CancelAt, 0) if err := s.userManager.ChangeBilling(u); err != nil { return err } @@ -240,8 +242,9 @@ func (s *Server) handleAccountBillingWebhook(w http.ResponseWriter, r *http.Requ func (s *Server) handleAccountBillingWebhookSubscriptionUpdated(stripeCustomerID string, event json.RawMessage) error { status := gjson.GetBytes(event, "status") currentPeriodEnd := gjson.GetBytes(event, "current_period_end") + cancelAt := gjson.GetBytes(event, "cancel_at") priceID := gjson.GetBytes(event, "items.data.0.price.id") - if !status.Exists() || !currentPeriodEnd.Exists() || !priceID.Exists() { + if !status.Exists() || !currentPeriodEnd.Exists() || !cancelAt.Exists() || !priceID.Exists() { return errHTTPBadRequestInvalidStripeRequest } log.Info("Stripe: customer %s: subscription updated to %s, with price %s", stripeCustomerID, status, priceID) @@ -258,6 +261,7 @@ func (s *Server) handleAccountBillingWebhookSubscriptionUpdated(stripeCustomerID } u.Billing.StripeSubscriptionStatus = stripe.SubscriptionStatus(status.String()) u.Billing.StripeSubscriptionPaidUntil = time.Unix(currentPeriodEnd.Int(), 0) + u.Billing.StripeSubscriptionCancelAt = time.Unix(cancelAt.Int(), 0) if err := s.userManager.ChangeBilling(u); err != nil { return err } @@ -280,6 +284,7 @@ func (s *Server) handleAccountBillingWebhookSubscriptionDeleted(stripeCustomerID u.Billing.StripeSubscriptionID = "" u.Billing.StripeSubscriptionStatus = "" u.Billing.StripeSubscriptionPaidUntil = time.Unix(0, 0) + u.Billing.StripeSubscriptionCancelAt = time.Unix(0, 0) if err := s.userManager.ChangeBilling(u); err != nil { return err } diff --git a/server/types.go b/server/types.go index cee114d..fc81a2a 100644 --- a/server/types.go +++ b/server/types.go @@ -273,6 +273,7 @@ type apiAccountBilling struct { Subscription bool `json:"subscription"` Status string `json:"status,omitempty"` PaidUntil int64 `json:"paid_until,omitempty"` + CancelAt int64 `json:"cancel_at,omitempty"` } type apiAccountResponse struct { diff --git a/user/manager.go b/user/manager.go index 7e50b4a..7b37b8f 100644 --- a/user/manager.go +++ b/user/manager.go @@ -63,7 +63,8 @@ const ( stripe_customer_id TEXT, stripe_subscription_id TEXT, stripe_subscription_status TEXT, - stripe_subscription_paid_until INT, + stripe_subscription_paid_until INT, + stripe_subscription_cancel_at INT, created_by TEXT NOT NULL, created_at INT NOT NULL, last_seen INT NOT NULL, @@ -103,20 +104,20 @@ const ( ` selectUserByNameQuery = ` - SELECT u.user, u.pass, u.role, u.prefs, u.sync_topic, u.stats_messages, u.stats_emails, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_paid_until, p.code, p.name, p.paid, p.messages_limit, p.messages_expiry_duration, p.emails_limit, p.reservations_limit, p.attachment_file_size_limit, p.attachment_total_size_limit, p.attachment_expiry_duration, p.stripe_price_id + SELECT u.user, u.pass, u.role, u.prefs, u.sync_topic, u.stats_messages, u.stats_emails, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_paid_until, u.stripe_subscription_cancel_at, p.code, p.name, p.paid, p.messages_limit, p.messages_expiry_duration, p.emails_limit, p.reservations_limit, p.attachment_file_size_limit, p.attachment_total_size_limit, p.attachment_expiry_duration, p.stripe_price_id FROM user u LEFT JOIN tier p on p.id = u.tier_id WHERE user = ? ` selectUserByTokenQuery = ` - SELECT u.user, u.pass, u.role, u.prefs, u.sync_topic, u.stats_messages, u.stats_emails, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_paid_until, p.code, p.name, p.paid, p.messages_limit, p.messages_expiry_duration, p.emails_limit, p.reservations_limit, p.attachment_file_size_limit, p.attachment_total_size_limit, p.attachment_expiry_duration, p.stripe_price_id + SELECT u.user, u.pass, u.role, u.prefs, u.sync_topic, u.stats_messages, u.stats_emails, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_paid_until, u.stripe_subscription_cancel_at , p.code, p.name, p.paid, p.messages_limit, p.messages_expiry_duration, p.emails_limit, p.reservations_limit, p.attachment_file_size_limit, p.attachment_total_size_limit, p.attachment_expiry_duration, p.stripe_price_id FROM user u JOIN user_token t on u.id = t.user_id LEFT JOIN tier p on p.id = u.tier_id WHERE t.token = ? AND t.expires >= ? ` selectUserByStripeCustomerIDQuery = ` - SELECT u.user, u.pass, u.role, u.prefs, u.sync_topic, u.stats_messages, u.stats_emails, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_paid_until, p.code, p.name, p.paid, p.messages_limit, p.messages_expiry_duration, p.emails_limit, p.reservations_limit, p.attachment_file_size_limit, p.attachment_total_size_limit, p.attachment_expiry_duration, p.stripe_price_id + SELECT u.user, u.pass, u.role, u.prefs, u.sync_topic, u.stats_messages, u.stats_emails, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_paid_until, u.stripe_subscription_cancel_at , p.code, p.name, p.paid, p.messages_limit, p.messages_expiry_duration, p.emails_limit, p.reservations_limit, p.attachment_file_size_limit, p.attachment_total_size_limit, p.attachment_expiry_duration, p.stripe_price_id FROM user u LEFT JOIN tier p on p.id = u.tier_id WHERE u.stripe_customer_id = ? @@ -236,7 +237,7 @@ const ( updateBillingQuery = ` UPDATE user - SET stripe_customer_id = ?, stripe_subscription_id = ?, stripe_subscription_status = ?, stripe_subscription_paid_until = ? + SET stripe_customer_id = ?, stripe_subscription_id = ?, stripe_subscription_status = ?, stripe_subscription_paid_until = ?, stripe_subscription_cancel_at = ? WHERE user = ? ` ) @@ -607,11 +608,11 @@ func (a *Manager) readUser(rows *sql.Rows) (*User, error) { var stripeCustomerID, stripeSubscriptionID, stripeSubscriptionStatus, stripePriceID, tierCode, tierName sql.NullString var paid sql.NullBool var messages, emails int64 - var messagesLimit, messagesExpiryDuration, emailsLimit, reservationsLimit, attachmentFileSizeLimit, attachmentTotalSizeLimit, attachmentExpiryDuration, stripeSubscriptionPaidUntil sql.NullInt64 + var messagesLimit, messagesExpiryDuration, emailsLimit, reservationsLimit, attachmentFileSizeLimit, attachmentTotalSizeLimit, attachmentExpiryDuration, stripeSubscriptionPaidUntil, stripeSubscriptionCancelAt sql.NullInt64 if !rows.Next() { return nil, ErrUserNotFound } - if err := rows.Scan(&username, &hash, &role, &prefs, &syncTopic, &messages, &emails, &stripeCustomerID, &stripeSubscriptionID, &stripeSubscriptionStatus, &stripeSubscriptionPaidUntil, &tierCode, &tierName, &paid, &messagesLimit, &messagesExpiryDuration, &emailsLimit, &reservationsLimit, &attachmentFileSizeLimit, &attachmentTotalSizeLimit, &attachmentExpiryDuration, &stripePriceID); err != nil { + if err := rows.Scan(&username, &hash, &role, &prefs, &syncTopic, &messages, &emails, &stripeCustomerID, &stripeSubscriptionID, &stripeSubscriptionStatus, &stripeSubscriptionPaidUntil, &stripeSubscriptionCancelAt, &tierCode, &tierName, &paid, &messagesLimit, &messagesExpiryDuration, &emailsLimit, &reservationsLimit, &attachmentFileSizeLimit, &attachmentTotalSizeLimit, &attachmentExpiryDuration, &stripePriceID); err != nil { return nil, err } else if err := rows.Err(); err != nil { return nil, err @@ -631,6 +632,7 @@ func (a *Manager) readUser(rows *sql.Rows) (*User, error) { StripeSubscriptionID: stripeSubscriptionID.String, // May be empty StripeSubscriptionStatus: stripe.SubscriptionStatus(stripeSubscriptionStatus.String), // May be empty StripeSubscriptionPaidUntil: time.Unix(stripeSubscriptionPaidUntil.Int64, 0), // May be zero + StripeSubscriptionCancelAt: time.Unix(stripeSubscriptionCancelAt.Int64, 0), // May be zero }, } if err := json.Unmarshal([]byte(prefs), user.Prefs); err != nil { @@ -875,7 +877,7 @@ func (a *Manager) CreateTier(tier *Tier) error { } func (a *Manager) ChangeBilling(user *User) error { - if _, err := a.db.Exec(updateBillingQuery, nullString(user.Billing.StripeCustomerID), nullString(user.Billing.StripeSubscriptionID), nullString(string(user.Billing.StripeSubscriptionStatus)), nullInt64(user.Billing.StripeSubscriptionPaidUntil.Unix()), user.Name); err != nil { + if _, err := a.db.Exec(updateBillingQuery, nullString(user.Billing.StripeCustomerID), nullString(user.Billing.StripeSubscriptionID), nullString(string(user.Billing.StripeSubscriptionStatus)), nullInt64(user.Billing.StripeSubscriptionPaidUntil.Unix()), nullInt64(user.Billing.StripeSubscriptionCancelAt.Unix()), user.Name); err != nil { return err } return nil diff --git a/user/types.go b/user/types.go index e9a689f..2aca565 100644 --- a/user/types.go +++ b/user/types.go @@ -90,6 +90,7 @@ type Billing struct { StripeSubscriptionID string StripeSubscriptionStatus stripe.SubscriptionStatus StripeSubscriptionPaidUntil time.Time + StripeSubscriptionCancelAt time.Time } // Grant is a struct that represents an access control entry to a topic by a user diff --git a/web/public/static/langs/en.json b/web/public/static/langs/en.json index f18aedf..56e6366 100644 --- a/web/public/static/langs/en.json +++ b/web/public/static/langs/en.json @@ -183,7 +183,9 @@ "account_usage_tier_none": "Basic", "account_usage_tier_upgrade_button": "Upgrade to Pro", "account_usage_tier_change_button": "Change", + "account_usage_tier_paid_until": "Subscription paid until {{date}}, and will auto-renew", "account_usage_tier_payment_overdue": "Your payment is overdue. Please update your payment method, or your account will be downgraded soon.", + "account_usage_tier_canceled_subscription": "Your subscription was canceled and will be downgraded to a free account on {{date}}.", "account_usage_manage_billing_button": "Manage billing", "account_usage_messages_title": "Published messages", "account_usage_emails_title": "Emails sent", diff --git a/web/src/app/utils.js b/web/src/app/utils.js index 8603ec5..b34af7e 100644 --- a/web/src/app/utils.js +++ b/web/src/app/utils.js @@ -184,6 +184,11 @@ export const formatShortDateTime = (timestamp) => { .format(new Date(timestamp * 1000)); } +export const formatShortDate = (timestamp) => { + return new Intl.DateTimeFormat('default', {dateStyle: 'short'}) + .format(new Date(timestamp * 1000)); +} + export const formatBytes = (bytes, decimals = 2) => { if (bytes === 0) return '0 bytes'; const k = 1024; diff --git a/web/src/components/Account.js b/web/src/components/Account.js index 8744dbf..622ca42 100644 --- a/web/src/components/Account.js +++ b/web/src/components/Account.js @@ -18,7 +18,7 @@ import TextField from "@mui/material/TextField"; import DialogActions from "@mui/material/DialogActions"; import routes from "./routes"; import IconButton from "@mui/material/IconButton"; -import {formatBytes, formatShortDateTime} from "../app/utils"; +import {formatBytes, formatShortDate, formatShortDateTime} from "../app/utils"; import accountApi, {UnauthorizedError} from "../app/AccountApi"; import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; import {Pref, PrefGroup} from "./Pref"; @@ -201,7 +201,7 @@ const Stats = () => { 0} title={t("account_usage_tier_title")} >
@@ -213,6 +213,11 @@ const Stats = () => { } {account.role === "user" && account.tier && account.tier.name} {account.role === "user" && !account.tier && t("account_usage_tier_none")} + {account.billing?.paid_until && + + + + } {config.enable_payments && account.role === "user" && (!account.tier || !account.tier.paid) &&