0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-06 20:40:08 -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 { useCallback, useContext, useMemo } from 'react';
import { z } from 'zod'; import { z } from 'zod';
@ -6,12 +12,15 @@ import { isCloud } from '@/consts/env';
import { TenantsContext } from '@/contexts/TenantsProvider'; import { TenantsContext } from '@/contexts/TenantsProvider';
import useCurrentUser from '@/hooks/use-current-user'; import useCurrentUser from '@/hooks/use-current-user';
import type { UserOnboardingData } from '../types'; const useUserOnboardingData = (): {
import { Project, userOnboardingDataGuard } from '../types'; data: UserOnboardingData;
error: unknown;
const userOnboardingDataKey = 'onboarding'; isLoading: boolean;
isLoaded: boolean;
const useUserOnboardingData = () => { isOnboarding: boolean;
isBusinessPlan: boolean;
update: (data: Partial<UserOnboardingData>) => Promise<void>;
} => {
const { customData, error, isLoading, isLoaded, updateCustomData } = useCurrentUser(); const { customData, error, isLoading, isLoaded, updateCustomData } = useCurrentUser();
const { currentTenantId } = useContext(TenantsContext); const { currentTenantId } = useContext(TenantsContext);

View file

@ -1,4 +1,5 @@
import { withAppInsights } from '@logto/app-insights/react'; import { withAppInsights } from '@logto/app-insights/react';
import { type Questionnaire, Project } from '@logto/schemas';
import { conditional } from '@silverhand/essentials'; import { conditional } from '@silverhand/essentials';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { Controller, useForm } from 'react-hook-form'; 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 { trySubmitSafe } from '@/utils/form';
import { CardSelector, MultiCardSelector } from '../../components/CardSelector'; import { CardSelector, MultiCardSelector } from '../../components/CardSelector';
import type { Questionnaire } from '../../types'; import { OnboardingPage } from '../../types';
import { OnboardingPage, Project } from '../../types';
import { getOnboardingPage } from '../../utils'; import { getOnboardingPage } from '../../utils';
import * as styles from './index.module.scss'; 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 Building from '@/assets/icons/building.svg';
import Pizza from '@/assets/icons/pizza.svg'; import Pizza from '@/assets/icons/pizza.svg';
import type { import type {
@ -5,8 +7,6 @@ import type {
MultiCardSelectorOption, MultiCardSelectorOption,
} from '@/onboarding/components/CardSelector'; } from '@/onboarding/components/CardSelector';
import { Project, Stage, AdditionalFeatures } from '../../types';
export const projectOptions: CardSelectorOption[] = [ export const projectOptions: CardSelectorOption[] = [
{ {
icon: <Pizza />, icon: <Pizza />,

View file

@ -1,5 +1,3 @@
import { z } from 'zod';
export enum OnboardingRoute { export enum OnboardingRoute {
Onboarding = 'onboarding', Onboarding = 'onboarding',
} }
@ -12,85 +10,3 @@ export enum OnboardingPage {
/** @deprecated Remove this to shorten onboarding process. */ /** @deprecated Remove this to shorten onboarding process. */
Congrats = 'congrats', 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 { Component, CoreEvent, getEventName } from '@logto/app-insights/custom-event';
import { appInsights } from '@logto/app-insights/node'; import { appInsights } from '@logto/app-insights/node';
import type { User } from '@logto/schemas'; import type { User, UserOnboardingData } from '@logto/schemas';
import { import {
AdminTenantRole, AdminTenantRole,
SignInMode, SignInMode,
@ -13,6 +13,8 @@ import {
getTenantRole, getTenantRole,
TenantRole, TenantRole,
defaultManagementApiAdminName, defaultManagementApiAdminName,
OrganizationInvitationStatus,
userOnboardingDataKey,
} from '@logto/schemas'; } from '@logto/schemas';
import { generateStandardId } from '@logto/shared'; import { generateStandardId } from '@logto/shared';
import { conditional, conditionalArray, trySafe } from '@silverhand/essentials'; import { conditional, conditionalArray, trySafe } from '@silverhand/essentials';
@ -90,8 +92,11 @@ async function handleSubmitRegister(
log?: LogEntry log?: LogEntry
) { ) {
const { provider, libraries, queries, cloudConnection, id: tenantId } = tenantContext; const { provider, libraries, queries, cloudConnection, id: tenantId } = tenantContext;
const { hasActiveUsers } = queries.users; const {
const { updateDefaultSignInExperience } = queries.signInExperiences; users: { hasActiveUsers },
signInExperiences: { updateDefaultSignInExperience },
organizations,
} = queries;
const { const {
users: { generateUserId, insertUser }, users: { generateUserId, insertUser },
@ -110,6 +115,15 @@ async function handleSubmitRegister(
const isCreatingFirstAdminUser = const isCreatingFirstAdminUser =
isInAdminTenant && String(client_id) === adminConsoleApplicationId && !(await hasActiveUsers()); 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( await insertUser(
{ {
id, id,
@ -119,6 +133,16 @@ async function handleSubmitRegister(
mfaVerifications, mfaVerifications,
} }
), ),
...conditional(
// Skip onboarding flow if the new user has pending Cloud invitations
hasPendingInvitations && {
customData: {
[userOnboardingDataKey]: {
isOnboardingDone: true,
} satisfies UserOnboardingData,
},
}
),
...conditional( ...conditional(
mfaSkipped && { mfaSkipped && {
logtoConfig: { logtoConfig: {
@ -142,8 +166,8 @@ async function handleSubmitRegister(
// Create tenant organization and assign the admin user to it. // 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. // This is only for Cloud integration tests and data alignment, OSS still uses the legacy Management API user role.
const organizationId = getTenantOrganizationId(defaultTenantId); const organizationId = getTenantOrganizationId(defaultTenantId);
await queries.organizations.relations.users.insert([organizationId, id]); await organizations.relations.users.insert([organizationId, id]);
await queries.organizations.relations.rolesUsers.insert([ await organizations.relations.rolesUsers.insert([
organizationId, organizationId,
getTenantRole(TenantRole.Admin).id, getTenantRole(TenantRole.Admin).id,
id, id,

View file

@ -27,3 +27,4 @@ export * from './tenant-organization.js';
export * from './mapi-proxy.js'; export * from './mapi-proxy.js';
export * from './consent.js'; export * from './consent.js';
export * from './jwt-customizer.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>;