Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
645de0b
feat: add mfa events to oauth flows
sherzod-bakhodirov Feb 16, 2026
81e26e4
feat: implement mfa events in wallet kit
sherzod-bakhodirov Feb 18, 2026
b74e465
Merge branch 'master' into sherzodbakhodirov/emb-211-implement-headle…
sherzod-bakhodirov Feb 18, 2026
666591f
chore: update yarn.lock
sherzod-bakhodirov Feb 18, 2026
739cd2d
fix: properly display OAuth errors in pending view and clean up handles
sherzod-bakhodirov Feb 19, 2026
7682de9
Update packages/@magic-ext/wallet-kit/src/context/OAuthLoginContext.tsx
sherzod-bakhodirov Feb 19, 2026
dc3258f
Update packages/@magic-ext/oauth2/src/index.ts
sherzod-bakhodirov Feb 19, 2026
033da7b
Update packages/@magic-ext/oauth2/src/index.ts
sherzod-bakhodirov Feb 19, 2026
0304431
Update packages/@magic-ext/wallet-kit/src/context/OAuthLoginContext.tsx
sherzod-bakhodirov Feb 19, 2026
0712608
refactor: rename email login types to generic OTP for SMS support
sherzod-bakhodirov Feb 19, 2026
7f8bc69
feat: add MFA and recovery code events for SMS login
sherzod-bakhodirov Feb 19, 2026
4f42136
feat: implement SMS login support in wallet kit
sherzod-bakhodirov Feb 19, 2026
a7130f5
refactor: update components and views to use generic OTP naming
sherzod-bakhodirov Feb 19, 2026
667201c
feat: add SMS support to useMfa hook and exports
sherzod-bakhodirov Feb 19, 2026
0b89ae0
chore: install libphonenumber-js
sherzod-bakhodirov Feb 19, 2026
57ab1b5
refactor: minor improvements
sherzod-bakhodirov Feb 20, 2026
d6d2902
chore: add tests for whitelable sms flow
sherzod-bakhodirov Feb 20, 2026
d68df5d
feat: implement webauthn support
sherzod-bakhodirov Feb 23, 2026
4084ee3
chore: minor improvements in wallet kit sms flow
sherzod-bakhodirov Feb 23, 2026
591c418
Merge branch 'master' into sherzodbakhodirov/emb-253-implement-sms-we…
sherzod-bakhodirov Feb 23, 2026
917627e
chore: remove webauthn from wallet kit
sherzod-bakhodirov Feb 25, 2026
8a47afe
feat: update design for sms flow in wallet kit
sherzod-bakhodirov Mar 4, 2026
760fb2e
Merge branch 'master' into sherzodbakhodirov/emb-253-implement-sms-we…
sherzod-bakhodirov Mar 4, 2026
3e8aaa2
feat: reuse components from @magiclabs/ui-components for SMS login
sherzod-bakhodirov Mar 5, 2026
d7c9b00
chore: bump ui components version
sherzod-bakhodirov Mar 6, 2026
59588d6
chore: remove react-international-phone
sherzod-bakhodirov Mar 9, 2026
a8c17a8
chore: remove unused sms input file
sherzod-bakhodirov Mar 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/@magic-ext/wallet-kit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,12 @@
"@styled/patterns": "./styled-system/patterns"
},
"dependencies": {
"@magiclabs/ui-components": "^1.49.3",
"@magiclabs/ui-components": "^1.50.0",
"@reown/appkit": "^1.8.0",
"@reown/appkit-adapter-wagmi": "^1.8.0",
"@wagmi/core": "^2.0.0",
"@walletconnect/ethereum-provider": "^2.23.0",
"libphonenumber-js": "^1.12.37",
"wagmi": "^2.0.0"
},
"peerDependencies": {
Expand Down
36 changes: 21 additions & 15 deletions packages/@magic-ext/wallet-kit/src/MagicWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@ import AdditionalProvidersView from './views/AdditionalProvidersView';
import { getExtensionInstance } from './extension';
import { EmailLoginProvider } from './context/EmailLoginContext';
import { OAuthLoginProvider } from './context/OAuthLoginContext';
import { SmsLoginProvider } from './context/SmsLoginContext';
import { WidgetConfigProvider } from './context/WidgetConfigContext';
import { EmailOTPView } from './views/EmailOTPView';
import { OtpView } from './views/OtpView';
import { DeviceVerificationView } from './views/DeviceVerificationView';
import { LoginSuccessView } from './views/LoginSuccessView';
import { MFAView } from './views/MfaView';
import { RecoveryCodeView } from './views/RecoveryCode';
import { LostRecoveryCode } from './views/LostRecoveryCode';
import { WalletConnectView } from './views/WalletConnectView';
import { SmsLoginView } from './views/SmsLoginView';
import { FarcasterPendingView } from './views/FarcasterPendingView';
import { FarcasterSuccessView } from './views/FarcasterSuccessView';
import { FarcasterFailedView } from './views/FarcasterFailedView';
Expand Down Expand Up @@ -57,10 +59,12 @@ function WidgetContent({
const renderView = () => {
switch (state.view) {
case 'login':
return <LoginView dispatch={dispatch} />;
return <LoginView dispatch={dispatch} state={state} />;
case 'sms_login':
return <SmsLoginView state={state} />;
case 'wallet_pending':
if (!state.selectedProvider) {
return <LoginView dispatch={dispatch} />;
return <LoginView dispatch={dispatch} state={state} />;
}
return (
<WalletPendingView
Expand All @@ -74,7 +78,7 @@ function WidgetContent({
return <WalletConnectView key="walletconnect" dispatch={dispatch} />;
case 'oauth_pending':
if (!state.selectedProvider) {
return <LoginView dispatch={dispatch} />;
return <LoginView dispatch={dispatch} state={state} />;
}
return (
<OAuthPendingView
Expand All @@ -86,8 +90,8 @@ function WidgetContent({
);
case 'additional_providers':
return <AdditionalProvidersView dispatch={dispatch} />;
case 'email_otp_pending':
return <EmailOTPView state={state} dispatch={dispatch} />;
case 'otp_pending':
return <OtpView state={state} dispatch={dispatch} />;
case 'device_verification':
return <DeviceVerificationView state={state} dispatch={dispatch} />;
case 'mfa_pending':
Expand All @@ -105,20 +109,22 @@ function WidgetContent({
case 'farcaster_failed':
return <FarcasterFailedView state={state} dispatch={dispatch} />;
default:
return <LoginView dispatch={dispatch} />;
return <LoginView dispatch={dispatch} state={state} />;
}
};

return (
<EmailLoginProvider dispatch={dispatch}>
<OAuthLoginProvider dispatch={dispatch}>
<Modal isWidget fullscreen={isModal && isMobile}>
<VStack width="full" minWidth="380px">
{renderView()}
<Footer showLogo={showFooterLogo} />
</VStack>
</Modal>
</OAuthLoginProvider>
<SmsLoginProvider dispatch={dispatch}>
<OAuthLoginProvider dispatch={dispatch}>
<Modal isWidget fullscreen={isModal && isMobile}>
<VStack width="full" minWidth="380px">
{renderView()}
<Footer showLogo={showFooterLogo} />
</VStack>
</Modal>
</OAuthLoginProvider>
</SmsLoginProvider>
</EmailLoginProvider>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { ButtonContainer, IcoMessage, IcoPhone, Text } from '@magiclabs/ui-components';
import { css } from '@styled/css';
import { Box, Flex } from '@styled/jsx';
import { token } from '@styled/tokens';
import React from 'react';
import { getExtensionInstance } from '../extension';

export const ContinueWithSmsButton = ({ onClick }: { onClick: () => void }) => {
const config = getExtensionInstance().getConfig();
const isDarkMode = config?.theme.themeColor === 'dark';

return (
<Box flex={1}>
<ButtonContainer
onPress={onClick}
borderRadius={14}
className={css({
w: 'full',
px: 4,
py: 3,
bg: 'neutral.quaternary',
_hover: { bg: 'neutral.tertiary' },
})}
>
<Flex gap={3} w="full" justifyContent={'flex-start'} alignItems="center">
<IcoPhone width={24} height={24} {...(isDarkMode ? { color: token('colors.ink.70') } : {})} />
<Text styles={{ lineHeight: '1.5rem' }}>Continue with SMS</Text>
</Flex>
</ButtonContainer>
</Box>
);
};
26 changes: 16 additions & 10 deletions packages/@magic-ext/wallet-kit/src/components/EmailInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,33 +8,39 @@ import { Box } from '@styled/jsx';
import { token } from '@styled/tokens';
import { getExtensionInstance } from 'src/extension';

export const EmailInput = () => {
interface EmailInputProps {
error?: string;
isLoading?: boolean;
}

export const EmailInput = ({ error: externalError, isLoading }: EmailInputProps) => {
const { startEmailLogin } = useEmailLogin();
const [email, setEmail] = useState('');
const [isValidating, setIsValidating] = useState(false);
const [error, setError] = useState<string | null>(null);
const [localError, setLocalError] = useState<string | null>(null);
const [disabled, setDisabled] = useState(true);
const config = getExtensionInstance().getConfig();
const isDarkMode = config?.theme.themeColor === 'dark';

// Use external error if available, otherwise fall back to local error
const displayError = externalError || localError;

const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();

if (!isValidEmail(email)) {
setError(RpcErrorMessage.MalformedEmail);
setLocalError(RpcErrorMessage.MalformedEmail);
return;
} else if (isSanctionedEmail(email)) {
setError(RpcErrorMessage.SanEmail);
setLocalError(RpcErrorMessage.SanEmail);
return;
}

setDisabled(true);
setIsValidating(true);
startEmailLogin(email);
};

const handleInput = (e: string) => {
setError(null);
setLocalError(null);
setDisabled(!e.length);
setEmail(e.trim());
};
Expand All @@ -52,7 +58,7 @@ export const EmailInput = () => {
variant="text"
textStyle="neutral"
disabled={disabled}
validating={isValidating}
validating={isLoading}
type="submit"
>
<Button.LeadingIcon color={token('colors.text.tertiary')}>
Expand All @@ -62,9 +68,9 @@ export const EmailInput = () => {
</TextInput.ActionIcon>
</TextInput>
</Box>
{error && (
{displayError && (
<Text variant="error" size="sm" styles={{ textAlign: 'center' }}>
{error}
{displayError}
</Text>
)}
</form>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ interface ProviderButtonProps {
Icon: ElementType;
onPress: () => void;
hideLabel?: boolean;
center?: boolean;
}

export const ProviderButton = ({ label, Icon, onPress, hideLabel }: ProviderButtonProps) => {
export const ProviderButton = ({ label, Icon, onPress, hideLabel, center }: ProviderButtonProps) => {
return (
<ButtonContainer
onPress={onPress}
Expand All @@ -23,7 +24,7 @@ export const ProviderButton = ({ label, Icon, onPress, hideLabel }: ProviderButt
_hover: { bg: 'neutral.tertiary' },
})}
>
<Flex gap={3} w="full" justifyContent={hideLabel ? 'center' : 'flex-start'} alignItems="center">
<Flex gap={3} w="full" justifyContent={hideLabel || center ? 'center' : 'flex-start'} alignItems="center">
<Icon width={24} height={24} />
{!hideLabel && label && (
<Text fontWeight="medium" styles={{ lineHeight: '1.5rem' }}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export function EmailLoginProvider({ children, dispatch }: EmailLoginProviderPro
const startEmailLogin = useCallback(
(email: string) => {
emailRef.current = email;
dispatch({ type: 'EMAIL_OTP_START', email });
dispatch({ type: 'OTP_START', identifier: email, loginMethod: 'email' });

try {
const extension = getExtensionInstance();
Expand All @@ -56,17 +56,17 @@ export function EmailLoginProvider({ children, dispatch }: EmailLoginProviderPro

// OTP was sent successfully
handle.on(LoginWithEmailOTPEventOnReceived.EmailOTPSent, () => {
dispatch({ type: 'EMAIL_OTP_SENT' });
dispatch({ type: 'OTP_SENT' });
});

// Invalid OTP entered
handle.on(LoginWithEmailOTPEventOnReceived.InvalidEmailOtp, () => {
dispatch({ type: 'EMAIL_OTP_INVALID' });
dispatch({ type: 'OTP_INVALID' });
});

// OTP has expired
handle.on(LoginWithEmailOTPEventOnReceived.ExpiredEmailOtp, () => {
dispatch({ type: 'EMAIL_OTP_EXPIRED' });
dispatch({ type: 'OTP_EXPIRED' });
});

// Login throttled (too many attempts)
Expand All @@ -76,7 +76,7 @@ export function EmailLoginProvider({ children, dispatch }: EmailLoginProviderPro

// Max attempts reached
handle.on(LoginWithEmailOTPEventOnReceived.MaxAttemptsReached, () => {
dispatch({ type: 'EMAIL_OTP_MAX_ATTEMPTS_REACHED' });
dispatch({ type: 'OTP_MAX_ATTEMPTS_REACHED' });
});

// ==========================================
Expand Down Expand Up @@ -157,7 +157,7 @@ export function EmailLoginProvider({ children, dispatch }: EmailLoginProviderPro
const submitOTP = useCallback(
(otp: string) => {
if (handleRef.current) {
dispatch({ type: 'EMAIL_OTP_VERIFYING' });
dispatch({ type: 'OTP_VERIFYING' });
handleRef.current.emit(LoginWithEmailOTPEventEmit.VerifyEmailOtp, otp);
}
},
Expand Down
Loading