diff --git a/app/controllers/invite_to_controller.rb b/app/controllers/invite_to_controller.rb index 9fd5f1e73..63ebcd6ca 100644 --- a/app/controllers/invite_to_controller.rb +++ b/app/controllers/invite_to_controller.rb @@ -1,12 +1,11 @@ # Invite to X controller class InviteToController < ApplicationController def index - # TODO: uncomment when backend token updated - # decoded_params = decode_token(params[:t]) - # if decoded_params.is_a?(String) - # redirect_to decoded_params - # return - # end + decoded_params = decode_token(params[:t]) + if decoded_params.is_a?(String) + redirect_to decoded_params + return + end decoded_params ||= params @invite_to_props = props(decoded_params) # Get file upload URL for application diff --git a/app/javascript/__tests__/api/inviteToApplyApiService.test.ts b/app/javascript/__tests__/api/inviteToApiService.test.ts similarity index 87% rename from app/javascript/__tests__/api/inviteToApplyApiService.test.ts rename to app/javascript/__tests__/api/inviteToApiService.test.ts index 176820451..31306c74b 100644 --- a/app/javascript/__tests__/api/inviteToApplyApiService.test.ts +++ b/app/javascript/__tests__/api/inviteToApiService.test.ts @@ -1,5 +1,6 @@ import { post } from "../../api/apiService" import { recordResponse } from "../../api/inviteToApiService" +import { INVITE_TO_X } from "../../modules/constants" jest.mock("../../api/apiService", () => ({ post: jest.fn(), @@ -16,7 +17,7 @@ describe("inviteToApiService", () => { deadline: "2099-01-01", action: "submit", response: "submit", - type: "I2A", + type: INVITE_TO_X.APPLY, } await recordResponse(record) expect(post).toHaveBeenCalled() diff --git a/app/javascript/__tests__/pages/invite-to-apply.test.tsx b/app/javascript/__tests__/pages/invite-to-apply.test.tsx index e6b7c5faa..55fb7abee 100644 --- a/app/javascript/__tests__/pages/invite-to-apply.test.tsx +++ b/app/javascript/__tests__/pages/invite-to-apply.test.tsx @@ -9,6 +9,7 @@ import { localizedFormat } from "../../util/languageUtil" import { getListing } from "../../api/listingApiService" import { recordResponse } from "../../api/inviteToApiService" import { ConfigContext } from "../../lib/ConfigContext" +import { INVITE_TO_X } from "../../modules/constants" jest.mock("../../api/listingApiService") jest.mock("../../api/inviteToApiService", () => ({ @@ -98,7 +99,7 @@ describe("Invite to Apply", () => { { { { { { { { { assetPaths={"/"} documentsPath={true} urlParams={{ - type: "I2A", + type: INVITE_TO_X.APPLY, }} /> ) diff --git a/app/javascript/__tests__/pages/invite-to-interview.test.tsx b/app/javascript/__tests__/pages/invite-to-interview.test.tsx index 1b121757d..ebdd19133 100644 --- a/app/javascript/__tests__/pages/invite-to-interview.test.tsx +++ b/app/javascript/__tests__/pages/invite-to-interview.test.tsx @@ -8,6 +8,7 @@ import { renderAndLoadAsync } from "../__util__/renderUtils" import { ConfigContext } from "../../lib/ConfigContext" import { getListing } from "../../api/listingApiService" import { recordResponse } from "../../api/inviteToApiService" +import { INVITE_TO_X } from "../../modules/constants" jest.mock("../../api/listingApiService") jest.mock("../../api/inviteToApiService", () => ({ @@ -89,7 +90,7 @@ describe("Invite to Interview", () => { @@ -104,7 +105,7 @@ describe("Invite to Interview", () => { { { { { await userEvent.click(button) expect(recordResponse).toHaveBeenCalled() }) + + it("shows the withdrawn page when the action is no", async () => { + await renderWithContext( + + ) + expect(screen.getByText(t("inviteToInterviewPage.withdrawn.title"))).toBeInTheDocument() + }) + it("shows the withdrawn page without the body when the deadline is passed", async () => { + await renderWithContext( + + ) + expect(screen.queryByText(t("inviteToInterviewPage.withdrawn.body"))).not.toBeInTheDocument() + }) + it("shows the waitlist page when the action is contact", async () => { + await renderWithContext( + + ) + expect(screen.getByText(t("inviteToInterviewPage.waitlist.title"))).toBeInTheDocument() + }) }) diff --git a/app/javascript/modules/constants.tsx b/app/javascript/modules/constants.tsx index 4fb2561a8..2cb387e78 100644 --- a/app/javascript/modules/constants.tsx +++ b/app/javascript/modules/constants.tsx @@ -288,3 +288,8 @@ export const RELATIONSHIP_OPTIONS = [ { value: "Friend", label: "label.friend" }, { value: "Other", label: "label.Other" }, ] + +export enum INVITE_TO_X { + APPLY = "I2A", + INTERVIEW = "I2I", +} diff --git a/app/javascript/pages/inviteTo/InviteToContactMeLater.tsx b/app/javascript/pages/inviteTo/InviteToContactMeLater.tsx new file mode 100644 index 000000000..963902d98 --- /dev/null +++ b/app/javascript/pages/inviteTo/InviteToContactMeLater.tsx @@ -0,0 +1,82 @@ +import React from "react" +import { t, LoadingOverlay } from "@bloom-housing/ui-components" +import { Card, Heading } from "@bloom-housing/ui-seeds" +import styles from "./invite-to.module.scss" +import { renderMarkup, localizedFormat } from "../../util/languageUtil" +import RailsSaleListing from "../../api/types/rails/listings/RailsSaleListing" +import FormLayout from "../../layouts/FormLayout" +import InviteToApplyHeader from "./InviteToHeader" +import InviteToLeasingAgentInfo from "./InviteToLeasingAgentInfo" +import { INVITE_TO_X } from "../../modules/constants" + +interface InviteToContactMeLaterProps { + type: INVITE_TO_X + listing: RailsSaleListing | null + deadline: string + submitPreviewLink: string +} + +const InviteToContactMeLater = ({ + type, + listing, + deadline, + submitPreviewLink, +}: InviteToContactMeLaterProps) => { + return ( + + + + + + + {type === INVITE_TO_X.APPLY + ? t("inviteToApplyPage.contact.title", { + listingName: listing?.Building_Name_for_Process, + }) + : t("inviteToInterviewPage.waitlist.title")} + +

