diff --git a/packages/console/src/assets/docs/tutorial/integrate-sdk/android.mdx b/packages/console/src/assets/docs/tutorial/integrate-sdk/android.mdx index 88c6d82aa..b3f191229 100644 --- a/packages/console/src/assets/docs/tutorial/integrate-sdk/android.mdx +++ b/packages/console/src/assets/docs/tutorial/integrate-sdk/android.mdx @@ -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: @@ -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` + props.onError(2)} /> + ### Configure Logto Android SDK @@ -132,7 +143,7 @@ Notes: props.onNext(1)} +> + +### 前提条件 +* 最小 Android SDK 版本: Level 24 + +将 `mavenCentral()` 添加到构建脚本中: +```kotlin +repositories { + mavenCentral() +} +``` + +添加 Logto Android SDK 依赖: + + + + +```kotlin +dependencies { + implementation("io.logto.sdk:android:1.0.0") +} +``` + + + + + +```groovy +dependencies { + implementation 'io.logto.sdk:android:1.0.0' +} +``` + + + + + + + + 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` + + props.onError(1)} /> + +### 配置 Logto Android SDK + + + + + +```kotlin +import io.logto.sdk.android.LogtoClient +import io.logto.sdk.android.type.LogtoConfig + +class MainActivity : AppCompatActivity() { + val logtoConfig = LogtoConfig( + endpoint = "", + appId = "", + scopes = null, + resources = null, + usingPersistStorage = true, + ) + + val logtoClient = LogtoClient(logtoConfig, application) +} +``` + + + + + +```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( + "", + "", + null, + null, + true + ); + + logtoClient = new LogtoClient(logtoConfig, getApplication()); + } +} +``` + + + + + +Notes: + +- `` 是你运行 Logto 服务所在的地址. 若你的 Logto 服务运行在 `http://localhost:300`,则 `` 是你为自己的应用在管理员控制台中所创建的客户端ID. + + + + props.onNext(3)} +> + +### 执行登录 + + + + + +```kotlin +logtoClient.signInWithBrowser( + this, + "io.logto.android://io.logto.sample/callback", +) { logtoException: LogtoException? -> + // 后续处理逻辑 +} +``` + + + + + +```java +logtoClient.signInWithBrowser( + this, + "io.logto.android://io.logto.sample/callback", + logtoException -> { + // 后续处理逻辑 + } +); +``` + + + + + +### 登录成功后,你可以使用 SDK 提供的一些 API 来实现自己的业务逻辑 + + + + + +```kotlin +logtoClient.getAccessToken { logtoException: LogtoException?, result: AccessToken? -> + // 后续处理逻辑 +} + +logtoClient.getIdTokenClaims { logtoException: LogtoException?, result: IdTokenClaims? -> + // 后续处理逻辑 +} + +logtoClient.fetchUserInfo { logtoException: LogtoException?, userInfoResponse: UserInfoResponse? -> + // 后续处理逻辑 +} +``` + + + + + +```java +logtoClient.getAccessToken((logtoException, accessToken) -> { + // 后续处理逻辑 +}); + +logtoClient.getIdTokenClaims((logtoException, idTokenClaims) -> { + // 后续处理逻辑 +}); + +logtoClient.fetchUserInfo((logtoException, userInfoResponse) -> { + // 后续处理逻辑 +}); +``` + + + + + + + + props.onNext(4)} +> + +### 执行登出 + + + + + +```kotlin +logtoClient.signOut { logtoException: LogtoException? -> + // 后续处理逻辑 +} +``` + + + + + +```java +logtoClient.signOut(logtoException -> { + // 后续处理逻辑 +}); +``` + + + + + +注意: + +- 登出操作会清除本地存储的用户相关凭据,即使在登出过程中发生了异常。 + + + + + +- 获取用户信息 +- 配置社会化登录 +- 访问受保护的 API 资源 + + diff --git a/packages/console/src/assets/docs/tutorial/integrate-sdk/ios.mdx b/packages/console/src/assets/docs/tutorial/integrate-sdk/ios.mdx new file mode 100644 index 000000000..9e0f016f2 --- /dev/null +++ b/packages/console/src/assets/docs/tutorial/integrate-sdk/ios.mdx @@ -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'; + + 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. + + + Carthage + Carthage needs a `xcodeproj` file to build, 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. + + + + CocoaPods + CocoaPods does not support local dependency and monorepo, thus it's hard to create a `.podspec` for this repo. + + + + props.onNext(2)} +> + +```swift +import Logto + +let config = try? LogtoConfig( + endpoint: "", + appId: "" +) +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 +) +``` + + + props.onNext(3)} +> + +First, let’s configure your redirect URI + + props.onError(2)} /> + +```swift +do { + try await client.signInWithBrowser(redirectUri: "") + print(client.isAuthenticated) // true +} catch let error as LogtoClientErrors.SignIn { + // error occured during sign in +} +``` + + + props.onNext(4)} +> + +Calling `.signOut()` will clean all the Logto data in Keychain, if it has. + +```swift +await client.signOut() +``` + + + + +- [SDK Documentation](https://link-url-here.org) +- [OIDC Documentation](https://link-url-here.org) +- [Calling API to fetch accessToken](https://link-url-here.org) + + diff --git a/packages/console/src/assets/docs/tutorial/integrate-sdk/vue.mdx b/packages/console/src/assets/docs/tutorial/integrate-sdk/vue.mdx index 867a43bb9..2cf0bfc51 100644 --- a/packages/console/src/assets/docs/tutorial/integrate-sdk/vue.mdx +++ b/packages/console/src/assets/docs/tutorial/integrate-sdk/vue.mdx @@ -43,7 +43,6 @@ pnpm add @logto/vue - - - signOut('http://localhost:1234'); ``` - - - - signOut('http://localhost:1234'); ``` - { {createdApp && ( void; onComplete: (data: GuideForm) => Promise; }; const Guides: Record 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.React - ); +const GuideModal = ({ appName, appType, isOpen, onClose, onComplete }: Props) => { + const sdks = applicationTypeAndSdkTypeMappings[appType]; + const [selectedSdk, setSelectedSdk] = useState(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({ 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); }} /> - {cloneElement(, { + {cloneElement(, { className: styles.banner, - onChange: setSubtype, + onChange: setSelectedSdk, onToggle: () => { setActiveStepIndex(0); }, diff --git a/packages/console/src/pages/Applications/components/LibrarySelector/index.tsx b/packages/console/src/pages/Applications/components/LibrarySelector/index.tsx deleted file mode 100644 index 45289e9f5..000000000 --- a/packages/console/src/pages/Applications/components/LibrarySelector/index.tsx +++ /dev/null @@ -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( - () => ( - - - - {t('applications.guide.title')} - {t('applications.guide.subtitle')} - - - {Object.values(SupportedJavascriptLibraries).map((library) => ( - - {library} - - ))} - - - { - setIsFolded(true); - onToggle?.(); - }} - /> - - - ), - [className, libraryName, onChange, onToggle, t] - ); - - const librarySelectorFolded = useMemo( - () => ( - - - {t('applications.guide.description_by_library', { library: libraryName })} - - ), - [className, libraryName, t] - ); - - return isFolded ? librarySelectorFolded : librarySelector; -}; - -export default LibrarySelector; diff --git a/packages/console/src/pages/Applications/components/LibrarySelector/index.module.scss b/packages/console/src/pages/Applications/components/SdkSelector/index.module.scss similarity index 100% rename from packages/console/src/pages/Applications/components/LibrarySelector/index.module.scss rename to packages/console/src/pages/Applications/components/SdkSelector/index.module.scss diff --git a/packages/console/src/pages/Applications/components/SdkSelector/index.tsx b/packages/console/src/pages/Applications/components/SdkSelector/index.tsx new file mode 100644 index 000000000..8ad26cd09 --- /dev/null +++ b/packages/console/src/pages/Applications/components/SdkSelector/index.tsx @@ -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 ( + + + {t('applications.guide.description_by_sdk', { sdk: selectedSdk })} + + ); + } + + return ( + + + + {t('applications.guide.title')} + {t('applications.guide.subtitle')} + + + {sdks.length > 1 && + sdks.map((sdk) => ( + + {sdk} + + ))} + + + { + setIsFolded(true); + onToggle?.(); + }} + /> + + + ); +}; + +export default SdkSelector; diff --git a/packages/console/src/types/applications.ts b/packages/console/src/types/applications.ts index 0cfaf24c9..465b99acf 100644 --- a/packages/console/src/types/applications.ts +++ b/packages/console/src/types/applications.ts @@ -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); diff --git a/packages/phrases/src/locales/en.ts b/packages/phrases/src/locales/en.ts index 8fc06a1da..1a6c3718b 100644 --- a/packages/phrases/src/locales/en.ts +++ b/packages/phrases/src/locales/en.ts @@ -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: { diff --git a/packages/phrases/src/locales/zh-cn.ts b/packages/phrases/src/locales/zh-cn.ts index bb88723bc..bee7ca915 100644 --- a/packages/phrases/src/locales/zh-cn.ts +++ b/packages/phrases/src/locales/zh-cn.ts @@ -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: {