Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
88 changes: 82 additions & 6 deletions nuwa-kit/typescript/packages/identity-kit-web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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);

Expand All @@ -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 <div>Connecting...</div>;
}

if (!state.isConnected) {
return <button onClick={connect}>Connect</button>;
return <button onClick={handleConnect}>Connect</button>;
}

return (
Expand All @@ -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 (
<div>
<p>Connected: {state.agentDid}</p>
<button onClick={logout}>Logout</button>
</div>
);
}

return (
<button onClick={handleConnect} disabled={state.isConnecting}>
{state.isConnecting ? 'Connecting...' : 'Connect Wallet'}
</button>
);
}
```

### Advanced Usage

```typescript
Expand Down
101 changes: 98 additions & 3 deletions nuwa-kit/typescript/packages/identity-kit-web/src/IdentityKitWeb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
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<void | {
action: 'popup' | 'redirect' | 'copy' | 'manual';
url?: string;
success: boolean;
error?: string;
}> {
const idFragment = this.generateIdFragment();

const { url } = await this.deepLinkManager.buildAddKeyUrl({
Expand All @@ -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'
};
}
}

/**
Expand Down
Loading