diff --git a/libs/shared/src/types/protocol.ts b/libs/shared/src/types/protocol.ts index b3c6101a587..84239a5011a 100644 --- a/libs/shared/src/types/protocol.ts +++ b/libs/shared/src/types/protocol.ts @@ -84,6 +84,7 @@ export enum WalletSsoSource { Apple = 'apple', Email = 'email', Farcaster = 'farcaster', + SMS = 'SMS', Unknown = 'unknown', // address created after we launched SSO, before we started recording WalletSsoSource } diff --git a/packages/commonwealth/client/scripts/controllers/app/login.ts b/packages/commonwealth/client/scripts/controllers/app/login.ts index 9ff49e882ab..a729fb63ad8 100644 --- a/packages/commonwealth/client/scripts/controllers/app/login.ts +++ b/packages/commonwealth/client/scripts/controllers/app/login.ts @@ -318,17 +318,19 @@ async function constructMagic(isCosmos: boolean, chain?: string) { export async function startLoginWithMagicLink({ email, + phoneNumber, provider, chain, isCosmos, }: { email?: string; + phoneNumber?: string; provider?: WalletSsoSource; chain?: string; isCosmos: boolean; }) { - if (!email && !provider) - throw new Error('Must provide email or SSO provider'); + if (!email && !phoneNumber && !provider) + throw new Error('Must provide email or SMS or SSO provider'); const magic = await constructMagic(isCosmos, chain); if (email) { @@ -348,6 +350,18 @@ export async function startLoginWithMagicLink({ walletSsoSource: WalletSsoSource.Farcaster, }); + return { bearer, address }; + } else if (phoneNumber) { + const bearer = await magic.auth.loginWithSMS({ + phoneNumber, + showUI: true, + }); + + const { address } = await handleSocialLoginCallback({ + bearer, + walletSsoSource: WalletSsoSource.SMS, + }); + return { bearer, address }; } else { localStorage.setItem('magic_provider', provider!); @@ -416,12 +430,15 @@ export async function handleSocialLoginCallback({ } const isCosmos = desiredChain?.base === ChainBase.CosmosSDK; const magic = await constructMagic(isCosmos, desiredChain?.id); - const isEmail = walletSsoSource === WalletSsoSource.Email; // Code up to this line might run multiple times because of extra calls to useEffect(). // Those runs will be rejected because getRedirectResult purges the browser search param. let profileMetadata, magicAddress; - if (isEmail || walletSsoSource === WalletSsoSource.Farcaster) { + if ( + walletSsoSource === WalletSsoSource.Email || + walletSsoSource === WalletSsoSource.Farcaster || + walletSsoSource === WalletSsoSource.SMS + ) { const metadata = await magic.user.getMetadata(); profileMetadata = { username: null }; diff --git a/packages/commonwealth/client/scripts/views/components/AuthButton/constants.ts b/packages/commonwealth/client/scripts/views/components/AuthButton/constants.ts index 74e9a52acea..4c0f536e9e2 100644 --- a/packages/commonwealth/client/scripts/views/components/AuthButton/constants.ts +++ b/packages/commonwealth/client/scripts/views/components/AuthButton/constants.ts @@ -152,6 +152,13 @@ export const AUTH_TYPES: AuthTypesList = { }, label: 'Email', }, + SMS: { + icon: { + name: 'SMS', + isCustom: true, + }, + label: 'SMS', + }, farcaster: { icon: { name: 'farcaster', diff --git a/packages/commonwealth/client/scripts/views/components/AuthButton/types.ts b/packages/commonwealth/client/scripts/views/components/AuthButton/types.ts index 14d36dea4c8..0991239034d 100644 --- a/packages/commonwealth/client/scripts/views/components/AuthButton/types.ts +++ b/packages/commonwealth/client/scripts/views/components/AuthButton/types.ts @@ -10,7 +10,8 @@ export type AuthSSOs = | 'github' | 'apple' | 'email' - | 'farcaster'; + | 'farcaster' + | 'SMS'; export type CosmosWallets = 'keplr' | 'leap'; export type SubstrateWallets = 'polkadot'; export type SolanaWallets = 'phantom'; diff --git a/packages/commonwealth/client/scripts/views/components/component_kit/cw_icons/cw_icon_lookup.ts b/packages/commonwealth/client/scripts/views/components/component_kit/cw_icons/cw_icon_lookup.ts index 4bfbfae85f2..03b1e819512 100644 --- a/packages/commonwealth/client/scripts/views/components/component_kit/cw_icons/cw_icon_lookup.ts +++ b/packages/commonwealth/client/scripts/views/components/component_kit/cw_icons/cw_icon_lookup.ts @@ -21,6 +21,7 @@ import { CaretUp, ChatCenteredDots, ChatDots, + ChatText, Chats, Check, CheckCircle, @@ -325,6 +326,7 @@ export const customIconLookup = { x: CustomIcons.CWX, // twitter apple: CustomIcons.CWApple, farcaster: CustomIcons.CWFarcaster, + SMS: withPhosphorIcon(ChatText), }; export type IconName = keyof typeof iconLookup; diff --git a/packages/commonwealth/client/scripts/views/modals/AuthModal/common/ModalBase/ModalBase.tsx b/packages/commonwealth/client/scripts/views/modals/AuthModal/common/ModalBase/ModalBase.tsx index 79ca6844c11..1930f1d0f54 100644 --- a/packages/commonwealth/client/scripts/views/modals/AuthModal/common/ModalBase/ModalBase.tsx +++ b/packages/commonwealth/client/scripts/views/modals/AuthModal/common/ModalBase/ModalBase.tsx @@ -27,6 +27,7 @@ import { EVMWalletsSubModal } from './EVMWalletsSubModal'; import { EmailForm } from './EmailForm'; import { MobileWalletConfirmationSubModal } from './MobileWalletConfirmationSubModal'; import './ModalBase.scss'; +import { SMSForm } from './SMSForm'; const MODAL_COPY = { [AuthModalType.CreateAccount]: { @@ -58,6 +59,7 @@ const SSO_OPTIONS: AuthSSOs[] = [ 'github', 'email', 'farcaster', + 'SMS', ] as const; /** @@ -98,9 +100,11 @@ const ModalBase = ({ useState(false); const [isAuthenticatingWithEmail, setIsAuthenticatingWithEmail] = useState(false); + const [isAuthenticatingWithSMS, setIsAuthenticatingWithSMS] = useState(false); const handleClose = async () => { setIsAuthenticatingWithEmail(false); + setIsAuthenticatingWithSMS(false); setIsEVMWalletsModalVisible(false); isWalletConnectEnabled && (await onResetWalletConnect().catch(console.error)); @@ -119,6 +123,7 @@ const ModalBase = ({ isMobileWalletVerificationStep, onResetWalletConnect, onEmailLogin, + onSMSLogin, onWalletSelect, onSocialLogin, onVerifyMobileWalletSignature, @@ -239,6 +244,10 @@ const ModalBase = ({ setIsAuthenticatingWithEmail(true); return; } + if (option === 'SMS') { + setIsAuthenticatingWithSMS(true); + return; + } // if any wallet option is selected if (activeTabIndex === 0) { @@ -331,11 +340,13 @@ const ModalBase = ({ )} {/* - If email option is selected don't render SSO's list, + If email or SMS option is selected don't render SSO's list, else render wallets/SSO's list based on activeTabIndex */} {(activeTabIndex === 0 || - (activeTabIndex === 1 && !isAuthenticatingWithEmail)) && + (activeTabIndex === 1 && + !isAuthenticatingWithEmail && + !isAuthenticatingWithSMS)) && tabsList[activeTabIndex].options.map(renderAuthButton)} {/* If email option is selected from the SSO's list, show email form */} @@ -347,6 +358,15 @@ const ModalBase = ({ onSubmit={async ({ email }) => await onEmailLogin(email)} /> )} + {/* If SMS option is selected from the SSO's list, show SMS form */} + {activeTabIndex === 1 && isAuthenticatingWithSMS && ( + setIsAuthenticatingWithSMS(false)} + // eslint-disable-next-line @typescript-eslint/no-misused-promises + onSubmit={async ({ SMS }) => await onSMSLogin(SMS)} + /> + )} )} diff --git a/packages/commonwealth/client/scripts/views/modals/AuthModal/common/ModalBase/SMSForm/SMSForm.scss b/packages/commonwealth/client/scripts/views/modals/AuthModal/common/ModalBase/SMSForm/SMSForm.scss new file mode 100644 index 00000000000..7cbdbc6bb96 --- /dev/null +++ b/packages/commonwealth/client/scripts/views/modals/AuthModal/common/ModalBase/SMSForm/SMSForm.scss @@ -0,0 +1,37 @@ +@import '../../../../../../../styles/shared'; + +.SMSForm { + display: flex; + flex-direction: column; + gap: 12px; + width: 100%; + padding: 0 4px; + + .LoadingSpinner { + margin: auto; + } + + .action-btns { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + + .btn-border { + margin: 0 !important; + + @include extraSmall { + width: 100% !important; + + button { + width: 100% !important; + } + } + + &:focus-within { + border-width: 1px !important; + padding: 0px !important; + } + } + } +} diff --git a/packages/commonwealth/client/scripts/views/modals/AuthModal/common/ModalBase/SMSForm/SMSForm.tsx b/packages/commonwealth/client/scripts/views/modals/AuthModal/common/ModalBase/SMSForm/SMSForm.tsx new file mode 100644 index 00000000000..7f55cd4617b --- /dev/null +++ b/packages/commonwealth/client/scripts/views/modals/AuthModal/common/ModalBase/SMSForm/SMSForm.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import CWCircleMultiplySpinner from 'views/components/component_kit/new_designs/CWCircleMultiplySpinner'; +import { CWButton } from '../../../../../components/component_kit/new_designs/CWButton'; +import { CWForm } from '../../../../../components/component_kit/new_designs/CWForm'; +import { CWTextInput } from '../../../../../components/component_kit/new_designs/CWTextInput'; +import './SMSForm.scss'; +import { SMSValidationSchema } from './validation'; + +type SMSFormProps = { + onCancel: () => void; + onSubmit: (values: { SMS: string }) => void; + isLoading?: boolean; +}; + +const SMSForm = ({ onSubmit, onCancel, isLoading }: SMSFormProps) => { + return ( + {}} + > + {isLoading ? ( + + ) : ( + <> + +
+ + +
+ + )} +
+ ); +}; + +export { SMSForm }; diff --git a/packages/commonwealth/client/scripts/views/modals/AuthModal/common/ModalBase/SMSForm/index.ts b/packages/commonwealth/client/scripts/views/modals/AuthModal/common/ModalBase/SMSForm/index.ts new file mode 100644 index 00000000000..6470f8c3ded --- /dev/null +++ b/packages/commonwealth/client/scripts/views/modals/AuthModal/common/ModalBase/SMSForm/index.ts @@ -0,0 +1 @@ +export { SMSForm } from './SMSForm'; diff --git a/packages/commonwealth/client/scripts/views/modals/AuthModal/common/ModalBase/SMSForm/validation.ts b/packages/commonwealth/client/scripts/views/modals/AuthModal/common/ModalBase/SMSForm/validation.ts new file mode 100644 index 00000000000..0e03ddea43f --- /dev/null +++ b/packages/commonwealth/client/scripts/views/modals/AuthModal/common/ModalBase/SMSForm/validation.ts @@ -0,0 +1,7 @@ +import { z } from 'zod'; + +const SMSValidationSchema = z.object({ + SMS: z.string(), +}); + +export { SMSValidationSchema }; diff --git a/packages/commonwealth/client/scripts/views/modals/AuthModal/useAuthentication.tsx b/packages/commonwealth/client/scripts/views/modals/AuthModal/useAuthentication.tsx index 75d2668d1c3..67ee40405c6 100644 --- a/packages/commonwealth/client/scripts/views/modals/AuthModal/useAuthentication.tsx +++ b/packages/commonwealth/client/scripts/views/modals/AuthModal/useAuthentication.tsx @@ -64,6 +64,7 @@ type Wallet = IWebWallet; const useAuthentication = (props: UseAuthenticationProps) => { const [username, setUsername] = useState(DEFAULT_NAME); const [email, setEmail] = useState(); + const [SMS, setSMS] = useState(); const [wallets, setWallets] = useState>(); const [selectedWallet, setSelectedWallet] = useState(); const [primaryAccount, setPrimaryAccount] = useState(); @@ -148,6 +149,38 @@ const useAuthentication = (props: UseAuthenticationProps) => { }; // Handles Magic Link Login + const onSMSLogin = async (phoneNumber = '') => { + const tempSMSToUse = phoneNumber || SMS; + setSMS(tempSMSToUse); + + setIsMagicLoading(true); + + if (!phoneNumber) { + notifyError('Please enter a valid phone number.'); + setIsMagicLoading(false); + return; + } + + try { + const isCosmos = app.chain?.base === ChainBase.CosmosSDK; + const { address: magicAddress } = await startLoginWithMagicLink({ + phoneNumber: tempSMSToUse, + isCosmos, + chain: app.chain?.id, + }); + setIsMagicLoading(false); + + await handleSuccess(magicAddress, isNewlyCreated); + props?.onModalClose?.(); + + trackLoginEvent('SMS', true); + } catch (e) { + notifyError(`Error authenticating with SMS`); + console.error(`Error authenticating with SMS: ${e}`); + setIsMagicLoading(false); + } + }; + const onEmailLogin = async (emailToUse = '') => { const tempEmailToUse = emailToUse || email; setEmail(tempEmailToUse); @@ -560,8 +593,10 @@ const useAuthentication = (props: UseAuthenticationProps) => { onWalletSelect, onResetWalletConnect, onEmailLogin, + onSMSLogin, onSocialLogin, setEmail, + setSMS, onVerifyMobileWalletSignature, }; }; diff --git a/packages/commonwealth/client/scripts/views/pages/ComponentsShowcase/components/AuthButtons.showcase.tsx b/packages/commonwealth/client/scripts/views/pages/ComponentsShowcase/components/AuthButtons.showcase.tsx index 4f43c733b04..b2098a69977 100644 --- a/packages/commonwealth/client/scripts/views/pages/ComponentsShowcase/components/AuthButtons.showcase.tsx +++ b/packages/commonwealth/client/scripts/views/pages/ComponentsShowcase/components/AuthButtons.showcase.tsx @@ -16,6 +16,7 @@ const AuthButtonsShowcase = () => { + Disabled