@pmate/account-sdk 0.5.5 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +14 -21
- package/src/api/AccountService.ts +97 -0
- package/src/api/Api.ts +99 -0
- package/src/api/AppService.ts +71 -0
- package/src/api/EntityService.ts +41 -0
- package/src/api/ProfileService.ts +133 -0
- package/src/api/cacheInMem.ts +73 -0
- package/src/api/index.ts +5 -0
- package/src/atoms/accountAtom.ts +18 -0
- package/src/atoms/accountProfileAtom.ts +13 -0
- package/src/atoms/atomWithLoadable.ts +48 -0
- package/src/atoms/createProfileAtom.ts +27 -0
- package/src/atoms/index.ts +16 -0
- package/src/atoms/learningLangAtom.ts +8 -0
- package/src/atoms/localStorageAtom.ts +39 -0
- package/src/atoms/loginAtom.ts +8 -0
- package/src/atoms/motherTongueAtom.ts +8 -0
- package/src/atoms/profileAtom.ts +24 -0
- package/src/atoms/profileDraftAtom.ts +7 -0
- package/src/atoms/profilesAtom.ts +20 -0
- package/src/atoms/sessionCheckAtom.ts +10 -0
- package/src/atoms/switchProfileAtom.ts +9 -0
- package/src/atoms/updateProfileAtom.ts +35 -0
- package/src/atoms/uploadAvatarAtom.ts +49 -0
- package/src/atoms/userLogoutAtom.ts +8 -0
- package/src/atoms/userSettingsAtom.ts +58 -0
- package/src/components/AuthProviderV2.tsx +315 -0
- package/src/components/Button.tsx +39 -0
- package/src/components/Drawer.tsx +80 -0
- package/src/components/index.ts +1 -0
- package/src/constants.ts +1 -0
- package/src/hooks/index.ts +5 -0
- package/src/hooks/useAppBackgroundStyle.ts +25 -0
- package/src/hooks/useAppConfig.ts +44 -0
- package/src/hooks/useAuthApp.ts +165 -0
- package/src/hooks/useAuthSnapshot.ts +84 -0
- package/src/hooks/useIsAuthenticated.ts +19 -0
- package/src/hooks/useProfileStepFlow.ts +59 -0
- package/src/i18n/index.ts +59 -0
- package/src/index.ts +9 -0
- package/src/locales/ar-SA.json +183 -0
- package/src/locales/de-DE.json +183 -0
- package/src/locales/el-GR.json +183 -0
- package/src/locales/en.json +183 -0
- package/src/locales/es-ES.json +183 -0
- package/src/locales/fi-FI.json +183 -0
- package/src/locales/fil-PH.json +183 -0
- package/src/locales/fr-FR.json +183 -0
- package/src/locales/hi-IN.json +183 -0
- package/src/locales/ja-JP.json +183 -0
- package/src/locales/ko-KR.json +183 -0
- package/src/locales/pt-BR.json +183 -0
- package/src/locales/pt-PT.json +183 -0
- package/src/locales/ru-RU.json +183 -0
- package/src/locales/ta-IN.json +183 -0
- package/src/locales/uk-UA.json +183 -0
- package/src/locales/zh-CN.json +183 -0
- package/src/locales/zh-TW.json +183 -0
- package/src/types/account.types.ts +28 -0
- package/src/types/app.ts +22 -0
- package/src/types/profile.ts +6 -0
- package/src/utils/AccountManagerV2.ts +349 -0
- package/src/utils/Redirect.ts +17 -0
- package/src/utils/accountStorage.ts +46 -0
- package/src/utils/errors.ts +1 -0
- package/src/utils/index.ts +11 -0
- package/src/utils/location.ts +34 -0
- package/src/utils/profileStep.ts +26 -0
- package/src/utils/resolveAppId.ts +13 -0
- package/src/utils/selectedProfileStorage.ts +46 -0
- package/src/utils/tokenStorage.ts +47 -0
- package/dist/index.cjs.js +0 -22
- package/dist/index.cjs.js.map +0 -1
- package/dist/index.d.ts +0 -305
- package/dist/index.es.js +0 -8897
- package/dist/index.es.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,19 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pmate/account-sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"module": "./dist/index.es.js",
|
|
6
|
-
"types": "./dist/index.d.ts",
|
|
7
|
-
"exports": {
|
|
8
|
-
".": {
|
|
9
|
-
"types": "./dist/index.d.ts",
|
|
10
|
-
"import": "./dist/index.es.js",
|
|
11
|
-
"require": "./dist/index.cjs.js"
|
|
12
|
-
}
|
|
13
|
-
},
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"type": "module",
|
|
14
5
|
"files": [
|
|
15
|
-
"
|
|
6
|
+
"src",
|
|
7
|
+
"!src/tests",
|
|
8
|
+
"!src/tests/**"
|
|
16
9
|
],
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./src/index.ts"
|
|
12
|
+
},
|
|
17
13
|
"publishConfig": {
|
|
18
14
|
"access": "public"
|
|
19
15
|
},
|
|
@@ -26,7 +22,10 @@
|
|
|
26
22
|
"browser-image-compression": "^2.0.2",
|
|
27
23
|
"i18next": "^23.0.0",
|
|
28
24
|
"react-i18next": "^13.0.0",
|
|
29
|
-
"react-use": "^17.6.0"
|
|
25
|
+
"react-use": "^17.6.0",
|
|
26
|
+
"@pmate/lang": "1.0.2",
|
|
27
|
+
"@pmate/utils": "1.0.3",
|
|
28
|
+
"@pmate/meta": "1.1.3"
|
|
30
29
|
},
|
|
31
30
|
"devDependencies": {
|
|
32
31
|
"@types/react": "*",
|
|
@@ -34,18 +33,12 @@
|
|
|
34
33
|
"@vitejs/plugin-react": "^4.2.1",
|
|
35
34
|
"jsdom": "^26.1.0",
|
|
36
35
|
"react-dom": "*",
|
|
37
|
-
"rollup": "^4.21.2",
|
|
38
|
-
"rollup-plugin-dts": "^6.1.1",
|
|
39
36
|
"typescript": "^5.2.2",
|
|
40
37
|
"vite": "^7.3.1",
|
|
41
|
-
"vitest": "^4.0.17"
|
|
42
|
-
"@pmate/utils": "1.0.0",
|
|
43
|
-
"@pmate/lang": "1.0.0",
|
|
44
|
-
"@pmate/meta": "1.1.1"
|
|
38
|
+
"vitest": "^4.0.17"
|
|
45
39
|
},
|
|
46
40
|
"scripts": {
|
|
47
|
-
"
|
|
48
|
-
"npm:publish": "pnpm build && npm publish --registry https://registry.npmjs.org/",
|
|
41
|
+
"npm:publish": "npm publish --registry https://registry.npmjs.org/",
|
|
49
42
|
"test": "vitest run",
|
|
50
43
|
"test:watch": "vitest"
|
|
51
44
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AuthLoginResponse,
|
|
3
|
+
AuthRequest,
|
|
4
|
+
AuthSession,
|
|
5
|
+
ServerResponse,
|
|
6
|
+
VCodeIssueRequest,
|
|
7
|
+
VCodeIssueResult,
|
|
8
|
+
} from "@pmate/meta"
|
|
9
|
+
import { lru } from "@pmate/utils"
|
|
10
|
+
import { getAuthToken } from "../utils/tokenStorage"
|
|
11
|
+
|
|
12
|
+
const AUTH_ENDPOINT = "https://auth-api-v2.pmate.chat"
|
|
13
|
+
type AuthRequestInit = RequestInit & { token?: string; app?: string }
|
|
14
|
+
|
|
15
|
+
export class AccountService {
|
|
16
|
+
private static sessionCached = lru(
|
|
17
|
+
(token?: string, app?: string) =>
|
|
18
|
+
AccountService.authRequest<AuthSession | null>("/session", {
|
|
19
|
+
method: "GET",
|
|
20
|
+
token,
|
|
21
|
+
app,
|
|
22
|
+
}),
|
|
23
|
+
{
|
|
24
|
+
ttl: 3_000,
|
|
25
|
+
key: (token, app) => token ?? getAuthToken(app) ?? `default:${app ?? ""}`,
|
|
26
|
+
}
|
|
27
|
+
)
|
|
28
|
+
public static async vcode(
|
|
29
|
+
payload: VCodeIssueRequest
|
|
30
|
+
): Promise<VCodeIssueResult> {
|
|
31
|
+
return AccountService.authRequest<VCodeIssueResult>("/vcode", {
|
|
32
|
+
method: "POST",
|
|
33
|
+
body: JSON.stringify(payload),
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public static async login(request: AuthRequest): Promise<AuthLoginResponse> {
|
|
38
|
+
return AccountService.authRequest<AuthLoginResponse>("/login", {
|
|
39
|
+
method: "POST",
|
|
40
|
+
body: JSON.stringify(request),
|
|
41
|
+
token: request.nonce,
|
|
42
|
+
app: request.app,
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public static async logout(token?: string, app?: string) {
|
|
47
|
+
return AccountService.authRequest<{ success: boolean }>("/logout", {
|
|
48
|
+
method: "POST",
|
|
49
|
+
token,
|
|
50
|
+
app,
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public static async logoutAll(token?: string, app?: string) {
|
|
55
|
+
return AccountService.authRequest<{ success: boolean }>("/logout-all", {
|
|
56
|
+
method: "POST",
|
|
57
|
+
token,
|
|
58
|
+
app,
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public static async session(token?: string, app?: string) {
|
|
63
|
+
return AccountService.sessionCached(token, app)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public static clearSessionCache() {
|
|
67
|
+
AccountService.sessionCached.clean()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private static async authRequest<T>(path: string, init: AuthRequestInit) {
|
|
71
|
+
const { token, headers, app, ...rest } = init
|
|
72
|
+
const resolvedToken = token ?? getAuthToken(app)
|
|
73
|
+
const url = new URL(`${AUTH_ENDPOINT.replace(/\/+$/, "")}${path}`)
|
|
74
|
+
if (app) {
|
|
75
|
+
url.searchParams.set("app", app)
|
|
76
|
+
}
|
|
77
|
+
const response = await fetch(
|
|
78
|
+
url.toString(),
|
|
79
|
+
{
|
|
80
|
+
credentials: "include",
|
|
81
|
+
...rest,
|
|
82
|
+
headers: {
|
|
83
|
+
"Content-Type": "application/json",
|
|
84
|
+
...(resolvedToken
|
|
85
|
+
? { Authorization: `Bearer ${resolvedToken}` }
|
|
86
|
+
: {}),
|
|
87
|
+
...(headers ?? {}),
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
const json = (await response.json()) as ServerResponse<T>
|
|
92
|
+
if (!response.ok || !json.success) {
|
|
93
|
+
throw json
|
|
94
|
+
}
|
|
95
|
+
return json.data
|
|
96
|
+
}
|
|
97
|
+
}
|
package/src/api/Api.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Maybe, isMaybe } from "@pmate/utils"
|
|
2
|
+
import { ServerResponse } from "@pmate/meta"
|
|
3
|
+
import { cacheInMem } from "./cacheInMem"
|
|
4
|
+
|
|
5
|
+
const checkLink = async (url: string | Maybe<string>): Promise<boolean> => {
|
|
6
|
+
if (isMaybe(url)) {
|
|
7
|
+
if (url.isNothing()) {
|
|
8
|
+
return false
|
|
9
|
+
}
|
|
10
|
+
url = url.unwrap()
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
const response = await fetch(url, { method: "HEAD" })
|
|
14
|
+
return response.ok
|
|
15
|
+
} catch {
|
|
16
|
+
return false
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const cachedLinkCheck = cacheInMem(checkLink, "link", 60 * 60 * 1000)
|
|
21
|
+
|
|
22
|
+
export class Api {
|
|
23
|
+
public static async get<T>(url: string): Promise<T | null> {
|
|
24
|
+
const response = await fetch(url)
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
28
|
+
const json = (await response.json()) as ServerResponse<T>
|
|
29
|
+
return json.data ?? null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public static async post<T>(url: string, body: unknown): Promise<T> {
|
|
33
|
+
const response = await fetch(url, {
|
|
34
|
+
method: "POST",
|
|
35
|
+
headers: { "Content-Type": "application/json" },
|
|
36
|
+
body: JSON.stringify(body),
|
|
37
|
+
})
|
|
38
|
+
const json = (await response.json()) as ServerResponse<T>
|
|
39
|
+
if (!json.success) {
|
|
40
|
+
throw json
|
|
41
|
+
}
|
|
42
|
+
return json.data as T
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public static async put<T>(url: string, body: unknown): Promise<Maybe<T>> {
|
|
46
|
+
const response = await fetch(url, {
|
|
47
|
+
method: "PUT",
|
|
48
|
+
headers: { "Content-Type": "application/json" },
|
|
49
|
+
body: JSON.stringify(body),
|
|
50
|
+
})
|
|
51
|
+
const json = (await response.json()) as ServerResponse<T>
|
|
52
|
+
if (!json.success) {
|
|
53
|
+
throw json
|
|
54
|
+
}
|
|
55
|
+
return Maybe.Just<T>(json.data)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public static async getFile<T>(url: string): Promise<T | null>
|
|
59
|
+
public static async getFile<T>(url: string, type: "json"): Promise<T | null>
|
|
60
|
+
public static async getFile(url: string, type: "text"): Promise<string | null>
|
|
61
|
+
public static async getFile<T>(url: string, type: "log"): Promise<Maybe<T[]>>
|
|
62
|
+
public static async getFile<T>(
|
|
63
|
+
url: string,
|
|
64
|
+
type: "json" | "text" | "log" = "json"
|
|
65
|
+
): Promise<T | string | Maybe<T[]> | null> {
|
|
66
|
+
try {
|
|
67
|
+
const response = await fetch(url)
|
|
68
|
+
if (type === "log") {
|
|
69
|
+
if (!response.ok) {
|
|
70
|
+
return Maybe.Nothing()
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const text = await response.text()
|
|
74
|
+
const logs = text
|
|
75
|
+
.split("\n")
|
|
76
|
+
.filter(Boolean)
|
|
77
|
+
.map((line) => JSON.parse(line) as T)
|
|
78
|
+
return Maybe.Just(logs)
|
|
79
|
+
} catch {
|
|
80
|
+
return Maybe.Nothing()
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (!response.ok) {
|
|
84
|
+
return null
|
|
85
|
+
}
|
|
86
|
+
if (type === "text") {
|
|
87
|
+
return response.text()
|
|
88
|
+
}
|
|
89
|
+
return (await response.json()) as T
|
|
90
|
+
} catch (error) {
|
|
91
|
+
console.warn(error)
|
|
92
|
+
return null
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
public static exists(url: string | Maybe<string>): Promise<boolean> {
|
|
97
|
+
return cachedLinkCheck(url)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { lru } from "@pmate/utils"
|
|
2
|
+
import type { AppConfig, ProfileStep } from "../types/app"
|
|
3
|
+
|
|
4
|
+
const APPS_ENDPOINT = "https://apps-api.pmate.chat"
|
|
5
|
+
const DEFAULT_BACKGROUND = "linear-gradient(180deg, #9ca3af 0%, #6b7280 100%)"
|
|
6
|
+
const DEFAULT_ICON = "https://parrot-static.pmate.chat/parrot-logo.png"
|
|
7
|
+
|
|
8
|
+
type AppRegistryRecord = {
|
|
9
|
+
id: string
|
|
10
|
+
name: string
|
|
11
|
+
icon: string
|
|
12
|
+
theme?: {
|
|
13
|
+
background?: string
|
|
14
|
+
themeColor?: string
|
|
15
|
+
welcomeText?: string
|
|
16
|
+
}
|
|
17
|
+
profileSchema?: ProfileStep[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const buildDisplayNameFromAppId = (appId: string): string => {
|
|
21
|
+
const normalized = appId.replace(/^@/, "")
|
|
22
|
+
const segments = normalized.split("/").filter(Boolean)
|
|
23
|
+
const last = segments[segments.length - 1] || normalized
|
|
24
|
+
return last || "App"
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const toAppConfig = (record: AppRegistryRecord): AppConfig => {
|
|
28
|
+
const fallbackName = buildDisplayNameFromAppId(record.id)
|
|
29
|
+
return {
|
|
30
|
+
id: record.id,
|
|
31
|
+
name: record.name || fallbackName,
|
|
32
|
+
icon: record.icon || DEFAULT_ICON,
|
|
33
|
+
background: record.theme?.background || DEFAULT_BACKGROUND,
|
|
34
|
+
themeColor: record.theme?.themeColor,
|
|
35
|
+
welcomeText: record.theme?.welcomeText || `Welcome to ${fallbackName}`,
|
|
36
|
+
profiles: record.profileSchema ?? [],
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const getAppConfigCached = lru(
|
|
41
|
+
async (appId: string): Promise<AppConfig> => {
|
|
42
|
+
const response = await fetch(
|
|
43
|
+
`${APPS_ENDPOINT.replace(/\/+$/, "")}/apps/${encodeURIComponent(appId)}`
|
|
44
|
+
)
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
throw new Error(`Failed to load app config (${response.status})`)
|
|
47
|
+
}
|
|
48
|
+
const json = (await response.json()) as {
|
|
49
|
+
success?: boolean
|
|
50
|
+
data?: AppRegistryRecord
|
|
51
|
+
}
|
|
52
|
+
if (!json.success || !json.data) {
|
|
53
|
+
throw new Error("Failed to load app config")
|
|
54
|
+
}
|
|
55
|
+
return toAppConfig(json.data)
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
ttl: 60_000,
|
|
59
|
+
key: (appId) => appId,
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
export class AppService {
|
|
64
|
+
public static async getAppConfig(appId: string): Promise<AppConfig> {
|
|
65
|
+
return getAppConfigCached(appId)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public static clearAppConfigCache() {
|
|
69
|
+
getAppConfigCached.clean()
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { GroupInfo, Profile } from "@pmate/meta"
|
|
2
|
+
import { Api } from "./Api"
|
|
3
|
+
|
|
4
|
+
const endpoint = "https://api-global-qa.skedo.cn/account"
|
|
5
|
+
|
|
6
|
+
export class EntityService {
|
|
7
|
+
public static async entity<T>(id: string): Promise<T | null> {
|
|
8
|
+
return (await Api.get<T>(`${endpoint}/entity/${id}`)) || null
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
public static getProfile(id: string): Promise<Profile | null> {
|
|
12
|
+
return EntityService.entity<Profile>(id)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
public static getGroup(id: string): Promise<GroupInfo | null> {
|
|
16
|
+
return EntityService.entity<GroupInfo>(id)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public static async entities<T extends { id?: string }>(
|
|
20
|
+
ids: string[]
|
|
21
|
+
): Promise<Record<string, T>> {
|
|
22
|
+
if (!ids.length) {
|
|
23
|
+
return {}
|
|
24
|
+
}
|
|
25
|
+
const url = `${endpoint}/entities`
|
|
26
|
+
const list =
|
|
27
|
+
(await Api.post<T[]>(url, {
|
|
28
|
+
ids,
|
|
29
|
+
})) || []
|
|
30
|
+
return list.reduce<Record<string, T>>((acc, entity, index) => {
|
|
31
|
+
if (!entity) {
|
|
32
|
+
return acc
|
|
33
|
+
}
|
|
34
|
+
const key = entity.id ?? ids[index]
|
|
35
|
+
if (key) {
|
|
36
|
+
acc[key] = entity
|
|
37
|
+
}
|
|
38
|
+
return acc
|
|
39
|
+
}, {})
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AccountState,
|
|
3
|
+
CreateProfileRequest,
|
|
4
|
+
Profile,
|
|
5
|
+
ProfileScope,
|
|
6
|
+
UpdateProfileRequest,
|
|
7
|
+
} from "@pmate/meta"
|
|
8
|
+
import { resolveAppId } from "../utils/resolveAppId"
|
|
9
|
+
import { Api } from "./Api"
|
|
10
|
+
|
|
11
|
+
const PROFILE_ENDPOINT = "https://auth-api-v2.pmate.chat"
|
|
12
|
+
const UPLOADER_SERVICE_URL = "https://fc-uploader.skedo.cn".replace(/\/+$/, "")
|
|
13
|
+
|
|
14
|
+
const EXTENSION_CONTENT_TYPE: Record<string, string> = {
|
|
15
|
+
png: "image/png",
|
|
16
|
+
jpg: "image/jpeg",
|
|
17
|
+
jpeg: "image/jpeg",
|
|
18
|
+
webp: "image/webp",
|
|
19
|
+
gif: "image/gif",
|
|
20
|
+
svg: "image/svg+xml",
|
|
21
|
+
ico: "image/x-icon",
|
|
22
|
+
bmp: "image/bmp",
|
|
23
|
+
pdf: "application/pdf",
|
|
24
|
+
mobi: "application/x-mobipocket-ebook",
|
|
25
|
+
epub: "application/epub+zip",
|
|
26
|
+
txt: "text/plain",
|
|
27
|
+
json: "application/json",
|
|
28
|
+
mp3: "audio/mpeg",
|
|
29
|
+
wav: "audio/wav",
|
|
30
|
+
webm: "audio/webm",
|
|
31
|
+
weba: "audio/webm",
|
|
32
|
+
m4a: "audio/mp4",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type UploadRequest = {
|
|
36
|
+
user: string
|
|
37
|
+
base64: string
|
|
38
|
+
filename?: string
|
|
39
|
+
text?: string
|
|
40
|
+
contentType?: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class ProfileService {
|
|
44
|
+
public static async createProfile(
|
|
45
|
+
req: CreateProfileRequest,
|
|
46
|
+
): Promise<Profile> {
|
|
47
|
+
return Api.post<Profile>(`${PROFILE_ENDPOINT}/profile`, req)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public static async updateProfile(req: UpdateProfileRequest) {
|
|
51
|
+
await Api.put(`${PROFILE_ENDPOINT}/profile`, req)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public static async getProfiles(account: AccountState) {
|
|
55
|
+
const scope: ProfileScope = {
|
|
56
|
+
app: resolveAppId(account.app),
|
|
57
|
+
account: account.accountId,
|
|
58
|
+
}
|
|
59
|
+
return ProfileService.getProfilesByScope(scope)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public static async getProfilesByScope(scope: ProfileScope) {
|
|
63
|
+
const query = new URLSearchParams({
|
|
64
|
+
app: scope.app,
|
|
65
|
+
account: scope.account,
|
|
66
|
+
}).toString()
|
|
67
|
+
return (
|
|
68
|
+
(await Api.get<Profile[]>(`${PROFILE_ENDPOINT}/profiles?${query}`)) || []
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public static async updateAvatar(req: {
|
|
73
|
+
user: string
|
|
74
|
+
base64: string
|
|
75
|
+
filename: string
|
|
76
|
+
}): Promise<string> {
|
|
77
|
+
return ProfileService.uploadToUploaderService("/avatar", req)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public static async uploadMsgImage(req: {
|
|
81
|
+
user: string
|
|
82
|
+
base64: string
|
|
83
|
+
filename: string
|
|
84
|
+
}): Promise<string> {
|
|
85
|
+
return ProfileService.uploadToUploaderService("/msg", req)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
public static async updateMyVoice(req: {
|
|
89
|
+
user: string
|
|
90
|
+
base64: string
|
|
91
|
+
text: string
|
|
92
|
+
}): Promise<string> {
|
|
93
|
+
return ProfileService.uploadToUploaderService("/my-voice", {
|
|
94
|
+
...req,
|
|
95
|
+
filename: "voice.wav",
|
|
96
|
+
contentType: "audio/wav",
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
public static async uploadUserFile(req: {
|
|
101
|
+
user: string
|
|
102
|
+
base64: string
|
|
103
|
+
filename: string
|
|
104
|
+
}) {
|
|
105
|
+
return ProfileService.uploadToUploaderService("/file", req)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private static async uploadToUploaderService(
|
|
109
|
+
path: string,
|
|
110
|
+
payload: UploadRequest,
|
|
111
|
+
): Promise<string> {
|
|
112
|
+
const { filename, ...rest } = payload
|
|
113
|
+
const resolvedFilename = filename ?? "upload"
|
|
114
|
+
const ext = resolvedFilename.split(".").pop()?.toLowerCase() ?? ""
|
|
115
|
+
const contentType = payload.contentType ?? EXTENSION_CONTENT_TYPE[ext]
|
|
116
|
+
const response = await fetch(`${UPLOADER_SERVICE_URL}${path}`, {
|
|
117
|
+
method: "POST",
|
|
118
|
+
headers: {
|
|
119
|
+
"Content-Type": "application/json",
|
|
120
|
+
...(contentType ? { "x-file-content-type": contentType } : {}),
|
|
121
|
+
},
|
|
122
|
+
body: JSON.stringify({ ...rest, filename: resolvedFilename }),
|
|
123
|
+
})
|
|
124
|
+
if (!response.ok) {
|
|
125
|
+
throw new Error("Upload failed")
|
|
126
|
+
}
|
|
127
|
+
const json = (await response.json()) as { data?: string }
|
|
128
|
+
if (!json.data) {
|
|
129
|
+
throw new Error("Upload failed")
|
|
130
|
+
}
|
|
131
|
+
return json.data
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
type MaybeLike<T = unknown> = {
|
|
2
|
+
map: (fn: (value: T) => unknown) => MaybeLike
|
|
3
|
+
unwrapOr: (fallback: unknown) => unknown
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const isMaybeLike = (value: unknown): value is MaybeLike => {
|
|
7
|
+
return (
|
|
8
|
+
typeof value === "object" &&
|
|
9
|
+
value !== null &&
|
|
10
|
+
"map" in value &&
|
|
11
|
+
typeof (value as MaybeLike).map === "function" &&
|
|
12
|
+
"unwrapOr" in value &&
|
|
13
|
+
typeof (value as MaybeLike).unwrapOr === "function"
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const calculateSHA1Hash = async (value: string): Promise<string> => {
|
|
18
|
+
const data = new TextEncoder().encode(value)
|
|
19
|
+
const digest = await crypto.subtle.digest("SHA-1", data)
|
|
20
|
+
return Array.from(new Uint8Array(digest))
|
|
21
|
+
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
22
|
+
.join("")
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const cacheInMem = <T extends (...args: any[]) => Promise<any>>(
|
|
26
|
+
fn: T,
|
|
27
|
+
prefix: string,
|
|
28
|
+
timeout: number
|
|
29
|
+
): T => {
|
|
30
|
+
const cache = new Map<string, { value: any; expiry: number }>()
|
|
31
|
+
const ongoingRequests = new Map<string, Promise<any>>()
|
|
32
|
+
|
|
33
|
+
return (async (...args: Parameters<T>): Promise<ReturnType<T>> => {
|
|
34
|
+
const hashRaw = args
|
|
35
|
+
.map((x) => {
|
|
36
|
+
if (isMaybeLike(x)) {
|
|
37
|
+
return x.map((item) => JSON.stringify(item)).unwrapOr("")
|
|
38
|
+
}
|
|
39
|
+
return JSON.stringify(x)
|
|
40
|
+
})
|
|
41
|
+
.join("--")
|
|
42
|
+
|
|
43
|
+
const hash = await calculateSHA1Hash(hashRaw)
|
|
44
|
+
const cacheKey = `${prefix}:${hash}`
|
|
45
|
+
|
|
46
|
+
const cached = cache.get(cacheKey)
|
|
47
|
+
if (cached && cached.expiry > Date.now()) {
|
|
48
|
+
return cached.value
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const ongoingRequest = ongoingRequests.get(cacheKey)
|
|
52
|
+
if (ongoingRequest) {
|
|
53
|
+
return ongoingRequest
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const requestPromise = (async () => {
|
|
57
|
+
try {
|
|
58
|
+
const result = await fn(...args)
|
|
59
|
+
|
|
60
|
+
if (result) {
|
|
61
|
+
cache.set(cacheKey, { value: result, expiry: Date.now() + timeout })
|
|
62
|
+
}
|
|
63
|
+
return result
|
|
64
|
+
} finally {
|
|
65
|
+
setTimeout(() => ongoingRequests.delete(cacheKey), 50)
|
|
66
|
+
}
|
|
67
|
+
})()
|
|
68
|
+
|
|
69
|
+
ongoingRequests.set(cacheKey, requestPromise)
|
|
70
|
+
|
|
71
|
+
return requestPromise
|
|
72
|
+
}) as T
|
|
73
|
+
}
|
package/src/api/index.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { atom } from "jotai"
|
|
2
|
+
import {
|
|
3
|
+
AccountLifecycleState,
|
|
4
|
+
AccountSnapshot as AccountSnapshotType,
|
|
5
|
+
} from "../types/account.types"
|
|
6
|
+
|
|
7
|
+
export type AccountSnapshot = AccountSnapshotType
|
|
8
|
+
|
|
9
|
+
const emptySnapshot: AccountSnapshot = {
|
|
10
|
+
state: AccountLifecycleState.Idle,
|
|
11
|
+
profiles: [],
|
|
12
|
+
profile: null,
|
|
13
|
+
accountId: null,
|
|
14
|
+
account: null,
|
|
15
|
+
error: null,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const accountAtom = atom<AccountSnapshot>(emptySnapshot)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { AccountState, Profile } from "@pmate/meta"
|
|
2
|
+
import { atom } from "jotai"
|
|
3
|
+
import { accountAtom } from "./accountAtom"
|
|
4
|
+
|
|
5
|
+
export const accountStateAtom = atom((get): AccountState | undefined => {
|
|
6
|
+
const account = get(accountAtom).account
|
|
7
|
+
return account ?? undefined
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
export const profileAtom = atom((get): Profile | undefined => {
|
|
11
|
+
const profile = get(accountAtom).profile
|
|
12
|
+
return profile ?? undefined
|
|
13
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { isLoadable, Loadable } from "@pmate/utils"
|
|
2
|
+
import { atom, type Atom, type Getter } from "jotai"
|
|
3
|
+
import { unwrap } from "jotai/utils"
|
|
4
|
+
|
|
5
|
+
type AsyncFn<T> = (get: Getter) => Promise<T | Loadable<T> | undefined | null>
|
|
6
|
+
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T
|
|
7
|
+
interface LoadableOption<T> {
|
|
8
|
+
isValid?: (result: UnwrapPromise<ReturnType<AsyncFn<T>>>) => boolean
|
|
9
|
+
placeHolderBehavior?: "pending" | "stale"
|
|
10
|
+
placeHolderValue?: T
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const atomWithLoadable = <T>(
|
|
14
|
+
asyncFn: AsyncFn<T>,
|
|
15
|
+
options: LoadableOption<T> = {
|
|
16
|
+
isValid: undefined,
|
|
17
|
+
placeHolderBehavior: "pending",
|
|
18
|
+
},
|
|
19
|
+
) => {
|
|
20
|
+
const baseAtom = atom(async (get) => {
|
|
21
|
+
try {
|
|
22
|
+
const result = await asyncFn(get)
|
|
23
|
+
if (isLoadable(result)) {
|
|
24
|
+
return result
|
|
25
|
+
}
|
|
26
|
+
if (typeof result === "undefined" || result === null) {
|
|
27
|
+
return Loadable.Nothing<T>()
|
|
28
|
+
}
|
|
29
|
+
if (options?.isValid && !options.isValid(result)) {
|
|
30
|
+
return Loadable.Nothing<T>()
|
|
31
|
+
}
|
|
32
|
+
return Loadable.Just<T>(result)
|
|
33
|
+
} catch (error: any) {
|
|
34
|
+
return Loadable.Fail<T>(error)
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
return unwrap(baseAtom, (prev) => {
|
|
39
|
+
const { placeHolderBehavior, placeHolderValue } = options
|
|
40
|
+
if (placeHolderValue) {
|
|
41
|
+
return Loadable.Pending<T>(placeHolderValue)
|
|
42
|
+
}
|
|
43
|
+
if (placeHolderBehavior === "stale" && prev?.isJust()) {
|
|
44
|
+
return Loadable.Pending<T>(prev.unwrap())
|
|
45
|
+
}
|
|
46
|
+
return Loadable.Pending<T>()
|
|
47
|
+
}) as Atom<Loadable<T>>
|
|
48
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { atom } from "jotai"
|
|
2
|
+
import { AccountManagerV2 } from "../utils/AccountManagerV2"
|
|
3
|
+
import { profileDraftAtom } from "./profileDraftAtom"
|
|
4
|
+
import { resolveAppId } from "../utils/resolveAppId"
|
|
5
|
+
|
|
6
|
+
type CreateProfileParams = {
|
|
7
|
+
nickName: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const createProfileAtom = atom(
|
|
11
|
+
null,
|
|
12
|
+
async (get, _set, { nickName }: CreateProfileParams) => {
|
|
13
|
+
const draft = get(profileDraftAtom)
|
|
14
|
+
const bootstrapManager = AccountManagerV2.get(resolveAppId())
|
|
15
|
+
const account = await bootstrapManager.getAccountState()
|
|
16
|
+
|
|
17
|
+
if (!account) {
|
|
18
|
+
throw new Error("Account info is missing for profile creation")
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const manager = AccountManagerV2.get(resolveAppId(account.app))
|
|
22
|
+
return manager.createProfile({
|
|
23
|
+
nickName,
|
|
24
|
+
learningTargetLang: draft.learningTargetLang,
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
)
|