Add call verification
This commit is contained in:
		
							parent
							
								
									496d6e74b0
								
							
						
					
					
						commit
						2c81773d01
					
				
					 11 changed files with 93 additions and 74 deletions
				
			
		|  | @ -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-from-number` is the outgoing phone number you purchased, e.g. +18775132586  | ||||||
| * `twilio-verify-service` is the Twilio Verify service SID, e.g. VA12345beefbeef67890beefbeef122586 | * `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. | After you have configured phone calls, create a [tier](#tiers) with a call limit (e.g. `ntfy tier create --call-limit=10 ...`), | ||||||
| Users may then use the `X-Call` header to receive a phone call when publishing a message. | 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 | ## Rate limiting | ||||||
| !!! info | !!! info | ||||||
|  |  | ||||||
|  | @ -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  | 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. | 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**.  | **Phone numbers have to be previously verified** (via the [web app](https://ntfy.sh/account)), so this feature is  | ||||||
| To forward a message as a voice call, pass a phone number in the `X-Call` header (or its alias: `Call`), prefixed with a | **only available to authenticated users** (no anonymous phone calls). To forward a message as a voice call, pass a phone | ||||||
| plus sign and the country code, e.g. `+12223334444`. You may also simply pass `yes` as a value to pick the first of your  | number in the `X-Call` header (or its alias: `Call`), prefixed with a plus sign and the country code, e.g. `+12223334444`.  | ||||||
| verified phone numbers. | 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. | ||||||
|  | 
 | ||||||
|  | <figure markdown> | ||||||
|  |    | ||||||
|  |   <figcaption>Phone number verification in the <a href="https://ntfy.sh/account">web app</a></figcaption> | ||||||
|  | </figure> | ||||||
|  | 
 | ||||||
|  | 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 | !!! info | ||||||
|     As of today, the text-to-speed voice used will only support English. If there is demand for other languages, we'll |     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)**. | ||||||
|     be happy to add support for that. Please [open an issue on GitHub](https://github.com/binwiederhier/ntfy/issues). |     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)" | === "Command line (curl)" | ||||||
|     ``` |     ``` | ||||||
|  | @ -3432,11 +3442,12 @@ are configurable via the server side [rate limiting settings](config.md#rate-lim | ||||||
| but just in case, let's list them all: | but just in case, let's list them all: | ||||||
| 
 | 
 | ||||||
| | Limit                      | Description                                                                                                                                                                                                             | | | Limit                      | Description                                                                                                                                                                                                             | | ||||||
| |---------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | |----------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | ||||||
| | **Message length**         | Each message can be up to 4,096 bytes long. Longer messages are treated as [attachments](#attachments).                                                                                                                 | | | **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.                                                | | | **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.                                                                               | | | **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.                      | | | **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.                                                                                                                                   | | | **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 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 expiry**      | By default, the server deletes attachments after 3 hours and thereby frees up space from the total visitor attachment limit.                                                                                            | | ||||||
|  | @ -3470,6 +3481,7 @@ table in their canonical form. | ||||||
| | `X-Icon`        | `Icon`                                     | URL to use as notification [icon](#icons)                                                     | | | `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-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-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-Cache`       | `Cache`                                    | Allows disabling [message caching](#message-caching)                                          | | ||||||
| | `X-Firebase`    | `Firebase`                                 | Allows disabling [sending to Firebase](#disable-firebase)                                     | | | `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               | | | `X-UnifiedPush` | `UnifiedPush`, `up`                        | [UnifiedPush](#unifiedpush) publish option, only to be used by UnifiedPush apps               | | ||||||
|  |  | ||||||
							
								
								
									
										
											BIN
										
									
								
								docs/static/img/web-phone-verify.png
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								docs/static/img/web-phone-verify.png
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 22 KiB | 
							
								
								
									
										18
									
								
								go.sum
									
										
									
									
									
								
							
							
						
						
									
										18
									
								
								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.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.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 h1:sdFPBr6xG9/wkBbfhmUz/JmZC7X6LavQgcrVINrKiVA= | ||||||
| cloud.google.com/go v0.110.2/go.mod h1:k04UEeEtb6ZBRTv3dZz4CeJC3jKGxyhl0sAiVVquxiw= | 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 h1:GbJtPo8OKVHbVep8jvM57KidbYHxeE68LOVqouNLrDY= | ||||||
| cloud.google.com/go/compute v1.19.2/go.mod h1:5f5a+iC1IriXYauaQ0EyQmEAEq9CGRnV5xJSQSlTV08= | 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 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= | ||||||
| cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= | 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 h1:IBlRyxgGySXu5VuW0RgGFlTtLukSnNkpDiEOMkQkmpA= | ||||||
| cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE= | 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 h1:lyeCAU6jpnVNrE9zGQkTl3WgNgK/X+uWwaw0kynZJMU= | ||||||
| cloud.google.com/go/iam v1.0.1/go.mod h1:yR3tmSL8BcZB4bxByRv2jkSIahVmCtfKZwLYGBalRE8= | 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 h1:WDKiiNXFTaQ6qz/G8FCOkuY9kJmOJGY67wPUC1M2RbE= | ||||||
| cloud.google.com/go/longrunning v0.4.2/go.mod h1:OHrnaYyLUV6oqwh0xiS7e5sLQhP1m0QU9R+WhGDMgIQ= | cloud.google.com/go/longrunning v0.4.2/go.mod h1:OHrnaYyLUV6oqwh0xiS7e5sLQhP1m0QU9R+WhGDMgIQ= | ||||||
| cloud.google.com/go/storage v1.30.1 h1:uOdMxAs8HExqBlnLtnQyP0YkvbiDpdGShGKtx6U/oNM= | 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.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= | ||||||
| github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= | ||||||
| github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= | 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 h1:ImSIoaVkTUozHxa21AhwHYBjwc8fVSJJJB1Q7oaXzIw= | ||||||
| github.com/stripe/stripe-go/v74 v74.18.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw= | github.com/stripe/stripe-go/v74 v74.18.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw= | ||||||
| github.com/urfave/cli/v2 v2.25.3 h1:VJkt6wvEBOoSjPFQvOkv6iWIrsJyCrKGtCtxXWwmGeY= | 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-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-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.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 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= | ||||||
| golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= | 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= | 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-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-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.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 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= | ||||||
| golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= | 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-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.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 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8= | ||||||
| golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= | 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= | 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-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 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= | ||||||
| golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= | 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 h1:zDobeejm3E7pEG1mNHvdxvjs5XJoCMzyNH+CmwL94Es= | ||||||
| google.golang.org/api v0.122.0/go.mod h1:gcitW0lvnyWjSp9nKxAbdHKIZ6vF4aajGueeslZOyms= | google.golang.org/api v0.122.0/go.mod h1:gcitW0lvnyWjSp9nKxAbdHKIZ6vF4aajGueeslZOyms= | ||||||
| google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= | ||||||
|  |  | ||||||
|  | @ -112,6 +112,7 @@ var ( | ||||||
| 	errHTTPBadRequestPhoneNumberInvalid              = &errHTTP{40033, http.StatusBadRequest, "invalid request: phone number invalid", "https://ntfy.sh/docs/publish/#phone-calls", nil} | 	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} | 	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} | 	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} | 	errHTTPNotFound                                  = &errHTTP{40401, http.StatusNotFound, "page not found", "", nil} | ||||||
| 	errHTTPUnauthorized                              = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication", 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} | 	errHTTPForbidden                                 = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication", nil} | ||||||
|  |  | ||||||
|  | @ -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 { | func (s *Server) handleAccountPhoneNumberVerify(w http.ResponseWriter, r *http.Request, v *visitor) error { | ||||||
| 	u := v.User() | 	u := v.User() | ||||||
| 	req, err := readJSONWithLimit[apiAccountPhoneNumberRequest](r.Body, jsonBodyBytesLimit, false) | 	req, err := readJSONWithLimit[apiAccountPhoneNumberVerifyRequest](r.Body, jsonBodyBytesLimit, false) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} else if !phoneNumberRegex.MatchString(req.Number) { | ||||||
| 	if !phoneNumberRegex.MatchString(req.Number) { |  | ||||||
| 		return errHTTPBadRequestPhoneNumberInvalid | 		return errHTTPBadRequestPhoneNumberInvalid | ||||||
|  | 	} else if req.Channel != "sms" && req.Channel != "call" { | ||||||
|  | 		return errHTTPBadRequestPhoneNumberVerifyChannelInvalid | ||||||
| 	} | 	} | ||||||
| 	// Check user is allowed to add phone numbers | 	// Check user is allowed to add phone numbers | ||||||
| 	if u == nil || (u.IsUser() && u.Tier == nil) { | 	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 | 	// Actually add the unverified number, and send verification | ||||||
| 	logvr(v, r).Tag(tagAccount).Field("phone_number", req.Number).Debug("Sending phone number 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 err | ||||||
| 	} | 	} | ||||||
| 	return s.writeJSON(w, newSuccessResponse()) | 	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 { | func (s *Server) handleAccountPhoneNumberAdd(w http.ResponseWriter, r *http.Request, v *visitor) error { | ||||||
| 	u := v.User() | 	u := v.User() | ||||||
| 	req, err := readJSONWithLimit[apiAccountPhoneNumberRequest](r.Body, jsonBodyBytesLimit, false) | 	req, err := readJSONWithLimit[apiAccountPhoneNumberAddRequest](r.Body, jsonBodyBytesLimit, false) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		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 { | func (s *Server) handleAccountPhoneNumberDelete(w http.ResponseWriter, r *http.Request, v *visitor) error { | ||||||
| 	u := v.User() | 	u := v.User() | ||||||
| 	req, err := readJSONWithLimit[apiAccountPhoneNumberRequest](r.Body, jsonBodyBytesLimit, false) | 	req, err := readJSONWithLimit[apiAccountPhoneNumberAddRequest](r.Body, jsonBodyBytesLimit, false) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -18,13 +18,14 @@ const ( | ||||||
| <Response> | <Response> | ||||||
| 	<Pause length="1"/> | 	<Pause length="1"/> | ||||||
| 	<Say loop="3"> | 	<Say loop="3"> | ||||||
| 		You have a notification from notify on topic %s. Message: | 		You have a message from notify on topic %s. Message: | ||||||
| 		<break time="1s"/> | 		<break time="1s"/> | ||||||
| 		%s | 		%s | ||||||
| 		<break time="1s"/> | 		<break time="1s"/> | ||||||
| 		End message. | 		End of message. | ||||||
| 		<break time="1s"/> | 		<break time="1s"/> | ||||||
| 		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. | ||||||
| 		<break time="3s"/> | 		<break time="3s"/> | ||||||
| 	</Say> | 	</Say> | ||||||
| 	<Say>Goodbye.</Say> | 	<Say>Goodbye.</Say> | ||||||
|  | @ -97,11 +98,11 @@ func (s *Server) callPhoneInternal(data url.Values) (string, error) { | ||||||
| 	return string(response), nil | 	return string(response), nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (s *Server) verifyPhoneNumber(v *visitor, r *http.Request, phoneNumber string) error { | func (s *Server) verifyPhoneNumber(v *visitor, r *http.Request, phoneNumber, channel string) error { | ||||||
| 	ev := logvr(v, r).Tag(tagTwilio).Field("twilio_to", phoneNumber).Debug("Sending phone verification") | 	ev := logvr(v, r).Tag(tagTwilio).Field("twilio_to", phoneNumber).Field("twilio_channel", channel).Debug("Sending phone verification") | ||||||
| 	data := url.Values{} | 	data := url.Values{} | ||||||
| 	data.Set("To", phoneNumber) | 	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) | 	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())) | 	req, err := http.NewRequest(http.MethodPost, requestURL, strings.NewReader(data.Encode())) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|  |  | ||||||
|  | @ -311,9 +311,14 @@ type apiAccountTokenResponse struct { | ||||||
| 	Expires    int64  `json:"expires,omitempty"` // Unix timestamp | 	Expires    int64  `json:"expires,omitempty"` // Unix timestamp | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type apiAccountPhoneNumberRequest struct { | type apiAccountPhoneNumberVerifyRequest struct { | ||||||
| 	Number  string `json:"number"` | 	Number  string `json:"number"` | ||||||
| 	Code   string `json:"code,omitempty"` // Only supplied in "verify" call | 	Channel string `json:"channel"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type apiAccountPhoneNumberAddRequest struct { | ||||||
|  | 	Number string `json:"number"` | ||||||
|  | 	Code   string `json:"code,omitempty"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type apiAccountTier struct { | type apiAccountTier struct { | ||||||
|  |  | ||||||
|  | @ -188,17 +188,20 @@ | ||||||
|   "account_basics_password_dialog_button_submit": "Change password", |   "account_basics_password_dialog_button_submit": "Change password", | ||||||
|   "account_basics_password_dialog_current_password_incorrect": "Password incorrect", |   "account_basics_password_dialog_current_password_incorrect": "Password incorrect", | ||||||
|   "account_basics_phone_numbers_title": "Phone numbers", |   "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_description": "For phone call notifications", | ||||||
|   "account_basics_phone_numbers_no_phone_numbers_yet": "No phone numbers yet", |   "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_copied_to_clipboard": "Phone number copied to clipboard", | ||||||
|   "account_basics_phone_numbers_dialog_title": "Add phone number", |   "account_basics_phone_numbers_dialog_title": "Add phone number", | ||||||
|   "account_basics_phone_numbers_dialog_number_label": "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_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_label": "Verification code", | ||||||
|   "account_basics_phone_numbers_dialog_code_placeholder": "e.g. 123456", |   "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_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_title": "Usage", | ||||||
|   "account_usage_of_limit": "of {{limit}}", |   "account_usage_of_limit": "of {{limit}}", | ||||||
|   "account_usage_unlimited": "Unlimited", |   "account_usage_unlimited": "Unlimited", | ||||||
|  |  | ||||||
|  | @ -299,14 +299,15 @@ class AccountApi { | ||||||
|         return await response.json(); // May throw SyntaxError
 |         return await response.json(); // May throw SyntaxError
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async verifyPhoneNumber(phoneNumber) { |     async verifyPhoneNumber(phoneNumber, channel) { | ||||||
|         const url = accountPhoneVerifyUrl(config.base_url); |         const url = accountPhoneVerifyUrl(config.base_url); | ||||||
|         console.log(`[AccountApi] Sending phone verification ${url}`); |         console.log(`[AccountApi] Sending phone verification ${url}`); | ||||||
|         await fetchOrThrow(url, { |         await fetchOrThrow(url, { | ||||||
|             method: "PUT", |             method: "PUT", | ||||||
|             headers: withBearerAuth({}, session.token()), |             headers: withBearerAuth({}, session.token()), | ||||||
|             body: JSON.stringify({ |             body: JSON.stringify({ | ||||||
|                 number: phoneNumber |                 number: phoneNumber, | ||||||
|  |                 channel: channel | ||||||
|             }) |             }) | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -1,13 +1,13 @@ | ||||||
| import * as React from 'react'; | import * as React from 'react'; | ||||||
| import {useContext, useState} from 'react'; | import {useContext, useState} from 'react'; | ||||||
| import { | import { | ||||||
|     Alert, |     Alert, ButtonGroup, | ||||||
|     CardActions, |     CardActions, | ||||||
|     CardContent, Chip, |     CardContent, Chip, | ||||||
|     FormControl, |     FormControl, FormControlLabel, InputLabel, | ||||||
|     LinearProgress, |     LinearProgress, | ||||||
|     Link, |     Link, | ||||||
|     Portal, |     Portal, Radio, RadioGroup, | ||||||
|     Select, |     Select, | ||||||
|     Snackbar, |     Snackbar, | ||||||
|     Stack, |     Stack, | ||||||
|  | @ -47,12 +47,14 @@ import {AccountContext} from "./App"; | ||||||
| import DialogFooter from "./DialogFooter"; | import DialogFooter from "./DialogFooter"; | ||||||
| import {Paragraph} from "./styles"; | import {Paragraph} from "./styles"; | ||||||
| import CloseIcon from "@mui/icons-material/Close"; | 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 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"; | import {ProChip} from "./SubscriptionPopup"; | ||||||
| import AddIcon from "@mui/icons-material/Add"; | import AddIcon from "@mui/icons-material/Add"; | ||||||
|  | import ListItemIcon from "@mui/material/ListItemIcon"; | ||||||
|  | import ListItemText from "@mui/material/ListItemText"; | ||||||
| 
 | 
 | ||||||
| const Account = () => { | const Account = () => { | ||||||
|     if (!session.exists()) { |     if (!session.exists()) { | ||||||
|  | @ -408,6 +410,7 @@ const AddPhoneNumberDialog = (props) => { | ||||||
|     const { t } = useTranslation(); |     const { t } = useTranslation(); | ||||||
|     const [error, setError] = useState(""); |     const [error, setError] = useState(""); | ||||||
|     const [phoneNumber, setPhoneNumber] = useState(""); |     const [phoneNumber, setPhoneNumber] = useState(""); | ||||||
|  |     const [channel, setChannel] = useState("sms"); | ||||||
|     const [code, setCode] = useState(""); |     const [code, setCode] = useState(""); | ||||||
|     const [sending, setSending] = useState(false); |     const [sending, setSending] = useState(false); | ||||||
|     const [verificationCodeSent, setVerificationCodeSent] = useState(false); |     const [verificationCodeSent, setVerificationCodeSent] = useState(false); | ||||||
|  | @ -432,7 +435,7 @@ const AddPhoneNumberDialog = (props) => { | ||||||
|     const verifyPhone = async () => { |     const verifyPhone = async () => { | ||||||
|         try { |         try { | ||||||
|             setSending(true); |             setSending(true); | ||||||
|             await accountApi.verifyPhoneNumber(phoneNumber); |             await accountApi.verifyPhoneNumber(phoneNumber, channel); | ||||||
|             setVerificationCodeSent(true); |             setVerificationCodeSent(true); | ||||||
|         } catch (e) { |         } catch (e) { | ||||||
|             console.log(`[Account] Error sending verification`, e); |             console.log(`[Account] Error sending verification`, e); | ||||||
|  | @ -471,6 +474,7 @@ const AddPhoneNumberDialog = (props) => { | ||||||
|                     {t("account_basics_phone_numbers_dialog_description")} |                     {t("account_basics_phone_numbers_dialog_description")} | ||||||
|                 </DialogContentText> |                 </DialogContentText> | ||||||
|                 {!verificationCodeSent && |                 {!verificationCodeSent && | ||||||
|  |                     <div style={{display: "flex"}}> | ||||||
|                         <TextField |                         <TextField | ||||||
|                             margin="dense" |                             margin="dense" | ||||||
|                             label={t("account_basics_phone_numbers_dialog_number_label")} |                             label={t("account_basics_phone_numbers_dialog_number_label")} | ||||||
|  | @ -479,10 +483,17 @@ const AddPhoneNumberDialog = (props) => { | ||||||
|                             type="tel" |                             type="tel" | ||||||
|                             value={phoneNumber} |                             value={phoneNumber} | ||||||
|                             onChange={ev => setPhoneNumber(ev.target.value)} |                             onChange={ev => setPhoneNumber(ev.target.value)} | ||||||
|                         fullWidth |  | ||||||
|                             inputProps={{ inputMode: 'tel', pattern: '\+[0-9]*' }} |                             inputProps={{ inputMode: 'tel', pattern: '\+[0-9]*' }} | ||||||
|                             variant="standard" |                             variant="standard" | ||||||
|  |                             sx={{ flexGrow: 1 }} | ||||||
|                         /> |                         /> | ||||||
|  |                         <FormControl sx={{ flexWrap: "nowrap" }}> | ||||||
|  |                             <RadioGroup row sx={{ flexGrow: 1, marginTop: "8px", marginLeft: "5px" }}> | ||||||
|  |                                 <FormControlLabel value="sms" control={<Radio checked={channel === "sms"} onChange={(e) => setChannel(e.target.value)} />} label={t("account_basics_phone_numbers_dialog_channel_sms")} /> | ||||||
|  |                                 <FormControlLabel value="call" control={<Radio checked={channel === "call"} onChange={(e) => setChannel(e.target.value)} />} label={t("account_basics_phone_numbers_dialog_channel_call")} sx={{ marginRight: 0 }} /> | ||||||
|  |                             </RadioGroup> | ||||||
|  |                         </FormControl> | ||||||
|  |                     </div> | ||||||
|                 } |                 } | ||||||
|                 {verificationCodeSent && |                 {verificationCodeSent && | ||||||
|                     <TextField |                     <TextField | ||||||
|  | @ -502,7 +513,9 @@ const AddPhoneNumberDialog = (props) => { | ||||||
|             <DialogFooter status={error}> |             <DialogFooter status={error}> | ||||||
|                 <Button onClick={handleCancel}>{verificationCodeSent ? t("common_back") : t("common_cancel")}</Button> |                 <Button onClick={handleCancel}>{verificationCodeSent ? t("common_back") : t("common_cancel")}</Button> | ||||||
|                 <Button onClick={handleDialogSubmit} disabled={sending || !/^\+\d+$/.test(phoneNumber)}> |                 <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")} |                     {!verificationCodeSent && channel === "sms" && t("account_basics_phone_numbers_dialog_verify_button_sms")} | ||||||
|  |                     {!verificationCodeSent && channel === "call" && t("account_basics_phone_numbers_dialog_verify_button_call")} | ||||||
|  |                     {verificationCodeSent && t("account_basics_phone_numbers_dialog_check_verification_button")} | ||||||
|                 </Button> |                 </Button> | ||||||
|             </DialogFooter> |             </DialogFooter> | ||||||
|         </Dialog> |         </Dialog> | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue