Skip to content

Commit

Permalink
feat(core,console): social connector targets (#851)
Browse files Browse the repository at this point in the history
* feat(core,console): social connector targets

* fix: add test
  • Loading branch information
wangsijie authored May 17, 2022
1 parent 3031e3a commit 127664a
Show file tree
Hide file tree
Showing 19 changed files with 186 additions and 108 deletions.
54 changes: 54 additions & 0 deletions packages/console/src/hooks/use-connector-groups.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { ConnectorDTO } from '@logto/schemas';
import { useMemo } from 'react';
import useSWR from 'swr';

import { RequestError } from '@/hooks/use-api';
import { ConnectorGroup } from '@/types/connector';

// Group connectors by target
const useConnectorGroups = () => {
const { data, ...rest } = useSWR<ConnectorDTO[], RequestError>('/api/connectors');

const groups = useMemo(() => {
if (!data) {
return;
}

return data.reduce<ConnectorGroup[]>((previous, item) => {
const groupIndex = previous.findIndex(({ target }) => target === item.target);

if (groupIndex === -1) {
return [
...previous,
{
name: item.metadata.name,
logo: item.metadata.logo,
target: item.metadata.target,
enabled: item.enabled,
connectors: [item],
},
];
}

return previous.map((group, index) => {
if (index !== groupIndex) {
return group;
}

return {
...group,
connectors: [...group.connectors, item],
// Group is enabled when any of its connectors is enabled.
enabled: group.enabled || item.enabled,
};
});
}, []);
}, [data]);

return {
...rest,
data: groups,
};
};

export default useConnectorGroups;
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { ConnectorDTO } from '@logto/schemas';
import { conditionalString } from '@silverhand/essentials';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import useSWR from 'swr';

import Alert from '@/components/Alert';
import Transfer from '@/components/Transfer';
import UnnamedTrans from '@/components/UnnamedTrans';
import { RequestError } from '@/hooks/use-api';
import useConnectorGroups from '@/hooks/use-connector-groups';

import * as styles from './ConnectorsTransfer.module.scss';

Expand All @@ -18,7 +16,7 @@ type Props = {
};

const ConnectorsTransfer = ({ value, onChange }: Props) => {
const { data, error } = useSWR<ConnectorDTO[], RequestError>('/api/connectors');
const { data, error } = useConnectorGroups();
const isLoading = !data && !error;
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });

Expand All @@ -31,8 +29,8 @@ const ConnectorsTransfer = ({ value, onChange }: Props) => {
}

