From 4f5881304ea0e7bcc9fccc7e3a2a7581d5a95712 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Thu, 31 Aug 2023 12:14:19 +0800 Subject: [PATCH 01/30] refactor(test): add modal ui test helpers (#4412) --- .../src/tests/ui/connectors/helpers.ts | 23 +-- .../ui/connectors/social-connectors.test.ts | 8 +- .../src/tests/ui/rbac.test.ts | 143 ++++-------------- .../src/tests/ui/user-management.test.ts | 16 +- .../src/tests/ui/webhooks.test.ts | 52 +++---- .../integration-tests/src/ui-helpers/index.ts | 37 ++--- 6 files changed, 93 insertions(+), 186 deletions(-) diff --git a/packages/integration-tests/src/tests/ui/connectors/helpers.ts b/packages/integration-tests/src/tests/ui/connectors/helpers.ts index f3e7f3cc5..71b109b6f 100644 --- a/packages/integration-tests/src/tests/ui/connectors/helpers.ts +++ b/packages/integration-tests/src/tests/ui/connectors/helpers.ts @@ -1,7 +1,11 @@ import { ConnectorType } from '@logto/connector-kit'; import { type Page } from 'puppeteer'; -import { expectConfirmModalAndAct, waitForToast } from '#src/ui-helpers/index.js'; +import { + expectConfirmModalAndAct, + expectModalWithTitle, + waitForToast, +} from '#src/ui-helpers/index.js'; import { passwordlessConnectorTestCases, @@ -38,16 +42,13 @@ export const expectToSelectConnector = async ( page: Page, { groupFactoryId, factoryId, connectorType }: SelectConnectorOption ) => { - await expect(page).toMatchElement( - '.ReactModalPortal div[class$=header] div[class$=titleEllipsis]', - { - text: - connectorType === ConnectorType.Email - ? 'Set up email connector' - : connectorType === ConnectorType.Sms - ? 'Set up SMS connector' - : 'Add Social Connector', - } + await expectModalWithTitle( + page, + connectorType === ConnectorType.Email + ? 'Set up email connector' + : connectorType === ConnectorType.Sms + ? 'Set up SMS connector' + : 'Add Social Connector' ); if (groupFactoryId) { diff --git a/packages/integration-tests/src/tests/ui/connectors/social-connectors.test.ts b/packages/integration-tests/src/tests/ui/connectors/social-connectors.test.ts index 78c47ebea..5cab1d0c0 100644 --- a/packages/integration-tests/src/tests/ui/connectors/social-connectors.test.ts +++ b/packages/integration-tests/src/tests/ui/connectors/social-connectors.test.ts @@ -7,6 +7,7 @@ import { goToAdminConsole, expectToSaveChanges, waitForToast, + expectModalWithTitle, } from '#src/ui-helpers/index.js'; import { expectNavigation, appendPathname } from '#src/utils.js'; @@ -53,12 +54,7 @@ describe('social connectors', () => { text: 'Add Social Connector', }); - await expect(page).toMatchElement( - '.ReactModalPortal div[class$=header] div[class$=titleEllipsis]', - { - text: 'Add Social Connector', - } - ); + await expectModalWithTitle(page, 'Add Social Connector'); // Close modal await page.keyboard.press('Escape'); diff --git a/packages/integration-tests/src/tests/ui/rbac.test.ts b/packages/integration-tests/src/tests/ui/rbac.test.ts index 039a9586f..6be2d4f0e 100644 --- a/packages/integration-tests/src/tests/ui/rbac.test.ts +++ b/packages/integration-tests/src/tests/ui/rbac.test.ts @@ -1,7 +1,9 @@ import { logtoConsoleUrl as logtoConsoleUrlString } from '#src/constants.js'; import { expectConfirmModalAndAct, + expectModalWithTitle, expectToClickDetailsPageOption, + expectToClickModalAction, goToAdminConsole, waitForToast, } from '#src/ui-helpers/index.js'; @@ -49,21 +51,14 @@ describe('RBAC', () => { text: 'Create API Resource', }); - await expect(page).toMatchElement( - '.ReactModalPortal div[class$=header] div[class$=titleEllipsis]', - { - text: 'Create API Resource', - } - ); + await expectModalWithTitle(page, 'Create API Resource'); await expect(page).toFillForm('.ReactModalPortal form', { name: apiResourceName, indicator: apiResourceIndicator, }); - await expect(page).toClick('.ReactModalPortal div[class$=footer] button[type=submit] span', { - text: 'Create API Resource', - }); + await expectToClickModalAction(page, 'Create API Resource'); await waitForToast(page, { text: `The API resource ${apiResourceName} has been successfully created`, @@ -83,21 +78,14 @@ describe('RBAC', () => { text: 'Create Permission', }); - await expect(page).toMatchElement( - '.ReactModalPortal div[class$=header] div[class$=titleEllipsis]', - { - text: 'Create permission', - } - ); + await expectModalWithTitle(page, 'Create permission'); await expect(page).toFillForm('.ReactModalPortal form', { name: permissionName, description: permissionDescription, }); - await expect(page).toClick('.ReactModalPortal div[class$=footer] button[type=submit] span', { - text: 'Create permission', - }); + await expectToClickModalAction(page, 'Create permission'); await waitForToast(page, { text: `The permission ${permissionName} has been successfully created`, @@ -121,25 +109,15 @@ describe('RBAC', () => { it('create a user for rbac testing', async () => { await expect(page).toClick('div[class$=headline] button span', { text: 'Add User' }); - await expect(page).toMatchElement( - '.ReactModalPortal div[class$=header] div[class$=titleEllipsis]', - { - text: 'Add User', - } - ); + await expectModalWithTitle(page, 'Add User'); await expect(page).toFillForm('.ReactModalPortal form', { username: rbacTestUsername, }); - await expect(page).toClick('.ReactModalPortal div[class$=footer] button[type=submit] span', { - text: 'Add User', - }); + await expectToClickModalAction(page, 'Add User'); - await expect(page).toMatchElement( - '.ReactModalPortal div[class$=header] div[class$=titleEllipsis]', - { text: 'This user has been successfully created' } - ); + await expectModalWithTitle(page, 'This user has been successfully created'); await page.keyboard.press('Escape'); }); @@ -160,12 +138,7 @@ describe('RBAC', () => { text: 'Create Role', }); - await expect(page).toMatchElement( - '.ReactModalPortal div[class$=header] div[class$=titleEllipsis]', - { - text: 'Create Role', - } - ); + await expectModalWithTitle(page, 'Create Role'); await expect(page).toFillForm('.ReactModalPortal form', { name: roleName, @@ -187,24 +160,14 @@ describe('RBAC', () => { } ); - await expect(page).toClick('.ReactModalPortal div[class$=footer] button[type=submit] span', { - text: 'Create Role', - }); + await expectToClickModalAction(page, 'Create Role'); await waitForToast(page, { text: `The role ${roleName} has been successfully created.`, }); - await expect(page).toMatchElement( - '.ReactModalPortal div[class$=header] div[class$=titleEllipsis]', - { - text: 'Assign users', - } - ); - - await expect(page).toClick('.ReactModalPortal div[class$=footer] button span', { - text: 'Skip for now', - }); + await expectModalWithTitle(page, 'Assign users'); + await expectToClickModalAction(page, 'Skip for now'); await expect(page).toMatchElement('div[class$=header] div[class$=info] div[class$=name]', { text: roleName, @@ -222,15 +185,9 @@ describe('RBAC', () => { ); await expect(permissionRow).toClick('td[class$=deleteColumn] button'); - await expect(page).toMatchElement( - '.ReactModalPortal div[class$=header] div[class$=titleEllipsis]', - { - text: 'Reminder', - } - ); - - await expect(page).toClick('.ReactModalPortal div[class$=footer] button span', { - text: 'Remove', + await expectConfirmModalAndAct(page, { + title: 'Reminder', + actionText: 'Remove', }); await waitForToast(page, { @@ -243,12 +200,7 @@ describe('RBAC', () => { text: 'Assign Permissions', }); - await expect(page).toMatchElement( - '.ReactModalPortal div[class$=header] div[class$=titleEllipsis]', - { - text: 'Assign permissions', - } - ); + await expectModalWithTitle(page, 'Assign permissions'); await expect(page).toClick( '.ReactModalPortal div[class$=resourceItem] div[class$=title] div[class$=name]', @@ -264,9 +216,7 @@ describe('RBAC', () => { } ); - await expect(page).toClick('.ReactModalPortal div[class$=footer] button span', { - text: 'Assign Permissions', - }); + await expectToClickModalAction(page, 'Assign Permissions'); await waitForToast(page, { text: 'The selected permissions were successfully assigned to this role', @@ -286,12 +236,7 @@ describe('RBAC', () => { text: 'Assign Users', }); - await expect(page).toMatchElement( - '.ReactModalPortal div[class$=header] div[class$=titleEllipsis]', - { - text: 'Assign users', - } - ); + await expectModalWithTitle(page, 'Assign users'); await expect(page).toClick( '.ReactModalPortal div[class$=roleUsersTransfer] div[class$=item] div[class$=title]', @@ -299,10 +244,7 @@ describe('RBAC', () => { text: rbacTestUsername, } ); - - await expect(page).toClick('.ReactModalPortal div[class$=footer] button span', { - text: 'Assign users', - }); + await expectToClickModalAction(page, 'Assign users'); await waitForToast(page, { text: 'The selected users were successfully assigned to this role', @@ -338,15 +280,9 @@ describe('RBAC', () => { // Click remove button await expect(roleRow).toClick('td:last-of-type button'); - await expect(page).toMatchElement( - '.ReactModalPortal div[class$=header] div[class$=titleEllipsis]', - { - text: 'Reminder', - } - ); - - await expect(page).toClick('.ReactModalPortal div[class$=footer] button span', { - text: 'Remove', + await expectConfirmModalAndAct(page, { + title: 'Reminder', + actionText: 'Remove', }); await waitForToast(page, { @@ -359,12 +295,7 @@ describe('RBAC', () => { text: 'Assign Roles', }); - await expect(page).toMatchElement( - '.ReactModalPortal div[class$=header] div[class$=titleEllipsis]', - { - text: `Assign roles to ${rbacTestUsername}`, - } - ); + await expectModalWithTitle(page, `Assign roles to ${rbacTestUsername}`); await expect(page).toClick( '.ReactModalPortal div[class$=rolesTransfer] div[class$=item] div[class$=name]', @@ -373,9 +304,7 @@ describe('RBAC', () => { } ); - await expect(page).toClick('.ReactModalPortal div[class$=footer] button span', { - text: 'Assign roles', - }); + await expectToClickModalAction(page, 'Assign roles'); await waitForToast(page, { text: 'Successfully assigned role(s)', @@ -458,16 +387,7 @@ describe('RBAC', () => { ); await expect(permissionRow).toClick('td[class$=deleteColumn] button'); - await expect(page).toMatchElement( - '.ReactModalPortal div[class$=header] div[class$=titleEllipsis]', - { - text: 'Reminder', - } - ); - - await expect(page).toClick('.ReactModalPortal div[class$=footer] button span', { - text: 'Delete', - }); + await expectConfirmModalAndAct(page, { title: 'Reminder', actionText: 'Delete' }); await waitForToast(page, { text: `The permission "${permissionName}" was successfully deleted.`, @@ -477,18 +397,11 @@ describe('RBAC', () => { it('delete api resource', async () => { await expectToClickDetailsPageOption(page, 'Delete'); - await expect(page).toMatchElement( - '.ReactModalPortal div[class$=header] div[class$=titleEllipsis]', - { - text: 'Reminder', - } - ); + await expectModalWithTitle(page, 'Reminder'); await expect(page).toFill('.ReactModalPortal input', apiResourceName); - await expect(page).toClick('.ReactModalPortal div[class$=footer] button span', { - text: 'Delete', - }); + await expectToClickModalAction(page, 'Delete'); await waitForToast(page, { text: `The API Resource ${apiResourceName} has been successfully deleted`, diff --git a/packages/integration-tests/src/tests/ui/user-management.test.ts b/packages/integration-tests/src/tests/ui/user-management.test.ts index 3ef039eec..ce86aaa7d 100644 --- a/packages/integration-tests/src/tests/ui/user-management.test.ts +++ b/packages/integration-tests/src/tests/ui/user-management.test.ts @@ -4,6 +4,8 @@ import { expectToSaveChanges, waitForToast, expectToDiscardChanges, + expectModalWithTitle, + expectToClickModalAction, } from '#src/ui-helpers/index.js'; import { appendPathname, @@ -44,14 +46,10 @@ describe('user management', () => { }); await expect(page).toClick('button[type=submit]'); await page.waitForSelector('div[class$=infoLine'); - await expect(page).toMatchElement( - '.ReactModalPortal div[class$=header] div[class$=titleEllipsis]', - { - text: 'This user has been successfully created', - } - ); + await expectModalWithTitle(page, 'This user has been successfully created'); + // Go to user details page - await expect(page).toClick('div.ReactModalPortal div[class$=footer] button:first-of-type'); + await expectToClickModalAction(page, 'Check user detail'); await expect(page).toMatchElement('div[class$=main] div[class$=metadata] div[class$=title]', { text: 'jdoe@gmail.com', }); @@ -131,7 +129,7 @@ describe('user management', () => { await page.waitForSelector('div[class$=infoLine'); // Go to the user details page - await expect(page).toClick('div.ReactModalPortal div[class$=footer] button:nth-of-type(1)'); + await expectToClickModalAction(page, 'Check user detail'); await expect(page).toMatchElement('div[class$=main] div[class$=metadata] div[class$=title]', { text: username, }); @@ -183,7 +181,7 @@ describe('user management', () => { await expect(page).toMatchElement('.ReactModalPortal div[class$=medium] div[class$=content]', { text: 'User needs to have at least one of the sign-in identifiers (username, email, phone number or social) to sign in. Are you sure you want to continue?', }); - await expect(page).toClick('div.ReactModalPortal div[class$=footer] button:nth-of-type(2)'); + await expectToClickModalAction(page, 'Confirm'); // After all identifiers, top userinfo card shows 'Unnamed' as the title await expect(page).toMatchElement('div[class$=main] div[class$=metadata] div[class$=title]', { text: 'Unnamed', diff --git a/packages/integration-tests/src/tests/ui/webhooks.test.ts b/packages/integration-tests/src/tests/ui/webhooks.test.ts index 3dfb27348..dc09065e1 100644 --- a/packages/integration-tests/src/tests/ui/webhooks.test.ts +++ b/packages/integration-tests/src/tests/ui/webhooks.test.ts @@ -1,5 +1,13 @@ import { logtoConsoleUrl as logtoConsoleUrlString } from '#src/constants.js'; -import { goToAdminConsole, expectToSaveChanges, waitForToast } from '#src/ui-helpers/index.js'; +import { + goToAdminConsole, + expectToSaveChanges, + waitForToast, + expectToClickModalAction, + expectToClickDetailsPageOption, + expectModalWithTitle, + expectConfirmModalAndAct, +} from '#src/ui-helpers/index.js'; import { appendPathname, expectNavigation } from '#src/utils.js'; await page.setViewport({ width: 1280, height: 720 }); @@ -93,38 +101,28 @@ describe('webhooks', () => { await createWebhookFromWebhooksPage(); // Disable webhook - await expect(page).toClick('div[class$=header] >div:nth-of-type(2) button'); - // Wait for the menu to be opened - await page.waitForTimeout(500); - await expect(page).toClick( - '.ReactModalPortal div[class$=dropdownContainer] div[role=menuitem]:nth-of-type(1)' - ); + await expectToClickDetailsPageOption(page, 'Disable webhook'); + await expectModalWithTitle(page, 'Reminder'); await expect(page).toMatchElement('.ReactModalPortal div[class$=content] div[class$=content]', { text: 'Are you sure you want to reactivate this webhook? Doing so will not send HTTP request to endpoint URL.', }); - await expect(page).toClick('.ReactModalPortal div[class$=footer] button:last-of-type'); - // Wait for the state to be updated - await page.waitForTimeout(500); + await expectToClickModalAction(page, 'Disable webhook'); + await expect(page).toMatchElement( 'div[class$=header] div[class$=metadata] div:nth-of-type(2) div[class$=outlined] div:nth-of-type(2)', { text: 'Not in use', + timeout: 1000, } ); - // Enable webhook - await expect(page).toClick('div[class$=header] >div:nth-of-type(2) button'); - // Wait for the menu to be opened - await page.waitForTimeout(500); - await expect(page).toClick( - '.ReactModalPortal div[class$=dropdownContainer] div[role=menuitem]:nth-of-type(1)' - ); - // Wait for the state to be updated - await page.waitForTimeout(500); - const stateDiv = await page.waitForSelector( + // Reactivate webhook + await expectToClickDetailsPageOption(page, 'Reactivate webhook'); + + // Wait for the active webhook state info to appear + await page.waitForSelector( 'div[class$=header] div[class$=metadata] div:nth-of-type(2) div[class$=state]' ); - expect(stateDiv).toBeTruthy(); }); it('can regenerate signing key for a webhook', async () => { @@ -132,13 +130,11 @@ describe('webhooks', () => { await createWebhookFromWebhooksPage(); await expect(page).toClick('button[class$=regenerateButton]'); - await expect(page).toMatchElement( - '.ReactModalPortal div[class$=content] div[class$=titleEllipsis]', - { - text: 'Regenerate signing key', - } - ); - await expect(page).toClick('.ReactModalPortal div[class$=footer] button:last-of-type'); + await expectConfirmModalAndAct(page, { + title: 'Regenerate signing key', + actionText: 'Regenerate', + }); + await waitForToast(page, { text: 'Signing key has been regenerated.' }); }); }); diff --git a/packages/integration-tests/src/ui-helpers/index.ts b/packages/integration-tests/src/ui-helpers/index.ts index 98765f964..877bd8197 100644 --- a/packages/integration-tests/src/ui-helpers/index.ts +++ b/packages/integration-tests/src/ui-helpers/index.ts @@ -45,9 +45,7 @@ export const expectUnsavedChangesAlert = async (page: Page) => { '.ReactModalPortal div[class$=content]::-p-text(You have made some changes. Are you sure you want to leave this page?)' ); - await expect(page).toClick('.ReactModalPortal div[class$=footer] button', { - text: 'Stay on Page', - }); + await expectToClickModalAction(page, 'Stay on Page'); }; export const expectToSaveChanges = async (page: Page) => { @@ -89,27 +87,32 @@ export const expectToClickDetailsPageOption = async (page: Page, optionText: str ); }; -type ExpectConfirmModalAndActOptions = { - title?: string | RegExp; - actionText?: string | RegExp; -}; - -export const expectConfirmModalAndAct = async ( - page: Page, - { title, actionText }: ExpectConfirmModalAndActOptions -) => { +export const expectModalWithTitle = async (page: Page, title: string | RegExp) => { await expect(page).toMatchElement( '.ReactModalPortal div[class$=header] div[class$=titleEllipsis]', { text: title, } ); +}; - if (actionText) { - await expect(page).toClick('.ReactModalPortal div[class$=footer] button span', { - text: actionText, - }); - } +export const expectToClickModalAction = async (page: Page, actionText: string | RegExp) => { + await expect(page).toClick('.ReactModalPortal div[class$=footer] button span', { + text: actionText, + }); +}; + +type ExpectConfirmModalAndActOptions = { + title: string | RegExp; + actionText: string | RegExp; +}; + +export const expectConfirmModalAndAct = async ( + page: Page, + { title, actionText }: ExpectConfirmModalAndActOptions +) => { + await expectModalWithTitle(page, title); + await expectToClickModalAction(page, actionText); }; export const expectToClickNavTab = async (page: Page, tab: string) => { From 2b39964fd2f88b8b820e6e704d32ced30f94e139 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Thu, 31 Aug 2023 14:31:03 +0800 Subject: [PATCH 02/30] test: add ui tests for applications (#4407) * test: add ui tests for applications * refactor(test): simplify application ui tests --- .../src/tests/ui/applications/constants.ts | 60 ++++ .../src/tests/ui/applications/helpers.ts | 123 ++++++++ .../src/tests/ui/applications/index.test.ts | 268 ++++++++++++++++++ .../integration-tests/src/ui-helpers/index.ts | 11 +- 4 files changed, 461 insertions(+), 1 deletion(-) create mode 100644 packages/integration-tests/src/tests/ui/applications/constants.ts create mode 100644 packages/integration-tests/src/tests/ui/applications/helpers.ts create mode 100644 packages/integration-tests/src/tests/ui/applications/index.test.ts diff --git a/packages/integration-tests/src/tests/ui/applications/constants.ts b/packages/integration-tests/src/tests/ui/applications/constants.ts new file mode 100644 index 000000000..dc4c8720e --- /dev/null +++ b/packages/integration-tests/src/tests/ui/applications/constants.ts @@ -0,0 +1,60 @@ +import { ApplicationType } from '@logto/schemas'; + +export type ApplicationCase = { + framework: string; + name: string; + description: string; + guideFilename: string; + sample: { + repo: string; + path: string; + }; + redirectUri: string; + postSignOutRedirectUri: string; +}; + +export const initialApp: ApplicationCase = { + framework: 'Next.js', + name: 'Next.js App', + description: 'This is a Next.js app', + guideFilename: 'web-next', + sample: { + repo: 'js', + path: 'packages/next-sample', + }, + redirectUri: 'https://my.test.app/sign-in', + postSignOutRedirectUri: 'https://my.test.app/sign-out', +}; + +export const testApp: ApplicationCase = { + framework: 'Go', + name: 'Go App', + description: 'This is a Go app', + guideFilename: 'web-go', + sample: { + repo: 'go', + path: 'gin-sample', + }, + redirectUri: 'https://my.test.app/sign-in', + postSignOutRedirectUri: 'https://my.test.app/sign-out', +}; + +export const frameworkGroupLabels = [ + 'Popular and for you', + 'Traditional web app', + 'Single page app', + 'Native', + 'Machine-to-machine', +] as const; + +export type ApplicationMetadata = { + type: ApplicationType; + name: string; + description: string; +}; + +export const applicationTypesMetadata = Object.entries(ApplicationType).map(([key, value]) => ({ + type: value, + name: `${key} app`, + description: `This is a ${key} app`, +})) satisfies ApplicationMetadata[]; diff --git a/packages/integration-tests/src/tests/ui/applications/helpers.ts b/packages/integration-tests/src/tests/ui/applications/helpers.ts new file mode 100644 index 000000000..cea8cebd0 --- /dev/null +++ b/packages/integration-tests/src/tests/ui/applications/helpers.ts @@ -0,0 +1,123 @@ +import { appendPath } from '@silverhand/essentials'; +import { type Page } from 'puppeteer'; + +import { + expectModalWithTitle, + expectToClickDetailsPageOption, + expectToClickModalAction, + expectToOpenNewPage, + waitForToast, +} from '#src/ui-helpers/index.js'; +import { expectNavigation } from '#src/utils.js'; + +import { frameworkGroupLabels, type ApplicationCase } from './constants.js'; + +export const expectFrameworksInGroup = async (page: Page, groupSelector: string) => { + /* eslint-disable no-await-in-loop */ + for (const groupLabel of frameworkGroupLabels) { + const frameGroup = await expect(page).toMatchElement(groupSelector, { + text: groupLabel, + }); + + const frameworks = await frameGroup.$$('div[class$=grid] div[class*=card]'); + expect(frameworks.length).toBeGreaterThan(0); + } + /* eslint-enable no-await-in-loop */ +}; + +export const expectToClickFramework = async (page: Page, framework: string) => { + const frameworkCard = await expect(page).toMatchElement( + 'div[class*=card]:has(div[class$=header] div[class$=name])', + { + text: framework, + } + ); + + await expect(frameworkCard).toClick('button span', { text: 'Start Building' }); +}; + +export const expectFrameworkExists = async (page: Page, framework: string) => { + await expect(page).toMatchElement('div[class*=card]:has(div[class$=header] div[class$=name])', { + text: framework, + }); +}; + +export const expectToProceedCreationFrom = async ( + page: Page, + { name, description }: ApplicationCase +) => { + // Expect the creation form to be open + await expectModalWithTitle(page, 'Create Application'); + + await expect(page).toFillForm('.ReactModalPortal form', { + name, + description, + }); + await expectToClickModalAction(page, 'Create Application'); + + await waitForToast(page, { text: 'Application created successfully.' }); +}; + +export const expectToProceedSdkGuide = async ( + page: Page, + { guideFilename, sample, redirectUri, postSignOutRedirectUri }: ApplicationCase, + skipFillForm = false +) => { + await expectModalWithTitle(page, 'Start with SDK and guides'); + + expect(page.url()).toContain(`/guide/${guideFilename}`); + + await expect(page).toClick('.ReactModalPortal aside[class$=sample] a span', { + text: 'Check out sample', + }); + + await expectToOpenNewPage( + browser, + appendPath(new URL('https://github.com/logto-io'), sample.repo, 'tree/HEAD', sample.path).href + ); + + if (!skipFillForm) { + const redirectUriFieldWrapper = await expect(page).toMatchElement( + 'div[class$=wrapper]:has(>div[class$=field]>div[class$=headline]>div[class$=title])', + { text: 'Redirect URI' } + ); + + await expect(redirectUriFieldWrapper).toFill('input', redirectUri); + + await expect(redirectUriFieldWrapper).toClick('button span', { text: 'Save' }); + + await waitForToast(page, { text: 'Saved' }); + + const postSignOutRedirectUriWrapper = await expect(page).toMatchElement( + 'div[class$=wrapper]:has(>div[class$=field]>div[class$=headline]>div[class$=title])', + { text: 'Post Sign-out Redirect URI' } + ); + + await expect(postSignOutRedirectUriWrapper).toFill('input', postSignOutRedirectUri); + + await expect(postSignOutRedirectUriWrapper).toClick('button span', { text: 'Save' }); + + await waitForToast(page, { text: 'Saved' }); + } + + // Finish guide + await expect(page).toClick('.ReactModalPortal nav[class$=actionBar] button span', { + text: 'Finish and done', + }); +}; + +export const expectToProceedAppDeletion = async (page: Page, appName: string) => { + // Delete the application + await expectToClickDetailsPageOption(page, 'Delete'); + + // Confirm deletion + await expectModalWithTitle(page, 'Reminder'); + + await expect(page).toFill('.ReactModalPortal div[class$=deleteConfirm] input', appName); + + await expectNavigation(expectToClickModalAction(page, 'Delete')); + + await waitForToast(page, { + text: `Application ${appName} has been successfully deleted`, + }); +}; diff --git a/packages/integration-tests/src/tests/ui/applications/index.test.ts b/packages/integration-tests/src/tests/ui/applications/index.test.ts new file mode 100644 index 000000000..24cbff1cf --- /dev/null +++ b/packages/integration-tests/src/tests/ui/applications/index.test.ts @@ -0,0 +1,268 @@ +import { logtoConsoleUrl as logtoConsoleUrlString } from '#src/constants.js'; +import { + expectConfirmModalAndAct, + expectModalWithTitle, + expectToClickModalAction, + expectToDiscardChanges, + expectToOpenNewPage, + expectToSaveChanges, + expectUnsavedChangesAlert, + goToAdminConsole, + waitForToast, +} from '#src/ui-helpers/index.js'; +import { expectNavigation, appendPathname } from '#src/utils.js'; + +import { + type ApplicationMetadata, + applicationTypesMetadata, + initialApp, + testApp, +} from './constants.js'; +import { + expectFrameworkExists, + expectToClickFramework, + expectToProceedCreationFrom, + expectToProceedSdkGuide, + expectToProceedAppDeletion, + expectFrameworksInGroup, +} from './helpers.js'; + +await page.setViewport({ width: 1920, height: 1080 }); + +describe('applications', () => { + const logtoConsoleUrl = new URL(logtoConsoleUrlString); + + beforeAll(async () => { + await goToAdminConsole(); + }); + + it('navigate to applications page', async () => { + await expectNavigation( + page.goto(appendPathname('/console/applications', logtoConsoleUrl).href) + ); + + expect(page.url()).toBe(new URL('/console/applications', logtoConsoleUrl).href); + }); + + it('the table placeholder should be rendered correctly', async () => { + await expect(page).toMatchElement( + 'div[class$=guideLibraryContainer] div[class$=titleEllipsis]', + { text: 'Select a framework or tutorial', timeout: 2000 } + ); + + await expectFrameworksInGroup(page, 'div[class$=guideGroup]:has(>label)'); + }); + + it('create the initial application from the table placeholder', async () => { + await expectToClickFramework(page, initialApp.framework); + + await expectToProceedCreationFrom(page, initialApp); + + await expectToProceedSdkGuide(page, initialApp, true); + + // Details page + await expect(page).toMatchElement('div[class$=main] div[class$=header] div[class$=name]', { + text: initialApp.name, + }); + + // Back to application list page + await expectNavigation( + expect(page).toClick('div[class$=main] a[class$=backLink]', { + text: 'Back to Applications', + }) + ); + + expect(page.url()).toBe(new URL('/console/applications', logtoConsoleUrl).href); + + /** + * Note: + * Reload the page to refresh new application data by the SWR, + * since test operations is so quick and the SWR is not updated + */ + await page.reload(); + + await expect(page).toMatchElement('table div[class$=item] a[class$=title]', { + text: initialApp.name, + }); + }); + + it('can open the logto github repo issue page when click on the framework not found button', async () => { + await expect(page).toClick('div[class$=main] div[class$=headline] button span', { + text: 'Create Application', + }); + + await expectModalWithTitle(page, 'Start with SDK and guides'); + + // Click request sdk button + await expect(page).toClick( + '.ReactModalPortal div[class$=header] button[class$=requestSdkButton]' + ); + + await expectToOpenNewPage(browser, 'https://github.com/logto-io/logto/issues'); + + // Return to the application list page + await expectNavigation( + expect(page).toClick('.ReactModalPortal div[class$=header] button:has(svg[class$=closeIcon])') + ); + + expect(page.url()).toBe(new URL('/console/applications', logtoConsoleUrl).href); + }); + + it('can create an application by framework from the app creation modal and modify its data', async () => { + await expect(page).toClick('div[class$=main] div[class$=headline] button span', { + text: 'Create Application', + }); + + await expectModalWithTitle(page, 'Start with SDK and guides'); + + await expectFrameworksInGroup(page, '.ReactModalPortal div[class$=guideGroup]:has(>label)'); + + // Expect the framework contains on the page + await expectFrameworkExists(page, testApp.framework); + + // Filter + await expect(page).toFill('div[class$=searchInput] input', testApp.framework); + + // Expect the framework exists after filtering + await expectFrameworkExists(page, testApp.framework); + + await expectToClickFramework(page, testApp.framework); + + await expectToProceedCreationFrom(page, testApp); + + await expectToProceedSdkGuide(page, testApp); + + // Expect on the details page + await expect(page).toMatchElement('div[class$=main] div[class$=header] div[class$=name]', { + text: testApp.name, + }); + + // Check guide + await expect(page).toClick('div[class$=header] button span', { text: 'Check Guide' }); + + // Wait for the guide drawer to be ready + await page.waitForTimeout(500); + + // Close guide + await expect(page).toClick( + '.ReactModalPortal div[class$=drawerContainer] div[class$=header] button:last-of-type' + ); + + // Wait for the guide drawer to disappear + await page.waitForSelector('.ReactModalPortal div[class$=drawerContainer]', { hidden: true }); + + // Update application data + await expect(page).toFillForm('form', { + description: `(New): ${testApp.description}`, + }); + + await expectUnsavedChangesAlert(page); + + await expectToSaveChanges(page); + await waitForToast(page, { text: 'Saved' }); + + const redirectUriFiled = await expect(page).toMatchElement( + 'div[class$=field]:has(>div[class$=headline]>div[class$=title]', + { text: 'Redirect URIs' } + ); + + // Add and remove redirect uri + await expect(redirectUriFiled).toClick('div[class$=multilineInput]>button>span', { + text: 'Add Another', + }); + + // Wait for the new redirect uri field + await page.waitForSelector( + 'div:has(>div[class$=deletableInput]):last-of-type button:has(svg[class$=minusIcon])' + ); + + await expect(redirectUriFiled).toFill( + 'div:has(>div[class$=deletableInput]):last-of-type input', + `${testApp.redirectUri}/v2` + ); + + await expectToSaveChanges(page); + + await waitForToast(page, { text: 'Saved' }); + + // Click delete button + await expect(redirectUriFiled).toClick( + 'div:has(>div[class$=deletableInput]):last-of-type button:has(svg[class$=minusIcon])' + ); + + await expectConfirmModalAndAct(page, { title: 'Reminder', actionText: 'Delete' }); + + await expectToSaveChanges(page); + + await waitForToast(page, { text: 'Saved' }); + + // Wait for the redirect uri field to be updated + await page.waitForTimeout(500); + + // Remove Redirect Uri + await expect(page).toFill(`input[value="${testApp.redirectUri}"]`, ''); + + await expectToSaveChanges(page); + + // Expect error + await expect(page).toMatchElement( + 'div[class$=field] div[class$=multilineInput] div[class$=error]', + { + text: 'You must enter at least one redirect URI', + } + ); + + await expectToDiscardChanges(page); + + await expectToProceedAppDeletion(page, testApp.name); + + expect(page.url()).toBe(new URL('/console/applications', logtoConsoleUrl).href); + }); + + it.each(applicationTypesMetadata)( + 'can create and modify a(n) $type application without framework', + async (app: ApplicationMetadata) => { + await expect(page).toClick('div[class$=main] div[class$=headline] button span', { + text: 'Create Application', + }); + + await expect(page).toClick('.ReactModalPortal nav[class$=actionBar] button span', { + text: 'Create app without framework', + }); + + await expectModalWithTitle(page, 'Create Application'); + + await expect(page).toClick(`div[class*=radio][role=radio]:has(input[value=${app.type}])`); + + await expect(page).toFillForm('.ReactModalPortal form', { + name: app.name, + description: app.description, + }); + await expectToClickModalAction(page, 'Create Application'); + + await waitForToast(page, { text: 'Application created successfully.' }); + + await expect(page).toMatchElement('div[class$=main] div[class$=header] div[class$=name]', { + text: app.name, + }); + + await expectToProceedAppDeletion(page, app.name); + + expect(page.url()).toBe(new URL('/console/applications', logtoConsoleUrl).href); + } + ); + + it('delete the initial application', async () => { + await expect(page).toClick('table tbody tr td div[class$=item] a[class$=title]', { + text: initialApp.name, + }); + + await expect(page).toMatchElement('div[class$=main] div[class$=header] div[class$=name]', { + text: initialApp.name, + }); + + await expectToProceedAppDeletion(page, initialApp.name); + + expect(page.url()).toBe(new URL('/console/applications', logtoConsoleUrl).href); + }); +}); diff --git a/packages/integration-tests/src/ui-helpers/index.ts b/packages/integration-tests/src/ui-helpers/index.ts index 877bd8197..5bf657e74 100644 --- a/packages/integration-tests/src/ui-helpers/index.ts +++ b/packages/integration-tests/src/ui-helpers/index.ts @@ -1,4 +1,4 @@ -import { type Page } from 'puppeteer'; +import { type Browser, type Page } from 'puppeteer'; import { consolePassword, @@ -120,3 +120,12 @@ export const expectToClickNavTab = async (page: Page, tab: string) => { text: tab, }); }; + +export const expectToOpenNewPage = async (browser: Browser, url: string) => { + const target = await browser.waitForTarget((target) => target.url() === url); + + const newPage = await target.page(); + expect(newPage).toBeTruthy(); + + await newPage?.close(); +}; From ac859afec98350f9edfbbc04c8a241e264b3c06d Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Sat, 2 Sep 2023 16:21:59 +0800 Subject: [PATCH 03/30] fix(console): update default tenant id on manual navigation only (#4417) --- .../Topbar/TenantSelector/index.tsx | 3 ++ .../src/containers/TenantAccess/index.tsx | 9 ----- .../src/hooks/use-user-default-tenant-id.ts | 33 ++++++++----------- 3 files changed, 16 insertions(+), 29 deletions(-) diff --git a/packages/console/src/containers/AppContent/components/Topbar/TenantSelector/index.tsx b/packages/console/src/containers/AppContent/components/Topbar/TenantSelector/index.tsx index 8115f7dba..b1933d173 100644 --- a/packages/console/src/containers/AppContent/components/Topbar/TenantSelector/index.tsx +++ b/packages/console/src/containers/AppContent/components/Topbar/TenantSelector/index.tsx @@ -9,6 +9,7 @@ import { TenantsContext } from '@/contexts/TenantsProvider'; import Divider from '@/ds-components/Divider'; import Dropdown from '@/ds-components/Dropdown'; import OverlayScrollbar from '@/ds-components/OverlayScrollbar'; +import useUserDefaultTenantId from '@/hooks/use-user-default-tenant-id'; import { onKeyDownHandler } from '@/utils/a11y'; import TenantDropdownItem from './TenantDropdownItem'; @@ -28,6 +29,7 @@ export default function TenantSelector() { const anchorRef = useRef(null); const [showDropdown, setShowDropdown] = useState(false); const [showCreateTenantModal, setShowCreateTenantModal] = useState(false); + const { updateDefaultTenantId } = useUserDefaultTenantId(); if (tenants.length === 0 || !currentTenantInfo) { return null; @@ -69,6 +71,7 @@ export default function TenantSelector() { isSelected={tenantData.id === currentTenantId} onClick={() => { navigateTenant(tenantData.id); + void updateDefaultTenantId(tenantData.id); setShowDropdown(false); }} /> diff --git a/packages/console/src/containers/TenantAccess/index.tsx b/packages/console/src/containers/TenantAccess/index.tsx index 43a8d4174..3d92438ba 100644 --- a/packages/console/src/containers/TenantAccess/index.tsx +++ b/packages/console/src/containers/TenantAccess/index.tsx @@ -10,7 +10,6 @@ import AppLoading from '@/components/AppLoading'; // eslint-disable-next-line unused-imports/no-unused-imports import type ProtectedRoutes from '@/containers/ProtectedRoutes'; import { TenantsContext } from '@/contexts/TenantsProvider'; -import useUserDefaultTenantId from '@/hooks/use-user-default-tenant-id'; /** * The container that ensures the user has access to the current tenant. When the user is @@ -47,7 +46,6 @@ export default function TenantAccess() { const { getAccessToken, signIn, isAuthenticated } = useLogto(); const { currentTenant, currentTenantId, currentTenantStatus, setCurrentTenantStatus } = useContext(TenantsContext); - const { updateIfNeeded } = useUserDefaultTenantId(); const { mutate } = useSWRConfig(); // Clean the cache when the current tenant ID changes. This is required because the @@ -104,12 +102,5 @@ export default function TenantAccess() { signIn, ]); - // Update the user's default tenant ID if the current tenant is validated. - useEffect(() => { - if (currentTenantStatus === 'validated') { - void updateIfNeeded(); - } - }, [currentTenantStatus, updateIfNeeded]); - return currentTenantStatus === 'validated' ? : ; } diff --git a/packages/console/src/hooks/use-user-default-tenant-id.ts b/packages/console/src/hooks/use-user-default-tenant-id.ts index 26f1d0360..687beb224 100644 --- a/packages/console/src/hooks/use-user-default-tenant-id.ts +++ b/packages/console/src/hooks/use-user-default-tenant-id.ts @@ -1,6 +1,6 @@ import { defaultTenantId as ossDefaultTenantId } from '@logto/schemas'; import { trySafe } from '@silverhand/essentials'; -import { useCallback, useContext, useMemo, useState } from 'react'; +import { useCallback, useContext, useMemo } from 'react'; import { z } from 'zod'; import { isCloud } from '@/consts/env'; @@ -24,8 +24,6 @@ const useUserDefaultTenantId = () => { () => trySafe(() => z.object({ [key]: z.string() }).parse(data)[key]), [data] ); - /** The last tenant ID that has been updated in the user's `customData`. */ - const [updatedTenantId, setUpdatedTenantId] = useState(storedId); const defaultTenantId = useMemo(() => { // Directly return the default tenant ID for OSS because it's single tenant. @@ -42,31 +40,26 @@ const useUserDefaultTenantId = () => { return tenants[0]?.id; }, [storedId, tenants]); - const updateIfNeeded = useCallback(async () => { - // No need for updating for OSS because it's single tenant. - if (!isCloud) { - return; - } + const updateDefaultTenantId = useCallback( + async (tenantId: string) => { + // No need for updating for OSS because it's single tenant. + if (!isCloud) { + return; + } - // Note storedId is not checked here because it's by design that the default tenant ID - // should be updated only when the user manually changes the current tenant. That is, - // if the user opens a new tab and go back to the original tab, the default tenant ID - // should still be the ID of the new tab. - if (currentTenantId !== updatedTenantId) { - setUpdatedTenantId(currentTenantId); await updateMeCustomData({ - [key]: currentTenantId, + [key]: tenantId, }); - } - }, [currentTenantId, updateMeCustomData, updatedTenantId]); + }, + [updateMeCustomData] + ); return useMemo( () => ({ defaultTenantId, - /** Update the default tenant ID to the current tenant ID. */ - updateIfNeeded, + updateDefaultTenantId, }), - [defaultTenantId, updateIfNeeded] + [defaultTenantId, updateDefaultTenantId] ); }; From 8607b3eb75d8fb333d59304e9e71b657e6c6069e Mon Sep 17 00:00:00 2001 From: simeng-li Date: Mon, 4 Sep 2023 10:55:04 +0800 Subject: [PATCH 04/30] refactor(core): clean up useless interaction guard (#4409) refactor(core): clean up unused interaction guards clean up unused interaction guards --- packages/core/src/routes/interaction/index.ts | 6 ++-- .../src/routes/interaction/types/guard.ts | 22 +------------ .../src/routes/interaction/types/index.ts | 33 ++++++++++++------- packages/schemas/src/types/interactions.ts | 7 ++-- 4 files changed, 28 insertions(+), 40 deletions(-) diff --git a/packages/core/src/routes/interaction/index.ts b/packages/core/src/routes/interaction/index.ts index ab35a3160..61c884e0b 100644 --- a/packages/core/src/routes/interaction/index.ts +++ b/packages/core/src/routes/interaction/index.ts @@ -88,16 +88,16 @@ export default function interactionRoutes( verifyProfileSettings(profile, signInExperience); } - const verifiedIdentifier = identifier && [ + const verifiedIdentifiers = identifier && [ await verifyIdentifierPayload(ctx, tenant, identifier, { event, }), ]; - eventLog.append({ profile, verifiedIdentifier }); + eventLog.append({ profile, verifiedIdentifiers }); await storeInteractionResult( - { event, identifiers: verifiedIdentifier, profile }, + { event, identifiers: verifiedIdentifiers, profile }, ctx, provider ); diff --git a/packages/core/src/routes/interaction/types/guard.ts b/packages/core/src/routes/interaction/types/guard.ts index 664c683b2..57f52d9aa 100644 --- a/packages/core/src/routes/interaction/types/guard.ts +++ b/packages/core/src/routes/interaction/types/guard.ts @@ -1,6 +1,6 @@ import { socialUserInfoGuard } from '@logto/connector-kit'; import { validateRedirectUrl } from '@logto/core-kit'; -import { eventGuard, profileGuard, InteractionEvent } from '@logto/schemas'; +import { eventGuard, profileGuard } from '@logto/schemas'; import { z } from 'zod'; // Social Authorization Uri Route Payload Guard @@ -46,26 +46,6 @@ export const anonymousInteractionResultGuard = z.object({ identifiers: z.array(identifierGuard).optional(), }); -export const verifiedRegisterInteractionResultGuard = z.object({ - event: z.literal(InteractionEvent.Register), - profile: profileGuard.optional(), - identifiers: z.array(identifierGuard).optional(), -}); - -export const verifiedSignInteractionResultGuard = z.object({ - event: z.literal(InteractionEvent.SignIn), - accountId: z.string(), - profile: profileGuard.optional(), - identifiers: z.array(identifierGuard), -}); - export const forgotPasswordProfileGuard = z.object({ password: z.string(), }); - -export const verifiedForgotPasswordInteractionResultGuard = z.object({ - event: z.literal(InteractionEvent.ForgotPassword), - accountId: z.string(), - identifiers: z.array(identifierGuard), - profile: forgotPasswordProfileGuard, -}); diff --git a/packages/core/src/routes/interaction/types/index.ts b/packages/core/src/routes/interaction/types/index.ts index be87c3202..7fd92d561 100644 --- a/packages/core/src/routes/interaction/types/index.ts +++ b/packages/core/src/routes/interaction/types/index.ts @@ -6,6 +6,7 @@ import type { InteractionEvent, SocialEmailPayload, SocialPhonePayload, + Profile, } from '@logto/schemas'; import type { z } from 'zod'; @@ -17,9 +18,6 @@ import type { socialIdentifierGuard, identifierGuard, anonymousInteractionResultGuard, - verifiedRegisterInteractionResultGuard, - verifiedSignInteractionResultGuard, - verifiedForgotPasswordInteractionResultGuard, } from './guard.js'; /* Payload Types */ @@ -44,7 +42,7 @@ export type SocialIdentifier = z.infer; export type Identifier = z.infer; -// Interaction +// Interaction Result export type AnonymousInteractionResult = z.infer; export type RegisterInteractionResult = Omit & { @@ -59,6 +57,7 @@ export type ForgotPasswordInteractionResult = Omit & { accountId: string; @@ -73,15 +72,27 @@ export type IdentifierVerifiedInteractionResult = | RegisterInteractionResult | AccountVerifiedInteractionResult; -export type VerifiedRegisterInteractionResult = z.infer< - typeof verifiedRegisterInteractionResultGuard ->; +export type VerifiedRegisterInteractionResult = { + event: InteractionEvent.Register; + profile?: Profile; + identifiers?: Identifier[]; +}; -export type VerifiedSignInInteractionResult = z.infer; +export type VerifiedSignInInteractionResult = { + event: InteractionEvent.SignIn; + accountId: string; + identifiers: Identifier[]; + profile?: Profile; +}; -export type VerifiedForgotPasswordInteractionResult = z.infer< - typeof verifiedForgotPasswordInteractionResultGuard ->; +export type VerifiedForgotPasswordInteractionResult = { + event: InteractionEvent.ForgotPassword; + accountId: string; + identifiers: Identifier[]; + profile: { + password: string; + }; +}; export type VerifiedInteractionResult = | VerifiedRegisterInteractionResult diff --git a/packages/schemas/src/types/interactions.ts b/packages/schemas/src/types/interactions.ts index c5e356f9c..6da992c90 100644 --- a/packages/schemas/src/types/interactions.ts +++ b/packages/schemas/src/types/interactions.ts @@ -13,9 +13,8 @@ import { } from './verification-code.js'; /** - * Detailed Identifier Methods guard + * Detailed interaction identifier payload guard */ - export const usernamePasswordPayloadGuard = z.object({ username: z.string().min(1), password: z.string().min(1), @@ -54,9 +53,7 @@ export const socialPhonePayloadGuard = z.object({ export type SocialPhonePayload = z.infer; -// Interaction Payload Guard - -/** Interaction flow (main flow) types. */ +// Interaction flow event types export enum InteractionEvent { SignIn = 'SignIn', Register = 'Register', From 89300379bdf03ab89f1d91a6fc0ff4c535115f6a Mon Sep 17 00:00:00 2001 From: Charles Zhao Date: Mon, 4 Sep 2023 10:57:18 +0800 Subject: [PATCH 05/30] fix(console): block margin should be the same on get-started page (#4414) --- .../src/pages/GetStarted/FreePlanNotification/index.module.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/console/src/pages/GetStarted/FreePlanNotification/index.module.scss b/packages/console/src/pages/GetStarted/FreePlanNotification/index.module.scss index d53eddef4..875435143 100644 --- a/packages/console/src/pages/GetStarted/FreePlanNotification/index.module.scss +++ b/packages/console/src/pages/GetStarted/FreePlanNotification/index.module.scss @@ -8,7 +8,6 @@ justify-content: space-between; align-items: center; gap: _.unit(6); - margin-bottom: _.unit(4); .image { flex-shrink: 0; From e8c544afcb1c8baf02c339fb2cf6c3cae9695968 Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Mon, 4 Sep 2023 12:55:32 +0800 Subject: [PATCH 06/30] refactor(console): update relative path checking logic --- packages/console/src/hooks/use-tenant-pathname.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/console/src/hooks/use-tenant-pathname.ts b/packages/console/src/hooks/use-tenant-pathname.ts index 3c017c4bd..96bb7d373 100644 --- a/packages/console/src/hooks/use-tenant-pathname.ts +++ b/packages/console/src/hooks/use-tenant-pathname.ts @@ -67,7 +67,7 @@ function useTenantPathname(): TenantPathname { const match = useCallback( (pathname: string, exact = false) => { // Match relative pathnames directly - if (pathname.startsWith('.')) { + if (!pathname.startsWith('/')) { return ( matchPath(joinPath(location.pathname, pathname, exact ? '' : '*'), location.pathname) !== null From d1be138cad548dcf8dedb093ad9f315f978f9d5b Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Mon, 4 Sep 2023 15:27:31 +0800 Subject: [PATCH 07/30] refactor(console): fix comment --- packages/console/src/pages/SignInExperience/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/console/src/pages/SignInExperience/index.tsx b/packages/console/src/pages/SignInExperience/index.tsx index 0782ed269..60b4d4644 100644 --- a/packages/console/src/pages/SignInExperience/index.tsx +++ b/packages/console/src/pages/SignInExperience/index.tsx @@ -214,7 +214,7 @@ function SignInExperience() { {t('sign_in_exp.tabs.content')} - {/* Uncomment until all the changes are merged */} + {/* Remove the `isCloud` check until all the changes are merged */} {isCloud && ( {t('sign_in_exp.tabs.password_policy')} )} From b2d6a9a72f690e086146adaf5471f2a095e71c27 Mon Sep 17 00:00:00 2001 From: Charles Zhao Date: Mon, 4 Sep 2023 16:05:30 +0800 Subject: [PATCH 08/30] chore(console): remove unused v1 guides and mdx-components (#4423) --- .../assets/docs/guides/m2m-general/README.mdx | 6 +- .../guides/native-android-java/README.mdx | 6 +- .../docs/guides/native-android-kt/README.mdx | 6 +- .../docs/guides/native-capacitor/README.mdx | 6 +- .../docs/guides/native-flutter/README.mdx | 6 +- .../docs/guides/native-ios-swift/README.mdx | 6 +- .../assets/docs/guides/spa-react/README.mdx | 6 +- .../assets/docs/guides/spa-vanilla/README.mdx | 6 +- .../src/assets/docs/guides/spa-vue/README.mdx | 6 +- .../docs/guides/web-asp-net-core/README.mdx | 6 +- .../assets/docs/guides/web-express/README.mdx | 6 +- .../src/assets/docs/guides/web-go/README.mdx | 6 +- .../docs/guides/web-gpt-plugin/README.mdx | 6 +- .../guides/web-next-app-router/README.mdx | 6 +- .../assets/docs/guides/web-next/README.mdx | 6 +- .../assets/docs/guides/web-outline/README.mdx | 6 +- .../src/assets/docs/guides/web-php/README.mdx | 6 +- .../assets/docs/guides/web-python/README.mdx | 6 +- .../assets/docs/guides/web-remix/README.mdx | 6 +- .../docs/tutorial/integrate-sdk/android.mdx | 229 ---------- .../tutorial/integrate-sdk/android_zh-cn.mdx | 222 ---------- .../docs/tutorial/integrate-sdk/express.mdx | 227 ---------- .../tutorial/integrate-sdk/express_zh-cn.mdx | 222 ---------- .../assets/docs/tutorial/integrate-sdk/go.mdx | 391 ------------------ .../docs/tutorial/integrate-sdk/go_zh-cn.mdx | 371 ----------------- .../docs/tutorial/integrate-sdk/ios.mdx | 151 ------- .../docs/tutorial/integrate-sdk/ios_zh-cn.mdx | 139 ------- .../docs/tutorial/integrate-sdk/next.mdx | 276 ------------- .../tutorial/integrate-sdk/next_zh-cn.mdx | 270 ------------ .../docs/tutorial/integrate-sdk/react.mdx | 201 --------- .../tutorial/integrate-sdk/react_zh-cn.mdx | 189 --------- .../docs/tutorial/integrate-sdk/vanilla.mdx | 166 -------- .../tutorial/integrate-sdk/vanilla_zh-cn.mdx | 152 ------- .../docs/tutorial/integrate-sdk/vue.mdx | 220 ---------- .../docs/tutorial/integrate-sdk/vue_zh-cn.mdx | 207 ---------- .../mdx-components-v2/Step/index.module.scss | 15 - .../src/mdx-components-v2/Step/index.tsx | 32 -- .../UriInputField/index.module.scss | 27 -- .../mdx-components-v2/UriInputField/index.tsx | 161 -------- .../ApplicationCredentials/index.module.scss | 0 .../ApplicationCredentials/index.tsx | 0 .../README.md | 0 .../Sample/index.module.scss | 0 .../Sample/index.tsx | 0 .../src/mdx-components/Step/index.module.scss | 102 +---- .../console/src/mdx-components/Step/index.tsx | 104 +---- .../Steps/FurtherReadings.tsx | 0 .../Steps/index.module.scss | 0 .../Steps/index.tsx | 0 .../mdx-components/TabItem/index.module.scss | 35 ++ .../UriInputField/index.module.scss | 7 + .../mdx-components/UriInputField/index.tsx | 30 +- 52 files changed, 141 insertions(+), 4119 deletions(-) delete mode 100644 packages/console/src/assets/docs/tutorial/integrate-sdk/android.mdx delete mode 100644 packages/console/src/assets/docs/tutorial/integrate-sdk/android_zh-cn.mdx delete mode 100644 packages/console/src/assets/docs/tutorial/integrate-sdk/express.mdx delete mode 100644 packages/console/src/assets/docs/tutorial/integrate-sdk/express_zh-cn.mdx delete mode 100644 packages/console/src/assets/docs/tutorial/integrate-sdk/go.mdx delete mode 100644 packages/console/src/assets/docs/tutorial/integrate-sdk/go_zh-cn.mdx delete mode 100644 packages/console/src/assets/docs/tutorial/integrate-sdk/ios.mdx delete mode 100644 packages/console/src/assets/docs/tutorial/integrate-sdk/ios_zh-cn.mdx delete mode 100644 packages/console/src/assets/docs/tutorial/integrate-sdk/next.mdx delete mode 100644 packages/console/src/assets/docs/tutorial/integrate-sdk/next_zh-cn.mdx delete mode 100644 packages/console/src/assets/docs/tutorial/integrate-sdk/react.mdx delete mode 100644 packages/console/src/assets/docs/tutorial/integrate-sdk/react_zh-cn.mdx delete mode 100644 packages/console/src/assets/docs/tutorial/integrate-sdk/vanilla.mdx delete mode 100644 packages/console/src/assets/docs/tutorial/integrate-sdk/vanilla_zh-cn.mdx delete mode 100644 packages/console/src/assets/docs/tutorial/integrate-sdk/vue.mdx delete mode 100644 packages/console/src/assets/docs/tutorial/integrate-sdk/vue_zh-cn.mdx delete mode 100644 packages/console/src/mdx-components-v2/Step/index.module.scss delete mode 100644 packages/console/src/mdx-components-v2/Step/index.tsx delete mode 100644 packages/console/src/mdx-components-v2/UriInputField/index.module.scss delete mode 100644 packages/console/src/mdx-components-v2/UriInputField/index.tsx rename packages/console/src/{mdx-components-v2 => mdx-components}/ApplicationCredentials/index.module.scss (100%) rename packages/console/src/{mdx-components-v2 => mdx-components}/ApplicationCredentials/index.tsx (100%) rename packages/console/src/{mdx-components-v2 => mdx-components}/README.md (100%) rename packages/console/src/{mdx-components-v2 => mdx-components}/Sample/index.module.scss (100%) rename packages/console/src/{mdx-components-v2 => mdx-components}/Sample/index.tsx (100%) rename packages/console/src/{mdx-components-v2 => mdx-components}/Steps/FurtherReadings.tsx (100%) rename packages/console/src/{mdx-components-v2 => mdx-components}/Steps/index.module.scss (100%) rename packages/console/src/{mdx-components-v2 => mdx-components}/Steps/index.tsx (100%) create mode 100644 packages/console/src/mdx-components/TabItem/index.module.scss diff --git a/packages/console/src/assets/docs/guides/m2m-general/README.mdx b/packages/console/src/assets/docs/guides/m2m-general/README.mdx index 911259fa6..563981849 100644 --- a/packages/console/src/assets/docs/guides/m2m-general/README.mdx +++ b/packages/console/src/assets/docs/guides/m2m-general/README.mdx @@ -1,7 +1,7 @@ import InlineNotification from '@/ds-components/InlineNotification'; -import Steps from '@/mdx-components-v2/Steps'; -import Step from '@/mdx-components-v2/Step'; -import ApplicationCredentials from '@/mdx-components-v2/ApplicationCredentials'; +import Steps from '@/mdx-components/Steps'; +import Step from '@/mdx-components/Step'; +import ApplicationCredentials from '@/mdx-components/ApplicationCredentials'; import EnableAdminAccess from './components/EnableAdminAccess'; import EnableAdminAccessSrc from './assets/enable-admin-access.png'; import AppIdentifierSrc from './assets/api-identifier.png'; diff --git a/packages/console/src/assets/docs/guides/native-android-java/README.mdx b/packages/console/src/assets/docs/guides/native-android-java/README.mdx index 506a40e30..a6c661d4f 100644 --- a/packages/console/src/assets/docs/guides/native-android-java/README.mdx +++ b/packages/console/src/assets/docs/guides/native-android-java/README.mdx @@ -1,7 +1,7 @@ -import UriInputField from '@/mdx-components-v2/UriInputField'; +import UriInputField from '@/mdx-components/UriInputField'; import InlineNotification from '@/ds-components/InlineNotification'; -import Steps from '@/mdx-components-v2/Steps'; -import Step from '@/mdx-components-v2/Step'; +import Steps from '@/mdx-components/Steps'; +import Step from '@/mdx-components/Step'; diff --git a/packages/console/src/assets/docs/guides/native-android-kt/README.mdx b/packages/console/src/assets/docs/guides/native-android-kt/README.mdx index 051a036a9..760cb27aa 100644 --- a/packages/console/src/assets/docs/guides/native-android-kt/README.mdx +++ b/packages/console/src/assets/docs/guides/native-android-kt/README.mdx @@ -1,7 +1,7 @@ -import UriInputField from '@/mdx-components-v2/UriInputField'; +import UriInputField from '@/mdx-components/UriInputField'; import InlineNotification from '@/ds-components/InlineNotification'; -import Steps from '@/mdx-components-v2/Steps'; -import Step from '@/mdx-components-v2/Step'; +import Steps from '@/mdx-components/Steps'; +import Step from '@/mdx-components/Step'; diff --git a/packages/console/src/assets/docs/guides/native-capacitor/README.mdx b/packages/console/src/assets/docs/guides/native-capacitor/README.mdx index aaee37e7c..2d8cf9624 100644 --- a/packages/console/src/assets/docs/guides/native-capacitor/README.mdx +++ b/packages/console/src/assets/docs/guides/native-capacitor/README.mdx @@ -1,6 +1,6 @@ -import UriInputField from '@/mdx-components-v2/UriInputField'; -import Steps from '@/mdx-components-v2/Steps'; -import Step from '@/mdx-components-v2/Step'; +import UriInputField from '@/mdx-components/UriInputField'; +import Steps from '@/mdx-components/Steps'; +import Step from '@/mdx-components/Step'; import capaticorIos from './assets/capacitor-ios.webp'; import logtoSignInPage from './assets/logto-sign-in-page.webp'; diff --git a/packages/console/src/assets/docs/guides/native-flutter/README.mdx b/packages/console/src/assets/docs/guides/native-flutter/README.mdx index 84fd7e02c..c9832af1e 100644 --- a/packages/console/src/assets/docs/guides/native-flutter/README.mdx +++ b/packages/console/src/assets/docs/guides/native-flutter/README.mdx @@ -1,6 +1,6 @@ -import UriInputField from '@/mdx-components-v2/UriInputField'; -import Steps from '@/mdx-components-v2/Steps'; -import Step from '@/mdx-components-v2/Step'; +import UriInputField from '@/mdx-components/UriInputField'; +import Steps from '@/mdx-components/Steps'; +import Step from '@/mdx-components/Step'; import Tabs from '@mdx/components/Tabs'; import TabItem from '@mdx/components/TabItem'; import InlineNotification from '@/ds-components/InlineNotification'; diff --git a/packages/console/src/assets/docs/guides/native-ios-swift/README.mdx b/packages/console/src/assets/docs/guides/native-ios-swift/README.mdx index a46513fb0..9e6cde0e2 100644 --- a/packages/console/src/assets/docs/guides/native-ios-swift/README.mdx +++ b/packages/console/src/assets/docs/guides/native-ios-swift/README.mdx @@ -1,9 +1,9 @@ -import UriInputField from '@/mdx-components-v2/UriInputField'; +import UriInputField from '@/mdx-components/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'; +import Steps from '@/mdx-components/Steps'; +import Step from '@/mdx-components/Step'; diff --git a/packages/console/src/assets/docs/guides/spa-react/README.mdx b/packages/console/src/assets/docs/guides/spa-react/README.mdx index b35e61bfc..ae3289786 100644 --- a/packages/console/src/assets/docs/guides/spa-react/README.mdx +++ b/packages/console/src/assets/docs/guides/spa-react/README.mdx @@ -1,9 +1,9 @@ -import UriInputField from '@/mdx-components-v2/UriInputField'; +import UriInputField from '@/mdx-components/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'; +import Steps from '@/mdx-components/Steps'; +import Step from '@/mdx-components/Step'; diff --git a/packages/console/src/assets/docs/guides/spa-vanilla/README.mdx b/packages/console/src/assets/docs/guides/spa-vanilla/README.mdx index 6c69599e8..450ad45d5 100644 --- a/packages/console/src/assets/docs/guides/spa-vanilla/README.mdx +++ b/packages/console/src/assets/docs/guides/spa-vanilla/README.mdx @@ -1,9 +1,9 @@ -import UriInputField from '@/mdx-components-v2/UriInputField'; +import UriInputField from '@/mdx-components/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'; +import Steps from '@/mdx-components/Steps'; +import Step from '@/mdx-components/Step'; diff --git a/packages/console/src/assets/docs/guides/spa-vue/README.mdx b/packages/console/src/assets/docs/guides/spa-vue/README.mdx index cab94a588..70a94dc49 100644 --- a/packages/console/src/assets/docs/guides/spa-vue/README.mdx +++ b/packages/console/src/assets/docs/guides/spa-vue/README.mdx @@ -1,9 +1,9 @@ -import UriInputField from '@/mdx-components-v2/UriInputField'; +import UriInputField from '@/mdx-components/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'; +import Steps from '@/mdx-components/Steps'; +import Step from '@/mdx-components/Step'; diff --git a/packages/console/src/assets/docs/guides/web-asp-net-core/README.mdx b/packages/console/src/assets/docs/guides/web-asp-net-core/README.mdx index 294579975..ce0256690 100644 --- a/packages/console/src/assets/docs/guides/web-asp-net-core/README.mdx +++ b/packages/console/src/assets/docs/guides/web-asp-net-core/README.mdx @@ -1,10 +1,10 @@ -import UriInputField from '@/mdx-components-v2/UriInputField'; +import UriInputField from '@/mdx-components/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'; +import Steps from '@/mdx-components/Steps'; +import Step from '@/mdx-components/Step'; diff --git a/packages/console/src/assets/docs/guides/web-express/README.mdx b/packages/console/src/assets/docs/guides/web-express/README.mdx index 467c7cad0..e335f5410 100644 --- a/packages/console/src/assets/docs/guides/web-express/README.mdx +++ b/packages/console/src/assets/docs/guides/web-express/README.mdx @@ -1,10 +1,10 @@ -import UriInputField from '@/mdx-components-v2/UriInputField'; +import UriInputField from '@/mdx-components/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'; +import Steps from '@/mdx-components/Steps'; +import Step from '@/mdx-components/Step'; diff --git a/packages/console/src/assets/docs/guides/web-go/README.mdx b/packages/console/src/assets/docs/guides/web-go/README.mdx index 520063e52..0764722b8 100644 --- a/packages/console/src/assets/docs/guides/web-go/README.mdx +++ b/packages/console/src/assets/docs/guides/web-go/README.mdx @@ -1,6 +1,6 @@ -import UriInputField from '@/mdx-components-v2/UriInputField'; -import Steps from '@/mdx-components-v2/Steps'; -import Step from '@/mdx-components-v2/Step'; +import UriInputField from '@/mdx-components/UriInputField'; +import Steps from '@/mdx-components/Steps'; +import Step from '@/mdx-components/Step'; import InlineNotification from '@/ds-components/InlineNotification'; diff --git a/packages/console/src/assets/docs/guides/web-gpt-plugin/README.mdx b/packages/console/src/assets/docs/guides/web-gpt-plugin/README.mdx index 143a299bd..52e45be88 100644 --- a/packages/console/src/assets/docs/guides/web-gpt-plugin/README.mdx +++ b/packages/console/src/assets/docs/guides/web-gpt-plugin/README.mdx @@ -1,6 +1,6 @@ -import Steps from '@/mdx-components-v2/Steps'; -import Step from '@/mdx-components-v2/Step'; -import UriInputField from '@/mdx-components-v2/UriInputField'; +import Steps from '@/mdx-components/Steps'; +import Step from '@/mdx-components/Step'; +import UriInputField from '@/mdx-components/UriInputField'; import AlwaysIssueRefreshToken from './components/AlwaysIssueRefreshToken'; import AiPluginJson from './components/AiPluginJson'; diff --git a/packages/console/src/assets/docs/guides/web-next-app-router/README.mdx b/packages/console/src/assets/docs/guides/web-next-app-router/README.mdx index fc865e117..6f69d958c 100644 --- a/packages/console/src/assets/docs/guides/web-next-app-router/README.mdx +++ b/packages/console/src/assets/docs/guides/web-next-app-router/README.mdx @@ -1,10 +1,10 @@ -import UriInputField from '@/mdx-components-v2/UriInputField'; +import UriInputField from '@/mdx-components/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'; +import Steps from '@/mdx-components/Steps'; +import Step from '@/mdx-components/Step'; diff --git a/packages/console/src/assets/docs/guides/web-next/README.mdx b/packages/console/src/assets/docs/guides/web-next/README.mdx index 4fe94ff22..464471abc 100644 --- a/packages/console/src/assets/docs/guides/web-next/README.mdx +++ b/packages/console/src/assets/docs/guides/web-next/README.mdx @@ -1,10 +1,10 @@ -import UriInputField from '@/mdx-components-v2/UriInputField'; +import UriInputField from '@/mdx-components/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'; +import Steps from '@/mdx-components/Steps'; +import Step from '@/mdx-components/Step'; diff --git a/packages/console/src/assets/docs/guides/web-outline/README.mdx b/packages/console/src/assets/docs/guides/web-outline/README.mdx index 8386bda5b..1404ee241 100644 --- a/packages/console/src/assets/docs/guides/web-outline/README.mdx +++ b/packages/console/src/assets/docs/guides/web-outline/README.mdx @@ -1,6 +1,6 @@ -import UriInputField from '@/mdx-components-v2/UriInputField'; -import Steps from '@/mdx-components-v2/Steps'; -import Step from '@/mdx-components-v2/Step'; +import UriInputField from '@/mdx-components/UriInputField'; +import Steps from '@/mdx-components/Steps'; +import Step from '@/mdx-components/Step'; import logtoSignInExperience from './assets/logto-sign-in-experience.png'; import outlineHome from './assets/outline-home.png'; diff --git a/packages/console/src/assets/docs/guides/web-php/README.mdx b/packages/console/src/assets/docs/guides/web-php/README.mdx index 3b144464d..9d7700117 100644 --- a/packages/console/src/assets/docs/guides/web-php/README.mdx +++ b/packages/console/src/assets/docs/guides/web-php/README.mdx @@ -1,10 +1,10 @@ -import UriInputField from '@/mdx-components-v2/UriInputField'; +import UriInputField from '@/mdx-components/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'; +import Steps from '@/mdx-components/Steps'; +import Step from '@/mdx-components/Step'; diff --git a/packages/console/src/assets/docs/guides/web-python/README.mdx b/packages/console/src/assets/docs/guides/web-python/README.mdx index 4e96659f5..1856cb84f 100644 --- a/packages/console/src/assets/docs/guides/web-python/README.mdx +++ b/packages/console/src/assets/docs/guides/web-python/README.mdx @@ -1,10 +1,10 @@ -import UriInputField from '@/mdx-components-v2/UriInputField'; +import UriInputField from '@/mdx-components/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'; +import Steps from '@/mdx-components/Steps'; +import Step from '@/mdx-components/Step'; diff --git a/packages/console/src/assets/docs/guides/web-remix/README.mdx b/packages/console/src/assets/docs/guides/web-remix/README.mdx index 85bf6d2d0..3a8788f00 100644 --- a/packages/console/src/assets/docs/guides/web-remix/README.mdx +++ b/packages/console/src/assets/docs/guides/web-remix/README.mdx @@ -1,10 +1,10 @@ -import UriInputField from '@/mdx-components-v2/UriInputField'; +import UriInputField from '@/mdx-components/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'; +import Steps from '@/mdx-components/Steps'; +import Step from '@/mdx-components/Step'; diff --git a/packages/console/src/assets/docs/tutorial/integrate-sdk/android.mdx b/packages/console/src/assets/docs/tutorial/integrate-sdk/android.mdx deleted file mode 100644 index 302d90de5..000000000 --- a/packages/console/src/assets/docs/tutorial/integrate-sdk/android.mdx +++ /dev/null @@ -1,229 +0,0 @@ -import UriInputField from '@mdx/components/UriInputField'; -import Step from '@mdx/components/Step'; -import Tabs from '@mdx/components/Tabs'; -import TabItem from '@mdx/components/TabItem'; -import InlineNotification from '@/ds-components/InlineNotification'; - - props.onNext(1)} -> - -The minimum supported Android API is level 24 - -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") -} -``` - - - - - -```groovy -dependencies { - implementation 'io.logto.sdk:android:1.0.0' -} -``` - - - - - - - - props.onNext(2)} -> - - - - - -
-  
-    {`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.appId}",
-        scopes = null,
-        resources = null,
-        usingPersistStorage = true,
-    )
-
-    val logtoClient = LogtoClient(logtoConfig, application)
-}`}
-  
-
- -
- - - -
-  
-    {`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.appId}",
-            null,
-            null,
-            true
-        );
-
-        logtoClient = new LogtoClient(logtoConfig, getApplication());
-    }
-}`}
-  
-
- -
- -
- -
- - props.onNext(3)} -> - -### Configure Redirect URI - -First, let’s configure your redirect URI. E.g. `io.logto.android://io.logto.sample/callback` - - - -Go back to your IDE/editor, use the following code to implement sign-in: - - - - - -
-  
-    {`logtoClient.signIn(this, "${
-      props.redirectUris[0] ?? ''
-    }") { logtoException: LogtoException? ->
-    // User signed in successfully if \`logtoException\` is null.
-}`}
-  
-
- -
- - - -
-  
-    {`logtoClient.signIn(this, "${
-      props.redirectUris[0] ?? ''
-    }", logtoException -> {
-    // User signed in successfully if \`logtoException\` is null.
-});`}
-  
-
- -
- -
- -After signing in successfully, `logtoClient.isAuthenticated` will be `true`. - -
- - props.onNext(4)} -> - -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. -} -``` - - - - - -```java -logtoClient.signOut(logtoException -> { - // Local credentials are cleared regardless of whether `logtoException` is null. -}); -``` - - - - - - - - - -- [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) - - diff --git a/packages/console/src/assets/docs/tutorial/integrate-sdk/android_zh-cn.mdx b/packages/console/src/assets/docs/tutorial/integrate-sdk/android_zh-cn.mdx deleted file mode 100644 index 0e0fce383..000000000 --- a/packages/console/src/assets/docs/tutorial/integrate-sdk/android_zh-cn.mdx +++ /dev/null @@ -1,222 +0,0 @@ -import UriInputField from '@mdx/components/UriInputField'; -import Step from '@mdx/components/Step'; -import Tabs from '@mdx/components/Tabs'; -import TabItem from '@mdx/components/TabItem'; -import InlineNotification from '@/ds-components/InlineNotification'; - - props.onNext(1)} -> - - - Logto Andorid SDK 支持的最小 Android API 级别为 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)} -> - - - - - -
-
-{`import io.logto.sdk.android.LogtoClient
-import io.logto.sdk.android.type.LogtoConfig
-
-class MainActivity : AppCompatActivity() {
-    val logtoConfig = LogtoConfig(
-        endpoint = "${props.endpoint}",${props.alternativeEndpoint ? ` // 或 "${props.alternativeEndpoint}"` : ''}
-        appId = "${props.appId}",
-        scopes = null,
-        resources = null,
-        usingPersistStorage = true,
-    )
-
-    val logtoClient = LogtoClient(logtoConfig, application)
-}`}
-
-
- -
- - - -
-
-{`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 ? ` // 或 "${props.alternativeEndpoint}"` : ''}
-            "${props.appId}",
-            null,
-            null,
-            true
-        );
-
-        logtoClient = new LogtoClient(logtoConfig, getApplication());
-    }
-}`}
-
-
- -
- -
- -
- - props.onNext(3)} -> - -### 配置 Redirect URI - -首先,我们来添加 Redirect URI。例如 `io.logto.android://io.logto.sample/callback` - - - -返回你的 IDE 或编辑器,使用如下代码实现登录: - - - - - -
-
-{`logtoClient.signIn(this, "${props.redirectUris[0] ?? ''}") { logtoException: LogtoException? ->
-    // 当 \`logtoException\` 为 null 时,则登录成功。
-}`}
-
-
- -
- - - -
-
-{`logtoClient.signIn(this, "${props.redirectUris[0] ?? ''}", logtoException -> {
-    // 当 \`logtoException\` 为 null 时,则登录成功。
-});`}
-
-
- -
- -
- -当成功登录后,`logtoClient.isAuthenticated` 的值将为 `true`。 - -
- - props.onNext(4)} -> - -调用 `.signOut(completion)` 操作会清除本地存储的用户相关凭据,即使在退出登录过程中发生了异常。 - - - - - -```kotlin -logtoClient.signOut { logtoException: LogtoException? -> - // 无论是否存在 `logtoException`,本地存储的用户相关凭据都已清除。 -} -``` - - - - - -```java -logtoClient.signOut(logtoException -> { - // 无论是否存在 `logtoException`,本地存储的用户相关凭据都已清除。 -}); -``` - - - - - - - - - -- [自定义登录体验](https://docs.logto.io/zh-cn/docs/recipes/customize-sie) -- [启用短信或邮件验证码登录](https://docs.logto.io/zh-cn/docs/tutorials/get-started/enable-passcode-sign-in) -- [启用社交登录](https://docs.logto.io/zh-cn/docs/tutorials/get-started/enable-social-sign-in) -- [保护你的 API](https://docs.logto.io/zh-cn/docs/recipes/protect-your-api) - - diff --git a/packages/console/src/assets/docs/tutorial/integrate-sdk/express.mdx b/packages/console/src/assets/docs/tutorial/integrate-sdk/express.mdx deleted file mode 100644 index 9ab1a841c..000000000 --- a/packages/console/src/assets/docs/tutorial/integrate-sdk/express.mdx +++ /dev/null @@ -1,227 +0,0 @@ -import UriInputField from '@mdx/components/UriInputField'; -import Step from '@mdx/components/Step'; -import Tabs from '@mdx/components/Tabs'; -import TabItem from '@mdx/components/TabItem'; -import InlineNotification from '@/ds-components/InlineNotification'; -import { buildIdGenerator } from '@logto/shared/universal'; - - props.onNext(1)} -> - - - - -```bash -npm i @logto/express cookie-parser express-session -``` - - - - -```bash -yarn add @logto/express cookie-parser express-session -``` - - - - -```bash -pnpm add @logto/express cookie-parser express-session -``` - - - - - - props.onNext(2)} -> - - - In the following steps, we assume your app is running on http://localhost:3000. - - -Import and initialize LogtoClient: - -
-  
-    {`import LogtoClient from '@logto/express';
-
-export const logtoClient = new LogtoClient({
-  endpoint: '${props.endpoint}',${props.alternativeEndpoint ? ` // or "${props.alternativeEndpoint}"` : ''}
-  appId: '${props.appId}',
-  appSecret: '${props.appSecret}',
-  baseUrl: 'http://localhost:3000', // Change to your own base URL
-});`}
-  
-
- -
- - props.onNext(3)} -> - -The SDK requires [express-session](https://www.npmjs.com/package/express-session) to be configured in prior. - -
-  
-    {`import cookieParser from 'cookie-parser';
-import session from 'express-session';
-
-app.use(cookieParser());
-app.use(session({ secret: '${buildIdGenerator(32)()}', cookie: { maxAge: 14 * 24 * 60 * 60 } }));`}
-  
-
- -
- - props.onNext(4)} -> - -### Configure Redirect URI - -First, let’s enter your redirect URI. E.g. `http://localhost:3000/api/logto/sign-in-callback`. - - - -### Prepare Logto routes - -Prepare routes to connect with Logto. - -Go back to your IDE/editor, use the following code to implement the API routes first: - -```ts -import { handleAuthRoutes } from '@logto/express'; - -app.use(handleAuthRoutes(config)); -``` - -This will create 3 routes automatically: - -1. `/logto/sign-in`: Sign in with Logto. -2. `/logto/sign-in-callback`: Handle sign-in callback. -3. `/logto/sign-out`: Sign out with Logto. - -### Implement sign-in - -We're almost there! Now, create a sign-in button to redirect to the sign-in route on user click. - -```ts -app.get('/', (req, res) => { - res.setHeader('content-type', 'text/html'); - res.end(``); -}); -``` - - - - props.onNext(5)} -> - -In order to get user profile, we need to use the `withLogto` middleware: - -```ts -import { withLogto } from '@logto/express'; - -app.use(withLogto(config)); -``` - -Then the user profile will be attached to `req`, example usage: - -```ts -app.get('/user', (req, res) => { - res.json(req.user); -}); -``` - - - - props.onNext(6)} -> - -After setting up `withLogto` in the previous step, we can protect routes by creating a simple middleware: - -```ts -const requireAuth = async (req: Request, res: Response, next: NextFunction) => { - if (!req.user.isAuthenticated) { - res.redirect('/logto/sign-in'); - } - - next(); -}; -``` - -And then: - -```ts -app.get('/protected', requireAuth, (req, res) => { - res.end('protected resource'); -}); -``` - - - - props.onNext(7)} -> - -Calling `/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 your user back to your website. Let's add `http://localhost:3000` as one of the Post Sign-out URIs in Admin Console (shows under Redirect URIs). - - - - - -- [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) - - diff --git a/packages/console/src/assets/docs/tutorial/integrate-sdk/express_zh-cn.mdx b/packages/console/src/assets/docs/tutorial/integrate-sdk/express_zh-cn.mdx deleted file mode 100644 index 33f9f7701..000000000 --- a/packages/console/src/assets/docs/tutorial/integrate-sdk/express_zh-cn.mdx +++ /dev/null @@ -1,222 +0,0 @@ -import UriInputField from '@mdx/components/UriInputField'; -import Step from '@mdx/components/Step'; -import Tabs from '@mdx/components/Tabs'; -import TabItem from '@mdx/components/TabItem'; -import InlineNotification from '@/ds-components/InlineNotification'; -import { buildIdGenerator } from '@logto/shared/universal'; - - props.onNext(1)} -> - - - - -```bash -npm i @logto/express cookie-parser express-session -``` - - - - -```bash -yarn add @logto/express cookie-parser express-session -``` - - - - -```bash -pnpm add @logto/express cookie-parser express-session -``` - - - - - - props.onNext(2)} -> - - - 在如下代码示例中, 我们均先假设你的 React 应用运行在 http://localhost:3000 上。 - - -引入并实例化 LogtoClient: - -
-
-{`import LogtoClient from '@logto/express';
-
-export const logtoClient = new LogtoClient({
-  endpoint: '${props.endpoint}',${props.alternativeEndpoint ? ` // 或 "${props.alternativeEndpoint}"` : ''}
-  appId: '${props.appId}',
-  appSecret: '${props.appSecret}',
-  baseUrl: 'http://localhost:3000', // 你可以修改为自己真实的 URL
-});`}
-
-
- -
- - props.onNext(3)} -> - -本 SDK 要求预先安装并配置好 [express-session](https://www.npmjs.com/package/express-session)。 - -
-
-{`import cookieParser from 'cookie-parser';
-import session from 'express-session';
-
-app.use(cookieParser());
-app.use(session({ secret: '${buildIdGenerator(32)()}', cookie: { maxAge: 14 * 24 * 60 * 60 } }));`}
-
-
- -
- - props.onNext(4)} -> - -### 配置 Redirect URI - -首先,我们来添加 Redirect URI,如:`http://localhost:3000/api/logto/sign-in-callback`. - - - -### 准备 Logto 路由 - -准备与 Logto 后台交互的路由。 - -返回你的 IDE 或编辑器,首先让我们使用如下代码来实现一组 API 路由: - -```ts -import { handleAuthRoutes } from '@logto/express'; - -app.use(handleAuthRoutes(config)); -``` - -这将为你自动创建好 3 个路由,分别是: - -1. `/logto/sign-in`: 登录 -2. `/logto/sign-in-callback`: 处理登录重定向 -3. `/logto/sign-out`: 登出 - -### 实现登录 - -马上就要大功告成!创建一个登录按钮,点击后将会跳转到登录路由。 - -```ts -app.get('/', (req, res) => { - res.setHeader('content-type', 'text/html'); - res.end(``); -}); -``` - - - - props.onNext(5)} -> - -需要集成 `withLogto` 中间件来获取用户信息: - -```ts -import { withLogto } from '@logto/express'; - -app.use(withLogto(config)); -``` - -之后用户信息将会被注入到 `req`, 用法举例: - -```ts -app.get('/user', (req, res) => { - res.json(req.user); -}); -``` - - - - props.onNext(6)} -> - -根据前面的步骤配置好 `withLogto` 后, 我们可以创建一个简单的中间件来保护路由: - -```ts -const requireAuth = async (req: Request, res: Response, next: NextFunction) => { - if (!req.user.isAuthenticated) { - res.redirect('/logto/sign-in'); - } - - next(); -}; -``` - -然后: - -```ts -app.get('/protected', requireAuth, (req, res) => { - res.end('protected resource'); -}); -``` - - - - props.onNext(7)} -> - -调用 `/logto/sign-out` 将清理内存与 cookies 中的所有 Logto 数据(如果有)。 - -在退出登录后,让你的用户重新回到你的网站是个不错的选择。让我们将 `http://localhost:3000` 添加至「管理控制台」里的 Post Sign-out URIs 中(位于 Redirect URIs 下方)。 - - - - - -- [自定义登录体验](https://docs.logto.io/zh-cn/docs/recipes/customize-sie) -- [启用短信或邮件验证码登录](https://docs.logto.io/zh-cn/docs/tutorials/get-started/enable-passcode-sign-in) -- [启用社交登录](https://docs.logto.io/zh-cn/docs/tutorials/get-started/enable-social-sign-in) -- [保护你的 API](https://docs.logto.io/zh-cn/docs/recipes/protect-your-api) - - diff --git a/packages/console/src/assets/docs/tutorial/integrate-sdk/go.mdx b/packages/console/src/assets/docs/tutorial/integrate-sdk/go.mdx deleted file mode 100644 index c8b53c7ae..000000000 --- a/packages/console/src/assets/docs/tutorial/integrate-sdk/go.mdx +++ /dev/null @@ -1,391 +0,0 @@ -import UriInputField from '@mdx/components/UriInputField'; -import Step from '@mdx/components/Step'; -import InlineNotification from '@/ds-components/InlineNotification'; - - props.onNext(1)} -> - - The following demonstration is built upon the Gin Web Framework. - 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 http://localhost:8080. - - -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") -} -``` - - - - props.onNext(2)} -> - -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. - - - 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. - - -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} -``` - - - - props.onNext(3)} -> - -### Create LogtConfig - -
-  
-    {`// main.go
-func main() {
-	// ...
-
-	logtoConfig := &client.LogtoConfig{
-		Endpoint:           "${props.endpoint}",${props.alternativeEndpoint ? ` // or "${props.alternativeEndpoint}"` : ''}
-		AppId:              "${props.appId}",
-		AppSecret:          "${props.appSecret}",
-	}
-
-	// ...
-}`}
-  
-
- -### 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 := `

Hello Logto

` + - "
" + authState + "
" - - ctx.Data(http.StatusOK, "text/html; charset=utf-8", []byte(homePage)) - }) - - // ... -} -``` - -
- - props.onNext(4)} -> - -### 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. - - - -### 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 := `

Hello Logto

` + - "
" + authState + "
" + - // Add link - `` - - 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, "/") - }) - - // ... -} -``` - -
- - props.onNext(5)} -> - -### Configure Post Sign-out Redirect URI - -Add `http://localhost:8080` to the Post Sign-out Redirect URI filed: - - - -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 := `

Hello Logto

` + - "
" + authState + "
" + - `` + - // Add link - `` - - 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. - -
- - - -- [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) - - diff --git a/packages/console/src/assets/docs/tutorial/integrate-sdk/go_zh-cn.mdx b/packages/console/src/assets/docs/tutorial/integrate-sdk/go_zh-cn.mdx deleted file mode 100644 index b46b33d99..000000000 --- a/packages/console/src/assets/docs/tutorial/integrate-sdk/go_zh-cn.mdx +++ /dev/null @@ -1,371 +0,0 @@ -import UriInputField from '@mdx/components/UriInputField'; -import Step from '@mdx/components/Step'; -import InlineNotification from '@/ds-components/InlineNotification'; - - props.onNext(1)} -> - - 在本指南中,我们基于 Gin Web 框架 示范 SDK 的集成过程。你也可以采取同样的步骤轻松地将 Logto 集成到其他的 Web 框架中。 - 在示例代码中,我们假定你的应用运行在 http://localhost:8080 上。 - - -在项目目录下执行: - -```bash -go get github.com/logto-io/go -``` - -将 `github.com/logto-io/go/client` 包依赖添加到到代码中: - -```go -// main.go -package main - -import ( - "github.com/gin-gonic/gin" - // 添加依赖 - "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") -} -``` - - - props.onNext(2)} -> - -在传统网页应用中,用户的认证信息将会被存储在用户的 session 中。 - -Logto SDK 提供了一个 `Storage` 接口,你可以结合自己所使用的网络框架实现一个 `Storage` 的适配器,使 Logto SDK 能将用户认证信息存储到 session 中。 - - - 我们推荐使用非 cookie 的 session,因为 Logto 所存储的信息可能会超过 cookie 的大小限制。在示例中我们使用基于内存 session,在实际项目中,你可以根据需要使用 Redis、 MongoDB 等技术来存储 session。 - - -Logto SDK 中的 `Storage` 类型如下: - -```go -// github.com/logto-io/client/storage.go -pacakge client - -type Storage interface { - GetItem(key string) string - SetItem(key, value string) -} -``` - -我们将以 [github.com/gin-contrib/sessions](https://github.com/gin-contrib/sessions) 为例,示范这个过程。 - -### 在应用中使用 Session 中间件 - -在应用中使用 [github.com/gin-contrib/sessions](https://github.com/gin-contrib/sessions) 中间件,这样在请求的路由处理方法中就可以根据用户请求的上下文获取用户的 session: - -```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() - - // 示例中使用基于内存的 session - store := memstore.NewStore([]byte("your session secret")) - router.Use(sessions.Sessions("logto-session", store)) - - router.GET("/", func(ctx *gin.Context) { - // 获取用户的 session - session := sessions.Default(ctx) - // ... - ctx.String(200, "Hello Logto!") - }) - router.Run(":8080") -} -``` - -### 创建 session storage 供 Logto 存储用户认证信息 - -创建一个 `session_storage.go` 文件,定义一个 `SessionStorage` 并实现 Logto SDK 定义的 `Storage` 的接口: - -```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() -} -``` - -至此,你就可以在请求的路由处理方法中通过以下方式创建一个 session storage 给 Logto 使用了: - -```go -session := sessions.Default(ctx) -sessionStorage := &SessionStorage{session: session} -``` - - - - props.onNext(3)} -> - -### 创建 Logto 配置 - -
-
-{`// main.go
-func main() {
-	// ...
-
-	logtoConfig := &client.LogtoConfig{
-		Endpoint:           "${props.endpoint}",${props.alternativeEndpoint ? ` // 或 "${props.alternativeEndpoint}"` : ''}
-		AppId:              "${props.appId}",
-		AppSecret:          "${props.appSecret}",
-	}
-
-	// ...
-}`}
-
-
- -### 为每个请求初始化 LogtoClient - -```go -// main.go -func main() { - // ... - - router.GET("/", func(ctx *gin.Context) { - // 初始化 LogtoClient - session := sessions.Default(ctx) - logtoClient := client.NewLogtoClient( - logtoConfig, - &SessionStorage{session: session}, - ) - - // 使用 Logto 来控制首页的显示内容 - authState := "You are not logged in to this website. :(" - - if logtoClient.IsAuthenticated() { - authState = "You are logged in to this website! :)" - } - - homePage := `

Hello Logto

` + - "
" + authState + "
" - - ctx.Data(http.StatusOK, "text/html; charset=utf-8", []byte(homePage)) - }) - - // ... -} -``` - -
- - props.onNext(4)} -> - -### 配置 Redirect URI - -将 `http://localhost:8080/sign-in-callback` 添加到 Redirect URI,使用户登录 Logto 后能重定向到应用处理登录回调的 `/sign-in-callback` 路由: - - - -### 添加处理登录请求路由 - -```go -//main.go -func main() { - // ... - - // 在 Home 页面添加登录请求的入口 - router.GET("/", func(ctx *gin.Context) { - // ... - homePage := `

Hello Logto

` + - "
" + authState + "
" + - // 添加登录请求的入口 - `` - - ctx.Data(http.StatusOK, "text/html; charset=utf-8", []byte(homePage)) - }) - - // 添加处理登录请求的路由 - router.GET("/sign-in", func(ctx *gin.Context) { - session := sessions.Default(ctx) - logtoClient := client.NewLogtoClient( - logtoConfig, - &SessionStorage{session: session}, - ) - - // 由 Logto 处理登录请求,指定登录成功后重定向到 Redirect URI - signInUri, err := logtoClient.SignIn("http://localhost:8080/sign-in-callback") - if err != nil { - ctx.String(http.StatusInternalServerError, err.Error()) - return - } - - // 将页面重定向到 Logto 登录页 - ctx.Redirect(http.StatusTemporaryRedirect, signInUri) - }) - - // ... -} -``` - -### 添加处理登录回调路由 - -当我们在 Logto 登录页登录成功后,Logto 会将用户重定向 Redirect URI。 -因 Redirect URI 是 `http://localhost:8080/sign-in-callback`,所以我们添加 `/sign-in-callback` 路由来处理登录后的回调。 - -```go -// main.go -func main() { - // ... - - // 添加处理登录回调的路由 - router.GET("/sign-in-callback", func(ctx *gin.Context) { - session := sessions.Default(ctx) - logtoClient := client.NewLogtoClient( - logtoConfig, - &SessionStorage{session: session}, - ) - - // 由 Logto 处理登录回调 - err := logtoClient.HandleSignInCallback(ctx.Request) - if err != nil { - ctx.String(http.StatusInternalServerError, err.Error()) - return - } - - // 根据需求在登录成功后跳转到某个页面。(该示例中使用户回到首页) - ctx.Redirect(http.StatusTemporaryRedirect, "/") - }) - - // ... -} -``` - -
- - props.onNext(5)} -> - -### 配置 Post Sign-out Redirect URI - -为应用添加 Post Sign-out Redirect URI,使用户退出登录 Logto 之后将用户重定向回我们的应用。 -将 `http://localhost:8080` 添加到 Post Sign-out Redirect URI,使用户退出登录后回到应用首页: - - - -### 添加退出登录请求路由 - -```go -//main.go -func main() { - // ... - - // 在 Home 页面添加退出登录请求的入口 - router.GET("/", func(ctx *gin.Context) { - // ... - homePage := `

Hello Logto

` + - "
" + authState + "
" + - `` + - // 添加退出登录请求的入口 - `` - - ctx.Data(http.StatusOK, "text/html; charset=utf-8", []byte(homePage)) - }) - - // 添加处理退出登录请求的路由 - router.GET("/sign-out", func(ctx *gin.Context) { - session := sessions.Default(ctx) - logtoClient := client.NewLogtoClient( - logtoConfig, - &SessionStorage{session: session}, - ) - - // 由 Logto 处理退出登录请求,指定用户退出登录后重定向回 Post Sign-out Redirect URI - signOutUri, signOutErr := logtoClient.SignOut("http://localhost:8080") - - if signOutErr != nil { - ctx.String(http.StatusOK, signOutErr.Error()) - return - } - - ctx.Redirect(http.StatusTemporaryRedirect, signOutUri) - }) - - // ... -} -``` - -当用户发起退出登录后,Logto 会清除 session 中所有用户相关的认证信息。 - -
- - - -- [自定义登录体验](https://docs.logto.io/zh-cn/docs/recipes/customize-sie) -- [启用短信或邮件验证码登录](https://docs.logto.io/zh-cn/docs/tutorials/get-started/enable-passcode-sign-in) -- [启用社交登录](https://docs.logto.io/zh-cn/docs/tutorials/get-started/enable-social-sign-in) -- [保护你的 API](https://docs.logto.io/zh-cn/docs/recipes/protect-your-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 deleted file mode 100644 index ec3d4da46..000000000 --- a/packages/console/src/assets/docs/tutorial/integrate-sdk/ios.mdx +++ /dev/null @@ -1,151 +0,0 @@ -import UriInputField from '@mdx/components/UriInputField'; -import Step from '@mdx/components/Step'; -import Tabs from '@mdx/components/Tabs'; -import TabItem from '@mdx/components/TabItem'; -import InlineNotification from '@/ds-components/InlineNotification'; - - 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](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. - -
- -
- CocoaPods - -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. - -
- -
- - props.onNext(2)} -> - -
-  
-    {`import Logto
-import LogtoClient
-
-let config = try? LogtoConfig(
-  endpoint: "${props.endpoint}",${props.alternativeEndpoint ? ` // or "${props.alternativeEndpoint}"` : ''}
-  appId: "${props.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)} -> - -### Configure Redirect URI - -First, let’s configure your redirect URI scheme. E.g. `io.logto://callback` - - - - - The Redirect URI in iOS SDK is only for internal use. There's NO NEED to add a{' '} - - Custom URL Scheme - {' '} - until a connector asks. - - -Go back to Xcode, use the following code to implement sign-in: - -
-  
-    {`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
-}`}
-  
-
- -
- - props.onNext(4)} -> - -Calling `.signOut()` will clean all the Logto data in Keychain, if they exist. - -```swift -await client.signOut() -``` - - - - - -- [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) - - diff --git a/packages/console/src/assets/docs/tutorial/integrate-sdk/ios_zh-cn.mdx b/packages/console/src/assets/docs/tutorial/integrate-sdk/ios_zh-cn.mdx deleted file mode 100644 index 242a4cbb2..000000000 --- a/packages/console/src/assets/docs/tutorial/integrate-sdk/ios_zh-cn.mdx +++ /dev/null @@ -1,139 +0,0 @@ -import UriInputField from '@mdx/components/UriInputField'; -import Step from '@mdx/components/Step'; -import Tabs from '@mdx/components/Tabs'; -import TabItem from '@mdx/components/TabItem'; -import InlineNotification from '@/ds-components/InlineNotification'; - - props.onNext(1)} -> - -使用如下的 URL 将 Logto SDK 添加至 Swift Package Manager 的依赖中。 - -```bash -https://github.com/logto-io/swift.git -``` - -从 Xcode 11 开始,无需任何额外工具你就可以 [引入一个 Swift package](https://developer.apple.com/documentation/swift_packages/adding_package_dependencies_to_your_app)。 - -因为一些技术原因,我们暂时不支持 **Carthage** 和 **CocoaPods**。 - -
- Carthage - -Carthage [需要创建一个 `xcodeproj` 文件才能编译](https://github.com/Carthage/Carthage/issues/1226#issuecomment-290931385),但由于我们内置了一些社交插件所用到的二进制目标文件,导致使用 `swift package generate-xcodeproj` 命令时报错。我们会继续努力寻求解决方案。 - -
- -
- CocoaPods - -CocoaPods [不支持本地依赖](https://github.com/CocoaPods/CocoaPods/issues/3276) 和 monorepo,所以要在此工程创建 `.podspec` 文件使用 Cocoapods 的话将非常困难。 - -
- -
- - props.onNext(2)} -> - -
-
-{`import Logto
-import LogtoClient
-
-let config = try? LogtoConfig(
-  endpoint: "${props.endpoint}",${props.alternativeEndpoint ? ` // 或 "${props.alternativeEndpoint}"` : ''}
-  appId: "${props.appId}"
-)
-let logtoClient = LogtoClient(useConfig: config)`}
-
-
- -我们默认会把例如 ID Token 和 Refresh Token 这样的凭据存储在 Keychain 中,如此一来用户在重新打开应用的时候无需再次登录。 - -如果需要禁用这个行为,可将 `usingPersistStorage` 设置成 `false`: - -```swift -let config = try? LogtoConfig( - // ... - usingPersistStorage: false -) -``` - -
- - props.onNext(3)} -> - -### 配置 Redirect URI - -首先,我们来配置你的 redirect URI scheme。例如 `io.logto://callback` - - - - - iOS SDK 中的 Redirect URI 仅用于内部。除非连接器有要求,否则 无需 在项目中添加 Custom URL Scheme。 - - -回到 Xcode,使用如下代码实现登录: - -
-
-{`do {
-  try await client.signInWithBrowser(redirectUri: "${props.redirectUris[0] ?? 'io.logto://callback'}")
-  print(client.isAuthenticated) // true
-} catch let error as LogtoClientErrors.SignIn {
-  // 登录过程中有错误发生
-}`}
-
-
- -
- - props.onNext(4)} -> - -调用 `.signOut()` 将清除 Keychain 中所有 Logto 的数据(如果有)。 - -```swift -await client.signOut() -``` - - - - - -- [自定义登录体验](https://docs.logto.io/zh-cn/docs/recipes/customize-sie) -- [启用短信或邮件验证码登录](https://docs.logto.io/zh-cn/docs/tutorials/get-started/enable-passcode-sign-in) -- [启用社交登录](https://docs.logto.io/zh-cn/docs/tutorials/get-started/enable-social-sign-in) -- [保护你的 API](https://docs.logto.io/zh-cn/docs/recipes/protect-your-api) - - diff --git a/packages/console/src/assets/docs/tutorial/integrate-sdk/next.mdx b/packages/console/src/assets/docs/tutorial/integrate-sdk/next.mdx deleted file mode 100644 index 8d71d12dd..000000000 --- a/packages/console/src/assets/docs/tutorial/integrate-sdk/next.mdx +++ /dev/null @@ -1,276 +0,0 @@ -import UriInputField from '@mdx/components/UriInputField'; -import Step from '@mdx/components/Step'; -import Tabs from '@mdx/components/Tabs'; -import TabItem from '@mdx/components/TabItem'; -import InlineNotification from '@/ds-components/InlineNotification'; -import { buildIdGenerator } from '@logto/shared/universal'; - - props.onNext(1)} -> - - - -```bash -npm i @logto/next -``` - - - - -```bash -yarn add @logto/next -``` - - - - -```bash -pnpm add @logto/next -``` - - - - - - props.onNext(2)} -> - - - In the following steps, we assume your app is running on http://localhost:3000. - - -Import and initialize LogtoClient: - -
-  
-    {`// libraries/logto.js
-import LogtoClient from '@logto/next';
-
-export const logtoClient = new LogtoClient({
-  endpoint: '${props.endpoint}',${props.alternativeEndpoint ? ` // or "${props.alternativeEndpoint}"` : ''}
-  appId: '${props.appId}',
-  appSecret: '${props.appSecret}',
-  baseUrl: 'http://localhost:3000', // Change to your own base URL
-  cookieSecret: '${buildIdGenerator(32)()}', // Auto-generated 32 digit secret
-  cookieSecure: process.env.NODE_ENV === 'production',
-});`}
-  
-
- -
- - props.onNext(3)} -> - -### Configure Redirect URI - -First, let’s enter your redirect URI. E.g. `http://localhost:3000/api/logto/sign-in-callback`. - - - -### 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(); - -; -``` - -Now you will be navigated to Logto sign-in page when you click the button. - - - - props.onNext(4)} -> - -### 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('/api/logto/user'); - - return
User ID: {data?.claims?.sub}
; -}; - -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
User ID: {user.claims?.sub}
; -}; - -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`. - -
- - props.onNext(5)} -> - -### 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 }, - }; -}); -``` - - - - props.onNext(6)} -> - -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`. - - - -### Implement a sign-out button - -```tsx - -``` - - - - - -- [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) - - diff --git a/packages/console/src/assets/docs/tutorial/integrate-sdk/next_zh-cn.mdx b/packages/console/src/assets/docs/tutorial/integrate-sdk/next_zh-cn.mdx deleted file mode 100644 index c14558682..000000000 --- a/packages/console/src/assets/docs/tutorial/integrate-sdk/next_zh-cn.mdx +++ /dev/null @@ -1,270 +0,0 @@ -import UriInputField from '@mdx/components/UriInputField'; -import Step from '@mdx/components/Step'; -import Tabs from '@mdx/components/Tabs'; -import TabItem from '@mdx/components/TabItem'; -import InlineNotification from '@/ds-components/InlineNotification'; -import { buildIdGenerator } from '@logto/shared/universal'; - - props.onNext(1)} -> - - - -```bash -npm i @logto/next -``` - - - - -```bash -yarn add @logto/next -``` - - - - -```bash -pnpm add @logto/next -``` - - - - - - props.onNext(2)} -> - - - 在如下代码示例中, 我们均先假设你的 React 应用运行在 http://localhost:3000 上。 - - -引入并实例化 LogtoClient: - -
-
-{`// libraries/logto.js
-import LogtoClient from '@logto/next';
-
-export const logtoClient = new LogtoClient({
-  endpoint: '${props.endpoint}',${props.alternativeEndpoint ? ` // 或 "${props.alternativeEndpoint}"` : ''}
-  appId: '${props.appId}',
-  appSecret: '${props.appSecret}',
-  baseUrl: 'http://localhost:3000', // 你可以修改为自己真实的 URL
-  cookieSecret: '${buildIdGenerator(32)()}', // Logto 自动帮你生成的 32 位密钥
-  cookieSecure: process.env.NODE_ENV === 'production',
-});`}
-
-
- -
- - props.onNext(3)} -> - -### 配置 Redirect URI - -首先,我们来添加 Redirect URI,如:`http://localhost:3000/api/logto/sign-in-callback`. - - - -### 准备 API 路由 - -实现与 Logto 后台交互的 [API 路由](https://nextjs.org/docs/api-routes/introduction)。 - -返回你的 IDE 或编辑器,首先让我们使用如下代码来实现一组 API 路由: - -```ts -// pages/api/logto/[action].ts -import { logtoClient } from '../../../libraries/logto'; - -export default logtoClient.handleAuthRoutes(); -``` - -这将为你自动创建好 4 个路由,分别是: - -1. `/api/logto/sign-in`:登录 -2. `/api/logto/sign-in-callback`:处理登录重定向 -3. `/api/logto/sign-out`:登出 -4. `/api/logto/user`:检查用户是否已通过 Logto 获得授权。如果是,则返回用户信息。 - -### 实现登录按钮 - -马上就要大功告成!在这最后一步,我们将用如下代码实现一个登录按钮: - -```tsx -import { useRouter } from 'next/router'; - -const { push } = useRouter(); - - -``` - -现在你可以尝试点击登录按钮了,点击之后页面会跳转到 Logto 的登录界面。 - - - - props.onNext(4)} -> - -### 通过前端发送 API 请求获取 - -你可以调用 `/api/logto/user` 接口来获取用户信息,如: - -```tsx -import { LogtoUser } from '@logto/next'; -import useSWR from 'swr'; - -const Home = () => { - const { data } = useSWR('/api/logto/user'); - - return
用户 ID:{data?.claims?.sub}
; -}; - -export default Profile; -``` - -你可以查看 [这篇教程](https://swr.vercel.app/docs/getting-started) 来了解有关 `useSWR` 的更多信息。 - -### 通过后端的 `getServerSideProps` 方法获取 - -```tsx -import { LogtoUser } from '@logto/next'; -import { logtoClient } from '../libraries/logto'; - -type Props = { - user: LogtoUser; -}; - -const Profile = ({ user }: Props) => { - return
用户 ID:{user.claims?.sub}
; -}; - -export default Profile; - -export const getServerSideProps = logtoClient.withLogtoSsr(({ request }) => { - const { user } = request; - - return { - props: { user }, - }; -}); -``` - -查看 [Next.js 官方文档](https://nextjs.org/docs/basic-features/data-fetching/get-server-side-props) 来了解有关 `getServerSideProps` 的更多信息。 - -
- - props.onNext(5)} -> - -### 保护 API 路由 - -使用 `logtoClient.withLogtoApiRoute` 来包裹 API 逻辑: - -```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: '未授权' }); - - return; - } - - response.json({ - data: '这是受保护的 API 资源', - }); -}); -``` - -### 保护页面 - -如果你不想匿名用户访问你的某个页面,你可以使用 `logtoClient.withLogtoSsr` 来获取登录认证状态,如未登录则自动跳转到登录页面。 - -```ts -export const getServerSideProps = logtoClient.withLogtoSsr(async function ({ request, response }) { - const { user } = request; - - if (!user.isAuthenticated) { - response.setHeader('location', '/api/logto/sign-in'); - response.statusCode = 302; - response.end(); - } - - return { - props: { user }, - }; -}); -``` - - - - props.onNext(6)} -> - -调用 `/api/logto/sign-out` 将清理内存与 cookies 中的所有 Logto 数据(如果有)。 - -在退出登录后,让你的用户重新回到你的网站是个不错的选择。在调用 `/api/logto/sign-out` 发起退出登录操作之前,让我们先将 `http://localhost:3000` 添加至下面的输入框。 - - - -### 实现退出登录按钮 - -```tsx - -``` - - - - - -- [自定义登录体验](https://docs.logto.io/zh-cn/docs/recipes/customize-sie) -- [启用短信或邮件验证码登录](https://docs.logto.io/zh-cn/docs/tutorials/get-started/enable-passcode-sign-in) -- [启用社交登录](https://docs.logto.io/zh-cn/docs/tutorials/get-started/enable-social-sign-in) -- [保护你的 API](https://docs.logto.io/zh-cn/docs/recipes/protect-your-api) - - diff --git a/packages/console/src/assets/docs/tutorial/integrate-sdk/react.mdx b/packages/console/src/assets/docs/tutorial/integrate-sdk/react.mdx deleted file mode 100644 index 9d310a88d..000000000 --- a/packages/console/src/assets/docs/tutorial/integrate-sdk/react.mdx +++ /dev/null @@ -1,201 +0,0 @@ -import UriInputField from '@mdx/components/UriInputField'; -import Step from '@mdx/components/Step'; -import Tabs from '@mdx/components/Tabs'; -import TabItem from '@mdx/components/TabItem'; -import InlineNotification from '@/ds-components/InlineNotification'; - - props.onNext(1)} -> - - - -```bash -npm i @logto/react -``` - - - - -```bash -yarn add @logto/react -``` - - - - -```bash -pnpm add @logto/react -``` - - - - - - props.onNext(2)} -> - -Import and use `LogtoProvider` to provide a Logto context: - -
-  
-    {`import { LogtoProvider, LogtoConfig } from '@logto/react';
-
-const config: LogtoConfig = {
-  endpoint: '${props.endpoint}',${props.alternativeEndpoint ? ` // or "${props.alternativeEndpoint}"` : ''}
-  appId: '${props.appId}',
-};
-
-const App = () => (
-  
-    
-  
-);`}
-  
-
- -
- - props.onNext(3)} -> - - - In the following steps, we assume your app is running on http://localhost:3000. - - -### Configure Redirect URI - -First, let’s enter your redirect URI. E.g. `http://localhost:3000/callback`. - - - -### Implement a sign-in button - -We provide two hooks `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: - -
-  
-    {`import { useLogto } from '@logto/react';
-
-const SignIn = () => {
-  const { signIn, isAuthenticated } = useLogto();
-
-  if (isAuthenticated) {
-    return 
Signed in
; - } - - return ( - - ); -};`} -
-
- -### 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: - -```tsx -import { useHandleSignInCallback } from '@logto/react'; - -const Callback = () => { - const { isLoading } = useHandleSignInCallback(() => { - // Navigate to root path when finished - }); - - // When it's working in progress - if (isLoading) { - return
Redirecting...
; - } -}; -``` - -Finally insert the code below to create a `/callback` route which does NOT require authentication: - -```tsx -// Assuming react-router -} /> -``` - -
- - props.onNext(4)} -> - -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()`. - - - -### Implement a sign-out button - -
-  
-    {`const SignOut = () => {
-  const { signOut } = useLogto();
-
-  return (
-    
-  );
-};`}
-  
-
- -
- - - -- [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) - - diff --git a/packages/console/src/assets/docs/tutorial/integrate-sdk/react_zh-cn.mdx b/packages/console/src/assets/docs/tutorial/integrate-sdk/react_zh-cn.mdx deleted file mode 100644 index 0e86f6a51..000000000 --- a/packages/console/src/assets/docs/tutorial/integrate-sdk/react_zh-cn.mdx +++ /dev/null @@ -1,189 +0,0 @@ -import UriInputField from '@mdx/components/UriInputField'; -import Step from '@mdx/components/Step'; -import Tabs from '@mdx/components/Tabs'; -import TabItem from '@mdx/components/TabItem'; -import InlineNotification from '@/ds-components/InlineNotification'; - - props.onNext(1)} -> - - - -```bash -npm i @logto/react -``` - - - - -```bash -yarn add @logto/react -``` - - - - -```bash -pnpm add @logto/react -``` - - - - - - props.onNext(2)} -> - -Import 并使用 `LogtoProvider` 来提供 Logto context: - -
-
-{`import { LogtoProvider, LogtoConfig } from '@logto/react';
-
-const config: LogtoConfig = {
-  endpoint: '${props.endpoint}',${props.alternativeEndpoint ? ` // 或 "${props.alternativeEndpoint}"` : ''}
-  appId: '${props.appId}',
-};
-
-const App = () => (
-  
-    
-  
-);`}
-
-
- -
- - props.onNext(3)} -> - - - 在如下代码示例中, 我们均先假设你的 React 应用运行在 http://localhost:3000 上。 - - -### 配置 Redirect URI - -首先,我们来添加 Redirect URI,如:`http://localhost:3000/callback`。 - - - -### 实现登录按钮 - -我们提供了两个 hook 方法 `useHandleSignInCallback()` 和 `useLogto()`,它们可以帮助你轻松完成登录认证流程。 - -返回你的 IDE 或编辑器,使用如下代码来实现一个登录按钮: - -
-
-{`import { useLogto } from '@logto/react';
-
-const SignIn = () => {
-  const { signIn, isAuthenticated } = useLogto();
-
-  if (isAuthenticated) {
-    return 
已登录
; - } - - return ( - - ); -};`} -
-
- -### 处理重定向 - -马上就要大功告成!在上一步,我们将 `http://localhost:3000/callback` 用作 Redirect URI,现在我们需要对其妥善处理。 - -首先,让我们来创建一个 callback 组件: - -```tsx -import { useHandleSignInCallback } from '@logto/react'; - -const Callback = () => { - const { isLoading } = useHandleSignInCallback(() => { - // 完成时跳转至根路由 - }); - - // 当登录认证尚未完成时 - if (isLoading) { - return
正在重定向...
; - } -}; -``` - -最后我们插入如下代码来实现一个 _无需_ 登录的 `/callback` 路由: - -```tsx -// 假设用 react-router -} /> -``` - -
- - props.onNext(4)} -> - -调用 `.signOut()` 将清理内存与 localStorage 中的所有 Logto 数据(如果有)。 - -在退出登录后,让你的用户重新回到你的网站是个不错的选择。让我们将 `http://localhost:3000` 添加至下面的输入框,并将其作为调用 `.signOut()` 的参数。 - - - -### 实现退出登录按钮 - -
-
-{`const SignOut = () => {
-  const { signOut } = useLogto();
-
-  return (
-    
-  );
-};`}
-
-
- -
- - - -- [自定义登录体验](https://docs.logto.io/zh-cn/docs/recipes/customize-sie) -- [启用短信或邮件验证码登录](https://docs.logto.io/zh-cn/docs/tutorials/get-started/enable-passcode-sign-in) -- [启用社交登录](https://docs.logto.io/zh-cn/docs/tutorials/get-started/enable-social-sign-in) -- [保护你的 API](https://docs.logto.io/zh-cn/docs/recipes/protect-your-api) - - diff --git a/packages/console/src/assets/docs/tutorial/integrate-sdk/vanilla.mdx b/packages/console/src/assets/docs/tutorial/integrate-sdk/vanilla.mdx deleted file mode 100644 index 47d6ce599..000000000 --- a/packages/console/src/assets/docs/tutorial/integrate-sdk/vanilla.mdx +++ /dev/null @@ -1,166 +0,0 @@ -import UriInputField from '@mdx/components/UriInputField'; -import Step from '@mdx/components/Step'; -import Tabs from '@mdx/components/Tabs'; -import TabItem from '@mdx/components/TabItem'; -import InlineNotification from '@/ds-components/InlineNotification'; - - props.onNext(1)} -> - - - -```bash -npm i @logto/browser -``` - - - - -```bash -yarn add @logto/browser -``` - - - - -```bash -pnpm add @logto/browser -``` - - - - - - props.onNext(2)} -> - -Import and init `LogtoClient` by passing config: - -
-  
-    {`import LogtoClient from '@logto/browser';
-
-const logtoClient = new LogtoClient({
-  endpoint: '${props.endpoint}',${props.alternativeEndpoint ? ` // or "${props.alternativeEndpoint}"` : ''}
-  appId: '${props.appId}',
-});`}
-  
-
- -
- - props.onNext(3)} -> - - - In the following steps, we assume your app is running on http://localhost:3000. - - -### Configure Redirect URI - -First, let’s enter your redirect URI. E.g. `http://localhost:3000/callback`. - - - -### Implement a sign-in button - -Go back to your IDE/editor, use the following code to implement the sign-in button: - -
-  
-    {``}
-  
-
- -### 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. - -
- - props.onNext(4)} -> - -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()`. - - - -### Implement a sign-out button - -
-  
-    {``}
-  
-
- -
- - - -- [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) - - diff --git a/packages/console/src/assets/docs/tutorial/integrate-sdk/vanilla_zh-cn.mdx b/packages/console/src/assets/docs/tutorial/integrate-sdk/vanilla_zh-cn.mdx deleted file mode 100644 index ed4953f2d..000000000 --- a/packages/console/src/assets/docs/tutorial/integrate-sdk/vanilla_zh-cn.mdx +++ /dev/null @@ -1,152 +0,0 @@ -import UriInputField from '@mdx/components/UriInputField'; -import Step from '@mdx/components/Step'; -import Tabs from '@mdx/components/Tabs'; -import TabItem from '@mdx/components/TabItem'; -import InlineNotification from '@/ds-components/InlineNotification'; - - props.onNext(1)} -> - - - -```bash -npm i @logto/browser -``` - - - - -```bash -yarn add @logto/browser -``` - - - - -```bash -pnpm add @logto/browser -``` - - - - - - props.onNext(2)} -> - -Import 并传入 config 以初始化 `LogtoClient`: - -
-
-{`import LogtoClient from '@logto/browser';
-
-const logtoClient = new LogtoClient({
-  endpoint: '${props.endpoint}',${props.alternativeEndpoint ? ` // 或 "${props.alternativeEndpoint}"` : ''}
-  appId: '${props.appId}',
-});`}
-
-
- -
- - props.onNext(3)} -> - - - 在如下代码示例中, 我们均先假设你的应用运行在 http://localhost:3000 上。 - - -### 配置 Redirect URI - -首先,我们来添加 redirect URI,如: `http://localhost:3000/callback`。 - - - -### 实现登录按钮 - -返回你的 IDE 或编辑器,使用如下代码来实现一个登录按钮: - -
-
-{``}
-
-
- -### 处理重定向 - -马上就要大功告成!在上一步,我们将 `http://localhost:3000/callback` 用作 Redirect URI,现在我们需要对其妥善处理。 - -在你的 `/callback` 路由下插入如下代码: - -```ts -try { - await logtoClient.handleSignInCallback(window.location.href); - console.log(await logtoClient.isAuthenticated()); // true -} catch { - // 处理错误 -} -``` - -现在可以测试登录流程了。 - -
- - props.onNext(4)} -> - -调用 `.signOut()` 将清理内存与 localStorage 中的所有 Logto 数据(如果有)。 - -在退出登录后,让你的用户重新回到你的网站是个不错的选择。让我们将 `http://localhost:3000` 添加至下面的输入框,并将其作为调用 `.signOut()` 的参数。 - - - -### 实现退出登录按钮 - -
-
-{``}
-
-
- -
- - - -- [自定义登录体验](https://docs.logto.io/zh-cn/docs/recipes/customize-sie) -- [启用短信或邮件验证码登录](https://docs.logto.io/zh-cn/docs/tutorials/get-started/enable-passcode-sign-in) -- [启用社交登录](https://docs.logto.io/zh-cn/docs/tutorials/get-started/enable-social-sign-in) -- [保护你的 API](https://docs.logto.io/zh-cn/docs/recipes/protect-your-api) - - diff --git a/packages/console/src/assets/docs/tutorial/integrate-sdk/vue.mdx b/packages/console/src/assets/docs/tutorial/integrate-sdk/vue.mdx deleted file mode 100644 index ad27f4a12..000000000 --- a/packages/console/src/assets/docs/tutorial/integrate-sdk/vue.mdx +++ /dev/null @@ -1,220 +0,0 @@ -import UriInputField from '@mdx/components/UriInputField'; -import Step from '@mdx/components/Step'; -import Tabs from '@mdx/components/Tabs'; -import TabItem from '@mdx/components/TabItem'; -import InlineNotification from '@/ds-components/InlineNotification'; - - props.onNext(1)} -> - - - -```bash -npm i @logto/vue -``` - - - - -```bash -yarn add @logto/vue -``` - - - - -```bash -pnpm add @logto/vue -``` - - - - - - props.onNext(2)} -> - - - We only support Vue 3 Composition API at this point. Will add support to Vue Options API and - possibly Vue 2 in future releases. - - -Import and use `createLogto` to install Logto plugin: - -
-  
-    {`import { createLogto, LogtoConfig } from '@logto/vue';
-
-const config: LogtoConfig = {
-  endpoint: '${props.endpoint}',${props.alternativeEndpoint ? ` // or "${props.alternativeEndpoint}"` : ''}
-  appId: '${props.appId}',
-};
-
-const app = createApp(App);
-
-app.use(createLogto, config);
-app.mount("#app");`}
-  
-
- -
- - props.onNext(3)} -> - - - In the following steps, we assume your app is running on http://localhost:3000. - - -### Configure Redirect URI - -First, let’s enter your redirect URI. E.g. `http://localhost:3000/callback`. - - - -### 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: - -
-
-{``}
-
-
-
- -```html - -``` - -### 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 - - -``` - -```html - -``` - -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, - }, - ], -}); -``` - -
- - props.onNext(4)} -> - -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()`. - - - -### Implement a sign-out button - -
-
-{``}
-
-
-
- -```html - -``` - -
- - - -- [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) - - diff --git a/packages/console/src/assets/docs/tutorial/integrate-sdk/vue_zh-cn.mdx b/packages/console/src/assets/docs/tutorial/integrate-sdk/vue_zh-cn.mdx deleted file mode 100644 index db0361347..000000000 --- a/packages/console/src/assets/docs/tutorial/integrate-sdk/vue_zh-cn.mdx +++ /dev/null @@ -1,207 +0,0 @@ -import UriInputField from '@mdx/components/UriInputField'; -import Step from '@mdx/components/Step'; -import Tabs from '@mdx/components/Tabs'; -import TabItem from '@mdx/components/TabItem'; -import InlineNotification from '@/ds-components/InlineNotification'; - - props.onNext(1)} -> - - - -```bash -npm i @logto/vue -``` - - - - -```bash -yarn add @logto/vue -``` - - - - -```bash -pnpm add @logto/vue -``` - - - - - - props.onNext(2)} -> - - - 目前仅支持 Vue 3 的 组合式(Composition)API,我们会在后续版本中陆续添加对选项式(Options)API 和 Vue 2 的支持。 - - -Import 并使用 `createLogto` 以插件的形式安装 Logto: - -
-
-{`import { createLogto, LogtoConfig } from '@logto/vue';
-
-const config: LogtoConfig = {
-  endpoint: '${props.endpoint}',${props.alternativeEndpoint ? ` // 或 "${props.alternativeEndpoint}"` : ''}
-  appId: '${props.appId}',
-};
-
-const app = createApp(App);
-
-app.use(createLogto, config);
-app.mount("#app");`}
-
-
- -
- - props.onNext(3)} -> - - - 在如下代码示例中, 我们均先假设你的 Vue 应用运行在 http://localhost:3000 上。 - - -### 配置 Redirect URI - -首先,我们来添加 Redirect URI,如:`http://localhost:3000/callback`。 - - - -### 实现登录按钮 - -我们提供了两个组合式 API `useHandleSignInCallback()` 和 `useLogto()`,它们可以帮助你轻松完成登录认证流程。 - -返回你的 IDE 或编辑器,使用如下代码来实现一个登录按钮: - -
-
-{``}
-
-
- -```html - -``` - -### 处理重定向 - -马上就要大功告成!在上一步,我们将 `http://localhost:3000/callback` 用作 Redirect URI,现在我们需要对其妥善处理。 - -首先,让我们来创建一个 Callback 组件: - -```html - - -``` - -```html - -``` - -最后我们插入如下代码来实现一个 _无需_ 登录的 `/callback` 路由: - -```ts -// 假设用 vue-router -const router = createRouter({ - routes: [ - { - path: '/callback', - name: 'callback', - component: CallbackView, - }, - ], -}); -``` - -
- - props.onNext(4)} -> - -调用 `.signOut()` 将清理内存与 localStorage 中的所有 Logto 数据(如果有)。 - -在退出登录后,让你的用户重新回到你的网站是个不错的选择。让我们将 `http://localhost:3000` 添加至下面的输入框,并将其作为调用 `.signOut()` 的参数。 - - - -### 实现退出登录按钮 - -
-
-{``}
-
-
- -```html - -``` - -
- - - -- [自定义登录体验](https://docs.logto.io/zh-cn/docs/recipes/customize-sie) -- [启用短信或邮件验证码登录](https://docs.logto.io/zh-cn/docs/tutorials/get-started/enable-passcode-sign-in) -- [启用社交登录](https://docs.logto.io/zh-cn/docs/tutorials/get-started/enable-social-sign-in) -- [保护你的 API](https://docs.logto.io/zh-cn/docs/recipes/protect-your-api) - - diff --git a/packages/console/src/mdx-components-v2/Step/index.module.scss b/packages/console/src/mdx-components-v2/Step/index.module.scss deleted file mode 100644 index 2278718e3..000000000 --- a/packages/console/src/mdx-components-v2/Step/index.module.scss +++ /dev/null @@ -1,15 +0,0 @@ -@use '@/scss/underscore' as _; - -.wrapper { - border-radius: 16px; - background: var(--color-layer-1); - padding: _.unit(5) _.unit(6); - width: 100%; - - header { - display: flex; - align-items: center; - gap: _.unit(4); - margin-bottom: _.unit(6); - } -} diff --git a/packages/console/src/mdx-components-v2/Step/index.tsx b/packages/console/src/mdx-components-v2/Step/index.tsx deleted file mode 100644 index 9f13d6b0c..000000000 --- a/packages/console/src/mdx-components-v2/Step/index.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { type ReactNode, forwardRef, type Ref } from 'react'; - -import Index from '@/components/Index'; -import CardTitle from '@/ds-components/CardTitle'; -import DangerousRaw from '@/ds-components/DangerousRaw'; - -import * as styles from './index.module.scss'; - -export type Props = { - index?: number; - title: string; - subtitle?: string; - children: ReactNode; -}; - -function Step({ title, subtitle, index, children }: Props, ref?: Ref) { - return ( -
-
- - {title}} - subtitle={{subtitle}} - /> -
-
{children}
-
- ); -} - -export default forwardRef(Step); diff --git a/packages/console/src/mdx-components-v2/UriInputField/index.module.scss b/packages/console/src/mdx-components-v2/UriInputField/index.module.scss deleted file mode 100644 index 436b5764d..000000000 --- a/packages/console/src/mdx-components-v2/UriInputField/index.module.scss +++ /dev/null @@ -1,27 +0,0 @@ -@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); - } -} - -/* Avoid using element selector directly in mdx components, as it will - * affect all elements in the entire admin console app. - */ -.form { - margin: _.unit(4) 0; -} diff --git a/packages/console/src/mdx-components-v2/UriInputField/index.tsx b/packages/console/src/mdx-components-v2/UriInputField/index.tsx deleted file mode 100644 index 057f1fbe2..000000000 --- a/packages/console/src/mdx-components-v2/UriInputField/index.tsx +++ /dev/null @@ -1,161 +0,0 @@ -import type { AdminConsoleKey } from '@logto/phrases'; -import type { Application } from '@logto/schemas'; -import { conditional } from '@silverhand/essentials'; -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/Guide'; -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'; - /** The default value of the input field when there's no data. */ - defaultValue?: string; -}; - -function UriInputField({ name, defaultValue }: Props) { - const methods = useForm>(); - const { - control, - getValues, - handleSubmit, - reset, - formState: { isSubmitting }, - } = methods; - const { - app: { id: appId }, - isCompact, - } = useContext(GuideContext); - const isSingle = !isCompact; - const { data, mutate } = useSWR(`api/applications/${appId}`); - - const ref = useRef(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(); - void mutate(updatedApp); - toast.success(t('general.saved')); - - // Reset form to set 'isDirty' to false - reset(getValues()); - }); - - const onKeyPress = (event: KeyboardEvent, value: string[]) => { - if (event.key === 'Enter') { - event.preventDefault(); - void handleSubmit(async () => onSubmit(value))(); - } - }; - - const clientMetadata = data?.oidcClientMetadata[name]; - const defaultValueArray = clientMetadata?.length - ? clientMetadata - : conditional(defaultValue && [defaultValue]); - - return ( - -
- !value || uriValidator(value), - message: t('errors.invalid_uri_format'), - }, - }), - }} - render={({ field: { onChange, value = [] }, fieldState: { error, isDirty } }) => { - const errorObject = convertRhfErrorMessage(error?.message); - - return ( -
- {isSingle && ( - - { - onChange([value]); - }} - onKeyPress={(event) => { - onKeyPress(event, value); - }} - /> - - )} - {!isSingle && ( - { - onKeyPress(event, value); - }} - /> - )} -
- ); - }} - /> - -
- ); -} - -export default UriInputField; diff --git a/packages/console/src/mdx-components-v2/ApplicationCredentials/index.module.scss b/packages/console/src/mdx-components/ApplicationCredentials/index.module.scss similarity index 100% rename from packages/console/src/mdx-components-v2/ApplicationCredentials/index.module.scss rename to packages/console/src/mdx-components/ApplicationCredentials/index.module.scss diff --git a/packages/console/src/mdx-components-v2/ApplicationCredentials/index.tsx b/packages/console/src/mdx-components/ApplicationCredentials/index.tsx similarity index 100% rename from packages/console/src/mdx-components-v2/ApplicationCredentials/index.tsx rename to packages/console/src/mdx-components/ApplicationCredentials/index.tsx diff --git a/packages/console/src/mdx-components-v2/README.md b/packages/console/src/mdx-components/README.md similarity index 100% rename from packages/console/src/mdx-components-v2/README.md rename to packages/console/src/mdx-components/README.md diff --git a/packages/console/src/mdx-components-v2/Sample/index.module.scss b/packages/console/src/mdx-components/Sample/index.module.scss similarity index 100% rename from packages/console/src/mdx-components-v2/Sample/index.module.scss rename to packages/console/src/mdx-components/Sample/index.module.scss diff --git a/packages/console/src/mdx-components-v2/Sample/index.tsx b/packages/console/src/mdx-components/Sample/index.tsx similarity index 100% rename from packages/console/src/mdx-components-v2/Sample/index.tsx rename to packages/console/src/mdx-components/Sample/index.tsx diff --git a/packages/console/src/mdx-components/Step/index.module.scss b/packages/console/src/mdx-components/Step/index.module.scss index cdc720846..2278718e3 100644 --- a/packages/console/src/mdx-components/Step/index.module.scss +++ b/packages/console/src/mdx-components/Step/index.module.scss @@ -1,103 +1,15 @@ @use '@/scss/underscore' as _; -.card { +.wrapper { + border-radius: 16px; + background: var(--color-layer-1); padding: _.unit(5) _.unit(6); - display: flex; - flex-direction: column; - scroll-margin: _.unit(5); + width: 100%; - .header { + header { display: flex; - flex-direction: row; align-items: center; - cursor: pointer; - - > svg { - color: var(--color-text-secondary); - } - - .index { - margin-right: _.unit(4); - } - } - - .buttonWrapper { - display: flex; - justify-content: flex-end; - margin-top: _.unit(6); - } - - .content { - margin-top: 0; - height: 0; - overflow: hidden; - - &.expanded { - height: auto; - overflow: unset; - } - - > *:first-child { - margin-top: _.unit(6); - } - } - - li { - font: var(--font-body-2); - - ul, - ol { - padding-inline-start: 1ch; - } - } - - ul { - padding-inline-start: 4ch; - - > li { - margin-block-start: _.unit(2); - margin-block-end: _.unit(2); - padding-inline-start: _.unit(1); - } - } - - ol { - padding-inline-start: 2ch; - - > li { - margin-block-start: _.unit(3); - margin-block-end: _.unit(3); - padding-inline-start: _.unit(1); - } - } - - h3 { - font: var(--font-title-2); - color: var(--color-text-secondary); - margin: _.unit(6) 0 _.unit(3); - } - - p { - font: var(--font-body-2); - margin: _.unit(3) 0; - } - - strong { - font: var(--font-label-2); - } - - pre { - margin: _.unit(3) 0; - } - - code:not(pre > code) { - background: var(--color-layer-2); - font: var(--font-body-2); - padding: _.unit(1) _.unit(1); - border-radius: 4px; + gap: _.unit(4); + margin-bottom: _.unit(6); } } - -.card + .card { - margin-top: _.unit(6); -} diff --git a/packages/console/src/mdx-components/Step/index.tsx b/packages/console/src/mdx-components/Step/index.tsx index 686bb93d0..9f13d6b0c 100644 --- a/packages/console/src/mdx-components/Step/index.tsx +++ b/packages/console/src/mdx-components/Step/index.tsx @@ -1,110 +1,32 @@ -import type { AdminConsoleKey } from '@logto/phrases'; -import classNames from 'classnames'; -import type { PropsWithChildren } from 'react'; -import { useEffect, useRef, useState, useCallback } from 'react'; +import { type ReactNode, forwardRef, type Ref } from 'react'; -import KeyboardArrowDown from '@/assets/icons/keyboard-arrow-down.svg'; -import KeyboardArrowUp from '@/assets/icons/keyboard-arrow-up.svg'; import Index from '@/components/Index'; -import Button from '@/ds-components/Button'; -import Card from '@/ds-components/Card'; import CardTitle from '@/ds-components/CardTitle'; import DangerousRaw from '@/ds-components/DangerousRaw'; -import IconButton from '@/ds-components/IconButton'; -import Spacer from '@/ds-components/Spacer'; -import { onKeyDownHandler } from '@/utils/a11y'; import * as styles from './index.module.scss'; -type Props = PropsWithChildren<{ +export type Props = { + index?: number; title: string; subtitle?: string; - index: number; - activeIndex: number; - buttonText?: AdminConsoleKey; - buttonHtmlType?: 'submit' | 'button'; - buttonType?: 'primary' | 'outline'; - isLoading?: boolean; - onButtonClick?: () => void; -}>; - -function Step({ - children, - title, - subtitle, - index, - activeIndex, - buttonText = 'general.next', - buttonHtmlType = 'button', - buttonType = 'outline', - isLoading, - onButtonClick, -}: Props) { - const [isExpanded, setIsExpanded] = useState(false); - const isActive = index === activeIndex; - const isComplete = index < activeIndex; - const ref = useRef(null); - - useEffect(() => { - if (isActive) { - setIsExpanded(true); - } - }, [isActive]); - - useEffect(() => { - if (isExpanded) { - ref.current?.scrollIntoView({ block: 'start', behavior: 'smooth' }); - } - }, [isExpanded]); - - const onToggle = useCallback(() => { - setIsExpanded((expand) => !expand); - }, [setIsExpanded]); + children: ReactNode; +}; +function Step({ title, subtitle, index, children }: Props, ref?: Ref) { return ( - -
{ - setIsExpanded(false); - }, - Enter: onToggle, - ' ': onToggle, - })} - onClick={onToggle} - > - +
+
+ {title}} subtitle={{subtitle}} /> - - {isExpanded ? : } -
-
- {children} -
-
-
-
+ +
{children}
+ ); } -export default Step; +export default forwardRef(Step); diff --git a/packages/console/src/mdx-components-v2/Steps/FurtherReadings.tsx b/packages/console/src/mdx-components/Steps/FurtherReadings.tsx similarity index 100% rename from packages/console/src/mdx-components-v2/Steps/FurtherReadings.tsx rename to packages/console/src/mdx-components/Steps/FurtherReadings.tsx diff --git a/packages/console/src/mdx-components-v2/Steps/index.module.scss b/packages/console/src/mdx-components/Steps/index.module.scss similarity index 100% rename from packages/console/src/mdx-components-v2/Steps/index.module.scss rename to packages/console/src/mdx-components/Steps/index.module.scss diff --git a/packages/console/src/mdx-components-v2/Steps/index.tsx b/packages/console/src/mdx-components/Steps/index.tsx similarity index 100% rename from packages/console/src/mdx-components-v2/Steps/index.tsx rename to packages/console/src/mdx-components/Steps/index.tsx diff --git a/packages/console/src/mdx-components/TabItem/index.module.scss b/packages/console/src/mdx-components/TabItem/index.module.scss new file mode 100644 index 000000000..294c202a0 --- /dev/null +++ b/packages/console/src/mdx-components/TabItem/index.module.scss @@ -0,0 +1,35 @@ +@use '@/scss/underscore' as _; + +.container { + width: 100%; + margin-top: _.unit(3); + + ul { + border-bottom: 1px solid var(--color-divider); + display: flex; + margin: _.unit(1) 0; + padding: 0; + + li { + list-style: none; + margin-right: _.unit(6); + padding-bottom: _.unit(1); + font: var(--font-label-2); + color: var(--color-text-secondary); + margin-block-end: unset; + padding-inline-start: unset; + cursor: pointer; + } + + li[aria-selected='true'] { + color: var(--color-text-link); + border-bottom: 2px solid var(--color-text-link); + margin-bottom: -1px; + outline: none; + } + } + + .hidden { + display: none; + } +} diff --git a/packages/console/src/mdx-components/UriInputField/index.module.scss b/packages/console/src/mdx-components/UriInputField/index.module.scss index 1b8cea632..436b5764d 100644 --- a/packages/console/src/mdx-components/UriInputField/index.module.scss +++ b/packages/console/src/mdx-components/UriInputField/index.module.scss @@ -18,3 +18,10 @@ margin: _.unit(6) 0 0 _.unit(2); } } + +/* Avoid using element selector directly in mdx components, as it will + * affect all elements in the entire admin console app. + */ +.form { + margin: _.unit(4) 0; +} diff --git a/packages/console/src/mdx-components/UriInputField/index.tsx b/packages/console/src/mdx-components/UriInputField/index.tsx index 3d28e3a0d..057f1fbe2 100644 --- a/packages/console/src/mdx-components/UriInputField/index.tsx +++ b/packages/console/src/mdx-components/UriInputField/index.tsx @@ -1,7 +1,8 @@ import type { AdminConsoleKey } from '@logto/phrases'; import type { Application } from '@logto/schemas'; +import { conditional } from '@silverhand/essentials'; import type { KeyboardEvent } from 'react'; -import { useRef } 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'; @@ -17,6 +18,7 @@ import { 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/Guide'; import type { GuideForm } from '@/types/guide'; import { trySubmitSafe } from '@/utils/form'; import { uriValidator } from '@/utils/validator'; @@ -24,13 +26,12 @@ import { uriValidator } from '@/utils/validator'; import * as styles from './index.module.scss'; type Props = { - appId: string; name: 'redirectUris' | 'postLogoutRedirectUris'; - title: AdminConsoleKey; - isSingle?: boolean; + /** The default value of the input field when there's no data. */ + defaultValue?: string; }; -function UriInputField({ appId, name, title, isSingle = false }: Props) { +function UriInputField({ name, defaultValue }: Props) { const methods = useForm>(); const { control, @@ -39,12 +40,20 @@ function UriInputField({ appId, name, title, isSingle = false }: Props) { reset, formState: { isSubmitting }, } = methods; - + const { + app: { id: appId }, + isCompact, + } = useContext(GuideContext); + const isSingle = !isCompact; const { data, mutate } = useSWR(`api/applications/${appId}`); const ref = useRef(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 @@ -70,13 +79,18 @@ function UriInputField({ appId, name, title, isSingle = false }: Props) { } }; + const clientMetadata = data?.oidcClientMetadata[name]; + const defaultValueArray = clientMetadata?.length + ? clientMetadata + : conditional(defaultValue && [defaultValue]); + return ( -
+ Date: Mon, 4 Sep 2023 18:12:18 +0800 Subject: [PATCH 09/30] refactor(phrases): update no-social connector notification (#4426) --- .../sign-up-and-sign-in/sad-path.test.ts | 2 +- .../de/translation/admin-console/sign-in-exp/index.ts | 8 +++----- .../en/translation/admin-console/sign-in-exp/index.ts | 4 +--- .../es/translation/admin-console/sign-in-exp/index.ts | 4 +--- .../fr/translation/admin-console/sign-in-exp/index.ts | 4 +--- .../it/translation/admin-console/sign-in-exp/index.ts | 4 +--- .../ja/translation/admin-console/sign-in-exp/index.ts | 4 +--- .../ko/translation/admin-console/sign-in-exp/index.ts | 4 +--- .../pl-pl/translation/admin-console/sign-in-exp/index.ts | 4 +--- .../pt-br/translation/admin-console/sign-in-exp/index.ts | 4 +--- .../pt-pt/translation/admin-console/sign-in-exp/index.ts | 4 +--- .../ru/translation/admin-console/sign-in-exp/index.ts | 4 +--- .../tr-tr/translation/admin-console/sign-in-exp/index.ts | 4 +--- .../zh-cn/translation/admin-console/sign-in-exp/index.ts | 3 +-- .../zh-hk/translation/admin-console/sign-in-exp/index.ts | 3 +-- .../zh-tw/translation/admin-console/sign-in-exp/index.ts | 3 +-- 16 files changed, 18 insertions(+), 45 deletions(-) diff --git a/packages/integration-tests/src/tests/ui/sign-in-experience/sign-up-and-sign-in/sad-path.test.ts b/packages/integration-tests/src/tests/ui/sign-in-experience/sign-up-and-sign-in/sad-path.test.ts index 59e931d2e..c03e59c23 100644 --- a/packages/integration-tests/src/tests/ui/sign-in-experience/sign-up-and-sign-in/sad-path.test.ts +++ b/packages/integration-tests/src/tests/ui/sign-in-experience/sign-up-and-sign-in/sad-path.test.ts @@ -166,7 +166,7 @@ describe('sign-in experience(sad path): sign-up and sign-in', () => { it('should display no social connector notification in social sign-in field', async () => { await expectNotificationInFiled(page, { field: 'Social sign-in', - content: /No social connector set-up yet./, + content: /You haven’t set up any social connector yet./, }); }); }); diff --git a/packages/phrases/src/locales/de/translation/admin-console/sign-in-exp/index.ts b/packages/phrases/src/locales/de/translation/admin-console/sign-in-exp/index.ts index 1a05fb17a..4ac54b79d 100644 --- a/packages/phrases/src/locales/de/translation/admin-console/sign-in-exp/index.ts +++ b/packages/phrases/src/locales/de/translation/admin-console/sign-in-exp/index.ts @@ -55,13 +55,11 @@ const sign_in_exp = { others, setup_warning: { no_connector_sms: - 'Es wurde noch kein SMS-Konnektor eingerichtet. Bevor die Konfiguration abgeschlossen werden kann, können sich Benutzer nicht mit dieser Methode anmelden. {{link}} in "Connectors"', + 'Es wurde noch kein SMS-Konnektor eingerichtet. Bevor die Konfiguration abgeschlossen werden kann, können sich Benutzer nicht mit dieser Methode anmelden. {{link}} in "Verbindungen".', no_connector_email: - 'Es wurde noch kein E-Mail-Konnektor eingerichtet. Bevor die Konfiguration abgeschlossen werden kann, können sich Benutzer nicht mit dieser Methode anmelden. {{link}} in "Connectors"', + 'Es wurde noch kein E-Mail-Konnektor eingerichtet. Bevor die Konfiguration abgeschlossen werden kann, können sich Benutzer nicht mit dieser Methode anmelden. {{link}} in "Verbindungen".', no_connector_social: - 'Es wurde noch kein Sozial-Konnektor eingerichtet. Bevor die Konfiguration abgeschlossen werden kann, können sich Benutzer nicht mit dieser Methode anmelden. {{link}} in "Connectors"', - no_added_social_connector: - 'Du hast jetzt ein paar Soziale Konnektoren eingerichtet. Füge jetzt einige zu deinem Anmeldeerlebnis hinzu.', + 'Sie haben noch keine soziale Verbindung eingerichtet. Fügen Sie zuerst Verbindungen hinzu, um soziale Anmeldeverfahren anzuwenden. {{link}} in "Verbindungen".', setup_link: 'Einrichtung', }, save_alert: { diff --git a/packages/phrases/src/locales/en/translation/admin-console/sign-in-exp/index.ts b/packages/phrases/src/locales/en/translation/admin-console/sign-in-exp/index.ts index 1c24ec4bd..600279417 100644 --- a/packages/phrases/src/locales/en/translation/admin-console/sign-in-exp/index.ts +++ b/packages/phrases/src/locales/en/translation/admin-console/sign-in-exp/index.ts @@ -58,9 +58,7 @@ const sign_in_exp = { no_connector_email: 'No email connector set-up yet. Before completing the configuration, users will not be able to sign in with this method. {{link}} in "Connectors"', no_connector_social: - 'No social connector set-up yet. Before completing the configuration, users will not be able to sign in with this method. {{link}} in "Connectors"', - no_added_social_connector: - 'You’ve set up a few social connectors now. Make sure to add some to your sign in experience.', + 'You haven’t set up any social connector yet. Add connectors first to apply social sign in methods. {{link}} in “Connectors”.', setup_link: 'Set up', }, save_alert: { diff --git a/packages/phrases/src/locales/es/translation/admin-console/sign-in-exp/index.ts b/packages/phrases/src/locales/es/translation/admin-console/sign-in-exp/index.ts index 167ec58d7..66421c2f5 100644 --- a/packages/phrases/src/locales/es/translation/admin-console/sign-in-exp/index.ts +++ b/packages/phrases/src/locales/es/translation/admin-console/sign-in-exp/index.ts @@ -60,9 +60,7 @@ const sign_in_exp = { no_connector_email: 'Aún no se ha configurado el conector de correo electrónico. Antes de completar la configuración, los usuarios no podrán iniciar sesión con este método. {{link}} en "Conectores"', no_connector_social: - 'Aún no se ha configurado el conector social. Antes de completar la configuración, los usuarios no podrán iniciar sesión con este método. {{link}} en "Conectores"', - no_added_social_connector: - 'Ha configurado algunos conectores sociales ahora. Asegúrese de agregar algunos a su experiencia de inicio de sesión.', + 'Todavía no ha configurado ningún conector social. Agregue conectores primero para aplicar métodos de inicio de sesión social. {{link}} en "Conectores".', setup_link: 'Configuración', }, save_alert: { diff --git a/packages/phrases/src/locales/fr/translation/admin-console/sign-in-exp/index.ts b/packages/phrases/src/locales/fr/translation/admin-console/sign-in-exp/index.ts index 4275d2f72..e86b93123 100644 --- a/packages/phrases/src/locales/fr/translation/admin-console/sign-in-exp/index.ts +++ b/packages/phrases/src/locales/fr/translation/admin-console/sign-in-exp/index.ts @@ -60,9 +60,7 @@ const sign_in_exp = { no_connector_email: 'Aucun connecteur d\'email n\'a été configuré. Avant de terminer la configuration, les utilisateurs ne pourront pas se connecter avec cette méthode. {{link}}dans"Connectors"', no_connector_social: - 'Aucun connecteur social n\'a été configuré. Avant de terminer la configuration, les utilisateurs ne pourront pas se connecter avec cette méthode. {{link}} dans "Connectors"', - no_added_social_connector: - "Vous avez maintenant configuré quelques connecteurs sociaux. Assurez-vous d'en ajouter quelques-uns à votre expérience de connexion.", + 'Vous n’avez pas encore configuré de connecteur social. Ajoutez d’abord des connecteurs pour appliquer des méthodes de connexion sociale. {{link}} dans "Connecteurs".', setup_link: 'Configurer', }, save_alert: { diff --git a/packages/phrases/src/locales/it/translation/admin-console/sign-in-exp/index.ts b/packages/phrases/src/locales/it/translation/admin-console/sign-in-exp/index.ts index c97bf8eec..475a6939c 100644 --- a/packages/phrases/src/locales/it/translation/admin-console/sign-in-exp/index.ts +++ b/packages/phrases/src/locales/it/translation/admin-console/sign-in-exp/index.ts @@ -59,9 +59,7 @@ const sign_in_exp = { no_connector_email: 'Nessun connettore email ancora configurato. Prima di completare la configurazione, gli utenti non saranno in grado di accedere con questo metodo. {{link}} in "Connettori"', no_connector_social: - 'Nessun connettore sociale ancora configurato. Prima di completare la configurazione, gli utenti non saranno in grado di accedere con questo metodo. {{link}} in "Connettori"', - no_added_social_connector: - 'Hai configurato alcuni connettori sociali adesso. Assicurati di aggiungerne alcuni alla tua esperienza di accesso.', + 'Non hai ancora configurato nessun connettore sociale. Aggiungi prima i connettori per applicare i metodi di accesso sociale. {{link}} in "Connettori".', setup_link: 'Configura', }, save_alert: { diff --git a/packages/phrases/src/locales/ja/translation/admin-console/sign-in-exp/index.ts b/packages/phrases/src/locales/ja/translation/admin-console/sign-in-exp/index.ts index 8a911227e..c39bc2a5e 100644 --- a/packages/phrases/src/locales/ja/translation/admin-console/sign-in-exp/index.ts +++ b/packages/phrases/src/locales/ja/translation/admin-console/sign-in-exp/index.ts @@ -58,9 +58,7 @@ const sign_in_exp = { no_connector_email: 'まだメールコネクタが設定されていません。構成を完了する前に、この方法でのサインインはできません。{{link}}「コネクタ」に移動してください', no_connector_social: - 'まだソーシャルコネクタが設定されていません。構成を完了する前に、この方法でのサインインはできません。{{link}}「コネクタ」に移動してください', - no_added_social_connector: - 'いくつかのソーシャルコネクタを設定しました。サインインエクスペリエンスにいくつか追加してください。', + 'まだソーシャルコネクタを設定していません。ソーシャルサインインの方法を適用するには、まずコネクタを追加してください。{{link}} の中で「コネクタ」をご覧ください。', setup_link: '設定', }, save_alert: { diff --git a/packages/phrases/src/locales/ko/translation/admin-console/sign-in-exp/index.ts b/packages/phrases/src/locales/ko/translation/admin-console/sign-in-exp/index.ts index 8341f10a8..44681826b 100644 --- a/packages/phrases/src/locales/ko/translation/admin-console/sign-in-exp/index.ts +++ b/packages/phrases/src/locales/ko/translation/admin-console/sign-in-exp/index.ts @@ -56,9 +56,7 @@ const sign_in_exp = { no_connector_email: '이메일 연동 설정이 아직 없어요. 이 구성을 완료하기 전에는 사용자가 이 로그인 방식으로 로그인 할 수 없어요. "연동 설정"에서 {{link}}하세요.', no_connector_social: - '소셜 연동 설정이 아직 없어요. 이 구성을 완료하기 전에는 사용자가 이 로그인 방식으로 로그인 할 수 없어요. "연동 설정"에서 {{link}}하세요.', - no_added_social_connector: - '보다 많은 소셜 연동들을 설정하여 고객에게 보다 나은 경험을 제공해보세요.', + '아직 소셜 커넥터를 설정하지 않았습니다. 소셜 로그인 방법을 적용하려면 먼저 커넥터를 추가하십시오. "커넥터"에서 {{link}}을(를) 확인하십시오.', setup_link: '설정', }, save_alert: { diff --git a/packages/phrases/src/locales/pl-pl/translation/admin-console/sign-in-exp/index.ts b/packages/phrases/src/locales/pl-pl/translation/admin-console/sign-in-exp/index.ts index c42ee0bed..5abac48a3 100644 --- a/packages/phrases/src/locales/pl-pl/translation/admin-console/sign-in-exp/index.ts +++ b/packages/phrases/src/locales/pl-pl/translation/admin-console/sign-in-exp/index.ts @@ -59,9 +59,7 @@ const sign_in_exp = { no_connector_email: 'Nie ustawiono jeszcze łącznika e-mail. Przed zakończeniem konfiguracji użytkownicy nie będą mogli się zalogować przy użyciu tej metody. {{link}} w sekcji „Łączniki“', no_connector_social: - 'Nie ustawiono jeszcze łącznika społecznościowego. Przed zakończeniem konfiguracji użytkownicy nie będą mogli się zalogować przy użyciu tej metody. {{link}} w sekcji„Łączniki"', - no_added_social_connector: - 'Skonfigurowałeś teraz kilka łączników społecznościowych. Upewnij się, że dodano niektóre z nich do Twojego doświadczenia logowania.', + 'Nie skonfigurowałeś jeszcze żadnego konektora społecznościowego. Najpierw dodaj konektory, aby zastosować metody logowania społecznościowego. {{link}} w "Konektory".', setup_link: 'Konfiguracja', }, save_alert: { diff --git a/packages/phrases/src/locales/pt-br/translation/admin-console/sign-in-exp/index.ts b/packages/phrases/src/locales/pt-br/translation/admin-console/sign-in-exp/index.ts index 60230771e..d76fdeb7d 100644 --- a/packages/phrases/src/locales/pt-br/translation/admin-console/sign-in-exp/index.ts +++ b/packages/phrases/src/locales/pt-br/translation/admin-console/sign-in-exp/index.ts @@ -59,9 +59,7 @@ const sign_in_exp = { no_connector_email: 'Nenhum conector de e-mail configurado ainda. Até terminar de configurar seu conector de e-mail, seus usuários não poderão fazer login. {{link}} em "Conectores"', no_connector_social: - 'Nenhum conector social configurado ainda. Até terminar de configurar seu conector social, seus usuários não poderão fazer login. {{link}} em "Conectores"', - no_added_social_connector: - 'Você configurou alguns conectores sociais agora. Certifique-se de adicionar alguns à sua experiência de login.', + 'Você ainda não configurou nenhum conector social. Adicione conectores primeiro para aplicar métodos de login social. {{link}} em "Conectores".', setup_link: 'Configurar', }, save_alert: { diff --git a/packages/phrases/src/locales/pt-pt/translation/admin-console/sign-in-exp/index.ts b/packages/phrases/src/locales/pt-pt/translation/admin-console/sign-in-exp/index.ts index 0afcad325..659184577 100644 --- a/packages/phrases/src/locales/pt-pt/translation/admin-console/sign-in-exp/index.ts +++ b/packages/phrases/src/locales/pt-pt/translation/admin-console/sign-in-exp/index.ts @@ -58,9 +58,7 @@ const sign_in_exp = { no_connector_email: 'Ainda não foi configurado qualquer conector de email. Antes de concluir a configuração, os utilizadores não poderão iniciar sessão com este método. {{link}} em "Conectores"', no_connector_social: - 'Ainda não foi configurado nenhum conector social. Antes de concluir a configuração, os utilizadores não poderão iniciar sessão com este método. {{link}} em "Conectores"', - no_added_social_connector: - 'Configurou alguns conectores sociais agora. Certifique-se de adicionar alguns a experiência de login.', + 'Você ainda não configurou nenhum conector social. Adicione conectores primeiro para aplicar métodos de login social. {{link}} em "Conectores".', setup_link: 'Configurar', }, save_alert: { diff --git a/packages/phrases/src/locales/ru/translation/admin-console/sign-in-exp/index.ts b/packages/phrases/src/locales/ru/translation/admin-console/sign-in-exp/index.ts index bb7da6ff4..cfc02381f 100644 --- a/packages/phrases/src/locales/ru/translation/admin-console/sign-in-exp/index.ts +++ b/packages/phrases/src/locales/ru/translation/admin-console/sign-in-exp/index.ts @@ -60,9 +60,7 @@ const sign_in_exp = { no_connector_email: 'Еще не настроен коннектор по электронной почте. Пока не завершено настройка, пользователи не смогут войти с помощью этого метода. {{link}} в «Коннекторах»', no_connector_social: - 'Еще не настроен социальный коннектор. Пока не завершено настройка, пользователи не смогут войти с помощью этого метода. {{link}} в «Коннекторах»', - no_added_social_connector: - 'Вы уже настроили несколько социальных коннекторов. Убедитесь, что вы добавили их в настройки входа в систему.', + 'Вы еще не настроили социальный коннектор. Сначала добавьте коннекторы, чтобы применить методы социальной авторизации. {{link}} в разделе "Коннекторы".', setup_link: 'Настройка', }, save_alert: { diff --git a/packages/phrases/src/locales/tr-tr/translation/admin-console/sign-in-exp/index.ts b/packages/phrases/src/locales/tr-tr/translation/admin-console/sign-in-exp/index.ts index e3d685ea7..52261a2d8 100644 --- a/packages/phrases/src/locales/tr-tr/translation/admin-console/sign-in-exp/index.ts +++ b/packages/phrases/src/locales/tr-tr/translation/admin-console/sign-in-exp/index.ts @@ -59,9 +59,7 @@ const sign_in_exp = { no_connector_email: 'Henüz e-posta konektörü kurulmadı. Yapılandırmayı tamamlamadan önce, kullanıcılar bu yöntemle oturum açamazlar. "Konektörler"deki {{link}}', no_connector_social: - 'Henüz sosyal konektör kurulmadı. Yapılandırmayı tamamlamadan önce, kullanıcılar bu yöntemle oturum açamazlar. "Konektörler"deki {{link}}', - no_added_social_connector: - 'Şimdi birkaç sosyal konektör eklediniz. Oturum açma deneyiminize bazı şeyler eklediğinizden emin olun.', + 'Henüz hiçbir sosyal bağlayıcıyı ayarlamadınız. Sosyal giriş yöntemlerini uygulamak için önce bağlayıcı ekleyin. "Bağlayıcılar" bölümünde {{link}} görüntüleyin.', setup_link: 'Kurulum yapın', }, save_alert: { diff --git a/packages/phrases/src/locales/zh-cn/translation/admin-console/sign-in-exp/index.ts b/packages/phrases/src/locales/zh-cn/translation/admin-console/sign-in-exp/index.ts index 3c98fabd1..0052183da 100644 --- a/packages/phrases/src/locales/zh-cn/translation/admin-console/sign-in-exp/index.ts +++ b/packages/phrases/src/locales/zh-cn/translation/admin-console/sign-in-exp/index.ts @@ -56,8 +56,7 @@ const sign_in_exp = { no_connector_email: '尚未设置电子邮件连接器。在完成该配置前,用户将无法通过此登录方式登录。{{link}}连接器。', no_connector_social: - '尚未设置社交连接器。在完成该配置前,用户将无法通过此登录方式登录。{{link}}连接器。', - no_added_social_connector: '你已经成功设置了一些社交连接器。点按「+」添加一些到你的登录体验。', + '您还没有设置任何社交连接器。首先添加连接器以应用社交登录方法。{{link}}连接器。', setup_link: '立即设置', }, save_alert: { diff --git a/packages/phrases/src/locales/zh-hk/translation/admin-console/sign-in-exp/index.ts b/packages/phrases/src/locales/zh-hk/translation/admin-console/sign-in-exp/index.ts index 6014ec8d8..062c347ac 100644 --- a/packages/phrases/src/locales/zh-hk/translation/admin-console/sign-in-exp/index.ts +++ b/packages/phrases/src/locales/zh-hk/translation/admin-console/sign-in-exp/index.ts @@ -56,8 +56,7 @@ const sign_in_exp = { no_connector_email: '尚未設置電子郵件連接器。在完成該配置前,用戶將無法通過此登錄方式登錄。{{link}}連接器。', no_connector_social: - '尚未設置社交連接器。在完成該配置前,用戶將無法通過此登錄方式登錄。{{link}}連接器。', - no_added_social_connector: '你已經成功設置了一些社交連接器。點按「+」添加一些到您的登錄體驗。', + '您還沒有設置任何社交連接器。首先添加連接器以應用社交登錄方法。{{link}}連接器。', setup_link: '立即設置', }, save_alert: { diff --git a/packages/phrases/src/locales/zh-tw/translation/admin-console/sign-in-exp/index.ts b/packages/phrases/src/locales/zh-tw/translation/admin-console/sign-in-exp/index.ts index c029fbc73..536848dfc 100644 --- a/packages/phrases/src/locales/zh-tw/translation/admin-console/sign-in-exp/index.ts +++ b/packages/phrases/src/locales/zh-tw/translation/admin-console/sign-in-exp/index.ts @@ -56,8 +56,7 @@ const sign_in_exp = { no_connector_email: '尚未設置電子郵件連接器。在完成該配置前,用戶將無法通過此登錄方式登錄。{{link}}連接器。', no_connector_social: - '尚未設置社交連接器。在完成該配置前,用戶將無法通過此登錄方式登錄。{{link}}連接器。', - no_added_social_connector: '你已經成功設置了一些社交連接器。點按「+」添加一些到你的登錄體驗。', + '您還沒有設置任何社交連接器。首先添加連接器以應用社交登錄方法。{{link}}連接器。', setup_link: '立即設置', }, save_alert: { From 533d68193933a3f627b066ab1507774ccc962930 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Mon, 4 Sep 2023 19:03:15 +0800 Subject: [PATCH 10/30] refactor(console): remove add connectors tip when no social connector is set up (#4422) --- .../SocialConnectorEditBox/index.tsx | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignIn/components/SocialConnectorEditBox/index.tsx b/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignIn/components/SocialConnectorEditBox/index.tsx index b1ae37174..ee8c41821 100644 --- a/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignIn/components/SocialConnectorEditBox/index.tsx +++ b/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignIn/components/SocialConnectorEditBox/index.tsx @@ -48,14 +48,14 @@ function SocialConnectorEditBox({ value, onChange }: Props) { ); }; + const socialConnectors = connectorData.filter(({ type }) => type === ConnectorType.Social); + const selectedConnectorItems = value - .map((connectorTarget) => connectorData.find(({ target }) => target === connectorTarget)) + .map((connectorTarget) => socialConnectors.find(({ target }) => target === connectorTarget)) // eslint-disable-next-line unicorn/prefer-native-coercion-functions .filter((item): item is ConnectorGroup => Boolean(item)); - const connectorOptions = connectorData.filter( - ({ target, type }) => !value.includes(target) && type === ConnectorType.Social - ); + const connectorOptions = socialConnectors.filter(({ target }) => !value.includes(target)); return (
@@ -85,13 +85,15 @@ function SocialConnectorEditBox({ value, onChange }: Props) { }} /> -
- {t('sign_in_exp.sign_up_and_sign_in.social_sign_in.set_up_hint.not_in_list')} - - {t('sign_in_exp.sign_up_and_sign_in.social_sign_in.set_up_hint.set_up_more')} - - {t('sign_in_exp.sign_up_and_sign_in.social_sign_in.set_up_hint.go_to')} -
+ {socialConnectors.length > 0 && ( +
+ {t('sign_in_exp.sign_up_and_sign_in.social_sign_in.set_up_hint.not_in_list')} + + {t('sign_in_exp.sign_up_and_sign_in.social_sign_in.set_up_hint.set_up_more')} + + {t('sign_in_exp.sign_up_and_sign_in.social_sign_in.set_up_hint.go_to')} +
+ )}
); } From 8e4e08f73184ced1f1d7c2f55925781838828460 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Tue, 5 Sep 2023 10:59:01 +0800 Subject: [PATCH 11/30] refactor(console): remove no-data social section from changes alert modal (#4424) * refactor(console): remove no-data social section from changes alert modal * test(console): add sign-in config diff ui tests --- .../SocialTargetsDiffSection.tsx | 6 +--- .../sign-up-and-sign-in/happy-path.test.ts | 35 +++++++++++++++++-- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/packages/console/src/pages/SignInExperience/components/SignUpAndSignInChangePreview/SignUpAndSignInDiffSection/SocialTargetsDiffSection.tsx b/packages/console/src/pages/SignInExperience/components/SignUpAndSignInChangePreview/SignUpAndSignInDiffSection/SocialTargetsDiffSection.tsx index 742e55125..b2d2e5029 100644 --- a/packages/console/src/pages/SignInExperience/components/SignUpAndSignInChangePreview/SignUpAndSignInDiffSection/SocialTargetsDiffSection.tsx +++ b/packages/console/src/pages/SignInExperience/components/SignUpAndSignInChangePreview/SignUpAndSignInDiffSection/SocialTargetsDiffSection.tsx @@ -25,11 +25,7 @@ function SocialTargetsDiffSection({ before, after, isAfter = false }: Props) { const hasChanged = (target: string) => !(before.includes(target) && after.includes(target)); - if (!groups) { - return null; - } - - if (error) { + if (!groups || displayTargets.length === 0 || error) { return null; } diff --git a/packages/integration-tests/src/tests/ui/sign-in-experience/sign-up-and-sign-in/happy-path.test.ts b/packages/integration-tests/src/tests/ui/sign-in-experience/sign-up-and-sign-in/happy-path.test.ts index 164c3e5f5..a1a04a879 100644 --- a/packages/integration-tests/src/tests/ui/sign-in-experience/sign-up-and-sign-in/happy-path.test.ts +++ b/packages/integration-tests/src/tests/ui/sign-in-experience/sign-up-and-sign-in/happy-path.test.ts @@ -1,5 +1,12 @@ import { logtoConsoleUrl as logtoConsoleUrlString } from '#src/constants.js'; -import { expectToClickNavTab, goToAdminConsole } from '#src/ui-helpers/index.js'; +import { + expectModalWithTitle, + expectToClickModalAction, + expectToClickNavTab, + expectToSaveChanges, + goToAdminConsole, + waitForToast, +} from '#src/ui-helpers/index.js'; import { expectNavigation, appendPathname } from '#src/utils.js'; import { expectToSaveSignInExperience, waitForFormCard } from '../helpers.js'; @@ -547,7 +554,31 @@ describe('sign-in experience(happy path): sign-up and sign-in', () => { it('add social sign-in connector', async () => { await expectToAddSocialSignInConnector(page, 'Apple'); - await expectToSaveSignInExperience(page, { needToConfirmChanges: true }); + // Should have diffs about social sign-in connector + await expectToSaveChanges(page); + await expectModalWithTitle(page, 'Reminder'); + // No social content in the before section + const beforeSection = await expect(page).toMatchElement( + 'div[class$=section]:has(div[class$=title])', + { text: 'Before' } + ); + await expect( + expect(beforeSection).toMatchElement('div[class$=title]', { + text: 'Social', + }) + ).rejects.toThrow(); + + // Have social content in the after section + const afterSection = await expect(page).toMatchElement( + 'div[class$=section]:has(div[class$=title])', + { text: 'After' } + ); + await expect(afterSection).toMatchElement('div[class$=title]', { + text: 'Social', + }); + + await expectToClickModalAction(page, 'Confirm'); + await waitForToast(page, { text: 'Saved' }); // Reset await expectToRemoveSocialSignInConnector(page, 'Apple'); From c5b1976117cfc536f4894a51a81a576f97fcca1b Mon Sep 17 00:00:00 2001 From: Darcy Ye Date: Tue, 5 Sep 2023 11:07:38 +0800 Subject: [PATCH 12/30] refactor(toolkit): update input params type of SendMessageFunction (#4287) --- packages/schemas/src/types/system.ts | 14 ++++---- packages/toolkit/connector-kit/src/types.ts | 38 +++++++++++---------- 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/packages/schemas/src/types/system.ts b/packages/schemas/src/types/system.ts index 401577840..18b632e9b 100644 --- a/packages/schemas/src/types/system.ts +++ b/packages/schemas/src/types/system.ts @@ -69,7 +69,7 @@ export enum EmailServiceProvider { SendGrid = 'SendGrid', } -export const sendgridEmailServiceDataGuard = z.object({ +export const sendgridEmailServiceConfigGuard = z.object({ provider: z.literal(EmailServiceProvider.SendGrid), apiKey: z.string(), templateId: z.string(), @@ -77,26 +77,26 @@ export const sendgridEmailServiceDataGuard = z.object({ fromEmail: z.string(), }); -export type SendgridEmailServiceData = z.infer; +export type SendgridEmailServiceConfig = z.infer; -export const emailServiceDataGuard = z.discriminatedUnion('provider', [ - sendgridEmailServiceDataGuard, +export const emailServiceConfigGuard = z.discriminatedUnion('provider', [ + sendgridEmailServiceConfigGuard, ]); -export type EmailServiceData = z.infer; +export type EmailServiceConfig = z.infer; export enum EmailServiceProviderKey { EmailServiceProvider = 'emailServiceProvider', } export type EmailServiceProviderType = { - [EmailServiceProviderKey.EmailServiceProvider]: EmailServiceData; + [EmailServiceProviderKey.EmailServiceProvider]: EmailServiceConfig; }; export const emailServiceProviderGuard: Readonly<{ [key in EmailServiceProviderKey]: ZodType; }> = Object.freeze({ - [EmailServiceProviderKey.EmailServiceProvider]: emailServiceDataGuard, + [EmailServiceProviderKey.EmailServiceProvider]: emailServiceConfigGuard, }); // Demo social connectors diff --git a/packages/toolkit/connector-kit/src/types.ts b/packages/toolkit/connector-kit/src/types.ts index b1a9ce0c5..69ce63512 100644 --- a/packages/toolkit/connector-kit/src/types.ts +++ b/packages/toolkit/connector-kit/src/types.ts @@ -242,29 +242,31 @@ export const emailServiceBrandingGuard = z export type EmailServiceBranding = z.infer; export type SendMessagePayload = { - to: string; - type: VerificationCodeType; - payload: { - /** - * The dynamic verification code to send. - * - * @example '123456' - */ - code: string; - } & EmailServiceBranding; + /** + * The dynamic verification code to send. + * + * @example '123456' + */ + code: string; }; export const sendMessagePayloadGuard = z.object({ - to: z.string(), - type: verificationCodeTypeGuard, - payload: z - .object({ - code: z.string(), - }) - .merge(emailServiceBrandingGuard), + code: z.string(), }) satisfies z.ZodType; -export type SendMessageFunction = (data: SendMessagePayload, config?: unknown) => Promise; +export type SendMessageData = { + to: string; + type: VerificationCodeType; + payload: SendMessagePayload; +}; + +export const sendMessageDataGuard = z.object({ + to: z.string(), + type: verificationCodeTypeGuard, + payload: sendMessagePayloadGuard, +}) satisfies z.ZodType; + +export type SendMessageFunction = (data: SendMessageData, config?: unknown) => Promise; export type GetUsageFunction = (startFrom?: Date) => Promise; From c18eb490cd8a86acf79f682235e2423096b3ff00 Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Tue, 5 Sep 2023 14:31:15 +0800 Subject: [PATCH 13/30] refactor(connector): update aad connector (#4435) --- .../connectors/connector-azuread/README.md | 26 +++++++++++++------ .../connector-azuread/src/constant.ts | 3 ++- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/packages/connectors/connector-azuread/README.md b/packages/connectors/connector-azuread/README.md index 27a3c40e6..6578b956e 100644 --- a/packages/connectors/connector-azuread/README.md +++ b/packages/connectors/connector-azuread/README.md @@ -5,26 +5,36 @@ The Microsoft Azure AD connector provides a succinct way for your application to **Table of contents** - [Microsoft Azure AD connector](#microsoft-azure-ad-connector) - [Set up Microsoft Azure AD in the Azure Portal](#set-up-microsoft-azure-ad-in-the-azure-portal) + - [Fill in the configuration](#fill-in-the-configuration) - [Configure your client secret](#configure-your-client-secret) - - [Config types](#config-types) + - [Config types](#config-types) - [References](#references) ## Set up Microsoft Azure AD in the Azure Portal - Visit the [Azure Portal](https://portal.azure.com/#home) and sign in with your Azure account. You need to have an active subscription to access Microsoft Azure AD. - Click the **Azure Active Directory** from the services they offer, and click the **App Registrations** from the left menu. -- Click **New Registration** at the top and enter a description, select your **access type** and add your **Redirect URI**, which redirect the user to the application after logging in. In our case, this will be `${your_logto_origin}/callback/${connector_id}`. e.g. `https://logto.dev/callback/${connector_id}`. You need to select Web as Platform. The `connector_id` can be found on the top bar of the Logto Admin Console connector details page. +- Click **New Registration** at the top and enter a description, select your **access type** and add your **Redirect URI**, which redirect the user to the application after logging in. In our case, this will be `${your_logto_endpoint}/callback/${connector_id}`. e.g. `https://foo.logto.app/callback/${connector_id}`. (The `connector_id` can be also found on the top bar of the Logto Admin Console connector details page) +- You need to select Web as Platform. - If you select **Sign in users of a specific organization only** for access type then you need to enter **TenantID**. - If you select **Sign in users with work and school accounts or personal Microsoft accounts** for access type then you need to enter **common**. - If you select **Sign in users with work and school accounts** for access type then you need to enter **organizations**. - If you select **Sign in users with personal Microsoft accounts (MSA) only** for access type then you need to enter **consumers**. -## Configure your client secret -- In your newly created project, click the **Certificates & Secrets** to get a client secret, and click the **New client secret** from the top. -- Enter a description and an expiration. -- This will only show your client secret once. Save the **value** to a secure location. +> You can copy the `Callback URI` in the configuration section. -### Config types +## Fill in the configuration + +In details page of the newly registered app, you can find the **Application (client) ID** and **Directory (tenant) ID**. + +For **Cloud Instance**, usually it is `https://login.microsoftonline.com/`. See [Azure AD authentication endpoints](https://learn.microsoft.com/en-us/azure/active-directory/develop/authentication-national-cloud#azure-ad-authentication-endpoints) for more information. + +## Configure your client secret +- In your newly created application, click the **Certificates & Secrets** to get a client secret, and click the **New client secret** from the top. +- Enter a description and an expiration. +- This will only show your client secret once. Fill the **value** to the Logto connector configuration and save it to a secure location. + +## Config types | Name | Type | | ------------- | ------ | @@ -34,4 +44,4 @@ The Microsoft Azure AD connector provides a succinct way for your application to | cloudInstance | string | ## References -* [Web app that signs in users](https://docs.microsoft.com/en-us/azure/active-directory/develop/scenario-web-app-sign-user-overview?tabs=nodejs) +* [Web app that signs in users](https://docs.microsoft.com/en-us/azure/active-directory/develop/scenario-web-app-sign-user-overview) diff --git a/packages/connectors/connector-azuread/src/constant.ts b/packages/connectors/connector-azuread/src/constant.ts index 085bf9b15..ccec7fe29 100644 --- a/packages/connectors/connector-azuread/src/constant.ts +++ b/packages/connectors/connector-azuread/src/constant.ts @@ -43,7 +43,8 @@ export const defaultMetadata: ConnectorMetadata = { type: ConnectorConfigFormItemType.Text, required: true, label: 'Cloud Instance', - placeholder: '', + placeholder: 'https://login.microsoftonline.com', + defaultValue: 'https://login.microsoftonline.com', }, { key: 'tenantId', From 1af47e545d015c0c142be746b236b9f142dd9277 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 5 Sep 2023 14:41:15 +0800 Subject: [PATCH 14/30] chore(deps): update supercharge/redis-github-action action to v1.7.0 (#4421) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/integration-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 59b14aff6..1d23366da 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -74,7 +74,7 @@ jobs: uses: ikalnytskyi/action-setup-postgres@v4 - name: Setup Redis - uses: supercharge/redis-github-action@1.6.0 + uses: supercharge/redis-github-action@6dc7a5eeaf9a8f860b6464e05195a15f8b9f3bbb # 1.7.0 with: redis-version: 6 From ef0e38249f1b54540fb3d774e290e790603ceb55 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 5 Sep 2023 16:13:24 +0800 Subject: [PATCH 15/30] chore(deps): update actions/checkout action to v4 (#4431) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/changesets.yml | 2 +- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/commitlint.yml | 2 +- .github/workflows/integration-test.yml | 4 ++-- .github/workflows/main.yml | 12 ++++++------ .github/workflows/master-codecov-report.yml | 2 +- .github/workflows/pen-tests.yml | 2 +- .github/workflows/release.yml | 6 +++--- .github/workflows/update-pr-metadata.yml | 2 +- .github/workflows/upload-annotations.yml | 2 +- 10 files changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/changesets.yml b/.github/workflows/changesets.yml index 522fb288b..251f4d135 100644 --- a/.github/workflows/changesets.yml +++ b/.github/workflows/changesets.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: token: ${{ secrets.BOT_PAT }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 0750286db..bb395d567 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -41,7 +41,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml index 28da4c052..c91b7a526 100644 --- a/.github/workflows/commitlint.yml +++ b/.github/workflows/commitlint.yml @@ -16,7 +16,7 @@ jobs: if: github.event_name == 'pull_request' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 1d23366da..b29bfddbe 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup Node and pnpm uses: silverhand-io/actions-node-pnpm-run-steps@v3 @@ -45,7 +45,7 @@ jobs: DB_URL: postgres://postgres:postgres@localhost:5432/postgres steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: path: tests diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index aa13b886f..c895479a3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup Node and pnpm uses: silverhand-io/actions-node-pnpm-run-steps@v3 @@ -28,7 +28,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup Node and pnpm uses: silverhand-io/actions-node-pnpm-run-steps@v3 @@ -46,7 +46,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup Node and pnpm uses: silverhand-io/actions-node-pnpm-run-steps@v3 @@ -73,7 +73,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 @@ -92,7 +92,7 @@ jobs: steps: # ** Checkout fresh and alteration ref ** - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 path: ./fresh @@ -102,7 +102,7 @@ jobs: working-directory: ./fresh run: echo "current=$(git describe --match "@logto/schemas@*" --abbrev=0)" >> $GITHUB_OUTPUT - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: ref: ${{ steps.version.outputs.current }} path: ./alteration diff --git a/.github/workflows/master-codecov-report.yml b/.github/workflows/master-codecov-report.yml index b233b41a3..58373f243 100644 --- a/.github/workflows/master-codecov-report.yml +++ b/.github/workflows/master-codecov-report.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup Node and pnpm uses: silverhand-io/actions-node-pnpm-run-steps@v3 diff --git a/.github/workflows/pen-tests.yml b/.github/workflows/pen-tests.yml index 0cf85d920..291aa3404 100644 --- a/.github/workflows/pen-tests.yml +++ b/.github/workflows/pen-tests.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Docker Compose up run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b69f21af3..1bdc73b11 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,7 @@ jobs: id-token: write steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 @@ -72,7 +72,7 @@ jobs: if: ${{ !startsWith(github.ref, 'refs/tags/') }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: # Set Git operations with the bot PAT since we have tag protection rule token: ${{ secrets.BOT_PAT }} @@ -104,7 +104,7 @@ jobs: if: startsWith(github.ref, 'refs/tags/') steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 diff --git a/.github/workflows/update-pr-metadata.yml b/.github/workflows/update-pr-metadata.yml index 6d28681ec..e8ada6373 100644 --- a/.github/workflows/update-pr-metadata.yml +++ b/.github/workflows/update-pr-metadata.yml @@ -33,7 +33,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: # https://github.com/actions/checkout/issues/518#issuecomment-890401887 ref: refs/pull/${{ github.event.number }}/merge diff --git a/.github/workflows/upload-annotations.yml b/.github/workflows/upload-annotations.yml index 369626017..3218d152a 100644 --- a/.github/workflows/upload-annotations.yml +++ b/.github/workflows/upload-annotations.yml @@ -19,7 +19,7 @@ jobs: if: github.event.pull_request.head.repo.full_name == github.repository steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup Node and pnpm uses: silverhand-io/actions-node-pnpm-run-steps@v3 From 90ea2e2f71bf76a069af8a3a51111b74a4043503 Mon Sep 17 00:00:00 2001 From: Charles Zhao Date: Tue, 5 Sep 2023 23:36:30 +0800 Subject: [PATCH 16/30] refactor(console): adjust sdk guide sorting order (#4427) * refactor(console): adjust sdk guide sorting order * docs(console): update rules about the sorting order of the sdk guides --- .../console/src/assets/docs/guides/README.md | 6 +- .../assets/docs/guides/generate-metadata.js | 8 +- .../console/src/assets/docs/guides/index.ts | 162 +++++++++--------- .../docs/guides/m2m-general/config.json | 3 + .../guides/native-android-java/config.json | 2 +- .../docs/guides/native-android-kt/config.json | 2 +- .../docs/guides/native-flutter/config.json | 2 +- .../docs/guides/native-ios-swift/config.json | 2 +- .../assets/docs/guides/spa-react/config.json | 2 +- .../docs/guides/spa-vanilla/config.json | 2 +- .../assets/docs/guides/spa-vue/config.json | 2 +- .../docs/guides/web-asp-net-core/config.json | 2 +- .../docs/guides/web-express/config.json | 2 +- .../src/assets/docs/guides/web-go/config.json | 2 +- .../docs/guides/web-gpt-plugin/config.json | 2 +- .../assets/docs/guides/web-java/config.json | 3 + .../docs/guides/web-outline/config.json | 2 +- .../assets/docs/guides/web-php/config.json | 2 +- .../assets/docs/guides/web-python/config.json | 2 +- .../assets/docs/guides/web-remix/config.json | 2 +- 20 files changed, 107 insertions(+), 105 deletions(-) create mode 100644 packages/console/src/assets/docs/guides/m2m-general/config.json create mode 100644 packages/console/src/assets/docs/guides/web-java/config.json diff --git a/packages/console/src/assets/docs/guides/README.md b/packages/console/src/assets/docs/guides/README.md index 0f0803d72..29f0f0dea 100644 --- a/packages/console/src/assets/docs/guides/README.md +++ b/packages/console/src/assets/docs/guides/README.md @@ -60,8 +60,10 @@ This may be fixed by replacing Parcel with something else. The guides are ordered by the following rules in ascending order: -1. The first segment of the directory name, which should be the target of the guide; -2. The `order` property of the guide. +1. The `order` property of all guides across all category groups +2. The sorting order should exactly follow the order in UX design. E.g. Next.js being the 1st one, React being the 2nd +3. The guides in featured group ("Popular and for you") should have `1.x` order value +4. The guides that are not listed in featured group can have order value equal to or greater than 2 You can configure the property by creating a `config.json` file in the guide directory. The file should be an object with the following structure: diff --git a/packages/console/src/assets/docs/guides/generate-metadata.js b/packages/console/src/assets/docs/guides/generate-metadata.js index 533653b5c..76eb4a05d 100644 --- a/packages/console/src/assets/docs/guides/generate-metadata.js +++ b/packages/console/src/assets/docs/guides/generate-metadata.js @@ -34,13 +34,7 @@ const data = await Promise.all( }; }) ); -const metadata = data.filter(Boolean).slice().sort((a, b) => { - if (a.name.split('-')[0] !== b.name.split('-')[0]) { - return a.name.localeCompare(b.name); - } - - return a.order - b.order; -}); +const metadata = data.filter(Boolean).slice().sort((a, b) => a.order - b.order); const camelCase = (value) => value.replaceAll(/-./g, (x) => x[1].toUpperCase()); const filename = 'index.ts'; diff --git a/packages/console/src/assets/docs/guides/index.ts b/packages/console/src/assets/docs/guides/index.ts index 04f92e922..ef9322446 100644 --- a/packages/console/src/assets/docs/guides/index.ts +++ b/packages/console/src/assets/docs/guides/index.ts @@ -24,69 +24,6 @@ import webPython from './web-python/index'; import webRemix from './web-remix/index'; const guides: Readonly = Object.freeze([ - { - order: Number.POSITIVE_INFINITY, - id: 'm2m-general', - Logo: lazy(async () => import('./m2m-general/logo.svg')), - Component: lazy(async () => import('./m2m-general/README.mdx')), - metadata: m2mGeneral, - }, - { - order: 1, - id: 'native-ios-swift', - Logo: lazy(async () => import('./native-ios-swift/logo.svg')), - Component: lazy(async () => import('./native-ios-swift/README.mdx')), - metadata: nativeIosSwift, - }, - { - order: 2, - id: 'native-android-java', - Logo: lazy(async () => import('./native-android-java/logo.svg')), - Component: lazy(async () => import('./native-android-java/README.mdx')), - metadata: nativeAndroidJava, - }, - { - order: 2.1, - id: 'native-android-kt', - Logo: lazy(async () => import('./native-android-kt/logo.svg')), - Component: lazy(async () => import('./native-android-kt/README.mdx')), - metadata: nativeAndroidKt, - }, - { - order: 3, - id: 'native-flutter', - Logo: lazy(async () => import('./native-flutter/logo.svg')), - Component: lazy(async () => import('./native-flutter/README.mdx')), - metadata: nativeFlutter, - }, - { - order: 4, - id: 'native-capacitor', - Logo: lazy(async () => import('./native-capacitor/logo.svg')), - Component: lazy(async () => import('./native-capacitor/README.mdx')), - metadata: nativeCapacitor, - }, - { - order: 1, - id: 'spa-react', - Logo: lazy(async () => import('./spa-react/logo.svg')), - Component: lazy(async () => import('./spa-react/README.mdx')), - metadata: spaReact, - }, - { - order: 2, - id: 'spa-vue', - Logo: lazy(async () => import('./spa-vue/logo.svg')), - Component: lazy(async () => import('./spa-vue/README.mdx')), - metadata: spaVue, - }, - { - order: 3, - id: 'spa-vanilla', - Logo: lazy(async () => import('./spa-vanilla/logo.svg')), - Component: lazy(async () => import('./spa-vanilla/README.mdx')), - metadata: spaVanilla, - }, { order: 1, id: 'web-next', @@ -94,6 +31,13 @@ const guides: Readonly = Object.freeze([ Component: lazy(async () => import('./web-next/README.mdx')), metadata: webNext, }, + { + order: 1.1, + id: 'spa-react', + Logo: lazy(async () => import('./spa-react/logo.svg')), + Component: lazy(async () => import('./spa-react/README.mdx')), + metadata: spaReact, + }, { order: 1.1, id: 'web-next-app-router', @@ -102,61 +46,117 @@ const guides: Readonly = Object.freeze([ metadata: webNextAppRouter, }, { - order: 2, + order: 1.2, + id: 'm2m-general', + Logo: lazy(async () => import('./m2m-general/logo.svg')), + Component: lazy(async () => import('./m2m-general/README.mdx')), + metadata: m2mGeneral, + }, + { + order: 1.2, id: 'web-express', Logo: lazy(async () => import('./web-express/logo.svg')), Component: lazy(async () => import('./web-express/README.mdx')), metadata: webExpress, }, { - order: 3, + order: 1.3, id: 'web-go', Logo: lazy(async () => import('./web-go/logo.svg')), Component: lazy(async () => import('./web-go/README.mdx')), metadata: webGo, }, { - order: 4, - id: 'web-python', - Logo: lazy(async () => import('./web-python/logo.svg')), - Component: lazy(async () => import('./web-python/README.mdx')), - metadata: webPython, + order: 1.5, + id: 'web-gpt-plugin', + Logo: lazy(async () => import('./web-gpt-plugin/logo.svg')), + Component: lazy(async () => import('./web-gpt-plugin/README.mdx')), + metadata: webGptPlugin, }, { - order: 5, + order: 1.6, + id: 'spa-vue', + Logo: lazy(async () => import('./spa-vue/logo.svg')), + Component: lazy(async () => import('./spa-vue/README.mdx')), + metadata: spaVue, + }, + { + order: 1.7, + id: 'native-ios-swift', + Logo: lazy(async () => import('./native-ios-swift/logo.svg')), + Component: lazy(async () => import('./native-ios-swift/README.mdx')), + metadata: nativeIosSwift, + }, + { + order: 2, + id: 'native-android-kt', + Logo: lazy(async () => import('./native-android-kt/logo.svg')), + Component: lazy(async () => import('./native-android-kt/README.mdx')), + metadata: nativeAndroidKt, + }, + { + order: 2, + id: 'spa-vanilla', + Logo: lazy(async () => import('./spa-vanilla/logo.svg')), + Component: lazy(async () => import('./spa-vanilla/README.mdx')), + metadata: spaVanilla, + }, + { + order: 2, id: 'web-php', Logo: lazy(async () => import('./web-php/logo.svg')), Component: lazy(async () => import('./web-php/README.mdx')), metadata: webPhp, }, { - order: 6, + order: 3, + id: 'native-android-java', + Logo: lazy(async () => import('./native-android-java/logo.svg')), + Component: lazy(async () => import('./native-android-java/README.mdx')), + metadata: nativeAndroidJava, + }, + { + order: 3, + id: 'web-python', + Logo: lazy(async () => import('./web-python/logo.svg')), + Component: lazy(async () => import('./web-python/README.mdx')), + metadata: webPython, + }, + { + order: 4, + id: 'native-capacitor', + Logo: lazy(async () => import('./native-capacitor/logo.svg')), + Component: lazy(async () => import('./native-capacitor/README.mdx')), + metadata: nativeCapacitor, + }, + { + order: 4, id: 'web-remix', Logo: lazy(async () => import('./web-remix/logo.svg')), Component: lazy(async () => import('./web-remix/README.mdx')), metadata: webRemix, }, { - order: 7, + order: 5, + id: 'native-flutter', + Logo: lazy(async () => import('./native-flutter/logo.svg')), + Component: lazy(async () => import('./native-flutter/README.mdx')), + metadata: nativeFlutter, + }, + { + order: 5, id: 'web-asp-net-core', Logo: lazy(async () => import('./web-asp-net-core/logo.svg')), Component: lazy(async () => import('./web-asp-net-core/README.mdx')), metadata: webAspNetCore, }, { - order: 8, + order: 6, id: 'web-outline', Logo: lazy(async () => import('./web-outline/logo.svg')), Component: lazy(async () => import('./web-outline/README.mdx')), metadata: webOutline, }, - { - order: 9, - id: 'web-gpt-plugin', - Logo: lazy(async () => import('./web-gpt-plugin/logo.svg')), - Component: lazy(async () => import('./web-gpt-plugin/README.mdx')), - metadata: webGptPlugin, - }, ]); export default guides; diff --git a/packages/console/src/assets/docs/guides/m2m-general/config.json b/packages/console/src/assets/docs/guides/m2m-general/config.json new file mode 100644 index 000000000..5a58e475d --- /dev/null +++ b/packages/console/src/assets/docs/guides/m2m-general/config.json @@ -0,0 +1,3 @@ +{ + "order": 1.2 +} \ No newline at end of file diff --git a/packages/console/src/assets/docs/guides/native-android-java/config.json b/packages/console/src/assets/docs/guides/native-android-java/config.json index c435a12c1..504132be0 100644 --- a/packages/console/src/assets/docs/guides/native-android-java/config.json +++ b/packages/console/src/assets/docs/guides/native-android-java/config.json @@ -1,3 +1,3 @@ { - "order": 2 + "order": 3 } diff --git a/packages/console/src/assets/docs/guides/native-android-kt/config.json b/packages/console/src/assets/docs/guides/native-android-kt/config.json index f00075c68..c435a12c1 100644 --- a/packages/console/src/assets/docs/guides/native-android-kt/config.json +++ b/packages/console/src/assets/docs/guides/native-android-kt/config.json @@ -1,3 +1,3 @@ { - "order": 2.1 + "order": 2 } diff --git a/packages/console/src/assets/docs/guides/native-flutter/config.json b/packages/console/src/assets/docs/guides/native-flutter/config.json index 504132be0..7b21bc7d1 100644 --- a/packages/console/src/assets/docs/guides/native-flutter/config.json +++ b/packages/console/src/assets/docs/guides/native-flutter/config.json @@ -1,3 +1,3 @@ { - "order": 3 + "order": 5 } diff --git a/packages/console/src/assets/docs/guides/native-ios-swift/config.json b/packages/console/src/assets/docs/guides/native-ios-swift/config.json index 66ec4e0b1..d01c4333d 100644 --- a/packages/console/src/assets/docs/guides/native-ios-swift/config.json +++ b/packages/console/src/assets/docs/guides/native-ios-swift/config.json @@ -1,3 +1,3 @@ { - "order": 1 + "order": 1.7 } diff --git a/packages/console/src/assets/docs/guides/spa-react/config.json b/packages/console/src/assets/docs/guides/spa-react/config.json index 66ec4e0b1..4721ad2f7 100644 --- a/packages/console/src/assets/docs/guides/spa-react/config.json +++ b/packages/console/src/assets/docs/guides/spa-react/config.json @@ -1,3 +1,3 @@ { - "order": 1 + "order": 1.1 } diff --git a/packages/console/src/assets/docs/guides/spa-vanilla/config.json b/packages/console/src/assets/docs/guides/spa-vanilla/config.json index 504132be0..c435a12c1 100644 --- a/packages/console/src/assets/docs/guides/spa-vanilla/config.json +++ b/packages/console/src/assets/docs/guides/spa-vanilla/config.json @@ -1,3 +1,3 @@ { - "order": 3 + "order": 2 } diff --git a/packages/console/src/assets/docs/guides/spa-vue/config.json b/packages/console/src/assets/docs/guides/spa-vue/config.json index c435a12c1..ca3d65f23 100644 --- a/packages/console/src/assets/docs/guides/spa-vue/config.json +++ b/packages/console/src/assets/docs/guides/spa-vue/config.json @@ -1,3 +1,3 @@ { - "order": 2 + "order": 1.6 } diff --git a/packages/console/src/assets/docs/guides/web-asp-net-core/config.json b/packages/console/src/assets/docs/guides/web-asp-net-core/config.json index 88177e1ec..7b21bc7d1 100644 --- a/packages/console/src/assets/docs/guides/web-asp-net-core/config.json +++ b/packages/console/src/assets/docs/guides/web-asp-net-core/config.json @@ -1,3 +1,3 @@ { - "order": 7 + "order": 5 } diff --git a/packages/console/src/assets/docs/guides/web-express/config.json b/packages/console/src/assets/docs/guides/web-express/config.json index c435a12c1..228d0926b 100644 --- a/packages/console/src/assets/docs/guides/web-express/config.json +++ b/packages/console/src/assets/docs/guides/web-express/config.json @@ -1,3 +1,3 @@ { - "order": 2 + "order": 1.2 } diff --git a/packages/console/src/assets/docs/guides/web-go/config.json b/packages/console/src/assets/docs/guides/web-go/config.json index 504132be0..dfe20f04f 100644 --- a/packages/console/src/assets/docs/guides/web-go/config.json +++ b/packages/console/src/assets/docs/guides/web-go/config.json @@ -1,3 +1,3 @@ { - "order": 3 + "order": 1.3 } diff --git a/packages/console/src/assets/docs/guides/web-gpt-plugin/config.json b/packages/console/src/assets/docs/guides/web-gpt-plugin/config.json index f24b1f1d7..cd39155a7 100644 --- a/packages/console/src/assets/docs/guides/web-gpt-plugin/config.json +++ b/packages/console/src/assets/docs/guides/web-gpt-plugin/config.json @@ -1,3 +1,3 @@ { - "order": 9 + "order": 1.5 } diff --git a/packages/console/src/assets/docs/guides/web-java/config.json b/packages/console/src/assets/docs/guides/web-java/config.json new file mode 100644 index 000000000..ca9f59b2e --- /dev/null +++ b/packages/console/src/assets/docs/guides/web-java/config.json @@ -0,0 +1,3 @@ +{ + "order": 1.4 +} \ No newline at end of file diff --git a/packages/console/src/assets/docs/guides/web-outline/config.json b/packages/console/src/assets/docs/guides/web-outline/config.json index c76b77fa3..61a30569e 100644 --- a/packages/console/src/assets/docs/guides/web-outline/config.json +++ b/packages/console/src/assets/docs/guides/web-outline/config.json @@ -1,3 +1,3 @@ { - "order": 8 + "order": 6 } diff --git a/packages/console/src/assets/docs/guides/web-php/config.json b/packages/console/src/assets/docs/guides/web-php/config.json index 7b21bc7d1..c435a12c1 100644 --- a/packages/console/src/assets/docs/guides/web-php/config.json +++ b/packages/console/src/assets/docs/guides/web-php/config.json @@ -1,3 +1,3 @@ { - "order": 5 + "order": 2 } diff --git a/packages/console/src/assets/docs/guides/web-python/config.json b/packages/console/src/assets/docs/guides/web-python/config.json index 8ab102f71..504132be0 100644 --- a/packages/console/src/assets/docs/guides/web-python/config.json +++ b/packages/console/src/assets/docs/guides/web-python/config.json @@ -1,3 +1,3 @@ { - "order": 4 + "order": 3 } diff --git a/packages/console/src/assets/docs/guides/web-remix/config.json b/packages/console/src/assets/docs/guides/web-remix/config.json index 61a30569e..8ab102f71 100644 --- a/packages/console/src/assets/docs/guides/web-remix/config.json +++ b/packages/console/src/assets/docs/guides/web-remix/config.json @@ -1,3 +1,3 @@ { - "order": 6 + "order": 4 } From 5e8b1bd598e0bf32e8abc932b7952282c8125bb4 Mon Sep 17 00:00:00 2001 From: simeng-li Date: Wed, 6 Sep 2023 09:55:22 +0800 Subject: [PATCH 17/30] fix(core,ui): clear ui preload link not used warning (#4429) fix(core,ui): address ui preload link not used warning address ui preload link not used warning --- .../src/middleware/koa-security-headers.ts | 8 +++++++- packages/ui/src/index.html | 20 +++++++++++++++++-- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/packages/core/src/middleware/koa-security-headers.ts b/packages/core/src/middleware/koa-security-headers.ts index 03c95b754..3cb494feb 100644 --- a/packages/core/src/middleware/koa-security-headers.ts +++ b/packages/core/src/middleware/koa-security-headers.ts @@ -106,7 +106,13 @@ export default function koaSecurityHeaders( "'self'", ...conditionalArray(!isProduction && ["'unsafe-eval'", "'unsafe-inline'"]), ], - connectSrc: ["'self'", ...adminOrigins, ...coreOrigins, ...developmentOrigins], + connectSrc: [ + "'self'", + ...adminOrigins, + ...coreOrigins, + ...developmentOrigins, + ...appInsightsOrigins, + ], // Allow Main Flow origin loaded in preview iframe frameSrc: ["'self'", ...adminOrigins, ...coreOrigins], }, diff --git a/packages/ui/src/index.html b/packages/ui/src/index.html index 8df3f2db5..7cdd85d13 100644 --- a/packages/ui/src/index.html +++ b/packages/ui/src/index.html @@ -5,8 +5,24 @@ - - + + From 54321d93a207aa76bf0db7015ef6d0559a77c81f Mon Sep 17 00:00:00 2001 From: Charles Zhao Date: Wed, 6 Sep 2023 10:10:35 +0800 Subject: [PATCH 18/30] refactor(console): add missing section title and adjust card layout in get-started page (#4428) --- .../console/src/hooks/use-window-resize.ts | 27 ++++++++++ .../components/GuideGroup/index.tsx | 17 +++---- .../console/src/pages/GetStarted/index.tsx | 51 ++++++++++++------- 3 files changed, 67 insertions(+), 28 deletions(-) create mode 100644 packages/console/src/hooks/use-window-resize.ts diff --git a/packages/console/src/hooks/use-window-resize.ts b/packages/console/src/hooks/use-window-resize.ts new file mode 100644 index 000000000..c4160667a --- /dev/null +++ b/packages/console/src/hooks/use-window-resize.ts @@ -0,0 +1,27 @@ +import { useEffect, useLayoutEffect, useRef } from 'react'; + +type Callback = (event?: UIEvent) => void; + +const useWindowResize = (callback: Callback) => { + const callbackRef = useRef(callback); + + useEffect(() => { + // eslint-disable-next-line @silverhand/fp/no-mutation + callbackRef.current = callback; + }, [callback]); + + useLayoutEffect(() => { + const handler: Callback = (event) => { + callbackRef.current(event); + }; + + handler(); + window.addEventListener('resize', handler); + + return () => { + window.removeEventListener('resize', handler); + }; + }, []); +}; + +export default useWindowResize; diff --git a/packages/console/src/pages/Applications/components/GuideGroup/index.tsx b/packages/console/src/pages/Applications/components/GuideGroup/index.tsx index 0888da16d..d72138a7c 100644 --- a/packages/console/src/pages/Applications/components/GuideGroup/index.tsx +++ b/packages/console/src/pages/Applications/components/GuideGroup/index.tsx @@ -1,4 +1,5 @@ import classNames from 'classnames'; +import { type Ref, forwardRef } from 'react'; import { type Guide } from '@/assets/docs/guides/types'; @@ -15,20 +16,16 @@ type GuideGroupProps = { onClickGuide: (data: SelectedGuide) => void; }; -function GuideGroup({ - className, - categoryName, - guides, - hasCardBorder, - isCompact, - onClickGuide, -}: GuideGroupProps) { +function GuideGroup( + { className, categoryName, guides, hasCardBorder, isCompact, onClickGuide }: GuideGroupProps, + ref: Ref +) { if (!guides?.length) { return null; } return ( -
+
{categoryName && }
{guides.map((guide) => ( @@ -45,4 +42,4 @@ function GuideGroup({ ); } -export default GuideGroup; +export default forwardRef(GuideGroup); diff --git a/packages/console/src/pages/GetStarted/index.tsx b/packages/console/src/pages/GetStarted/index.tsx index 752694db8..6dce2a2f5 100644 --- a/packages/console/src/pages/GetStarted/index.tsx +++ b/packages/console/src/pages/GetStarted/index.tsx @@ -1,7 +1,7 @@ import { withAppInsights } from '@logto/app-insights/react'; import { Theme, type Application } from '@logto/schemas'; import classNames from 'classnames'; -import { useCallback, useContext, useMemo, useState } from 'react'; +import { useCallback, useContext, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import CheckPreviewDark from '@/assets/icons/check-demo-dark.svg'; @@ -19,9 +19,11 @@ import Spacer from '@/ds-components/Spacer'; import TextLink from '@/ds-components/TextLink'; import useTenantPathname from '@/hooks/use-tenant-pathname'; import useTheme from '@/hooks/use-theme'; +import useWindowResize from '@/hooks/use-window-resize'; import CreateForm from '../Applications/components/CreateForm'; -import GuideCard, { type SelectedGuide } from '../Applications/components/GuideCard'; +import { type SelectedGuide } from '../Applications/components/GuideCard'; +import GuideGroup from '../Applications/components/GuideGroup'; import useAppGuideMetadata from '../Applications/components/GuideLibrary/hook'; import FreePlanNotification from './FreePlanNotification'; @@ -39,16 +41,26 @@ function GetStarted() { const [selectedGuide, setSelectedGuide] = useState(); const [_, getStructuredMetadata] = useAppGuideMetadata(); const [showCreateForm, setShowCreateForm] = useState(false); + // The number of visible guide cards to show in one row per the current screen width + const [visibleCardCount, setVisibleCardCount] = useState(4); + const containerRef = useRef(null); const theme = useTheme(); const { PreviewIcon, SocialIcon, RbacIcon } = icons[theme]; + useWindowResize(() => { + const containerWidth = containerRef.current?.clientWidth ?? 0; + + // Responsive breakpoints (1080, 680px) are defined in `GuideGroup` component SCSS, + // and we need to keep them consistent. + setVisibleCardCount(containerWidth > 1080 ? 4 : containerWidth > 680 ? 3 : 2); + }); + /** - * Only need 4 featured guides at most, since by design in get-started page we need to show - * a few most popular SDK guides, and 4 makes it easy to have a 4 x 1 or 2 x 2 card layout. + * Slice the guide metadata as we only need to show 1 row of guide cards in get-started page */ const featuredAppGuides = useMemo( - () => getStructuredMetadata().featured.slice(0, 4), - [getStructuredMetadata] + () => getStructuredMetadata().featured.slice(0, visibleCardCount), + [visibleCardCount, getStructuredMetadata] ); const onClickGuide = useCallback((data: SelectedGuide) => { @@ -78,21 +90,23 @@ function GetStarted() {
{t('get_started.develop.title')}
-
- {featuredAppGuides.map((guide) => ( - - ))} - {selectedGuide?.target !== 'API' && showCreateForm && ( - - )} -
+ + {selectedGuide?.target !== 'API' && showCreateForm && ( + + )} {t('get_started.view_all')}
+
{t('get_started.customize.title')}
@@ -134,6 +148,7 @@ function GetStarted() {
+
{t('get_started.manage.title')}
From b3fc33524e7d06dd9716aab6b673a58c544f8afd Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Wed, 6 Sep 2023 10:21:36 +0800 Subject: [PATCH 19/30] refactor(core): remove response data construction helper from libs (#4437) --- packages/core/src/__mocks__/index.ts | 18 -------- .../core/src/libraries/hook/index.test.ts | 10 +---- packages/core/src/libraries/hook/index.ts | 10 +---- packages/core/src/libraries/resource.test.ts | 44 ------------------- packages/core/src/libraries/resource.ts | 21 --------- packages/core/src/routes/hook.test.ts | 13 +----- packages/core/src/routes/hook.ts | 11 ++++- packages/core/src/routes/resource.test.ts | 15 +------ packages/core/src/routes/resource.ts | 13 ++++-- packages/core/src/tenants/Libraries.ts | 2 - packages/core/src/utils/resource.ts | 17 +++++++ 11 files changed, 41 insertions(+), 133 deletions(-) delete mode 100644 packages/core/src/libraries/resource.test.ts delete mode 100644 packages/core/src/libraries/resource.ts create mode 100644 packages/core/src/utils/resource.ts diff --git a/packages/core/src/__mocks__/index.ts b/packages/core/src/__mocks__/index.ts index 384e2cf71..7e2a3b4ae 100644 --- a/packages/core/src/__mocks__/index.ts +++ b/packages/core/src/__mocks__/index.ts @@ -44,24 +44,6 @@ export const mockResource: Resource = { isDefault: false, }; -export const mockResource2: Resource = { - tenantId: 'fake_tenant', - id: 'logto_api2', - name: 'management api', - indicator: 'logto.dev/api', - accessTokenTtl: 3600, - isDefault: false, -}; - -export const mockResource3: Resource = { - tenantId: 'fake_tenant', - id: 'logto_api3', - name: 'management api', - indicator: 'logto.dev/api', - accessTokenTtl: 3600, - isDefault: false, -}; - export const mockScope: Scope = { tenantId: 'fake_tenant', id: 'scope_id', diff --git a/packages/core/src/libraries/hook/index.test.ts b/packages/core/src/libraries/hook/index.test.ts index b6222bad5..720ac560a 100644 --- a/packages/core/src/libraries/hook/index.test.ts +++ b/packages/core/src/libraries/hook/index.test.ts @@ -2,7 +2,6 @@ import type { Hook } from '@logto/schemas'; import { HookEvent, InteractionEvent, LogResult } from '@logto/schemas'; import { createMockUtils } from '@logto/shared/esm'; -import { mockHook } from '#src/__mocks__/hook.js'; import RequestError from '#src/errors/RequestError/index.js'; import { generateHookTestPayload, parseResponse } from './utils.js'; @@ -49,7 +48,7 @@ const findAllHooks = jest.fn().mockResolvedValue([hook]); const findHookById = jest.fn().mockResolvedValue(hook); const { createHookLibrary } = await import('./index.js'); -const { triggerInteractionHooks, attachExecutionStatsToHook, testHook } = createHookLibrary( +const { triggerInteractionHooks, testHook } = createHookLibrary( new MockQueries({ users: { findUserById: jest.fn().mockReturnValue({ @@ -113,13 +112,6 @@ describe('triggerInteractionHooks()', () => { }); }); -describe('attachExecutionStatsToHook', () => { - it('should attach execution stats to a hook', async () => { - const result = await attachExecutionStatsToHook(mockHook); - expect(result).toEqual({ ...mockHook, executionStats: mockHookState }); - }); -}); - describe('testHook', () => { afterEach(() => { jest.clearAllMocks(); diff --git a/packages/core/src/libraries/hook/index.ts b/packages/core/src/libraries/hook/index.ts index 812e2b053..873380a88 100644 --- a/packages/core/src/libraries/hook/index.ts +++ b/packages/core/src/libraries/hook/index.ts @@ -4,8 +4,6 @@ import { InteractionEvent, LogResult, userInfoSelectFields, - type Hook, - type HookResponse, type HookConfig, } from '@logto/schemas'; import { generateStandardId } from '@logto/shared'; @@ -48,7 +46,7 @@ const eventToHook: Record = { export const createHookLibrary = (queries: Queries) => { const { applications: { findApplicationById }, - logs: { insertLog, getHookExecutionStatsByHookId }, + logs: { insertLog }, // TODO: @gao should we use the library function thus we can pass full userinfo to the payload? users: { findUserById }, hooks: { findAllHooks, findHookById }, @@ -159,14 +157,8 @@ export const createHookLibrary = (queries: Queries) => { } }; - const attachExecutionStatsToHook = async (hook: Hook): Promise => ({ - ...hook, - executionStats: await getHookExecutionStatsByHookId(hook.id), - }); - return { triggerInteractionHooks, - attachExecutionStatsToHook, testHook, }; }; diff --git a/packages/core/src/libraries/resource.test.ts b/packages/core/src/libraries/resource.test.ts deleted file mode 100644 index 3a1ab1925..000000000 --- a/packages/core/src/libraries/resource.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { mockResource, mockResource2, mockResource3, mockScope } from '#src/__mocks__/index.js'; -import { MockQueries } from '#src/test-utils/tenant.js'; - -const { jest } = import.meta; - -const findScopesByResourceIds = jest.fn(async () => [ - { ...mockScope, id: '1', resourceId: mockResource.id }, - { ...mockScope, id: '2', resourceId: mockResource.id }, - { ...mockScope, id: '3', resourceId: mockResource2.id }, -]); - -const { createResourceLibrary } = await import('./resource.js'); - -const { attachScopesToResources } = createResourceLibrary( - new MockQueries({ scopes: { findScopesByResourceIds } }) -); - -describe('attachScopesToResources', () => { - beforeEach(() => { - findScopesByResourceIds.mockClear(); - }); - - it('should find and attach scopes to each resource', async () => { - await expect( - attachScopesToResources([mockResource, mockResource2, mockResource3]) - ).resolves.toEqual([ - { - ...mockResource, - scopes: [ - { ...mockScope, id: '1', resourceId: mockResource.id }, - { ...mockScope, id: '2', resourceId: mockResource.id }, - ], - }, - { - ...mockResource2, - scopes: [{ ...mockScope, id: '3', resourceId: mockResource2.id }], - }, - { - ...mockResource3, - scopes: [], - }, - ]); - }); -}); diff --git a/packages/core/src/libraries/resource.ts b/packages/core/src/libraries/resource.ts deleted file mode 100644 index bcd93f8dd..000000000 --- a/packages/core/src/libraries/resource.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { Resource, ResourceResponse } from '@logto/schemas'; - -import type Queries from '#src/tenants/Queries.js'; - -export const createResourceLibrary = (queries: Queries) => { - const { findScopesByResourceIds } = queries.scopes; - - const attachScopesToResources = async ( - resources: readonly Resource[] - ): Promise => { - const resourceIds = resources.map(({ id }) => id); - const scopes = await findScopesByResourceIds(resourceIds); - - return resources.map((resource) => ({ - ...resource, - scopes: scopes.filter(({ resourceId }) => resourceId === resource.id), - })); - }; - - return { attachScopesToResources }; -}; diff --git a/packages/core/src/routes/hook.test.ts b/packages/core/src/routes/hook.test.ts index 079c4ad89..765620bfa 100644 --- a/packages/core/src/routes/hook.test.ts +++ b/packages/core/src/routes/hook.test.ts @@ -66,6 +66,7 @@ const logs = { count: 1, }), findLogs: jest.fn().mockResolvedValue([mockLog]), + getHookExecutionStatsByHookId: jest.fn().mockResolvedValue(mockExecutionStats), }; const { countLogs, findLogs } = logs; @@ -75,18 +76,10 @@ const mockQueries = { logs, }; -const attachExecutionStatsToHook = jest.fn().mockImplementation((hook) => ({ - ...hook, - executionStats: mockExecutionStats, -})); - const testHook = jest.fn(); const mockLibraries = { - hooks: { - attachExecutionStatsToHook, - testHook, - }, + hooks: { testHook }, quota: createMockQuotaLibrary(), }; @@ -117,7 +110,6 @@ describe('hook routes', () => { it('GET /hooks?includeExecutionStats', async () => { const response = await hookRequest.get('/hooks?includeExecutionStats=true'); - expect(attachExecutionStatsToHook).toHaveBeenCalledTimes(mockHookList.length); expect(response.body).toEqual( mockHookList.map((hook) => ({ ...hook, @@ -136,7 +128,6 @@ describe('hook routes', () => { it('GET /hooks/:id?includeExecutionStats', async () => { const hookIdInMockList = mockHookList[0]?.id ?? ''; const response = await hookRequest.get(`/hooks/${hookIdInMockList}?includeExecutionStats=true`); - expect(attachExecutionStatsToHook).toHaveBeenCalledWith(mockHookList[0]); expect(response.body).toEqual({ ...mockHookList[0], executionStats: mockExecutionStats, diff --git a/packages/core/src/routes/hook.ts b/packages/core/src/routes/hook.ts index 399597a40..6879d0e4c 100644 --- a/packages/core/src/routes/hook.ts +++ b/packages/core/src/routes/hook.ts @@ -5,6 +5,8 @@ import { hookEventsGuard, hookResponseGuard, hook, + type HookResponse, + type Hook, } from '@logto/schemas'; import { generateStandardId } from '@logto/shared'; import { conditional, deduplicate, yes } from '@silverhand/essentials'; @@ -36,14 +38,19 @@ export default function hookRoutes( updateHookById, deleteHookById, }, - logs: { countLogs, findLogs }, + logs: { countLogs, findLogs, getHookExecutionStatsByHookId }, } = queries; const { - hooks: { attachExecutionStatsToHook, testHook }, + hooks: { testHook }, quota, } = libraries; + const attachExecutionStatsToHook = async (hook: Hook): Promise => ({ + ...hook, + executionStats: await getHookExecutionStatsByHookId(hook.id), + }); + router.get( '/hooks', koaPagination({ isOptional: true }), diff --git a/packages/core/src/routes/resource.test.ts b/packages/core/src/routes/resource.test.ts index e790a7a59..10c9ec39b 100644 --- a/packages/core/src/routes/resource.test.ts +++ b/packages/core/src/routes/resource.test.ts @@ -3,7 +3,6 @@ import { pickDefault, createMockUtils } from '@logto/shared/esm'; import { type Nullable } from '@silverhand/essentials'; import { mockResource, mockScope } from '#src/__mocks__/index.js'; -import { createMockQuotaLibrary } from '#src/test-utils/quota.js'; import { MockTenant } from '#src/test-utils/tenant.js'; import { createRequester } from '#src/utils/test-utils.js'; @@ -38,6 +37,7 @@ const scopes = { findScopesByResourceId: async () => [mockScope], searchScopesByResourceId: async () => [mockScope], countScopesByResourceId: async () => ({ count: 1 }), + findScopesByResourceIds: async () => [], insertScope: jest.fn(async () => mockScope), updateScopeById: jest.fn(async () => mockScope), deleteScopeById: jest.fn(), @@ -45,23 +45,12 @@ const scopes = { }; const { insertScope, updateScopeById } = scopes; -const libraries = { - resources: { - attachScopesToResources: async (resources: readonly Resource[]) => - resources.map((resource) => ({ - ...resource, - scopes: [], - })), - }, - quota: createMockQuotaLibrary(), -}; - mockEsm('@logto/shared', () => ({ // eslint-disable-next-line unicorn/consistent-function-scoping buildIdGenerator: () => () => 'randomId', })); -const tenantContext = new MockTenant(undefined, { scopes, resources }, undefined, libraries); +const tenantContext = new MockTenant(undefined, { scopes, resources }, undefined); const resourceRoutes = await pickDefault(import('./resource.js')); diff --git a/packages/core/src/routes/resource.ts b/packages/core/src/routes/resource.ts index 3695164f3..4033d83d2 100644 --- a/packages/core/src/routes/resource.ts +++ b/packages/core/src/routes/resource.ts @@ -8,6 +8,7 @@ import koaGuard from '#src/middleware/koa-guard.js'; import koaPagination from '#src/middleware/koa-pagination.js'; import koaQuotaGuard from '#src/middleware/koa-quota-guard.js'; import assertThat from '#src/utils/assert-that.js'; +import { attachScopesToResources } from '#src/utils/resource.js'; import { parseSearchParamsForSearch } from '#src/utils/search.js'; import type { AuthedRouter, RouterInitArgs } from './types.js'; @@ -20,7 +21,7 @@ export default function resourceRoutes( router, { queries, - libraries: { quota, resources }, + libraries: { quota }, }, ]: RouterInitArgs ) { @@ -35,6 +36,7 @@ export default function resourceRoutes( updateResourceById, deleteResourceById, }, + scopes: scopeQueries, scopes: { countScopesByResourceId, deleteScopeById, @@ -44,7 +46,6 @@ export default function resourceRoutes( updateScopeById, }, } = queries; - const { attachScopesToResources } = resources; router.get( '/resources', @@ -64,7 +65,9 @@ export default function resourceRoutes( if (disabled) { const resources = await findAllResources(); - ctx.body = yes(includeScopes) ? await attachScopesToResources(resources) : resources; + ctx.body = yes(includeScopes) + ? await attachScopesToResources(resources, scopeQueries) + : resources; return next(); } @@ -75,7 +78,9 @@ export default function resourceRoutes( ]); ctx.pagination.totalCount = count; - ctx.body = yes(includeScopes) ? await attachScopesToResources(resources) : resources; + ctx.body = yes(includeScopes) + ? await attachScopesToResources(resources, scopeQueries) + : resources; return next(); } diff --git a/packages/core/src/tenants/Libraries.ts b/packages/core/src/tenants/Libraries.ts index 0de85ee4d..8cc720898 100644 --- a/packages/core/src/tenants/Libraries.ts +++ b/packages/core/src/tenants/Libraries.ts @@ -6,7 +6,6 @@ import { createHookLibrary } from '#src/libraries/hook/index.js'; import { createPasscodeLibrary } from '#src/libraries/passcode.js'; import { createPhraseLibrary } from '#src/libraries/phrase.js'; import { createQuotaLibrary } from '#src/libraries/quota.js'; -import { createResourceLibrary } from '#src/libraries/resource.js'; import { createSignInExperienceLibrary } from '#src/libraries/sign-in-experience/index.js'; import { createSocialLibrary } from '#src/libraries/social.js'; import { createUserLibrary } from '#src/libraries/user.js'; @@ -18,7 +17,6 @@ export default class Libraries { users = createUserLibrary(this.queries); signInExperiences = createSignInExperienceLibrary(this.queries, this.connectors); phrases = createPhraseLibrary(this.queries); - resources = createResourceLibrary(this.queries); hooks = createHookLibrary(this.queries); socials = createSocialLibrary(this.queries, this.connectors); passcodes = createPasscodeLibrary(this.queries, this.connectors); diff --git a/packages/core/src/utils/resource.ts b/packages/core/src/utils/resource.ts new file mode 100644 index 000000000..25dc3e86d --- /dev/null +++ b/packages/core/src/utils/resource.ts @@ -0,0 +1,17 @@ +import { type Resource, type ResourceResponse } from '@logto/schemas'; + +import type Queries from '#src/tenants/Queries.js'; + +export const attachScopesToResources = async ( + resources: readonly Resource[], + scopeQueries: Queries['scopes'] +): Promise => { + const { findScopesByResourceIds } = scopeQueries; + const resourceIds = resources.map(({ id }) => id); + const scopes = await findScopesByResourceIds(resourceIds); + + return resources.map((resource) => ({ + ...resource, + scopes: scopes.filter(({ resourceId }) => resourceId === resource.id), + })); +}; From 1453e1a2a1f176f707385e9138b1027b1b3079d0 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Wed, 6 Sep 2023 10:22:10 +0800 Subject: [PATCH 20/30] refactor(console): retrieve subscription data from tenant response (#4430) refactor(console): apply tenant subscription data from tenant response --- packages/console/package.json | 4 +- .../src/components/MauExceededModal/index.tsx | 33 +++-------- .../components/PaymentOverdueModal/index.tsx | 29 ++++------ .../TenantDropdownItem/TenantStatusTag.tsx | 57 ++++++------------- .../TenantDropdownItem/index.tsx | 27 +++++++-- .../console/src/contexts/TenantsProvider.tsx | 33 +++++++---- .../PaymentOverdueNotification/index.tsx | 23 ++++---- packages/console/src/types/subscriptions.ts | 4 +- packages/console/src/utils/subscription.ts | 10 ---- pnpm-lock.yaml | 34 +++-------- 10 files changed, 102 insertions(+), 152 deletions(-) diff --git a/packages/console/package.json b/packages/console/package.json index d9a20851c..b96b7aa28 100644 --- a/packages/console/package.json +++ b/packages/console/package.json @@ -26,7 +26,7 @@ "@fontsource/roboto-mono": "^5.0.0", "@jest/types": "^29.5.0", "@logto/app-insights": "workspace:^1.3.1", - "@logto/cloud": "0.2.5-1795c3d", + "@logto/cloud": "0.2.5-71b7fea", "@logto/connector-kit": "workspace:^1.1.1", "@logto/core-kit": "workspace:^2.0.1", "@logto/language-kit": "workspace:^1.0.0", @@ -60,7 +60,7 @@ "@types/react-helmet": "^6.1.6", "@types/react-modal": "^3.13.1", "@types/react-syntax-highlighter": "^15.5.1", - "@withtyped/client": "^0.7.21", + "@withtyped/client": "^0.7.22", "buffer": "^5.7.1", "classnames": "^2.3.1", "clean-deep": "^3.4.0", diff --git a/packages/console/src/components/MauExceededModal/index.tsx b/packages/console/src/components/MauExceededModal/index.tsx index f2884dd90..3b0fbc705 100644 --- a/packages/console/src/components/MauExceededModal/index.tsx +++ b/packages/console/src/components/MauExceededModal/index.tsx @@ -1,9 +1,7 @@ import { useContext, useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import ReactModal from 'react-modal'; -import useSWRImmutable from 'swr/immutable'; -import { useCloudApi } from '@/cloud/hooks/use-cloud-api'; import PlanUsage from '@/components/PlanUsage'; import { contactEmailLink } from '@/consts'; import { subscriptionPage } from '@/consts/pages'; @@ -21,35 +19,22 @@ import PlanName from '../PlanName'; import * as styles from './index.module.scss'; function MauExceededModal() { - const { currentTenantId } = useContext(TenantsContext); - const cloudApi = useCloudApi(); + const { currentTenant } = useContext(TenantsContext); + const { usage, subscription } = currentTenant ?? {}; + const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const { navigate } = useTenantPathname(); - const [hasClosed, setHasClosed] = useState(false); + const [hasClosed, setHasClosed] = useState(false); const handleCloseModal = () => { setHasClosed(true); }; const { data: subscriptionPlans } = useSubscriptionPlans(); - const { data: currentSubscription } = useSWRImmutable( - `/api/tenants/${currentTenantId}/subscription`, - async () => - cloudApi.get('/api/tenants/:tenantId/subscription', { params: { tenantId: currentTenantId } }) - ); + const currentPlan = subscriptionPlans?.find((plan) => plan.id === subscription?.planId); - const { data: currentUsage } = useSWRImmutable( - `/api/tenants/${currentTenantId}/usage`, - async () => - cloudApi.get('/api/tenants/:tenantId/usage', { params: { tenantId: currentTenantId } }) - ); - - const currentPlan = - currentSubscription && - subscriptionPlans?.find((plan) => plan.id === currentSubscription.planId); - - if (!currentPlan || !currentUsage || hasClosed) { + if (!subscription || !usage || !currentPlan || hasClosed) { return null; } @@ -58,7 +43,7 @@ function MauExceededModal() { name: planName, } = currentPlan; - const isMauExceeded = mauLimit !== null && currentUsage.activeUsers >= mauLimit; + const isMauExceeded = mauLimit !== null && usage.activeUsers >= mauLimit; if (!isMauExceeded) { return null; @@ -102,8 +87,8 @@ function MauExceededModal() { diff --git a/packages/console/src/components/PaymentOverdueModal/index.tsx b/packages/console/src/components/PaymentOverdueModal/index.tsx index f4f5762fc..968ef6fe7 100644 --- a/packages/console/src/components/PaymentOverdueModal/index.tsx +++ b/packages/console/src/components/PaymentOverdueModal/index.tsx @@ -1,11 +1,9 @@ -import { conditional } from '@silverhand/essentials'; -import { useContext, useMemo, useState } from 'react'; +import { useContext, useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import ReactModal from 'react-modal'; -import useSWRImmutable from 'swr/immutable'; -import { useCloudApi } from '@/cloud/hooks/use-cloud-api'; import { contactEmailLink } from '@/consts'; +import { isCloud } from '@/consts/env'; import { TenantsContext } from '@/contexts/TenantsProvider'; import Button from '@/ds-components/Button'; import FormField from '@/ds-components/FormField'; @@ -13,7 +11,6 @@ import InlineNotification from '@/ds-components/InlineNotification'; import ModalLayout from '@/ds-components/ModalLayout'; import useSubscribe from '@/hooks/use-subscribe'; import * as modalStyles from '@/scss/modal.module.scss'; -import { getLatestUnpaidInvoice } from '@/utils/subscription'; import BillInfo from '../BillInfo'; @@ -22,26 +19,17 @@ import * as styles from './index.module.scss'; function PaymentOverdueModal() { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const { currentTenant, currentTenantId } = useContext(TenantsContext); - const cloudApi = useCloudApi(); - const { data: invoicesResponse } = useSWRImmutable( - `/api/tenants/${currentTenantId}/invoices`, - async () => - cloudApi.get('/api/tenants/:tenantId/invoices', { params: { tenantId: currentTenantId } }) - ); + const { openInvoices = [] } = currentTenant ?? {}; + const { visitManagePaymentPage } = useSubscribe(); const [isActionLoading, setIsActionLoading] = useState(false); - const latestUnpaidInvoice = useMemo( - () => conditional(invoicesResponse && getLatestUnpaidInvoice(invoicesResponse.invoices)), - [invoicesResponse] - ); - const [hasClosed, setHasClosed] = useState(false); const handleCloseModal = () => { setHasClosed(true); }; - if (!invoicesResponse || !latestUnpaidInvoice || hasClosed) { + if (!isCloud || openInvoices.length === 0 || hasClosed) { return null; } @@ -82,7 +70,12 @@ function PaymentOverdueModal() { )} - + total + currentInvoice.amountDue, + 0 + )} + /> diff --git a/packages/console/src/containers/AppContent/components/Topbar/TenantSelector/TenantDropdownItem/TenantStatusTag.tsx b/packages/console/src/containers/AppContent/components/Topbar/TenantSelector/TenantDropdownItem/TenantStatusTag.tsx index 3f5f5be0d..9aab7c5ab 100644 --- a/packages/console/src/containers/AppContent/components/Topbar/TenantSelector/TenantDropdownItem/TenantStatusTag.tsx +++ b/packages/console/src/containers/AppContent/components/Topbar/TenantSelector/TenantDropdownItem/TenantStatusTag.tsx @@ -1,35 +1,16 @@ -import { conditional } from '@silverhand/essentials'; -import { useMemo } from 'react'; - +import { type TenantResponse } from '@/cloud/types/router'; import DynamicT from '@/ds-components/DynamicT'; import Tag from '@/ds-components/Tag'; -import useInvoices from '@/hooks/use-invoices'; -import useSubscriptionPlan from '@/hooks/use-subscription-plan'; -import useSubscriptionUsage from '@/hooks/use-subscription-usage'; -import { getLatestUnpaidInvoice } from '@/utils/subscription'; +import { type SubscriptionPlan } from '@/types/subscriptions'; type Props = { - tenantId: string; + tenantData: TenantResponse; + tenantPlan: SubscriptionPlan; className?: string; }; -function TenantStatusTag({ tenantId, className }: Props) { - const { data: usage, error: fetchUsageError } = useSubscriptionUsage(tenantId); - const { data: invoices, error: fetchInvoiceError } = useInvoices(tenantId); - const { data: subscriptionPlan, error: fetchSubscriptionError } = useSubscriptionPlan(tenantId); - - const isLoadingUsage = !usage && !fetchUsageError; - const isLoadingInvoice = !invoices && !fetchInvoiceError; - const isLoadingSubscription = !subscriptionPlan && !fetchSubscriptionError; - - const latestUnpaidInvoice = useMemo( - () => conditional(invoices && getLatestUnpaidInvoice(invoices)), - [invoices] - ); - - if (isLoadingUsage || isLoadingInvoice || isLoadingSubscription) { - return null; - } +function TenantStatusTag({ tenantData, tenantPlan, className }: Props) { + const { usage, openInvoices } = tenantData; /** * Tenant status priority: @@ -38,7 +19,7 @@ function TenantStatusTag({ tenantId, className }: Props) { * 3. mau exceeded */ - if (invoices && latestUnpaidInvoice) { + if (openInvoices.length > 0) { return ( @@ -46,22 +27,20 @@ function TenantStatusTag({ tenantId, className }: Props) { ); } - if (subscriptionPlan && usage) { - const { activeUsers } = usage; + const { activeUsers } = usage; - const { - quota: { mauLimit }, - } = subscriptionPlan; + const { + quota: { mauLimit }, + } = tenantPlan; - const isMauExceeded = mauLimit !== null && activeUsers >= mauLimit; + const isMauExceeded = mauLimit !== null && activeUsers >= mauLimit; - if (isMauExceeded) { - return ( - - - - ); - } + if (isMauExceeded) { + return ( + + + + ); } return null; diff --git a/packages/console/src/containers/AppContent/components/Topbar/TenantSelector/TenantDropdownItem/index.tsx b/packages/console/src/containers/AppContent/components/Topbar/TenantSelector/TenantDropdownItem/index.tsx index b18a7ca4b..31d2e6515 100644 --- a/packages/console/src/containers/AppContent/components/Topbar/TenantSelector/TenantDropdownItem/index.tsx +++ b/packages/console/src/containers/AppContent/components/Topbar/TenantSelector/TenantDropdownItem/index.tsx @@ -1,10 +1,11 @@ import classNames from 'classnames'; +import { useMemo } from 'react'; import Tick from '@/assets/icons/tick.svg'; import { type TenantResponse } from '@/cloud/types/router'; import PlanName from '@/components/PlanName'; import { DropdownItem } from '@/ds-components/Dropdown'; -import useSubscriptionPlan from '@/hooks/use-subscription-plan'; +import useSubscriptionPlans from '@/hooks/use-subscription-plans'; import TenantEnvTag from '../TenantEnvTag'; @@ -18,8 +19,18 @@ type Props = { }; function TenantDropdownItem({ tenantData, isSelected, onClick }: Props) { - const { id, name, tag } = tenantData; - const { data: tenantPlan } = useSubscriptionPlan(id); + const { + name, + tag, + subscription: { planId }, + } = tenantData; + + const { data: plans } = useSubscriptionPlans(); + const tenantPlan = useMemo(() => plans?.find((plan) => plan.id === planId), [plans, planId]); + + if (!tenantPlan) { + return null; + } return ( @@ -27,9 +38,15 @@ function TenantDropdownItem({ tenantData, isSelected, onClick }: Props) {
{name}
- + +
+
+
-
{tenantPlan && }
diff --git a/packages/console/src/contexts/TenantsProvider.tsx b/packages/console/src/contexts/TenantsProvider.tsx index 69d5de2ea..da10471a3 100644 --- a/packages/console/src/contexts/TenantsProvider.tsx +++ b/packages/console/src/contexts/TenantsProvider.tsx @@ -1,6 +1,7 @@ import { defaultManagementApi, defaultTenantId } from '@logto/schemas'; import { TenantTag } from '@logto/schemas/models'; import { conditionalArray, noop } from '@silverhand/essentials'; +import dayjs from 'dayjs'; import type { ReactNode } from 'react'; import { useCallback, useMemo, createContext, useState } from 'react'; import { useMatch, useNavigate } from 'react-router-dom'; @@ -65,17 +66,27 @@ const { tenantId, indicator } = defaultManagementApi.resource; * - For cloud, the initial tenants data is empty, and it will be fetched from the cloud API. * - OSS has a fixed tenant with ID `default` and no cloud API to dynamically fetch tenants. */ -const initialTenants = Object.freeze( - conditionalArray( - !isCloud && { - id: tenantId, - name: `tenant_${tenantId}`, - tag: TenantTag.Development, - indicator, - planId: `${ReservedPlanId.free}`, // `planId` is string type. - } - ) -); +const defaultTenantResponse: TenantResponse = { + id: tenantId, + name: `tenant_${tenantId}`, + tag: TenantTag.Development, + indicator, + subscription: { + status: 'active', + planId: ReservedPlanId.free, + currentPeriodStart: dayjs().toDate(), + currentPeriodEnd: dayjs().add(1, 'month').toDate(), + }, + usage: { + activeUsers: 0, + cost: 0, + }, + openInvoices: [], + isSuspended: false, + planId: ReservedPlanId.free, // Reserved for compatibility with cloud +}; + +const initialTenants = Object.freeze(conditionalArray(!isCloud && defaultTenantResponse)); export const TenantsContext = createContext({ tenants: initialTenants, diff --git a/packages/console/src/pages/TenantSettings/Subscription/CurrentPlan/PaymentOverdueNotification/index.tsx b/packages/console/src/pages/TenantSettings/Subscription/CurrentPlan/PaymentOverdueNotification/index.tsx index 22afa2c4c..c69812612 100644 --- a/packages/console/src/pages/TenantSettings/Subscription/CurrentPlan/PaymentOverdueNotification/index.tsx +++ b/packages/console/src/pages/TenantSettings/Subscription/CurrentPlan/PaymentOverdueNotification/index.tsx @@ -1,32 +1,29 @@ -import { conditional } from '@silverhand/essentials'; -import { useContext, useMemo, useState } from 'react'; +import { useContext, useState } from 'react'; import { TenantsContext } from '@/contexts/TenantsProvider'; import DynamicT from '@/ds-components/DynamicT'; import InlineNotification from '@/ds-components/InlineNotification'; -import useInvoices from '@/hooks/use-invoices'; import useSubscribe from '@/hooks/use-subscribe'; -import { getLatestUnpaidInvoice } from '@/utils/subscription'; type Props = { className?: string; }; function PaymentOverdueNotification({ className }: Props) { - const { currentTenantId } = useContext(TenantsContext); + const { currentTenant, currentTenantId } = useContext(TenantsContext); + const { openInvoices = [] } = currentTenant ?? {}; const { visitManagePaymentPage } = useSubscribe(); const [isActionLoading, setIsActionLoading] = useState(false); - const { data: invoices, error } = useInvoices(currentTenantId); - const isLoadingInvoices = !invoices && !error; - const latestUnpaidInvoice = useMemo( - () => conditional(invoices && getLatestUnpaidInvoice(invoices)), - [invoices] - ); - if (isLoadingInvoices || !latestUnpaidInvoice) { + if (openInvoices.length === 0) { return null; } + const totalAmountDue = openInvoices.reduce( + (total, currentInvoice) => total + currentInvoice.amountDue, + 0 + ); + return ( ); diff --git a/packages/console/src/types/subscriptions.ts b/packages/console/src/types/subscriptions.ts index 429535541..d1f3f20d2 100644 --- a/packages/console/src/types/subscriptions.ts +++ b/packages/console/src/types/subscriptions.ts @@ -76,6 +76,4 @@ export const localCheckoutSessionGuard = z.object({ export type LocalCheckoutSession = z.infer; -export type Invoice = InvoicesResponse['invoices'][number]; - -export type InvoiceStatus = Invoice['status']; +export type InvoiceStatus = InvoicesResponse['invoices'][number]['status']; diff --git a/packages/console/src/utils/subscription.ts b/packages/console/src/utils/subscription.ts index 343efa5ab..35b4a4e44 100644 --- a/packages/console/src/utils/subscription.ts +++ b/packages/console/src/utils/subscription.ts @@ -5,7 +5,6 @@ import { tryReadResponseErrorBody } from '@/cloud/hooks/use-cloud-api'; import { type SubscriptionPlanResponse } from '@/cloud/types/router'; import { communitySupportEnabledMap, ticketSupportResponseTimeMap } from '@/consts/plan-quotas'; import { reservedPlanIdOrder } from '@/consts/subscriptions'; -import { type Invoice } from '@/types/subscriptions'; export const addSupportQuotaToPlan = (subscriptionPlanResponse: SubscriptionPlanResponse) => { const { id, quota } = subscriptionPlanResponse; @@ -43,15 +42,6 @@ export const formatPeriod = ({ periodStart, periodEnd, displayYear }: FormatPeri return `${formattedStart} - ${formattedEnd}`; }; -export const getLatestUnpaidInvoice = (invoices: Invoice[]) => - invoices - .slice() - .sort( - (invoiceA, invoiceB) => - new Date(invoiceB.createdAt).getTime() - new Date(invoiceA.createdAt).getTime() - ) - .find(({ status }) => status === 'uncollectible'); - /** * Note: this is a temporary solution to handle the case when the user tries to downgrade but the quota limit is exceeded. * Need a better solution to handle this case by sharing the error type between the console and cloud. - LOG-6608 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 704a01571..29bd68951 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2834,8 +2834,8 @@ importers: specifier: workspace:^1.3.1 version: link:../app-insights '@logto/cloud': - specifier: 0.2.5-1795c3d - version: 0.2.5-1795c3d(zod@3.20.2) + specifier: 0.2.5-71b7fea + version: 0.2.5-71b7fea(zod@3.20.2) '@logto/connector-kit': specifier: workspace:^1.1.1 version: link:../toolkit/connector-kit @@ -2936,8 +2936,8 @@ importers: specifier: ^15.5.1 version: 15.5.1 '@withtyped/client': - specifier: ^0.7.21 - version: 0.7.21(zod@3.20.2) + specifier: ^0.7.22 + version: 0.7.22(zod@3.20.2) buffer: specifier: ^5.7.1 version: 5.7.1 @@ -7330,12 +7330,12 @@ packages: jose: 4.14.4 dev: true - /@logto/cloud@0.2.5-1795c3d(zod@3.20.2): - resolution: {integrity: sha512-zxy9zr5swOxbzYJNYtKXofj2tSIS9565d+1pT6RSbmx3Hn+JG6uzsb75PZXW9vlmmm7p1sGZeTQ+xVzKNFPsMg==} + /@logto/cloud@0.2.5-71b7fea(zod@3.20.2): + resolution: {integrity: sha512-howllmEV6kWAgusP+2OSloG5bZQ146UiKn0PpA7xi9HcpgM6Fd1NPuNjc3BZdInJ5Qn0en6LOZL7c2EwTRx3jw==} engines: {node: ^18.12.0} dependencies: '@silverhand/essentials': 2.8.4 - '@withtyped/server': 0.12.8(zod@3.20.2) + '@withtyped/server': 0.12.9(zod@3.20.2) transitivePeerDependencies: - zod dev: true @@ -10048,15 +10048,6 @@ packages: eslint-visitor-keys: 3.4.1 dev: true - /@withtyped/client@0.7.21(zod@3.20.2): - resolution: {integrity: sha512-N9dvH5nqIwaT7YxaIm83RRQf9AEjxwJ4ugJviZJSxtWy8zLul2/odEMc6epieylFVa6CcLg82yJmRSlqPtJiTw==} - dependencies: - '@withtyped/server': 0.12.8(zod@3.20.2) - '@withtyped/shared': 0.2.2 - transitivePeerDependencies: - - zod - dev: true - /@withtyped/client@0.7.22(zod@3.20.2): resolution: {integrity: sha512-emNtcO0jc0dFWhvL7eUIRYzhTfn+JqgIvCmXb8ZUFOR8wdSSGrr9VDlm+wgQD06DEBBpmqtTHMMHTNXJdUC/Qw==} dependencies: @@ -10064,17 +10055,6 @@ packages: '@withtyped/shared': 0.2.2 transitivePeerDependencies: - zod - dev: false - - /@withtyped/server@0.12.8(zod@3.20.2): - resolution: {integrity: sha512-fv9feTOKJhtlaoYM/Kbs2gSTcIXlmu4OMUFwGmK5jqdbVNIOkDBIPxtcC5ZEwevWFgOcd5OqBW+FvbjiaF27Fw==} - peerDependencies: - zod: ^3.19.1 - dependencies: - '@silverhand/essentials': 2.8.4 - '@withtyped/shared': 0.2.2 - zod: 3.20.2 - dev: true /@withtyped/server@0.12.9(zod@3.20.2): resolution: {integrity: sha512-K5zoV9D+WpawbghtlJKF1KOshKkBjq+gYzNRWuZk13YmFWFLcmZn+QCblNP55z9IGdcHWpTRknqb1APuicdzgA==} From 9e606d65a79e061b136ac54e2df024d8ffb07d82 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Wed, 6 Sep 2023 10:23:19 +0800 Subject: [PATCH 21/30] refactor(console): add headline spacing prop for `FormField` component (#4433) --- .../ds-components/FormField/index.module.scss | 4 ++++ .../src/ds-components/FormField/index.tsx | 10 ++++++++- .../pages/SignInExperience/index.module.scss | 4 ---- .../pages/SignInExperience/index.tsx | 17 +++----------- .../pages/Welcome/index.module.scss | 4 ---- .../src/onboarding/pages/Welcome/index.tsx | 22 ++++--------------- .../index.module.scss | 4 ---- .../EmailServiceConnectorForm/index.tsx | 4 ++-- .../tabs/Branding/BrandingForm.tsx | 7 ++---- .../SignInExperience/tabs/index.module.scss | 4 ---- 10 files changed, 24 insertions(+), 56 deletions(-) diff --git a/packages/console/src/ds-components/FormField/index.module.scss b/packages/console/src/ds-components/FormField/index.module.scss index 79191cc71..a9b14e612 100644 --- a/packages/console/src/ds-components/FormField/index.module.scss +++ b/packages/console/src/ds-components/FormField/index.module.scss @@ -11,6 +11,10 @@ align-items: center; margin-bottom: _.unit(1); + &.withLargeSpacing { + margin-bottom: _.unit(2); + } + .title { font: var(--font-label-2); color: var(--color-text); diff --git a/packages/console/src/ds-components/FormField/index.tsx b/packages/console/src/ds-components/FormField/index.tsx index b73e2de5d..b5c045667 100644 --- a/packages/console/src/ds-components/FormField/index.tsx +++ b/packages/console/src/ds-components/FormField/index.tsx @@ -20,6 +20,7 @@ export type Props = { isRequired?: boolean; isMultiple?: boolean; className?: string; + headlineSpacing?: 'default' | 'large'; headlineClassName?: string; tip?: ToggleTipProps['content']; }; @@ -30,6 +31,7 @@ function FormField({ isRequired, isMultiple, className, + headlineSpacing = 'default', tip, headlineClassName, }: Props) { @@ -37,7 +39,13 @@ function FormField({ return (
-
+
{typeof title === 'string' ? : title} {isMultiple && ( diff --git a/packages/console/src/onboarding/pages/SignInExperience/index.module.scss b/packages/console/src/onboarding/pages/SignInExperience/index.module.scss index 3cf5921cf..49ab6c146 100644 --- a/packages/console/src/onboarding/pages/SignInExperience/index.module.scss +++ b/packages/console/src/onboarding/pages/SignInExperience/index.module.scss @@ -23,10 +23,6 @@ font: var(--font-title-1); } - .cardFieldHeadline { - margin-bottom: _.unit(2); - } - .authnSelector { grid-template-columns: repeat(2, 1fr); } diff --git a/packages/console/src/onboarding/pages/SignInExperience/index.tsx b/packages/console/src/onboarding/pages/SignInExperience/index.tsx index f6a65c441..0b44080f4 100644 --- a/packages/console/src/onboarding/pages/SignInExperience/index.tsx +++ b/packages/console/src/onboarding/pages/SignInExperience/index.tsx @@ -176,10 +176,7 @@ function SignInExperience() { )} /> - + - + - + {t('cloud.welcome.title')}
{t('cloud.welcome.description')}
- + - + - + )} - + } - headlineClassName={conditional(isUserAssetsServiceReady && styles.imageFieldHeadline)} + headlineSpacing={isUserAssetsServiceReady ? 'large' : 'default'} > {isUserAssetsServiceReady ? ( } - headlineClassName={styles.imageFieldHeadline} + headlineSpacing="large" > @@ -131,10 +131,7 @@ function BrandingForm() { )} {isUserAssetsServiceReady ? ( - + Date: Wed, 6 Sep 2023 10:35:52 +0800 Subject: [PATCH 22/30] refactor(console): reorg connector creation modal (#4438) refactor(console): reorg connector create modal --- .../PlatformSelector/index.module.scss | 11 +++++++++-- .../src/components/CreateConnectorForm/index.tsx | 14 +++++++------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/packages/console/src/components/CreateConnectorForm/PlatformSelector/index.module.scss b/packages/console/src/components/CreateConnectorForm/PlatformSelector/index.module.scss index e5aa8e225..80329d76f 100644 --- a/packages/console/src/components/CreateConnectorForm/PlatformSelector/index.module.scss +++ b/packages/console/src/components/CreateConnectorForm/PlatformSelector/index.module.scss @@ -1,10 +1,17 @@ @use '@/scss/underscore' as _; .platforms { - margin-top: _.unit(6); + padding: _.unit(4) _.unit(6); + background: var(--color-bg-layer-2); + border-radius: 16px; + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: flex-start; + gap: _.unit(3); + margin-top: _.unit(4); .title { font: var(--font-label-2); - margin-bottom: _.unit(3); } } diff --git a/packages/console/src/components/CreateConnectorForm/index.tsx b/packages/console/src/components/CreateConnectorForm/index.tsx index 1a5963ec8..7e6f96f9f 100644 --- a/packages/console/src/components/CreateConnectorForm/index.tsx +++ b/packages/console/src/components/CreateConnectorForm/index.tsx @@ -144,6 +144,13 @@ function CreateConnectorForm({ onClose, isOpen: isFormOpen, type }: Props) { size={radioGroupSize} onChange={handleGroupChange} /> + {activeGroup && ( + + )} {standardGroups.length > 0 && ( <>
@@ -159,13 +166,6 @@ function CreateConnectorForm({ onClose, isOpen: isFormOpen, type }: Props) { /> )} - {activeGroup && ( - - )} ); From f1ded1168bf66b795629a0a1c13e0bb7f8027a72 Mon Sep 17 00:00:00 2001 From: Charles Zhao Date: Wed, 6 Sep 2023 22:55:11 +0800 Subject: [PATCH 23/30] refactor: improve guide responsive rules (#4436) --- .../assets/docs/guides/spa-vanilla/README.mdx | 1 - .../mdx-components/Steps/index.module.scss | 32 +++-- .../src/mdx-components/Steps/index.tsx | 49 +++---- .../mdx-components/UriInputField/index.tsx | 54 ++------ .../components/GuideDrawer/index.module.scss | 2 +- .../components/GuideDrawer/index.tsx | 1 - .../src/pages/ApplicationDetails/index.tsx | 2 +- .../components/Guide/index.module.scss | 82 +++++------- .../Applications/components/Guide/index.tsx | 35 +++-- .../components/GuideCard/index.module.scss | 5 +- .../components/GuideCard/index.tsx | 36 +++-- .../components/GuideGroup/index.tsx | 6 +- .../components/GuideHeader/index.module.scss | 9 +- .../components/GuideHeader/index.tsx | 75 ++++------- .../components/GuideLibrary/index.module.scss | 48 ++++++- .../components/GuideLibrary/index.tsx | 126 +++++++++--------- .../GuideLibraryModal/index.module.scss | 36 +++-- .../components/GuideLibraryModal/index.tsx | 23 ++-- .../components/GuideModal/index.module.scss | 19 +++ .../GuideModal.tsx => GuideModal/index.tsx} | 10 +- .../console/src/pages/Applications/index.tsx | 2 +- packages/console/src/scss/_dimensions.scss | 8 ++ 22 files changed, 346 insertions(+), 315 deletions(-) create mode 100644 packages/console/src/pages/Applications/components/GuideModal/index.module.scss rename packages/console/src/pages/Applications/components/{Guide/GuideModal.tsx => GuideModal/index.tsx} (65%) diff --git a/packages/console/src/assets/docs/guides/spa-vanilla/README.mdx b/packages/console/src/assets/docs/guides/spa-vanilla/README.mdx index 450ad45d5..8173355e4 100644 --- a/packages/console/src/assets/docs/guides/spa-vanilla/README.mdx +++ b/packages/console/src/assets/docs/guides/spa-vanilla/README.mdx @@ -115,7 +115,6 @@ After signing out, it'll be great to redirect user back to your website. Let's a diff --git a/packages/console/src/mdx-components/Steps/index.module.scss b/packages/console/src/mdx-components/Steps/index.module.scss index 1a02abfc5..4ee3bfd53 100644 --- a/packages/console/src/mdx-components/Steps/index.module.scss +++ b/packages/console/src/mdx-components/Steps/index.module.scss @@ -1,24 +1,20 @@ @use '@/scss/underscore' as _; +@use '@/scss/dimensions' as dim; .wrapper { - position: relative; -} - -.fullWidth { width: 100%; } .navigationAnchor { position: absolute; - inset: 0 auto 0 0; - transform: translateX(-100%); + inset: _.unit(6) auto _.unit(6) _.unit(6); } .navigation { position: sticky; - top: 0; + top: _.unit(6); flex-shrink: 0; - margin-right: _.unit(4); + margin-right: _.unit(7.5); width: 220px; > :not(:last-child) { @@ -27,11 +23,21 @@ } .content { - max-width: 858px; + width: 100%; + min-width: dim.$guide-content-min-width; + max-width: dim.$guide-content-max-width; + padding: dim.$guide-content-padding calc(dim.$guide-sidebar-width + dim.$guide-panel-gap + dim.$guide-content-padding); + margin: 0 auto; + position: relative; > :not(:last-child) { margin-bottom: _.unit(6); } + + &.compact { + min-width: 652px; + padding: 0; + } } .stepper { @@ -51,3 +57,11 @@ background: var(--color-focused-variant); } } + +@media screen and (max-width: dim.$guide-content-max-width) { + .content { + margin: 0; + padding-right: dim.$guide-content-padding; + max-width: calc(dim.$guide-main-content-max-width + dim.$guide-sidebar-width + dim.$guide-panel-gap + 2 * dim.$guide-content-padding); + } +} diff --git a/packages/console/src/mdx-components/Steps/index.tsx b/packages/console/src/mdx-components/Steps/index.tsx index adca53fb3..02f40c354 100644 --- a/packages/console/src/mdx-components/Steps/index.tsx +++ b/packages/console/src/mdx-components/Steps/index.tsx @@ -48,6 +48,7 @@ export default function Steps({ children: reactChildren }: Props) { : [reactChildren, furtherReadings], [furtherReadings, reactChildren] ); + const { isCompact } = useContext(GuideContext); useEffect(() => { @@ -85,30 +86,30 @@ export default function Steps({ children: reactChildren }: Props) { }; return ( -
- {!isCompact && ( -
- -
- )} -
+
+
+ {!isCompact && ( +
+ +
+ )} {children.map((component, index) => React.cloneElement(component, { diff --git a/packages/console/src/mdx-components/UriInputField/index.tsx b/packages/console/src/mdx-components/UriInputField/index.tsx index 057f1fbe2..4afde81b5 100644 --- a/packages/console/src/mdx-components/UriInputField/index.tsx +++ b/packages/console/src/mdx-components/UriInputField/index.tsx @@ -10,12 +10,10 @@ 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/Guide'; @@ -42,9 +40,7 @@ function UriInputField({ name, defaultValue }: Props) { } = methods; const { app: { id: appId }, - isCompact, } = useContext(GuideContext); - const isSingle = !isCompact; const { data, mutate } = useSWR(`api/applications/${appId}`); const ref = useRef(null); @@ -93,10 +89,7 @@ function UriInputField({ name, defaultValue }: Props) { defaultValue={defaultValueArray} rules={{ validate: createValidatorForRhf({ - required: t( - isSingle ? 'errors.required_field_missing' : 'errors.required_field_missing_plural', - { field: title } - ), + required: t('errors.required_field_missing_plural', { field: title }), pattern: { verify: (value) => !value || uriValidator(value), message: t('errors.invalid_uri_format'), @@ -108,39 +101,18 @@ function UriInputField({ name, defaultValue }: Props) { return (
- {isSingle && ( - - { - onChange([value]); - }} - onKeyPress={(event) => { - onKeyPress(event, value); - }} - /> - - )} - {!isSingle && ( - { - onKeyPress(event, value); - }} - /> - )} + { + onKeyPress(event, value); + }} + />
- - )} -
-
+ + {memorizedContext && ( + + )} + ); } diff --git a/packages/console/src/pages/Applications/components/GuideCard/index.module.scss b/packages/console/src/pages/Applications/components/GuideCard/index.module.scss index 9495c42b8..7f829d685 100644 --- a/packages/console/src/pages/Applications/components/GuideCard/index.module.scss +++ b/packages/console/src/pages/Applications/components/GuideCard/index.module.scss @@ -11,9 +11,10 @@ min-width: 220px; max-width: 460px; justify-content: space-between; + cursor: pointer; - &.compact { - cursor: pointer; + &.hasButton { + cursor: default; } &.hasBorder { diff --git a/packages/console/src/pages/Applications/components/GuideCard/index.tsx b/packages/console/src/pages/Applications/components/GuideCard/index.tsx index c23919f1d..f98a8f718 100644 --- a/packages/console/src/pages/Applications/components/GuideCard/index.tsx +++ b/packages/console/src/pages/Applications/components/GuideCard/index.tsx @@ -1,6 +1,6 @@ import { ApplicationType } from '@logto/schemas'; import classNames from 'classnames'; -import { Suspense, useContext } from 'react'; +import { Suspense, useCallback, useContext } from 'react'; import { type Guide, type GuideMetadata } from '@/assets/docs/guides/types'; import ProTag from '@/components/ProTag'; @@ -24,10 +24,10 @@ type Props = { data: Guide; onClick: (data: SelectedGuide) => void; hasBorder?: boolean; - isCompact?: boolean; + hasButton?: boolean; }; -function GuideCard({ data, onClick, hasBorder, isCompact }: Props) { +function GuideCard({ data, onClick, hasBorder, hasButton }: Props) { const { navigate } = useTenantPathname(); const { currentTenantId } = useContext(TenantsContext); const { data: currentPlan } = useSubscriptionPlan(currentTenantId); @@ -41,26 +41,26 @@ function GuideCard({ data, onClick, hasBorder, isCompact }: Props) { metadata: { target, name, description }, } = data; - const onClickCard = () => { - if (!isCompact) { - return; + const handleClick = useCallback(() => { + if (isSubscriptionRequired) { + navigate(subscriptionPage); + } else { + onClick({ id, target, name }); } - - onClick({ id, target, name }); - }; + }, [id, isSubscriptionRequired, name, target, navigate, onClick]); return (
@@ -77,19 +77,13 @@ function GuideCard({ data, onClick, hasBorder, isCompact }: Props) {
- {!isCompact && ( + {hasButton && (
diff --git a/packages/console/src/pages/Applications/components/GuideGroup/index.tsx b/packages/console/src/pages/Applications/components/GuideGroup/index.tsx index d72138a7c..903c13deb 100644 --- a/packages/console/src/pages/Applications/components/GuideGroup/index.tsx +++ b/packages/console/src/pages/Applications/components/GuideGroup/index.tsx @@ -12,12 +12,12 @@ type GuideGroupProps = { categoryName?: string; guides?: readonly Guide[]; hasCardBorder?: boolean; - isCompact?: boolean; + hasCardButton?: boolean; onClickGuide: (data: SelectedGuide) => void; }; function GuideGroup( - { className, categoryName, guides, hasCardBorder, isCompact, onClickGuide }: GuideGroupProps, + { className, categoryName, guides, hasCardBorder, hasCardButton, onClickGuide }: GuideGroupProps, ref: Ref ) { if (!guides?.length) { @@ -31,8 +31,8 @@ function GuideGroup( {guides.map((guide) => ( diff --git a/packages/console/src/pages/Applications/components/GuideHeader/index.module.scss b/packages/console/src/pages/Applications/components/GuideHeader/index.module.scss index 527eda8f6..3535d2a84 100644 --- a/packages/console/src/pages/Applications/components/GuideHeader/index.module.scss +++ b/packages/console/src/pages/Applications/components/GuideHeader/index.module.scss @@ -4,6 +4,7 @@ display: flex; align-items: center; background-color: var(--color-base); + width: 100%; height: 64px; padding: 0 _.unit(6); flex-shrink: 0; @@ -33,6 +34,12 @@ } .requestSdkButton { - margin-right: _.unit(2); + margin-right: _.unit(15); + } +} + +@media screen and (max-width: 918px) { + .header .requestSdkButton { + margin-right: 0; } } diff --git a/packages/console/src/pages/Applications/components/GuideHeader/index.tsx b/packages/console/src/pages/Applications/components/GuideHeader/index.tsx index 3ba4fbdd7..0ff210ade 100644 --- a/packages/console/src/pages/Applications/components/GuideHeader/index.tsx +++ b/packages/console/src/pages/Applications/components/GuideHeader/index.tsx @@ -1,5 +1,4 @@ import { useCallback, useState } from 'react'; -import { useTranslation } from 'react-i18next'; import Box from '@/assets/icons/box.svg'; import Close from '@/assets/icons/close.svg'; @@ -9,19 +8,15 @@ import Button from '@/ds-components/Button'; import CardTitle from '@/ds-components/CardTitle'; import IconButton from '@/ds-components/IconButton'; import Spacer from '@/ds-components/Spacer'; -import Tooltip from '@/ds-components/Tip/Tooltip'; import RequestGuide from './RequestGuide'; import * as styles from './index.module.scss'; type Props = { - isCompact?: boolean; onClose: () => void; }; -function GuideHeader({ isCompact = false, onClose }: Props) { - const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); - +function GuideHeader({ onClose }: Props) { const [isRequestGuideOpen, setIsRequestGuideOpen] = useState(false); const onRequestGuideClose = useCallback(() => { setIsRequestGuideOpen(false); @@ -29,51 +24,29 @@ function GuideHeader({ isCompact = false, onClose }: Props) { return (
- {isCompact && ( - <> - - - - - - - - )} - {!isCompact && ( - <> - - - -
- - -
); diff --git a/packages/console/src/pages/Applications/components/GuideLibrary/index.module.scss b/packages/console/src/pages/Applications/components/GuideLibrary/index.module.scss index 01f7bcd1a..d68e5eb25 100644 --- a/packages/console/src/pages/Applications/components/GuideLibrary/index.module.scss +++ b/packages/console/src/pages/Applications/components/GuideLibrary/index.module.scss @@ -1,17 +1,36 @@ @use '@/scss/underscore' as _; +@use '@/scss/dimensions' as dim; .container { - display: flex; - gap: _.unit(7); + width: 100%; +} + +.wrapper { + width: 100%; + min-width: dim.$guide-content-min-width; + max-width: dim.$guide-content-max-width; + margin: 0 auto; + position: relative; + + &.hasFilters { + padding: dim.$guide-content-padding calc(dim.$guide-sidebar-width + dim.$guide-panel-gap + dim.$guide-content-padding); + } +} + +.filterAnchor { + position: absolute; + top: 0; + right: 100%; } .filters { + position: sticky; + top: 0; display: flex; flex-direction: column; + width: dim.$guide-sidebar-width; gap: _.unit(4); - padding: _.unit(8) 0 _.unit(8) _.unit(11); - flex-shrink: 0; - overflow-y: auto; + margin-right: dim.$guide-panel-gap; label { font: var(--font-label-2); @@ -45,16 +64,23 @@ display: flex; flex-direction: column; padding-bottom: _.unit(8); - overflow-y: auto; + position: relative; > div { flex: unset; } } +.wrapper.hasFilters .groups { + max-width: dim.$guide-main-content-max-width; +} + .guideGroup { flex: 1; - margin: _.unit(8) _.unit(8) 0 0; + + + .guideGroup { + margin-top: _.unit(8); + } } .emptyPlaceholder { @@ -63,3 +89,11 @@ width: 100%; height: 70%; } + +@media screen and (max-width: dim.$guide-content-max-width) { + .wrapper.hasFilters { + margin-left: 0; + padding-right: dim.$guide-content-padding; + max-width: calc(dim.$guide-main-content-max-width + dim.$guide-sidebar-width + dim.$guide-panel-gap + 2 * dim.$guide-content-padding); + } +} diff --git a/packages/console/src/pages/Applications/components/GuideLibrary/index.tsx b/packages/console/src/pages/Applications/components/GuideLibrary/index.tsx index eaaef54b8..ca0e125bc 100644 --- a/packages/console/src/pages/Applications/components/GuideLibrary/index.tsx +++ b/packages/console/src/pages/Applications/components/GuideLibrary/index.tsx @@ -25,10 +25,11 @@ import * as styles from './index.module.scss'; type Props = { className?: string; hasCardBorder?: boolean; + hasCardButton?: boolean; hasFilters?: boolean; }; -function GuideLibrary({ className, hasCardBorder, hasFilters }: Props) { +function GuideLibrary({ className, hasCardBorder, hasCardButton, hasFilters }: Props) { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.applications.guide' }); const { navigate } = useTenantPathname(); const [keyword, setKeyword] = useState(''); @@ -69,66 +70,71 @@ function GuideLibrary({ className, hasCardBorder, hasFilters }: Props) { ); return ( -
- {hasFilters && ( -
- - } - placeholder={t('filter.placeholder')} - value={keyword} - onChange={(event) => { - setKeyword(event.currentTarget.value); - }} - /> -
- ({ - title: `applications.guide.categories.${category}`, - value: category, - }))} - value={filterCategories} - onChange={(value) => { - const sortedValue = allAppGuideCategories.filter((category) => - value.includes(category) - ); - setFilterCategories(sortedValue); - }} - /> - {isM2mDisabledForCurrentPlan && } -
-
- )} - {keyword && - (filteredMetadata?.length ? ( - - ) : ( - - ))} - {!keyword && ( - - {(filterCategories.length > 0 ? filterCategories : allAppGuideCategories).map( - (category) => - structuredMetadata[category].length > 0 && ( - +
+
+ {hasFilters && ( +
+
+ + } + placeholder={t('filter.placeholder')} + value={keyword} + onChange={(event) => { + setKeyword(event.currentTarget.value); + }} /> - ) +
+ ({ + title: `applications.guide.categories.${category}`, + value: category, + }))} + value={filterCategories} + onChange={(value) => { + const sortedValue = allAppGuideCategories.filter((category) => + value.includes(category) + ); + setFilterCategories(sortedValue); + }} + /> + {isM2mDisabledForCurrentPlan && } +
+
+
)} - - )} + {keyword && + (filteredMetadata?.length ? ( + + ) : ( + + ))} + {!keyword && + (filterCategories.length > 0 ? filterCategories : allAppGuideCategories).map( + (category) => + structuredMetadata[category].length > 0 && ( + + ) + )} +
+
{selectedGuide?.target !== 'API' && showCreateForm && ( )} -
+ ); } diff --git a/packages/console/src/pages/Applications/components/GuideLibraryModal/index.module.scss b/packages/console/src/pages/Applications/components/GuideLibraryModal/index.module.scss index 20e35b541..df8306352 100644 --- a/packages/console/src/pages/Applications/components/GuideLibraryModal/index.module.scss +++ b/packages/console/src/pages/Applications/components/GuideLibraryModal/index.module.scss @@ -1,38 +1,50 @@ @use '@/scss/underscore' as _; +@use '@/scss/dimensions' as dim; .container { display: flex; flex-direction: column; background-color: var(--color-base); + width: 100vw; height: 100vh; + overflow-x: auto; + + > * { + min-width: dim.$guide-content-min-width; + } .content { flex: 1; - display: flex; - width: 100%; overflow: hidden; } .actionBar { - display: flex; - align-items: center; inset: auto 0 0 0; - padding: _.unit(4) _.unit(8); + width: 100%; + padding: _.unit(4) _.unit(6); background-color: var(--color-layer-1); box-shadow: var(--shadow-3); z-index: 1; + .wrapper { + display: flex; + align-items: center; + justify-content: space-between; + max-width: dim.$guide-main-content-max-width; + margin: 0 auto; + } + .text { font: var(--font-body-2); color: var(--color-text); - margin-left: _.unit(62.5); - margin-right: _.unit(4); + margin-right: _.unit(3); @include _.multi-line-ellipsis(2); } - - .button { - margin-right: 0; - margin-left: auto; - } + } +} + +@media screen and (max-width: dim.$guide-content-max-width) { + .container .actionBar .wrapper { + margin: 0 0 0 _.unit(62.5); } } diff --git a/packages/console/src/pages/Applications/components/GuideLibraryModal/index.tsx b/packages/console/src/pages/Applications/components/GuideLibraryModal/index.tsx index 6dffef933..4d82f8e40 100644 --- a/packages/console/src/pages/Applications/components/GuideLibraryModal/index.tsx +++ b/packages/console/src/pages/Applications/components/GuideLibraryModal/index.tsx @@ -30,18 +30,19 @@ function GuideLibraryModal({ isOpen, onClose }: Props) { >
- +
{showCreateForm && ( diff --git a/packages/console/src/pages/Applications/components/GuideModal/index.module.scss b/packages/console/src/pages/Applications/components/GuideModal/index.module.scss new file mode 100644 index 000000000..8fae7bd24 --- /dev/null +++ b/packages/console/src/pages/Applications/components/GuideModal/index.module.scss @@ -0,0 +1,19 @@ +@use '@/scss/underscore' as _; +@use '@/scss/dimensions' as dim; + +.modalContainer { + display: flex; + flex-direction: column; + width: 100vw; + height: 100vh; + background-color: var(--color-base); + overflow-x: auto; + + > * { + min-width: dim.$guide-content-min-width; + } +} + +.guide { + flex: 1; +} diff --git a/packages/console/src/pages/Applications/components/Guide/GuideModal.tsx b/packages/console/src/pages/Applications/components/GuideModal/index.tsx similarity index 65% rename from packages/console/src/pages/Applications/components/Guide/GuideModal.tsx rename to packages/console/src/pages/Applications/components/GuideModal/index.tsx index 6e964bb27..9c06066f2 100644 --- a/packages/console/src/pages/Applications/components/Guide/GuideModal.tsx +++ b/packages/console/src/pages/Applications/components/GuideModal/index.tsx @@ -3,7 +3,10 @@ import Modal from 'react-modal'; import * as modalStyles from '@/scss/modal.module.scss'; -import Guide from '.'; +import Guide from '../Guide'; +import GuideHeader from '../GuideHeader'; + +import * as styles from './index.module.scss'; type Props = { guideId: string; @@ -27,7 +30,10 @@ function GuideModal({ guideId, app, onClose }: Props) { className={modalStyles.fullScreen} onRequestClose={closeModal} > - +
+ + +
); } diff --git a/packages/console/src/pages/Applications/index.tsx b/packages/console/src/pages/Applications/index.tsx index 252794af2..d7632e284 100644 --- a/packages/console/src/pages/Applications/index.tsx +++ b/packages/console/src/pages/Applications/index.tsx @@ -85,7 +85,7 @@ function Applications() { title="applications.guide.header_title" subtitle="applications.guide.header_subtitle" /> - + )} {(isLoading || !!applications?.length) && ( diff --git a/packages/console/src/scss/_dimensions.scss b/packages/console/src/scss/_dimensions.scss index c4e473c34..78a92386c 100644 --- a/packages/console/src/scss/_dimensions.scss +++ b/packages/console/src/scss/_dimensions.scss @@ -6,3 +6,11 @@ $modal-layout-grid-large: 850px; $modal-layout-grid-medium: 668px; $modal-layout-grid-small: 500px; $form-text-field-width: 556px; + +// Guide related dimensions +$guide-main-content-max-width: 858px; +$guide-sidebar-width: 220px; +$guide-panel-gap: 30px; +$guide-content-padding: 24px; +$guide-content-max-width: calc($guide-main-content-max-width + 2 * ($guide-sidebar-width + $guide-panel-gap + $guide-content-padding)); +$guide-content-min-width: 750px; From edfb0e9950b7b9c4dc364f42dc21dd4aecf732ba Mon Sep 17 00:00:00 2001 From: Charles Zhao Date: Wed, 6 Sep 2023 23:23:21 +0800 Subject: [PATCH 24/30] fix(console): redirectUri input error message should be displayed properly (#4440) --- packages/console/src/mdx-components/UriInputField/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/console/src/mdx-components/UriInputField/index.tsx b/packages/console/src/mdx-components/UriInputField/index.tsx index 4afde81b5..6290984ea 100644 --- a/packages/console/src/mdx-components/UriInputField/index.tsx +++ b/packages/console/src/mdx-components/UriInputField/index.tsx @@ -89,7 +89,7 @@ function UriInputField({ name, defaultValue }: Props) { defaultValue={defaultValueArray} rules={{ validate: createValidatorForRhf({ - required: t('errors.required_field_missing_plural', { field: title }), + required: t('errors.required_field_missing_plural', { field: t(title) }), pattern: { verify: (value) => !value || uriValidator(value), message: t('errors.invalid_uri_format'), From 6565dad0e6943bd6c8df2c0e3567b75e00a1a582 Mon Sep 17 00:00:00 2001 From: Charles Zhao Date: Wed, 6 Sep 2023 23:42:11 +0800 Subject: [PATCH 25/30] fix(console): optimize loading screens in app guides (#4441) --- .../src/pages/ApplicationDetails/index.tsx | 23 +++++++----- .../Applications/components/Guide/index.tsx | 37 ++++++++++--------- .../components/GuideModal/index.tsx | 15 ++------ .../StepsSkeleton/index.module.scss | 3 ++ 4 files changed, 39 insertions(+), 39 deletions(-) diff --git a/packages/console/src/pages/ApplicationDetails/index.tsx b/packages/console/src/pages/ApplicationDetails/index.tsx index 54307f530..30c0670ab 100644 --- a/packages/console/src/pages/ApplicationDetails/index.tsx +++ b/packages/console/src/pages/ApplicationDetails/index.tsx @@ -52,7 +52,7 @@ const mapToUriOriginFormatArrays = (value?: string[]) => function ApplicationDetails() { const { id, guideId, tab } = useParams(); const { navigate, match } = useTenantPathname(); - const isGuideView = id && guideId && match(`/applications/${id}/guide/${guideId}`); + const isGuideView = !!id && !!guideId && match(`/applications/${id}/guide/${guideId}`); const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const { data, error, mutate } = useSWR( @@ -145,6 +145,18 @@ function ApplicationDetails() { setIsReadmeOpen(false); }; + if (isGuideView) { + return ( + { + navigate(`/applications/${id}`); + }} + /> + ); + } + return ( )} - {isGuideView && ( - { - navigate(`/applications/${id}`); - }} - /> - )} ); } diff --git a/packages/console/src/pages/Applications/components/Guide/index.tsx b/packages/console/src/pages/Applications/components/Guide/index.tsx index 88c92613e..70415d9f8 100644 --- a/packages/console/src/pages/Applications/components/Guide/index.tsx +++ b/packages/console/src/pages/Applications/components/Guide/index.tsx @@ -51,7 +51,7 @@ export const GuideContext = createContext({ type Props = { className?: string; guideId: string; - app: ApplicationResponse; + app?: ApplicationResponse; isCompact?: boolean; onClose: () => void; }; @@ -67,20 +67,21 @@ function Guide({ className, guideId, app, isCompact, onClose }: Props) { const memorizedContext = useMemo( () => conditional( - !!guide && { - metadata: guide.metadata, - Logo: guide.Logo, - app, - endpoint: tenantEndpoint?.toString() ?? '', - alternativeEndpoint: conditional(isCustomDomainActive && tenantEndpoint?.toString()), - redirectUris: app.oidcClientMetadata.redirectUris, - postLogoutRedirectUris: app.oidcClientMetadata.postLogoutRedirectUris, - isCompact: Boolean(isCompact), - sampleUrls: { - origin: 'http://localhost:3001/', - callback: 'http://localhost:3001/callback', - }, - } + !!guide && + !!app && { + metadata: guide.metadata, + Logo: guide.Logo, + app, + endpoint: tenantEndpoint?.toString() ?? '', + alternativeEndpoint: conditional(isCustomDomainActive && tenantEndpoint?.toString()), + redirectUris: app.oidcClientMetadata.redirectUris, + postLogoutRedirectUris: app.oidcClientMetadata.postLogoutRedirectUris, + isCompact: Boolean(isCompact), + sampleUrls: { + origin: 'http://localhost:3001/', + callback: 'http://localhost:3001/callback', + }, + } ) satisfies GuideContextType | undefined, [guide, app, tenantEndpoint, isCustomDomainActive, isCompact] ); @@ -88,7 +89,9 @@ function Guide({ className, guideId, app, isCompact, onClose }: Props) { return ( <> - {memorizedContext ? ( + {!app && } + {!!app && !guide && } + {memorizedContext && ( - ) : ( - )} {memorizedContext && ( diff --git a/packages/console/src/pages/Applications/components/GuideModal/index.tsx b/packages/console/src/pages/Applications/components/GuideModal/index.tsx index 9c06066f2..8528d5466 100644 --- a/packages/console/src/pages/Applications/components/GuideModal/index.tsx +++ b/packages/console/src/pages/Applications/components/GuideModal/index.tsx @@ -15,21 +15,14 @@ type Props = { }; function GuideModal({ guideId, app, onClose }: Props) { - if (!app) { - return null; - } - const closeModal = () => { - onClose(app.id); + if (app) { + onClose(app.id); + } }; return ( - +
diff --git a/packages/console/src/pages/Applications/components/StepsSkeleton/index.module.scss b/packages/console/src/pages/Applications/components/StepsSkeleton/index.module.scss index ca2252314..e221c5164 100644 --- a/packages/console/src/pages/Applications/components/StepsSkeleton/index.module.scss +++ b/packages/console/src/pages/Applications/components/StepsSkeleton/index.module.scss @@ -1,4 +1,5 @@ @use '@/scss/underscore' as _; +@use '@/scss/dimensions' as dim; .step { display: flex; @@ -6,6 +7,8 @@ padding: _.unit(5) _.unit(6); border-radius: 16px; background-color: var(--color-layer-1); + max-width: dim.$guide-main-content-max-width; + margin: 0 auto; .index { @include _.shimmering-animation; From 143f8b3943783c5bfa8e1a2b795e8d52f6949356 Mon Sep 17 00:00:00 2001 From: Charles Zhao Date: Wed, 6 Sep 2023 23:44:25 +0800 Subject: [PATCH 26/30] =?UTF-8?q?style(console):=20update=20section=20subt?= =?UTF-8?q?itle=20and=20inline=20code=20styles=20in=20app=E2=80=A6=20(#444?= =?UTF-8?q?2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit style(console): update section subtitle and inline code styles in app guide --- .../components/Guide/index.module.scss | 67 ++++++++++++------- 1 file changed, 41 insertions(+), 26 deletions(-) diff --git a/packages/console/src/pages/Applications/components/Guide/index.module.scss b/packages/console/src/pages/Applications/components/Guide/index.module.scss index d4545d414..47532418f 100644 --- a/packages/console/src/pages/Applications/components/Guide/index.module.scss +++ b/packages/console/src/pages/Applications/components/Guide/index.module.scss @@ -6,38 +6,53 @@ width: 100%; position: relative; - section p { - font: var(--font-body-2); - margin: _.unit(4) 0; - } - - section ul > li, - section ol > li { - font: var(--font-body-2); - margin-block: _.unit(2); - padding-inline-start: _.unit(1); - } - - section table { - border-spacing: 0; - border: 1px solid var(--color-border); - font: var(--font-body-2); - - tr { - width: 100%; + section { + h3 { + font: var(--font-title-2); + color: var(--color-text-secondary); + margin: _.unit(6) 0 _.unit(3); } - td, - th { - padding: _.unit(2) _.unit(4); + p { + font: var(--font-body-2); + margin: _.unit(4) 0; } - thead { - font: var(--font-title-3); + ul > li, + ol > li { + font: var(--font-body-2); + margin-block: _.unit(2); + padding-inline-start: _.unit(1); } - tbody td { - border-top: 1px solid var(--color-border); + table { + border-spacing: 0; + border: 1px solid var(--color-border); + font: var(--font-body-2); + + tr { + width: 100%; + } + + td, + th { + padding: _.unit(2) _.unit(4); + } + + thead { + font: var(--font-title-3); + } + + tbody td { + border-top: 1px solid var(--color-border); + } + } + + code { + background: var(--color-layer-2); + font: var(--font-body-2); + padding: _.unit(1) _.unit(1); + border-radius: 4px; } } } From ba2245bc0e31bfeea3d661f0ef7a19f837eb2808 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Thu, 7 Sep 2023 00:41:51 +0800 Subject: [PATCH 27/30] fix(toolkit): support plus and hyphen in mobile uri scheme (#4434) --- .../components/Settings.tsx | 3 ++- .../schemas/src/foundations/jsonb-types.ts | 23 +++++-------------- packages/toolkit/core-kit/src/regex.ts | 2 +- .../toolkit/core-kit/src/utils/url.test.ts | 2 ++ 4 files changed, 11 insertions(+), 19 deletions(-) diff --git a/packages/console/src/pages/ApplicationDetails/components/Settings.tsx b/packages/console/src/pages/ApplicationDetails/components/Settings.tsx index 0accb4495..4a1f631df 100644 --- a/packages/console/src/pages/ApplicationDetails/components/Settings.tsx +++ b/packages/console/src/pages/ApplicationDetails/components/Settings.tsx @@ -1,5 +1,6 @@ +import { validateRedirectUrl } from '@logto/core-kit'; import type { Application } from '@logto/schemas'; -import { ApplicationType, validateRedirectUrl } from '@logto/schemas'; +import { ApplicationType } from '@logto/schemas'; import { useContext } from 'react'; import { Controller, useFormContext } from 'react-hook-form'; import { Trans, useTranslation } from 'react-i18next'; diff --git a/packages/schemas/src/foundations/jsonb-types.ts b/packages/schemas/src/foundations/jsonb-types.ts index 976d3b34c..24dc688df 100644 --- a/packages/schemas/src/foundations/jsonb-types.ts +++ b/packages/schemas/src/foundations/jsonb-types.ts @@ -1,4 +1,9 @@ -import { type PasswordPolicy, hexColorRegEx, passwordPolicyGuard } from '@logto/core-kit'; +import { + type PasswordPolicy, + hexColorRegEx, + passwordPolicyGuard, + validateRedirectUrl, +} from '@logto/core-kit'; import { languageTagGuard } from '@logto/language-kit'; import { type DeepPartial } from '@silverhand/essentials'; import type { Json } from '@withtyped/server'; @@ -37,22 +42,6 @@ export const oidcModelInstancePayloadGuard = z export type OidcModelInstancePayload = z.infer; -// Import from @logto/core-kit later, pending for new version publish -export const webRedirectUriProtocolRegEx = /^https?:$/; -export const mobileUriSchemeProtocolRegEx = /^[a-z][\d_a-z]*(\.[\d_a-z]+)+:$/; - -export const validateRedirectUrl = (urlString: string, type: 'web' | 'mobile') => { - try { - const { protocol } = new URL(urlString); - const protocolRegEx = - type === 'mobile' ? mobileUriSchemeProtocolRegEx : webRedirectUriProtocolRegEx; - - return protocolRegEx.test(protocol); - } catch { - return false; - } -}; - export const oidcClientMetadataGuard = z.object({ redirectUris: z .string() diff --git a/packages/toolkit/core-kit/src/regex.ts b/packages/toolkit/core-kit/src/regex.ts index 138852aea..3b010ad08 100644 --- a/packages/toolkit/core-kit/src/regex.ts +++ b/packages/toolkit/core-kit/src/regex.ts @@ -3,7 +3,7 @@ export const phoneRegEx = /^\d+$/; export const phoneInputRegEx = /^\+?[\d-( )]+$/; export const usernameRegEx = /^[A-Z_a-z]\w*$/; export const webRedirectUriProtocolRegEx = /^https?:$/; -export const mobileUriSchemeProtocolRegEx = /^[a-z][\d_a-z]*(\.[\d_a-z]+)+:$/; +export const mobileUriSchemeProtocolRegEx = /^[a-z][\d+_a-z-]*(\.[\d+_a-z-]+)+:$/; export const hexColorRegEx = /^#[\da-f]{3}([\da-f]{3})?$/i; export const dateRegex = /^\d{4}(-\d{2}){2}/; export const noSpaceRegEx = /^\S+$/; diff --git a/packages/toolkit/core-kit/src/utils/url.test.ts b/packages/toolkit/core-kit/src/utils/url.test.ts index 6f4aa588b..68afb1495 100644 --- a/packages/toolkit/core-kit/src/utils/url.test.ts +++ b/packages/toolkit/core-kit/src/utils/url.test.ts @@ -9,6 +9,8 @@ describe('url utilities', () => { expect(validateRedirectUrl('io.logto://my-app/callback', 'mobile')).toBeTruthy(); expect(validateRedirectUrl('com.company://myDemoApp/callback', 'mobile')).toBeTruthy(); expect(validateRedirectUrl('com.company://demo:1234', 'mobile')).toBeTruthy(); + expect(validateRedirectUrl('io.logto.SwiftUI-Demo://callback', 'mobile')).toBeTruthy(); + expect(validateRedirectUrl('io.logto.SwiftUI+Demo://callback', 'mobile')).toBeTruthy(); }); it('should detect invalid redirect URIs', () => { From fd08426935c11e266fd7e4bcfc1de8b31d7bd160 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Thu, 7 Sep 2023 17:32:55 +0800 Subject: [PATCH 28/30] chore: reorg webhook-related file structures (#4447) --- .../components/BasicWebhookForm/index.module.scss | 0 .../components/BasicWebhookForm/index.tsx | 10 +++++++--- .../Webhooks => }/components/SuccessRate/index.tsx | 0 .../CustomHeaderField/index.module.scss | 0 .../{components => }/CustomHeaderField/index.tsx | 0 .../{components => }/SigningKeyField/index.module.scss | 0 .../{components => }/SigningKeyField/index.tsx | 0 .../{components => }/TestWebhook/index.module.scss | 0 .../{components => }/TestWebhook/index.tsx | 0 .../src/pages/WebhookDetails/WebhookSettings/index.tsx | 8 ++++---- packages/console/src/pages/WebhookDetails/index.tsx | 3 +-- packages/console/src/pages/WebhookDetails/types.ts | 2 +- .../{components => }/CreateFormModal/CreateForm.tsx | 4 +--- .../{components => }/CreateFormModal/index.tsx | 0 packages/console/src/pages/Webhooks/index.tsx | 4 ++-- packages/console/src/pages/Webhooks/types.ts | 7 ------- 16 files changed, 16 insertions(+), 22 deletions(-) rename packages/console/src/{pages/Webhooks => }/components/BasicWebhookForm/index.module.scss (100%) rename packages/console/src/{pages/Webhooks => }/components/BasicWebhookForm/index.tsx (93%) rename packages/console/src/{pages/Webhooks => }/components/SuccessRate/index.tsx (100%) rename packages/console/src/pages/WebhookDetails/WebhookSettings/{components => }/CustomHeaderField/index.module.scss (100%) rename packages/console/src/pages/WebhookDetails/WebhookSettings/{components => }/CustomHeaderField/index.tsx (100%) rename packages/console/src/pages/WebhookDetails/WebhookSettings/{components => }/SigningKeyField/index.module.scss (100%) rename packages/console/src/pages/WebhookDetails/WebhookSettings/{components => }/SigningKeyField/index.tsx (100%) rename packages/console/src/pages/WebhookDetails/WebhookSettings/{components => }/TestWebhook/index.module.scss (100%) rename packages/console/src/pages/WebhookDetails/WebhookSettings/{components => }/TestWebhook/index.tsx (100%) rename packages/console/src/pages/Webhooks/{components => }/CreateFormModal/CreateForm.tsx (96%) rename packages/console/src/pages/Webhooks/{components => }/CreateFormModal/index.tsx (100%) delete mode 100644 packages/console/src/pages/Webhooks/types.ts diff --git a/packages/console/src/pages/Webhooks/components/BasicWebhookForm/index.module.scss b/packages/console/src/components/BasicWebhookForm/index.module.scss similarity index 100% rename from packages/console/src/pages/Webhooks/components/BasicWebhookForm/index.module.scss rename to packages/console/src/components/BasicWebhookForm/index.module.scss diff --git a/packages/console/src/pages/Webhooks/components/BasicWebhookForm/index.tsx b/packages/console/src/components/BasicWebhookForm/index.tsx similarity index 93% rename from packages/console/src/pages/Webhooks/components/BasicWebhookForm/index.tsx rename to packages/console/src/components/BasicWebhookForm/index.tsx index fc16b7b95..02a04b2d5 100644 --- a/packages/console/src/pages/Webhooks/components/BasicWebhookForm/index.tsx +++ b/packages/console/src/components/BasicWebhookForm/index.tsx @@ -1,4 +1,4 @@ -import { HookEvent } from '@logto/schemas'; +import { HookEvent, type Hook, type HookConfig } from '@logto/schemas'; import { Controller, useFormContext } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; @@ -8,8 +8,6 @@ import FormField from '@/ds-components/FormField'; import TextInput from '@/ds-components/TextInput'; import { uriValidator } from '@/utils/validator'; -import { type BasicWebhookFormType } from '../../types'; - import * as styles from './index.module.scss'; const hookEventOptions = Object.values(HookEvent).map((event) => ({ @@ -17,6 +15,12 @@ const hookEventOptions = Object.values(HookEvent).map((event) => ({ value: event, })); +export type BasicWebhookFormType = { + name: Hook['name']; + events: HookEvent[]; + url: HookConfig['url']; +}; + function BasicWebhookForm() { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const { diff --git a/packages/console/src/pages/Webhooks/components/SuccessRate/index.tsx b/packages/console/src/components/SuccessRate/index.tsx similarity index 100% rename from packages/console/src/pages/Webhooks/components/SuccessRate/index.tsx rename to packages/console/src/components/SuccessRate/index.tsx diff --git a/packages/console/src/pages/WebhookDetails/WebhookSettings/components/CustomHeaderField/index.module.scss b/packages/console/src/pages/WebhookDetails/WebhookSettings/CustomHeaderField/index.module.scss similarity index 100% rename from packages/console/src/pages/WebhookDetails/WebhookSettings/components/CustomHeaderField/index.module.scss rename to packages/console/src/pages/WebhookDetails/WebhookSettings/CustomHeaderField/index.module.scss diff --git a/packages/console/src/pages/WebhookDetails/WebhookSettings/components/CustomHeaderField/index.tsx b/packages/console/src/pages/WebhookDetails/WebhookSettings/CustomHeaderField/index.tsx similarity index 100% rename from packages/console/src/pages/WebhookDetails/WebhookSettings/components/CustomHeaderField/index.tsx rename to packages/console/src/pages/WebhookDetails/WebhookSettings/CustomHeaderField/index.tsx diff --git a/packages/console/src/pages/WebhookDetails/WebhookSettings/components/SigningKeyField/index.module.scss b/packages/console/src/pages/WebhookDetails/WebhookSettings/SigningKeyField/index.module.scss similarity index 100% rename from packages/console/src/pages/WebhookDetails/WebhookSettings/components/SigningKeyField/index.module.scss rename to packages/console/src/pages/WebhookDetails/WebhookSettings/SigningKeyField/index.module.scss diff --git a/packages/console/src/pages/WebhookDetails/WebhookSettings/components/SigningKeyField/index.tsx b/packages/console/src/pages/WebhookDetails/WebhookSettings/SigningKeyField/index.tsx similarity index 100% rename from packages/console/src/pages/WebhookDetails/WebhookSettings/components/SigningKeyField/index.tsx rename to packages/console/src/pages/WebhookDetails/WebhookSettings/SigningKeyField/index.tsx diff --git a/packages/console/src/pages/WebhookDetails/WebhookSettings/components/TestWebhook/index.module.scss b/packages/console/src/pages/WebhookDetails/WebhookSettings/TestWebhook/index.module.scss similarity index 100% rename from packages/console/src/pages/WebhookDetails/WebhookSettings/components/TestWebhook/index.module.scss rename to packages/console/src/pages/WebhookDetails/WebhookSettings/TestWebhook/index.module.scss diff --git a/packages/console/src/pages/WebhookDetails/WebhookSettings/components/TestWebhook/index.tsx b/packages/console/src/pages/WebhookDetails/WebhookSettings/TestWebhook/index.tsx similarity index 100% rename from packages/console/src/pages/WebhookDetails/WebhookSettings/components/TestWebhook/index.tsx rename to packages/console/src/pages/WebhookDetails/WebhookSettings/TestWebhook/index.tsx diff --git a/packages/console/src/pages/WebhookDetails/WebhookSettings/index.tsx b/packages/console/src/pages/WebhookDetails/WebhookSettings/index.tsx index e21837d37..7d0eba963 100644 --- a/packages/console/src/pages/WebhookDetails/WebhookSettings/index.tsx +++ b/packages/console/src/pages/WebhookDetails/WebhookSettings/index.tsx @@ -4,20 +4,20 @@ import { toast } from 'react-hot-toast'; import { useTranslation } from 'react-i18next'; import { useOutletContext } from 'react-router-dom'; +import BasicWebhookForm from '@/components/BasicWebhookForm'; import DetailsForm from '@/components/DetailsForm'; import FormCard from '@/components/FormCard'; import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal'; import useApi from '@/hooks/use-api'; import useDocumentationUrl from '@/hooks/use-documentation-url'; -import BasicWebhookForm from '@/pages/Webhooks/components/BasicWebhookForm'; import { trySubmitSafe } from '@/utils/form'; import { type WebhookDetailsFormType, type WebhookDetailsOutletContext } from '../types'; import { webhookDetailsParser } from '../utils'; -import CustomHeaderField from './components/CustomHeaderField'; -import SigningKeyField from './components/SigningKeyField'; -import TestWebhook from './components/TestWebhook'; +import CustomHeaderField from './CustomHeaderField'; +import SigningKeyField from './SigningKeyField'; +import TestWebhook from './TestWebhook'; function WebhookSettings() { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); diff --git a/packages/console/src/pages/WebhookDetails/index.tsx b/packages/console/src/pages/WebhookDetails/index.tsx index 43101b4af..6b33e44c9 100644 --- a/packages/console/src/pages/WebhookDetails/index.tsx +++ b/packages/console/src/pages/WebhookDetails/index.tsx @@ -15,6 +15,7 @@ import WebhookDark from '@/assets/icons/webhook-dark.svg'; import Webhook from '@/assets/icons/webhook.svg'; import DetailsPage from '@/components/DetailsPage'; import PageMeta from '@/components/PageMeta'; +import SuccessRate from '@/components/SuccessRate'; import { WebhookDetailsTabs } from '@/consts'; import ActionMenu, { ActionMenuItem } from '@/ds-components/ActionMenu'; import Card from '@/ds-components/Card'; @@ -28,8 +29,6 @@ import useTenantPathname from '@/hooks/use-tenant-pathname'; import useTheme from '@/hooks/use-theme'; import { buildUrl } from '@/utils/url'; -import SuccessRate from '../Webhooks/components/SuccessRate'; - import * as styles from './index.module.scss'; import { type WebhookDetailsOutletContext } from './types'; diff --git a/packages/console/src/pages/WebhookDetails/types.ts b/packages/console/src/pages/WebhookDetails/types.ts index df8d3152a..944057005 100644 --- a/packages/console/src/pages/WebhookDetails/types.ts +++ b/packages/console/src/pages/WebhookDetails/types.ts @@ -1,6 +1,6 @@ import { type HookResponse, type Hook } from '@logto/schemas'; -import { type BasicWebhookFormType } from '../Webhooks/types'; +import { type BasicWebhookFormType } from '@/components/BasicWebhookForm'; export type WebhookDetailsOutletContext = { hook: HookResponse; diff --git a/packages/console/src/pages/Webhooks/components/CreateFormModal/CreateForm.tsx b/packages/console/src/pages/Webhooks/CreateFormModal/CreateForm.tsx similarity index 96% rename from packages/console/src/pages/Webhooks/components/CreateFormModal/CreateForm.tsx rename to packages/console/src/pages/Webhooks/CreateFormModal/CreateForm.tsx index 5231a0a3c..3b8019aeb 100644 --- a/packages/console/src/pages/Webhooks/components/CreateFormModal/CreateForm.tsx +++ b/packages/console/src/pages/Webhooks/CreateFormModal/CreateForm.tsx @@ -3,6 +3,7 @@ import { useContext } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; import { Trans, useTranslation } from 'react-i18next'; +import BasicWebhookForm, { type BasicWebhookFormType } from '@/components/BasicWebhookForm'; import ContactUsPhraseLink from '@/components/ContactUsPhraseLink'; import PlanName from '@/components/PlanName'; import QuotaGuardFooter from '@/components/QuotaGuardFooter'; @@ -14,9 +15,6 @@ import useSubscriptionPlan from '@/hooks/use-subscription-plan'; import { trySubmitSafe } from '@/utils/form'; import { hasReachedQuotaLimit } from '@/utils/quota'; -import { type BasicWebhookFormType } from '../../types'; -import BasicWebhookForm from '../BasicWebhookForm'; - type Props = { totalWebhookCount: number; onClose: (createdHook?: Hook) => void; diff --git a/packages/console/src/pages/Webhooks/components/CreateFormModal/index.tsx b/packages/console/src/pages/Webhooks/CreateFormModal/index.tsx similarity index 100% rename from packages/console/src/pages/Webhooks/components/CreateFormModal/index.tsx rename to packages/console/src/pages/Webhooks/CreateFormModal/index.tsx diff --git a/packages/console/src/pages/Webhooks/index.tsx b/packages/console/src/pages/Webhooks/index.tsx index a0f5bd22c..f9b161a9d 100644 --- a/packages/console/src/pages/Webhooks/index.tsx +++ b/packages/console/src/pages/Webhooks/index.tsx @@ -13,6 +13,7 @@ import WebhooksEmptyDark from '@/assets/images/webhooks-empty-dark.svg'; import WebhooksEmpty from '@/assets/images/webhooks-empty.svg'; import ItemPreview from '@/components/ItemPreview'; import ListPage from '@/components/ListPage'; +import SuccessRate from '@/components/SuccessRate'; import { defaultPageSize } from '@/consts'; import { hookEventLabel } from '@/consts/webhooks'; import Button from '@/ds-components/Button'; @@ -25,8 +26,7 @@ import useTenantPathname from '@/hooks/use-tenant-pathname'; import useTheme from '@/hooks/use-theme'; import { buildUrl } from '@/utils/url'; -import CreateFormModal from './components/CreateFormModal'; -import SuccessRate from './components/SuccessRate'; +import CreateFormModal from './CreateFormModal'; import * as styles from './index.module.scss'; const pageSize = defaultPageSize; diff --git a/packages/console/src/pages/Webhooks/types.ts b/packages/console/src/pages/Webhooks/types.ts deleted file mode 100644 index 42432e313..000000000 --- a/packages/console/src/pages/Webhooks/types.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { type Hook, type HookConfig, type HookEvent } from '@logto/schemas'; - -export type BasicWebhookFormType = { - name: Hook['name']; - events: HookEvent[]; - url: HookConfig['url']; -}; From dbdc63238191b752e1e1bcf9b0b26369100cc5f6 Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Thu, 7 Sep 2023 17:35:27 +0800 Subject: [PATCH 29/30] refactor(ui): align hook style --- packages/ui/src/hooks/use-password-action.ts | 6 ++---- packages/ui/src/pages/Continue/SetPassword/index.tsx | 2 +- packages/ui/src/pages/RegisterPassword/index.tsx | 2 +- packages/ui/src/pages/ResetPassword/index.tsx | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/ui/src/hooks/use-password-action.ts b/packages/ui/src/hooks/use-password-action.ts index 9432308a6..e6275d055 100644 --- a/packages/ui/src/hooks/use-password-action.ts +++ b/packages/ui/src/hooks/use-password-action.ts @@ -26,7 +26,7 @@ const usePasswordAction = ({ errorHandlers, setErrorMessage, successHandler, -}: UsePasswordApiInit) => { +}: UsePasswordApiInit): [PasswordAction] => { const asyncAction = useApi(api); const handleError = useErrorHandler(); const { getErrorMessage, getErrorMessageFromBody } = usePasswordErrorMessage(); @@ -72,9 +72,7 @@ const usePasswordAction = ({ ] ); - return { - action, - }; + return [action]; }; export default usePasswordAction; diff --git a/packages/ui/src/pages/Continue/SetPassword/index.tsx b/packages/ui/src/pages/Continue/SetPassword/index.tsx index c0a79ebe3..a6131dd89 100644 --- a/packages/ui/src/pages/Continue/SetPassword/index.tsx +++ b/packages/ui/src/pages/Continue/SetPassword/index.tsx @@ -35,7 +35,7 @@ const SetPassword = () => { window.location.replace(result.redirectTo); } }, []); - const { action } = usePasswordAction({ + const [action] = usePasswordAction({ api: async (password) => addProfile({ password }), setErrorMessage, errorHandlers, diff --git a/packages/ui/src/pages/RegisterPassword/index.tsx b/packages/ui/src/pages/RegisterPassword/index.tsx index 9db7f7a84..1adf665d9 100644 --- a/packages/ui/src/pages/RegisterPassword/index.tsx +++ b/packages/ui/src/pages/RegisterPassword/index.tsx @@ -37,7 +37,7 @@ const RegisterPassword = () => { } }, []); - const { action } = usePasswordAction({ + const [action] = usePasswordAction({ api: setUserPassword, setErrorMessage, errorHandlers, diff --git a/packages/ui/src/pages/ResetPassword/index.tsx b/packages/ui/src/pages/ResetPassword/index.tsx index b55faea9a..68b691501 100644 --- a/packages/ui/src/pages/ResetPassword/index.tsx +++ b/packages/ui/src/pages/ResetPassword/index.tsx @@ -42,7 +42,7 @@ const ResetPassword = () => { [navigate, setToast, t] ); - const { action } = usePasswordAction({ + const [action] = usePasswordAction({ api: setUserPassword, setErrorMessage, errorHandlers, From 7846386a90944ae3cc7b947e5e3564ace85b7f8e Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Thu, 7 Sep 2023 17:57:28 +0800 Subject: [PATCH 30/30] refactor(ui): use native list translation --- .../src/locales/de/error/password-rejected.ts | 2 +- .../src/locales/en/error/password-rejected.ts | 2 +- .../src/locales/es/error/password-rejected.ts | 2 +- .../src/locales/fr/error/password-rejected.ts | 2 +- .../src/locales/it/error/password-rejected.ts | 2 +- .../src/locales/ja/error/password-rejected.ts | 2 +- .../src/locales/ko/error/password-rejected.ts | 2 +- .../locales/pl-pl/error/password-rejected.ts | 2 +- .../locales/pt-br/error/password-rejected.ts | 2 +- .../locales/pt-pt/error/password-rejected.ts | 2 +- .../src/locales/ru/error/password-rejected.ts | 2 +- .../locales/tr-tr/error/password-rejected.ts | 2 +- .../locales/zh-cn/error/password-rejected.ts | 2 +- .../locales/zh-hk/error/password-rejected.ts | 2 +- .../locales/zh-tw/error/password-rejected.ts | 2 +- .../ui/src/hooks/use-list-translation.test.ts | 107 ------------------ packages/ui/src/hooks/use-list-translation.ts | 78 ------------- .../src/hooks/use-password-error-message.ts | 7 +- 18 files changed, 17 insertions(+), 205 deletions(-) delete mode 100644 packages/ui/src/hooks/use-list-translation.test.ts delete mode 100644 packages/ui/src/hooks/use-list-translation.ts diff --git a/packages/phrases-ui/src/locales/de/error/password-rejected.ts b/packages/phrases-ui/src/locales/de/error/password-rejected.ts index fad0b338a..85ed217c0 100644 --- a/packages/phrases-ui/src/locales/de/error/password-rejected.ts +++ b/packages/phrases-ui/src/locales/de/error/password-rejected.ts @@ -4,7 +4,7 @@ const password_rejected = { character_types: 'At least {{min}} types of characters are required.', // UNTRANSLATED unsupported_characters: 'Unsupported character found.', // UNTRANSLATED pwned: 'Avoid using simple passwords that are easy to guess.', // UNTRANSLATED - restricted_found: 'Avoid overusing {{list}}.', // UNTRANSLATED + restricted_found: 'Avoid overusing {{list, list}}.', // UNTRANSLATED 'restricted.repetition': 'repeated characters', // UNTRANSLATED 'restricted.sequence': 'sequential characters', // UNTRANSLATED 'restricted.personal_info': 'your personal information', // UNTRANSLATED diff --git a/packages/phrases-ui/src/locales/en/error/password-rejected.ts b/packages/phrases-ui/src/locales/en/error/password-rejected.ts index d7311965a..c4fd6ed5c 100644 --- a/packages/phrases-ui/src/locales/en/error/password-rejected.ts +++ b/packages/phrases-ui/src/locales/en/error/password-rejected.ts @@ -6,7 +6,7 @@ const password_rejected = { character_types: 'At least {{min}} types of characters are required.', unsupported_characters: 'Unsupported character found.', pwned: 'Avoid using simple passwords that are easy to guess.', - restricted_found: 'Avoid overusing {{list}}.', + restricted_found: 'Avoid overusing {{list, list}}.', 'restricted.repetition': 'repeated characters', 'restricted.sequence': 'sequential characters', 'restricted.personal_info': 'your personal information', diff --git a/packages/phrases-ui/src/locales/es/error/password-rejected.ts b/packages/phrases-ui/src/locales/es/error/password-rejected.ts index fad0b338a..85ed217c0 100644 --- a/packages/phrases-ui/src/locales/es/error/password-rejected.ts +++ b/packages/phrases-ui/src/locales/es/error/password-rejected.ts @@ -4,7 +4,7 @@ const password_rejected = { character_types: 'At least {{min}} types of characters are required.', // UNTRANSLATED unsupported_characters: 'Unsupported character found.', // UNTRANSLATED pwned: 'Avoid using simple passwords that are easy to guess.', // UNTRANSLATED - restricted_found: 'Avoid overusing {{list}}.', // UNTRANSLATED + restricted_found: 'Avoid overusing {{list, list}}.', // UNTRANSLATED 'restricted.repetition': 'repeated characters', // UNTRANSLATED 'restricted.sequence': 'sequential characters', // UNTRANSLATED 'restricted.personal_info': 'your personal information', // UNTRANSLATED diff --git a/packages/phrases-ui/src/locales/fr/error/password-rejected.ts b/packages/phrases-ui/src/locales/fr/error/password-rejected.ts index fad0b338a..85ed217c0 100644 --- a/packages/phrases-ui/src/locales/fr/error/password-rejected.ts +++ b/packages/phrases-ui/src/locales/fr/error/password-rejected.ts @@ -4,7 +4,7 @@ const password_rejected = { character_types: 'At least {{min}} types of characters are required.', // UNTRANSLATED unsupported_characters: 'Unsupported character found.', // UNTRANSLATED pwned: 'Avoid using simple passwords that are easy to guess.', // UNTRANSLATED - restricted_found: 'Avoid overusing {{list}}.', // UNTRANSLATED + restricted_found: 'Avoid overusing {{list, list}}.', // UNTRANSLATED 'restricted.repetition': 'repeated characters', // UNTRANSLATED 'restricted.sequence': 'sequential characters', // UNTRANSLATED 'restricted.personal_info': 'your personal information', // UNTRANSLATED diff --git a/packages/phrases-ui/src/locales/it/error/password-rejected.ts b/packages/phrases-ui/src/locales/it/error/password-rejected.ts index fad0b338a..85ed217c0 100644 --- a/packages/phrases-ui/src/locales/it/error/password-rejected.ts +++ b/packages/phrases-ui/src/locales/it/error/password-rejected.ts @@ -4,7 +4,7 @@ const password_rejected = { character_types: 'At least {{min}} types of characters are required.', // UNTRANSLATED unsupported_characters: 'Unsupported character found.', // UNTRANSLATED pwned: 'Avoid using simple passwords that are easy to guess.', // UNTRANSLATED - restricted_found: 'Avoid overusing {{list}}.', // UNTRANSLATED + restricted_found: 'Avoid overusing {{list, list}}.', // UNTRANSLATED 'restricted.repetition': 'repeated characters', // UNTRANSLATED 'restricted.sequence': 'sequential characters', // UNTRANSLATED 'restricted.personal_info': 'your personal information', // UNTRANSLATED diff --git a/packages/phrases-ui/src/locales/ja/error/password-rejected.ts b/packages/phrases-ui/src/locales/ja/error/password-rejected.ts index fad0b338a..85ed217c0 100644 --- a/packages/phrases-ui/src/locales/ja/error/password-rejected.ts +++ b/packages/phrases-ui/src/locales/ja/error/password-rejected.ts @@ -4,7 +4,7 @@ const password_rejected = { character_types: 'At least {{min}} types of characters are required.', // UNTRANSLATED unsupported_characters: 'Unsupported character found.', // UNTRANSLATED pwned: 'Avoid using simple passwords that are easy to guess.', // UNTRANSLATED - restricted_found: 'Avoid overusing {{list}}.', // UNTRANSLATED + restricted_found: 'Avoid overusing {{list, list}}.', // UNTRANSLATED 'restricted.repetition': 'repeated characters', // UNTRANSLATED 'restricted.sequence': 'sequential characters', // UNTRANSLATED 'restricted.personal_info': 'your personal information', // UNTRANSLATED diff --git a/packages/phrases-ui/src/locales/ko/error/password-rejected.ts b/packages/phrases-ui/src/locales/ko/error/password-rejected.ts index fad0b338a..85ed217c0 100644 --- a/packages/phrases-ui/src/locales/ko/error/password-rejected.ts +++ b/packages/phrases-ui/src/locales/ko/error/password-rejected.ts @@ -4,7 +4,7 @@ const password_rejected = { character_types: 'At least {{min}} types of characters are required.', // UNTRANSLATED unsupported_characters: 'Unsupported character found.', // UNTRANSLATED pwned: 'Avoid using simple passwords that are easy to guess.', // UNTRANSLATED - restricted_found: 'Avoid overusing {{list}}.', // UNTRANSLATED + restricted_found: 'Avoid overusing {{list, list}}.', // UNTRANSLATED 'restricted.repetition': 'repeated characters', // UNTRANSLATED 'restricted.sequence': 'sequential characters', // UNTRANSLATED 'restricted.personal_info': 'your personal information', // UNTRANSLATED diff --git a/packages/phrases-ui/src/locales/pl-pl/error/password-rejected.ts b/packages/phrases-ui/src/locales/pl-pl/error/password-rejected.ts index fad0b338a..85ed217c0 100644 --- a/packages/phrases-ui/src/locales/pl-pl/error/password-rejected.ts +++ b/packages/phrases-ui/src/locales/pl-pl/error/password-rejected.ts @@ -4,7 +4,7 @@ const password_rejected = { character_types: 'At least {{min}} types of characters are required.', // UNTRANSLATED unsupported_characters: 'Unsupported character found.', // UNTRANSLATED pwned: 'Avoid using simple passwords that are easy to guess.', // UNTRANSLATED - restricted_found: 'Avoid overusing {{list}}.', // UNTRANSLATED + restricted_found: 'Avoid overusing {{list, list}}.', // UNTRANSLATED 'restricted.repetition': 'repeated characters', // UNTRANSLATED 'restricted.sequence': 'sequential characters', // UNTRANSLATED 'restricted.personal_info': 'your personal information', // UNTRANSLATED diff --git a/packages/phrases-ui/src/locales/pt-br/error/password-rejected.ts b/packages/phrases-ui/src/locales/pt-br/error/password-rejected.ts index fad0b338a..85ed217c0 100644 --- a/packages/phrases-ui/src/locales/pt-br/error/password-rejected.ts +++ b/packages/phrases-ui/src/locales/pt-br/error/password-rejected.ts @@ -4,7 +4,7 @@ const password_rejected = { character_types: 'At least {{min}} types of characters are required.', // UNTRANSLATED unsupported_characters: 'Unsupported character found.', // UNTRANSLATED pwned: 'Avoid using simple passwords that are easy to guess.', // UNTRANSLATED - restricted_found: 'Avoid overusing {{list}}.', // UNTRANSLATED + restricted_found: 'Avoid overusing {{list, list}}.', // UNTRANSLATED 'restricted.repetition': 'repeated characters', // UNTRANSLATED 'restricted.sequence': 'sequential characters', // UNTRANSLATED 'restricted.personal_info': 'your personal information', // UNTRANSLATED diff --git a/packages/phrases-ui/src/locales/pt-pt/error/password-rejected.ts b/packages/phrases-ui/src/locales/pt-pt/error/password-rejected.ts index fad0b338a..85ed217c0 100644 --- a/packages/phrases-ui/src/locales/pt-pt/error/password-rejected.ts +++ b/packages/phrases-ui/src/locales/pt-pt/error/password-rejected.ts @@ -4,7 +4,7 @@ const password_rejected = { character_types: 'At least {{min}} types of characters are required.', // UNTRANSLATED unsupported_characters: 'Unsupported character found.', // UNTRANSLATED pwned: 'Avoid using simple passwords that are easy to guess.', // UNTRANSLATED - restricted_found: 'Avoid overusing {{list}}.', // UNTRANSLATED + restricted_found: 'Avoid overusing {{list, list}}.', // UNTRANSLATED 'restricted.repetition': 'repeated characters', // UNTRANSLATED 'restricted.sequence': 'sequential characters', // UNTRANSLATED 'restricted.personal_info': 'your personal information', // UNTRANSLATED diff --git a/packages/phrases-ui/src/locales/ru/error/password-rejected.ts b/packages/phrases-ui/src/locales/ru/error/password-rejected.ts index fad0b338a..85ed217c0 100644 --- a/packages/phrases-ui/src/locales/ru/error/password-rejected.ts +++ b/packages/phrases-ui/src/locales/ru/error/password-rejected.ts @@ -4,7 +4,7 @@ const password_rejected = { character_types: 'At least {{min}} types of characters are required.', // UNTRANSLATED unsupported_characters: 'Unsupported character found.', // UNTRANSLATED pwned: 'Avoid using simple passwords that are easy to guess.', // UNTRANSLATED - restricted_found: 'Avoid overusing {{list}}.', // UNTRANSLATED + restricted_found: 'Avoid overusing {{list, list}}.', // UNTRANSLATED 'restricted.repetition': 'repeated characters', // UNTRANSLATED 'restricted.sequence': 'sequential characters', // UNTRANSLATED 'restricted.personal_info': 'your personal information', // UNTRANSLATED diff --git a/packages/phrases-ui/src/locales/tr-tr/error/password-rejected.ts b/packages/phrases-ui/src/locales/tr-tr/error/password-rejected.ts index fad0b338a..85ed217c0 100644 --- a/packages/phrases-ui/src/locales/tr-tr/error/password-rejected.ts +++ b/packages/phrases-ui/src/locales/tr-tr/error/password-rejected.ts @@ -4,7 +4,7 @@ const password_rejected = { character_types: 'At least {{min}} types of characters are required.', // UNTRANSLATED unsupported_characters: 'Unsupported character found.', // UNTRANSLATED pwned: 'Avoid using simple passwords that are easy to guess.', // UNTRANSLATED - restricted_found: 'Avoid overusing {{list}}.', // UNTRANSLATED + restricted_found: 'Avoid overusing {{list, list}}.', // UNTRANSLATED 'restricted.repetition': 'repeated characters', // UNTRANSLATED 'restricted.sequence': 'sequential characters', // UNTRANSLATED 'restricted.personal_info': 'your personal information', // UNTRANSLATED diff --git a/packages/phrases-ui/src/locales/zh-cn/error/password-rejected.ts b/packages/phrases-ui/src/locales/zh-cn/error/password-rejected.ts index fad0b338a..85ed217c0 100644 --- a/packages/phrases-ui/src/locales/zh-cn/error/password-rejected.ts +++ b/packages/phrases-ui/src/locales/zh-cn/error/password-rejected.ts @@ -4,7 +4,7 @@ const password_rejected = { character_types: 'At least {{min}} types of characters are required.', // UNTRANSLATED unsupported_characters: 'Unsupported character found.', // UNTRANSLATED pwned: 'Avoid using simple passwords that are easy to guess.', // UNTRANSLATED - restricted_found: 'Avoid overusing {{list}}.', // UNTRANSLATED + restricted_found: 'Avoid overusing {{list, list}}.', // UNTRANSLATED 'restricted.repetition': 'repeated characters', // UNTRANSLATED 'restricted.sequence': 'sequential characters', // UNTRANSLATED 'restricted.personal_info': 'your personal information', // UNTRANSLATED diff --git a/packages/phrases-ui/src/locales/zh-hk/error/password-rejected.ts b/packages/phrases-ui/src/locales/zh-hk/error/password-rejected.ts index fad0b338a..85ed217c0 100644 --- a/packages/phrases-ui/src/locales/zh-hk/error/password-rejected.ts +++ b/packages/phrases-ui/src/locales/zh-hk/error/password-rejected.ts @@ -4,7 +4,7 @@ const password_rejected = { character_types: 'At least {{min}} types of characters are required.', // UNTRANSLATED unsupported_characters: 'Unsupported character found.', // UNTRANSLATED pwned: 'Avoid using simple passwords that are easy to guess.', // UNTRANSLATED - restricted_found: 'Avoid overusing {{list}}.', // UNTRANSLATED + restricted_found: 'Avoid overusing {{list, list}}.', // UNTRANSLATED 'restricted.repetition': 'repeated characters', // UNTRANSLATED 'restricted.sequence': 'sequential characters', // UNTRANSLATED 'restricted.personal_info': 'your personal information', // UNTRANSLATED diff --git a/packages/phrases-ui/src/locales/zh-tw/error/password-rejected.ts b/packages/phrases-ui/src/locales/zh-tw/error/password-rejected.ts index fad0b338a..85ed217c0 100644 --- a/packages/phrases-ui/src/locales/zh-tw/error/password-rejected.ts +++ b/packages/phrases-ui/src/locales/zh-tw/error/password-rejected.ts @@ -4,7 +4,7 @@ const password_rejected = { character_types: 'At least {{min}} types of characters are required.', // UNTRANSLATED unsupported_characters: 'Unsupported character found.', // UNTRANSLATED pwned: 'Avoid using simple passwords that are easy to guess.', // UNTRANSLATED - restricted_found: 'Avoid overusing {{list}}.', // UNTRANSLATED + restricted_found: 'Avoid overusing {{list, list}}.', // UNTRANSLATED 'restricted.repetition': 'repeated characters', // UNTRANSLATED 'restricted.sequence': 'sequential characters', // UNTRANSLATED 'restricted.personal_info': 'your personal information', // UNTRANSLATED diff --git a/packages/ui/src/hooks/use-list-translation.test.ts b/packages/ui/src/hooks/use-list-translation.test.ts deleted file mode 100644 index b7a98ea5e..000000000 --- a/packages/ui/src/hooks/use-list-translation.test.ts +++ /dev/null @@ -1,107 +0,0 @@ -import useListTranslation from './use-list-translation'; - -const mockT = jest.fn((key: string) => key); - -jest.mock('react', () => ({ - ...jest.requireActual('react'), - useCallback: (function_: () => void) => function_, -})); -jest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: mockT, - }), -})); - -describe('useListTranslation (en)', () => { - const translateList = useListTranslation(); - - beforeAll(() => { - mockT.mockImplementation((key: string) => { - switch (key) { - case 'list.or': { - return 'or'; - } - case 'list.and': { - return 'and'; - } - case 'list.separator': { - return ','; - } - default: { - return key; - } - } - }); - }); - - it('returns undefined for an empty list', () => { - expect(translateList([])).toBeUndefined(); - }); - - it('returns the first item for a list of one item', () => { - expect(translateList(['a'])).toBe('a'); - }); - - it('returns the list with "or" for a list of two items', () => { - expect(translateList(['a', 'b'])).toBe('a or b'); - }); - - it('returns the list with the Oxford comma for a list of three items', () => { - expect(translateList(['a', 'b', 'c'])).toBe('a, b, or c'); - }); - - it('returns the list with the specified joint', () => { - expect(translateList(['a', 'b', 'c'], 'and')).toBe('a, b, and c'); - }); -}); - -describe('useListTranslation (zh)', () => { - const translateList = useListTranslation(); - - beforeAll(() => { - mockT.mockImplementation((key: string) => { - switch (key) { - case 'list.or': { - return '或'; - } - case 'list.and': { - return '和'; - } - case 'list.separator': { - return '、'; - } - default: { - return key; - } - } - }); - }); - - it('returns undefined for an empty list', () => { - expect(translateList([])).toBeUndefined(); - }); - - it('returns the first item for a list of one item', () => { - expect(translateList(['苹果'])).toBe('苹果'); - }); - - it('returns the list with "或" for a list of two items', () => { - expect(translateList(['苹果', '橘子'])).toBe('苹果或橘子'); - }); - - it('returns the list with the AP style for a list of three items', () => { - expect(translateList(['苹果', '橘子', '香蕉'])).toBe('苹果、橘子或香蕉'); - }); - - it('returns the list with the specified joint', () => { - expect(translateList(['苹果', '橘子', '香蕉'], 'and')).toBe('苹果、橘子和香蕉'); - }); - - it('adds a space between CJK and non-CJK characters', () => { - expect(translateList(['苹果', '橘子', 'banana'])).toBe('苹果、橘子或 banana'); - expect(translateList(['苹果', '橘子', 'banana'], 'and')).toBe('苹果、橘子和 banana'); - expect(translateList(['banana', '苹果', '橘子'])).toBe('banana、苹果或橘子'); - expect(translateList(['苹果', 'banana', '橘子'])).toBe('苹果、banana 或橘子'); - expect(translateList(['苹果', 'banana', 'orange'])).toBe('苹果、banana 或 orange'); - }); -}); diff --git a/packages/ui/src/hooks/use-list-translation.ts b/packages/ui/src/hooks/use-list-translation.ts deleted file mode 100644 index 2b1410afd..000000000 --- a/packages/ui/src/hooks/use-list-translation.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { condString, conditionalArray } from '@silverhand/essentials'; -import { useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; - -/** - * Returns whether the given character is a CJK character. - * - * @see https://stackoverflow.com/questions/43418812 - */ -const isCjk = (char?: string) => - Boolean( - char?.[0] && /[\u3040-\u30FF\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF\uFF66-\uFF9F]/.test(char[0]) - ); - -/** - * Returns a function that translates a list of strings into a human-readable list. If the list - * is empty, `undefined` is returned. - * - * For non-CJK languages, the list is translated with the Oxford comma. For CJK languages, the - * list is translated with the AP style. - * - * CAUTION: This function may not be suitable for translating lists of non-English strings if the - * target language does not have the same rules for list translation as English. - * - * @example - * ```ts - * const translateList = useListTranslation(); - * - * // en - * translateList([]); // undefined - * translateList(['a']); // 'a' - * translateList(['a', 'b']); // 'a and b' - * translateList(['a', 'b', 'c']); // 'a, b, or c' - * translateList(['a', 'b', 'c'], 'and'); // 'a, b, and c' - * - * // zh - * translateList(['a', 'b']); // 'a 或 b' - * translateList(['苹果', '橘子', '香蕉']); // '苹果、橘子或香蕉' - * translateList(['苹果', '橘子', 'banana']); // '苹果、橘子或 banana' - * ``` - */ -const useListTranslation = () => { - const { t } = useTranslation(); - - return useCallback( - (list: string[], joint: 'or' | 'and' = 'or') => { - if (list.length === 0) { - return; - } - - if (list.length === 1) { - return list[0]; - } - - const jointT = t(`list.${joint}`); - const prefix = list - .slice(0, -1) - .join(t('list.separator') + condString(!isCjk(jointT) && ' ')); - const suffix = list.at(-1)!; // eslint-disable-line @typescript-eslint/no-non-null-assertion -- `list` is not empty - - if (!isCjk(jointT) && list.length > 2) { - // Oxford comma - return `${prefix}${t(`list.separator`)} ${jointT} ${suffix}`; - } - - return conditionalArray( - prefix, - !isCjk(prefix.at(-1)) && ' ', - jointT, - !isCjk(suffix[0]) && ' ', - suffix - ).join(''); - }, - [t] - ); -}; - -export default useListTranslation; diff --git a/packages/ui/src/hooks/use-password-error-message.ts b/packages/ui/src/hooks/use-password-error-message.ts index 6472dbb9d..26fdf8422 100644 --- a/packages/ui/src/hooks/use-password-error-message.ts +++ b/packages/ui/src/hooks/use-password-error-message.ts @@ -3,14 +3,11 @@ import { type RequestErrorBody } from '@logto/schemas'; import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import useListTranslation from '@/hooks/use-list-translation'; - /** * Return an object with two functions for getting the error message from an array of {@link PasswordIssue} or a {@link RequestErrorBody}. */ const usePasswordErrorMessage = () => { const { t } = useTranslation(); - const translateList = useListTranslation(); const getErrorMessage = useCallback( (issues: PasswordIssue[]) => { // Errors that should be displayed first and alone @@ -39,11 +36,11 @@ const usePasswordErrorMessage = () => { if (restrictedErrors.length > 0) { return t('error.password_rejected.restricted_found', { - list: translateList(restrictedErrors), + list: restrictedErrors, }); } }, - [translateList, t] + [t] ); const getErrorMessageFromBody = useCallback(