From 7196491cf09e3152bf6cf55aea81a85591968604 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B0=B4=E6=9C=AC=20=E7=A5=90=E5=B8=8C?= Date: Tue, 17 Oct 2023 07:15:49 +0000 Subject: [PATCH] =?UTF-8?q?Merged=20PR=20472:=20=E7=94=BB=E9=9D=A2?= =?UTF-8?q?=E4=BD=9C=E6=88=90=EF=BC=88=E5=88=A9=E7=94=A8=E8=A6=8F=E7=B4=84?= =?UTF-8?q?=E5=90=8C=E6=84=8F=E7=94=BB=E9=9D=A2=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task2802: 画面作成(利用規約同意画面)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2802) - 何をどう変更したか、追加したライブラリなど - 利用規約同意画面の実装を行いました - このPull Requestでの対象/対象外 - api.tsおよびstyles - 影響範囲(他の機能にも影響があるか) - ありません ## レビューポイント - 特にレビューしてほしい箇所 - URLの妥当性(動作確認のため別タスクで追加していますが、内容は本タスクで見てほしいです) 違和感ないか確認お願いします。 } /> - 各処理のエラーハンドリングについて ## UIの変更 - Before/Afterのスクショなど - スクショ置き場 https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task2802?csf=1&web=1&e=otF5YX ## 動作確認状況 - ローカルで確認済 ## 補足 - 相談、参考資料などがあれば --- dictation_client/src/AppRouter.tsx | 4 +- dictation_client/src/app/store.ts | 2 + dictation_client/src/common/token.ts | 13 ++ .../src/features/terms/constants.ts | 8 + dictation_client/src/features/terms/index.ts | 4 + .../src/features/terms/operations.ts | 158 +++++++++++++++ .../src/features/terms/selectors.ts | 20 ++ dictation_client/src/features/terms/state.ts | 15 ++ .../src/features/terms/termsSlice.ts | 64 ++++++ .../src/pages/AcceptToUsePage/index.tsx | 25 --- .../src/pages/LoginPage/index.tsx | 2 +- .../src/pages/SignupPage/signupInput.tsx | 10 +- .../src/pages/TermsPage/index.tsx | 188 ++++++++++++++++++ dictation_client/src/styles/app.module.scss | 14 +- dictation_client/src/translation/de.json | 4 +- dictation_client/src/translation/en.json | 4 +- dictation_client/src/translation/es.json | 4 +- dictation_client/src/translation/fr.json | 4 +- 18 files changed, 494 insertions(+), 49 deletions(-) create mode 100644 dictation_client/src/features/terms/constants.ts create mode 100644 dictation_client/src/features/terms/index.ts create mode 100644 dictation_client/src/features/terms/operations.ts create mode 100644 dictation_client/src/features/terms/selectors.ts create mode 100644 dictation_client/src/features/terms/state.ts create mode 100644 dictation_client/src/features/terms/termsSlice.ts delete mode 100644 dictation_client/src/pages/AcceptToUsePage/index.tsx create mode 100644 dictation_client/src/pages/TermsPage/index.tsx 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"))} `} +