@rockerone/xprnkit 0.3.8 → 0.3.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,7 +5,7 @@ type XPRNIdentityProofGateProps = {
5
5
  */
6
6
  children: React.ReactNode;
7
7
  /**
8
- * Content to render when identity proof is required but not yet obtained.
8
+ * Content to render when identity proof is not obtained.
9
9
  * If not provided, renders nothing.
10
10
  */
11
11
  fallback?: React.ReactNode;
@@ -24,11 +24,6 @@ type XPRNIdentityProofGateProps = {
24
24
  * If not provided, uses fallback.
25
25
  */
26
26
  notConnected?: React.ReactNode;
27
- /**
28
- * If true, only gates when identityProof.required is true in config.
29
- * If false (default), gates whenever identity proof is configured.
30
- */
31
- onlyWhenRequired?: boolean;
32
27
  };
33
28
  /**
34
29
  * Conditionally renders children based on identity proof status.
@@ -51,9 +46,7 @@ export declare const XPRNIdentityProofGate: React.FunctionComponent<XPRNIdentity
51
46
  * Hook to check identity proof gate status
52
47
  * Returns the same information used by XPRNIdentityProofGate
53
48
  */
54
- export declare function useIdentityProofGate(options?: {
55
- onlyWhenRequired?: boolean;
56
- }): {
49
+ export declare function useIdentityProofGate(): {
57
50
  isGateActive: boolean;
58
51
  isConnected: boolean;
59
52
  isInProgress: boolean;
@@ -17,16 +17,12 @@ import { useXPRN } from "../../providers/XPRNProvider";
17
17
  * </XPRNIdentityProofGate>
18
18
  * ```
19
19
  */
20
- export const XPRNIdentityProofGate = ({ children, fallback = null, loading, error, notConnected, onlyWhenRequired = false, }) => {
21
- const { session, identityProof, identityProofStatus, isIdentityProofEnabled, isIdentityProofRequired, } = useXPRN();
20
+ export const XPRNIdentityProofGate = ({ children, fallback = null, loading, error, notConnected, }) => {
21
+ const { session, identityProof, identityProofStatus, isIdentityProofEnabled, } = useXPRN();
22
22
  // If identity proof is not enabled, render children directly
23
23
  if (!isIdentityProofEnabled) {
24
24
  return _jsx(_Fragment, { children: children });
25
25
  }
26
- // If onlyWhenRequired is true and identity proof is not required, render children
27
- if (onlyWhenRequired && !isIdentityProofRequired) {
28
- return _jsx(_Fragment, { children: children });
29
- }
30
26
  // No session connected
31
27
  if (!session) {
32
28
  return _jsx(_Fragment, { children: notConnected ?? fallback });
@@ -51,26 +47,25 @@ XPRNIdentityProofGate.displayName = "XPRNIdentityProofGate";
51
47
  * Hook to check identity proof gate status
52
48
  * Returns the same information used by XPRNIdentityProofGate
53
49
  */
54
- export function useIdentityProofGate(options) {
55
- const { session, identityProof, identityProofStatus, isIdentityProofEnabled, isIdentityProofRequired, needsIdentityProof, requestIdentityProof, } = useXPRN();
56
- const onlyWhenRequired = options?.onlyWhenRequired ?? false;
57
- // Determine if gating should be active
58
- const isGateActive = isIdentityProofEnabled && (!onlyWhenRequired || isIdentityProofRequired);
50
+ export function useIdentityProofGate() {
51
+ const { session, identityProof, identityProofStatus, isIdentityProofEnabled, requestIdentityProof, } = useXPRN();
59
52
  // Determine current state
60
53
  const isConnected = session !== null;
61
54
  const isInProgress = identityProofStatus === "signing" || identityProofStatus === "verifying" || identityProofStatus === "validating";
62
55
  const hasError = identityProofStatus === "error" || identityProofStatus === "expired";
63
56
  const isVerified = identityProof !== null;
64
57
  // Should render protected content?
65
- const shouldRenderChildren = !isGateActive || isVerified;
58
+ const shouldRenderChildren = !isIdentityProofEnabled || isVerified;
59
+ // Needs identity proof: enabled, connected, and not yet verified
60
+ const needsIdentityProof = isIdentityProofEnabled && isConnected && !isVerified;
66
61
  return {
67
- isGateActive,
62
+ isGateActive: isIdentityProofEnabled,
68
63
  isConnected,
69
64
  isInProgress,
70
65
  hasError,
71
66
  isVerified,
72
67
  shouldRenderChildren,
73
- needsIdentityProof: isGateActive && needsIdentityProof,
68
+ needsIdentityProof,
74
69
  requestIdentityProof,
75
70
  identityProofStatus,
76
71
  };
@@ -7,7 +7,7 @@ import { useXPRN } from "../../providers/XPRNProvider";
7
7
  * Get badge styling based on identity proof status
8
8
  */
9
9
  function useIdentityProofBadgeStyle() {
10
- const { identityProofStatus, isIdentityProofEnabled, needsIdentityProof } = useXPRN();
10
+ const { identityProofStatus, isIdentityProofEnabled, identityProof, session } = useXPRN();
11
11
  return useMemo(() => {
12
12
  if (!isIdentityProofEnabled) {
13
13
  return { visible: false, className: "" };
@@ -18,6 +18,7 @@ function useIdentityProofBadgeStyle() {
18
18
  const isSuccess = identityProofStatus === "success";
19
19
  const isError = identityProofStatus === "error" || identityProofStatus === "expired";
20
20
  const isIdle = identityProofStatus === "idle";
21
+ const needsIdentityProof = session !== null && identityProof === null;
21
22
  // Build className based on status
22
23
  let className = "";
23
24
  if (isPending) {
@@ -42,12 +43,12 @@ function useIdentityProofBadgeStyle() {
42
43
  isSuccess,
43
44
  isError,
44
45
  };
45
- }, [identityProofStatus, isIdentityProofEnabled, needsIdentityProof]);
46
+ }, [identityProofStatus, isIdentityProofEnabled, identityProof, session]);
46
47
  }
47
- export const XPRNSessionName = ({ children, className, }) => {
48
- const { profile, session, config } = useXPRN();
48
+ export const XPRNSessionName = ({ children, className, showIdentityProof = false, }) => {
49
+ const { profile, session } = useXPRN();
49
50
  const badgeStyle = useIdentityProofBadgeStyle();
50
- const showBadge = (config?.identityProof?.required || config?.identityProof?.enforceOnConnect) && badgeStyle.visible;
51
+ const showBadge = showIdentityProof && badgeStyle.visible;
51
52
  const rootClasses = classNames({
52
53
  "flex gap-2 items-center justify-center": true,
53
54
  [`${className}`]: className,
@@ -35,12 +35,8 @@ export type XPRNIdentityProofConfig = {
35
35
  validationUrl?: string;
36
36
  /** Time in seconds before expiration to trigger validation (default: 300) */
37
37
  validationBuffer?: number;
38
- /** Automatically authenticate on connect (default: false) */
39
- enforceOnConnect?: boolean;
40
38
  /** Automatically re-authenticate when token expires (default: false) */
41
39
  autoReauthenticate?: boolean;
42
- /** Require identity proof for the app (default: false) */
43
- required?: boolean;
44
40
  /** Additional headers for identity proof requests */
45
41
  headers?: Record<string, string>;
46
42
  /** Request timeout in milliseconds */
@@ -58,8 +54,6 @@ export type XPRProviderConfig = {
58
54
  identityProof?: XPRNIdentityProofConfig;
59
55
  /** @deprecated Use identityProof.createUrl instead */
60
56
  authenticationUrl?: string;
61
- /** @deprecated Use identityProof.enforceOnConnect instead */
62
- enforceAuthentication?: boolean;
63
57
  };
64
58
  type XPRNProviderProps = {
65
59
  children: React.ReactNode | React.ReactNode[];
@@ -73,13 +67,9 @@ type XPRNProviderContext = {
73
67
  rpc: JsonRpc | null;
74
68
  identityProof: XPRNIdentityProof | null;
75
69
  identityProofStatus: IdentityProofStatus;
76
- /** True if identity proof is configured but not yet obtained for current session */
77
- needsIdentityProof: boolean;
78
- /** True if identity proof is configured with required=true */
79
- isIdentityProofRequired: boolean;
80
70
  /** True if identity proof is enabled (config.identityProof is defined) */
81
71
  isIdentityProofEnabled: boolean;
82
- connect: (restore?: boolean, onSession?: (session: LinkSession, link: ProtonWebLink | Link) => void, onProfile?: (profile: XPRNProfile) => void) => void;
72
+ connect: (restore?: boolean, onSession?: (session: LinkSession, link: ProtonWebLink | Link) => void, onProfile?: (profile: XPRNProfile) => void, onIdentityProof?: (proof: XPRNIdentityProof) => void, onIdentityProofError?: (error: Error) => void) => void;
83
73
  disconnect: () => Promise<void>;
84
74
  requestIdentityProof: (success: (res: XPRNIdentityProof) => void, fail: (e: any) => void) => void;
85
75
  listStoredSessions: () => StoredSessionRef[];
@@ -14,8 +14,6 @@ const XPRNContext = React.createContext({
14
14
  rpc: null,
15
15
  identityProof: null,
16
16
  identityProofStatus: "idle",
17
- needsIdentityProof: false,
18
- isIdentityProofRequired: false,
19
17
  isIdentityProofEnabled: false,
20
18
  connect: () => { },
21
19
  disconnect: async () => { },
@@ -44,26 +42,29 @@ export const XPRNProvider = ({ children, config, }) => {
44
42
  if (config.authenticationUrl) {
45
43
  return {
46
44
  createUrl: config.authenticationUrl,
47
- enforceOnConnect: config.enforceAuthentication,
48
45
  };
49
46
  }
50
47
  return null;
51
- }, [config.identityProof, config.authenticationUrl, config.enforceAuthentication]);
52
- // Helper booleans
48
+ }, [config.identityProof, config.authenticationUrl]);
49
+ // Helper boolean
53
50
  const isIdentityProofEnabled = identityProofConfig !== null;
54
- const isIdentityProofRequired = identityProofConfig?.required ?? false;
55
- const needsIdentityProof = isIdentityProofEnabled && session !== null && identityProof === null;
56
51
  // Internal refs
57
52
  const jsonRpcRef = useRef(new JsonRpc(config.endpoints));
58
53
  const errorsStackRef = useRef([]);
59
54
  const onSessionRef = useRef();
60
55
  const onProfileRef = useRef();
56
+ const onIdentityProofRef = useRef();
57
+ const onIdentityProofErrorRef = useRef();
61
58
  const isRestoringRef = useRef(false);
62
59
  const isAuthenticatingRef = useRef(false);
63
60
  const isSwitchingSessionRef = useRef(false);
64
61
  const sessionRef = useRef(null);
65
- // Keep sessionRef in sync with session state
62
+ const linkRef = useRef(null);
63
+ // Queue for pending callbacks when requestIdentityProof is called while already in progress
64
+ const pendingCallbacksRef = useRef([]);
65
+ // Keep refs in sync with state
66
66
  sessionRef.current = session;
67
+ linkRef.current = link;
67
68
  // List stored sessions from proton-web-sdk localStorage
68
69
  const listStoredSessions = useCallback(() => {
69
70
  return sessionStorage.getLinkList(config.requesterAccount);
@@ -162,7 +163,7 @@ export const XPRNProvider = ({ children, config, }) => {
162
163
  // Mark session switch as complete
163
164
  isSwitchingSessionRef.current = false;
164
165
  }, [link, config.requesterAccount, config.apiMode, identityProofConfig]);
165
- const connect = useCallback((restoreSession, onSession, onProfile) => {
166
+ const connect = useCallback((restoreSession, onSession, onProfile, onIdentityProof, onIdentityProofError) => {
166
167
  ConnectWallet({
167
168
  linkOptions: {
168
169
  endpoints: config.endpoints,
@@ -177,6 +178,8 @@ export const XPRNProvider = ({ children, config, }) => {
177
178
  }).then(res => {
178
179
  onSessionRef.current = onSession;
179
180
  onProfileRef.current = onProfile;
181
+ onIdentityProofRef.current = onIdentityProof;
182
+ onIdentityProofErrorRef.current = onIdentityProofError;
180
183
  if (res.link && res.session) {
181
184
  // Update state with new session
182
185
  setLink(res.link);
@@ -218,12 +221,15 @@ export const XPRNProvider = ({ children, config, }) => {
218
221
  });
219
222
  }, [config]);
220
223
  const disconnect = useCallback(async () => {
221
- if (!session || !link) {
224
+ // Use refs to get latest values (avoid stale closure issues)
225
+ const currentSession = sessionRef.current;
226
+ const currentLink = linkRef.current;
227
+ if (!currentSession || !currentLink) {
222
228
  console.warn("No session to disconnect");
223
229
  return;
224
230
  }
225
231
  try {
226
- await link.removeSession(config.requesterAccount, session.auth, session.chainId);
232
+ await currentLink.removeSession(config.requesterAccount, currentSession.auth, currentSession.chainId);
227
233
  }
228
234
  catch (e) {
229
235
  console.error("Error removing session:", e);
@@ -234,7 +240,7 @@ export const XPRNProvider = ({ children, config, }) => {
234
240
  setIdentityProof(null);
235
241
  setIdentityProofStatus("idle");
236
242
  // Note: Keep link reference for potential session switching
237
- }, [session, link, config.requesterAccount]);
243
+ }, [config.requesterAccount]);
238
244
  const addTxError = useCallback((message) => {
239
245
  errorsStackRef.current.push(parseTransactionErrorMessage(message));
240
246
  }, []);
@@ -249,8 +255,9 @@ export const XPRNProvider = ({ children, config, }) => {
249
255
  fail(new Error("Identity proof not configured"));
250
256
  return;
251
257
  }
258
+ // Idempotent: if already in progress, queue callbacks to be called when complete
252
259
  if (isAuthenticatingRef.current) {
253
- fail(new Error("Identity proof already in progress"));
260
+ pendingCallbacksRef.current.push({ success, fail });
254
261
  return;
255
262
  }
256
263
  isAuthenticatingRef.current = true;
@@ -278,10 +285,30 @@ export const XPRNProvider = ({ children, config, }) => {
278
285
  sessionStorage.updateIdentityProofToken(config.requesterAccount, { actor, permission }, chainId, verifyRes.token);
279
286
  }
280
287
  success(identityProofData);
288
+ // Call connect-level callback if registered
289
+ if (onIdentityProofRef.current) {
290
+ onIdentityProofRef.current(identityProofData);
291
+ }
292
+ // Call all queued callbacks with success
293
+ const pendingCallbacks = pendingCallbacksRef.current;
294
+ pendingCallbacksRef.current = [];
295
+ for (const cb of pendingCallbacks) {
296
+ cb.success(identityProofData);
297
+ }
281
298
  }
282
299
  catch (error) {
283
300
  setIdentityProofStatus("error");
284
301
  fail(error);
302
+ // Call connect-level error callback if registered
303
+ if (onIdentityProofErrorRef.current) {
304
+ onIdentityProofErrorRef.current(error instanceof Error ? error : new Error(String(error)));
305
+ }
306
+ // Call all queued callbacks with error
307
+ const pendingCallbacks = pendingCallbacksRef.current;
308
+ pendingCallbacksRef.current = [];
309
+ for (const cb of pendingCallbacks) {
310
+ cb.fail(error);
311
+ }
285
312
  }
286
313
  finally {
287
314
  isAuthenticatingRef.current = false;
@@ -347,7 +374,7 @@ export const XPRNProvider = ({ children, config, }) => {
347
374
  return false;
348
375
  }
349
376
  }, [identityProofConfig, config.requesterAccount]);
350
- // Handle token restoration and auto-authentication when session is established
377
+ // Handle token restoration when session is established (no auto-authentication)
351
378
  // Note: This effect is skipped when switchToSession is handling the identity proof state
352
379
  useEffect(() => {
353
380
  // Skip if we're in the middle of a session switch (switchToSession handles this)
@@ -355,28 +382,19 @@ export const XPRNProvider = ({ children, config, }) => {
355
382
  return;
356
383
  if (!session || identityProof)
357
384
  return;
358
- if (!identityProofConfig)
385
+ if (!identityProofConfig?.validationUrl)
359
386
  return;
360
387
  const actor = session.auth.actor.toString();
361
388
  const permission = session.auth.permission.toString();
362
389
  const chainId = session.chainId.toString();
363
- // Check if there's a stored token and try to validate it
390
+ // Check if there's a stored token and try to validate it silently
364
391
  const storedEntry = sessionStorage.get(config.requesterAccount, { actor, permission }, chainId);
365
392
  const hasStoredToken = storedEntry?.identityProofToken && typeof storedEntry.identityProofToken === 'string' && storedEntry.identityProofToken.length > 0;
366
- if (hasStoredToken && identityProofConfig.validationUrl) {
367
- // Try to validate stored token silently
368
- validateStoredToken(actor, permission, chainId).then(isValid => {
369
- if (!isValid && (identityProofConfig.enforceOnConnect || identityProofConfig.required)) {
370
- // Token invalid and identity proof is required - trigger full auth flow
371
- requestIdentityProof(() => { }, () => { });
372
- }
373
- });
374
- }
375
- else if (identityProofConfig.enforceOnConnect || identityProofConfig.required) {
376
- // No stored token (or no validation URL) but identity proof is required - trigger auth flow
377
- requestIdentityProof(() => { }, () => { });
393
+ if (hasStoredToken) {
394
+ // Try to validate stored token silently - dev handles the result via callbacks
395
+ validateStoredToken(actor, permission, chainId);
378
396
  }
379
- }, [session, identityProof, identityProofConfig, config.requesterAccount, validateStoredToken, requestIdentityProof]);
397
+ }, [session, identityProof, identityProofConfig, config.requesterAccount, validateStoredToken]);
380
398
  // Handle profile callbacks
381
399
  useEffect(() => {
382
400
  if (profile && onProfileRef.current) {
@@ -405,9 +423,6 @@ export const XPRNProvider = ({ children, config, }) => {
405
423
  // Identity proof state
406
424
  identityProof,
407
425
  identityProofStatus,
408
- // Helper booleans
409
- needsIdentityProof,
410
- isIdentityProofRequired,
411
426
  isIdentityProofEnabled,
412
427
  // Core methods
413
428
  connect,
@@ -430,8 +445,6 @@ export const XPRNProvider = ({ children, config, }) => {
430
445
  profile,
431
446
  identityProof,
432
447
  identityProofStatus,
433
- needsIdentityProof,
434
- isIdentityProofRequired,
435
448
  isIdentityProofEnabled,
436
449
  connect,
437
450
  disconnect,
@@ -28,7 +28,15 @@ export async function createIdentityProof(session, options) {
28
28
  time: new Date().toISOString().slice(0, -1),
29
29
  });
30
30
  // Sign the transaction without broadcasting
31
- const txResult = await session.transact({ actions: [authenticationAction] }, { broadcast: false });
31
+ let txResult;
32
+ try {
33
+ txResult = await session.transact({ actions: [authenticationAction] }, { broadcast: false });
34
+ }
35
+ catch (error) {
36
+ // Re-throw with clearer context for wallet signing failures
37
+ const message = error instanceof Error ? error.message : String(error);
38
+ throw new Error(`Wallet signing failed: ${message}`);
39
+ }
32
40
  // Check for abort after wallet interaction
33
41
  if (options?.signal?.aborted) {
34
42
  throw new DOMException("Aborted", "AbortError");
@@ -34,12 +34,8 @@ export type IdentityProofConfig = {
34
34
  validationUrl?: string;
35
35
  /** Time in seconds before expiration to trigger validation (default: 300 = 5 minutes) */
36
36
  validationBuffer?: number;
37
- /** Automatically authenticate on connect (default: false) */
38
- enforceOnConnect?: boolean;
39
37
  /** Automatically re-authenticate when token expires (default: false) */
40
38
  autoReauthenticate?: boolean;
41
- /** Require identity proof for the app - enables helper booleans and gate component (default: false) */
42
- required?: boolean;
43
39
  /** Additional headers for identity proof requests */
44
40
  headers?: Record<string, string>;
45
41
  /** Request timeout in milliseconds */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rockerone/xprnkit",
3
- "version": "0.3.8",
3
+ "version": "0.3.10",
4
4
  "source": "src/index.ts",
5
5
  "main": "build/index.js",
6
6
  "type": "module",