Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhance UI/UX for Credential Selection Popup #241

Merged
merged 8 commits into from
Apr 25, 2024
209 changes: 153 additions & 56 deletions src/components/Popups/SelectCredentials.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,69 @@ import React, { useEffect, useState } from 'react';
import Modal from 'react-modal';
import { useNavigate } from 'react-router-dom';
import { FaShare } from 'react-icons/fa';
import { MdOutlineCheckBox, MdOutlineCheckBoxOutlineBlank } from "react-icons/md";
import { useTranslation, Trans } from 'react-i18next';
import { useApi } from '../../api';
import { CredentialImage } from '../Credentials/CredentialImage';
import CredentialInfo from '../Credentials/CredentialInfo';
import GetButton from '../Buttons/GetButton';
import { extractCredentialFriendlyName } from "../../functions/extractCredentialFriendlyName";

const formatTitle = (title) => {
return title.replace(/([A-Z])/g, ' $1').trim();
};

const StepBar = ({ totalSteps, currentStep, stepTitles }) => {

return (
<div className="flex items-center justify-center w-full my-4">
{Array.from({ length: totalSteps }, (_, index) => {
const isActive = index + 1 < currentStep;
const isCurrent = index + 1 === currentStep;
return (
<React.Fragment key={index}>
<div className="flex flex-col items-center">
<div
className={`w-8 h-8 rounded-full flex items-center justify-center text-xs font-bold ${isActive ? 'text-white bg-primary dark:bg-primary-light border-2 border-primary dark:border-primary-light' : isCurrent ? 'text-primary dark:text-white dark:bg-gray-700 border-2 border-primary dark:border-primary-light' : 'text-gray-400 border-2 border-gray-400 dark:border-gray-400'
}`}
>
{index + 1}
</div>
<p
className={`text-xs font-bold mt-1 ${isActive ? 'text-primary dark:text-primary-light' : isCurrent ? 'text-primary dark:text-white' : 'text-gray-400'} max-w-[60px] sm:max-w-[100px] text-center overflow-hidden whitespace-nowrap overflow-ellipsis`}
title={formatTitle(stepTitles[index])}
>
{formatTitle(stepTitles[index])}
</p>
</div>
{index < totalSteps - 1 && (
<div className="flex-auto h-[2px] bg-gray-400">
<div
className={`h-[2px] ${isActive ? 'bg-primary dark:bg-primary-light' : ''} transition-all duration-300`}
style={{ width: isActive ? '100%' : '0%' }}
></div>
</div>
)}
</React.Fragment>
);
})}
</div>
);
};

function SelectCredentials({ showPopup, setShowPopup, setSelectionMap, conformantCredentialsMap, verifierDomainName }) {
const api = useApi();
const [vcEntities, setVcEntities] = useState([]);
const navigate = useNavigate();
const { t } = useTranslation();

const keys = Object.keys(conformantCredentialsMap);
const stepTitles = Object.keys(conformantCredentialsMap).map(key => key);
const [currentIndex, setCurrentIndex] = useState(0);
const [currentSelectionMap, setCurrentSelectionMap] = useState({});
const [requestedFields, setRequestedFields] = useState([]);
const [showRequestedFields, setShowRequestedFields] = useState(false);
const [showAllFields, setShowAllFields] = useState(false);
const [credentialDisplay, setCredentialDisplay] = useState({});
const [selectedCredential, setSelectedCredential] = useState(null);

useEffect(() => {
const getData = async () => {
Expand Down Expand Up @@ -53,17 +97,35 @@ function SelectCredentials({ showPopup, setShowPopup, setSelectionMap, conforman
getData();
}, [api, currentIndex]);

useEffect(() => {
const currentKey = keys[currentIndex];
const selectedId = currentSelectionMap[currentKey];
setSelectedCredential(selectedId);
}, [currentIndex, currentSelectionMap, keys]);


const goToNextSelection = () => {
setShowAllFields(false);
setCurrentIndex((i) => i + 1);

}

const goToPreviousSelection = () => {
if (currentIndex > 0) {
setShowAllFields(false);
setCurrentIndex(currentIndex - 1);
}
};

const handleClick = (credentialIdentifier) => {
const descriptorId = keys[currentIndex];
setCurrentSelectionMap((currentMap) => {
currentMap[descriptorId] = credentialIdentifier;
return currentMap;
});
goToNextSelection();
if (selectedCredential === credentialIdentifier) {
setSelectedCredential(null);
setCurrentSelectionMap((prev) => ({ ...prev, [descriptorId]: undefined }));
} else {
setSelectedCredential(credentialIdentifier);
setCurrentSelectionMap((prev) => ({ ...prev, [descriptorId]: credentialIdentifier }));
}
};

const handleCancel = () => {
Expand All @@ -75,17 +137,27 @@ function SelectCredentials({ showPopup, setShowPopup, setSelectionMap, conforman
return null;
};

const toggleRequestedFields = () => {
setShowRequestedFields(!showRequestedFields);
};

const toggleCredentialDisplay = (identifier) => {
setCredentialDisplay(prev => ({
...prev,
[identifier]: !prev[identifier]
}));
};

const handleToggleFields = () => {
setShowAllFields(!showAllFields);
};

const requestedFieldsText = (() => {
if (requestedFields.length === 2 && !showAllFields) {
return `${requestedFields[0]} & ${requestedFields[1]}`;
} else if (showAllFields) {
return requestedFields.slice(0, -1).join(', ') + (requestedFields.length > 1 ? ' & ' : '') + requestedFields.slice(-1);
} else {
return requestedFields.slice(0, 2).join(', ') + (requestedFields.length > 2 ? '...' : '');
}
})();

return (
<Modal
isOpen={true}
Expand All @@ -95,61 +167,63 @@ function SelectCredentials({ showPopup, setShowPopup, setSelectionMap, conforman
>
<h2 className="text-lg font-bold mb-2 text-primary dark:text-white">
<FaShare size={20} className="inline mr-1 mb-1" />
{t('selectCredentialPopup.title')}
{t('selectCredentialPopup.title') + formatTitle(stepTitles[currentIndex])}
</h2>

{keys.length > 1 && (
<StepBar totalSteps={keys.length} currentStep={currentIndex + 1} stepTitles={stepTitles} />
)}
<hr className="mb-2 border-t border-primary/80 dark:border-white/80" />
{verifierDomainName && (

<p className="italic pd-2 text-gray-700 dark:text-gray-300">
<Trans
i18nKey="selectCredentialPopup.description"
values={{ verifierDomainName }}
components={{ strong: <strong /> }}
/>
</p>
)}
{requestedFields && (
<div className="my-3 w-full">
<div className="mb-2 flex items-center">
<GetButton
content={showRequestedFields ? `${t('selectCredentialPopup.requestedFieldsHide')}` : `${t('selectCredentialPopup.requestedFieldsShow')}`}
onClick={toggleRequestedFields}
variant="primary"
additionalClassName='text-xs'
/>
</div>

<hr className="border-t border-primary/80 dark:border-primary-light/80" />

<div className={`transition-all ease-in-out duration-1000 p-2 overflow-hidden rounded-md shadow-md bg-gray-50 dark:bg-gray-800 ${showRequestedFields ? 'max-h-[500px] opacity-100' : 'max-h-0 opacity-0'}`}>
<>
<textarea
readOnly
value={requestedFields.join('\n')}
className={`p-2 border rounded-lg text-sm dark:bg-gray-700 dark:text-white ${showRequestedFields ? 'visible' : 'hidden'}`}
style={{ width: '-webkit-fill-available' }}
rows={Math.min(3, Math.max(1, requestedFields.length))}

></textarea>
</>
</div>
</div>
)}
{requestedFieldsText && requestedFields.length > 0 && verifierDomainName && (
<>
<p className="pd-2 text-gray-700 text-sm dark:text-white">
<span>
<Trans
i18nKey={requestedFields.length === 1 ? "selectCredentialPopup.descriptionFieldsSingle" : "selectCredentialPopup.descriptionFieldsMultiple"}
values={{ verifierDomainName }}
components={{ strong: <strong /> }}
/>
</span>
&nbsp;
<strong>
{requestedFieldsText}
</strong>
{requestedFields.length > 2 && (
<>
{' '}
< button onClick={handleToggleFields} className="text-primary dark:text-extra-light hover:underline inline">
{showAllFields ? `${t('selectCredentialPopup.requestedFieldsLess')}` : `${t('selectCredentialPopup.requestedFieldsMore')}`}
</button>
</>
)}.
</p>
<p className="text-gray-700 dark:text-white text-sm mt-2 mb-4">
{t('selectCredentialPopup.descriptionSelect')}
</p>
</>
)
}

<div className='flex flex-wrap justify-center flex overflow-y-auto max-h-[40vh] custom-scrollbar bg-gray-50 dark:bg-gray-800 shadow-md rounded-xl mb-2'>
{vcEntities.map(vcEntity => (
<>
<div key={vcEntity.credentialIdentifier} className="m-3 flex flex-col items-center">
<button
className="relative rounded-xl w-2/3 overflow-hidden transition-shadow shadow-md hover:shadow-xl cursor-pointer"
className={`relative rounded-xl w-2/3 overflow-hidden transition-shadow shadow-md hover:shadow-xl cursor-pointer ${selectedCredential === vcEntity.credentialIdentifier ? 'opacity-100' : 'opacity-50'}`}
onClick={() => handleClick(vcEntity.credentialIdentifier)}
aria-label={`${vcEntity.friendlyName}`}
title={t('selectCredentialPopup.credentialSelectTitle', { friendlyName: vcEntity.friendlyName })}
>
<CredentialImage key={vcEntity.credentialIdentifier} credential={vcEntity.credential}
className={"w-full object-cover rounded-xl"}
/>
<div className="absolute bottom-2 right-2" style={{ zIndex: "2000" }}>
{selectedCredential === vcEntity.credentialIdentifier ? (
<MdOutlineCheckBox size={20} className="text-white" />
) : (
<MdOutlineCheckBoxOutlineBlank size={20} className="text-white" />
)}
</div>
</button>
<div className='w-2/3 mt-2'>
<GetButton
Expand All @@ -168,12 +242,35 @@ function SelectCredentials({ showPopup, setShowPopup, setSelectionMap, conforman
</>
))}
</div>
<GetButton
content={t('common.cancel')}
onClick={handleCancel}
variant="cancel"
/>
</Modal>
<div className="flex justify-between mt-4">
<GetButton
content={t('common.cancel')}
onClick={handleCancel}
variant="cancel"
className="mr-2"
/>

<div className="flex gap-2">
{currentIndex > 0 && (
<GetButton
content={t('common.previous')}
onClick={goToPreviousSelection}
variant="secondary"
/>
)}

<GetButton
content={currentIndex < keys.length - 1 ? t('common.next') : t('common.navItemSendCredentialsSimple')}
onClick={goToNextSelection}
variant="primary"
disabled={!selectedCredential}
title={!selectedCredential ? t('selectCredentialPopup.nextButtonDisabledTitle') : ''}

/>
</div>
</div>

</Modal >
);
}

Expand Down
15 changes: 10 additions & 5 deletions src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
"navItemSendCredentials": "Send Credentials",
"navItemSendCredentialsSimple": "Send",
"navItemSettings": "Settings",
"next": "Next",
"previous": "Previous",
"save": "Save",
"submit": "submit",
"submit": "Submit",
"walletName": "wwWallet"
},
"browserSupportWarningPortal": {
Expand Down Expand Up @@ -221,12 +223,15 @@
},
"selectCredentialPopup": {
"credentialSelectTitle": "Click to select the {{friendlyName}}",
"description": "The <strong> {{verifierDomainName}} </strong> requests the following credentials. Select the one you want to present.",
"descriptionFieldsSingle": "The <strong> {{verifierDomainName}} </strong> requests the field:",
"descriptionFieldsMultiple": "The <strong> {{verifierDomainName}} </strong> requests the fields:",
"descriptionSelect": "Select the one Credential you want to present.",
"detailsHide": "Hide Details",
"detailsShow": "Show Details",
"requestedFieldsHide": "Hide Requested Fields",
"requestedFieldsShow": "Show Requested Fields",
"title": "Select an Option:"
"nextButtonDisabledTitle": "You need to select a Credential to proceed",
"requestedFieldsLess": "Show less",
"requestedFieldsMore": "Show more",
"title": "Select : "
},
"sidebar": {
"navItemLogout": "Logout",
Expand Down
Loading