Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

User-Onboarding: Auth Type guidance modal #7856

Merged
merged 17 commits into from
May 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
46 changes: 40 additions & 6 deletions packages/commonwealth/client/scripts/controllers/app/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import { initAppState } from 'state';

import { OpenFeature } from '@openfeature/web-sdk';
import axios from 'axios';
import { getUniqueUserAddresses } from 'client/scripts/helpers/user';
import { fetchProfilesByAddress } from 'client/scripts/state/api/profiles/fetchProfilesByAddress';
import { authModal } from 'client/scripts/state/ui/modals/authModal';
import { welcomeOnboardModal } from 'client/scripts/state/ui/modals/welcomeOnboardModal';
import moment from 'moment';
import app from 'state';
Expand Down Expand Up @@ -358,13 +361,17 @@ export async function startLoginWithMagicLink({
const magic = await constructMagic(isCosmos, chain);

if (email) {
const authModalState = authModal.getState();
// email-based login
const bearer = await magic.auth.loginWithMagicLink({ email });
const address = await handleSocialLoginCallback({
const { address, isAddressNew } = await handleSocialLoginCallback({
bearer,
walletSsoSource: WalletSsoSource.Email,
returnEarlyIfNewAddress:
authModalState.shouldOpenGuidanceModalAfterMagicSSORedirect,
});
return { bearer, address };

return { bearer, address, isAddressNew };
} else {
const params = `?redirectTo=${
redirectTo ? encodeURIComponent(redirectTo) : ''
Expand Down Expand Up @@ -418,11 +425,13 @@ export async function handleSocialLoginCallback({
bearer,
chain,
walletSsoSource,
returnEarlyIfNewAddress = false,
}: {
bearer?: string;
chain?: string;
walletSsoSource?: string;
}): Promise<string> {
returnEarlyIfNewAddress?: boolean;
}): Promise<{ address: string; isAddressNew: boolean }> {
// desiredChain may be empty if social login was initialized from
// a page without a chain, in which case we default to an eth login
const desiredChain = app.chain?.meta || app.config.chains.getById(chain);
Expand Down Expand Up @@ -460,6 +469,25 @@ export async function handleSocialLoginCallback({
}
}

// check if this address exists in db
const profileAddresses = await fetchProfilesByAddress({
currentChainId: '',
profileAddresses: [magicAddress],
profileChainIds: [isCosmos ? ChainBase.CosmosSDK : ChainBase.Ethereum],
initiateProfilesAfterFetch: false,
});

const isAddressNew = profileAddresses?.length === 0;
const isAttemptingToConnectAddressToCommunity =
app.isLoggedIn() && app.activeChainId();
if (
isAddressNew &&
!isAttemptingToConnectAddressToCommunity &&
returnEarlyIfNewAddress
) {
return { address: magicAddress, isAddressNew };
}

let authedSessionPayload, authedSignature;
try {
// Sign a session
Expand Down Expand Up @@ -577,19 +605,25 @@ export async function handleSocialLoginCallback({
await app.user.updateEmail(ssoEmail, false);
}

const userUniqueAddresses = getUniqueUserAddresses({});

// if account is created in last few minutes and has a single
// profile (no account linking) then open the welcome modal.
// profile and address (no account linking) then open the welcome modal.
const isCreatedInLast5Minutes =
accountCreatedTime &&
moment().diff(moment(accountCreatedTime), 'minutes') < 5;
if (isCreatedInLast5Minutes && profiles?.length === 1) {
if (
isCreatedInLast5Minutes &&
profiles?.length === 1 &&
userUniqueAddresses.length === 1
) {
setTimeout(() => {
welcomeOnboardModal.getState().setIsWelcomeOnboardModalOpen(true);
}, 1000);
}
}

return magicAddress;
return { address: magicAddress, isAddressNew };
} else {
throw new Error(`Social auth unsuccessful: ${response.status}`);
}
Expand Down
21 changes: 21 additions & 0 deletions packages/commonwealth/client/scripts/helpers/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import app from '../state';

type GetUniqueUserAddressesArgs = {
forChain?: string;
};

export const getUniqueUserAddresses = ({
forChain,
}: GetUniqueUserAddressesArgs) => {
const addresses = app?.user?.addresses || [];

const filteredAddresses = forChain
? addresses.filter((x) => x?.community?.base === forChain)
: addresses;

const addressStrings = filteredAddresses.map((x) => x.address);

const uniqueAddresses = [...new Set(addressStrings)];

return uniqueAddresses;
};
mzparacha marked this conversation as resolved.
Show resolved Hide resolved
67 changes: 60 additions & 7 deletions packages/commonwealth/client/scripts/hooks/useWallets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ import { setDarkMode } from '../helpers/darkMode';
import { getAddressFromWallet, loginToNear } from '../helpers/wallet';
import Account from '../models/Account';
import IWebWallet from '../models/IWebWallet';
import { DISCOURAGED_NONREACTIVE_fetchProfilesByAddress } from '../state/api/profiles/fetchProfilesByAddress';
import {
DISCOURAGED_NONREACTIVE_fetchProfilesByAddress,
fetchProfilesByAddress,
} from '../state/api/profiles/fetchProfilesByAddress';
import { authModal } from '../state/ui/modals/authModal';
import {
breakpointFnValidator,
isWindowMediumSmallInclusive,
Expand Down Expand Up @@ -73,6 +77,7 @@ type IuseWalletProps = {
initialWallets?: IWebWallet<any>[];
onSuccess?: (address?: string | undefined, isNewlyCreated?: boolean) => void;
onModalClose: () => void;
onUnrecognizedAddressReceived?: () => boolean;
useSessionKeyLoginFlow?: boolean;
};

Expand Down Expand Up @@ -207,14 +212,33 @@ const useWallets = (walletProps: IuseWalletProps) => {

try {
const isCosmos = app.chain?.base === ChainBase.CosmosSDK;
const { address: magicAddress } = await startLoginWithMagicLink({
email: tempEmailToUse,
isCosmos,
redirectTo: document.location.pathname + document.location.search,
chain: app.chain?.id,
});
const isAttemptingToConnectAddressToCommunity =
app.isLoggedIn() && app.activeChainId();
const { address: magicAddress, isAddressNew } =
await startLoginWithMagicLink({
email: tempEmailToUse,
isCosmos,
redirectTo: document.location.pathname + document.location.search,
chain: app.chain?.id,
});
setIsMagicLoading(false);

// if SSO account address is not already present in db,
// and `shouldOpenGuidanceModalAfterMagicSSORedirect` is `true`,
// and the user isn't trying to link address to community,
// then open the user auth type guidance modal
// else clear state of `shouldOpenGuidanceModalAfterMagicSSORedirect`
if (
isAddressNew &&
!isAttemptingToConnectAddressToCommunity &&
!app.isLoggedIn()
) {
authModal
.getState()
.validateAndOpenAuthTypeGuidanceModalOnSSORedirectReceived();
return;
}

if (walletProps.onSuccess) {
walletProps.onSuccess(magicAddress, isNewlyCreated);
}
Expand Down Expand Up @@ -583,6 +607,35 @@ const useWallets = (walletProps: IuseWalletProps) => {
} else {
const selectedAddress = getAddressFromWallet(wallet);

// check if address exists
const profileAddresses = await fetchProfilesByAddress({
currentChainId: '',
profileAddresses: [
wallet.chain === ChainBase.Substrate
? addressSwapper({
address: selectedAddress,
currentPrefix: parseInt(
(app.chain as Substrate)?.meta.ss58Prefix,
10,
),
})
: selectedAddress,
],
profileChainIds: [app.activeChainId() ?? wallet.chain],
initiateProfilesAfterFetch: false,
});
const addressExists = profileAddresses?.length > 0;
const isAttemptingToConnectAddressToCommunity =
app.isLoggedIn() && app.activeChainId();
if (
!addressExists &&
!isAttemptingToConnectAddressToCommunity &&
walletProps.onUnrecognizedAddressReceived
) {
const shouldContinue = walletProps.onUnrecognizedAddressReceived();
if (!shouldContinue) return;
}

if (walletProps.useSessionKeyLoginFlow) {
await onSessionKeyRevalidation(wallet, selectedAddress);
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,26 @@ interface FetchProfilesByAddressProps {
currentChainId: string;
profileChainIds: string[];
profileAddresses: string[];
initiateProfilesAfterFetch?: boolean;
}

const fetchProfilesByAddress = async ({
export const fetchProfilesByAddress = async ({
currentChainId,
profileAddresses,
profileChainIds,
initiateProfilesAfterFetch = true,
}: FetchProfilesByAddressProps) => {
const response = await axios.post(
`${app.serverUrl()}${ApiEndpoints.FETCH_PROFILES}`,
{
addresses: profileAddresses,
communities: profileChainIds,
jwt: app.user.jwt,
...(app?.user?.jwt && { jwt: app.user.jwt }),
},
);

if (!initiateProfilesAfterFetch) return response.data.result;

return response.data.result.map((t) => {
const profile = new MinimumProfile(t.address, currentChainId);
profile.initialize(
Expand All @@ -48,6 +52,7 @@ const useFetchProfilesByAddressesQuery = ({
profileChainIds,
profileAddresses = [],
apiCallEnabled = true,
initiateProfilesAfterFetch = true,
}: UseFetchProfilesByAddressesQuery) => {
return useQuery({
// eslint-disable-next-line @tanstack/query/exhaustive-deps
Expand All @@ -64,6 +69,7 @@ const useFetchProfilesByAddressesQuery = ({
currentChainId,
profileAddresses,
profileChainIds,
initiateProfilesAfterFetch,
}),
staleTime: PROFILES_STALE_TIME,
enabled: apiCallEnabled,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const useFetchSelfProfileQuery = ({
updateAddressesOnSuccess = false,
}: UseFetchSelfProfileQuery) => {
return useQuery({
queryKey: [ApiEndpoints.FETCH_PROFILES],
queryKey: [ApiEndpoints.FETCH_SELF_PROFILE],
queryFn: fetchSelfProfile,
onSuccess: (profile) => {
if (
Expand Down
68 changes: 68 additions & 0 deletions packages/commonwealth/client/scripts/state/ui/modals/authModal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { AuthModalType } from 'client/scripts/views/modals/AuthModal/types';
import { createBoundedUseStore } from 'state/ui/utils';
import { devtools, persist } from 'zustand/middleware';
import { createStore } from 'zustand/vanilla';

interface AuthModalStore {
shouldOpenGuidanceModalAfterMagicSSORedirect: boolean;
setShouldOpenGuidanceModalAfterMagicSSORedirect: (
shouldOpen: boolean,
) => void;
triggerOpenModalType: AuthModalType;
setTriggerOpenModalType: (modalType: AuthModalType) => void;
validateAndOpenAuthTypeGuidanceModalOnSSORedirectReceived: () => void;
}

export const authModal = createStore<AuthModalStore>()(
devtools(
persist(
(set) => ({
shouldOpenGuidanceModalAfterMagicSSORedirect: false,
setShouldOpenGuidanceModalAfterMagicSSORedirect: (shouldOpen) => {
set((state) => {
return {
...state,
shouldOpenGuidanceModalAfterMagicSSORedirect: shouldOpen,
};
});
},
triggerOpenModalType: null,
setTriggerOpenModalType: (modalType) => {
set((state) => {
return {
...state,
triggerOpenModalType: modalType,
};
});
},
validateAndOpenAuthTypeGuidanceModalOnSSORedirectReceived: () => {
set((state) => {
if (state.shouldOpenGuidanceModalAfterMagicSSORedirect) {
return {
...state,
triggerOpenModalType: AuthModalType.AccountTypeGuidance,
shouldOpenGuidanceModalAfterMagicSSORedirect: false,
};
}

return {
...state,
shouldOpenGuidanceModalAfterMagicSSORedirect: false,
};
});
},
}),
{
name: 'auth-modal', // unique name
partialize: (state) => ({
shouldOpenGuidanceModalAfterMagicSSORedirect:
state.shouldOpenGuidanceModalAfterMagicSSORedirect,
}), // persist only shouldOpenGuidanceModalAfterMagicSSORedirect
},
),
),
);

const useAuthModalStore = createBoundedUseStore(authModal);

export default useAuthModalStore;
2 changes: 2 additions & 0 deletions packages/commonwealth/client/scripts/state/ui/modals/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import useAuthModalStore from './authModal';
import useManageCommunityStakeModalStore from './manageCommunityStakeModal';
import useNewTopicModalStore from './newTopicModal';
import useWelcomeOnboardModal from './welcomeOnboardModal';

export {
useAuthModalStore,
useManageCommunityStakeModalStore,
useNewTopicModalStore,
useWelcomeOnboardModal,
Expand Down
Loading
Loading