Update web app with SMS and calls stuff
This commit is contained in:
parent
7677c50b0e
commit
eb0805a470
14 changed files with 274 additions and 39 deletions
|
@ -1180,6 +1180,10 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
|
||||||
|
|
||||||
## ntfy server v2.5.0 (UNRELEASED)
|
## ntfy server v2.5.0 (UNRELEASED)
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
|
||||||
|
* Support for SMS and voice calls using Twilio (no ticket)
|
||||||
|
|
||||||
**Bug fixes + maintenance:**
|
**Bug fixes + maintenance:**
|
||||||
|
|
||||||
* Removed old ntfy website from ntfy entirely (no ticket)
|
* Removed old ntfy website from ntfy entirely (no ticket)
|
||||||
|
|
|
@ -529,6 +529,8 @@ func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, _ *visi
|
||||||
EnableLogin: s.config.EnableLogin,
|
EnableLogin: s.config.EnableLogin,
|
||||||
EnableSignup: s.config.EnableSignup,
|
EnableSignup: s.config.EnableSignup,
|
||||||
EnablePayments: s.config.StripeSecretKey != "",
|
EnablePayments: s.config.StripeSecretKey != "",
|
||||||
|
EnableSMS: s.config.TwilioAccount != "",
|
||||||
|
EnableCalls: s.config.TwilioAccount != "",
|
||||||
EnableReservations: s.config.EnableReservations,
|
EnableReservations: s.config.EnableReservations,
|
||||||
BillingContact: s.config.BillingContact,
|
BillingContact: s.config.BillingContact,
|
||||||
DisallowedTopics: s.config.DisallowedTopics,
|
DisallowedTopics: s.config.DisallowedTopics,
|
||||||
|
|
|
@ -68,6 +68,8 @@ func (s *Server) handleBillingTiersGet(w http.ResponseWriter, _ *http.Request, _
|
||||||
Messages: freeTier.MessageLimit,
|
Messages: freeTier.MessageLimit,
|
||||||
MessagesExpiryDuration: int64(freeTier.MessageExpiryDuration.Seconds()),
|
MessagesExpiryDuration: int64(freeTier.MessageExpiryDuration.Seconds()),
|
||||||
Emails: freeTier.EmailLimit,
|
Emails: freeTier.EmailLimit,
|
||||||
|
SMS: freeTier.SMSLimit,
|
||||||
|
Calls: freeTier.CallLimit,
|
||||||
Reservations: freeTier.ReservationsLimit,
|
Reservations: freeTier.ReservationsLimit,
|
||||||
AttachmentTotalSize: freeTier.AttachmentTotalSizeLimit,
|
AttachmentTotalSize: freeTier.AttachmentTotalSizeLimit,
|
||||||
AttachmentFileSize: freeTier.AttachmentFileSizeLimit,
|
AttachmentFileSize: freeTier.AttachmentFileSizeLimit,
|
||||||
|
@ -96,6 +98,8 @@ func (s *Server) handleBillingTiersGet(w http.ResponseWriter, _ *http.Request, _
|
||||||
Messages: tier.MessageLimit,
|
Messages: tier.MessageLimit,
|
||||||
MessagesExpiryDuration: int64(tier.MessageExpiryDuration.Seconds()),
|
MessagesExpiryDuration: int64(tier.MessageExpiryDuration.Seconds()),
|
||||||
Emails: tier.EmailLimit,
|
Emails: tier.EmailLimit,
|
||||||
|
SMS: tier.SMSLimit,
|
||||||
|
Calls: tier.CallLimit,
|
||||||
Reservations: tier.ReservationLimit,
|
Reservations: tier.ReservationLimit,
|
||||||
AttachmentTotalSize: tier.AttachmentTotalSizeLimit,
|
AttachmentTotalSize: tier.AttachmentTotalSizeLimit,
|
||||||
AttachmentFileSize: tier.AttachmentFileSizeLimit,
|
AttachmentFileSize: tier.AttachmentFileSizeLimit,
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"heckel.io/ntfy/log"
|
"heckel.io/ntfy/log"
|
||||||
|
"heckel.io/ntfy/user"
|
||||||
"heckel.io/ntfy/util"
|
"heckel.io/ntfy/util"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -32,7 +33,7 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Server) sendSMS(v *visitor, r *http.Request, m *message, to string) {
|
func (s *Server) sendSMS(v *visitor, r *http.Request, m *message, to string) {
|
||||||
body := fmt.Sprintf("%s\n\n--\n%s", m.Message, s.messageFooter(m))
|
body := fmt.Sprintf("%s\n\n--\n%s", m.Message, s.messageFooter(v.User(), m))
|
||||||
data := url.Values{}
|
data := url.Values{}
|
||||||
data.Set("From", s.config.TwilioFromNumber)
|
data.Set("From", s.config.TwilioFromNumber)
|
||||||
data.Set("To", to)
|
data.Set("To", to)
|
||||||
|
@ -41,7 +42,7 @@ func (s *Server) sendSMS(v *visitor, r *http.Request, m *message, to string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) callPhone(v *visitor, r *http.Request, m *message, to string) {
|
func (s *Server) callPhone(v *visitor, r *http.Request, m *message, to string) {
|
||||||
body := fmt.Sprintf(twilioCallFormat, xmlEscapeText(m.Topic), xmlEscapeText(m.Message), xmlEscapeText(s.messageFooter(m)))
|
body := fmt.Sprintf(twilioCallFormat, xmlEscapeText(m.Topic), xmlEscapeText(m.Message), xmlEscapeText(s.messageFooter(v.User(), m)))
|
||||||
data := url.Values{}
|
data := url.Values{}
|
||||||
data.Set("From", s.config.TwilioFromNumber)
|
data.Set("From", s.config.TwilioFromNumber)
|
||||||
data.Set("To", to)
|
data.Set("To", to)
|
||||||
|
@ -97,11 +98,11 @@ func (s *Server) performTwilioRequestInternal(endpoint string, data url.Values)
|
||||||
return string(response), nil
|
return string(response), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) messageFooter(m *message) string {
|
func (s *Server) messageFooter(u *user.User, m *message) string { // u may be nil!
|
||||||
topicURL := s.config.BaseURL + "/" + m.Topic
|
topicURL := s.config.BaseURL + "/" + m.Topic
|
||||||
sender := m.Sender.String()
|
sender := m.Sender.String()
|
||||||
if m.User != "" {
|
if u != nil {
|
||||||
sender = fmt.Sprintf("%s (%s)", m.User, m.Sender)
|
sender = fmt.Sprintf("%s (%s)", u.Name, m.Sender)
|
||||||
}
|
}
|
||||||
return fmt.Sprintf(twilioMessageFooterFormat, sender, util.ShortTopicURL(topicURL))
|
return fmt.Sprintf(twilioMessageFooterFormat, sender, util.ShortTopicURL(topicURL))
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,8 @@ package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"heckel.io/ntfy/user"
|
||||||
|
"heckel.io/ntfy/util"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
@ -27,6 +29,7 @@ func TestServer_Twilio_SMS(t *testing.T) {
|
||||||
c.TwilioAccount = "AC1234567890"
|
c.TwilioAccount = "AC1234567890"
|
||||||
c.TwilioAuthToken = "AAEAA1234567890"
|
c.TwilioAuthToken = "AAEAA1234567890"
|
||||||
c.TwilioFromNumber = "+1234567890"
|
c.TwilioFromNumber = "+1234567890"
|
||||||
|
c.VisitorSMSDailyLimit = 1
|
||||||
s := newTestServer(t, c)
|
s := newTestServer(t, c)
|
||||||
|
|
||||||
response := request(t, s, "POST", "/mytopic", "test", map[string]string{
|
response := request(t, s, "POST", "/mytopic", "test", map[string]string{
|
||||||
|
@ -38,6 +41,56 @@ func TestServer_Twilio_SMS(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestServer_Twilio_SMS_With_User(t *testing.T) {
|
||||||
|
var called atomic.Bool
|
||||||
|
twilioServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if called.Load() {
|
||||||
|
t.Fatal("Should be only called once")
|
||||||
|
}
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.Equal(t, "/2010-04-01/Accounts/AC1234567890/Messages.json", r.URL.Path)
|
||||||
|
require.Equal(t, "Basic QUMxMjM0NTY3ODkwOkFBRUFBMTIzNDU2Nzg5MA==", r.Header.Get("Authorization"))
|
||||||
|
require.Equal(t, "Body=test%0A%0A--%0AThis+message+was+sent+by+phil+%289.9.9.9%29+via+ntfy.sh%2Fmytopic&From=%2B1234567890&To=%2B11122233344", string(body))
|
||||||
|
called.Store(true)
|
||||||
|
}))
|
||||||
|
defer twilioServer.Close()
|
||||||
|
|
||||||
|
c := newTestConfigWithAuthFile(t)
|
||||||
|
c.BaseURL = "https://ntfy.sh"
|
||||||
|
c.TwilioBaseURL = twilioServer.URL
|
||||||
|
c.TwilioAccount = "AC1234567890"
|
||||||
|
c.TwilioAuthToken = "AAEAA1234567890"
|
||||||
|
c.TwilioFromNumber = "+1234567890"
|
||||||
|
s := newTestServer(t, c)
|
||||||
|
|
||||||
|
// Add tier and user
|
||||||
|
require.Nil(t, s.userManager.AddTier(&user.Tier{
|
||||||
|
Code: "pro",
|
||||||
|
MessageLimit: 10,
|
||||||
|
SMSLimit: 1,
|
||||||
|
}))
|
||||||
|
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
|
||||||
|
require.Nil(t, s.userManager.ChangeTier("phil", "pro"))
|
||||||
|
|
||||||
|
// Do request with user
|
||||||
|
response := request(t, s, "POST", "/mytopic", "test", map[string]string{
|
||||||
|
"Authorization": util.BasicAuth("phil", "phil"),
|
||||||
|
"SMS": "+11122233344",
|
||||||
|
})
|
||||||
|
require.Equal(t, "test", toMessage(t, response.Body.String()).Message)
|
||||||
|
waitFor(t, func() bool {
|
||||||
|
return called.Load()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Second one should fail due to rate limits
|
||||||
|
response = request(t, s, "POST", "/mytopic", "test", map[string]string{
|
||||||
|
"Authorization": util.BasicAuth("phil", "phil"),
|
||||||
|
"SMS": "+11122233344",
|
||||||
|
})
|
||||||
|
require.Equal(t, 42910, toHTTPError(t, response.Body.String()).Code)
|
||||||
|
}
|
||||||
|
|
||||||
func TestServer_Twilio_Call(t *testing.T) {
|
func TestServer_Twilio_Call(t *testing.T) {
|
||||||
var called atomic.Bool
|
var called atomic.Bool
|
||||||
twilioServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
twilioServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -55,6 +108,7 @@ func TestServer_Twilio_Call(t *testing.T) {
|
||||||
c.TwilioAccount = "AC1234567890"
|
c.TwilioAccount = "AC1234567890"
|
||||||
c.TwilioAuthToken = "AAEAA1234567890"
|
c.TwilioAuthToken = "AAEAA1234567890"
|
||||||
c.TwilioFromNumber = "+1234567890"
|
c.TwilioFromNumber = "+1234567890"
|
||||||
|
c.VisitorCallDailyLimit = 1
|
||||||
s := newTestServer(t, c)
|
s := newTestServer(t, c)
|
||||||
|
|
||||||
body := `this message has
|
body := `this message has
|
||||||
|
@ -69,6 +123,48 @@ and "quotes and other 'quotes`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestServer_Twilio_Call_With_User(t *testing.T) {
|
||||||
|
var called atomic.Bool
|
||||||
|
twilioServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if called.Load() {
|
||||||
|
t.Fatal("Should be only called once")
|
||||||
|
}
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.Equal(t, "/2010-04-01/Accounts/AC1234567890/Calls.json", r.URL.Path)
|
||||||
|
require.Equal(t, "Basic QUMxMjM0NTY3ODkwOkFBRUFBMTIzNDU2Nzg5MA==", r.Header.Get("Authorization"))
|
||||||
|
require.Equal(t, "From=%2B1234567890&To=%2B11122233344&Twiml=%0A%3CResponse%3E%0A%09%3CPause+length%3D%221%22%2F%3E%0A%09%3CSay%3EYou+have+a+message+from+notify+on+topic+mytopic.+Message%3A%3C%2FSay%3E%0A%09%3CPause+length%3D%221%22%2F%3E%0A%09%3CSay%3Ehi+there%3C%2FSay%3E%0A%09%3CPause+length%3D%221%22%2F%3E%0A%09%3CSay%3EEnd+message.%3C%2FSay%3E%0A%09%3CPause+length%3D%221%22%2F%3E%0A%09%3CSay%3EThis+message+was+sent+by+phil+%289.9.9.9%29+via+127.0.0.1%3A12345%2Fmytopic%3C%2FSay%3E%0A%09%3CPause+length%3D%221%22%2F%3E%0A%3C%2FResponse%3E", string(body))
|
||||||
|
called.Store(true)
|
||||||
|
}))
|
||||||
|
defer twilioServer.Close()
|
||||||
|
|
||||||
|
c := newTestConfigWithAuthFile(t)
|
||||||
|
c.TwilioBaseURL = twilioServer.URL
|
||||||
|
c.TwilioAccount = "AC1234567890"
|
||||||
|
c.TwilioAuthToken = "AAEAA1234567890"
|
||||||
|
c.TwilioFromNumber = "+1234567890"
|
||||||
|
s := newTestServer(t, c)
|
||||||
|
|
||||||
|
// Add tier and user
|
||||||
|
require.Nil(t, s.userManager.AddTier(&user.Tier{
|
||||||
|
Code: "pro",
|
||||||
|
MessageLimit: 10,
|
||||||
|
CallLimit: 1,
|
||||||
|
}))
|
||||||
|
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
|
||||||
|
require.Nil(t, s.userManager.ChangeTier("phil", "pro"))
|
||||||
|
|
||||||
|
// Do the thing
|
||||||
|
response := request(t, s, "POST", "/mytopic", "hi there", map[string]string{
|
||||||
|
"authorization": util.BasicAuth("phil", "phil"),
|
||||||
|
"x-call": "+11122233344",
|
||||||
|
})
|
||||||
|
require.Equal(t, "hi there", toMessage(t, response.Body.String()).Message)
|
||||||
|
waitFor(t, func() bool {
|
||||||
|
return called.Load()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestServer_Twilio_Call_InvalidNumber(t *testing.T) {
|
func TestServer_Twilio_Call_InvalidNumber(t *testing.T) {
|
||||||
c := newTestConfig(t)
|
c := newTestConfig(t)
|
||||||
c.TwilioBaseURL = "https://127.0.0.1"
|
c.TwilioBaseURL = "https://127.0.0.1"
|
||||||
|
|
|
@ -351,6 +351,8 @@ type apiConfigResponse struct {
|
||||||
EnableLogin bool `json:"enable_login"`
|
EnableLogin bool `json:"enable_login"`
|
||||||
EnableSignup bool `json:"enable_signup"`
|
EnableSignup bool `json:"enable_signup"`
|
||||||
EnablePayments bool `json:"enable_payments"`
|
EnablePayments bool `json:"enable_payments"`
|
||||||
|
EnableSMS bool `json:"enable_sms"`
|
||||||
|
EnableCalls bool `json:"enable_calls"`
|
||||||
EnableReservations bool `json:"enable_reservations"`
|
EnableReservations bool `json:"enable_reservations"`
|
||||||
BillingContact string `json:"billing_contact"`
|
BillingContact string `json:"billing_contact"`
|
||||||
DisallowedTopics []string `json:"disallowed_topics"`
|
DisallowedTopics []string `json:"disallowed_topics"`
|
||||||
|
|
|
@ -127,26 +127,26 @@ const (
|
||||||
`
|
`
|
||||||
|
|
||||||
selectUserByIDQuery = `
|
selectUserByIDQuery = `
|
||||||
SELECT u.id, u.user, u.pass, u.role, u.prefs, u.sync_topic, u.stats_messages, u.stats_emails, u.stats_sms, u.stats_calls, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_interval, u.stripe_subscription_paid_until, u.stripe_subscription_cancel_at, deleted, t.id, t.code, t.name, t.messages_limit, t.messages_expiry_duration, t.emails_limit, t.reservations_limit, t.attachment_file_size_limit, t.attachment_total_size_limit, t.attachment_expiry_duration, t.attachment_bandwidth_limit, t.stripe_monthly_price_id, t.stripe_yearly_price_id
|
SELECT u.id, u.user, u.pass, u.role, u.prefs, u.sync_topic, u.stats_messages, u.stats_emails, u.stats_sms, u.stats_calls, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_interval, u.stripe_subscription_paid_until, u.stripe_subscription_cancel_at, deleted, t.id, t.code, t.name, t.messages_limit, t.messages_expiry_duration, t.emails_limit, t.sms_limit, t.calls_limit, t.reservations_limit, t.attachment_file_size_limit, t.attachment_total_size_limit, t.attachment_expiry_duration, t.attachment_bandwidth_limit, t.stripe_monthly_price_id, t.stripe_yearly_price_id
|
||||||
FROM user u
|
FROM user u
|
||||||
LEFT JOIN tier t on t.id = u.tier_id
|
LEFT JOIN tier t on t.id = u.tier_id
|
||||||
WHERE u.id = ?
|
WHERE u.id = ?
|
||||||
`
|
`
|
||||||
selectUserByNameQuery = `
|
selectUserByNameQuery = `
|
||||||
SELECT u.id, u.user, u.pass, u.role, u.prefs, u.sync_topic, u.stats_messages, u.stats_emails, u.stats_sms, u.stats_calls, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_interval, u.stripe_subscription_paid_until, u.stripe_subscription_cancel_at, deleted, t.id, t.code, t.name, t.messages_limit, t.messages_expiry_duration, t.emails_limit, t.reservations_limit, t.attachment_file_size_limit, t.attachment_total_size_limit, t.attachment_expiry_duration, t.attachment_bandwidth_limit, t.stripe_monthly_price_id, t.stripe_yearly_price_id
|
SELECT u.id, u.user, u.pass, u.role, u.prefs, u.sync_topic, u.stats_messages, u.stats_emails, u.stats_sms, u.stats_calls, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_interval, u.stripe_subscription_paid_until, u.stripe_subscription_cancel_at, deleted, t.id, t.code, t.name, t.messages_limit, t.messages_expiry_duration, t.emails_limit, t.sms_limit, t.calls_limit, t.reservations_limit, t.attachment_file_size_limit, t.attachment_total_size_limit, t.attachment_expiry_duration, t.attachment_bandwidth_limit, t.stripe_monthly_price_id, t.stripe_yearly_price_id
|
||||||
FROM user u
|
FROM user u
|
||||||
LEFT JOIN tier t on t.id = u.tier_id
|
LEFT JOIN tier t on t.id = u.tier_id
|
||||||
WHERE user = ?
|
WHERE user = ?
|
||||||
`
|
`
|
||||||
selectUserByTokenQuery = `
|
selectUserByTokenQuery = `
|
||||||
SELECT u.id, u.user, u.pass, u.role, u.prefs, u.sync_topic, u.stats_messages, u.stats_emails, u.stats_sms, u.stats_calls, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_interval, u.stripe_subscription_paid_until, u.stripe_subscription_cancel_at, deleted, t.id, t.code, t.name, t.messages_limit, t.messages_expiry_duration, t.emails_limit, t.reservations_limit, t.attachment_file_size_limit, t.attachment_total_size_limit, t.attachment_expiry_duration, t.attachment_bandwidth_limit, t.stripe_monthly_price_id, t.stripe_yearly_price_id
|
SELECT u.id, u.user, u.pass, u.role, u.prefs, u.sync_topic, u.stats_messages, u.stats_emails, u.stats_sms, u.stats_calls, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_interval, u.stripe_subscription_paid_until, u.stripe_subscription_cancel_at, deleted, t.id, t.code, t.name, t.messages_limit, t.messages_expiry_duration, t.emails_limit, t.sms_limit, t.calls_limit, t.reservations_limit, t.attachment_file_size_limit, t.attachment_total_size_limit, t.attachment_expiry_duration, t.attachment_bandwidth_limit, t.stripe_monthly_price_id, t.stripe_yearly_price_id
|
||||||
FROM user u
|
FROM user u
|
||||||
JOIN user_token tk on u.id = tk.user_id
|
JOIN user_token tk on u.id = tk.user_id
|
||||||
LEFT JOIN tier t on t.id = u.tier_id
|
LEFT JOIN tier t on t.id = u.tier_id
|
||||||
WHERE tk.token = ? AND (tk.expires = 0 OR tk.expires >= ?)
|
WHERE tk.token = ? AND (tk.expires = 0 OR tk.expires >= ?)
|
||||||
`
|
`
|
||||||
selectUserByStripeCustomerIDQuery = `
|
selectUserByStripeCustomerIDQuery = `
|
||||||
SELECT u.id, u.user, u.pass, u.role, u.prefs, u.sync_topic, u.stats_messages, u.stats_emails, u.stats_sms, u.stats_calls, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_interval, u.stripe_subscription_paid_until, u.stripe_subscription_cancel_at, deleted, t.id, t.code, t.name, t.messages_limit, t.messages_expiry_duration, t.emails_limit, t.reservations_limit, t.attachment_file_size_limit, t.attachment_total_size_limit, t.attachment_expiry_duration, t.attachment_bandwidth_limit, t.stripe_monthly_price_id, t.stripe_yearly_price_id
|
SELECT u.id, u.user, u.pass, u.role, u.prefs, u.sync_topic, u.stats_messages, u.stats_emails, u.stats_sms, u.stats_calls, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_interval, u.stripe_subscription_paid_until, u.stripe_subscription_cancel_at, deleted, t.id, t.code, t.name, t.messages_limit, t.messages_expiry_duration, t.emails_limit, t.sms_limit, t.calls_limit, t.reservations_limit, t.attachment_file_size_limit, t.attachment_total_size_limit, t.attachment_expiry_duration, t.attachment_bandwidth_limit, t.stripe_monthly_price_id, t.stripe_yearly_price_id
|
||||||
FROM user u
|
FROM user u
|
||||||
LEFT JOIN tier t on t.id = u.tier_id
|
LEFT JOIN tier t on t.id = u.tier_id
|
||||||
WHERE u.stripe_customer_id = ?
|
WHERE u.stripe_customer_id = ?
|
||||||
|
@ -927,11 +927,11 @@ func (a *Manager) readUser(rows *sql.Rows) (*User, error) {
|
||||||
var id, username, hash, role, prefs, syncTopic string
|
var id, username, hash, role, prefs, syncTopic string
|
||||||
var stripeCustomerID, stripeSubscriptionID, stripeSubscriptionStatus, stripeSubscriptionInterval, stripeMonthlyPriceID, stripeYearlyPriceID, tierID, tierCode, tierName sql.NullString
|
var stripeCustomerID, stripeSubscriptionID, stripeSubscriptionStatus, stripeSubscriptionInterval, stripeMonthlyPriceID, stripeYearlyPriceID, tierID, tierCode, tierName sql.NullString
|
||||||
var messages, emails, sms, calls int64
|
var messages, emails, sms, calls int64
|
||||||
var messagesLimit, messagesExpiryDuration, emailsLimit, reservationsLimit, attachmentFileSizeLimit, attachmentTotalSizeLimit, attachmentExpiryDuration, attachmentBandwidthLimit, stripeSubscriptionPaidUntil, stripeSubscriptionCancelAt, deleted sql.NullInt64
|
var messagesLimit, messagesExpiryDuration, emailsLimit, smsLimit, callsLimit, reservationsLimit, attachmentFileSizeLimit, attachmentTotalSizeLimit, attachmentExpiryDuration, attachmentBandwidthLimit, stripeSubscriptionPaidUntil, stripeSubscriptionCancelAt, deleted sql.NullInt64
|
||||||
if !rows.Next() {
|
if !rows.Next() {
|
||||||
return nil, ErrUserNotFound
|
return nil, ErrUserNotFound
|
||||||
}
|
}
|
||||||
if err := rows.Scan(&id, &username, &hash, &role, &prefs, &syncTopic, &messages, &emails, &sms, &calls, &stripeCustomerID, &stripeSubscriptionID, &stripeSubscriptionStatus, &stripeSubscriptionInterval, &stripeSubscriptionPaidUntil, &stripeSubscriptionCancelAt, &deleted, &tierID, &tierCode, &tierName, &messagesLimit, &messagesExpiryDuration, &emailsLimit, &reservationsLimit, &attachmentFileSizeLimit, &attachmentTotalSizeLimit, &attachmentExpiryDuration, &attachmentBandwidthLimit, &stripeMonthlyPriceID, &stripeYearlyPriceID); err != nil {
|
if err := rows.Scan(&id, &username, &hash, &role, &prefs, &syncTopic, &messages, &emails, &sms, &calls, &stripeCustomerID, &stripeSubscriptionID, &stripeSubscriptionStatus, &stripeSubscriptionInterval, &stripeSubscriptionPaidUntil, &stripeSubscriptionCancelAt, &deleted, &tierID, &tierCode, &tierName, &messagesLimit, &messagesExpiryDuration, &emailsLimit, &smsLimit, &callsLimit, &reservationsLimit, &attachmentFileSizeLimit, &attachmentTotalSizeLimit, &attachmentExpiryDuration, &attachmentBandwidthLimit, &stripeMonthlyPriceID, &stripeYearlyPriceID); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if err := rows.Err(); err != nil {
|
} else if err := rows.Err(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -971,6 +971,8 @@ func (a *Manager) readUser(rows *sql.Rows) (*User, error) {
|
||||||
MessageLimit: messagesLimit.Int64,
|
MessageLimit: messagesLimit.Int64,
|
||||||
MessageExpiryDuration: time.Duration(messagesExpiryDuration.Int64) * time.Second,
|
MessageExpiryDuration: time.Duration(messagesExpiryDuration.Int64) * time.Second,
|
||||||
EmailLimit: emailsLimit.Int64,
|
EmailLimit: emailsLimit.Int64,
|
||||||
|
SMSLimit: smsLimit.Int64,
|
||||||
|
CallLimit: callsLimit.Int64,
|
||||||
ReservationLimit: reservationsLimit.Int64,
|
ReservationLimit: reservationsLimit.Int64,
|
||||||
AttachmentFileSizeLimit: attachmentFileSizeLimit.Int64,
|
AttachmentFileSizeLimit: attachmentFileSizeLimit.Int64,
|
||||||
AttachmentTotalSizeLimit: attachmentTotalSizeLimit.Int64,
|
AttachmentTotalSizeLimit: attachmentTotalSizeLimit.Int64,
|
||||||
|
|
|
@ -6,12 +6,14 @@
|
||||||
// During web development, you may change values here for rapid testing.
|
// During web development, you may change values here for rapid testing.
|
||||||
|
|
||||||
var config = {
|
var config = {
|
||||||
base_url: window.location.origin, // Change to test against a different server
|
base_url: "http://127.0.0.1:2586",// FIXME window.location.origin, // Change to test against a different server
|
||||||
app_root: "/app",
|
app_root: "/app",
|
||||||
enable_login: true,
|
enable_login: true,
|
||||||
enable_signup: true,
|
enable_signup: true,
|
||||||
enable_payments: true,
|
enable_payments: true,
|
||||||
enable_reservations: true,
|
enable_reservations: true,
|
||||||
|
enable_sms: true,
|
||||||
|
enable_calls: true,
|
||||||
billing_contact: "",
|
billing_contact: "",
|
||||||
disallowed_topics: ["docs", "static", "file", "app", "account", "settings", "signup", "login", "v1"]
|
disallowed_topics: ["docs", "static", "file", "app", "account", "settings", "signup", "login", "v1"]
|
||||||
};
|
};
|
||||||
|
|
|
@ -127,6 +127,12 @@
|
||||||
"publish_dialog_email_label": "Email",
|
"publish_dialog_email_label": "Email",
|
||||||
"publish_dialog_email_placeholder": "Address to forward the notification to, e.g. phil@example.com",
|
"publish_dialog_email_placeholder": "Address to forward the notification to, e.g. phil@example.com",
|
||||||
"publish_dialog_email_reset": "Remove email forward",
|
"publish_dialog_email_reset": "Remove email forward",
|
||||||
|
"publish_dialog_sms_label": "SMS",
|
||||||
|
"publish_dialog_sms_placeholder": "Phone number to send SMS to, e.g. +12223334444",
|
||||||
|
"publish_dialog_sms_reset": "Remove SMS message",
|
||||||
|
"publish_dialog_call_label": "Phone call",
|
||||||
|
"publish_dialog_call_placeholder": "Phone number to call with the message, e.g. +12223334444",
|
||||||
|
"publish_dialog_call_reset": "Remove phone call",
|
||||||
"publish_dialog_attach_label": "Attachment URL",
|
"publish_dialog_attach_label": "Attachment URL",
|
||||||
"publish_dialog_attach_placeholder": "Attach file by URL, e.g. https://f-droid.org/F-Droid.apk",
|
"publish_dialog_attach_placeholder": "Attach file by URL, e.g. https://f-droid.org/F-Droid.apk",
|
||||||
"publish_dialog_attach_reset": "Remove attachment URL",
|
"publish_dialog_attach_reset": "Remove attachment URL",
|
||||||
|
@ -138,6 +144,8 @@
|
||||||
"publish_dialog_other_features": "Other features:",
|
"publish_dialog_other_features": "Other features:",
|
||||||
"publish_dialog_chip_click_label": "Click URL",
|
"publish_dialog_chip_click_label": "Click URL",
|
||||||
"publish_dialog_chip_email_label": "Forward to email",
|
"publish_dialog_chip_email_label": "Forward to email",
|
||||||
|
"publish_dialog_chip_sms_label": "Send SMS",
|
||||||
|
"publish_dialog_chip_call_label": "Phone call",
|
||||||
"publish_dialog_chip_attach_url_label": "Attach file by URL",
|
"publish_dialog_chip_attach_url_label": "Attach file by URL",
|
||||||
"publish_dialog_chip_attach_file_label": "Attach local file",
|
"publish_dialog_chip_attach_file_label": "Attach local file",
|
||||||
"publish_dialog_chip_delay_label": "Delay delivery",
|
"publish_dialog_chip_delay_label": "Delay delivery",
|
||||||
|
@ -203,6 +211,10 @@
|
||||||
"account_basics_tier_manage_billing_button": "Manage billing",
|
"account_basics_tier_manage_billing_button": "Manage billing",
|
||||||
"account_usage_messages_title": "Published messages",
|
"account_usage_messages_title": "Published messages",
|
||||||
"account_usage_emails_title": "Emails sent",
|
"account_usage_emails_title": "Emails sent",
|
||||||
|
"account_usage_sms_title": "SMS sent",
|
||||||
|
"account_usage_sms_none": "No SMS can be sent with this account",
|
||||||
|
"account_usage_calls_title": "Phone calls made",
|
||||||
|
"account_usage_calls_none": "No phone calls can be made with this account",
|
||||||
"account_usage_reservations_title": "Reserved topics",
|
"account_usage_reservations_title": "Reserved topics",
|
||||||
"account_usage_reservations_none": "No reserved topics for this account",
|
"account_usage_reservations_none": "No reserved topics for this account",
|
||||||
"account_usage_attachment_storage_title": "Attachment storage",
|
"account_usage_attachment_storage_title": "Attachment storage",
|
||||||
|
@ -232,6 +244,12 @@
|
||||||
"account_upgrade_dialog_tier_features_messages_other": "{{messages}} daily messages",
|
"account_upgrade_dialog_tier_features_messages_other": "{{messages}} daily messages",
|
||||||
"account_upgrade_dialog_tier_features_emails_one": "{{emails}} daily email",
|
"account_upgrade_dialog_tier_features_emails_one": "{{emails}} daily email",
|
||||||
"account_upgrade_dialog_tier_features_emails_other": "{{emails}} daily emails",
|
"account_upgrade_dialog_tier_features_emails_other": "{{emails}} daily emails",
|
||||||
|
"account_upgrade_dialog_tier_features_sms_one": "{{sms}} daily SMS",
|
||||||
|
"account_upgrade_dialog_tier_features_sms_other": "{{sms}} daily SMS",
|
||||||
|
"account_upgrade_dialog_tier_features_no_sms": "No daily SMS",
|
||||||
|
"account_upgrade_dialog_tier_features_calls_one": "{{calls}} daily phone calls",
|
||||||
|
"account_upgrade_dialog_tier_features_calls_other": "{{calls}} daily phone calls",
|
||||||
|
"account_upgrade_dialog_tier_features_no_calls": "No daily phone calls",
|
||||||
"account_upgrade_dialog_tier_features_attachment_file_size": "{{filesize}} per file",
|
"account_upgrade_dialog_tier_features_attachment_file_size": "{{filesize}} per file",
|
||||||
"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_price_per_month": "month",
|
"account_upgrade_dialog_tier_price_per_month": "month",
|
||||||
|
|
|
@ -206,10 +206,12 @@ export const formatBytes = (bytes, decimals = 2) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const formatNumber = (n) => {
|
export const formatNumber = (n) => {
|
||||||
if (n % 1000 === 0) {
|
if (n === 0) {
|
||||||
|
return n;
|
||||||
|
} else if (n % 1000 === 0) {
|
||||||
return `${n/1000}k`;
|
return `${n/1000}k`;
|
||||||
}
|
}
|
||||||
return n;
|
return n.toLocaleString();
|
||||||
}
|
}
|
||||||
|
|
||||||
export const formatPrice = (n) => {
|
export const formatPrice = (n) => {
|
||||||
|
|
|
@ -51,6 +51,7 @@ import {ContentCopy, Public} from "@mui/icons-material";
|
||||||
import MenuItem from "@mui/material/MenuItem";
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
import DialogContentText from "@mui/material/DialogContentText";
|
import DialogContentText from "@mui/material/DialogContentText";
|
||||||
import {IncorrectPasswordError, UnauthorizedError} from "../app/errors";
|
import {IncorrectPasswordError, UnauthorizedError} from "../app/errors";
|
||||||
|
import {ProChip} from "./SubscriptionPopup";
|
||||||
|
|
||||||
const Account = () => {
|
const Account = () => {
|
||||||
if (!session.exists()) {
|
if (!session.exists()) {
|
||||||
|
@ -337,23 +338,18 @@ const Stats = () => {
|
||||||
{t("account_usage_title")}
|
{t("account_usage_title")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<PrefGroup>
|
<PrefGroup>
|
||||||
<Pref title={t("account_usage_reservations_title")}>
|
{(account.role === Role.ADMIN || account.limits.reservations > 0) &&
|
||||||
{(account.role === Role.ADMIN || account.limits.reservations > 0) &&
|
<Pref title={t("account_usage_reservations_title")}>
|
||||||
<>
|
<div>
|
||||||
<div>
|
<Typography variant="body2" sx={{float: "left"}}>{account.stats.reservations.toLocaleString()}</Typography>
|
||||||
<Typography variant="body2" sx={{float: "left"}}>{account.stats.reservations}</Typography>
|
<Typography variant="body2" sx={{float: "right"}}>{account.role === Role.USER ? t("account_usage_of_limit", { limit: account.limits.reservations.toLocaleString() }) : t("account_usage_unlimited")}</Typography>
|
||||||
<Typography variant="body2" sx={{float: "right"}}>{account.role === Role.USER ? t("account_usage_of_limit", {limit: account.limits.reservations}) : t("account_usage_unlimited")}</Typography>
|
</div>
|
||||||
</div>
|
<LinearProgress
|
||||||
<LinearProgress
|
variant="determinate"
|
||||||
variant="determinate"
|
value={account.role === Role.USER && account.limits.reservations > 0 ? normalize(account.stats.reservations, account.limits.reservations) : 100}
|
||||||
value={account.role === Role.USER && account.limits.reservations > 0 ? normalize(account.stats.reservations, account.limits.reservations) : 100}
|
/>
|
||||||
/>
|
</Pref>
|
||||||
</>
|
}
|
||||||
}
|
|
||||||
{account.role === Role.USER && account.limits.reservations === 0 &&
|
|
||||||
<em>{t("account_usage_reservations_none")}</em>
|
|
||||||
}
|
|
||||||
</Pref>
|
|
||||||
<Pref title={
|
<Pref title={
|
||||||
<>
|
<>
|
||||||
{t("account_usage_messages_title")}
|
{t("account_usage_messages_title")}
|
||||||
|
@ -361,8 +357,8 @@ const Stats = () => {
|
||||||
</>
|
</>
|
||||||
}>
|
}>
|
||||||
<div>
|
<div>
|
||||||
<Typography variant="body2" sx={{float: "left"}}>{account.stats.messages}</Typography>
|
<Typography variant="body2" sx={{float: "left"}}>{account.stats.messages.toLocaleString()}</Typography>
|
||||||
<Typography variant="body2" sx={{float: "right"}}>{account.role === Role.USER ? t("account_usage_of_limit", { limit: account.limits.messages }) : t("account_usage_unlimited")}</Typography>
|
<Typography variant="body2" sx={{float: "right"}}>{account.role === Role.USER ? t("account_usage_of_limit", { limit: account.limits.messages.toLocaleString() }) : t("account_usage_unlimited")}</Typography>
|
||||||
</div>
|
</div>
|
||||||
<LinearProgress
|
<LinearProgress
|
||||||
variant="determinate"
|
variant="determinate"
|
||||||
|
@ -376,14 +372,48 @@ const Stats = () => {
|
||||||
</>
|
</>
|
||||||
}>
|
}>
|
||||||
<div>
|
<div>
|
||||||
<Typography variant="body2" sx={{float: "left"}}>{account.stats.emails}</Typography>
|
<Typography variant="body2" sx={{float: "left"}}>{account.stats.emails.toLocaleString()}</Typography>
|
||||||
<Typography variant="body2" sx={{float: "right"}}>{account.role === Role.USER ? t("account_usage_of_limit", { limit: account.limits.emails }) : t("account_usage_unlimited")}</Typography>
|
<Typography variant="body2" sx={{float: "right"}}>{account.role === Role.USER ? t("account_usage_of_limit", { limit: account.limits.emails.toLocaleString() }) : t("account_usage_unlimited")}</Typography>
|
||||||
</div>
|
</div>
|
||||||
<LinearProgress
|
<LinearProgress
|
||||||
variant="determinate"
|
variant="determinate"
|
||||||
value={account.role === Role.USER ? normalize(account.stats.emails, account.limits.emails) : 100}
|
value={account.role === Role.USER ? normalize(account.stats.emails, account.limits.emails) : 100}
|
||||||
/>
|
/>
|
||||||
</Pref>
|
</Pref>
|
||||||
|
{(account.role === Role.ADMIN || account.limits.sms > 0) &&
|
||||||
|
<Pref title={
|
||||||
|
<>
|
||||||
|
{t("account_usage_sms_title")}
|
||||||
|
<Tooltip title={t("account_usage_limits_reset_daily")}><span><InfoIcon/></span></Tooltip>
|
||||||
|
</>
|
||||||
|
}>
|
||||||
|
<div>
|
||||||
|
<Typography variant="body2" sx={{float: "left"}}>{account.stats.sms.toLocaleString()}</Typography>
|
||||||
|
<Typography variant="body2" sx={{float: "right"}}>{account.role === Role.USER ? t("account_usage_of_limit", { limit: account.limits.sms.toLocaleString() }) : t("account_usage_unlimited")}</Typography>
|
||||||
|
</div>
|
||||||
|
<LinearProgress
|
||||||
|
variant="determinate"
|
||||||
|
value={account.role === Role.USER && account.limits.sms > 0 ? normalize(account.stats.sms, account.limits.sms) : 100}
|
||||||
|
/>
|
||||||
|
</Pref>
|
||||||
|
}
|
||||||
|
{(account.role === Role.ADMIN || account.limits.calls > 0) &&
|
||||||
|
<Pref title={
|
||||||
|
<>
|
||||||
|
{t("account_usage_calls_title")}
|
||||||
|
<Tooltip title={t("account_usage_limits_reset_daily")}><span><InfoIcon/></span></Tooltip>
|
||||||
|
</>
|
||||||
|
}>
|
||||||
|
<div>
|
||||||
|
<Typography variant="body2" sx={{float: "left"}}>{account.stats.calls.toLocaleString()}</Typography>
|
||||||
|
<Typography variant="body2" sx={{float: "right"}}>{account.role === Role.USER ? t("account_usage_of_limit", { limit: account.limits.calls.toLocaleString() }) : t("account_usage_unlimited")}</Typography>
|
||||||
|
</div>
|
||||||
|
<LinearProgress
|
||||||
|
variant="determinate"
|
||||||
|
value={account.role === Role.USER && account.limits.sms > 0 ? normalize(account.stats.calls, account.limits.calls) : 100}
|
||||||
|
/>
|
||||||
|
</Pref>
|
||||||
|
}
|
||||||
<Pref
|
<Pref
|
||||||
alignTop
|
alignTop
|
||||||
title={t("account_usage_attachment_storage_title")}
|
title={t("account_usage_attachment_storage_title")}
|
||||||
|
@ -404,6 +434,21 @@ const Stats = () => {
|
||||||
value={account.role === Role.USER ? normalize(account.stats.attachment_total_size, account.limits.attachment_total_size) : 100}
|
value={account.role === Role.USER ? normalize(account.stats.attachment_total_size, account.limits.attachment_total_size) : 100}
|
||||||
/>
|
/>
|
||||||
</Pref>
|
</Pref>
|
||||||
|
{config.enable_reservations && account.role === Role.USER && account.limits.reservations === 0 &&
|
||||||
|
<Pref title={<>{t("account_usage_reservations_title")}{config.enable_payments && <ProChip/>}</>}>
|
||||||
|
<em>{t("account_usage_reservations_none")}</em>
|
||||||
|
</Pref>
|
||||||
|
}
|
||||||
|
{config.enable_sms && account.role === Role.USER && account.limits.sms === 0 &&
|
||||||
|
<Pref title={<>{t("account_usage_sms_title")}{config.enable_payments && <ProChip/>}</>}>
|
||||||
|
<em>{t("account_usage_sms_none")}</em>
|
||||||
|
</Pref>
|
||||||
|
}
|
||||||
|
{config.enable_calls && account.role === Role.USER && account.limits.calls === 0 &&
|
||||||
|
<Pref title={<>{t("account_usage_calls_title")}{config.enable_payments && <ProChip/>}</>}>
|
||||||
|
<em>{t("account_usage_calls_none")}</em>
|
||||||
|
</Pref>
|
||||||
|
}
|
||||||
</PrefGroup>
|
</PrefGroup>
|
||||||
{account.role === Role.USER && account.limits.basis === LimitBasis.IP &&
|
{account.role === Role.USER && account.limits.basis === LimitBasis.IP &&
|
||||||
<Typography variant="body1">
|
<Typography variant="body1">
|
||||||
|
|
|
@ -45,6 +45,8 @@ const PublishDialog = (props) => {
|
||||||
const [filename, setFilename] = useState("");
|
const [filename, setFilename] = useState("");
|
||||||
const [filenameEdited, setFilenameEdited] = useState(false);
|
const [filenameEdited, setFilenameEdited] = useState(false);
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
|
const [sms, setSms] = useState("");
|
||||||
|
const [call, setCall] = useState("");
|
||||||
const [delay, setDelay] = useState("");
|
const [delay, setDelay] = useState("");
|
||||||
const [publishAnother, setPublishAnother] = useState(false);
|
const [publishAnother, setPublishAnother] = useState(false);
|
||||||
|
|
||||||
|
@ -52,6 +54,8 @@ const PublishDialog = (props) => {
|
||||||
const [showClickUrl, setShowClickUrl] = useState(false);
|
const [showClickUrl, setShowClickUrl] = useState(false);
|
||||||
const [showAttachUrl, setShowAttachUrl] = useState(false);
|
const [showAttachUrl, setShowAttachUrl] = useState(false);
|
||||||
const [showEmail, setShowEmail] = useState(false);
|
const [showEmail, setShowEmail] = useState(false);
|
||||||
|
const [showSms, setShowSms] = useState(false);
|
||||||
|
const [showCall, setShowCall] = useState(false);
|
||||||
const [showDelay, setShowDelay] = useState(false);
|
const [showDelay, setShowDelay] = useState(false);
|
||||||
|
|
||||||
const showAttachFile = !!attachFile && !showAttachUrl;
|
const showAttachFile = !!attachFile && !showAttachUrl;
|
||||||
|
@ -124,6 +128,12 @@ const PublishDialog = (props) => {
|
||||||
if (email.trim()) {
|
if (email.trim()) {
|
||||||
url.searchParams.append("email", email.trim());
|
url.searchParams.append("email", email.trim());
|
||||||
}
|
}
|
||||||
|
if (sms.trim()) {
|
||||||
|
url.searchParams.append("sms", sms.trim());
|
||||||
|
}
|
||||||
|
if (call.trim()) {
|
||||||
|
url.searchParams.append("call", call.trim());
|
||||||
|
}
|
||||||
if (delay.trim()) {
|
if (delay.trim()) {
|
||||||
url.searchParams.append("delay", delay.trim());
|
url.searchParams.append("delay", delay.trim());
|
||||||
}
|
}
|
||||||
|
@ -406,6 +416,48 @@ const PublishDialog = (props) => {
|
||||||
/>
|
/>
|
||||||
</ClosableRow>
|
</ClosableRow>
|
||||||
}
|
}
|
||||||
|
{showSms &&
|
||||||
|
<ClosableRow disabled={disabled} closeLabel={t("publish_dialog_sms_reset")} onClose={() => {
|
||||||
|
setSms("");
|
||||||
|
setShowSms(false);
|
||||||
|
}}>
|
||||||
|
<TextField
|
||||||
|
margin="dense"
|
||||||
|
label={t("publish_dialog_sms_label")}
|
||||||
|
placeholder={t("publish_dialog_sms_placeholder")}
|
||||||
|
value={sms}
|
||||||
|
onChange={ev => setSms(ev.target.value)}
|
||||||
|
disabled={disabled}
|
||||||
|
type="tel"
|
||||||
|
variant="standard"
|
||||||
|
fullWidth
|
||||||
|
inputProps={{
|
||||||
|
"aria-label": t("publish_dialog_sms_label")
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ClosableRow>
|
||||||
|
}
|
||||||
|
{showCall &&
|
||||||
|
<ClosableRow disabled={disabled} closeLabel={t("publish_dialog_call_reset")} onClose={() => {
|
||||||
|
setCall("");
|
||||||
|
setShowCall(false);
|
||||||
|
}}>
|
||||||
|
<TextField
|
||||||
|
margin="dense"
|
||||||
|
label={t("publish_dialog_call_label")}
|
||||||
|
placeholder={t("publish_dialog_call_placeholder")}
|
||||||
|
value={call}
|
||||||
|
onChange={ev => setCall(ev.target.value)}
|
||||||
|
disabled={disabled}
|
||||||
|
type="tel"
|
||||||
|
variant="standard"
|
||||||
|
fullWidth
|
||||||
|
inputProps={{
|
||||||
|
"aria-label": t("publish_dialog_call_label")
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ClosableRow>
|
||||||
|
}
|
||||||
{showAttachUrl &&
|
{showAttachUrl &&
|
||||||
<ClosableRow disabled={disabled} closeLabel={t("publish_dialog_attach_reset")} onClose={() => {
|
<ClosableRow disabled={disabled} closeLabel={t("publish_dialog_attach_reset")} onClose={() => {
|
||||||
setAttachUrl("");
|
setAttachUrl("");
|
||||||
|
@ -510,6 +562,8 @@ const PublishDialog = (props) => {
|
||||||
<div>
|
<div>
|
||||||
{!showClickUrl && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_click_label")} aria-label={t("publish_dialog_chip_click_label")} onClick={() => setShowClickUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
|
{!showClickUrl && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_click_label")} aria-label={t("publish_dialog_chip_click_label")} onClick={() => setShowClickUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
|
||||||
{!showEmail && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_email_label")} aria-label={t("publish_dialog_chip_email_label")} onClick={() => setShowEmail(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
|
{!showEmail && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_email_label")} aria-label={t("publish_dialog_chip_email_label")} onClick={() => setShowEmail(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
|
||||||
|
{!showSms && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_sms_label")} aria-label={t("publish_dialog_chip_sms_label")} onClick={() => setShowSms(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
|
||||||
|
{!showCall && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_call_label")} aria-label={t("publish_dialog_chip_call_label")} onClick={() => setShowCall(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
|
||||||
{!showAttachUrl && !showAttachFile && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_attach_url_label")} aria-label={t("publish_dialog_chip_attach_url_label")} onClick={() => setShowAttachUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
|
{!showAttachUrl && !showAttachFile && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_attach_url_label")} aria-label={t("publish_dialog_chip_attach_url_label")} onClick={() => setShowAttachUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
|
||||||
{!showAttachFile && !showAttachUrl && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_attach_file_label")} aria-label={t("publish_dialog_chip_attach_file_label")} onClick={() => handleAttachFileClick()} sx={{marginRight: 1, marginBottom: 1}}/>}
|
{!showAttachFile && !showAttachUrl && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_attach_file_label")} aria-label={t("publish_dialog_chip_attach_file_label")} onClick={() => handleAttachFileClick()} sx={{marginRight: 1, marginBottom: 1}}/>}
|
||||||
{!showDelay && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_delay_label")} aria-label={t("publish_dialog_chip_delay_label")} onClick={() => setShowDelay(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
|
{!showDelay && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_delay_label")} aria-label={t("publish_dialog_chip_delay_label")} onClick={() => setShowDelay(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
|
||||||
|
|
|
@ -277,14 +277,14 @@ const LimitReachedChip = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ProChip = () => {
|
export const ProChip = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<Chip
|
<Chip
|
||||||
label={"ntfy Pro"}
|
label={"ntfy Pro"}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="primary"
|
color="primary"
|
||||||
sx={{ opacity: 0.8, borderWidth: "2px", height: "24px", marginLeft: "5px" }}
|
sx={{ opacity: 0.8, fontWeight: "bold", borderWidth: "2px", height: "24px", marginLeft: "5px" }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -298,11 +298,14 @@ const TierCard = (props) => {
|
||||||
</div>
|
</div>
|
||||||
<List dense>
|
<List dense>
|
||||||
{tier.limits.reservations > 0 && <Feature>{t("account_upgrade_dialog_tier_features_reservations", { reservations: tier.limits.reservations, count: tier.limits.reservations })}</Feature>}
|
{tier.limits.reservations > 0 && <Feature>{t("account_upgrade_dialog_tier_features_reservations", { reservations: tier.limits.reservations, count: tier.limits.reservations })}</Feature>}
|
||||||
{tier.limits.reservations === 0 && <NoFeature>{t("account_upgrade_dialog_tier_features_no_reservations")}</NoFeature>}
|
|
||||||
<Feature>{t("account_upgrade_dialog_tier_features_messages", { messages: formatNumber(tier.limits.messages), count: tier.limits.messages })}</Feature>
|
<Feature>{t("account_upgrade_dialog_tier_features_messages", { messages: formatNumber(tier.limits.messages), count: tier.limits.messages })}</Feature>
|
||||||
<Feature>{t("account_upgrade_dialog_tier_features_emails", { emails: formatNumber(tier.limits.emails), count: tier.limits.emails })}</Feature>
|
<Feature>{t("account_upgrade_dialog_tier_features_emails", { emails: formatNumber(tier.limits.emails), count: tier.limits.emails })}</Feature>
|
||||||
|
{tier.limits.sms > 0 && <Feature>{t("account_upgrade_dialog_tier_features_sms", { sms: formatNumber(tier.limits.sms), count: tier.limits.sms })}</Feature>}
|
||||||
|
{tier.limits.calls > 0 && <Feature>{t("account_upgrade_dialog_tier_features_calls", { calls: formatNumber(tier.limits.calls), count: tier.limits.calls })}</Feature>}
|
||||||
<Feature>{t("account_upgrade_dialog_tier_features_attachment_file_size", { filesize: formatBytes(tier.limits.attachment_file_size, 0) })}</Feature>
|
<Feature>{t("account_upgrade_dialog_tier_features_attachment_file_size", { filesize: formatBytes(tier.limits.attachment_file_size, 0) })}</Feature>
|
||||||
<Feature>{t("account_upgrade_dialog_tier_features_attachment_total_size", { totalsize: formatBytes(tier.limits.attachment_total_size, 0) })}</Feature>
|
{tier.limits.reservations === 0 && <NoFeature>{t("account_upgrade_dialog_tier_features_no_reservations")}</NoFeature>}
|
||||||
|
{tier.limits.sms === 0 && <NoFeature>{t("account_upgrade_dialog_tier_features_no_sms")}</NoFeature>}
|
||||||
|
{tier.limits.calls === 0 && <NoFeature>{t("account_upgrade_dialog_tier_features_no_calls")}</NoFeature>}
|
||||||
</List>
|
</List>
|
||||||
{tier.prices && props.interval === SubscriptionInterval.MONTH &&
|
{tier.prices && props.interval === SubscriptionInterval.MONTH &&
|
||||||
<Typography variant="body2" color="gray">
|
<Typography variant="body2" color="gray">
|
||||||
|
|
Loading…
Reference in a new issue