mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-30 14:58:08 +00:00
/login and /signup draw "Continue with wallet" and "Continue with phone" after Google and GitHub, each only where IAM's methods say it is on. Wallet: where IAM signs a wallet in (@hanzo/iam 0.21.37 `wallet`) and the browser injects an EIP-1193 wallet; the press is loginWithWallet (IAM's challenge, personal_sign of its EIP-4361 text, IAM's verify) and ends as a password's sign-in ends. A wallet IAM has not seen is made an account in an org of its own, with no email. Phone: where IAM says it sends codes (`code`) and has answered. The number is read in its country with the SMS consent ticked, then IAM's code flow. IAM's address lookup answers an email address only, so a number is texted as any is and the account that holds it is signed in by the code. Settings -> General -> Sign in another way adds an email or a phone to the signed-in account through IAM's own proof (addAddress, PUT /v1/iam/address), drawn only where IAM's methods say `address`. Same account, org, keys and balance; nothing is created, so nothing earns twice. Gates: wallet.spec (a throwaway secp256k1 key behind an injected provider), phone.spec, address.spec. login.spec expects the phone after the providers.
58 lines
2.6 KiB
TypeScript
58 lines
2.6 KiB
TypeScript
/**
|
|
* A phone number, as the sign-in panel asks for one. The sign-in itself is IAM's
|
|
* code flow with a number for the address (@hanzo/iam `sendLoginCode`, then
|
|
* `loginWithCode`): IAM texts the code through notify and spends it at login.
|
|
* The number IAM is sent is @hanzo/iam/validation `e164`; this file holds what
|
|
* the page says and counts around it, and is pure, so it is tested without a
|
|
* browser.
|
|
*/
|
|
|
|
import { getCountries, getCountryCallingCode, parsePhoneNumberFromString } from 'libphonenumber-js/min'
|
|
|
|
/** IAM's wait between two codes to one number, in seconds (internal/otp `ResendInterval`). */
|
|
export const RESEND = 30
|
|
|
|
/** Wrong codes IAM takes against one it sent before that code is spent (internal/otp `MaxAttempts`). */
|
|
export const TRIES = 5
|
|
|
|
/**
|
|
* The SMS consent a person agrees to before a number is texted: hanzo/id's
|
|
* `smsConsentText` for Hanzo AI, word for word, because carrier campaign review
|
|
* (A2P 10DLC) compares the disclosure across every surface that collects a number.
|
|
*/
|
|
export const CONSENT =
|
|
'I agree to receive text messages (SMS) from Hanzo AI at the number provided, ' +
|
|
'including one-time passcodes and two-factor authentication, account and security ' +
|
|
'alerts, and transactional notifications. Message frequency varies. Message and data ' +
|
|
'rates may apply. Reply STOP to opt out at any time, or HELP for help. Consent is not ' +
|
|
'a condition of any purchase.'
|
|
|
|
/** One country a number may be in: its ISO code, its name in the reader's language, its calling code. */
|
|
export type Country = { value: string; label: string; callingCode: string }
|
|
|
|
/**
|
|
* Every country libphonenumber numbers, named in `locale` (`Intl.DisplayNames`,
|
|
* the ISO code where the runtime has no name) and in that locale's order.
|
|
*/
|
|
export function countries(locale: string): Country[] {
|
|
let names: Intl.DisplayNames | null = null
|
|
try {
|
|
names = new Intl.DisplayNames([locale], { type: 'region' })
|
|
} catch {
|
|
/* no names here: the ISO codes stand */
|
|
}
|
|
return getCountries()
|
|
.map((value) => ({ value, label: names?.of(value) ?? value, callingCode: getCountryCallingCode(value) }))
|
|
.sort((a, b) => a.label.localeCompare(b.label, locale))
|
|
}
|
|
|
|
/** An E.164 number as a person reads it: "+1 415 555 0134". */
|
|
export function shown(number: string): string {
|
|
return parsePhoneNumberFromString(number)?.formatInternational() ?? number
|
|
}
|
|
|
|
/** Seconds until another code may be asked for, for one sent at `at` (ms), at `now` (ms). */
|
|
export function wait(at: number, now: number): number {
|
|
return Math.max(0, RESEND - Math.floor((now - at) / 1000))
|
|
}
|