mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
refactor(ui): add ui social connector filters (#822)
* refactor(ui): add ui social connector filters add ui social connector filters * refactor(ui): add some comments add some comments
This commit is contained in:
parent
ab6c124620
commit
907a63b52a
5 changed files with 186 additions and 65 deletions
|
@ -6,7 +6,7 @@ import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
|
|||
import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider';
|
||||
import { socialConnectors, mockSignInExperienceSettings } from '@/__mocks__/logto';
|
||||
import * as socialSignInApi from '@/apis/social';
|
||||
import { generateState, storeState } from '@/hooks/use-social';
|
||||
import { generateState, storeState } from '@/hooks/utils';
|
||||
|
||||
import SecondarySocialSignIn, { defaultSize } from './SecondarySocialSignIn';
|
||||
|
||||
|
|
|
@ -3,67 +3,20 @@ import { useTranslation } from 'react-i18next';
|
|||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { invokeSocialSignIn, signInWithSocial } from '@/apis/social';
|
||||
import { generateRandomString, parseQueryParameters } from '@/utils';
|
||||
import { parseQueryParameters } from '@/utils';
|
||||
|
||||
import useApi, { ErrorHandlers } from './use-api';
|
||||
import { PageContext } from './use-page-context';
|
||||
import useTerms from './use-terms';
|
||||
|
||||
/**
|
||||
* Social Connector State Utility Methods
|
||||
* @param state
|
||||
* @param state.uuid - unique id
|
||||
* @param state.platform - platform
|
||||
* @param state.callbackLink - callback uri scheme
|
||||
*/
|
||||
|
||||
type State = {
|
||||
uuid: string;
|
||||
platform: 'web' | 'ios' | 'android';
|
||||
callbackLink?: string;
|
||||
};
|
||||
|
||||
const storageKeyPrefix = 'social_auth_state';
|
||||
|
||||
const getLogtoNativeSdk = () => {
|
||||
if (typeof logtoNativeSdk !== 'undefined') {
|
||||
return logtoNativeSdk;
|
||||
}
|
||||
};
|
||||
|
||||
export const generateState = () => {
|
||||
const uuid = generateRandomString();
|
||||
const platform = getLogtoNativeSdk()?.platform ?? 'web';
|
||||
const callbackLink = getLogtoNativeSdk()?.callbackLink;
|
||||
|
||||
const state: State = { uuid, platform, callbackLink };
|
||||
|
||||
return btoa(JSON.stringify(state));
|
||||
};
|
||||
|
||||
export const decodeState = (state: string) => {
|
||||
try {
|
||||
return JSON.parse(atob(state)) as State;
|
||||
} catch {}
|
||||
};
|
||||
|
||||
export const stateValidation = (state: string, connectorId: string) => {
|
||||
const stateStorage = sessionStorage.getItem(`${storageKeyPrefix}:${connectorId}`);
|
||||
|
||||
return stateStorage === state;
|
||||
};
|
||||
|
||||
export const storeState = (state: string, connectorId: string) => {
|
||||
sessionStorage.setItem(`${storageKeyPrefix}:${connectorId}`, state);
|
||||
};
|
||||
|
||||
/* ============================================================================ */
|
||||
|
||||
const isNativeWebview = () => {
|
||||
const platform = getLogtoNativeSdk()?.platform ?? '';
|
||||
|
||||
return ['ios', 'android'].includes(platform);
|
||||
};
|
||||
import {
|
||||
getLogtoNativeSdk,
|
||||
isNativeWebview,
|
||||
generateState,
|
||||
decodeState,
|
||||
stateValidation,
|
||||
storeState,
|
||||
filterSocialConnectors,
|
||||
} from './utils';
|
||||
|
||||
const useSocial = () => {
|
||||
const { setToast, experienceSettings } = useContext(PageContext);
|
||||
|
@ -87,13 +40,9 @@ const useSocial = () => {
|
|||
[navigate, parameters.connector]
|
||||
);
|
||||
|
||||
// Filter native supported social connectors
|
||||
const socialConnectors = useMemo(
|
||||
() =>
|
||||
(experienceSettings?.socialConnectors ?? []).filter(({ id }) => {
|
||||
return !isNativeWebview() || getLogtoNativeSdk()?.supportedSocialConnectorIds.includes(id);
|
||||
}),
|
||||
[experienceSettings?.socialConnectors]
|
||||
() => filterSocialConnectors(experienceSettings?.socialConnectors),
|
||||
[experienceSettings]
|
||||
);
|
||||
|
||||
const { run: asyncInvokeSocialSignIn } = useApi(invokeSocialSignIn);
|
||||
|
|
45
packages/ui/src/hooks/utils.test.ts
Normal file
45
packages/ui/src/hooks/utils.test.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
import { ConnectorData } from '@/types';
|
||||
|
||||
import { filterSocialConnectors } from './utils';
|
||||
|
||||
const mockConnectors = [
|
||||
{ platform: 'Web', target: 'facebook' },
|
||||
{ platform: 'Web', target: 'google' },
|
||||
{ platform: 'Universal', target: 'facebook' },
|
||||
{ platform: 'Universal', target: 'WeChat' },
|
||||
{ platform: 'Native', target: 'WeChat' },
|
||||
{ platform: 'Native', target: 'Alipay' },
|
||||
] as ConnectorData[];
|
||||
|
||||
describe('filterSocialConnectors', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('undefined input should return empty list', () => {
|
||||
expect(filterSocialConnectors()).toEqual([]);
|
||||
});
|
||||
|
||||
it('filter web connectors', () => {
|
||||
expect(filterSocialConnectors(mockConnectors)).toEqual([
|
||||
{ platform: 'Web', target: 'facebook' },
|
||||
{ platform: 'Web', target: 'google' },
|
||||
{ platform: 'Universal', target: 'WeChat' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('filter Native connectors', () => {
|
||||
/* eslint-disable @silverhand/fp/no-mutation */
|
||||
// @ts-expect-error mock global object
|
||||
globalThis.logtoNativeSdk = {
|
||||
platform: 'ios',
|
||||
supportedSocialConnectorIds: ['Web', 'WeChat'],
|
||||
};
|
||||
/* eslint-enable @silverhand/fp/no-mutation */
|
||||
|
||||
expect(filterSocialConnectors(mockConnectors)).toEqual([
|
||||
{ platform: 'Universal', target: 'facebook' },
|
||||
{ platform: 'Native', target: 'WeChat' },
|
||||
]);
|
||||
});
|
||||
});
|
120
packages/ui/src/hooks/utils.ts
Normal file
120
packages/ui/src/hooks/utils.ts
Normal file
|
@ -0,0 +1,120 @@
|
|||
import { ConnectorData } from '@/types';
|
||||
import { generateRandomString } from '@/utils';
|
||||
|
||||
/**
|
||||
* Native SDK Utility Methods
|
||||
*/
|
||||
export const getLogtoNativeSdk = () => {
|
||||
if (typeof logtoNativeSdk !== 'undefined') {
|
||||
return logtoNativeSdk;
|
||||
}
|
||||
};
|
||||
|
||||
export const isNativeWebview = () => {
|
||||
const platform = getLogtoNativeSdk()?.platform ?? '';
|
||||
|
||||
return ['ios', 'android'].includes(platform);
|
||||
};
|
||||
|
||||
/**
|
||||
* Social Connector State Utility Methods
|
||||
* @param state
|
||||
* @param state.uuid - unique id
|
||||
* @param state.platform - platform
|
||||
* @param state.callbackLink - callback uri scheme
|
||||
*/
|
||||
|
||||
type State = {
|
||||
uuid: string;
|
||||
platform: 'web' | 'ios' | 'android';
|
||||
callbackLink?: string;
|
||||
};
|
||||
|
||||
const storageKeyPrefix = 'social_auth_state';
|
||||
|
||||
export const generateState = () => {
|
||||
const uuid = generateRandomString();
|
||||
const platform = getLogtoNativeSdk()?.platform ?? 'web';
|
||||
const callbackLink = getLogtoNativeSdk()?.callbackLink;
|
||||
|
||||
const state: State = { uuid, platform, callbackLink };
|
||||
|
||||
return btoa(JSON.stringify(state));
|
||||
};
|
||||
|
||||
export const decodeState = (state: string) => {
|
||||
try {
|
||||
return JSON.parse(atob(state)) as State;
|
||||
} catch {}
|
||||
};
|
||||
|
||||
export const stateValidation = (state: string, connectorId: string) => {
|
||||
const stateStorage = sessionStorage.getItem(`${storageKeyPrefix}:${connectorId}`);
|
||||
|
||||
return stateStorage === state;
|
||||
};
|
||||
|
||||
export const storeState = (state: string, connectorId: string) => {
|
||||
sessionStorage.setItem(`${storageKeyPrefix}:${connectorId}`, state);
|
||||
};
|
||||
|
||||
/**
|
||||
* Social Connectors Filter Utility Methods
|
||||
*/
|
||||
export const filterSocialConnectors = (socialConnectors?: ConnectorData[]) => {
|
||||
if (!socialConnectors) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const connectorMap = new Map<string, ConnectorData>();
|
||||
|
||||
if (!isNativeWebview()) {
|
||||
for (const connector of socialConnectors) {
|
||||
const { platform, target } = connector;
|
||||
|
||||
if (platform === 'Native') {
|
||||
continue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts both web and universal platform connectors.
|
||||
* Insert universal connectors only if there is no web platform connector provided with the same target.
|
||||
* Web platform has higher priority.
|
||||
**/
|
||||
if (platform === 'Web' || !connectorMap.get(target)) {
|
||||
connectorMap.set(target, connector);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(connectorMap.values());
|
||||
}
|
||||
|
||||
for (const connector of socialConnectors) {
|
||||
const { platform, target } = connector;
|
||||
|
||||
if (platform === 'Web') {
|
||||
continue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts both Native and universal platform connectors.
|
||||
* Insert universal connectors only if there is no Native platform connector provided with the same target.
|
||||
* Native platform has higher priority.
|
||||
**/
|
||||
if (
|
||||
platform === 'Native' &&
|
||||
getLogtoNativeSdk()?.supportedSocialConnectorIds.includes(target)
|
||||
) {
|
||||
connectorMap.set(target, connector);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (platform === 'Universal' && !connectorMap.get(target)) {
|
||||
connectorMap.set(target, connector);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(connectorMap.values());
|
||||
};
|
|
@ -1,4 +1,10 @@
|
|||
import { Branding, LanguageInfo, TermsOfUse, ConnectorMetadata } from '@logto/schemas';
|
||||
import {
|
||||
Branding,
|
||||
LanguageInfo,
|
||||
TermsOfUse,
|
||||
ConnectorMetadata,
|
||||
ConnectorPlatform,
|
||||
} from '@logto/schemas';
|
||||
|
||||
export type UserFlow = 'sign-in' | 'register';
|
||||
export type SignInMethod = 'username' | 'email' | 'sms' | 'social';
|
||||
|
@ -10,6 +16,7 @@ export enum SearchParameters {
|
|||
|
||||
export interface ConnectorData extends ConnectorMetadata {
|
||||
id: string;
|
||||
platform: ConnectorPlatform;
|
||||
}
|
||||
|
||||
export type SignInExperienceSettings = {
|
||||
|
|
Loading…
Reference in a new issue