feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
const { defineConfig } = require('@coze-arch/stylelint-config');
|
||||
|
||||
module.exports = defineConfig({
|
||||
extends: [],
|
||||
});
|
||||
16
frontend/packages/foundation/account-adapter/README.md
Normal file
16
frontend/packages/foundation/account-adapter/README.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# @coze-foundation/account-adapter
|
||||
|
||||
> 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`
|
||||
@@ -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);
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"operationSettings": [
|
||||
{
|
||||
"operationName": "test:cov",
|
||||
"outputFolderNames": ["coverage"]
|
||||
},
|
||||
{
|
||||
"operationName": "ts-check",
|
||||
"outputFolderNames": ["dist"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
const { defineConfig } = require('@coze-arch/eslint-config');
|
||||
|
||||
module.exports = defineConfig({
|
||||
packageRoot: __dirname,
|
||||
preset: 'web',
|
||||
rules: {},
|
||||
ignores: ['src/**/__tests__/**'],
|
||||
});
|
||||
43
frontend/packages/foundation/account-adapter/package.json
Normal file
43
frontend/packages/foundation/account-adapter/package.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "@coze-foundation/account-adapter",
|
||||
"version": "0.0.1",
|
||||
"description": " "account & login related utils & hooks & stores for open-source version"",
|
||||
"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-foundation/account-base": "workspace:*",
|
||||
"@coze-studio/api-schema": "workspace:*",
|
||||
"react-router-dom": "^6.22.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@coze-arch/bot-typings": "workspace:*",
|
||||
"@coze-arch/eslint-config": "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",
|
||||
"react-dom": ">=18.2.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* 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 { useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useCheckLoginBase } from '@coze-foundation/account-base';
|
||||
|
||||
import { signPath, signRedirectKey } from '../../utils/constants';
|
||||
import { checkLoginImpl } from '../../utils';
|
||||
import { useCheckLogin } from '..';
|
||||
|
||||
vi.mock('react-router-dom', () => ({
|
||||
useLocation: vi.fn(),
|
||||
useNavigate: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../src/utils', () => ({
|
||||
checkLoginImpl: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@coze-foundation/account-base', () => ({
|
||||
useCheckLoginBase: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockNavigate = vi.fn();
|
||||
const mockUseLocation = vi.mocked(useLocation);
|
||||
const mockUseNavigate = vi.mocked(useNavigate);
|
||||
|
||||
describe('useCheckLogin', () => {
|
||||
const mockLocation = {
|
||||
pathname: '/test',
|
||||
search: '?query=123',
|
||||
} as any;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockUseNavigate.mockReturnValue(mockNavigate);
|
||||
mockUseLocation.mockReturnValue(mockLocation);
|
||||
});
|
||||
|
||||
it('should call useCheckLoginBase with correct parameters', () => {
|
||||
renderHook(() => useCheckLogin({ needLogin: true }));
|
||||
expect(useCheckLoginBase).toBeCalledWith(
|
||||
true,
|
||||
checkLoginImpl,
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('should navigate to default loginFallbackPath when not provided and user is not logged in', () => {
|
||||
const mockUseCheckLoginBase = vi.mocked(useCheckLoginBase);
|
||||
mockUseCheckLoginBase.mockImplementation(
|
||||
(needLogin, checkLoginImpl, goLogin) => {
|
||||
goLogin();
|
||||
},
|
||||
);
|
||||
|
||||
renderHook(() => useCheckLogin({ needLogin: true }));
|
||||
|
||||
expect(mockNavigate).toBeCalledWith(
|
||||
`${signPath}?${signRedirectKey}=${encodeURIComponent(`${mockLocation.pathname}${mockLocation.search}`)}`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should navigate to custom loginFallbackPath when provided and user is not logged in', () => {
|
||||
const loginFallbackPath = '/custom-login';
|
||||
const mockUseCheckLoginBase = vi.mocked(useCheckLoginBase);
|
||||
mockUseCheckLoginBase.mockImplementation(
|
||||
(needLogin, checkLoginImpl, goLogin) => {
|
||||
goLogin();
|
||||
},
|
||||
);
|
||||
|
||||
renderHook(() => useCheckLogin({ needLogin: true, loginFallbackPath }));
|
||||
|
||||
expect(mockNavigate).toBeCalledWith(
|
||||
`${loginFallbackPath}${mockLocation.search}`,
|
||||
{ replace: true },
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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 { useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useCheckLoginBase } from '@coze-foundation/account-base';
|
||||
|
||||
import { signPath, signRedirectKey } from '../utils/constants';
|
||||
import { checkLoginImpl } from '../utils';
|
||||
|
||||
const useGoLogin = (loginFallbackPath?: string) => {
|
||||
const navigate = useNavigate();
|
||||
const { pathname, search } = useLocation();
|
||||
return () => {
|
||||
const redirectPath = `${pathname}${search}`;
|
||||
if (loginFallbackPath) {
|
||||
navigate(`${loginFallbackPath}${search}`, { replace: true });
|
||||
} else {
|
||||
navigate(
|
||||
`${signPath}?${signRedirectKey}=${encodeURIComponent(redirectPath)}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const useCheckLogin = ({
|
||||
needLogin,
|
||||
loginFallbackPath,
|
||||
}: {
|
||||
needLogin?: boolean;
|
||||
loginFallbackPath?: string;
|
||||
}) => {
|
||||
const goLogin = useGoLogin(loginFallbackPath);
|
||||
useCheckLoginBase(!!needLogin, checkLoginImpl, goLogin);
|
||||
};
|
||||
44
frontend/packages/foundation/account-adapter/src/index.ts
Normal file
44
frontend/packages/foundation/account-adapter/src/index.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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 {
|
||||
getUserInfo,
|
||||
getLoginStatus,
|
||||
resetUserStore,
|
||||
setUserInfo,
|
||||
getUserLabel,
|
||||
useUserInfo,
|
||||
useLoginStatus,
|
||||
useAlterOnLogout,
|
||||
useHasError,
|
||||
useUserLabel,
|
||||
useUserAuthInfo,
|
||||
getUserAuthInfos,
|
||||
subscribeUserAuthInfos,
|
||||
useSyncLocalStorageUid,
|
||||
usernameRegExpValidate,
|
||||
type UserInfo,
|
||||
type LoginStatus,
|
||||
} from '@coze-foundation/account-base';
|
||||
export {
|
||||
refreshUserInfo,
|
||||
logout,
|
||||
checkLogin,
|
||||
connector2Redirect,
|
||||
} from './utils';
|
||||
export { useCheckLogin } from './hooks';
|
||||
|
||||
export { passportApi } from './passport-api';
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* 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 } from 'vitest';
|
||||
import { passport } from '@coze-studio/api-schema';
|
||||
import { passportApi } from '../index';
|
||||
|
||||
// 模拟 passport API
|
||||
vi.mock('@coze-studio/api-schema/passport', () => ({}));
|
||||
vi.mock('@coze-studio/api-schema', () => ({
|
||||
passport: {
|
||||
PassportAccountInfoV2: vi.fn(),
|
||||
PassportWebLogoutGet: vi.fn(),
|
||||
UserUpdateAvatar: vi.fn(),
|
||||
PassportWebEmailPasswordResetGet: vi.fn(),
|
||||
UserUpdateProfile: vi.fn(),
|
||||
},
|
||||
}));
|
||||
vi.mock('@coze-foundation/account-base', () => ({
|
||||
resetUserStore: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('passportApi', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('checkLogin', () => {
|
||||
it('should correctly return user information', async () => {
|
||||
const mockUserInfo = { name: 'test' };
|
||||
vi.mocked(passport.PassportAccountInfoV2).mockResolvedValueOnce({
|
||||
data: mockUserInfo,
|
||||
});
|
||||
|
||||
const result = await passportApi.checkLogin();
|
||||
expect(result).toEqual(mockUserInfo);
|
||||
expect(passport.PassportAccountInfoV2).toHaveBeenCalledWith({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('logout', () => {
|
||||
it('should correctly call the logout API', async () => {
|
||||
await passportApi.logout();
|
||||
expect(passport.PassportWebLogoutGet).toHaveBeenCalledWith({
|
||||
next: '/',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('uploadAvatar', () => {
|
||||
it('should correctly upload avatar', async () => {
|
||||
const mockFile = new File([''], 'test.png');
|
||||
const mockResponse = { data: { url: 'test-url' } };
|
||||
vi.mocked(passport.UserUpdateAvatar).mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await passportApi.uploadAvatar({ avatar: mockFile });
|
||||
expect(result).toEqual(mockResponse.data);
|
||||
expect(passport.UserUpdateAvatar).toHaveBeenCalledWith({
|
||||
avatar: mockFile,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('updatePassword', () => {
|
||||
it('should correctly call the password reset API', async () => {
|
||||
const params = { password: 'newpass', email: 'test@example.com' };
|
||||
await passportApi.updatePassword(params);
|
||||
expect(passport.PassportWebEmailPasswordResetGet).toHaveBeenCalledWith({
|
||||
...params,
|
||||
code: '',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateUserProfile', () => {
|
||||
it('should correctly update user profile', async () => {
|
||||
const mockProfile = { nickname: 'newname' };
|
||||
await passportApi.updateUserProfile(mockProfile);
|
||||
expect(passport.UserUpdateProfile).toHaveBeenCalledWith(mockProfile);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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 UserUpdateProfileRequest } from '@coze-studio/api-schema/passport';
|
||||
import { passport } from '@coze-studio/api-schema';
|
||||
import { resetUserStore, type UserInfo } from '@coze-foundation/account-base';
|
||||
|
||||
export const passportApi = {
|
||||
checkLogin: async () => {
|
||||
const res = (await passport.PassportAccountInfoV2({})) as unknown as {
|
||||
data: UserInfo;
|
||||
};
|
||||
return res.data;
|
||||
},
|
||||
|
||||
logout: async () => {
|
||||
await passport.PassportWebLogoutGet({
|
||||
next: '/',
|
||||
});
|
||||
},
|
||||
|
||||
uploadAvatar: async ({ avatar }: { avatar: File }) => {
|
||||
const res = await passport.UserUpdateAvatar({
|
||||
avatar,
|
||||
});
|
||||
|
||||
return res.data;
|
||||
},
|
||||
|
||||
updatePassword: async (params: { password: string; email: string }) => {
|
||||
await passport.PassportWebEmailPasswordResetGet({ ...params, code: '' });
|
||||
// 更新密码后,当前登录态失效,重置 store
|
||||
resetUserStore();
|
||||
},
|
||||
|
||||
updateUserProfile: (params: UserUpdateProfileRequest) =>
|
||||
passport.UserUpdateProfile(params),
|
||||
};
|
||||
17
frontend/packages/foundation/account-adapter/src/typings.d.ts
vendored
Normal file
17
frontend/packages/foundation/account-adapter/src/typings.d.ts
vendored
Normal 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.
|
||||
*/
|
||||
|
||||
/// <reference types='@coze-arch/bot-typings' />
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* 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 } from 'vitest';
|
||||
import {
|
||||
refreshUserInfo,
|
||||
logout,
|
||||
checkLoginImpl,
|
||||
checkLogin,
|
||||
connector2Redirect,
|
||||
} from '../index';
|
||||
import {
|
||||
refreshUserInfoBase,
|
||||
logoutBase,
|
||||
checkLoginBase,
|
||||
} from '@coze-foundation/account-base';
|
||||
import { passportApi } from '../../passport-api';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('@coze-foundation/account-base', () => ({
|
||||
refreshUserInfoBase: vi.fn(),
|
||||
logoutBase: vi.fn(),
|
||||
checkLoginBase: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../passport-api', () => ({
|
||||
passportApi: {
|
||||
checkLogin: vi.fn(),
|
||||
logout: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('utils/index.ts', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('refreshUserInfo', () => {
|
||||
it('should call refreshUserInfoBase with passportApi.checkLogin', () => {
|
||||
refreshUserInfo();
|
||||
expect(refreshUserInfoBase).toHaveBeenCalledWith(passportApi.checkLogin);
|
||||
});
|
||||
});
|
||||
|
||||
describe('logout', () => {
|
||||
it('should call logoutBase with passportApi.logout', () => {
|
||||
logout();
|
||||
expect(logoutBase).toHaveBeenCalledWith(passportApi.logout);
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkLoginImpl', () => {
|
||||
it('should return userInfo when passportApi.checkLogin succeeds', async () => {
|
||||
const mockUserInfo = { id: '123', name: 'test' };
|
||||
vi.mocked(passportApi.checkLogin).mockResolvedValue(mockUserInfo);
|
||||
|
||||
const result = await checkLoginImpl();
|
||||
expect(result).toEqual({ userInfo: mockUserInfo });
|
||||
});
|
||||
|
||||
it('should return undefined userInfo when passportApi.checkLogin fails', async () => {
|
||||
vi.mocked(passportApi.checkLogin).mockRejectedValue(
|
||||
new Error('API error'),
|
||||
);
|
||||
|
||||
const result = await checkLoginImpl();
|
||||
expect(result).toEqual({ userInfo: undefined });
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkLogin', () => {
|
||||
it('should call checkLoginBase with checkLoginImpl', () => {
|
||||
checkLogin();
|
||||
expect(checkLoginBase).toHaveBeenCalledWith(checkLoginImpl);
|
||||
});
|
||||
});
|
||||
|
||||
describe('connector2Redirect', () => {
|
||||
it('should return undefined (open source version)', () => {
|
||||
const result = connector2Redirect();
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* 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 const signPath = '/sign';
|
||||
export const signRedirectKey = 'redirect';
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/* eslint-disable @coze-arch/use-error-in-catch */
|
||||
import {
|
||||
refreshUserInfoBase,
|
||||
logoutBase,
|
||||
checkLoginBase,
|
||||
type Connector2Redirect,
|
||||
} from '@coze-foundation/account-base';
|
||||
|
||||
import { passportApi } from '../passport-api';
|
||||
|
||||
export const refreshUserInfo = () =>
|
||||
refreshUserInfoBase(passportApi.checkLogin);
|
||||
|
||||
export const logout = () => logoutBase(passportApi.logout);
|
||||
|
||||
export const checkLoginImpl = async () => {
|
||||
try {
|
||||
const res = await passportApi.checkLogin();
|
||||
return { userInfo: res };
|
||||
} catch (e) {
|
||||
return { userInfo: undefined };
|
||||
}
|
||||
};
|
||||
|
||||
export const checkLogin = () => checkLoginBase(checkLoginImpl);
|
||||
|
||||
// 开源版本不支持渠道授权,暂无实现
|
||||
export const connector2Redirect: Connector2Redirect = () => undefined;
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"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", "./src/**/*.test.ts"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../account-base/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../arch/api-schema/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../arch/bot-typings/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"
|
||||
}
|
||||
]
|
||||
}
|
||||
15
frontend/packages/foundation/account-adapter/tsconfig.json
Normal file
15
frontend/packages/foundation/account-adapter/tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"exclude": ["**/*"],
|
||||
"compilerOptions": {
|
||||
"composite": true
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.misc.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"extends": "@coze-arch/ts-config/tsconfig.web.json",
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./",
|
||||
"outDir": "./dist",
|
||||
"jsx": "react-jsx",
|
||||
"lib": ["DOM", "ESNext"],
|
||||
"target": "ES2020"
|
||||
},
|
||||
"include": ["**/__tests__", "vitest.config.ts"],
|
||||
"exclude": ["node_modules", "dist"],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.build.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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__/**'],
|
||||
},
|
||||
});
|
||||
16
frontend/packages/foundation/account-base/README.md
Normal file
16
frontend/packages/foundation/account-base/README.md
Normal 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`
|
||||
@@ -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);
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"operationSettings": [
|
||||
{
|
||||
"operationName": "test:cov",
|
||||
"outputFolderNames": ["coverage"]
|
||||
},
|
||||
{
|
||||
"operationName": "ts-check",
|
||||
"outputFolderNames": ["dist"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
const { defineConfig } = require('@coze-arch/eslint-config');
|
||||
|
||||
module.exports = defineConfig({
|
||||
packageRoot: __dirname,
|
||||
preset: 'web',
|
||||
rules: {},
|
||||
ignores: ['**/__tests__/*'],
|
||||
});
|
||||
47
frontend/packages/foundation/account-base/package.json
Normal file
47
frontend/packages/foundation/account-base/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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]);
|
||||
};
|
||||
88
frontend/packages/foundation/account-base/src/hooks/index.ts
Normal file
88
frontend/packages/foundation/account-base/src/hooks/index.ts
Normal 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);
|
||||
@@ -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]);
|
||||
};
|
||||
56
frontend/packages/foundation/account-base/src/index.ts
Normal file
56
frontend/packages/foundation/account-base/src/index.ts
Normal 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';
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
89
frontend/packages/foundation/account-base/src/store/user.ts
Normal file
89
frontend/packages/foundation/account-base/src/store/user.ts
Normal 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 });
|
||||
};
|
||||
114
frontend/packages/foundation/account-base/src/types/index.ts
Normal file
114
frontend/packages/foundation/account-base/src/types/index.ts
Normal 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';
|
||||
@@ -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; //加密state,bind_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;
|
||||
19
frontend/packages/foundation/account-base/src/typings.d.ts
vendored
Normal file
19
frontend/packages/foundation/account-base/src/typings.d.ts
vendored
Normal 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;
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
66
frontend/packages/foundation/account-base/src/utils/index.ts
Normal file
66
frontend/packages/foundation/account-base/src/utils/index.ts
Normal 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;
|
||||
};
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
15
frontend/packages/foundation/account-base/tsconfig.json
Normal file
15
frontend/packages/foundation/account-base/tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"exclude": ["**/*"],
|
||||
"compilerOptions": {
|
||||
"composite": true
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.misc.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
20
frontend/packages/foundation/account-base/tsconfig.misc.json
Normal file
20
frontend/packages/foundation/account-base/tsconfig.misc.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
26
frontend/packages/foundation/account-base/vitest.config.ts
Normal file
26
frontend/packages/foundation/account-base/vitest.config.ts
Normal 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__/**'],
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
const { defineConfig } = require('@coze-arch/stylelint-config');
|
||||
|
||||
module.exports = defineConfig({
|
||||
extends: [],
|
||||
});
|
||||
16
frontend/packages/foundation/account-ui-adapter/README.md
Normal file
16
frontend/packages/foundation/account-ui-adapter/README.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# @coze-foundation/account-ui-adapter
|
||||
|
||||
> 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`
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"operationSettings": [
|
||||
{
|
||||
"operationName": "test:cov",
|
||||
"outputFolderNames": ["coverage"]
|
||||
},
|
||||
{
|
||||
"operationName": "ts-check",
|
||||
"outputFolderNames": ["dist"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
const { defineConfig } = require('@coze-arch/eslint-config');
|
||||
|
||||
module.exports = defineConfig({
|
||||
packageRoot: __dirname,
|
||||
preset: 'web',
|
||||
rules: {},
|
||||
});
|
||||
64
frontend/packages/foundation/account-ui-adapter/package.json
Normal file
64
frontend/packages/foundation/account-ui-adapter/package.json
Normal file
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"name": "@coze-foundation/account-ui-adapter",
|
||||
"version": "0.0.1",
|
||||
"description": "账号登录 UI 组件",
|
||||
"license": "Apache-2.0",
|
||||
"author": "duwenhan@bytedance.com",
|
||||
"maintainers": [],
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./login-page": "./src/pages/login-page/index.tsx"
|
||||
},
|
||||
"main": "src/index.ts",
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
".": [
|
||||
"./src/index.ts"
|
||||
],
|
||||
"login-page": [
|
||||
"./src/pages/login-page/index.tsx"
|
||||
]
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "exit 0",
|
||||
"lint": "eslint ./ --cache",
|
||||
"test": "vitest --run --passWithNoTests",
|
||||
"test:cov": "npm run test -- --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@coze-arch/bot-semi": "workspace:*",
|
||||
"@coze-arch/coze-design": "0.0.6-alpha.346d77",
|
||||
"@coze-arch/i18n": "workspace:*",
|
||||
"@coze-foundation/account-adapter": "workspace:*",
|
||||
"@coze-foundation/account-ui-base": "workspace:*",
|
||||
"@coze-studio/api-schema": "workspace:*",
|
||||
"@coze-studio/components": "workspace:*",
|
||||
"ahooks": "^3.7.8",
|
||||
"classnames": "^2.3.2",
|
||||
"react-router-dom": "^6.22.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@coze-arch/bot-typings": "workspace:*",
|
||||
"@coze-arch/eslint-config": "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",
|
||||
"react-dom": ">=18.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
22
frontend/packages/foundation/account-ui-adapter/src/index.ts
Normal file
22
frontend/packages/foundation/account-ui-adapter/src/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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 {
|
||||
useLogout,
|
||||
RequireAuthContainer,
|
||||
} from '@coze-foundation/account-ui-base';
|
||||
|
||||
export { LoginPage } from './pages/login-page';
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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 FaviconBase from './favicon-base.png';
|
||||
import FaviconAddon from './favicon-addon.png';
|
||||
|
||||
export const Favicon = () => (
|
||||
<div className="relative flex items-center">
|
||||
<img
|
||||
src={FaviconBase}
|
||||
className="w-[100px] h-[100px] rounded-[21px] border border-solid coz-stroke-plus"
|
||||
/>
|
||||
<img
|
||||
src={FaviconAddon}
|
||||
className="absolute left-1/2 translate-x-[34px] top-[40px] w-[51px]"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* 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 FC, useState } from 'react';
|
||||
|
||||
import { CozeBrand } from '@coze-studio/components/coze-brand';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Button, Form } from '@coze-arch/coze-design';
|
||||
import { SignFrame, SignPanel } from '@coze-arch/bot-semi';
|
||||
|
||||
import { useLoginService } from './service';
|
||||
import { Favicon } from './favicon';
|
||||
|
||||
export const LoginPage: FC = () => {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [hasError, setHasError] = useState(false);
|
||||
|
||||
const { login, register, loginLoading, registerLoading } = useLoginService({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
const submitDisabled = !email || !password || hasError;
|
||||
|
||||
return (
|
||||
<SignFrame brandNode={<CozeBrand isOversea={IS_OVERSEA} />}>
|
||||
<SignPanel className="w-[600px] h-[640px] pt-[96px]">
|
||||
<div className="flex flex-col items-center w-full h-full">
|
||||
<Favicon />
|
||||
<div className="text-[24px] font-medium coze-fg-plug leading-[36px] mt-[32px]">
|
||||
{I18n.t('open_source_login_welcome')}
|
||||
</div>
|
||||
<div className="mt-[64px] w-[320px] flex flex-col items-stretch [&_.semi-input-wrapper]:overflow-hidden">
|
||||
<Form
|
||||
onErrorChange={errors => {
|
||||
setHasError(Object.keys(errors).length > 0);
|
||||
}}
|
||||
>
|
||||
<Form.Input
|
||||
data-testid="login.input.email"
|
||||
noLabel
|
||||
type="email"
|
||||
field="email"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: I18n.t('open_source_login_placeholder_email'),
|
||||
},
|
||||
{
|
||||
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
|
||||
message: I18n.t('open_source_login_placeholder_email'),
|
||||
},
|
||||
]}
|
||||
onChange={newVal => {
|
||||
setEmail(newVal);
|
||||
}}
|
||||
placeholder={I18n.t('open_source_login_placeholder_email')}
|
||||
/>
|
||||
<Form.Input
|
||||
data-testid="login.input.password"
|
||||
noLabel
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: I18n.t('open_source_login_placeholder_password'),
|
||||
},
|
||||
]}
|
||||
field="password"
|
||||
type="password"
|
||||
onChange={setPassword}
|
||||
placeholder={I18n.t('open_source_login_placeholder_password')}
|
||||
/>
|
||||
</Form>
|
||||
<Button
|
||||
data-testid="login.button.login"
|
||||
className="mt-[12px]"
|
||||
disabled={submitDisabled || registerLoading}
|
||||
onClick={login}
|
||||
loading={loginLoading}
|
||||
color="hgltplus"
|
||||
>
|
||||
{I18n.t('login_button_text')}
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="login.button.signup"
|
||||
className="mt-[20px]"
|
||||
disabled={submitDisabled || loginLoading}
|
||||
onClick={register}
|
||||
loading={registerLoading}
|
||||
color="primary"
|
||||
>
|
||||
{I18n.t('register')}
|
||||
</Button>
|
||||
<div className="mt-[12px] flex justify-center">
|
||||
<a
|
||||
data-testid="login.link.terms"
|
||||
href="https://spdx.org/licenses/Apache-2.0.html"
|
||||
target="_blank"
|
||||
className="no-underline coz-fg-hglt"
|
||||
>
|
||||
{I18n.t('open_source_terms_linkname')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SignPanel>
|
||||
</SignFrame>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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 { useNavigate } from 'react-router-dom';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useRequest } from 'ahooks';
|
||||
import { passport } from '@coze-studio/api-schema';
|
||||
import {
|
||||
setUserInfo,
|
||||
useLoginStatus,
|
||||
type UserInfo,
|
||||
} from '@coze-foundation/account-adapter';
|
||||
|
||||
export const useLoginService = ({
|
||||
email,
|
||||
password,
|
||||
}: {
|
||||
email: string;
|
||||
password: string;
|
||||
}) => {
|
||||
const loginService = useRequest(
|
||||
async () => {
|
||||
const res = (await passport.PassportWebEmailLoginPost({
|
||||
email,
|
||||
password,
|
||||
})) as unknown as { data: UserInfo };
|
||||
return res.data;
|
||||
},
|
||||
{
|
||||
manual: true,
|
||||
onSuccess: setUserInfo,
|
||||
},
|
||||
);
|
||||
|
||||
const registerService = useRequest(
|
||||
async () => {
|
||||
const res = (await passport.PassportWebEmailRegisterV2Post({
|
||||
email,
|
||||
password,
|
||||
})) as unknown as { data: UserInfo };
|
||||
return res.data;
|
||||
},
|
||||
{
|
||||
manual: true,
|
||||
onSuccess: setUserInfo,
|
||||
},
|
||||
);
|
||||
|
||||
const loginStatus = useLoginStatus();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if (loginStatus === 'logined') {
|
||||
navigate('/');
|
||||
}
|
||||
}, [loginStatus]);
|
||||
|
||||
return {
|
||||
login: loginService.run,
|
||||
register: registerService.run,
|
||||
loginLoading: loginService.loading,
|
||||
registerLoading: registerService.loading,
|
||||
};
|
||||
};
|
||||
19
frontend/packages/foundation/account-ui-adapter/src/typings.d.ts
vendored
Normal file
19
frontend/packages/foundation/account-ui-adapter/src/typings.d.ts
vendored
Normal 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_OVERSEA: boolean;
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"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"],
|
||||
"module": "ESNext",
|
||||
"target": "ES2020",
|
||||
"moduleResolution": "bundler",
|
||||
"tsBuildInfoFile": "dist/tsconfig.build.tsbuildinfo"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../account-adapter/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../account-ui-base/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../arch/api-schema/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../arch/bot-typings/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../arch/i18n/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../components/bot-semi/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": "../../studio/components/tsconfig.build.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"exclude": ["**/*"],
|
||||
"compilerOptions": {
|
||||
"composite": true
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.misc.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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", "stories"],
|
||||
"exclude": ["./dist"],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.build.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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',
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
const { defineConfig } = require('@coze-arch/stylelint-config');
|
||||
|
||||
module.exports = defineConfig({
|
||||
extends: [],
|
||||
});
|
||||
16
frontend/packages/foundation/account-ui-base/README.md
Normal file
16
frontend/packages/foundation/account-ui-base/README.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# @coze-foundation/account-ui-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`
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"operationSettings": [
|
||||
{
|
||||
"operationName": "test:cov",
|
||||
"outputFolderNames": ["coverage"]
|
||||
},
|
||||
{
|
||||
"operationName": "ts-check",
|
||||
"outputFolderNames": ["dist"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
const { defineConfig } = require('@coze-arch/eslint-config');
|
||||
|
||||
module.exports = defineConfig({
|
||||
packageRoot: __dirname,
|
||||
preset: 'web',
|
||||
rules: {},
|
||||
});
|
||||
55
frontend/packages/foundation/account-ui-base/package.json
Normal file
55
frontend/packages/foundation/account-ui-base/package.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"name": "@coze-foundation/account-ui-base",
|
||||
"version": "0.0.1",
|
||||
"description": "账号、登录通用UI package",
|
||||
"license": "Apache-2.0",
|
||||
"author": "sunzhiyuan.evan@bytedance.com",
|
||||
"maintainers": [],
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
"build": "exit 0",
|
||||
"lint": "eslint ./ --cache",
|
||||
"test": "vitest --run --passWithNoTests",
|
||||
"test:cov": "npm run test -- --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@coze-arch/bot-api": "workspace:*",
|
||||
"@coze-arch/bot-http": "workspace:*",
|
||||
"@coze-arch/bot-semi": "workspace:*",
|
||||
"@coze-arch/bot-utils": "workspace:*",
|
||||
"@coze-arch/coze-design": "0.0.6-alpha.346d77",
|
||||
"@coze-arch/foundation-sdk": "workspace:*",
|
||||
"@coze-arch/i18n": "workspace:*",
|
||||
"@coze-arch/report-events": "workspace:*",
|
||||
"@coze-common/assets": "workspace:*",
|
||||
"@coze-common/biz-components": "workspace:*",
|
||||
"@coze-foundation/account-adapter": "workspace:*",
|
||||
"@coze-studio/user-store": "workspace:*",
|
||||
"ahooks": "^3.7.8",
|
||||
"classnames": "^2.3.2",
|
||||
"react-router-dom": "^6.22.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@coze-arch/bot-typings": "workspace:*",
|
||||
"@coze-arch/eslint-config": "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",
|
||||
"react-dom": ">=18.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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 FC } from 'react';
|
||||
|
||||
import { Spin } from '@coze-arch/bot-semi';
|
||||
|
||||
export const LoadingContainer: FC = () => (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<Spin spinning style={{ height: '100%', width: '100%' }} />
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,18 @@
|
||||
.container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.mask {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
background-color: #F7F7FA;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* 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 PropsWithChildren, type FC } from 'react';
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { UIButton } from '@coze-arch/bot-semi';
|
||||
import {
|
||||
useHasError,
|
||||
checkLogin,
|
||||
useLoginStatus,
|
||||
} from '@coze-foundation/account-adapter';
|
||||
|
||||
import { LoadingContainer } from '../loading-container';
|
||||
|
||||
interface ErrorPageProps {
|
||||
onRetry: () => void;
|
||||
}
|
||||
|
||||
const ErrorContainer: FC<ErrorPageProps> = ({ onRetry }) => (
|
||||
<div className="w-full h-full flex items-center justify-center flex-col">
|
||||
{I18n.t('login_failed')}
|
||||
<UIButton onClick={onRetry}>{I18n.t('Retry')}</UIButton>
|
||||
</div>
|
||||
);
|
||||
|
||||
const Mask: FC<PropsWithChildren> = ({ children }) => (
|
||||
<div className="z-1 absolute bg-[#F7F7FA] w-full h-full left-0 top-0">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
// 在需要时渲染错误状态 & loading
|
||||
const LoginCheckMask: FC<{ needLogin: boolean; loginOptional: boolean }> = ({
|
||||
needLogin,
|
||||
loginOptional,
|
||||
}) => {
|
||||
const loginStatus = useLoginStatus();
|
||||
const isLogined = loginStatus === 'logined';
|
||||
const hasError = useHasError();
|
||||
if (hasError && needLogin) {
|
||||
return (
|
||||
<Mask>
|
||||
<ErrorContainer onRetry={checkLogin} />;
|
||||
</Mask>
|
||||
);
|
||||
}
|
||||
|
||||
if (needLogin && !loginOptional && !isLogined) {
|
||||
return (
|
||||
<Mask>
|
||||
<LoadingContainer />
|
||||
</Mask>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const RequireAuthContainer: FC<
|
||||
PropsWithChildren<{ needLogin: boolean; loginOptional: boolean }>
|
||||
> = ({ children, needLogin, loginOptional }) => (
|
||||
<>
|
||||
<LoginCheckMask needLogin={needLogin} loginOptional={loginOptional} />
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
@@ -0,0 +1,92 @@
|
||||
.update-avatar {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.edit-profile {
|
||||
:global {
|
||||
.coz-icon-button-mini {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.coz-typography {
|
||||
@apply coz-fg-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.label-wrap {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
align-self: flex-start;
|
||||
justify-content: flex-start;
|
||||
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.label {
|
||||
width: 76px;
|
||||
margin-bottom: 0;
|
||||
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.submit {
|
||||
width: 100%;
|
||||
margin-top: 48px;
|
||||
}
|
||||
|
||||
.count {
|
||||
margin: 0 12px 0 8px;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
color: #737577;
|
||||
}
|
||||
|
||||
.upload-button {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
|
||||
a {
|
||||
font-weight: 400;
|
||||
color: var(--light-usage-primary-color-primary, #0077fa);
|
||||
}
|
||||
}
|
||||
|
||||
.filed-readonly {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
.text {
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
> button {
|
||||
> button:last-child,
|
||||
> span:last-child {
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.field-edit {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
|
||||
.field-edit-children {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.field-btn {
|
||||
align-self: end;
|
||||
}
|
||||
|
||||
.btn {
|
||||
margin-left: 16px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,437 @@
|
||||
/*
|
||||
* 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 PropsWithChildren,
|
||||
type ReactNode,
|
||||
useRef,
|
||||
useState,
|
||||
useEffect,
|
||||
} from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { useRequest } from 'ahooks';
|
||||
import { userStoreService } from '@coze-studio/user-store';
|
||||
import {
|
||||
passportApi,
|
||||
usernameRegExpValidate,
|
||||
} from '@coze-foundation/account-adapter';
|
||||
import { UpdateUserAvatar } from '@coze-common/biz-components';
|
||||
import { REPORT_EVENTS, createReportEvent } from '@coze-arch/report-events';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { refreshUserInfo } from '@coze-arch/foundation-sdk';
|
||||
import { IconCozWarningCircleFillPalette } from '@coze-arch/coze-design/icons';
|
||||
import { Input, Toast, Select } from '@coze-arch/coze-design';
|
||||
import { Form, type Upload } from '@coze-arch/bot-semi';
|
||||
import { isApiError } from '@coze-arch/bot-http';
|
||||
import { DeveloperApi } from '@coze-arch/bot-api';
|
||||
|
||||
import { UsernameInput } from './username-input';
|
||||
import { UserInfoField, type UserInfoFieldProps } from './user-info-field';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
// 用户输入 username 自动检查的时间
|
||||
export const CHECK_USER_NAME_DEBOUNCE_TIME = 1000;
|
||||
|
||||
const WrappedInputWithCount: React.FC<
|
||||
Pick<UserInfoFieldProps, 'value' | 'onChange' | 'onEnterPress'>
|
||||
> = ({ value, onChange, onEnterPress }) => (
|
||||
<Input
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
maxLength={20}
|
||||
autoFocus
|
||||
onEnterPress={onEnterPress}
|
||||
placeholder={I18n.t('setting_name_placeholder')}
|
||||
/>
|
||||
);
|
||||
|
||||
const WrappedUsernameInput: React.FC<
|
||||
Pick<
|
||||
UserInfoFieldProps,
|
||||
'value' | 'onChange' | 'onEnterPress' | 'errorMessage'
|
||||
>
|
||||
> = ({ value, onChange, onEnterPress, errorMessage }) => (
|
||||
<UsernameInput
|
||||
style={{ marginBottom: 0 }}
|
||||
value={value}
|
||||
errorMessage={errorMessage}
|
||||
onChange={onChange}
|
||||
autoFocus
|
||||
onEnterPress={onEnterPress}
|
||||
/>
|
||||
);
|
||||
|
||||
const WrappedPasswordInput: React.FC<
|
||||
Pick<UserInfoFieldProps, 'value' | 'onChange' | 'onEnterPress'>
|
||||
> = ({ value, onChange, onEnterPress }) => (
|
||||
<Input
|
||||
mode="password"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
autoFocus
|
||||
onEnterPress={onEnterPress}
|
||||
/>
|
||||
);
|
||||
|
||||
const getLanguageOptions = () => [
|
||||
{
|
||||
label: I18n.t('settings_language_zh'),
|
||||
value: 'zh-CN',
|
||||
},
|
||||
{
|
||||
label: I18n.t('settings_language_en'),
|
||||
value: 'en-US',
|
||||
},
|
||||
];
|
||||
|
||||
const WrappedSelectInput: React.FC<
|
||||
Pick<
|
||||
UserInfoFieldProps,
|
||||
'value' | 'onChange' | 'onEnterPress' | 'errorMessage'
|
||||
>
|
||||
> = ({ value, onChange, onEnterPress, errorMessage }) => (
|
||||
<Select
|
||||
optionList={getLanguageOptions()}
|
||||
value={value}
|
||||
onChange={val => {
|
||||
onChange?.(val as string);
|
||||
}}
|
||||
className="w-[120px]"
|
||||
/>
|
||||
);
|
||||
|
||||
const UserInfoFieldWrap: React.FC<PropsWithChildren<{ label?: ReactNode }>> = ({
|
||||
children,
|
||||
label,
|
||||
}) => (
|
||||
<div className={styles['label-wrap']}>
|
||||
<Form.Label text={label} className={styles.label} />
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
const updateProfileEvent = createReportEvent({
|
||||
eventName: REPORT_EVENTS.editUserProfile,
|
||||
});
|
||||
|
||||
const updateProfileCheckEvent = createReportEvent({
|
||||
eventName: REPORT_EVENTS.updateUserProfileCheck,
|
||||
});
|
||||
|
||||
const getUserName = (userInfo?: DataItem.UserInfo | null): string =>
|
||||
userInfo?.bui_audit_info?.audit_status === 1
|
||||
? (userInfo?.bui_audit_info?.audit_info.user_unique_name ??
|
||||
userInfo?.app_user_info.user_unique_name ??
|
||||
'')
|
||||
: (userInfo?.app_user_info.user_unique_name ?? '');
|
||||
|
||||
// eslint-disable-next-line @coze-arch/max-line-per-function
|
||||
export const UserInfoPanel = () => {
|
||||
const userInfo = userStoreService.useUserInfo();
|
||||
|
||||
const [nickname, setNickname] = useState(userInfo?.name);
|
||||
|
||||
const [username, setUsername] = useState(getUserName(userInfo));
|
||||
|
||||
const [userNameErrorInfo, setUsernameErrorInfo] = useState('');
|
||||
|
||||
const [lang, setLang] = useState(
|
||||
userInfo?.locale ?? navigator.language ?? 'en-US',
|
||||
);
|
||||
|
||||
const [password, setPassword] = useState('');
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [avatar, setAvatar] = useState(userInfo?.avatar_url ?? '');
|
||||
const uploadRef = useRef<Upload>(null);
|
||||
|
||||
const onNicknameChange = async (name?: string) => {
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
updateProfileEvent.start();
|
||||
|
||||
setLoading(true);
|
||||
await passportApi.updateUserProfile({
|
||||
name,
|
||||
});
|
||||
updateProfileEvent.success();
|
||||
} catch (error) {
|
||||
updateProfileEvent.error({
|
||||
error: error as Error,
|
||||
reason: 'update nickname failed',
|
||||
});
|
||||
throw error;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onPasswordChange = async (newPassword?: string) => {
|
||||
try {
|
||||
updateProfileEvent.start();
|
||||
await passportApi.updatePassword({
|
||||
password: newPassword ?? '',
|
||||
email: userInfo?.email ?? '',
|
||||
});
|
||||
updateProfileEvent.success();
|
||||
} catch (error) {
|
||||
updateProfileEvent.error({
|
||||
error: error as Error,
|
||||
reason: 'update password failed',
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const onLanguageChange = async (newLang?: string) => {
|
||||
if (!newLang) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
updateProfileEvent.start();
|
||||
|
||||
await passportApi.updateUserProfile({
|
||||
locale: newLang,
|
||||
});
|
||||
localStorage.setItem('i18next', newLang === 'en-US' ? 'en' : newLang);
|
||||
updateProfileEvent.success();
|
||||
// 更新语言设置需要刷新页面才能生效
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 500);
|
||||
} catch (error) {
|
||||
updateProfileEvent.error({
|
||||
error: error as Error,
|
||||
reason: 'update language failed',
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const handleUsernameRegexpError = (value?: string) => {
|
||||
if (!value) {
|
||||
setUsernameErrorInfo('');
|
||||
return '';
|
||||
}
|
||||
const message = usernameRegExpValidate(value) || '';
|
||||
setUsernameErrorInfo(message);
|
||||
return message;
|
||||
};
|
||||
|
||||
const { run: validateUsername, cancel: cancelValidateUsername } = useRequest(
|
||||
async (innerUsername: string) => {
|
||||
await DeveloperApi.UpdateUserProfileCheck(
|
||||
{
|
||||
user_unique_name: innerUsername,
|
||||
},
|
||||
{ __disableErrorToast: true },
|
||||
);
|
||||
},
|
||||
{
|
||||
manual: true,
|
||||
debounceWait: CHECK_USER_NAME_DEBOUNCE_TIME,
|
||||
debounceLeading: false,
|
||||
debounceTrailing: true,
|
||||
onBefore: () => {
|
||||
updateProfileCheckEvent.start();
|
||||
setLoading(true);
|
||||
},
|
||||
onError: error => {
|
||||
updateProfileCheckEvent.error({ error, reason: error.message });
|
||||
if (isApiError(error)) {
|
||||
setUsernameErrorInfo(error.msg ?? '');
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
updateProfileCheckEvent.success();
|
||||
setUsernameErrorInfo('');
|
||||
},
|
||||
onFinally: () => {
|
||||
setLoading(false);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const onUsernameChange = async (innerUsername?: string) => {
|
||||
if (!innerUsername) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
updateProfileEvent.start();
|
||||
|
||||
setLoading(true);
|
||||
|
||||
await passportApi.updateUserProfile({
|
||||
user_unique_name: innerUsername,
|
||||
});
|
||||
updateProfileEvent.success();
|
||||
} catch (error) {
|
||||
updateProfileEvent.error({
|
||||
error: error as Error,
|
||||
reason: 'update username failed',
|
||||
});
|
||||
|
||||
if (isApiError(error)) {
|
||||
setUsernameErrorInfo(error.msg ?? '');
|
||||
}
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onUserInfoFieldCancel = () => {
|
||||
refreshUserInfo();
|
||||
setUsernameErrorInfo('');
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setNickname(userInfo?.name);
|
||||
setUsername(getUserName(userInfo));
|
||||
setAvatar(userInfo?.avatar_url ?? '');
|
||||
}, [userInfo]);
|
||||
|
||||
// 在进入和离开时均刷新一次用户信息
|
||||
useEffect(() => {
|
||||
refreshUserInfo();
|
||||
return () => {
|
||||
refreshUserInfo();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
styles['edit-profile'],
|
||||
'flex flex-col w-full h-full',
|
||||
)}
|
||||
>
|
||||
<UpdateUserAvatar
|
||||
className={styles['update-avatar']}
|
||||
value={avatar}
|
||||
onSuccess={url => {
|
||||
setAvatar(url);
|
||||
Toast.success({
|
||||
content: I18n.t('upload_avatar_success'),
|
||||
showClose: false,
|
||||
});
|
||||
}}
|
||||
onError={() =>
|
||||
Toast.error({
|
||||
content: 'upload_avatar_failed',
|
||||
})
|
||||
}
|
||||
ref={uploadRef}
|
||||
/>
|
||||
<UserInfoFieldWrap label={I18n.t('user_info_username')}>
|
||||
<div className="flex">
|
||||
<UserInfoField
|
||||
loading={loading}
|
||||
className={styles['info-field']}
|
||||
value={username}
|
||||
onChange={v => {
|
||||
setUsername(v ?? '');
|
||||
const message = handleUsernameRegexpError(v);
|
||||
if (message) {
|
||||
cancelValidateUsername();
|
||||
setLoading(false);
|
||||
} else {
|
||||
v && validateUsername(v);
|
||||
}
|
||||
}}
|
||||
customContent={
|
||||
!username ? (
|
||||
<div
|
||||
className={classNames(
|
||||
'inline-flex items-center gap-[2px] shrink-0',
|
||||
'text-[12px] font-[500] coz-fg-hglt-red',
|
||||
)}
|
||||
>
|
||||
<IconCozWarningCircleFillPalette />
|
||||
{I18n.t('setting_username_empty')}
|
||||
</div>
|
||||
) : undefined
|
||||
}
|
||||
errorMessage={userNameErrorInfo}
|
||||
customComponent={WrappedUsernameInput}
|
||||
onSave={onUsernameChange}
|
||||
onCancel={() => {
|
||||
setUsername(getUserName(userInfo));
|
||||
onUserInfoFieldCancel();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</UserInfoFieldWrap>
|
||||
<UserInfoFieldWrap label={I18n.t('user_info_custom_name')}>
|
||||
<div className="flex">
|
||||
<UserInfoField
|
||||
loading={loading}
|
||||
className={styles['info-field']}
|
||||
value={nickname}
|
||||
onChange={setNickname}
|
||||
customComponent={WrappedInputWithCount}
|
||||
onSave={onNicknameChange}
|
||||
onCancel={onUserInfoFieldCancel}
|
||||
/>
|
||||
</div>
|
||||
</UserInfoFieldWrap>
|
||||
<UserInfoFieldWrap label={I18n.t('user_info_email')}>
|
||||
<div className="flex">
|
||||
<UserInfoField
|
||||
readonly
|
||||
className={styles['info-field']}
|
||||
value={userInfo?.email || '-'}
|
||||
/>
|
||||
</div>
|
||||
</UserInfoFieldWrap>
|
||||
<UserInfoFieldWrap label={I18n.t('user_info_password')}>
|
||||
<div className="flex">
|
||||
<UserInfoField
|
||||
className={styles['info-field']}
|
||||
value={password}
|
||||
customContent={'******' /*<PasswordDesc value={password} />*/}
|
||||
customComponent={WrappedPasswordInput}
|
||||
onChange={val => setPassword(val ?? '')}
|
||||
onSave={onPasswordChange}
|
||||
onCancel={onUserInfoFieldCancel}
|
||||
/>
|
||||
</div>
|
||||
</UserInfoFieldWrap>
|
||||
<UserInfoFieldWrap label={I18n.t('language')}>
|
||||
<div className="flex">
|
||||
<UserInfoField
|
||||
className={styles['info-field']}
|
||||
value={lang}
|
||||
customContent={
|
||||
getLanguageOptions().find(item => item.value === lang)?.label
|
||||
}
|
||||
customComponent={WrappedSelectInput}
|
||||
onChange={langValue =>
|
||||
setLang((langValue as 'zh-CN' | 'en-US') ?? 'zh-CN')
|
||||
}
|
||||
onSave={onLanguageChange}
|
||||
/>
|
||||
</div>
|
||||
</UserInfoFieldWrap>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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 { useState } from 'react';
|
||||
|
||||
import { IconCozEyeClose, IconCozEye } from '@coze-arch/coze-design/icons';
|
||||
|
||||
export const PasswordDesc = ({ value }: { value: string }) => {
|
||||
const [show] = useState(false);
|
||||
|
||||
const displayValue = show ? value : '******';
|
||||
|
||||
return (
|
||||
<div className="flex">
|
||||
<div>{displayValue}</div>
|
||||
{show ? <IconCozEye /> : <IconCozEyeClose />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,200 @@
|
||||
/*
|
||||
* 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 CSSProperties,
|
||||
type ComponentType,
|
||||
type PropsWithChildren,
|
||||
type ReactNode,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozEdit } from '@coze-arch/coze-design/icons';
|
||||
import {
|
||||
IconButton,
|
||||
Input,
|
||||
Button,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from '@coze-arch/coze-design';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
interface BaseValueProps {
|
||||
value?: string;
|
||||
onChange?: (v?: string) => void;
|
||||
onEnterPress?: () => void;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export interface UserInfoFieldProps extends BaseValueProps {
|
||||
onSave?: (v?: string) => Promise<void>;
|
||||
onCancel?: () => void;
|
||||
loading?: boolean;
|
||||
customComponent?: ComponentType<BaseValueProps>;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
readonly?: boolean;
|
||||
disabled?: boolean;
|
||||
disabledTip?: ReactNode;
|
||||
customContent?: ReactNode;
|
||||
}
|
||||
|
||||
const EditWrap: React.FC<
|
||||
PropsWithChildren<
|
||||
Pick<
|
||||
UserInfoFieldProps,
|
||||
| 'onSave'
|
||||
| 'onCancel'
|
||||
| 'loading'
|
||||
| 'className'
|
||||
| 'style'
|
||||
| 'errorMessage'
|
||||
| 'value'
|
||||
>
|
||||
>
|
||||
> = ({
|
||||
onSave,
|
||||
onCancel,
|
||||
loading,
|
||||
children,
|
||||
className,
|
||||
style,
|
||||
errorMessage,
|
||||
value,
|
||||
}) => (
|
||||
<div className={classNames(s['field-edit'], className)} style={style}>
|
||||
<div className={s['field-edit-children']}>{children}</div>
|
||||
<Button
|
||||
className={s.btn}
|
||||
color="primary"
|
||||
loading={loading}
|
||||
onClick={() => {
|
||||
onCancel?.();
|
||||
}}
|
||||
data-testid="bot-edit-field-cancel-button"
|
||||
>
|
||||
{I18n.t('Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={Boolean(errorMessage) || !value}
|
||||
className={s.btn}
|
||||
loading={loading}
|
||||
onClick={() => {
|
||||
onSave?.();
|
||||
}}
|
||||
data-testid="bot-edit-field-save-button"
|
||||
>
|
||||
{I18n.t('setting_name_save')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const UserInfoField: React.FC<UserInfoFieldProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
onCancel,
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
customComponent: CustomComponent,
|
||||
onSave,
|
||||
loading,
|
||||
className,
|
||||
style,
|
||||
readonly,
|
||||
disabled,
|
||||
disabledTip,
|
||||
errorMessage,
|
||||
customContent,
|
||||
}) => {
|
||||
const [isEdit, setEdit] = useState(false);
|
||||
const handleSave = async () => {
|
||||
await onSave?.(value);
|
||||
setEdit(false);
|
||||
};
|
||||
const EditButton = (
|
||||
<IconButton
|
||||
disabled={disabled}
|
||||
icon={<IconCozEdit />}
|
||||
size="mini"
|
||||
color="secondary"
|
||||
className="ml-[8px]"
|
||||
onClick={() => {
|
||||
setEdit(true);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
if (!isEdit) {
|
||||
return (
|
||||
<div className={classNames(s['filed-readonly'], className)} style={style}>
|
||||
{customContent ? (
|
||||
customContent
|
||||
) : (
|
||||
<Typography.Text
|
||||
fontSize="14px"
|
||||
className="!font-medium coz-fg-primary"
|
||||
ellipsis
|
||||
>
|
||||
{value}
|
||||
</Typography.Text>
|
||||
)}
|
||||
{!readonly &&
|
||||
(disabled && disabledTip ? (
|
||||
<Tooltip content={disabledTip}>{EditButton}</Tooltip>
|
||||
) : (
|
||||
EditButton
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (CustomComponent) {
|
||||
return (
|
||||
<EditWrap
|
||||
value={value}
|
||||
errorMessage={errorMessage}
|
||||
onSave={handleSave}
|
||||
loading={loading}
|
||||
onCancel={() => {
|
||||
setEdit(false);
|
||||
onCancel?.();
|
||||
}}
|
||||
>
|
||||
<CustomComponent
|
||||
errorMessage={errorMessage}
|
||||
onEnterPress={handleSave}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</EditWrap>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<EditWrap
|
||||
value={value}
|
||||
errorMessage={errorMessage}
|
||||
onSave={handleSave}
|
||||
loading={loading}
|
||||
onCancel={() => {
|
||||
setEdit(false);
|
||||
onCancel?.();
|
||||
}}
|
||||
>
|
||||
<Input onEnterPress={handleSave} value={value} onChange={onChange} />
|
||||
</EditWrap>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,94 @@
|
||||
.input {
|
||||
input {
|
||||
/* stylelint-disable-next-line declaration-no-important */
|
||||
padding: 6px 0 6px 16px !important;
|
||||
}
|
||||
|
||||
:global(.semi-input-prefix) {
|
||||
width: 42px;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0 16px;
|
||||
|
||||
border-radius: 8px 0 0 8px;
|
||||
}
|
||||
|
||||
:global(.semi-input-suffix) {
|
||||
>span {
|
||||
padding-right: 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
|
||||
>span:first-child {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
>span:last-child {
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.page {
|
||||
border-color: rgba(29, 28, 35, 16%);
|
||||
}
|
||||
|
||||
&.modal {
|
||||
border-color: rgba(29, 28, 35, 8%);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--semi-color-focus-border);
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
border-color: var(--semi-color-focus-border);
|
||||
}
|
||||
|
||||
&.error {
|
||||
border-color: var(--semi-color-danger-light-hover);
|
||||
|
||||
:global(.semi-input-prefix) {
|
||||
background: #ffe0d2;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
// border-color: var(--semi-color-danger-light-hover) !important;
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
/* stylelint-disable-next-line declaration-no-important */
|
||||
border-color: var(--semi-color-danger) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page {
|
||||
margin-bottom: 12px;
|
||||
|
||||
:global(.semi-input-prefix) {
|
||||
border-right: 1px solid rgba(29, 28, 35, 16%);
|
||||
}
|
||||
|
||||
&.error {
|
||||
:global(.semi-input-prefix) {
|
||||
border-right-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal {
|
||||
margin-bottom: 8px;
|
||||
|
||||
:global(.semi-input-prefix) {
|
||||
border-right: 1px solid rgba(29, 28, 35, 8%);
|
||||
}
|
||||
|
||||
&.error {
|
||||
:global(.semi-input-prefix) {
|
||||
border-right-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 classNames from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Form, Input, type InputProps } from '@coze-arch/coze-design';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
export const USER_NAME_MAX_LEN = 20;
|
||||
|
||||
interface InputWithCountProps extends InputProps {
|
||||
// 设置字数限制并显示字数统计
|
||||
getValueLength?: (value?: InputProps['value'] | string) => number;
|
||||
}
|
||||
|
||||
export interface UsernameInputProps
|
||||
extends Omit<
|
||||
InputWithCountProps,
|
||||
'prefix' | 'placeholder' | 'maxLength' | 'validateStatus'
|
||||
> {
|
||||
scene?: 'modal' | 'page';
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export const UsernameInput: React.FC<UsernameInputProps> = ({
|
||||
className,
|
||||
scene = 'page',
|
||||
errorMessage,
|
||||
...props
|
||||
}) => {
|
||||
const isError = Boolean(errorMessage);
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
className={classNames(
|
||||
s.input,
|
||||
isError && s.error,
|
||||
scene === 'modal' ? s.modal : s.page,
|
||||
className,
|
||||
)}
|
||||
validateStatus={isError ? 'error' : 'default'}
|
||||
prefix="@"
|
||||
placeholder={I18n.t('username_placeholder')}
|
||||
maxLength={USER_NAME_MAX_LEN}
|
||||
{...props}
|
||||
/>
|
||||
<Form.ErrorMessage error={errorMessage} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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 { useNavigate } from 'react-router-dom';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Modal } from '@coze-arch/coze-design';
|
||||
import { logout } from '@coze-foundation/account-adapter';
|
||||
|
||||
export interface UseLogoutReturnType {
|
||||
open: () => void;
|
||||
close: () => void;
|
||||
node: JSX.Element;
|
||||
}
|
||||
|
||||
export const useLogout = (): UseLogoutReturnType => {
|
||||
const navigate = useNavigate();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const node = (
|
||||
<Modal
|
||||
visible={visible}
|
||||
title={I18n.t('log_out_desc')}
|
||||
okText={I18n.t('basic_log_out')}
|
||||
cancelText={I18n.t('Cancel')}
|
||||
centered
|
||||
onOk={async () => {
|
||||
await logout();
|
||||
setVisible(false);
|
||||
// 跳转到根路径
|
||||
navigate('/');
|
||||
}}
|
||||
onCancel={() => {
|
||||
setVisible(false);
|
||||
}}
|
||||
okButtonColor="red"
|
||||
/>
|
||||
);
|
||||
|
||||
return {
|
||||
node,
|
||||
open: () => {
|
||||
setVisible(true);
|
||||
},
|
||||
close: () => {
|
||||
setVisible(false);
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,152 @@
|
||||
/* stylelint-disable declaration-no-important */
|
||||
@import '@coze-common/assets/style/common.less';
|
||||
|
||||
.profile-modal {
|
||||
.profile-left {
|
||||
position: relative;
|
||||
min-width: 200px;
|
||||
padding: 0 8px;
|
||||
|
||||
:global {
|
||||
.semi-tabs-tab-button.semi-tabs-tab {
|
||||
@apply coz-fg-plus;
|
||||
|
||||
padding: 8px;
|
||||
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.semi-tabs-tab-button.semi-tabs-tab-active,
|
||||
.semi-tabs-tab-button:hover {
|
||||
@apply coz-mg-primary;
|
||||
}
|
||||
|
||||
.semi-tabs-tab:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.semi-tabs-bar-left {
|
||||
width: 184px;
|
||||
}
|
||||
|
||||
.semi-tabs-tab-button.semi-tabs-tab-disabled {
|
||||
cursor: default;
|
||||
background-color: transparent;
|
||||
|
||||
@apply coz-fg-secondary;
|
||||
|
||||
}
|
||||
|
||||
.semi-tabs-bar-button.semi-tabs-bar-left .semi-tabs-tab:not(:last-of-type) {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.semi-tabs-tab:last-child::before {
|
||||
content: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 200px;
|
||||
|
||||
height: 600px;
|
||||
|
||||
border-right: 1px solid var(--coz-stroke-primary);
|
||||
}
|
||||
|
||||
.text-20 {
|
||||
@apply coz-fg-plus;
|
||||
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.text-16 {
|
||||
@apply coz-fg-plus;
|
||||
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.profile-right {
|
||||
overflow-y: auto;
|
||||
padding: 0 8px;
|
||||
|
||||
.title {
|
||||
position: sticky;
|
||||
z-index: 1;
|
||||
top:0;
|
||||
|
||||
width: 100%;
|
||||
|
||||
font-size: 20px;
|
||||
font-weight: 500 !important;
|
||||
line-height: 40px;
|
||||
|
||||
@apply coz-fg-plus bg-background-2;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.container{
|
||||
width: 100%;
|
||||
height: calc(100% - 86px);
|
||||
}
|
||||
|
||||
:global {
|
||||
.semi-modal .semi-modal-content {
|
||||
max-height: 600px;
|
||||
padding-top:0;
|
||||
padding-left: 0;
|
||||
|
||||
.semi-modal-body {
|
||||
padding-top: 0;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.semi-modal-close {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
right:24px;
|
||||
background-color: rgba(var(--coze-bg-2), 1);
|
||||
}
|
||||
}
|
||||
|
||||
/* 当浏览器窗口的高度大于等于760px时, 高度固定 */
|
||||
@media screen and (min-height: 760px) {
|
||||
:global {
|
||||
.semi-modal {
|
||||
height: 600px;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-right {
|
||||
height: 574px; // = 600px - 26px(margin-top + border)
|
||||
}
|
||||
}
|
||||
|
||||
/* 当浏览器窗口的高度小于760px时, 高度 100vh - 160px */
|
||||
@media screen and (max-height: 759px) {
|
||||
:global {
|
||||
.semi-modal {
|
||||
height: calc(100vh - 160px);
|
||||
}
|
||||
}
|
||||
|
||||
.profile-right {
|
||||
height: max(448px, calc(100vh - 160px - 26px));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* 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 ReactElement, useState } from 'react';
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Typography, Space } from '@coze-arch/coze-design';
|
||||
import { UITabBar, Divider } from '@coze-arch/bot-semi';
|
||||
|
||||
import { useModal } from './use-modal';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
export interface TabItem {
|
||||
id: string;
|
||||
tabName: string;
|
||||
/**
|
||||
* @param close 关闭setting弹窗
|
||||
* @returns ReactElement
|
||||
*/
|
||||
content: (close?: () => void) => ReactElement;
|
||||
}
|
||||
|
||||
export const useAccountSettings = ({
|
||||
tabs,
|
||||
onClose,
|
||||
}: {
|
||||
tabs: Array<TabItem | 'divider'>;
|
||||
onClose?: () => void;
|
||||
}) => {
|
||||
const realTabs = tabs.filter(item => item !== 'divider');
|
||||
|
||||
const [currentTab, setCurrentTab] = useState(() => realTabs[0]?.id);
|
||||
|
||||
const { content, tabName } =
|
||||
realTabs.find(item => String(item.id) === currentTab) || {};
|
||||
|
||||
const tabList = tabs.map(item => {
|
||||
if (item === 'divider') {
|
||||
return {
|
||||
tab: <Divider className="disabled pt-[1px] pb-[1px]" />,
|
||||
itemKey: 'general',
|
||||
disabled: true,
|
||||
};
|
||||
}
|
||||
return {
|
||||
tab: item.tabName,
|
||||
itemKey: String(item.id),
|
||||
};
|
||||
});
|
||||
|
||||
const { open, close, modal } = useModal({
|
||||
title: null,
|
||||
centered: true,
|
||||
onCancel: () => {
|
||||
onClose?.();
|
||||
close();
|
||||
},
|
||||
className: s['profile-modal'],
|
||||
height: 600,
|
||||
width: 1120,
|
||||
linearGradientMask: true,
|
||||
});
|
||||
|
||||
const Content = (
|
||||
<Space align="start" spacing={24} className="flex">
|
||||
<Space vertical align="start" spacing={16} className={s['profile-left']}>
|
||||
<Typography.Text className={`${s['text-20']} pl-[8px]`}>
|
||||
{I18n.t('profile_settings')}
|
||||
</Typography.Text>
|
||||
<UITabBar
|
||||
wrapperClass={s['profile-tab']}
|
||||
tabList={tabList}
|
||||
activeKey={currentTab}
|
||||
onChange={setCurrentTab}
|
||||
tabPosition="left"
|
||||
type="button"
|
||||
/>
|
||||
</Space>
|
||||
<div className={s.divider}></div>
|
||||
<Space vertical className={'w-full' + ` ${s['profile-right']}`}>
|
||||
<Typography.Text className={`${s.title}`}>{tabName}</Typography.Text>
|
||||
<div className={s.container}>{content?.(close)}</div>
|
||||
</Space>
|
||||
</Space>
|
||||
);
|
||||
|
||||
return {
|
||||
node: <>{modal(Content)}</>,
|
||||
open: (tabId?: string) => {
|
||||
if (tabId && realTabs.find(item => String(item.id) === tabId)) {
|
||||
setCurrentTab(tabId);
|
||||
}
|
||||
open();
|
||||
},
|
||||
close: () => {
|
||||
close();
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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 { useState } from 'react';
|
||||
|
||||
import { Modal, type ModalProps } from '@coze-arch/coze-design';
|
||||
|
||||
export type UseModalParams = Omit<ModalProps, 'visible'>;
|
||||
|
||||
export interface UseModalReturnValue {
|
||||
modal: (inner: JSX.Element) => JSX.Element;
|
||||
open: () => void;
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
export const useModal = (params: UseModalParams): UseModalReturnValue => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
return {
|
||||
modal: inner => (
|
||||
<Modal {...params} visible={visible}>
|
||||
{inner}
|
||||
</Modal>
|
||||
),
|
||||
open: () => setVisible(true),
|
||||
close: () => setVisible(false),
|
||||
};
|
||||
};
|
||||
21
frontend/packages/foundation/account-ui-base/src/index.ts
Normal file
21
frontend/packages/foundation/account-ui-base/src/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* 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 { useLogout } from './hooks/logout';
|
||||
export { RequireAuthContainer } from './components/require-auth-container';
|
||||
export { LoadingContainer } from './components/loading-container';
|
||||
export { UserInfoPanel } from './components/user-info-panel';
|
||||
export { useAccountSettings } from './hooks/use-account-settings';
|
||||
17
frontend/packages/foundation/account-ui-base/src/typings.d.ts
vendored
Normal file
17
frontend/packages/foundation/account-ui-base/src/typings.d.ts
vendored
Normal 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.
|
||||
*/
|
||||
|
||||
/// <reference types='@coze-arch/bot-typings' />
|
||||
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"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"],
|
||||
"module": "ESNext",
|
||||
"target": "ES2020",
|
||||
"moduleResolution": "bundler",
|
||||
"tsBuildInfoFile": "dist/tsconfig.build.tsbuildinfo"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../account-adapter/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../arch/bot-api/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../arch/bot-http/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../arch/bot-typings/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../arch/bot-utils/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../arch/foundation-sdk/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../arch/i18n/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../arch/report-events/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../common/assets/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../common/biz-components/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../components/bot-semi/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": "../../studio/user-store/tsconfig.build.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
15
frontend/packages/foundation/account-ui-base/tsconfig.json
Normal file
15
frontend/packages/foundation/account-ui-base/tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"exclude": ["**/*"],
|
||||
"compilerOptions": {
|
||||
"composite": true
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.misc.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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", "stories"],
|
||||
"exclude": ["./dist"],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.build.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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',
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import { mergeConfig } from 'vite';
|
||||
import svgr from 'vite-plugin-svgr';
|
||||
|
||||
/** @type { import('@storybook/react-vite').StorybookConfig } */
|
||||
const config = {
|
||||
stories: ['../stories/**/*.mdx', '../stories/**/*.stories.tsx'],
|
||||
addons: [
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-essentials',
|
||||
'@storybook/addon-onboarding',
|
||||
'@storybook/addon-interactions',
|
||||
],
|
||||
framework: {
|
||||
name: '@storybook/react-vite',
|
||||
options: {},
|
||||
},
|
||||
docs: {
|
||||
autodocs: 'tag',
|
||||
},
|
||||
viteFinal: config =>
|
||||
mergeConfig(config, {
|
||||
plugins: [
|
||||
svgr({
|
||||
svgrOptions: {
|
||||
native: false,
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
};
|
||||
export default config;
|
||||
@@ -0,0 +1,14 @@
|
||||
/** @type { import('@storybook/react').Preview } */
|
||||
const preview = {
|
||||
parameters: {
|
||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default preview;
|
||||
@@ -0,0 +1,5 @@
|
||||
const { defineConfig } = require('@coze-arch/stylelint-config');
|
||||
|
||||
module.exports = defineConfig({
|
||||
extends: [],
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
# @coze-foundation/browser-upgrade-banner
|
||||
|
||||
低版本浏览器升级条幅
|
||||
|
||||
## Features
|
||||
|
||||
- [x] eslint & ts
|
||||
- [x] esm bundle
|
||||
- [x] umd bundle
|
||||
- [x] storybook
|
||||
|
||||
## Commands
|
||||
|
||||
- init: `rush update`
|
||||
- dev: `npm run dev`
|
||||
- build: `npm run build`
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"operationSettings": [
|
||||
{
|
||||
"operationName": "test:cov",
|
||||
"outputFolderNames": ["coverage"]
|
||||
},
|
||||
{
|
||||
"operationName": "ts-check",
|
||||
"outputFolderNames": ["./dist"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
const { defineConfig } = require('@coze-arch/eslint-config');
|
||||
|
||||
module.exports = defineConfig({
|
||||
packageRoot: __dirname,
|
||||
preset: 'web',
|
||||
rules: {},
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "@coze-foundation/browser-upgrade-banner",
|
||||
"version": "0.0.1",
|
||||
"description": "低版本浏览器升级条幅",
|
||||
"license": "Apache-2.0",
|
||||
"author": "liushuoyan@bytedance.com",
|
||||
"maintainers": [],
|
||||
"main": "src/index.tsx",
|
||||
"scripts": {
|
||||
"build": "exit 0",
|
||||
"lint": "eslint ./ --cache",
|
||||
"test": "vitest --run --passWithNoTests",
|
||||
"test:cov": "npm run test -- --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@coze-arch/coze-design": "0.0.6-alpha.346d77",
|
||||
"@coze-arch/i18n": "workspace:*",
|
||||
"@coze-arch/logger": "workspace:*",
|
||||
"@types/detect-browser": "~4.0.0",
|
||||
"classnames": "^2.3.2",
|
||||
"detect-browser": "~5.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@coze-arch/bot-typings": "workspace:*",
|
||||
"@coze-arch/eslint-config": "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",
|
||||
"react-dom": ">=18.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
.flex-helper {
|
||||
/* stylelint-disable-next-line value-no-vendor-prefix */
|
||||
display: -webkit-flex; /* 新版本语法: Chrome 21+ */
|
||||
display: flex; /* 新版本语法: Opera 12.1, Firefox 22+ */
|
||||
display: -webkit-box; /* 老版本语法: Safari, iOS, Android browser, older WebKit browsers. */
|
||||
display: -moz-box; /* 老版本语法: Firefox (buggy) */
|
||||
/* stylelint-disable-next-line value-no-vendor-prefix */
|
||||
display: -ms-flexbox; /* 混合版本语法: IE 10 */
|
||||
}
|
||||
|
||||
.flex-1-helper {
|
||||
/* stylelint-disable-next-line property-no-vendor-prefix */
|
||||
-webkit-flex: 1; /* Chrome */
|
||||
/* stylelint-disable-next-line property-no-vendor-prefix */
|
||||
-ms-flex: 1; /* IE 10 */
|
||||
flex: 1; /* NEW, Spec - Opera 12.1, Firefox 20+ */
|
||||
|
||||
-webkit-box-flex: 1; /* OLD - iOS 6-, Safari 3.1-6 */
|
||||
-moz-box-flex: 1; /* OLD - Firefox 19- */
|
||||
}
|
||||
|
||||
.flex-direction-row-helper {
|
||||
/* stylelint-disable-next-line property-no-vendor-prefix */
|
||||
-webkit-flex-direction: row;
|
||||
/* stylelint-disable-next-line property-no-vendor-prefix */
|
||||
-ms-flex-direction: row;
|
||||
flex-direction: row;
|
||||
|
||||
-webkit-box-direction: normal;
|
||||
-moz-box-direction: normal;
|
||||
-webkit-box-orient: horizontal;
|
||||
-moz-box-orient: horizontal;
|
||||
}
|
||||
|
||||
.flex-items-center {
|
||||
/* stylelint-disable-next-line property-no-vendor-prefix */
|
||||
-webkit-align-items: center; /* Chrome 21+, Safari 6.1+, Opera 15+ */
|
||||
align-items: center; /* 新语法 */
|
||||
|
||||
-ms-flex-align: center; /* IE 10 */
|
||||
}
|
||||
|
||||
.flex-justify-center {
|
||||
/* stylelint-disable-next-line property-no-vendor-prefix */
|
||||
-webkit-justify-content: center; /* Chrome 21+, Safari 6.1+ */
|
||||
justify-content: center; /* 新版浏览器 */
|
||||
|
||||
-webkit-box-pack: center; /* iOS 6-, Safari 3.1-6 */
|
||||
-moz-box-pack: center; /* 早期版本的 Firefox */
|
||||
-ms-flex-pack: center; /* IE 10 */
|
||||
}
|
||||
|
||||
.banner-wrapper {
|
||||
width: 100%;
|
||||
min-height: 48px;
|
||||
padding: 0 64px;
|
||||
background: #E5B65C;
|
||||
}
|
||||
|
||||
.banner-item {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
color: #FFF;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.banner-upgrade-button {
|
||||
cursor: pointer;
|
||||
|
||||
margin-left: 6px;
|
||||
|
||||
font-weight: bold;
|
||||
text-decoration-line: underline;
|
||||
text-underline-offset: 4px;
|
||||
}
|
||||
|
||||
.close {
|
||||
cursor: pointer;
|
||||
color: #FFF;
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* 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,
|
||||
useRef,
|
||||
useState,
|
||||
type FC,
|
||||
type PropsWithChildren,
|
||||
} from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { reporter } from '@coze-arch/logger';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozCross } from '@coze-arch/coze-design/icons';
|
||||
|
||||
import { testLowVersionBrowse } from '../../utils';
|
||||
import { EventNames } from '../../constants';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
type IProps = Record<string, unknown>;
|
||||
|
||||
interface BannerInfo {
|
||||
url: string;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
export const BrowserUpgradeWrap: FC<PropsWithChildren<IProps>> = props => {
|
||||
const { children } = props;
|
||||
|
||||
const [bannerInfo, setBannerInfo] = useState<BannerInfo>({
|
||||
url: '',
|
||||
visible: false,
|
||||
});
|
||||
const [bannerHeight, setBannerHeight] = useState(0);
|
||||
|
||||
const bannerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const result = testLowVersionBrowse();
|
||||
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { downloadUrl } = result;
|
||||
|
||||
reporter.event({
|
||||
eventName: EventNames.BrowserUpgradeTipsVisible,
|
||||
});
|
||||
|
||||
setBannerInfo({ url: downloadUrl, visible: !!downloadUrl });
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!bannerRef.current) {
|
||||
setBannerHeight(0);
|
||||
return;
|
||||
}
|
||||
|
||||
setBannerHeight(bannerRef.current.getBoundingClientRect().height ?? 0);
|
||||
}, [bannerInfo]);
|
||||
|
||||
const handleClick = () => {
|
||||
if (!bannerInfo.url) {
|
||||
return;
|
||||
}
|
||||
|
||||
reporter.event({
|
||||
eventName: EventNames.BrowserUpgradeClick,
|
||||
});
|
||||
|
||||
window.open(bannerInfo.url);
|
||||
};
|
||||
|
||||
const handleBannerClose = () => {
|
||||
setBannerInfo(prevState => ({ ...prevState, visible: false }));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{bannerInfo.visible ? (
|
||||
<div
|
||||
className={classNames(
|
||||
styles['banner-wrapper'],
|
||||
styles['flex-helper'],
|
||||
styles['flex-direction-row-helper'],
|
||||
styles['flex-items-center'],
|
||||
)}
|
||||
ref={bannerRef}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
styles['banner-item'],
|
||||
styles['flex-1-helper'],
|
||||
styles['flex-items-center'],
|
||||
styles['flex-justify-center'],
|
||||
)}
|
||||
>
|
||||
<span>{I18n.t('browser_upgrade')}: </span>
|
||||
<span
|
||||
className={styles['banner-upgrade-button']}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{I18n.t('browser_upgrade_button')}
|
||||
</span>
|
||||
</div>
|
||||
<div onClick={handleBannerClose}>
|
||||
<IconCozCross className={styles.close} />
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<div
|
||||
style={{
|
||||
height: `calc(100% - ${bannerHeight}px)`,
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 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 const enum EventNames {
|
||||
BrowserUpgradeTipsVisible = 'browserUpgradeTipsVisible',
|
||||
BrowserUpgradeClick = 'browserUpgradeClick',
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
export { BrowserUpgradeWrap } from './components/browser-upgrade-wrap';
|
||||
17
frontend/packages/foundation/browser-upgrade-banner/src/typings.d.ts
vendored
Normal file
17
frontend/packages/foundation/browser-upgrade-banner/src/typings.d.ts
vendored
Normal 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.
|
||||
*/
|
||||
|
||||
/// <reference types='@coze-arch/bot-typings' />
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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 function compareVersion(version1: string, version2: string): number {
|
||||
// 将版本号字符串分割成数字数组,这里使用map(Number)确保转换为数字类型
|
||||
const parts1 = version1.split('.').map(Number);
|
||||
const parts2 = version2.split('.').map(Number);
|
||||
|
||||
// 计算出最长的版本号长度
|
||||
const maxLength = Math.max(parts1.length, parts2.length);
|
||||
|
||||
// 逐个比较版本号中的每个部分
|
||||
for (let i = 0; i < maxLength; i++) {
|
||||
// 如果某个版本号在这个位置没有对应的数字,则视为0
|
||||
const part1 = i < parts1.length ? parts1[i] : 0;
|
||||
const part2 = i < parts2.length ? parts2[i] : 0;
|
||||
|
||||
// 比较两个版本号的当前部分
|
||||
if (part1 > part2) {
|
||||
return 1;
|
||||
}
|
||||
if (part1 < part2) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果所有部分都相等,则版本号相等
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* 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 { detect, type Browser } from 'detect-browser';
|
||||
|
||||
import { compareVersion } from './compare-version';
|
||||
|
||||
type VersionConfig = {
|
||||
[K in Browser]?: string;
|
||||
};
|
||||
|
||||
type DownloadConfig = {
|
||||
[K in Browser]?: string;
|
||||
};
|
||||
|
||||
const PC_VERSION_CONFIG: VersionConfig = {
|
||||
['chrome']: '87.0.0',
|
||||
['edge-chromium']: '100.0.0',
|
||||
['edge']: '100.0.0',
|
||||
['safari']: '14.0.0',
|
||||
['firefox']: '79.0.0',
|
||||
['ie']: '999999.0.0',
|
||||
};
|
||||
|
||||
// cp-disable-next-line
|
||||
const CN_CHROME_URL = 'https://www.google.cn/chrome/';
|
||||
// cp-disable-next-line
|
||||
const INTERNATIONAL_CHROME_URL = 'https://www.google.com/chrome/';
|
||||
|
||||
// cp-disable-next-line
|
||||
const CN_EDGE_URL = 'https://www.microsoft.com/zh-cn/edge';
|
||||
// cp-disable-next-line
|
||||
const INTERNATIONAL_EDGE_URL = 'https://www.microsoft.com/edge';
|
||||
|
||||
const CN_BROWSER_DOWNLOAD_CONFIG: DownloadConfig = {
|
||||
['chrome']: CN_CHROME_URL,
|
||||
['edge-chromium']: CN_EDGE_URL,
|
||||
['edge']: CN_EDGE_URL,
|
||||
// cp-disable-next-line
|
||||
['safari']: 'https://apps.apple.com/cn/app/safari/id1146562112',
|
||||
// cp-disable-next-line
|
||||
['firefox']: 'https://www.mozilla.org/zh-CN/firefox/new/',
|
||||
['ie']: CN_CHROME_URL,
|
||||
};
|
||||
|
||||
const INTERNATIONAL_BROWSER_DOWNLOAD_CONFIG: DownloadConfig = {
|
||||
['chrome']: INTERNATIONAL_CHROME_URL,
|
||||
['edge-chromium']: INTERNATIONAL_EDGE_URL,
|
||||
['edge']: INTERNATIONAL_EDGE_URL,
|
||||
// cp-disable-next-line
|
||||
['safari']: 'https://apps.apple.com/app/safari/id1146562112',
|
||||
// cp-disable-next-line
|
||||
['firefox']: 'https://www.mozilla.org/firefox/new/',
|
||||
['ie']: INTERNATIONAL_CHROME_URL,
|
||||
};
|
||||
|
||||
/**
|
||||
* 目前看起来 移动端 / PC 版本一致无需区分,后期如果区分,在这里通过条件区分
|
||||
*/
|
||||
export const testLowVersionBrowse = () => testPCVersion();
|
||||
|
||||
const testPCVersion = () => {
|
||||
const browserInfo = detect(navigator.userAgent);
|
||||
|
||||
if (!browserInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { name, version } = browserInfo;
|
||||
|
||||
// 显示的判断,用 includes 类型推断不正确
|
||||
if (name === 'bot' || name === 'react-native' || name === 'node') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const configVersion = PC_VERSION_CONFIG[name];
|
||||
|
||||
if (!configVersion) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (compareVersion(version, configVersion) >= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
downloadUrl: IS_OVERSEA
|
||||
? (INTERNATIONAL_BROWSER_DOWNLOAD_CONFIG[name] ??
|
||||
INTERNATIONAL_CHROME_URL)
|
||||
: (CN_BROWSER_DOWNLOAD_CONFIG[name] ?? CN_CHROME_URL),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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 const isMobileFromUA = () => {
|
||||
const { userAgent } = navigator;
|
||||
// 检查是否为移动设备
|
||||
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
|
||||
userAgent,
|
||||
);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user