Skip to content

Commit

Permalink
UX changes for BS / Merits login
Browse files Browse the repository at this point in the history
Fixes #2535
  • Loading branch information
tom2drum committed Jan 30, 2025
1 parent a0a3b38 commit cf37559
Show file tree
Hide file tree
Showing 14 changed files with 170 additions and 45 deletions.
5 changes: 4 additions & 1 deletion lib/contexts/rewards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ type TRewardsContext = {
isLoginModalOpen: boolean;
openLoginModal: () => void;
closeLoginModal: () => void;
saveApiToken: (token: string | undefined) => void;
login: (refCode: string) => Promise<{ isNewUser?: boolean; invalidRefCodeError?: boolean }>;
claim: () => Promise<void>;
};
Expand All @@ -68,6 +69,7 @@ const initialState = {
isLoginModalOpen: false,
openLoginModal: () => {},
closeLoginModal: () => {},
saveApiToken: () => {},
login: async() => ({}),
claim: async() => {},
};
Expand Down Expand Up @@ -265,6 +267,7 @@ export function RewardsContextProvider({ children }: Props) {
rewardsConfigQuery,
checkUserQuery,
apiToken,
saveApiToken,
isInitialized,
isLoginModalOpen,
openLoginModal: setIsLoginModalOpen.on,
Expand All @@ -274,7 +277,7 @@ export function RewardsContextProvider({ children }: Props) {
};
}, [
isLoginModalOpen, setIsLoginModalOpen, balancesQuery, dailyRewardQuery, checkUserQuery,
apiToken, login, claim, referralsQuery, rewardsConfigQuery, isInitialized,
apiToken, login, claim, referralsQuery, rewardsConfigQuery, isInitialized, saveApiToken,
]);

return (
Expand Down
4 changes: 4 additions & 0 deletions types/api/rewards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ export type RewardsConfigResponse = {
daily_claim: string;
referral_share: string;
};
auth: {
shared_siwe_login: boolean;
};
};

export type RewardsCheckRefCodeResponse = {
Expand All @@ -13,6 +16,7 @@ export type RewardsCheckRefCodeResponse = {

export type RewardsNonceResponse = {
nonce: string;
merits_login_nonce?: string;
};

export type RewardsCheckUserResponse = {
Expand Down
2 changes: 1 addition & 1 deletion ui/pages/MyProfile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const MyProfile = () => {
useRedirectForInvalidAuthToken();

const handleAddWalletClick = React.useCallback(() => {
setAuthInitialScreen({ type: 'connect_wallet', isAuth: true });
setAuthInitialScreen({ type: 'connect_wallet', isAuth: true, loginToRewards: true });
authModal.onOpen();
}, [ authModal ]);

Expand Down
26 changes: 16 additions & 10 deletions ui/rewards/login/RewardsLoginModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Modal, ModalOverlay, ModalContent, ModalHeader, ModalCloseButton, ModalBody, useBoolean, useDisclosure } from '@chakra-ui/react';
import React, { useCallback, useEffect } from 'react';

import type { Screen } from 'ui/snippets/auth/types';

import { useRewardsContext } from 'lib/contexts/rewards';
import useIsMobile from 'lib/hooks/useIsMobile';
import useWallet from 'lib/web3/useWallet';
Expand All @@ -21,11 +23,11 @@ const MIXPANEL_CONFIG = {
const RewardsLoginModal = () => {
const { isOpen: isWalletModalOpen } = useWallet({ source: 'Merits' });
const isMobile = useIsMobile();
const { isLoginModalOpen, closeLoginModal } = useRewardsContext();
const { isLoginModalOpen, closeLoginModal, saveApiToken } = useRewardsContext();

const [ isLoginStep, setIsLoginStep ] = useBoolean(true);
const [ isReferral, setIsReferral ] = useBoolean(false);
const [ isAuth, setIsAuth ] = useBoolean(false);
const [ authModalInitialScreen, setAuthModalInitialScreen ] = React.useState<Screen>();
const authModal = useDisclosure();

useEffect(() => {
Expand All @@ -42,15 +44,19 @@ const RewardsLoginModal = () => {
setIsLoginStep.off();
}, [ setIsLoginStep, setIsReferral ]);

const handleAuthModalOpen = useCallback((isAuth: boolean) => {
setIsAuth[isAuth ? 'on' : 'off']();
const handleAuthModalOpen = useCallback((isAuth: boolean, trySharedLogin?: boolean) => {
setAuthModalInitialScreen({ type: 'connect_wallet', isAuth, loginToRewards: trySharedLogin });
authModal.onOpen();
}, [ authModal, setIsAuth ]);
}, [ authModal, setAuthModalInitialScreen ]);

const handleAuthModalClose = useCallback(() => {
setIsAuth.off();
const handleAuthModalClose = useCallback((isSuccess?: boolean, rewardsApiToken?: string) => {
if (isSuccess && rewardsApiToken) {
saveApiToken(rewardsApiToken);
goNext(false);
}
setAuthModalInitialScreen(undefined);
authModal.onClose();
}, [ authModal, setIsAuth ]);
}, [ authModal, setAuthModalInitialScreen, goNext, saveApiToken ]);

return (
<>
Expand All @@ -74,10 +80,10 @@ const RewardsLoginModal = () => {
</ModalBody>
</ModalContent>
</Modal>
{ authModal.isOpen && (
{ authModal.isOpen && authModalInitialScreen && (
<AuthModal
onClose={ handleAuthModalClose }
initialScreen={{ type: 'connect_wallet', isAuth }}
initialScreen={ authModalInitialScreen }
mixpanelConfig={ MIXPANEL_CONFIG }
closeOnError
/>
Expand Down
32 changes: 23 additions & 9 deletions ui/rewards/login/steps/LoginStepContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import useProfileQuery from 'ui/snippets/auth/useProfileQuery';
type Props = {
goNext: (isReferral: boolean) => void;
closeModal: () => void;
openAuthModal: (isAuth: boolean) => void;
openAuthModal: (isAuth: boolean, trySharedLogin?: boolean) => void;
};

const LoginStepContent = ({ goNext, closeModal, openAuthModal }: Props) => {
Expand All @@ -26,7 +26,7 @@ const LoginStepContent = ({ goNext, closeModal, openAuthModal }: Props) => {
const [ isLoading, setIsLoading ] = useBoolean(false);
const [ refCode, setRefCode ] = useState(savedRefCode || '');
const [ refCodeError, setRefCodeError ] = useBoolean(false);
const { login, checkUserQuery } = useRewardsContext();
const { login, checkUserQuery, rewardsConfigQuery } = useRewardsContext();
const profileQuery = useProfileQuery();

const isAddressMismatch = useMemo(() =>
Expand All @@ -43,6 +43,8 @@ const LoginStepContent = ({ goNext, closeModal, openAuthModal }: Props) => {
isConnected && !isAddressMismatch && !checkUserQuery.isFetching && !checkUserQuery.data?.exists,
[ isConnected, isAddressMismatch, checkUserQuery ]);

const canTrySharedLogin = rewardsConfigQuery.data?.auth.shared_siwe_login && checkUserQuery.data?.exists !== false && !isLoggedIntoAccountWithWallet;

const handleRefCodeChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => {
setRefCode(event.target.value);
}, []);
Expand Down Expand Up @@ -74,23 +76,35 @@ const LoginStepContent = ({ goNext, closeModal, openAuthModal }: Props) => {
}
}, [ refCode, isRefCodeUsed, isSignUp ]); // eslint-disable-line react-hooks/exhaustive-deps

const handleLogin = useCallback(async() => {
const handleButtonClick = React.useCallback(() => {
if (canTrySharedLogin) {
return openAuthModal(Boolean(profileQuery.data?.email), true);
}

if (!isConnected) {
return connect();
}

if (isLoggedIntoAccountWithWallet) {
loginToRewardsProgram();
} else {
openAuthModal(Boolean(profileQuery.data?.email));
return loginToRewardsProgram();
}
}, [ loginToRewardsProgram, openAuthModal, isLoggedIntoAccountWithWallet, profileQuery ]);

return openAuthModal(Boolean(profileQuery.data?.email));
}, [ loginToRewardsProgram, openAuthModal, profileQuery, connect, isConnected, isLoggedIntoAccountWithWallet, canTrySharedLogin ]);

const buttonText = useMemo(() => {
if (canTrySharedLogin) {
return 'Continue with wallet';
}

if (!isConnected) {
return 'Connect wallet';
}
if (isLoggedIntoAccountWithWallet) {
return isSignUp ? 'Get started' : 'Continue';
}
return profileQuery.data?.email ? 'Add wallet to account' : 'Log in to account';
}, [ isConnected, isLoggedIntoAccountWithWallet, profileQuery.data, isSignUp ]);
}, [ canTrySharedLogin, isConnected, isLoggedIntoAccountWithWallet, profileQuery.data?.email, isSignUp ]);

return (
<>
Expand Down Expand Up @@ -148,7 +162,7 @@ const LoginStepContent = ({ goNext, closeModal, openAuthModal }: Props) => {
w="full"
whiteSpace="normal"
mb={ 4 }
onClick={ isConnected ? handleLogin : connect }
onClick={ handleButtonClick }
isLoading={ isLoading || profileQuery.isLoading || checkUserQuery.isFetching }
loadingText={ isLoading ? 'Sign message in your wallet' : undefined }
isDisabled={ isAddressMismatch || refCodeError }
Expand Down
16 changes: 13 additions & 3 deletions ui/snippets/auth/AuthModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { Screen, ScreenSuccess } from './types';

import config from 'configs/app';
import { getResourceKey } from 'lib/api/useApiQuery';
import * as cookies from 'lib/cookies';
import useGetCsrfToken from 'lib/hooks/useGetCsrfToken';
import * as mixpanel from 'lib/mixpanel';
import IconSvg from 'ui/shared/IconSvg';
Expand All @@ -22,7 +23,7 @@ const feature = config.features.account;

interface Props {
initialScreen: Screen;
onClose: (isSuccess?: boolean) => void;
onClose: (isSuccess?: boolean, rewardsApiToken?: string) => void;
mixpanelConfig?: {
wallet_connect?: {
source: mixpanel.EventPayload<mixpanel.EventTypes.WALLET_CONNECT>['Source'];
Expand All @@ -37,6 +38,7 @@ interface Props {
const AuthModal = ({ initialScreen, onClose, mixpanelConfig, closeOnError }: Props) => {
const [ steps, setSteps ] = React.useState<Array<Screen>>([ initialScreen ]);
const [ isSuccess, setIsSuccess ] = React.useState(false);
const [ rewardsApiToken, setRewardsApiToken ] = React.useState<string | undefined>(undefined);

const router = useRouter();
const csrfQuery = useGetCsrfToken();
Expand Down Expand Up @@ -87,12 +89,18 @@ const AuthModal = ({ initialScreen, onClose, mixpanelConfig, closeOnError }: Pro

queryClient.setQueryData(getResourceKey('user_info'), () => screen.profile);
await csrfQuery.refetch();

if ('rewardsToken' in screen && screen.rewardsToken) {
setRewardsApiToken(screen.rewardsToken);
cookies.set(cookies.NAMES.REWARDS_API_TOKEN, screen.rewardsToken, { expires: 365 });
}

onNextStep(screen);
}, [ initialScreen, mixpanelConfig?.account_link_info.source, onNextStep, csrfQuery, queryClient ]);

const onModalClose = React.useCallback(() => {
onClose(isSuccess);
}, [ isSuccess, onClose ]);
onClose(isSuccess, rewardsApiToken);
}, [ isSuccess, rewardsApiToken, onClose ]);

const header = (() => {
const currentStep = steps[steps.length - 1];
Expand Down Expand Up @@ -122,6 +130,7 @@ const AuthModal = ({ initialScreen, onClose, mixpanelConfig, closeOnError }: Pro
onSuccess={ onAuthSuccess }
onError={ onReset }
isAuth={ currentStep.isAuth }
loginToRewards={ currentStep.loginToRewards }
source={ mixpanelConfig?.wallet_connect?.source }
/>
);
Expand Down Expand Up @@ -153,6 +162,7 @@ const AuthModal = ({ initialScreen, onClose, mixpanelConfig, closeOnError }: Pro
onClose={ onModalClose }
isAuth={ currentStep.isAuth }
profile={ currentStep.profile }
rewardsToken={ currentStep.rewardsToken }
/>
);
}
Expand Down
8 changes: 5 additions & 3 deletions ui/snippets/auth/screens/AuthModalScreenConnectWallet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@ interface Props {
onError: (isAuth?: boolean) => void;
isAuth?: boolean;
source?: mixpanel.EventPayload<mixpanel.EventTypes.WALLET_CONNECT>['Source'];
loginToRewards?: boolean;
}

const AuthModalScreenConnectWallet = ({ onSuccess, onError, isAuth, source }: Props) => {
const AuthModalScreenConnectWallet = ({ onSuccess, onError, isAuth, source, loginToRewards }: Props) => {
const isStartedRef = React.useRef(false);
const recaptcha = useReCaptcha();

const handleSignInSuccess = React.useCallback(({ address, profile }: { address: string; profile: UserInfo }) => {
onSuccess({ type: 'success_wallet', address, isAuth, profile });
const handleSignInSuccess = React.useCallback(({ address, profile, rewardsToken }: { address: string; profile: UserInfo; rewardsToken?: string }) => {
onSuccess({ type: 'success_wallet', address, isAuth, profile, rewardsToken });
}, [ onSuccess, isAuth ]);

const handleSignInError = React.useCallback(() => {
Expand All @@ -35,6 +36,7 @@ const AuthModalScreenConnectWallet = ({ onSuccess, onError, isAuth, source }: Pr
source,
isAuth,
executeRecaptchaAsync: recaptcha.executeAsync,
loginToRewards,
});

React.useEffect(() => {
Expand Down
2 changes: 1 addition & 1 deletion ui/snippets/auth/screens/AuthModalScreenSelectMethod.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const AuthModalScreenSelectMethod = ({ onSelectMethod }: Props) => {
Action: 'Wallet',
Source: 'Options selector',
});
onSelectMethod({ type: 'connect_wallet' });
onSelectMethod({ type: 'connect_wallet', loginToRewards: true });
}, [ onSelectMethod ]);

return (
Expand Down
2 changes: 1 addition & 1 deletion ui/snippets/auth/screens/AuthModalScreenSuccessEmail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ interface Props {

const AuthModalScreenSuccessEmail = ({ email, onConnectWallet, onClose, isAuth, profile }: Props) => {
const handleConnectWalletClick = React.useCallback(() => {
onConnectWallet({ type: 'connect_wallet', isAuth: true });
onConnectWallet({ type: 'connect_wallet', isAuth: true, loginToRewards: true });
}, [ onConnectWallet ]);

if (isAuth) {
Expand Down
6 changes: 4 additions & 2 deletions ui/snippets/auth/screens/AuthModalScreenSuccessWallet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ interface Props {
onClose: () => void;
isAuth?: boolean;
profile: UserInfo | undefined;
rewardsToken?: string;
}

const AuthModalScreenSuccessWallet = ({ address, onAddEmail, onClose, isAuth, profile }: Props) => {
const AuthModalScreenSuccessWallet = ({ address, onAddEmail, onClose, isAuth, profile, rewardsToken }: Props) => {
const handleAddEmailClick = React.useCallback(() => {
onAddEmail({ type: 'email', isAuth: true });
}, [ onAddEmail ]);
Expand Down Expand Up @@ -45,7 +46,8 @@ const AuthModalScreenSuccessWallet = ({ address, onAddEmail, onClose, isAuth, pr
<Text>
Wallet{ ' ' }
<chakra.span fontWeight="700">{ shortenString(address) }</chakra.span>{ ' ' }
has been successfully used to log in to your Blockscout account.
has been successfully used to log in to your Blockscout account
{ Boolean(rewardsToken) && ` and Merits Program` }.
</Text>
{ !profile?.email ? (
<>
Expand Down
2 changes: 2 additions & 0 deletions ui/snippets/auth/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ export type ScreenSuccess = {
address: string;
profile: UserInfo;
isAuth?: boolean;
rewardsToken?: string;
};
export type Screen = {
type: 'select_method';
} | {
type: 'connect_wallet';
isAuth?: boolean;
loginToRewards?: boolean;
} | {
type: 'email';
isAuth?: boolean;
Expand Down
Loading

0 comments on commit cf37559

Please sign in to comment.