Skip to content

Commit

Permalink
Single beneficiary summons (#854)
Browse files Browse the repository at this point in the history
* #853 support beneficiary summon links

* #853 shifting to base32 for ben component of summon link

* #853 use summons when generating a single beneficiary link

* #853 adding message to explain the single beneficiary link changes
  • Loading branch information
drimpact authored Nov 10, 2023
1 parent e3674d0 commit 0fbd227
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 17 deletions.
14 changes: 14 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@
"redux-storage-engine-localstorage": "1.1.4",
"redux-thunk": "2.4.1",
"semantic-ui-react": "1.3.1",
"thirty-two": "^1.0.2",
"url-search-params-polyfill": "4.0.1"
},
"lint-staged": {
Expand Down
2 changes: 1 addition & 1 deletion src/app/components/AssessmentConfig/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ const AssessmentConfigInner = withFormik<IProps, IAssessmentConfigAndDebounce>({
errors.date = t("Please select a date in the past");
}
}
if (values.requiredTags.filter((v) => v === undefined).length > 0) {
if ((values.requiredTags || []).filter((v) => v === undefined).length > 0) {
errors.requiredTags = t("Required tags not provided");
}
return errors;
Expand Down
42 changes: 31 additions & 11 deletions src/app/containers/AssessmentConfig/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import {
import { QuestionnairishType } from "components/QuestionnairesAndSequencesHoC";
import { externalLinkURI, forwardURLParam, isUrlAbsolute } from "helpers/url";
import { ISummonConfig } from "./summonConfig";
import { IGetOrgResult, getOrganisation } from "apollo/modules/organisation";
import { Encode } from "helpers/summon";

interface IProps
extends IMeetingMutation,
Expand All @@ -43,6 +45,7 @@ interface IProps
type: string;
};
};
org?: IGetOrgResult;
}

