@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.
- package/package.json +18 -20
- 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 -302
- package/dist/index.es.js +0 -8884
- 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
|
+
}
|