-
-
Notifications
You must be signed in to change notification settings - Fork 50
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
…ue-sdk feat(vue): create vue sdk
- Loading branch information
Showing
10 changed files
with
695 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import type { Config } from '@jest/types'; | ||
|
||
const config: Config.InitialOptions = { | ||
preset: 'ts-jest', | ||
collectCoverageFrom: ['src/**/*.ts'], | ||
coverageReporters: ['lcov', 'text-summary'], | ||
testEnvironment: 'jsdom', | ||
}; | ||
|
||
export default config; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
{ | ||
"name": "@logto/vue", | ||
"version": "0.1.7", | ||
"main": "./lib/index.js", | ||
"exports": "./lib/index.js", | ||
"typings": "./lib/index.d.ts", | ||
"files": [ | ||
"lib" | ||
], | ||
"license": "MIT", | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/logto-io/js.git", | ||
"directory": "packages/vue" | ||
}, | ||
"scripts": { | ||
"dev:tsc": "tsc -p tsconfig.build.json -w --preserveWatchOutput", | ||
"preinstall": "npx only-allow pnpm", | ||
"precommit": "lint-staged", | ||
"build": "rm -rf lib/ && tsc -p tsconfig.build.json", | ||
"lint": "eslint --ext .ts src", | ||
"test": "jest", | ||
"test:coverage": "jest --silent --coverage", | ||
"prepack": "pnpm test" | ||
}, | ||
"dependencies": { | ||
"@logto/browser": "^0.1.7" | ||
}, | ||
"devDependencies": { | ||
"@jest/types": "^27.5.1", | ||
"@silverhand/eslint-config": "^0.14.0", | ||
"@silverhand/ts-config": "^0.14.0", | ||
"@types/jest": "^27.4.1", | ||
"eslint": "^8.9.0", | ||
"jest": "^27.5.1", | ||
"lint-staged": "^12.3.4", | ||
"postcss": "^8.4.6", | ||
"prettier": "^2.5.1", | ||
"stylelint": "^14.8.2", | ||
"ts-jest": "^27.0.4", | ||
"typescript": "^4.6.2", | ||
"vue": "^3.2.35" | ||
}, | ||
"peerDependencies": { | ||
"vue": ">=3.0.0" | ||
}, | ||
"eslintConfig": { | ||
"extends": "@silverhand", | ||
"rules": { | ||
"unicorn/prevent-abbreviations": ["error", { "replacements": { "ref": false }}] | ||
} | ||
}, | ||
"prettier": "@silverhand/eslint-config/.prettierrc", | ||
"publishConfig": { | ||
"access": "public" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export const logtoInjectionKey = '@logto/vue'; | ||
export const contextInjectionKey = '@logto/vue:context'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import LogtoClient from '@logto/browser'; | ||
import { computed, ComputedRef, reactive, Ref, toRefs, UnwrapRef } from 'vue'; | ||
|
||
type LogtoContextProperties = { | ||
logtoClient: LogtoClient | undefined; | ||
isAuthenticated: boolean; | ||
loadingCount: number; | ||
error: Error | undefined; | ||
}; | ||
|
||
export type Context = { | ||
// Wrong type workaround. https://github.com/vuejs/core/issues/2981 | ||
logtoClient: Ref<UnwrapRef<LogtoClient | undefined>>; | ||
isAuthenticated: Ref<boolean>; | ||
loadingCount: Ref<number>; | ||
error: Ref<Error | undefined>; | ||
isLoading: ComputedRef<boolean>; | ||
setError: (error: unknown, fallbackErrorMessage?: string | undefined) => void; | ||
setIsAuthenticated: (isAuthenticated: boolean) => void; | ||
setLoading: (isLoading: boolean) => void; | ||
}; | ||
|
||
export const createContext = (client: LogtoClient): Context => { | ||
const context = toRefs( | ||
reactive<LogtoContextProperties>({ | ||
logtoClient: client, | ||
isAuthenticated: client.isAuthenticated, | ||
loadingCount: 0, | ||
error: undefined, | ||
}) | ||
); | ||
|
||
const { isAuthenticated, loadingCount, error } = context; | ||
|
||
const isLoading = computed(() => loadingCount.value > 0); | ||
|
||
/* eslint-disable @silverhand/fp/no-mutation */ | ||
const setError = (_error: unknown, fallbackErrorMessage?: string) => { | ||
if (_error instanceof Error) { | ||
error.value = _error; | ||
} else if (fallbackErrorMessage) { | ||
error.value = new Error(fallbackErrorMessage); | ||
} | ||
}; | ||
|
||
const setLoading = (isLoading: boolean) => { | ||
if (isLoading) { | ||
loadingCount.value += 1; | ||
} else { | ||
loadingCount.value = Math.max(0, loadingCount.value - 1); | ||
} | ||
}; | ||
|
||
const setIsAuthenticated = (_isAuthenticated: boolean) => { | ||
isAuthenticated.value = _isAuthenticated; | ||
}; | ||
/* eslint-enable @silverhand/fp/no-mutation */ | ||
|
||
return { ...context, isLoading, setError, setLoading, setIsAuthenticated }; | ||
}; | ||
|
||
export const throwContextError = (): never => { | ||
throw new Error('Must install Logto plugin first.'); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
import LogtoClient from '@logto/browser'; | ||
import { App, readonly } from 'vue'; | ||
|
||
import { useLogto, useHandleSignInCallback, createLogto } from '.'; | ||
import { contextInjectionKey, logtoInjectionKey } from './consts'; | ||
import { createContext } from './context'; | ||
import { createPluginMethods } from './plugin'; | ||
|
||
const isSignInRedirected = jest.fn(() => false); | ||
const handleSignInCallback = jest.fn(async () => Promise.resolve()); | ||
const getAccessToken = jest.fn(() => { | ||
throw new Error('not authenticated'); | ||
}); | ||
const injectMock = jest.fn<any, string[]>((): any => { | ||
return undefined; | ||
}); | ||
|
||
jest.mock('@logto/browser', () => { | ||
return jest.fn().mockImplementation(() => { | ||
return { | ||
isAuthenticated: false, | ||
isSignInRedirected, | ||
handleSignInCallback, | ||
getAccessToken, | ||
signIn: jest.fn(async () => Promise.resolve()), | ||
signOut: jest.fn(async () => Promise.resolve()), | ||
}; | ||
}); | ||
}); | ||
|
||
jest.mock('vue', () => { | ||
return { | ||
...jest.requireActual('vue'), | ||
inject: (key: string) => { | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return | ||
return injectMock(key); | ||
}, | ||
}; | ||
}); | ||
|
||
const appId = 'foo'; | ||
const endpoint = 'https://endpoint.com'; | ||
|
||
const appMock = { | ||
provide: jest.fn(), | ||
} as any as App; | ||
|
||
describe('createLogto.install', () => { | ||
test('should call LogtoClient constructor and provide Logto context data', async () => { | ||
createLogto.install(appMock, { appId, endpoint }); | ||
|
||
expect(LogtoClient).toHaveBeenCalledWith({ endpoint, appId }); | ||
expect(appMock.provide).toBeCalled(); | ||
}); | ||
}); | ||
|
||
describe('Logto plugin not installed', () => { | ||
test('should throw error if calling `useLogto` before install', () => { | ||
expect(() => { | ||
useLogto(); | ||
}).toThrowError('Must install Logto plugin first.'); | ||
}); | ||
|
||
test('should throw error if calling `useHandleSignInCallback` before install', () => { | ||
expect(() => { | ||
useHandleSignInCallback(); | ||
}).toThrowError('Must install Logto plugin first.'); | ||
}); | ||
}); | ||
|
||
describe('useLogto', () => { | ||
beforeEach(() => { | ||
const client = new LogtoClient({ appId, endpoint }); | ||
const context = createContext(client); | ||
const { isAuthenticated, isLoading, error } = context; | ||
|
||
injectMock.mockImplementationOnce(() => { | ||
return { | ||
isAuthenticated: readonly(isAuthenticated), | ||
isLoading: readonly(isLoading), | ||
error: readonly(error), | ||
...createPluginMethods(context), | ||
}; | ||
}); | ||
}); | ||
|
||
test('should inject Logto context data', () => { | ||
const { | ||
isAuthenticated, | ||
isLoading, | ||
error, | ||
signIn, | ||
signOut, | ||
getAccessToken, | ||
getIdTokenClaims, | ||
fetchUserInfo, | ||
} = useLogto(); | ||
|
||
expect(isAuthenticated.value).toBe(false); | ||
expect(isLoading.value).toBe(false); | ||
expect(error?.value).toBeUndefined(); | ||
expect(signIn).toBeInstanceOf(Function); | ||
expect(signOut).toBeInstanceOf(Function); | ||
expect(getAccessToken).toBeInstanceOf(Function); | ||
expect(getIdTokenClaims).toBeInstanceOf(Function); | ||
expect(fetchUserInfo).toBeInstanceOf(Function); | ||
}); | ||
|
||
test('should return error when getAccessToken fails', async () => { | ||
const client = new LogtoClient({ appId, endpoint }); | ||
const context = createContext(client); | ||
const { getAccessToken } = createPluginMethods(context); | ||
const { error } = context; | ||
|
||
await getAccessToken(); | ||
expect(error.value).not.toBeUndefined(); | ||
expect(error.value?.message).toBe('not authenticated'); | ||
}); | ||
}); | ||
|
||
describe('useHandleSignInCallback', () => { | ||
beforeEach(() => { | ||
const client = new LogtoClient({ appId, endpoint }); | ||
const context = createContext(client); | ||
|
||
injectMock.mockImplementation((key: string) => { | ||
if (key === contextInjectionKey) { | ||
return context; | ||
} | ||
|
||
if (key === logtoInjectionKey) { | ||
const { isAuthenticated, isLoading, error } = context; | ||
|
||
return { | ||
isAuthenticated: readonly(isAuthenticated), | ||
isLoading: readonly(isLoading), | ||
error: readonly(error), | ||
...createPluginMethods(context), | ||
}; | ||
} | ||
}); | ||
}); | ||
|
||
test('not in callback url should not call `handleSignInCallback`', async () => { | ||
const { signIn } = useLogto(); | ||
useHandleSignInCallback(); | ||
|
||
await signIn('https://example.com'); | ||
expect(handleSignInCallback).not.toHaveBeenCalled(); | ||
}); | ||
|
||
test('in callback url should call `handleSignInCallback`', async () => { | ||
isSignInRedirected.mockImplementationOnce(() => true); | ||
const { signIn } = useLogto(); | ||
useHandleSignInCallback(); | ||
|
||
await signIn('https://example.com'); | ||
|
||
expect(handleSignInCallback).toHaveBeenCalledTimes(1); | ||
}); | ||
}); |
Oops, something went wrong.