diff --git a/nuwa-kit/typescript/packages/identity-kit-web/README.md b/nuwa-kit/typescript/packages/identity-kit-web/README.md
index c1c79804..4cb7a66d 100644
--- a/nuwa-kit/typescript/packages/identity-kit-web/README.md
+++ b/nuwa-kit/typescript/packages/identity-kit-web/README.md
@@ -4,12 +4,14 @@ Web extensions for Nuwa Identity Kit, providing browser-friendly implementations
## Features
-- Multiple KeyStore implementations:
+- **Popup Blocker Handling** - Automatically detects and handles popup blockers with multiple fallback strategies
+- **Multiple KeyStore implementations**:
- `LocalStorageKeyStore` - Uses browser's localStorage for key storage
- `IndexedDBKeyStore` - Uses IndexedDB for key storage, supports CryptoKey objects
-- `DeepLinkManager` - Manages deep link authentication flow
-- `IdentityKitWeb` - High-level API for web applications
-- React hooks (optional) - `useIdentityKit` hook for React applications
+- **DeepLinkManager** - Manages deep link authentication flow with CSRF protection
+- **IdentityKitWeb** - High-level API for web applications
+- **React hooks** - `useIdentityKit` and `useIdentityKitWithFallbacks` hooks for React applications
+- **Session Key Scopes** - Support for custom permission scopes in authentication flow
## Installation
@@ -27,9 +29,33 @@ import { IdentityKitWeb } from '@nuwa-ai/identity-kit-web';
// Initialize the SDK
const nuwa = await IdentityKitWeb.init();
-// Connect to Cadop
+// Basic connect (backward compatible - returns void)
await nuwa.connect();
+// OR: Connect with popup blocker handling (returns detailed result)
+const result = await nuwa.connect({
+ scopes: ['0xdefi::swap::*'],
+ fallbackMethod: 'copy', // Options: 'redirect' | 'copy' | 'manual'
+ returnResult: true // Enable detailed result
+});
+
+if (result) { // Only when returnResult=true
+ switch (result.action) {
+ case 'popup':
+ console.log('Popup opened successfully');
+ break;
+ case 'copy':
+ console.log('URL copied to clipboard');
+ break;
+ case 'redirect':
+ console.log('Page will redirect');
+ break;
+ case 'manual':
+ console.log('Manual handling required:', result.url);
+ break;
+ }
+}
+
// Handle callback (in your callback page)
await nuwa.handleCallback(location.search);
@@ -51,12 +77,28 @@ import { useIdentityKit } from '@nuwa-ai/identity-kit-web';
function MyComponent() {
const { state, connect, sign, verify, logout } = useIdentityKit();
+ const handleConnect = async () => {
+ // Option 1: Backward compatible (no detailed result)
+ await connect();
+
+ // Option 2: With detailed result for popup handling
+ const result = await connectWithResult({
+ fallbackMethod: 'copy'
+ });
+
+ if (result.action === 'copy' && result.success) {
+ alert('Authorization link copied to clipboard!');
+ } else if (result.action === 'manual') {
+ alert(`Please open this link: ${result.url}`);
+ }
+ };
+
if (state.isConnecting) {
return
Connecting...
;
}
if (!state.isConnected) {
- return ;
+ return ;
}
return (
@@ -68,6 +110,40 @@ function MyComponent() {
}
```
+### Enhanced React Hook (Recommended)
+
+```tsx
+import { useIdentityKitWithFallbacks } from '@nuwa-ai/identity-kit-web';
+
+function MyComponent() {
+ const { state, connectWithFallbacks, logout } = useIdentityKitWithFallbacks();
+
+ const handleConnect = async () => {
+ await connectWithFallbacks({
+ scopes: ['0xdefi::swap::*'],
+ onPopupBlocked: () => alert('Popup blocked by browser'),
+ onUrlCopied: () => alert('Link copied! Paste in new tab'),
+ onManualUrl: (url) => alert(`Please click: ${url}`)
+ });
+ };
+
+ if (state.isConnected) {
+ return (
+
+
Connected: {state.agentDid}
+
+
+ );
+ }
+
+ return (
+
+ );
+}
+```
+
### Advanced Usage
```typescript
diff --git a/nuwa-kit/typescript/packages/identity-kit-web/src/IdentityKitWeb.ts b/nuwa-kit/typescript/packages/identity-kit-web/src/IdentityKitWeb.ts
index e9f7fc78..57d97168 100644
--- a/nuwa-kit/typescript/packages/identity-kit-web/src/IdentityKitWeb.ts
+++ b/nuwa-kit/typescript/packages/identity-kit-web/src/IdentityKitWeb.ts
@@ -116,8 +116,20 @@ export class IdentityKitWeb {
/**
* Connect to Cadop
* This will open a new window with the Cadop add-key page
+ * For backward compatibility, returns void by default. Set returnResult=true to get detailed result.
*/
- async connect(options?: { scopes?: string[] }): Promise {
+ async connect(options?: {
+ scopes?: string[];
+ /** Fallback method when popup is blocked */
+ fallbackMethod?: 'redirect' | 'copy' | 'manual';
+ /** Return detailed result instead of void (for popup blocker handling) */
+ returnResult?: boolean;
+ }): Promise {
const idFragment = this.generateIdFragment();
const { url } = await this.deepLinkManager.buildAddKeyUrl({
@@ -126,8 +138,91 @@ export class IdentityKitWeb {
scopes: options?.scopes,
});
- // Open the URL in a new window/tab
- window.open(url, '_blank');
+ try {
+ // Try to open popup first
+ const popup = window.open(url, '_blank', 'noopener,noreferrer');
+
+ // Add a small timeout to allow popup.closed to update
+ const isPopupBlocked = await new Promise((resolve) => {
+ setTimeout(() => {
+ resolve(!popup || popup.closed || typeof popup.closed === 'undefined');
+ }, 100); // 100ms timeout
+ });
+
+ if (isPopupBlocked) {
+ // Popup was blocked, try fallback method
+ const result = await this.handlePopupBlocked(url, options?.fallbackMethod);
+ return options?.returnResult ? result : undefined;
+ }
+
+ // Popup opened successfully
+ const result = {
+ action: 'popup' as const,
+ url,
+ success: true
+ };
+ return options?.returnResult ? result : undefined;
+ } catch (error) {
+ // Error opening popup, try fallback method
+ const result = await this.handlePopupBlocked(url, options?.fallbackMethod, error instanceof Error ? error.message : String(error));
+ return options?.returnResult ? result : undefined;
+ }
+ }
+
+ /**
+ * Handle popup blocker scenarios with fallback methods
+ */
+ private async handlePopupBlocked(
+ url: string,
+ fallbackMethod?: 'redirect' | 'copy' | 'manual',
+ originalError?: string
+ ): Promise<{
+ action: 'redirect' | 'copy' | 'manual';
+ url: string;
+ success: boolean;
+ error?: string;
+ }> {
+ const method = fallbackMethod || 'redirect'; // Default to redirect
+
+ switch (method) {
+ case 'redirect':
+ // Redirect current page to CADOP
+ window.location.href = url;
+ return {
+ action: 'redirect',
+ url,
+ success: true
+ };
+
+ case 'copy':
+ // Copy URL to clipboard and show user notification
+ try {
+ await navigator.clipboard.writeText(url);
+ return {
+ action: 'copy',
+ url,
+ success: true
+ };
+ } catch (clipboardError) {
+ // Fallback to manual if clipboard fails
+ return {
+ action: 'manual',
+ url,
+ success: false,
+ error: `Clipboard access failed: ${clipboardError instanceof Error ? clipboardError.message : String(clipboardError)}`
+ };
+ }
+
+ case 'manual':
+ default:
+ // Return URL for manual handling by the application
+ return {
+ action: 'manual',
+ url,
+ success: false,
+ error: originalError ? `Popup blocked: ${originalError}` : 'Popup blocked by browser'
+ };
+ }
}
/**
diff --git a/nuwa-kit/typescript/packages/identity-kit-web/src/react/useIdentityKit.ts b/nuwa-kit/typescript/packages/identity-kit-web/src/react/useIdentityKit.ts
index 1c95d0fb..c5d8f42e 100644
--- a/nuwa-kit/typescript/packages/identity-kit-web/src/react/useIdentityKit.ts
+++ b/nuwa-kit/typescript/packages/identity-kit-web/src/react/useIdentityKit.ts
@@ -10,15 +10,49 @@ export interface IdentityKitState {
error: string | null;
}
+export interface ConnectResult {
+ action: 'popup' | 'redirect' | 'copy' | 'manual';
+ url?: string;
+ success: boolean;
+ error?: string;
+}
+
export interface IdentityKitHook {
state: IdentityKitState;
- connect: (options?: { scopes?: string[] }) => Promise;
+ connect: (options?: {
+ scopes?: string[];
+ fallbackMethod?: 'redirect' | 'copy' | 'manual';
+ }) => Promise;
+ /** Enhanced connect method that returns detailed result for popup blocker handling */
+ connectWithResult: (options?: {
+ scopes?: string[];
+ fallbackMethod?: 'redirect' | 'copy' | 'manual';
+ }) => Promise;
sign: (payload: any) => Promise;
verify: (sig: NIP1SignedObject) => Promise;
logout: () => Promise;
sdk: IdentityKitWeb | null;
}
+/**
+ * Enhanced hook that provides user-friendly popup blocker handling
+ */
+export interface IdentityKitHookWithFallbacks extends IdentityKitHook {
+ /**
+ * Connect with automatic fallback handling and user notifications
+ * Returns a promise that resolves with instructions for the user
+ */
+ connectWithFallbacks: (options?: {
+ scopes?: string[];
+ /** Callback to show user notifications */
+ onPopupBlocked?: (result: ConnectResult) => void;
+ /** Callback to show clipboard success notification */
+ onUrlCopied?: (url: string) => void;
+ /** Callback to show manual URL handling */
+ onManualUrl?: (url: string) => void;
+ }) => Promise;
+}
+
export interface UseIdentityKitOptions {
appName?: string;
cadopDomain?: string;
@@ -102,8 +136,11 @@ export function useIdentityKit(options: UseIdentityKitOptions = {}): IdentityKit
return () => window.removeEventListener('message', handleMessage);
}, [sdk]);
- // Connect action
- const connect = useCallback(async (options?: { scopes?: string[] }) => {
+ // Connect action (backward compatible)
+ const connect = useCallback(async (options?: {
+ scopes?: string[];
+ fallbackMethod?: 'redirect' | 'copy' | 'manual';
+ }) => {
if (!sdk) {
setState(prev => ({ ...prev, error: 'SDK not initialized' }));
return;
@@ -112,9 +149,64 @@ export function useIdentityKit(options: UseIdentityKitOptions = {}): IdentityKit
setState(prev => ({ ...prev, isConnecting: true, error: null }));
try {
- await sdk.connect(options);
- // Actual connection result will be handled via postMessage in callback
- setState(prev => ({ ...prev, isConnecting: false }));
+ const result = await sdk.connect({ ...options, returnResult: true });
+ if (result.success) {
+ setState(prev => ({ ...prev, isConnecting: false }));
+ } else if (options?.fallbackMethod) {
+ // Handle fallback method if connection fails
+ setState(prev => ({ ...prev, isConnecting: false, error: `Fallback method triggered: ${options.fallbackMethod}` }));
+ } else {
+ setState(prev => ({ ...prev, isConnecting: false, error: 'Connection failed without fallback' }));
+ }
+ } catch (error) {
+ setState({
+ isConnected: false,
+ isConnecting: false,
+ agentDid: null,
+ keyId: null,
+ error: `Connection failed: ${error instanceof Error ? error.message : String(error)}`,
+ });
+ }
+ }, [sdk]);
+
+ // Enhanced connect action with result
+ const connectWithResult = useCallback(async (options?: {
+ scopes?: string[];
+ fallbackMethod?: 'redirect' | 'copy' | 'manual';
+ }): Promise => {
+ if (!sdk) {
+ setState(prev => ({ ...prev, error: 'SDK not initialized' }));
+ return {
+ action: 'manual',
+ success: false,
+ error: 'SDK not initialized'
+ };
+ }
+
+ setState(prev => ({ ...prev, isConnecting: true, error: null }));
+
+ try {
+ const result = await sdk.connect({
+ ...options,
+ returnResult: true // Request detailed result
+ });
+
+ if (result && typeof result === 'object' && 'action' in result) {
+ // Only set connecting to false if action is not redirect
+ // (redirect will navigate away from current page)
+ if (result.action !== 'redirect') {
+ setState(prev => ({ ...prev, isConnecting: false }));
+ }
+
+ return result as ConnectResult;
+ } else {
+ setState(prev => ({ ...prev, isConnecting: false, error: 'Unexpected response from SDK' }));
+ return {
+ action: 'manual',
+ success: false,
+ error: 'Unexpected response from SDK'
+ };
+ }
} catch (error) {
setState({
isConnected: false,
@@ -123,6 +215,12 @@ export function useIdentityKit(options: UseIdentityKitOptions = {}): IdentityKit
keyId: null,
error: `Connection failed: ${error instanceof Error ? error.message : String(error)}`,
});
+
+ return {
+ action: 'manual',
+ success: false,
+ error: `Connection failed: ${error instanceof Error ? error.message : String(error)}`
+ };
}
}, [sdk]);
@@ -159,5 +257,59 @@ export function useIdentityKit(options: UseIdentityKitOptions = {}): IdentityKit
});
}, [sdk]);
- return { state, connect, sign, verify, logout, sdk };
+ return { state, connect, connectWithResult, sign, verify, logout, sdk };
+}
+
+/**
+ * Enhanced version of useIdentityKit with automatic popup blocker fallbacks
+ */
+export function useIdentityKitWithFallbacks(options: UseIdentityKitOptions = {}): IdentityKitHookWithFallbacks {
+ const baseHook = useIdentityKit(options);
+
+ const connectWithFallbacks = useCallback(async (connectOptions?: {
+ scopes?: string[];
+ onPopupBlocked?: (result: ConnectResult) => void;
+ onUrlCopied?: (url: string) => void;
+ onManualUrl?: (url: string) => void;
+ }): Promise => {
+ // Try popup with detailed result
+ const result = await baseHook.connectWithResult({
+ scopes: connectOptions?.scopes,
+ fallbackMethod: 'copy' // Default to copy fallback
+ });
+
+ // Handle different scenarios
+ switch (result.action) {
+ case 'popup':
+ // Success, no additional handling needed
+ break;
+
+ case 'redirect':
+ // Page will redirect, no notification needed
+ break;
+
+ case 'copy':
+ if (result.success && result.url) {
+ connectOptions?.onUrlCopied?.(result.url);
+ } else {
+ // Copy failed, show manual URL
+ connectOptions?.onManualUrl?.(result.url || '');
+ }
+ break;
+
+ case 'manual':
+ connectOptions?.onPopupBlocked?.(result);
+ if (result.url) {
+ connectOptions?.onManualUrl?.(result.url);
+ }
+ break;
+ }
+
+ return result;
+ }, [baseHook.connectWithResult]);
+
+ return {
+ ...baseHook,
+ connectWithFallbacks
+ };
}
\ No newline at end of file