diff --git a/apps/client-web/src/App.tsx b/apps/client-web/src/App.tsx
index 2f28ed6..8052f14 100644
--- a/apps/client-web/src/App.tsx
+++ b/apps/client-web/src/App.tsx
@@ -25,7 +25,11 @@ function App() {
}, [dispatch]);
useEffect(() => {
- if (state.advice && state.connectionState === ConnectionState.CONNECTING) {
+ if (
+ state.advice &&
+ (state.connectionState === ConnectionState.CONNECTED ||
+ state.connectionState === ConnectionState.CONNECTING)
+ ) {
state.advice.showClient();
}
}, [state.advice, state.connectionState]);
@@ -64,7 +68,7 @@ function App() {
{state.connectionState === ConnectionState.CONNECTING && (
@@ -395,7 +399,7 @@ function Authorize({
diff --git a/apps/client-web/src/client/utils.ts b/apps/client-web/src/client/utils.ts
index 9837fc5..21a932c 100644
--- a/apps/client-web/src/client/utils.ts
+++ b/apps/client-web/src/client/utils.ts
@@ -12,13 +12,13 @@ export function loadPODsFromStorage(): Record {
try {
const serializedCollections = JSON.parse(storedSerializedPODs) as unknown;
const parsed = v.parse(
- v.record(v.string(), v.array(v.string())),
+ v.record(v.string(), v.array(v.any())),
serializedCollections
);
for (const [collectionId, serializedPODs] of Object.entries(parsed)) {
result[collectionId] = new PODCollection(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
- serializedPODs.map((str) => POD.fromJSON(JSON.parse(str)))
+ serializedPODs.map((obj) => POD.fromJSON(obj))
);
}
} catch (e) {
diff --git a/apps/client-web/src/state.tsx b/apps/client-web/src/state.tsx
index 136a4ee..439358e 100644
--- a/apps/client-web/src/state.tsx
+++ b/apps/client-web/src/state.tsx
@@ -17,10 +17,10 @@ import { PODCollectionManager } from "./client/pod_collection_manager";
import { getIdentity } from "./client/utils";
export enum ConnectionState {
- DISCONNECTED,
- CONNECTING,
- CONNECTED,
- AUTHORIZED
+ DISCONNECTED = "DISCONNECTED",
+ CONNECTING = "CONNECTING",
+ CONNECTED = "CONNECTED",
+ AUTHORIZED = "AUTHORIZED"
}
export const StateContext = createContext<
@@ -93,8 +93,7 @@ export type ClientState = {
export type ClientAction =
| {
- type: "login";
- loggedIn: boolean;
+ type: "connect";
}
| {
type: "authorize";
@@ -122,13 +121,13 @@ export type ClientAction =
type: "clear-proof-in-progress";
}
| {
- type: "logout";
+ type: "cancel-connection";
};
export function clientReducer(state: ClientState, action: ClientAction) {
switch (action.type) {
- case "login":
- return { ...state, loggedIn: action.loggedIn };
+ case "connect":
+ return { ...state, connectionState: ConnectionState.CONNECTED };
case "authorize":
if (!state.zapp || !state.zappOrigin) {
throw new Error("No zapp or zapp origin");
@@ -227,7 +226,7 @@ export function clientReducer(state: ClientState, action: ClientAction) {
...state,
proofInProgress: undefined
};
- case "logout":
+ case "cancel-connection":
return initializeState();
}
}
diff --git a/packages/app-connector-react/src/context.tsx b/packages/app-connector-react/src/context.tsx
index 23c6a95..c353473 100644
--- a/packages/app-connector-react/src/context.tsx
+++ b/packages/app-connector-react/src/context.tsx
@@ -1,6 +1,12 @@
import { type ParcnetAPI, type Zapp, connect } from "@parcnet-js/app-connector";
import type { ReactNode } from "react";
-import { createContext, useCallback, useRef, useState } from "react";
+import {
+ createContext,
+ useCallback,
+ useContext,
+ useRef,
+ useState
+} from "react";
export enum ClientConnectionState {
DISCONNECTED = "DISCONNECTED",
@@ -13,6 +19,16 @@ export const ParcnetClientContext = createContext<
ParcnetClientContextType | undefined
>(undefined);
+export const useParcnetClientContext = (): ParcnetClientContextType => {
+ const context = useContext(ParcnetClientContext);
+ if (!context) {
+ throw new Error(
+ "useParcnetClientContext must be used within a ParcnetClientProvider"
+ );
+ }
+ return context;
+};
+
type ParcnetContextBase = {
zapp: Zapp;
connect: () => Promise;
@@ -79,7 +95,8 @@ export function ParcnetIframeProvider({
}
if (
url !== connectUrl ||
- connectionState === ClientConnectionState.DISCONNECTED
+ connectionState === ClientConnectionState.DISCONNECTED ||
+ connectionState === ClientConnectionState.ERROR
) {
setConnectionState(ClientConnectionState.CONNECTING);
try {
diff --git a/packages/app-connector/src/adapters/hosted.ts b/packages/app-connector/src/adapters/hosted.ts
index 81c595e..4d3997c 100644
--- a/packages/app-connector/src/adapters/hosted.ts
+++ b/packages/app-connector/src/adapters/hosted.ts
@@ -2,6 +2,7 @@ import type { Zapp } from "@parcnet-js/client-rpc";
import { InitializationMessageType } from "@parcnet-js/client-rpc";
import { createNanoEvents } from "nanoevents";
import { ParcnetAPI } from "../api_wrapper.js";
+import { UserCancelledConnectionError } from "../errors.js";
import { ParcnetRPCConnector } from "../rpc_client.js";
import type { DialogController, ModalEvents } from "./iframe.js";
import { postWindowMessage } from "./iframe.js";
@@ -28,7 +29,7 @@ export function connectToHost(zapp: Zapp): Promise {
const emitter = createNanoEvents();
- return new Promise((resolve) => {
+ return new Promise((resolve, reject) => {
// Create a new MessageChannel to communicate with the parent window
const chan = new MessageChannel();
@@ -42,9 +43,14 @@ export function connectToHost(zapp: Zapp): Promise {
// promise and return the API wrapper to the caller.
// See below for how the other port of the message channel is sent to
// the client.
- client.start(() => {
- resolve(new ParcnetAPI(client, emitter));
- });
+ client.start(
+ () => {
+ resolve(new ParcnetAPI(client, emitter));
+ },
+ () => {
+ reject(new UserCancelledConnectionError());
+ }
+ );
// Send the other port of the message channel to the client
postWindowMessage(
diff --git a/packages/app-connector/src/adapters/iframe.ts b/packages/app-connector/src/adapters/iframe.ts
index 35ffe66..0d92582 100644
--- a/packages/app-connector/src/adapters/iframe.ts
+++ b/packages/app-connector/src/adapters/iframe.ts
@@ -6,6 +6,7 @@ import type {
import { InitializationMessageType } from "@parcnet-js/client-rpc";
import { createNanoEvents } from "nanoevents";
import { ParcnetAPI } from "../api_wrapper.js";
+import { UserCancelledConnectionError } from "../errors.js";
import { ParcnetRPCConnector } from "../rpc_client.js";
export type ModalEvents = {
@@ -118,7 +119,10 @@ export function connect(
iframe.style.height = "100%";
iframe.src = normalizedUrl.toString();
- return new Promise((resolve) => {
+ return new Promise((resolve, reject) => {
+ const unsubscribeCloseEvent = emitter.on("close", () => {
+ reject(new UserCancelledConnectionError());
+ });
iframe.addEventListener(
"load",
() => {
@@ -132,12 +136,21 @@ export function connect(
);
// Tell the RPC client to start. It will call the function we pass in
// when the connection is ready, at which point we can resolve the
- // promise and return the API wrapper to the caller.
+ // promise and return the API wrapper to the caller. Alternatively,
+ // the user can cancel the connection, in which case we reject the
+ // promise.
// See below for how the other port of the message channel is sent to
// the client.
- client.start(() => {
- resolve(new ParcnetAPI(client, emitter));
- });
+ client.start(
+ () => {
+ unsubscribeCloseEvent();
+ resolve(new ParcnetAPI(client, emitter));
+ },
+ () => {
+ unsubscribeCloseEvent();
+ reject(new UserCancelledConnectionError());
+ }
+ );
if (iframe.contentWindow) {
const contentWindow = iframe.contentWindow;
diff --git a/packages/app-connector/src/errors.ts b/packages/app-connector/src/errors.ts
index 4ff8d6f..dd286b2 100644
--- a/packages/app-connector/src/errors.ts
+++ b/packages/app-connector/src/errors.ts
@@ -15,3 +15,9 @@ export class ClientDisconnectedError extends Error {
super("Client disconnected");
}
}
+
+export class UserCancelledConnectionError extends Error {
+ constructor() {
+ super("User cancelled connection");
+ }
+}
diff --git a/packages/app-connector/src/rpc_client.ts b/packages/app-connector/src/rpc_client.ts
index f904ae4..77e3971 100644
--- a/packages/app-connector/src/rpc_client.ts
+++ b/packages/app-connector/src/rpc_client.ts
@@ -266,8 +266,8 @@ export class ParcnetRPCConnector implements ParcnetRPC, ParcnetEvents {
*
* @param onConnect - Callback to call when the client is ready.
*/
- public start(onConnect: () => void): void {
- const eventLoop = this.main(onConnect);
+ public start(onConnect: () => void, onCancel: () => void): void {
+ const eventLoop = this.main(onConnect, onCancel);
eventLoop.next();
// Set up a listener for messages from the client
@@ -304,7 +304,10 @@ export class ParcnetRPCConnector implements ParcnetRPC, ParcnetEvents {
*
* @param onConnect - Callback to call when the client is ready.
*/
- private *main(onConnect: () => void): Generator {
+ private *main(
+ onConnect: () => void,
+ onCancel: () => void
+ ): Generator {
// Loop indefinitely until we get a PARCNET_CLIENT_READY message
// In the meantime, we will handle PARCNET_CLIENT_SHOW and
// PARCNET_CLIENT_HIDE, as these may be necessary for the client to allow
@@ -321,6 +324,9 @@ export class ParcnetRPCConnector implements ParcnetRPC, ParcnetEvents {
this.#dialogController.show();
} else if (event.type === RPCMessageType.PARCNET_CLIENT_HIDE) {
this.#dialogController.close();
+ } else if (event.type === RPCMessageType.PARCNET_CLIENT_CANCEL) {
+ onCancel();
+ return;
}
}
diff --git a/packages/client-helpers/src/connection/advice.ts b/packages/client-helpers/src/connection/advice.ts
index f325014..2ef6d2f 100644
--- a/packages/client-helpers/src/connection/advice.ts
+++ b/packages/client-helpers/src/connection/advice.ts
@@ -21,4 +21,5 @@ export interface ConnectorAdvice {
result: SubscriptionUpdateResult,
subscriptionSerial: number
): void;
+ cancel(): void;
}
diff --git a/packages/client-helpers/src/connection/iframe.ts b/packages/client-helpers/src/connection/iframe.ts
index 0f13c93..ddcd19d 100644
--- a/packages/client-helpers/src/connection/iframe.ts
+++ b/packages/client-helpers/src/connection/iframe.ts
@@ -50,6 +50,12 @@ export class AdviceChannel implements ConnectorAdvice {
});
}
+ public cancel(): void {
+ this.port.postMessage({
+ type: RPCMessageType.PARCNET_CLIENT_CANCEL
+ });
+ }
+
public subscriptionUpdate(
{ update, subscriptionId }: SubscriptionUpdateResult,
subscriptionSerial: number
diff --git a/packages/client-rpc/src/protocol.ts b/packages/client-rpc/src/protocol.ts
index 3519d08..0497f63 100644
--- a/packages/client-rpc/src/protocol.ts
+++ b/packages/client-rpc/src/protocol.ts
@@ -13,6 +13,7 @@ export enum RPCMessageType {
PARCNET_CLIENT_INVOKE_RESULT = "zupass-client-invoke-result",
PARCNET_CLIENT_INVOKE_ERROR = "zupass-client-invoke-error",
PARCNET_CLIENT_READY = "zupass-client-ready",
+ PARCNET_CLIENT_CANCEL = "zupass-client-cancel",
PARCNET_CLIENT_SHOW = "zupass-client-show",
PARCNET_CLIENT_HIDE = "zupass-client-hide",
PARCNET_CLIENT_SUBSCRIPTION_UPDATE = "zupass-client-subscription-update"
@@ -57,6 +58,9 @@ export const RPCMessageSchema = v.variant("type", [
v.object({
type: v.literal(RPCMessageType.PARCNET_CLIENT_READY)
}),
+ v.object({
+ type: v.literal(RPCMessageType.PARCNET_CLIENT_CANCEL)
+ }),
v.object({
type: v.literal(RPCMessageType.PARCNET_CLIENT_SHOW)
}),