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 () {