@pmate/account-sdk 0.5.5 → 0.6.1
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 +35 -28
- 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/appConfigAtom.ts +24 -0
- package/src/atoms/atomWithLoadable.ts +48 -0
- package/src/atoms/createProfileAtom.ts +27 -0
- package/src/atoms/index.ts +17 -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 +300 -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 +51 -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 +78 -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/node/index.ts +271 -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 +352 -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
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
|
|
3
|
+
type ButtonVariant = "primary" | "plain"
|
|
4
|
+
|
|
5
|
+
export interface ButtonProps
|
|
6
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
7
|
+
variant?: ButtonVariant
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const joinClassNames = (...parts: Array<string | false | null | undefined>) =>
|
|
11
|
+
parts.filter(Boolean).join(" ")
|
|
12
|
+
|
|
13
|
+
const baseClassName =
|
|
14
|
+
"inline-flex items-center rounded-md px-3 py-1.5 text-sm font-medium transition-colors"
|
|
15
|
+
|
|
16
|
+
const variantClassName: Record<ButtonVariant, string> = {
|
|
17
|
+
primary: "bg-slate-900 text-white hover:bg-slate-800",
|
|
18
|
+
plain: "border border-slate-200 bg-white text-slate-700 hover:bg-slate-50",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const Button = ({
|
|
22
|
+
variant = "primary",
|
|
23
|
+
className,
|
|
24
|
+
disabled,
|
|
25
|
+
...props
|
|
26
|
+
}: ButtonProps) => {
|
|
27
|
+
return (
|
|
28
|
+
<button
|
|
29
|
+
{...props}
|
|
30
|
+
disabled={disabled}
|
|
31
|
+
className={joinClassNames(
|
|
32
|
+
baseClassName,
|
|
33
|
+
variantClassName[variant],
|
|
34
|
+
disabled && "cursor-not-allowed opacity-60",
|
|
35
|
+
className,
|
|
36
|
+
)}
|
|
37
|
+
/>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
@@ -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"
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const DEFAULT_APP_ID = "@pmate/chat"
|
|
@@ -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,51 @@
|
|
|
1
|
+
import { useEffect, useState } from "react"
|
|
2
|
+
import { resolveAppConfigId } from "../atoms/appConfigAtom"
|
|
3
|
+
import { AppService } from "../api/AppService"
|
|
4
|
+
import type { AppConfig } from "../types/app"
|
|
5
|
+
|
|
6
|
+
type AppConfigState = {
|
|
7
|
+
appConfig: AppConfig | null
|
|
8
|
+
isLoading: boolean
|
|
9
|
+
error: Error | null
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const useAppConfig = (app?: string | null): AppConfigState => {
|
|
13
|
+
const resolvedApp = resolveAppConfigId(app)
|
|
14
|
+
const [state, setState] = useState<AppConfigState>({
|
|
15
|
+
appConfig: null,
|
|
16
|
+
isLoading: true,
|
|
17
|
+
error: null,
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
let active = true
|
|
22
|
+
setState({ appConfig: null, isLoading: true, error: null })
|
|
23
|
+
|
|
24
|
+
AppService.getAppConfig(resolvedApp)
|
|
25
|
+
.then((appConfig) => {
|
|
26
|
+
if (!active) {
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
setState({ appConfig, isLoading: false, error: null })
|
|
30
|
+
})
|
|
31
|
+
.catch((error: unknown) => {
|
|
32
|
+
if (!active) {
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
setState({
|
|
36
|
+
appConfig: null,
|
|
37
|
+
isLoading: false,
|
|
38
|
+
error:
|
|
39
|
+
error instanceof Error
|
|
40
|
+
? error
|
|
41
|
+
: new Error("Failed to load app config"),
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
return () => {
|
|
46
|
+
active = false
|
|
47
|
+
}
|
|
48
|
+
}, [resolvedApp])
|
|
49
|
+
|
|
50
|
+
return state
|
|
51
|
+
}
|
|
@@ -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,78 @@
|
|
|
1
|
+
import { useCallback, useMemo } from "react"
|
|
2
|
+
import { isProfileStepType, ProfileStepType } from "../utils/profileStep"
|
|
3
|
+
import { useAppConfig } from "./useAppConfig"
|
|
4
|
+
|
|
5
|
+
type UseProfileStepFlowParams = {
|
|
6
|
+
params: URLSearchParams
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const useProfileStepFlow = ({
|
|
10
|
+
params,
|
|
11
|
+
}: UseProfileStepFlowParams) => {
|
|
12
|
+
const appParam = params.get("app")
|
|
13
|
+
const redirectParam = params.get("redirect")
|
|
14
|
+
const { appConfig, error, isLoading } = useAppConfig(appParam)
|
|
15
|
+
const shouldBlockStep = Boolean(appParam) && isLoading && !appConfig && !error
|
|
16
|
+
|
|
17
|
+
// The app registry decides the canonical order of profile steps.
|
|
18
|
+
// When the config is still loading for an explicit app, we intentionally
|
|
19
|
+
// block the create flow instead of briefly falling back to a synthetic
|
|
20
|
+
// default step that does not exist in the app schema.
|
|
21
|
+
const appProfileSteps = useMemo<ProfileStepType[]>(
|
|
22
|
+
() =>
|
|
23
|
+
(appConfig?.profiles ?? [])
|
|
24
|
+
.map((profile) => profile.type)
|
|
25
|
+
.filter((type): type is ProfileStepType => isProfileStepType(type)),
|
|
26
|
+
[appConfig]
|
|
27
|
+
)
|
|
28
|
+
const stepParam = params.get("step")
|
|
29
|
+
const normalizedStep: ProfileStepType | null =
|
|
30
|
+
appProfileSteps.find((item) => item === stepParam) ?? null
|
|
31
|
+
|
|
32
|
+
// step=... wins when it is valid for the current app.
|
|
33
|
+
// Otherwise we use the first configured step. If the app does not declare
|
|
34
|
+
// any profile schema, create-profile should stay unavailable instead of
|
|
35
|
+
// inventing a fallback step.
|
|
36
|
+
const defaultStep: ProfileStepType | null = shouldBlockStep
|
|
37
|
+
? null
|
|
38
|
+
: (appProfileSteps[0] ?? null)
|
|
39
|
+
const activeStep: ProfileStepType | null = normalizedStep ?? defaultStep
|
|
40
|
+
|
|
41
|
+
// createSteps stays empty while we intentionally block rendering.
|
|
42
|
+
// Once app config is ready, it mirrors the registry schema order exactly.
|
|
43
|
+
const createSteps = useMemo<ProfileStepType[]>(() => {
|
|
44
|
+
return shouldBlockStep ? [] : appProfileSteps
|
|
45
|
+
}, [appProfileSteps, shouldBlockStep])
|
|
46
|
+
const currentStepIndex =
|
|
47
|
+
activeStep === null ? -1 : createSteps.indexOf(activeStep)
|
|
48
|
+
const nextStep =
|
|
49
|
+
currentStepIndex >= 0 ? createSteps[currentStepIndex + 1] : undefined
|
|
50
|
+
const isCreateFlowStep =
|
|
51
|
+
activeStep === null ? false : createSteps.includes(activeStep)
|
|
52
|
+
const buildStepUrl = useCallback(
|
|
53
|
+
(next: ProfileStepType) => {
|
|
54
|
+
const search = new URLSearchParams()
|
|
55
|
+
search.set("step", next)
|
|
56
|
+
if (appParam) {
|
|
57
|
+
search.set("app", appParam)
|
|
58
|
+
}
|
|
59
|
+
if (redirectParam) {
|
|
60
|
+
search.set("redirect", redirectParam)
|
|
61
|
+
}
|
|
62
|
+
return `/create-profile?${search.toString()}`
|
|
63
|
+
},
|
|
64
|
+
[appParam, redirectParam]
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
activeStep,
|
|
69
|
+
appProfileSteps,
|
|
70
|
+
buildStepUrl,
|
|
71
|
+
createSteps,
|
|
72
|
+
error,
|
|
73
|
+
isLoading,
|
|
74
|
+
isCreateFlowStep,
|
|
75
|
+
isReady: activeStep !== null,
|
|
76
|
+
nextStep,
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -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"
|