mirror of
https://github.com/logto-io/logto.git
synced 2025-03-24 22:41:28 -05:00
refactor(ui): replace social flow api (#2751)
This commit is contained in:
parent
e02f7e5ac2
commit
be2c65dc0c
13 changed files with 140 additions and 174 deletions
|
@ -2,12 +2,21 @@ import { maskUserInfo } from './format.js';
|
|||
|
||||
describe('maskUserInfo', () => {
|
||||
it('phone', () => {
|
||||
expect(maskUserInfo({ type: 'phone', value: '1234567890' })).toBe('****7890');
|
||||
expect(maskUserInfo({ type: 'phone', value: '1234567890' })).toEqual({
|
||||
type: 'phone',
|
||||
value: '****7890',
|
||||
});
|
||||
});
|
||||
it('email with name less than 5', () => {
|
||||
expect(maskUserInfo({ type: 'email', value: 'test@logto.io' })).toBe('****@logto.io');
|
||||
expect(maskUserInfo({ type: 'email', value: 'test@logto.io' })).toEqual({
|
||||
type: 'email',
|
||||
value: '****@logto.io',
|
||||
});
|
||||
});
|
||||
it('email with name more than 4', () => {
|
||||
expect(maskUserInfo({ type: 'email', value: 'foo_test@logto.io' })).toBe('foo_****@logto.io');
|
||||
expect(maskUserInfo({ type: 'email', value: 'foo_test@logto.io' })).toEqual({
|
||||
type: 'email',
|
||||
value: 'foo_****@logto.io',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,17 +1,26 @@
|
|||
export const maskUserInfo = ({ type, value }: { type: 'email' | 'phone'; value: string }) => {
|
||||
export const maskUserInfo = (info: { type: 'email' | 'phone'; value: string }) => {
|
||||
const { type, value } = info;
|
||||
|
||||
if (!value) {
|
||||
return value;
|
||||
return info;
|
||||
}
|
||||
|
||||
if (type === 'phone') {
|
||||
return `****${value.slice(-4)}`;
|
||||
return {
|
||||
type,
|
||||
value: `****${value.slice(-4)}`,
|
||||
};
|
||||
}
|
||||
|
||||
// Email
|
||||
const [name = '', domain = ''] = value.split('@');
|
||||
|
||||
const preview = name.length > 4 ? `${name.slice(0, 4)}` : '';
|
||||
|
||||
return `${preview}****@${domain}`;
|
||||
return {
|
||||
type,
|
||||
value: `${preview}****@${domain}`,
|
||||
};
|
||||
};
|
||||
|
||||
export const stringifyError = (error: Error) =>
|
||||
|
|
|
@ -1,13 +1,6 @@
|
|||
import ky from 'ky';
|
||||
|
||||
import { consent } from './consent';
|
||||
import {
|
||||
invokeSocialSignIn,
|
||||
signInWithSocial,
|
||||
bindSocialAccount,
|
||||
bindSocialRelatedUser,
|
||||
registerWithSocial,
|
||||
} from './social';
|
||||
|
||||
jest.mock('ky', () => ({
|
||||
extend: () => ky,
|
||||
|
@ -17,10 +10,6 @@ jest.mock('ky', () => ({
|
|||
}));
|
||||
|
||||
describe('api', () => {
|
||||
const phone = '18888888';
|
||||
const code = '111111';
|
||||
const email = 'foo@logto.io';
|
||||
|
||||
const mockKyPost = ky.post as jest.Mock;
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -31,56 +20,4 @@ describe('api', () => {
|
|||
await consent();
|
||||
expect(ky.post).toBeCalledWith('/api/session/consent');
|
||||
});
|
||||
|
||||
it('invokeSocialSignIn', async () => {
|
||||
await invokeSocialSignIn('connectorId', 'state', 'redirectUri');
|
||||
expect(ky.post).toBeCalledWith('/api/session/sign-in/social', {
|
||||
json: {
|
||||
connectorId: 'connectorId',
|
||||
state: 'state',
|
||||
redirectUri: 'redirectUri',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('signInWithSocial', async () => {
|
||||
const parameters = {
|
||||
connectorId: 'connectorId',
|
||||
data: {
|
||||
redirectUri: 'redirectUri',
|
||||
code: 'code',
|
||||
},
|
||||
};
|
||||
await signInWithSocial(parameters);
|
||||
expect(ky.post).toBeCalledWith('/api/session/sign-in/social/auth', {
|
||||
json: parameters,
|
||||
});
|
||||
});
|
||||
|
||||
it('bindSocialAccount', async () => {
|
||||
await bindSocialAccount('connectorId');
|
||||
expect(ky.post).toBeCalledWith('/api/session/bind-social', {
|
||||
json: {
|
||||
connectorId: 'connectorId',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('bindSocialRelatedUser', async () => {
|
||||
await bindSocialRelatedUser('connectorId');
|
||||
expect(ky.post).toBeCalledWith('/api/session/sign-in/bind-social-related-user', {
|
||||
json: {
|
||||
connectorId: 'connectorId',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('registerWithSocial', async () => {
|
||||
await registerWithSocial('connectorId');
|
||||
expect(ky.post).toBeCalledWith('/api/session/register/social', {
|
||||
json: {
|
||||
connectorId: 'connectorId',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,6 +7,8 @@ import type {
|
|||
PhonePasswordPayload,
|
||||
EmailPasscodePayload,
|
||||
PhonePasscodePayload,
|
||||
SocialConnectorPayload,
|
||||
SocialIdentityPayload,
|
||||
} from '@logto/schemas';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
|
||||
|
@ -28,15 +30,20 @@ export const signInWithPasswordIdentifier = async (
|
|||
payload: PasswordSignInPayload,
|
||||
socialToBind?: string
|
||||
) => {
|
||||
await api.put(`${interactionPrefix}`, {
|
||||
json: {
|
||||
event: InteractionEvent.SignIn,
|
||||
identifier: payload,
|
||||
},
|
||||
});
|
||||
|
||||
if (socialToBind) {
|
||||
// TODO: bind social account
|
||||
await api.patch(`${interactionPrefix}/identifiers`, {
|
||||
json: payload,
|
||||
});
|
||||
await api.patch(`${interactionPrefix}/profile`, {
|
||||
json: { connectorId: socialToBind },
|
||||
});
|
||||
} else {
|
||||
await api.put(`${interactionPrefix}`, {
|
||||
json: {
|
||||
event: InteractionEvent.SignIn,
|
||||
identifier: payload,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return api.post(`${interactionPrefix}/submit`).json<Response>();
|
||||
|
@ -89,9 +96,9 @@ export const signInWithPasscodeIdentifier = async (
|
|||
json: payload,
|
||||
});
|
||||
|
||||
if (socialToBind) {
|
||||
// TODO: bind social account
|
||||
}
|
||||
await api.patch(`${interactionPrefix}/profile`, {
|
||||
json: { connectorId: socialToBind },
|
||||
});
|
||||
|
||||
return api.post(`${interactionPrefix}/submit`).json<Response>();
|
||||
};
|
||||
|
@ -110,9 +117,9 @@ export const addProfileWithPasscodeIdentifier = async (
|
|||
json: identifier,
|
||||
});
|
||||
|
||||
if (socialToBind) {
|
||||
// TODO: bind social account
|
||||
}
|
||||
await api.patch(`${interactionPrefix}/profile`, {
|
||||
json: { connectorId: socialToBind },
|
||||
});
|
||||
|
||||
return api.post(`${interactionPrefix}/submit`).json<Response>();
|
||||
};
|
||||
|
@ -159,9 +166,65 @@ export const addProfile = async (
|
|||
) => {
|
||||
await api.patch(`${interactionPrefix}/profile`, { json: payload });
|
||||
|
||||
if (socialToBind) {
|
||||
// TODO: bind social account
|
||||
}
|
||||
await api.patch(`${interactionPrefix}/profile`, {
|
||||
json: { connectorId: socialToBind },
|
||||
});
|
||||
|
||||
return api.post(`${interactionPrefix}/submit`).json<Response>();
|
||||
};
|
||||
|
||||
export const getSocialAuthorizationUrl = async (
|
||||
connectorId: string,
|
||||
state: string,
|
||||
redirectUri: string
|
||||
) => {
|
||||
await api.put(`${interactionPrefix}`, { json: { event: InteractionEvent.SignIn } });
|
||||
|
||||
return api
|
||||
.post(`${interactionPrefix}/${verificationPath}/social-authorization-uri`, {
|
||||
json: {
|
||||
connectorId,
|
||||
state,
|
||||
redirectUri,
|
||||
},
|
||||
})
|
||||
.json<Response>();
|
||||
};
|
||||
|
||||
export const signInWithSocial = async (payload: SocialConnectorPayload) => {
|
||||
await api.patch(`${interactionPrefix}/identifiers`, {
|
||||
json: payload,
|
||||
});
|
||||
|
||||
return api.post(`${interactionPrefix}/submit`).json<Response>();
|
||||
};
|
||||
|
||||
export const registerWithVerifiedSocial = async (connectorId: string) => {
|
||||
await api.put(`${interactionPrefix}/event`, {
|
||||
json: {
|
||||
event: InteractionEvent.Register,
|
||||
},
|
||||
});
|
||||
|
||||
await api.patch(`${interactionPrefix}/profile`, {
|
||||
json: {
|
||||
connectorId,
|
||||
},
|
||||
});
|
||||
|
||||
return api.post(`${interactionPrefix}/submit`).json<Response>();
|
||||
};
|
||||
|
||||
export const bindSocialRelatedUser = async (payload: SocialIdentityPayload) => {
|
||||
await api.patch(`${interactionPrefix}/identifiers`, {
|
||||
json: payload,
|
||||
});
|
||||
|
||||
await api.patch(`${interactionPrefix}/profile`, {
|
||||
json: {
|
||||
connectorId: payload.connectorId,
|
||||
},
|
||||
});
|
||||
|
||||
return api.post(`${interactionPrefix}/submit`).json<Response>();
|
||||
};
|
||||
|
|
|
@ -1,37 +1,5 @@
|
|||
import api from './api';
|
||||
|
||||
export const invokeSocialSignIn = async (
|
||||
connectorId: string,
|
||||
state: string,
|
||||
redirectUri: string
|
||||
) => {
|
||||
type Response = {
|
||||
redirectTo: string;
|
||||
};
|
||||
|
||||
return api
|
||||
.post('/api/session/sign-in/social', {
|
||||
json: {
|
||||
connectorId,
|
||||
state,
|
||||
redirectUri,
|
||||
},
|
||||
})
|
||||
.json<Response>();
|
||||
};
|
||||
|
||||
export const signInWithSocial = async (json: { connectorId: string; data: unknown }) => {
|
||||
type Response = {
|
||||
redirectTo: string;
|
||||
};
|
||||
|
||||
return api
|
||||
.post('/api/session/sign-in/social/auth', {
|
||||
json,
|
||||
})
|
||||
.json<Response>();
|
||||
};
|
||||
|
||||
export const bindSocialAccount = async (connectorId: string) => {
|
||||
return api
|
||||
.post('/api/session/bind-social', {
|
||||
|
@ -41,31 +9,3 @@ export const bindSocialAccount = async (connectorId: string) => {
|
|||
})
|
||||
.json();
|
||||
};
|
||||
|
||||
export const bindSocialRelatedUser = async (connectorId: string) => {
|
||||
type Response = {
|
||||
redirectTo: string;
|
||||
};
|
||||
|
||||
return api
|
||||
.post('/api/session/sign-in/bind-social-related-user', {
|
||||
json: {
|
||||
connectorId,
|
||||
},
|
||||
})
|
||||
.json<Response>();
|
||||
};
|
||||
|
||||
export const registerWithSocial = async (connectorId: string) => {
|
||||
type Response = {
|
||||
redirectTo: string;
|
||||
};
|
||||
|
||||
return api
|
||||
.post('/api/session/register/social', {
|
||||
json: {
|
||||
connectorId,
|
||||
},
|
||||
})
|
||||
.json<Response>();
|
||||
};
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { InteractionEvent } from '@logto/schemas';
|
||||
|
||||
import { UserFlow } from '@/types';
|
||||
import { UserFlow, SearchParameters } from '@/types';
|
||||
import { getSearchParameters } from '@/utils';
|
||||
|
||||
import type { SendPasscodePayload } from './interaction';
|
||||
import { putInteraction, sendPasscode } from './interaction';
|
||||
|
@ -10,7 +11,10 @@ export const getSendPasscodeApi = (type: UserFlow) => async (payload: SendPassco
|
|||
await putInteraction(InteractionEvent.ForgotPassword);
|
||||
}
|
||||
|
||||
if (type === UserFlow.signIn) {
|
||||
const socialToBind = getSearchParameters(location.search, SearchParameters.bindWithSocial);
|
||||
|
||||
// Init a new interaction only if the user is not binding with a social
|
||||
if (type === UserFlow.signIn && !socialToBind) {
|
||||
await putInteraction(InteractionEvent.SignIn);
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import { fireEvent, waitFor } from '@testing-library/react';
|
|||
|
||||
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
|
||||
import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider';
|
||||
import { registerWithSocial, bindSocialRelatedUser } from '@/apis/social';
|
||||
import { registerWithVerifiedSocial, bindSocialRelatedUser } from '@/apis/interaction';
|
||||
|
||||
import SocialCreateAccount from '.';
|
||||
|
||||
|
@ -11,12 +11,12 @@ const mockNavigate = jest.fn();
|
|||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useNavigate: () => mockNavigate,
|
||||
useLocation: () => ({ state: { relatedUser: 'foo@logto.io' } }),
|
||||
useLocation: () => ({ state: { relatedUser: { type: 'email', value: 'foo@logto.io' } } }),
|
||||
}));
|
||||
|
||||
jest.mock('@/apis/social', () => ({
|
||||
registerWithSocial: jest.fn(async () => 0),
|
||||
bindSocialRelatedUser: jest.fn(async () => 0),
|
||||
jest.mock('@/apis/interaction', () => ({
|
||||
registerWithVerifiedSocial: jest.fn(async () => ({ redirectTo: '/' })),
|
||||
bindSocialRelatedUser: jest.fn(async () => ({ redirectTo: '/' })),
|
||||
}));
|
||||
|
||||
describe('SocialCreateAccount', () => {
|
||||
|
@ -31,7 +31,7 @@ describe('SocialCreateAccount', () => {
|
|||
expect(queryByText('secondary.social_bind_with')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should call registerWithSocial when click create button', async () => {
|
||||
it('should call registerWithVerifiedSocial when click create button', async () => {
|
||||
const { getByText } = renderWithPageContext(<SocialCreateAccount connectorId="github" />);
|
||||
const createButton = getByText('action.create');
|
||||
|
||||
|
@ -39,7 +39,7 @@ describe('SocialCreateAccount', () => {
|
|||
fireEvent.click(createButton);
|
||||
});
|
||||
|
||||
expect(registerWithSocial).toBeCalledWith('github');
|
||||
expect(registerWithVerifiedSocial).toBeCalledWith('github');
|
||||
});
|
||||
|
||||
it('should render bindUser Button when relatedUserInfo found', async () => {
|
||||
|
@ -48,6 +48,6 @@ describe('SocialCreateAccount', () => {
|
|||
await waitFor(() => {
|
||||
fireEvent.click(bindButton);
|
||||
});
|
||||
expect(bindSocialRelatedUser).toBeCalledWith('github');
|
||||
expect(bindSocialRelatedUser).toBeCalledWith({ connectorId: 'github', identityType: 'email' });
|
||||
});
|
||||
});
|
||||
|
|
|
@ -27,9 +27,9 @@ const SocialCreateAccount = ({ connectorId, className }: Props) => {
|
|||
<div className={styles.desc}>{t('description.social_bind_with_existing')}</div>
|
||||
<Button
|
||||
title="action.bind"
|
||||
i18nProps={{ address: relatedUser }}
|
||||
i18nProps={{ address: relatedUser.value }}
|
||||
onClick={() => {
|
||||
bindSocialRelatedUser(connectorId);
|
||||
bindSocialRelatedUser({ connectorId, identityType: relatedUser.type });
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import type { SocialIdentityPayload } from '@logto/schemas';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { is } from 'superstruct';
|
||||
|
||||
import { registerWithSocial, bindSocialRelatedUser } from '@/apis/social';
|
||||
import { registerWithVerifiedSocial, bindSocialRelatedUser } from '@/apis/interaction';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import { bindSocialStateGuard } from '@/types/guard';
|
||||
|
||||
|
@ -14,7 +15,7 @@ const useBindSocial = () => {
|
|||
const requiredProfileErrorHandlers = useRequiredProfileErrorHandler();
|
||||
|
||||
const { result: registerResult, run: asyncRegisterWithSocial } = useApi(
|
||||
registerWithSocial,
|
||||
registerWithVerifiedSocial,
|
||||
requiredProfileErrorHandlers
|
||||
);
|
||||
const { result: bindUserResult, run: asyncBindSocialRelatedUser } = useApi(
|
||||
|
@ -30,8 +31,8 @@ const useBindSocial = () => {
|
|||
);
|
||||
|
||||
const bindRelatedUserHandler = useCallback(
|
||||
(connectorId: string) => {
|
||||
void asyncBindSocialRelatedUser(connectorId);
|
||||
(payload: SocialIdentityPayload) => {
|
||||
void asyncBindSocialRelatedUser(payload);
|
||||
},
|
||||
[asyncBindSocialRelatedUser]
|
||||
);
|
||||
|
|
|
@ -3,7 +3,7 @@ import { useEffect, useCallback, useContext, useMemo } from 'react';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { signInWithSocial } from '@/apis/social';
|
||||
import { signInWithSocial } from '@/apis/interaction';
|
||||
import { parseQueryParameters } from '@/utils';
|
||||
import { stateValidation } from '@/utils/social-connectors';
|
||||
|
||||
|
@ -57,7 +57,7 @@ const useSocialSignInListener = () => {
|
|||
async (connectorId: string, data: Record<string, unknown>) => {
|
||||
void asyncSignInWithSocial({
|
||||
connectorId,
|
||||
data: {
|
||||
connectorData: {
|
||||
redirectUri: `${window.location.origin}/callback/${connectorId}`, // For validation use only
|
||||
...data,
|
||||
},
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import type { ConnectorMetadata } from '@logto/schemas';
|
||||
import { useCallback, useContext } from 'react';
|
||||
|
||||
import { invokeSocialSignIn } from '@/apis/social';
|
||||
import { getSocialAuthorizationUrl } from '@/apis/interaction';
|
||||
import { getLogtoNativeSdk, isNativeWebview } from '@/utils/native-sdk';
|
||||
import { generateState, storeState, buildSocialLandingUri } from '@/utils/social-connectors';
|
||||
|
||||
|
@ -13,7 +13,7 @@ const useSocial = () => {
|
|||
const { experienceSettings, theme } = useContext(PageContext);
|
||||
const { termsValidation } = useTerms();
|
||||
|
||||
const { run: asyncInvokeSocialSignIn } = useApi(invokeSocialSignIn);
|
||||
const { run: asyncInvokeSocialSignIn } = useApi(getSocialAuthorizationUrl);
|
||||
|
||||
const nativeSignInHandler = useCallback((redirectTo: string, connector: ConnectorMetadata) => {
|
||||
const { id: connectorId, platform } = connector;
|
||||
|
|
|
@ -3,18 +3,18 @@ import { MemoryRouter, Route, Routes } from 'react-router-dom';
|
|||
|
||||
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
|
||||
import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider';
|
||||
import * as socialSignInApi from '@/apis/social';
|
||||
import { signInWithSocial } from '@/apis/interaction';
|
||||
import { generateState, storeState } from '@/utils/social-connectors';
|
||||
|
||||
import SocialCallback from '.';
|
||||
|
||||
const origin = 'http://localhost:3000';
|
||||
|
||||
describe('SocialCallbackPage with code', () => {
|
||||
const signInWithSocialSpy = jest
|
||||
.spyOn(socialSignInApi, 'signInWithSocial')
|
||||
.mockResolvedValue({ redirectTo: `/sign-in` });
|
||||
jest.mock('@/apis/interaction', () => ({
|
||||
signInWithSocial: jest.fn().mockResolvedValue({ redirectTo: `/sign-in` }),
|
||||
}));
|
||||
|
||||
describe('SocialCallbackPage with code', () => {
|
||||
it('callback validation and signIn with social', async () => {
|
||||
const state = generateState();
|
||||
storeState(state, 'github');
|
||||
|
@ -43,7 +43,7 @@ describe('SocialCallbackPage with code', () => {
|
|||
|
||||
await act(async () => {
|
||||
await waitFor(() => {
|
||||
expect(signInWithSocialSpy).toBeCalled();
|
||||
expect(signInWithSocial).toBeCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,7 +2,10 @@ import { SignInIdentifier, MissingProfile } from '@logto/schemas';
|
|||
import * as s from 'superstruct';
|
||||
|
||||
export const bindSocialStateGuard = s.object({
|
||||
relatedUser: s.optional(s.string()),
|
||||
relatedUser: s.object({
|
||||
type: s.union([s.literal('email'), s.literal('phone')]),
|
||||
value: s.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const passcodeStateGuard = s.object({
|
||||
|
|
Loading…
Add table
Reference in a new issue