diff --git a/dictation_client/src/AppRouter.tsx b/dictation_client/src/AppRouter.tsx index 8ac2ccc..7abc3ca 100644 --- a/dictation_client/src/AppRouter.tsx +++ b/dictation_client/src/AppRouter.tsx @@ -21,7 +21,7 @@ import WorkflowPage from "pages/WorkflowPage"; import TypistGroupSettingPage from "pages/TypistGroupSettingPage"; import WorktypeIdSettingPage from "pages/WorkTypeIdSettingPage"; import AccountPage from "pages/AccountPage"; -import AcceptToUsePage from "pages/AcceptToUsePage"; +import AcceptToUsePage from "pages/TermsPage"; import { TemplateFilePage } from "pages/TemplateFilePage"; import { AccountDeleteSuccess } from "pages/AccountPage/accountDeleteSuccess"; @@ -35,7 +35,7 @@ const AppRouter: React.FC = () => ( path="/signup" element={} /> - } /> + } /> } /> } /> } /> diff --git a/dictation_client/src/app/store.ts b/dictation_client/src/app/store.ts index e48a4ba..fe711ab 100644 --- a/dictation_client/src/app/store.ts +++ b/dictation_client/src/app/store.ts @@ -18,6 +18,7 @@ import worktype from "features/workflow/worktype/worktypeSlice"; import account from "features/account/accountSlice"; import template from "features/workflow/template/templateSlice"; import workflow from "features/workflow/workflowSlice"; +import terms from "features/terms/termsSlice"; export const store = configureStore({ reducer: { @@ -40,6 +41,7 @@ export const store = configureStore({ account, template, workflow, + terms, }, }); diff --git a/dictation_client/src/common/token.ts b/dictation_client/src/common/token.ts index 587a0ea..c41442f 100644 --- a/dictation_client/src/common/token.ts +++ b/dictation_client/src/common/token.ts @@ -62,3 +62,16 @@ export const isIdToken = (arg: any): arg is IdToken => { return true; }; + +export const getIdTokenFromLocalStorage = ( + localStorageKeyforIdToken: string +): string | null => { + const idTokenString = localStorage.getItem(localStorageKeyforIdToken); + if (idTokenString) { + const idTokenObject = JSON.parse(idTokenString); + if (isIdToken(idTokenObject)) { + return idTokenObject.secret; + } + } + return null; +}; diff --git a/dictation_client/src/features/terms/constants.ts b/dictation_client/src/features/terms/constants.ts new file mode 100644 index 0000000..dd78e43 --- /dev/null +++ b/dictation_client/src/features/terms/constants.ts @@ -0,0 +1,8 @@ +/** + * 利用規約の種類 + * @const {string[]} + */ +export const TERMS_DOCUMENT_TYPE = { + DPA: "DPA", + EULA: "EULA", +} as const; diff --git a/dictation_client/src/features/terms/index.ts b/dictation_client/src/features/terms/index.ts new file mode 100644 index 0000000..8692ec6 --- /dev/null +++ b/dictation_client/src/features/terms/index.ts @@ -0,0 +1,4 @@ +export * from "./termsSlice"; +export * from "./state"; +export * from "./operations"; +export * from "./selectors"; diff --git a/dictation_client/src/features/terms/operations.ts b/dictation_client/src/features/terms/operations.ts new file mode 100644 index 0000000..511dfe9 --- /dev/null +++ b/dictation_client/src/features/terms/operations.ts @@ -0,0 +1,158 @@ +import { createAsyncThunk } from "@reduxjs/toolkit"; +import type { RootState } from "app/store"; +import { ErrorObject, createErrorObject } from "common/errors"; +import { getTranslationID } from "translation"; +import { openSnackbar } from "features/ui/uiSlice"; +import { getIdTokenFromLocalStorage } from "common/token"; +import { TIERS } from "components/auth/constants"; +import { + UsersApi, + GetAccountInfoMinimalAccessResponse, + AccountsApi, + TermsApi, + GetTermsInfoResponse, +} from "../../api/api"; +import { Configuration } from "../../api/configuration"; + +export const getAccountInfoMinimalAccessAsync = createAsyncThunk< + GetAccountInfoMinimalAccessResponse, + { + localStorageKeyforIdToken: string; + }, + { + // rejectした時の返却値の型 + rejectValue: { + error: ErrorObject; + }; + } +>("accept/getAccountInfoMinimalAccessAsync", async (args, thunkApi) => { + const { localStorageKeyforIdToken } = args; + // apiのConfigurationを取得する + const { getState } = thunkApi; + const state = getState() as RootState; + const { configuration, accessToken } = state.auth; + const config = new Configuration(configuration); + const accountApi = new AccountsApi(config); + + try { + // IDトークンの取得 + const idToken = getIdTokenFromLocalStorage(localStorageKeyforIdToken); + + // IDトークンが取得できない場合エラーとする + if (!idToken) { + throw new Error("Unable to retrieve the ID token."); + } + const res = await accountApi.getAccountInfoMinimalAccess( + { idToken }, + { + headers: { authorization: `Bearer ${accessToken}` }, + } + ); + return res.data; + } catch (e) { + const error = createErrorObject(e); + thunkApi.dispatch( + openSnackbar({ + level: "error", + message: getTranslationID("common.message.internalServerError"), + }) + ); + return thunkApi.rejectWithValue({ error }); + } +}); + +export const getTermsInfoAsync = createAsyncThunk< + GetTermsInfoResponse, + void, + { + // rejectした時の返却値の型 + rejectValue: { + error: ErrorObject; + }; + } +>("accept/getTermsInfoAsync", async (_args, thunkApi) => { + // apiのConfigurationを取得する + const { getState } = thunkApi; + const state = getState() as RootState; + const { configuration, accessToken } = state.auth; + const config = new Configuration(configuration); + const termsApi = new TermsApi(config); + + try { + const termsInfo = await termsApi.getTermsInfo({ + headers: { authorization: `Bearer ${accessToken}` }, + }); + + return termsInfo.data; + } catch (e) { + // e ⇒ errorObjectに変換" + const error = createErrorObject(e); + thunkApi.dispatch( + openSnackbar({ + level: "error", + message: getTranslationID("common.message.internalServerError"), + }) + ); + return thunkApi.rejectWithValue({ error }); + } +}); + +export const updateAcceptedVersionAsync = createAsyncThunk< + { + /* Empty Object */ + }, + { + tier: number; + localStorageKeyforIdToken: string; + updateAccceptVersions: { + acceptedVerDPA: string; + acceptedVerEULA: string; + }; + }, + { + // rejectした時の返却値の型 + rejectValue: { + error: ErrorObject; + }; + } +>("accept/UpdateAcceptedVersionAsync", async (args, thunkApi) => { + const { tier, localStorageKeyforIdToken, updateAccceptVersions } = args; + // apiのConfigurationを取得する + const { getState } = thunkApi; + const state = getState() as RootState; + const { configuration, accessToken } = state.auth; + const config = new Configuration(configuration); + const userApi = new UsersApi(config); + + try { + // IDトークンの取得 + const idToken = getIdTokenFromLocalStorage(localStorageKeyforIdToken); + + // IDトークンが取得できない場合エラーとする + if (!idToken) { + throw new Error("Unable to retrieve the ID token."); + } + await userApi.updateAcceptedVersion( + { + idToken, + acceptedEULAVersion: updateAccceptVersions.acceptedVerEULA, + acceptedDPAVersion: !(TIERS.TIER5 === tier.toString()) + ? updateAccceptVersions.acceptedVerDPA + : undefined, + }, + { + headers: { authorization: `Bearer ${accessToken}` }, + } + ); + return {}; + } catch (e) { + const error = createErrorObject(e); + thunkApi.dispatch( + openSnackbar({ + level: "error", + message: getTranslationID("common.message.internalServerError"), + }) + ); + return thunkApi.rejectWithValue({ error }); + } +}); diff --git a/dictation_client/src/features/terms/selectors.ts b/dictation_client/src/features/terms/selectors.ts new file mode 100644 index 0000000..5cd00f7 --- /dev/null +++ b/dictation_client/src/features/terms/selectors.ts @@ -0,0 +1,20 @@ +import { RootState } from "app/store"; +import { TERMS_DOCUMENT_TYPE } from "features/terms/constants"; + +export const selectTermVersions = (state: RootState) => { + const { termsInfo } = state.terms.domain; + + const acceptedVerDPA = + termsInfo.find( + (termInfo) => termInfo.documentType === TERMS_DOCUMENT_TYPE.DPA + )?.version || ""; + + const acceptedVerEULA = + termsInfo.find( + (termInfo) => termInfo.documentType === TERMS_DOCUMENT_TYPE.EULA + )?.version || ""; + + return { acceptedVerDPA, acceptedVerEULA }; +}; + +export const selectTier = (state: RootState) => state.terms.domain.tier; diff --git a/dictation_client/src/features/terms/state.ts b/dictation_client/src/features/terms/state.ts new file mode 100644 index 0000000..0c724dc --- /dev/null +++ b/dictation_client/src/features/terms/state.ts @@ -0,0 +1,15 @@ +import { TermInfo } from "../../api/api"; + +export interface AcceptState { + domain: Domain; + apps: Apps; +} + +export interface Domain { + tier: number; + termsInfo: TermInfo[]; +} + +export interface Apps { + isLoading: boolean; +} diff --git a/dictation_client/src/features/terms/termsSlice.ts b/dictation_client/src/features/terms/termsSlice.ts new file mode 100644 index 0000000..7f88271 --- /dev/null +++ b/dictation_client/src/features/terms/termsSlice.ts @@ -0,0 +1,64 @@ +import { createSlice } from "@reduxjs/toolkit"; +import { AcceptState } from "./state"; +import { + getAccountInfoMinimalAccessAsync, + getTermsInfoAsync, + updateAcceptedVersionAsync, +} from "./operations"; + +const initialState: AcceptState = { + domain: { + tier: 0, + termsInfo: [ + { + documentType: "", + version: "", + }, + ], + }, + apps: { + isLoading: false, + }, +}; + +export const termsSlice = createSlice({ + name: "terms", + initialState, + reducers: {}, + extraReducers: (builder) => { + builder.addCase(getAccountInfoMinimalAccessAsync.pending, (state) => { + state.apps.isLoading = true; + }); + builder.addCase( + getAccountInfoMinimalAccessAsync.fulfilled, + (state, actions) => { + state.apps.isLoading = false; + state.domain.tier = actions.payload.tier; + } + ); + builder.addCase(getAccountInfoMinimalAccessAsync.rejected, (state) => { + state.apps.isLoading = false; + }); + builder.addCase(getTermsInfoAsync.pending, (state) => { + state.apps.isLoading = true; + }); + builder.addCase(getTermsInfoAsync.fulfilled, (state, actions) => { + state.apps.isLoading = false; + state.domain.termsInfo = actions.payload.termsInfo; + }); + builder.addCase(getTermsInfoAsync.rejected, (state) => { + state.apps.isLoading = false; + }); + builder.addCase(updateAcceptedVersionAsync.pending, (state) => { + state.apps.isLoading = true; + }); + builder.addCase(updateAcceptedVersionAsync.fulfilled, (state) => { + state.apps.isLoading = false; + }); + builder.addCase(updateAcceptedVersionAsync.rejected, (state) => { + state.apps.isLoading = false; + }); + }, +}); + +export default termsSlice.reducer; diff --git a/dictation_client/src/pages/AcceptToUsePage/index.tsx b/dictation_client/src/pages/AcceptToUsePage/index.tsx deleted file mode 100644 index a6e5d0e..0000000 --- a/dictation_client/src/pages/AcceptToUsePage/index.tsx +++ /dev/null @@ -1,25 +0,0 @@ -/* eslint-disable jsx-a11y/label-has-associated-control */ -import styles from "styles/app.module.scss"; -import { useNavigate } from "react-router-dom"; - -const AcceptToUsePage: React.FC = (): JSX.Element => { - // 遷移確認用のダミーページ - - const navigate = useNavigate(); - - const navigateToLoginPage = () => { - navigate("/login"); - }; - - return ( -
- 利用規約同意画面のダミー画面 -
- {/* eslint-disable-next-line */} - -
-
- ); -}; - -export default AcceptToUsePage; diff --git a/dictation_client/src/pages/LoginPage/index.tsx b/dictation_client/src/pages/LoginPage/index.tsx index c4cf355..fedb428 100644 --- a/dictation_client/src/pages/LoginPage/index.tsx +++ b/dictation_client/src/pages/LoginPage/index.tsx @@ -30,7 +30,7 @@ const LoginPage: React.FC = (): JSX.Element => { if (isErrorObject(payload)) { // 未同意の規約がある場合は利用規約同意画面に遷移する if (payload.error.code === "E010209") { - navigate("/accept-to-use"); + navigate("/terms"); return; } } diff --git a/dictation_client/src/pages/SignupPage/signupInput.tsx b/dictation_client/src/pages/SignupPage/signupInput.tsx index b64e777..3f388ce 100644 --- a/dictation_client/src/pages/SignupPage/signupInput.tsx +++ b/dictation_client/src/pages/SignupPage/signupInput.tsx @@ -268,10 +268,9 @@ const SignupInput: React.FC = (): JSX.Element => { /> {isPushCreateButton && hasErrorEmptyAdminName && ( - {" "} - {t( + {` ${t( getTranslationID("signupPage.message.inputEmptyError") - )} + )}`} )} @@ -373,8 +372,9 @@ const SignupInput: React.FC = (): JSX.Element => { }} > {t(getTranslationID("signupPage.label.termsLink"))} - {" "} - {t(getTranslationID("signupPage.label.termsLinkFor"))}
+ + {` ${t(getTranslationID("signupPage.label.termsLinkFor"))} `} +