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

Manage Errors #12

Merged
merged 5 commits into from
May 3, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 38 additions & 21 deletions js-library/src/request-verifiable-presentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,18 @@ const iiWindows: Map<FlowId, Window | null> = new Map();
const createFlowId = (): FlowId => nanoid();

type FlowId = string;
const currentFlows = new Set<FlowId>();
/**
* State Machine of the flow:
* /--------------------------------------------- Identity Provider closes window -----------------------------------------------------\
* |--- User closes window ----\---------------------------------\---------------------------------\ |
lmuntaner marked this conversation as resolved.
Show resolved Hide resolved
* | | | | |
* (Off) -- open window --> (ongoing) -- receive ready msg --> (ongoing) -- send request msg --> (ongoing) -- receive response msg --> (finalized)
*
* We care about how the window is closed because in case of the user closing the window, we want to call `onError` with `ERROR_USER_INTERRUPT`.
* But we can't listen to the event of the user closing the window. The only way we know about it is by checking at intervals whether the window is closed.
*/
type Status = "initialized" | "started" | "ongoing" | "finalized";
const currentFlows = new Map<FlowId, Status>();

const INTERRUPT_CHECK_INTERVAL = 500;
export const ERROR_USER_INTERRUPT = "UserInterrupt";
Expand Down Expand Up @@ -106,7 +117,8 @@ const isExpectedNotification = ({
evnt: MessageEvent;
flowId: FlowId;
}): boolean =>
evnt.data?.method === VC_START_METHOD && !currentFlows.has(flowId);
evnt.data?.method === VC_START_METHOD &&
currentFlows.get(flowId) === "initialized";

// As defined in the spec: https://github.com/dfinity/internet-identity/blob/main/docs/vc-spec.md#3-get-a-response
const isKnownFlowMessage = ({
Expand All @@ -115,7 +127,8 @@ const isKnownFlowMessage = ({
}: {
evnt: MessageEvent;
flowId: FlowId;
}): boolean => currentFlows.has(evnt.data?.id) && evnt.data?.id === flowId;
}): boolean =>
currentFlows.get(evnt.data?.id) === "ongoing" && evnt.data?.id === flowId;

