@bloom-housing/ui-components 4.2.1-alpha.0 → 4.2.1-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,14 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [4.2.1-alpha.1](https://github.com/bloom-housing/bloom/compare/@bloom-housing/ui-components@4.2.1-alpha.0...@bloom-housing/ui-components@4.2.1-alpha.1) (2022-04-07)
7
+
8
+ **Note:** Version bump only for package @bloom-housing/ui-components
9
+
10
+
11
+
12
+
13
+
6
14
  ## [4.2.1-alpha.0](https://github.com/bloom-housing/bloom/compare/@bloom-housing/ui-components@4.1.3-alpha.5...@bloom-housing/ui-components@4.2.1-alpha.0) (2022-04-06)
7
15
 
8
16
 
package/index.ts CHANGED
@@ -118,6 +118,7 @@ export * from "./src/page_components/listing/listing_sidebar/Waitlist"
118
118
  export * from "./src/page_components/listing/listing_sidebar/WhatToExpect"
119
119
  export * from "./src/page_components/listing/listing_sidebar/events/DownloadLotteryResults"
120
120
  export * from "./src/page_components/listing/listing_sidebar/events/EventSection"
121
+ export * from "./src/page_components/sign-in/FormTerms"
121
122
  export * from "./src/page_components/sign-in/FormSignIn"
122
123
  export * from "./src/page_components/sign-in/FormSignInMFAType"
123
124
  export * from "./src/page_components/sign-in/FormSignInMFACode"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bloom-housing/ui-components",
3
- "version": "4.2.1-alpha.0",
3
+ "version": "4.2.1-alpha.1",
4
4
  "author": "Sean Albert <sean.albert@exygy.com>",
5
5
  "description": "Shared user interface components for Bloom affordable housing system",
6
6
  "homepage": "https://github.com/bloom-housing/bloom/tree/master/shared/ui-components",
@@ -69,7 +69,7 @@
69
69
  "webpack": "^4.44.2"
70
70
  },
71
71
  "dependencies": {
72
- "@bloom-housing/backend-core": "^4.2.1-alpha.0",
72
+ "@bloom-housing/backend-core": "^4.2.1-alpha.1",
73
73
  "@mapbox/mapbox-sdk": "^0.13.0",
74
74
  "@types/body-scroll-lock": "^2.6.1",
75
75
  "@types/jwt-decode": "^2.2.1",
@@ -100,5 +100,5 @@
100
100
  "tailwindcss": "2.2.10",
101
101
  "typesafe-actions": "^5.1.0"
102
102
  },
103
- "gitHead": "fbf9158cbeafed4cedaf923b0881ae3863eff307"
103
+ "gitHead": "a6f7d7cd4b295500aff5be357fd77c360e13af28"
104
104
  }
@@ -29,6 +29,7 @@ import {
29
29
  useEffect,
30
30
  useMemo,
31
31
  useReducer,
32
+ useCallback,
32
33
  } from "react"
33
34
  import qs from "qs"
34
35
  import axiosStatic from "axios"
@@ -51,6 +52,7 @@ type ContextProps = {
51
52
  reservedCommunityTypeService: ReservedCommunityTypesService
52
53
  unitPriorityService: UnitAccessibilityPriorityTypesService
53
54
  unitTypesService: UnitTypesService
55
+ loadProfile: (redirect?: string) => void
54
56
  login: (
55
57
  email: string,
56
58
  password: string,
@@ -99,7 +101,7 @@ const saveToken = createAction("SAVE_TOKEN")<{
99
101
  accessToken: string
100
102
  dispatch: DispatchType
101
103
  }>()
102
- const saveProfile = createAction("SAVE_PROFILE")<User>()
104
+ const saveProfile = createAction("SAVE_PROFILE")<User | null>()
103
105
  const startLoading = createAction("START_LOADING")()
104
106
  const stopLoading = createAction("STOP_LOADING")()
105
107
  const signOut = createAction("SIGN_OUT")()
@@ -218,23 +220,30 @@ export const AuthProvider: FunctionComponent = ({ children }) => {
218
220
  }
219
221
  }, [apiUrl, storageType])
