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"))} `}
+