const datasource = data
? data.map(({ id, metadata: { name }, enabled }) => ({
value: id,
? data.map(({ target, name, enabled }) => ({
value: target,
title: (
<UnnamedTrans
resource={name}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ const SignInMethodsForm = () => {
{primaryMethod === SignInMethodKey.Social && (
<div className={styles.primarySocial}>
<Controller
name="socialSignInConnectorIds"
name="socialSignInConnectorTargets"
control={control}
render={({ field: { value, onChange } }) => (
<ConnectorsTransfer value={value} onChange={onChange} />
Expand All @@ -107,7 +107,7 @@ const SignInMethodsForm = () => {
{social && (
<FormField title="admin_console.sign_in_exp.sign_in_methods.define_social_methods">
<Controller
name="socialSignInConnectorIds"
name="socialSignInConnectorTargets"
control={control}
render={({ field: { value, onChange } }) => (
<ConnectorsTransfer value={value} onChange={onChange} />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { ConnectorDTO, SignInExperience, SignInMethodKey, SignInMethodState } from '@logto/schemas';
import { SignInExperience, SignInMethodKey, SignInMethodState } from '@logto/schemas';
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import useSWR from 'swr';

import UnnamedTrans from '@/components/UnnamedTrans';
import { RequestError } from '@/hooks/use-api';
import useConnectorGroups from '@/hooks/use-connector-groups';

import * as styles from './SaveAlert.module.scss';

Expand All @@ -13,37 +12,33 @@ type Props = {
};

const SignInMethodsPreview = ({ data }: Props) => {
const { data: connectors, error } = useSWR<ConnectorDTO[], RequestError>('/api/connectors');
const { signInMethods, socialSignInConnectorIds } = data;
const { data: groups, error } = useConnectorGroups();
const { signInMethods, socialSignInConnectorTargets } = data;
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });

const connectorNames = useMemo(() => {
if (!connectors) {
if (!groups) {
return null;
}

return socialSignInConnectorIds.map((connectorId) => {
const connector = connectors.find(({ id }) => id === connectorId);
return socialSignInConnectorTargets.map((connectorTarget) => {
const group = groups.find(({ target }) => target === connectorTarget);

if (!connector) {
if (!group) {
return null;
}

return (
<UnnamedTrans
key={connectorId}
className={styles.connector}
resource={connector.metadata.name}
/>
<UnnamedTrans key={connectorTarget} className={styles.connector} resource={group.name} />
);
});
}, [connectors, socialSignInConnectorIds]);
}, [groups, socialSignInConnectorTargets]);

return (
<div>
{!connectors && !error && <div>loading</div>}
{!connectors && error && <div>{error.body?.message ?? error.message}</div>}
{connectors &&
{!groups && !error && <div>loading</div>}
{!groups && error && <div>{error.body?.message ?? error.message}</div>}
{groups &&
Object.values(SignInMethodKey)
.filter((key) => signInMethods[key] !== SignInMethodState.Disabled)
.map((key) => (
Expand Down
8 changes: 6 additions & 2 deletions packages/console/src/pages/SignInExperience/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,15 @@ export const compareSignInMethods = (
before: SignInExperience,
after: SignInExperience
): boolean => {
if (before.socialSignInConnectorIds.length !== after.socialSignInConnectorIds.length) {
if (before.socialSignInConnectorTargets.length !== after.socialSignInConnectorTargets.length) {
return false;
}

if (before.socialSignInConnectorIds.some((id) => !after.socialSignInConnectorIds.includes(id))) {
if (
before.socialSignInConnectorTargets.some(
(target) => !after.socialSignInConnectorTargets.includes(target)
)
) {
return false;
}

Expand Down
6 changes: 6 additions & 0 deletions packages/console/src/types/connector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { ConnectorDTO } from '@logto/schemas';

export type ConnectorGroup = Pick<ConnectorDTO['metadata'], 'name' | 'logo' | 'target'> & {
enabled: boolean;
connectors: ConnectorDTO[];
};
30 changes: 30 additions & 0 deletions packages/core/src/__mocks__/connector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,36 @@ export const mockGithubConnectorInstance = {
},
};

export const mockWechatConnectorInstance = {
connector: {
...mockConnector,
id: 'wechat',
target: 'wechat',
platform: ConnectorPlatform.Web,
},
metadata: {
...mockMetadata,
target: 'wechat',
type: ConnectorType.Social,
platform: ConnectorPlatform.Web,
},
};

export const mockWechatNativeConnectorInstance = {
connector: {
...mockConnector,
id: 'wechat-native',
target: 'wechat',
platform: ConnectorPlatform.Native,
},
metadata: {
...mockMetadata,
target: 'wechat',
type: ConnectorType.Social,
platform: ConnectorPlatform.Native,
},
};

export const mockGoogleConnectorInstance = {
connector: {
...mockConnector,
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/__mocks__/sign-in-experience.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export const mockSignInExperience: SignInExperience = {
sms: SignInMethodState.Disabled,
social: SignInMethodState.Secondary,
},
socialSignInConnectorIds: ['github', 'facebook'],
socialSignInConnectorTargets: ['github', 'facebook', 'wechat'],
};

export const mockBranding: Branding = {
Expand Down
16 changes: 8 additions & 8 deletions packages/core/src/lib/sign-in-experience.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const isEnabled = (state: SignInMethodState) => state !== SignInMethodSta

export const validateSignInMethods = (
signInMethods: SignInMethods,
socialSignInConnectorIds: Optional<string[]>,
socialSignInConnectorTargets: Optional<string[]>,
enabledConnectorInstances: ConnectorInstance[]
) => {
const signInMethodStates = Object.values(signInMethods);
Expand Down Expand Up @@ -60,17 +60,17 @@ export const validateSignInMethods = (
);

assertThat(
socialSignInConnectorIds && socialSignInConnectorIds.length > 0,
socialSignInConnectorTargets && socialSignInConnectorTargets.length > 0,
'sign_in_experiences.empty_social_connectors'
);

const enabledSocialConnectorIds = new Set(
enabledConnectorInstances
.filter((instance) => instance.metadata.type === ConnectorType.Social)
.map((instance) => instance.connector.id)
);
assertThat(
socialSignInConnectorIds.every((id) => enabledSocialConnectorIds.has(id)),
socialSignInConnectorTargets.every((connectorTarget) =>
enabledConnectorInstances.some(
({ metadata: { target, type } }) =>
target === connectorTarget && type === ConnectorType.Social
)
),
'sign_in_experiences.invalid_social_connectors'
);
}
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/queries/sign-in-experience.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@ describe('sign-in-experience query', () => {
branding: JSON.stringify(mockSignInExperience.branding),
termsOfUse: JSON.stringify(mockSignInExperience.termsOfUse),
languageInfo: JSON.stringify(mockSignInExperience.languageInfo),
signInMethods: JSON.stringify(mockSignInExperience.socialSignInConnectorIds),
socialSignInConnectorIds: JSON.stringify(mockSignInExperience.socialSignInConnectorIds),
signInMethods: JSON.stringify(mockSignInExperience.socialSignInConnectorTargets),
socialSignInConnectorTargets: JSON.stringify(mockSignInExperience.socialSignInConnectorTargets),
};

it('findDefaultSignInExperience', async () => {
/* eslint-disable sql/no-unsafe-query */
const expectSql = `
select "id", "branding", "language_info", "terms_of_use", "sign_in_methods", "social_sign_in_connector_ids"
select "id", "branding", "language_info", "terms_of_use", "sign_in_methods", "social_sign_in_connector_targets"
from "sign_in_experiences"
where "id" = $1
`;
Expand Down
12 changes: 12 additions & 0 deletions packages/core/src/routes/session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
mockFacebookConnectorInstance,
mockGithubConnectorInstance,
mockGoogleConnectorInstance,
mockWechatConnectorInstance,
mockWechatNativeConnectorInstance,
} from '@/__mocks__';
import { ConnectorType } from '@/connectors/types';
import RequestError from '@/errors/RequestError';
Expand Down Expand Up @@ -111,6 +113,8 @@ const getConnectorInstances = jest.fn(async () => [
mockFacebookConnectorInstance,
mockGithubConnectorInstance,
mockGoogleConnectorInstance,
mockWechatConnectorInstance,
mockWechatNativeConnectorInstance,
]);
jest.mock('@/connectors', () => ({
getSocialConnectorInstanceById: async (connectorId: string) => {
Expand Down Expand Up @@ -925,6 +929,14 @@ describe('sessionRoutes', () => {
...mockFacebookConnectorInstance.metadata,
id: mockFacebookConnectorInstance.connector.id,
},
{
...mockWechatConnectorInstance.metadata,
id: mockWechatConnectorInstance.connector.id,
},
{
...mockWechatNativeConnectorInstance.metadata,
id: mockWechatNativeConnectorInstance.connector.id,
},
],
})
);
Expand Down
19 changes: 13 additions & 6 deletions packages/core/src/routes/session.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable max-lines */
import path from 'path';

import { ConnectorMetadata } from '@logto/connector-types';
import { LogtoErrorCode } from '@logto/phrases';
import { PasscodeType, userInfoSelectFields } from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
Expand Down Expand Up @@ -581,12 +582,18 @@ export default function sessionRoutes<T extends AnonymousRouter>(router: T, prov
router.get('/sign-in-settings', async (ctx, next) => {
const signInExperience = await findDefaultSignInExperience();
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 };
});
const socialConnectors = signInExperience.socialSignInConnectorTargets.reduce<
Array<ConnectorMetadata & { id: string }>
>((previous, connectorTarget) => {
const connectors = connectorInstances.filter(
({ metadata: { target } }) => target === connectorTarget
);

return [
...previous,
...connectors.map(({ metadata, connector: { id } }) => ({ ...metadata, id })),
];
}, []);
ctx.body = { ...signInExperience, socialConnectors };

return next();
Expand Down
Loading

0 comments on commit 127664a

Please sign in to comment.