mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
refactor(schemas,core,console): skip onboarding if user has pending invitations (#5547)
This commit is contained in:
parent
618c38f134
commit
a387bf2868
7 changed files with 132 additions and 100 deletions
|
@ -1,4 +1,10 @@
|
|||
import { adminTenantId } from '@logto/schemas';
|
||||
import {
|
||||
adminTenantId,
|
||||
Project,
|
||||
type UserOnboardingData,
|
||||
userOnboardingDataGuard,
|
||||
userOnboardingDataKey,
|
||||
} from '@logto/schemas';
|
||||
import { useCallback, useContext, useMemo } from 'react';
|
||||
import { z } from 'zod';
|
||||
|
||||
|
@ -6,12 +12,15 @@ import { isCloud } from '@/consts/env';
|
|||
import { TenantsContext } from '@/contexts/TenantsProvider';
|
||||
import useCurrentUser from '@/hooks/use-current-user';
|
||||
|
||||
import type { UserOnboardingData } from '../types';
|
||||
import { Project, userOnboardingDataGuard } from '../types';
|
||||
|
||||
const userOnboardingDataKey = 'onboarding';
|
||||
|
||||
const useUserOnboardingData = () => {
|
||||
const useUserOnboardingData = (): {
|
||||
data: UserOnboardingData;
|
||||
error: unknown;
|
||||
isLoading: boolean;
|
||||
isLoaded: boolean;
|
||||
isOnboarding: boolean;
|
||||
isBusinessPlan: boolean;
|
||||
update: (data: Partial<UserOnboardingData>) => Promise<void>;
|
||||
} => {
|
||||
const { customData, error, isLoading, isLoaded, updateCustomData } = useCurrentUser();
|
||||
const { currentTenantId } = useContext(TenantsContext);
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { withAppInsights } from '@logto/app-insights/react';
|
||||
import { type Questionnaire, Project } from '@logto/schemas';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import { useEffect } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
|
@ -17,8 +18,7 @@ import * as pageLayout from '@/onboarding/scss/layout.module.scss';
|
|||
import { trySubmitSafe } from '@/utils/form';
|
||||
|
||||
import { CardSelector, MultiCardSelector } from '../../components/CardSelector';
|
||||
import type { Questionnaire } from '../../types';
|
||||
import { OnboardingPage, Project } from '../../types';
|
||||
import { OnboardingPage } from '../../types';
|
||||
import { getOnboardingPage } from '../../utils';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { AdditionalFeatures, Project, Stage } from '@logto/schemas';
|
||||
|
||||
import Building from '@/assets/icons/building.svg';
|
||||
import Pizza from '@/assets/icons/pizza.svg';
|
||||
import type {
|
||||
|
@ -5,8 +7,6 @@ import type {
|
|||
MultiCardSelectorOption,
|
||||
} from '@/onboarding/components/CardSelector';
|
||||
|
||||
import { Project, Stage, AdditionalFeatures } from '../../types';
|
||||
|
||||
export const projectOptions: CardSelectorOption[] = [
|
||||
{
|
||||
icon: <Pizza />,
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
export enum OnboardingRoute {
|
||||
Onboarding = 'onboarding',
|
||||
}
|
||||
|
@ -12,85 +10,3 @@ export enum OnboardingPage {
|
|||
/** @deprecated Remove this to shorten onboarding process. */
|
||||
Congrats = 'congrats',
|
||||
}
|
||||
|
||||
export enum Project {
|
||||
Personal = 'personal',
|
||||
Company = 'company',
|
||||
}
|
||||
|
||||
/** @deprecated Open-source options was for cloud preview use, no longer needed. Use default `Cloud` value for placeholder. */
|
||||
enum DeploymentType {
|
||||
OpenSource = 'open-source',
|
||||
Cloud = 'cloud',
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
// eslint-disable-next-line import/no-unused-modules
|
||||
export enum Title {
|
||||
Developer = 'developer',
|
||||
TeamLead = 'team-lead',
|
||||
Ceo = 'ceo',
|
||||
Cto = 'cto',
|
||||
Product = 'product',
|
||||
Others = 'others',
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
// eslint-disable-next-line import/no-unused-modules
|
||||
export enum CompanySize {
|
||||
Scale1 = '1',
|
||||
Scale2 = '2-49',
|
||||
Scale3 = '50-199',
|
||||
Scale4 = '200-999',
|
||||
Scale5 = '1000+',
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
// eslint-disable-next-line import/no-unused-modules
|
||||
export enum Reason {
|
||||
Passwordless = 'passwordless',
|
||||
Efficiency = 'efficiency',
|
||||
AccessControl = 'access-control',
|
||||
MultiTenancy = 'multi-tenancy',
|
||||
Enterprise = 'enterprise',
|
||||
Others = 'others',
|
||||
}
|
||||
|
||||
export enum Stage {
|
||||
NewProduct = 'new-product',
|
||||
ExistingProduct = 'existing-product',
|
||||
TargetEnterpriseReady = 'target-enterprise-ready',
|
||||
}
|
||||
|
||||
export enum AdditionalFeatures {
|
||||
CustomizeUiAndFlow = 'customize-ui-and-flow',
|
||||
Compliance = 'compliance',
|
||||
ExportUserDataFromLogto = 'export-user-data-from-logto',
|
||||
BudgetControl = 'budget-control',
|
||||
BringOwnAuth = 'bring-own-auth',
|
||||
Others = 'others',
|
||||
}
|
||||
|
||||
const questionnaireGuard = z.object({
|
||||
project: z.nativeEnum(Project).optional(),
|
||||
/** @deprecated Open-source options was for cloud preview use, no longer needed. Use default `Cloud` value for placeholder. */
|
||||
deploymentType: z.nativeEnum(DeploymentType).optional().default(DeploymentType.Cloud),
|
||||
/** @deprecated */
|
||||
titles: z.array(z.nativeEnum(Title)).optional(),
|
||||
companyName: z.string().optional(),
|
||||
/** @deprecated */
|
||||
companySize: z.nativeEnum(CompanySize).optional(),
|
||||
/** @deprecated */
|
||||
reasons: z.array(z.nativeEnum(Reason)).optional(),
|
||||
stage: z.nativeEnum(Stage).optional(),
|
||||
additionalFeatures: z.array(z.nativeEnum(AdditionalFeatures)).optional(),
|
||||
});
|
||||
|
||||
export type Questionnaire = z.infer<typeof questionnaireGuard>;
|
||||
|
||||
export const userOnboardingDataGuard = z.object({
|
||||
questionnaire: questionnaireGuard.optional(),
|
||||
isOnboardingDone: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export type UserOnboardingData = z.infer<typeof userOnboardingDataGuard>;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Component, CoreEvent, getEventName } from '@logto/app-insights/custom-event';
|
||||
import { appInsights } from '@logto/app-insights/node';
|
||||
import type { User } from '@logto/schemas';
|
||||
import type { User, UserOnboardingData } from '@logto/schemas';
|
||||
import {
|
||||
AdminTenantRole,
|
||||
SignInMode,
|
||||
|
@ -13,6 +13,8 @@ import {
|
|||
getTenantRole,
|
||||
TenantRole,
|
||||
defaultManagementApiAdminName,
|
||||
OrganizationInvitationStatus,
|
||||
userOnboardingDataKey,
|
||||
} from '@logto/schemas';
|
||||
import { generateStandardId } from '@logto/shared';
|
||||
import { conditional, conditionalArray, trySafe } from '@silverhand/essentials';
|
||||
|
@ -90,8 +92,11 @@ async function handleSubmitRegister(
|
|||
log?: LogEntry
|
||||
) {
|
||||
const { provider, libraries, queries, cloudConnection, id: tenantId } = tenantContext;
|
||||
const { hasActiveUsers } = queries.users;
|
||||
const { updateDefaultSignInExperience } = queries.signInExperiences;
|
||||
const {
|
||||
users: { hasActiveUsers },
|
||||
signInExperiences: { updateDefaultSignInExperience },
|
||||
organizations,
|
||||
} = queries;
|
||||
|
||||
const {
|
||||
users: { generateUserId, insertUser },
|
||||
|
@ -110,6 +115,15 @@ async function handleSubmitRegister(
|
|||
const isCreatingFirstAdminUser =
|
||||
isInAdminTenant && String(client_id) === adminConsoleApplicationId && !(await hasActiveUsers());
|
||||
|
||||
// If it's Logto Cloud, Check if the new user has any pending invitations, if yes, skip onboarding flow.
|
||||
const invitations =
|
||||
isCloud && userProfile.primaryEmail
|
||||
? await organizations.invitations.findEntities({ invitee: userProfile.primaryEmail })
|
||||
: [];
|
||||
const hasPendingInvitations = invitations.some(
|
||||
(invitation) => invitation.status === OrganizationInvitationStatus.Pending
|
||||
);
|
||||
|
||||
await insertUser(
|
||||
{
|
||||
id,
|
||||
|
@ -119,6 +133,16 @@ async function handleSubmitRegister(
|
|||
mfaVerifications,
|
||||
}
|
||||
),
|
||||
...conditional(
|
||||
// Skip onboarding flow if the new user has pending Cloud invitations
|
||||
hasPendingInvitations && {
|
||||
customData: {
|
||||
[userOnboardingDataKey]: {
|
||||
isOnboardingDone: true,
|
||||
} satisfies UserOnboardingData,
|
||||
},
|
||||
}
|
||||
),
|
||||
...conditional(
|
||||
mfaSkipped && {
|
||||
logtoConfig: {
|
||||
|
@ -142,8 +166,8 @@ async function handleSubmitRegister(
|
|||
// Create tenant organization and assign the admin user to it.
|
||||
// This is only for Cloud integration tests and data alignment, OSS still uses the legacy Management API user role.
|
||||
const organizationId = getTenantOrganizationId(defaultTenantId);
|
||||
await queries.organizations.relations.users.insert([organizationId, id]);
|
||||
await queries.organizations.relations.rolesUsers.insert([
|
||||
await organizations.relations.users.insert([organizationId, id]);
|
||||
await organizations.relations.rolesUsers.insert([
|
||||
organizationId,
|
||||
getTenantRole(TenantRole.Admin).id,
|
||||
id,
|
||||
|
|
|
@ -27,3 +27,4 @@ export * from './tenant-organization.js';
|
|||
export * from './mapi-proxy.js';
|
||||
export * from './consent.js';
|
||||
export * from './jwt-customizer.js';
|
||||
export * from './onboarding.js';
|
||||
|
|
82
packages/schemas/src/types/onboarding.ts
Normal file
82
packages/schemas/src/types/onboarding.ts
Normal file
|
@ -0,0 +1,82 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
export const userOnboardingDataKey = 'onboarding';
|
||||
|
||||
export enum Project {
|
||||
Personal = 'personal',
|
||||
Company = 'company',
|
||||
}
|
||||
|
||||
/** @deprecated Open-source options was for cloud preview use, no longer needed. Use default `Cloud` value for placeholder. */
|
||||
enum DeploymentType {
|
||||
OpenSource = 'open-source',
|
||||
Cloud = 'cloud',
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
export enum Title {
|
||||
Developer = 'developer',
|
||||
TeamLead = 'team-lead',
|
||||
Ceo = 'ceo',
|
||||
Cto = 'cto',
|
||||
Product = 'product',
|
||||
Others = 'others',
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
export enum CompanySize {
|
||||
Scale1 = '1',
|
||||
Scale2 = '2-49',
|
||||
Scale3 = '50-199',
|
||||
Scale4 = '200-999',
|
||||
Scale5 = '1000+',
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
export enum Reason {
|
||||
Passwordless = 'passwordless',
|
||||
Efficiency = 'efficiency',
|
||||
AccessControl = 'access-control',
|
||||
MultiTenancy = 'multi-tenancy',
|
||||
Enterprise = 'enterprise',
|
||||
Others = 'others',
|
||||
}
|
||||
|
||||
export enum Stage {
|
||||
NewProduct = 'new-product',
|
||||
ExistingProduct = 'existing-product',
|
||||
TargetEnterpriseReady = 'target-enterprise-ready',
|
||||
}
|
||||
|
||||
export enum AdditionalFeatures {
|
||||
CustomizeUiAndFlow = 'customize-ui-and-flow',
|
||||
Compliance = 'compliance',
|
||||
ExportUserDataFromLogto = 'export-user-data-from-logto',
|
||||
BudgetControl = 'budget-control',
|
||||
BringOwnAuth = 'bring-own-auth',
|
||||
Others = 'others',
|
||||
}
|
||||
|
||||
const questionnaireGuard = z.object({
|
||||
project: z.nativeEnum(Project).optional(),
|
||||
/** @deprecated Open-source options was for cloud preview use, no longer needed. Use default `Cloud` value for placeholder. */
|
||||
deploymentType: z.nativeEnum(DeploymentType).optional().default(DeploymentType.Cloud),
|
||||
/** @deprecated */
|
||||
titles: z.array(z.nativeEnum(Title)).optional(),
|
||||
companyName: z.string().optional(),
|
||||
/** @deprecated */
|
||||
companySize: z.nativeEnum(CompanySize).optional(),
|
||||
/** @deprecated */
|
||||
reasons: z.array(z.nativeEnum(Reason)).optional(),
|
||||
stage: z.nativeEnum(Stage).optional(),
|
||||
additionalFeatures: z.array(z.nativeEnum(AdditionalFeatures)).optional(),
|
||||
});
|
||||
|
||||
export type Questionnaire = z.infer<typeof questionnaireGuard>;
|
||||
|
||||
export const userOnboardingDataGuard = z.object({
|
||||
questionnaire: questionnaireGuard.optional(),
|
||||
isOnboardingDone: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export type UserOnboardingData = z.infer<typeof userOnboardingDataGuard>;
|
Loading…
Reference in a new issue