Skip to content
This repository was archived by the owner on Feb 9, 2021. It is now read-only.

feat: 🎸 Context based useApi hook #15

Merged
merged 1 commit into from
Feb 4, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
@@ -12,10 +12,11 @@ module.exports = {
extends: [
'airbnb-typescript/base',
],
ignorePatterns: ['*.test.{js,ts}'],
ignorePatterns: ['*.test.{js,ts,jsx,tsx}'],
rules: {
'no-param-reassign': 0,
'@typescript-eslint/semi': [2, 'never'],
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'max-len': ['error', { code: 120, 'ignoreUrls': true }],
'import/prefer-default-export': 0,
},
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -12,6 +12,22 @@

```yarn add @skolplattformen/react-native-embedded-api```

## ApiProvider

In order to use api hooks, you must wrap your app in an ApiProvider

```javascript
import React from 'react'
import { ApiProvider } from '@skolplattformen/react-native-embedded-api'
import { RootComponent } from './components/root

export default () => (
<ApiProvider>
<RootComponent />
</ApiProvider>
)
```

## Login / logout

```javascript
1 change: 1 addition & 0 deletions babel.config.js
Original file line number Diff line number Diff line change
@@ -3,5 +3,6 @@ module.exports = {
presets: [
['@babel/preset-env', { targets: { node: 'current' } }],
'@babel/preset-typescript',
'@babel/preset-react',
],
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -22,7 +22,7 @@
"@react-native-async-storage/async-storage": "^1.13.3",
"@react-native-community/cookies": "^5.0.1",
"@reduxjs/toolkit": "^1.5.0",
"@skolplattformen/embedded-api": "^0.18.0",
"@skolplattformen/embedded-api": "^0.20.0",
"luxon": "^1.25.0",
"react": "^16.11.0",
"react-native": "^0.62.2",
@@ -31,6 +31,7 @@
"devDependencies": {
"@babel/core": "^7.12.10",
"@babel/preset-env": "^7.12.11",
"@babel/preset-react": "^7.12.13",
"@babel/preset-typescript": "^7.12.7",
"@testing-library/react": "^11.2.3",
"@testing-library/react-hooks": "^5.0.3",
73 changes: 73 additions & 0 deletions src/context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import React, { createContext, FC, PropsWithChildren, useContext, useEffect, useState } from 'react'
import { LoginStatusChecker } from '@skolplattformen/embedded-api'
import api from './api'

type LoginEvent = 'login' | 'logout'
interface ApiEventEmitter {
on: (event: LoginEvent, listener: () => void) => ApiEventEmitter,
off: (event: LoginEvent, listener: () => void) => ApiEventEmitter,
}

export interface ApiContext extends ApiEventEmitter {
isLoggedIn: boolean,
isFake: boolean,
cookie?: string | null,
login: (personalNumber: string) => Promise<LoginStatusChecker>,
logout: () => Promise<void>,
}

const Context = createContext<ApiContext>({
isLoggedIn: false,
isFake: false,
login: (personalNumber: string) => api.login(personalNumber),
logout: () => api.logout(),
on: (event, listener) => api.on(event, listener),
off: (event, listener) => api.off(event, listener),
})

export const ApiProvider: FC<PropsWithChildren<{}>> = ({ children }) => {
const [isLoggedIn, setIsLoggedIn] = useState(api.isLoggedIn)
const [isFake, setIsFake] = useState(api.isFake)
const [cookie, setCookie] = useState(api.getSessionCookie())

let mounted = false

const sessionListener = () => {
if (!mounted) return
setIsLoggedIn(api.isLoggedIn)
setIsFake(api.isFake)
if (!api.isFake) {
setCookie(api.isLoggedIn ? api.getSessionCookie() : undefined)
}
}

useEffect(() => {
mounted = true
api.on('login', sessionListener)
api.on('logout', sessionListener)

return () => {
mounted = false
api.off('login', sessionListener)
api.off('logout', sessionListener)
}
}, [])

const value: ApiContext = {
isLoggedIn,
isFake,
cookie,
login: (personalNumber: string) => api.login(personalNumber),
logout: () => api.logout(),
on: (event, listener) => api.on(event, listener),
off: (event, listener) => api.off(event, listener),
}

return (
<Context.Provider value={value}>
{children}
</Context.Provider>
)
}

export const useApi = () => useContext(Context)
38 changes: 0 additions & 38 deletions src/hooks.ts
Original file line number Diff line number Diff line change
@@ -98,44 +98,6 @@ const createEntityHook = <T, A extends any[]>(entity: string, apiCall: ApiCall<T
)

// Hooks
export const useApi = () => {
let mounted = false
const [isLoggedIn, setIsLoggedIn] = useState(api.isLoggedIn)
const [isFake, setIsFake] = useState(api.isFake)
const [cookie, setCookie] = useState(api.getSessionCookie())

const sessionListener = () => {
if (!mounted) return
setIsLoggedIn(api.isLoggedIn)
setIsFake(api.isFake)
if (!api.isFake) {
setCookie(api.isLoggedIn ? api.getSessionCookie() : undefined)
}
}

useEffect(() => {
mounted = true
api.on('login', sessionListener)
api.on('logout', sessionListener)

return () => {
mounted = false
api.off('login', sessionListener)
api.off('logout', sessionListener)
}
}, [])

return {
isLoggedIn,
isFake,
cookie,
login: (personalNumber: string) => api.login(personalNumber),
logout: () => api.logout(),
on: (event: 'login' | 'logout', listener: () => any) => api.on(event, listener),
once: (event: 'login' | 'logout', listener: () => any) => api.once(event, listener),
off: (event: 'login' | 'logout', listener: () => any) => api.off(event, listener),
}
}
export const useCalendar = createEntityHook<CalendarItem[], [Child]>(
'calendar', (child) => api.getCalendar(child), (child) => child.id, [],
)
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -2,3 +2,4 @@ export * from '@skolplattformen/embedded-api/dist/types'
export * from './types'
export { default as api } from './api'
export * from './hooks'
export * from './context'
30 changes: 16 additions & 14 deletions src/useApi.test.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import React from 'react'
import { renderHook, act } from '@testing-library/react-hooks'
import api from './api'
import { useApi } from './hooks'
import { useApi, ApiProvider } from './context'

describe('useApi', () => {
let status
let emitter
const wrapper = ({ children }) => <ApiProvider>{children}</ApiProvider>
beforeEach(() => {
emitter = api.emitter
status = {}
@@ -18,7 +20,7 @@ describe('useApi', () => {
describe('#login', () => {
it('calls through to login', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook(() => useApi())
const { result, waitForNextUpdate } = renderHook(() => useApi(), { wrapper })
await waitForNextUpdate()
const { login } = result.current
login('pnr')
@@ -27,7 +29,7 @@ describe('useApi', () => {
})
})
it('returns the status checker', async () => {
const { result } = renderHook(() => useApi())
const { result } = renderHook(() => useApi(), { wrapper })
const { login } = result.current
const loginStatus = await login('pnr')

@@ -37,21 +39,21 @@ describe('useApi', () => {
const error = new Error()
api.login.mockRejectedValue(error)

const { result } = renderHook(() => useApi())
const { result } = renderHook(() => useApi(), { wrapper })
const { login } = result.current
await expect(login()).rejects.toThrow(error)
})
})
describe('.isLoggedIn', () => {
it('defaults to false if api.isLoggedIn = false', () => {
const { result } = renderHook(() => useApi())
const { result } = renderHook(() => useApi(), { wrapper })
const { isLoggedIn } = result.current

expect(isLoggedIn).toEqual(false)
})
it('defaults to true if api.isLoggedIn = true', () => {
api.isLoggedIn = true
const { result } = renderHook(() => useApi())
const { result } = renderHook(() => useApi(), { wrapper })
const { isLoggedIn } = result.current

expect(isLoggedIn).toEqual(true)
@@ -60,7 +62,7 @@ describe('useApi', () => {
it.skip('changes to true on(`login`)', async () => {
await act(async () => {
api.isLoggedIn = false
const { result, waitForNextUpdate } = renderHook(() => useApi())
const { result, waitForNextUpdate } = renderHook(() => useApi(), { wrapper })

await waitForNextUpdate()
expect(result.current.isLoggedIn).toEqual(false)
@@ -75,7 +77,7 @@ describe('useApi', () => {
it.skip('changes to false on(`logout`)', async () => {
await act(async () => {
api.isLoggedIn = true
const { result, waitForNextUpdate } = renderHook(() => useApi())
const { result, waitForNextUpdate } = renderHook(() => useApi(), { wrapper })

await waitForNextUpdate()
expect(result.current.isLoggedIn).toEqual(true)
@@ -89,21 +91,21 @@ describe('useApi', () => {
})
describe('.cookie', () => {
it('defaults to undefined', () => {
const { result } = renderHook(() => useApi())
const { result } = renderHook(() => useApi(), { wrapper })
const { cookie } = result.current

expect(cookie).toEqual(undefined)
})
it('defaults to cookie if session cookie exists', () => {
api.getSessionCookie.mockReturnValue('cookie')

const { result } = renderHook(() => useApi())
const { result } = renderHook(() => useApi(), { wrapper })
const { cookie } = result.current

expect(cookie).toEqual('cookie')
})
it.skip('updates value on(`login`)', async () => {
const { result, waitForNextUpdate } = renderHook(() => useApi())
const { result, waitForNextUpdate } = renderHook(() => useApi(), { wrapper })

expect(result.current.cookie).toEqual(undefined)

@@ -118,7 +120,7 @@ describe('useApi', () => {
it.skip('changes to false on(`logout`)', async () => {
api.isLoggedIn = true
api.getSessionCookie.mockReturnValue('cookie')
const { result, waitForNextUpdate } = renderHook(() => useApi())
const { result, waitForNextUpdate } = renderHook(() => useApi(), { wrapper })

expect(result.current.cookie).toEqual('cookie')

@@ -133,7 +135,7 @@ describe('useApi', () => {
})
describe('#logout', () => {
it('calls through to logout', () => {
const { result } = renderHook(() => useApi())
const { result } = renderHook(() => useApi(), { wrapper })
const { logout } = result.current
logout()

@@ -143,7 +145,7 @@ describe('useApi', () => {
const error = new Error()
api.logout.mockRejectedValue(error)

const { result } = renderHook(() => useApi())
const { result } = renderHook(() => useApi(), { wrapper })
const { logout } = result.current
await expect(logout()).rejects.toThrow(error)
})
18 changes: 10 additions & 8 deletions src/useApi_fake.test.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
import React from 'react'
import AsyncStorage from '@react-native-async-storage/async-storage'
import { act, renderHook } from '@testing-library/react-hooks'
import api from './api'
import { useApi, useChildList, useUser } from './hooks'
import { clearStore } from './store'
import { useChildList, useUser } from './hooks'
import { ApiProvider, useApi } from './context'

jest.mock('@skolplattformen/embedded-api',
() => jest.requireActual('@skolplattformen/embedded-api'))

describe('useApi - fake mode', () => {
const wrapper = ({ children }) => <ApiProvider>{children}</ApiProvider>
afterEach(async () => {
api.isLoggedIn = false
api.isFake = false
})
const pnr = '121212121212'
it('status.token is "fake"', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook(() => useApi())
const { result, waitForNextUpdate } = renderHook(() => useApi(), { wrapper })
await waitForNextUpdate()

const { login } = result.current
@@ -26,7 +28,7 @@ describe('useApi - fake mode', () => {
})
it('sets isLoggedIn to true', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook(() => useApi())
const { result, waitForNextUpdate } = renderHook(() => useApi(), { wrapper })
await waitForNextUpdate()

const { login } = result.current
@@ -38,7 +40,7 @@ describe('useApi - fake mode', () => {
})
it('sets isFake to true', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook(() => useApi())
const { result, waitForNextUpdate } = renderHook(() => useApi(), { wrapper })
await waitForNextUpdate()
expect(result.current.isFake).toEqual(false)

@@ -51,7 +53,7 @@ describe('useApi - fake mode', () => {
})
it('returns fake data', async () => {
await act(async () => {
const { result: apiResult, waitForNextUpdate: waitApi } = renderHook(() => useApi())
const { result: apiResult, waitForNextUpdate: waitApi } = renderHook(() => useApi(), { wrapper })
await waitApi()

const { login } = apiResult.current
@@ -71,7 +73,7 @@ describe('useApi - fake mode', () => {
await act(async () => {
const spy = jest.spyOn(AsyncStorage, 'getItem')

const { result: apiResult, waitForNextUpdate: waitApi } = renderHook(() => useApi())
const { result: apiResult, waitForNextUpdate: waitApi } = renderHook(() => useApi(), { wrapper })
await waitApi()

const { login } = apiResult.current
@@ -88,7 +90,7 @@ describe('useApi - fake mode', () => {
await act(async () => {
const spy = jest.spyOn(AsyncStorage, 'setItem')

const { result: apiResult, waitForNextUpdate: waitApi } = renderHook(() => useApi())
const { result: apiResult, waitForNextUpdate: waitApi } = renderHook(() => useApi(), { wrapper })
await waitApi()

const { login } = apiResult.current
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -6,7 +6,8 @@
"outDir": "./dist",
"strict": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true
"allowSyntheticDefaultImports": true,
"jsx": "react-native"
},
"include": [
"src"
Loading