mirror of
https://github.com/logto-io/logto.git
synced 2025-01-06 20:40:08 -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 SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider';
|
||||||
import { socialConnectors, mockSignInExperienceSettings } from '@/__mocks__/logto';
|
import { socialConnectors, mockSignInExperienceSettings } from '@/__mocks__/logto';
|
||||||
import * as socialSignInApi from '@/apis/social';
|
import * as socialSignInApi from '@/apis/social';
|
||||||
import { generateState, storeState } from '@/hooks/use-social';
|
import { generateState, storeState } from '@/hooks/utils';
|
||||||
|
|
||||||
import SecondarySocialSignIn, { defaultSize } from './SecondarySocialSignIn';
|
import SecondarySocialSignIn, { defaultSize } from './SecondarySocialSignIn';
|
||||||
|
|
||||||
|
|
|
@ -3,67 +3,20 @@ import { useTranslation } from 'react-i18next';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { invokeSocialSignIn, signInWithSocial } from '@/apis/social';
|
import { invokeSocialSignIn, signInWithSocial } from '@/apis/social';
|
||||||
import { generateRandomString, parseQueryParameters } from '@/utils';
|
import { parseQueryParameters } from '@/utils';
|
||||||
|
|
||||||
import useApi, { ErrorHandlers } from './use-api';
|
import useApi, { ErrorHandlers } from './use-api';
|
||||||
import { PageContext } from './use-page-context';
|
import { PageContext } from './use-page-context';
|
||||||
import useTerms from './use-terms';
|
import useTerms from './use-terms';
|
||||||
|
import {
|
||||||
/**
|
getLogtoNativeSdk,
|
||||||
* Social Connector State Utility Methods
|
isNativeWebview,
|
||||||
* @param state
|
generateState,
|
||||||
* @param state.uuid - unique id
|
decodeState,
|
||||||
* @param state.platform - platform
|
stateValidation,
|
||||||
* @param state.callbackLink - callback uri scheme
|
storeState,
|
||||||
*/
|
filterSocialConnectors,
|
||||||
|
} from './utils';
|
||||||
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
const useSocial = () => {
|
const useSocial = () => {
|
||||||
const { setToast, experienceSettings } = useContext(PageContext);
|
const { setToast, experienceSettings } = useContext(PageContext);
|
||||||
|
@ -87,13 +40,9 @@ const useSocial = () => {
|
||||||
[navigate, parameters.connector]
|
[navigate, parameters.connector]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Filter native supported social connectors
|
|
||||||
const socialConnectors = useMemo(
|
const socialConnectors = useMemo(
|
||||||
() =>
|
() => filterSocialConnectors(experienceSettings?.socialConnectors),
|
||||||
(experienceSettings?.socialConnectors ?? []).filter(({ id }) => {
|
[experienceSettings]
|
||||||
return !isNativeWebview() || getLogtoNativeSdk()?.supportedSocialConnectorIds.includes(id);
|
|
||||||
}),
|
|
||||||
[experienceSettings?.socialConnectors]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const { run: asyncInvokeSocialSignIn } = useApi(invokeSocialSignIn);
|
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 UserFlow = 'sign-in' | 'register';
|
||||||
export type SignInMethod = 'username' | 'email' | 'sms' | 'social';
|
export type SignInMethod = 'username' | 'email' | 'sms' | 'social';
|
||||||
|
@ -10,6 +16,7 @@ export enum SearchParameters {
|
||||||
|
|
||||||
export interface ConnectorData extends ConnectorMetadata {
|
export interface ConnectorData extends ConnectorMetadata {
|
||||||
id: string;
|
id: string;
|
||||||
|
platform: ConnectorPlatform;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SignInExperienceSettings = {
|
export type SignInExperienceSettings = {
|
||||||
|
|
Loading…
Reference in a new issue