WIP Twilio
This commit is contained in:
parent
214efbde36
commit
cea434a57c
34 changed files with 311 additions and 143 deletions
|
@ -455,6 +455,8 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit
|
|||
return s.ensureUser(s.withAccountSync(s.handleAccountPhoneNumberAdd))(w, r, v)
|
||||
} else if r.Method == http.MethodPost && r.URL.Path == apiAccountPhonePath {
|
||||
return s.ensureUser(s.withAccountSync(s.handleAccountPhoneNumberVerify))(w, r, v)
|
||||
} else if r.Method == http.MethodDelete && r.URL.Path == apiAccountPhonePath {
|
||||
return s.ensureUser(s.withAccountSync(s.handleAccountPhoneNumberDelete))(w, r, v)
|
||||
} else if r.Method == http.MethodGet && r.URL.Path == apiStatsPath {
|
||||
return s.handleStats(w, r, v)
|
||||
} else if r.Method == http.MethodGet && r.URL.Path == apiTiersPath {
|
||||
|
@ -692,6 +694,9 @@ func (s *Server) handlePublishInternal(r *http.Request, v *visitor) (*message, e
|
|||
} else if call != "" && !vrate.CallAllowed() {
|
||||
return nil, errHTTPTooManyRequestsLimitCalls.With(t)
|
||||
}
|
||||
|
||||
// FIXME check allowed phone numbers
|
||||
|
||||
if m.PollID != "" {
|
||||
m = newPollRequestMessage(t.ID, m.PollID)
|
||||
}
|
||||
|
|
|
@ -146,13 +146,7 @@ func (s *Server) handleAccountGet(w http.ResponseWriter, r *http.Request, v *vis
|
|||
return err
|
||||
}
|
||||
if len(phoneNumbers) > 0 {
|
||||
response.PhoneNumbers = make([]*apiAccountPhoneNumberResponse, 0)
|
||||
for _, p := range phoneNumbers {
|
||||
response.PhoneNumbers = append(response.PhoneNumbers, &apiAccountPhoneNumberResponse{
|
||||
Number: p.Number,
|
||||
Verified: p.Verified,
|
||||
})
|
||||
}
|
||||
response.PhoneNumbers = phoneNumbers
|
||||
}
|
||||
} else {
|
||||
response.Username = user.Everyone
|
||||
|
@ -542,19 +536,15 @@ func (s *Server) handleAccountPhoneNumberAdd(w http.ResponseWriter, r *http.Requ
|
|||
} else if u.IsUser() && u.Tier.CallLimit == 0 {
|
||||
return errHTTPUnauthorized
|
||||
}
|
||||
// Actually add the unverified number, and send verification
|
||||
logvr(v, r).
|
||||
Tag(tagAccount).
|
||||
Fields(log.Context{
|
||||
"number": req.Number,
|
||||
}).
|
||||
Debug("Adding phone number, and sending verification")
|
||||
if err := s.userManager.AddPhoneNumber(u.ID, req.Number); err != nil {
|
||||
if err == user.ErrPhoneNumberExists {
|
||||
return errHTTPConflictPhoneNumberExists
|
||||
}
|
||||
// Check if phone number exists
|
||||
phoneNumbers, err := s.userManager.PhoneNumbers(u.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if util.Contains(phoneNumbers, req.Number) {
|
||||
return errHTTPConflictPhoneNumberExists
|
||||
}
|
||||
// Actually add the unverified number, and send verification
|
||||
logvr(v, r).Tag(tagAccount).Field("phone_number", req.Number).Debug("Sending phone number verification")
|
||||
if err := s.verifyPhone(v, r, req.Number); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -570,31 +560,27 @@ func (s *Server) handleAccountPhoneNumberVerify(w http.ResponseWriter, r *http.R
|
|||
if !phoneNumberRegex.MatchString(req.Number) {
|
||||
return errHTTPBadRequestPhoneNumberInvalid
|
||||
}
|
||||
// Get phone numbers, and check if it's in the list
|
||||
phoneNumbers, err := s.userManager.PhoneNumbers(u.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
found := false
|
||||
for _, phoneNumber := range phoneNumbers {
|
||||
if phoneNumber.Number == req.Number && !phoneNumber.Verified {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return errHTTPBadRequestPhoneNumberInvalid
|
||||
}
|
||||
if err := s.checkVerifyPhone(v, r, req.Number, req.Code); err != nil {
|
||||
return err
|
||||
}
|
||||
logvr(v, r).
|
||||
Tag(tagAccount).
|
||||
Fields(log.Context{
|
||||
"number": req.Number,
|
||||
}).
|
||||
Debug("Marking phone number as verified")
|
||||
if err := s.userManager.MarkPhoneNumberVerified(u.ID, req.Number); err != nil {
|
||||
logvr(v, r).Tag(tagAccount).Field("phone_number", req.Number).Debug("Adding phone number as verified")
|
||||
if err := s.userManager.AddPhoneNumber(u.ID, req.Number); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.writeJSON(w, newSuccessResponse())
|
||||
}
|
||||
|
||||
func (s *Server) handleAccountPhoneNumberDelete(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
||||
u := v.User()
|
||||
req, err := readJSONWithLimit[apiAccountPhoneNumberRequest](r.Body, jsonBodyBytesLimit, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !phoneNumberRegex.MatchString(req.Number) {
|
||||
return errHTTPBadRequestPhoneNumberInvalid
|
||||
}
|
||||
logvr(v, r).Tag(tagAccount).Field("phone_number", req.Number).Debug("Deleting phone number")
|
||||
if err := s.userManager.DeletePhoneNumber(u.ID, req.Number); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.writeJSON(w, newSuccessResponse())
|
||||
|
|
|
@ -282,11 +282,6 @@ type apiAccountPhoneNumberRequest struct {
|
|||
Code string `json:"code,omitempty"` // Only supplied in "verify" call
|
||||
}
|
||||
|
||||
type apiAccountPhoneNumberResponse struct {
|
||||
Number string `json:"number"`
|
||||
Verified bool `json:"verified"`
|
||||
}
|
||||
|
||||
type apiAccountTier struct {
|
||||
Code string `json:"code"`
|
||||
Name string `json:"name"`
|
||||
|
@ -336,19 +331,19 @@ type apiAccountBilling struct {
|
|||
}
|
||||
|
||||
type apiAccountResponse struct {
|
||||
Username string `json:"username"`
|
||||
Role string `json:"role,omitempty"`
|
||||
SyncTopic string `json:"sync_topic,omitempty"`
|
||||
Language string `json:"language,omitempty"`
|
||||
Notification *user.NotificationPrefs `json:"notification,omitempty"`
|
||||
Subscriptions []*user.Subscription `json:"subscriptions,omitempty"`
|
||||
Reservations []*apiAccountReservation `json:"reservations,omitempty"`
|
||||
Tokens []*apiAccountTokenResponse `json:"tokens,omitempty"`
|
||||
PhoneNumbers []*apiAccountPhoneNumberResponse `json:"phone_numbers,omitempty"`
|
||||
Tier *apiAccountTier `json:"tier,omitempty"`
|
||||
Limits *apiAccountLimits `json:"limits,omitempty"`
|
||||
Stats *apiAccountStats `json:"stats,omitempty"`
|
||||
Billing *apiAccountBilling `json:"billing,omitempty"`
|
||||
Username string `json:"username"`
|
||||
Role string `json:"role,omitempty"`
|
||||
SyncTopic string `json:"sync_topic,omitempty"`
|
||||
Language string `json:"language,omitempty"`
|
||||
Notification *user.NotificationPrefs `json:"notification,omitempty"`
|
||||
Subscriptions []*user.Subscription `json:"subscriptions,omitempty"`
|
||||
Reservations []*apiAccountReservation `json:"reservations,omitempty"`
|
||||
Tokens []*apiAccountTokenResponse `json:"tokens,omitempty"`
|
||||
PhoneNumbers []string `json:"phone_numbers,omitempty"`
|
||||
Tier *apiAccountTier `json:"tier,omitempty"`
|
||||
Limits *apiAccountLimits `json:"limits,omitempty"`
|
||||
Stats *apiAccountStats `json:"stats,omitempty"`
|
||||
Billing *apiAccountBilling `json:"billing,omitempty"`
|
||||
}
|
||||
|
||||
type apiAccountReservationRequest struct {
|
||||
|
|
|
@ -115,7 +115,6 @@ const (
|
|||
CREATE TABLE IF NOT EXISTS user_phone (
|
||||
user_id TEXT NOT NULL,
|
||||
phone_number TEXT NOT NULL,
|
||||
verified INT NOT NULL,
|
||||
PRIMARY KEY (user_id, phone_number),
|
||||
FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE
|
||||
);
|
||||
|
@ -268,9 +267,9 @@ const (
|
|||
)
|
||||
`
|
||||
|
||||
selectPhoneNumbersQuery = `SELECT phone_number, verified FROM user_phone WHERE user_id = ?`
|
||||
insertPhoneNumberQuery = `INSERT INTO user_phone (user_id, phone_number, verified) VALUES (?, ?, 0)`
|
||||
updatePhoneNumberVerifiedQuery = `UPDATE user_phone SET verified=1 WHERE user_id = ? AND phone_number = ?`
|
||||
selectPhoneNumbersQuery = `SELECT phone_number FROM user_phone WHERE user_id = ?`
|
||||
insertPhoneNumberQuery = `INSERT INTO user_phone (user_id, phone_number) VALUES (?, ?)`
|
||||
deletePhoneNumberQuery = `DELETE FROM user_phone WHERE user_id = ? AND phone_number = ?`
|
||||
|
||||
insertTierQuery = `
|
||||
INSERT INTO tier (id, code, name, messages_limit, messages_expiry_duration, emails_limit, calls_limit, reservations_limit, attachment_file_size_limit, attachment_total_size_limit, attachment_expiry_duration, attachment_bandwidth_limit, stripe_monthly_price_id, stripe_yearly_price_id)
|
||||
|
@ -414,7 +413,6 @@ const (
|
|||
CREATE TABLE IF NOT EXISTS user_phone (
|
||||
user_id TEXT NOT NULL,
|
||||
phone_number TEXT NOT NULL,
|
||||
verified INT NOT NULL,
|
||||
PRIMARY KEY (user_id, phone_number),
|
||||
FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE
|
||||
);
|
||||
|
@ -648,13 +646,14 @@ func (a *Manager) RemoveExpiredTokens() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (a *Manager) PhoneNumbers(userID string) ([]*PhoneNumber, error) {
|
||||
// PhoneNumbers returns all phone numbers for the user with the given user ID
|
||||
func (a *Manager) PhoneNumbers(userID string) ([]string, error) {
|
||||
rows, err := a.db.Query(selectPhoneNumbersQuery, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
phoneNumbers := make([]*PhoneNumber, 0)
|
||||
phoneNumbers := make([]string, 0)
|
||||
for {
|
||||
phoneNumber, err := a.readPhoneNumber(rows)
|
||||
if err == ErrPhoneNumberNotFound {
|
||||
|
@ -667,23 +666,20 @@ func (a *Manager) PhoneNumbers(userID string) ([]*PhoneNumber, error) {
|
|||
return phoneNumbers, nil
|
||||
}
|
||||
|
||||
func (a *Manager) readPhoneNumber(rows *sql.Rows) (*PhoneNumber, error) {
|
||||
func (a *Manager) readPhoneNumber(rows *sql.Rows) (string, error) {
|
||||
var phoneNumber string
|
||||
var verified bool
|
||||
if !rows.Next() {
|
||||
return nil, ErrPhoneNumberNotFound
|
||||
return "", ErrPhoneNumberNotFound
|
||||
}
|
||||
if err := rows.Scan(&phoneNumber, &verified); err != nil {
|
||||
return nil, err
|
||||
if err := rows.Scan(&phoneNumber); err != nil {
|
||||
return "", err
|
||||
} else if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
return "", err
|
||||
}
|
||||
return &PhoneNumber{
|
||||
Number: phoneNumber,
|
||||
Verified: verified,
|
||||
}, nil
|
||||
return phoneNumber, nil
|
||||
}
|
||||
|
||||
// AddPhoneNumber adds a phone number to the user with the given user ID
|
||||
func (a *Manager) AddPhoneNumber(userID string, phoneNumber string) error {
|
||||
if _, err := a.db.Exec(insertPhoneNumberQuery, userID, phoneNumber); err != nil {
|
||||
if sqliteErr, ok := err.(sqlite3.Error); ok && sqliteErr.ExtendedCode == sqlite3.ErrConstraintUnique {
|
||||
|
@ -694,11 +690,10 @@ func (a *Manager) AddPhoneNumber(userID string, phoneNumber string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (a *Manager) MarkPhoneNumberVerified(userID string, phoneNumber string) error {
|
||||
if _, err := a.db.Exec(updatePhoneNumberVerifiedQuery, userID, phoneNumber); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
// DeletePhoneNumber deletes a phone number from the user with the given user ID
|
||||
func (a *Manager) DeletePhoneNumber(userID string, phoneNumber string) error {
|
||||
_, err := a.db.Exec(deletePhoneNumberQuery, userID, phoneNumber)
|
||||
return err
|
||||
}
|
||||
|
||||
// RemoveDeletedUsers deletes all users that have been marked deleted for
|
||||
|
|
|
@ -71,11 +71,6 @@ type TokenUpdate struct {
|
|||
LastOrigin netip.Addr
|
||||
}
|
||||
|
||||
type PhoneNumber struct {
|
||||
Number string
|
||||
Verified bool
|
||||
}
|
||||
|
||||
// Prefs represents a user's configuration settings
|
||||
type Prefs struct {
|
||||
Language *string `json:"language,omitempty"`
|
||||
|
|
|
@ -152,7 +152,7 @@
|
|||
"publish_dialog_chip_delay_label": "تأخير التسليم",
|
||||
"subscribe_dialog_login_description": "هذا الموضوع محمي بكلمة مرور. الرجاء إدخال اسم المستخدم وكلمة المرور للاشتراك.",
|
||||
"subscribe_dialog_subscribe_button_cancel": "إلغاء",
|
||||
"subscribe_dialog_login_button_back": "العودة",
|
||||
"common_back": "العودة",
|
||||
"prefs_notifications_sound_play": "تشغيل الصوت المحدد",
|
||||
"prefs_notifications_min_priority_title": "أولوية دنيا",
|
||||
"prefs_notifications_min_priority_max_only": "الأولوية القصوى فقط",
|
||||
|
@ -225,7 +225,7 @@
|
|||
"account_tokens_table_expires_header": "تنتهي مدة صلاحيته في",
|
||||
"account_tokens_table_never_expires": "لا تنتهي صلاحيتها أبدا",
|
||||
"account_tokens_table_current_session": "جلسة المتصفح الحالية",
|
||||
"account_tokens_table_copy_to_clipboard": "انسخ إلى الحافظة",
|
||||
"common_copy_to_clipboard": "انسخ إلى الحافظة",
|
||||
"account_tokens_table_cannot_delete_or_edit": "لا يمكن تحرير أو حذف الرمز المميز للجلسة الحالية",
|
||||
"account_tokens_table_create_token_button": "إنشاء رمز مميز للوصول",
|
||||
"account_tokens_table_last_origin_tooltip": "من عنوان IP {{ip}}، انقر للبحث",
|
||||
|
|
|
@ -104,7 +104,7 @@
|
|||
"subscribe_dialog_subscribe_topic_placeholder": "Име на темата, напр. phils_alerts",
|
||||
"subscribe_dialog_subscribe_use_another_label": "Използване на друг сървър",
|
||||
"subscribe_dialog_login_username_label": "Потребител, напр. phil",
|
||||
"subscribe_dialog_login_button_back": "Назад",
|
||||
"common_back": "Назад",
|
||||
"subscribe_dialog_subscribe_button_cancel": "Отказ",
|
||||
"subscribe_dialog_login_description": "Темата е защитена. За да се абонирате въведете потребител и парола.",
|
||||
"subscribe_dialog_subscribe_button_subscribe": "Абониране",
|
||||
|
|
|
@ -91,7 +91,7 @@
|
|||
"subscribe_dialog_subscribe_button_subscribe": "Přihlásit odběr",
|
||||
"subscribe_dialog_login_username_label": "Uživatelské jméno, např. phil",
|
||||
"subscribe_dialog_login_password_label": "Heslo",
|
||||
"subscribe_dialog_login_button_back": "Zpět",
|
||||
"common_back": "Zpět",
|
||||
"subscribe_dialog_login_button_login": "Přihlásit se",
|
||||
"subscribe_dialog_error_user_not_authorized": "Uživatel {{username}} není autorizován",
|
||||
"subscribe_dialog_error_user_anonymous": "anonymně",
|
||||
|
@ -305,7 +305,7 @@
|
|||
"account_tokens_table_expires_header": "Vyprší",
|
||||
"account_tokens_table_never_expires": "Nikdy nevyprší",
|
||||
"account_tokens_table_current_session": "Současná relace prohlížeče",
|
||||
"account_tokens_table_copy_to_clipboard": "Kopírování do schránky",
|
||||
"common_copy_to_clipboard": "Kopírování do schránky",
|
||||
"account_tokens_table_label_header": "Popisek",
|
||||
"account_tokens_table_cannot_delete_or_edit": "Nelze upravit nebo odstranit aktuální token relace",
|
||||
"account_tokens_table_create_token_button": "Vytvořit přístupový token",
|
||||
|
|
|
@ -91,7 +91,7 @@
|
|||
"publish_dialog_delay_label": "Forsinkelse",
|
||||
"publish_dialog_button_send": "Send",
|
||||
"subscribe_dialog_subscribe_button_subscribe": "Tilmeld",
|
||||
"subscribe_dialog_login_button_back": "Tilbage",
|
||||
"common_back": "Tilbage",
|
||||
"subscribe_dialog_login_username_label": "Brugernavn, f.eks. phil",
|
||||
"account_basics_title": "Konto",
|
||||
"subscribe_dialog_error_topic_already_reserved": "Emnet er allerede reserveret",
|
||||
|
@ -209,7 +209,7 @@
|
|||
"subscribe_dialog_subscribe_use_another_label": "Brug en anden server",
|
||||
"account_basics_tier_upgrade_button": "Opgrader til Pro",
|
||||
"account_upgrade_dialog_tier_features_messages_other": "{{messages}} daglige beskeder",
|
||||
"account_tokens_table_copy_to_clipboard": "Kopier til udklipsholder",
|
||||
"common_copy_to_clipboard": "Kopier til udklipsholder",
|
||||
"prefs_reservations_edit_button": "Rediger emneadgang",
|
||||
"account_upgrade_dialog_title": "Skift kontoniveau",
|
||||
"account_upgrade_dialog_tier_features_reservations_other": "{{reservations}} reserverede emner",
|
||||
|
|
|
@ -94,7 +94,7 @@
|
|||
"publish_dialog_delay_placeholder": "Auslieferung verzögern, z.B. {{unixTimestamp}}, {{relativeTime}}, oder \"{{naturalLanguage}}\" (nur Englisch)",
|
||||
"prefs_appearance_title": "Darstellung",
|
||||
"subscribe_dialog_login_password_label": "Kennwort",
|
||||
"subscribe_dialog_login_button_back": "Zurück",
|
||||
"common_back": "Zurück",
|
||||
"publish_dialog_chip_attach_url_label": "Datei von URL anhängen",
|
||||
"publish_dialog_chip_delay_label": "Auslieferung verzögern",
|
||||
"publish_dialog_chip_topic_label": "Thema ändern",
|
||||
|
@ -284,7 +284,7 @@
|
|||
"account_tokens_table_expires_header": "Verfällt",
|
||||
"account_tokens_table_never_expires": "Verfällt nie",
|
||||
"account_tokens_table_current_session": "Aktuelle Browser-Sitzung",
|
||||
"account_tokens_table_copy_to_clipboard": "In die Zwischenablage kopieren",
|
||||
"common_copy_to_clipboard": "In die Zwischenablage kopieren",
|
||||
"account_tokens_table_copied_to_clipboard": "Access-Token kopiert",
|
||||
"account_tokens_table_cannot_delete_or_edit": "Aktuelles Token kann nicht bearbeitet oder gelöscht werden",
|
||||
"account_tokens_table_create_token_button": "Access-Token erzeugen",
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
"common_cancel": "Cancel",
|
||||
"common_save": "Save",
|
||||
"common_add": "Add",
|
||||
"common_back": "Back",
|
||||
"common_copy_to_clipboard": "Copy to clipboard",
|
||||
"signup_title": "Create a ntfy account",
|
||||
"signup_form_username": "Username",
|
||||
"signup_form_password": "Password",
|
||||
|
@ -169,7 +171,6 @@
|
|||
"subscribe_dialog_login_description": "This topic is password-protected. Please enter username and password to subscribe.",
|
||||
"subscribe_dialog_login_username_label": "Username, e.g. phil",
|
||||
"subscribe_dialog_login_password_label": "Password",
|
||||
"subscribe_dialog_login_button_back": "Back",
|
||||
"subscribe_dialog_login_button_login": "Login",
|
||||
"subscribe_dialog_error_user_not_authorized": "User {{username}} not authorized",
|
||||
"subscribe_dialog_error_topic_already_reserved": "Topic already reserved",
|
||||
|
@ -187,7 +188,17 @@
|
|||
"account_basics_password_dialog_button_submit": "Change password",
|
||||
"account_basics_password_dialog_current_password_incorrect": "Password incorrect",
|
||||
"account_basics_phone_numbers_title": "Phone numbers",
|
||||
"account_basics_phone_numbers_dialog_description": "To use the call notification feature, you need to add and verify at least one phone number. Adding it will send a verification SMS to your phone.",
|
||||
"account_basics_phone_numbers_description": "For phone call notifications",
|
||||
"account_basics_phone_numbers_no_phone_numbers_yet": "No phone numbers yet",
|
||||
"account_basics_phone_numbers_copied_to_clipboard": "Phone number copied to clipboard",
|
||||
"account_basics_phone_numbers_dialog_title": "Add phone number",
|
||||
"account_basics_phone_numbers_dialog_number_label": "Phone number",
|
||||
"account_basics_phone_numbers_dialog_number_placeholder": "e.g. +1222333444",
|
||||
"account_basics_phone_numbers_dialog_send_verification_button": "Send verification",
|
||||
"account_basics_phone_numbers_dialog_code_label": "Verification code",
|
||||
"account_basics_phone_numbers_dialog_code_placeholder": "e.g. 123456",
|
||||
"account_basics_phone_numbers_dialog_check_verification_button": "Confirm code",
|
||||
"account_usage_title": "Usage",
|
||||
"account_usage_of_limit": "of {{limit}}",
|
||||
"account_usage_unlimited": "Unlimited",
|
||||
|
@ -265,7 +276,6 @@
|
|||
"account_tokens_table_expires_header": "Expires",
|
||||
"account_tokens_table_never_expires": "Never expires",
|
||||
"account_tokens_table_current_session": "Current browser session",
|
||||
"account_tokens_table_copy_to_clipboard": "Copy to clipboard",
|
||||
"account_tokens_table_copied_to_clipboard": "Access token copied",
|
||||
"account_tokens_table_cannot_delete_or_edit": "Cannot edit or delete current session token",
|
||||
"account_tokens_table_create_token_button": "Create access token",
|
||||
|
|
|
@ -81,7 +81,7 @@
|
|||
"subscribe_dialog_login_description": "Este tópico está protegido por contraseña. Por favor, introduzca su nombre de usuario y contraseña para suscribirse.",
|
||||
"subscribe_dialog_login_username_label": "Nombre de usuario, ej. phil",
|
||||
"subscribe_dialog_login_password_label": "Contraseña",
|
||||
"subscribe_dialog_login_button_back": "Volver",
|
||||
"common_back": "Volver",
|
||||
"subscribe_dialog_login_button_login": "Iniciar sesión",
|
||||
"subscribe_dialog_error_user_not_authorized": "Usuario {{username}} no autorizado",
|
||||
"subscribe_dialog_error_user_anonymous": "anónimo",
|
||||
|
@ -257,7 +257,7 @@
|
|||
"account_tokens_table_expires_header": "Expira",
|
||||
"account_tokens_table_never_expires": "Nunca expira",
|
||||
"account_tokens_table_current_session": "Sesión del navegador actual",
|
||||
"account_tokens_table_copy_to_clipboard": "Copiar al portapapeles",
|
||||
"common_copy_to_clipboard": "Copiar al portapapeles",
|
||||
"account_tokens_table_copied_to_clipboard": "Token de acceso copiado",
|
||||
"account_tokens_table_cannot_delete_or_edit": "No se puede editar ni eliminar el token de sesión actual",
|
||||
"account_tokens_table_create_token_button": "Crear token de acceso",
|
||||
|
|
|
@ -106,7 +106,7 @@
|
|||
"prefs_notifications_title": "Notifications",
|
||||
"prefs_notifications_delete_after_title": "Supprimer les notifications",
|
||||
"prefs_users_add_button": "Ajouter un utilisateur",
|
||||
"subscribe_dialog_login_button_back": "Retour",
|
||||
"common_back": "Retour",
|
||||
"subscribe_dialog_error_user_anonymous": "anonyme",
|
||||
"prefs_notifications_sound_no_sound": "Aucun son",
|
||||
"prefs_notifications_min_priority_title": "Priorité minimum",
|
||||
|
@ -293,7 +293,7 @@
|
|||
"account_tokens_table_expires_header": "Expire",
|
||||
"account_tokens_table_never_expires": "N'expire jamais",
|
||||
"account_tokens_table_current_session": "Session de navigation actuelle",
|
||||
"account_tokens_table_copy_to_clipboard": "Copier dans le presse-papier",
|
||||
"common_copy_to_clipboard": "Copier dans le presse-papier",
|
||||
"account_tokens_table_copied_to_clipboard": "Jeton d'accès copié",
|
||||
"account_tokens_table_create_token_button": "Créer un jeton d'accès",
|
||||
"account_tokens_table_last_origin_tooltip": "Depuis l'adresse IP {{ip}}, cliquer pour rechercher",
|
||||
|
|
|
@ -84,7 +84,7 @@
|
|||
"subscribe_dialog_login_description": "Ez a téma jelszóval védett. Jelentkezz be a feliratkozáshoz.",
|
||||
"subscribe_dialog_login_username_label": "Felhasználónév, pl: jozsi",
|
||||
"subscribe_dialog_login_password_label": "Jelszó",
|
||||
"subscribe_dialog_login_button_back": "Vissza",
|
||||
"common_back": "Vissza",
|
||||
"subscribe_dialog_login_button_login": "Belépés",
|
||||
"subscribe_dialog_error_user_anonymous": "névtelen",
|
||||
"subscribe_dialog_error_user_not_authorized": "A(z) {{username}} felhasználónak nincs hozzáférése",
|
||||
|
|
|
@ -116,7 +116,7 @@
|
|||
"common_save": "Simpan",
|
||||
"prefs_appearance_title": "Tampilan",
|
||||
"subscribe_dialog_login_password_label": "Kata sandi",
|
||||
"subscribe_dialog_login_button_back": "Kembali",
|
||||
"common_back": "Kembali",
|
||||
"prefs_notifications_sound_title": "Suara notifikasi",
|
||||
"prefs_notifications_min_priority_low_and_higher": "Prioritas rendah dan lebih tinggi",
|
||||
"prefs_notifications_min_priority_default_and_higher": "Prioritas bawaan dan lebih tinggi",
|
||||
|
@ -278,7 +278,7 @@
|
|||
"account_tokens_table_expires_header": "Kedaluwarsa",
|
||||
"account_tokens_table_never_expires": "Tidak pernah kedaluwarsa",
|
||||
"account_tokens_table_current_session": "Sesi peramban saat ini",
|
||||
"account_tokens_table_copy_to_clipboard": "Salin ke papan klip",
|
||||
"common_copy_to_clipboard": "Salin ke papan klip",
|
||||
"account_tokens_table_copied_to_clipboard": "Token akses disalin",
|
||||
"account_tokens_table_cannot_delete_or_edit": "Tidak dapat menyunting atau menghapus token sesi saat ini",
|
||||
"account_tokens_table_create_token_button": "Buat token akses",
|
||||
|
|
|
@ -178,7 +178,7 @@
|
|||
"prefs_notifications_sound_play": "Riproduci il suono selezionato",
|
||||
"prefs_notifications_min_priority_title": "Priorità minima",
|
||||
"subscribe_dialog_login_description": "Questo argomento è protetto da password. Per favore inserisci username e password per iscriverti.",
|
||||
"subscribe_dialog_login_button_back": "Indietro",
|
||||
"common_back": "Indietro",
|
||||
"subscribe_dialog_error_user_not_authorized": "Utente {{username}} non autorizzato",
|
||||
"prefs_notifications_title": "Notifiche",
|
||||
"prefs_notifications_delete_after_title": "Elimina le notifiche",
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
"subscribe_dialog_login_description": "このトピックはログインする必要があります。ユーザー名とパスワードを入力してください。",
|
||||
"subscribe_dialog_login_username_label": "ユーザー名, 例) phil",
|
||||
"subscribe_dialog_login_password_label": "パスワード",
|
||||
"subscribe_dialog_login_button_back": "戻る",
|
||||
"common_back": "戻る",
|
||||
"subscribe_dialog_login_button_login": "ログイン",
|
||||
"prefs_notifications_min_priority_high_and_higher": "優先度高 およびそれ以上",
|
||||
"prefs_notifications_min_priority_max_only": "優先度最高のみ",
|
||||
|
@ -258,7 +258,7 @@
|
|||
"account_tokens_table_expires_header": "期限",
|
||||
"account_tokens_table_never_expires": "無期限",
|
||||
"account_tokens_table_current_session": "現在のブラウザセッション",
|
||||
"account_tokens_table_copy_to_clipboard": "クリップボードにコピー",
|
||||
"common_copy_to_clipboard": "クリップボードにコピー",
|
||||
"account_tokens_table_copied_to_clipboard": "アクセストークンをコピーしました",
|
||||
"account_tokens_table_cannot_delete_or_edit": "現在のセッショントークンは編集または削除できません",
|
||||
"account_tokens_table_create_token_button": "アクセストークンを生成",
|
||||
|
|
|
@ -93,7 +93,7 @@
|
|||
"subscribe_dialog_error_user_not_authorized": "사용자 {{username}} 은(는) 인증되지 않았습니다",
|
||||
"subscribe_dialog_login_username_label": "사용자 이름, 예를 들면 phil",
|
||||
"subscribe_dialog_login_password_label": "비밀번호",
|
||||
"subscribe_dialog_login_button_back": "뒤로가기",
|
||||
"common_back": "뒤로가기",
|
||||
"subscribe_dialog_login_button_login": "로그인",
|
||||
"prefs_notifications_title": "알림",
|
||||
"prefs_notifications_sound_title": "알림 효과음",
|
||||
|
|
|
@ -113,7 +113,7 @@
|
|||
"prefs_notifications_delete_after_one_week_description": "Merknader slettes automatisk etter én uke",
|
||||
"prefs_notifications_delete_after_one_month_description": "Merknader slettes automatisk etter én måned",
|
||||
"priority_min": "min.",
|
||||
"subscribe_dialog_login_button_back": "Tilbake",
|
||||
"common_back": "Tilbake",
|
||||
"prefs_notifications_delete_after_three_hours": "Etter tre timer",
|
||||
"prefs_users_table_base_url_header": "Tjeneste-nettadresse",
|
||||
"common_cancel": "Avbryt",
|
||||
|
|
|
@ -140,7 +140,7 @@
|
|||
"subscribe_dialog_subscribe_title": "Onderwerp abonneren",
|
||||
"subscribe_dialog_subscribe_description": "Onderwerpen zijn mogelijk niet beschermd met een wachtwoord, kies daarom een moeilijk te raden naam. Na abonneren kun je notificaties via PUT/POST sturen.",
|
||||
"subscribe_dialog_login_password_label": "Wachtwoord",
|
||||
"subscribe_dialog_login_button_back": "Terug",
|
||||
"common_back": "Terug",
|
||||
"subscribe_dialog_login_button_login": "Aanmelden",
|
||||
"subscribe_dialog_error_user_not_authorized": "Gebruiker {{username}} heeft geen toegang",
|
||||
"subscribe_dialog_error_user_anonymous": "anoniem",
|
||||
|
@ -331,7 +331,7 @@
|
|||
"account_upgrade_dialog_button_cancel_subscription": "Abonnement opzeggen",
|
||||
"account_tokens_table_last_access_header": "Laatste toegang",
|
||||
"account_tokens_table_expires_header": "Verloopt op",
|
||||
"account_tokens_table_copy_to_clipboard": "Kopieer naar klembord",
|
||||
"common_copy_to_clipboard": "Kopieer naar klembord",
|
||||
"account_tokens_table_copied_to_clipboard": "Toegangstoken gekopieerd",
|
||||
"account_tokens_delete_dialog_submit_button": "Token definitief verwijderen",
|
||||
"prefs_users_description_no_sync": "Gebruikers en wachtwoorden worden niet gesynchroniseerd met uw account.",
|
||||
|
|
|
@ -107,7 +107,7 @@
|
|||
"subscribe_dialog_login_username_label": "Nazwa użytkownika, np. phil",
|
||||
"subscribe_dialog_login_password_label": "Hasło",
|
||||
"publish_dialog_button_cancel": "Anuluj",
|
||||
"subscribe_dialog_login_button_back": "Powrót",
|
||||
"common_back": "Powrót",
|
||||
"subscribe_dialog_login_button_login": "Zaloguj się",
|
||||
"subscribe_dialog_error_user_not_authorized": "Użytkownik {{username}} nie ma uprawnień",
|
||||
"subscribe_dialog_error_user_anonymous": "anonim",
|
||||
|
@ -253,7 +253,7 @@
|
|||
"account_tokens_table_expires_header": "Termin ważności",
|
||||
"account_tokens_table_never_expires": "Bezterminowy",
|
||||
"account_tokens_table_current_session": "Aktualna sesja przeglądarki",
|
||||
"account_tokens_table_copy_to_clipboard": "Kopiuj do schowka",
|
||||
"common_copy_to_clipboard": "Kopiuj do schowka",
|
||||
"account_tokens_table_copied_to_clipboard": "Token został skopiowany",
|
||||
"account_tokens_table_cannot_delete_or_edit": "Nie można edytować ani usunąć tokenu aktualnej sesji",
|
||||
"account_tokens_table_create_token_button": "Utwórz token dostępowy",
|
||||
|
|
|
@ -144,7 +144,7 @@
|
|||
"subscribe_dialog_login_description": "Esse tópico é protegido por palavra-passe. Por favor insira um nome de utilizador e palavra-passe para subscrever.",
|
||||
"subscribe_dialog_login_username_label": "Nome, por exemplo: \"filipe\"",
|
||||
"subscribe_dialog_login_password_label": "Palavra-passe",
|
||||
"subscribe_dialog_login_button_back": "Voltar",
|
||||
"common_back": "Voltar",
|
||||
"subscribe_dialog_login_button_login": "Autenticar",
|
||||
"subscribe_dialog_error_user_anonymous": "anónimo",
|
||||
"prefs_notifications_title": "Notificações",
|
||||
|
|
|
@ -93,7 +93,7 @@
|
|||
"prefs_notifications_min_priority_low_and_higher": "Baixa prioridade e acima",
|
||||
"prefs_notifications_min_priority_default_and_higher": "Prioridade padrão e acima",
|
||||
"subscribe_dialog_login_password_label": "Senha",
|
||||
"subscribe_dialog_login_button_back": "Voltar",
|
||||
"common_back": "Voltar",
|
||||
"prefs_notifications_min_priority_high_and_higher": "Alta prioridade e acima",
|
||||
"prefs_notifications_min_priority_max_only": "Apenas prioridade máxima",
|
||||
"prefs_notifications_delete_after_title": "Apagar notificações",
|
||||
|
|
|
@ -98,7 +98,7 @@
|
|||
"subscribe_dialog_login_description": "Эта тема защищена паролем. Пожалуйста, введите имя пользователя и пароль, чтобы подписаться.",
|
||||
"subscribe_dialog_login_username_label": "Имя пользователя. Например, phil",
|
||||
"subscribe_dialog_login_password_label": "Пароль",
|
||||
"subscribe_dialog_login_button_back": "Назад",
|
||||
"common_back": "Назад",
|
||||
"subscribe_dialog_login_button_login": "Войти",
|
||||
"subscribe_dialog_error_user_not_authorized": "Пользователь {{username}} не авторизован",
|
||||
"subscribe_dialog_error_user_anonymous": "анонимный пользователь",
|
||||
|
@ -206,7 +206,7 @@
|
|||
"account_basics_tier_free": "Бесплатный",
|
||||
"account_tokens_dialog_title_create": "Создать токен доступа",
|
||||
"account_tokens_dialog_title_delete": "Удалить токен доступа",
|
||||
"account_tokens_table_copy_to_clipboard": "Скопировать в буфер обмена",
|
||||
"common_copy_to_clipboard": "Скопировать в буфер обмена",
|
||||
"account_tokens_dialog_button_cancel": "Отмена",
|
||||
"account_tokens_dialog_expires_unchanged": "Оставить срок истечения без изменений",
|
||||
"account_tokens_dialog_expires_x_days": "Токен истекает через {{days}} дней",
|
||||
|
|
|
@ -95,14 +95,14 @@
|
|||
"publish_dialog_email_placeholder": "Adress att vidarebefordra meddelandet till, t.ex. phil@example.com",
|
||||
"publish_dialog_details_examples_description": "Exempel och en detaljerad beskrivning av alla sändningsfunktioner finns i <docsLink>dokumentationen</docsLink> .",
|
||||
"publish_dialog_button_send": "Skicka",
|
||||
"subscribe_dialog_login_button_back": "Tillbaka",
|
||||
"common_back": "Tillbaka",
|
||||
"account_basics_tier_free": "Gratis",
|
||||
"account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} reserverat ämne",
|
||||
"account_delete_title": "Ta bort konto",
|
||||
"account_upgrade_dialog_tier_features_messages_other": "{{messages}} dagliga meddelanden",
|
||||
"account_upgrade_dialog_tier_features_emails_one": "{{emails}} dagligt e-postmeddelande",
|
||||
"account_upgrade_dialog_button_cancel": "Avbryt",
|
||||
"account_tokens_table_copy_to_clipboard": "Kopiera till urklipp",
|
||||
"common_copy_to_clipboard": "Kopiera till urklipp",
|
||||
"account_tokens_table_copied_to_clipboard": "Åtkomsttoken kopierat",
|
||||
"account_tokens_description": "Använd åtkomsttoken när du publicerar och prenumererar via ntfy API, så att du inte behöver skicka dina kontouppgifter. Läs mer i <Link>dokumentationen</Link>.",
|
||||
"account_tokens_table_create_token_button": "Skapa åtkomsttoken",
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
"subscribe_dialog_login_description": "Bu konu parola korumalı. Abone olmak için lütfen kullanıcı adı ve parola girin.",
|
||||
"subscribe_dialog_login_username_label": "Kullanıcı adı, örn. phil",
|
||||
"subscribe_dialog_login_password_label": "Parola",
|
||||
"subscribe_dialog_login_button_back": "Geri",
|
||||
"common_back": "Geri",
|
||||
"subscribe_dialog_login_button_login": "Oturum aç",
|
||||
"subscribe_dialog_error_user_not_authorized": "{{username}} kullanıcısı yetkili değil",
|
||||
"subscribe_dialog_error_user_anonymous": "anonim",
|
||||
|
@ -268,7 +268,7 @@
|
|||
"account_tokens_table_token_header": "Belirteç",
|
||||
"account_tokens_table_label_header": "Etiket",
|
||||
"account_tokens_table_current_session": "Geçerli tarayıcı oturumu",
|
||||
"account_tokens_table_copy_to_clipboard": "Panoya kopyala",
|
||||
"common_copy_to_clipboard": "Panoya kopyala",
|
||||
"account_tokens_table_copied_to_clipboard": "Erişim belirteci kopyalandı",
|
||||
"account_tokens_table_cannot_delete_or_edit": "Geçerli oturum belirteci düzenlenemez veya silinemez",
|
||||
"account_tokens_table_create_token_button": "Erişim belirteci oluştur",
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
"subscribe_dialog_subscribe_use_another_label": "Використовувати інший сервер",
|
||||
"subscribe_dialog_subscribe_base_url_label": "URL служби",
|
||||
"subscribe_dialog_login_password_label": "Пароль",
|
||||
"subscribe_dialog_login_button_back": "Назад",
|
||||
"common_back": "Назад",
|
||||
"subscribe_dialog_error_user_not_authorized": "{{username}} користувач не авторизований",
|
||||
"prefs_notifications_sound_description_none": "Сповіщення не відтворюють жодного звуку при надходженні",
|
||||
"prefs_notifications_sound_description_some": "Сповіщення відтворюють звук {{sound}}",
|
||||
|
|
|
@ -103,7 +103,7 @@
|
|||
"subscribe_dialog_login_description": "本主题受密码保护,请输入用户名和密码进行订阅。",
|
||||
"subscribe_dialog_login_username_label": "用户名,例如 phil",
|
||||
"subscribe_dialog_login_password_label": "密码",
|
||||
"subscribe_dialog_login_button_back": "返回",
|
||||
"common_back": "返回",
|
||||
"subscribe_dialog_login_button_login": "登录",
|
||||
"subscribe_dialog_error_user_not_authorized": "未授权 {{username}} 用户",
|
||||
"subscribe_dialog_error_user_anonymous": "匿名",
|
||||
|
@ -333,7 +333,7 @@
|
|||
"account_tokens_table_expires_header": "过期",
|
||||
"account_tokens_table_never_expires": "永不过期",
|
||||
"account_tokens_table_current_session": "当前浏览器会话",
|
||||
"account_tokens_table_copy_to_clipboard": "复制到剪贴板",
|
||||
"common_copy_to_clipboard": "复制到剪贴板",
|
||||
"account_tokens_table_copied_to_clipboard": "已复制访问令牌",
|
||||
"account_tokens_table_cannot_delete_or_edit": "无法编辑或删除当前会话令牌",
|
||||
"account_tokens_table_create_token_button": "创建访问令牌",
|
||||
|
|
|
@ -70,7 +70,7 @@
|
|||
"subscribe_dialog_subscribe_button_subscribe": "訂閱",
|
||||
"emoji_picker_search_clear": "清除",
|
||||
"subscribe_dialog_login_password_label": "密碼",
|
||||
"subscribe_dialog_login_button_back": "返回",
|
||||
"common_back": "返回",
|
||||
"subscribe_dialog_login_button_login": "登入",
|
||||
"prefs_notifications_delete_after_never": "從不",
|
||||
"prefs_users_add_button": "新增使用者",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import {
|
||||
accountBillingPortalUrl,
|
||||
accountBillingSubscriptionUrl,
|
||||
accountPasswordUrl,
|
||||
accountPasswordUrl, accountPhoneUrl,
|
||||
accountReservationSingleUrl,
|
||||
accountReservationUrl,
|
||||
accountSettingsUrl,
|
||||
|
@ -299,6 +299,43 @@ class AccountApi {
|
|||
return await response.json(); // May throw SyntaxError
|
||||
}
|
||||
|
||||
async verifyPhone(phoneNumber) {
|
||||
const url = accountPhoneUrl(config.base_url);
|
||||
console.log(`[AccountApi] Sending phone verification ${url}`);
|
||||
await fetchOrThrow(url, {
|
||||
method: "PUT",
|
||||
headers: withBearerAuth({}, session.token()),
|
||||
body: JSON.stringify({
|
||||
number: phoneNumber
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
async checkVerifyPhone(phoneNumber, code) {
|
||||
const url = accountPhoneUrl(config.base_url);
|
||||
console.log(`[AccountApi] Checking phone verification code ${url}`);
|
||||
await fetchOrThrow(url, {
|
||||
method: "POST",
|
||||
headers: withBearerAuth({}, session.token()),
|
||||
body: JSON.stringify({
|
||||
number: phoneNumber,
|
||||
code: code
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
async deletePhoneNumber(phoneNumber, code) {
|
||||
const url = accountPhoneUrl(config.base_url);
|
||||
console.log(`[AccountApi] Deleting phone number ${url}`);
|
||||
await fetchOrThrow(url, {
|
||||
method: "DELETE",
|
||||
headers: withBearerAuth({}, session.token()),
|
||||
body: JSON.stringify({
|
||||
number: phoneNumber
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
async sync() {
|
||||
try {
|
||||
if (!session.token()) {
|
||||
|
|
|
@ -27,6 +27,7 @@ export const accountReservationUrl = (baseUrl) => `${baseUrl}/v1/account/reserva
|
|||
export const accountReservationSingleUrl = (baseUrl, topic) => `${baseUrl}/v1/account/reservation/${topic}`;
|
||||
export const accountBillingSubscriptionUrl = (baseUrl) => `${baseUrl}/v1/account/billing/subscription`;
|
||||
export const accountBillingPortalUrl = (baseUrl) => `${baseUrl}/v1/account/billing/portal`;
|
||||
export const accountPhoneUrl = (baseUrl) => `${baseUrl}/v1/account/phone`;
|
||||
export const tiersUrl = (baseUrl) => `${baseUrl}/v1/tiers`;
|
||||
export const shortUrl = (url) => url.replaceAll(/https?:\/\//g, "");
|
||||
export const expandUrl = (url) => [`https://${url}`, `http://${url}`];
|
||||
|
|
|
@ -325,37 +325,183 @@ const AccountType = () => {
|
|||
const PhoneNumbers = () => {
|
||||
const { t } = useTranslation();
|
||||
const { account } = useContext(AccountContext);
|
||||
const [dialogKey, setDialogKey] = useState(0);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [snackOpen, setSnackOpen] = useState(false);
|
||||
const labelId = "prefPhoneNumbers";
|
||||
|
||||
const handleAdd = () => {
|
||||
|
||||
const handleDialogOpen = () => {
|
||||
setDialogKey(prev => prev+1);
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
|
||||
const handleDialogClose = () => {
|
||||
setDialogOpen(false);
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
|
||||
const handleCopy = (phoneNumber) => {
|
||||
navigator.clipboard.writeText(phoneNumber);
|
||||
setSnackOpen(true);
|
||||
};
|
||||
|
||||
const handleDelete = async (phoneNumber) => {
|
||||
try {
|
||||
await accountApi.deletePhoneNumber(phoneNumber);
|
||||
} catch (e) {
|
||||
console.log(`[Account] Error deleting phone number`, e);
|
||||
if (e instanceof UnauthorizedError) {
|
||||
session.resetAndRedirect(routes.login);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!config.enable_calls) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Pref labelId={labelId} title={t("account_basics_phone_numbers_title")} description={t("account_basics_phone_numbers_description")}>
|
||||
<div aria-labelledby={labelId}>
|
||||
{account?.phone_numbers.map(p =>
|
||||
<Chip
|
||||
label={p.number}
|
||||
variant="outlined"
|
||||
onClick={() => navigator.clipboard.writeText(p.number)}
|
||||
onDelete={() => handleDelete(p.number)}
|
||||
/>
|
||||
{account?.phone_numbers?.map(phoneNumber =>
|
||||
<Chip
|
||||
label={
|
||||
<Tooltip title={t("common_copy_to_clipboard")}>
|
||||
<span>{phoneNumber}</span>
|
||||
</Tooltip>
|
||||
}
|
||||
variant="outlined"
|
||||
onClick={() => handleCopy(phoneNumber)}
|
||||
onDelete={() => handleDelete(phoneNumber)}
|
||||
/>
|
||||
)}
|
||||
<IconButton onClick={() => handleAdd()}><AddIcon/></IconButton>
|
||||
{!account?.phone_numbers &&
|
||||
<em>{t("account_basics_phone_numbers_no_phone_numbers_yet")}</em>
|
||||
}
|
||||
<IconButton onClick={handleDialogOpen}><AddIcon/></IconButton>
|
||||
</div>
|
||||
<AddPhoneNumberDialog
|
||||
key={`addPhoneNumberDialog${dialogKey}`}
|
||||
open={dialogOpen}
|
||||
onClose={handleDialogClose}
|
||||
/>
|
||||
<Portal>
|
||||
<Snackbar
|
||||
open={snackOpen}
|
||||
autoHideDuration={3000}
|
||||
onClose={() => setSnackOpen(false)}
|
||||
message={t("account_basics_phone_numbers_copied_to_clipboard")}
|
||||
/>
|
||||
</Portal>
|
||||
</Pref>
|
||||
)
|
||||
};
|
||||
|
||||
const AddPhoneNumberDialog = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const [error, setError] = useState("");
|
||||
const [phoneNumber, setPhoneNumber] = useState("");
|
||||
const [code, setCode] = useState("");
|
||||
const [sending, setSending] = useState(false);
|
||||
const [verificationCodeSent, setVerificationCodeSent] = useState(false);
|
||||
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
|
||||
const handleDialogSubmit = async () => {
|
||||
if (!verificationCodeSent) {
|
||||
await verifyPhone();
|
||||
} else {
|
||||
await checkVerifyPhone();
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
if (verificationCodeSent) {
|
||||
setVerificationCodeSent(false);
|
||||
} else {
|
||||
props.onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const verifyPhone = async () => {
|
||||
try {
|
||||
setSending(true);
|
||||
await accountApi.verifyPhone(phoneNumber);
|
||||
setVerificationCodeSent(true);
|
||||
} catch (e) {
|
||||
console.log(`[Account] Error sending verification`, e);
|
||||
if (e instanceof UnauthorizedError) {
|
||||
session.resetAndRedirect(routes.login);
|
||||
} else {
|
||||
setError(e.message);
|
||||
}
|
||||
} finally {
|
||||
setSending(false);
|
||||
}
|
||||
};
|
||||
|
||||
const checkVerifyPhone = async () => {
|
||||
try {
|
||||
setSending(true);
|
||||
await accountApi.checkVerifyPhone(phoneNumber, code);
|
||||
props.onClose();
|
||||
} catch (e) {
|
||||
console.log(`[Account] Error confirming verification`, e);
|
||||
if (e instanceof UnauthorizedError) {
|
||||
session.resetAndRedirect(routes.login);
|
||||
} else {
|
||||
setError(e.message);
|
||||
}
|
||||
} finally {
|
||||
setSending(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={props.open} onClose={props.onCancel} fullScreen={fullScreen}>
|
||||
<DialogTitle>{t("account_basics_phone_numbers_dialog_title")}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
{t("account_basics_phone_numbers_dialog_description")}
|
||||
</DialogContentText>
|
||||
{!verificationCodeSent &&
|
||||
<TextField
|
||||
margin="dense"
|
||||
label={t("account_basics_phone_numbers_dialog_number_label")}
|
||||
aria-label={t("account_basics_phone_numbers_dialog_number_label")}
|
||||
placeholder={t("account_basics_phone_numbers_dialog_number_placeholder")}
|
||||
type="tel"
|
||||
value={phoneNumber}
|
||||
onChange={ev => setPhoneNumber(ev.target.value)}
|
||||
fullWidth
|
||||
inputProps={{ inputMode: 'tel', pattern: '\+[0-9]*' }}
|
||||
variant="standard"
|
||||
/>
|
||||
}
|
||||
{verificationCodeSent &&
|
||||
<TextField
|
||||
margin="dense"
|
||||
label={t("account_basics_phone_numbers_dialog_code_label")}
|
||||
aria-label={t("account_basics_phone_numbers_dialog_code_label")}
|
||||
placeholder={t("account_basics_phone_numbers_dialog_code_placeholder")}
|
||||
type="text"
|
||||
value={code}
|
||||
onChange={ev => setCode(ev.target.value)}
|
||||
fullWidth
|
||||
inputProps={{ inputMode: 'numeric', pattern: '[0-9]*' }}
|
||||
variant="standard"
|
||||
/>
|
||||
}
|
||||
</DialogContent>
|
||||
<DialogFooter status={error}>
|
||||
<Button onClick={handleCancel}>{verificationCodeSent ? t("common_back") : t("common_cancel")}</Button>
|
||||
<Button onClick={handleDialogSubmit} disabled={sending || !/^\+\d+$/.test(phoneNumber)}>
|
||||
{verificationCodeSent ?t("account_basics_phone_numbers_dialog_check_verification_button") : t("account_basics_phone_numbers_dialog_send_verification_button")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const Stats = () => {
|
||||
const { t } = useTranslation();
|
||||
const { account } = useContext(AccountContext);
|
||||
|
@ -594,7 +740,7 @@ const TokensTable = (props) => {
|
|||
<span>
|
||||
<span style={{fontFamily: "Monospace", fontSize: "0.9rem"}}>{token.token.slice(0, 12)}</span>
|
||||
...
|
||||
<Tooltip title={t("account_tokens_table_copy_to_clipboard")} placement="right">
|
||||
<Tooltip title={t("common_copy_to_clipboard")} placement="right">
|
||||
<IconButton onClick={() => handleCopy(token.token)}><ContentCopy/></IconButton>
|
||||
</Tooltip>
|
||||
</span>
|
||||
|
|
|
@ -288,7 +288,7 @@ const LoginPage = (props) => {
|
|||
/>
|
||||
</DialogContent>
|
||||
<DialogFooter status={error}>
|
||||
<Button onClick={props.onBack}>{t("subscribe_dialog_login_button_back")}</Button>
|
||||
<Button onClick={props.onBack}>{t("common_back")}</Button>
|
||||
<Button onClick={handleLogin}>{t("subscribe_dialog_login_button_login")}</Button>
|
||||
</DialogFooter>
|
||||
</>
|
||||
|
|
|
@ -300,11 +300,9 @@ const TierCard = (props) => {
|
|||
{tier.limits.reservations > 0 && <Feature>{t("account_upgrade_dialog_tier_features_reservations", { reservations: tier.limits.reservations, count: tier.limits.reservations })}</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>
|
||||
{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>
|
||||
{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>
|
||||
{tier.prices && props.interval === SubscriptionInterval.MONTH &&
|
||||
|
|
Loading…
Reference in a new issue