0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-02-24 22:05:56 -05:00

Merge pull request #941 from logto-io/charles-log-2588-add-integration-guide-for-native-sdks

refactor(console): refactor integration guide to support native SDKs
This commit is contained in:
Charles Zhao 2022-05-25 16:00:00 +08:00 committed by GitHub
commit 95e394ba1c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 521 additions and 110 deletions

View file

@ -1,3 +1,4 @@
import MultiTextInputField from '@mdx/components/MultiTextInputField';
import Step from '@mdx/components/Step';
import Tabs from '@mdx/components/Tabs';
import TabItem from '@mdx/components/TabItem';
@ -12,8 +13,16 @@ import TabItem from '@mdx/components/TabItem';
>
### Prerequisite
* Minimal Android SDK: Level 24 (TBD)
* Minimal Android SDK: Level 24
Add the `mavenCentral()` repository to your Gradle project build file:
```kotlin
repositories {
mavenCentral()
}
```
Add the Logto Android SDK to your dependencies:
<Tabs>
<TabItem value="kotlin" label="Kotlin">
@ -55,7 +64,7 @@ The `Redirect URI` indicates the endpoint that the application used to receive a
### Configure Redirect URI in Admin Console
Add the following value to Redirect URIs on the application settings page.
Add the following value to Redirect URIs input field
```bash
$(LOGTO_REDIRECT_SCHEME)://$(YOUR_APP_PACKAGE)/callback
@ -67,6 +76,8 @@ Notes:
e.g. `io.logto.android://io.logto.sample/callback`
<MultiTextInputField name="redirectUris" title="Redirect URI" onError={() => props.onError(2)} />
### Configure Logto Android SDK
<Tabs>
@ -132,7 +143,7 @@ Notes:
<Step
title="Sign In"
subtitle="Sin In to your application by Logto and do some extra works"
subtitle="Sign In to your application by Logto and do some extra works"
index={2}
activeIndex={props.activeStepIndex}
invalidIndex={props.invalidStepIndex}

View file

