diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..c7713d7 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,25 @@ +import globals from "globals"; +import pluginJs from "@eslint/js"; +import pluginReact from "eslint-plugin-react"; +import stylisticJs from '@stylistic/eslint-plugin-js'; + +/** @type {import('eslint').Linter.Config[]} */ +export default [ + {files: ["**/*.{js,mjs,cjs,jsx}"]}, + { + plugins: { + '@stylistic/js': stylisticJs + }, + rules: { + "indent": ["warn", 2], + "semi": ["warn", "always"], + }, + }, + { + languageOptions: { + globals: globals.browser, + } + }, + pluginJs.configs.recommended, + pluginReact.configs.flat.recommended, +]; \ No newline at end of file diff --git a/package.json b/package.json index b9a1ec7..bc7ca8e 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "@testing-library/jest-dom": "5.11.4", "@testing-library/react": "11.1.0", "@testing-library/user-event": "12.1.10", + "axios": "1.7.7", "base64url": "https://github.com/VerusCoin/base64url.git", "bignumber.js": "9.1.2", "blake2b": "https://github.com/VerusCoin/blake2b.git", @@ -68,8 +69,13 @@ "@babel/polyfill": "7.0.0", "@babel/preset-env": "7.0.0", "@babel/preset-react": "7.0.0", + "@eslint/js": "9.16.0", + "@stylistic/eslint-plugin-js": "2.11.0", "babel-loader": "8.3.0", "css-loader": "4.3.0", + "eslint": "9.16.0", + "eslint-plugin-react": "7.37.2", + "globals": "15.13.0", "html-webpack-plugin": "4.5.2", "mini-css-extract-plugin": "1.6.2", "node-sass": "9.0.0", diff --git a/src/components/LoginConsent/Consent/Consent.render.js b/src/components/LoginConsent/Consent/Consent.render.js index 2cc3374..b87630a 100644 --- a/src/components/LoginConsent/Consent/Consent.render.js +++ b/src/components/LoginConsent/Consent/Consent.render.js @@ -86,7 +86,7 @@ export const ConsentRender = function () { diff --git a/src/components/LoginConsent/Login/Login.js b/src/components/LoginConsent/Login/Login.js index fcef6bc..76baa1a 100644 --- a/src/components/LoginConsent/Login/Login.js +++ b/src/components/LoginConsent/Login/Login.js @@ -4,11 +4,21 @@ import { setExternalAction, setNavigationPath } from '../../../redux/reducers/na import { LoginRender } from './Login.render'; -import { CONSENT_TO_SCOPE, EXTERNAL_ACTION, EXTERNAL_CHAIN_START, REDIRECT } from '../../../utils/constants' +import { + CONSENT_TO_SCOPE, + EXTERNAL_ACTION, + EXTERNAL_CHAIN_START, + REDIRECT, + PROVISIONING_FORM +} from '../../../utils/constants' import { checkAndUpdateIdentities, setActiveVerusId } from '../../../redux/reducers/identity/identity.actions'; import { signResponse } from '../../../rpc/calls/signResponse'; import { setError } from '../../../redux/reducers/error/error.actions'; -import { LoginConsentDecision, LoginConsentResponse } from 'verus-typescript-primitives'; +import { + ID_ADDRESS_VDXF_KEY, + LOGIN_CONSENT_ID_PROVISIONING_WEBHOOK_VDXF_KEY, + LoginConsentDecision, LoginConsentResponse +} from 'verus-typescript-primitives'; import BigNumber from 'bignumber.js'; class Login extends React.Component { @@ -19,7 +29,31 @@ class Login extends React.Component { loading: false } + // Check to see if provisioning is an option. + const { request } = this.props.loginConsentRequest; + + // See if the webhook exists. + let canProvision = request.challenge.provisioning_info && request.challenge.provisioning_info.some(x => { + return ( + x.vdxfkey === LOGIN_CONSENT_ID_PROVISIONING_WEBHOOK_VDXF_KEY.vdxfid + ); + }); + + // Provisioning is not an option if the subject is specified to be one of the identities that the user owns. + if (this.props.identities.length > 0) { + const identitySubjects = + request.challenge.subject.filter(item => item.vdxfkey === ID_ADDRESS_VDXF_KEY.vdxfid).map(id => id.data); + + const identitySubjectMatches = this.props.identities.filter(id => identitySubjects.includes(id.identity.identityaddress)); + + if (identitySubjectMatches.length > 0) { + canProvision = false; + } + } + + this.canProvision = canProvision; this.tryLogin = this.tryLogin.bind(this); + this.tryProvision = this.tryProvision.bind(this); this.selectId = this.selectId.bind(this); this.cancel = this.cancel.bind(this); } @@ -62,6 +96,10 @@ class Login extends React.Component { }) } + tryProvision() { + this.props.dispatch(setNavigationPath(PROVISIONING_FORM)); + } + cancel() { this.props.dispatch(setNavigationPath(CONSENT_TO_SCOPE)); } diff --git a/src/components/LoginConsent/Login/Login.render.js b/src/components/LoginConsent/Login/Login.render.js index 3c01752..13d5d4d 100644 --- a/src/components/LoginConsent/Login/Login.render.js +++ b/src/components/LoginConsent/Login/Login.render.js @@ -44,7 +44,8 @@ export const LoginRender = function () { padding: 8, }} > - {`Select an Identity`} + {`Select an Identity` + + (this.canProvision ? " or Request an Identity" : "")}
@@ -88,6 +90,29 @@ export const LoginRender = function () {
+
+ {this.canProvision && } +
), + [PROVISIONING_FORM]: ( + + ), + [PROVISIONING_CONFIRM]: ( + + ), + [PROVISIONING_RESULT]: ( + + ), [LOADING_DISPLAY]: ( ) diff --git a/src/components/LoginConsent/ProvisionIdentity/ProvisionIdentityConfirm/ProvisionIdentityConfirm.js b/src/components/LoginConsent/ProvisionIdentity/ProvisionIdentityConfirm/ProvisionIdentityConfirm.js new file mode 100644 index 0000000..4aa80ec --- /dev/null +++ b/src/components/LoginConsent/ProvisionIdentity/ProvisionIdentityConfirm/ProvisionIdentityConfirm.js @@ -0,0 +1,391 @@ +import React, { useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { PROVISIONING_FORM, PROVISIONING_RESULT } from '../../../../utils/constants'; +import { setNavigationPath } from '../../../../redux/reducers/navigation/navigation.actions'; +import Button from '@mui/material/Button'; +import { VerusIdLogo } from '../../../../images'; +import Card from '@mui/material/Card'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemText from '@mui/material/ListItemText'; +import Divider from '@mui/material/Divider'; +import Box from '@mui/material/Box'; +import { + LOGIN_CONSENT_ID_PROVISIONING_WEBHOOK_VDXF_KEY, + LoginConsentProvisioningRequest, + LoginConsentProvisioningChallenge, + LoginConsentRequest, + fromBase58Check, + LoginConsentProvisioningResponse, + LOGIN_CONSENT_PROVISIONING_RESULT_STATE_FAILED, + LOGIN_CONSENT_PROVISIONING_RESULT_STATE_PENDINGAPPROVAL, + LOGIN_CONSENT_PROVISIONING_RESULT_STATE_COMPLETE, +} from 'verus-typescript-primitives'; +import { getIdentity } from '../../../../rpc/calls/getIdentity'; +import { getVdxfId } from '../../../../rpc/calls/getVdxfId'; +import { signIdProvisioningRequest } from '../../../../rpc/calls/signIdProvisioningRequest'; +import axios from 'axios'; +import { SnackbarAlert } from '../../../../containers/SnackbarAlert'; +import { verifyIdProvisioningResponse } from '../../../../rpc/calls/verifyIdProvisioningResponse'; +import { + setProvisioningName, + setProvisioningResponse, + setRequestedFqn, + setRequestedId +} from '../../../../redux/reducers/provision/provision.actions'; + +const ProvisionIdentityConfirm = () => { + const dispatch = useDispatch(); + const { request } = useSelector((state) => state.rpc.loginConsentRequest); + const provisioningInfo = useSelector((state) => state.provision.provisioningInfo); + const identityToProvisionField = useSelector((state) => state.provision.identityToProvisionField); + const primaryAddress = useSelector((state) => state.provision.primaryAddress); + + const { + provAddress, + provSystemId, + provFqn, + provParent, + friendlyNameMap, + } = provisioningInfo; + + let displayIdentity; + + if (provFqn) { + displayIdentity = provFqn.data; + } else { + if (friendlyNameMap[identityToProvisionField]) { + displayIdentity = `${friendlyNameMap[identityToProvisionField]}@`; + } else { + displayIdentity = identityToProvisionField; + } + } + + let displayParent; + + if (provParent != null) { + if (friendlyNameMap[provParent.data]) { + displayParent = friendlyNameMap[provParent.data]; + } else { + displayParent = provParent.data; + } + } else { + displayParent = null; + } + + let displaySystemid; + + if (provSystemId != null) { + if (friendlyNameMap[provSystemId.data]) { + displaySystemid = friendlyNameMap[provSystemId.data]; + } else { + displaySystemid = provSystemId.data; + } + } else { + displaySystemid = null; + } + + const [loading, setLoading] = useState(false); + + const [submissionError, setSubmissionError] = useState({ + showError: false, + description: '' + }); + + const handleProvisioningResponse = async (response, requestedId, requestedFqn) => { + const res = new LoginConsentProvisioningResponse(response); + + // Check the response to see if it is valid and if there are errors. + const verificationCheck = await verifyIdProvisioningResponse(res); + const verified = verificationCheck.verified; + + if (!verified) throw new Error('Failed to verify response from the provisioning service.'); + + const {decision} = res; + const {result} = decision; + const { + error_desc, + state, + } = result; + + if (state === LOGIN_CONSENT_PROVISIONING_RESULT_STATE_FAILED.vdxfid) { + throw new Error(error_desc); + } else if (state === LOGIN_CONSENT_PROVISIONING_RESULT_STATE_PENDINGAPPROVAL.vdxfid || + state === LOGIN_CONSENT_PROVISIONING_RESULT_STATE_COMPLETE.vdxfid) { + + if (!result.identity_address && !result.fully_qualified_name) { + throw new Error('Provisioning response did not contain an identity or fully qualified name.'); + } + + if (result.identity_address && result.identity_address !== requestedId) { + throw new Error(`Provisioning response identity [${result.identity_address}] + address does not match requested identity address[${requestedId}].`); + } + + if (result.fully_qualified_name && result.fully_qualified_name.toLowerCase() !== requestedFqn.toLowerCase()) { + throw new Error(`Provisioning response fully qualified name [${result.fully_qualified_name.toLowerCase()}] + does not match requested fully qualified name[${requestedFqn.toLowerCase()}].`); + } + } + }; + + const cancel = () => { + dispatch(setNavigationPath(PROVISIONING_FORM)); + }; + + const submitData = async () => { + setLoading(true); + + const submissionSuccess = (response, requestedFqn, provisioningName, requestedId) => { + setLoading(false); + dispatch(setProvisioningResponse(response)); + dispatch(setRequestedFqn(requestedFqn)); + dispatch(setProvisioningName(provisioningName)); + dispatch(setRequestedId(requestedId)); + dispatch(setNavigationPath(PROVISIONING_RESULT)); + }; + + const submissionError = (msg) => { + setSubmissionError({ + showError: true, + description: msg, + }); + setLoading(false); + }; + + try { + const loginRequest = new LoginConsentRequest(request); + + const webhookSubject = loginRequest.challenge.provisioning_info ? loginRequest.challenge.provisioning_info.find(x => { + return x.vdxfkey === LOGIN_CONSENT_ID_PROVISIONING_WEBHOOK_VDXF_KEY.vdxfid; + }) : null; + + if (webhookSubject == null) throw new Error('No endpoint for ID provisioning'); + + const webhookUrl = webhookSubject.data; + + const identity = + identityToProvisionField != null + ? identityToProvisionField.trim() + : ''; + + let identityName; + let isIAddress; + let parent; + let systemid; + let nameId; + let requestedFqn; + + try { + fromBase58Check(identity); + isIAddress = true; + } catch { + isIAddress = false; + } + + if (isIAddress) { + const identityObj = await getIdentity(request.chainTicker, identity); + + identityName = identityObj.identity.name; + parent = identityObj.identity.parent; + systemid = identityObj.identity.systemid; + nameId = identity; + requestedFqn = identityObj.fullyqualifiedname; + + } else { + identityName = identity.split('@')[0]; + parent = provParent ? provParent.data : null; + systemid = provSystemId ? provSystemId.data : null; + const parentObj = await getIdentity(request.chainTicker, parent ? parent : loginRequest.system_id); + + requestedFqn = `${identityName.split('.')[0]}.${parentObj.fullyqualifiedname}`; + nameId = (await getVdxfId(request.chainTicker, requestedFqn)).vdxfid; + } + + const provisionRequest = new LoginConsentProvisioningRequest({ + signing_address: primaryAddress, + + challenge: new LoginConsentProvisioningChallenge({ + challenge_id: loginRequest.challenge.challenge_id, + created_at: Number((Date.now() / 1000).toFixed(0)), + name: identityName, + system_id: systemid, + parent: parent + }), + }); + + const signedRequest = await signIdProvisioningRequest(request.chainTicker, provisionRequest, primaryAddress); + + // The responding server should include the error within the response instead of + // using an error code. + const res = await axios.post( + webhookUrl, + signedRequest + ); + + const provisionResponse = res.data; + await handleProvisioningResponse(provisionResponse, nameId, requestedFqn); + + const provisioningName = (await getIdentity(request.chainTicker, loginRequest.signing_id)).identity.name; + + submissionSuccess(res.data, requestedFqn, provisioningName, nameId); + } catch (e) { + submissionError(e.message); + } + }; + + + return ( +
+
+ +
+
+ {`Review the Provisioning Request`} +
+
+ + + + + + + + {provAddress && + + + + + + + + } + + + + + + {displayParent && + + + + + + + + } + {provFqn && + + + + + + + + } + {displaySystemid && + + + + + + + + } + + + + {setSubmissionError(false, '');}} + > +
+
+ + +
+
+
+
+ ); +} + +export default ProvisionIdentityConfirm; \ No newline at end of file diff --git a/src/components/LoginConsent/ProvisionIdentity/ProvisionIdentityForm/ProvisionIdentityForm.js b/src/components/LoginConsent/ProvisionIdentity/ProvisionIdentityForm/ProvisionIdentityForm.js new file mode 100644 index 0000000..2f649ff --- /dev/null +++ b/src/components/LoginConsent/ProvisionIdentity/ProvisionIdentityForm/ProvisionIdentityForm.js @@ -0,0 +1,418 @@ +import React, { useEffect, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { PROVISIONING_CONFIRM, SELECT_LOGIN_ID } from '../../../../utils/constants'; +import { setNavigationPath } from '../../../../redux/reducers/navigation/navigation.actions'; +import { + ID_ADDRESS_VDXF_KEY, + ID_SYSTEMID_VDXF_KEY, + ID_FULLYQUALIFIEDNAME_VDXF_KEY, + ID_PARENT_VDXF_KEY, + LOGIN_CONSENT_ID_PROVISIONING_WEBHOOK_VDXF_KEY, + fromBase58Check, +} from 'verus-typescript-primitives'; +import Button from '@mui/material/Button'; +import TextField from '@mui/material/TextField'; +import FormControl from '@mui/material/FormControl'; +import InputAdornment from '@mui/material/InputAdornment'; +import Select from '@mui/material/Select'; +import Box from '@mui/material/Box'; +import CircularProgress from '@mui/material/CircularProgress'; +import MenuItem from '@mui/material/MenuItem'; +import InputLabel from '@mui/material/InputLabel'; +import { VerusIdLogo } from '../../../../images'; +import { getIdentity } from '../../../../rpc/calls/getIdentity'; +import { setIdentityToProvisionField, setPrimaryAddress, setProvisioningInfo } from '../../../../redux/reducers/provision/provision.actions'; +import { getAddresses } from '../../../../rpc/calls/getAddresses'; + +const ProvisionIdentityForm = () => { + const dispatch = useDispatch(); + const { request } = useSelector((state) => state.rpc.loginConsentRequest); + const identityToProvisionField = useSelector((state) => state.provision.identityToProvisionField); + const initialPrimaryAddress = useSelector((state) => state.provision.primaryAddress); + + const hasProvisioningInfo = request != null && request.challenge.provisioning_info != null; + + const [friendlyNameMap, setFriendlyNameMap] = useState({}); + + const [provAddress, setProvAddress] = useState(null); + const [provSystemId, setProvSystemId] = useState(null); + const [provFqn, setProvFqn] = useState(null); + const [provParent, setProvParent] = useState(null); + const [provWebhook, setProvWebhook] = useState(null); + const [assignedIdentity, setAssignedIdentity] = useState(null); + + const [loading, setLoading] = useState(false); + const [parentName, setParentName] = useState(''); + const [publicAddresses, setPublicAddresses] = useState([]); + const [selectedPublicAddress, setSelectedPublicAddress] = useState(initialPrimaryAddress); + + const [formError, setFormError] = useState({ + error: false, + description: '' + }); + + useEffect(() => { + + // Extract the provisioning info from the request. + const updateProvisioningInfoProcessedData = async () => { + if (!hasProvisioningInfo) return; + + const findProvisioningInfo = (key) => + request.challenge.provisioning_info.find( + (x) => x.vdxfkey === key.vdxfid + ); + + const address = findProvisioningInfo(ID_ADDRESS_VDXF_KEY); + const systemId = findProvisioningInfo(ID_SYSTEMID_VDXF_KEY); + const fqn = findProvisioningInfo(ID_FULLYQUALIFIEDNAME_VDXF_KEY); + const parent = findProvisioningInfo(ID_PARENT_VDXF_KEY); + const webhook = findProvisioningInfo(LOGIN_CONSENT_ID_PROVISIONING_WEBHOOK_VDXF_KEY); + + return {address, systemId, fqn, parent, webhook}; + }; + + const initializeState = async () => { + setLoading(true); + + const {address, systemId, fqn, parent, webhook} = await updateProvisioningInfoProcessedData(); + + // Get the addresses of the wallet so the identity can be provisioned to one of them. + const addresses = await getAddresses(request.chainTicker, true, false); + const publicAddressObjects = addresses.public.filter((address) => address.tag === 'public'); + // Extract just the r-address from the address object. + const publicAddresses = publicAddressObjects.map(addressObj => addressObj.address); + setPublicAddresses(publicAddresses); + + const provIdKey = address || fqn || null; + + const identitykeys = provIdKey == null ? [] : [provIdKey]; + if (parent) identitykeys.push(parent); + if (systemId) identitykeys.push(systemId); + + const fetchIdentities = async () => { + let newFriendlyNameMap = friendlyNameMap; + for (const idKey of identitykeys) { + if (idKey != null) { + try { + const identity = await getIdentity(request.chainTicker, idKey.data); + + if (identity) { + // Get only the first part of the name to match the 'name' part of a getidentity call. + let name = ''; + const firstDoxIndex = identity.identity.name.indexOf('.'); + if (firstDoxIndex === -1) name = identity.identity.name; + else name = identity.identity.name.substring(0, firstDoxIndex); + + newFriendlyNameMap[identity.identity.identityaddress] = + name; + + if (provIdKey != null && idKey.data === provIdKey.data) { + setAssignedIdentity(identity.identity.identityaddress); + dispatch(setIdentityToProvisionField(name)); + } + if (idKey.vdxfkey === ID_PARENT_VDXF_KEY.vdxfid) { + const parentName = `.${identity.fullyqualifiedname}`; + setParentName(parentName); + } + } + } catch { + // If the given fully qualified name doesn't exist, then + // it is not valid and should be ignored. + if (idKey.data === provFqn.data) { + setProvFqn(null); + } + } + } + } + + setFriendlyNameMap(newFriendlyNameMap); + setProvAddress(address); + setProvSystemId(systemId); + setProvFqn(fqn); + setProvParent(parent); + setProvWebhook(webhook); + }; + + fetchIdentities(); + setLoading(false); + }; + + initializeState(); + }, []); + + const formHasError = () => { + const identity = identityToProvisionField ? identityToProvisionField.trim() : ''; + + if (!identity) { + setFormError({ + error: true, + description: 'Identity is a required field.' + }); + return true; + } + + try { + fromBase58Check(identity); + if (parentName) { + setFormError({ + error: true, + description: 'i-Address cannot have a parent name.' + }); + return true; + } + } catch { + const formattedId = parentName ? `${identity}${parentName}` : `${identity}@`; + if (!formattedId.endsWith('@')) { + setFormError({ + error: true, + description: 'Identity not a valid identity handle or iAddress.' + }); + return true; + } + } + + // Clear any old errors. + setFormError({ + error: false, + description: '' + }); + + return false; + }; + + const submitData = async () => { + if (formHasError()) return; + + setLoading(true); + + const identity = identityToProvisionField; + + let formattedId; + + try { + fromBase58Check(identity); + formattedId = identity; + } catch { + formattedId = parentName ? `${identity}${parentName}` : `${identity}.${request.chainName}@`; + } + + let identityError = false; + + try { + await getIdentity(request.chainTicker, formattedId); + + // If we get a result back, that means the identity must already exist. + // That is expected if the identity is already assigned by the provisioning service. + if (!assignedIdentity) { + identityError = true; + setFormError({ + error: true, + description: 'Identity name taken, please select a different name.' + }); + } + } catch (e) { + // Check for an invalid identity, otherwise the identity is valid since it does not already exist + // and it is using valid characters. + if (e.message.includes('Identity parameter must be valid friendly name or identity address')) { + identityError = true; + setFormError({ + error: true, + description: `Identity name must not include / : * ? ' < > | @ .` + }); + } + } + + setLoading(false); + + if (!identityError) { + dispatch(setPrimaryAddress(selectedPublicAddress)); + dispatch(setProvisioningInfo({ + provAddress: provAddress, + provSystemId: provSystemId, + provFqn: provFqn, + provParent: provParent, + provWebhook: provWebhook, + friendlyNameMap: friendlyNameMap + })); + dispatch(setNavigationPath(PROVISIONING_CONFIRM)); + } + }; + + const cancel = () => { + dispatch(setNavigationPath(SELECT_LOGIN_ID)); + }; + + return ( +
+
+ +
+
+ {`Request a VerusID`} +
+
+ +
+ {loading ? + + + + : + + { + const text = event.target.value; + if (assignedIdentity == null) { + dispatch(setIdentityToProvisionField(text)); + } + }} + InputProps={{ + endAdornment: + + {parentName ? parentName : ``} + , + }} + > + + + + Select a Primary Address + + + + + } +
+ +
+
+ + +
+
+
+
+ ); +}; + +export default ProvisionIdentityForm; \ No newline at end of file diff --git a/src/components/LoginConsent/ProvisionIdentity/ProvisionIdentityResult/ProvisionIdentityResult.js b/src/components/LoginConsent/ProvisionIdentity/ProvisionIdentityResult/ProvisionIdentityResult.js new file mode 100644 index 0000000..2e3c3ad --- /dev/null +++ b/src/components/LoginConsent/ProvisionIdentity/ProvisionIdentityResult/ProvisionIdentityResult.js @@ -0,0 +1,286 @@ +import React, { useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { PROVISIONING_FORM, SELECT_LOGIN_ID } from '../../../../utils/constants'; +import { setNavigationPath } from '../../../../redux/reducers/navigation/navigation.actions'; +import { setIdentities } from '../../../../redux/reducers/identity/identity.actions'; +import { VerusIdLogo } from '../../../../images'; +import Button from '@mui/material/Button'; +import Box from '@mui/material/Box'; +import CircularProgress from '@mui/material/CircularProgress'; +import CheckCircleIcon from '@mui/icons-material/CheckCircle'; +import ErrorIcon from '@mui/icons-material/Error'; +import { useInterval } from '../../../../utils/interval'; +import axios from 'axios'; +import { + LOGIN_CONSENT_PROVISIONING_ERROR_KEY_CREATION_FAILED, + LOGIN_CONSENT_PROVISIONING_ERROR_KEY_NAMETAKEN, + LOGIN_CONSENT_PROVISIONING_RESULT_STATE_FAILED, + LoginConsentProvisioningResponse +} from 'verus-typescript-primitives'; +import { loadIdentities } from '../../../../rpc/calls/identities'; +import { verifyIdProvisioningResponse } from '../../../../rpc/calls/verifyIdProvisioningResponse'; +import { setIdentityToProvisionField, setPrimaryAddress } from '../../../../redux/reducers/provision/provision.actions'; + +export const checkForProvisioningStatus = async ( + infoUri, + request, + setCheckForId, + setProvisioningError, + setCheckForProvisioningStatus, +) => { + const failed = (description, allowRetry) => { + setCheckForId(false); + setCheckForProvisioningStatus(false); + setProvisioningError({ + error: true, + description: description, + allowRetry: allowRetry, + }); + }; + + if (!infoUri) { + failed('Provisioning timed out with no response from the provisioning service.', false); + return; + } + + try { + const res = await axios.get(infoUri); + const provisioningResponse = new LoginConsentProvisioningResponse(res.data); + const verificationCheck = await verifyIdProvisioningResponse(provisioningResponse); + const verified = verificationCheck.verified; + + if (provisioningResponse.signing_id !== request.signing_id || !verified) { + throw new Error('Failed to verify response from the provisioning service.'); + } + + if (provisioningResponse.decision.result.state === LOGIN_CONSENT_PROVISIONING_RESULT_STATE_FAILED.vdxfid) { + if (provisioningResponse.decision.result.error_key === LOGIN_CONSENT_PROVISIONING_ERROR_KEY_NAMETAKEN.vdxfid) { + failed('Name is already taken.', true); + } else if (provisioningResponse.decision.result.error_key === LOGIN_CONSENT_PROVISIONING_ERROR_KEY_CREATION_FAILED.vdxfid) { + failed('Unable to register the identity.', true); + } else { + failed('Provisioning failed for unknown reasons.', true); + } + } + } catch (e) { + if (e.message === 'Network Error') { + failed('Failed to get a response from the provisioning service.', false); + } else { + failed(e.message, false); + } + } +}; + +export const checkForNewId = async ( + dispatch, + chainId, + requestedId, + setCheckForId, + setCheckForProvisioningStatus +) => { + try { + const identities = await loadIdentities(chainId); + dispatch(setIdentities(identities)); + const found = identities.find(id => { + return id.identity.identityaddress === requestedId; + }); + + if (found) { + setCheckForId(false); + setCheckForProvisioningStatus(false); + } + } catch (e) { + console.error(e); + } +}; + +const ProvisionIdentityResult = () => { + const dispatch = useDispatch(); + + const { request } = useSelector((state) => state.rpc.loginConsentRequest); + const provisioningResponse = useSelector((state) => state.provision.provisioningResponse); + const requestedFqn = useSelector((state) => state.provision.requestedFqn); + const requestedId = useSelector((state) => state.provision.requestedId); + const provisioningName = useSelector((state) => state.provision.provisioningName); + const provisioningCheckDelay = 600000; // Ten minute delay. + const idCheckDelay = 5000; // 5 second delay. + + let formattedName = ''; + const lastDotIndex = requestedFqn.lastIndexOf('.'); + if (lastDotIndex === -1) formattedName = requestedFqn; // return the original string if there's no dot + else formattedName = requestedFqn.substring(0, lastDotIndex); + + const [checkForId, setCheckForId] = useState(true); + const [checkProvisioningStatus, setCheckForProvisioningStatus] = useState(true); + const [provisioningError, setProvisioningError] = useState({ + error: false, + description: '', + allowRetry: false, + }); + + useInterval( + async () => await checkForProvisioningStatus( + provisioningResponse.decision.result.info_uri, + request, + setCheckForId, + setProvisioningError, + setCheckForProvisioningStatus, + ), + checkProvisioningStatus ? provisioningCheckDelay : null, + ); + + useInterval( + async () => await checkForNewId( + dispatch, + request.chainTicker, + requestedId, + setCheckForId, + setCheckForProvisioningStatus, + ), + checkForId ? idCheckDelay : null, + ); + + const finishSend = () => { + // Clear the chosen name and address after leaving. + dispatch(setIdentityToProvisionField('')); + dispatch(setPrimaryAddress('')); + dispatch(setNavigationPath(SELECT_LOGIN_ID)); + }; + + const retry = () => { + // Clear the chosen name before choosing a new one. + dispatch(setIdentityToProvisionField('')); + dispatch(setNavigationPath(PROVISIONING_FORM)); + }; + + return ( +
+
+ +
+
+ + {`${formattedName}@ is being provisioned by ${provisioningName}@`} + + + {`Estimated waiting time is 5 minutes`} + +
+
+ + + {checkForId ? + : + provisioningError.error ? + + : + + } + {!checkForId && provisioningError.error ? provisioningError.description : ``} + + +
+
+ {provisioningError.error ? + + {provisioningError.allowRetry && + + } + + + : + + } +
+
+
+
+ ); +}; + +export default ProvisionIdentityResult; \ No newline at end of file diff --git a/src/components/LoginConsent/Redirect/Redirect.render.js b/src/components/LoginConsent/Redirect/Redirect.render.js index b9a47d3..71fd30f 100644 --- a/src/components/LoginConsent/Redirect/Redirect.render.js +++ b/src/components/LoginConsent/Redirect/Redirect.render.js @@ -79,7 +79,7 @@ export const RedirectRender = function () {