feat: manually mirror opencoze's code from bytedance

Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
fanlv
2025-07-20 17:36:12 +08:00
commit 890153324f
14811 changed files with 1923430 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
# @coze-foundation/account-base
> Project template for react component with storybook.
## Features
- [x] eslint & ts
- [x] esm bundle
- [x] umd bundle
- [x] storybook
## Commands
- init: `rush update`
- dev: `npm run dev`
- build: `npm run build`

View File

@@ -0,0 +1,17 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
vi.stubGlobal('IS_DEV_MODE', false);

View File

@@ -0,0 +1,12 @@
{
"operationSettings": [
{
"operationName": "test:cov",
"outputFolderNames": ["coverage"]
},
{
"operationName": "ts-check",
"outputFolderNames": ["dist"]
}
]
}

View File

@@ -0,0 +1,8 @@
const { defineConfig } = require('@coze-arch/eslint-config');
module.exports = defineConfig({
packageRoot: __dirname,
preset: 'web',
rules: {},
ignores: ['**/__tests__/*'],
});

View File

@@ -0,0 +1,47 @@
{
"name": "@coze-foundation/account-base",
"version": "0.0.1",
"description": "account & login related utils & hooks & stores",
"license": "Apache-2.0",
"author": "duwenhan@bytedance.com",
"maintainers": [],
"main": "src/index.ts",
"scripts": {
"build": "exit 0",
"dev": "exit 0",
"lint": "eslint ./ --cache",
"test": "vitest --run --passWithNoTests",
"test:cov": "npm run test -- --coverage"
},
"dependencies": {
"@coze-arch/bot-api": "workspace:*",
"@coze-arch/i18n": "workspace:*",
"@coze-arch/logger": "workspace:*",
"@coze-foundation/local-storage": "workspace:*",
"ahooks": "^3.7.8",
"zustand": "^4.4.7"
},
"devDependencies": {
"@coze-arch/bot-typings": "workspace:*",
"@coze-arch/eslint-config": "workspace:*",
"@coze-arch/foundation-sdk": "workspace:*",
"@coze-arch/idl": "workspace:*",
"@coze-arch/stylelint-config": "workspace:*",
"@coze-arch/ts-config": "workspace:*",
"@coze-arch/vitest-config": "workspace:*",
"@testing-library/jest-dom": "^6.1.5",
"@testing-library/react": "^14.1.2",
"@testing-library/react-hooks": "^8.0.1",
"@types/react": "18.2.37",
"@types/react-dom": "18.2.15",
"@vitest/coverage-v8": "~3.0.5",
"react": "~18.2.0",
"react-dom": "~18.2.0",
"stylelint": "^15.11.0",
"vite-plugin-svgr": "~3.3.0",
"vitest": "~3.0.5"
},
"peerDependencies": {
"react": ">=18.2.0"
}
}

View File