+ {type === INVITE_TO_X.APPLY && t("inviteToApplyPage.contact.subtitle")} + {type === INVITE_TO_X.INTERVIEW && + renderMarkup( + `${t("inviteToInterviewPage.waitlist.subtitle", { + listingName: listing?.Building_Name_for_Process, + link: `/listings/${listing?.Id}`, + })}\n\n${t("inviteToInterviewPage.waitlist.p1")}`, + "" + )} +

+
+ + + {t("inviteToApplyPage.leasingAgent.p1")} + +

{t("inviteToApplyPage.leasingAgent.p2")}

+

{t("inviteToApplyPage.leasingAgent.p3")}

+ + {type === INVITE_TO_X.APPLY && + renderMarkup( + t("inviteToApplyPage.submitYourInfo", { + listingName: listing?.Building_Name_for_Process, + link: submitPreviewLink, + deadline: localizedFormat(deadline, "ll"), + }), + "" + )} + {type === INVITE_TO_X.INTERVIEW && + renderMarkup( + t("inviteToInterviewPage.waitlist.body", { + listingName: listing?.Building_Name_for_Process, + link: submitPreviewLink, + deadline: localizedFormat(deadline, "ll"), + }), + "" + )} +
+
+
+
+ ) +} + +export default InviteToContactMeLater diff --git a/app/javascript/pages/inviteTo/InviteToLayout.tsx b/app/javascript/pages/inviteTo/InviteToLayout.tsx index 664128d60..14df846e3 100644 --- a/app/javascript/pages/inviteTo/InviteToLayout.tsx +++ b/app/javascript/pages/inviteTo/InviteToLayout.tsx @@ -12,6 +12,7 @@ import { import styles from "./invite-to.module.scss" import InviteToLeasingAgentInfo from "./InviteToLeasingAgentInfo" import Layout from "../../layouts/Layout" +import { INVITE_TO_X } from "../../modules/constants" const InviteToHeader = ({ listing, @@ -40,9 +41,9 @@ const DeadlineBanner = ({ }: { deadline: string listing: RailsSaleListing - type: string + type: INVITE_TO_X }) => { - if (type === "I2A") { + if (type === INVITE_TO_X.APPLY) { return ( { + return ( + + + + + + + {t("inviteToApplyPage.withdrawn.title")} + + + {!isDeadlinePassed(deadline) && ( + + + {t("inviteToApplyPage.leasingAgent.p1")} + +

{t("inviteToApplyPage.leasingAgent.p2")}

+

{t("inviteToApplyPage.leasingAgent.p3")}

+ + {type === INVITE_TO_X.APPLY && + renderMarkup( + `${t("inviteToApplyPage.submitYourInfo", { + listingName: listing?.Building_Name_for_Process, + link: submitPreviewLink, + deadline: localizedFormat(deadline, "ll"), + })}`, + "" + )} + {type === INVITE_TO_X.INTERVIEW && + renderMarkup( + `${t("inviteToInterviewPage.withdrawn.body", { + listingName: listing?.Building_Name_for_Process, + link: submitPreviewLink, + deadline: localizedFormat(deadline, "ll"), + })}`, + "" + )} +
+ )} +
+
+
+ ) +} + +export default InviteToWithdrawn diff --git a/app/javascript/pages/inviteTo/invite-to.tsx b/app/javascript/pages/inviteTo/invite-to.tsx index 39cf21e3c..1269723d5 100644 --- a/app/javascript/pages/inviteTo/invite-to.tsx +++ b/app/javascript/pages/inviteTo/invite-to.tsx @@ -5,8 +5,9 @@ import { AppPages, generateSubmitLink } from "../../util/routeUtil" import { getListing } from "../../api/listingApiService" import { useFeatureFlag, useVariantFlag } from "../../hooks/useFeatureFlag" import InviteToDeadlinePassed from "./InviteToDeadlinePassed" -import InviteToApplyWithdrawn from "./inviteToApply/InviteToApplyWithdrawn" -import InviteToApplyContactMeLater from "./inviteToApply/InviteToApplyContactMeLater" +import InviteToWithdrawn from "./InviteToWithdrawn" +import { INVITE_TO_X } from "../../modules/constants" +import InviteToContactMeLater from "./InviteToContactMeLater" import InviteToApplyNextSteps from "./inviteToApply/InviteToApplyNextSteps" import InviteToApplyDocuments from "./inviteToApply/InviteToApplyDocuments" import InviteToInterviewDocuments from "./inviteToInterview/InviteToInterviewDocuments" @@ -16,7 +17,7 @@ import { getPathWithoutLanguagePrefix } from "../../util/languageUtil" import { isDeadlinePassed } from "../../util/listingUtil" interface UrlParams { - type?: "I2A" | "I2I" + type?: INVITE_TO_X deadline?: string act?: "yes" | "no" | "contact" | "submit" | "appointment" appId?: string @@ -63,9 +64,16 @@ const InviteToPage = ({ })() : [] const isI2IEnabled = isI2IEnabledFlag && listing?.Id && enabledListingIds.includes(listing.Id) + const previewLink = generateSubmitLink( + appId, + deadline, + listing?.Id, + type, + submitPreviewLinkTokenParam + ) /* I2I - Invite to Interview pages */ - if (type === "I2I") { + if (type === INVITE_TO_X.INTERVIEW) { if (!isI2IEnabled) { return null } @@ -77,16 +85,11 @@ const InviteToPage = ({ } if (act === "no") { return ( - ) } @@ -96,19 +99,15 @@ const InviteToPage = ({ if (isDeadlinePassed(deadline)) return if (act === "contact") { return ( - ) } + return null } /* I2A - Invite to Apply pages */ @@ -130,16 +129,11 @@ const InviteToPage = ({ } if (act === "no") { return ( - ) } @@ -156,16 +150,11 @@ const InviteToPage = ({ if (isDeadlinePassed(deadline)) return if (act === "contact") { return ( - ) } diff --git a/app/javascript/pages/inviteTo/inviteToApply/InviteToApplyContactMeLater.tsx b/app/javascript/pages/inviteTo/inviteToApply/InviteToApplyContactMeLater.tsx deleted file mode 100644 index 828a2ecc5..000000000 --- a/app/javascript/pages/inviteTo/inviteToApply/InviteToApplyContactMeLater.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import React from "react" -import { t, LoadingOverlay } from "@bloom-housing/ui-components" -import { Card, Heading } from "@bloom-housing/ui-seeds" -import styles from "../invite-to.module.scss" -import { renderMarkup, localizedFormat } from "../../../util/languageUtil" -import RailsSaleListing from "../../../api/types/rails/listings/RailsSaleListing" -import FormLayout from "../../../layouts/FormLayout" -import InviteToApplyHeader from "../InviteToHeader" -import InviteToApplyLeasingAgentInfo from "../InviteToLeasingAgentInfo" - -interface InviteToApplyContactMeLaterProps { - listing: RailsSaleListing | null - deadline: string - submitPreviewLink: string -} - -const InviteToApplyContactMeLater = ({ - listing, - deadline, - submitPreviewLink, -}: InviteToApplyContactMeLaterProps) => { - return ( - - - - - - - {t("inviteToApplyPage.contact.title", { - listingName: listing?.Building_Name_for_Process, - })} - -

{t("inviteToApplyPage.contact.subtitle")}

-
- - - {t("inviteToApplyPage.leasingAgent.p1")} - -

{t("inviteToApplyPage.leasingAgent.p2")}

-

{t("inviteToApplyPage.leasingAgent.p3")}

- - {renderMarkup( - `${t("inviteToApplyPage.submitYourInfo", { - listingName: listing?.Building_Name_for_Process, - link: submitPreviewLink, - deadline: localizedFormat(deadline, "ll"), - })}`, - "" - )} -
-
-
-
- ) -} - -export default InviteToApplyContactMeLater diff --git a/app/javascript/pages/inviteTo/inviteToApply/InviteToApplyNextSteps.tsx b/app/javascript/pages/inviteTo/inviteToApply/InviteToApplyNextSteps.tsx index 375c5def3..0fe469c8d 100644 --- a/app/javascript/pages/inviteTo/inviteToApply/InviteToApplyNextSteps.tsx +++ b/app/javascript/pages/inviteTo/inviteToApply/InviteToApplyNextSteps.tsx @@ -16,6 +16,7 @@ import InviteToLayout from "../InviteToLayout" import { recordResponse } from "../../../api/inviteToApiService" import InviteToGetHelp from "../InviteToGetHelp" import InviteToLeasingAgentInfo from "../InviteToLeasingAgentInfo" +import { INVITE_TO_X } from "../../../modules/constants" interface InviteToApplyNextStepsProps { listing: RailsSaleListing | null @@ -71,7 +72,7 @@ const WhatToDo = ({ deadline, action: "submit", response: "submit", - type: "I2A", + type: INVITE_TO_X.APPLY, }) } setIsSubmitting(false) @@ -202,7 +203,7 @@ const InviteToApplyNextSteps = ({ return ( { - return ( - - - - - - - {t("inviteToApplyPage.withdrawn.title")} - - - {!isDeadlinePassed(deadline) && ( - - - {t("inviteToApplyPage.leasingAgent.p1")} - -

{t("inviteToApplyPage.leasingAgent.p2")}

-

{t("inviteToApplyPage.leasingAgent.p3")}

- - {renderMarkup( - `${t("inviteToApplyPage.submitYourInfo", { - listingName: listing?.Building_Name_for_Process, - link: submitPreviewLink, - deadline: localizedFormat(deadline, "ll"), - })}`, - "" - )} -
- )} -
-
-
- ) -} - -export default InviteToApplyWithdrawn diff --git a/app/javascript/pages/inviteTo/inviteToInterview/InviteToInterviewNextSteps.tsx b/app/javascript/pages/inviteTo/inviteToInterview/InviteToInterviewNextSteps.tsx index 785cd50f9..74f7ecaf9 100644 --- a/app/javascript/pages/inviteTo/inviteToInterview/InviteToInterviewNextSteps.tsx +++ b/app/javascript/pages/inviteTo/inviteToInterview/InviteToInterviewNextSteps.tsx @@ -11,6 +11,7 @@ import InviteToLayout from "../InviteToLayout" import InviteToGetHelp from "../InviteToGetHelp" import InviteToLeasingAgentInfo from "../InviteToLeasingAgentInfo" import { recordResponse } from "../../../api/inviteToApiService" +import { INVITE_TO_X } from "../../../modules/constants" interface InviteToInterviewNextStepsProps { listing: RailsSaleListing @@ -29,7 +30,7 @@ const WhatToDo = ({ }) => { const [isSubmitting, setIsSubmitting] = useState(false) const handleSubmitClick = useCallback(() => { - const url = listing?.Leaseup_Appointment_Scheduling_URL + const url = listing.Leaseup_Appointment_Scheduling_URL void (async () => { setIsSubmitting(true) window.open(url, "_blank") @@ -42,7 +43,7 @@ const WhatToDo = ({ deadline, action: "submit", response: "submit", - type: "I2I", + type: INVITE_TO_X.INTERVIEW, }) } setIsSubmitting(false) @@ -83,7 +84,7 @@ const WhatToDo = ({ variant="primary-outlined" onClick={() => window.open( - `/${getCurrentLanguage()}/listings/${listing?.Id}/next-steps/documents`, + `/${getCurrentLanguage()}/listings/${listing.Id}/next-steps/documents`, "_blank" ) } @@ -144,7 +145,7 @@ const InviteToInterviewNextSteps = ({ return ( { // Parse deadline as end of day in Pacific Time diff --git a/app/javascript/util/routeUtil.ts b/app/javascript/util/routeUtil.ts index 57e8bb6b6..d07cfaab6 100644 --- a/app/javascript/util/routeUtil.ts +++ b/app/javascript/util/routeUtil.ts @@ -1,3 +1,4 @@ +import { INVITE_TO_X } from "../modules/constants" import { getCurrentLanguage, getPathWithoutLanguagePrefix, @@ -160,7 +161,7 @@ export const generateSubmitLink = ( appId: string, deadline: string, listingId: string, - type: string, + type: INVITE_TO_X, submitPreviewLinkTokenParam?: string ) => { const submitLinkQueryStr = submitPreviewLinkTokenParam diff --git a/app/services/dahlia_backend/message_service.rb b/app/services/dahlia_backend/message_service.rb index 3371cfda9..fea02e121 100644 --- a/app/services/dahlia_backend/message_service.rb +++ b/app/services/dahlia_backend/message_service.rb @@ -73,14 +73,14 @@ def send_invite_to_response(_deadline, _app_id, _application_number, _response, _application_number, _app_id, _action) return if fields.nil? - log_info("Prepared fields for Invite to Apply response: #{fields.inspect}") + log_info("Prepared fields for I2X response: #{fields.inspect}") endpoint = get_response_endpoint(_action, _response) return log_error("Invalid action type: #{_action}", nil) unless endpoint send_message(endpoint, fields) rescue StandardError => e - log_error('Error sending Invite to Apply', e) + log_error('Error sending I2X response', e) nil end diff --git a/spec/controllers/invite_to_controller_spec.rb b/spec/controllers/invite_to_controller_spec.rb index e8ae6a3ef..712406317 100644 --- a/spec/controllers/invite_to_controller_spec.rb +++ b/spec/controllers/invite_to_controller_spec.rb @@ -131,17 +131,16 @@ expect(assigns(:invite_to_props)).to have_key(:submitPreviewLinkTokenParam) end - # TODO: update deprecated I2A pilot - # it 'redirects to the listing details page if token is blank' do - # get :index, params: { id: listing_id } - # expect(response).to redirect_to("/listings/#{listing_id}") - # end + it 'redirects to the listing details page if token is blank' do + get :index, params: { id: listing_id } + expect(response).to redirect_to("/listings/#{listing_id}") + end - # it 'redirects to the listing details page if token is invalid' do - # allow(JWT).to receive(:decode).and_raise(JWT::DecodeError) - # get :index, params: { id: listing_id, t: 'invalid_test_token' } - # expect(response).to redirect_to('/') - # end + it 'redirects to the listing details page if token is invalid' do + allow(JWT).to receive(:decode).and_raise(JWT::DecodeError) + get :index, params: { id: listing_id, t: 'invalid_test_token' } + expect(response).to redirect_to('/') + end end end