@pmate/account-sdk 0.5.4 → 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.
Files changed (76) hide show
  1. package/package.json +18 -20
  2. package/src/api/AccountService.ts +97 -0
  3. package/src/api/Api.ts +99 -0
  4. package/src/api/AppService.ts +71 -0
  5. package/src/api/EntityService.ts +41 -0
  6. package/src/api/ProfileService.ts +133 -0
  7. package/src/api/cacheInMem.ts +73 -0
  8. package/src/api/index.ts +5 -0
  9. package/src/atoms/accountAtom.ts +18 -0
  10. package/src/atoms/accountProfileAtom.ts +13 -0
  11. package/src/atoms/atomWithLoadable.ts +48 -0
  12. package/src/atoms/createProfileAtom.ts +27 -0
  13. package/src/atoms/index.ts +16 -0
  14. package/src/atoms/learningLangAtom.ts +8 -0
  15. package/src/atoms/localStorageAtom.ts +39 -0
  16. package/src/atoms/loginAtom.ts +8 -0
  17. package/src/atoms/motherTongueAtom.ts +8 -0
  18. package/src/atoms/profileAtom.ts +24 -0
  19. package/src/atoms/profileDraftAtom.ts +7 -0
  20. package/src/atoms/profilesAtom.ts +20 -0
  21. package/src/atoms/sessionCheckAtom.ts +10 -0
  22. package/src/atoms/switchProfileAtom.ts +9 -0
  23. package/src/atoms/updateProfileAtom.ts +35 -0
  24. package/src/atoms/uploadAvatarAtom.ts +49 -0
  25. package/src/atoms/userLogoutAtom.ts +8 -0
  26. package/src/atoms/userSettingsAtom.ts +58 -0
  27. package/src/components/AuthProviderV2.tsx +315 -0
  28. package/src/components/Button.tsx +39 -0
  29. package/src/components/Drawer.tsx +80 -0
  30. package/src/components/index.ts +1 -0
  31. package/src/constants.ts +1 -0
  32. package/src/hooks/index.ts +5 -0
  33. package/src/hooks/useAppBackgroundStyle.ts +25 -0
  34. package/src/hooks/useAppConfig.ts +44 -0
  35. package/src/hooks/useAuthApp.ts +165 -0
  36. package/src/hooks/useAuthSnapshot.ts +84 -0
  37. package/src/hooks/useIsAuthenticated.ts +19 -0
  38. package/src/hooks/useProfileStepFlow.ts +59 -0
  39. package/src/i18n/index.ts +59 -0
  40. package/src/index.ts +9 -0
  41. package/src/locales/ar-SA.json +183 -0
  42. package/src/locales/de-DE.json +183 -0
  43. package/src/locales/el-GR.json +183 -0
  44. package/src/locales/en.json +183 -0
  45. package/src/locales/es-ES.json +183 -0
  46. package/src/locales/fi-FI.json +183 -0
  47. package/src/locales/fil-PH.json +183 -0
  48. package/src/locales/fr-FR.json +183 -0
  49. package/src/locales/hi-IN.json +183 -0
  50. package/src/locales/ja-JP.json +183 -0
  51. package/src/locales/ko-KR.json +183 -0
  52. package/src/locales/pt-BR.json +183 -0
  53. package/src/locales/pt-PT.json +183 -0
  54. package/src/locales/ru-RU.json +183 -0
  55. package/src/locales/ta-IN.json +183 -0
  56. package/src/locales/uk-UA.json +183 -0
  57. package/src/locales/zh-CN.json +183 -0
  58. package/src/locales/zh-TW.json +183 -0
  59. package/src/types/account.types.ts +28 -0
  60. package/src/types/app.ts +22 -0
  61. package/src/types/profile.ts +6 -0
  62. package/src/utils/AccountManagerV2.ts +349 -0
  63. package/src/utils/Redirect.ts +17 -0
  64. package/src/utils/accountStorage.ts +46 -0
  65. package/src/utils/errors.ts +1 -0
  66. package/src/utils/index.ts +11 -0
  67. package/src/utils/location.ts +34 -0
  68. package/src/utils/profileStep.ts +26 -0
  69. package/src/utils/resolveAppId.ts +13 -0
  70. package/src/utils/selectedProfileStorage.ts +46 -0
  71. package/src/utils/tokenStorage.ts +47 -0
  72. package/dist/index.cjs.js +0 -22
  73. package/dist/index.cjs.js.map +0 -1
  74. package/dist/index.d.ts +0 -302
  75. package/dist/index.es.js +0 -8884
  76. package/dist/index.es.js.map +0 -1
