mirror of
https://github.com/logto-io/logto.git
synced 2025-03-24 22:41:28 -05:00
refactor(console): application sdk integration guide
This commit is contained in:
parent
4905a5d72f
commit
477b5988a9
30 changed files with 467 additions and 389 deletions
|
@ -8,8 +8,7 @@ import TabItem from '@mdx/components/TabItem';
|
|||
subtitle="Install Logto Android SDK with Gradle"
|
||||
index={0}
|
||||
activeIndex={props.activeStepIndex}
|
||||
invalidIndex={props.invalidStepIndex}
|
||||
onNext={() => props.onNext(1)}
|
||||
onButtonClick={() => props.onNext(1)}
|
||||
>
|
||||
|
||||
### Prerequisite
|
||||
|
@ -54,8 +53,7 @@ dependencies {
|
|||
subtitle="Configure your application and LogtoClient"
|
||||
index={1}
|
||||
activeIndex={props.activeStepIndex}
|
||||
invalidIndex={props.invalidStepIndex}
|
||||
onNext={() => props.onNext(2)}
|
||||
onButtonClick={() => props.onNext(2)}
|
||||
>
|
||||
|
||||
### Configure Redirect URI
|
||||
|
@ -76,7 +74,7 @@ Notes:
|
|||
|
||||
e.g. `io.logto.android://io.logto.sample/callback`
|
||||
|
||||
<MultiTextInputField name="redirectUris" title="Redirect URI" onError={() => props.onError(2)} />
|
||||
<MultiTextInputField name="redirectUris" title="Redirect URI" />
|
||||
|
||||
### Configure Logto Android SDK
|
||||
|
||||
|
@ -146,8 +144,7 @@ Notes:
|
|||
subtitle="Sign In to your application by Logto and do some extra works"
|
||||
index={2}
|
||||
activeIndex={props.activeStepIndex}
|
||||
invalidIndex={props.invalidStepIndex}
|
||||
onNext={() => props.onNext(3)}
|
||||
onButtonClick={() => props.onNext(3)}
|
||||
>
|
||||
|
||||
### Perform a signing-in action
|
||||
|
@ -231,8 +228,7 @@ logtoClient.fetchUserInfo((logtoException, userInfoResponse) -> {
|
|||
title="Sign Out"
|
||||
index={3}
|
||||
activeIndex={props.activeStepIndex}
|
||||
invalidIndex={props.invalidStepIndex}
|
||||
onNext={() => props.onNext(4)}
|
||||
onButtonClick={() => props.onNext(4)}
|
||||
>
|
||||
|
||||
### Perform a signing-out action
|
||||
|
@ -272,9 +268,8 @@ Notes:
|
|||
subtitle="3 steps"
|
||||
index={4}
|
||||
activeIndex={props.activeStepIndex}
|
||||
invalidIndex={props.invalidStepIndex}
|
||||
buttonText="general.done"
|
||||
buttonHtmlType="submit"
|
||||
onButtonClick={props.onComplete}
|
||||
>
|
||||
|
||||
- Fetch User Info
|
||||
|
|
|
@ -8,8 +8,7 @@ import TabItem from '@mdx/components/TabItem';
|
|||
subtitle="从 Gradle 安装 Logto Android SDK"
|
||||
index={0}
|
||||
activeIndex={props.activeStepIndex}
|
||||
invalidIndex={props.invalidStepIndex}
|
||||
onNext={() => props.onNext(1)}
|
||||
onButtonClick={() => props.onNext(1)}
|
||||
>
|
||||
|
||||
### 前提条件
|
||||
|
@ -54,8 +53,7 @@ dependencies {
|
|||
subtitle="Configure your application and LogtoClient"
|
||||
index={1}
|
||||
activeIndex={props.activeStepIndex}
|
||||
invalidIndex={props.invalidStepIndex}
|
||||
onNext={() => props.onNext(2)}
|
||||
onButtonClick={() => props.onNext(2)}
|
||||
>
|
||||
|
||||
### 配置 Redirect URI
|
||||
|
@ -76,7 +74,7 @@ $(LOGTO_REDIRECT_SCHEME)://$(YOUR_APP_PACKAGE)/callback
|
|||
|
||||
例: `io.logto.android://io.logto.sample/callback`
|
||||
|
||||
<MultiTextInputField name="redirectUris" title="Redirect URI" onError={() => props.onError(1)} />
|
||||
<MultiTextInputField name="redirectUris" title="Redirect URI" />
|
||||
|
||||
### 配置 Logto Android SDK
|
||||
|
||||
|
@ -146,8 +144,7 @@ Notes:
|
|||
subtitle="Sign In to your application by Logto and do some extra works"
|
||||
index={2}
|
||||
activeIndex={props.activeStepIndex}
|
||||
invalidIndex={props.invalidStepIndex}
|
||||
onNext={() => props.onNext(3)}
|
||||
onButtonClick={() => props.onNext(3)}
|
||||
>
|
||||
|
||||
### 执行登录
|
||||
|
@ -231,8 +228,7 @@ logtoClient.fetchUserInfo((logtoException, userInfoResponse) -> {
|
|||
title="登出"
|
||||
index={3}
|
||||
activeIndex={props.activeStepIndex}
|
||||
invalidIndex={props.invalidStepIndex}
|
||||
onNext={() => props.onNext(4)}
|
||||
onButtonClick={() => props.onNext(4)}
|
||||
>
|
||||
|
||||
### 执行登出
|
||||
|
@ -272,9 +268,8 @@ logtoClient.signOut(logtoException -> {
|
|||
subtitle="3 steps"
|
||||
index={4}
|
||||
activeIndex={props.activeStepIndex}
|
||||
invalidIndex={props.invalidStepIndex}
|
||||
buttonText="general.done"
|
||||
buttonHtmlType="submit"
|
||||
onButtonClick={props.onComplete}
|
||||
>
|
||||
|
||||
- 获取用户信息
|
||||
|
|
|
@ -8,8 +8,7 @@ import TabItem from '@mdx/components/TabItem';
|
|||
subtitle="Add Logto SDK as a Dependency"
|
||||
index={0}
|
||||
activeIndex={props.activeStepIndex}
|
||||
invalidIndex={props.invalidStepIndex}
|
||||
onNext={() => props.onNext(1)}
|
||||
onButtonClick={() => props.onNext(1)}
|
||||
>
|
||||
|
||||
Use the following URL to add Logto SDK as a dependency in Swift Package Manager.
|
||||
|
@ -37,8 +36,7 @@ We do not support **Carthage** and **CocoaPods** at the time due to some technic
|
|||
title="Init LogtoClient"
|
||||
index={1}
|
||||
activeIndex={props.activeStepIndex}
|
||||
invalidIndex={props.invalidStepIndex}
|
||||
onNext={() => props.onNext(2)}
|
||||
onButtonClick={() => props.onNext(2)}
|
||||
>
|
||||
|
||||
```swift
|
||||
|
@ -67,13 +65,12 @@ let config = try? LogtoConfig(
|
|||
title="Sign In"
|
||||
index={2}
|
||||
activeIndex={props.activeStepIndex}
|
||||
invalidIndex={props.invalidStepIndex}
|
||||
onNext={() => props.onNext(3)}
|
||||
onButtonClick={() => props.onNext(3)}
|
||||
>
|
||||
|
||||
First, let’s configure your redirect URI
|
||||
|
||||
<MultiTextInputField name="redirectUris" title="Redirect URI" onError={() => props.onError(2)} />
|
||||
<MultiTextInputField name="redirectUris" title="Redirect URI" />
|
||||
|
||||
```swift
|
||||
do {
|
||||
|
@ -90,8 +87,7 @@ do {
|
|||
subtitle="1 step"
|
||||
index={3}
|
||||
activeIndex={props.activeStepIndex}
|
||||
invalidIndex={props.invalidStepIndex}
|
||||
onNext={() => props.onNext(4)}
|
||||
onButtonClick={() => props.onNext(4)}
|
||||
>
|
||||
|
||||
Calling `.signOut()` will clean all the Logto data in Keychain, if it has.
|
||||
|
@ -106,9 +102,8 @@ await client.signOut()
|
|||
subtitle="3 steps"
|
||||
index={4}
|
||||
activeIndex={props.activeStepIndex}
|
||||
invalidIndex={props.invalidStepIndex}
|
||||
buttonText="general.done"
|
||||
buttonHtmlType="submit"
|
||||
onButtonClick={props.onComplete}
|
||||
>
|
||||
|
||||
- [SDK Documentation](https://link-url-here.org)
|
||||
|
|
|
@ -8,8 +8,7 @@ import TabItem from '@mdx/components/TabItem';
|
|||
subtitle="Please select your favorite package manager"
|
||||
index={0}
|
||||
activeIndex={props.activeStepIndex}
|
||||
invalidIndex={props.invalidStepIndex}
|
||||
onNext={() => props.onNext(1)}
|
||||
onButtonClick={() => props.onNext(1)}
|
||||
>
|
||||
<Tabs>
|
||||
<TabItem value="npm" label="npm">
|
||||
|
@ -56,8 +55,7 @@ pnpm build
|
|||
subtitle="1 step"
|
||||
index={1}
|
||||
activeIndex={props.activeStepIndex}
|
||||
invalidIndex={props.invalidStepIndex}
|
||||
onNext={() => props.onNext(2)}
|
||||
onButtonClick={() => props.onNext(2)}
|
||||
>
|
||||
|
||||
Add the following code to your main html file. You may need client ID and authorization domain.
|
||||
|
@ -71,7 +69,7 @@ import React from 'react';
|
|||
const App = () => {
|
||||
const config: LogtoConfig = {
|
||||
clientId: 'foo',
|
||||
endpoint: 'https://your-endpoint-domain.com'
|
||||
endpoint: 'https://your-endpoint-domain.com',
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -102,15 +100,18 @@ const App = () => {
|
|||
subtitle="2 steps"
|
||||
index={2}
|
||||
activeIndex={props.activeStepIndex}
|
||||
invalidIndex={props.invalidStepIndex}
|
||||
onNext={() => props.onNext(3)}
|
||||
onButtonClick={() => props.onNext(3)}
|
||||
>
|
||||
|
||||
### Setup your login
|
||||
### Step 1: Setup your login
|
||||
|
||||
The Logto React SDK provides you tools and hooks to quickly implement your own authorization flow. First, let’s enter your redirect URI
|
||||
|
||||
<MultiTextInputField name="redirectUris" title="Redirect URI" onError={() => props.onError(2)} />
|
||||
<MultiTextInputField
|
||||
appId={props.appId}
|
||||
name="redirectUris"
|
||||
title="Redirect URI"
|
||||
/>
|
||||
|
||||
Add the following code to your web app
|
||||
|
||||
|
@ -128,7 +129,7 @@ const SignInButton = () => {
|
|||
export default SignInButton;
|
||||
```
|
||||
|
||||
### Retrieve Auth Status
|
||||
### Step 2: Retrieve Auth Status
|
||||
|
||||
```tsx
|
||||
import React from "react";
|
||||
|
@ -155,13 +156,16 @@ const App = () => {
|
|||
subtitle="1 step"
|
||||
index={3}
|
||||
activeIndex={props.activeStepIndex}
|
||||
invalidIndex={props.invalidStepIndex}
|
||||
onNext={() => props.onNext(4)}
|
||||
onButtonClick={() => 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)} />
|
||||
<MultiTextInputField
|
||||
appId={props.appId}
|
||||
name="postLogoutRedirectUris"
|
||||
title="Post sign out redirect URI"
|
||||
/>
|
||||
|
||||
Add the following code to your web app
|
||||
|
||||
|
@ -185,9 +189,8 @@ export default SignOutButton;
|
|||
subtitle="3 steps"
|
||||
index={4}
|
||||
activeIndex={props.activeStepIndex}
|
||||
invalidIndex={props.invalidStepIndex}
|
||||
buttonText="general.done"
|
||||
buttonHtmlType="submit"
|
||||
onButtonClick={props.onComplete}
|
||||
>
|
||||
|
||||
- [SDK Documentation](https://link-url-here.org)
|
||||
|
|
|
@ -8,8 +8,7 @@ import TabItem from '@mdx/components/TabItem';
|
|||
subtitle="选择您熟悉的安装方式"
|
||||
index={0}
|
||||
activeIndex={props.activeStepIndex}
|
||||
invalidIndex={props.invalidStepIndex}
|
||||
onNext={() => props.onNext(1)}
|
||||
onButtonClick={() => props.onNext(1)}
|
||||
>
|
||||
<Tabs>
|
||||
<TabItem value="npm" label="npm">
|
||||
|
@ -56,8 +55,7 @@ pnpm build
|
|||
subtitle="1 step"
|
||||
index={1}
|
||||
activeIndex={props.activeStepIndex}
|
||||
invalidIndex={props.invalidStepIndex}
|
||||
onNext={() => props.onNext(2)}
|
||||
onButtonClick={() => props.onNext(2)}
|
||||
>
|
||||
|
||||
在项目的 html 文件中,加入如下代码(需提前准备好 client ID 以及 authorization domain)
|
||||
|
@ -103,15 +101,14 @@ const App = () => {
|
|||
subtitle="2 steps"
|
||||
index={2}
|
||||
activeIndex={props.activeStepIndex}
|
||||
invalidIndex={props.invalidStepIndex}
|
||||
onNext={() => props.onNext(3)}
|
||||
onButtonClick={() => props.onNext(3)}
|
||||
>
|
||||
|
||||
### Setup your login
|
||||
### Step 1: Setup your login
|
||||
|
||||
The Logto React SDK provides you tools and hooks to quickly implement your own authorization flow. First, let’s enter your redirect URI
|
||||
|
||||
<MultiTextInputField name="redirectUris" title="Redirect URI" onError={() => props.onError(2)} />
|
||||
<MultiTextInputField name="redirectUris" title="Redirect URI" />
|
||||
|
||||
Add the following code to your web app
|
||||
|
||||
|
@ -129,7 +126,7 @@ const SignInButton = () => {
|
|||
export default SignInButton;
|
||||
```
|
||||
|
||||
### Retrieve Auth Status
|
||||
### Step 2: Retrieve Auth Status
|
||||
|
||||
```tsx
|
||||
import React from "react";
|
||||
|
@ -156,13 +153,12 @@ const App = () => {
|
|||
subtitle="1 step"
|
||||
index={3}
|
||||
activeIndex={props.activeStepIndex}
|
||||
invalidIndex={props.invalidStepIndex}
|
||||
onNext={() => props.onNext(4)}
|
||||
onButtonClick={() => 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)} />
|
||||
<MultiTextInputField name="postLogoutRedirectUris" title="Post sign out redirect URI" />
|
||||
|
||||
Add the following code to your web app
|
||||
|
||||
|
@ -186,8 +182,7 @@ export default SignOutButton;
|
|||
subtitle="3 steps"
|
||||
index={4}
|
||||
activeIndex={props.activeStepIndex}
|
||||
invalidIndex={props.invalidStepIndex}
|
||||
buttonHtmlType="submit"
|
||||
onButtonClick={props.onComplete}
|
||||
>
|
||||
|
||||
- [SDK Documentation](https://link-url-here.org)
|
||||
|
|
|
@ -8,8 +8,7 @@ import TabItem from '@mdx/components/TabItem';
|
|||
subtitle="Please select your favorite package manager"
|
||||
index={0}
|
||||
activeIndex={props.activeStepIndex}
|
||||
invalidIndex={props.invalidStepIndex}
|
||||
onNext={() => props.onNext(1)}
|
||||
onButtonClick={() => props.onNext(1)}
|
||||
>
|
||||
<Tabs>
|
||||
<TabItem value="npm" label="npm">
|
||||
|
@ -48,8 +47,7 @@ pnpm add @logto/vue
|
|||
subtitle="1 step"
|
||||
index={1}
|
||||
activeIndex={props.activeStepIndex}
|
||||
invalidIndex={props.invalidStepIndex}
|
||||
onNext={() => props.onNext(2)}
|
||||
onButtonClick={() => props.onNext(2)}
|
||||
>
|
||||
|
||||
`import` and use `createLogto` to install Logto plugin:
|
||||
|
@ -74,8 +72,7 @@ app.mount("#app");
|
|||
subtitle="2 steps"
|
||||
index={2}
|
||||
activeIndex={props.activeStepIndex}
|
||||
invalidIndex={props.invalidStepIndex}
|
||||
onNext={() => props.onNext(3)}
|
||||
onButtonClick={() => props.onNext(3)}
|
||||
>
|
||||
|
||||
We provide two composables `useHandleSignInCallback()` and `useLogto()` which can help you easily manage the authentication flow.
|
||||
|
@ -86,7 +83,7 @@ In order to handle what comes from Logto, the application needs to have a dedica
|
|||
|
||||
First, let’s enter your redirect URI. E.g. `http://localhost:1234/callback`
|
||||
|
||||
<MultiTextInputField name="redirectUris" title="Redirect URI" onError={() => props.onError(2)} />
|
||||
<MultiTextInputField name="redirectUris" title="Redirect URI" />
|
||||
|
||||
Then let's create a callback component:
|
||||
|
||||
|
@ -152,8 +149,7 @@ const { isAuthenticated } = useLogto();
|
|||
subtitle="1 step"
|
||||
index={3}
|
||||
activeIndex={props.activeStepIndex}
|
||||
invalidIndex={props.invalidStepIndex}
|
||||
onNext={() => props.onNext(4)}
|
||||
onButtonClick={() => props.onNext(4)}
|
||||
>
|
||||
|
||||
Calling `.signOut()` will clear all the Logto data in memory and LocalStorage, if there is any.
|
||||
|
@ -161,7 +157,7 @@ Calling `.signOut()` will clear all the Logto data in memory and LocalStorage, i
|
|||
To make the user come back to your application after signing out,
|
||||
it's necessary to add `http://localhost:1234` as one of the Post Sign Out URIs and use the URL as the parameter when calling `.signOut()`.
|
||||
|
||||
<MultiTextInputField name="postLogoutRedirectUris" title="Post sign out redirect URI" onError={() => props.onError(3)} />
|
||||
<MultiTextInputField name="postLogoutRedirectUris" title="Post sign out redirect URI" />
|
||||
|
||||
```ts
|
||||
import { useLogto } from "@logto/vue";
|
||||
|
@ -180,9 +176,8 @@ const onClickSignOut = () => signOut('http://localhost:1234');
|
|||
subtitle="3 steps"
|
||||
index={4}
|
||||
activeIndex={props.activeStepIndex}
|
||||
invalidIndex={props.invalidStepIndex}
|
||||
buttonText="general.done"
|
||||
buttonHtmlType="submit"
|
||||
onButtonClick={props.onComplete}
|
||||
>
|
||||
|
||||
- [SDK Documentation](https://link-url-here.org)
|
||||
|
|
|
@ -8,8 +8,7 @@ import TabItem from '@mdx/components/TabItem';
|
|||
subtitle="请选择你喜欢的包管理工具"
|
||||
index={0}
|
||||
activeIndex={props.activeStepIndex}
|
||||
invalidIndex={props.invalidStepIndex}
|
||||
onNext={() => props.onNext(1)}
|
||||
onButtonClick={() => props.onNext(1)}
|
||||
>
|
||||
<Tabs>
|
||||
<TabItem value="npm" label="npm">
|
||||
|
@ -48,8 +47,7 @@ pnpm add @logto/vue
|
|||
subtitle="1 step"
|
||||
index={1}
|
||||
activeIndex={props.activeStepIndex}
|
||||
invalidIndex={props.invalidStepIndex}
|
||||
onNext={() => props.onNext(2)}
|
||||
onButtonClick={() => props.onNext(2)}
|
||||
>
|
||||
|
||||
`import` 并使用 `createLogto` 以插件的形式安装 Logto:
|
||||
|
@ -74,8 +72,7 @@ app.mount("#app");
|
|||
subtitle="2 steps"
|
||||
index={2}
|
||||
activeIndex={props.activeStepIndex}
|
||||
invalidIndex={props.invalidStepIndex}
|
||||
onNext={() => props.onNext(3)}
|
||||
onButtonClick={() => props.onNext(3)}
|
||||
>
|
||||
|
||||
我们提供了两个组合式 API `useHandleSignInCallback()` 和 `useLogto()`,它们可以帮助你轻松完成登录认证流程。
|
||||
|
@ -86,7 +83,7 @@ app.mount("#app");
|
|||
|
||||
但首先, 让我们先在下方输入 redirect URI,如:`http://localhost:1234/callback`
|
||||
|
||||
<MultiTextInputField name="redirectUris" title="Redirect URI" onError={() => props.onError(2)} />
|
||||
<MultiTextInputField name="redirectUris" title="Redirect URI" />
|
||||
|
||||
然后,让我们来创建一个 CallbackView 组件:
|
||||
|
||||
|
@ -153,15 +150,14 @@ const { isAuthenticated } = useLogto();
|
|||
subtitle="1 step"
|
||||
index={3}
|
||||
activeIndex={props.activeStepIndex}
|
||||
invalidIndex={props.invalidStepIndex}
|
||||
onNext={() => props.onNext(4)}
|
||||
onButtonClick={() => props.onNext(4)}
|
||||
>
|
||||
|
||||
调用 `.signOut()` 方法会清除所有在缓存或者 localStorage 中的 Logto 数据(如果有)。
|
||||
|
||||
为了确保用户登出后能够跳转回你的应用,我们需要首先在管理界面中将 `http://localhost:1234` 添加到允许登出后跳转的地址列表(Post Sign Out URIs)中。
|
||||
|
||||
<MultiTextInputField name="postLogoutRedirectUris" title="Post sign out redirect URI" onError={() => props.onError(3)} />
|
||||
<MultiTextInputField name="postLogoutRedirectUris" title="Post sign out redirect URI" />
|
||||
|
||||
```ts
|
||||
import { useLogto } from "@logto/vue";
|
||||
|
@ -180,9 +176,8 @@ const onClickSignOut = () => signOut('http://localhost:1234');
|
|||
subtitle="3 steps"
|
||||
index={4}
|
||||
activeIndex={props.activeStepIndex}
|
||||
invalidIndex={props.invalidStepIndex}
|
||||
buttonText="general.done"
|
||||
buttonHtmlType="submit"
|
||||
onButtonClick={props.onComplete}
|
||||
>
|
||||
|
||||
- [SDK Documentation](https://link-url-here.org)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { I18nKey } from '@logto/phrases';
|
||||
import classNames from 'classnames';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import React, { KeyboardEvent, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import * as textButtonStyles from '@/components/TextButton/index.module.scss';
|
||||
|
@ -16,10 +16,11 @@ type Props = {
|
|||
title: I18nKey;
|
||||
value?: string[];
|
||||
onChange: (value: string[]) => void;
|
||||
onKeyPress?: (event: KeyboardEvent<HTMLInputElement>) => void;
|
||||
error?: MultiTextInputError;
|
||||
};
|
||||
|
||||
const MultiTextInput = ({ title, value, onChange, error }: Props) => {
|
||||
const MultiTextInput = ({ title, value, onChange, onKeyPress, error }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [deleteFieldIndex, setDeleteFieldIndex] = useState<number>();
|
||||
|
@ -58,6 +59,7 @@ const MultiTextInput = ({ title, value, onChange, error }: Props) => {
|
|||
onChange={({ currentTarget: { value } }) => {
|
||||
handleInputChange(value, fieldIndex);
|
||||
}}
|
||||
onKeyPress={onKeyPress}
|
||||
/>
|
||||
{fieldIndex > 0 && (
|
||||
<IconButton
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.select {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 _.unit(3);
|
||||
background: var(--color-layer-1);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
|
@ -36,18 +40,22 @@
|
|||
}
|
||||
|
||||
.icon {
|
||||
position: absolute;
|
||||
right: _.unit(2);
|
||||
|
||||
> svg {
|
||||
color: var(--color-icon);
|
||||
}
|
||||
display: flex;
|
||||
margin-left: _.unit(3);
|
||||
color: var(--color-icon);
|
||||
}
|
||||
|
||||
.clear {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&.clearable:hover {
|
||||
.clear {
|
||||
display: block;
|
||||
|
@ -59,18 +67,14 @@
|
|||
}
|
||||
|
||||
&.small {
|
||||
padding: _.unit(1) _.unit(4) _.unit(1) _.unit(3);
|
||||
|
||||
.icon {
|
||||
top: 2px;
|
||||
}
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
&.medium {
|
||||
padding: _.unit(2) _.unit(5) _.unit(2) _.unit(3);
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
top: 6px;
|
||||
}
|
||||
&.large {
|
||||
height: 36px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ type Option<T> = {
|
|||
};
|
||||
|
||||
type Props<T> = {
|
||||
className?: string;
|
||||
value?: T;
|
||||
options: Array<Option<T>>;
|
||||
onChange?: (value?: T) => void;
|
||||
|
@ -21,10 +22,11 @@ type Props<T> = {
|
|||
hasError?: boolean;
|
||||
placeholder?: ReactNode;
|
||||
isClearable?: boolean;
|
||||
size?: 'small' | 'medium';
|
||||
size?: 'small' | 'medium' | 'large';
|
||||
};
|
||||
|
||||
const Select = <T extends string>({
|
||||
className,
|
||||
value,
|
||||
options,
|
||||
onChange,
|
||||
|
@ -32,7 +34,7 @@ const Select = <T extends string>({
|
|||
hasError,
|
||||
placeholder,
|
||||
isClearable,
|
||||
size = 'medium',
|
||||
size = 'large',
|
||||
}: Props<T>) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const anchorRef = useRef<HTMLInputElement>(null);
|
||||
|
@ -59,7 +61,8 @@ const Select = <T extends string>({
|
|||
isOpen && styles.open,
|
||||
isReadOnly && styles.readOnly,
|
||||
hasError && styles.error,
|
||||
isClearable && value && styles.clearable
|
||||
isClearable && value && styles.clearable,
|
||||
className
|
||||
)}
|
||||
role="button"
|
||||
onClick={() => {
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.field {
|
||||
width: 556px;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
|
@ -1,64 +1,102 @@
|
|||
import { I18nKey } from '@logto/phrases';
|
||||
import { Application } from '@logto/schemas';
|
||||
import React, { useRef } from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { Controller, FormProvider, useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import FormField from '@/components/FormField';
|
||||
import MultiTextInput from '@/components/MultiTextInput';
|
||||
import { createValidatorForRhf, convertRhfErrorMessage } from '@/components/MultiTextInput/utils';
|
||||
import useApi, { RequestError } from '@/hooks/use-api';
|
||||
import { GuideForm } from '@/types/guide';
|
||||
import { uriValidator } from '@/utilities/validator';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
appId: string;
|
||||
name: 'redirectUris' | 'postLogoutRedirectUris';
|
||||
title: I18nKey;
|
||||
onError?: () => void;
|
||||
};
|
||||
|
||||
const MultiTextInputField = ({ name, title, onError }: Props) => {
|
||||
const MultiTextInputField = ({ appId, name, title }: Props) => {
|
||||
const methods = useForm<GuideForm>();
|
||||
const {
|
||||
control,
|
||||
formState: { errors },
|
||||
} = useFormContext<GuideForm>();
|
||||
getValues,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isSubmitting },
|
||||
} = methods;
|
||||
|
||||
const { data, mutate } = useSWR<Application, RequestError>(`/api/applications/${appId}`);
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const api = useApi();
|
||||
|
||||
const firstErrorKey = Object.keys(errors)[0];
|
||||
const isFirstErrorField = firstErrorKey && firstErrorKey === name;
|
||||
const onSubmit = async (value: string[]) => {
|
||||
const updatedApp = await api
|
||||
.patch(`/api/applications/${appId}`, {
|
||||
json: {
|
||||
oidcClientMetadata: {
|
||||
[name]: value.filter(Boolean),
|
||||
},
|
||||
},
|
||||
})
|
||||
.json<Application>();
|
||||
void mutate(updatedApp);
|
||||
|
||||
if (isFirstErrorField) {
|
||||
ref.current?.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
||||
onError?.();
|
||||
}
|
||||
// Reset form to set 'isDirty' to false
|
||||
reset(getValues());
|
||||
};
|
||||
|
||||
return (
|
||||
<FormField isRequired title={title}>
|
||||
<Controller
|
||||
name={name}
|
||||
control={control}
|
||||
defaultValue={[]}
|
||||
rules={{
|
||||
validate: createValidatorForRhf({
|
||||
required: t('errors.required_field_missing_plural', { field: title }),
|
||||
pattern: {
|
||||
verify: (value) => !value || uriValidator(value),
|
||||
message: t('errors.invalid_uri_format'),
|
||||
},
|
||||
}),
|
||||
}}
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||
<div ref={ref}>
|
||||
<MultiTextInput
|
||||
title={title}
|
||||
value={value}
|
||||
error={convertRhfErrorMessage(error?.message)}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormProvider {...methods}>
|
||||
<form>
|
||||
<FormField isRequired className={styles.field} title={title}>
|
||||
<Controller
|
||||
name={name}
|
||||
control={control}
|
||||
defaultValue={data?.oidcClientMetadata[name]}
|
||||
rules={{
|
||||
validate: createValidatorForRhf({
|
||||
required: t('errors.required_field_missing_plural', { field: title }),
|
||||
pattern: {
|
||||
verify: (value) => !value || uriValidator(value),
|
||||
message: t('errors.invalid_uri_format'),
|
||||
},
|
||||
}),
|
||||
}}
|
||||
render={({ field: { onChange, value }, fieldState: { error, isDirty } }) => (
|
||||
<div ref={ref} className={styles.wrapper}>
|
||||
<MultiTextInput
|
||||
title={title}
|
||||
value={value}
|
||||
error={convertRhfErrorMessage(error?.message)}
|
||||
onChange={onChange}
|
||||
onKeyPress={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
void handleSubmit(async () => onSubmit(value))();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
disabled={!isDirty}
|
||||
isLoading={isSubmitting}
|
||||
title="general.save"
|
||||
type="primary"
|
||||
onClick={handleSubmit(async () => onSubmit(value))}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -29,15 +29,40 @@
|
|||
|
||||
.content {
|
||||
margin-top: 0;
|
||||
max-height: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
|
||||
&.expanded {
|
||||
margin-top: _.unit(6);
|
||||
max-height: 9999px;
|
||||
height: auto;
|
||||
overflow: unset;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-inline-start: 1ch;
|
||||
}
|
||||
|
||||
li {
|
||||
list-style-type: '-';
|
||||
margin-block-end: _.unit(6);
|
||||
padding-inline-start: _.unit(1.5);
|
||||
}
|
||||
|
||||
a {
|
||||
font: var(--font-body-medium);
|
||||
color: var(--color-text-link);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font: var(--font-title-medium);
|
||||
color: var(--color-caption);
|
||||
margin: _.unit(6) 0;
|
||||
}
|
||||
|
||||
p {
|
||||
font: var(--font-body-medium);
|
||||
margin: _.unit(6) 0;
|
||||
}
|
||||
}
|
||||
|
||||
.card + .card {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { I18nKey } from '@logto/phrases';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import classNames from 'classnames';
|
||||
import React, { PropsWithChildren, useEffect, useRef, useState } from 'react';
|
||||
|
||||
|
@ -19,10 +18,8 @@ type Props = PropsWithChildren<{
|
|||
subtitle?: string;
|
||||
index: number;
|
||||
activeIndex: number;
|
||||
invalidIndex?: number;
|
||||
buttonText?: I18nKey;
|
||||
buttonHtmlType: 'submit' | 'button';
|
||||
onNext?: () => void;
|
||||
onButtonClick?: () => void;
|
||||
}>;
|
||||
|
||||
const Step = ({
|
||||
|
@ -31,22 +28,19 @@ const Step = ({
|
|||
subtitle,
|
||||
index,
|
||||
activeIndex,
|
||||
invalidIndex,
|
||||
buttonText = 'general.next',
|
||||
buttonHtmlType = 'button',
|
||||
onNext,
|
||||
onButtonClick,
|
||||
}: Props) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const isActive = index === activeIndex;
|
||||
const isComplete = index < activeIndex;
|
||||
const isInvalid = index === invalidIndex;
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isActive || isInvalid) {
|
||||
if (isActive) {
|
||||
setIsExpanded(true);
|
||||
}
|
||||
}, [isActive, isInvalid]);
|
||||
}, [isActive]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isExpanded) {
|
||||
|
@ -79,12 +73,7 @@ const Step = ({
|
|||
<div className={classNames(styles.content, isExpanded && styles.expanded)}>
|
||||
{children}
|
||||
<div className={styles.buttonWrapper}>
|
||||
<Button
|
||||
htmlType={buttonHtmlType}
|
||||
type="primary"
|
||||
title={buttonText}
|
||||
onClick={conditional(onNext)}
|
||||
/>
|
||||
<Button type="outline" size="large" title={buttonText} onClick={onButtonClick} />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
.container {
|
||||
width: 100%;
|
||||
margin-top: _.unit(6);
|
||||
|
||||
ul {
|
||||
border-bottom: 1px solid var(--color-divider);
|
||||
|
@ -15,6 +16,8 @@
|
|||
padding-bottom: _.unit(1);
|
||||
font: var(--font-subhead-2);
|
||||
color: var(--color-caption);
|
||||
margin-block-end: unset;
|
||||
padding-inline-start: unset;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import { Application, ApplicationType } from '@logto/schemas';
|
|||
import React, { useState } from 'react';
|
||||
import { useController, useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Modal from 'react-modal';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import FormField from '@/components/FormField';
|
||||
|
@ -10,10 +11,10 @@ import RadioGroup, { Radio } from '@/components/RadioGroup';
|
|||
import TextInput from '@/components/TextInput';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import useSettings from '@/hooks/use-settings';
|
||||
import * as modalStyles from '@/scss/modal.module.scss';
|
||||
import { applicationTypeI18nKey } from '@/types/applications';
|
||||
import { GuideForm } from '@/types/guide';
|
||||
|
||||
import GuideModal from '../GuideModal';
|
||||
import Guide from '../Guide';
|
||||
import TypeDescription from '../TypeDescription';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
|
@ -55,30 +56,10 @@ const CreateForm = ({ onClose }: Props) => {
|
|||
|
||||
const createdApp = await api.post('/api/applications', { json: data }).json<Application>();
|
||||
setCreatedApp(createdApp);
|
||||
|
||||
setIsGetStartedModalOpen(true);
|
||||
void updateSettings({ createApplication: true });
|
||||
});
|
||||
|
||||
const onComplete = async (data: GuideForm) => {
|
||||
if (!createdApp) {
|
||||
return;
|
||||
}
|
||||
|
||||
const application = await api
|
||||
.patch(`/api/applications/${createdApp.id}`, {
|
||||
json: {
|
||||
oidcClientMetadata: {
|
||||
redirectUris: data.redirectUris.filter(Boolean),
|
||||
postLogoutRedirectUris: data.postLogoutRedirectUris.filter(Boolean),
|
||||
},
|
||||
},
|
||||
})
|
||||
.json<Application>();
|
||||
await updateSettings({ createApplication: true });
|
||||
setCreatedApp(application);
|
||||
closeModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalLayout
|
||||
title="applications.create"
|
||||
|
@ -128,13 +109,9 @@ const CreateForm = ({ onClose }: Props) => {
|
|||
</FormField>
|
||||
</form>
|
||||
{createdApp && (
|
||||
<GuideModal
|
||||
appName={createdApp.name}
|
||||
appType={createdApp.type}
|
||||
isOpen={isGetStartedModalOpen}
|
||||
onClose={closeModal}
|
||||
onComplete={onComplete}
|
||||
/>
|
||||
<Modal isOpen={isGetStartedModalOpen} className={modalStyles.fullScreen}>
|
||||
<Guide app={createdApp} onClose={closeModal} />
|
||||
</Modal>
|
||||
)}
|
||||
</ModalLayout>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--color-base);
|
||||
height: 100vh;
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
overflow-y: auto;
|
||||
padding: _.unit(6) _.unit(6) _.unit(20);
|
||||
|
||||
> * {
|
||||
max-width: 858px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: _.unit(6);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.markdownContent {
|
||||
margin-top: _.unit(6);
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
import { Application } from '@logto/schemas';
|
||||
import { MDXProvider } from '@mdx-js/react';
|
||||
import i18next from 'i18next';
|
||||
import { MDXProps } from 'mdx/types';
|
||||
import React, { cloneElement, lazy, LazyExoticComponent, Suspense, useState } from 'react';
|
||||
|
||||
import CodeEditor from '@/components/CodeEditor';
|
||||
import { applicationTypeAndSdkTypeMappings, SupportedSdk } from '@/types/applications';
|
||||
|
||||
import GuideHeader from '../GuideHeader';
|
||||
import SdkSelector from '../SdkSelector';
|
||||
import StepsSkeleton from '../StepsSkeleton';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
app: Application;
|
||||
isCompact?: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const Guides: Record<string, LazyExoticComponent<(props: MDXProps) => JSX.Element>> = {
|
||||
ios: lazy(async () => import('@/assets/docs/tutorial/integrate-sdk/ios.mdx')),
|
||||
android: lazy(async () => import('@/assets/docs/tutorial/integrate-sdk/android.mdx')),
|
||||
react: lazy(async () => import('@/assets/docs/tutorial/integrate-sdk/react.mdx')),
|
||||
vue: lazy(async () => import('@/assets/docs/tutorial/integrate-sdk/vue.mdx')),
|
||||
'android_zh-cn': lazy(
|
||||
async () => import('@/assets/docs/tutorial/integrate-sdk/android_zh-cn.mdx')
|
||||
),
|
||||
'react_zh-cn': lazy(async () => import('@/assets/docs/tutorial/integrate-sdk/react_zh-cn.mdx')),
|
||||
'vue_zh-cn': lazy(async () => import('@/assets/docs/tutorial/integrate-sdk/vue_zh-cn.mdx')),
|
||||
};
|
||||
|
||||
const Guide = ({ app, isCompact, onClose }: Props) => {
|
||||
const { id: appId, name: appName, type: appType } = app;
|
||||
const sdks = applicationTypeAndSdkTypeMappings[appType];
|
||||
const [selectedSdk, setSelectedSdk] = useState<SupportedSdk>(sdks[0]);
|
||||
const [activeStepIndex, setActiveStepIndex] = useState(-1);
|
||||
|
||||
const locale = i18next.language;
|
||||
const guideI18nKey = `${selectedSdk}_${locale}`.toLowerCase();
|
||||
const GuideComponent = Guides[guideI18nKey] ?? Guides[selectedSdk.toLowerCase()];
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<GuideHeader
|
||||
appName={appName}
|
||||
selectedSdk={selectedSdk}
|
||||
isCompact={isCompact}
|
||||
onClose={onClose}
|
||||
/>
|
||||
<div className={styles.content}>
|
||||
{cloneElement(<SdkSelector sdks={sdks} selectedSdk={selectedSdk} />, {
|
||||
className: styles.banner,
|
||||
isCompact,
|
||||
onChange: setSelectedSdk,
|
||||
onToggle: () => {
|
||||
setActiveStepIndex(0);
|
||||
},
|
||||
})}
|
||||
<MDXProvider
|
||||
components={{
|
||||
code: ({ className, children }) => {
|
||||
const [, language] = /language-(\w+)/.exec(className ?? '') ?? [];
|
||||
|
||||
return <CodeEditor isReadonly language={language} value={String(children)} />;
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Suspense fallback={<StepsSkeleton />}>
|
||||
{GuideComponent && (
|
||||
<GuideComponent
|
||||
appId={appId}
|
||||
activeStepIndex={activeStepIndex}
|
||||
onNext={(nextIndex: number) => {
|
||||
setActiveStepIndex(nextIndex);
|
||||
}}
|
||||
onComplete={onClose}
|
||||
/>
|
||||
)}
|
||||
</Suspense>
|
||||
</MDXProvider>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Guide;
|
|
@ -0,0 +1,23 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: var(--color-layer-1);
|
||||
height: 64px;
|
||||
padding: 0 _.unit(6);
|
||||
|
||||
.separator {
|
||||
@include _.vertical-bar;
|
||||
height: 20px;
|
||||
margin: 0 _.unit(5) 0 _.unit(4);
|
||||
}
|
||||
|
||||
.closeIcon {
|
||||
color: var(--color-icon);
|
||||
}
|
||||
|
||||
.getSampleButton {
|
||||
margin-right: _.unit(15);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
import React from 'react';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import CardTitle from '@/components/CardTitle';
|
||||
import DangerousRaw from '@/components/DangerousRaw';
|
||||
import IconButton from '@/components/IconButton';
|
||||
import Spacer from '@/components/Spacer';
|
||||
import Close from '@/icons/Close';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
appName: string;
|
||||
selectedSdk: string;
|
||||
isCompact?: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const onClickFetchSampleProject = (projectName: string) => {
|
||||
const sampleUrl = `https://github.com/logto-io/js/tree/master/packages/${projectName}-sample`;
|
||||
window.open(sampleUrl, '_blank');
|
||||
};
|
||||
|
||||
const GuideHeader = ({ appName, selectedSdk, isCompact = false, onClose }: Props) => {
|
||||
return (
|
||||
<div className={styles.header}>
|
||||
{isCompact && (
|
||||
<>
|
||||
<CardTitle
|
||||
size="small"
|
||||
title={<DangerousRaw>{appName}</DangerousRaw>}
|
||||
subtitle="applications.guide.header_description"
|
||||
/>
|
||||
<Spacer />
|
||||
<IconButton size="large" onClick={onClose}>
|
||||
<Close className={styles.closeIcon} />
|
||||
</IconButton>
|
||||
</>
|
||||
)}
|
||||
{!isCompact && (
|
||||
<>
|
||||
<IconButton size="large" onClick={onClose}>
|
||||
<Close className={styles.closeIcon} />
|
||||
</IconButton>
|
||||
<div className={styles.separator} />
|
||||
<CardTitle
|
||||
size="small"
|
||||
title={<DangerousRaw>{appName}</DangerousRaw>}
|
||||
subtitle="applications.guide.header_description"
|
||||
/>
|
||||
<Spacer />
|
||||
<Button type="plain" size="small" title="general.skip" onClick={onClose} />
|
||||
<Button
|
||||
className={styles.getSampleButton}
|
||||
type="outline"
|
||||
title="admin_console.applications.guide.get_sample_file"
|
||||
onClick={() => {
|
||||
onClickFetchSampleProject(selectedSdk);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GuideHeader;
|
|
@ -1,51 +0,0 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--color-base);
|
||||
height: 100vh;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: var(--color-layer-1);
|
||||
height: 64px;
|
||||
padding: 0 _.unit(21) 0 _.unit(2);
|
||||
|
||||
button {
|
||||
margin-left: _.unit(4);
|
||||
}
|
||||
|
||||
.separator {
|
||||
@include _.vertical-bar;
|
||||
height: 20px;
|
||||
margin: 0 _.unit(5) 0 _.unit(4);
|
||||
}
|
||||
|
||||
.closeIcon {
|
||||
color: var(--color-icon);
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
overflow-y: auto;
|
||||
padding: _.unit(6) 0 _.unit(20);
|
||||
|
||||
> * {
|
||||
width: 858px;
|
||||
}
|
||||
|
||||
.banner {
|
||||
margin-bottom: _.unit(6);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.markdownContent {
|
||||
margin-top: _.unit(6);
|
||||
}
|
|
@ -1,137 +0,0 @@
|
|||
import { ApplicationType } from '@logto/schemas';
|
||||
import { MDXProvider } from '@mdx-js/react';
|
||||
import i18next from 'i18next';
|
||||
import { MDXProps } from 'mdx/types';
|
||||
import React, { cloneElement, lazy, LazyExoticComponent, Suspense, useState } from 'react';
|
||||
import { useForm, FormProvider } from 'react-hook-form';
|
||||
import Modal from 'react-modal';
|
||||
|
||||
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 * as modalStyles from '@/scss/modal.module.scss';
|
||||
import { applicationTypeAndSdkTypeMappings, SupportedSdk } from '@/types/applications';
|
||||
import { GuideForm } from '@/types/guide';
|
||||
|
||||
import SdkSelector from '../SdkSelector';
|
||||
import StepsSkeleton from '../StepsSkeleton';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
appName: string;
|
||||
appType: ApplicationType;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onComplete: (data: GuideForm) => Promise<void>;
|
||||
};
|
||||
|
||||
const Guides: Record<string, LazyExoticComponent<(props: MDXProps) => JSX.Element>> = {
|
||||
ios: lazy(async () => import('@/assets/docs/tutorial/integrate-sdk/ios.mdx')),
|
||||
android: lazy(async () => import('@/assets/docs/tutorial/integrate-sdk/android.mdx')),
|
||||
react: lazy(async () => import('@/assets/docs/tutorial/integrate-sdk/react.mdx')),
|
||||
vue: lazy(async () => import('@/assets/docs/tutorial/integrate-sdk/vue.mdx')),
|
||||
'android_zh-cn': lazy(
|
||||
async () => import('@/assets/docs/tutorial/integrate-sdk/android_zh-cn.mdx')
|
||||
),
|
||||
'react_zh-cn': lazy(async () => import('@/assets/docs/tutorial/integrate-sdk/react_zh-cn.mdx')),
|
||||
'vue_zh-cn': lazy(async () => import('@/assets/docs/tutorial/integrate-sdk/vue_zh-cn.mdx')),
|
||||
};
|
||||
|
||||
const onClickFetchSampleProject = (projectName: string) => {
|
||||
const sampleUrl = `https://github.com/logto-io/js/tree/master/packages/${projectName}-sample`;
|
||||
window.open(sampleUrl, '_blank');
|
||||
};
|
||||
|
||||
const GuideModal = ({ appName, appType, isOpen, onClose, onComplete }: Props) => {
|
||||
const sdks = applicationTypeAndSdkTypeMappings[appType];
|
||||
const [selectedSdk, setSelectedSdk] = useState<SupportedSdk>(sdks[0]);
|
||||
const [activeStepIndex, setActiveStepIndex] = useState(-1);
|
||||
const [invalidStepIndex, setInvalidStepIndex] = useState(-1);
|
||||
|
||||
const locale = i18next.language;
|
||||
const guideI18nKey = `${selectedSdk}_${locale}`.toLowerCase();
|
||||
const GuideComponent = Guides[guideI18nKey] ?? Guides[selectedSdk.toLowerCase()];
|
||||
|
||||
const methods = useForm<GuideForm>({ mode: 'onSubmit', reValidateMode: 'onChange' });
|
||||
const {
|
||||
formState: { isSubmitting },
|
||||
handleSubmit,
|
||||
} = methods;
|
||||
|
||||
const onSubmit = handleSubmit((data) => {
|
||||
if (isSubmitting) {
|
||||
return;
|
||||
}
|
||||
void onComplete(data);
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} className={modalStyles.fullScreen}>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
<IconButton size="large" onClick={onClose}>
|
||||
<Close className={styles.closeIcon} />
|
||||
</IconButton>
|
||||
<div className={styles.separator} />
|
||||
<CardTitle
|
||||
size="small"
|
||||
title={<DangerousRaw>{appName}</DangerousRaw>}
|
||||
subtitle="applications.guide.header_description"
|
||||
/>
|
||||
<Spacer />
|
||||
<Button type="plain" size="small" title="general.skip" onClick={onClose} />
|
||||
<Button
|
||||
type="outline"
|
||||
title="admin_console.applications.guide.get_sample_file"
|
||||
onClick={() => {
|
||||
onClickFetchSampleProject(selectedSdk);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<FormProvider {...methods}>
|
||||
<form onSubmit={onSubmit}>
|
||||
{cloneElement(<SdkSelector sdks={sdks} selectedSdk={selectedSdk} />, {
|
||||
className: styles.banner,
|
||||
onChange: setSelectedSdk,
|
||||
onToggle: () => {
|
||||
setActiveStepIndex(0);
|
||||
},
|
||||
})}
|
||||
<MDXProvider
|
||||
components={{
|
||||
code: ({ className, children }) => {
|
||||
const [, language] = /language-(\w+)/.exec(className ?? '') ?? [];
|
||||
|
||||
return <CodeEditor isReadonly language={language} value={String(children)} />;
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Suspense fallback={<StepsSkeleton />}>
|
||||
{GuideComponent && (
|
||||
<GuideComponent
|
||||
activeStepIndex={activeStepIndex}
|
||||
invalidStepIndex={invalidStepIndex}
|
||||
onNext={(nextIndex: number) => {
|
||||
setActiveStepIndex(nextIndex);
|
||||
}}
|
||||
onError={(invalidIndex: number) => {
|
||||
setInvalidStepIndex(invalidIndex);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Suspense>
|
||||
</MDXProvider>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default GuideModal;
|
|
@ -31,6 +31,11 @@
|
|||
max-width: unset;
|
||||
}
|
||||
|
||||
.select {
|
||||
background: var(--color-guide-dropdown-background);
|
||||
border-color: var(--color-guide-dropdown-border);
|
||||
}
|
||||
|
||||
&.folded {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
@ -44,11 +49,12 @@
|
|||
color: var(--color-text);
|
||||
|
||||
img {
|
||||
margin-right: _.unit(3);
|
||||
margin: 0 _.unit(4) 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.buttonWrapper {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: _.unit(6);
|
||||
|
|
|
@ -7,6 +7,8 @@ import tada from '@/assets/images/tada.svg';
|
|||
import Button from '@/components/Button';
|
||||
import Card from '@/components/Card';
|
||||
import RadioGroup, { Radio } from '@/components/RadioGroup';
|
||||
import Select from '@/components/Select';
|
||||
import Spacer from '@/components/Spacer';
|
||||
import { SupportedSdk } from '@/types/applications';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
@ -15,12 +17,20 @@ type Props = {
|
|||
className?: string;
|
||||
sdks: readonly SupportedSdk[];
|
||||
selectedSdk: SupportedSdk;
|
||||
isCompact?: boolean;
|
||||
onChange?: (value: string) => void;
|
||||
onToggle?: () => void;
|
||||
};
|
||||
|
||||
const SdkSelector = ({ className, sdks, selectedSdk, onChange, onToggle }: Props) => {
|
||||
const [isFolded, setIsFolded] = useState(false);
|
||||
const SdkSelector = ({
|
||||
className,
|
||||
sdks,
|
||||
selectedSdk,
|
||||
isCompact = false,
|
||||
onChange,
|
||||
onToggle,
|
||||
}: Props) => {
|
||||
const [isFolded, setIsFolded] = useState(isCompact);
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
if (isFolded) {
|
||||
|
@ -28,6 +38,16 @@ const SdkSelector = ({ className, sdks, selectedSdk, onChange, onToggle }: Props
|
|||
<div className={classNames(styles.card, styles.folded, className)}>
|
||||
<img src={tada} alt="Tada!" />
|
||||
<span>{t('applications.guide.description_by_sdk', { sdk: selectedSdk })}</span>
|
||||
<Spacer />
|
||||
<Select
|
||||
className={styles.select}
|
||||
value={selectedSdk}
|
||||
size="medium"
|
||||
options={sdks.map((sdk) => ({ value: sdk, title: sdk }))}
|
||||
onChange={(value) => {
|
||||
onChange?.(value ?? selectedSdk);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -55,8 +75,9 @@ const SdkSelector = ({ className, sdks, selectedSdk, onChange, onToggle }: Props
|
|||
</RadioGroup>
|
||||
<div className={styles.buttonWrapper}>
|
||||
<Button
|
||||
type="primary"
|
||||
type="outline"
|
||||
title="general.next"
|
||||
size="large"
|
||||
onClick={() => {
|
||||
setIsFolded(true);
|
||||
onToggle?.();
|
||||
|
|
|
@ -125,7 +125,6 @@ const GuideModal = ({ connector, isOpen, onClose }: Props) => {
|
|||
subtitle="Lorem ipsum dolor sit amet, consectetuer adipiscing elit."
|
||||
index={0}
|
||||
activeIndex={activeStepIndex}
|
||||
buttonHtmlType="submit"
|
||||
buttonText={steps === 1 ? 'general.done' : undefined}
|
||||
>
|
||||
<Controller
|
||||
|
@ -145,9 +144,8 @@ const GuideModal = ({ connector, isOpen, onClose }: Props) => {
|
|||
subtitle="Lorem ipsum dolor sit amet, consectetuer adipiscing elit."
|
||||
index={1}
|
||||
activeIndex={activeStepIndex}
|
||||
buttonHtmlType="button"
|
||||
buttonText="general.done"
|
||||
onNext={onClose}
|
||||
onButtonClick={onClose}
|
||||
>
|
||||
<SenderTester connectorType={connectorType} />
|
||||
</Step>
|
||||
|
|
|
@ -44,7 +44,7 @@ const updateApplication = buildUpdateWhere<CreateApplication, Application>(Appli
|
|||
export const updateApplicationById = async (
|
||||
id: string,
|
||||
set: Partial<OmitAutoSetFields<CreateApplication>>
|
||||
) => updateApplication({ set, where: { id }, jsonbMode: 'replace' });
|
||||
) => updateApplication({ set, where: { id }, jsonbMode: 'merge' });
|
||||
|
||||
export const deleteApplicationById = async (id: string) => {
|
||||
const { rowCount } = await envSet.pool.query(sql`
|
||||
|
|
|
@ -75,7 +75,7 @@ export default function applicationRoutes<T extends AuthedRouter>(router: T) {
|
|||
'/applications/:id',
|
||||
koaGuard({
|
||||
params: object({ id: string().min(1) }),
|
||||
body: Applications.createGuard.omit({ id: true, createdAt: true }).partial(),
|
||||
body: Applications.createGuard.omit({ id: true, createdAt: true }).deepPartial(),
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const {
|
||||
|
|
|
@ -7,6 +7,7 @@ const translation = {
|
|||
retry: 'Try again',
|
||||
done: 'Done',
|
||||
search: 'Search',
|
||||
save: 'Save',
|
||||
save_changes: 'Save Changes',
|
||||
loading: 'Loading...',
|
||||
redirecting: 'Redirecting...',
|
||||
|
|
|
@ -9,6 +9,7 @@ const translation = {
|
|||
retry: '重试',
|
||||
done: '完成',
|
||||
search: '搜索',
|
||||
save: '保存',
|
||||
save_changes: '保存更改',
|
||||
loading: '读取中...',
|
||||
redirecting: '页面跳转中...',
|
||||
|
|
|
@ -160,6 +160,9 @@
|
|||
--color-tooltip-background: #34353f; // dark theme Surface-4
|
||||
--color-tooltip-text: var(--color-neutral-99);
|
||||
--color-overlay: rgba(0, 0, 0, 30%);
|
||||
--color-drawer-overlay: rgba(0, 0, 0, 40%);
|
||||
--color-guide-dropdown-background: var(--color-white);
|
||||
--color-guide-dropdown-border: var(--color-border);
|
||||
--color-skeleton-shimmer-rgb: 255, 255, 255; // rgb of Layer-1
|
||||
}
|
||||
|
||||
|
@ -325,5 +328,8 @@
|
|||
--color-tooltip-background: var(--color-surface-4);
|
||||
--color-tooltip-text: var(--color-neutral-10);
|
||||
--color-overlay: rgba(0, 0, 0, 30%);
|
||||
--color-drawer-overlay: rgba(0, 0, 0, 60%);
|
||||
--color-guide-dropdown-background: var(--color-neutral-variant-80);
|
||||
--color-guide-dropdown-border: var(--color-neutral-variant-70);
|
||||
--color-skeleton-shimmer-rgb: 42, 44, 50; // rgb of Layer-1
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue