Files
zitadel/internal/domain/human_phone.go
T
597f385ca4 feat: Add SMS country code selector to SMT OTP (LoginV1) (#11373)
<!--
Please inform yourself about the contribution guidelines on submitting a
PR here:
https://github.com/zitadel/zitadel/blob/main/CONTRIBUTING.md#submit-a-pull-request-pr.
Take note of how PR/commit titles should be written and replace the
template texts in the sections below. Don't remove any of the sections.
It is important that the commit history clearly shows what is changed
and why.
Important: By submitting a contribution you agree to the terms from our
Licensing Policy as described here:
https://github.com/zitadel/zitadel/blob/main/LICENSING.md#community-contributions.
-->

# Which Problems Are Solved

- This adds rudimentary country code handling for LoginV1 SMS OTP
- Previously defaulting to CH with no prompt to user causing e.g UK
(+44) based numbers to be re-routed to CH. (+41)

# Additional Context

- Support for SMS OTP is not complete for LoginV2 (according to latest
support email)

Tested with Twilio on both UK + DE # . Appears to work. 

- Closes #8891 

<img width="544" height="485" alt="image"
src="https://github.com/user-attachments/assets/84d31bd8-8863-465f-8860-b51602439dfb"
/>

<img width="858" height="1298" alt="Screenshot 2026-01-16 at 11 23 22"
src="https://github.com/user-attachments/assets/4bf16f0b-2fec-4d63-95b0-f0b73f3a131a"
/>

---------

Co-authored-by: Livio Spring <[email protected]>
Co-authored-by: Silvan <[email protected]>
2026-01-22 06:47:54 +00:00

126 lines
2.8 KiB
Go

package domain
import (
"fmt"
"sort"
"time"
"github.com/ttacon/libphonenumber"
"golang.org/x/text/language"
"golang.org/x/text/language/display"
"github.com/zitadel/zitadel/internal/crypto"
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/zerrors"
)
const defaultRegion = "CH"
type PhoneNumber string
func (p PhoneNumber) Normalize() (PhoneNumber, error) {
return p.NormalizeWithRegion(defaultRegion)
}
func (p PhoneNumber) NormalizeWithRegion(region string) (PhoneNumber, error) {
if p == "" {
return p, zerrors.ThrowInvalidArgument(nil, "PHONE-Zt0NV", "Errors.User.Phone.Empty")
}
if region == "" {
region = defaultRegion
}
phoneNr, err := libphonenumber.Parse(string(p), region)
if err != nil {
return p, zerrors.ThrowInvalidArgument(err, "PHONE-so0wa", "Errors.User.Phone.Invalid")
}
return PhoneNumber(libphonenumber.Format(phoneNr, libphonenumber.E164)), nil
}
type Phone struct {
es_models.ObjectRoot
PhoneNumber PhoneNumber
IsPhoneVerified bool
// PlainCode is set by the command and can be used to return it to the caller (API)
PlainCode *string
}
type PhoneCode struct {
es_models.ObjectRoot
Code *crypto.CryptoValue
Expiry time.Duration
}
func (p *Phone) Normalize() error {
if p == nil {
return zerrors.ThrowInvalidArgument(nil, "PHONE-YlbwO", "Errors.User.Phone.Empty")
}
normalizedNumber, err := p.PhoneNumber.Normalize()
if err != nil {
return err
}
// Issue for avoiding mutating state: https://github.com/zitadel/zitadel/issues/5412
p.PhoneNumber = normalizedNumber
return nil
}
type PhoneState int32
const (
PhoneStateUnspecified PhoneState = iota
PhoneStateActive
PhoneStateRemoved
phoneStateCount
)
func (s PhoneState) Valid() bool {
return s >= 0 && s < phoneStateCount
}
func (s PhoneState) Exists() bool {
return s == PhoneStateActive
}
// CountryCode represents a country with its ISO code and calling code
type CountryCode struct {
ISOCode string
CallingCode string
Name string
}
// GetCountryCodes returns a list of country codes for phone number input using libphonenumber
func GetCountryCodes(lang language.Tag) []CountryCode {
regions := libphonenumber.GetSupportedRegions()
namer := display.Regions(lang)
codes := make([]CountryCode, 0, len(regions))
for region := range regions {
countryCode := libphonenumber.GetCountryCodeForRegion(region)
if countryCode == 0 {
continue
}
// Get the display name for the region
regionTag, err := language.ParseRegion(region)
name := region
if err == nil {
name = namer.Name(regionTag)
}
codes = append(codes, CountryCode{
ISOCode: region,
CallingCode: fmt.Sprintf("+%d", countryCode),
Name: name,
})
}
// Sort by country name
sort.Slice(codes, func(i, j int) bool {
return codes[i].Name < codes[j].Name
})
return codes
}