@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.
Files changed (78) hide show
  1. package/package.json +35 -28
  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/appConfigAtom.ts +24 -0
  12. package/src/atoms/atomWithLoadable.ts +48 -0
  13. package/src/atoms/createProfileAtom.ts +27 -0
  14. package/src/atoms/index.ts +17 -0
  15. package/src/atoms/learningLangAtom.ts +8 -0
  16. package/src/atoms/localStorageAtom.ts +39 -0
  17. package/src/atoms/loginAtom.ts +8 -0
  18. package/src/atoms/motherTongueAtom.ts +8 -0
  19. package/src/atoms/profileAtom.ts +24 -0
  20. package/src/atoms/profileDraftAtom.ts +7 -0
  21. package/src/atoms/profilesAtom.ts +20 -0
  22. package/src/atoms/sessionCheckAtom.ts +10 -0
  23. package/src/atoms/switchProfileAtom.ts +9 -0
  24. package/src/atoms/updateProfileAtom.ts +35 -0
  25. package/src/atoms/uploadAvatarAtom.ts +49 -0
  26. package/src/atoms/userLogoutAtom.ts +8 -0
  27. package/src/atoms/userSettingsAtom.ts +58 -0
  28. package/src/components/AuthProviderV2.tsx +300 -0
  29. package/src/components/Button.tsx +39 -0
  30. package/src/components/Drawer.tsx +80 -0
  31. package/src/components/index.ts +1 -0
  32. package/src/constants.ts +1 -0
  33. package/src/hooks/index.ts +5 -0
  34. package/src/hooks/useAppBackgroundStyle.ts +25 -0
  35. package/src/hooks/useAppConfig.ts +51 -0
  36. package/src/hooks/useAuthApp.ts +165 -0
  37. package/src/hooks/useAuthSnapshot.ts +84 -0
  38. package/src/hooks/useIsAuthenticated.ts +19 -0
  39. package/src/hooks/useProfileStepFlow.ts +78 -0
  40. package/src/i18n/index.ts +59 -0
  41. package/src/index.ts +9 -0
  42. package/src/locales/ar-SA.json +183 -0
  43. package/src/locales/de-DE.json +183 -0
  44. package/src/locales/el-GR.json +183 -0
  45. package/src/locales/en.json +183 -0
  46. package/src/locales/es-ES.json +183 -0
  47. package/src/locales/fi-FI.json +183 -0
  48. package/src/locales/fil-PH.json +183 -0
  49. package/src/locales/fr-FR.json +183 -0
  50. package/src/locales/hi-IN.json +183 -0
  51. package/src/locales/ja-JP.json +183 -0
  52. package/src/locales/ko-KR.json +183 -0
  53. package/src/locales/pt-BR.json +183 -0
  54. package/src/locales/pt-PT.json +183 -0
  55. package/src/locales/ru-RU.json +183 -0
  56. package/src/locales/ta-IN.json +183 -0
  57. package/src/locales/uk-UA.json +183 -0
  58. package/src/locales/zh-CN.json +183 -0
  59. package/src/locales/zh-TW.json +183 -0
  60. package/src/node/index.ts +271 -0
  61. package/src/types/account.types.ts +28 -0
  62. package/src/types/app.ts +22 -0
  63. package/src/types/profile.ts +6 -0
  64. package/src/utils/AccountManagerV2.ts +352 -0
  65. package/src/utils/Redirect.ts +17 -0
  66. package/src/utils/accountStorage.ts +46 -0
  67. package/src/utils/errors.ts +1 -0
  68. package/src/utils/index.ts +11 -0
  69. package/src/utils/location.ts +34 -0
  70. package/src/utils/profileStep.ts +26 -0
  71. package/src/utils/resolveAppId.ts +13 -0
  72. package/src/utils/selectedProfileStorage.ts +46 -0
  73. package/src/utils/tokenStorage.ts +47 -0
  74. package/dist/index.cjs.js +0 -22
  75. package/dist/index.cjs.js.map +0 -1
  76. package/dist/index.d.ts +0 -305
  77. package/dist/index.es.js +0 -8897
  78. package/dist/index.es.js.map +0 -1
