Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement screens for authentications #5

Open
wants to merge 33 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
34b512e
define blank login and sign up screens
aruneko Feb 16, 2022
3c5b657
define new navigation props
aruneko Feb 16, 2022
5a37c10
define home screen
aruneko Feb 16, 2022
a9efb4b
define skelton onboarding screen
aruneko Feb 16, 2022
e1499d0
make stack for screens
aruneko Feb 16, 2022
408c571
store apikey as a recoil state
aruneko Feb 16, 2022
86e1f24
save reporter as a recoil state
aruneko Feb 16, 2022
510c664
put logout button temporary
aruneko Feb 16, 2022
30ec60a
echo tmp message
aruneko Feb 16, 2022
1028b70
add home screen
aruneko Feb 16, 2022
aac191f
implement login button
aruneko Feb 16, 2022
863c04d
install expo-app-loading
aruneko Feb 16, 2022
a545882
define init screen
aruneko Feb 16, 2022
1e3b840
pass authenticated state
aruneko Feb 16, 2022
3acac74
show init screen
aruneko Feb 16, 2022
c6f6144
remove unused screen
aruneko Feb 16, 2022
36dc678
remove api key in logout button
aruneko Feb 16, 2022
8583410
navigate screen in logout button
aruneko Feb 16, 2022
7f2b109
hide header in home screen
aruneko Feb 16, 2022
fa76dee
install rn-elements
aruneko Feb 16, 2022
920368f
support avoiding keyboard screen
aruneko Feb 16, 2022
6e01e14
define blank forget password screen
aruneko Feb 16, 2022
cd761c7
fix typo
aruneko Feb 16, 2022
003030c
use safe area view
aruneko Feb 16, 2022
cc7b145
inplement continue as guest button
aruneko Feb 16, 2022
82c27f6
fix tests
aruneko Feb 16, 2022
06da5e5
refactoring
aruneko Feb 17, 2022
f191bce
use async default value
aruneko Mar 16, 2022
216e35b
remove unused lines
aruneko Mar 16, 2022
fe54b1d
remove unused files
aruneko Mar 16, 2022
a0e246b
use selector
aruneko Mar 16, 2022
9ebf29b
refactoring
aruneko Mar 16, 2022
a9464a4
use suspense
aruneko Mar 16, 2022
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
5 changes: 4 additions & 1 deletion __tests__/reporter/models/reporter.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ApiKey } from '@libs/apiKey/models/apiKey'
import { NickName } from '@libs/reporter/models/nickname'
import { Reporter } from '@libs/reporter/models/reporter'
import { ReporterID } from '@libs/reporter/models/reporterID'
Expand All @@ -6,8 +7,10 @@ describe('Reporter model', () => {
it('valid case', () => {
const reporterID = ReporterID.of(1)
const nickName = NickName.of('example')
const reporter = new Reporter(reporterID, nickName)
const apiKey = ApiKey.of('8023caed8f0843f84aa69444898ad31c6c9ce599')
const reporter = new Reporter(reporterID, nickName, apiKey)
expect(reporter.id).toEqual(reporterID.value)
expect(reporter.nickname).toEqual(nickName.value)
expect(reporter.apiKey).toEqual(apiKey.value)
})
})
5 changes: 3 additions & 2 deletions __tests__/reporter/usecase/InMemoryReporterClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ export class InMemoryReporterClient implements ReporterClient {

const id = ReporterID.of(1)
const nickname = NickName.of(username)
const apikey = ApiKey.of('d012ded50105308c87f1b595f9c34b3ed3d078e1')

return new Reporter(id, nickname)
return new Reporter(id, nickname, apikey)
}

public async fetchWithApiKey(apiKey: ApiKey): Promise<Reporter> {
Expand All @@ -32,6 +33,6 @@ export class InMemoryReporterClient implements ReporterClient {
const id = ReporterID.of(1)
const nickname = NickName.of('example')

return new Reporter(id, nickname)
return new Reporter(id, nickname, apiKey)
}
}
1 change: 1 addition & 0 deletions __tests__/reporter/usecase/fetchReporterWithApiKey.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ describe('Reporter usecases', () => {

expect(fetchedReporter.id).toEqual(1)
expect(fetchedReporter.nickname).toEqual('example')
expect(fetchedReporter.apiKey).toEqual(apiKey.value)
})