const Wrapper = (p: {
Expand Down Expand Up @@ -80,27 +83,42 @@ const AssessmentConfigInner = (p: IProps) => {
);
const [link, setLink] = useState<string>();

const startSummon = (c: ISummonConfig): Promise<void> => {
if (typ !== AssessmentType.summon) {
const startSummon = (c: ISummonConfig, ben?: string): Promise<void> => {
if (typ !== AssessmentType.summon && typ !== AssessmentType.remote) {
return Promise.reject("unexpected error");
}
const promFn =
c.qishType === QuestionnairishType.QUESTIONNAIRE
? p.generateSummon
: p.generateSequenceSummon;
return promFn(c.qishID, c.tags).then((smn) => {
setLink(`smn/${smn}`);
setLink(`smn/${Encode(smn, ben)}`);
});
};

const startRemote = (c: IAssessmentConfig): Promise<void> => {
const promFn =
c.qishType === QuestionnairishType.QUESTIONNAIRE
? p.newRemoteMeeting
: p.startRemoteSequence;
return promFn(c, defaultRemoteMeetingLimit).then((jti) => {
setLink(`jti/${jti}`);
});
// by default, for single beneficiary links, we use specalised summons
// so that the link can be reused multiple times
const orgPlugins = (p.org?.getOrganisation?.plugins || []).map((p) => p.id);
const noSingleBenSummons =
orgPlugins.indexOf("no-single-ben-summon") !== -1;
if (noSingleBenSummons) {
const promFn =
c.qishType === QuestionnairishType.QUESTIONNAIRE
? p.newRemoteMeeting
: p.startRemoteSequence;
return promFn(c, defaultRemoteMeetingLimit).then((jti) => {
setLink(`jti/${jti}`);
});
}
return startSummon(
{
qishID: c.qishID,
qishType: c.qishType,
tags: c.tags,
},
c.beneficiaryID
);
};

const startMeeting = (c: IAssessmentConfig): Promise<void> => {
Expand Down Expand Up @@ -164,7 +182,9 @@ export const AssessmentConfig = newRemoteMeeting<IProps>(
newMeeting(
generateSummon(
startSequence(
startRemoteSequence(generateSequenceSummon(AssessmentConfigInner))
startRemoteSequence(
generateSequenceSummon(getOrganisation(AssessmentConfigInner, "org"))
)
)
)
)
Expand Down
12 changes: 12 additions & 0 deletions src/app/containers/AssessmentConfig/link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export const QuestionnaireLink = (p: {
}): JSX.Element => {
const { t } = useTranslation();
const url = `${config.app.root}/${p.link}`;
const newStyleSingleLink =
p.typ == AssessmentType.remote && p.link.includes("smn/");
return (
<Message success={true}>
<Message.Header>{t("Success")}</Message.Header>
Expand All @@ -26,6 +28,16 @@ export const QuestionnaireLink = (p: {
<div style={{ marginTop: "1em" }}>
<CopyBox text={url} />
</div>
{newStyleSingleLink && (
<div style={{ marginTop: "1em" }}>
<b>{t("Single beneficiary links have recently changed")}: </b>
<span>
{t(
"Each time the link is used, a new record will be created. So there is no need to create multiple links for the same beneficiary now. Enjoy!"
)}
</span>
</div>
)}
</Message>
);
};
47 changes: 42 additions & 5 deletions src/app/containers/Summon/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import * as React from "react";
import React, { useEffect, useState } from "react";
import { SummonForm } from "./form";
import { useNavigator } from "redux/modules/url";
import { ISummonAcceptanceMutation } from "apollo/modules/summon";
import { newMeetingFromSummon } from "../../apollo/modules/summon";
import { PageWrapperHoC } from "../../components/PageWrapperHoC";
import ReactGA from "react-ga4";
import { useTranslation } from "react-i18next";
import { Loader } from "semantic-ui-react";
import { CustomError } from "components/Error";
import { Decode } from "helpers/summon";

interface IProps extends ISummonAcceptanceMutation {
match: {
Expand All @@ -18,6 +21,27 @@ interface IProps extends ISummonAcceptanceMutation {
const SummonAcceptanceInner = (p: IProps) => {
const { t } = useTranslation();
const setURL = useNavigator();
const [idNeeded, setIDNeeded] = useState(false);
const [error, setError] = useState<Error>(undefined);

useEffect(() => {
try {
const { id, ben } = Decode(p.match.params.id);
if (ben) {
createRecord(ben, id).catch((e) => {
setError(e);
});
} else {
setIDNeeded(true);
}
} catch (e) {
setError(
new Error(
t("We did not recognise this link. Please request a new link")
)
);
}
}, []);

const logResult = (label: string) => {
ReactGA.event({
Expand All @@ -27,9 +51,12 @@ const SummonAcceptanceInner = (p: IProps) => {
});
};

const createRecord = (beneficiaryID: string): Promise<void> => {
const createRecord = (
beneficiaryID: string,
summonID?: string
): Promise<void> => {
return p
.newMeetingFromSummon(p.match.params.id, beneficiaryID)
.newMeetingFromSummon(summonID ?? p.match.params.id, beneficiaryID)
.then((jti) => {
logResult("success");
setURL(`/jti/${jti}`);
Expand All @@ -44,7 +71,10 @@ const SummonAcceptanceInner = (p: IProps) => {
throw new Error(
t("This link has been exhausted. Please request a new link")
);
} else if (e.message.includes("found")) {
} else if (
e.message.includes("found") ||
e.message.includes("no documents")
) {
throw new Error(
t("We did not recognise this link. Please request a new link")
);
Expand All @@ -57,7 +87,14 @@ const SummonAcceptanceInner = (p: IProps) => {
}
});
};
return <SummonForm onBeneficiarySelect={createRecord} />;

if (idNeeded) {
return <SummonForm onBeneficiarySelect={createRecord} />;
} else if (error) {
return <CustomError inner={<span>{error.message}</span>} />;
} else {
return <Loader active={true} inline="centered" />;
}
};

const SummonAcceptanceData = newMeetingFromSummon<IProps>(
Expand Down
31 changes: 31 additions & 0 deletions src/app/helpers/summon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { decode, encode } from "thirty-two";

// For single beneficiary summons, the first component of the ID
// is a base 32 (without padding) encoded beneficiary ID. The remaining components
// are the Summon ID

// throws an error if decoding fails
export const Decode = (smn: string): { id: string; ben?: string } => {
const guidParts = smn.split("-");
const idIncludesBen = guidParts.length === 6;
if (idIncludesBen) {
let benEnc = guidParts[0];
while (benEnc.length % 8 !== 0) {
benEnc += "=";
}
return {
id: guidParts.slice(1).join("-"),
ben: decode(benEnc).toString(),
};
}
return {
id: smn,
};
};

export const Encode = (id: string, ben?: string): string => {
if (ben) {
return `${encode(ben).toString().toLowerCase().replaceAll("=", "")}-${id}`;
}
return id;
};

0 comments on commit 0fbd227

Please sign in to comment.