@ -0,0 +1,284 @@
import MultiTextInputField from '@mdx/components/MultiTextInputField';
import Step from '@mdx/components/Step';
import Tabs from '@mdx/components/Tabs';
import TabItem from '@mdx/components/TabItem';
<Step
title="安装 SDK"
subtitle="从 Gradle 安装 Logto Android SDK"
index={0}
activeIndex={props.activeStepIndex}
invalidIndex={props.invalidStepIndex}
onNext={() => props.onNext(1)}
>
### 前提条件
* 最小 Android SDK 版本: Level 24
将 `mavenCentral()` 添加到构建脚本中:
```kotlin
repositories {
mavenCentral()
}
```
添加 Logto Android SDK 依赖:
<Tabs>
<TabItem value="kotlin" label="Kotlin">
```kotlin
dependencies {
implementation("io.logto.sdk:android:1.0.0")
}
```
</TabItem>
<TabItem value="groovy" label="Groovy">
```groovy
dependencies {
implementation 'io.logto.sdk:android:1.0.0'
}
```
</TabItem>
</Tabs>
</Step>
<Step
title="配置"
subtitle="Configure your application and LogtoClient"
index={1}
activeIndex={props.activeStepIndex}
invalidIndex={props.invalidStepIndex}
onNext={() => props.onNext(2)}
>
### 配置 Redirect URI
Redirect URI 指定了应用用来接受授权结果的端口Logto Android SDK 内部实现了该重定向的功能
### 在管理员控制台中配置 Redirect URI
将以下值添加到下面的 Redirect URI 输入框中:
```bash
$(LOGTO_REDIRECT_SCHEME)://$(YOUR_APP_PACKAGE)/callback
```
注意:
- `LOGTO_REDIRECT_SCHEME` 应为自定义的反向域名格式的一串字符。
- 将上述的 `$(LOGTO_REDIRECT_SCHEME)` 替换成你定义的值。
例: `io.logto.android://io.logto.sample/callback`
<MultiTextInputField name="redirectUris" title="Redirect URI" onError={() => props.onError(1)} />
### 配置 Logto Android SDK
<Tabs>
<TabItem value="kotlin" label="Kotlin">
```kotlin
import io.logto.sdk.android.LogtoClient
import io.logto.sdk.android.type.LogtoConfig
class MainActivity : AppCompatActivity() {
val logtoConfig = LogtoConfig(
endpoint = "<your-logto-endpoint>",
appId = "<your-app-id>",
scopes = null,
resources = null,
usingPersistStorage = true,
)
val logtoClient = LogtoClient(logtoConfig, application)
}
```
</TabItem>
<TabItem value="java" label="Java">
```java
import io.logto.sdk.android.LogtoClient;
import io.logto.sdk.android.type.LogtoConfig;
public class MainActivity extends AppCompatActivity {
private LogtoClient logtoClient;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
LogtoConfig logtoConfig = new LogtoConfig(
"<your-logto-endpoint>",
"<your-app-id>",
null,
null,
true
);
logtoClient = new LogtoClient(logtoConfig, getApplication());
}
}
```
</TabItem>
</Tabs>
Notes:
- `<your-logto-endpoint>` 是你运行 Logto 服务所在的地址. 若你的 Logto 服务运行在 `http://localhost:300`,则 `<your-logto-endpoint` 为 `http://localhost:300`
- `<your-app-id>` 是你为自己的应用在管理员控制台中所创建的客户端ID.
</Step>
<Step
title="登录"
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)}
>
### 执行登录
<Tabs>
<TabItem value="kotlin" label="Kotlin">
```kotlin
logtoClient.signInWithBrowser(
this,
"io.logto.android://io.logto.sample/callback",
) { logtoException: LogtoException? ->
// 后续处理逻辑
}
```
</TabItem>
<TabItem value="java" label="Java">
```java
logtoClient.signInWithBrowser(
this,
"io.logto.android://io.logto.sample/callback",
logtoException -> {
// 后续处理逻辑
}
);
```
</TabItem>
</Tabs>
### 登录成功后,你可以使用 SDK 提供的一些 API 来实现自己的业务逻辑
<Tabs>
<TabItem value="kotlin" label="Kotlin">
```kotlin
logtoClient.getAccessToken { logtoException: LogtoException?, result: AccessToken? ->
// 后续处理逻辑
}
logtoClient.getIdTokenClaims { logtoException: LogtoException?, result: IdTokenClaims? ->
// 后续处理逻辑
}
logtoClient.fetchUserInfo { logtoException: LogtoException?, userInfoResponse: UserInfoResponse? ->
// 后续处理逻辑
}
```
</TabItem>
<TabItem value="java" label="Java">
```java
logtoClient.getAccessToken((logtoException, accessToken) -> {
// 后续处理逻辑
});
logtoClient.getIdTokenClaims((logtoException, idTokenClaims) -> {
// 后续处理逻辑
});
logtoClient.fetchUserInfo((logtoException, userInfoResponse) -> {
// 后续处理逻辑
});
```
</TabItem>
</Tabs>
</Step>
<Step
title="登出"
index={3}
activeIndex={props.activeStepIndex}
invalidIndex={props.invalidStepIndex}
onNext={() => props.onNext(4)}
>
### 执行登出
<Tabs>
<TabItem value="kotlin" label="Kotlin">
```kotlin
logtoClient.signOut { logtoException: LogtoException? ->
// 后续处理逻辑
}
```
</TabItem>
<TabItem value="java" label="Java">
```java
logtoClient.signOut(logtoException -> {
// 后续处理逻辑
});
```
</TabItem>
</Tabs>
注意:
- 登出操作会清除本地存储的用户相关凭据,即使在登出过程中发生了异常。
</Step>
<Step
title="延伸阅读"
subtitle="3 steps"
index={4}
activeIndex={props.activeStepIndex}
invalidIndex={props.invalidStepIndex}
buttonText="general.done"
buttonHtmlType="submit"
>
- 获取用户信息
- 配置社会化登录
- 访问受保护的 API 资源
</Step>

View file

