Skip to content

Commit

Permalink
Support concurrent flows (#9)
Browse files Browse the repository at this point in the history
# Motivation

The main motivation is to allow the library to start multiple concurrent
flows.

# Changes

* Store opened windows in a Map instead of one single instance.
* Pass the flowId to the helper instead of creating it inside the
helper.
* Scope the flowId per handler so that each handler is about one flow.

# Tests

* Test that the function supports two concurrent flows.
  • Loading branch information
lmuntaner authored May 2, 2024
1 parent 8761ab7 commit c3d3d24
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 9 deletions.
32 changes: 23 additions & 9 deletions js-library/src/request-verifiable-presentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export type CredentialsRequest = {
* Helper functions
*/
// TODO: Support multiple flows at the same time.
let iiWindow: Window | null = null;
const iiWindows: Map<FlowId, Window | null> = new Map();
const createFlowId = (): FlowId => nanoid();

type FlowId = string;
Expand All @@ -48,12 +48,13 @@ const createCredentialRequest = ({
issuerData,
derivationOrigin,
credentialData: { credentialSpec, credentialSubject },
nextFlowId,
}: {
issuerData: IssuerData;
derivationOrigin: string | undefined;
credentialData: CredentialRequestData;
nextFlowId: FlowId;
}): CredentialsRequest => {
const nextFlowId = createFlowId();
return {
id: nextFlowId,
jsonrpc: JSON_RPC_VERSION,
Expand Down Expand Up @@ -98,17 +99,24 @@ export const requestVerifiablePresentation = ({
derivationOrigin: string | undefined;
identityProvider: string;
}) => {
const handleFlow = (evnt: MessageEvent) => {
const handleFlowFactory = (currentFlowId: FlowId) => (evnt: MessageEvent) => {
// Check how AuthClient does it: https://github.com/dfinity/agent-js/blob/a51bd5b837fd5f98daca5a45dfc4a060a315e62e/packages/auth-client/src/index.ts#L504
if (evnt.data?.method === "vc-flow-ready") {
if (
evnt.data?.method === "vc-flow-ready" &&
!currentFlows.has(currentFlowId)
) {
const request = createCredentialRequest({
derivationOrigin,
issuerData,
credentialData,
nextFlowId: currentFlowId,
});
currentFlows.add(request.id);
evnt.source?.postMessage(request, { targetOrigin: evnt.origin });
} else if (currentFlows.has(evnt.data?.id)) {
} else if (
currentFlows.has(evnt.data?.id) &&
evnt.data?.id === currentFlowId
) {
try {
const credential = getCredential(evnt);
onSuccess(credential);
Expand All @@ -118,15 +126,21 @@ export const requestVerifiablePresentation = ({
onError(`Error getting the verifiable credential: ${message}`);
} finally {
currentFlows.delete(evnt.data.id);
iiWindow?.close();
window.removeEventListener("message", handleFlow);
iiWindows.get(currentFlowId)?.close();
iiWindows.delete(currentFlowId);
window.removeEventListener("message", handleCurrentFlow);
}
}
};
const nextFlowId = createFlowId();
const handleCurrentFlow = handleFlowFactory(nextFlowId);
// TODO: Check if user closed the window and return an error.
// Check how AuthClient does it: https://github.com/dfinity/agent-js/blob/a51bd5b837fd5f98daca5a45dfc4a060a315e62e/packages/auth-client/src/index.ts#L489
window.addEventListener("message", handleFlow);
window.addEventListener("message", handleCurrentFlow);
const url = new URL(identityProvider);
url.pathname = "vc-flow/";
iiWindow = window.open(url, "idpWindow", windowOpenerFeatures);
const iiWindow = window.open(url, "idpWindow", windowOpenerFeatures);
if (iiWindow !== null) {
iiWindows.set(nextFlowId, iiWindow);
}
};
46 changes: 46 additions & 0 deletions js-library/src/tests/request-verifiable-presentation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,52 @@ describe("Request Verifiable Credentials function", () => {
);
});

it("supports multiple concurrent flows", async () => {
const onSuccess1 = vi.fn();
const closeWindow1 = vi.fn();
const onSuccess2 = vi.fn();
const closeWindow2 = vi.fn();
window.open = vi
.fn()
.mockReturnValueOnce({ close: closeWindow1 })
.mockReturnValueOnce({ close: closeWindow2 });
requestVerifiablePresentation({
onSuccess: onSuccess1,
onError: unreachableFn,
credentialData,
issuerData,
derivationOrigin: undefined,
identityProvider,
});
const {
request: { id: id1 },
} = await startVcFlow();
expect(onSuccess1).not.toHaveBeenCalled();
requestVerifiablePresentation({
onSuccess: onSuccess2,
onError: unreachableFn,
credentialData,
issuerData,
derivationOrigin: undefined,
identityProvider,
});
const {
request: { id: id2 },
} = await startVcFlow();
mockMessageFromIdentityProvider(
vcVerifiablePresentationMessageSuccess(id2),
);
expect(onSuccess2).toHaveBeenCalledTimes(1);
expect(closeWindow2).toHaveBeenCalledTimes(1);
expect(onSuccess1).not.toHaveBeenCalled();
expect(closeWindow1).not.toHaveBeenCalled();
mockMessageFromIdentityProvider(
vcVerifiablePresentationMessageSuccess(id1),
);
expect(onSuccess1).toHaveBeenCalledTimes(1);
expect(closeWindow1).toHaveBeenCalledTimes(1);
});

// TODO: Add functionality after refactor.
it.skip("ignores messages from other origins than identity provider", () =>
new Promise<void>((done) => done()));
Expand Down

0 comments on commit c3d3d24

Please sign in to comment.