Skip to content

Commit

Permalink
refactor(experience): migrate the social and sso flow
Browse files Browse the repository at this point in the history
migrate the social and sso flow
  • Loading branch information
simeng-li committed Aug 7, 2024
1 parent 5aead51 commit 241ed0a
Show file tree
Hide file tree
Showing 14 changed files with 285 additions and 103 deletions.
96 changes: 85 additions & 11 deletions packages/experience/src/apis/experience.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
InteractionEvent,
type PasswordVerificationPayload,
SignInIdentifier,
type SocialVerificationCallbackPayload,
type UpdateProfileApiPayload,
type VerificationCodeIdentifier,
} from '@logto/schemas';
Expand Down Expand Up @@ -51,11 +52,21 @@ const updateInteractionEvent = async (interactionEvent: InteractionEvent) =>
},
});

const identifyAndSubmitInteraction = async (payload?: IdentificationApiPayload) => {
export const identifyAndSubmitInteraction = async (payload?: IdentificationApiPayload) => {
await identifyUser(payload);
return submitInteraction();
};

export const registerWithVerifiedIdentifier = async (verificationId: string) => {
await updateInteractionEvent(InteractionEvent.Register);
return identifyAndSubmitInteraction({ verificationId });
};

export const signInWithVerifiedIdentifier = async (verificationId: string) => {
await updateInteractionEvent(InteractionEvent.SignIn);
return identifyAndSubmitInteraction({ verificationId });
};

// Password APIs
export const signInWithPasswordIdentifier = async (payload: PasswordVerificationPayload) => {
await initInteraction(InteractionEvent.SignIn);
Expand Down Expand Up @@ -113,16 +124,6 @@ export const identifyWithVerificationCode = async (json: VerificationCodePayload
return identifyAndSubmitInteraction({ verificationId });
};

export const registerWithVerifiedIdentifier = async (verificationId: string) => {
await updateInteractionEvent(InteractionEvent.Register);
return identifyAndSubmitInteraction({ verificationId });
};

export const signInWithVerifiedIdentifier = async (verificationId: string) => {
await updateInteractionEvent(InteractionEvent.SignIn);
return identifyAndSubmitInteraction({ verificationId });
};

// Profile APIs

export const updateProfileWithVerificationCode = async (json: VerificationCodePayload) => {
Expand All @@ -149,3 +150,76 @@ export const resetPassword = async (password: string) => {

return submitInteraction();
};

// Social and SSO APIs

export const getSocialAuthorizationUrl = async (
connectorId: string,
state: string,
redirectUri: string
) => {
await initInteraction(InteractionEvent.SignIn);

return api
.post(`${experienceRoutes.verification}/social/${connectorId}/authorization-uri`, {
json: {
state,
redirectUri,
},
})
.json<
VerificationResponse & {
authorizationUri: string;
}
>();
};

export const verifySocialVerification = async (
connectorId: string,
payload: SocialVerificationCallbackPayload
) =>
api
.post(`${experienceRoutes.verification}/social/${connectorId}/verify`, {
json: payload,
})
.json<VerificationResponse>();

export const bindSocialRelatedUser = async (verificationId: string) => {
await updateInteractionEvent(InteractionEvent.SignIn);
await identifyUser({ verificationId, linkSocialIdentity: true });
return submitInteraction();
};

export const getSsoConnectors = async (email: string) =>
api
.get(`${experienceRoutes.verification}/sso/connectors`, {
searchParams: {
email,
},
})
.json<{ connectorIds: string[] }>();

export const getSsoAuthorizationUrl = async (connectorId: string, payload: unknown) => {
await initInteraction(InteractionEvent.SignIn);

return api
.post(`${experienceRoutes.verification}/sso/${connectorId}/authorization-uri`, {
json: payload,
})
.json<
VerificationResponse & {
authorizationUri: string;
}
>();
};

export const signInWithSso = async (
connectorId: string,
payload: SocialVerificationCallbackPayload & { verificationId: string }
) => {
await api.post(`${experienceRoutes.verification}/sso/${connectorId}/verify`, {
json: payload,
});

return identifyAndSubmitInteraction({ verificationId: payload.verificationId });
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { fireEvent, waitFor } from '@testing-library/react';
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider';
import { mockSignInExperienceSettings } from '@/__mocks__/logto';
import { registerWithVerifiedSocial, bindSocialRelatedUser } from '@/apis/interaction';
import { bindSocialRelatedUser, registerWithVerifiedSocial } from '@/apis/interaction';

import SocialLinkAccount from '.';

Expand All @@ -30,7 +30,7 @@ describe('SocialLinkAccount', () => {
it('should render bindUser Button', async () => {
const { getByText } = renderWithPageContext(
<SettingsProvider>
<SocialLinkAccount connectorId="github" relatedUser={relatedUser} />
<SocialLinkAccount connectorId="github" relatedUser={relatedUser} verificationId="foo" />
</SettingsProvider>
);
const bindButton = getByText('action.bind');
Expand All @@ -57,7 +57,7 @@ describe('SocialLinkAccount', () => {
},
}}
>
<SocialLinkAccount connectorId="github" relatedUser={relatedUser} />
<SocialLinkAccount connectorId="github" relatedUser={relatedUser} verificationId="foo" />
</SettingsProvider>
);

Expand All @@ -77,7 +77,7 @@ describe('SocialLinkAccount', () => {
},
}}
>
<SocialLinkAccount connectorId="github" relatedUser={relatedUser} />
<SocialLinkAccount connectorId="github" relatedUser={relatedUser} verificationId="foo" />
</SettingsProvider>
);

Expand All @@ -97,7 +97,7 @@ describe('SocialLinkAccount', () => {
},
}}
>
<SocialLinkAccount connectorId="github" relatedUser={relatedUser} />
<SocialLinkAccount connectorId="github" relatedUser={relatedUser} verificationId="foo" />
</SettingsProvider>
);

Expand All @@ -108,7 +108,7 @@ describe('SocialLinkAccount', () => {
it('should call registerWithVerifiedSocial when click create button', async () => {
const { getByText } = renderWithPageContext(
<SettingsProvider>
<SocialLinkAccount connectorId="github" relatedUser={relatedUser} />
<SocialLinkAccount connectorId="github" relatedUser={relatedUser} verificationId="foo" />
</SettingsProvider>
);
const createButton = getByText('action.create_account_without_linking');
Expand Down
10 changes: 4 additions & 6 deletions packages/experience/src/containers/SocialLinkAccount/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import useBindSocialRelatedUser from './use-social-link-related-user';
type Props = {
readonly className?: string;
readonly connectorId: string;
readonly verificationId: string;
readonly relatedUser: SocialRelatedUserInfo;
};

Expand All @@ -39,7 +40,7 @@ const getCreateAccountActionText = (signUpMethods: string[]): TFuncKey => {
return 'action.create_account_without_linking';
};

const SocialLinkAccount = ({ connectorId, className, relatedUser }: Props) => {
const SocialLinkAccount = ({ connectorId, verificationId, className, relatedUser }: Props) => {
const { t } = useTranslation();
const { signUpMethods } = useSieMethods();

Expand All @@ -58,10 +59,7 @@ const SocialLinkAccount = ({ connectorId, className, relatedUser }: Props) => {
title="action.bind"
i18nProps={{ address: type === 'email' ? maskEmail(value) : maskPhone(value) }}
onClick={() => {
void bindSocialRelatedUser({
connectorId,
...(type === 'email' ? { email: value } : { phone: value }),
});
void bindSocialRelatedUser(verificationId);
}}
/>

Expand All @@ -72,7 +70,7 @@ const SocialLinkAccount = ({ connectorId, className, relatedUser }: Props) => {
<TextLink
text={actionText}
onClick={() => {
void registerWithSocial(connectorId);
void registerWithSocial(verificationId);
}}
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useCallback } from 'react';

import { bindSocialRelatedUser } from '@/apis/interaction';
import { bindSocialRelatedUser } from '@/apis/experience';
import useApi from '@/hooks/use-api';
import useErrorHandler from '@/hooks/use-error-handler';
import useGlobalRedirectTo from '@/hooks/use-global-redirect-to';
Expand Down
17 changes: 13 additions & 4 deletions packages/experience/src/containers/SocialSignInList/use-social.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import {
AgreeToTermsPolicy,
ConnectorPlatform,
VerificationType,
type ExperienceSocialConnector,
} from '@logto/schemas';
import { useCallback, useContext } from 'react';

import PageContext from '@/Providers/PageContextProvider/PageContext';
import { getSocialAuthorizationUrl } from '@/apis/interaction';
import UserInteractionContext from '@/Providers/UserInteractionContextProvider/UserInteractionContext';
import { getSocialAuthorizationUrl } from '@/apis/experience';
import useApi from '@/hooks/use-api';
import useErrorHandler from '@/hooks/use-error-handler';
import useGlobalRedirectTo from '@/hooks/use-global-redirect-to';
Expand All @@ -20,6 +22,8 @@ const useSocial = () => {
const handleError = useErrorHandler();
const asyncInvokeSocialSignIn = useApi(getSocialAuthorizationUrl);
const { termsValidation, agreeToTermsPolicy } = useTerms();
const { setVerificationId } = useContext(UserInteractionContext);

const redirectTo = useGlobalRedirectTo({
shouldClearInteractionContextSession: false,
isReplace: false,
Expand Down Expand Up @@ -69,26 +73,31 @@ const useSocial = () => {
return;
}

if (!result?.redirectTo) {
if (!result) {
return;
}

const { verificationId, authorizationUri } = result;

setVerificationId(VerificationType.Social, verificationId);

// Invoke native social sign-in flow
if (isNativeWebview()) {
nativeSignInHandler(result.redirectTo, connector);
nativeSignInHandler(authorizationUri, connector);

return;
}

// Invoke web social sign-in flow
await redirectTo(result.redirectTo);
await redirectTo(authorizationUri);
},
[
agreeToTermsPolicy,
asyncInvokeSocialSignIn,
handleError,
nativeSignInHandler,
redirectTo,
setVerificationId,
termsValidation,
]
);
Expand Down
10 changes: 5 additions & 5 deletions packages/experience/src/hooks/use-check-single-sign-on.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { experience, type SsoConnectorMetadata } from '@logto/schemas';
import { useCallback, useState, useContext } from 'react';
import { useCallback, useContext, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';

import UserInteractionContext from '@/Providers/UserInteractionContextProvider/UserInteractionContext';
import { getSingleSignOnConnectors } from '@/apis/single-sign-on';
import { getSsoConnectors } from '@/apis/experience';
import useApi from '@/hooks/use-api';
import useErrorHandler from '@/hooks/use-error-handler';

Expand All @@ -13,7 +13,7 @@ import useSingleSignOn from './use-single-sign-on';
const useCheckSingleSignOn = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const request = useApi(getSingleSignOnConnectors);
const request = useApi(getSsoConnectors);
const [errorMessage, setErrorMessage] = useState<string | undefined>();
const { setSsoEmail, setSsoConnectors, availableSsoConnectorsMap } =
useContext(UserInteractionContext);
Expand Down Expand Up @@ -56,8 +56,8 @@ const useCheckSingleSignOn = () => {
return;
}

const connectors = result
?.map((connectorId) => availableSsoConnectorsMap.get(connectorId))
const connectors = result?.connectorIds
.map((connectorId) => availableSsoConnectorsMap.get(connectorId))
// eslint-disable-next-line unicorn/prefer-native-coercion-functions -- make the type more specific
.filter((connector): connector is SsoConnectorMetadata => Boolean(connector));

Expand Down
8 changes: 4 additions & 4 deletions packages/experience/src/hooks/use-single-sign-on-watch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import {
experience,
type SsoConnectorMetadata,
} from '@logto/schemas';
import { useEffect, useCallback, useContext } from 'react';
import { useCallback, useContext, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';

import SingleSignOnFormModeContext from '@/Providers/SingleSignOnFormModeContextProvider/SingleSignOnFormModeContext';
import UserInteractionContext from '@/Providers/UserInteractionContextProvider/UserInteractionContext';
import { getSingleSignOnConnectors } from '@/apis/single-sign-on';
import { getSsoConnectors } from '@/apis/experience';
import type { IdentifierInputValue } from '@/components/InputFields/SmartInputField';
import useApi from '@/hooks/use-api';
import useSingleSignOn from '@/hooks/use-single-sign-on';
Expand All @@ -28,7 +28,7 @@ const useSingleSignOnWatch = (identifierInput?: IdentifierInputValue) => {

const { showSingleSignOnForm, setShowSingleSignOnForm } = useContext(SingleSignOnFormModeContext);

const request = useApi(getSingleSignOnConnectors, { silent: true });
const request = useApi(getSsoConnectors, { silent: true });

const singleSignOn = useSingleSignOn();

Expand All @@ -43,7 +43,7 @@ const useSingleSignOnWatch = (identifierInput?: IdentifierInputValue) => {
return false;
}

const connectors = result
const connectors = result.connectorIds
.map((connectorId) => availableSsoConnectorsMap.get(connectorId))
// eslint-disable-next-line unicorn/prefer-native-coercion-functions -- make the type more specific
.filter((connector): connector is SsoConnectorMetadata => Boolean(connector));
Expand Down
Loading

0 comments on commit 241ed0a

Please sign in to comment.