diff --git a/docs/config.md b/docs/config.md index 353a9d03..d6f6e408 100644 --- a/docs/config.md +++ b/docs/config.md @@ -868,8 +868,8 @@ are the easiest), and then configure the following options: * `twilio-from-number` is the outgoing phone number you purchased, e.g. +18775132586 * `twilio-verify-service` is the Twilio Verify service SID, e.g. VA12345beefbeef67890beefbeef122586 -After you have configured phone calls, create a [tier](#tiers) with a call limit, and then assign it to a user. -Users may then use the `X-Call` header to receive a phone call when publishing a message. +After you have configured phone calls, create a [tier](#tiers) with a call limit (e.g. `ntfy tier create --call-limit=10 ...`), +and then assign it to a user. Users may then use the `X-Call` header to receive a phone call when publishing a message. ## Rate limiting !!! info diff --git a/docs/publish.md b/docs/publish.md index 98f3e876..3cca6fc6 100644 --- a/docs/publish.md +++ b/docs/publish.md @@ -2702,16 +2702,26 @@ You can use ntfy to call a phone and **read the message out loud using text-to-s Similar to email notifications, this can be useful to blast-notify yourself on all possible channels, or to notify people that do not have the ntfy app installed on their phone. -**Phone numbers have to be previously verified** (via the web app), so this feature is **only available to authenticated users**. -To forward a message as a voice call, pass a phone number in the `X-Call` header (or its alias: `Call`), prefixed with a -plus sign and the country code, e.g. `+12223334444`. You may also simply pass `yes` as a value to pick the first of your -verified phone numbers. +**Phone numbers have to be previously verified** (via the [web app](https://ntfy.sh/account)), so this feature is +**only available to authenticated users** (no anonymous phone calls). To forward a message as a voice call, pass a phone +number in the `X-Call` header (or its alias: `Call`), prefixed with a plus sign and the country code, e.g. `+12223334444`. +You may also simply pass `yes` as a value to pick the first of your verified phone numbers. +On ntfy.sh, this feature is only supported to [ntfy Pro](https://ntfy.sh/app) plans. + +
+ ![e-mail publishing](static/img/web-phone-verify.png) +
Phone number verification in the web app
+
+ +As of today, the text-to-speed voice used will only support English. If there is demand for other languages, we'll +be happy to add support for that. Please [open an issue on GitHub](https://github.com/binwiederhier/ntfy/issues). !!! info - As of today, the text-to-speed voice used will only support English. If there is demand for other languages, we'll - be happy to add support for that. Please [open an issue on GitHub](https://github.com/binwiederhier/ntfy/issues). + You are responsible for the message content, and **you must abide by the [Twilio Acceptable Use Policy](https://www.twilio.com/en-us/legal/aup)**. + This particularly means that you must not use this feature to send unsolicited messages, or messages that are illegal or + violate the rights of others. Please read the policy for details. Failure to do so may result in your account being suspended or terminated. -On ntfy.sh, this feature is only supported to [ntfy Pro](https://ntfy.sh/app) plans. +Here's how you use it: === "Command line (curl)" ``` @@ -3431,17 +3441,18 @@ There are a few limitations to the API to prevent abuse and to keep the server h are configurable via the server side [rate limiting settings](config.md#rate-limiting). Most of these limits you won't run into, but just in case, let's list them all: -| Limit | Description | -|---------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| **Message length** | Each message can be up to 4,096 bytes long. Longer messages are treated as [attachments](#attachments). | -| **Requests** | By default, the server is configured to allow 60 requests per visitor at once, and then refills the your allowed requests bucket at a rate of one request per 5 seconds. | -| **Daily messages** | By default, the number of messages is governed by the request limits. This can be overridden. On ntfy.sh, the daily message limit is 250. | -| **E-mails** | By default, the server is configured to allow sending 16 e-mails per visitor at once, and then refills the your allowed e-mail bucket at a rate of one per hour. On ntfy.sh, the daily limit is 5. | -| **Subscription limit** | By default, the server allows each visitor to keep 30 connections to the server open. | -| **Attachment size limit** | By default, the server allows attachments up to 15 MB in size, up to 100 MB in total per visitor and up to 5 GB across all visitors. On ntfy.sh, the attachment size limit is 2 MB, and the per-visitor total is 20 MB. | -| **Attachment expiry** | By default, the server deletes attachments after 3 hours and thereby frees up space from the total visitor attachment limit. | -| **Attachment bandwidth** | By default, the server allows 500 MB of GET/PUT/POST traffic for attachments per visitor in a 24 hour period. Traffic exceeding that is rejected. On ntfy.sh, the daily bandwidth limit is 200 MB. | -| **Total number of topics** | By default, the server is configured to allow 15,000 topics. The ntfy.sh server has higher limits though. | +| Limit | Description | +|----------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **Message length** | Each message can be up to 4,096 bytes long. Longer messages are treated as [attachments](#attachments). | +| **Requests** | By default, the server is configured to allow 60 requests per visitor at once, and then refills the your allowed requests bucket at a rate of one request per 5 seconds. | +| **Daily messages** | By default, the number of messages is governed by the request limits. This can be overridden. On ntfy.sh, the daily message limit is 250. | +| **E-mails** | By default, the server is configured to allow sending 16 e-mails per visitor at once, and then refills the your allowed e-mail bucket at a rate of one per hour. On ntfy.sh, the daily limit is 5. | +| **Phone calls** | By default, the server does not allow any phone calls, except for users with a tier that has a call limit. | +| **Subscription limit** | By default, the server allows each visitor to keep 30 connections to the server open. | +| **Attachment size limit** | By default, the server allows attachments up to 15 MB in size, up to 100 MB in total per visitor and up to 5 GB across all visitors. On ntfy.sh, the attachment size limit is 2 MB, and the per-visitor total is 20 MB. | +| **Attachment expiry** | By default, the server deletes attachments after 3 hours and thereby frees up space from the total visitor attachment limit. | +| **Attachment bandwidth** | By default, the server allows 500 MB of GET/PUT/POST traffic for attachments per visitor in a 24 hour period. Traffic exceeding that is rejected. On ntfy.sh, the daily bandwidth limit is 200 MB. | +| **Total number of topics** | By default, the server is configured to allow 15,000 topics. The ntfy.sh server has higher limits though. | These limits can be changed on a per-user basis using [tiers](config.md#tiers). If [payments](config.md#payments) are enabled, a user tier can be changed by purchasing a higher tier. ntfy.sh offers multiple paid tiers, which allows for much hier limits than the ones listed above. @@ -3470,6 +3481,7 @@ table in their canonical form. | `X-Icon` | `Icon` | URL to use as notification [icon](#icons) | | `X-Filename` | `Filename`, `file`, `f` | Optional [attachment](#attachments) filename, as it appears in the client | | `X-Email` | `X-E-Mail`, `Email`, `E-Mail`, `mail`, `e` | E-mail address for [e-mail notifications](#e-mail-notifications) | +| `X-Call` | `Call` | Phone number for [phone calls](#phone-calls) | | `X-Cache` | `Cache` | Allows disabling [message caching](#message-caching) | | `X-Firebase` | `Firebase` | Allows disabling [sending to Firebase](#disable-firebase) | | `X-UnifiedPush` | `UnifiedPush`, `up` | [UnifiedPush](#unifiedpush) publish option, only to be used by UnifiedPush apps | diff --git a/docs/static/img/web-phone-verify.png b/docs/static/img/web-phone-verify.png new file mode 100644 index 00000000..335aeef1 Binary files /dev/null and b/docs/static/img/web-phone-verify.png differ diff --git a/go.sum b/go.sum index ccb1ae4d..ff2d580f 100644 --- a/go.sum +++ b/go.sum @@ -1,23 +1,15 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.110.1 h1:oDJ19Fu9TX9Xs06iyCw4yifSqZ7JQ8BeuVHcTmWQlOA= -cloud.google.com/go v0.110.1/go.mod h1:uc+V/WjzxQ7vpkxfJhgW4Q4axWXyfAerpQOuSNDZyFw= cloud.google.com/go v0.110.2 h1:sdFPBr6xG9/wkBbfhmUz/JmZC7X6LavQgcrVINrKiVA= cloud.google.com/go v0.110.2/go.mod h1:k04UEeEtb6ZBRTv3dZz4CeJC3jKGxyhl0sAiVVquxiw= -cloud.google.com/go/compute v1.19.1 h1:am86mquDUgjGNWxiGn+5PGLbmgiWXlE/yNWpIpNvuXY= -cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE= cloud.google.com/go/compute v1.19.2 h1:GbJtPo8OKVHbVep8jvM57KidbYHxeE68LOVqouNLrDY= cloud.google.com/go/compute v1.19.2/go.mod h1:5f5a+iC1IriXYauaQ0EyQmEAEq9CGRnV5xJSQSlTV08= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/firestore v1.9.0 h1:IBlRyxgGySXu5VuW0RgGFlTtLukSnNkpDiEOMkQkmpA= cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE= -cloud.google.com/go/iam v1.0.0 h1:hlQJMovyJJwYjZcTohUH4o1L8Z8kYz+E+W/zktiLCBc= -cloud.google.com/go/iam v1.0.0/go.mod h1:ikbQ4f1r91wTmBmmOtBCOtuEOei6taatNXytzB7Cxew= cloud.google.com/go/iam v1.0.1 h1:lyeCAU6jpnVNrE9zGQkTl3WgNgK/X+uWwaw0kynZJMU= cloud.google.com/go/iam v1.0.1/go.mod h1:yR3tmSL8BcZB4bxByRv2jkSIahVmCtfKZwLYGBalRE8= -cloud.google.com/go/longrunning v0.4.1 h1:v+yFJOfKC3yZdY6ZUI933pIYdhyhV8S3NpWrXWmg7jM= -cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo= cloud.google.com/go/longrunning v0.4.2 h1:WDKiiNXFTaQ6qz/G8FCOkuY9kJmOJGY67wPUC1M2RbE= cloud.google.com/go/longrunning v0.4.2/go.mod h1:OHrnaYyLUV6oqwh0xiS7e5sLQhP1m0QU9R+WhGDMgIQ= cloud.google.com/go/storage v1.30.1 h1:uOdMxAs8HExqBlnLtnQyP0YkvbiDpdGShGKtx6U/oNM= @@ -147,8 +139,6 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stripe/stripe-go/v74 v74.17.0 h1:qVWSzmADr6gudznuAcPjB9ewzgxfyIhBCkyTbkxJcCw= -github.com/stripe/stripe-go/v74 v74.17.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw= github.com/stripe/stripe-go/v74 v74.18.0 h1:ImSIoaVkTUozHxa21AhwHYBjwc8fVSJJJB1Q7oaXzIw= github.com/stripe/stripe-go/v74 v74.18.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw= github.com/urfave/cli/v2 v2.25.3 h1:VJkt6wvEBOoSjPFQvOkv6iWIrsJyCrKGtCtxXWwmGeY= @@ -163,8 +153,6 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ= -golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -186,14 +174,10 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= -golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g= -golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8= golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -241,8 +225,6 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -google.golang.org/api v0.121.0 h1:8Oopoo8Vavxx6gt+sgs8s8/X60WBAtKQq6JqnkF+xow= -google.golang.org/api v0.121.0/go.mod h1:gcitW0lvnyWjSp9nKxAbdHKIZ6vF4aajGueeslZOyms= google.golang.org/api v0.122.0 h1:zDobeejm3E7pEG1mNHvdxvjs5XJoCMzyNH+CmwL94Es= google.golang.org/api v0.122.0/go.mod h1:gcitW0lvnyWjSp9nKxAbdHKIZ6vF4aajGueeslZOyms= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= diff --git a/server/errors.go b/server/errors.go index a42641b4..b67558db 100644 --- a/server/errors.go +++ b/server/errors.go @@ -112,6 +112,7 @@ var ( errHTTPBadRequestPhoneNumberInvalid = &errHTTP{40033, http.StatusBadRequest, "invalid request: phone number invalid", "https://ntfy.sh/docs/publish/#phone-calls", nil} errHTTPBadRequestPhoneNumberNotVerified = &errHTTP{40034, http.StatusBadRequest, "invalid request: phone number not verified, or no matching verified numbers found", "https://ntfy.sh/docs/publish/#phone-calls", nil} errHTTPBadRequestAnonymousCallsNotAllowed = &errHTTP{40035, http.StatusBadRequest, "invalid request: anonymous phone calls are not allowed", "https://ntfy.sh/docs/publish/#phone-calls", nil} + errHTTPBadRequestPhoneNumberVerifyChannelInvalid = &errHTTP{40036, http.StatusBadRequest, "invalid request: verification channel must be 'sms' or 'call'", "https://ntfy.sh/docs/publish/#phone-calls", nil} errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", "", nil} errHTTPUnauthorized = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication", nil} errHTTPForbidden = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication", nil} diff --git a/server/server_account.go b/server/server_account.go index 2330eab8..b9997ef3 100644 --- a/server/server_account.go +++ b/server/server_account.go @@ -523,12 +523,13 @@ func (s *Server) maybeRemoveMessagesAndExcessReservations(r *http.Request, v *vi func (s *Server) handleAccountPhoneNumberVerify(w http.ResponseWriter, r *http.Request, v *visitor) error { u := v.User() - req, err := readJSONWithLimit[apiAccountPhoneNumberRequest](r.Body, jsonBodyBytesLimit, false) + req, err := readJSONWithLimit[apiAccountPhoneNumberVerifyRequest](r.Body, jsonBodyBytesLimit, false) if err != nil { return err - } - if !phoneNumberRegex.MatchString(req.Number) { + } else if !phoneNumberRegex.MatchString(req.Number) { return errHTTPBadRequestPhoneNumberInvalid + } else if req.Channel != "sms" && req.Channel != "call" { + return errHTTPBadRequestPhoneNumberVerifyChannelInvalid } // Check user is allowed to add phone numbers if u == nil || (u.IsUser() && u.Tier == nil) { @@ -545,7 +546,7 @@ func (s *Server) handleAccountPhoneNumberVerify(w http.ResponseWriter, r *http.R } // 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.verifyPhoneNumber(v, r, req.Number); err != nil { + if err := s.verifyPhoneNumber(v, r, req.Number, req.Channel); err != nil { return err } return s.writeJSON(w, newSuccessResponse()) @@ -553,7 +554,7 @@ func (s *Server) handleAccountPhoneNumberVerify(w http.ResponseWriter, r *http.R func (s *Server) handleAccountPhoneNumberAdd(w http.ResponseWriter, r *http.Request, v *visitor) error { u := v.User() - req, err := readJSONWithLimit[apiAccountPhoneNumberRequest](r.Body, jsonBodyBytesLimit, false) + req, err := readJSONWithLimit[apiAccountPhoneNumberAddRequest](r.Body, jsonBodyBytesLimit, false) if err != nil { return err } @@ -572,7 +573,7 @@ func (s *Server) handleAccountPhoneNumberAdd(w http.ResponseWriter, r *http.Requ func (s *Server) handleAccountPhoneNumberDelete(w http.ResponseWriter, r *http.Request, v *visitor) error { u := v.User() - req, err := readJSONWithLimit[apiAccountPhoneNumberRequest](r.Body, jsonBodyBytesLimit, false) + req, err := readJSONWithLimit[apiAccountPhoneNumberAddRequest](r.Body, jsonBodyBytesLimit, false) if err != nil { return err } diff --git a/server/server_twilio.go b/server/server_twilio.go index f8067490..11f58aa7 100644 --- a/server/server_twilio.go +++ b/server/server_twilio.go @@ -18,13 +18,14 @@ const ( - You have a notification from notify on topic %s. Message: + You have a message from notify on topic %s. Message: %s - End message. + End of message. - This message was sent by user %s. It will be repeated up to three times. + This message was sent by user %s. It will be repeated three times. + To unsubscribe from calls like this, remove your phone number in the notify web app. Goodbye. @@ -97,11 +98,11 @@ func (s *Server) callPhoneInternal(data url.Values) (string, error) { return string(response), nil } -func (s *Server) verifyPhoneNumber(v *visitor, r *http.Request, phoneNumber string) error { - ev := logvr(v, r).Tag(tagTwilio).Field("twilio_to", phoneNumber).Debug("Sending phone verification") +func (s *Server) verifyPhoneNumber(v *visitor, r *http.Request, phoneNumber, channel string) error { + ev := logvr(v, r).Tag(tagTwilio).Field("twilio_to", phoneNumber).Field("twilio_channel", channel).Debug("Sending phone verification") data := url.Values{} data.Set("To", phoneNumber) - data.Set("Channel", "sms") + data.Set("Channel", channel) requestURL := fmt.Sprintf("%s/v2/Services/%s/Verifications", s.config.TwilioVerifyBaseURL, s.config.TwilioVerifyService) req, err := http.NewRequest(http.MethodPost, requestURL, strings.NewReader(data.Encode())) if err != nil { diff --git a/server/types.go b/server/types.go index a1d18926..3b733678 100644 --- a/server/types.go +++ b/server/types.go @@ -311,9 +311,14 @@ type apiAccountTokenResponse struct { Expires int64 `json:"expires,omitempty"` // Unix timestamp } -type apiAccountPhoneNumberRequest struct { +type apiAccountPhoneNumberVerifyRequest struct { + Number string `json:"number"` + Channel string `json:"channel"` +} + +type apiAccountPhoneNumberAddRequest struct { Number string `json:"number"` - Code string `json:"code,omitempty"` // Only supplied in "verify" call + Code string `json:"code,omitempty"` } type apiAccountTier struct { diff --git a/web/public/static/langs/en.json b/web/public/static/langs/en.json index f2120e5f..588a1f9f 100644 --- a/web/public/static/langs/en.json +++ b/web/public/static/langs/en.json @@ -188,17 +188,20 @@ "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_dialog_description": "To use the call notification feature, you need to add and verify at least one phone number. Verification can be done via SMS or a phone call.", "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_verify_button_sms": "Send SMS", + "account_basics_phone_numbers_dialog_verify_button_call": "Call me", "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_basics_phone_numbers_dialog_channel_sms": "SMS", + "account_basics_phone_numbers_dialog_channel_call": "Call", "account_usage_title": "Usage", "account_usage_of_limit": "of {{limit}}", "account_usage_unlimited": "Unlimited", diff --git a/web/src/app/AccountApi.js b/web/src/app/AccountApi.js index b5bfcd29..8908f306 100644 --- a/web/src/app/AccountApi.js +++ b/web/src/app/AccountApi.js @@ -299,14 +299,15 @@ class AccountApi { return await response.json(); // May throw SyntaxError } - async verifyPhoneNumber(phoneNumber) { + async verifyPhoneNumber(phoneNumber, channel) { const url = accountPhoneVerifyUrl(config.base_url); console.log(`[AccountApi] Sending phone verification ${url}`); await fetchOrThrow(url, { method: "PUT", headers: withBearerAuth({}, session.token()), body: JSON.stringify({ - number: phoneNumber + number: phoneNumber, + channel: channel }) }); } diff --git a/web/src/components/Account.js b/web/src/components/Account.js index b4a378e6..b480ea6b 100644 --- a/web/src/components/Account.js +++ b/web/src/components/Account.js @@ -1,13 +1,13 @@ import * as React from 'react'; import {useContext, useState} from 'react'; import { - Alert, + Alert, ButtonGroup, CardActions, CardContent, Chip, - FormControl, + FormControl, FormControlLabel, InputLabel, LinearProgress, Link, - Portal, + Portal, Radio, RadioGroup, Select, Snackbar, Stack, @@ -47,12 +47,14 @@ import {AccountContext} from "./App"; import DialogFooter from "./DialogFooter"; import {Paragraph} from "./styles"; import CloseIcon from "@mui/icons-material/Close"; -import {ContentCopy, Public} from "@mui/icons-material"; +import {Check, ContentCopy, DeleteForever, Public} from "@mui/icons-material"; import MenuItem from "@mui/material/MenuItem"; import DialogContentText from "@mui/material/DialogContentText"; import {IncorrectPasswordError, UnauthorizedError} from "../app/errors"; import {ProChip} from "./SubscriptionPopup"; import AddIcon from "@mui/icons-material/Add"; +import ListItemIcon from "@mui/material/ListItemIcon"; +import ListItemText from "@mui/material/ListItemText"; const Account = () => { if (!session.exists()) { @@ -408,6 +410,7 @@ const AddPhoneNumberDialog = (props) => { const { t } = useTranslation(); const [error, setError] = useState(""); const [phoneNumber, setPhoneNumber] = useState(""); + const [channel, setChannel] = useState("sms"); const [code, setCode] = useState(""); const [sending, setSending] = useState(false); const [verificationCodeSent, setVerificationCodeSent] = useState(false); @@ -432,7 +435,7 @@ const AddPhoneNumberDialog = (props) => { const verifyPhone = async () => { try { setSending(true); - await accountApi.verifyPhoneNumber(phoneNumber); + await accountApi.verifyPhoneNumber(phoneNumber, channel); setVerificationCodeSent(true); } catch (e) { console.log(`[Account] Error sending verification`, e); @@ -471,18 +474,26 @@ const AddPhoneNumberDialog = (props) => { {t("account_basics_phone_numbers_dialog_description")} {!verificationCodeSent && - setPhoneNumber(ev.target.value)} - fullWidth - inputProps={{ inputMode: 'tel', pattern: '\+[0-9]*' }} - variant="standard" - /> +
+ setPhoneNumber(ev.target.value)} + inputProps={{ inputMode: 'tel', pattern: '\+[0-9]*' }} + variant="standard" + sx={{ flexGrow: 1 }} + /> + + + setChannel(e.target.value)} />} label={t("account_basics_phone_numbers_dialog_channel_sms")} /> + setChannel(e.target.value)} />} label={t("account_basics_phone_numbers_dialog_channel_call")} sx={{ marginRight: 0 }} /> + + +
} {verificationCodeSent && {