it('invalid api key', () => {
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@
]
},
"dependencies": {
"@react-navigation/elements": "^1.3.1",
"@react-navigation/native": "^6.0.8",
"@react-navigation/native-stack": "^6.5.0",
"dotenv": "^16.0.0",
"expo": "~44.0.0",
"expo-app-loading": "~1.3.0",
"expo-secure-store": "~11.1.0",
"expo-status-bar": "~1.2.0",
"react": "17.0.1",
Expand Down
9 changes: 6 additions & 3 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import React from 'react'
import React, { Suspense } from 'react'
import AppLoading from 'expo-app-loading'
import { StatusBar } from 'expo-status-bar'
import { NavigationContainer } from '@react-navigation/native'
import { MainStackNavigation } from '@screens/mainStackNavigation'
import { RecoilRoot } from 'recoil'
import { InitScreen } from '@screens/initScreen'

export const App: React.VFC = () => {
return (
<RecoilRoot>
<NavigationContainer>
<MainStackNavigation />
<Suspense fallback={<AppLoading />}>
<InitScreen />
</Suspense>
<StatusBar style="auto" />
</NavigationContainer>
</RecoilRoot>
Expand Down
34 changes: 34 additions & 0 deletions src/libs/apiKey/infrastructure/react/atoms/apiKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { ApiKey } from '@libs/apiKey/models/apiKey'
import { atom, selector } from 'recoil'
import { ApiKeyObj } from '../object/apiKey'
import { getApiKeyInstance } from './apiKeyUseCases'

const apiKeyAtom = atom<ApiKeyObj | undefined>({
key: 'ApiKeyAtom',
default: undefined,
})

export const apiKeyState = selector<ApiKeyObj | undefined>({
key: 'ApiKey',
get: async ({ get }) => {
const storedApiKey = get(apiKeyAtom)
if (storedApiKey !== undefined) {
return storedApiKey
}
try {
const getApiKey = get(getApiKeyInstance)
const apiKeyDTO = await getApiKey()
const apiKey = ApiKey.of(apiKeyDTO.value)
return apiKey
} catch (e) {
if (e instanceof Error) {
// TODO: This message should sent into Sentry
console.log(`API key is not saved: ${e.message}`)
}
return undefined
}
},
set: ({ set }, newApiKey) => {
set(apiKeyAtom, newApiKey)
},
})
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { atom } from 'recoil'
import { ExpoApiKeyRepository } from '../expoApiKeyRepository'
import { ExpoApiKeyRepository } from '../../expoApiKeyRepository'

export const apiKeyRepository = atom({
key: 'apiKeyRepository',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@ import {
import { selector } from 'recoil'
import { apiKeyRepository } from './apiKeyRepository'

export const getApiKey = selector({
export const getApiKeyInstance = selector({
key: 'getApiKeyUseCase',
get: ({ get }) => getApiKeyUseCase(get(apiKeyRepository)),
})

export const saveApiKey = selector({
export const saveApiKeyInstance = selector({
key: 'saveApiKeyUseCase',
get: ({ get }) => saveApiKeyUseCase(get(apiKeyRepository)),
})

export const deleteApiKey = selector({
export const deleteApiKeyInstance = selector({
key: 'deleteApiKeyUseCase',
get: ({ get }) => deleteApiKeyUseCase(get(apiKeyRepository)),
})
3 changes: 3 additions & 0 deletions src/libs/apiKey/infrastructure/react/object/apiKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type ApiKeyObj = {
value: string
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { fetchReporterWithApiKeyUseCase } from '@libs/reporter/usecase/usecases'
import { selector } from 'recoil'
import { reporterClient } from './reporterClient'

export const fetchReporterWithApiKey = selector({
export const fetchReporterWithApiKeyInstance = selector({
key: 'FetchReporterWithApiKeyUseCase',
get: ({ get }) => fetchReporterWithApiKeyUseCase(get(reporterClient)),
})
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { fetchReporterWithPasswordUseCase } from '@libs/reporter/usecase/usecase
import { selector } from 'recoil'
import { reporterClient } from './reporterClient'

export const fetchReporterWithPassword = selector({
export const fetchReporterWithPasswordInstance = selector({
key: 'FetchReporterWithPasswordUseCase',
get: ({ get }) => fetchReporterWithPasswordUseCase(get(reporterClient)),
})
40 changes: 40 additions & 0 deletions src/libs/reporter/infrastructure/react/atoms/reporter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { apiKeyState } from '@libs/apiKey/infrastructure/react/atoms/apiKey'
import { ApiKey } from '@libs/apiKey/models/apiKey'
import { atom, selector } from 'recoil'
import { ReporterObj } from '../object/reporter'
import { fetchReporterWithApiKeyInstance } from './fetchReporterWithApiKeyUseCase'

const reporterAtom = atom<ReporterObj | undefined>({
key: 'ReporterAtom',
default: undefined,
})

export const reporterState = selector<ReporterObj | undefined>({
key: 'Reporter',
get: async ({ get }) => {
const storedReporter = get(reporterAtom)
if (storedReporter !== undefined) {
return storedReporter
}
const fetchReporterWithApiKey = get(fetchReporterWithApiKeyInstance)
const apiKeyObj = get(apiKeyState)
if (apiKeyObj !== undefined) {
try {
const apiKey = ApiKey.of(apiKeyObj.value)
const reporterDTO = await fetchReporterWithApiKey(apiKey)
return reporterDTO
} catch (e) {
if (e instanceof Error) {
// TODO: This message should sent into Sentry
console.log(`Failed to auth with stored api key: ${e.message}`)
}
return undefined
}
} else {
return undefined
}
},
set: ({ set }, newReporter) => {
set(reporterAtom, newReporter)
},
})
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { redmineBaseURL } from '@libs/redmine/infrastructure/atoms/redmineBaseURL'
import { selector } from 'recoil'
import { RedmineReporterClient } from '../redmineReporterClient'
import { RedmineReporterClient } from '../../redmineReporterClient'

export const reporterClient = selector({
key: 'ReporterClient',
Expand Down
4 changes: 4 additions & 0 deletions src/libs/reporter/infrastructure/react/object/reporter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type ReporterObj = {
id: number
nickname: string
}
3 changes: 2 additions & 1 deletion src/libs/reporter/infrastructure/redmineReporterClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ export class RedmineReporterClient implements ReporterClient {

const id = ReporterID.of(user.id)
const nickname = NickName.of(user.displayname)
const apiKey = ApiKey.of(user.api_key)

return new Reporter(id, nickname)
return new Reporter(id, nickname, apiKey)
}
}
8 changes: 7 additions & 1 deletion src/libs/reporter/models/reporter.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { ApiKey } from '@libs/apiKey/models/apiKey'
import { NickName } from './nickname'
import { ReporterID } from './reporterID'

export class Reporter {
constructor(
private readonly _id: ReporterID,
private readonly _nickname: NickName
private readonly _nickname: NickName,
private readonly _apiKey: ApiKey
) {}

public get id(): number {
Expand All @@ -14,4 +16,8 @@ export class Reporter {
public get nickname(): string {
return this._nickname.value
}

public get apiKey(): string {
return this._apiKey.value
}
}
6 changes: 4 additions & 2 deletions src/libs/reporter/usecase/reporterDTO.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ export class ReporterDTO {
public static fromReporter(reporter: Reporter): ReporterDTO {
const id = reporter.id
const nickname = reporter.nickname
const apiKey = reporter.apiKey

return new ReporterDTO(id, nickname)
return new ReporterDTO(id, nickname, apiKey)
}

private constructor(
public readonly id: number,
public readonly nickname: string
public readonly nickname: string,
public readonly apiKey: string
) {}
}
10 changes: 10 additions & 0 deletions src/screens/forgetPasswordScreen/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from 'react'
import { Text, View } from 'react-native'

export const ForgetPasswordScreen: React.VFC = () => {
return (
<View>
<Text>Forget Password</Text>
</View>
)
}
33 changes: 33 additions & 0 deletions src/screens/homeScreen/components/logoutButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { apiKeyState } from '@libs/apiKey/infrastructure/react/atoms/apiKey'
import { deleteApiKeyInstance } from '@libs/apiKey/infrastructure/react/atoms/apiKeyUseCases'
import { reporterState } from '@libs/reporter/infrastructure/react/atoms/reporter'
import { CommonActions, useNavigation } from '@react-navigation/native'
import { OnboardingScreenNavigationProp } from '@screens/mainStackNavigation/navigationProps'
import React, { useCallback } from 'react'
import { Button } from 'react-native'
import { useRecoilValue, useSetRecoilState } from 'recoil'

export const LogoutButton: React.VFC = () => {
const deleteApiKey = useRecoilValue(deleteApiKeyInstance)
const setReporter = useSetRecoilState(reporterState)
const setApiKey = useSetRecoilState(apiKeyState)

const logout = useCallback(async () => {
await deleteApiKey()
setReporter(undefined)
setApiKey(undefined)
}, [deleteApiKey, setApiKey, setReporter])

const navigation = useNavigation<OnboardingScreenNavigationProp>()

const onPress = useCallback(async () => {
await logout()
const resetAction = CommonActions.reset({
index: 1,
routes: [{ name: 'OnboardingScreen' }],
})
navigation.dispatch(resetAction)
}, [logout, navigation])

return <Button title="Logout" onPress={onPress} />
}
12 changes: 12 additions & 0 deletions src/screens/homeScreen/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from 'react'
import { SafeAreaView, Text } from 'react-native'
import { LogoutButton } from './components/logoutButton'

export const HomeScreen: React.VFC = () => {
return (
<SafeAreaView>
<Text>Home Screen</Text>
<LogoutButton />
</SafeAreaView>
)
}
15 changes: 15 additions & 0 deletions src/screens/initScreen/hooks/useIsAuthenticated.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { apiKeyState } from '@libs/apiKey/infrastructure/react/atoms/apiKey'
import { reporterState } from '@libs/reporter/infrastructure/react/atoms/reporter'
import { selector } from 'recoil'

export const isAuthenticated = selector<boolean>({
key: 'IsAuthenticated',
get: ({ get }) => {
const apiKey = get(apiKeyState)
const reporter = get(reporterState)

const hasApiKey = apiKey !== undefined
const hasReporter = reporter !== undefined
return hasApiKey && hasReporter
},
})
10 changes: 10 additions & 0 deletions src/screens/initScreen/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { MainStackNavigation } from '@screens/mainStackNavigation'
import React from 'react'
import { isAuthenticated } from './hooks/useIsAuthenticated'
import { useRecoilValue } from 'recoil'

export const InitScreen: React.VFC = () => {
const authenticated = useRecoilValue(isAuthenticated)

return <MainStackNavigation isAuthenticated={authenticated} />
}
14 changes: 14 additions & 0 deletions src/screens/loginScreen/components/forgetPasswordButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useNavigation } from '@react-navigation/native'
import { ForgetPasswordScreenNavigationProp } from '@screens/mainStackNavigation/navigationProps'
import React, { useCallback } from 'react'
import { Button } from 'react-native'

export const ForgetPasswordButton: React.VFC = () => {
const navigation = useNavigation<ForgetPasswordScreenNavigationProp>()

const onPress = useCallback(() => {
navigation.navigate('ForgetPasswordScreen')
}, [navigation])

return <Button title="Forget password" onPress={onPress} />
}
Loading