@@ -0,0 +1,101 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { vi, describe, it, expect, beforeEach, type Mock } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import {
handleAPIErrorEvent,
removeAPIErrorEvent,
APIErrorEvent,
} from '@coze-arch/bot-api';
import { useCheckLoginBase } from '../factory';
import { useUserStore } from '../../store/user';
vi.mock('@coze-arch/bot-api', () => ({
handleAPIErrorEvent: vi.fn(),
removeAPIErrorEvent: vi.fn(),
APIErrorEvent: { UNAUTHORIZED: 'UNAUTHORIZED' },
}));
const mockCheckLoginImpl = vi
.fn()
.mockResolvedValue({ userInfo: null, hasError: false });
const mockGoLogin = vi.fn();
const mockReset = vi.fn();
const originalUserStore = {
...useUserStore.getState(),
reset: mockReset,
};
beforeEach(() => {
useUserStore.setState(originalUserStore);
vi.clearAllMocks();
mockGoLogin.mockReset();
});
describe('useCheckLoginBase', () => {
it('should call checkLoginBase when isSettled is false', () => {
useUserStore.setState({ isSettled: false });
renderHook(() => useCheckLoginBase(true, mockCheckLoginImpl, mockGoLogin));
expect(mockCheckLoginImpl).toHaveBeenCalledTimes(1);
});
it('should call checkLoginBase when isSettled is false and require auth is false', () => {
useUserStore.setState({ isSettled: false });
renderHook(() => useCheckLoginBase(false, mockCheckLoginImpl, mockGoLogin));
expect(mockCheckLoginImpl).toHaveBeenCalledTimes(1);
});
it('should redirect to login when needLogin is true and user is not logged in', () => {
useUserStore.setState({ isSettled: true, userInfo: null });
renderHook(() => useCheckLoginBase(true, mockCheckLoginImpl, mockGoLogin));
expect(mockGoLogin).toHaveBeenCalled();
});
it('should not redirect when user is logged in', () => {
useUserStore.setState({
isSettled: true,
userInfo: { user_id_str: '123' },
});
renderHook(() => useCheckLoginBase(true, mockCheckLoginImpl, mockGoLogin));
expect(mockGoLogin).not.toHaveBeenCalled();
});
it('should handle UNAUTHORIZED event and redirect', () => {
const { unmount } = renderHook(() =>
useCheckLoginBase(true, mockCheckLoginImpl, mockGoLogin),
);
const handleUnauthorized = (handleAPIErrorEvent as Mock).mock.calls[0][1];
act(() => handleUnauthorized());
expect(mockReset).toHaveBeenCalled();
expect(mockGoLogin).toHaveBeenCalled();
unmount();
expect(removeAPIErrorEvent).toHaveBeenCalledWith(
APIErrorEvent.UNAUTHORIZED,
handleUnauthorized,
);
});
it('should not redirect on UNAUTHORIZED if needLogin is false', () => {
renderHook(() => useCheckLoginBase(false, mockCheckLoginImpl, mockGoLogin));
const handleUnauthorized = (handleAPIErrorEvent as Mock).mock.calls[0][1];
act(() => handleUnauthorized());
expect(mockGoLogin).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,194 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
import { useDocumentVisibility } from 'ahooks';
import { renderHook, act } from '@testing-library/react';
import { useLoginStatus, useAlterOnLogout } from '../index';
import { useUserStore } from '../../store/user';
// Mock ahooks
vi.mock('ahooks', async importOriginal => {
const original = await importOriginal();
return {
...original,
useDocumentVisibility: vi.fn(),
};
});
// Mock useUserStore
vi.mock('../../store/user', () => ({
useUserStore: vi.fn(),
}));
const UID_KEY = 'coze_current_uid';
const localStorageMock = (() => {
let store: Record<string, string> = {};
return {
getItem: (key: string) => store[key] || null,
setItem: (key: string, value: string) => {
store[key] = value.toString();
},
removeItem: (key: string) => {
delete store[key];
},
clear: () => {
store = {};
},
length: 0,
key: (index: number) => null,
};
})();
Object.defineProperty(window, 'localStorage', {
value: localStorageMock,
});
Object.defineProperty(window, 'addEventListener', {
value: vi.fn(),
});
Object.defineProperty(window, 'removeEventListener', {
value: vi.fn(),
});
describe('Account Hooks from index.ts', () => {
beforeEach(() => {
vi.clearAllMocks();
localStorageMock.clear();
// Default mock for useUserStore to return a function that can be called with a selector
(useUserStore as unknown as Mock).mockImplementation(selector =>
selector(mockUserState),
);
});
let mockUserState: any;
describe('useLoginStatus', () => {
it('should return "settling" if store is not settled', () => {
mockUserState = { isSettled: false, userInfo: null };
const { result } = renderHook(() => useLoginStatus());
expect(result.current).toBe('settling');
});
it('should return "not_login" if store is settled and no userInfo', () => {
mockUserState = { isSettled: true, userInfo: null };
const { result } = renderHook(() => useLoginStatus());
expect(result.current).toBe('not_login');
});
it('should return "not_login" if store is settled and userInfo has no user_id_str', () => {
mockUserState = { isSettled: true, userInfo: { name: 'Test' } };
const { result } = renderHook(() => useLoginStatus());
expect(result.current).toBe('not_login');
});
it('should return "logined" if store is settled and userInfo has user_id_str', () => {
mockUserState = { isSettled: true, userInfo: { user_id_str: '123' } };
const { result } = renderHook(() => useLoginStatus());
expect(result.current).toBe('logined');
});
});
describe('useAlterOnLogout', () => {
let alertMock: Mock;
beforeEach(() => {
alertMock = vi.fn();
(useDocumentVisibility as Mock).mockReturnValue('visible');
// Mock getState for the effect cleanup function
(useUserStore as any).getState = vi.fn(() => mockUserState);
});
it('should not call alert if document is visible or user is not logged in', () => {
mockUserState = { isSettled: true, userInfo: null }; // Not logged in
renderHook(() => useAlterOnLogout(alertMock));
// Simulate visibility change to hidden and back to visible (triggering cleanup and re-run)
act(() => {
(useDocumentVisibility as Mock).mockReturnValue('hidden');
});
act(() => {
(useDocumentVisibility as Mock).mockReturnValue('visible');
});
expect(alertMock).not.toHaveBeenCalled();
});
it('should call alert if user was logged in, document becomes visible, and UID in localStorage is different or null', () => {
const currentUserId = 'user123';
mockUserState = {
isSettled: true,
userInfo: { user_id_str: currentUserId },
}; // Logged in
localStorageMock.setItem(UID_KEY, currentUserId); // UID in localStorage matches
(useDocumentVisibility as Mock).mockReturnValue('hidden'); // Start hidden
const { rerender } = renderHook(() => useAlterOnLogout(alertMock));
// Simulate user logging out in another tab (localStorage UID changes)
localStorageMock.removeItem(UID_KEY);
// Simulate tab becoming visible
act(() => {
(useDocumentVisibility as Mock).mockReturnValue('visible');
});
rerender(); // Rerender to trigger useEffect with new visibility
expect(alertMock).toHaveBeenCalledTimes(1);
});
it('should call alert if user was logged in, document becomes visible, and UID in localStorage is different', () => {
const currentUserId = 'user123';
const otherTabUserId = 'user456';
mockUserState = {
isSettled: true,
userInfo: { user_id_str: currentUserId },
}; // Logged in
localStorageMock.setItem(UID_KEY, currentUserId); // UID in localStorage matches
(useDocumentVisibility as Mock).mockReturnValue('hidden'); // Start hidden
const { rerender } = renderHook(() => useAlterOnLogout(alertMock));
localStorageMock.setItem(UID_KEY, otherTabUserId); // UID changes in another tab
act(() => {
(useDocumentVisibility as Mock).mockReturnValue('visible');
});
rerender();
expect(alertMock).toHaveBeenCalledTimes(1);
});
it('should NOT call alert if user was logged in, document becomes visible, and UID in localStorage matches', () => {
const currentUserId = 'user123';
mockUserState = {
isSettled: true,
userInfo: { user_id_str: currentUserId },
}; // Logged in
localStorageMock.setItem(UID_KEY, currentUserId);
(useDocumentVisibility as Mock).mockReturnValue('hidden');
const { rerender } = renderHook(() => useAlterOnLogout(alertMock));
act(() => {
(useDocumentVisibility as Mock).mockReturnValue('visible');
});
rerender();
expect(alertMock).not.toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest';
import { renderHook } from '@testing-library/react';
import { localStorageService } from '@coze-foundation/local-storage';
import { useSyncLocalStorageUid } from '../use-sync-local-storage-uid';
import { useLoginStatus, useUserInfo } from '../index';
// Mock hooks and services
vi.mock('../index', () => ({
useLoginStatus: vi.fn(),
useUserInfo: vi.fn(),
}));
vi.mock('@coze-foundation/local-storage', () => ({
localStorageService: {
setUserId: vi.fn(),
},
}));
describe('useSyncLocalStorageUid', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('update uid when login status changes', () => {
const mockUserInfo = { user_id_str: '123456' };
const { rerender } = renderHook(() => useSyncLocalStorageUid(), {
initialProps: {},
});
// 初始状态:未登录
(useLoginStatus as Mock).mockReturnValue('not_login');
(useUserInfo as Mock).mockReturnValue(null);
rerender();
expect(localStorageService.setUserId).toHaveBeenCalledWith();
// 切换到登录状态
(useLoginStatus as Mock).mockReturnValue('logined');
(useUserInfo as Mock).mockReturnValue(mockUserInfo);
rerender();
expect(localStorageService.setUserId).toHaveBeenCalledWith(
mockUserInfo.user_id_str,
);
});
});

View File

@@ -0,0 +1,80 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useEffect } from 'react';
import { useMemoizedFn } from 'ahooks';
import {
APIErrorEvent,
handleAPIErrorEvent,
removeAPIErrorEvent,
} from '@coze-arch/bot-api';
import { checkLoginBase } from '../utils/factory';
import { type UserInfo } from '../types';
import { useUserStore } from '../store/user';
/**
* 用于页面初始化时,检查登录状态,并监听登录态失效的接口报错
* 在登录态失效时,会重定向到登录页
* @param needLogin 是否需要登录
* @param checkLogin 检查登录状态的具体实现
* @param goLogin 重定向到登录页的具体实现
*/
export const useCheckLoginBase = (
needLogin: boolean,
checkLoginImpl: () => Promise<{
userInfo?: UserInfo;
hasError?: boolean;
}>,
goLogin: () => void,
) => {
const isSettled = useUserStore(state => state.isSettled);
const memoizedGoLogin = useMemoizedFn(goLogin);
useEffect(() => {
if (!isSettled) {
checkLoginBase(checkLoginImpl);
}
}, [isSettled]);
useEffect(() => {
const isLogined = !!useUserStore.getState().userInfo?.user_id_str;
// 当前页面要求登录,登录检查结果为未登录时,重定向回登录页
if (needLogin && isSettled && !isLogined) {
memoizedGoLogin();
}
}, [needLogin, isSettled]);
useEffect(() => {
let fired = false;
const handleUnauthorized = () => {
useUserStore.getState().reset();
if (needLogin) {
if (!fired) {
fired = true;
memoizedGoLogin();
}
}
};
// ajax 请求后端接口出现未 授权/登录 时,触发该函数
handleAPIErrorEvent(APIErrorEvent.UNAUTHORIZED, handleUnauthorized);
return () => {
removeAPIErrorEvent(APIErrorEvent.UNAUTHORIZED, handleUnauthorized);
};
}, [needLogin]);
};

View File

@@ -0,0 +1,88 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useEffect } from 'react';
import { useDocumentVisibility, useMemoizedFn } from 'ahooks';
import { type LoginStatus } from '../types';
import { useUserStore } from '../store/user';
/**
* @description 用于获取用户登录状态
* @returns 登录状态
*/
export const useLoginStatus = (): LoginStatus =>
useUserStore(state => {
if (state.isSettled) {
return state.userInfo?.user_id_str ? 'logined' : 'not_login';
}
return 'settling';
});
/**
* @description 用于获取用户信息
* @returns 用户信息
*/
export const useUserInfo = () => useUserStore(state => state.userInfo);
/**
* @description 当前是否为错误状态
* @returns 是否为错误状态
*/
export const useHasError = () => useUserStore(state => state.hasError);
const currentUidLSKey = 'coze_current_uid';
/**
* 用于打开多页签情况下,探测其它页签下发生的登出事件并在当前触发提示
* @param alert 触发提示的具体实现
*/
export const useAlterOnLogout = (alert: () => void) => {
const visibility = useDocumentVisibility();
const loginStatus = useLoginStatus();
const isLogined = loginStatus === 'logined';
const memoizedAlert = useMemoizedFn(() => {
alert();
});
useEffect(() => {
if (visibility === 'hidden' && isLogined) {
const lastUserId = useUserStore.getState().userInfo?.user_id_str;
// 登录态下,每次页面从后台回到前台,重新检查一次登录用户是否发生了变化
return () => {
const latestUserId = localStorage.getItem(currentUidLSKey);
if (lastUserId !== latestUserId) {
memoizedAlert();
}
};
}
}, [visibility, isLogined]);
// 在登录态变化后,更新本地缓存状态
useEffect(() => {
if (loginStatus !== 'settling') {
localStorage.setItem(
currentUidLSKey,
useUserStore.getState().userInfo?.user_id_str ?? '',
);
}
}, [loginStatus]);
};
export const useUserLabel = () => useUserStore(state => state.userLabel);
export const useUserAuthInfo = () => useUserStore(state => state.userAuthInfos);

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useEffect } from 'react';
import { localStorageService } from '@coze-foundation/local-storage';
import { useLoginStatus, useUserInfo } from './index';
export const useSyncLocalStorageUid = () => {
const userInfo = useUserInfo();
const loginStatus = useLoginStatus();
useEffect(() => {
if (loginStatus === 'logined') {
localStorageService.setUserId(userInfo?.user_id_str);
}
if (loginStatus === 'not_login') {
localStorageService.setUserId();
}
}, [loginStatus, userInfo?.user_id_str]);
};

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// types
export type { UserInfo, LoginStatus } from './types';
export type {
OAuth2RedirectConfig,
Connector2Redirect,
} from './types/passport';
// common hooks
export {
useLoginStatus,
useUserInfo,
useHasError,
useAlterOnLogout,
useUserLabel,
useUserAuthInfo,
} from './hooks';
export { useSyncLocalStorageUid } from './hooks/use-sync-local-storage-uid';
// common utils
export {
getUserInfo,
getUserLabel,
getLoginStatus,
resetUserStore,
setUserInfo,
getUserAuthInfos,
subscribeUserAuthInfos,
usernameRegExpValidate,
} from './utils';
// base hooks
export { useCheckLoginBase } from './hooks/factory';
// base utils
export {
refreshUserInfoBase,
logoutBase,
checkLoginBase,
} from './utils/factory';

View File

@@ -0,0 +1,140 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { vi, describe, it, expect, beforeEach, type Mock } from 'vitest';
import { DeveloperApi, PlaygroundApi } from '@coze-arch/bot-api';
import { useUserStore, defaultState } from '../user';
vi.mock('@coze-arch/bot-api', () => ({
DeveloperApi: {
GetUserAuthList: vi.fn(),
},
PlaygroundApi: {
MGetUserBasicInfo: vi.fn(),
},
}));
describe('useUserStore', () => {
beforeEach(() => {
useUserStore.setState(defaultState);
vi.clearAllMocks();
});
it('should have the correct default state', () => {
const state = useUserStore.getState();
expect(state.isSettled).toBe(false);
expect(state.userInfo).toBeNull();
expect(state.hasError).toBe(false);
expect(state.userAuthInfos).toEqual([]);
expect(state.userLabel).toBeNull();
});
describe('actions', () => {
it('reset should reset state to default and set isSettled to true', () => {
useUserStore.setState({
userInfo: { user_id_str: '123' } as any,
isSettled: false,
});
useUserStore.getState().reset();
const state = useUserStore.getState();
expect(state.userInfo).toBeNull();
expect(state.userAuthInfos).toEqual([]);
expect(state.userLabel).toBeNull();
expect(state.isSettled).toBe(true);
expect(state.hasError).toBe(false);
});
it('setIsSettled should update isSettled', () => {
useUserStore.getState().setIsSettled(true);
expect(useUserStore.getState().isSettled).toBe(true);
useUserStore.getState().setIsSettled(false);
expect(useUserStore.getState().isSettled).toBe(false);
});
describe('setUserInfo', () => {
it('should update userInfo', () => {
const newUserInfo = {
user_id_str: 'testUser',
name: 'Test User',
} as any;
useUserStore.getState().setUserInfo(newUserInfo);
expect(useUserStore.getState().userInfo).toEqual(newUserInfo);
});
it('should call fetchUserLabel if user_id_str changes', async () => {
const newUserInfo = {
user_id_str: 'newUser123',
name: 'New User',
} as any;
const initialUserInfo = {
user_id_str: 'oldUser456',
name: 'Old User',
} as any;
// Set an initial user
useUserStore.setState({ userInfo: initialUserInfo });
(PlaygroundApi.MGetUserBasicInfo as Mock).mockResolvedValueOnce({
id_user_info_map: {
[newUserInfo.user_id_str]: {
user_label: { label_type: 1, text: 'Test Label' },
},
},
});
useUserStore.getState().setUserInfo(newUserInfo);
expect(useUserStore.getState().userInfo).toEqual(newUserInfo);
await vi.waitFor(() => {
expect(PlaygroundApi.MGetUserBasicInfo).toHaveBeenCalledWith({
user_ids: [newUserInfo.user_id_str],
});
});
await vi.waitFor(() => {
expect(useUserStore.getState().userLabel).toEqual({
label_type: 1,
text: 'Test Label',
});
});
});
it('should not call fetchUserLabel if user_id_str is the same', () => {
const userInfo = { user_id_str: 'user123', name: 'Test User' } as any;
useUserStore.setState({ userInfo });
useUserStore.getState().setUserInfo(userInfo);
expect(PlaygroundApi.MGetUserBasicInfo).not.toHaveBeenCalled();
});
});
describe('getUserAuthInfos', () => {
it('should fetch and set userAuthInfos on success', async () => {
const mockAuthInfos = [
{ auth_type: 'email', auth_key: 'test@example.com' },
];
(DeveloperApi.GetUserAuthList as Mock).mockResolvedValueOnce({
data: mockAuthInfos,
});
await useUserStore.getState().getUserAuthInfos();
expect(DeveloperApi.GetUserAuthList).toHaveBeenCalledTimes(1);
expect(useUserStore.getState().userAuthInfos).toEqual(mockAuthInfos);
});
});
});
});

View File

@@ -0,0 +1,89 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { devtools, subscribeWithSelector } from 'zustand/middleware';
import { create } from 'zustand';
import {
type UserAuthInfo,
type UserLabel,
} from '@coze-arch/bot-api/developer_api';
import { DeveloperApi, PlaygroundApi } from '@coze-arch/bot-api';
import { type UserInfo } from '../types';
export interface UserStoreState {
isSettled: boolean;
hasError: boolean;
userInfo: UserInfo | null;
userAuthInfos: UserAuthInfo[];
userLabel: UserLabel | null;
}
export interface UserStoreAction {
reset: () => void;
setIsSettled: (isSettled: boolean) => void;
setUserInfo: (userInfo: UserInfo | null) => void;
getUserAuthInfos: () => Promise<void>;
}
export const defaultState: UserStoreState = {
isSettled: false,
userInfo: null,
hasError: false,
userAuthInfos: [],
userLabel: null,
};
export const useUserStore = create<UserStoreState & UserStoreAction>()(
devtools(
subscribeWithSelector((set, get) => ({
...defaultState,
reset: () => {
set({ ...defaultState, isSettled: true });
},
setIsSettled: isSettled => {
set({
isSettled,
});
},
setUserInfo: (userInfo: UserInfo | null) => {
if (
userInfo?.user_id_str &&
userInfo?.user_id_str !== get().userInfo?.user_id_str
) {
fetchUserLabel(userInfo?.user_id_str);
}
set({
userInfo,
});
},
getUserAuthInfos: async () => {
const { data = [] } = await DeveloperApi.GetUserAuthList();
set({ userAuthInfos: data });
},
})),
{
enabled: IS_DEV_MODE,
name: 'botStudio.userStore',
},
),
);
const fetchUserLabel = async (id: string) => {
const res = await PlaygroundApi.MGetUserBasicInfo({ user_ids: [id] });
const userLabel = res?.id_user_info_map?.[id]?.user_label;
useUserStore.setState({ userLabel });
};

View File

@@ -0,0 +1,114 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* 当前登录账号的用户信息
*/
export interface UserInfo {
app_id: number;
/**
* @deprecated 会因为溢出丢失精度,使用 user_id_str
*/
user_id: number;
user_id_str: string;
odin_user_type: number;
name: string;
screen_name: string;
avatar_url: string;
user_verified: boolean;
email?: string;
email_collected: boolean;
expend_attrs?: Record<string, unknown>;
phone_collected: boolean;
verified_content: string;
verified_agency: string;
is_blocked: number;
is_blocking: number;
bg_img_url: string;
gender: number;
media_id: number;
user_auth_info: string;
industry: string;
area: string;
can_be_found_by_phone: number;
mobile: string;
birthday: string;
description: string;
status: number;
new_user: number;
first_login_app: number;
session_key: string;
is_recommend_allowed: number;
recommend_hint_message: string;
followings_count: number;
followers_count: number;
visit_count_recent: number;
skip_edit_profile: number;
is_manual_set_user_info: boolean;
device_id: number;
country_code: number;
has_password: number;
share_to_repost: number;
user_decoration: string;
user_privacy_extend: number;
old_user_id: number;
old_user_id_str: string;
sec_user_id: string;
sec_old_user_id: string;
vcd_account: number;
vcd_relation: number;
can_bind_visitor_account: boolean;
is_visitor_account: boolean;
is_only_bind_ins: boolean;
user_device_record_status: number;
is_kids_mode: number;
source: string;
is_employee: boolean;
passport_enterprise_user_type: number;
need_device_create: number;
need_ttwid_migration: number;
user_auth_status: number;
user_safe_mobile_2fa: string;
safe_mobile_country_code: number;
lite_user_info_string: string;
lite_user_info_demotion: number;
app_user_info: {
user_unique_name?: string;
};
need_check_bind_status: boolean;
bui_audit_info?: {
audit_info: {
user_unique_name?: string;
avatar_url?: string;
name?: string;
[key: string]: unknown;
}; // Record<string, unknown>;
// int值。1审核中2审核通过3审核不通过
audit_status: 1 | 2 | 3;
details: Record<string, unknown>;
is_auditing: boolean;
last_update_time: number;
unpass_reason: string;
};
}
/**
* 登录状态
* - settling: 登录状态检测中,一般用于首屏,会有一定的延迟
* - not_login: 未登录
* - logined: 已登录
*/
export type LoginStatus = 'settling' | 'not_login' | 'logined';

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export type OAuth2StateType = 'login' | 'delete_account' | 'oauth';
export interface OAuth2RedirectConfig {
/**
* 最终的OAuth2鉴权信息将作为路由参数跳转这个参数指定目标路由地址注意在目标路由上使用
* useAuthLoginDataRouteFromOAuth2来提取路由参数并转换成用户中台三方登陆服务authLogin的参数
* 默认值为当前路径名称即不传navigatePath参数时当前路由一定要注册useAuthLoginDataRouteFromOAuth2才有效
*/
navigatePath?: string;
/**
* OAuth2回调后拿到的鉴权信息的使用场景用于在一些路由组件中区分不符合对应场景的不能用于消费
*/
type: OAuth2StateType;
/**
* 传递给OAuth2服务器的state字段会在回调时传回用于恢复网页状态
*/
extra?: {
// @ts-expect-error -- linter-disable-autofix
origin?: string;
[x: string]: string; // 用于安全监测
// @ts-expect-error -- linter-disable-autofix
encrypt_state?: string; //加密statebind_type 为 4时使用
};
scope?: string;
optionalScope?: string;
}
export interface AuthLoginInfo {
app_id?: string;
response_type?: string;
authorize_url?: string;
scope?: string;
client_id?: string;
duration?: string;
aid?: string;
client_key?: string;
}
export type Connector2Redirect = (
oauth2Config: OAuth2RedirectConfig,
platform: string,
authInfo: AuthLoginInfo,
) => void;

View File

@@ -0,0 +1,19 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/// <reference types='@coze-arch/bot-typings' />
declare const IS_DEV_MODE: boolean;

View File

@@ -0,0 +1,126 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
import { setUserInfoContext } from '@coze-arch/logger';
import { refreshUserInfoBase, logoutBase, checkLoginBase } from '../factory';
import { type UserInfo } from '../../types';
import { useUserStore } from '../../store/user';
// Mock dependencies
vi.mock('@coze-arch/logger', () => ({
setUserInfoContext: vi.fn(),
}));
vi.mock('../../store/user', () => ({
useUserStore: {
getState: vi.fn(),
setState: vi.fn(),
},
}));
describe('factory.ts utility functions', () => {
let mockGetState: Mock;
let mockSetState: Mock;
let mockSetUserInfo: Mock;
let mockReset: Mock;
beforeEach(() => {
vi.clearAllMocks();
mockSetUserInfo = vi.fn();
mockReset = vi.fn();
mockGetState = useUserStore.getState as Mock;
mockSetState = useUserStore.setState as Mock;
mockGetState.mockReturnValue({
setUserInfo: mockSetUserInfo,
reset: mockReset,
});
});
describe('refreshUserInfoBase', () => {
it('should correctly refresh user information', async () => {
const mockUserInfo = {
user_id_str: '123',
name: 'Test User',
} as UserInfo;
const mockCheckLogin = vi.fn().mockResolvedValue(mockUserInfo);
await refreshUserInfoBase(mockCheckLogin);
expect(mockSetState).toHaveBeenCalledWith({ hasError: false });
expect(mockCheckLogin).toHaveBeenCalled();
expect(mockSetUserInfo).toHaveBeenCalledWith(mockUserInfo);
});
});
describe('logoutBase', () => {
it('should correctly execute logout operation', async () => {
const mockLogout = vi.fn().mockResolvedValue(undefined);
await logoutBase(mockLogout);
expect(mockLogout).toHaveBeenCalled();
expect(mockReset).toHaveBeenCalled();
});
});
describe('checkLoginBase', () => {
const mockSetUserInfoContext = setUserInfoContext as Mock;
it('should correctly handle successful login state', async () => {
const mockUserInfo = {
user_id_str: '123',
name: 'Test User',
} as UserInfo;
const mockCheckLoginImpl = vi
.fn()
.mockResolvedValue({ userInfo: mockUserInfo });
await checkLoginBase(mockCheckLoginImpl);
expect(mockSetState).toHaveBeenCalledWith({ hasError: false });
expect(mockSetUserInfoContext).toHaveBeenCalledWith(mockUserInfo);
expect(mockSetState).toHaveBeenCalledWith({
userInfo: mockUserInfo,
isSettled: true,
});
});
it('should correctly handle login error state', async () => {
const mockCheckLoginImpl = vi.fn().mockResolvedValue({ hasError: true });
await checkLoginBase(mockCheckLoginImpl);
expect(mockSetState).toHaveBeenCalledWith({ hasError: false });
expect(mockSetState).toHaveBeenCalledWith({ hasError: true });
expect(mockSetUserInfoContext).not.toHaveBeenCalled();
});
it('should correctly handle not logged in state', async () => {
const mockCheckLoginImpl = vi.fn().mockResolvedValue({ userInfo: null });
await checkLoginBase(mockCheckLoginImpl);
expect(mockSetState).toHaveBeenCalledWith({ hasError: false });
expect(mockSetState).toHaveBeenCalledWith({
userInfo: null,
isSettled: true,
});
expect(mockSetUserInfoContext).not.toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,199 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
import { I18n } from '@coze-arch/i18n';
import {
getUserInfo,
getLoginStatus,
resetUserStore,
setUserInfo,
getUserLabel,
getUserAuthInfos,
subscribeUserAuthInfos,
usernameRegExpValidate,
} from '../index';
import { useUserStore } from '../../store/user';
// Mock useUserStore
vi.mock('../../store/user', () => ({
useUserStore: {
getState: vi.fn(),
setState: vi.fn(), // Though not directly used by all utils, good to have for setUserInfo
subscribe: vi.fn(),
},
}));
// Mock I18n
vi.mock('@coze-arch/i18n', () => ({
I18n: {
t: vi.fn(key => key), // Simple mock that returns the key
},
}));
describe('Utility functions from utils/index.ts', () => {
let mockGetState: Mock;
let mockSubscribe: Mock;
beforeEach(() => {
vi.clearAllMocks();
mockGetState = useUserStore.getState as Mock;
mockSubscribe = useUserStore.subscribe as Mock;
});
describe('getUserInfo', () => {
it('should return userInfo from userStore.getState()', () => {
const mockUser = { user_id_str: 'testUser', name: 'Test User' };
mockGetState.mockReturnValue({ userInfo: mockUser });
expect(getUserInfo()).toEqual(mockUser);
expect(mockGetState).toHaveBeenCalledTimes(1);
});
});
describe('getLoginStatus', () => {
it('should return "settling" if store is not settled', () => {
mockGetState.mockReturnValue({ isSettled: false, userInfo: null });
expect(getLoginStatus()).toBe('settling');
});
it('should return "not_login" if store is settled and no userInfo', () => {
mockGetState.mockReturnValue({ isSettled: true, userInfo: null });
expect(getLoginStatus()).toBe('not_login');
});
it('should return "not_login" if store is settled and userInfo has no user_id_str', () => {
mockGetState.mockReturnValue({
isSettled: true,
userInfo: { name: 'Test' },
});
expect(getLoginStatus()).toBe('not_login');
});
it('should return "logined" if store is settled and userInfo has user_id_str', () => {
mockGetState.mockReturnValue({
isSettled: true,
userInfo: { user_id_str: '123' },
});
expect(getLoginStatus()).toBe('logined');
});
});
describe('resetUserStore', () => {
it('should call reset on userStore.getState()', () => {
const mockReset = vi.fn();
mockGetState.mockReturnValue({ reset: mockReset });
resetUserStore();
expect(mockReset).toHaveBeenCalledTimes(1);
});
});
describe('setUserInfo', () => {
it('should call setUserInfo on userStore.getState() with the provided user info', () => {
const mockSetUserInfo = vi.fn();
mockGetState.mockReturnValue({ setUserInfo: mockSetUserInfo });
const newUser = { user_id_str: 'newUser', name: 'New User' };
setUserInfo(newUser);
expect(mockSetUserInfo).toHaveBeenCalledWith(newUser);
});
it('should call setUserInfo on userStore.getState() with null', () => {
const mockSetUserInfo = vi.fn();
mockGetState.mockReturnValue({ setUserInfo: mockSetUserInfo });
setUserInfo(null);
expect(mockSetUserInfo).toHaveBeenCalledWith(null);
});
});
describe('getUserLabel', () => {
it('should return userLabel from userStore.getState()', () => {
const mockLabel = { label_type: 1, text: 'VIP' };
mockGetState.mockReturnValue({ userLabel: mockLabel });
expect(getUserLabel()).toEqual(mockLabel);
expect(mockGetState).toHaveBeenCalledTimes(1);
});
});
describe('getUserAuthInfos', () => {
it('should call getUserAuthInfos on userStore.getState()', async () => {
const mockGetUserAuthInfos = vi.fn().mockResolvedValue([]);
mockGetState.mockReturnValue({ getUserAuthInfos: mockGetUserAuthInfos });
await getUserAuthInfos();
expect(mockGetUserAuthInfos).toHaveBeenCalledTimes(1);
});
});
describe('subscribeUserAuthInfos', () => {
it('should call userStore.subscribe with a selector for userAuthInfos and the callback', () => {
const callback = vi.fn();
const mockUserAuthInfos = [
{ auth_type: 'email', auth_key: 'test@example.com' },
];
// Mock the subscribe implementation to immediately call the listener with selected state
mockSubscribe.mockImplementation(
(selector, cb) => vi.fn(), // Return unsubscribe function
);
subscribeUserAuthInfos(callback);
expect(mockSubscribe).toHaveBeenCalledTimes(1);
// Check that the selector passed to subscribe correctly extracts userAuthInfos
const selectorArg = mockSubscribe.mock.calls[0][0];
expect(selectorArg({ userAuthInfos: mockUserAuthInfos })).toEqual(
mockUserAuthInfos,
);
expect(mockSubscribe.mock.calls[0][1]).toBe(callback);
});
});
describe('usernameRegExpValidate', () => {
it('should return null for valid usernames', () => {
expect(usernameRegExpValidate('validUser123')).toBeNull();
expect(usernameRegExpValidate('another_valid_user')).toBeNull();
expect(usernameRegExpValidate('USER')).toBeNull();
expect(usernameRegExpValidate('1234')).toBeNull();
});
it('should return "username_invalid_letter" for usernames with invalid characters', () => {
(I18n.t as Mock).mockReturnValueOnce('username_invalid_letter');
expect(usernameRegExpValidate('invalid-char')).toBe(
'username_invalid_letter',
);
(I18n.t as Mock).mockReturnValueOnce('username_invalid_letter');
expect(usernameRegExpValidate('invalid char')).toBe(
'username_invalid_letter',
);
(I18n.t as Mock).mockReturnValueOnce('username_invalid_letter');
expect(usernameRegExpValidate('!@#$%^')).toBe('username_invalid_letter');
expect(I18n.t).toHaveBeenCalledWith('username_invalid_letter');
});
it('should return "username_too_short" for usernames shorter than minLength (4)', () => {
(I18n.t as Mock).mockReturnValueOnce('username_too_short');
expect(usernameRegExpValidate('abc')).toBe('username_too_short');
(I18n.t as Mock).mockReturnValueOnce('username_too_short');
expect(usernameRegExpValidate('us')).toBe('username_too_short');
expect(I18n.t).toHaveBeenCalledWith('username_too_short');
});
it('should return "username_invalid_letter" if invalid char before checking length', () => {
// This case tests if the invalid character check takes precedence
(I18n.t as Mock).mockReturnValueOnce('username_invalid_letter');
expect(usernameRegExpValidate('a-b')).toBe('username_invalid_letter'); // Length 3, but invalid char
expect(I18n.t).toHaveBeenCalledWith('username_invalid_letter');
});
});
});

View File

@@ -0,0 +1,64 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { setUserInfoContext } from '@coze-arch/logger';
import { type UserInfo } from '../types';
import { useUserStore } from '../store/user';
/**
* 主动触发刷新用户信息
* @param checkLogin 登录检查函数
*/
export const refreshUserInfoBase = async (
checkLogin: () => Promise<UserInfo>,
) => {
useUserStore.setState({
hasError: false,
});
const userInfo = await checkLogin();
useUserStore.getState().setUserInfo(userInfo);
};
export const logoutBase = async (logout: () => Promise<void>) => {
await logout();
useUserStore.getState().reset();
};
export const checkLoginBase = async (
checkLoginImpl: () => Promise<{
userInfo?: UserInfo;
hasError?: boolean;
}>,
) => {
useUserStore.setState({
hasError: false,
});
const { userInfo, hasError } = await checkLoginImpl();
if (hasError) {
useUserStore.setState({
hasError: true,
});
return;
}
if (userInfo) {
setUserInfoContext(userInfo);
}
useUserStore.setState({
userInfo,
isSettled: true,
});
};

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type UserAuthInfo } from '@coze-arch/idl/developer_api';
import { I18n } from '@coze-arch/i18n';
import { type UserInfo, type LoginStatus } from '../types';
import { useUserStore } from '../store/user';
/**
* 获取用户信息
* @returns UserInfo 用户信息
*/
export const getUserInfo = () => useUserStore.getState().userInfo;
/**
* 获取登录状态
* @returns LoginStatus 登录状态
*/
export const getLoginStatus = (): LoginStatus => {
const state = useUserStore.getState();
if (state.isSettled) {
return state.userInfo?.user_id_str ? 'logined' : 'not_login';
}
return 'settling';
};
export const resetUserStore = () => useUserStore.getState().reset();
export const setUserInfo = (userInfo: UserInfo | null) =>
useUserStore.getState().setUserInfo(userInfo);
export const getUserLabel = () => useUserStore.getState().userLabel;
export const getUserAuthInfos = () =>
useUserStore.getState().getUserAuthInfos();
export const subscribeUserAuthInfos = (
callback: (state: UserAuthInfo[], prev: UserAuthInfo[]) => void,
) => useUserStore.subscribe(state => state.userAuthInfos, callback);
const usernameRegExp = /^[0-9A-Za-z_]+$/;
const minLength = 4;
export const usernameRegExpValidate = (value: string) => {
if (!usernameRegExp.exec(value)) {
return I18n.t('username_invalid_letter');
}
if (value.length < minLength) {
return I18n.t('username_too_short');
}
return null;
};

View File

@@ -0,0 +1,49 @@
{
"extends": "@coze-arch/ts-config/tsconfig.web.json",
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"jsx": "react-jsx",
"lib": ["DOM", "ESNext"],
"target": "ES2020",
"tsBuildInfoFile": "dist/tsconfig.build.tsbuildinfo"
},
"include": ["src"],
"exclude": ["node_modules", "dist", "**/__tests__"],
"references": [
{
"path": "../../arch/bot-api/tsconfig.build.json"
},
{
"path": "../../arch/bot-typings/tsconfig.build.json"
},
{
"path": "../../arch/foundation-sdk/tsconfig.build.json"
},
{
"path": "../../arch/i18n/tsconfig.build.json"
},
{
"path": "../../arch/idl/tsconfig.build.json"
},
{
"path": "../../arch/logger/tsconfig.build.json"
},
{
"path": "../../../config/eslint-config/tsconfig.build.json"
},
{
"path": "../../../config/stylelint-config/tsconfig.build.json"
},
{
"path": "../../../config/ts-config/tsconfig.build.json"
},
{
"path": "../../../config/vitest-config/tsconfig.build.json"
},
{
"path": "../local-storage/tsconfig.build.json"
}
]
}

View File

@@ -0,0 +1,15 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"exclude": ["**/*"],
"compilerOptions": {
"composite": true
},
"references": [
{
"path": "./tsconfig.build.json"
},
{
"path": "./tsconfig.misc.json"
}
]
}

View File

@@ -0,0 +1,20 @@
{
"extends": "@coze-arch/ts-config/tsconfig.web.json",
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"rootDir": "./",
"outDir": "./dist",
"jsx": "react-jsx",
"lib": ["DOM", "ESNext"],
"module": "ESNext",
"target": "ES2020",
"moduleResolution": "bundler"
},
"include": ["**/__tests__", "vitest.config.ts"],
"exclude": ["node_modules", "dist"],
"references": [
{
"path": "./tsconfig.build.json"
}
]
}

View File

@@ -0,0 +1,26 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { defineConfig } from '@coze-arch/vitest-config';
export default defineConfig({
dirname: __dirname,
preset: 'web',
test: {
setupFiles: ['./__tests__/setup-vitest.ts'],
includeSource: ['./src/**/__tests__/**'],
},
});