0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

Merge pull request #702 from logto-io/charles-log-2171-refactor-sdk-integration-guide-with-mdx

refactor(console): sdk integration guide with mdx
This commit is contained in:
Charles Zhao 2022-04-28 21:06:04 +08:00 committed by GitHub
commit cd9ce73228
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 1209 additions and 1625 deletions

3
.gitignore vendored
View file

@ -17,6 +17,9 @@ node_modules
/packages/*/lib
/packages/*/dist
# docs copied to admin console
/packages/console/**/*.mdx
# logs
logs
*.log*

View file

@ -8,7 +8,7 @@
"scripts": {
"preinstall": "npx only-allow pnpm",
"precommit": "lint-staged",
"copyfiles": "copyfiles -u 1 public/**/*.* dist",
"copyfiles": "copyfiles -u 2 \"../docs/**/*.mdx\" src/assets",
"start": "pnpm copyfiles && parcel src/index.html",
"dev": "pnpm copyfiles && PORT=5002 parcel src/index.html --public-url /console --no-cache --hmr-port 6002",
"check": "tsc --noEmit",
@ -21,17 +21,40 @@
"@logto/phrases": "^0.1.0",
"@logto/react": "^0.1.3",
"@logto/schemas": "^0.1.0",
"@mdx-js/react": "^1.6.22",
"@monaco-editor/react": "^4.3.1",
"@parcel/core": "^2.5.0",
"@parcel/transformer-mdx": "^2.5.0",
"@parcel/transformer-sass": "^2.5.0",
"@silverhand/eslint-config": "^0.10.2",
"@silverhand/eslint-config-react": "^0.10.3",
"@silverhand/essentials": "^1.1.6",
"@silverhand/ts-config": "^0.10.2",
"@silverhand/ts-config-react": "^0.10.3",
"@tsconfig/docusaurus": "^1.0.5",
"@types/lodash.kebabcase": "^4.1.6",
"@types/mdx": "^2.0.1",
"@types/mdx-js__react": "^1.5.5",
"@types/react": "^17.0.14",
"@types/react-dom": "^17.0.9",
"@types/react-modal": "^3.13.1",
"classnames": "^2.3.1",
"copyfiles": "^2.4.1",
"csstype": "^3.0.11",
"dnd-core": "^16.0.0",
"eslint": "^8.10.0",
"i18next": "^21.6.12",
"i18next-browser-languagedetector": "^6.1.3",
"ky": "^0.30.0",
"lint-staged": "^12.0.0",
"lodash.kebabcase": "^4.1.1",
"monaco-editor": "^0.33.0",
"nanoid": "^3.1.23",
"parcel": "^2.5.0",
"postcss": "^8.4.6",
"postcss-modules": "^4.3.0",
"prettier": "^2.3.2",
"process": "^0.11.10",
"react": "^17.0.2",
"react-dnd": "^16.0.0",
"react-dnd-html5-backend": "^16.0.0",
@ -44,30 +67,13 @@
"react-paginate": "^8.1.2",
"react-router-dom": "^6.2.2",
"remark-gfm": "^3.0.1",
"swr": "^1.2.2",
"@parcel/core": "^2.3.2",
"@parcel/transformer-sass": "^2.3.2",
"@silverhand/eslint-config": "^0.10.2",
"@silverhand/eslint-config-react": "^0.10.3",
"@silverhand/ts-config": "^0.10.2",
"@silverhand/ts-config-react": "^0.10.3",
"@types/lodash.kebabcase": "^4.1.6",
"@types/react": "^17.0.14",
"@types/react-dom": "^17.0.9",
"@types/react-modal": "^3.13.1",
"copyfiles": "^2.4.1",
"eslint": "^8.10.0",
"lint-staged": "^12.0.0",
"parcel": "^2.3.2",
"postcss": "^8.4.6",
"postcss-modules": "^4.3.0",
"prettier": "^2.3.2",
"process": "^0.11.10",
"stylelint": "^13.13.1",
"swr": "^1.2.2",
"typescript": "^4.6.2"
},
"alias": {
"@/*": "./src/$1"
"@/*": "./src/$1",
"@theme/*": "./src/mdx-components/$1"
},
"eslintConfig": {
"extends": "@silverhand/react"

View file

@ -1,3 +0,0 @@
{
"files": ["step1.md", "step2.md", "step3.md", "step4.md", "step5.md"]
}

View file

@ -1,34 +0,0 @@
---
title: Install Logto SDK
subtitle: 1 step
---
## Option 1: Install your SDK dependency
Run the CLI command under your project's root directory.
```
// installation with npm
npm install @logto/react --save
// installation with yarn
yarn add @logto/react
// installation with pnpm
pnpm install @logto/react --save
```
## Option 2: Add script tag to your HTML
```
<script src="https://logto.io/js/logto-sdk-react/0.1.0/logto-sdk-react.production.js"></script>
```
## Option 3: Fork your own from github
```
git clone https://github.com/logto-io/js.git
```
```
pnpm build
```

View file

@ -1,35 +0,0 @@
---
title: Initiate LogtoClient
subtitle: 1 step | Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
---
Add the following code to your main html file. You may need client ID and authorization domain.
```typescript
import { LogtoProvider, LogtoConfig } from '@logto/react';
import React from 'react';
...
const App = () => {
const config: LogtoConfig = { clientId: 'foo', endpoint: 'https://your-endpoint-domain.com' }
return (
<BrowserRouter>
<LogtoProvider config={config}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/callback" element={<Callback />} />
<Route
path="/protected-resource"
element={
<RequireAuth>
<ProtectedResource />
</RequireAuth>
}
/>
</Routes>
</LogtoProvider>
</BrowserRouter>
);
};
```

View file

@ -1,47 +0,0 @@
---
title: Sign In
subtitle: 2 steps
---
## Step 1: Setup your login
The Logto React SDK provides you tools and hooks to quickly implement your own authorization flow. First, lets enter your redirect URI
```redirectUris
Redirect URI
```
Add the following code to your web app
```typescript
import React from "react";
import { useLogto } from '@logto/react';
const SignInButton = () => {
const { signIn } = useLogto();
const redirectUrl = window.location.origin + '/callback';
return <button onClick={() => signIn(redirectUrl)}>Sign In</button>;
};
export default SignInButton;
```
## Step 2: Retrieve Auth Status
```typescript
import React from "react";
import { useLogto } from '@logto/react';
const App = () => {
const { isAuthenticated, signIn } = useLogto();
if !(isAuthenticated) {
return <SignInButton />
}
return <>
<AppContent />
<SignOutButton />
</>
};
```

View file

@ -1,26 +0,0 @@
---
title: Sign Out
subtitle: 1 steps
---
Execute signOut() methods will redirect users to the Logto sign out page. After a success sign out, all use session data and auth status will be cleared.
```postLogoutRedirectUris
Post sign out redirect URI
```
Add the following code to your web app
```typescript
import React from "react";
import { useLogto } from '@logto/react';
const SignOutButton = () => {
const { signOut } = useLogto();
return (
<button onClick={() => signOut(window.location.origin)}>Sign out</button>
);
};
export default SignOutButton;
```

View file

@ -1,15 +0,0 @@
---
title: Advanced Settings & Documentation Links
subtitle: 2 steps
---
## Step 1: Advanced Settings
Go to application details page and switch to advanced settings tab
## Step 2: Now check out the documentation links below
[- SDK Documentation](https://link-url-here.org)
[- OIDC Documentation](https://link-url-here.org)
[- Calling API to fetch accessToken](https://link-url-here.org)

View file

@ -1,3 +0,0 @@
{
"files": ["step1.md", "step2.md", "step3.md", "step4.md", "step5.md"]
}

View file

@ -1,34 +0,0 @@
---
title: 安装 Logto SDK
subtitle: 1 step
---
## 方案1: 安装 SDK 依赖包
Run the CLI command under your project's root directory.
```
// installation with npm
npm install @logto/react --save
// installation with yarn
yarn add @logto/react
// installation with pnpm
pnpm install @logto/react --save
```
## 方案2: Add script tag to your HTML
```
<script src="https://logto.io/js/logto-sdk-react/0.1.0/logto-sdk-react.production.js"></script>
```
## 方案3: Fork your own from github
```
git clone https://github.com/logto-io/js.git
```
```
pnpm build
```

View file

@ -1,35 +0,0 @@
---
title: 初始化 LogtoClient
subtitle: 1 step | Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
---
Add the following code to your main html file. You may need client ID and authorization domain.
```typescript
import { LogtoProvider, LogtoConfig } from '@logto/react';
import React from 'react';
...
const App = () => {
const config: LogtoConfig = { clientId: 'foo', endpoint: 'https://your-endpoint-domain.com' }
return (
<BrowserRouter>
<LogtoProvider config={config}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/callback" element={<Callback />} />
<Route
path="/protected-resource"
element={
<RequireAuth>
<ProtectedResource />
</RequireAuth>
}
/>
</Routes>
</LogtoProvider>
</BrowserRouter>
);
};
```

View file

@ -1,47 +0,0 @@
---
title: 登录
subtitle: 2 steps
---
## Step 1: Setup your login
The Logto React SDK provides you tools and hooks to quickly implement your own authorization flow. First, lets enter your redirect URI
```redirectUris
Redirect URI
```
Add the following code to your web app
```typescript
import React from "react";
import { useLogto } from '@logto/react';
const SignInButton = () => {
const { signIn } = useLogto();
const redirectUrl = window.location.origin + '/callback';
return <button onClick={() => signIn(redirectUrl)}>Sign In</button>;
};
export default SignInButton;
```
## Step 2: Retrieve Auth Status
```typescript
import React from "react";
import { useLogto } from '@logto/react';
const App = () => {
const { isAuthenticated, signIn } = useLogto();
if !(isAuthenticated) {
return <SignInButton />
}
return <>
<AppContent />
<SignOutButton />
</>
};
```

View file

@ -1,26 +0,0 @@
---
title: 登出
subtitle: 1 steps
---
Execute signOut() methods will redirect users to the Logto sign out page. After a success sign out, all use session data and auth status will be cleared.
```postLogoutRedirectUris
Post sign out redirect URI
```
Add the following code to your web app
```typescript
import React from "react";
import { useLogto } from '@logto/react';
const SignOutButton = () => {
const { signOut } = useLogto();
return (
<button onClick={() => signOut(window.location.origin)}>Sign out</button>
);
};
export default SignOutButton;
```

View file

@ -1,15 +0,0 @@
---
title: 高级设置以及相关文档链接
subtitle: 2 steps
---
## Step 1: Advanced Settings
Go to application details page and switch to advanced settings tab
## Step 2: Now check out the documentation links below
[- SDK Documentation](https://link-url-here.org)
[- OIDC Documentation](https://link-url-here.org)
[- Calling API to fetch accessToken](https://link-url-here.org)

View file

@ -0,0 +1,25 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React, { ReactNode } from 'react';
export type Props = {
children: ReactNode;
className?: string;
value: string;
label?: string;
};
const TabItem = ({ children, ...rest }: Props): JSX.Element => {
return (
<div role="tabpanel" {...rest}>
{children}
</div>
);
};
export default TabItem;

View file

@ -0,0 +1,108 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import { Nullable } from '@silverhand/essentials';
import React, { useState, isValidElement, type ReactElement, cloneElement } from 'react';
import type { Props as TabItemProps } from '../TabItem';
type Props = {
className?: string;
children: ReactElement<TabItemProps>;
};
// A very rough duck type, but good enough to guard against mistakes while
// allowing customization
function isTabItem(comp: ReactElement): comp is ReactElement<TabItemProps> {
return typeof comp.props.value !== 'undefined';
}
const Tabs = ({ className, children }: Props): JSX.Element => {
const verifiedChildren = React.Children.map(children, (child) => {
if (isValidElement(child) && isTabItem(child)) {
return child;
}
});
const values =
// Only pick keys that we recognize. MDX would inject some keys by default
verifiedChildren.map(({ props: { value, label } }) => ({
value,
label,
}));
const [selectedValue, setSelectedValue] = useState<string>();
const tabReferences: Array<Nullable<HTMLLIElement>> = [];
const handleTabChange = (
event: React.FocusEvent<HTMLLIElement> | React.MouseEvent<HTMLLIElement>
) => {
const newTab = event.currentTarget;
const newTabIndex = tabReferences.indexOf(newTab);
const newTabValue = values[newTabIndex]?.value;
if (newTabValue !== selectedValue) {
setSelectedValue(newTabValue);
}
};
const handleKeydown = (event: React.KeyboardEvent<HTMLLIElement>) => {
// eslint-disable-next-line @silverhand/fp/no-let
let focusElement: Nullable<HTMLLIElement> = null;
switch (event.key) {
case 'ArrowRight': {
const nextTab = tabReferences.indexOf(event.currentTarget) + 1;
// eslint-disable-next-line @silverhand/fp/no-mutation
focusElement = tabReferences[nextTab] ?? tabReferences[0] ?? null;
break;
}
case 'ArrowLeft': {
const previousTab = tabReferences.indexOf(event.currentTarget) - 1;
// eslint-disable-next-line @silverhand/fp/no-mutation
focusElement =
tabReferences[previousTab] ?? tabReferences[tabReferences.length - 1] ?? null;
break;
}
default:
break;
}
focusElement?.focus();
};
return (
<div>
<ul role="tablist" aria-orientation="horizontal" className={className}>
{values.map(({ value, label }) => (
<li
key={value}
ref={(tabControl) => tabReferences.concat(tabControl)}
role="tab"
tabIndex={selectedValue === value ? 0 : -1}
aria-selected={selectedValue === value}
onKeyDown={handleKeydown}
onFocus={handleTabChange}
onClick={handleTabChange}
>
{label ?? value}
</li>
))}
</ul>
<div>
{verifiedChildren.map((tabItem) =>
cloneElement(tabItem, {
key: tabItem.props.value,
})
)}
</div>
</div>
);
};
export default Tabs;

View file

@ -10,9 +10,9 @@ import RadioGroup, { Radio } from '@/components/RadioGroup';
import TextInput from '@/components/TextInput';
import useApi from '@/hooks/use-api';
import { applicationTypeI18nKey } from '@/types/applications';
import { GetStartedForm } from '@/types/get-started';
import { GuideForm } from '@/types/guide';
import GetStartedModal from '../GetStartedModal';
import GuideModal from '../GuideModal';
import TypeDescription from '../TypeDescription';
import * as styles from './index.module.scss';
@ -57,7 +57,7 @@ const CreateForm = ({ onClose }: Props) => {
setIsGetStartedModalOpen(true);
});
const onComplete = async (data: GetStartedForm) => {
const onComplete = async (data: GuideForm) => {
if (!createdApp) {
return;
}
@ -118,7 +118,7 @@ const CreateForm = ({ onClose }: Props) => {
</FormField>
</form>
{createdApp && (
<GetStartedModal
<GuideModal
appName={createdApp.name}
isOpen={isGetStartedModalOpen}
onClose={closeModal}

View file

@ -1,10 +1,10 @@
import React from 'react';
import Modal from 'react-modal';
import GetStarted from '@/pages/GetStarted';
import Guide from '@/pages/Guide';
import * as modalStyles from '@/scss/modal.module.scss';
import { SupportedJavascriptLibraries } from '@/types/applications';
import { GetStartedForm } from '@/types/get-started';
import { GuideForm } from '@/types/guide';
import LibrarySelector from '../LibrarySelector';
@ -12,16 +12,15 @@ type Props = {
appName: string;
isOpen: boolean;
onClose: () => void;
onComplete: (data: GetStartedForm) => Promise<void>;
onComplete: (data: GuideForm) => Promise<void>;
};
const GetStartedModal = ({ appName, isOpen, onClose, onComplete }: Props) => (
const GuideModal = ({ appName, isOpen, onClose, onComplete }: Props) => (
<Modal isOpen={isOpen} className={modalStyles.fullScreen}>
<GetStarted
<Guide
bannerComponent={<LibrarySelector />}
title={appName}
subtitle="applications.get_started.header_description"
type="application"
defaultSubtype={SupportedJavascriptLibraries.React}
onClose={onClose}
onComplete={onComplete}
@ -29,4 +28,4 @@ const GetStartedModal = ({ appName, isOpen, onClose, onComplete }: Props) => (
</Modal>
);
export default GetStartedModal;
export default GuideModal;

View file

@ -11,7 +11,7 @@ import UnnamedTrans from '@/components/UnnamedTrans';
import { RequestError } from '@/hooks/use-api';
import * as modalStyles from '@/scss/modal.module.scss';
import GetStartedModal from '../GetStartedModal';
import GuideModal from '../GuideModal';
import * as styles from './index.module.scss';
type Props = {
@ -101,7 +101,7 @@ const CreateForm = ({ onClose, isOpen: isFormOpen, type }: Props) => {
</RadioGroup>
)}
{activeConnector && (
<GetStartedModal
<GuideModal
connector={activeConnector}
isOpen={isGetStartedModalOpen}
onClose={closeModal}

View file

@ -16,9 +16,9 @@ import Spacer from '@/components/Spacer';
import useApi from '@/hooks/use-api';
import Close from '@/icons/Close';
import SenderTester from '@/pages/ConnectorDetails/components/SenderTester';
import Step from '@/pages/GetStarted/components/Step';
import Step from '@/pages/Guide/components/Step';
import * as modalStyles from '@/scss/modal.module.scss';
import { GetStartedForm } from '@/types/get-started';
import { GuideForm } from '@/types/guide';
import * as styles from './index.module.scss';
@ -26,7 +26,7 @@ type Props = {
connector: ConnectorDTO;
isOpen: boolean;
onClose: () => void;
onComplete?: (data: GetStartedForm) => Promise<void>;
onComplete?: (data: GuideForm) => Promise<void>;
};
const onClickFetchSampleProject = (name: string) => {
@ -34,7 +34,7 @@ const onClickFetchSampleProject = (name: string) => {
window.open(sampleUrl, '_blank');
};
const GetStartedModal = ({ connector, isOpen, onClose }: Props) => {
const GuideModal = ({ connector, isOpen, onClose }: Props) => {
const api = useApi();
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const {
@ -48,7 +48,7 @@ const GetStartedModal = ({ connector, isOpen, onClose }: Props) => {
const isSocialConnector =
connectorType !== ConnectorType.SMS && connectorType !== ConnectorType.Email;
const [activeStepIndex, setActiveStepIndex] = useState<number>(0);
const methods = useForm<GetStartedForm>({ reValidateMode: 'onBlur' });
const methods = useForm<GuideForm>({ reValidateMode: 'onBlur' });
const {
control,
formState: { isSubmitting },
@ -126,9 +126,7 @@ const GetStartedModal = ({ connector, isOpen, onClose }: Props) => {
title="Enter your json here"
subtitle="Lorem ipsum dolor sit amet, consectetuer adipiscing elit."
index={0}
isActive={activeStepIndex === 0}
isComplete={activeStepIndex > 0}
isFinalStep={isSocialConnector}
activeIndex={activeStepIndex}
buttonHtmlType="submit"
>
<Controller
@ -144,13 +142,12 @@ const GetStartedModal = ({ connector, isOpen, onClose }: Props) => {
</FormProvider>
{!isSocialConnector && (
<Step
isFinalStep
title="Test your message"
subtitle="Lorem ipsum dolor sit amet, consectetuer adipiscing elit."
index={1}
isActive={activeStepIndex === 1}
isComplete={activeStepIndex > 1}
activeIndex={activeStepIndex}
buttonHtmlType="button"
buttonText="general.done"
onNext={onClose}
>
<SenderTester connectorType={connectorType} />
@ -163,4 +160,4 @@ const GetStartedModal = ({ connector, isOpen, onClose }: Props) => {
);
};
export default GetStartedModal;
export default GuideModal;

View file

@ -1,79 +0,0 @@
import React, { PropsWithChildren, useEffect, useRef } from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { CodeProps } from 'react-markdown/lib/ast-to-react.js';
import CodeEditor from '@/components/CodeEditor';
import DangerousRaw from '@/components/DangerousRaw';
import FormField from '@/components/FormField';
import MultiTextInput from '@/components/MultiTextInput';
import { createValidatorForRhf, convertRhfErrorMessage } from '@/components/MultiTextInput/utils';
import { GetStartedForm } from '@/types/get-started';
import { noSpaceRegex } from '@/utilities/regex';
type Props = PropsWithChildren<CodeProps> & { onError: () => void };
const CodeComponentRenderer = ({ className, children, onError }: Props) => {
const {
control,
formState: { errors },
} = useFormContext<GetStartedForm>();
const ref = useRef<HTMLDivElement>(null);
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const [, codeBlockType] = /language-(\w+)/.exec(className ?? '') ?? [];
const content = String(children);
/** Code block types defined in markdown. E.g.
* ```typescript
* some code
* ```
* These two custom code block types should be replaced with `MultiTextInput` component:
* 'redirectUris' and 'postLogoutRedirectUris'
*/
const isMultilineInput =
codeBlockType === 'redirectUris' || codeBlockType === 'postLogoutRedirectUris';
const firstErrorKey = Object.keys(errors)[0];
const isFirstErrorField = firstErrorKey && firstErrorKey === codeBlockType;
useEffect(() => {
if (isFirstErrorField) {
onError();
}
}, [isFirstErrorField, onError]);
if (isMultilineInput) {
return (
<FormField isRequired title={<DangerousRaw>{content}</DangerousRaw>}>
<Controller
name={codeBlockType}
control={control}
defaultValue={[]}
rules={{
validate: createValidatorForRhf({
required: t('errors.required_field_missing_plural', { field: content }),
pattern: {
regex: noSpaceRegex,
message: t('errors.no_space_in_uri'),
},
}),
}}
render={({ field: { onChange, value }, fieldState: { error } }) => (
<div ref={ref}>
<MultiTextInput
value={value}
error={convertRhfErrorMessage(error?.message)}
onChange={onChange}
/>
</div>
)}
/>
</FormField>
);
}
return <CodeEditor isReadonly language={codeBlockType} value={content} />;
};
export default CodeComponentRenderer;

View file

@ -1,43 +0,0 @@
import i18next from 'i18next';
import { useMemo } from 'react';
// eslint-disable-next-line node/file-extension-in-import
import useSWRImmutable from 'swr/immutable';
import { StepMetadata } from '@/types/get-started';
import { parseMarkdownWithYamlFrontmatter } from '@/utilities/markdown';
type DocumentFileNames = {
files: string[];
};
export type GetStartedType = 'application' | 'connector';
/**
* Fetch the markdown files for the given type and subtype.
* @param type 'application' or 'connector'
* @param subtype Application library name or connector name
* @returns List of step metadata including Yaml frontmatter and markdown content
*/
export const useGetStartedSteps = (type: GetStartedType, subtype?: string) => {
const subPath = subtype ? `/${subtype}` : '';
const publicPath = useMemo(
() => `/console/get-started/${type}${subPath}/${i18next.language}`.toLowerCase(),
[type, subPath]
);
const { data: jsonData } = useSWRImmutable<DocumentFileNames>(`${publicPath}/index.json`);
const { data: steps } = useSWRImmutable<StepMetadata[]>(
jsonData,
async ({ files }: DocumentFileNames) =>
Promise.all(
files.map(async (fileName) => {
const response = await fetch(`${publicPath}/${fileName}`);
const markdownFile = await response.text();
return parseMarkdownWithYamlFrontmatter<StepMetadata>(markdownFile);
})
)
);
return steps;
};

View file

@ -0,0 +1,64 @@
import React, { useRef } from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import DangerousRaw from '@/components/DangerousRaw';
import FormField from '@/components/FormField';
import MultiTextInput from '@/components/MultiTextInput';
import { createValidatorForRhf, convertRhfErrorMessage } from '@/components/MultiTextInput/utils';
import { GuideForm } from '@/types/guide';
import { noSpaceRegex } from '@/utilities/regex';
type Props = {
name: 'redirectUris' | 'postLogoutRedirectUris';
title: string;
onError?: () => void;
};
const MultiTextInputField = ({ name, title, onError }: Props) => {
const {
control,
formState: { errors },
} = useFormContext<GuideForm>();
const ref = useRef<HTMLDivElement>(null);
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const firstErrorKey = Object.keys(errors)[0];
const isFirstErrorField = firstErrorKey && firstErrorKey === name;
if (isFirstErrorField) {
ref.current?.scrollIntoView({ block: 'center', behavior: 'smooth' });
onError?.();
}
return (
<FormField isRequired title={<DangerousRaw>{title}</DangerousRaw>}>
<Controller
name={name}
control={control}
defaultValue={[]}
rules={{
validate: createValidatorForRhf({
required: t('errors.required_field_missing_plural', { field: title }),
pattern: {
regex: noSpaceRegex,
message: t('errors.no_space_in_uri'),
},
}),
}}
render={({ field: { onChange, value }, fieldState: { error } }) => (
<div ref={ref}>
<MultiTextInput
value={value}
error={convertRhfErrorMessage(error?.message)}
onChange={onChange}
/>
</div>
)}
/>
</FormField>
);
};
export default MultiTextInputField;

View file

@ -1,16 +1,7 @@
import { I18nKey } from '@logto/phrases';
import { conditional } from '@silverhand/essentials';
import classNames from 'classnames';
import React, {
cloneElement,
isValidElement,
PropsWithChildren,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { CodeProps } from 'react-markdown/lib/ast-to-react.js';
import React, { PropsWithChildren, useEffect, useRef, useState } from 'react';
import Button from '@/components/Button';
import Card from '@/components/Card';
@ -21,16 +12,15 @@ import Spacer from '@/components/Spacer';
import { ArrowDown, ArrowUp } from '@/icons/Arrow';
import Tick from '@/icons/Tick';
import CodeComponentRenderer from '../CodeComponentRenderer';
import * as styles from './index.module.scss';
type Props = PropsWithChildren<{
title: string;
subtitle?: string;
index: number;
isActive: boolean;
isComplete: boolean;
isFinalStep: boolean;
activeIndex: number;
invalidIndex?: number;
buttonText?: I18nKey;
buttonHtmlType: 'submit' | 'button';
onNext?: () => void;
}>;
@ -40,46 +30,30 @@ const Step = ({
title,
subtitle,
index,
isActive,
isComplete,
isFinalStep,
buttonHtmlType,
activeIndex,
invalidIndex,
buttonText = 'general.next',
buttonHtmlType = 'button',
onNext,
}: Props) => {
const [isExpanded, setIsExpanded] = useState(false);
const isActive = index === activeIndex;
const isComplete = index < activeIndex;
const isInvalid = index === invalidIndex;
const ref = useRef<HTMLDivElement>(null);
const scrollToStep = useCallback(() => {
ref.current?.scrollIntoView({ block: 'start', behavior: 'smooth' });
}, []);
const onError = useCallback(() => {
setIsExpanded(true);
scrollToStep();
}, [scrollToStep]);
useEffect(() => {
if (isActive) {
if (isActive || isInvalid) {
setIsExpanded(true);
}
}, [isActive]);
}, [isActive, isInvalid]);
useEffect(() => {
if (isExpanded) {
scrollToStep();
ref.current?.scrollIntoView({ block: 'start', behavior: 'smooth' });
}
}, [isExpanded, scrollToStep]);
}, [isExpanded]);
const memoizedComponents = useMemo(
() => ({
code: ({ ...props }: PropsWithChildren<CodeProps>) => (
<CodeComponentRenderer {...props} onError={onError} />
),
}),
[onError]
);
// TODO: add more styles to markdown renderer
return (
<Card key={title} ref={ref} className={styles.card}>
<div
@ -106,13 +80,13 @@ const Step = ({
<IconButton>{isExpanded ? <ArrowUp /> : <ArrowDown />}</IconButton>
</div>
<div className={classNames(styles.content, isExpanded && styles.expanded)}>
{isValidElement(children) && cloneElement(children, { components: memoizedComponents })}
{children}
<div className={styles.buttonWrapper}>
<Button
htmlType={buttonHtmlType}
type="primary"
title={`general.${isFinalStep ? 'done' : 'next'}`}
onClick={conditional(!isFinalStep && onNext)}
title={buttonText}
onClick={conditional(onNext)}
/>
</div>
</div>

View file

@ -39,6 +39,10 @@
.banner {
margin-bottom: _.unit(6);
}
h1 {
display: none;
}
}
}

View file

@ -1,31 +1,52 @@
import { AdminConsoleKey } from '@logto/phrases';
import React, { cloneElement, isValidElement, PropsWithChildren, ReactNode, useState } from 'react';
import { MDXProvider } from '@mdx-js/react';
import i18next from 'i18next';
import { MDXProps } from 'mdx/types';
import React, {
cloneElement,
isValidElement,
lazy,
LazyExoticComponent,
PropsWithChildren,
ReactNode,
Suspense,
useState,
} from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import ReactMarkdown from 'react-markdown';
import Button from '@/components/Button';
import CardTitle from '@/components/CardTitle';
import CodeEditor from '@/components/CodeEditor';
import DangerousRaw from '@/components/DangerousRaw';
import IconButton from '@/components/IconButton';
import Spacer from '@/components/Spacer';
import Close from '@/icons/Close';
import { GetStartedForm } from '@/types/get-started';
import { GuideForm } from '@/types/guide';
import MultiTextInputField from './components/MultiTextInputField';
import Step from './components/Step';
import { GetStartedType, useGetStartedSteps } from './hooks';
import * as styles from './index.module.scss';
const Guides: Record<string, LazyExoticComponent<(props: MDXProps) => JSX.Element>> = {
react: lazy(async () => import('@/assets/docs/tutorial/integrate-sdk/react/index.mdx')),
'react_zh-cn': lazy(
async () =>
import(
'@/assets/i18n/zh-cn/docusaurus-plugin-content-docs/current/tutorial/integrate-sdk/react/index.mdx'
)
),
};
type Props = PropsWithChildren<{
title: string;
subtitle?: AdminConsoleKey;
type: GetStartedType;
/** `subtype` can be an actual type of an application or connector.
* e.g. React, Angular, Vue, etc. for application. Or Github, WeChat, etc. for connector.
*/
defaultSubtype?: string;
bannerComponent?: ReactNode;
onClose?: () => void;
onComplete?: (data: GetStartedForm) => Promise<void>;
onComplete?: (data: GuideForm) => Promise<void>;
}>;
const onClickFetchSampleProject = (projectName: string) => {
@ -33,19 +54,23 @@ const onClickFetchSampleProject = (projectName: string) => {
window.open(sampleUrl, '_blank');
};
const GetStarted = ({
const Guide = ({
title,
subtitle,
type,
defaultSubtype,
defaultSubtype = '',
bannerComponent,
onClose,
onComplete,
}: Props) => {
const [subtype, setSubtype] = useState(defaultSubtype);
const [activeStepIndex, setActiveStepIndex] = useState<number>(-1);
const steps = useGetStartedSteps(type, subtype) ?? [];
const methods = useForm<GetStartedForm>({ reValidateMode: 'onBlur' });
const [activeStepIndex, setActiveStepIndex] = useState(-1);
const [invalidStepIndex, setInvalidStepIndex] = useState(-1);
const locale = i18next.language;
const guideKey = `${subtype}_${locale}`.toLowerCase();
const GuideComponent = Guides[guideKey] ?? Guides[subtype];
const methods = useForm<GuideForm>({ mode: 'onSubmit', reValidateMode: 'onChange' });
const {
formState: { isSubmitting },
handleSubmit,
@ -89,32 +114,32 @@ const GetStarted = ({
setActiveStepIndex(0);
},
})}
{steps.map(({ title, subtitle, metadata }, index) => {
if (!title) {
return null;
}
const isFinalStep = index === steps.length - 1;
<MDXProvider
components={{
code: ({ className, children }) => {
const [, language] = /language-(\w+)/.exec(className ?? '') ?? [];
return (
<Step
key={title}
title={title}
subtitle={subtitle}
index={index}
isActive={activeStepIndex === index}
isComplete={activeStepIndex > index}
isFinalStep={isFinalStep}
buttonHtmlType={isFinalStep ? 'submit' : 'button'}
onNext={() => {
setActiveStepIndex(index + 1);
}}
>
{metadata && (
<ReactMarkdown className={styles.markdownContent}>{metadata}</ReactMarkdown>
)}
</Step>
);
})}
return <CodeEditor isReadonly language={language} value={String(children)} />;
},
MultiTextInputField,
Step,
}}
>
<Suspense fallback={<div>Loading...</div>}>
{GuideComponent && (
<GuideComponent
activeStepIndex={activeStepIndex}
invalidStepIndex={invalidStepIndex}
onNext={(nextIndex: number) => {
setActiveStepIndex(nextIndex);
}}
onError={(invalidIndex: number) => {
setInvalidStepIndex(invalidIndex);
}}
/>
)}
</Suspense>
</MDXProvider>
</form>
</FormProvider>
</div>
@ -122,4 +147,4 @@ const GetStarted = ({
);
};
export default GetStarted;
export default Guide;

View file

@ -7,7 +7,7 @@ export const applicationTypeI18nKey = Object.freeze({
} as const);
export enum SupportedJavascriptLibraries {
Angular = 'Angular',
React = 'React',
Vue = 'Vue',
Angular = 'angular',
React = 'react',
Vue = 'vue',
}

View file

@ -1,11 +0,0 @@
export type StepMetadata = {
title?: string;
subtitle?: string;
metadata: string; // Markdown formatted string
};
export type GetStartedForm = {
redirectUris: string[];
postLogoutRedirectUris: string[];
connectorConfigJson: string;
};

View file

@ -0,0 +1,5 @@
export type GuideForm = {
redirectUris: string[];
postLogoutRedirectUris: string[];
connectorConfigJson: string;
};

View file

@ -1,45 +0,0 @@
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
## Install SDK
<Tabs>
<TabItem value="npm" label="npm">
```bash
npm i @logto/react
```
</TabItem>
<TabItem value="yarn" label="Yarn">
```bash
yarn add @logto/react
```
</TabItem>
<TabItem value="pnpm" label="pnpm">
```bash
pnpm add @logto/react
```
</TabItem>
<TabItem value="script" label="script">
```html
<script src="https://logto.io/js/logto-sdk-react/0.1.0/logto-sdk-react.production.js" />
```
</TabItem>
<TabItem value="git" label="Git">
```bash
git clone https://github.com/logto-io/js.git
pnpm build
```
</TabItem>
</Tabs>

View file

@ -1,33 +0,0 @@
## Initiate LogtoClient
Add the following code to your main html file. You may need client ID and authorization domain.
```tsx
import { LogtoProvider, LogtoConfig } from '@logto/react';
import React from 'react';
//...
const App = () => {
const config: LogtoConfig = { clientId: 'foo', endpoint: 'https://your-endpoint-domain.com' }
return (
<BrowserRouter>
<LogtoProvider config={config}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/callback" element={<Callback />} />
<Route
path="/protected-resource"
element={
<RequireAuth>
<ProtectedResource />
</RequireAuth>
}
/>
</Routes>
</LogtoProvider>
</BrowserRouter>
);
};
```

View file

@ -1,45 +0,0 @@
## Sign In
### Setup your login
The Logto React SDK provides you tools and hooks to quickly implement your own authorization flow. First, lets enter your redirect URI
```redirectUris
Redirect URI
```
Add the following code to your web app
```typescript
import React from "react";
import { useLogto } from '@logto/react';
const SignInButton = () => {
const { signIn } = useLogto();
const redirectUrl = window.location.origin + '/callback';
return <button onClick={() => signIn(redirectUrl)}>Sign In</button>;
};
export default SignInButton;
```
### Retrieve Auth Status
```tsx
import React from "react";
import { useLogto } from '@logto/react';
const App = () => {
const { isAuthenticated, signIn } = useLogto();
if !(isAuthenticated) {
return <SignInButton />
}
return <>
<AppContent />
<SignOutButton />
</>
};
```

View file

@ -1,24 +0,0 @@
## Sign Out
Execute signOut() methods will redirect users to the Logto sign out page. After a success sign out, all use session data and auth status will be cleared.
```postLogoutRedirectUris
Post sign out redirect URI
```
Add the following code to your web app
```tsx
import React from "react";
import { useLogto } from '@logto/react';
const SignOutButton = () => {
const { signOut } = useLogto();
return (
<button onClick={() => signOut(window.location.origin)}>Sign out</button>
);
};
export default SignOutButton;
```

View file

@ -1,5 +0,0 @@
## Further Readings
- [SDK Documentation](https://link-url-here.org)
- [OIDC Documentation](https://link-url-here.org)
- [Calling API to fetch accessToken](https://link-url-here.org)

View file

@ -1,13 +1,190 @@
import Step1 from './_step-1.mdx'
import Step2 from './_step-2.md'
import Step3 from './_step-3.md'
import Step4 from './_step-4.md'
import Step5 from './_step-5.md'
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
# Integrate `@logto/react`
<Step1 />
<Step2 />
<Step3 />
<Step4 />
<Step5 />
<Step
title="Install SDK"
subtitle="Please select your favorite package manager"
index={0}
activeIndex={props.activeStepIndex}
invalidIndex={props.invalidStepIndex}
onNext={() => props.onNext(1)}
>
<Tabs>
<TabItem value="npm" label="npm">
```bash
npm i @logto/react
```
</TabItem>
<TabItem value="yarn" label="Yarn">
```bash
yarn add @logto/react
```
</TabItem>
<TabItem value="pnpm" label="pnpm">
```bash
pnpm add @logto/react
```
</TabItem>
<TabItem value="script" label="script">
```html
<script src="https://logto.io/js/logto-sdk-react/0.1.0/logto-sdk-react.production.js" />
```
</TabItem>
<TabItem value="git" label="Git">
```bash
git clone https://github.com/logto-io/js.git
pnpm build
```
</TabItem>
</Tabs>
</Step>
<Step
title="Initiate LogtoClient"
subtitle="1 step"
index={1}
activeIndex={props.activeStepIndex}
invalidIndex={props.invalidStepIndex}
onNext={() => props.onNext(2)}
>
Add the following code to your main html file. You may need client ID and authorization domain.
```typescript
import { LogtoProvider, LogtoConfig } from '@logto/react';
import React from 'react';
//...
const App = () => {
const config: LogtoConfig = { clientId: 'foo', endpoint: 'https://your-endpoint-domain.com' };
return (
<BrowserRouter>
<LogtoProvider config={config}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/callback" element={<Callback />} />
<Route
path="/protected-resource"
element={
<RequireAuth>
<ProtectedResource />
</RequireAuth>
}
/>
</Routes>
</LogtoProvider>
</BrowserRouter>
);
};
```
</Step>
<Step
title="Sign In"
subtitle="2 steps"
index={2}
activeIndex={props.activeStepIndex}
invalidIndex={props.invalidStepIndex}
onNext={() => props.onNext(3)}
>
### Setup your login
The Logto React SDK provides you tools and hooks to quickly implement your own authorization flow. First, lets enter your redirect URI
<MultiTextInputField name="redirectUris" title="Redirect URI" onError={() => props.onError(2)} />
Add the following code to your web app
```typescript
import React from 'react';
import { useLogto } from '@logto/react';
const SignInButton = () => {
const { signIn } = useLogto();
const redirectUrl = window.location.origin + '/callback';
return <button onClick={() => signIn(redirectUrl)}>Sign In</button>;
};
export default SignInButton;
```
### Retrieve Auth Status
```typescript
import React from "react";
import { useLogto } from '@logto/react';
const App = () => {
const { isAuthenticated, signIn } = useLogto();
if !(isAuthenticated) {
return <SignInButton />
}
return <>
<AppContent />
<SignOutButton />
</>
};
```
</Step>
<Step
title="Sign Out"
subtitle="1 step"
index={3}
activeIndex={props.activeStepIndex}
invalidIndex={props.invalidStepIndex}
onNext={() => props.onNext(4)}
>
Execute signOut() methods will redirect users to the Logto sign out page. After a success sign out, all use session data and auth status will be cleared.
<MultiTextInputField name="postLogoutRedirectUris" title="Post sign out redirect URI" onError={() => props.onError(3)} />
Add the following code to your web app
```typescript
import React from 'react';
import { useLogto } from '@logto/react';
const SignOutButton = () => {
const { signOut } = useLogto();
return <button onClick={() => signOut(window.location.origin)}>Sign out</button>;
};
export default SignOutButton;
```
</Step>
<Step
title="Further Readings"
subtitle="3 steps"
index={4}
activeIndex={props.activeStepIndex}
invalidIndex={props.invalidStepIndex}
buttonText="general.done"
buttonHtmlType="submit"
>
- [SDK Documentation](https://link-url-here.org)
- [OIDC Documentation](https://link-url-here.org)
- [Calling API to fetch accessToken](https://link-url-here.org)
</Step>

View file

@ -1,45 +0,0 @@
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
## 安装 Logto SDK
<Tabs>
<TabItem value="npm" label="npm">
```bash
npm i @logto/react
```
</TabItem>
<TabItem value="yarn" label="Yarn">
```bash
yarn add @logto/react
```
</TabItem>
<TabItem value="pnpm" label="pnpm">
```bash
pnpm add @logto/react
```
</TabItem>
<TabItem value="script" label="script">
```html
<script src="https://logto.io/js/logto-sdk-react/0.1.0/logto-sdk-react.production.js" />
```
</TabItem>
<TabItem value="git" label="Git">
```bash
git clone https://github.com/logto-io/js.git
pnpm build
```
</TabItem>
</Tabs>

View file

@ -1,33 +0,0 @@
## 初始化 LogtoClient
Add the following code to your main html file. You may need client ID and authorization domain.
```tsx
import { LogtoProvider, LogtoConfig } from '@logto/react';
import React from 'react';
...
const App = () => {
const config: LogtoConfig = { clientId: 'foo', endpoint: 'https://your-endpoint-domain.com' }
return (
<BrowserRouter>
<LogtoProvider config={config}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/callback" element={<Callback />} />
<Route
path="/protected-resource"
element={
<RequireAuth>
<ProtectedResource />
</RequireAuth>
}
/>
</Routes>
</LogtoProvider>
</BrowserRouter>
);
};
```

View file

@ -1,45 +0,0 @@
## 登录
### Setup your login
The Logto React SDK provides you tools and hooks to quickly implement your own authorization flow. First, lets enter your redirect URI
```redirectUris
Redirect URI
```
Add the following code to your web app
```tsx
import React from "react";
import { useLogto } from '@logto/react';
const SignInButton = () => {
const { signIn } = useLogto();
const redirectUrl = window.location.origin + '/callback';
return <button onClick={() => signIn(redirectUrl)}>Sign In</button>;
};
export default SignInButton;
```
### Retrieve Auth Status
```tsx
import React from "react";
import { useLogto } from '@logto/react';
const App = () => {
const { isAuthenticated, signIn } = useLogto();
if !(isAuthenticated) {
return <SignInButton />
}
return <>
<AppContent />
<SignOutButton />
</>
};
```

View file

@ -1,24 +0,0 @@
## 登出
Execute signOut() methods will redirect users to the Logto sign out page. After a success sign out, all use session data and auth status will be cleared.
```postLogoutRedirectUris
Post sign out redirect URI
```
Add the following code to your web app
```tsx
import React from "react";
import { useLogto } from '@logto/react';
const SignOutButton = () => {
const { signOut } = useLogto();
return (
<button onClick={() => signOut(window.location.origin)}>Sign out</button>
);
};
export default SignOutButton;
```

View file

@ -1,5 +0,0 @@
## Further Readings
- [SDK Documentation](https://link-url-here.org)
- [OIDC Documentation](https://link-url-here.org)
- [Calling API to fetch accessToken](https://link-url-here.org)

View file

@ -1,13 +1,190 @@
import Step1 from './_step-1.mdx'
import Step2 from './_step-2.md'
import Step3 from './_step-3.md'
import Step4 from './_step-4.md'
import Step5 from './_step-5.md'
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
# 集成 `@logto/react`
<Step1 />
<Step2 />
<Step3 />
<Step4 />
<Step5 />
<Step
title="安装 SDK"
subtitle="选择您熟悉的安装方式"
index={0}
activeIndex={props.activeStepIndex}
invalidIndex={props.invalidStepIndex}
onNext={() => props.onNext(1)}
>
<Tabs>
<TabItem value="npm" label="npm">
```bash
npm i @logto/react
```
</TabItem>
<TabItem value="yarn" label="Yarn">
```bash
yarn add @logto/react
```
</TabItem>
<TabItem value="pnpm" label="pnpm">
```bash
pnpm add @logto/react
```
</TabItem>
<TabItem value="script" label="script">
```html
<script src="https://logto.io/js/logto-sdk-react/0.1.0/logto-sdk-react.production.js" />
```
</TabItem>
<TabItem value="git" label="Git">
```bash
git clone https://github.com/logto-io/js.git
pnpm build
```
</TabItem>
</Tabs>
</Step>
<Step
title="Initiate LogtoClient"
subtitle="1 step"
index={1}
activeIndex={props.activeStepIndex}
invalidIndex={props.invalidStepIndex}
onNext={() => props.onNext(2)}
>
在项目的 html 文件中,加入如下代码(需提前准备好 client ID 以及 authorization domain
Add the following code to your main html file. You may need client ID and authorization domain.
```typescript
import { LogtoProvider, LogtoConfig } from '@logto/react';
import React from 'react';
//...
const App = () => {
const config: LogtoConfig = { clientId: 'foo', endpoint: 'https://your-endpoint-domain.com' };
return (
<BrowserRouter>
<LogtoProvider config={config}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/callback" element={<Callback />} />
<Route
path="/protected-resource"
element={
<RequireAuth>
<ProtectedResource />
</RequireAuth>
}
/>
</Routes>
</LogtoProvider>
</BrowserRouter>
);
};
```
</Step>
<Step
title="Sign In"
subtitle="2 steps"
index={2}
activeIndex={props.activeStepIndex}
invalidIndex={props.invalidStepIndex}
onNext={() => props.onNext(3)}
>
### Setup your login
The Logto React SDK provides you tools and hooks to quickly implement your own authorization flow. First, lets enter your redirect URI
<MultiTextInputField name="redirectUris" title="Redirect URI" onError={() => props.onError(2)} />
Add the following code to your web app
```typescript
import React from 'react';
import { useLogto } from '@logto/react';
const SignInButton = () => {
const { signIn } = useLogto();
const redirectUrl = window.location.origin + '/callback';
return <button onClick={() => signIn(redirectUrl)}>Sign In</button>;
};
export default SignInButton;
```
### Retrieve Auth Status
```typescript
import React from "react";
import { useLogto } from '@logto/react';
const App = () => {
const { isAuthenticated, signIn } = useLogto();
if !(isAuthenticated) {
return <SignInButton />
}
return <>
<AppContent />
<SignOutButton />
</>
};
```
</Step>
<Step
title="Sign Out"
subtitle="1 step"
index={3}
activeIndex={props.activeStepIndex}
invalidIndex={props.invalidStepIndex}
onNext={() => props.onNext(4)}
>
Execute signOut() methods will redirect users to the Logto sign out page. After a success sign out, all use session data and auth status will be cleared.
<MultiTextInputField name="postLogoutRedirectUris" title="Post sign out redirect URI" onError={() => props.onError(3)} />
Add the following code to your web app
```typescript
import React from 'react';
import { useLogto } from '@logto/react';
const SignOutButton = () => {
const { signOut } = useLogto();
return <button onClick={() => signOut(window.location.origin)}>Sign out</button>;
};
export default SignOutButton;
```
</Step>
<Step
title="Further Readings"
subtitle="3 steps"
index={4}
activeIndex={props.activeStepIndex}
invalidIndex={props.invalidStepIndex}
buttonHtmlType="submit"
>
- [SDK Documentation](https://link-url-here.org)
- [OIDC Documentation](https://link-url-here.org)
- [Calling API to fetch accessToken](https://link-url-here.org)
</Step>

View file

@ -19,8 +19,8 @@
"@logto/jest-config": "^0.1.0",
"@logto/phrases": "^0.1.0",
"@logto/schemas": "^0.1.0",
"@parcel/core": "^2.3.2",
"@parcel/transformer-sass": "^2.3.2",
"@parcel/core": "^2.5.0",
"@parcel/transformer-sass": "^2.5.0",
"@peculiar/webcrypto": "^1.3.3",
"@silverhand/eslint-config": "^0.10.2",
"@silverhand/eslint-config-react": "^0.10.3",
@ -42,7 +42,7 @@
"ky": "^0.30.0",
"libphonenumber-js": "^1.9.49",
"lint-staged": "^12.0.0",
"parcel": "^2.3.2",
"parcel": "^2.5.0",
"postcss": "^8.4.6",
"postcss-modules": "^4.3.0",
"prettier": "^2.3.2",

View file

@ -23,8 +23,7 @@
inset: 0;
}
// React modal animation
/* stylelint-disable selector-class-pattern */
/* stylelint-disable-next-line selector-pseudo-class-no-unknown */
:global {
.ReactModal__Content[role='popup'] {
@ -32,7 +31,6 @@
transition: transform 0.3s ease-in-out;
}
/* stylelint-disable selector-class-pattern */
.ReactModal__Content--after-open[role='popup'] {
transform: translateY(0);
}
@ -40,5 +38,6 @@
.ReactModal__Content--before-close[role='popup'] {
transform: translateY(100%);
}
/* stylelint-enable selector-class-pattern */
}
/* stylelint-enable selector-class-pattern */

File diff suppressed because it is too large Load diff