Fixes to session management (#117)
* Update session-management to solve incorrectly dropped sessions * Reset the nav on account switch * Reset the feed on me.load() * Update tests to reflect new account-switching behavior
This commit is contained in:
@@ -180,7 +180,7 @@ describe('Account flows', () => {
|
|||||||
|
|
||||||
// signed in
|
// signed in
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(getByTestId('settingsScreen')).toBeTruthy() // we go back to settings in this situation
|
expect(getByTestId('homeFeed')).toBeTruthy() // we go back to settings in this situation
|
||||||
expect(rootStore?.me?.displayName).toBe('Bob')
|
expect(rootStore?.me?.displayName).toBe('Bob')
|
||||||
expect(rootStore?.me?.handle).toBe('bob.test')
|
expect(rootStore?.me?.handle).toBe('bob.test')
|
||||||
expect(rootStore?.session.accounts.length).toBe(2)
|
expect(rootStore?.session.accounts.length).toBe(2)
|
||||||
@@ -191,7 +191,15 @@ describe('Account flows', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('can instantly switch between accounts', async () => {
|
it('can instantly switch between accounts', async () => {
|
||||||
const {getByTestId} = render(<MobileShell />, rootStore)
|
const {getAllByTestId, getByTestId} = render(<MobileShell />, rootStore)
|
||||||
|
await waitFor(() => expect(getByTestId('homeFeed')).toBeTruthy(), WAIT_OPTS)
|
||||||
|
|
||||||
|
// open side menu
|
||||||
|
fireEvent.press(getAllByTestId('viewHeaderBackOrMenuBtn')[0])
|
||||||
|
await waitFor(() => expect(getByTestId('menuView')).toBeTruthy(), WAIT_OPTS)
|
||||||
|
|
||||||
|
// nav to settings
|
||||||
|
fireEvent.press(getByTestId('menuItemButton-Settings'))
|
||||||
await waitFor(
|
await waitFor(
|
||||||
() => expect(getByTestId('settingsScreen')).toBeTruthy(),
|
() => expect(getByTestId('settingsScreen')).toBeTruthy(),
|
||||||
WAIT_OPTS,
|
WAIT_OPTS,
|
||||||
@@ -212,7 +220,15 @@ describe('Account flows', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('will prompt for a password if you sign out', async () => {
|
it('will prompt for a password if you sign out', async () => {
|
||||||
const {getByTestId} = render(<MobileShell />, rootStore)
|
const {getAllByTestId, getByTestId} = render(<MobileShell />, rootStore)
|
||||||
|
await waitFor(() => expect(getByTestId('homeFeed')).toBeTruthy(), WAIT_OPTS)
|
||||||
|
|
||||||
|
// open side menu
|
||||||
|
fireEvent.press(getAllByTestId('viewHeaderBackOrMenuBtn')[0])
|
||||||
|
await waitFor(() => expect(getByTestId('menuView')).toBeTruthy(), WAIT_OPTS)
|
||||||
|
|
||||||
|
// nav to settings
|
||||||
|
fireEvent.press(getByTestId('menuItemButton-Settings'))
|
||||||
await waitFor(
|
await waitFor(
|
||||||
() => expect(getByTestId('settingsScreen')).toBeTruthy(),
|
() => expect(getByTestId('settingsScreen')).toBeTruthy(),
|
||||||
WAIT_OPTS,
|
WAIT_OPTS,
|
||||||
|
|||||||
+2
-11
@@ -1,6 +1,6 @@
|
|||||||
import {autorun} from 'mobx'
|
import {autorun} from 'mobx'
|
||||||
import {Platform} from 'react-native'
|
import {Platform} from 'react-native'
|
||||||
import {sessionClient as AtpApi, SessionServiceClient} from '@atproto/api'
|
import {sessionClient as SessionAtpApi} from '@atproto/api'
|
||||||
import {RootStoreModel} from './models/root-store'
|
import {RootStoreModel} from './models/root-store'
|
||||||
import * as libapi from './lib/api'
|
import * as libapi from './lib/api'
|
||||||
import * as storage from './lib/storage'
|
import * as storage from './lib/storage'
|
||||||
@@ -19,7 +19,7 @@ export async function setupState(serviceUri = DEFAULT_SERVICE) {
|
|||||||
|
|
||||||
libapi.doPolyfill()
|
libapi.doPolyfill()
|
||||||
|
|
||||||
const api = AtpApi.service(serviceUri) as SessionServiceClient
|
const api = SessionAtpApi.service(serviceUri)
|
||||||
rootStore = new RootStoreModel(api)
|
rootStore = new RootStoreModel(api)
|
||||||
try {
|
try {
|
||||||
data = (await storage.load(ROOT_STATE_STORAGE_KEY)) || {}
|
data = (await storage.load(ROOT_STATE_STORAGE_KEY)) || {}
|
||||||
@@ -38,15 +38,6 @@ export async function setupState(serviceUri = DEFAULT_SERVICE) {
|
|||||||
.catch((e: any) => {
|
.catch((e: any) => {
|
||||||
rootStore.log.warn('Failed initial connect', e)
|
rootStore.log.warn('Failed initial connect', e)
|
||||||
})
|
})
|
||||||
// @ts-ignore .on() is correct -prf
|
|
||||||
api.sessionManager.on('session', () => {
|
|
||||||
if (!api.sessionManager.session && rootStore.session.hasSession) {
|
|
||||||
// reset session
|
|
||||||
rootStore.session.clear()
|
|
||||||
} else if (api.sessionManager.session) {
|
|
||||||
rootStore.session.updateAuthTokens(api.sessionManager.session)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// track changes & save to storage
|
// track changes & save to storage
|
||||||
autorun(() => {
|
autorun(() => {
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// import {ReactNativeStore} from './auth'
|
// import {ReactNativeStore} from './auth'
|
||||||
import {
|
import AtpApi, {
|
||||||
sessionClient as AtpApi,
|
sessionClient as SessionAtpApi,
|
||||||
AppBskyEmbedImages,
|
AppBskyEmbedImages,
|
||||||
AppBskyEmbedExternal,
|
AppBskyEmbedExternal,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
@@ -21,6 +21,7 @@ const TIMEOUT = 10e3 // 10s
|
|||||||
|
|
||||||
export function doPolyfill() {
|
export function doPolyfill() {
|
||||||
AtpApi.xrpc.fetch = fetchHandler
|
AtpApi.xrpc.fetch = fetchHandler
|
||||||
|
SessionAtpApi.xrpc.fetch = fetchHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExternalEmbedDraft {
|
export interface ExternalEmbedDraft {
|
||||||
|
|||||||
@@ -245,6 +245,21 @@ export class FeedModel {
|
|||||||
// public api
|
// public api
|
||||||
// =
|
// =
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nuke all data
|
||||||
|
*/
|
||||||
|
clear() {
|
||||||
|
this.isLoading = false
|
||||||
|
this.isRefreshing = false
|
||||||
|
this.hasNewLatest = false
|
||||||
|
this.hasLoaded = false
|
||||||
|
this.error = ''
|
||||||
|
this.hasMore = true
|
||||||
|
this.loadMoreCursor = undefined
|
||||||
|
this.pollCursor = undefined
|
||||||
|
this.feed = []
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load for first render
|
* Load for first render
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ export class MeModel {
|
|||||||
this.avatar = ''
|
this.avatar = ''
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
this.mainFeed.clear()
|
||||||
this.mainFeed = new FeedModel(this.rootStore, 'home', {
|
this.mainFeed = new FeedModel(this.rootStore, 'home', {
|
||||||
algorithm: 'reverse-chronological',
|
algorithm: 'reverse-chronological',
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,7 +3,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {makeAutoObservable} from 'mobx'
|
import {makeAutoObservable} from 'mobx'
|
||||||
import {sessionClient as AtpApi, SessionServiceClient} from '@atproto/api'
|
import {
|
||||||
|
sessionClient as SessionAtpApi,
|
||||||
|
SessionServiceClient,
|
||||||
|
} from '@atproto/api'
|
||||||
import {createContext, useContext} from 'react'
|
import {createContext, useContext} from 'react'
|
||||||
import {DeviceEventEmitter, EmitterSubscription} from 'react-native'
|
import {DeviceEventEmitter, EmitterSubscription} from 'react-native'
|
||||||
import BackgroundFetch from 'react-native-background-fetch'
|
import BackgroundFetch from 'react-native-background-fetch'
|
||||||
@@ -19,6 +22,7 @@ import {OnboardModel} from './onboard'
|
|||||||
import {isNetworkError} from '../../lib/errors'
|
import {isNetworkError} from '../../lib/errors'
|
||||||
|
|
||||||
export class RootStoreModel {
|
export class RootStoreModel {
|
||||||
|
api: SessionServiceClient
|
||||||
log = new LogModel()
|
log = new LogModel()
|
||||||
session = new SessionModel(this)
|
session = new SessionModel(this)
|
||||||
nav = new NavigationModel()
|
nav = new NavigationModel()
|
||||||
@@ -28,7 +32,9 @@ export class RootStoreModel {
|
|||||||
profiles = new ProfilesViewModel(this)
|
profiles = new ProfilesViewModel(this)
|
||||||
linkMetas = new LinkMetasViewModel(this)
|
linkMetas = new LinkMetasViewModel(this)
|
||||||
|
|
||||||
constructor(public api: SessionServiceClient) {
|
constructor(api: SessionServiceClient) {
|
||||||
|
this.api = api // to keep typescript from whining
|
||||||
|
this.setAPI(api)
|
||||||
makeAutoObservable(this, {
|
makeAutoObservable(this, {
|
||||||
api: false,
|
api: false,
|
||||||
resolveName: false,
|
resolveName: false,
|
||||||
@@ -38,6 +44,24 @@ export class RootStoreModel {
|
|||||||
this.initBgFetch()
|
this.initBgFetch()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setAPI(api: SessionServiceClient) {
|
||||||
|
if (this.api) {
|
||||||
|
this.api.sessionManager.removeAllListeners('session')
|
||||||
|
}
|
||||||
|
this.api = api
|
||||||
|
this.api.sessionManager.on('session', this.onSessionChange.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
onSessionChange() {
|
||||||
|
if (!this.api.sessionManager.session && this.session.hasSession) {
|
||||||
|
this.log.debug('Session invalidated, logging the user out')
|
||||||
|
this.session.clear()
|
||||||
|
} else if (this.api.sessionManager.session) {
|
||||||
|
this.log.debug('Session refreshed, updating auth tokens')
|
||||||
|
this.session.updateAuthTokens(this.api.sessionManager.session)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async resolveName(didOrHandle: string) {
|
async resolveName(didOrHandle: string) {
|
||||||
if (!didOrHandle) {
|
if (!didOrHandle) {
|
||||||
throw new Error('Invalid handle: ""')
|
throw new Error('Invalid handle: ""')
|
||||||
@@ -147,7 +171,9 @@ export class RootStoreModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const throwawayInst = new RootStoreModel(AtpApi.service('http://localhost')) // this will be replaced by the loader, we just need to supply a value at init
|
const throwawayInst = new RootStoreModel(
|
||||||
|
SessionAtpApi.service('http://localhost'),
|
||||||
|
) // this will be replaced by the loader, we just need to supply a value at init
|
||||||
const RootStoreContext = createContext<RootStoreModel>(throwawayInst)
|
const RootStoreContext = createContext<RootStoreModel>(throwawayInst)
|
||||||
export const RootStoreProvider = RootStoreContext.Provider
|
export const RootStoreProvider = RootStoreContext.Provider
|
||||||
export const useStores = () => useContext(RootStoreContext)
|
export const useStores = () => useContext(RootStoreContext)
|
||||||
|
|||||||
+22
-40
@@ -1,8 +1,7 @@
|
|||||||
import {makeAutoObservable, runInAction} from 'mobx'
|
import {makeAutoObservable, runInAction} from 'mobx'
|
||||||
import {
|
import AtpApi, {
|
||||||
sessionClient as AtpApi,
|
sessionClient as SessionAtpApi,
|
||||||
Session,
|
Session,
|
||||||
SessionServiceClient,
|
|
||||||
ComAtprotoServerGetAccountsConfig as GetAccountsConfig,
|
ComAtprotoServerGetAccountsConfig as GetAccountsConfig,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {isObj, hasProp} from '../lib/type-guards'
|
import {isObj, hasProp} from '../lib/type-guards'
|
||||||
@@ -95,6 +94,7 @@ export class SessionModel {
|
|||||||
|
|
||||||
setState(data: SessionData) {
|
setState(data: SessionData) {
|
||||||
this.data = data
|
this.data = data
|
||||||
|
this.addSessionToAccounts()
|
||||||
}
|
}
|
||||||
|
|
||||||
setOnline(online: boolean, attemptingConnect?: boolean) {
|
setOnline(online: boolean, attemptingConnect?: boolean) {
|
||||||
@@ -124,7 +124,12 @@ export class SessionModel {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const serviceUri = new URL(this.data.service)
|
const serviceUri = new URL(this.data.service)
|
||||||
this.rootStore.api.xrpc.uri = serviceUri
|
const api = SessionAtpApi.service(serviceUri)
|
||||||
|
api.sessionManager.set({
|
||||||
|
refreshJwt: this.data.refreshJwt,
|
||||||
|
accessJwt: this.data.accessJwt,
|
||||||
|
})
|
||||||
|
this.rootStore.setAPI(api)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.rootStore.log.error(
|
this.rootStore.log.error(
|
||||||
`Invalid service URL: ${this.data.service}. Resetting session.`,
|
`Invalid service URL: ${this.data.service}. Resetting session.`,
|
||||||
@@ -133,11 +138,6 @@ export class SessionModel {
|
|||||||
this.clear()
|
this.clear()
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
this.rootStore.api.sessionManager.set({
|
|
||||||
refreshJwt: this.data.refreshJwt,
|
|
||||||
accessJwt: this.data.accessJwt,
|
|
||||||
})
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,17 +212,9 @@ export class SessionModel {
|
|||||||
if (this.rootStore.me.did !== sess.data.did) {
|
if (this.rootStore.me.did !== sess.data.did) {
|
||||||
this.rootStore.me.clear()
|
this.rootStore.me.clear()
|
||||||
}
|
}
|
||||||
this.rootStore.me
|
this.rootStore.me.load().then(() => {
|
||||||
.load()
|
this.addSessionToAccounts()
|
||||||
.catch(e => {
|
})
|
||||||
this.rootStore.log.error(
|
|
||||||
'Failed to fetch local user information',
|
|
||||||
e,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
this.addSessionToAccounts()
|
|
||||||
})
|
|
||||||
return true // success
|
return true // success
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@@ -242,7 +234,7 @@ export class SessionModel {
|
|||||||
* Helper to fetch the accounts config settings from an account.
|
* Helper to fetch the accounts config settings from an account.
|
||||||
*/
|
*/
|
||||||
async describeService(service: string): Promise<ServiceDescription> {
|
async describeService(service: string): Promise<ServiceDescription> {
|
||||||
const api = AtpApi.service(service) as SessionServiceClient
|
const api = AtpApi.service(service)
|
||||||
const res = await api.com.atproto.server.getAccountsConfig({})
|
const res = await api.com.atproto.server.getAccountsConfig({})
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
@@ -259,7 +251,7 @@ export class SessionModel {
|
|||||||
handle: string
|
handle: string
|
||||||
password: string
|
password: string
|
||||||
}) {
|
}) {
|
||||||
const api = AtpApi.service(service) as SessionServiceClient
|
const api = AtpApi.service(service)
|
||||||
const res = await api.com.atproto.session.create({handle, password})
|
const res = await api.com.atproto.session.create({handle, password})
|
||||||
if (res.data.accessJwt && res.data.refreshJwt) {
|
if (res.data.accessJwt && res.data.refreshJwt) {
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -271,14 +263,9 @@ export class SessionModel {
|
|||||||
})
|
})
|
||||||
this.configureApi()
|
this.configureApi()
|
||||||
this.setOnline(true, false)
|
this.setOnline(true, false)
|
||||||
this.rootStore.me
|
this.rootStore.me.load().then(() => {
|
||||||
.load()
|
this.addSessionToAccounts()
|
||||||
.catch(e => {
|
})
|
||||||
this.rootStore.log.error('Failed to fetch local user information', e)
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
this.addSessionToAccounts()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,7 +278,7 @@ export class SessionModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// test that the session is good
|
// test that the session is good
|
||||||
const api = AtpApi.service(account.service)
|
const api = SessionAtpApi.service(account.service)
|
||||||
api.sessionManager.set({
|
api.sessionManager.set({
|
||||||
refreshJwt: account.refreshJwt,
|
refreshJwt: account.refreshJwt,
|
||||||
accessJwt: account.accessJwt,
|
accessJwt: account.accessJwt,
|
||||||
@@ -339,7 +326,7 @@ export class SessionModel {
|
|||||||
handle: string
|
handle: string
|
||||||
inviteCode?: string
|
inviteCode?: string
|
||||||
}) {
|
}) {
|
||||||
const api = AtpApi.service(service) as SessionServiceClient
|
const api = AtpApi.service(service)
|
||||||
const res = await api.com.atproto.account.create({
|
const res = await api.com.atproto.account.create({
|
||||||
handle,
|
handle,
|
||||||
password,
|
password,
|
||||||
@@ -356,14 +343,9 @@ export class SessionModel {
|
|||||||
})
|
})
|
||||||
this.rootStore.onboard.start()
|
this.rootStore.onboard.start()
|
||||||
this.configureApi()
|
this.configureApi()
|
||||||
this.rootStore.me
|
this.rootStore.me.load().then(() => {
|
||||||
.load()
|
this.addSessionToAccounts()
|
||||||
.catch(e => {
|
})
|
||||||
this.rootStore.log.error('Failed to fetch local user information', e)
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
this.addSessionToAccounts()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
import * as EmailValidator from 'email-validator'
|
import * as EmailValidator from 'email-validator'
|
||||||
import {sessionClient as AtpApi, SessionServiceClient} from '@atproto/api'
|
import AtpApi from '@atproto/api'
|
||||||
import {useAnalytics} from '@segment/analytics-react-native'
|
import {useAnalytics} from '@segment/analytics-react-native'
|
||||||
import {LogoTextHero} from './Logo'
|
import {LogoTextHero} from './Logo'
|
||||||
import {Text} from '../util/text/Text'
|
import {Text} from '../util/text/Text'
|
||||||
@@ -471,7 +471,7 @@ const ForgotPasswordForm = ({
|
|||||||
setIsProcessing(true)
|
setIsProcessing(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const api = AtpApi.service(serviceUrl) as SessionServiceClient
|
const api = AtpApi.service(serviceUrl)
|
||||||
await api.com.atproto.account.requestPasswordReset({email})
|
await api.com.atproto.account.requestPasswordReset({email})
|
||||||
onEmailSent()
|
onEmailSent()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@@ -602,7 +602,7 @@ const SetNewPasswordForm = ({
|
|||||||
setIsProcessing(true)
|
setIsProcessing(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const api = AtpApi.service(serviceUrl) as SessionServiceClient
|
const api = AtpApi.service(serviceUrl)
|
||||||
await api.com.atproto.account.resetPassword({token: resetCode, password})
|
await api.com.atproto.account.resetPassword({token: resetCode, password})
|
||||||
onPasswordSet()
|
onPasswordSet()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export const Home = observer(function Home({
|
|||||||
store.me.mainFeed.setup()
|
store.me.mainFeed.setup()
|
||||||
}
|
}
|
||||||
return cleanup
|
return cleanup
|
||||||
}, [visible, store, navIdx, doPoll, wasVisible])
|
}, [visible, store, store.me.mainFeed, navIdx, doPoll, wasVisible])
|
||||||
|
|
||||||
const onPressCompose = (imagesOpen?: boolean) => {
|
const onPressCompose = (imagesOpen?: boolean) => {
|
||||||
store.shell.openComposer({imagesOpen})
|
store.shell.openComposer({imagesOpen})
|
||||||
|
|||||||
@@ -39,11 +39,13 @@ export const Settings = observer(function Settings({
|
|||||||
setIsSwitching(true)
|
setIsSwitching(true)
|
||||||
if (await store.session.resumeSession(acct)) {
|
if (await store.session.resumeSession(acct)) {
|
||||||
setIsSwitching(false)
|
setIsSwitching(false)
|
||||||
|
store.nav.tab.fixedTabReset()
|
||||||
Toast.show(`Signed in as ${acct.displayName || acct.handle}`)
|
Toast.show(`Signed in as ${acct.displayName || acct.handle}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setIsSwitching(false)
|
setIsSwitching(false)
|
||||||
Toast.show('Sorry! We need you to enter your password.')
|
Toast.show('Sorry! We need you to enter your password.')
|
||||||
|
store.nav.tab.fixedTabReset()
|
||||||
store.session.clear()
|
store.session.clear()
|
||||||
}
|
}
|
||||||
const onPressAddAccount = () => {
|
const onPressAddAccount = () => {
|
||||||
|
|||||||
@@ -12813,11 +12813,16 @@ tslib@^1.8.1:
|
|||||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
||||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||||
|
|
||||||
tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0:
|
tslib@^2.0.1, tslib@^2.0.3, tslib@^2.4.0:
|
||||||
version "2.4.1"
|
version "2.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e"
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e"
|
||||||
integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==
|
integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==
|
||||||
|
|
||||||
|
tslib@^2.1.0:
|
||||||
|
version "2.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf"
|
||||||
|
integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==
|
||||||
|
|
||||||
tsutils@^3.21.0:
|
tsutils@^3.21.0:
|
||||||
version "3.21.0"
|
version "3.21.0"
|
||||||
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
|
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
|
||||||
|
|||||||
Reference in New Issue
Block a user