@@ -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
+ )
@@ -0,0 +1,17 @@
1
+ export * from "./uploadAvatarAtom"
2
+ export * from "./appConfigAtom"
3
+ export * from "./accountAtom"
4
+ export * from "./accountProfileAtom"
5
+ export * from "./switchProfileAtom"
6
+ export * from "./createProfileAtom"
7
+ export * from "./profileDraftAtom"
8
+ export * from "./profileAtom"
9
+ export * from "./profilesAtom"
10
+ export * from "./updateProfileAtom"
11
+ export * from "./loginAtom"
12
+ export * from "./sessionCheckAtom"
13
+ export * from "./userLogoutAtom"
14
+ export * from "./motherTongueAtom"
15
+ export * from "./learningLangAtom"
16
+ export * from "./userSettingsAtom"
17
+ export * from "./localStorageAtom"
@@ -0,0 +1,8 @@
1
+ import { normalizeLang } from "@pmate/lang"
2
+ import { atom } from "jotai"
3
+ import { profileAtom } from "./accountProfileAtom"
4
+
5
+ export const learningLangAtom = atom((get) => {
6
+ const profile = get(profileAtom)
7
+ return normalizeLang(profile?.learningTargetLang)
8
+ })
@@ -0,0 +1,39 @@
1
+ import { atom } from "jotai"
2
+ import { atomFamily, type AtomFamily } from "jotai-family"
3
+ import type { WritableAtom } from "jotai"
4
+
5
+ export enum LSKEYS {
6
+ USER_SETTINGS = "user-settings",
7
+ }
8
+
9
+ const cacheAtom = atomFamily((_: LSKEYS) => atom<any>(undefined))
10
+
11
+ type LocalStorageJsonAtomFamily = AtomFamily<
12
+ LSKEYS,
13
+ WritableAtom<any, [any], void>
14
+ >
15
+
16
+ export const localStorageJsonAtom: LocalStorageJsonAtomFamily = atomFamily(
17
+ (key: LSKEYS) =>
18
+ atom(
19
+ (get) => {
20
+ const cache = get(cacheAtom(key))
21
+ if (cache) {
22
+ return cache
23
+ }
24
+ try {
25
+ const item = localStorage.getItem(key)
26
+ if (!item) {
27
+ return null
28
+ }
29
+ return JSON.parse(item)
30
+ } catch {
31
+ return null
32
+ }
33
+ },
34
+ (_, set, value: any) => {
35
+ localStorage.setItem(key, JSON.stringify(value))
36
+ set(cacheAtom(key), value)
37
+ }
38
+ )
39
+ )
@@ -0,0 +1,8 @@
1
+ import { atom } from "jotai"
2
+ import { AccountManagerV2 } from "../utils/AccountManagerV2"
3
+ import { resolveAppId } from "../utils/resolveAppId"
4
+
5
+ export const loginAtom = atom(null, async () => {
6
+ const manager = AccountManagerV2.get(resolveAppId())
7
+ return manager.login()
8
+ })
@@ -0,0 +1,8 @@
1
+ import { normalizeLang } from "@pmate/lang"
2
+ import { atom } from "jotai"
3
+ import { profileAtom } from "./accountProfileAtom"
4
+
5
+ export const motherTongueAtom = atom((get) => {
6
+ const profile = get(profileAtom)
7
+ return normalizeLang(profile?.motherTongue, "zh-CN")
8
+ })
@@ -0,0 +1,24 @@
1
+ import { Profile } from "@pmate/meta"
2
+ import { atomWithRefresh } from "jotai/utils"
3
+ import { atomFamily, type AtomFamily } from "jotai-family"
4
+ import type { WritableAtom } from "jotai"
5
+ import { EntityService } from "../api/EntityService"
6
+
7
+ type ProfileByIdAtomFamily = AtomFamily<
8
+ string,
9
+ WritableAtom<Promise<Profile | null>, [], void>
10
+ >
11
+
12
+ export const profileByIdAtom: ProfileByIdAtomFamily = atomFamily(
13
+ (profileId: string) =>
14
+ atomWithRefresh(async () => {
15
+ if (!profileId) {
16
+ return null
17
+ }
18
+ try {
19
+ return await EntityService.entity<Profile>(profileId)
20
+ } catch {
21
+ return null
22
+ }
23
+ })
24
+ )
@@ -0,0 +1,7 @@
1
+ import { atom } from "jotai"
2
+ import type { ProfileDraft } from "../types/profile"
3
+
4
+ /**
5
+ * Temporary profile info during registration flow
6
+ */
7
+ export const profileDraftAtom = atom<ProfileDraft>({})
@@ -0,0 +1,20 @@
1
+ import { Profile } from "@pmate/meta"
2
+ import { atomWithLoadable } from "./atomWithLoadable"
3
+ import { ProfileService } from "../api/ProfileService"
4
+ import { resolveAppId } from "../utils/resolveAppId"
5
+ import { AccountManagerV2 } from "../utils/AccountManagerV2"
6
+ import { accountAtom } from "./accountAtom"
7
+
8
+ export const profilesAtom = atomWithLoadable<Profile[]>(async (get) => {
9
+ get(accountAtom)
10
+ const manager = AccountManagerV2.get(resolveAppId())
11
+ const acc = await manager.getAccountState()
12
+ if (!acc) {
13
+ return [] as Profile[]
14
+ }
15
+ try {
16
+ return await ProfileService.getProfiles(acc)
17
+ } catch {
18
+ return [] as Profile[]
19
+ }
20
+ })
@@ -0,0 +1,10 @@
1
+ import { atom } from "jotai"
2
+ import { AccountService } from "../api/AccountService"
3
+
4
+ export const sessionCheckAtom = atom(null, async () => {
5
+ try {
6
+ return await AccountService.session()
7
+ } catch {
8
+ return null
9
+ }
10
+ })
@@ -0,0 +1,9 @@
1
+ import { Profile } from "@pmate/meta"
2
+ import { atom } from "jotai"
3
+ import { AccountManagerV2 } from "../utils/AccountManagerV2"
4
+ import { resolveAppId } from "../utils/resolveAppId"
5
+
6
+ export const switchProfileAtom = atom(null, (_get, _, profile: Profile) => {
7
+ const app = resolveAppId(profile.app)
8
+ AccountManagerV2.get(app).setSelectedProfile(profile)
9
+ })
@@ -0,0 +1,35 @@
1
+ import { atom } from "jotai"
2
+ import { ProfileService } from "../api/ProfileService"
3
+ import { AccountManagerV2 } from "../utils/AccountManagerV2"
4
+ import { resolveAppId } from "../utils/resolveAppId"
5
+ import { profileAtom } from "./accountProfileAtom"
6
+ import { profileByIdAtom } from "./profileAtom"
7
+
8
+ export const updateProfileAtom = atom(
9
+ null,
10
+ async (get, _set, profileId: string, updates: Record<string, any>) => {
11
+ const selected = get(profileAtom)
12
+ const target =
13
+ selected && selected.id === profileId
14
+ ? selected
15
+ : await get(profileByIdAtom(profileId))
16
+
17
+ if (!target) {
18
+ return
19
+ }
20
+
21
+ await ProfileService.updateProfile({
22
+ profileId: target.id,
23
+ ...updates,
24
+ })
25
+
26
+ const app = resolveAppId(target.app)
27
+ const manager = AccountManagerV2.get(app)
28
+ const updated = { ...target, ...updates }
29
+ if (selected && selected.id === profileId) {
30
+ manager.setSelectedProfile(updated)
31
+ }
32
+
33
+ await manager.getProfiles()
34
+ }
35
+ )
@@ -0,0 +1,49 @@
1
+ import imageCompression from "browser-image-compression"
2
+ import { atom } from "jotai"
3
+ import { ProfileService } from "../api/ProfileService"
4
+
5
+ type UploadAvatarParams = {
6
+ file: File
7
+ userId: string
8
+ }
9
+
10
+ const fileToBase64 = (file: File) =>
11
+ new Promise<string>((resolve, reject) => {
12
+ const reader = new FileReader()
13
+ reader.onloadend = () => {
14
+ const result = reader.result
15
+ if (typeof result !== "string") {
16
+ reject(new Error("Failed to read file"))
17
+ return
18
+ }
19
+ const [, base64] = result.split(",")
20
+ if (!base64) {
21
+ reject(new Error("Failed to parse base64 data"))
22
+ return
23
+ }
24
+ resolve(base64)
25
+ }
26
+ reader.onerror = () => {
27
+ reject(new Error("Failed to read file"))
28
+ }
29
+ reader.readAsDataURL(file)
30
+ })
31
+
32
+ export const uploadAvatarAtom = atom(
33
+ null,
34
+ async (_get, _set, { file, userId }: UploadAvatarParams) => {
35
+ const compressed = await imageCompression(file, {
36
+ maxSizeMB: 1,
37
+ maxWidthOrHeight: 128,
38
+ useWebWorker: true,
39
+ })
40
+
41
+ const base64 = await fileToBase64(compressed)
42
+
43
+ return ProfileService.updateAvatar({
44
+ user: userId,
45
+ base64,
46
+ filename: compressed.name,
47
+ })
48
+ }
49
+ )
@@ -0,0 +1,8 @@
1
+ import { atom } from "jotai"
2
+ import { AccountManagerV2 } from "../utils/AccountManagerV2"
3
+ import { resolveAppId } from "../utils/resolveAppId"
4
+
5
+ export const userLogoutAtom = atom(null, async () => {
6
+ const manager = AccountManagerV2.get(resolveAppId())
7
+ await manager.logout()
8
+ })
@@ -0,0 +1,58 @@
1
+ import { VoiceList } from "@pmate/meta"
2
+ import { Difficulty, UserSettings } from "@pmate/meta"
3
+ import { WritableAtom, atom } from "jotai"
4
+ import { LSKEYS, localStorageJsonAtom } from "./localStorageAtom"
5
+
6
+ const defaultSettings: UserSettings = {
7
+ intensive: false,
8
+ bilingual: true,
9
+ fontColor: "#000000",
10
+ backgroundColor: "white",
11
+ books: [],
12
+ advancedMode: false,
13
+ autoread: true,
14
+ playSpeed: 1,
15
+ scrollDirection: "vertical",
16
+ companion: "",
17
+ difficulty: Difficulty.Medium,
18
+ "chatVoice@v2": VoiceList.KOKORO_af_alloy,
19
+ uiLang: "zh-CN",
20
+ motherTongue: "zh-CN",
21
+ }
22
+
23
+ const settingsCache = new Map<keyof UserSettings, WritableAtom<any, any, any>>()
24
+
25
+ export const userSettingsAtom = <T extends keyof UserSettings>(key: T) => {
26
+ if (settingsCache.has(key)) {
27
+ return settingsCache.get(key)! as WritableAtom<
28
+ UserSettings[T],
29
+ [value: UserSettings[T]],
30
+ void
31
+ >
32
+ }
33
+
34
+ const settingAtom = atom(
35
+ (get) => {
36
+ const stored = get(localStorageJsonAtom(LSKEYS.USER_SETTINGS)) as
37
+ | UserSettings
38
+ | null
39
+ if (stored && key in stored) {
40
+ return stored[key] as UserSettings[T]
41
+ }
42
+ return defaultSettings[key]
43
+ },
44
+ (get, set, value: UserSettings[T]) => {
45
+ const stored = get(localStorageJsonAtom(LSKEYS.USER_SETTINGS)) as
46
+ | UserSettings
47
+ | null
48
+ const next = {
49
+ ...(stored ?? defaultSettings),
50
+ [key]: value,
51
+ } as UserSettings
52
+ set(localStorageJsonAtom(LSKEYS.USER_SETTINGS), next)
53
+ }
54
+ )
55
+
56
+ settingsCache.set(key, settingAtom)
57
+ return settingAtom as WritableAtom<UserSettings[T], [value: UserSettings[T]], void>
58
+ }
@@ -0,0 +1,300 @@
1
+ import { Button } from "./Button"
2
+ import { Drawer } from "./Drawer"
3
+ import { useSetAtom } from "jotai"
4
+ import {
5
+ Component,
6
+ PropsWithChildren,
7
+ useCallback,
8
+ useEffect,
9
+ useState,
10
+ } from "react"
11
+ import { useAuthSnapshot } from "../hooks/useAuthSnapshot"
12
+ import { accountAtom } from "../atoms/accountAtom"
13
+ import { AccountLifecycleState } from "../types/account.types"
14
+ import { userLogoutAtom } from "../atoms/userLogoutAtom"
15
+ import { Redirect } from "../utils/Redirect"
16
+ import { NotAuthenticatedError } from "../utils/errors"
17
+ import {
18
+ getWindowPathname,
19
+ subscribeToLocationChange,
20
+ } from "../utils/location"
21
+
22
+ export type AuthRoute =
23
+ | string
24
+ | {
25
+ path: string
26
+ behavior?: "redirect" | "prompt"
27
+ }
28
+
29
+ const escapeRegExp = (value: string) =>
30
+ value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
31
+
32
+ const matchRoutePath = (pattern: string, pathname: string) => {
33
+ if (pattern === "*") {
34
+ return true
35
+ }
36
+ if (pattern === "/") {
37
+ return pathname === "/"
38
+ }
39
+ const segments = pattern.split("/").filter(Boolean)
40
+ const parts = segments.map((segment) => {
41
+ if (segment === "*") {
42
+ return ".*"
43
+ }
44
+ if (segment.startsWith(":")) {
45
+ return "[^/]+"
46
+ }
47
+ return escapeRegExp(segment)
48
+ })
49
+ const regex = new RegExp(`^/${parts.join("/")}$`)
50
+ return regex.test(pathname)
51
+ }
52
+
53
+ const getAuthBehaviors = (
54
+ authRoutes: AuthRoute[] | undefined,
55
+ pathname: string
56
+ ) => {
57
+ const matchedAuthRoute = authRoutes?.find((route) => {
58
+ const path = typeof route === "string" ? route : route.path
59
+ return matchRoutePath(path, pathname)
60
+ })
61
+ const authBehavior =
62
+ matchedAuthRoute && typeof matchedAuthRoute !== "string"
63
+ ? (matchedAuthRoute.behavior ?? "redirect")
64
+ : "redirect"
65
+ const requiresAuth = Boolean(matchedAuthRoute)
66
+ return {
67
+ authBehavior,
68
+ requiresAuth,
69
+ }
70
+ }
71
+
72
+ type AuthProviderV2Props = PropsWithChildren<{
73
+ app: string
74
+ authRoutes?: AuthRoute[]
75
+ onLoginSuccess?: () => void | Promise<void>
76
+ rtcProvider?: React.ComponentType<{ children: React.ReactNode }>
77
+ pathname?: string
78
+ }>
79
+
80
+ const useWindowPathname = () => {
81
+ const [pathname, setPathname] = useState(getWindowPathname())
82
+ useEffect(() => {
83
+ const update = () => setPathname(getWindowPathname())
84
+ const unsubscribe = subscribeToLocationChange(update)
85
+ return () => unsubscribe()
86
+ }, [])
87
+ return pathname
88
+ }
89
+
90
+ export const AuthProviderV2 = ({
91
+ app,
92
+ authRoutes,
93
+ rtcProvider: RtcProvider,
94
+ pathname: pathnameProp,
95
+ children,
96
+ }: AuthProviderV2Props) => {
97
+ const pathname = pathnameProp ?? useWindowPathname()
98
+ const [isLoginPromptOpen, setIsLoginPromptOpen] = useState(false)
99
+ const [isLoginErrorDismissed, setIsLoginErrorDismissed] = useState(false)
100
+ const setAccountSnapshot = useSetAtom(accountAtom)
101
+ const { authBehavior, requiresAuth } = getAuthBehaviors(
102
+ authRoutes,
103
+ pathname
104
+ )
105
+ const { loading, snapshot } = useAuthSnapshot({
106
+ app,
107
+ behaviors: {
108
+ authBehavior,
109
+ requiresAuth,
110
+ },
111
+ })
112
+ const loginError = snapshot.error
113
+ const hasAccount =
114
+ snapshot.state === AccountLifecycleState.Idle
115
+ ? null
116
+ : Boolean(snapshot.account)
117
+
118
+ useEffect(() => {
119
+ setAccountSnapshot(snapshot)
120
+ }, [setAccountSnapshot, snapshot])
121
+
122
+ useEffect(() => {
123
+ if (!requiresAuth || loading) {
124
+ return
125
+ }
126
+ if (
127
+ snapshot.account &&
128
+ snapshot.profiles.length === 0 &&
129
+ pathname !== "/create-profile"
130
+ ) {
131
+ Redirect.toCreateProfile(app)
132
+ return
133
+ }
134
+ if (!snapshot.account && authBehavior === "redirect") {
135
+ Redirect.toLogin(app)
136
+ }
137
+ }, [
138
+ app,
139
+ authBehavior,
140
+ loading,
141
+ pathname,
142
+ requiresAuth,
143
+ snapshot.account,
144
+ snapshot.profiles.length,
145
+ ])
146
+
147
+ useEffect(() => {
148
+ if (loading) {
149
+ return
150
+ }
151
+ setIsLoginPromptOpen(
152
+ requiresAuth && !snapshot.account && authBehavior === "prompt",
153
+ )
154
+ }, [authBehavior, loading, requiresAuth, snapshot.account])
155
+ useEffect(() => {
156
+ if (!loginError) {
157
+ setIsLoginErrorDismissed(false)
158
+ }
159
+ }, [loginError])
160
+
161
+ const handleBack = () => {
162
+ setIsLoginPromptOpen(false)
163
+ if (window.history.length > 1) {
164
+ window.history.back()
165
+ return
166
+ }
167
+ // window.location.href = "/"
168
+ }
169
+
170
+ if (loading && requiresAuth) {
171
+ return null
172
+ }
173
+ if (loginError && requiresAuth && !isLoginErrorDismissed) {
174
+ return (
175
+ <div className="flex min-h-screen items-center justify-center bg-slate-50 p-6">
176
+ <div className="w-full max-w-md rounded-xl border border-rose-200 bg-white p-6 shadow-sm">
177
+ <div className="text-sm font-semibold text-rose-600">
178
+ Login failed
179
+ </div>
180
+ <p className="mt-2 text-sm text-slate-600">
181
+ We could not restore your session. Please try again.
182
+ </p>
183
+ <div className="mt-3 text-xs text-rose-500">{loginError.message}</div>
184
+ <div className="mt-4 flex flex-wrap gap-3">
185
+ <button
186
+ className="rounded-md bg-rose-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-rose-700"
187
+ type="button"
188
+ onClick={() => window.location.reload()}
189
+ >
190
+ Reload
191
+ </button>
192
+ </div>
193
+ </div>
194
+ </div>
195
+ )
196
+ }
197
+ if (requiresAuth && hasAccount === null) {
198
+ return null
199
+ }
200
+ if (requiresAuth && hasAccount === false && authBehavior === "prompt") {
201
+ return (
202
+ <Drawer
203
+ open={isLoginPromptOpen}
204
+ onClose={handleBack}
205
+ anchor="bottom"
206
+ overlayClassName="bg-black/40"
207
+ >
208
+ <div className="rounded-t-2xl px-6 pb-6 pt-4">
209
+ <div className="mx-auto mb-3 h-1.5 w-12 rounded-full bg-slate-200" />
210
+ <div className="text-lg font-semibold text-slate-900">
211
+ You need login to continue ?
212
+ </div>
213
+ <div className="mt-5 flex items-center justify-end gap-3">
214
+ <Button
215
+ type="button"
216
+ variant="plain"
217
+ className="min-w-[96px] justify-center"
218
+ onClick={handleBack}
219
+ >
220
+ Back
221
+ </Button>
222
+ <Button
223
+ type="button"
224
+ className="min-w-[96px] justify-center"
225
+ onClick={() => Redirect.toLogin(app)}
226
+ >
227
+ Login
228
+ </Button>
229
+ </div>
230
+ </div>
231
+ </Drawer>
232
+ )
233
+ }
234
+ if (!requiresAuth) {
235
+ return children
236
+ }
237
+ const RtcWrapper = RtcProvider ?? (({ children }) => <>{children}</>)
238
+ return (
239
+ <AuthErrorBoundary app={app}>
240
+ <RtcWrapper>{children}</RtcWrapper>
241
+ </AuthErrorBoundary>
242
+ )
243
+ }
244
+
245
+ interface AuthErrorBoundaryState {
246
+ hasError: boolean
247
+ }
248
+
249
+ interface AuthErrorBoundaryProps extends PropsWithChildren {
250
+ onAuthError: () => Promise<void>
251
+ }
252
+
253
+ class AuthErrorBoundaryInner extends Component<
254
+ AuthErrorBoundaryProps,
255
+ AuthErrorBoundaryState
256
+ > {
257
+ state: AuthErrorBoundaryState = { hasError: false }
258
+
259
+ static getDerivedStateFromError(): AuthErrorBoundaryState {
260
+ return { hasError: true }
261
+ }
262
+
263
+ componentDidCatch(error: unknown) {
264
+ if (error instanceof NotAuthenticatedError) {
265
+ this.props.onAuthError().then(() => {
266
+ this.setState({ hasError: false })
267
+ })
268
+ return
269
+ }
270
+
271
+ console.error(error)
272
+ }
273
+
274
+ render() {
275
+ if (this.state.hasError) {
276
+ return null
277
+ }
278
+
279
+ return this.props.children
280
+ }
281
+ }
282
+
283
+ const AuthErrorBoundary = ({
284
+ children,
285
+ app,
286
+ }: PropsWithChildren<{
287
+ app: string
288
+ }>) => {
289
+ const logout = useSetAtom(userLogoutAtom)
290
+ const handleAuthError = useCallback(async () => {
291
+ await logout()
292
+ Redirect.toLogin(app)
293
+ }, [app, logout])
294
+
295
+ return (
296
+ <AuthErrorBoundaryInner onAuthError={handleAuthError}>
297
+ {children}
298
+ </AuthErrorBoundaryInner>
299
+ )
300
+ }