@velocitycareerlabs/velocity-registrar-app 1.25.0-dev-build.1253d0f86 → 1.25.0-dev-build.1e7c93977

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/.env.dev CHANGED
@@ -1,8 +1,8 @@
1
- VITE_REGISTRAR_AUTH0_DOMAIN=vnf-localdev.us.auth0.com
2
- VITE_REGISTRAR_AUTH0_CLIENT_ID=GKZ5MeATHcBFADukezS4Pa3hCB1XbkX6
3
- VITE_REGISTRAR_AUTH0_REDIRECT_URI=http://localhost:5173
1
+ VITE_REGISTRAR_AUTH0_DOMAIN=devauth.velocitynetwork.foundation
2
+ VITE_REGISTRAR_AUTH0_CLIENT_ID=RkHjVKfdzpdemtZeJ7lFxDMMAe4CfyYa
3
+ VITE_REGISTRAR_AUTH0_REDIRECT_URI=https://devregistrarapp.velocitynetwork.foundation
4
4
  VITE_REGISTRAR_AUDIENCE=https://registrar.velocitynetwork.foundation
5
- VITE_REGISTRAR_CONNECTION=vnf-localdev-users-connection
5
+ VITE_REGISTRAR_CONNECTION=vnf-dev-users-connection
6
6
  VITE_REGISTRAR_API=https://devregistrar.velocitynetwork.foundation/api/v0.6
7
7
  VITE_X_AUTO_ACTIVATE=true
8
8
  VITE_CHAIN_NAME=Devnet
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velocitycareerlabs/velocity-registrar-app",
3
- "version": "1.25.0-dev-build.1253d0f86",
3
+ "version": "1.25.0-dev-build.1e7c93977",
4
4
  "description": "Velocity Registrar",
5
5
  "license": "Apache-2.0",
6
6
  "publishConfig": {
@@ -60,5 +60,5 @@
60
60
  "not ie <= 11",
61
61
  "not op_mini all"
62
62
  ],
63
- "gitHead": "7f963613f230d1f27dfad8bbbe3d4bf1159618d3"
63
+ "gitHead": "4873c27eaf8638fd0ce5b5e1330691d819ddf438"
64
64
  }
package/src/App.jsx CHANGED
@@ -15,26 +15,21 @@
15
15
  *
16
16
  */
17
17
 
18
+ import React from 'react';
18
19
  import { Route, useLocation, useNavigate } from 'react-router-dom';
19
20
  import { CustomRoutes, Resource } from 'react-admin';
20
- import React from 'react';
21
21
  import { Auth0Provider } from '@auth0/auth0-react';
22
22
  import OrganizationIcon from '@mui/icons-material/Business';
23
23
  import {
24
24
  OrganizationShow,
25
25
  OrganizationEdit,
26
- OrganizationCreate,
27
26
  } from '@velocitycareerlabs/components-organizations-registrar/pages/organizations';
28
27
  import {
29
28
  IndividualsDashboard,
30
29
  IndividualsShow,
31
30
  IndividualsEdit,
32
31
  } from '@velocitycareerlabs/components-organizations-registrar/pages/individuals';
33
- import { ServicesList } from '@velocitycareerlabs/components-organizations-registrar/pages/services';
34
- import {
35
- CreateOrganisationFromInvitation,
36
- InvitationsList,
37
- } from '@velocitycareerlabs/components-organizations-registrar/pages/invitations';
32
+ import { InvitationsList } from '@velocitycareerlabs/components-organizations-registrar/pages/invitations';
38
33
  import {
39
34
  PrivacyPolicy,
40
35
  TermsAndConditions,
@@ -48,7 +43,12 @@ import {
48
43
  PrivateAppRoot,
49
44
  useConfig,
50
45
  } from '@velocitycareerlabs/components-organizations-registrar';
46
+
51
47
  import { AuthProviderBridge } from './AuthProviderBridge.jsx';
48
+ import { ServicesList } from './pages/services/ServiceList.jsx';
49
+ import { OrganizationCreate } from './pages/organizations/OrganizationCreate.jsx';
50
+ import { CreateOrganisationFromInvitation } from './pages/invitations/CreateOrganisationFromInvitation.jsx';
51
+ import { extendedRemoteDataProvider } from './utils/remoteDataProvider';
52
52
 
53
53
  const PublicRoutes = ['/privacy-policy', '/terms-and-conditions', /^\/signatories\/[^/]+$/];
54
54
 
@@ -94,7 +94,7 @@ const PrivateRegistrarApp = () => {
94
94
  }}
95
95
  >
96
96
  <AuthProviderBridge config={config}>
97
- <PrivateAppRoot>
97
+ <PrivateAppRoot extendedRemoteDataProvider={extendedRemoteDataProvider()}>
98
98
  <Resource
99
99
  name="organizations"
100
100
  icon={OrganizationIcon}
