0
Fork 0
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:
Charles Zhao 2024-03-25 17:45:06 +08:00 committed by GitHub
parent 618c38f134
commit a387bf2868
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 132 additions and 100 deletions

View file

@ -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);

View file

@ -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';

View file

@ -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 />,

View file

@ -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>;

View file

@ -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,

View file

@ -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';

View 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>;