diff --git a/packages/core/src/routes/session.test.ts b/packages/core/src/routes/session.test.ts index 51c5dc24a..0d16bf063 100644 --- a/packages/core/src/routes/session.test.ts +++ b/packages/core/src/routes/session.test.ts @@ -2,7 +2,15 @@ import { User } from '@logto/schemas'; import { Provider } from 'oidc-provider'; -import { mockSignInExperience, mockUser } from '@/__mocks__'; +import { + mockSignInExperience, + mockUser, + mockAliyunDmConnectorInstance, + mockAliyunSmsConnectorInstance, + mockFacebookConnectorInstance, + mockGithubConnectorInstance, + mockGoogleConnectorInstance, +} from '@/__mocks__'; import { ConnectorType } from '@/connectors/types'; import RequestError from '@/errors/RequestError'; import * as signInExperienceQueries from '@/queries/sign-in-experience'; @@ -99,6 +107,13 @@ const getConnectorInstanceById = jest.fn(async (connectorId: string) => { return { connector, metadata, getAuthorizationUri }; }); +const getConnectorInstances = jest.fn(async () => [ + mockAliyunDmConnectorInstance, + mockAliyunSmsConnectorInstance, + mockFacebookConnectorInstance, + mockGithubConnectorInstance, + mockGoogleConnectorInstance, +]); jest.mock('@/connectors', () => ({ getSocialConnectorInstanceById: async (connectorId: string) => { const connectorInstance = await getConnectorInstanceById(connectorId); @@ -112,6 +127,7 @@ jest.mock('@/connectors', () => ({ return connectorInstance; }, + getConnectorInstances: async () => getConnectorInstances(), })); const grantSave = jest.fn(async () => 'finalGrantId'); @@ -431,11 +447,11 @@ describe('sessionRoutes', () => { }); }); - describe('POST /session/bind-social-related-user', () => { + describe('POST /session/sign-in/bind-social-related-user', () => { it('throw if session is not authorized', async () => { await expect( sessionRequest - .post('/session/bind-social-related-user') + .post('/session/sign-in/bind-social-related-user') .send({ connectorId: 'connectorId' }) ).resolves.toHaveProperty('statusCode', 400); }); @@ -445,7 +461,7 @@ describe('sessionRoutes', () => { }); await expect( sessionRequest - .post('/session/bind-social-related-user') + .post('/session/sign-in/bind-social-related-user') .send({ connectorId: 'connectorId' }) ).resolves.toHaveProperty('statusCode', 400); }); @@ -459,7 +475,7 @@ describe('sessionRoutes', () => { }, }, }); - const response = await sessionRequest.post('/session/bind-social-related-user').send({ + const response = await sessionRequest.post('/session/sign-in/bind-social-related-user').send({ connectorId: 'connectorId', }); expect(response.statusCode).toEqual(200); @@ -890,15 +906,29 @@ describe('sessionRoutes', () => { }); describe('GET /sign-in-settings', () => { - const signInExperienceQuerySpon = jest + const signInExperienceQuerySpyOn = jest .spyOn(signInExperienceQueries, 'findDefaultSignInExperience') .mockResolvedValue(mockSignInExperience); - it('should call findDefaultSignInExperience', async () => { + it('should return github and facebook connector instances', async () => { const response = await sessionRequest.get('/sign-in-settings'); - expect(signInExperienceQuerySpon).toHaveBeenCalledTimes(1); + expect(signInExperienceQuerySpyOn).toHaveBeenCalledTimes(1); expect(response.status).toEqual(200); - expect(response.body).toEqual(mockSignInExperience); + expect(response.body).toMatchObject( + expect.objectContaining({ + ...mockSignInExperience, + socialConnectors: [ + { + ...mockGithubConnectorInstance.metadata, + id: mockGithubConnectorInstance.connector.id, + }, + { + ...mockFacebookConnectorInstance.metadata, + id: mockFacebookConnectorInstance.connector.id, + }, + ], + }) + ); }); }); diff --git a/packages/core/src/routes/session.ts b/packages/core/src/routes/session.ts index 11214352b..b25ee58bb 100644 --- a/packages/core/src/routes/session.ts +++ b/packages/core/src/routes/session.ts @@ -8,7 +8,7 @@ import pick from 'lodash.pick'; import { Provider } from 'oidc-provider'; import { object, string } from 'zod'; -import { getSocialConnectorInstanceById } from '@/connectors'; +import { getConnectorInstances, getSocialConnectorInstanceById } from '@/connectors'; import RequestError from '@/errors/RequestError'; import { createPasscode, sendPasscode, verifyPasscode } from '@/lib/passcode'; import { assignInteractionResults, saveUserFirstConsentedAppId } from '@/lib/session'; @@ -259,7 +259,7 @@ export default function sessionRoutes(router: T, prov ); router.post( - '/session/bind-social-related-user', + '/session/sign-in/bind-social-related-user', koaGuard({ body: object({ connectorId: string() }), }), @@ -577,9 +577,15 @@ export default function sessionRoutes(router: T, prov }); router.get('/sign-in-settings', async (ctx, next) => { - // TODO: Social Connector Details const signInExperience = await findDefaultSignInExperience(); - ctx.body = signInExperience; + const connectorInstances = await getConnectorInstances(); + const instanceMap = new Map( + connectorInstances.map((instance) => [instance.connector.id, instance]) + ); + const socialConnectors = signInExperience.socialSignInConnectorIds.map((id) => { + return { ...instanceMap.get(id)?.metadata, id }; + }); + ctx.body = { ...signInExperience, socialConnectors }; return next(); }); diff --git a/packages/ui/src/__mocks__/logto.tsx b/packages/ui/src/__mocks__/logto.tsx index fc57f0135..13d247f90 100644 --- a/packages/ui/src/__mocks__/logto.tsx +++ b/packages/ui/src/__mocks__/logto.tsx @@ -1,5 +1,11 @@ import { Language } from '@logto/phrases'; -import { BrandingStyle, SignInExperience, SignInMethodState } from '@logto/schemas'; +import { + BrandingStyle, + ConnectorPlatform, + ConnectorType, + SignInExperience, + SignInMethodState, +} from '@logto/schemas'; import { SignInExperienceSettings } from '@/types'; @@ -7,44 +13,86 @@ export const appLogo = 'https://avatars.githubusercontent.com/u/88327661?s=200&v export const appHeadline = 'Build user identity in a modern way'; export const socialConnectors = [ { + id: 'BE8QXN0VsrOH7xdWFDJZ9', target: 'github', + platform: ConnectorPlatform.Web, + type: ConnectorType.Social, logo: 'https://user-images.githubusercontent.com/5717882/156983224-7ea0296b-38fa-419d-9515-67e8a9612e09.png', name: { en: 'Sign in with GitHub', 'zh-CN': '使用 GitHub 登录', }, + description: { + en: 'Sign in with GitHub', + 'zh-CN': '使用 GitHub 登录', + }, + readme: '', + configTemplate: '', }, { + id: '24yt_xIUl5btN4UwvFokt', target: 'alipay', + platform: ConnectorPlatform.Web, + type: ConnectorType.Social, logo: 'https://user-images.githubusercontent.com/5717882/156983224-7ea0296b-38fa-419d-9515-67e8a9612e09.png', name: { en: 'Sign in with Alipay', 'zh-CN': '使用 Alipay 登录', }, + description: { + en: 'Sign in with Alipay', + 'zh-CN': '使用 Alipay 登录', + }, + readme: '', + configTemplate: '', }, { + id: 'E5kb2gdq769qOEYaLg1V5', target: 'wechat', + platform: ConnectorPlatform.Web, + type: ConnectorType.Social, logo: 'https://user-images.githubusercontent.com/5717882/156983224-7ea0296b-38fa-419d-9515-67e8a9612e09.png', name: { en: 'Sign in with WeChat', 'zh-CN': '使用 WeChat 登录', }, + description: { + en: 'Sign in with WeChat', + 'zh-CN': '使用 WeChat 登录', + }, + readme: '', + configTemplate: '', }, { + id: 'xY2YZEweMFPKxphngGHhy', target: 'google', + platform: ConnectorPlatform.Web, + type: ConnectorType.Social, logo: 'https://user-images.githubusercontent.com/5717882/156983224-7ea0296b-38fa-419d-9515-67e8a9612e09.png', name: { en: 'Sign in with Google', 'zh-CN': '使用 Google 登录', }, + description: { en: 'Sign in with Google', 'zh-CN': '使用 Google 登录' }, + readme: '', + configTemplate: '', }, { + id: 'lcXT4o2GSjbV9kg2shZC7', target: 'facebook', + platform: ConnectorPlatform.Web, + type: ConnectorType.Social, logo: 'https://user-images.githubusercontent.com/5717882/156983224-7ea0296b-38fa-419d-9515-67e8a9612e09.png', name: { en: 'Sign in with Meta', 'zh-CN': '使用 Meta 登录', }, + description: { + en: 'Sign in with Meta', + 'zh-CN': '使用 Meta 登录', + }, + readme: '', + configTemplate: '', }, ]; @@ -73,7 +121,7 @@ export const mockSignInExperience: SignInExperience = { sms: SignInMethodState.Secondary, social: SignInMethodState.Secondary, }, - socialSignInConnectorIds: ['github', 'facebook'], + socialSignInConnectorIds: ['BE8QXN0VsrOH7xdWFDJZ9', 'lcXT4o2GSjbV9kg2shZC7'], }; export const mockSignInExperienceSettings: SignInExperienceSettings = { diff --git a/packages/ui/src/apis/settings.ts b/packages/ui/src/apis/settings.ts index 24ed1942b..188c56376 100644 --- a/packages/ui/src/apis/settings.ts +++ b/packages/ui/src/apis/settings.ts @@ -6,6 +6,6 @@ import { SignInExperience } from '@logto/schemas'; import ky from 'ky'; -export const getSignInExperience = async () => { - return ky.get('/api/sign-in-settings').json(); +export const getSignInExperience = async (): Promise => { + return ky.get('/api/sign-in-settings').json(); }; diff --git a/packages/ui/src/containers/SocialSignIn/SecondarySocialSignIn.tsx b/packages/ui/src/containers/SocialSignIn/SecondarySocialSignIn.tsx index cb49c1b78..8e0bfbfd1 100644 --- a/packages/ui/src/containers/SocialSignIn/SecondarySocialSignIn.tsx +++ b/packages/ui/src/containers/SocialSignIn/SecondarySocialSignIn.tsx @@ -32,11 +32,11 @@ const SecondarySocialSignIn = ({ className }: Props) => {
{displayConnectors.map((connector) => ( { - void invokeSocialSignIn(connector.target); + void invokeSocialSignIn(connector.id); }} /> ))} diff --git a/packages/ui/src/hooks/use-social.ts b/packages/ui/src/hooks/use-social.ts index 352d70e31..eac819f36 100644 --- a/packages/ui/src/hooks/use-social.ts +++ b/packages/ui/src/hooks/use-social.ts @@ -90,10 +90,8 @@ const useSocial = () => { // Filter native supported social connectors const socialConnectors = useMemo( () => - (experienceSettings?.socialConnectors ?? []).filter(({ target }) => { - return ( - !isNativeWebview() || getLogtoNativeSdk()?.supportedSocialConnectorIds.includes(target) - ); + (experienceSettings?.socialConnectors ?? []).filter(({ id }) => { + return !isNativeWebview() || getLogtoNativeSdk()?.supportedSocialConnectorIds.includes(id); }), [experienceSettings?.socialConnectors] ); diff --git a/packages/ui/src/pages/Callback/index.tsx b/packages/ui/src/pages/Callback/index.tsx index 146256c12..0395a6739 100644 --- a/packages/ui/src/pages/Callback/index.tsx +++ b/packages/ui/src/pages/Callback/index.tsx @@ -19,9 +19,7 @@ const Callback = () => { const { t } = useTranslation(undefined, { keyPrefix: 'main_flow' }); const connectorLabel = useMemo(() => { - const connector = experienceSettings?.socialConnectors.find( - ({ target }) => target === connectorId - ); + const connector = experienceSettings?.socialConnectors.find(({ id }) => id === connectorId); if (connector) { return ( diff --git a/packages/ui/src/types/index.ts b/packages/ui/src/types/index.ts index 9d8876a4f..bfdfc5aa3 100644 --- a/packages/ui/src/types/index.ts +++ b/packages/ui/src/types/index.ts @@ -8,7 +8,9 @@ export enum SearchParameters { bindWithSocial = 'bw', } -type ConnectorData = Pick; +export interface ConnectorData extends ConnectorMetadata { + id: string; +} export type SignInExperienceSettings = { branding: Branding; diff --git a/packages/ui/src/utils/sign-in-experience.ts b/packages/ui/src/utils/sign-in-experience.ts index 2b386fd6a..c76c43ce0 100644 --- a/packages/ui/src/utils/sign-in-experience.ts +++ b/packages/ui/src/utils/sign-in-experience.ts @@ -3,11 +3,10 @@ * TODO: Remove this once we have a better way to get the sign in experience through SSR */ -import { SignInMethods } from '@logto/schemas'; +import { SignInMethods, SignInExperience } from '@logto/schemas'; -import { socialConnectors } from '@/__mocks__/logto'; import { getSignInExperience } from '@/apis/settings'; -import { SignInMethod, SignInExperienceSettings } from '@/types'; +import { ConnectorData, SignInMethod, SignInExperienceSettings } from '@/types'; const getPrimarySignInMethod = (signInMethods: SignInMethods) => { for (const [key, value] of Object.entries(signInMethods)) { @@ -28,8 +27,11 @@ const getSecondarySignInMethods = (signInMethods: SignInMethods) => return methods; }, []); -const getSignInExperienceSettings = async (): Promise => { - const { branding, languageInfo, termsOfUse, signInMethods } = await getSignInExperience(); +const getSignInExperienceSettings = async < + T extends SignInExperience & { socialConnectors: ConnectorData[] } +>(): Promise => { + const { branding, languageInfo, termsOfUse, signInMethods, socialConnectors } = + await getSignInExperience(); return { branding,