@@ -0,0 +1,80 @@
1
+ import React, { useEffect, useState } from "react"
2
+
3
+ type DrawerAnchor = "left" | "right" | "top" | "bottom"
4
+
5
+ export interface DrawerProps {
6
+ open: boolean
7
+ onClose: () => void
8
+ children: React.ReactNode
9
+ anchor?: DrawerAnchor
10
+ className?: string
11
+ overlayClassName?: string
12
+ id?: string
13
+ style?: React.CSSProperties
14
+ }
15
+
16
+ const joinClassNames = (...parts: Array<string | false | null | undefined>) =>
17
+ parts.filter(Boolean).join(" ")
18
+
19
+ const positionClassMap: Record<DrawerAnchor, string> = {
20
+ left: "left-0 top-0 h-full",
21
+ right: "right-0 top-0 h-full",
22
+ top: "left-0 top-0 w-full",
23
+ bottom: "bottom-0 left-0 w-full",
24
+ }
25
+
26
+ const hiddenTransformMap: Record<DrawerAnchor, string> = {
27
+ left: "-translate-x-full",
28
+ right: "translate-x-full",
29
+ top: "-translate-y-full",
30
+ bottom: "translate-y-full",
31
+ }
32
+
33
+ export const Drawer = ({
34
+ open,
35
+ onClose,
36
+ children,
37
+ anchor = "right",
38
+ className,
39
+ overlayClassName,
40
+ id,
41
+ style,
42
+ }: DrawerProps) => {
43
+ const [mounted, setMounted] = useState(open)
44
+
45
+ useEffect(() => {
46
+ if (open) {
47
+ setMounted(true)
48
+ }
49
+ }, [open])
50
+
51
+ const handleTransitionEnd = () => {
52
+ if (!open) {
53
+ setMounted(false)
54
+ }
55
+ }
56
+
57
+ if (!mounted) return null
58
+
59
+ return (
60
+ <div
61
+ id={id}
62
+ className={joinClassNames("fixed inset-0 z-[1002]", overlayClassName)}
63
+ onClick={onClose}
64
+ >
65
+ <div
66
+ className={joinClassNames(
67
+ "absolute bg-white transition-transform duration-300",
68
+ positionClassMap[anchor],
69
+ open ? "translate-x-0 translate-y-0" : hiddenTransformMap[anchor],
70
+ className,
71
+ )}
72
+ style={style}
73
+ onClick={(event) => event.stopPropagation()}
74
+ onTransitionEnd={handleTransitionEnd}
75
+ >
76
+ {children}
77
+ </div>
78
+ </div>
79
+ )
80
+ }
@@ -0,0 +1 @@
1
+ export * from "./AuthProviderV2"
@@ -0,0 +1 @@
1
+ export const DEFAULT_APP_ID = "@pmate/chat"
@@ -0,0 +1,5 @@
1
+ export * from "./useProfileStepFlow"
2
+ export * from "./useAuthApp"
3
+ export * from "./useAppBackgroundStyle"
4
+ export * from "./useAppConfig"
5
+ export * from "./useIsAuthenticated"
@@ -0,0 +1,25 @@
1
+ import { useEffect, useMemo, useState } from "react"
2
+ import { getWindowSearch, subscribeToLocationChange } from "../utils/location"
3
+ import { useAppConfig } from "./useAppConfig"
4
+
5
+ export const useAppBackgroundStyle = () => {
6
+ const [search, setSearch] = useState(getWindowSearch())
7
+ useEffect(() => {
8
+ const update = () => setSearch(getWindowSearch())
9
+ const unsubscribe = subscribeToLocationChange(update)
10
+ return () => unsubscribe()
11
+ }, [])
12
+ const appParam = useMemo(
13
+ () => new URLSearchParams(search).get("app"),
14
+ [search]
15
+ )
16
+ const { appConfig } = useAppConfig(appParam)
17
+
18
+ return useMemo(
19
+ () => ({
20
+ background:
21
+ appConfig?.background || "linear-gradient(180deg, #9ca3af 0%, #6b7280 100%)",
22
+ }),
23
+ [appConfig]
24
+ )
25
+ }
@@ -0,0 +1,44 @@
1
+ import { useEffect, useState } from "react"
2
+ import { AppService } from "../api/AppService"
3
+ import { DEFAULT_APP_ID } from "../constants"
4
+ import type { AppConfig } from "../types/app"
5
+ import { resolveAppId } from "../utils/resolveAppId"
6
+
7
+ type AppConfigState = {
8
+ appConfig: AppConfig | null
9
+ isLoading: boolean
10
+ error: Error | null
11
+ }
12
+
13
+ export const useAppConfig = (app?: string | null): AppConfigState => {
14
+ const resolvedApp = resolveAppId(app ?? DEFAULT_APP_ID)
15
+ const [state, setState] = useState<AppConfigState>({
16
+ appConfig: null,
17
+ isLoading: true,
18
+ error: null,
19
+ })
20
+
21
+ useEffect(() => {
22
+ let active = true
23
+ setState({ appConfig: null, isLoading: true, error: null })
24
+ AppService.getAppConfig(resolvedApp)
25
+ .then((appConfig) => {
26
+ if (!active) return
27
+ setState({ appConfig, isLoading: false, error: null })
28
+ })
29
+ .catch((error: unknown) => {
30
+ if (!active) return
31
+ setState({
32
+ appConfig: null,
33
+ isLoading: false,
34
+ error: error instanceof Error ? error : new Error("Failed to load app config"),
35
+ })
36
+ })
37
+
38
+ return () => {
39
+ active = false
40
+ }
41
+ }, [resolvedApp])
42
+
43
+ return state
44
+ }
@@ -0,0 +1,165 @@
1
+ import { useCallback, useMemo } from "react"
2
+ import { DEFAULT_APP_ID } from "../constants"
3
+ import { resolveAppId } from "../utils/resolveAppId"
4
+ import type { ProfileStepType } from "../utils/profileStep"
5
+
6
+ type UseAuthAppOptions = {
7
+ app?: string
8
+ redirect?: string
9
+ }
10
+
11
+ type AuthAppRedirectOptions = {
12
+ app?: string
13
+ redirect?: string
14
+ }
15
+
16
+ type LogoutScope = "app" | "all"
17
+
18
+ type AuthLogoutRedirectOptions = AuthAppRedirectOptions & {
19
+ scope?: LogoutScope
20
+ }
21
+
22
+ type AuthProfileRedirectOptions = AuthAppRedirectOptions & {
23
+ step?: ProfileStepType
24
+ }
25
+
26
+ const AUTH_APP_BASE = "https://auth.pmate.chat"
27
+
28
+ const getDefaultRedirect = () => {
29
+ if (typeof window === "undefined") {
30
+ return ""
31
+ }
32
+ return window.location.href
33
+ }
34
+
35
+ export const useAuthApp = (options: UseAuthAppOptions = {}) => {
36
+ const app = resolveAppId(options.app ?? DEFAULT_APP_ID)
37
+ const redirect = useMemo(
38
+ () => options.redirect ?? getDefaultRedirect(),
39
+ [options.redirect]
40
+ )
41
+
42
+ const buildUrl = useCallback(
43
+ (path = "/", overrides: AuthAppRedirectOptions = {}) => {
44
+ const targetRedirect = overrides.redirect ?? redirect
45
+ const targetApp = overrides.app ?? app
46
+ const url = new URL(path, AUTH_APP_BASE)
47
+ if (targetRedirect) {
48
+ url.searchParams.set("redirect", targetRedirect)
49
+ }
50
+ if (targetApp) {
51
+ url.searchParams.set("app", targetApp)
52
+ }
53
+ return url.toString()
54
+ },
55
+ [app, redirect]
56
+ )
57
+
58
+ const buildProfileUrl = useCallback(
59
+ (path: string, overrides: AuthProfileRedirectOptions = {}) => {
60
+ const url = new URL(buildUrl(path, overrides))
61
+ if (overrides.step) {
62
+ url.searchParams.set("step", overrides.step)
63
+ }
64
+ return url.toString()
65
+ },
66
+ [buildUrl]
67
+ )
68
+
69
+ const buildLogoutUrl = useCallback(
70
+ (overrides?: AuthLogoutRedirectOptions) => {
71
+ const url = new URL(buildUrl("/logout", overrides))
72
+ if (overrides?.scope === "all") {
73
+ url.searchParams.set("scope", "all")
74
+ }
75
+ return url.toString()
76
+ },
77
+ [buildUrl]
78
+ )
79
+
80
+ const buildLoginUrl = useCallback(
81
+ (overrides?: AuthAppRedirectOptions) => buildUrl("/", overrides),
82
+ [buildUrl]
83
+ )
84
+
85
+ const buildCreateProfileUrl = useCallback(
86
+ (overrides?: AuthProfileRedirectOptions) =>
87
+ buildProfileUrl("/create-profile", overrides),
88
+ [buildProfileUrl]
89
+ )
90
+
91
+ const buildSelectProfileUrl = useCallback(
92
+ (overrides?: AuthAppRedirectOptions) =>
93
+ buildUrl("/select-profile", overrides),
94
+ [buildUrl]
95
+ )
96
+
97
+ const buildEditProfileUrl = useCallback(
98
+ (overrides?: AuthProfileRedirectOptions) =>
99
+ buildProfileUrl("/edit-profile", overrides),
100
+ [buildProfileUrl]
101
+ )
102
+
103
+ const login = useCallback(
104
+ (overrides?: AuthAppRedirectOptions) => {
105
+ if (typeof window === "undefined") {
106
+ return
107
+ }
108
+ window.location.assign(buildLoginUrl(overrides))
109
+ },
110
+ [buildLoginUrl]
111
+ )
112
+
113
+ const logout = useCallback(
114
+ (scopeOrOverrides?: LogoutScope | AuthLogoutRedirectOptions) => {
115
+ if (typeof window === "undefined") {
116
+ return
117
+ }
118
+ const overrides =
119
+ typeof scopeOrOverrides === "string"
120
+ ? { scope: scopeOrOverrides }
121
+ : (scopeOrOverrides ?? {})
122
+ window.location.assign(buildLogoutUrl(overrides))
123
+ },
124
+ [buildLogoutUrl]
125
+ )
126
+
127
+ const redirectToCreateProfile = useCallback(
128
+ (overrides?: AuthProfileRedirectOptions) => {
129
+ if (typeof window === "undefined") {
130
+ return
131
+ }
132
+ window.location.assign(buildCreateProfileUrl(overrides))
133
+ },
134
+ [buildCreateProfileUrl]
135
+ )
136
+
137
+ const redirectToSelectProfile = useCallback(
138
+ (overrides?: AuthAppRedirectOptions) => {
139
+ if (typeof window === "undefined") {
140
+ return
141
+ }
142
+ window.location.assign(buildSelectProfileUrl(overrides))
143
+ },
144
+ [buildSelectProfileUrl]
145
+ )
146
+
147
+ const redirectToEditProfile = useCallback(
148
+ (overrides?: AuthProfileRedirectOptions) => {
149
+ if (typeof window === "undefined") {
150
+ return
151
+ }
152
+ window.location.assign(buildEditProfileUrl(overrides))
153
+ },
154
+ [buildEditProfileUrl]
155
+ )
156
+
157
+ return {
158
+ app,
159
+ login,
160
+ logout,
161
+ selectProfile: redirectToSelectProfile,
162
+ createProfile: redirectToCreateProfile,
163
+ updateProfile: redirectToEditProfile,
164
+ }
165
+ }
@@ -0,0 +1,84 @@
1
+ import { useEffect, useState } from "react"
2
+ import type { AccountSnapshot, AuthBehaviors } from "../types/account.types"
3
+ import { AccountLifecycleState } from "../types/account.types"
4
+ import { AccountManagerEvent, AccountManagerV2 } from "../utils/AccountManagerV2"
5
+
6
+ const checkAuth = async ({
7
+ app,
8
+ behaviors,
9
+ }: {
10
+ app: string
11
+ behaviors: AuthBehaviors
12
+ }): Promise<AccountSnapshot> => {
13
+ const manager = AccountManagerV2.get(app)
14
+ if (behaviors.requiresAuth === false) {
15
+ return manager.getSnapshot()
16
+ }
17
+
18
+ try {
19
+ const account = await manager.loginUrlSessionOverride()
20
+ if (account) {
21
+ const profiles = await manager.getProfiles()
22
+ if (profiles.length > 0) {
23
+ manager.setSelectedProfile(profiles[0].id)
24
+ }
25
+ }
26
+ return manager.getSnapshot()
27
+ } catch (error) {
28
+ console.error(error)
29
+ return manager.getSnapshot()
30
+ }
31
+ }
32
+
33
+ export const useAuthSnapshot = ({
34
+ app,
35
+ behaviors,
36
+ }: {
37
+ app: string
38
+ behaviors: AuthBehaviors
39
+ }) => {
40
+ const [loading, setLoading] = useState(true)
41
+ const [snapshot, setSnapshot] = useState<AccountSnapshot>({
42
+ state: AccountLifecycleState.Idle,
43
+ profiles: [],
44
+ profile: null,
45
+ accountId: null,
46
+ account: null,
47
+ error: null,
48
+ })
49
+
50
+ useEffect(() => {
51
+ let isActive = true
52
+ const manager = AccountManagerV2.get(app)
53
+
54
+ const refreshSnapshot = async () => {
55
+ const next = await manager.getSnapshot()
56
+ if (!isActive) {
57
+ return
58
+ }
59
+ setSnapshot(next)
60
+ }
61
+
62
+ const loadSnapshot = async () => {
63
+ setLoading(true)
64
+ const snap = await checkAuth({ app, behaviors })
65
+ if (!isActive) {
66
+ return
67
+ }
68
+ setSnapshot(snap)
69
+ setLoading(false)
70
+ }
71
+
72
+ void loadSnapshot()
73
+ const unsubscribe = manager.on(AccountManagerEvent.StateChange, () => {
74
+ void refreshSnapshot()
75
+ })
76
+
77
+ return () => {
78
+ isActive = false
79
+ unsubscribe()
80
+ }
81
+ }, [app, behaviors.authBehavior, behaviors.requiresAuth])
82
+
83
+ return { loading, snapshot }
84
+ }
@@ -0,0 +1,19 @@
1
+ import { DEFAULT_APP_ID } from "../constants"
2
+ import { useAuthSnapshot } from "./useAuthSnapshot"
3
+ import { resolveAppId } from "../utils/resolveAppId"
4
+
5
+ export const useIsAuthenticated = (app?: string) => {
6
+ const { loading, snapshot } = useAuthSnapshot({
7
+ app: resolveAppId(app ?? DEFAULT_APP_ID),
8
+ behaviors: {
9
+ authBehavior: "prompt",
10
+ requiresAuth: false,
11
+ },
12
+ })
13
+
14
+ if (loading) {
15
+ return false
16
+ }
17
+
18
+ return Boolean(snapshot.account)
19
+ }
@@ -0,0 +1,59 @@
1
+ import { useCallback, useMemo } from "react"
2
+ import { isProfileStepType, ProfileStepType } from "../utils/profileStep"
3
+ import { useAppConfig } from "./useAppConfig"
4
+
5
+ const DEFAULT_CREATE_STEP: ProfileStepType = "learning-language"
6
+
7
+ type UseProfileStepFlowParams = {
8
+ params: URLSearchParams
9
+ }
10
+
11
+ export const useProfileStepFlow = ({
12
+ params,
13
+ }: UseProfileStepFlowParams) => {
14
+ const appParam = params.get("app")
15
+ const redirectParam = params.get("redirect")
16
+ const { appConfig } = useAppConfig(appParam)
17
+ const appProfileSteps = useMemo<ProfileStepType[]>(
18
+ () =>
19
+ (appConfig?.profiles ?? [])
20
+ .map((profile) => profile.type)
21
+ .filter((type): type is ProfileStepType => isProfileStepType(type)),
22
+ [appConfig]
23
+ )
24
+ const stepParam = params.get("step")
25
+ const normalizedStep: ProfileStepType | null =
26
+ appProfileSteps.find((item) => item === stepParam) ?? null
27
+ const defaultStep: ProfileStepType = appProfileSteps[0] ?? DEFAULT_CREATE_STEP
28
+ const activeStep: ProfileStepType = normalizedStep ?? defaultStep
29
+ const createSteps = useMemo<ProfileStepType[]>(() => {
30
+ return appProfileSteps.length > 0 ? appProfileSteps : [DEFAULT_CREATE_STEP]
31
+ }, [appProfileSteps])
32
+ const currentStepIndex = createSteps.indexOf(activeStep)
33
+ const nextStep =
34
+ currentStepIndex >= 0 ? createSteps[currentStepIndex + 1] : undefined
35
+ const isCreateFlowStep = createSteps.includes(activeStep)
36
+ const buildStepUrl = useCallback(
37
+ (next: ProfileStepType) => {
38
+ const search = new URLSearchParams()
39
+ search.set("step", next)
40
+ if (appParam) {
41
+ search.set("app", appParam)
42
+ }
43
+ if (redirectParam) {
44
+ search.set("redirect", redirectParam)
45
+ }
46
+ return `/create-profile?${search.toString()}`
47
+ },
48
+ [appParam, redirectParam]
49
+ )
50
+
51
+ return {
52
+ activeStep,
53
+ appProfileSteps,
54
+ buildStepUrl,
55
+ createSteps,
56
+ isCreateFlowStep,
57
+ nextStep,
58
+ }
59
+ }
@@ -0,0 +1,59 @@
1
+ import i18next from "i18next"
2
+ import {
3
+ I18nextProvider,
4
+ initReactI18next,
5
+ useTranslation as useI18nTranslation,
6
+ } from "react-i18next"
7
+ import arSA from "../locales/ar-SA.json"
8
+ import deDE from "../locales/de-DE.json"
9
+ import elGR from "../locales/el-GR.json"
10
+ import en from "../locales/en.json"
11
+ import esES from "../locales/es-ES.json"
12
+ import fiFI from "../locales/fi-FI.json"
13
+ import filPH from "../locales/fil-PH.json"
14
+ import frFR from "../locales/fr-FR.json"
15
+ import hiIN from "../locales/hi-IN.json"
16
+ import jaJP from "../locales/ja-JP.json"
17
+ import koKR from "../locales/ko-KR.json"
18
+ import ptBR from "../locales/pt-BR.json"
19
+ import ptPT from "../locales/pt-PT.json"
20
+ import ruRU from "../locales/ru-RU.json"
21
+ import taIN from "../locales/ta-IN.json"
22
+ import ukUA from "../locales/uk-UA.json"
23
+ import zhCN from "../locales/zh-CN.json"
24
+ import zhTW from "../locales/zh-TW.json"
25
+
26
+ const resources = {
27
+ en: { translation: en },
28
+ "ar-SA": { translation: arSA },
29
+ "de-DE": { translation: deDE },
30
+ "el-GR": { translation: elGR },
31
+ "es-ES": { translation: esES },
32
+ "fi-FI": { translation: fiFI },
33
+ "fil-PH": { translation: filPH },
34
+ "fr-FR": { translation: frFR },
35
+ "hi-IN": { translation: hiIN },
36
+ "ja-JP": { translation: jaJP },
37
+ "ko-KR": { translation: koKR },
38
+ "pt-BR": { translation: ptBR },
39
+ "pt-PT": { translation: ptPT },
40
+ "ru-RU": { translation: ruRU },
41
+ "ta-IN": { translation: taIN },
42
+ "uk-UA": { translation: ukUA },
43
+ "zh-CN": { translation: zhCN },
44
+ "zh-TW": { translation: zhTW },
45
+ } as const
46
+
47
+ const supportedLngs = Object.keys(resources)
48
+
49
+ const i18n = i18next.createInstance()
50
+ i18n.use(initReactI18next).init({
51
+ resources,
52
+ supportedLngs,
53
+ lng: "zh-CN",
54
+ fallbackLng: "zh-CN",
55
+ interpolation: { escapeValue: false },
56
+ })
57
+
58
+ export { i18n, I18nextProvider }
59
+ export const useTranslation = () => useI18nTranslation().t
package/src/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ export * from "./atoms"
2
+ export * from "./components"
3
+ export * from "./i18n"
4
+ export { DEFAULT_APP_ID } from "./constants"
5
+ export type { AppConfig, ProfileStep } from "./types/app"
6
+ export * from "./hooks"
7
+ export * from "./utils"
8
+ export * from "./api"
9
+ export type { ProfileDraft } from "./types/profile"