Skip to content

Commit b44133c

Browse files
authored
Fixed: handle dialog closure case in confirmation flow (#195)
* fix(gator-permissions-snap): handle dialog closure case in confirmation flow When the confirmation dialog is closed (result is null), the promise should resolve with false to indicate that the user did not grant confirmation. This adds proper handling for the dialog closure scenario and includes a corresponding test case. This change ensures that the confirmation dialog properly handles the case where the user closes the dialog without making a decision, returning false instead of undefined or other unexpected values. The test case verifies this behavior.
1 parent cc2caf8 commit b44133c

File tree

2 files changed

+63
-52
lines changed

2 files changed

+63
-52
lines changed

packages/gator-permissions-snap/src/core/confirmation.tsx

Lines changed: 44 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -68,17 +68,13 @@ export class ConfirmationDialog {
6868
const interfaceId = this.#interfaceId;
6969

7070
const isConfirmationGranted = new Promise<boolean>((resolve, reject) => {
71-
// cleanup can't be defined before the click handlers, so cannot be const
72-
// eslint-disable-next-line prefer-const
73-
let cleanup: () => Promise<void>;
74-
7571
const { unbind: unbindGrantButtonClick } = this.#userEventDispatcher.on({
7672
elementName: ConfirmationDialog.#grantButton,
7773
eventType: UserInputEventType.ButtonClickEvent,
7874
interfaceId,
7975
handler: async () => {
80-
await cleanup();
81-
76+
await this.#cleanup();
77+
this.#interfaceId = undefined;
8278
resolve(true);
8379
},
8480
});
@@ -88,38 +84,18 @@ export class ConfirmationDialog {
8884
eventType: UserInputEventType.ButtonClickEvent,
8985
interfaceId,
9086
handler: async () => {
91-
await cleanup();
92-
87+
await this.#cleanup();
88+
this.#interfaceId = undefined;
9389
resolve(false);
9490
},
9591
});
9692

97-
cleanup = async () => {
98-
unbindGrantButtonClick();
99-
unbindCancelButtonClick();
100-
101-
// clear our stored unbind handler reference
102-
this.#unbindHandlers = undefined;
103-
104-
try {
105-
await this.#snaps.request({
106-
method: 'snap_resolveInterface',
107-
params: {
108-
id: interfaceId,
109-
value: {},
110-
},
111-
});
112-
} catch (error) {
113-
const reason = error as Error;
114-
reject(reason);
115-
}
116-
};
117-
11893
// store hooks so we can close/reject programmatically on error
11994
this.#unbindHandlers = () => {
12095
unbindGrantButtonClick();
12196
unbindCancelButtonClick();
12297
};
98+
12399
this.#decisionReject = reject;
124100

125101
// we don't await this, because we only want to present the dialog, and
@@ -131,6 +107,14 @@ export class ConfirmationDialog {
131107
id: interfaceId,
132108
},
133109
})
110+
.then(async (result) => {
111+
// Should resolve with false when dialog is closed.
112+
if (result === null) {
113+
await this.#cleanup(false);
114+
115+
resolve(false);
116+
}
117+
})
134118
.catch((error) => {
135119
const reason = error as Error;
136120
reject(reason);
@@ -142,6 +126,33 @@ export class ConfirmationDialog {
142126
};
143127
}
144128

129+
/**
130+
* Clean up event handlers and optionally resolve the interface.
131+
* @param resolveInterface - Whether to resolve the interface. Defaults to true.
132+
*/
133+
async #cleanup(resolveInterface = true): Promise<void> {
134+
// Unbind any listeners to avoid leaks
135+
if (this.#unbindHandlers) {
136+
try {
137+
this.#unbindHandlers();
138+
} catch {
139+
// ignore
140+
} finally {
141+
this.#unbindHandlers = undefined;
142+
}
143+
}
144+
145+
if (resolveInterface && this.#interfaceId) {
146+
await this.#snaps.request({
147+
method: 'snap_resolveInterface',
148+
params: {
149+
id: this.#interfaceId,
150+
value: {},
151+
},
152+
});
153+
}
154+
}
155+
145156
#buildConfirmation(): JSX.Element {
146157
return (
147158
<Container>
@@ -203,30 +214,11 @@ export class ConfirmationDialog {
203214
return;
204215
}
205216

206-
// Unbind any listeners to avoid leaks
207-
if (this.#unbindHandlers) {
208-
try {
209-
this.#unbindHandlers();
210-
} catch {
211-
// ignore
212-
} finally {
213-
this.#unbindHandlers = undefined;
214-
}
215-
}
217+
// Clean up handlers and resolve interface
218+
await this.#cleanup(true);
216219

217-
try {
218-
await this.#snaps.request({
219-
method: 'snap_resolveInterface',
220-
params: {
221-
id: this.#interfaceId,
222-
value: {},
223-
},
224-
});
225-
} catch (error) {
226-
// If closing fails, still reject with the original reason
227-
} finally {
228-
this.#interfaceId = undefined;
229-
}
220+
// Clear interface ID after cleanup
221+
this.#interfaceId = undefined;
230222

231223
if (this.#decisionReject) {
232224
this.#decisionReject(reason);

packages/gator-permissions-snap/test/core/confirmation.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,25 @@ describe('ConfirmationDialog', () => {
140140
expect(result).toStrictEqual({ isConfirmationGranted: false });
141141
});
142142

143+
it('should resolve with false when dialog is closed', async () => {
144+
// Simulate dialog closure
145+
mockSnaps.request.mockResolvedValueOnce(null);
146+
147+
const awaitingUserDecision =
148+
confirmationDialog.displayConfirmationDialogAndAwaitUserDecision();
149+
150+
expect(mockUserEventDispatcher.on).toHaveBeenCalledTimes(2);
151+
expect(mockUnbindFunctions).toHaveLength(2);
152+
153+
const result = await awaitingUserDecision;
154+
155+
mockUnbindFunctions.forEach((mockUnbindFn) => {
156+
expect(mockUnbindFn).toHaveBeenCalledTimes(1);
157+
});
158+
159+
expect(result).toStrictEqual({ isConfirmationGranted: false });
160+
});
161+
143162
it('should clean up event listeners after decision', async () => {
144163
const awaitingUserDecision =
145164
confirmationDialog.displayConfirmationDialogAndAwaitUserDecision();

0 commit comments

Comments
 (0)