220
222
 
223
+ const loadProfile = useCallback(
224
+ async (redirect?: string) => {
225
+ try {
226
+ const profile = await userService?.userControllerProfile()
227
+ if (profile) {
228
+ dispatch(saveProfile(profile))
229
+ }
230
+ } finally {
231
+ dispatch(stopLoading())
232
+
233
+ if (redirect) {
234
+ router.push(redirect)
235
+ }
236
+ }
237
+ },
238
+ [userService, router]
239
+ )
240
+
221
241
  // Load our profile as soon as we have an access token available
222
242
  useEffect(() => {
223
243
  if (!state.profile && state.accessToken && !state.loading) {
224
- const loadProfile = async () => {
225
- dispatch(startLoading())
226
- try {
227
- const profile = await userService?.userControllerProfile()
228
- if (profile) {
229
- dispatch(saveProfile(profile))
230
- }
231
- } finally {
232
- dispatch(stopLoading())
233
- }
234
- }
235
244
  void loadProfile()
236
245
  }
237
- }, [state.profile, state.accessToken, apiUrl, userService, state.loading])
246
+ }, [state.profile, state.accessToken, apiUrl, userService, state.loading, loadProfile])
238
247
 
239
248
  const contextValues: ContextProps = {
240
249
  amiChartsService: new AmiChartsService(),
@@ -254,6 +263,7 @@ export const AuthProvider: FunctionComponent = ({ children }) => {
254
263
  accessToken: state.accessToken,
255
264
  initialStateLoaded: state.initialStateLoaded,
256
265
  profile: state.profile,
266
+ loadProfile,
257
267
  login: async (
258
268
  email,
259
269
  password,
@@ -1,4 +1,4 @@
1
- import React, { FunctionComponent, useContext, useEffect } from "react"
1
+ import React, { FunctionComponent, useContext, useEffect, useState } from "react"
2
2
  import { clearSiteAlertMessage, setSiteAlertMessage } from "../notifications/SiteAlert"
3
3
  import { NavigationContext } from "../config/NavigationContext"
4
4
  import { AuthContext } from "./AuthContext"
@@ -12,6 +12,7 @@ type XOR<T, U> = T | U extends Record<string, unknown>
12
12
  type RequireLoginProps = {
13
13
  signInPath: string
14
14
  signInMessage: string
15
+ termsPath?: string // partners portal required accepted terms after sign-in
15
16
  } & XOR<{ requireForRoutes?: string[] }, { skipForRoutes: string[] }>
16
17
 
17
18
  /**
@@ -24,10 +25,12 @@ const RequireLogin: FunctionComponent<RequireLoginProps> = ({
24
25
  children,
25
26
  signInPath,
26
27
  signInMessage,
28
+ termsPath,
27
29
  ...rest
28
30
  }) => {
29
31
  const { router } = useContext(NavigationContext)
30
32
  const { profile, initialStateLoaded } = useContext(AuthContext)
33
+ const [hasTerms, setHasTerms] = useState(false)
31
34
 
32
35
  // Parse just the pathname portion of the signInPath (in case we want to pass URL params)
33
36
  const [signInPathname] = signInPath.split("?")
@@ -44,6 +47,12 @@ const RequireLogin: FunctionComponent<RequireLoginProps> = ({
44
47
  ? !rest.skipForRoutes.some((path) => new RegExp(path).exec(router.pathname))
45
48
  : true)
46
49
 
50
+ useEffect(() => {
51
+ if (profile?.jurisdictions?.some((jurisdiction) => jurisdiction.partnerTerms)) {
52
+ setHasTerms(true)
53
+ }
54
+ }, [profile])
55
+
47
56
  useEffect(() => {
48
57
  if (loginRequiredForPath && initialStateLoaded && !profile) {
49
58
  setSiteAlertMessage(signInMessage, "notice")
@@ -51,9 +60,25 @@ const RequireLogin: FunctionComponent<RequireLoginProps> = ({
51
60
  } else {
52
61
  clearSiteAlertMessage("notice")
53
62
  }
54
- }, [loginRequiredForPath, initialStateLoaded, profile, router, signInPath, signInMessage])
55
63
 
56
- if (loginRequiredForPath && !profile) {
64
+ if (termsPath && profile && !profile?.agreedToTermsOfService && hasTerms) {
65
+ void router.push(termsPath)
66
+ }
67
+ }, [
68
+ loginRequiredForPath,
69
+ initialStateLoaded,
70
+ profile,
71
+ router,
72
+ signInPath,
73
+ signInMessage,
74
+ termsPath,
75
+ hasTerms,
76
+ ])
77
+
78
+ if (
79
+ loginRequiredForPath &&
80
+ (!profile || (hasTerms && termsPath && !profile.agreedToTermsOfService))
81
+ ) {
57
82
  return null
58
83
  }
59
84
 
@@ -499,6 +499,10 @@
499
499
  "authentication.timeout.action": "Stay logged in",
500
500
  "authentication.timeout.signOutMessage": "We care about your security. We logged you out due to inactivity. Please sign in to continue.",
501
501
  "authentication.timeout.text": "To protect your identity, your session will expire in one minute due to inactivity. You will lose any unsaved information and be logged out if you choose not to respond.",
502
+ "authentication.terms.termsOfService": "Terms of Service",
503
+ "authentication.terms.reviewToc": "Review Terms of Service",
504
+ "authentication.terms.youMustAcceptToc": "To continue you must accept the Terms of Service",
505
+ "authentication.terms.acceptToc": "I accept the Terms of Service",
502
506
  "config.routePrefix": "",
503
507
  "errors.agreeError": "You must agree to the terms in order to continue",
504
508
  "errors.alert.badRequest": "Looks like something went wrong. Please try again. \n\nContact your housing department if you're still experiencing issues.",
@@ -843,7 +847,7 @@
843
847
  "t.none": "None",
844
848
  "t.noneFound": "None found.",
845
849
  "t.occupancy": "Occupancy",
846
- "t.ok": "OK",
850
+ "t.ok": "Ok",
847
851
  "t.or": "or",
848
852
  "t.people": "people",
849
853
  "t.perMonth": "per month",
@@ -50,9 +50,9 @@
50
50
  }
51
51
 
52
52
  .fixed-overlay__inner-slim {
53
- width: 75vw;
53
+ width: 90vw;
54
54
 
55
55
  @screen md {
56
- width: 50vw;
56
+ width: 75vw;
57
57
  }
58
58
  }
@@ -0,0 +1,94 @@
1
+ import React, { useContext, useCallback, useMemo } from "react"
2
+ import {
3
+ AuthContext,
4
+ AppearanceStyleType,
5
+ Button,
6
+ Field,
7
+ Form,
8
+ FormCard,
9
+ Icon,
10
+ MarkdownSection,
11
+ t,
12
+ } from "@bloom-housing/ui-components"
13
+ import Markdown from "markdown-to-jsx"
14
+ import { useForm } from "react-hook-form"
15
+
16
+ type FormTermsInValues = {
17
+ agree: boolean
18
+ }
19
+
20
+ const FormTerms = () => {
21
+ const { profile, userProfileService, loadProfile } = useContext(AuthContext)
22
+
23
+ // eslint-disable-next-line @typescript-eslint/unbound-method
24
+ const { handleSubmit, register, errors } = useForm<FormTermsInValues>()
25
+
26
+ const onSubmit = useCallback(async () => {
27
+ if (!profile) return
28
+
29
+ const jurisdictionIds =
30
+ profile?.jurisdictions.map((item) => ({
31
+ id: item.id,
32
+ })) || []
33
+
34
+ await userProfileService?.update({
35
+ body: { ...profile, jurisdictions: jurisdictionIds, agreedToTermsOfService: true },
36
+ })
37
+
38
+ loadProfile?.("/")
39
+ }, [loadProfile, profile, userProfileService])
40
+
41
+ const jurisdictionTerms = useMemo(() => {
42
+ const jurisdiction = profile?.jurisdictions.find((jurisdiction) => jurisdiction.partnerTerms)
43
+ return jurisdiction ? jurisdiction.partnerTerms : ""
44
+ }, [profile])
45
+
46
+ return (
47
+ <Form id="terms" className="mt-10" onSubmit={handleSubmit(onSubmit)}>
48
+ <FormCard>
49
+ <div className="form-card__lead text-center">
50
+ <Icon size="2xl" symbol="settings" />
51
+ <h2 className="form-card__title">{t(`authentication.terms.reviewToc`)}</h2>
52
+ <p className="field-note mt-4 text-center">
53
+ {t(`authentication.terms.youMustAcceptToc`)}
54
+ </p>
55
+
56
+ <div className="overflow-y-auto max-h-96 mt-5 pr-4 text-left">
57
+ {jurisdictionTerms && (
58
+ <MarkdownSection padding={false} fullwidth={true}>
59
+ <Markdown options={{ disableParsingRawHTML: false }}>{jurisdictionTerms}</Markdown>
60
+ </MarkdownSection>
61
+ )}
62
+ </div>
63
+ </div>
64
+
65
+ <div className="form-card__group pt-0">
66
+ <Field
67
+ id="agree"
68
+ name="agree"
69
+ type="checkbox"
70
+ className="flex flex-col justify-center items-center"
71
+ label={t(`authentication.terms.acceptToc`)}
72
+ register={register}
73
+ validation={{ required: true }}
74
+ error={!!errors.agree}
75
+ errorMessage={t("errors.agreeError")}
76
+ dataTestId="agree"
77
+ />
78
+ </div>
79
+
80
+ <div className="border-b" />
81
+
82
+ <div className="form-card__pager">
83
+ <div className="form-card__pager-row primary">
84
+ <Button styleType={AppearanceStyleType.primary} data-test-id="form-submit">
85
+ {t("t.submit")}
86
+ </Button>
87
+ </div>
88
+ </div>
89
+ </FormCard>
90
+ </Form>
91
+ )
92
+ }
93
+
94
+ export { FormTerms as default, FormTerms }
@@ -1,10 +1,13 @@
1
1
  .markdown-section {
2
- @apply px-5;
3
2
  @apply my-6;
4
3
 
5
4
  @screen md {
6
5
  @apply my-12;
7
6
  }
7
+
8
+ &--with-padding {
9
+ @apply px-5;
10
+ }
8
11
  }
9
12
 
10
13
  .markdown-section__inner {
@@ -3,16 +3,20 @@ import "./MarkdownSection.scss"
3
3
 
4
4
  export interface MarkdownSectionProps {
5
5
  fullwidth?: boolean
6
+ padding?: boolean
6
7
  children: React.ReactNode
7
8
  }
8
9
 
9
- export const MarkdownSection = (props: MarkdownSectionProps) => {
10
- const contentWidth = props.fullwidth ? "markdown" : "markdown max-w-2xl"
10
+ export const MarkdownSection = ({ fullwidth, padding = true, children }: MarkdownSectionProps) => {
11
+ const contentWidth = fullwidth ? "markdown" : "markdown max-w-2xl"
12
+ const sectionClassNames = ["markdown-section"]
13
+
14
+ if (padding) sectionClassNames.push("markdown-section--with-padding")
11
15
 
12
16
  return (
13
- <div className="markdown-section">
17
+ <div className={sectionClassNames.join(" ")}>
14
18
  <div className="markdown-section__inner">
15
- <article className={contentWidth}>{props.children}</article>
19
+ <article className={contentWidth}>{children}</article>
16
20
  </div>
17
21
  </div>
18
22
  )