Skip to content

Commit

Permalink
feat(ui): [Frontend] Multi-sig IPEX: Group Status in Credential Issua…
Browse files Browse the repository at this point in the history
…nce (#783)

* feat(ui): issue group credential

* fix(ui): fix group member on android UI

* fix(ui): group member status in small device

* fix(ui): add credential when reach threshold

* fix(ui): update pending screen and improve ux when loading page

* fix(ui): fix UT

* feat(ui): change member component to share component

* fix(core): remove notification after credential has confirmed (#787)

* fix(core): remove notification after credential has confirmed

* fix: resolve comment

* fix: update unittest for AppWrapper

* fix: update unittest for emit event NotificationRemoved

* fix(ui): remove unsupported method

---------

Co-authored-by: Vu Van Duc <[email protected]>
Co-authored-by: Bao Hoang <[email protected]>
  • Loading branch information
3 people authored Oct 17, 2024
1 parent 8e3a8dc commit bc9c736
Show file tree
Hide file tree
Showing 18 changed files with 589 additions and 56 deletions.
9 changes: 9 additions & 0 deletions src/core/agent/event.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ enum EventTypes {
ConnectionStateChanged = "ConnectionStateChanged",
AcdcStateChanged = "AcdcStateChanged",
KeriaStatusChanged = "KeriaStatusChanged",
NotificationRemoved = "NotificationRemoved",
}

interface NotificationAddedEvent extends BaseEventEmitter {
Expand Down Expand Up @@ -66,6 +67,13 @@ interface KeriaStatusChangedEvent extends BaseEventEmitter {
};
}

interface NotificationRemovedEvent extends BaseEventEmitter {
type: typeof EventTypes.NotificationRemoved;
payload: {
keriaNotif: KeriaNotification;
};
}

export type {
NotificationAddedEvent,
OperationCompleteEvent,
Expand All @@ -74,5 +82,6 @@ export type {
AcdcStateChangedEvent,
KeriaStatusChangedEvent,
OperationAddedEvent,
NotificationRemovedEvent,
};
export { EventTypes };
31 changes: 29 additions & 2 deletions src/core/agent/services/keriaNotificationService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1918,15 +1918,15 @@ describe("Long running operation tracker", () => {
displayName: "holder",
signifyName: "764c965c-d997-4842-b940-aebd514fce42",
multisigManageAid: "EAL7pX9Hklc_iq7pkVYSjAilCfQX3sr5RbX76AxYs2UH",
createdAt: new Date(),
createdAt: new Date("2024-08-01T10:36:17.814Z"),
updatedAt: new Date(),
});

notificationStorage.findAllByQuery = jest.fn().mockResolvedValue([
{
type: "NotificationRecord",
id: "id",
createdAt: new Date(),
createdAt: new Date("2024-08-01T10:36:17.814Z"),
a: {
r: NotificationRoute.ExnIpexGrant,
d: "EIDUavcmyHBseNZAdAHR3SF8QMfX1kSJ3Ct0OqS0-HCW",
Expand All @@ -1947,6 +1947,23 @@ describe("Long running operation tracker", () => {
CredentialStatus.CONFIRMED
);
expect(notificationStorage.deleteById).toBeCalledWith("id");
expect(eventEmitter.emit).toBeCalledTimes(1);
expect(eventEmitter.emit).toHaveBeenCalledWith({
type: EventTypes.NotificationRemoved,
payload: {
keriaNotif: {
a: {
d: "EIDUavcmyHBseNZAdAHR3SF8QMfX1kSJ3Ct0OqS0-HCW",
r: NotificationRoute.ExnIpexGrant,
},
connectionId: "EEFjBBDcUM2IWpNF7OclCme_bE76yKE3hzULLzTOFE8E",
createdAt: "2024-08-01T10:36:17.814Z",
id: "id",
multisigId: undefined,
read: true,
},
},
});
expect(operationPendingStorage.deleteById).toBeCalledTimes(1);
});

Expand Down Expand Up @@ -2365,6 +2382,16 @@ describe("Long running operation tracker", () => {
);
});

test("Should register callback for NotificationRemoved event", () => {
const callback = jest.fn();
keriaNotificationService.onRemoveNotification(callback);

expect(eventEmitter.on).toHaveBeenCalledWith(
EventTypes.NotificationRemoved,
callback
);
});

test("Should retry connection when \"Failed to fetch\" error occurs when process operation", async () => {
const operationRecord = {
type: "OperationPendingRecord",
Expand Down
19 changes: 19 additions & 0 deletions src/core/agent/services/keriaNotificationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
EventTypes,
OperationCompleteEvent,
OperationAddedEvent,
NotificationRemovedEvent,
} from "../event.types";
import { deleteNotificationRecordById } from "./utils";
import { CredentialService } from "./credentialService";
Expand Down Expand Up @@ -904,6 +905,20 @@ class KeriaNotificationService extends AgentService {
notification.id,
notification.a.r as NotificationRoute
);

this.props.eventEmitter.emit<NotificationRemovedEvent>({
type: EventTypes.NotificationRemoved,
payload: {
keriaNotif: {
id: notification.id,
createdAt: notification.createdAt.toISOString(),
a: notification.a,
multisigId: notification.multisigId,
connectionId: notification.connectionId,
read: notification.read,
},
},
});
}
}
await this.credentialService.markAcdc(
Expand Down Expand Up @@ -1048,6 +1063,10 @@ class KeriaNotificationService extends AgentService {
onLongOperationComplete(callback: (event: OperationCompleteEvent) => void) {
this.props.eventEmitter.on(EventTypes.OperationComplete, callback);
}

onRemoveNotification(callback: (event: NotificationRemovedEvent) => void) {
this.props.eventEmitter.on(EventTypes.NotificationRemoved, callback);
}
}

export { KeriaNotificationService };
4 changes: 3 additions & 1 deletion src/locales/en/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1017,7 +1017,8 @@
"cancel": "Cancel",
"back": "Back",
"choosecredential": "Choose credential",
"providecredential": "Provide credential"
"providecredential": "Provide credential",
"addcred": "Add Credential"
},
"credential": {
"request": {
Expand Down Expand Up @@ -1050,6 +1051,7 @@
"from": "from",
"credentialpending": "Credential pending",
"credentialdetailbutton": "Credential details",
"members": "Group member",
"alert": {
"textdecline": "Are you sure you want to decline this credential issuance? You won't be able to change your decision later."
}
Expand Down
1 change: 1 addition & 0 deletions src/ui/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ jest.mock("../core/agent/agent", () => ({
startNotification: jest.fn(),
onNewNotification: jest.fn(),
onLongOperationComplete: jest.fn(),
onRemoveNotification: jest.fn(),
},
onKeriaStatusStateChanged: jest.fn(),
peerConnectionMetadataStorage: {
Expand Down
1 change: 1 addition & 0 deletions src/ui/components/AppWrapper/AppWrapper.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ jest.mock("../../../core/agent/agent", () => ({
getAllNotifications: jest.fn(),
onNewNotification: jest.fn(),
onLongOperationComplete: jest.fn(),
onRemoveNotification: jest.fn(),
},
getKeriaOnlineStatus: jest.fn(),
onKeriaStatusStateChanged: jest.fn(),
Expand Down
6 changes: 5 additions & 1 deletion src/ui/components/AppWrapper/AppWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -411,12 +411,16 @@ const AppWrapper = (props: { children: ReactNode }) => {
}
);
Agent.agent.keriaNotifications.onNewNotification((event) => {
notificatiStateChanged(event.payload.keriaNotif, dispatch);
notificatiStateChanged(event, dispatch);
});

Agent.agent.keriaNotifications.onLongOperationComplete((event) => {
signifyOperationStateChangeHandler(event.payload, dispatch);
});

Agent.agent.keriaNotifications.onRemoveNotification((event) => {
notificatiStateChanged(event, dispatch);
});
};

const initApp = async () => {
Expand Down
20 changes: 17 additions & 3 deletions src/ui/components/AppWrapper/coreEventListeners.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,34 @@
import { Agent } from "../../../core/agent/agent";
import { KeriaNotification } from "../../../core/agent/agent.types";
import {
EventTypes,
NotificationAddedEvent,
NotificationRemovedEvent,
} from "../../../core/agent/event.types";
import { OperationPendingRecordType } from "../../../core/agent/records/operationPendingRecord.type";
import { useAppDispatch } from "../../../store/hooks";
import { updateIsPending } from "../../../store/reducers/identifiersCache";
import {
addNotification,
deleteNotification,
setNotificationsCache,
} from "../../../store/reducers/notificationsCache";
import { setToastMsg } from "../../../store/reducers/stateCache";
import { ToastMsgType } from "../../globals/types";

const notificatiStateChanged = (
notif: KeriaNotification,
event: NotificationRemovedEvent | NotificationAddedEvent,
dispatch: ReturnType<typeof useAppDispatch>
) => {
dispatch(addNotification(notif));
switch (event.type) {
case EventTypes.NotificationAdded:
dispatch(addNotification(event.payload.keriaNotif));
break;
case EventTypes.NotificationRemoved:
dispatch(deleteNotification(event.payload.keriaNotif));
break;
default:
break;
}
};

const signifyOperationStateChangeHandler = async (
Expand Down
3 changes: 2 additions & 1 deletion src/ui/components/Scanner/Scanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
} from "@ionic/react";
import { scanOutline } from "ionicons/icons";
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
import { v4 as uuidv4 } from "uuid";
import { Agent } from "../../../core/agent/agent";
import {
ConnectionStatus,
Expand Down Expand Up @@ -314,7 +315,7 @@ const Scanner = forwardRef(
return;
}

const pendingId = crypto.randomUUID();
const pendingId = uuidv4();
dispatch(
updateOrAddConnectionCache({
id: pendingId,
Expand Down
14 changes: 9 additions & 5 deletions src/ui/pages/NotificationDetails/NotificationDetails.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,15 @@ describe("Notification Detail", () => {
</Provider>
</IonReactMemoryRouter>
);
expect(
getAllByText(
EN_TRANSLATIONS.tabs.notifications.details.credential.receive.title
)[0]
).toBeVisible();

await waitFor(() => {
expect(
getAllByText(
EN_TRANSLATIONS.tabs.notifications.details.credential.receive.title
)[0]
).toBeVisible();
});

expect(
getByText(EN_TRANSLATIONS.tabs.notifications.details.buttons.close)
).toBeVisible();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
.member {
--padding-start: 0;
--inner-padding-end: 0;
--padding-top: 0.75rem;
--padding-bottom: 0.75rem;
--min-height: auto;

.member-avatar {
width: 1.5rem;
height: 1.5rem;
}

.member-name {
width: 70%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 500;
font-size: 1rem;

&.md {
margin-inline-end: 0;
}
}

.status {
border-radius: 50%;
padding: 0.3125rem;
width: 0.875rem;
height: 0.875rem;

&.accepted {
background: var(--ion-color-primary-gradient);
}

&.rejected {
background: #ff7d7a;
}

&.waiting {
background: rgba(var(--ion-color-dark-grey-rgb), 0.5);
}
}

@media screen and (min-width: 250px) and (max-width: 370px) {
--padding-top: 0.5rem;
--padding-bottom: 0.5rem;

.member-name {
font-size: 0.875rem;
}

.status {
padding: 0.125rem;
width: 0.675rem;
height: 0.675rem;

&.md {
margin-inline-start: 0;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { IonIcon, IonItem, IonText } from "@ionic/react";
import { checkmark, closeOutline, hourglassOutline } from "ionicons/icons";
import { useMemo } from "react";
import KeriLogo from "../../../../assets/images/KeriGeneric.jpg";
import { combineClassNames } from "../../../../utils/style";
import "./MultisigMember.scss";
import { MemberAcceptStatus, MemberProps } from "./MultisigMember.types";

const MultisigMember = ({ name, status }: MemberProps) => {
const statusClasses = combineClassNames("status", {
accepted: status === MemberAcceptStatus.Accepted,
waiting: status === MemberAcceptStatus.Waiting,
rejected: status === MemberAcceptStatus.Rejected,
});

const icon = useMemo(() => {
switch (status) {
case MemberAcceptStatus.Accepted:
return checkmark;
case MemberAcceptStatus.Rejected:
return closeOutline;
default:
return hourglassOutline;
}
}, [status]);

return (
<IonItem
lines="none"
className="member"
>
<img
className="member-avatar"
slot="start"
src={KeriLogo}
alt="keri"
/>
<IonText
slot="start"
className="member-name"
>
{name}
</IonText>
<IonIcon
slot="end"
icon={icon}
className={statusClasses}
/>
</IonItem>
);
};

export { MultisigMember };
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
enum MemberAcceptStatus {
Accepted,
Waiting,
Rejected,
}

interface MemberProps {
name: string;
status: MemberAcceptStatus;
}

export type { MemberProps };

export { MemberAcceptStatus };
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./MultisigMember";
export * from "./MultisigMember.types";
Loading

0 comments on commit bc9c736

Please sign in to comment.