@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,349 @@
1
+ import { EmitterV2 } from "@pmate/utils"
2
+ import {
3
+ AccountState,
4
+ LangShort,
5
+ LocalProfileState,
6
+ Profile,
7
+ RoomPeerInfo,
8
+ } from "@pmate/meta"
9
+ import { AccountLifecycleState } from "../types/account.types"
10
+ import type { AccountSnapshot } from "../types/account.types"
11
+ import { AccountService, ProfileService } from "../api"
12
+ import { clearAccountState, setAccountState } from "./accountStorage"
13
+ import { resolveAppId } from "./resolveAppId"
14
+ import {
15
+ clearSelectedProfileId,
16
+ getSelectedProfileId,
17
+ setSelectedProfileId,
18
+ } from "./selectedProfileStorage"
19
+ import { clearAuthToken, getAuthToken, setAuthToken } from "./tokenStorage"
20
+
21
+ export enum AccountManagerEvent {
22
+ StateChange = "stateChange",
23
+ }
24
+
25
+ export type AccountManagerEventMap = {
26
+ [AccountManagerEvent.StateChange]: void
27
+ }
28
+
29
+ export class AccountManagerV2 extends EmitterV2<AccountManagerEventMap> {
30
+ private static instances = new Map<string, AccountManagerV2>()
31
+
32
+ constructor(private readonly app: string) {
33
+ super()
34
+ }
35
+
36
+ public static get(app?: string): AccountManagerV2 {
37
+ const resolvedApp = resolveAppId(app)
38
+ const existing = AccountManagerV2.instances.get(resolvedApp)
39
+ if (existing) {
40
+ return existing
41
+ }
42
+ const manager = new AccountManagerV2(resolvedApp)
43
+ AccountManagerV2.instances.set(resolvedApp, manager)
44
+ return manager
45
+ }
46
+
47
+ public async session() {
48
+ try {
49
+ return await AccountService.session(undefined, this.app)
50
+ } catch (error) {
51
+ return null
52
+ }
53
+ }
54
+
55
+ public async login(): Promise<AccountState> {
56
+ this.transition(AccountLifecycleState.LoggingIn, null)
57
+ try {
58
+ const session = await AccountService.session(undefined, this.app)
59
+ if (!session) {
60
+ throw new Error("Session not found")
61
+ }
62
+ const issuedAtMs = Date.parse(session.issuedAt)
63
+ const state: AccountState = {
64
+ accountId: session.identity.accountId,
65
+ token: "",
66
+ signTime: Number.isNaN(issuedAtMs) ? Date.now() : issuedAtMs,
67
+ app: this.app,
68
+ }
69
+ this.setAccountState(state)
70
+ this.transition(AccountLifecycleState.Authenticated, null)
71
+ this.transition(AccountLifecycleState.ProfileInitializing, null)
72
+ const profiles = await this.prepareProfiles(state)
73
+ this.transition(
74
+ profiles.length > 0
75
+ ? AccountLifecycleState.ProfileReady
76
+ : AccountLifecycleState.ProfileUninitialized,
77
+ null,
78
+ )
79
+ return state
80
+ } catch (error) {
81
+ const nextError =
82
+ error instanceof Error ? error : new Error("Login failed")
83
+ this.transition(AccountLifecycleState.Error, nextError)
84
+ throw nextError
85
+ }
86
+ }
87
+
88
+ public hasUrlSession(): boolean {
89
+ if (typeof window === "undefined") {
90
+ return false
91
+ }
92
+ const params = new URLSearchParams(window.location.search)
93
+ return params.has("sessionId")
94
+ }
95
+
96
+ /**
97
+ * When a URL contains a sessionId parameter, this method logs in the user.
98
+ * If URL does not contain a sessionId parameter, this method logs in from existing session.
99
+ */
100
+ public async loginUrlSessionOverride(): Promise<AccountState> {
101
+ if (typeof window === "undefined") {
102
+ return this.login()
103
+ }
104
+ const params = new URLSearchParams(window.location.search)
105
+ const sessionId = params.get("sessionId")
106
+ if (sessionId) {
107
+ this.setAuthToken(sessionId)
108
+ }
109
+ const account = await this.login()
110
+ if (sessionId) {
111
+ params.delete("sessionId")
112
+ const nextSearch = params.toString()
113
+ const nextUrl = `${window.location.pathname}${
114
+ nextSearch ? `?${nextSearch}` : ""
115
+ }${window.location.hash}`
116
+ window.history.replaceState(null, "", nextUrl)
117
+ }
118
+ return account
119
+ }
120
+
121
+ public async logout(scope: "app" | "all" = "app"): Promise<void> {
122
+ this.transition(AccountLifecycleState.LoggingOut, null)
123
+ try {
124
+ const token = getAuthToken(this.app) || undefined
125
+ if (scope === "all") {
126
+ await AccountService.logoutAll(token, this.app)
127
+ } else {
128
+ await AccountService.logout(token, this.app)
129
+ }
130
+ } catch {
131
+ // ignore logout failures to ensure local session is cleared
132
+ } finally {
133
+ if (scope === "all") {
134
+ AccountManagerV2.clearAllLocalSessions()
135
+ } else {
136
+ this.clearSession()
137
+ }
138
+ this.transition(AccountLifecycleState.Unauthenticated, null)
139
+ }
140
+ }
141
+
142
+ public async getSnapshot(): Promise<AccountSnapshot> {
143
+ const account = await this.getAccountState()
144
+ if (!account) {
145
+ return {
146
+ state: AccountLifecycleState.Unauthenticated,
147
+ account: null,
148
+ accountId: null,
149
+ error: null,
150
+ profiles: [],
151
+ profile: null,
152
+ }
153
+ }
154
+ const profiles = await this.getProfiles()
155
+ const profile = await this.getSelectedProfile(profiles)
156
+ const state =
157
+ profiles.length > 0
158
+ ? AccountLifecycleState.ProfileReady
159
+ : AccountLifecycleState.ProfileUninitialized
160
+ return {
161
+ state,
162
+ account,
163
+ accountId: account.accountId ?? null,
164
+ error: null,
165
+ profiles,
166
+ profile,
167
+ }
168
+ }
169
+
170
+ public transition(next: AccountLifecycleState, error?: Error | null): void {
171
+ void next
172
+ void error
173
+ this.emit(AccountManagerEvent.StateChange)
174
+ }
175
+
176
+ public async getAccountState(): Promise<AccountState | null> {
177
+ const session = await this.session()
178
+ if (!session) {
179
+ return null
180
+ }
181
+ const issuedAtMs = Date.parse(session.issuedAt)
182
+ const state: AccountState = {
183
+ accountId: session.identity.accountId,
184
+ token: getAuthToken(this.app) || "",
185
+ signTime: Number.isNaN(issuedAtMs) ? Date.now() : issuedAtMs,
186
+ app: this.app,
187
+ }
188
+ return state
189
+ }
190
+
191
+ public setAccountState(state: AccountState): void {
192
+ setAccountState(state, this.app)
193
+ }
194
+
195
+ public clearAccountState(): void {
196
+ clearAccountState(this.app)
197
+ }
198
+
199
+ public async getSelectedProfile(
200
+ profiles?: Profile[],
201
+ ): Promise<Profile | null> {
202
+ const storedId = getSelectedProfileId(this.app)
203
+ if (!storedId) {
204
+ return null
205
+ }
206
+ const resolvedProfiles = profiles ?? (await this.getProfiles())
207
+ return resolvedProfiles.find((profile) => profile.id === storedId) ?? null
208
+ }
209
+
210
+ public setSelectedProfile(
211
+ profile: Profile | LocalProfileState | string,
212
+ ): void {
213
+ const nextId = typeof profile === "string" ? profile : profile.id
214
+ setSelectedProfileId(nextId, this.app)
215
+ this.emit(AccountManagerEvent.StateChange)
216
+ }
217
+
218
+ public clearSelectedProfile(): void {
219
+ clearSelectedProfileId(this.app)
220
+ this.emit(AccountManagerEvent.StateChange)
221
+ }
222
+
223
+ public async getProfiles(): Promise<Profile[]> {
224
+ const account = await this.getAccountState()
225
+ if (!account) {
226
+ return []
227
+ }
228
+ try {
229
+ return await this.prepareProfiles(account)
230
+ } catch {
231
+ return []
232
+ }
233
+ }
234
+
235
+ public clearProfiles(): void {
236
+ this.emit(AccountManagerEvent.StateChange)
237
+ }
238
+
239
+ public clearSession(): void {
240
+ this.clearAccountState()
241
+ this.clearSelectedProfile()
242
+ this.clearProfiles()
243
+ this.clearAuthToken()
244
+ }
245
+
246
+ public getAuthToken(): string | null {
247
+ return getAuthToken(this.app)
248
+ }
249
+
250
+ public setAuthToken(token: string): void {
251
+ setAuthToken(token, this.app)
252
+ AccountService.clearSessionCache()
253
+ this.emit(AccountManagerEvent.StateChange)
254
+ }
255
+
256
+ public clearAuthToken(): void {
257
+ clearAuthToken(this.app)
258
+ AccountService.clearSessionCache()
259
+ this.emit(AccountManagerEvent.StateChange)
260
+ }
261
+
262
+ private static clearAllLocalSessions() {
263
+ if (typeof localStorage === "undefined") {
264
+ return
265
+ }
266
+
267
+ const prefixes = [
268
+ "pmate-auth-token:",
269
+ "account-state-v2:",
270
+ "selected-profile-v2:",
271
+ ]
272
+ const keysToRemove: string[] = []
273
+ for (let index = 0; index < localStorage.length; index += 1) {
274
+ const key = localStorage.key(index)
275
+ if (!key) continue
276
+ if (prefixes.some((prefix) => key.startsWith(prefix))) {
277
+ keysToRemove.push(key)
278
+ }
279
+ }
280
+
281
+ for (const key of keysToRemove) {
282
+ localStorage.removeItem(key)
283
+ }
284
+
285
+ AccountService.clearSessionCache()
286
+ for (const manager of AccountManagerV2.instances.values()) {
287
+ manager.emit(AccountManagerEvent.StateChange)
288
+ }
289
+ }
290
+
291
+ public async getLocalProfile(): Promise<LocalProfileState | null> {
292
+ const account = await this.getAccountState()
293
+ const profile = await this.getSelectedProfile()
294
+ if (!account || !profile) {
295
+ return null
296
+ }
297
+ const { id, ...rest } = profile
298
+ return { ...account, ...rest, id } as LocalProfileState
299
+ }
300
+
301
+ public async getCurrentPeer(): Promise<RoomPeerInfo | undefined> {
302
+ const user = await this.getLocalProfile()
303
+ if (!user) {
304
+ return
305
+ }
306
+ return {
307
+ ...user,
308
+ gender: user.gender ?? "",
309
+ email: user.email || "",
310
+ isOnline: true,
311
+ } as RoomPeerInfo
312
+ }
313
+
314
+ public async createProfile(payload: {
315
+ nickName: string
316
+ learningTargetLang?: LangShort
317
+ }): Promise<Profile> {
318
+ const account = await this.getAccountState()
319
+ if (!account) {
320
+ throw new Error("Account info is missing for profile creation")
321
+ }
322
+ const profile = await ProfileService.createProfile({
323
+ app: resolveAppId(account.app),
324
+ account: account.accountId,
325
+ nickName: payload.nickName,
326
+ learningTargetLang: payload.learningTargetLang,
327
+ })
328
+ await this.prepareProfiles(account)
329
+ if (!(await this.getSelectedProfile())) {
330
+ this.setSelectedProfile(profile)
331
+ }
332
+ return profile
333
+ }
334
+
335
+ private async prepareProfiles(account: AccountState): Promise<Profile[]> {
336
+ const profiles = await ProfileService.getProfiles(account)
337
+ const storedId = getSelectedProfileId(this.app)
338
+ if (storedId && profiles.some((profile) => profile.id === storedId)) {
339
+ return profiles
340
+ }
341
+ const nextSelected = profiles[0]
342
+ if (nextSelected) {
343
+ this.setSelectedProfile(nextSelected)
344
+ } else {
345
+ this.clearSelectedProfile()
346
+ }
347
+ return profiles
348
+ }
349
+ }
@@ -0,0 +1,17 @@
1
+ const AUTH_APP_BASE = "https://auth.pmate.chat"
2
+
3
+ export class Redirect {
4
+ static toLogin(app: string, redirect?: string) {
5
+ const url = new URL(AUTH_APP_BASE)
6
+ url.searchParams.set("redirect", redirect ?? window.location.href)
7
+ url.searchParams.set("app", app)
8
+ window.location.href = url.toString()
9
+ }
10
+
11
+ static toCreateProfile(app: string, redirect?: string) {
12
+ const url = new URL("/create-profile", AUTH_APP_BASE)
13
+ url.searchParams.set("redirect", redirect ?? window.location.href)
14
+ url.searchParams.set("app", app)
15
+ window.location.href = url.toString()
16
+ }
17
+ }
@@ -0,0 +1,46 @@
1
+ import type { AccountState } from "@pmate/meta"
2
+ import { resolveAppId } from "./resolveAppId"
3
+
4
+ export const ACCOUNT_STATE_KEY = "account-state-v2"
5
+
6
+ const canUseStorage = () => typeof localStorage !== "undefined"
7
+
8
+ const buildAccountStateStorageKey = (app?: string): string =>
9
+ `${ACCOUNT_STATE_KEY}:${resolveAppId(app)}`
10
+
11
+ export const getAccountState = (app?: string): AccountState | null => {
12
+ if (!canUseStorage()) {
13
+ return null
14
+ }
15
+ try {
16
+ const raw = localStorage.getItem(buildAccountStateStorageKey(app))
17
+ if (!raw) {
18
+ return null
19
+ }
20
+ return JSON.parse(raw) as AccountState
21
+ } catch {
22
+ return null
23
+ }
24
+ }
25
+
26
+ export const setAccountState = (state: AccountState, app?: string): void => {
27
+ if (!canUseStorage()) {
28
+ return
29
+ }
30
+ try {
31
+ localStorage.setItem(buildAccountStateStorageKey(app), JSON.stringify(state))
32
+ } catch {
33
+ // ignore write errors
34
+ }
35
+ }
36
+
37
+ export const clearAccountState = (app?: string): void => {
38
+ if (!canUseStorage()) {
39
+ return
40
+ }
41
+ try {
42
+ localStorage.removeItem(buildAccountStateStorageKey(app))
43
+ } catch {
44
+ // ignore delete errors
45
+ }
46
+ }
@@ -0,0 +1 @@
1
+ export class NotAuthenticatedError extends Error {}
@@ -0,0 +1,11 @@
1
+ export {
2
+ getLangFull,
3
+ isNicknameValid,
4
+ legacyLangMap,
5
+ normalizeLang,
6
+ } from "@pmate/lang"
7
+ export * from "./AccountManagerV2"
8
+ export * from "./resolveAppId"
9
+ export * from "./errors"
10
+ export * from "./profileStep"
11
+ export * from "./Redirect"
@@ -0,0 +1,34 @@
1
+ const LOCATION_CHANGE_EVENT = "pmate:locationchange"
2
+
3
+ const ensureLocationEvents = () => {
4
+ const history = window.history as History & { __pmatePatched?: boolean }
5
+ if (history.__pmatePatched) {
6
+ return
7
+ }
8
+ history.__pmatePatched = true
9
+ const notify = () => window.dispatchEvent(new Event(LOCATION_CHANGE_EVENT))
10
+ const wrap = (type: "pushState" | "replaceState") => {
11
+ const original = history[type]
12
+ history[type] = function (...args) {
13
+ const result = original.apply(
14
+ history,
15
+ args as Parameters<History["pushState"]>
16
+ )
17
+ notify()
18
+ return result
19
+ } as History["pushState"]
20
+ }
21
+ wrap("pushState")
22
+ wrap("replaceState")
23
+ window.addEventListener("popstate", notify)
24
+ }
25
+
26
+ export const subscribeToLocationChange = (listener: () => void) => {
27
+ ensureLocationEvents()
28
+ window.addEventListener(LOCATION_CHANGE_EVENT, listener)
29
+ return () => window.removeEventListener(LOCATION_CHANGE_EVENT, listener)
30
+ }
31
+
32
+ export const getWindowPathname = () => window.location.pathname
33
+
34
+ export const getWindowSearch = () => window.location.search
@@ -0,0 +1,26 @@
1
+ export type ProfileStepType =
2
+ | "nickname"
3
+ | "learning-language"
4
+ | "mother-tongue"
5
+ | "gender"
6
+ | "is-adult"
7
+ | "age"
8
+
9
+ const PROFILE_STEP_TYPE_SET: Record<ProfileStepType, true> = {
10
+ nickname: true,
11
+ "learning-language": true,
12
+ "mother-tongue": true,
13
+ gender: true,
14
+ "is-adult": true,
15
+ age: true,
16
+ }
17
+
18
+ export const isProfileStepType = (value: string): value is ProfileStepType => {
19
+ return Boolean(PROFILE_STEP_TYPE_SET[value as ProfileStepType])
20
+ }
21
+
22
+ export interface ProfileStep {
23
+ type: ProfileStepType
24
+ title: string
25
+ required: boolean
26
+ }
@@ -0,0 +1,13 @@
1
+ export const resolveAppId = (fallbackApp?: string) => {
2
+ const envApp = ""
3
+ if (envApp) {
4
+ return envApp
5
+ }
6
+ if (typeof window !== "undefined") {
7
+ const urlApp = new URLSearchParams(window.location.search).get("app")
8
+ if (urlApp) {
9
+ return urlApp
10
+ }
11
+ }
12
+ return fallbackApp || "pmate"
13
+ }
@@ -0,0 +1,46 @@
1
+ import { resolveAppId } from "./resolveAppId"
2
+
3
+ export const SELECTED_PROFILE_KEY = "selected-profile-v2"
4
+
5
+ const canUseStorage = () => typeof localStorage !== "undefined"
6
+
7
+ const buildSelectedProfileStorageKey = (app?: string): string =>
8
+ `${SELECTED_PROFILE_KEY}:${resolveAppId(app)}`
9
+
10
+ export const getSelectedProfileId = (app?: string): string | null => {
11
+ if (!canUseStorage()) {
12
+ return null
13
+ }
14
+ try {
15
+ const raw = localStorage.getItem(buildSelectedProfileStorageKey(app))
16
+ if (!raw) {
17
+ return null
18
+ }
19
+ const parsed = JSON.parse(raw) as unknown
20
+ return typeof parsed === "string" ? parsed : null
21
+ } catch {
22
+ return null
23
+ }
24
+ }
25
+
26
+ export const setSelectedProfileId = (id: string, app?: string): void => {
27
+ if (!canUseStorage()) {
28
+ return
29
+ }
30
+ try {
31
+ localStorage.setItem(buildSelectedProfileStorageKey(app), JSON.stringify(id))
32
+ } catch {
33
+ // ignore write errors
34
+ }
35
+ }
36
+
37
+ export const clearSelectedProfileId = (app?: string): void => {
38
+ if (!canUseStorage()) {
39
+ return
40
+ }
41
+ try {
42
+ localStorage.removeItem(buildSelectedProfileStorageKey(app))
43
+ } catch {
44
+ // ignore delete errors
45
+ }
46
+ }
@@ -0,0 +1,47 @@
1
+ import { resolveAppId } from "./resolveAppId"
2
+
3
+ export const TOKEN_STORAGE_KEY = "pmate-auth-token"
4
+
5
+ const canUseStorage = () => typeof localStorage !== "undefined"
6
+
7
+ const buildTokenStorageKey = (app?: string): string => {
8
+ return `${TOKEN_STORAGE_KEY}:${resolveAppId(app)}`
9
+ }
10
+
11
+ export const getAuthToken = (app?: string): string | null => {
12
+ if (!canUseStorage()) {
13
+ return null
14
+ }
15
+ try {
16
+ const raw = localStorage.getItem(buildTokenStorageKey(app))
17
+ if (!raw) {
18
+ return null
19
+ }
20
+ const parsed = JSON.parse(raw) as unknown
21
+ return typeof parsed === "string" ? parsed : null
22
+ } catch {
23
+ return null
24
+ }
25
+ }
26
+
27
+ export const setAuthToken = (token: string, app?: string): void => {
28
+ if (!canUseStorage()) {
29
+ return
30
+ }
31
+ try {
32
+ localStorage.setItem(buildTokenStorageKey(app), JSON.stringify(token))
33
+ } catch {
34
+ // ignore write errors
35
+ }
36
+ }
37
+
38
+ export const clearAuthToken = (app?: string): void => {
39
+ if (!canUseStorage()) {
40
+ return
41
+ }
42
+ try {
43
+ localStorage.removeItem(buildTokenStorageKey(app))
44
+ } catch {
45
+ // ignore delete errors
46
+ }
47
+ }