refactor(console): migrate existing guides to v2 (#4330)
* refactor(console): migrate existing guides to v2 * refactor(console): update content * refactor(console): use context for uri input component
|
@ -19,9 +19,17 @@ The `README.mdx` file contains the actual guide content. The `assets` directory
|
||||||
|
|
||||||
### Create the guide directory
|
### Create the guide directory
|
||||||
|
|
||||||
The guide directory should be named `[target]-name`, where `[target]` is the target of the guide in kebab-case (see `types.ts` for a list of all targets) and `name` is the name of the guide. The name should be kebab-cased and should not contain any special characters.
|
The guide directory should be named `[target]-name`, where `[target]` is the target of the guide in kebab-case and `name` is the name of the guide. The name should be kebab-cased and should not contain any special characters.
|
||||||
|
|
||||||
For example, a guide for the `MachineToMachine` target with the name `General` should be placed in the directory `machine-to-machine-general`; a guide for the `SPA` target with the name `React` should be placed in the directory `spa-react`.
|
Currently we have the following targets:
|
||||||
|
|
||||||
|
- `spa`: Single-page application
|
||||||
|
- `web`: Web application
|
||||||
|
- `native`: Native application
|
||||||
|
- `m2m`: Machine-to-machine
|
||||||
|
- `api`: API resource
|
||||||
|
|
||||||
|
For example, a guide for the `MachineToMachine` target with the name `General` should be placed in the directory `m2m-general`; a guide for the `SPA` target with the name `React` should be placed in the directory `spa-react`.
|
||||||
|
|
||||||
> **Note**
|
> **Note**
|
||||||
> The directory name will be the unique identifier of the guide.
|
> The directory name will be the unique identifier of the guide.
|
||||||
|
|
|
@ -2,11 +2,39 @@
|
||||||
|
|
||||||
import { lazy } from 'react';
|
import { lazy } from 'react';
|
||||||
|
|
||||||
|
import nativeAndroidJava from './native-android-java/index';
|
||||||
|
import nativeAndroidKt from './native-android-kt/index';
|
||||||
|
import nativeIosSwift from './native-ios-swift/index';
|
||||||
import spaReact from './spa-react/index';
|
import spaReact from './spa-react/index';
|
||||||
|
import spaVanilla from './spa-vanilla/index';
|
||||||
|
import spaVue from './spa-vue/index';
|
||||||
import { type Guide } from './types';
|
import { type Guide } from './types';
|
||||||
import webExpress from './web-express/index';
|
import webExpress from './web-express/index';
|
||||||
|
import webGo from './web-go/index';
|
||||||
|
import webNext from './web-next/index';
|
||||||
|
|
||||||
const guides: Readonly<Guide[]> = Object.freeze([
|
const guides: Readonly<Guide[]> = Object.freeze([
|
||||||
|
{
|
||||||
|
id: 'native-android-java',
|
||||||
|
Logo: lazy(async () => import('./native-android-java/logo.svg')),
|
||||||
|
Component: lazy(async () => import('./native-android-java/README.mdx')),
|
||||||
|
metadata: nativeAndroidJava,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'native-android-kt',
|
||||||
|
Logo: lazy(async () => import('./native-android-kt/logo.svg')),
|
||||||
|
Component: lazy(async () => import('./native-android-kt/README.mdx')),
|
||||||
|
metadata: nativeAndroidKt,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'native-ios-swift',
|
||||||
|
Logo: lazy(async () => import('./native-ios-swift/logo.svg')),
|
||||||
|
Component: lazy(async () => import('./native-ios-swift/README.mdx')),
|
||||||
|
metadata: nativeIosSwift,
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
id: 'spa-react',
|
id: 'spa-react',
|
||||||
Logo: lazy(async () => import('./spa-react/logo.svg')),
|
Logo: lazy(async () => import('./spa-react/logo.svg')),
|
||||||
|
@ -14,12 +42,40 @@ const guides: Readonly<Guide[]> = Object.freeze([
|
||||||
metadata: spaReact,
|
metadata: spaReact,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'spa-vanilla',
|
||||||
|
Logo: lazy(async () => import('./spa-vanilla/logo.svg')),
|
||||||
|
Component: lazy(async () => import('./spa-vanilla/README.mdx')),
|
||||||
|
metadata: spaVanilla,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'spa-vue',
|
||||||
|
Logo: lazy(async () => import('./spa-vue/logo.svg')),
|
||||||
|
Component: lazy(async () => import('./spa-vue/README.mdx')),
|
||||||
|
metadata: spaVue,
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
id: 'web-express',
|
id: 'web-express',
|
||||||
Logo: lazy(async () => import('./web-express/logo.svg')),
|
Logo: lazy(async () => import('./web-express/logo.svg')),
|
||||||
Component: lazy(async () => import('./web-express/README.mdx')),
|
Component: lazy(async () => import('./web-express/README.mdx')),
|
||||||
metadata: webExpress,
|
metadata: webExpress,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'web-go',
|
||||||
|
Logo: lazy(async () => import('./web-go/logo.svg')),
|
||||||
|
Component: lazy(async () => import('./web-go/README.mdx')),
|
||||||
|
metadata: webGo,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'web-next',
|
||||||
|
Logo: lazy(async () => import('./web-next/logo.svg')),
|
||||||
|
Component: lazy(async () => import('./web-next/README.mdx')),
|
||||||
|
metadata: webNext,
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export default guides;
|
export default guides;
|
||||||
|
|
|
@ -0,0 +1,121 @@
|
||||||
|
import UriInputField from '@/mdx-components-v2/UriInputField';
|
||||||
|
import InlineNotification from '@/ds-components/InlineNotification';
|
||||||
|
import Steps from '@/mdx-components-v2/Steps';
|
||||||
|
import Step from '@/mdx-components-v2/Step';
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
|
||||||
|
<Step
|
||||||
|
title="Integrate Logto Android SDK"
|
||||||
|
subtitle="Add Logto SDK as a dependency"
|
||||||
|
>
|
||||||
|
|
||||||
|
<InlineNotification>The minimum supported Android API is level 24</InlineNotification>
|
||||||
|
|
||||||
|
Add the `mavenCentral()` repository to your Gradle project build file:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Add Logto Android SDK to your dependencies:
|
||||||
|
|
||||||
|
```groovy
|
||||||
|
dependencies {
|
||||||
|
implementation 'io.logto.sdk:android:1.0.0'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step
|
||||||
|
title="Init LogtoClient"
|
||||||
|
subtitle="1 step"
|
||||||
|
>
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<code className="language-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(
|
||||||
|
"${props.endpoint}",${props.alternativeEndpoint ? ` // or "${props.alternativeEndpoint}"` : ''}
|
||||||
|
"${props.app.id}",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
logtoClient = new LogtoClient(logtoConfig, getApplication());
|
||||||
|
}
|
||||||
|
}`}
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step
|
||||||
|
title="Sign in"
|
||||||
|
subtitle="2 steps"
|
||||||
|
>
|
||||||
|
|
||||||
|
### Configure Redirect URI
|
||||||
|
|
||||||
|
First, let’s configure your redirect URI. E.g. `io.logto.android://io.logto.sample/callback`
|
||||||
|
|
||||||
|
<UriInputField name="redirectUris" />
|
||||||
|
|
||||||
|
Go back to your IDE/editor, use the following code to implement sign-in:
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<code className="language-java">
|
||||||
|
{`logtoClient.signIn(this, "${
|
||||||
|
props.redirectUris[0] ?? '<your-redirect-uri>'
|
||||||
|
}", logtoException -> {
|
||||||
|
// User signed in successfully if \`logtoException\` is null.
|
||||||
|
});`}
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
After signing in successfully, `logtoClient.isAuthenticated` will be `true`.
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step
|
||||||
|
title="Sign out"
|
||||||
|
subtitle="1 step"
|
||||||
|
>
|
||||||
|
|
||||||
|
Calling `.signOut(completion)` will always clear local credentials even if errors occurred.
|
||||||
|
|
||||||
|
```java
|
||||||
|
logtoClient.signOut(logtoException -> {
|
||||||
|
// Local credentials are cleared regardless of whether `logtoException` is null.
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step
|
||||||
|
title="Further readings"
|
||||||
|
subtitle="4 articles"
|
||||||
|
>
|
||||||
|
|
||||||
|
- [Customize sign-in experience](https://docs.logto.io/docs/tutorials/get-started/customize-sign-in-experience)
|
||||||
|
- [Enable SMS or email passcode sign-in](https://docs.logto.io/docs/tutorials/get-started/passwordless-sign-in-by-adding-connectors#enable-sms-or-email-passwordless-sign-in)
|
||||||
|
- [Enable social sign-in](https://docs.logto.io/docs/tutorials/get-started/passwordless-sign-in-by-adding-connectors#enable-social-sign-in)
|
||||||
|
- [Protect your API](https://docs.logto.io/docs/recipes/protect-your-api)
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
</Steps>
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { ApplicationType } from '@logto/schemas';
|
||||||
|
|
||||||
|
import { type GuideMetadata } from '../types';
|
||||||
|
|
||||||
|
const metadata: Readonly<GuideMetadata> = Object.freeze({
|
||||||
|
name: 'Android (Java)',
|
||||||
|
description: 'Android integration for Java.',
|
||||||
|
target: ApplicationType.Native,
|
||||||
|
sample: {
|
||||||
|
repo: 'kotlin',
|
||||||
|
path: 'android-sample-java',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default metadata;
|
|
@ -0,0 +1,8 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||||
|
<path
|
||||||
|
d="M11.622 24.74s-1.23.748.855.962c2.51.32 3.847.267 6.625-.267a10.02 10.02 0 0 0 1.763.855c-6.25 2.672-14.16-.16-9.244-1.55zm-.8-3.473s-1.336 1.015.748 1.23c2.725.267 4.862.32 8.55-.427a3.26 3.26 0 0 0 1.282.801c-7.534 2.244-15.976.214-10.58-1.603zm14.747 6.09s.908.748-1.015 1.336c-3.58 1.07-15.014 1.39-18.22 0-1.122-.48 1.015-1.175 1.7-1.282.695-.16 1.07-.16 1.07-.16-1.23-.855-8.175 1.763-3.526 2.51 12.77 2.084 23.296-.908 19.983-2.404zM12.2 17.633s-5.824 1.39-2.084 1.87c1.603.214 4.755.16 7.694-.053 2.404-.214 4.81-.64 4.81-.64s-.855.374-1.443.748c-5.93 1.55-17.312.855-14.052-.748 2.778-1.336 5.076-1.175 5.076-1.175zm10.42 5.824c5.984-3.1 3.206-6.09 1.282-5.717-.48.107-.695.214-.695.214s.16-.32.534-.427c3.794-1.336 6.786 4.007-1.23 6.09 0 0 .053-.053.107-.16zm-9.83 8.442c5.77.374 14.587-.214 14.8-2.94 0 0-.427 1.07-4.755 1.87-4.916.908-11.007.8-14.587.214 0 0 .748.64 4.542.855z"
|
||||||
|
fill="#4e7896" />
|
||||||
|
<path
|
||||||
|
d="M18.996.001s3.313 3.366-3.152 8.442c-5.183 4.114-1.175 6.465 0 9.137-3.046-2.725-5.236-5.13-3.74-7.373C14.294 6.893 20.332 5.3 18.996.001zm-1.7 15.335c1.55 1.763-.427 3.366-.427 3.366s3.954-2.03 2.137-4.542c-1.656-2.404-2.94-3.58 4.007-7.587 0 0-10.953 2.725-5.717 8.763z"
|
||||||
|
fill="#f58219" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
|
@ -0,0 +1,113 @@
|
||||||
|
import UriInputField from '@/mdx-components-v2/UriInputField';
|
||||||
|
import InlineNotification from '@/ds-components/InlineNotification';
|
||||||
|
import Steps from '@/mdx-components-v2/Steps';
|
||||||
|
import Step from '@/mdx-components-v2/Step';
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
|
||||||
|
<Step
|
||||||
|
title="Integrate Logto Android SDK"
|
||||||
|
subtitle="Add Logto SDK as a dependency"
|
||||||
|
>
|
||||||
|
|
||||||
|
<InlineNotification>The minimum supported Android API is level 24</InlineNotification>
|
||||||
|
|
||||||
|
Add the `mavenCentral()` repository to your Gradle project build file:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Add Logto Android SDK to your dependencies:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
dependencies {
|
||||||
|
implementation("io.logto.sdk:android:1.0.0")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step
|
||||||
|
title="Init LogtoClient"
|
||||||
|
subtitle="1 step"
|
||||||
|
>
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<code className="language-kotlin">
|
||||||
|
{`import io.logto.sdk.android.LogtoClient
|
||||||
|
import io.logto.sdk.android.type.LogtoConfig
|
||||||
|
|
||||||
|
class MainActivity : AppCompatActivity() {
|
||||||
|
val logtoConfig = LogtoConfig(
|
||||||
|
endpoint = "${props.endpoint}",${props.alternativeEndpoint ? ` // or "${props.alternativeEndpoint}"` : ''}
|
||||||
|
appId = "${props.app.id}",
|
||||||
|
scopes = null,
|
||||||
|
resources = null,
|
||||||
|
usingPersistStorage = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
val logtoClient = LogtoClient(logtoConfig, application)
|
||||||
|
}`}
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step
|
||||||
|
title="Sign in"
|
||||||
|
subtitle="2 steps"
|
||||||
|
>
|
||||||
|
|
||||||
|
### Configure Redirect URI
|
||||||
|
|
||||||
|
First, let’s configure your redirect URI. E.g. `io.logto.android://io.logto.sample/callback`
|
||||||
|
|
||||||
|
<UriInputField name="redirectUris" />
|
||||||
|
|
||||||
|
Go back to your IDE/editor, use the following code to implement sign-in:
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<code className="language-kotlin">
|
||||||
|
{`logtoClient.signIn(this, "${
|
||||||
|
props.redirectUris[0] ?? '<your-redirect-uri>'
|
||||||
|
}") { logtoException: LogtoException? ->
|
||||||
|
// User signed in successfully if \`logtoException\` is null.
|
||||||
|
}`}
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
After signing in successfully, `logtoClient.isAuthenticated` will be `true`.
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step
|
||||||
|
title="Sign out"
|
||||||
|
subtitle="1 step"
|
||||||
|
>
|
||||||
|
|
||||||
|
Calling `.signOut(completion)` will always clear local credentials even if errors occurred.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
logtoClient.signOut { logtoException: LogtoException? ->
|
||||||
|
// Local credentials are cleared regardless of whether `logtoException` is null.
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step
|
||||||
|
title="Further readings"
|
||||||
|
subtitle="4 articles"
|
||||||
|
>
|
||||||
|
|
||||||
|
- [Customize sign-in experience](https://docs.logto.io/docs/tutorials/get-started/customize-sign-in-experience)
|
||||||
|
- [Enable SMS or email passcode sign-in](https://docs.logto.io/docs/tutorials/get-started/passwordless-sign-in-by-adding-connectors#enable-sms-or-email-passwordless-sign-in)
|
||||||
|
- [Enable social sign-in](https://docs.logto.io/docs/tutorials/get-started/passwordless-sign-in-by-adding-connectors#enable-social-sign-in)
|
||||||
|
- [Protect your API](https://docs.logto.io/docs/recipes/protect-your-api)
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
</Steps>
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { ApplicationType } from '@logto/schemas';
|
||||||
|
|
||||||
|
import { type GuideMetadata } from '../types';
|
||||||
|
|
||||||
|
const metadata: Readonly<GuideMetadata> = Object.freeze({
|
||||||
|
name: 'Android (Kotlin)',
|
||||||
|
description: 'Android integration for Kotlin.',
|
||||||
|
target: ApplicationType.Native,
|
||||||
|
sample: {
|
||||||
|
repo: 'kotlin',
|
||||||
|
path: 'android-sample-kotlin',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default metadata;
|
|
@ -0,0 +1,25 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
viewBox="0 0 8.64 8.633">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="A" x1="2.039" y1="11.659" x2="9.95" y2="3.748"
|
||||||
|
gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset=".108" stop-color="#c757bc" />
|
||||||
|
<stop offset=".173" stop-color="#cd5ca9" />
|
||||||
|
<stop offset=".492" stop-color="#e8744f" />
|
||||||
|
<stop offset=".716" stop-color="#f88316" />
|
||||||
|
<stop offset=".823" stop-color="#ff8900" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="B" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset=".296" stop-color="#00afff" />
|
||||||
|
<stop offset=".694" stop-color="#5282ff" />
|
||||||
|
<stop offset="1" stop-color="#945dff" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="C" x1="3.369" y1="6.189" x2="6.073" y2="3.484" xlink:href="#B" />
|
||||||
|
<linearGradient xlink:href="#B" id="D" x1="6.184" y1="13.878" x2="10.04" y2="10.022" />
|
||||||
|
</defs>
|
||||||
|
<g transform="matrix(1.016327 0 0 1.016327 -3.52726 -3.909123)">
|
||||||
|
<path d="M7.74 3.843L3.47 8.33v4.013l4.262-4.27 4.24-4.232z" fill="url(#A)" />
|
||||||
|
<path d="M3.47 12.344l4.262-4.27 4.24 4.27z" fill="url(#D)" />
|
||||||
|
<path d="M3.47 3.843H7.74L3.47 8.33z" fill="url(#C)" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
|
@ -0,0 +1,134 @@
|
||||||
|
import UriInputField from '@/mdx-components-v2/UriInputField';
|
||||||
|
import Tabs from '@mdx/components/Tabs';
|
||||||
|
import TabItem from '@mdx/components/TabItem';
|
||||||
|
import InlineNotification from '@/ds-components/InlineNotification';
|
||||||
|
import Steps from '@/mdx-components-v2/Steps';
|
||||||
|
import Step from '@/mdx-components-v2/Step';
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
|
||||||
|
<Step
|
||||||
|
title="Integrate Logto Swift SDK"
|
||||||
|
subtitle="Add Logto SDK as a dependency"
|
||||||
|
>
|
||||||
|
|
||||||
|
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 [needs a `xcodeproj` file to build](https://github.com/Carthage/Carthage/issues/1226#issuecomment-290931385), 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 [does not support local dependency](https://github.com/CocoaPods/CocoaPods/issues/3276) and monorepo, thus it's hard to create a `.podspec` for this repo.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step
|
||||||
|
title="Init LogtoClient"
|
||||||
|
subtitle="1 step"
|
||||||
|
>
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<code className="language-swift">
|
||||||
|
{`import Logto
|
||||||
|
import LogtoClient
|
||||||
|
|
||||||
|
let config = try? LogtoConfig(
|
||||||
|
endpoint: "${props.endpoint}",${props.alternativeEndpoint ? ` // or "${props.alternativeEndpoint}"` : ''}
|
||||||
|
appId: "${props.app.id}"
|
||||||
|
)
|
||||||
|
let logtoClient = LogtoClient(useConfig: config)`}
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
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"
|
||||||
|
subtitle="2 steps"
|
||||||
|
>
|
||||||
|
|
||||||
|
### Configure Redirect URI
|
||||||
|
|
||||||
|
First, let’s configure your redirect URI scheme. E.g. `io.logto://callback`
|
||||||
|
|
||||||
|
<UriInputField name="redirectUris" />
|
||||||
|
|
||||||
|
<InlineNotification>
|
||||||
|
The Redirect URI in iOS SDK is only for internal use. There's <em>NO NEED</em> to add a{' '}
|
||||||
|
<a href="https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app">
|
||||||
|
Custom URL Scheme
|
||||||
|
</a>{' '}
|
||||||
|
until a connector asks.
|
||||||
|
</InlineNotification>
|
||||||
|
|
||||||
|
Go back to Xcode, use the following code to implement sign-in:
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<code className="language-swift">
|
||||||
|
{`do {
|
||||||
|
try await client.signInWithBrowser(redirectUri: "${
|
||||||
|
props.redirectUris[0] ?? 'io.logto://callback'
|
||||||
|
}")
|
||||||
|
print(client.isAuthenticated) // true
|
||||||
|
} catch let error as LogtoClientErrors.SignIn {
|
||||||
|
// error occured during sign in
|
||||||
|
}`}
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step
|
||||||
|
title="Sign out"
|
||||||
|
subtitle="1 step"
|
||||||
|
>
|
||||||
|
|
||||||
|
Calling `.signOut()` will clean all the Logto data in Keychain, if they exist.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
await client.signOut()
|
||||||
|
```
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step
|
||||||
|
title="Further readings"
|
||||||
|
subtitle="4 articles"
|
||||||
|
>
|
||||||
|
|
||||||
|
- [Customize sign-in experience](https://docs.logto.io/docs/tutorials/get-started/customize-sign-in-experience)
|
||||||
|
- [Enable SMS or email passcode sign-in](https://docs.logto.io/docs/tutorials/get-started/passwordless-sign-in-by-adding-connectors#enable-sms-or-email-passwordless-sign-in)
|
||||||
|
- [Enable social sign-in](https://docs.logto.io/docs/tutorials/get-started/passwordless-sign-in-by-adding-connectors#enable-social-sign-in)
|
||||||
|
- [Protect your API](https://docs.logto.io/docs/recipes/protect-your-api)
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
</Steps>
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { ApplicationType } from '@logto/schemas';
|
||||||
|
|
||||||
|
import { type GuideMetadata } from '../types';
|
||||||
|
|
||||||
|
const metadata: Readonly<GuideMetadata> = Object.freeze({
|
||||||
|
name: 'iOS (Swift)',
|
||||||
|
description: 'iOS (Swift) application integration guide.',
|
||||||
|
target: ApplicationType.Native,
|
||||||
|
sample: {
|
||||||
|
repo: 'swift',
|
||||||
|
path: 'Demos/SwiftUI%20Demo',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default metadata;
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<svg viewBox="0 0 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
|
||||||
|
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="-1845.5007" y1="1255.6392"
|
||||||
|
x2="-1797.1339" y2="981.3379" gradientTransform="matrix(-1 0 0 -1 -1693.2107 1246.5044)">
|
||||||
|
<stop offset="0" style="stop-color:#FAAE42" />
|
||||||
|
<stop offset="1" style="stop-color:#EF3E31" />
|
||||||
|
</linearGradient>
|
||||||
|
<path fill="url(#SVGID_1_)"
|
||||||
|
d="M56.9,0c1.5,0,139.3,0,141.8,0c6.9,0,13.6,1.1,20.1,3.4c9.4,3.4,17.9,9.4,24.3,17.2c6.5,7.8,10.8,17.4,12.3,27.4c0.6,3.7,0.7,7.4,0.7,11.1c0,3.4,0,123.2,0,128.6c0,3.2,0,6.5,0,9.7c0,4.4-0.2,8.9-1.1,13.2c-2,9.9-6.7,19.2-13.5,26.7c-6.7,7.5-15.5,13.1-25,16.1c-5.8,1.8-11.8,2.6-17.9,2.6c-2.7,0-142.1,0-144.2-0.1c-10.2-0.5-20.3-3.8-28.8-9.5c-8.3-5.6-15.1-13.4-19.5-22.4c-3.8-7.7-5.7-16.3-5.7-24.9c0-2,0-140.2,0-142.2C0.2,48.4,2,40,5.7,32.4c4.3-9,11-16.9,19.3-22.5c8.5-5.8,18.5-9.2,28.7-9.7C54.7,0,55.8,0,56.9,0z" />
|
||||||
|
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="130.6117" y1="4.1363"
|
||||||
|
x2="95.213" y2="204.8927">
|
||||||
|
<stop offset="0" style="stop-color:#E39F3A" />
|
||||||
|
<stop offset="1" style="stop-color:#D33929" />
|
||||||
|
</linearGradient>
|
||||||
|
<path fill="url(#SVGID_2_)"
|
||||||
|
d="M216,209.4c-0.9-1.4-1.9-2.8-3-4.1c-2.5-3-5.4-5.6-8.6-7.8c-4-2.7-8.7-4.4-13.5-4.6c-3.4-0.2-6.8,0.4-10,1.6c-3.2,1.1-6.3,2.7-9.3,4.3c-3.5,1.8-7,3.6-10.7,5.1c-4.4,1.8-9,3.2-13.7,4.2c-5.9,1.1-11.9,1.5-17.8,1.4c-10.7-0.2-21.4-1.8-31.6-4.8c-9-2.7-17.6-6.4-25.7-11.1c-7.1-4.1-13.7-8.8-19.9-14.1c-5.1-4.4-9.8-9.1-14.2-14.1c-3-3.5-5.9-7.2-8.6-11c-1.1-1.5-2.1-3.1-3-4.7c0,0,0,0,0,0c0,0,0,0,0,0L0,121.2V56.7C0,25.4,25.3,0,56.6,0h50.5l37.4,38c0,0,0,0,0,0c84.4,57.4,57.1,120.7,57.1,120.7S225.6,185.7,216,209.4z" />
|
||||||
|
<path fill="#FFFFFF"
|
||||||
|
d="M144.7,38c84.4,57.4,57.1,120.7,57.1,120.7s24,27.1,14.3,50.8c0,0-9.9-16.6-26.5-16.6c-16,0-25.4,16.6-57.6,16.6c-71.7,0-105.6-59.9-105.6-59.9C91,192.1,135.1,162,135.1,162c-29.1-16.9-91-97.7-91-97.7c53.9,45.9,77.2,58,77.2,58c-13.9-11.5-52.9-67.7-52.9-67.7c31.2,31.6,93.2,75.7,93.2,75.7C179.2,81.5,144.7,38,144.7,38z" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.2 KiB |
|
@ -1,4 +1,4 @@
|
||||||
import UriInputField from '@mdx/components/UriInputField';
|
import UriInputField from '@/mdx-components-v2/UriInputField';
|
||||||
import Tabs from '@mdx/components/Tabs';
|
import Tabs from '@mdx/components/Tabs';
|
||||||
import TabItem from '@mdx/components/TabItem';
|
import TabItem from '@mdx/components/TabItem';
|
||||||
import InlineNotification from '@/ds-components/InlineNotification';
|
import InlineNotification from '@/ds-components/InlineNotification';
|
||||||
|
@ -75,12 +75,7 @@ const App = () => (
|
||||||
|
|
||||||
First, let’s enter your redirect URI. E.g. `http://localhost:3000/callback`.
|
First, let’s enter your redirect URI. E.g. `http://localhost:3000/callback`.
|
||||||
|
|
||||||
<UriInputField
|
<UriInputField name="redirectUris" />
|
||||||
appId={props.app.id}
|
|
||||||
isSingle={!props.isCompact}
|
|
||||||
name="redirectUris"
|
|
||||||
title="application_details.redirect_uri"
|
|
||||||
/>
|
|
||||||
|
|
||||||
### Implement a sign-in button
|
### Implement a sign-in button
|
||||||
|
|
||||||
|
@ -147,12 +142,7 @@ Calling `.signOut()` will clear all the Logto data in memory and localStorage if
|
||||||
|
|
||||||
After signing out, it'll be great to redirect user back to your website. Let's add `http://localhost:3000` as the Post Sign-out URI below, and use it as the parameter when calling `.signOut()`.
|
After signing out, it'll be great to redirect user back to your website. Let's add `http://localhost:3000` as the Post Sign-out URI below, and use it as the parameter when calling `.signOut()`.
|
||||||
|
|
||||||
<UriInputField
|
<UriInputField name="postLogoutRedirectUris" />
|
||||||
appId={props.app.id}
|
|
||||||
isSingle={!props.isCompact}
|
|
||||||
name="postLogoutRedirectUris"
|
|
||||||
title="application_details.post_sign_out_redirect_uri"
|
|
||||||
/>
|
|
||||||
|
|
||||||
### Implement a sign-out button
|
### Implement a sign-out button
|
||||||
|
|
||||||
|
|
149
packages/console/src/assets/docs/guides/spa-vanilla/README.mdx
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
import UriInputField from '@/mdx-components-v2/UriInputField';
|
||||||
|
import Tabs from '@mdx/components/Tabs';
|
||||||
|
import TabItem from '@mdx/components/TabItem';
|
||||||
|
import InlineNotification from '@/ds-components/InlineNotification';
|
||||||
|
import Steps from '@/mdx-components-v2/Steps';
|
||||||
|
import Step from '@/mdx-components-v2/Step';
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
|
||||||
|
<Step
|
||||||
|
title="Add Logto SDK as a dependency"
|
||||||
|
subtitle="Please select your favorite package manager"
|
||||||
|
>
|
||||||
|
<Tabs>
|
||||||
|
<TabItem value="npm" label="npm">
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm i @logto/browser
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
<TabItem value="yarn" label="Yarn">
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn add @logto/browser
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
<TabItem value="pnpm" label="pnpm">
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add @logto/browser
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step
|
||||||
|
title="Init LogtoClient"
|
||||||
|
subtitle="1 step"
|
||||||
|
>
|
||||||
|
|
||||||
|
Import and init `LogtoClient` by passing config:
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<code className="language-ts">
|
||||||
|
{`import LogtoClient from '@logto/browser';
|
||||||
|
|
||||||
|
const logtoClient = new LogtoClient({
|
||||||
|
endpoint: '${props.endpoint}',${props.alternativeEndpoint ? ` // or "${props.alternativeEndpoint}"` : ''}
|
||||||
|
appId: '${props.app.id}',
|
||||||
|
});`}
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step
|
||||||
|
title="Sign in"
|
||||||
|
subtitle="3 steps"
|
||||||
|
>
|
||||||
|
|
||||||
|
<InlineNotification>
|
||||||
|
In the following steps, we assume your app is running on <code>http://localhost:3000</code>.
|
||||||
|
</InlineNotification>
|
||||||
|
|
||||||
|
### Configure Redirect URI
|
||||||
|
|
||||||
|
First, let’s enter your redirect URI. E.g. `http://localhost:3000/callback`.
|
||||||
|
|
||||||
|
<UriInputField name="redirectUris" />
|
||||||
|
|
||||||
|
### Implement a sign-in button
|
||||||
|
|
||||||
|
Go back to your IDE/editor, use the following code to implement the sign-in button:
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<code className="language-html">
|
||||||
|
{`<button onclick="logtoClient.signIn('${
|
||||||
|
props.redirectUris[0] ?? 'http://localhost:3000/callback'
|
||||||
|
}')">
|
||||||
|
Sign In
|
||||||
|
</button>`}
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
### Handle redirect
|
||||||
|
|
||||||
|
We're almost there! In the last step, we use `http://localhost:3000/callback` as the Redirect URI, and now we need to handle it properly.
|
||||||
|
|
||||||
|
Insert the code below in your `/callback` route:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
try {
|
||||||
|
await logtoClient.handleSignInCallback(window.location.href);
|
||||||
|
console.log(await logtoClient.isAuthenticated()); // true
|
||||||
|
} catch {
|
||||||
|
// Handle error
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now you can test the sign-in flow.
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step
|
||||||
|
title="Sign out"
|
||||||
|
subtitle="1 step"
|
||||||
|
>
|
||||||
|
|
||||||
|
Calling `.signOut()` will clear all the Logto data in memory and localStorage if they exist.
|
||||||
|
|
||||||
|
After signing out, it'll be great to redirect user back to your website. Let's add `http://localhost:3000` as the Post Sign-out URI below, and use it as the parameter when calling `.signOut()`.
|
||||||
|
|
||||||
|
<UriInputField
|
||||||
|
appId={props.app.id}
|
||||||
|
isSingle={!props.isCompact}
|
||||||
|
name="postLogoutRedirectUris"
|
||||||
|
title="application_details.post_sign_out_redirect_uri"
|
||||||
|
/>
|
||||||
|
|
||||||
|
### Implement a sign-out button
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<code className="language-html">
|
||||||
|
{`<button onclick="logtoClient.signOut('${
|
||||||
|
props.postLogoutRedirectUris[0] ?? 'http://localhost:3000'
|
||||||
|
}')">
|
||||||
|
Sign Out
|
||||||
|
</button>`}
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step
|
||||||
|
title="Further readings"
|
||||||
|
subtitle="4 articles"
|
||||||
|
>
|
||||||
|
|
||||||
|
- [Customize sign-in experience](https://docs.logto.io/docs/tutorials/get-started/customize-sign-in-experience)
|
||||||
|
- [Enable SMS or email passcode sign-in](https://docs.logto.io/docs/tutorials/get-started/passwordless-sign-in-by-adding-connectors#enable-sms-or-email-passwordless-sign-in)
|
||||||
|
- [Enable social sign-in](https://docs.logto.io/docs/tutorials/get-started/passwordless-sign-in-by-adding-connectors#enable-social-sign-in)
|
||||||
|
- [Protect your API](https://docs.logto.io/docs/recipes/protect-your-api)
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
</Steps>
|
15
packages/console/src/assets/docs/guides/spa-vanilla/index.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { ApplicationType } from '@logto/schemas';
|
||||||
|
|
||||||
|
import { type GuideMetadata } from '../types';
|
||||||
|
|
||||||
|
const metadata: Readonly<GuideMetadata> = Object.freeze({
|
||||||
|
name: 'Vanilla JS',
|
||||||
|
description: 'The framework-agnostic JavaScript integration.',
|
||||||
|
target: ApplicationType.SPA,
|
||||||
|
sample: {
|
||||||
|
repo: 'js',
|
||||||
|
path: 'packages/browser-sample',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default metadata;
|
12
packages/console/src/assets/docs/guides/spa-vanilla/logo.svg
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo
|
||||||
|
Mixer Tools -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg"
|
||||||
|
aria-label="JavaScript" role="img"
|
||||||
|
viewBox="0 0 512 512">
|
||||||
|
<rect
|
||||||
|
width="512" height="512"
|
||||||
|
rx="15%"
|
||||||
|
fill="#f7df1e" />
|
||||||
|
<path
|
||||||
|
d="M324 370c10 17 24 29 47 29c20 0 33-10 33 -24c0-16 -13 -22 -35 -32l-12-5c-35-15 -58 -33 -58 -72c0-36 27 -64 70 -64c31 0 53 11 68 39l-37 24c-8-15 -17 -21 -31 -21c-14 0-23 9 -23 21c0 14 9 20 30 29l12 5c41 18 64 35 64 76c0 43-34 67 -80 67c-45 0-74 -21 -88 -49zm-170 4c8 13 14 25 31 25c16 0 26-6 26 -30V203h48v164c0 50-29 72 -72 72c-39 0-61 -20 -72 -44z" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 668 B |
198
packages/console/src/assets/docs/guides/spa-vue/README.mdx
Normal file
|
@ -0,0 +1,198 @@
|
||||||
|
import UriInputField from '@/mdx-components-v2/UriInputField';
|
||||||
|
import Tabs from '@mdx/components/Tabs';
|
||||||
|
import TabItem from '@mdx/components/TabItem';
|
||||||
|
import InlineNotification from '@/ds-components/InlineNotification';
|
||||||
|
import Steps from '@/mdx-components-v2/Steps';
|
||||||
|
import Step from '@/mdx-components-v2/Step';
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
|
||||||
|
<Step
|
||||||
|
title="Add Logto SDK as a dependency"
|
||||||
|
subtitle="Please select your favorite package manager"
|
||||||
|
>
|
||||||
|
<Tabs>
|
||||||
|
<TabItem value="npm" label="npm">
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm i @logto/vue
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
<TabItem value="yarn" label="Yarn">
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn add @logto/vue
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
<TabItem value="pnpm" label="pnpm">
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add @logto/vue
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step
|
||||||
|
title="Init LogtoClient"
|
||||||
|
subtitle="1 step"
|
||||||
|
>
|
||||||
|
|
||||||
|
<InlineNotification>
|
||||||
|
We only support Vue 3 Composition API at this point. Will add support to Vue Options API and
|
||||||
|
possibly Vue 2 in future releases.
|
||||||
|
</InlineNotification>
|
||||||
|
|
||||||
|
Import and use `createLogto` to install Logto plugin:
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<code className="language-ts">
|
||||||
|
{`import { createLogto, LogtoConfig } from '@logto/vue';
|
||||||
|
|
||||||
|
const config: LogtoConfig = {
|
||||||
|
endpoint: '${props.endpoint}',${props.alternativeEndpoint ? ` // or "${props.alternativeEndpoint}"` : ''}
|
||||||
|
appId: '${props.app.id}',
|
||||||
|
};
|
||||||
|
|
||||||
|
const app = createApp(App);
|
||||||
|
|
||||||
|
app.use(createLogto, config);
|
||||||
|
app.mount("#app");`}
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step
|
||||||
|
title="Sign in"
|
||||||
|
subtitle="3 steps"
|
||||||
|
>
|
||||||
|
|
||||||
|
<InlineNotification>
|
||||||
|
In the following steps, we assume your app is running on <code>http://localhost:3000</code>.
|
||||||
|
</InlineNotification>
|
||||||
|
|
||||||
|
### Configure Redirect URI
|
||||||
|
|
||||||
|
First, let’s enter your redirect URI. E.g. `http://localhost:3000/callback`.
|
||||||
|
|
||||||
|
<UriInputField name="redirectUris" />
|
||||||
|
|
||||||
|
### Implement a sign-in button
|
||||||
|
|
||||||
|
We provide two composables `useHandleSignInCallback()` and `useLogto()`, which can help you easily manage the authentication flow.
|
||||||
|
|
||||||
|
Go back to your IDE/editor, use the following code to implement the sign-in button:
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<code className="language-html">
|
||||||
|
{`<script setup lang="ts">
|
||||||
|
import { useLogto } from "@logto/vue";
|
||||||
|
|
||||||
|
const { signIn, isAuthenticated } = useLogto();
|
||||||
|
const onClickSignIn = () => signIn('${props.redirectUris[0] ?? 'http://localhost:3000/callback'}');
|
||||||
|
</script>`}
|
||||||
|
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
```html
|
||||||
|
<template>
|
||||||
|
<div v-if="isAuthenticated">
|
||||||
|
<div>Signed in</div>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<button @click="onClickSignIn">Sign In</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Handle redirect
|
||||||
|
|
||||||
|
We're almost there! In the last step, we use `http://localhost:3000/callback` as the Redirect URI, and now we need to handle it properly.
|
||||||
|
|
||||||
|
First let's create a callback component:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- CallbackView.vue -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useHandleSignInCallback } from '@logto/vue';
|
||||||
|
const { isLoading } = useHandleSignInCallback(() => {
|
||||||
|
// Navigate to root path when finished
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
```html
|
||||||
|
<template>
|
||||||
|
<!-- When it's working in progress -->
|
||||||
|
<p v-if="isLoading">Redirecting...</p>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally insert the code below to create a `/callback` route which does NOT require authentication:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// Assuming vue-router
|
||||||
|
const router = createRouter({
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/callback',
|
||||||
|
name: 'callback',
|
||||||
|
component: CallbackView,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step
|
||||||
|
title="Sign out"
|
||||||
|
subtitle="1 step"
|
||||||
|
>
|
||||||
|
|
||||||
|
Calling `.signOut()` will clear all the Logto data in memory and localStorage if they exist.
|
||||||
|
|
||||||
|
After signing out, it'll be great to redirect user back to your website. Let's add `http://localhost:3000` as the Post Sign-out URI below, and use it as the parameter when calling `.signOut()`.
|
||||||
|
|
||||||
|
<UriInputField name="postLogoutRedirectUris" />
|
||||||
|
|
||||||
|
### Implement a sign-out button
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<code className="language-html">
|
||||||
|
{`<script setup lang="ts">
|
||||||
|
import { useLogto } from "@logto/vue";
|
||||||
|
|
||||||
|
const { signOut } = useLogto();
|
||||||
|
const onClickSignOut = () => signOut('${props.postLogoutRedirectUris[0] ?? 'http://localhost:3000'}');
|
||||||
|
</script>`}
|
||||||
|
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
```html
|
||||||
|
<template>
|
||||||
|
<button @click="onClickSignOut">Sign Out</button>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step
|
||||||
|
title="Further readings"
|
||||||
|
subtitle="4 articles"
|
||||||
|
>
|
||||||
|
|
||||||
|
- [Customize sign-in experience](https://docs.logto.io/docs/tutorials/get-started/customize-sign-in-experience)
|
||||||
|
- [Enable SMS or email passcode sign-in](https://docs.logto.io/docs/tutorials/get-started/passwordless-sign-in-by-adding-connectors#enable-sms-or-email-passwordless-sign-in)
|
||||||
|
- [Enable social sign-in](https://docs.logto.io/docs/tutorials/get-started/passwordless-sign-in-by-adding-connectors#enable-social-sign-in)
|
||||||
|
- [Protect your API](https://docs.logto.io/docs/recipes/protect-your-api)
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
</Steps>
|
16
packages/console/src/assets/docs/guides/spa-vue/index.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { ApplicationType } from '@logto/schemas';
|
||||||
|
|
||||||
|
import { type GuideMetadata } from '../types';
|
||||||
|
|
||||||
|
const metadata: Readonly<GuideMetadata> = Object.freeze({
|
||||||
|
name: 'Vue',
|
||||||
|
description:
|
||||||
|
'Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.',
|
||||||
|
target: ApplicationType.SPA,
|
||||||
|
sample: {
|
||||||
|
repo: 'js',
|
||||||
|
path: 'packages/vue-sample',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default metadata;
|
2
packages/console/src/assets/docs/guides/spa-vue/logo.svg
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 -17.5 256 256" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet"><path d="M204.8 0H256L128 220.8 0 0h97.92L128 51.2 157.44 0h47.36z" fill="#41B883"/><path d="M0 0l128 220.8L256 0h-51.2L128 132.48 50.56 0H0z" fill="#41B883"/><path d="M50.56 0L128 133.12 204.8 0h-47.36L128 51.2 97.92 0H50.56z" fill="#35495E"/></svg>
|
After Width: | Height: | Size: 500 B |
|
@ -1,4 +1,4 @@
|
||||||
import UriInputField from '@mdx/components/UriInputField';
|
import UriInputField from '@/mdx-components-v2/UriInputField';
|
||||||
import Tabs from '@mdx/components/Tabs';
|
import Tabs from '@mdx/components/Tabs';
|
||||||
import TabItem from '@mdx/components/TabItem';
|
import TabItem from '@mdx/components/TabItem';
|
||||||
import InlineNotification from '@/ds-components/InlineNotification';
|
import InlineNotification from '@/ds-components/InlineNotification';
|
||||||
|
@ -92,12 +92,7 @@ app.use(session({ secret: '${buildIdGenerator(32)()}', cookie: { maxAge: 14 * 24
|
||||||
|
|
||||||
First, let’s enter your redirect URI. E.g. `http://localhost:3000/api/logto/sign-in-callback`.
|
First, let’s enter your redirect URI. E.g. `http://localhost:3000/api/logto/sign-in-callback`.
|
||||||
|
|
||||||
<UriInputField
|
<UriInputField name="redirectUris" />
|
||||||
appId={props.app.id}
|
|
||||||
isSingle={!props.isCompact}
|
|
||||||
name="redirectUris"
|
|
||||||
title="application_details.redirect_uri"
|
|
||||||
/>
|
|
||||||
|
|
||||||
### Prepare Logto routes
|
### Prepare Logto routes
|
||||||
|
|
||||||
|
|
366
packages/console/src/assets/docs/guides/web-go/README.mdx
Normal file
|
@ -0,0 +1,366 @@
|
||||||
|
import UriInputField from '@/mdx-components-v2/UriInputField';
|
||||||
|
import Steps from '@/mdx-components-v2/Steps';
|
||||||
|
import Step from '@/mdx-components-v2/Step';
|
||||||
|
import InlineNotification from '@/ds-components/InlineNotification';
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
|
||||||
|
<Step
|
||||||
|
title="Add Logto SDK as a dependency"
|
||||||
|
>
|
||||||
|
<InlineNotification>
|
||||||
|
The following demonstration is built upon the <a href="https://gin-gonic.com">Gin Web Framework</a>.
|
||||||
|
You may also integrate Logto into other frameworks by taking the same steps.
|
||||||
|
In the following code snippets, we assume your app is running on <code>http://localhost:8080</code>.
|
||||||
|
</InlineNotification>
|
||||||
|
|
||||||
|
Run in the project root directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get github.com/logto-io/go
|
||||||
|
```
|
||||||
|
|
||||||
|
Add the `github.com/logto-io/go/client` package to your application code:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// main.go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
// Add dependency
|
||||||
|
"github.com/logto-io/go/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
router := gin.Default()
|
||||||
|
router.GET("/", func(c *gin.Context) {
|
||||||
|
c.String(200, "Hello Logto!")
|
||||||
|
})
|
||||||
|
router.Run(":8080")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step
|
||||||
|
title="Use sessions to store user authentication information"
|
||||||
|
subtitle="2 steps"
|
||||||
|
>
|
||||||
|
|
||||||
|
In traditional web applications, the user authentication information will be stored in the user session.
|
||||||
|
|
||||||
|
Logto SDK provides a `Storage` interface, you can implement a `Storage` adapter based on your web framework so that the Logto SDK can store user authentication information in the session.
|
||||||
|
|
||||||
|
<InlineNotification>
|
||||||
|
We do NOT recommend using cookie-based sessions, as user authentication information stored by
|
||||||
|
Logto may exceed the cookie size limit. In this example, we use memory-based sessions. You can use
|
||||||
|
Redis, MongoDB, and other technologies in production to store sessions as needed.
|
||||||
|
</InlineNotification>
|
||||||
|
|
||||||
|
The `Storage` type in the Logto SDK is as follows:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// github.com/logto-io/client/storage.go
|
||||||
|
package client
|
||||||
|
|
||||||
|
type Storage interface {
|
||||||
|
GetItem(key string) string
|
||||||
|
SetItem(key, value string)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We will use [github.com/gin-contrib/sessions](https://github.com/gin-contrib/sessions) as an example to demonstrate this process.
|
||||||
|
|
||||||
|
### Apply session middleware
|
||||||
|
|
||||||
|
Apply the [github.com/gin-contrib/sessions](https://github.com/gin-contrib/sessions) middleware to the application, so that we can get the user session by the user request context in the route handler:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-contrib/sessions"
|
||||||
|
"github.com/gin-contrib/sessions/memstore"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/logto-io/go/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
router := gin.Default()
|
||||||
|
|
||||||
|
// We use memory-based session in this example
|
||||||
|
store := memstore.NewStore([]byte("your session secret"))
|
||||||
|
router.Use(sessions.Sessions("logto-session", store))
|
||||||
|
|
||||||
|
router.GET("/", func(ctx *gin.Context) {
|
||||||
|
// Get user session
|
||||||
|
session := sessions.Default(ctx)
|
||||||
|
// ...
|
||||||
|
ctx.String(200, "Hello Logto!")
|
||||||
|
})
|
||||||
|
router.Run(":8080")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Create session storage for Logto to store user authentication information
|
||||||
|
|
||||||
|
Create a `session_storage.go` file, define a `SessionStorage` and implement the Logto SDK's `Storage` interfaces:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// session_storage.go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-contrib/sessions"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SessionStorage struct {
|
||||||
|
session sessions.Session
|
||||||
|
}
|
||||||
|
|
||||||
|
func (storage *SessionStorage) GetItem(key string) string {
|
||||||
|
value := storage.session.Get(key)
|
||||||
|
if value == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return value.(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (storage *SessionStorage) SetItem(key, value string) {
|
||||||
|
storage.session.Set(key, value)
|
||||||
|
storage.session.Save()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, in the route handler, you can create a session storage for Logto as follows:
|
||||||
|
|
||||||
|
```go
|
||||||
|
session := sessions.Default(ctx)
|
||||||
|
sessionStorage := &SessionStorage{session: session}
|
||||||
|
```
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step
|
||||||
|
title="Init LogtoClient"
|
||||||
|
subtitle="2 steps"
|
||||||
|
>
|
||||||
|
|
||||||
|
### Create LogtConfig
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<code className="language-go">
|
||||||
|
{`// main.go
|
||||||
|
func main() {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
logtoConfig := &client.LogtoConfig{
|
||||||
|
Endpoint: "${props.endpoint}",${props.alternativeEndpoint ? ` // or "${props.alternativeEndpoint}"` : ''}
|
||||||
|
AppId: "${props.app.id}",
|
||||||
|
AppSecret: "${props.app.secret}",
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}`}
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
### Init LogtoClient for each user request
|
||||||
|
|
||||||
|
```go
|
||||||
|
// main.go
|
||||||
|
func main() {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
router.GET("/", func(ctx *gin.Context) {
|
||||||
|
// Init LogtoClient
|
||||||
|
session := sessions.Default(ctx)
|
||||||
|
logtoClient := client.NewLogtoClient(
|
||||||
|
logtoConfig,
|
||||||
|
&SessionStorage{session: session},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Use Logto to control the content of the home page
|
||||||
|
authState := "You are not logged in to this website. :("
|
||||||
|
|
||||||
|
if logtoClient.IsAuthenticated() {
|
||||||
|
authState = "You are logged in to this website! :)"
|
||||||
|
}
|
||||||
|
|
||||||
|
homePage := `<h1>Hello Logto</h1>` +
|
||||||
|
"<div>" + authState + "</div>"
|
||||||
|
|
||||||
|
ctx.Data(http.StatusOK, "text/html; charset=utf-8", []byte(homePage))
|
||||||
|
})
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step
|
||||||
|
title="Sign in"
|
||||||
|
subtitle="3 steps"
|
||||||
|
>
|
||||||
|
|
||||||
|
### Configure Redirect URI
|
||||||
|
|
||||||
|
Add `http://localhost:8080/sign-in-callback` to the Redirect URI field.
|
||||||
|
This allows Logto to redirect the user to the `/sign-in-callback` route of your application after signing in.
|
||||||
|
|
||||||
|
<UriInputField name="redirectUris" />
|
||||||
|
|
||||||
|
### Add a route for handling sign-in requests
|
||||||
|
|
||||||
|
```go
|
||||||
|
//main.go
|
||||||
|
func main() {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
// Add a link to perform a sign-in request on the home page
|
||||||
|
router.GET("/", func(ctx *gin.Context) {
|
||||||
|
// ...
|
||||||
|
homePage := `<h1>Hello Logto</h1>` +
|
||||||
|
"<div>" + authState + "</div>" +
|
||||||
|
// Add link
|
||||||
|
`<div><a href="/sign-in">Sign In</a></div>`
|
||||||
|
|
||||||
|
ctx.Data(http.StatusOK, "text/html; charset=utf-8", []byte(homePage))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add a route for handling sign-in requests
|
||||||
|
router.GET("/sign-in", func(ctx *gin.Context) {
|
||||||
|
session := sessions.Default(ctx)
|
||||||
|
logtoClient := client.NewLogtoClient(
|
||||||
|
logtoConfig,
|
||||||
|
&SessionStorage{session: session},
|
||||||
|
)
|
||||||
|
|
||||||
|
// The sign-in request is handled by Logto.
|
||||||
|
// The user will be redirected to the Redirect URI on signed in.
|
||||||
|
signInUri, err := logtoClient.SignIn("http://localhost:8080/sign-in-callback")
|
||||||
|
if err != nil {
|
||||||
|
ctx.String(http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect the user to the Logto sign-in page.
|
||||||
|
ctx.Redirect(http.StatusTemporaryRedirect, signInUri)
|
||||||
|
})
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Add a route for handling sign-in callback requests
|
||||||
|
|
||||||
|
When the user signs in successfully on the Logto sign-in page, Logto will redirect the user to the Redirect URI.
|
||||||
|
|
||||||
|
Since the Redirect URI is `http://localhost:8080/sign-in-callback`, we add the `/sign-in-callback` route to handle the callback after signing in.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// main.go
|
||||||
|
func main() {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
// Add a route for handling sign-in callback requests
|
||||||
|
router.GET("/sign-in-callback", func(ctx *gin.Context) {
|
||||||
|
session := sessions.Default(ctx)
|
||||||
|
logtoClient := client.NewLogtoClient(
|
||||||
|
logtoConfig,
|
||||||
|
&SessionStorage{session: session},
|
||||||
|
)
|
||||||
|
|
||||||
|
// The sign-in callback request is handled by Logto
|
||||||
|
err := logtoClient.HandleSignInCallback(ctx.Request)
|
||||||
|
if err != nil {
|
||||||
|
ctx.String(http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jump to the page specified by the developer.
|
||||||
|
// This example takes the user back to the home page.
|
||||||
|
ctx.Redirect(http.StatusTemporaryRedirect, "/")
|
||||||
|
})
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step
|
||||||
|
title="Sign out"
|
||||||
|
subtitle="2 steps"
|
||||||
|
>
|
||||||
|
|
||||||
|
### Configure Post Sign-out Redirect URI
|
||||||
|
|
||||||
|
Add `http://localhost:8080` to the Post Sign-out Redirect URI filed:
|
||||||
|
|
||||||
|
<UriInputField name="postLogoutUris" />
|
||||||
|
|
||||||
|
This configuration enables the user to return to the home page after signing out.
|
||||||
|
|
||||||
|
### Add a route for handling signing out requests
|
||||||
|
|
||||||
|
```go
|
||||||
|
//main.go
|
||||||
|
func main() {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
// Add a link to perform a sign-out request on the home page
|
||||||
|
router.GET("/", func(ctx *gin.Context) {
|
||||||
|
// ...
|
||||||
|
homePage := `<h1>Hello Logto</h1>` +
|
||||||
|
"<div>" + authState + "</div>" +
|
||||||
|
`<div><a href="/sign-in">Sign In</a></div>` +
|
||||||
|
// Add link
|
||||||
|
`<div><a href="/sign-out">Sign Out</a></div>`
|
||||||
|
|
||||||
|
ctx.Data(http.StatusOK, "text/html; charset=utf-8", []byte(homePage))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add a route for handling signing out requests
|
||||||
|
router.GET("/sign-out", func(ctx *gin.Context) {
|
||||||
|
session := sessions.Default(ctx)
|
||||||
|
logtoClient := client.NewLogtoClient(
|
||||||
|
logtoConfig,
|
||||||
|
&SessionStorage{session: session},
|
||||||
|
)
|
||||||
|
|
||||||
|
// The sign-out request is handled by Logto.
|
||||||
|
// The user will be redirected to the Post Sign-out Redirect URI on signed out.
|
||||||
|
signOutUri, signOutErr := logtoClient.SignOut("http://localhost:8080")
|
||||||
|
|
||||||
|
if signOutErr != nil {
|
||||||
|
ctx.String(http.StatusOK, signOutErr.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Redirect(http.StatusTemporaryRedirect, signOutUri)
|
||||||
|
})
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
After the user makes a signing-out request, Logto will clear all user authentication information in the session.
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step
|
||||||
|
title="Further readings"
|
||||||
|
subtitle="4 articles"
|
||||||
|
>
|
||||||
|
|
||||||
|
- [Customize sign-in experience](https://docs.logto.io/docs/tutorials/get-started/customize-sign-in-experience)
|
||||||
|
- [Enable SMS or email passcode sign-in](https://docs.logto.io/docs/tutorials/get-started/passwordless-sign-in-by-adding-connectors#enable-sms-or-email-passwordless-sign-in)
|
||||||
|
- [Enable social sign-in](https://docs.logto.io/docs/tutorials/get-started/passwordless-sign-in-by-adding-connectors#enable-social-sign-in)
|
||||||
|
- [Protect your API](https://docs.logto.io/docs/recipes/protect-your-api)
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
</Steps>
|
16
packages/console/src/assets/docs/guides/web-go/index.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { ApplicationType } from '@logto/schemas';
|
||||||
|
|
||||||
|
import { type GuideMetadata } from '../types';
|
||||||
|
|
||||||
|
const metadata: Readonly<GuideMetadata> = Object.freeze({
|
||||||
|
name: 'Go',
|
||||||
|
description:
|
||||||
|
'Go is an open source programming language that makes it easy to build simple, reliable, and efficient software.',
|
||||||
|
target: ApplicationType.Traditional,
|
||||||
|
sample: {
|
||||||
|
repo: 'go',
|
||||||
|
path: 'gin-sample',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default metadata;
|
1
packages/console/src/assets/docs/guides/web-go/logo.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" x="0" y="0" width="120" height="60" xml:space="preserve"><style>.st0{fill:#00acd7}</style><switch><g><path class="st0" d="M22.3 24.7c-.1 0-.2-.1-.1-.2l.7-1c.1-.1.2-.2.4-.2h12.6c.1 0 .2.1.1.2l-.6.9c-.1.1-.2.2-.4.2l-12.7.1zM17 27.9c-.1 0-.2-.1-.1-.2l.7-1c.1-.1.2-.2.4-.2h16.1c.1 0 .2.1.2.2l-.3 1c0 .1-.2.2-.3.2H17zm8.5 3.3c-.1 0-.2-.1-.1-.2l.5-.9c.1-.1.2-.2.4-.2h7c.1 0 .2.1.2.2l-.1.8c0 .1-.1.2-.2.2l-7.7.1zM62.1 24l-5.9 1.5c-.5.1-.6.2-1-.4-.5-.6-.9-1-1.7-1.3-2.2-1.1-4.4-.8-6.4.5-2.4 1.5-3.6 3.8-3.6 6.7 0 2.8 2 5.1 4.8 5.5 2.4.3 4.4-.5 6-2.3.3-.4.6-.8 1-1.3h-6.8c-.7 0-.9-.5-.7-1.1.5-1.1 1.3-2.9 1.8-3.8.1-.2.4-.6.9-.6h12.8c-.1 1-.1 1.9-.2 2.9-.4 2.5-1.3 4.9-2.9 6.9-2.5 3.3-5.8 5.4-10 6-3.5.5-6.7-.2-9.5-2.3-2.6-2-4.1-4.6-4.5-7.8-.5-3.8.7-7.3 3-10.3 2.5-3.3 5.8-5.4 9.9-6.1 3.3-.6 6.5-.2 9.3 1.7 1.9 1.2 3.2 2.9 4.1 5 .1.4 0 .5-.4.6z"/><path class="st0" d="M73.7 43.5c-3.2-.1-6.1-1-8.6-3.1-2.1-1.8-3.4-4.1-3.8-6.8-.6-4 .5-7.5 2.9-10.6 2.6-3.4 5.7-5.1 9.9-5.9 3.6-.6 7-.3 10 1.8 2.8 1.9 4.5 4.5 5 7.9.6 4.8-.8 8.6-4 11.9-2.3 2.4-5.2 3.8-8.4 4.5-1.1.2-2.1.2-3 .3zm8.4-14.2c0-.5 0-.8-.1-1.2-.6-3.5-3.8-5.5-7.2-4.7-3.3.7-5.4 2.8-6.2 6.1-.6 2.7.7 5.5 3.2 6.7 1.9.8 3.9.7 5.7-.2 2.9-1.4 4.4-3.7 4.6-6.7z"/></g></switch></svg>
|
After Width: | Height: | Size: 1.3 KiB |
248
packages/console/src/assets/docs/guides/web-next/README.mdx
Normal file
|
@ -0,0 +1,248 @@
|
||||||
|
import UriInputField from '@/mdx-components-v2/UriInputField';
|
||||||
|
import Tabs from '@mdx/components/Tabs';
|
||||||
|
import TabItem from '@mdx/components/TabItem';
|
||||||
|
import InlineNotification from '@/ds-components/InlineNotification';
|
||||||
|
import { buildIdGenerator } from '@logto/shared/universal';
|
||||||
|
import Steps from '@/mdx-components-v2/Steps';
|
||||||
|
import Step from '@/mdx-components-v2/Step';
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
|
||||||
|
<Step
|
||||||
|
title="Add Logto SDK as a dependency"
|
||||||
|
subtitle="Please select your favorite package manager"
|
||||||
|
>
|
||||||
|
<Tabs>
|
||||||
|
<TabItem value="npm" label="npm">
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm i @logto/next
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
<TabItem value="yarn" label="Yarn">
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn add @logto/next
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
<TabItem value="pnpm" label="pnpm">
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add @logto/next
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step
|
||||||
|
title="Init LogtoClient"
|
||||||
|
subtitle="1 step"
|
||||||
|
>
|
||||||
|
|
||||||
|
<InlineNotification>
|
||||||
|
In the following steps, we assume your app is running on <code>http://localhost:3000</code>.
|
||||||
|
</InlineNotification>
|
||||||
|
|
||||||
|
Import and initialize LogtoClient:
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<code className="language-ts">
|
||||||
|
{`// libraries/logto.js
|
||||||
|
import LogtoClient from '@logto/next';
|
||||||
|
|
||||||
|
export const logtoClient = new LogtoClient({
|
||||||
|
endpoint: '${props.endpoint}',${props.alternativeEndpoint ? ` // or "${props.alternativeEndpoint}"` : ''}
|
||||||
|
appId: '${props.app.id}',
|
||||||
|
appSecret: '${props.app.secret}',
|
||||||
|
baseUrl: 'http://localhost:3000', // Change to your own base URL
|
||||||
|
cookieSecret: '${buildIdGenerator(32)()}', // Auto-generated 32 digit secret
|
||||||
|
cookieSecure: process.env.NODE_ENV === 'production',
|
||||||
|
});`}
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step
|
||||||
|
title="Sign in"
|
||||||
|
subtitle="3 steps"
|
||||||
|
>
|
||||||
|
|
||||||
|
### Configure Redirect URI
|
||||||
|
|
||||||
|
First, let’s enter your redirect URI. E.g. `http://localhost:3000/api/logto/sign-in-callback`.
|
||||||
|
|
||||||
|
<UriInputField name="redirectUris" />
|
||||||
|
|
||||||
|
### Prepare API routes
|
||||||
|
|
||||||
|
Prepare [API routes](https://nextjs.org/docs/api-routes/introduction) to connect with Logto.
|
||||||
|
|
||||||
|
Go back to your IDE/editor, use the following code to implement the API routes first:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// pages/api/logto/[action].ts
|
||||||
|
import { logtoClient } from '../../../libraries/logto';
|
||||||
|
|
||||||
|
export default logtoClient.handleAuthRoutes();
|
||||||
|
```
|
||||||
|
|
||||||
|
This will create 4 routes automatically:
|
||||||
|
|
||||||
|
1. `/api/logto/sign-in`: Sign in with Logto.
|
||||||
|
2. `/api/logto/sign-in-callback`: Handle sign-in callback.
|
||||||
|
3. `/api/logto/sign-out`: Sign out with Logto.
|
||||||
|
4. `/api/logto/user`: Check if user is authenticated with Logto, if yes, return user info.
|
||||||
|
|
||||||
|
### Implement sign-in button
|
||||||
|
|
||||||
|
We're almost there! In the last step, we will create a sign-in button:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
|
||||||
|
const { push } = useRouter();
|
||||||
|
|
||||||
|
<button onClick={() => push('/api/logto/sign-in')}>Sign In</button>;
|
||||||
|
```
|
||||||
|
|
||||||
|
Now you will be navigated to Logto sign-in page when you click the button.
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step
|
||||||
|
title="Get user profile"
|
||||||
|
subtitle="2 ways"
|
||||||
|
>
|
||||||
|
|
||||||
|
### Through API request in the frontend
|
||||||
|
|
||||||
|
You can fetch user info by calling `/api/logto/user`.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { LogtoUser } from '@logto/next';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
const Home = () => {
|
||||||
|
const { data } = useSWR<LogtoUser>('/api/logto/user');
|
||||||
|
|
||||||
|
return <div>User ID: {data?.claims?.sub}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Profile;
|
||||||
|
```
|
||||||
|
|
||||||
|
Check [this guide](https://swr.vercel.app/docs/getting-started) to learn more about `useSWR`.
|
||||||
|
|
||||||
|
### Through `getServerSideProps` in the backend
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { LogtoUser } from '@logto/next';
|
||||||
|
import { logtoClient } from '../libraries/logto';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
user: LogtoUser;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Profile = ({ user }: Props) => {
|
||||||
|
return <div>User ID: {user.claims?.sub}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Profile;
|
||||||
|
|
||||||
|
export const getServerSideProps = logtoClient.withLogtoSsr(({ request }) => {
|
||||||
|
const { user } = request;
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: { user },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Check [Next.js documentation](https://nextjs.org/docs/basic-features/data-fetching/get-server-side-props) for more details on `getServerSideProps`.
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step
|
||||||
|
title="Protect API and pages"
|
||||||
|
subtitle="2 steps"
|
||||||
|
>
|
||||||
|
|
||||||
|
### Protect API routes
|
||||||
|
|
||||||
|
Wrap your handler with `logtoClient.withLogtoApiRoute`.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// pages/api/protected-resource.ts
|
||||||
|
import { logtoClient } from '../../libraries/logto';
|
||||||
|
|
||||||
|
export default logtoClient.withLogtoApiRoute((request, response) => {
|
||||||
|
if (!request.user.isAuthenticated) {
|
||||||
|
response.status(401).json({ message: 'Unauthorized' });
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
response.json({
|
||||||
|
data: 'this_is_protected_resource',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Protect pages
|
||||||
|
|
||||||
|
If you don't want anonymous users to access a page, use `logtoClient.withLogtoSsr` to get auth state, and redirect to sign-in route if not authenticated.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export const getServerSideProps = logtoClient.withLogtoSsr(async function ({ req, res }) {
|
||||||
|
const { user } = req;
|
||||||
|
|
||||||
|
if (!user.isAuthenticated) {
|
||||||
|
res.setHeader('location', '/api/logto/sign-in');
|
||||||
|
res.statusCode = 302;
|
||||||
|
res.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: { user },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step
|
||||||
|
title="Sign out"
|
||||||
|
subtitle="1 step"
|
||||||
|
>
|
||||||
|
|
||||||
|
Calling `/api/logto/sign-out` will clear all the Logto data in memory and cookies if they exist.
|
||||||
|
|
||||||
|
After signing out, it'll be great to redirect user back to your website. Let's add `http://localhost:3000` as the Post Sign-out URI below before calling `/api/logto/sign-out`.
|
||||||
|
|
||||||
|
<UriInputField name="postLogoutRedirectUris" />
|
||||||
|
|
||||||
|
### Implement a sign-out button
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<button onClick={() => push('/api/logto/sign-out')}>Sign Out</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step
|
||||||
|
title="Further readings"
|
||||||
|
subtitle="4 articles"
|
||||||
|
>
|
||||||
|
|
||||||
|
- [Customize sign-in experience](https://docs.logto.io/docs/tutorials/get-started/customize-sign-in-experience)
|
||||||
|
- [Enable SMS or email passcode sign-in](https://docs.logto.io/docs/tutorials/get-started/passwordless-sign-in-by-adding-connectors#enable-sms-or-email-passwordless-sign-in)
|
||||||
|
- [Enable social sign-in](https://docs.logto.io/docs/tutorials/get-started/passwordless-sign-in-by-adding-connectors#enable-social-sign-in)
|
||||||
|
- [Protect your API](https://docs.logto.io/docs/recipes/protect-your-api)
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
</Steps>
|
16
packages/console/src/assets/docs/guides/web-next/index.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { ApplicationType } from '@logto/schemas';
|
||||||
|
|
||||||
|
import { type GuideMetadata } from '../types';
|
||||||
|
|
||||||
|
const metadata: Readonly<GuideMetadata> = Object.freeze({
|
||||||
|
name: 'Next.js',
|
||||||
|
description:
|
||||||
|
'Next.js is a React framework for production - it makes building fullstack React apps and sites a breeze and ships with built-in SSR.',
|
||||||
|
target: ApplicationType.Traditional,
|
||||||
|
sample: {
|
||||||
|
repo: 'js',
|
||||||
|
path: 'packages/next-sample',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default metadata;
|
11
packages/console/src/assets/docs/guides/web-next/logo.svg
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<svg viewBox="0 0 256 256" version="1.1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
preserveAspectRatio="xMidYMid">
|
||||||
|
<g>
|
||||||
|
<path
|
||||||
|
d="M119.616813,0.0688905149 C119.066276,0.118932037 117.314565,0.294077364 115.738025,0.419181169 C79.3775171,3.69690087 45.3192571,23.3131775 23.7481916,53.4631946 C11.7364614,70.2271045 4.05395894,89.2428829 1.15112414,109.384595 C0.12512219,116.415429 0,118.492153 0,128.025062 C0,137.557972 0.12512219,139.634696 1.15112414,146.665529 C8.10791789,194.730411 42.3163245,235.11392 88.7116325,250.076335 C97.0197458,252.753556 105.778299,254.580072 115.738025,255.680985 C119.616813,256.106338 136.383187,256.106338 140.261975,255.680985 C157.453763,253.779407 172.017986,249.525878 186.382014,242.194795 C188.584164,241.068861 189.00958,240.768612 188.709286,240.518404 C188.509091,240.36828 179.124927,227.782837 167.86393,212.570214 L147.393939,184.922273 L121.743891,146.965779 C107.630108,126.098464 96.0187683,109.034305 95.9186706,109.034305 C95.8185728,109.009284 95.7184751,125.873277 95.6684262,146.465363 C95.5933529,182.52028 95.5683284,183.971484 95.1178886,184.82219 C94.4672532,186.048207 93.9667644,186.548623 92.915738,187.099079 C92.114956,187.499411 91.4142717,187.574474 87.6355816,187.574474 L83.3063539,187.574474 L82.1552297,186.848872 C81.4044966,186.373477 80.8539589,185.747958 80.4785924,185.022356 L79.9530792,183.896422 L80.0031281,133.729796 L80.0782014,83.5381493 L80.8539589,82.5623397 C81.25435,82.0369037 82.1051808,81.3613431 82.7057674,81.0360732 C83.7317693,80.535658 84.1321603,80.4856165 88.4613881,80.4856165 C93.5663734,80.4856165 94.4172043,80.6857826 95.7434995,82.1369867 C96.1188661,82.5373189 110.007429,103.454675 126.623656,128.650581 C143.239883,153.846488 165.962072,188.250034 177.122972,205.139048 L197.392766,235.839522 L198.418768,235.163961 C207.502639,229.259062 217.112023,220.852086 224.719453,212.09482 C240.910264,193.504394 251.345455,170.835585 254.848876,146.665529 C255.874878,139.634696 256,137.557972 256,128.025062 C256,118.492153 255.874878,116.415429 254.848876,109.384595 C247.892082,61.3197135 213.683675,20.9362052 167.288368,5.97379012 C159.105376,3.32158945 150.396872,1.49507389 140.637341,0.394160408 C138.234995,0.143952798 121.693842,-0.131275573 119.616813,0.0688905149 L119.616813,0.0688905149 Z M172.017986,77.4831252 C173.219159,78.0836234 174.195112,79.2345784 174.545455,80.435575 C174.74565,81.0861148 174.795699,94.9976579 174.74565,126.348671 L174.670577,171.336 L166.73783,159.17591 L158.780059,147.01582 L158.780059,114.313685 C158.780059,93.1711423 158.880156,81.2862808 159.030303,80.7108033 C159.430694,79.3096407 160.306549,78.2087272 161.507722,77.5581875 C162.533724,77.0327515 162.909091,76.98271 166.837928,76.98271 C170.541544,76.98271 171.19218,77.0327515 172.017986,77.4831252 Z"
|
||||||
|
fill="#000000">
|
||||||
|
|
||||||
|
</path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.9 KiB |
|
@ -0,0 +1,20 @@
|
||||||
|
@use '@/scss/underscore' as _;
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.field {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.multiTextInput {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.saveButton {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin: _.unit(6) 0 0 _.unit(2);
|
||||||
|
}
|
||||||
|
}
|
153
packages/console/src/mdx-components-v2/UriInputField/index.tsx
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
import type { AdminConsoleKey } from '@logto/phrases';
|
||||||
|
import type { Application } from '@logto/schemas';
|
||||||
|
import type { KeyboardEvent } from 'react';
|
||||||
|
import { useContext, useRef } from 'react';
|
||||||
|
import { Controller, FormProvider, useForm } from 'react-hook-form';
|
||||||
|
import { toast } from 'react-hot-toast';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
import MultiTextInputField from '@/components/MultiTextInputField';
|
||||||
|
import Button from '@/ds-components/Button';
|
||||||
|
import FormField from '@/ds-components/FormField';
|
||||||
|
import {
|
||||||
|
convertRhfErrorMessage,
|
||||||
|
createValidatorForRhf,
|
||||||
|
} from '@/ds-components/MultiTextInput/utils';
|
||||||
|
import TextInput from '@/ds-components/TextInput';
|
||||||
|
import type { RequestError } from '@/hooks/use-api';
|
||||||
|
import useApi from '@/hooks/use-api';
|
||||||
|
import { GuideContext } from '@/pages/Applications/components/GuideV2';
|
||||||
|
import type { GuideForm } from '@/types/guide';
|
||||||
|
import { trySubmitSafe } from '@/utils/form';
|
||||||
|
import { uriValidator } from '@/utils/validator';
|
||||||
|
|
||||||
|
import * as styles from './index.module.scss';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
name: 'redirectUris' | 'postLogoutRedirectUris';
|
||||||
|
};
|
||||||
|
|
||||||
|
function UriInputField({ name }: Props) {
|
||||||
|
const methods = useForm<Partial<GuideForm>>();
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
getValues,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
formState: { isSubmitting },
|
||||||
|
} = methods;
|
||||||
|
const {
|
||||||
|
app: { id: appId },
|
||||||
|
isCompact,
|
||||||
|
} = useContext(GuideContext);
|
||||||
|
const isSingle = !isCompact;
|
||||||
|
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 title: AdminConsoleKey =
|
||||||
|
name === 'redirectUris'
|
||||||
|
? 'application_details.redirect_uri'
|
||||||
|
: 'application_details.post_sign_out_redirect_uri';
|
||||||
|
|
||||||
|
const onSubmit = trySubmitSafe(async (value: string[]) => {
|
||||||
|
const updatedApp = await api
|
||||||
|
.patch(`api/applications/${appId}`, {
|
||||||
|
json: {
|
||||||
|
oidcClientMetadata: {
|
||||||
|
[name]: value.filter(Boolean),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.json<Application>();
|
||||||
|
void mutate(updatedApp);
|
||||||
|
toast.success(t('general.saved'));
|
||||||
|
|
||||||
|
// Reset form to set 'isDirty' to false
|
||||||
|
reset(getValues());
|
||||||
|
});
|
||||||
|
|
||||||
|
const onKeyPress = (event: KeyboardEvent<HTMLInputElement>, value: string[]) => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
event.preventDefault();
|
||||||
|
void handleSubmit(async () => onSubmit(value))();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider {...methods}>
|
||||||
|
<form>
|
||||||
|
<Controller
|
||||||
|
name={name}
|
||||||
|
control={control}
|
||||||
|
defaultValue={data?.oidcClientMetadata[name]}
|
||||||
|
rules={{
|
||||||
|
validate: createValidatorForRhf({
|
||||||
|
required: t(
|
||||||
|
isSingle ? 'errors.required_field_missing' : '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 } }) => {
|
||||||
|
const errorObject = convertRhfErrorMessage(error?.message);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref} className={styles.wrapper}>
|
||||||
|
{isSingle && (
|
||||||
|
<FormField
|
||||||
|
isRequired={name === 'redirectUris'}
|
||||||
|
className={styles.field}
|
||||||
|
title={title}
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
className={styles.field}
|
||||||
|
value={value[0]}
|
||||||
|
error={errorObject?.required ?? errorObject?.inputs?.[0]}
|
||||||
|
onChange={({ currentTarget: { value } }) => {
|
||||||
|
onChange([value]);
|
||||||
|
}}
|
||||||
|
onKeyPress={(event) => {
|
||||||
|
onKeyPress(event, value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
)}
|
||||||
|
{!isSingle && (
|
||||||
|
<MultiTextInputField
|
||||||
|
isRequired={name === 'redirectUris'}
|
||||||
|
formFieldClassName={styles.field}
|
||||||
|
title={title}
|
||||||
|
value={value}
|
||||||
|
error={errorObject}
|
||||||
|
className={styles.multiTextInput}
|
||||||
|
onChange={onChange}
|
||||||
|
onKeyPress={(event) => {
|
||||||
|
onKeyPress(event, value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
className={styles.saveButton}
|
||||||
|
disabled={!isDirty}
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
title="general.save"
|
||||||
|
type="primary"
|
||||||
|
onClick={handleSubmit(async () => onSubmit(value))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UriInputField;
|
|
@ -14,10 +14,14 @@
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: _.unit(6) _.unit(6) max(10vh, 120px);
|
padding: _.unit(6) _.unit(6) max(10vh, 120px);
|
||||||
|
|
||||||
.banner {
|
section p {
|
||||||
display: flex;
|
font: var(--font-body-2);
|
||||||
align-items: center;
|
margin: _.unit(4) 0;
|
||||||
margin-bottom: _.unit(6);
|
}
|
||||||
|
|
||||||
|
section ul > li {
|
||||||
|
margin-block: _.unit(2);
|
||||||
|
padding-inline-start: _.unit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|