@@ -0,0 +1,6 @@
1
+ import { CreateOrganisationFromInvitation as CreateOrganization } from '@velocitycareerlabs/components-organizations-registrar/pages/invitations';
2
+ import { SecureIntegrationPopup } from '../services/components/SecureIntegrationPopup/index.jsx';
3
+
4
+ export const CreateOrganisationFromInvitation = () => {
5
+ return <CreateOrganization InterceptOnCreate={SecureIntegrationPopup} />;
6
+ };
@@ -0,0 +1,6 @@
1
+ import { OrganizationAddService as OrganizationAddServiceBase } from '@velocitycareerlabs/components-organizations-registrar/pages/organizations';
2
+ import { SecureIntegrationPopup } from '../services/components/SecureIntegrationPopup/index.jsx';
3
+
4
+ export const OrganizationAddService = (props) => {
5
+ return <OrganizationAddServiceBase {...props} InterceptOnCreate={SecureIntegrationPopup} />;
6
+ };
@@ -0,0 +1,12 @@
1
+ import { OrganizationCreate as OrganizationCreateBase } from '@velocitycareerlabs/components-organizations-registrar/pages/organizations';
2
+ import { OrganizationAddService } from './OrganizationAddService.jsx';
3
+ import { MockOrganization } from './components/MockOrganization.jsx';
4
+
5
+ export const OrganizationCreate = () => {
6
+ return (
7
+ <OrganizationCreateBase
8
+ CreateServiceComponent={OrganizationAddService}
9
+ MockOrganization={MockOrganization}
10
+ />
11
+ );
12
+ };
@@ -0,0 +1,44 @@
1
+ import { useCallback } from 'react';
2
+ import { Button } from 'react-admin';
3
+ import { Box } from '@mui/material';
4
+ import PropTypes from 'prop-types';
5
+
6
+ import { useConfig, chainNames } from '@velocitycareerlabs/components-organizations-registrar';
7
+ import { initialRecordMock } from '../utils/mockOrganization';
8
+
9
+ export const MockOrganization = ({ setInitialRecord }) => {
10
+ const config = useConfig();
11
+ const isMockDataAllowed = [chainNames.localnet, chainNames.devnet, chainNames.qanet].includes(
12
+ config.chainName,
13
+ );
14
+ const mockOrganization = useCallback(() => {
15
+ setInitialRecord(initialRecordMock);
16
+ }, [setInitialRecord]);
17
+
18
+ return isMockDataAllowed ? (
19
+ <Box display="flex" alignSelf="flex-end">
20
+ <Button
21
+ variant="text"
22
+ color="secondary"
23
+ size="large"
24
+ sx={sx.mockButton}
25
+ onClick={mockOrganization}
26
+ >
27
+ Mock Organization
28
+ </Button>
29
+ </Box>
30
+ ) : null;
31
+ };
32
+
33
+ const sx = {
34
+ mockButton: {
35
+ color: 'primary.main',
36
+ },
37
+ };
38
+
39
+ // eslint-disable-next-line better-mutation/no-mutation
40
+ MockOrganization.propTypes = {
41
+ setInitialRecord: PropTypes.func.isRequired,
42
+ };
43
+
44
+ export default MockOrganization;
@@ -0,0 +1,21 @@
1
+ export const initialRecordMock = {
2
+ profile: {
3
+ name: `Mock Organization, Inc. ${new Date().getTime()}`,
4
+ website: `https://mockcompany${new Date().getTime()}.com`,
5
+ linkedInProfile: 'https://www.linkedin.com/company/mockcompanyid',
6
+ physicalAddress: { line1: '123 Mock Street' },
7
+ location: { countryCode: 'US' },
8
+ contactEmail: 'support@mockcompany.com',
9
+ technicalEmail: 'tech@mockcompany.com',
10
+ description: 'This is a mock description for local development.',
11
+ registrationNumbers: [{ authority: 'DunnAndBradstreet', number: '12345' }],
12
+ adminGivenName: 'AdminFirst',
13
+ adminFamilyName: 'AdminLast',
14
+ adminTitle: 'Administrator',
15
+ adminEmail: 'admin@mockcompany.com',
16
+ signatoryGivenName: 'SignerFirst',
17
+ signatoryFamilyName: 'SignerLast',
18
+ signatoryTitle: 'Signatory Officer',
19
+ signatoryEmail: 'sign@mockcompany.com',
20
+ },
21
+ };
@@ -0,0 +1,6 @@
1
+ import { ServiceCreateForm as ServiceCreateFormBase } from '@velocitycareerlabs/components-organizations-registrar/pages/services';
2
+ import { SecureIntegrationPopup } from './components/SecureIntegrationPopup/index.jsx';
3
+
4
+ export const ServiceCreateForm = (props) => {
5
+ return <ServiceCreateFormBase {...props} InterceptOnCreate={SecureIntegrationPopup} />;
6
+ };
@@ -0,0 +1,6 @@
1
+ import { ServicesList as ServicesListBase } from '@velocitycareerlabs/components-organizations-registrar/pages/services';
2
+ import { ServiceCreateForm } from './ServiceCreateForm.jsx';
3
+
4
+ export const ServicesList = () => {
5
+ return <ServicesListBase CreateComponent={ServiceCreateForm} />;
6
+ };
@@ -0,0 +1,70 @@
1
+ import React, { useEffect } from 'react';
2
+ import { useGetOne } from 'react-admin';
3
+ import PropTypes from 'prop-types';
4
+ import { useSelectedOrganization } from '@velocitycareerlabs/components-organizations-registrar';
5
+ import { Popup } from '@velocitycareerlabs/components-organizations-registrar/components/common';
6
+ import { Loading } from '@velocitycareerlabs/components-organizations-registrar/components';
7
+
8
+ import { SecureMessageURL } from '../SecureMessageUrl/index.jsx';
9
+ import { SecureTransfer } from '../SecureTransfer/index.jsx';
10
+ import { dataResources } from '../../../../utils/remoteDataProvider';
11
+
12
+ export const SecureIntegrationPopup = ({
13
+ isInterceptOnCreateOpen,
14
+ serviceId,
15
+ onNext,
16
+ onClose,
17
+ isIssueOrInspection,
18
+ selectedCAO,
19
+ }) => {
20
+ const [did] = useSelectedOrganization();
21
+
22
+ const { data: secureMessageTransfer, isLoading: isCheckingCAOSecureTranferSupport } = useGetOne(
23
+ dataResources.SECURE_MESSAGE_SUPPORTED,
24
+ {
25
+ id: selectedCAO,
26
+ },
27
+ {
28
+ enabled: !!selectedCAO && isIssueOrInspection,
29
+ },
30
+ );
31
+
32
+ useEffect(() => {
33
+ if (secureMessageTransfer && !secureMessageTransfer?.supported) {
34
+ onClose();
35
+ }
36
+ }, [secureMessageTransfer, onClose]);
37
+
38
+ return isIssueOrInspection && isCheckingCAOSecureTranferSupport ? (
39
+ <Loading sx={styles.loading} />
40
+ ) : (
41
+ <Popup
42
+ onClose={() => {}}
43
+ title=""
44
+ isOpen={isInterceptOnCreateOpen}
45
+ mainContainerStyles={styles.mainContainer}
46
+ disableCloseButton={true}
47
+ >
48
+ {isIssueOrInspection ? (
49
+ <SecureTransfer serviceId={serviceId} did={did} onClose={onClose} />
50
+ ) : (
51
+ <SecureMessageURL did={did} onSave={() => onNext(true)} onSkip={() => onNext(true)} />
52
+ )}
53
+ </Popup>
54
+ );
55
+ };
56
+
57
+ const styles = {
58
+ mainContainer: { pt: 2 },
59
+ loading: { pt: '60px' },
60
+ };
61
+
62
+ // eslint-disable-next-line better-mutation/no-mutation
63
+ SecureIntegrationPopup.propTypes = {
64
+ isInterceptOnCreateOpen: PropTypes.bool.isRequired,
65
+ serviceId: PropTypes.string.isRequired,
66
+ onNext: PropTypes.func.isRequired,
67
+ onClose: PropTypes.func.isRequired,
68
+ isIssueOrInspection: PropTypes.bool.isRequired,
69
+ selectedCAO: PropTypes.string,
70
+ };
@@ -0,0 +1,23 @@
1
+ import React from 'react';
2
+ import { useFormContext } from 'react-hook-form';
3
+ import PropTypes from 'prop-types';
4
+ import { Save } from '@velocitycareerlabs/components-organizations-registrar/components/common';
5
+
6
+ const SaveButtonComponent = ({ saveInProgress, disabled }) => {
7
+ const {
8
+ formState: { errors },
9
+ } = useFormContext();
10
+
11
+ const hasValidationError = Boolean(errors?.secureMessagesUrl);
12
+ const isDisabled = saveInProgress || hasValidationError || disabled;
13
+
14
+ return <Save saveInProgress={saveInProgress} isDisabled={isDisabled} />;
15
+ };
16
+
17
+ // eslint-disable-next-line better-mutation/no-mutation
18
+ SaveButtonComponent.propTypes = {
19
+ saveInProgress: PropTypes.bool.isRequired,
20
+ disabled: PropTypes.bool.isRequired,
21
+ };
22
+
23
+ export const SaveButton = React.memo(SaveButtonComponent);
@@ -0,0 +1,44 @@
1
+ import React from 'react';
2
+ import { useFormContext } from 'react-hook-form';
3
+ import { Button } from '@mui/material';
4
+ import { Loading } from '@velocitycareerlabs/components-organizations-registrar/components';
5
+ import PropTypes from 'prop-types';
6
+
7
+ const TestButtonComponent = ({ onTest, testInProgress }) => {
8
+ const {
9
+ watch,
10
+ formState: { errors },
11
+ } = useFormContext();
12
+ const secureMessagesUrl = watch('secureMessagesUrl');
13
+ const hasValidationError = Boolean(errors?.secureMessagesUrl);
14
+
15
+ return (
16
+ <Button
17
+ variant="outlined"
18
+ sx={styles.testButton}
19
+ onClick={() => onTest(secureMessagesUrl)}
20
+ endIcon={testInProgress ? <Loading color="error" sx={styles.loader} size={26} /> : null}
21
+ disabled={testInProgress || !secureMessagesUrl || hasValidationError}
22
+ >
23
+ TEST
24
+ </Button>
25
+ );
26
+ };
27
+
28
+ const styles = {
29
+ button: { px: 4, py: 1, fontSize: '16px', width: '160px', fontWeight: '600' },
30
+ testButton: {
31
+ borderColor: 'primary.main',
32
+ color: 'text.main',
33
+ padding: '12.5px',
34
+ fontWeight: '600',
35
+ fontSize: '16px',
36
+ },
37
+ };
38
+ // eslint-disable-next-line better-mutation/no-mutation
39
+ TestButtonComponent.propTypes = {
40
+ onTest: PropTypes.func.isRequired,
41
+ testInProgress: PropTypes.bool.isRequired,
42
+ };
43
+
44
+ export const TestButton = React.memo(TestButtonComponent);
@@ -0,0 +1,73 @@
1
+ import React from 'react';
2
+ import { Box, Typography, Button, useTheme } from '@mui/material';
3
+ import InfoIcon from '@mui/icons-material/Info';
4
+ import { Popup } from '@velocitycareerlabs/components-organizations-registrar/components/common';
5
+ import PropTypes from 'prop-types';
6
+
7
+ const TestSecureMessageWarningComponent = ({ isModalOpened, onClose, onContinue }) => {
8
+ const theme = useTheme();
9
+
10
+ return (
11
+ <Popup
12
+ onClose={onClose}
13
+ title=""
14
+ isOpen={isModalOpened}
15
+ mainContainerStyles={styles.mainContainer}
16
+ isBackBoxVisible
17
+ backBoxColor={theme.palette.warning.main}
18
+ >
19
+ <InfoIcon color="warning" sx={styles.icon} />
20
+ <Typography sx={styles.title}>Secure message URL should be tested before saving</Typography>
21
+ <Typography textAlign="center">
22
+ They will not be available again and are critical for managing your organization data.
23
+ </Typography>
24
+ <Box sx={styles.buttonBlock}>
25
+ <Button variant="outlined" sx={styles.button} onClick={onClose}>
26
+ BACK TO TEST
27
+ </Button>
28
+ <Button variant="text" sx={[styles.button, styles.continueButton]} onClick={onContinue}>
29
+ CONTINUE ANYWAY
30
+ </Button>
31
+ </Box>
32
+ </Popup>
33
+ );
34
+ };
35
+
36
+ const styles = {
37
+ mainContainer: {
38
+ display: 'flex',
39
+ flexDirection: 'column',
40
+ alignItems: 'center',
41
+ pt: '19px',
42
+ maxWidth: '620px',
43
+ },
44
+ title: {
45
+ fontWeight: 600,
46
+ fontSize: '32px',
47
+ lineHeight: '39px',
48
+ textAlign: 'center',
49
+ mb: '32px',
50
+ mt: '35px',
51
+ },
52
+ button: { px: 4, py: 1, fontSize: '16px' },
53
+ continueButton: {
54
+ color: 'text.disabled',
55
+ },
56
+ buttonBlock: {
57
+ marginTop: '52px',
58
+ display: 'flex',
59
+ flexDirection: 'column',
60
+ gap: '10px',
61
+ },
62
+ icon: { width: '50px', height: '50px' },
63
+ loader: { pl: '10px' },
64
+ };
65
+
66
+ // eslint-disable-next-line better-mutation/no-mutation
67
+ TestSecureMessageWarningComponent.propTypes = {
68
+ isModalOpened: PropTypes.bool.isRequired,
69
+ onClose: PropTypes.func.isRequired,
70
+ onContinue: PropTypes.func.isRequired,
71
+ };
72
+
73
+ export const TestSecureMessageWarning = React.memo(TestSecureMessageWarningComponent);
@@ -0,0 +1,27 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { useFormContext } from 'react-hook-form';
3
+ import PropTypes from 'prop-types';
4
+
5
+ const TrackTestStatusComponent = ({ tested, reset }) => {
6
+ const { watch } = useFormContext();
7
+ const secureMessagesUrl = watch('secureMessagesUrl');
8
+ const prevUrlRef = useRef(secureMessagesUrl);
9
+
10
+ useEffect(() => {
11
+ if (tested && secureMessagesUrl !== prevUrlRef.current) {
12
+ reset();
13
+ }
14
+ // eslint-disable-next-line better-mutation/no-mutation
15
+ prevUrlRef.current = secureMessagesUrl;
16
+ }, [secureMessagesUrl, tested, reset]);
17
+
18
+ return null;
19
+ };
20
+
21
+ // eslint-disable-next-line better-mutation/no-mutation
22
+ TrackTestStatusComponent.propTypes = {
23
+ tested: PropTypes.bool,
24
+ reset: PropTypes.func.isRequired,
25
+ };
26
+
27
+ export const TrackTestStatus = React.memo(TrackTestStatusComponent);
@@ -0,0 +1,157 @@
1
+ import React, { useState, useCallback } from 'react';
2
+ import { Form, FormDataConsumer, TextInput } from 'react-admin';
3
+ import { Box, Button, Stack, Typography } from '@mui/material';
4
+ import CheckCircleIcon from '@mui/icons-material/CheckCircle';
5
+ import CancelIcon from '@mui/icons-material/Cancel';
6
+ import PropTypes from 'prop-types';
7
+
8
+ import { validateUrlOptional } from '../../utils/index.jsx';
9
+ import { useSecureMessageTest } from '../../hooks/useSecureMessageTest';
10
+ import { TestSecureMessageWarning } from './TestSecureMessageWarning/index.jsx';
11
+ import { SaveButton } from './SaveButton/index.jsx';
12
+ import { TestButton } from './TestButton/index.jsx';
13
+ import { TrackTestStatus } from './TrackTestStatus/index.jsx';
14
+
15
+ export const SecureMessageURL = ({ did, onSave: onSaveCallback, onSkip }) => {
16
+ const [isWarningModalOpen, setIsWarningModalOpen] = useState(false);
17
+
18
+ const { onTest, testPassed, testError, testInProgress, tested, reset, onSave, saveInProgress } =
19
+ useSecureMessageTest({
20
+ did,
21
+ });
22
+
23
+ const handleSave = useCallback(
24
+ async (data) => {
25
+ const { secureMessagesUrl } = data;
26
+ if (secureMessagesUrl && !testPassed) {
27
+ setIsWarningModalOpen(true);
28
+ } else {
29
+ await onSave(secureMessagesUrl, onSaveCallback);
30
+ }
31
+ },
32
+ [testPassed, onSave, onSaveCallback],
33
+ );
34
+
35
+ return (
36
+ <>
37
+ <Typography sx={styles.title} mb={2}>
38
+ Set a Secure Messages URL
39
+ </Typography>
40
+ <Typography>
41
+ Ease your client&apos;s efforts by using a Secure Messages endpoint at which a Credential
42
+ Agent Operator. The Secure Messages endpoint is used to securely receive clients. Ensure
43
+ that the webhook is active and responding before setting this value otherwise the profile
44
+ cannot be saved.
45
+ </Typography>
46
+ <Form onSubmit={handleSave} mode="onChange" defaultValues={{ secureMessagesUrl: '' }}>
47
+ <TrackTestStatus tested={tested} reset={reset} />
48
+ <FormDataConsumer>
49
+ {({ formData }) => (
50
+ <Stack>
51
+ <Box sx={styles.fields}>
52
+ {testPassed && (
53
+ <Box sx={[styles.statusBlock, styles.successBlock]}>
54
+ <CheckCircleIcon sx={[styles.statusIcon, styles.iconSuccess]} />
55
+ <Typography>Secure message URL tested successfully!</Typography>
56
+ </Box>
57
+ )}
58
+ {testError && (
59
+ <Box sx={[styles.statusBlock, styles.errorBlock]}>
60
+ <CancelIcon sx={[styles.statusIcon, styles.iconError]} />
61
+ <Typography>Failed to test the secure message URL.</Typography>
62
+ </Box>
63
+ )}
64
+ <TextInput
65
+ source="secureMessagesUrl"
66
+ label="Secure message URL"
67
+ validate={[...validateUrlOptional]}
68
+ parse={(value) => value?.trim() ?? ''}
69
+ />
70
+ <TestButton onTest={onTest} testInProgress={testInProgress} />
71
+ </Box>
72
+
73
+ <Box sx={styles.buttonBlock}>
74
+ <Button
75
+ variant="outlined"
76
+ sx={[styles.button, styles.secondaryButton]}
77
+ onClick={onSkip}
78
+ >
79
+ Skip
80
+ </Button>
81
+ <SaveButton
82
+ saveInProgress={saveInProgress || testInProgress}
83
+ disabled={testError}
84
+ />
85
+ </Box>
86
+ <TestSecureMessageWarning
87
+ isModalOpened={isWarningModalOpen}
88
+ onClose={() => setIsWarningModalOpen(false)}
89
+ onContinue={() => {
90
+ onSave(formData.secureMessagesUrl, onSaveCallback);
91
+ }}
92
+ />
93
+ </Stack>
94
+ )}
95
+ </FormDataConsumer>
96
+ </Form>
97
+ </>
98
+ );
99
+ };
100
+
101
+ const styles = {
102
+ step: { color: (theme) => theme.palette.primary.main, pb: '20px', display: 'block' },
103
+ title: {
104
+ fontSize: '32px',
105
+ fontWeight: '600',
106
+ lineHeight: '38px',
107
+ },
108
+ statusBlock: {
109
+ display: 'flex',
110
+ gap: '20px',
111
+ alignItems: 'center',
112
+ padding: '11px 15px',
113
+ borderRadius: '4px',
114
+ border: '1px solid',
115
+ marginBottom: '32px',
116
+ },
117
+ successBlock: {
118
+ backgroundColor: 'success.light',
119
+ borderColor: 'success.main',
120
+ },
121
+ errorBlock: {
122
+ backgroundColor: 'error.light',
123
+ borderColor: 'primary.main',
124
+ },
125
+ statusIcon: {
126
+ fontSize: '32px',
127
+ padding: '2.5px',
128
+ },
129
+ iconSuccess: {
130
+ color: 'success.main',
131
+ },
132
+ iconError: {
133
+ color: 'error.main',
134
+ },
135
+ fields: {
136
+ padding: '32px 0 40px',
137
+ display: 'flex',
138
+ flexDirection: 'column',
139
+ gap: '0px',
140
+ },
141
+ buttonBlock: {
142
+ display: 'flex',
143
+ justifyContent: 'center',
144
+ gap: '24px',
145
+ },
146
+ button: { px: 4, py: 1, fontSize: '16px', width: '160px', fontWeight: '600' },
147
+ secondaryButton: {
148
+ borderColor: 'secondary.light',
149
+ color: 'text.primary',
150
+ },
151
+ };
152
+ // eslint-disable-next-line better-mutation/no-mutation
153
+ SecureMessageURL.propTypes = {
154
+ did: PropTypes.string.isRequired,
155
+ onSave: PropTypes.func.isRequired,
156
+ onSkip: PropTypes.func.isRequired,
157
+ };
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+ import { Typography, Link } from '@mui/material';
3
+
4
+ const ConsentLabelComponent = () => (
5
+ <Typography>
6
+ Confirm that you consent to the secure transfer of your keys to your Credential Agent Operator.{' '}
7
+ <Link href="./" target="_blank">
8
+ Click here for more information
9
+ </Link>
10
+ </Typography>
11
+ );
12
+
13
+ export const ConsentLabel = React.memo(ConsentLabelComponent);
@@ -0,0 +1,20 @@
1
+ import React from 'react';
2
+ // eslint-disable-next-line import/no-extraneous-dependencies
3
+ import { useFormContext } from 'react-hook-form';
4
+ import PropTypes from 'prop-types';
5
+ import { Save } from '@velocitycareerlabs/components-organizations-registrar/components/common';
6
+
7
+ export const SaveButton = ({ saveInProgress }) => {
8
+ const {
9
+ formState: { errors },
10
+ watch,
11
+ } = useFormContext();
12
+ const consent = watch('consent');
13
+ const hasValidationError = Boolean(Object.keys(errors).length);
14
+ const isDisabled = saveInProgress || hasValidationError || !consent;
15
+ return <Save saveInProgress={saveInProgress} isDisabled={isDisabled} label="Confirm" />;
16
+ };
17
+ // eslint-disable-next-line better-mutation/no-mutation
18
+ SaveButton.propTypes = {
19
+ saveInProgress: PropTypes.bool.isRequired,
20
+ };
@@ -0,0 +1,102 @@
1
+ import React, { useState } from 'react';
2
+ import { useCreate, Form, useNotify } from 'react-admin';
3
+ import { Box, Button, Stack, Typography } from '@mui/material';
4
+ import { CustomBooleanInput } from '@velocitycareerlabs/components-organizations-registrar/components/common';
5
+ import PropTypes from 'prop-types';
6
+ import { dataResources } from '../../../../utils/remoteDataProvider';
7
+ import { SaveButton } from './SaveButton/index.jsx';
8
+ import { ConsentLabel } from './ConsentLabel/index.jsx';
9
+
10
+ const MESSAGE_CODES = {
11
+ keys_sent: 'keys_sent',
12
+ keys_not_sent: 'keys_not_sent',
13
+ };
14
+
15
+ export const SecureTransfer = ({ did, serviceId, onClose }) => {
16
+ const [transferKeys] = useCreate();
17
+ const notify = useNotify();
18
+
19
+ const [saveInProgress, setSaveInProgress] = useState(false);
20
+
21
+ const onTransfer = async () => {
22
+ setSaveInProgress(true);
23
+ try {
24
+ const result = await transferKeys(
25
+ dataResources.SECURE_MESSAGE_TRANSFER,
26
+ {
27
+ data: { serviceIds: [serviceId] },
28
+ meta: { organizationId: did },
29
+ },
30
+ { returnPromise: true },
31
+ );
32
+ if (result.messageCode === MESSAGE_CODES.keys_sent) {
33
+ notify('Succeded', { type: 'success' }, { autoHideDuration: 500 });
34
+ onClose(true);
35
+ } else {
36
+ notify('Error transferring keys', { type: 'warning' }, { autoHideDuration: 500 });
37
+ }
38
+ } catch (error) {
39
+ notify('Error transferring keys', { type: 'warning' }, { autoHideDuration: 500 });
40
+ } finally {
41
+ setSaveInProgress(false);
42
+ }
43
+ };
44
+
45
+ return (
46
+ <>
47
+ <Typography sx={styles.title} mb={2}>
48
+ Automatically setup your network integration
49
+ </Typography>
50
+ <Typography>This Credential Agent Operator supports the secure transfer of keys.</Typography>
51
+ <Form onSubmit={onTransfer} mode="onChange" defaultValues={{ secureMessagesUrl: '' }}>
52
+ <Stack>
53
+ <Box sx={styles.fields}>
54
+ <CustomBooleanInput source="consent" label={<ConsentLabel />} defaultValue={false} />
55
+ </Box>
56
+ <Box sx={styles.buttonBlock}>
57
+ <Button variant="outlined" sx={[styles.button, styles.backButton]} onClick={onClose}>
58
+ Skip
59
+ </Button>
60
+ <SaveButton saveInProgress={saveInProgress} />
61
+ </Box>
62
+ </Stack>
63
+ </Form>
64
+ </>
65
+ );
66
+ };
67
+
68
+ const styles = {
69
+ step: { color: (theme) => theme.palette.primary.main, pb: '20px', display: 'block' },
70
+ title: {
71
+ fontSize: '32px',
72
+ fontWeight: '600',
73
+ lineHeight: '38px',
74
+ },
75
+ fields: {
76
+ padding: '32px 0 40px',
77
+ display: 'flex',
78
+ flexDirection: 'column',
79
+ gap: '0px',
80
+ },
81
+ buttonBlock: {
82
+ display: 'flex',
83
+ justifyContent: 'center',
84
+ gap: '24px',
85
+ },
86
+ button: { px: 4, py: 1, fontSize: '16px', width: '160px', fontWeight: '600' },
87
+ backButton: {
88
+ borderColor: 'secondary.light',
89
+ color: 'text.primary',
90
+ },
91
+ skipButton: {
92
+ color: 'text.disabled',
93
+ marginTop: '16px',
94
+ },
95
+ };
96
+
97
+ // eslint-disable-next-line better-mutation/no-mutation
98
+ SecureTransfer.propTypes = {
99
+ did: PropTypes.string.isRequired,
100
+ serviceId: PropTypes.string.isRequired,
101
+ onClose: PropTypes.func.isRequired,
102
+ };
@@ -0,0 +1,86 @@
1
+ import { useState, useCallback } from 'react';
2
+ import { useCreate } from 'react-admin';
3
+ import { dataResources } from '../../../utils/remoteDataProvider';
4
+
5
+ export const useSecureMessageTest = ({ did }) => {
6
+ const [testSecureMessage] = useCreate();
7
+ const [testPassed, setTestPassed] = useState(false);
8
+ const [testError, setTestError] = useState(null);
9
+ const [testInProgress, setTestInProgress] = useState(false);
10
+ const [tested, setTested] = useState(false);
11
+
12
+ const [saveInProgress, setSaveInProgress] = useState(false);
13
+
14
+ const reset = useCallback(() => {
15
+ setTestPassed(false);
16
+ setTestError(null);
17
+ setTested(false);
18
+ }, []);
19
+
20
+ const onTest = useCallback(
21
+ async (secureMessagesUrl) => {
22
+ setTestInProgress(true);
23
+ setTestPassed(false);
24
+ setTestError(null);
25
+ try {
26
+ const result = await testSecureMessage(
27
+ dataResources.SECURE_MESSAGE_TEST,
28
+ {
29
+ data: { secureMessagesUrl },
30
+ meta: { organizationId: did },
31
+ },
32
+ { returnPromise: true },
33
+ );
34
+ if (result.status === 204) {
35
+ setTestPassed(true);
36
+ } else {
37
+ setTestError(true);
38
+ }
39
+ } catch (error) {
40
+ setTestError(error);
41
+ } finally {
42
+ setTestInProgress(false);
43
+ setTested(true);
44
+ }
45
+ },
46
+ [did, testSecureMessage],
47
+ );
48
+
49
+ const onSave = useCallback(
50
+ async (secureMessagesUrl, callback) => {
51
+ setSaveInProgress(true);
52
+ try {
53
+ const result = await testSecureMessage(
54
+ dataResources.SECURE_MESSAGE_SAVE,
55
+ {
56
+ data: { secureMessagesUrl },
57
+ meta: { organizationId: did },
58
+ },
59
+ { returnPromise: true },
60
+ );
61
+ if (result.status !== 204) {
62
+ throw new Error('Error testing secure message URL');
63
+ }
64
+ if (callback) {
65
+ callback(result);
66
+ }
67
+ } catch (error) {
68
+ setTestError(error);
69
+ } finally {
70
+ setSaveInProgress(false);
71
+ }
72
+ },
73
+ [did, testSecureMessage],
74
+ );
75
+
76
+ return {
77
+ onTest,
78
+ testPassed,
79
+ testError,
80
+ testInProgress,
81
+ tested,
82
+ reset,
83
+ onSave,
84
+ saveInProgress,
85
+ };
86
+ };
@@ -0,0 +1,49 @@
1
+ import { maxLength } from 'react-admin';
2
+
3
+ export const REGEXP_VALID_URL = new RegExp(
4
+ '^' +
5
+ // protocol
6
+ '((https?):\\/\\/)' +
7
+ // authentication
8
+ '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)@)?' +
9
+ // hostname or IP
10
+ '((\\d{1,3}\\.){3}\\d{1,3}' + // IPv4
11
+ '|(([a-z\\d]([a-z\\d-]*[a-z\\d])*\\.)+[a-z]{2,}))' +
12
+ // optional port
13
+ '(\\:\\d+)?' +
14
+ // path
15
+ '(\\/[-a-z\\d%_.~+]*)*' +
16
+ // query string
17
+ '(\\?[;&a-z\\d%_.~+=-]*)?' +
18
+ // fragment
19
+ '(\\#[-a-z\\d_]*)?' +
20
+ ')$',
21
+ 'i',
22
+ );
23
+
24
+ export const REGEXP_HTTPS_URL = new RegExp(
25
+ '^https:\\/\\/' +
26
+ '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)@)?' +
27
+ '((\\d{1,3}\\.){3}\\d{1,3}' + // IPv4
28
+ '|(([a-z\\d]([a-z\\d-]*[a-z\\d])*\\.)+[a-z]{2,}))' +
29
+ '(\\:\\d+)?' +
30
+ '(\\/[-a-z\\d%_.~+]*)*' +
31
+ '(\\?[;&a-z\\d%_.~+=-]*)?' +
32
+ '(\\#[-a-z\\d_]*)?)$',
33
+ 'i',
34
+ );
35
+
36
+ const optionalUrlValidation = (value) => {
37
+ if (!value) {
38
+ return undefined;
39
+ }
40
+ if (!REGEXP_VALID_URL.test(value.trim())) {
41
+ return 'Please type in a valid URL';
42
+ }
43
+ if (!REGEXP_HTTPS_URL.test(value.trim())) {
44
+ return 'Https is required';
45
+ }
46
+ return undefined;
47
+ };
48
+
49
+ export const validateUrlOptional = [optionalUrlValidation, maxLength(1024)];
@@ -0,0 +1,54 @@
1
+ export const dataResources = {
2
+ SECURE_MESSAGE_TEST: 'test-url',
3
+ SECURE_MESSAGE_SAVE: 'set-url',
4
+ SECURE_MESSAGE_SUPPORTED: 'are-supported',
5
+ SECURE_MESSAGE_TRANSFER: 'transfer-keys',
6
+ };
7
+ export const extendedRemoteDataProvider = () => {
8
+ return (client, apiUrl, baseDataProvider, dataResourcesBase) => ({
9
+ getOne: (resource, params) => {
10
+ switch (resource) {
11
+ case dataResources.SECURE_MESSAGE_SUPPORTED: {
12
+ return client(
13
+ `${apiUrl}/${dataResourcesBase.ORGANIZATIONS}/${params.id}/secure-cao-messages/${resource}`,
14
+ ).then(({ json }) => {
15
+ return {
16
+ data: { ...json, id: new Date().getTime() },
17
+ };
18
+ });
19
+ }
20
+ default: {
21
+ return baseDataProvider.getOne(resource, params);
22
+ }
23
+ }
24
+ },
25
+ create: (resource, params) => {
26
+ switch (resource) {
27
+ case dataResources.SECURE_MESSAGE_TEST: {
28
+ const url = `${apiUrl}/${dataResourcesBase.ORGANIZATIONS}/${params.meta.organizationId}/secure-cao-messages/${resource}`;
29
+ return client(url, {
30
+ method: 'POST',
31
+ body: JSON.stringify(params.data),
32
+ }).then((res) => ({ data: { ...res, status: res.status, id: Date.now() } }));
33
+ }
34
+ case dataResources.SECURE_MESSAGE_SAVE: {
35
+ const url = `${apiUrl}/${dataResourcesBase.ORGANIZATIONS}/${params.meta.organizationId}/secure-cao-messages/${resource}`;
36
+ return client(url, {
37
+ method: 'POST',
38
+ body: JSON.stringify(params.data),
39
+ }).then((res) => ({ data: { ...res, status: res.status, id: Date.now() } }));
40
+ }
41
+ case dataResources.SECURE_MESSAGE_TRANSFER: {
42
+ const url = `${apiUrl}/${dataResourcesBase.ORGANIZATIONS}/${params.meta.organizationId}/secure-cao-messages/${resource}`;
43
+ return client(url, {
44
+ method: 'POST',
45
+ body: JSON.stringify(params.data),
46
+ }).then(({ json }) => ({ data: { ...json, id: Date.now() } }));
47
+ }
48
+ default: {
49
+ return baseDataProvider.create(resource, params);
50
+ }
51
+ }
52
+ },
53
+ });
54
+ };