export const requestVerifiablePresentation = ({
onSuccess,
Expand Down Expand Up @@ -155,35 +168,36 @@ export const requestVerifiablePresentation = ({
}

if (isExpectedNotification({ evnt, flowId: currentFlowId })) {
currentFlows.set(currentFlowId, "started");
const request = createCredentialRequest({
derivationOrigin,
issuerData,
credentialData,
nextFlowId: currentFlowId,
});
currentFlows.add(request.id);
evnt.source?.postMessage(request, { targetOrigin: evnt.origin });
currentFlows.set(nextFlowId, "ongoing");
return;
}

if (isKnownFlowMessage({ evnt, flowId: currentFlowId })) {
try {
// Identity Provider closes the window after sending the response.
// We are checking in an interval whether the user closed the window.
// Removing the flow from currentFlows prevents interpreting that the user interrupted the flow.
// To check this in a test, I put `onSuccess` call inside a setTimeout
// to simulate that handling took longer than the check for the user closing the window.
// Setting the status to "finalized" to avoid calling `onError` in `checkInterruption` while we are dealing with the response.
// To check this in the test "should not call onError when window closes after successful flow"
lmuntaner marked this conversation as resolved.
Show resolved Hide resolved
// I put `onSuccess` call inside a setTimeout to simulate that handling took long
// and force the `checkValidation` to see that the window was closed.
// Then I advanced the time in the test and checked that `onSuccess` was called, and not `onError`.
// The test "should not call onError when window closes after successful flow" was failing
// if we didn't remove the curret flow id from currentFlows.
currentFlows.delete(evnt.data.id);
currentFlows.set(evnt.data.id, "finalized");
const credentialResponse = getCredentialResponse(evnt);
onSuccess(credentialResponse);
} catch (err) {
const message =
err instanceof Error ? err.message : JSON.stringify(err);
onError(`Error getting the verifiable credential: ${message}`);
} finally {
currentFlows.delete(currentFlowId);
iiWindows.get(currentFlowId)?.close();
iiWindows.delete(currentFlowId);
window.removeEventListener("message", handleCurrentFlow);
Expand All @@ -196,28 +210,31 @@ export const requestVerifiablePresentation = ({
);
};
const nextFlowId = createFlowId();
currentFlows.set(nextFlowId, "initialized");
const handleCurrentFlow = handleFlowFactory(nextFlowId);
window.addEventListener("message", handleCurrentFlow);
const url = new URL(identityProvider);
url.pathname = "vc-flow/";
// As defined in the spec: https://github.com/dfinity/internet-identity/blob/main/docs/vc-spec.md#1-load-ii-in-a-new-window
const iiWindow = window.open(url, "idpWindow", windowOpenerFeatures);
// Check if the _idpWindow is closed by user.
const checkInterruption = (flowId: FlowId): void => {
// The _idpWindow is opened and not yet closed by the client
if (iiWindow) {
if (iiWindow.closed && currentFlows.has(flowId)) {
if (iiWindow !== null) {
iiWindows.set(nextFlowId, iiWindow);
// Check if the _idpWindow is closed by user.
const checkInterruption = (flowId: FlowId): void => {
// The _idpWindow is opened and not yet closed by the client
if (
iiWindow.closed &&
lmuntaner marked this conversation as resolved.
Show resolved Hide resolved
currentFlows.has(flowId) &&
currentFlows.get(flowId) !== "finalized"
) {
currentFlows.delete(flowId);
iiWindows.delete(flowId);
window.removeEventListener("message", handleCurrentFlow);
onError(ERROR_USER_INTERRUPT);
} else {
} else if (iiWindows.has(flowId)) {
setTimeout(() => checkInterruption(flowId), INTERRUPT_CHECK_INTERVAL);
}
}
};
checkInterruption(nextFlowId);
if (iiWindow !== null) {
iiWindows.set(nextFlowId, iiWindow);
};
checkInterruption(nextFlowId);
}
};
37 changes: 35 additions & 2 deletions js-library/src/tests/request-verifiable-presentation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ describe("Request Verifiable Credentials function", () => {
};

beforeEach(() => {
window.open = vi.fn();
window.open = vi.fn().mockReturnValue({ close: vi.fn(), closed: false });
vi.spyOn(console, "warn").mockImplementation(() => undefined);
vi.useFakeTimers();
});
Expand Down Expand Up @@ -339,6 +339,7 @@ describe("Request Verifiable Credentials function", () => {
window.open = vi.fn().mockImplementation(() => {
const iiWindow = {
closed: false,
close: vi.fn(),
};
// User closes the window after 1 second
setTimeout(() => {
Expand Down Expand Up @@ -366,14 +367,46 @@ describe("Request Verifiable Credentials function", () => {
expect(onError).toHaveBeenCalledWith(ERROR_USER_INTERRUPT);
});

it("calls onError if user closes identity provider window even before the flow starts", async () => {
const onError = vi.fn();
const DURATION_BEFORE_USER_CLOSES_WINDOW = 1000;
window.open = vi.fn().mockImplementation(() => {
const iiWindow = {
closed: false,
close: vi.fn(),
};
// User closes the window after 1 second
setTimeout(() => {
iiWindow.closed = true;
}, DURATION_BEFORE_USER_CLOSES_WINDOW);

return iiWindow;
});
requestVerifiablePresentation({
onSuccess: unreachableFn,
onError,
credentialData,
issuerData,
derivationOrigin: undefined,
identityProvider,
});

vi.advanceTimersByTime(DURATION_BEFORE_USER_CLOSES_WINDOW / 2);
expect(onError).not.toHaveBeenCalled();

vi.advanceTimersByTime(DURATION_BEFORE_USER_CLOSES_WINDOW / 2);
expect(onError).toHaveBeenCalledTimes(1);
expect(onError).toHaveBeenCalledWith(ERROR_USER_INTERRUPT);
});

it("should not call onError when window closes after successful flow", async () => {
const onSuccess = vi.fn();
const onError = vi.fn();
const iiWindow = {
closed: false,
close() {
this.closed = true;
}
},
};
window.open = vi.fn().mockReturnValue(iiWindow);
requestVerifiablePresentation({
Expand Down
Loading