From d0d507ab79a90c0463a014ba694ac30ca10b0e45 Mon Sep 17 00:00:00 2001 From: simeng-li Date: Mon, 30 May 2022 09:42:59 +0800 Subject: [PATCH] refactor(ui): add social landing page (#972) * refactor(ui): add social landing page add social landing page * fix(ui): add ui coverage add ui converage job * fix(ui): fix ci fix ci * test(ui): add ut cases for parseQueryParameters add unit test case for util parseQueryParameter * refactor(ui): pass whole connector into the invokeSocialSignInHandler pass whole connector object into the invokeSocialSignInHandler * fix(ui): cr fix cr fix --- packages/phrases/src/locales/en.ts | 4 +- packages/phrases/src/locales/zh-cn.ts | 6 +- packages/ui/package.json | 1 + packages/ui/src/App.tsx | 7 ++- .../SocialLanding/index.module.scss | 22 +++++++ .../ui/src/containers/SocialLanding/index.tsx | 28 +++++++++ .../SocialSignInDropdown/index.tsx | 5 +- .../SocialSignInIconList/index.tsx | 2 +- .../SocialSignIn/SocialSignInList/index.tsx | 2 +- .../src/hooks/use-social-callback-handler.ts | 62 ++++++++----------- .../src/hooks/use-social-landing-handler.ts | 34 ++++++++++ packages/ui/src/hooks/use-social.ts | 38 +++++++++--- packages/ui/src/hooks/utils.test.ts | 30 ++++++++- packages/ui/src/hooks/utils.ts | 55 +++++++++------- .../ui/src/pages/Callback/index.module.scss | 24 +------ packages/ui/src/pages/Callback/index.tsx | 48 +++++++------- .../src/pages/SocialLanding/index.module.scss | 12 ++++ .../ui/src/pages/SocialLanding/index.test.tsx | 49 +++++++++++++++ packages/ui/src/pages/SocialLanding/index.tsx | 35 +++++++++++ packages/ui/src/types/index.ts | 4 +- packages/ui/src/utils/index.test.ts | 6 ++ 21 files changed, 343 insertions(+), 131 deletions(-) create mode 100644 packages/ui/src/containers/SocialLanding/index.module.scss create mode 100644 packages/ui/src/containers/SocialLanding/index.tsx create mode 100644 packages/ui/src/hooks/use-social-landing-handler.ts create mode 100644 packages/ui/src/pages/SocialLanding/index.module.scss create mode 100644 packages/ui/src/pages/SocialLanding/index.test.tsx create mode 100644 packages/ui/src/pages/SocialLanding/index.tsx diff --git a/packages/phrases/src/locales/en.ts b/packages/phrases/src/locales/en.ts index 32b64b1bc..25bc26e84 100644 --- a/packages/phrases/src/locales/en.ts +++ b/packages/phrases/src/locales/en.ts @@ -81,12 +81,12 @@ const translation = { username_valid_charset: 'Username should only contain letters, numbers, or underscore.', invalid_email: 'The email is invalid', invalid_phone: 'The phone number is invalid', - invalid_connector_auth: 'The authorization is invalid', - missing_auth_data: 'The authorization code or state is missing', password_min_length: 'Password requires a minimum of {{min}} characters.', passwords_do_not_match: 'Passwords do not match.', agree_terms_required: 'You must agree to the Terms of Use before continuing.', invalid_passcode: 'The passcode is invalid.', + invalid_connector_auth: 'The authorization is invalid.', + invalid_connector_request: 'The connector data is invalid.', request: 'Request error {{message}}', unknown: 'Unknown error, please try again later.', invalid_session: 'Session not found. Please go back and sign in again.', diff --git a/packages/phrases/src/locales/zh-cn.ts b/packages/phrases/src/locales/zh-cn.ts index 32455249b..e444ab5cc 100644 --- a/packages/phrases/src/locales/zh-cn.ts +++ b/packages/phrases/src/locales/zh-cn.ts @@ -81,13 +81,13 @@ const translation = { username_valid_charset: '用户名只能包含英文字母、数字或下划线。', invalid_email: '无效的邮箱。', invalid_phone: '无效的手机号。', - invalid_connector_auth: '登录失败', - missing_auth_data: '未找到有效的登录信息', password_min_length: '密码最少需要{{min}}个字符。', passwords_do_not_match: '密码不匹配。', agree_terms_required: '你需要同意使用条款以继续。', invalid_passcode: '无效的验证码。', - request: '请求错误:{{ message }}', + invalid_connector_auth: '登录失败。', + invalid_connector_request: '无效的登录请求。', + request: '请求错误:{{ message }}。', unknown: '未知错误,请稍后重试。', invalid_session: '未找到有效的会话,请重新登录。', }, diff --git a/packages/ui/package.json b/packages/ui/package.json index 42a9febca..9d2569e6c 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -13,6 +13,7 @@ "lint": "eslint --ext .ts --ext .tsx src", "lint:report": "pnpm lint --format json --output-file report.json", "stylelint": "stylelint \"src/**/*.scss\"", + "test:coverage": "jest --coverage --silent", "test": "jest" }, "devDependencies": { diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx index 60a846e40..29757fdff 100644 --- a/packages/ui/src/App.tsx +++ b/packages/ui/src/App.tsx @@ -14,6 +14,7 @@ import Passcode from './pages/Passcode'; import Register from './pages/Register'; import SecondarySignIn from './pages/SecondarySignIn'; import SignIn from './pages/SignIn'; +import SocialLanding from './pages/SocialLanding'; import SocialRegister from './pages/SocialRegister'; import SocialSignInCallback from './pages/SocialSignInCallback'; import getSignInExperienceSettings from './utils/sign-in-experience'; @@ -55,12 +56,16 @@ const App = () => { } /> } /> } /> - } /> } /> } /> } /> + + {/* social sign-in pages */} + } /> } /> } /> + } /> + } /> img { + width: 96px; + height: 96px; + @include _.image-align-center; +} + +.message { + margin-top: _.unit(2); +} + +.container { + @include _.flex-column; +} + + +:global(body.desktop) { + .message { + margin-top: _.unit(1); + } +} diff --git a/packages/ui/src/containers/SocialLanding/index.tsx b/packages/ui/src/containers/SocialLanding/index.tsx new file mode 100644 index 000000000..b4b8ef6a4 --- /dev/null +++ b/packages/ui/src/containers/SocialLanding/index.tsx @@ -0,0 +1,28 @@ +import classNames from 'classnames'; +import React, { useContext } from 'react'; + +import { PageContext } from '@/hooks/use-page-context'; + +import * as styles from './index.module.scss'; + +type Props = { + className?: string; + connectorId: string; + message?: string; +}; + +const SocialLanding = ({ className, connectorId, message }: Props) => { + const { experienceSettings } = useContext(PageContext); + const connector = experienceSettings?.socialConnectors.find(({ id }) => id === connectorId); + + return ( +
+
+ {connector?.logo ? : connectorId} +
+
{message}
+
+ ); +}; + +export default SocialLanding; diff --git a/packages/ui/src/containers/SocialSignIn/SocialSignInDropdown/index.tsx b/packages/ui/src/containers/SocialSignIn/SocialSignInDropdown/index.tsx index 352f5c205..a7b7abbed 100644 --- a/packages/ui/src/containers/SocialSignIn/SocialSignInDropdown/index.tsx +++ b/packages/ui/src/containers/SocialSignIn/SocialSignInDropdown/index.tsx @@ -45,7 +45,8 @@ const SocialSignInDropdown = ({ isOpen, onClose, connectors, anchorRef }: Props) setContentStyle(undefined); }} > - {connectors.map(({ id, name, logo, target }) => { + {connectors.map((connector) => { + const { id, name, logo } = connector; const languageKey = Object.keys(name).find((key) => key === language) ?? 'en'; const localName = name[languageKey as Language]; @@ -53,7 +54,7 @@ const SocialSignInDropdown = ({ isOpen, onClose, connectors, anchorRef }: Props) { - void invokeSocialSignIn(id, target, onClose); + void invokeSocialSignIn(connector, onClose); }} > {id} diff --git a/packages/ui/src/containers/SocialSignIn/SocialSignInIconList/index.tsx b/packages/ui/src/containers/SocialSignIn/SocialSignInIconList/index.tsx index c596c5699..04a4c443a 100644 --- a/packages/ui/src/containers/SocialSignIn/SocialSignInIconList/index.tsx +++ b/packages/ui/src/containers/SocialSignIn/SocialSignInIconList/index.tsx @@ -34,7 +34,7 @@ const SocialSignInIconList = ({ className={styles.socialButton} connector={connector} onClick={() => { - void invokeSocialSignIn(connector.id, connector.target); + void invokeSocialSignIn(connector); }} /> ))} diff --git a/packages/ui/src/containers/SocialSignIn/SocialSignInList/index.tsx b/packages/ui/src/containers/SocialSignIn/SocialSignInList/index.tsx index 4f18b83f1..13dc1fa27 100644 --- a/packages/ui/src/containers/SocialSignIn/SocialSignInList/index.tsx +++ b/packages/ui/src/containers/SocialSignIn/SocialSignInList/index.tsx @@ -42,7 +42,7 @@ const SocialSignInList = ({ className={styles.socialLinkButton} connector={connector} onClick={() => { - void invokeSocialSignIn(connector.id, connector.target, onSocialSignInCallback); + void invokeSocialSignIn(connector, onSocialSignInCallback); }} /> ))} diff --git a/packages/ui/src/hooks/use-social-callback-handler.ts b/packages/ui/src/hooks/use-social-callback-handler.ts index c96f4d509..95eb03c5e 100644 --- a/packages/ui/src/hooks/use-social-callback-handler.ts +++ b/packages/ui/src/hooks/use-social-callback-handler.ts @@ -1,48 +1,46 @@ import { useCallback, useContext } from 'react'; import { useTranslation } from 'react-i18next'; -import { useParams, useNavigate } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; import { parseQueryParameters } from '@/utils'; import { PageContext } from './use-page-context'; -import { decodeState } from './utils'; +import { getCallbackLinkFromStorage } from './utils'; const useSocialCallbackHandler = () => { const { setToast } = useContext(PageContext); const { t } = useTranslation(undefined, { keyPrefix: 'main_flow' }); - const parameters = useParams(); const navigate = useNavigate(); - const socialCallbackHandler = useCallback(() => { - const data = window.location.search || '?' + window.location.hash.slice(1); - const { state, error, error_description } = parseQueryParameters(data); - const connectorId = parameters.connector; + const socialCallbackHandler = useCallback( + (connectorId: string) => { + const data = window.location.search || '?' + window.location.hash.slice(1); + const { state, error, error_description } = parseQueryParameters(data); - // Connector auth error - if (error) { - setToast(`${error}${error_description ? `: ${error_description}` : ''}`); - } + // Connector auth error + if (error) { + setToast(`${error}${error_description ? `: ${error_description}` : ''}`); - // Connector auth missing state - if (!state || !connectorId) { - setToast(t('error.missing_auth_data')); + return; + } - return; - } + // Connector auth missing state + if (!state || !connectorId) { + setToast(t('error.invalid_connector_auth')); - const decodedState = decodeState(state); + return; + } - // Invalid state - if (!decodedState) { - setToast(t('error.missing_auth_data')); + // Get native callback link from storage + const callbackLink = getCallbackLinkFromStorage(connectorId); - return; - } + if (callbackLink) { + window.location.replace(new URL(`${callbackLink}${window.location.search}`)); - const { platform, callbackLink } = decodedState; + return; + } - // Web/Mobile-Web redirect to sign-in/callback page to login - if (platform === 'web') { + // Web flow navigate( { pathname: `/sign-in/callback/${connectorId}`, @@ -52,17 +50,9 @@ const useSocialCallbackHandler = () => { replace: true, } ); - - return; - } - - // Native Webview redirect to native app - if (!callbackLink) { - throw new Error('CallbackLink is empty'); - } - - window.location.assign(new URL(`${callbackLink}${data}`)); - }, [navigate, parameters.connector, setToast, t]); + }, + [navigate, setToast, t] + ); return socialCallbackHandler; }; diff --git a/packages/ui/src/hooks/use-social-landing-handler.ts b/packages/ui/src/hooks/use-social-landing-handler.ts new file mode 100644 index 000000000..5f84f0a48 --- /dev/null +++ b/packages/ui/src/hooks/use-social-landing-handler.ts @@ -0,0 +1,34 @@ +import { useEffect, useContext } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { SearchParameters } from '@/types'; +import { getSearchParameters } from '@/utils'; + +import { PageContext } from './use-page-context'; +import { storeCallbackLink } from './utils'; + +const useSocialLandingHandler = (connectorId?: string) => { + const { setToast } = useContext(PageContext); + const { t } = useTranslation(undefined, { keyPrefix: 'main_flow' }); + const { search } = window.location; + + useEffect(() => { + const redirectUri = getSearchParameters(search, SearchParameters.redirectTo); + + if (!redirectUri || !connectorId) { + setToast(t('error.invalid_connector_request')); + + return; + } + + const nativeCallbackLink = getSearchParameters(search, SearchParameters.nativeCallbackLink); + + if (nativeCallbackLink) { + storeCallbackLink(connectorId, nativeCallbackLink); + } + + window.location.replace(redirectUri); + }, [connectorId, search, setToast, t]); +}; + +export default useSocialLandingHandler; diff --git a/packages/ui/src/hooks/use-social.ts b/packages/ui/src/hooks/use-social.ts index a55a53fe5..bee545fb5 100644 --- a/packages/ui/src/hooks/use-social.ts +++ b/packages/ui/src/hooks/use-social.ts @@ -1,11 +1,18 @@ import { useCallback, useContext } from 'react'; import { invokeSocialSignIn } from '@/apis/social'; +import { ConnectorData } from '@/types'; import useApi from './use-api'; import { PageContext } from './use-page-context'; import useTerms from './use-terms'; -import { getLogtoNativeSdk, isNativeWebview, generateState, storeState } from './utils'; +import { + getLogtoNativeSdk, + isNativeWebview, + generateState, + storeState, + buildSocialLandingUri, +} from './utils'; const useSocial = () => { const { experienceSettings } = useContext(PageContext); @@ -13,21 +20,35 @@ const useSocial = () => { const { run: asyncInvokeSocialSignIn } = useApi(invokeSocialSignIn); + const nativeSignInHandler = useCallback((redirectTo: string, connector: ConnectorData) => { + const { id: connectorId, platform } = connector; + + const redirectUri = + platform === 'Universal' + ? buildSocialLandingUri(`/social-landing/${connectorId}`, redirectTo).toString() + : redirectTo; + + getLogtoNativeSdk()?.getPostMessage()({ + callbackUri: `${window.location.origin}/sign-in/callback/${connectorId}`, + redirectTo: redirectUri, + }); + }, []); + const invokeSocialSignInHandler = useCallback( - async (connectorId: string, target: string, callback?: () => void) => { + async (connector: ConnectorData, callback?: () => void) => { if (!(await termsValidation())) { return; } + const { id: connectorId } = connector; + const state = generateState(); storeState(state, connectorId); - const { origin } = window.location; - const result = await asyncInvokeSocialSignIn( connectorId, state, - `${origin}/callback/${connectorId}` + `${window.location.origin}/callback/${connectorId}` ); if (!result?.redirectTo) { @@ -39,10 +60,7 @@ const useSocial = () => { // Invoke Native Social Sign In flow if (isNativeWebview()) { - getLogtoNativeSdk()?.getPostMessage()({ - callbackUri: `${origin}/sign-in/callback/${connectorId}`, - redirectTo: result.redirectTo, - }); + nativeSignInHandler(result.redirectTo, connector); return; } @@ -50,7 +68,7 @@ const useSocial = () => { // Invoke Web Social Sign In flow window.location.assign(result.redirectTo); }, - [asyncInvokeSocialSignIn, termsValidation] + [asyncInvokeSocialSignIn, nativeSignInHandler, termsValidation] ); return { diff --git a/packages/ui/src/hooks/utils.test.ts b/packages/ui/src/hooks/utils.test.ts index 9afe11c6a..6a3481e45 100644 --- a/packages/ui/src/hooks/utils.test.ts +++ b/packages/ui/src/hooks/utils.test.ts @@ -1,6 +1,10 @@ -import { ConnectorData } from '@/types'; +import { ConnectorData, SearchParameters } from '@/types'; -import { filterSocialConnectors, filterPreviewSocialConnectors } from './utils'; +import { + filterSocialConnectors, + filterPreviewSocialConnectors, + buildSocialLandingUri, +} from './utils'; const mockConnectors = [ { platform: 'Web', target: 'facebook' }, @@ -90,3 +94,25 @@ describe('filterPreviewSocialConnectors', () => { ]); }); }); + +describe('buildSocialLandingUri', () => { + it('buildSocialLandingUri', () => { + /* eslint-disable @silverhand/fp/no-mutation */ + // @ts-expect-error mock global object + globalThis.logtoNativeSdk = { + platform: 'ios', + callbackLink: 'logto://callback', + }; + /* eslint-enable @silverhand/fp/no-mutation */ + + const redirectUri = 'https://www.example.com/callback'; + const socialLandingPath = '/social-landing'; + const callbackUri = buildSocialLandingUri(socialLandingPath, redirectUri); + + expect(callbackUri.pathname).toEqual(socialLandingPath); + expect(callbackUri.searchParams.get(SearchParameters.redirectTo)).toEqual(redirectUri); + expect(callbackUri.searchParams.get(SearchParameters.nativeCallbackLink)).toEqual( + 'logto://callback' + ); + }); +}); diff --git a/packages/ui/src/hooks/utils.ts b/packages/ui/src/hooks/utils.ts index 4878e67f3..922b82321 100644 --- a/packages/ui/src/hooks/utils.ts +++ b/packages/ui/src/hooks/utils.ts @@ -1,4 +1,4 @@ -import { ConnectorData, Platform } from '@/types'; +import { ConnectorData, Platform, SearchParameters } from '@/types'; import { generateRandomString } from '@/utils'; /** @@ -18,44 +18,51 @@ export const isNativeWebview = () => { /** * 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 storageStateKeyPrefix = '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)); + return uuid; }; -export const decodeState = (state: string) => { - try { - return JSON.parse(atob(state)) as State; - } catch {} +export const storeState = (state: string, connectorId: string) => { + sessionStorage.setItem(`${storageStateKeyPrefix}:${connectorId}`, state); }; export const stateValidation = (state: string, connectorId: string) => { - const stateStorage = sessionStorage.getItem(`${storageKeyPrefix}:${connectorId}`); + const stateStorage = sessionStorage.getItem(`${storageStateKeyPrefix}:${connectorId}`); return stateStorage === state; }; -export const storeState = (state: string, connectorId: string) => { - sessionStorage.setItem(`${storageKeyPrefix}:${connectorId}`, state); +/** + * Native Social Redirect Utility Methods + */ +export const storageCallbackLinkKeyPrefix = 'social_callback_data'; + +export const buildSocialLandingUri = (path: string, redirectTo: string) => { + const { origin } = window.location; + const url = new URL(`${origin}${path}`); + url.searchParams.set(SearchParameters.redirectTo, redirectTo); + + const callbackLink = getLogtoNativeSdk()?.callbackLink; + + if (callbackLink) { + url.searchParams.set(SearchParameters.nativeCallbackLink, callbackLink); + } + + return url; +}; + +export const storeCallbackLink = (connectorId: string, callbackLink: string) => { + sessionStorage.setItem(`${storageCallbackLinkKeyPrefix}:${connectorId}`, callbackLink); +}; + +export const getCallbackLinkFromStorage = (connectorId: string) => { + return sessionStorage.getItem(`${storageCallbackLinkKeyPrefix}:${connectorId}`); }; /** diff --git a/packages/ui/src/pages/Callback/index.module.scss b/packages/ui/src/pages/Callback/index.module.scss index ac6d49a6d..efb487d4d 100644 --- a/packages/ui/src/pages/Callback/index.module.scss +++ b/packages/ui/src/pages/Callback/index.module.scss @@ -6,34 +6,12 @@ @include _.flex-column; } -.connector > img { - width: 96px; - height: 96px; - @include _.image-align-center; -} -.loadingLabel { - margin-bottom: _.unit(6); -} - -.container { +.connectorContainer { flex: 1; - @include _.flex-column; } .button { @include _.full-width; margin-bottom: _.unit(4); } - -:global(body.mobile) { - .connector { - margin-bottom: _.unit(6); - } -} - -:global(body.desktop) { - .connector { - margin-bottom: _.unit(1); - } -} diff --git a/packages/ui/src/pages/Callback/index.tsx b/packages/ui/src/pages/Callback/index.tsx index 87c9bd8b7..13c3b2abc 100644 --- a/packages/ui/src/pages/Callback/index.tsx +++ b/packages/ui/src/pages/Callback/index.tsx @@ -1,50 +1,48 @@ -import React, { useEffect, useContext, useMemo } from 'react'; +import React, { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; import Button from '@/components/Button'; -import { PageContext } from '@/hooks/use-page-context'; +import SocialLanding from '@/containers/SocialLanding'; import useSocialCallbackHandler from '@/hooks/use-social-callback-handler'; import * as styles from './index.module.scss'; type Props = { - connector?: string; + connector: string; }; const Callback = () => { const { connector: connectorId } = useParams(); - const { experienceSettings } = useContext(PageContext); const { t } = useTranslation(undefined, { keyPrefix: 'main_flow' }); const socialCallbackHandler = useSocialCallbackHandler(); - const connectorLabel = useMemo(() => { - const connector = experienceSettings?.socialConnectors.find(({ id }) => id === connectorId); - - if (connector) { - return ( -
- -
- ); - } - - return
{connectorId}
; - }, [connectorId, experienceSettings?.socialConnectors]); - // SocialSignIn Callback Handler useEffect(() => { - socialCallbackHandler(); - }, [socialCallbackHandler]); + if (!connectorId) { + return; + } + socialCallbackHandler(connectorId); + }, [socialCallbackHandler, connectorId]); + + if (!connectorId) { + return null; + } return (
-
- {connectorLabel} -
loading...
-
-
diff --git a/packages/ui/src/pages/SocialLanding/index.module.scss b/packages/ui/src/pages/SocialLanding/index.module.scss new file mode 100644 index 000000000..05aacdf36 --- /dev/null +++ b/packages/ui/src/pages/SocialLanding/index.module.scss @@ -0,0 +1,12 @@ +@use '@/scss/underscore' as _; + + +.wrapper { + @include _.full-page; + @include _.flex-column; +} + + +.connectorContainer { + flex: 1; +} diff --git a/packages/ui/src/pages/SocialLanding/index.test.tsx b/packages/ui/src/pages/SocialLanding/index.test.tsx new file mode 100644 index 000000000..353f62e7f --- /dev/null +++ b/packages/ui/src/pages/SocialLanding/index.test.tsx @@ -0,0 +1,49 @@ +import { waitFor } from '@testing-library/react'; +import React from 'react'; +import { MemoryRouter, Route, Routes } from 'react-router-dom'; + +import renderWithPageContext from '@/__mocks__/RenderWithPageContext'; +import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider'; +import { getCallbackLinkFromStorage } from '@/hooks/utils'; +import { SearchParameters } from '@/types'; +import { queryStringify } from '@/utils'; + +import SocialLanding from '.'; + +describe(`SocialLanding Page`, () => { + const replace = jest.fn(); + it('Should set session storage and redirect', async () => { + const callbackLink = 'logto:logto.android.com'; + const redirectUri = 'www.github.com'; + + /* eslint-disable @silverhand/fp/no-mutating-methods */ + Object.defineProperty(window, 'location', { + value: { + origin, + href: `/social-landing?`, + search: queryStringify({ + [SearchParameters.redirectTo]: redirectUri, + [SearchParameters.nativeCallbackLink]: callbackLink, + }), + replace, + }, + }); + /* eslint-enable @silverhand/fp/no-mutating-methods */ + + renderWithPageContext( + + + + } /> + + + + ); + + await waitFor(() => { + expect(replace).toBeCalledWith(redirectUri); + }); + + expect(getCallbackLinkFromStorage('github')).toBe(callbackLink); + }); +}); diff --git a/packages/ui/src/pages/SocialLanding/index.tsx b/packages/ui/src/pages/SocialLanding/index.tsx new file mode 100644 index 000000000..a4565c42c --- /dev/null +++ b/packages/ui/src/pages/SocialLanding/index.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useParams } from 'react-router-dom'; + +import SocialLandingContainer from '@/containers/SocialLanding'; +import useSocialLandingHandler from '@/hooks/use-social-landing-handler'; + +import * as styles from './index.module.scss'; + +type Parameters = { + connector: string; +}; + +const SocialLanding = () => { + const { connector: connectorId } = useParams(); + const { t } = useTranslation(undefined, { keyPrefix: 'main_flow' }); + + useSocialLandingHandler(connectorId); + + if (!connectorId) { + return null; + } + + return ( +
+ +
+ ); +}; + +export default SocialLanding; diff --git a/packages/ui/src/types/index.ts b/packages/ui/src/types/index.ts index 6379ba3c7..462d75819 100644 --- a/packages/ui/src/types/index.ts +++ b/packages/ui/src/types/index.ts @@ -11,7 +11,9 @@ export type SignInMethod = 'username' | 'email' | 'sms' | 'social'; export type LocalSignInMethod = 'username' | 'email' | 'sms'; export enum SearchParameters { - bindWithSocial = 'bw', + bindWithSocial = 'bind_with', + nativeCallbackLink = 'native_callback', + redirectTo = 'redirect_to', } export type Platform = 'web' | 'mobile'; diff --git a/packages/ui/src/utils/index.test.ts b/packages/ui/src/utils/index.test.ts index 36a41f400..391786895 100644 --- a/packages/ui/src/utils/index.test.ts +++ b/packages/ui/src/utils/index.test.ts @@ -11,6 +11,12 @@ describe('util methods', () => { expect(parameters).toEqual({ foo: 'test', bar: 'test2' }); }); + it('parseQueryParameters with encoded url', () => { + const url = 'http://logto.io'; + const parameters = parseQueryParameters(`?callback=${encodeURIComponent(url)}`); + expect(parameters).toEqual({ callback: url }); + }); + it('queryStringify', () => { expect(queryStringify(new URLSearchParams({ foo: 'test' }))).toEqual('foo=test'); });