@ -0,0 +1,118 @@
import MultiTextInputField from '@mdx/components/MultiTextInputField';
import Step from '@mdx/components/Step';
import Tabs from '@mdx/components/Tabs';
import TabItem from '@mdx/components/TabItem';
<Step
title="Install SDK"
subtitle="Add Logto SDK as a Dependency"
index={0}
activeIndex={props.activeStepIndex}
invalidIndex={props.invalidStepIndex}
onNext={() => props.onNext(1)}
>
Use the following URL to add Logto SDK as a dependency in Swift Package Manager.
```bash
https://github.com/logto-io/swift.git
```
Since Xcode 11, you can [directly import a swift package](https://developer.apple.com/documentation/swift_packages/adding_package_dependencies_to_your_app) w/o any additional tool.
We do not support **Carthage** and **CocoaPods** at the time due to some technical issues.
<details>
<summary>Carthage</summary>
Carthage <a href="https://github.com/Carthage/Carthage/issues/1226#issuecomment-290931385">needs a `xcodeproj` file to build</a>, but `swift package generate-xcodeproj` will report a failure since we are using binary targets for native social plugins. We will try to find a workaround later.
</details>
<details>
<summary>CocoaPods</summary>
CocoaPods <a href="https://github.com/CocoaPods/CocoaPods/issues/3276">does not support local dependency</a> and monorepo, thus it's hard to create a `.podspec` for this repo.
</details>
</Step>
<Step
title="Init LogtoClient"
index={1}
activeIndex={props.activeStepIndex}
invalidIndex={props.invalidStepIndex}
onNext={() => props.onNext(2)}
>
```swift
import Logto
let config = try? LogtoConfig(
endpoint: "<your-logto-endpoint>",
appId: "<your-application-id>"
)
let logtoClient = LogtoClient(useConfig: config)
```
By default, we store credentials like ID Token and Refresh Token in Keychain. Thus the user doesn't need to sign in again when he returns.
To turn off this behavior, set `usingPersistStorage` to `false`:
```swift
let config = try? LogtoConfig(
// ...
usingPersistStorage: false
)
```
</Step>
<Step
title="Sign In"
index={2}
activeIndex={props.activeStepIndex}
invalidIndex={props.invalidStepIndex}
onNext={() => props.onNext(3)}
>
First, lets configure your redirect URI
<MultiTextInputField name="redirectUris" title="Redirect URI" onError={() => props.onError(2)} />
```swift
do {
try await client.signInWithBrowser(redirectUri: "<your-redirect-uri>")
print(client.isAuthenticated) // true
} catch let error as LogtoClientErrors.SignIn {
// error occured during sign in
}
```
</Step>
<Step
title="Sign Out"
subtitle="1 step"
index={3}
activeIndex={props.activeStepIndex}
invalidIndex={props.invalidStepIndex}
onNext={() => props.onNext(4)}
>
Calling `.signOut()` will clean all the Logto data in Keychain, if it has.
```swift
await client.signOut()
```
</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

@ -43,7 +43,6 @@ pnpm add @logto/vue
</TabItem>
</Tabs>
</Step>
<Step
title="Initiate LogtoClient"
subtitle="1 step"
@ -70,7 +69,6 @@ app.mount("#app");
```
</Step>
<Step
title="Sign In"
subtitle="2 steps"
@ -149,7 +147,6 @@ const { isAuthenticated } = useLogto();
```
</Step>
<Step
title="Sign Out"
subtitle="1 step"
@ -178,7 +175,6 @@ const onClickSignOut = () => signOut('http://localhost:1234');
```
</Step>
<Step
title="Further Readings"
subtitle="3 steps"

View file

@ -43,7 +43,6 @@ pnpm add @logto/vue
</TabItem>
</Tabs>
</Step>
<Step
title="初始化 LogtoClient"
subtitle="1 step"
@ -70,7 +69,6 @@ app.mount("#app");
```
</Step>
<Step
title="Sign In"
subtitle="2 steps"
@ -150,7 +148,6 @@ const { isAuthenticated } = useLogto();
```
</Step>
<Step
title="Sign Out"
subtitle="1 step"
@ -178,7 +175,6 @@ const onClickSignOut = () => signOut('http://localhost:1234');
```
</Step>
<Step
title="延伸阅读"
subtitle="3 steps"

View file

@ -130,6 +130,7 @@ const CreateForm = ({ onClose }: Props) => {
{createdApp && (
<GuideModal
appName={createdApp.name}
appType={createdApp.type}
isOpen={isGetStartedModalOpen}
onClose={closeModal}
onComplete={onComplete}

View file

@ -1,3 +1,4 @@
import { ApplicationType } from '@logto/schemas';
import { MDXProvider } from '@mdx-js/react';
import i18next from 'i18next';
import { MDXProps } from 'mdx/types';
@ -13,23 +14,29 @@ import IconButton from '@/components/IconButton';
import Spacer from '@/components/Spacer';
import Close from '@/icons/Close';
import * as modalStyles from '@/scss/modal.module.scss';
import { SupportedJavascriptLibraries } from '@/types/applications';
import { applicationTypeAndSdkTypeMappings, SupportedSdk } from '@/types/applications';
import { GuideForm } from '@/types/guide';
import LibrarySelector from '../LibrarySelector';
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')),
};
@ -39,16 +46,15 @@ const onClickFetchSampleProject = (projectName: string) => {
window.open(sampleUrl, '_blank');
};
const GuideModal = ({ appName, isOpen, onClose, onComplete }: Props) => {
const [subtype, setSubtype] = useState<SupportedJavascriptLibraries>(
SupportedJavascriptLibraries.React
);
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 = `${subtype}_${locale}`.toLowerCase();
const GuideComponent = Guides[guideI18nKey] ?? Guides[subtype];
const guideI18nKey = `${selectedSdk}_${locale}`.toLowerCase();
const GuideComponent = Guides[guideI18nKey] ?? Guides[selectedSdk.toLowerCase()];
const methods = useForm<GuideForm>({ mode: 'onSubmit', reValidateMode: 'onChange' });
const {
@ -82,16 +88,16 @@ const GuideModal = ({ appName, isOpen, onClose, onComplete }: Props) => {
type="outline"
title="admin_console.applications.guide.get_sample_file"
onClick={() => {
onClickFetchSampleProject(subtype);
onClickFetchSampleProject(selectedSdk);
}}
/>
</div>
<div className={styles.content}>
<FormProvider {...methods}>
<form onSubmit={onSubmit}>
{cloneElement(<LibrarySelector libraryName={subtype} />, {
{cloneElement(<SdkSelector sdks={sdks} selectedSdk={selectedSdk} />, {
className: styles.banner,
onChange: setSubtype,
onChange: setSelectedSdk,
onToggle: () => {
setActiveStepIndex(0);
},

View file

@ -1,79 +0,0 @@
import classNames from 'classnames';
import React, { useState, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import congrats from '@/assets/images/congrats.svg';
import tada from '@/assets/images/tada.svg';
import Button from '@/components/Button';
import Card from '@/components/Card';
import RadioGroup, { Radio } from '@/components/RadioGroup';
import { SupportedJavascriptLibraries } from '@/types/applications';
import * as styles from './index.module.scss';
type Props = {
className?: string;
libraryName?: SupportedJavascriptLibraries;
onChange?: (value: string) => void;
onToggle?: () => void;
};
const LibrarySelector = ({
className,
libraryName = SupportedJavascriptLibraries.React,
onChange,
onToggle,
}: Props) => {
const [isFolded, setIsFolded] = useState(false);
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const librarySelector = useMemo(
() => (
<Card className={classNames(styles.card, className)}>
<img src={congrats} alt="success" />
<div>
<div className={styles.title}>{t('applications.guide.title')}</div>
<div className={styles.subtitle}>{t('applications.guide.subtitle')}</div>
</div>
<RadioGroup
className={styles.radioGroup}
name="libraryName"
value={libraryName}
type="card"
onChange={onChange}
>
{Object.values(SupportedJavascriptLibraries).map((library) => (
<Radio key={library} className={styles.radio} value={library}>
{library}
</Radio>
))}
</RadioGroup>
<div className={styles.buttonWrapper}>
<Button
type="primary"
title="general.next"
onClick={() => {
setIsFolded(true);
onToggle?.();
}}
/>
</div>
</Card>
),
[className, libraryName, onChange, onToggle, t]
);
const librarySelectorFolded = useMemo(
() => (
<div className={classNames(styles.card, styles.folded, className)}>
<img src={tada} alt="Tada!" />
<span>{t('applications.guide.description_by_library', { library: libraryName })}</span>
</div>
),
[className, libraryName, t]
);
return isFolded ? librarySelectorFolded : librarySelector;
};
export default LibrarySelector;

View file

@ -0,0 +1,70 @@
import classNames from 'classnames';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import congrats from '@/assets/images/congrats.svg';
import tada from '@/assets/images/tada.svg';
import Button from '@/components/Button';
import Card from '@/components/Card';
import RadioGroup, { Radio } from '@/components/RadioGroup';
import { SupportedSdk } from '@/types/applications';
import * as styles from './index.module.scss';
type Props = {
className?: string;
sdks: readonly SupportedSdk[];
selectedSdk: SupportedSdk;
onChange?: (value: string) => void;
onToggle?: () => void;
};
const SdkSelector = ({ className, sdks, selectedSdk, onChange, onToggle }: Props) => {
const [isFolded, setIsFolded] = useState(false);
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
if (isFolded) {
return (
<div className={classNames(styles.card, styles.folded, className)}>
<img src={tada} alt="Tada!" />
<span>{t('applications.guide.description_by_sdk', { sdk: selectedSdk })}</span>
</div>
);
}
return (
<Card className={classNames(styles.card, className)}>
<img src={congrats} alt="success" />
<div>
<div className={styles.title}>{t('applications.guide.title')}</div>
<div className={styles.subtitle}>{t('applications.guide.subtitle')}</div>
</div>
<RadioGroup
className={styles.radioGroup}
name="selectedSdk"
value={selectedSdk}
type="card"
onChange={onChange}
>
{sdks.length > 1 &&
sdks.map((sdk) => (
<Radio key={sdk} className={styles.radio} value={sdk}>
{sdk}
</Radio>
))}
</RadioGroup>
<div className={styles.buttonWrapper}>
<Button
type="primary"
title="general.next"
onClick={() => {
setIsFolded(true);
onToggle?.();
}}
/>
</div>
</Card>
);
};
export default SdkSelector;

View file

@ -6,8 +6,17 @@ export const applicationTypeI18nKey = Object.freeze({
[ApplicationType.Traditional]: 'applications.type.traditional',
} as const);
export enum SupportedJavascriptLibraries {
Angular = 'angular',
React = 'react',
Vue = 'vue',
export enum SupportedSdk {
iOS = 'iOS',
Android = 'Android',
Angular = 'Angular',
React = 'React',
Vue = 'Vue',
Traditional = 'Traditional',
}
export const applicationTypeAndSdkTypeMappings = Object.freeze({
[ApplicationType.Native]: [SupportedSdk.iOS, SupportedSdk.Android],
[ApplicationType.SPA]: [SupportedSdk.Angular, SupportedSdk.React, SupportedSdk.Vue],
[ApplicationType.Traditional]: [SupportedSdk.Traditional],
} as const);

View file

@ -169,9 +169,8 @@ const translation = {
'Follow a step by step guide to integrate your application or get a sample configured with your account settings',
title: 'Congratulations! The application has been created successfully.',
subtitle:
'Now follow the steps below to finish your app settings. Please select the JS library to continue.',
description_by_library:
'This quickstart demonstrates how to add Logto to {{library}} application.',
'Now follow the steps below to finish your app settings. Please select the SDK type to continue.',
description_by_sdk: 'This quickstart demonstrates how to add Logto to {{sdk}} application.',
},
},
application_details: {

View file

@ -168,8 +168,8 @@ const translation = {
header_description:
'参考如下教程,将 Logto 集成到您的应用中。您也可以点击右侧链接,获取我们为您准备好的示范工程。',
title: '恭喜!您的应用已成功创建。',
subtitle: '请参考以下步骤完成您的应用设置。首先,请选择您要使用的 Javascript 框架',
description_by_library: '本教程向您演示如何在 {{library}} 应用中集成 Logto 登录功能',
subtitle: '请参考以下步骤完成您的应用设置。首先,请选择您要使用的 SDK 类型',
description_by_sdk: '本教程向您演示如何在 {{sdk}} 应用中集成 Logto 登录功能',
},
},
application_details: {