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);
}}
>
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');
});