diff --git a/packages/core/src/qr-handlers/builtin/index.ts b/packages/core/src/qr-handlers/builtin/index.ts new file mode 100644 index 00000000..a36f8428 --- /dev/null +++ b/packages/core/src/qr-handlers/builtin/index.ts @@ -0,0 +1,30 @@ +/** + * Built-in QR Code Handlers + * + * This module provides pre-built handlers for common protocols used in + * decentralized identity wallets. These handlers can be used directly or + * extended for custom behavior. + * + * ## Available Handlers + * + * - **OID4VCHandler**: OpenID for Verifiable Credentials (OID4VC) + * + * ## Usage + * + * ```typescript + * import { OID4VCHandler } from '@docknetwork/wallet-sdk-core/src/qr-handlers/builtin'; + * + * const handler = new OID4VCHandler({ + * onImportCredential: async (uri) => { + * // Your credential import logic + * return { success: true }; + * }, + * }); + * + * processor.registerHandler(handler); + * ``` + * + * @module qr-handlers/builtin + */ + +export * from './oid4vc-handler'; \ No newline at end of file diff --git a/packages/core/src/qr-handlers/builtin/oid4vc-handler.ts b/packages/core/src/qr-handlers/builtin/oid4vc-handler.ts new file mode 100644 index 00000000..82c9c8b1 --- /dev/null +++ b/packages/core/src/qr-handlers/builtin/oid4vc-handler.ts @@ -0,0 +1,198 @@ +import { + QRCodeContext, + QRCodeHandler, + QRCodeHandlerResult, +} from '../types'; + +/** + * Configuration options for OID4VC handler + */ +export interface OID4VCHandlerConfig { + /** + * URI prefixes to match (default: ['openid-credential-offer://']) + */ + uriPrefixes?: string[]; + + /** + * Priority for this handler (default: 5) + */ + priority?: number; + + /** + * Callback to handle credential import + * This is where app-specific logic (navigation, UI, etc.) should be implemented + * + * @param uri - The OID4VC URI to process + * @param context - The full QR code context + * @returns Result of the import operation + */ + onImportCredential: ( + uri: string, + context: QRCodeContext, + ) => Promise; +} + +/** + * Result from importing an OID4VC credential + */ +export interface OID4VCImportResult { + /** + * Whether the import was successful + */ + success: boolean; + + /** + * Credential data if import succeeded + */ + credential?: any; + + /** + * Error if import failed + */ + error?: Error; + + /** + * Additional metadata + */ + metadata?: Record; +} + +/** + * Built-in handler for OID4VC (OpenID for Verifiable Credentials) URIs + * + * This is a generic handler that can be configured with app-specific callbacks + * for importing credentials. The handler itself only handles protocol detection + * and delegates the actual import logic to the configured callback. + * + * ## Example Usage + * + * ```typescript + * import { OID4VCHandler } from '@docknetwork/wallet-sdk-core/src/qr-handlers/builtin'; + * import { getCredentialProvider } from '@docknetwork/wallet-sdk-react-native'; + * + * const handler = new OID4VCHandler({ + * onImportCredential: async (uri, context) => { + * try { + * // Use SDK to import credential + * await getCredentialProvider().importCredentialFromURI({ + * uri, + * didProvider: getDIDProvider(), + * getAuthCode: async (authUrl) => { + * // App-specific auth handling + * return await showAuthWebView(authUrl); + * }, + * }); + * + * return { success: true }; + * } catch (error) { + * return { + * success: false, + * error: error instanceof Error ? error : new Error(String(error)), + * }; + * } + * }, + * }); + * + * processor.registerHandler(handler); + * ``` + * + * ## Handler Priority + * + * Default priority: 5 (very high) + * This ensures OID4VC URIs are checked before other credential handlers. + * + * @category Built-in Handlers + */ +export class OID4VCHandler implements QRCodeHandler { + id = 'oid4vc'; + priority: number; + + private uriPrefixes: string[]; + private onImportCredential: ( + uri: string, + context: QRCodeContext, + ) => Promise; + + constructor(config: OID4VCHandlerConfig) { + if (!config.onImportCredential) { + throw new Error('onImportCredential callback is required'); + } + + this.priority = config.priority ?? 5; + this.uriPrefixes = config.uriPrefixes ?? ['openid-credential-offer://']; + this.onImportCredential = config.onImportCredential; + } + + /** + * Check if this is an OID4VC URI + * + * Matches URIs that start with any of the configured prefixes. + * By default, matches: openid-credential-offer:// + * + * @param context - The QR code context + * @returns True if this handler can process the URI + */ + canHandle(context: QRCodeContext): boolean { + return this.uriPrefixes.some(prefix => context.data.startsWith(prefix)); + } + + /** + * Process the OID4VC credential offer URI + * + * Delegates to the configured onImportCredential callback for actual processing. + * This allows apps to implement their own navigation, UI, and error handling. + * + * @param context - The QR code context + * @returns Result of the processing + */ + async handle(context: QRCodeContext): Promise { + try { + const result = await this.onImportCredential(context.data, context); + + return { + success: result.success, + data: result.credential, + error: result.error, + metadata: { + type: 'oid4vc', + uri: context.data, + ...result.metadata, + }, + }; + } catch (error) { + const err = error instanceof Error ? error : new Error(String(error)); + return { + success: false, + error: err, + metadata: { + type: 'oid4vc', + uri: context.data, + }, + }; + } + } +} + +/** + * Create an OID4VC handler with custom configuration + * + * This is a convenience factory function for creating an OID4VC handler. + * + * @param config - Handler configuration + * @returns Configured OID4VC handler + * + * @example + * ```typescript + * const handler = createOID4VCHandler({ + * onImportCredential: async (uri) => { + * // Your import logic + * return { success: true }; + * }, + * }); + * ``` + */ +export function createOID4VCHandler( + config: OID4VCHandlerConfig, +): OID4VCHandler { + return new OID4VCHandler(config); +} \ No newline at end of file diff --git a/packages/core/src/qr-handlers/index.ts b/packages/core/src/qr-handlers/index.ts new file mode 100644 index 00000000..d4b5b186 --- /dev/null +++ b/packages/core/src/qr-handlers/index.ts @@ -0,0 +1,76 @@ +/** + * QR Code Handler System + * + * This module provides a generic, extensible system for processing QR codes + * in decentralized identity wallet applications. + * + * ## Overview + * + * The QR handler system is built around these core concepts: + * + * - **QRCodeContext**: Contains parsed QR data (raw string, JSON, URLs) + * - **QRCodeHandler**: Interface for implementing specific QR code processors + * - **QRCodeProcessor**: Manages handler registration and execution + * - **ProcessOptions**: Configuration for how QR codes are processed + * + * ## Basic Usage + * + * ```typescript + * import { createQRCodeProcessor } from '@docknetwork/wallet-sdk-core'; + * + * // Create processor + * const processor = createQRCodeProcessor(); + * + * // Register handlers + * processor.registerHandler({ + * id: 'my-handler', + * priority: 10, + * canHandle: (context) => context.data.startsWith('myprotocol://'), + * handle: async (context) => { + * // Process the QR code + * return { success: true }; + * } + * }); + * + * // Process QR code + * const result = await processor.process(scannedData); + * ``` + * + * ## Custom Handlers + * + * Handlers can be implemented as classes or objects: + * + * ```typescript + * class MyCustomHandler implements QRCodeHandler { + * id = 'my-custom-handler'; + * priority = 20; + * + * canHandle(context: QRCodeContext): boolean { + * return context.jsonData?.type === 'my-type'; + * } + * + * async handle(context: QRCodeContext): Promise { + * // Your processing logic + * return { success: true, data: processedData }; + * } + * } + * + * processor.registerHandler(new MyCustomHandler()); + * ``` + * + * ## Built-in Handlers + * + * The SDK provides built-in handlers for common protocols: + * - OID4VC (OpenID for Verifiable Credentials) + * - OID4VP (OpenID for Verifiable Presentations) + * - DIDComm (Decentralized Identity Communication) + * + * These handlers are available in separate modules and can be imported + * and registered as needed. + * + * @module qr-handlers + */ + +export * from './types'; +export * from './processor'; +export * from './builtin'; \ No newline at end of file diff --git a/packages/core/src/qr-handlers/processor.test.ts b/packages/core/src/qr-handlers/processor.test.ts new file mode 100644 index 00000000..2d045c5d --- /dev/null +++ b/packages/core/src/qr-handlers/processor.test.ts @@ -0,0 +1,514 @@ +import {DefaultQRCodeProcessor, createQRCodeProcessor} from './processor'; +import { + QRCodeContext, + QRCodeHandler, + QRCodeHandlerResult, +} from './types'; + +describe('DefaultQRCodeProcessor', () => { + let processor: DefaultQRCodeProcessor; + + beforeEach(() => { + processor = new DefaultQRCodeProcessor(); + }); + + describe('Handler Registration', () => { + it('should register a handler successfully', () => { + const handler: QRCodeHandler = { + id: 'test-handler', + canHandle: () => true, + handle: async () => ({success: true}), + }; + + processor.registerHandler(handler); + expect(processor.getHandlers()).toHaveLength(1); + expect(processor.getHandler('test-handler')).toBe(handler); + }); + + it('should throw error when registering handler with duplicate ID', () => { + const handler1: QRCodeHandler = { + id: 'test-handler', + canHandle: () => true, + handle: async () => ({success: true}), + }; + + const handler2: QRCodeHandler = { + id: 'test-handler', + canHandle: () => true, + handle: async () => ({success: true}), + }; + + processor.registerHandler(handler1); + expect(() => processor.registerHandler(handler2)).toThrow( + 'Handler with id "test-handler" is already registered', + ); + }); + + it('should unregister a handler successfully', () => { + const handler: QRCodeHandler = { + id: 'test-handler', + canHandle: () => true, + handle: async () => ({success: true}), + }; + + processor.registerHandler(handler); + expect(processor.getHandlers()).toHaveLength(1); + + const removed = processor.unregisterHandler('test-handler'); + expect(removed).toBe(true); + expect(processor.getHandlers()).toHaveLength(0); + }); + + it('should return false when unregistering non-existent handler', () => { + const removed = processor.unregisterHandler('non-existent'); + expect(removed).toBe(false); + }); + + it('should clear all handlers', () => { + processor.registerHandler({ + id: 'handler-1', + canHandle: () => true, + handle: async () => ({success: true}), + }); + processor.registerHandler({ + id: 'handler-2', + canHandle: () => true, + handle: async () => ({success: true}), + }); + + expect(processor.getHandlers()).toHaveLength(2); + processor.clearHandlers(); + expect(processor.getHandlers()).toHaveLength(0); + }); + }); + + describe('Handler Priority', () => { + it('should sort handlers by priority', () => { + const handler1: QRCodeHandler = { + id: 'handler-1', + priority: 50, + canHandle: () => true, + handle: async () => ({success: true}), + }; + + const handler2: QRCodeHandler = { + id: 'handler-2', + priority: 10, + canHandle: () => true, + handle: async () => ({success: true}), + }; + + const handler3: QRCodeHandler = { + id: 'handler-3', + priority: 30, + canHandle: () => true, + handle: async () => ({success: true}), + }; + + processor.registerHandler(handler1); + processor.registerHandler(handler2); + processor.registerHandler(handler3); + + const handlers = processor.getHandlers(); + expect(handlers[0].id).toBe('handler-2'); + expect(handlers[1].id).toBe('handler-3'); + expect(handlers[2].id).toBe('handler-1'); + }); + + it('should use default priority of 100 if not specified', () => { + const handler1: QRCodeHandler = { + id: 'handler-1', + canHandle: () => true, + handle: async () => ({success: true}), + }; + + const handler2: QRCodeHandler = { + id: 'handler-2', + priority: 50, + canHandle: () => true, + handle: async () => ({success: true}), + }; + + processor.registerHandler(handler1); + processor.registerHandler(handler2); + + const handlers = processor.getHandlers(); + expect(handlers[0].id).toBe('handler-2'); // priority 50 + expect(handlers[1].id).toBe('handler-1'); // default priority 100 + }); + }); + + describe('QR Code Processing', () => { + it('should process QR code with matching handler', async () => { + const handler: QRCodeHandler = { + id: 'test-handler', + canHandle: (context) => context.data.startsWith('test://'), + handle: async (context) => ({ + success: true, + data: {processed: context.data}, + }), + }; + + processor.registerHandler(handler); + const result = await processor.process('test://some-data'); + + expect(result.success).toBe(true); + expect(result.data).toEqual({processed: 'test://some-data'}); + expect(result.metadata?.handlerId).toBe('test-handler'); + }); + + it('should fail when no handlers are registered', async () => { + const result = await processor.process('test://data'); + + expect(result.success).toBe(false); + expect(result.error?.message).toBe('No handlers registered'); + }); + + it('should fail when no handler can process the data', async () => { + const handler: QRCodeHandler = { + id: 'test-handler', + canHandle: () => false, + handle: async () => ({success: true}), + }; + + processor.registerHandler(handler); + const result = await processor.process('test://data'); + + expect(result.success).toBe(false); + expect(result.error?.message).toContain('No handler could process'); + }); + + it('should try handlers in priority order', async () => { + const executionOrder: string[] = []; + + const handler1: QRCodeHandler = { + id: 'handler-1', + priority: 50, + canHandle: () => true, + handle: async () => { + executionOrder.push('handler-1'); + return {success: false}; + }, + }; + + const handler2: QRCodeHandler = { + id: 'handler-2', + priority: 10, + canHandle: () => true, + handle: async () => { + executionOrder.push('handler-2'); + return {success: true}; + }, + }; + + processor.registerHandler(handler1); + processor.registerHandler(handler2); + + await processor.process('test://data'); + + expect(executionOrder).toEqual(['handler-2', 'handler-1']); + }); + + it('should stop on first successful handler by default', async () => { + const executionOrder: string[] = []; + + const handler1: QRCodeHandler = { + id: 'handler-1', + priority: 10, + canHandle: () => true, + handle: async () => { + executionOrder.push('handler-1'); + return {success: true}; + }, + }; + + const handler2: QRCodeHandler = { + id: 'handler-2', + priority: 20, + canHandle: () => true, + handle: async () => { + executionOrder.push('handler-2'); + return {success: true}; + }, + }; + + processor.registerHandler(handler1); + processor.registerHandler(handler2); + + await processor.process('test://data'); + + expect(executionOrder).toEqual(['handler-1']); + }); + + it('should continue to other handlers when stopOnFirstSuccess is false', async () => { + const executionOrder: string[] = []; + + const handler1: QRCodeHandler = { + id: 'handler-1', + priority: 10, + canHandle: () => true, + handle: async () => { + executionOrder.push('handler-1'); + return {success: true}; + }, + }; + + const handler2: QRCodeHandler = { + id: 'handler-2', + priority: 20, + canHandle: () => true, + handle: async () => { + executionOrder.push('handler-2'); + return {success: true}; + }, + }; + + processor.registerHandler(handler1); + processor.registerHandler(handler2); + + await processor.process('test://data', {stopOnFirstSuccess: false}); + + expect(executionOrder).toEqual(['handler-1', 'handler-2']); + }); + + it('should skip handlers that cannot handle the data', async () => { + const executionOrder: string[] = []; + + const handler1: QRCodeHandler = { + id: 'handler-1', + priority: 10, + canHandle: () => false, + handle: async () => { + executionOrder.push('handler-1'); + return {success: true}; + }, + }; + + const handler2: QRCodeHandler = { + id: 'handler-2', + priority: 20, + canHandle: () => true, + handle: async () => { + executionOrder.push('handler-2'); + return {success: true}; + }, + }; + + processor.registerHandler(handler1); + processor.registerHandler(handler2); + + await processor.process('test://data'); + + expect(executionOrder).toEqual(['handler-2']); + }); + + it('should handle async canHandle method', async () => { + const handler: QRCodeHandler = { + id: 'test-handler', + canHandle: async (context) => { + await new Promise(resolve => setTimeout(resolve, 10)); + return context.data.startsWith('test://'); + }, + handle: async () => ({success: true}), + }; + + processor.registerHandler(handler); + const result = await processor.process('test://data'); + + expect(result.success).toBe(true); + }); + + it('should continue to next handler when one throws error', async () => { + const handler1: QRCodeHandler = { + id: 'handler-1', + priority: 10, + canHandle: () => true, + handle: async () => { + throw new Error('Handler 1 error'); + }, + }; + + const handler2: QRCodeHandler = { + id: 'handler-2', + priority: 20, + canHandle: () => true, + handle: async () => ({success: true, data: 'processed'}), + }; + + processor.registerHandler(handler1); + processor.registerHandler(handler2); + + const result = await processor.process('test://data'); + + expect(result.success).toBe(true); + expect(result.data).toBe('processed'); + }); + }); + + describe('Context Preparation', () => { + it('should prepare context with JSON data', async () => { + const jsonData = {type: 'credential', id: '123'}; + const handler: QRCodeHandler = { + id: 'test-handler', + canHandle: (context) => context.jsonData?.type === 'credential', + handle: async (context) => ({success: true, data: context.jsonData}), + }; + + processor.registerHandler(handler); + const result = await processor.process(JSON.stringify(jsonData)); + + expect(result.success).toBe(true); + expect(result.data).toEqual(jsonData); + }); + + it('should prepare context with URL', async () => { + const handler: QRCodeHandler = { + id: 'test-handler', + canHandle: (context) => !!context.url, + handle: async (context) => ({success: true, data: {url: context.url}}), + }; + + processor.registerHandler(handler); + const result = await processor.process('https://example.com/credential'); + + expect(result.success).toBe(true); + expect(result.data.url).toBe('https://example.com/credential'); + }); + + it('should use custom prepareContext function', async () => { + const handler: QRCodeHandler = { + id: 'test-handler', + canHandle: (context) => context.metadata?.custom === true, + handle: async (context) => ({success: true, data: context.metadata}), + }; + + processor.registerHandler(handler); + + const result = await processor.process('test://data', { + prepareContext: async (data) => ({ + data, + metadata: {custom: true, value: 'test'}, + }), + }); + + expect(result.success).toBe(true); + expect(result.data).toEqual({custom: true, value: 'test'}); + }); + }); + + describe('Callbacks', () => { + it('should call onSuccess callback when handler succeeds', async () => { + const onSuccess = jest.fn(); + const handler: QRCodeHandler = { + id: 'test-handler', + canHandle: () => true, + handle: async () => ({success: true, data: 'result'}), + }; + + processor.registerHandler(handler); + await processor.process('test://data', {onSuccess}); + + expect(onSuccess).toHaveBeenCalledWith( + expect.objectContaining({success: true, data: 'result'}), + handler, + ); + }); + + it('should call onError callback when handler throws', async () => { + const onError = jest.fn(); + const error = new Error('Handler error'); + const handler: QRCodeHandler = { + id: 'test-handler', + canHandle: () => true, + handle: async () => { + throw error; + }, + }; + + processor.registerHandler(handler); + await processor.process('test://data', {onError}); + + expect(onError).toHaveBeenCalledWith(error, handler); + }); + + it('should not fail if callback throws error', async () => { + const onSuccess = jest.fn(() => { + throw new Error('Callback error'); + }); + + const handler: QRCodeHandler = { + id: 'test-handler', + canHandle: () => true, + handle: async () => ({success: true}), + }; + + processor.registerHandler(handler); + const result = await processor.process('test://data', {onSuccess}); + + expect(result.success).toBe(true); + }); + }); + + describe('Timeout', () => { + it('should timeout handler that takes too long', async () => { + const handler: QRCodeHandler = { + id: 'slow-handler', + canHandle: () => true, + handle: async () => { + await new Promise(resolve => setTimeout(resolve, 1000)); + return {success: true}; + }, + }; + + processor.registerHandler(handler); + const result = await processor.process('test://data', {timeout: 100}); + + expect(result.success).toBe(false); + expect(result.error?.message).toContain('timed out'); + }); + + it('should use default timeout of 30 seconds', async () => { + const handler: QRCodeHandler = { + id: 'test-handler', + canHandle: () => true, + handle: async () => { + await new Promise(resolve => setTimeout(resolve, 50)); + return {success: true}; + }, + }; + + processor.registerHandler(handler); + const result = await processor.process('test://data'); + + expect(result.success).toBe(true); + }); + }); + + describe('Factory Function', () => { + it('should create processor with handlers', () => { + const handler1: QRCodeHandler = { + id: 'handler-1', + canHandle: () => true, + handle: async () => ({success: true}), + }; + + const handler2: QRCodeHandler = { + id: 'handler-2', + canHandle: () => true, + handle: async () => ({success: true}), + }; + + const proc = createQRCodeProcessor([handler1, handler2]); + + expect(proc.getHandlers()).toHaveLength(2); + expect(proc.getHandler('handler-1')).toBeDefined(); + expect(proc.getHandler('handler-2')).toBeDefined(); + }); + + it('should create processor without handlers', () => { + const proc = createQRCodeProcessor(); + + expect(proc.getHandlers()).toHaveLength(0); + }); + }); +}); \ No newline at end of file diff --git a/packages/core/src/qr-handlers/processor.ts b/packages/core/src/qr-handlers/processor.ts new file mode 100644 index 00000000..dae2c864 --- /dev/null +++ b/packages/core/src/qr-handlers/processor.ts @@ -0,0 +1,311 @@ +import { + QRCodeContext, + QRCodeHandler, + QRCodeHandlerResult, + QRCodeProcessor, + ProcessOptions, +} from './types'; + +/** + * Default implementation of QRCodeProcessor + * + * This processor manages a registry of QR code handlers and executes them + * in priority order to process scanned QR codes. It provides a flexible, + * extensible system for handling various types of QR codes in a wallet application. + * + * @example + * ```typescript + * const processor = new DefaultQRCodeProcessor(); + * + * // Register handlers + * processor.registerHandler(new OID4VCHandler()); + * processor.registerHandler(new CredentialHandler()); + * + * // Process QR code + * const result = await processor.process(scannedData); + * if (result.success) { + * console.log('QR code processed:', result.data); + * } else { + * console.error('Failed to process QR code:', result.error); + * } + * ``` + */ +export class DefaultQRCodeProcessor implements QRCodeProcessor { + private handlers: Map = new Map(); + + /** + * Register a new QR code handler + * + * @param handler - The handler to register + * @throws Error if a handler with the same ID is already registered + */ + registerHandler(handler: QRCodeHandler): void { + if (this.handlers.has(handler.id)) { + throw new Error( + `Handler with id "${handler.id}" is already registered. ` + + `Please use a unique ID or unregister the existing handler first.`, + ); + } + this.handlers.set(handler.id, handler); + } + + /** + * Unregister a QR code handler by its ID + * + * @param id - The ID of the handler to unregister + * @returns True if the handler was found and removed, false otherwise + */ + unregisterHandler(id: string): boolean { + return this.handlers.delete(id); + } + + /** + * Get all registered handlers sorted by priority + * + * @returns Array of registered handlers sorted by priority (lowest first) + */ + getHandlers(): QRCodeHandler[] { + return Array.from(this.handlers.values()).sort( + (a, b) => (a.priority ?? 100) - (b.priority ?? 100), + ); + } + + /** + * Get a specific handler by its ID + * + * @param id - The ID of the handler to retrieve + * @returns The handler if found, undefined otherwise + */ + getHandler(id: string): QRCodeHandler | undefined { + return this.handlers.get(id); + } + + /** + * Clear all registered handlers + */ + clearHandlers(): void { + this.handlers.clear(); + } + + /** + * Process QR code data through registered handlers + * + * This method: + * 1. Prepares the context from raw QR data + * 2. Executes handlers in priority order + * 3. Returns the first successful result (or continues if stopOnFirstSuccess is false) + * 4. Returns an error result if no handler can process the data + * + * @param data - Raw QR code data string + * @param options - Processing options + * @returns Result of the processing + */ + async process( + data: string, + options: ProcessOptions = {}, + ): Promise { + const { + timeout = 30000, + stopOnFirstSuccess = true, + prepareContext = this.defaultPrepareContext.bind(this), + onError, + onSuccess, + } = options; + + // Prepare context from raw data + let context: QRCodeContext; + try { + context = await this.withTimeout(prepareContext(data), timeout); + } catch (error) { + return { + success: false, + error: error instanceof Error ? error : new Error(String(error)), + metadata: {phase: 'context-preparation'}, + }; + } + + // Get sorted handlers + const handlers = this.getHandlers(); + + if (handlers.length === 0) { + return { + success: false, + error: new Error('No handlers registered'), + metadata: {phase: 'handler-execution'}, + }; + } + + let lastError: Error | undefined; + const attemptedHandlers: string[] = []; + + // Execute handlers in priority order + for (const handler of handlers) { + try { + // Check if handler can process this data + const canHandle = await this.withTimeout( + Promise.resolve(handler.canHandle(context)), + timeout, + ); + + if (!canHandle) { + continue; + } + + attemptedHandlers.push(handler.id); + + // Execute handler + const result = await this.withTimeout(handler.handle(context), timeout); + + // Call success callback if provided + if (result.success && onSuccess) { + try { + onSuccess(result, handler); + } catch (callbackError) { + console.error( + `Success callback error for handler ${handler.id}:`, + callbackError, + ); + } + } + + // Stop on first success if configured + if (result.success && stopOnFirstSuccess) { + return { + ...result, + metadata: { + ...result.metadata, + handlerId: handler.id, + attemptedHandlers, + }, + }; + } + + // Store error if handler failed + if (!result.success && result.error) { + lastError = result.error; + } + } catch (error) { + const handlerError = + error instanceof Error ? error : new Error(String(error)); + lastError = handlerError; + + // Call error callback if provided + if (onError) { + try { + onError(handlerError, handler); + } catch (callbackError) { + console.error( + `Error callback error for handler ${handler.id}:`, + callbackError, + ); + } + } + + // Continue to next handler + continue; + } + } + + // No handler could process the data + return { + success: false, + error: + lastError || + new Error( + `No handler could process the QR code. Attempted handlers: ${attemptedHandlers.join(', ') || 'none'}`, + ), + metadata: { + phase: 'handler-execution', + attemptedHandlers, + }, + }; + } + + /** + * Default context preparation function + * + * This method attempts to parse the raw QR data as JSON or URL. + * Override this by providing a custom prepareContext function in ProcessOptions. + * + * @param data - Raw QR code data string + * @returns Prepared context object + */ + private async defaultPrepareContext(data: string): Promise { + const context: QRCodeContext = {data}; + + // Try to parse as URL + try { + // Basic URL validation + if (data.startsWith('http://') || data.startsWith('https://')) { + const url = new URL(data); + context.url = data; + context.parsedUrl = data; + } + } catch { + // Not a valid URL, continue + } + + // Try to parse as JSON + try { + const parsed = JSON.parse(data); + context.jsonData = parsed; + } catch { + // Not valid JSON, continue + } + + return context; + } + + /** + * Execute a promise with a timeout + * + * @param promise - Promise to execute + * @param timeoutMs - Timeout in milliseconds + * @returns Result of the promise + * @throws Error if the promise times out + */ + private async withTimeout( + promise: Promise, + timeoutMs: number, + ): Promise { + return Promise.race([ + promise, + new Promise((_, reject) => + setTimeout( + () => reject(new Error(`Operation timed out after ${timeoutMs}ms`)), + timeoutMs, + ), + ), + ]); + } +} + +/** + * Create a new QR code processor instance + * + * This is a convenience factory function for creating a processor. + * + * @param handlers - Optional array of handlers to register immediately + * @returns New processor instance with handlers registered + * + * @example + * ```typescript + * const processor = createQRCodeProcessor([ + * new OID4VCHandler(), + * new CredentialHandler(), + * ]); + * ``` + */ +export function createQRCodeProcessor( + handlers?: QRCodeHandler[], +): QRCodeProcessor { + const processor = new DefaultQRCodeProcessor(); + + if (handlers) { + for (const handler of handlers) { + processor.registerHandler(handler); + } + } + + return processor; +} \ No newline at end of file diff --git a/packages/core/src/qr-handlers/types.ts b/packages/core/src/qr-handlers/types.ts new file mode 100644 index 00000000..09fd12a8 --- /dev/null +++ b/packages/core/src/qr-handlers/types.ts @@ -0,0 +1,228 @@ +/** + * QR Code Handler Types + * + * This module provides interfaces and types for building a generic, + * extensible QR code handler system that can process various types + * of QR codes in a decentralized identity wallet. + */ + +/** + * Context object containing parsed QR code data and metadata + * + * @interface QRCodeContext + */ +export interface QRCodeContext { + /** + * Raw scanned QR code data string + */ + data: string; + + /** + * Parsed JSON data if the QR code contained JSON, + * or data fetched from a URL if the QR code was a URL + */ + jsonData?: any; + + /** + * Original URL if the scanned data was a valid URL + */ + url?: string; + + /** + * Modified URL after processing (e.g., with authentication parameters added) + */ + parsedUrl?: string; + + /** + * Additional metadata that can be attached by context preparation + * or handlers for passing data between handlers + */ + metadata?: Record; +} + +/** + * Result returned by a QR code handler after processing + * + * @interface QRCodeHandlerResult + */ +export interface QRCodeHandlerResult { + /** + * Whether the handler successfully processed the QR code data + */ + success: boolean; + + /** + * Optional data returned by the handler + * Can be credentials, presentation requests, or any other processed data + */ + data?: any; + + /** + * Error object if processing failed + */ + error?: Error; + + /** + * Additional metadata about the processing result + */ + metadata?: Record; +} + +/** + * Handler interface for processing specific types of QR codes + * + * Handlers are responsible for: + * 1. Identifying if they can process the QR code (canHandle) + * 2. Processing the QR code data (handle) + * + * @interface QRCodeHandler + */ +export interface QRCodeHandler { + /** + * Unique identifier for this handler + * Used for registration, unregistration, and debugging + */ + id: string; + + /** + * Priority for handler execution (lower number = higher priority) + * Handlers are executed in priority order until one successfully handles the data + * + * @default 100 + */ + priority?: number; + + /** + * Check if this handler can process the given QR code data + * + * This method should be fast and only do basic checks (string matching, type checking) + * without performing expensive operations like network requests. + * + * @param context - The QR code context containing parsed data + * @returns True if this handler can process the data, false otherwise + */ + canHandle(context: QRCodeContext): boolean | Promise; + + /** + * Process the QR code data + * + * This method is only called if canHandle returns true. + * It should perform the actual processing logic (navigation, API calls, etc.) + * + * @param context - The QR code context containing parsed data + * @returns Result of the processing including success status and any data/errors + */ + handle(context: QRCodeContext): Promise; +} + +/** + * Options for processing QR codes + * + * @interface ProcessOptions + */ +export interface ProcessOptions { + /** + * Timeout in milliseconds for processing + * If a handler takes longer than this, it will be skipped + * + * @default 30000 (30 seconds) + */ + timeout?: number; + + /** + * Whether to stop processing after the first successful handler + * If false, all handlers will be tried even after one succeeds + * + * @default true + */ + stopOnFirstSuccess?: boolean; + + /** + * Custom context preparation function + * + * This function is called before any handlers to prepare the context + * from the raw scanned data. It can fetch data from URLs, parse JSON, + * add metadata, etc. + * + * @param data - Raw QR code data string + * @returns Prepared context object + */ + prepareContext?: (data: string) => Promise; + + /** + * Error handler callback + * + * Called when a handler throws an error during processing. + * Useful for logging, analytics, or custom error handling. + * + * @param error - The error that was thrown + * @param handler - The handler that threw the error + */ + onError?: (error: Error, handler: QRCodeHandler) => void; + + /** + * Success callback + * + * Called when a handler successfully processes the QR code. + * Useful for logging, analytics, or side effects. + * + * @param result - The result returned by the handler + * @param handler - The handler that processed the data + */ + onSuccess?: (result: QRCodeHandlerResult, handler: QRCodeHandler) => void; +} + +/** + * Main processor interface for managing and executing QR code handlers + * + * @interface QRCodeProcessor + */ +export interface QRCodeProcessor { + /** + * Register a new QR code handler + * + * @param handler - The handler to register + * @throws Error if a handler with the same ID is already registered + */ + registerHandler(handler: QRCodeHandler): void; + + /** + * Unregister a QR code handler by its ID + * + * @param id - The ID of the handler to unregister + * @returns True if the handler was found and removed, false otherwise + */ + unregisterHandler(id: string): boolean; + + /** + * Get all registered handlers sorted by priority + * + * @returns Array of registered handlers sorted by priority (lowest first) + */ + getHandlers(): QRCodeHandler[]; + + /** + * Get a specific handler by its ID + * + * @param id - The ID of the handler to retrieve + * @returns The handler if found, undefined otherwise + */ + getHandler(id: string): QRCodeHandler | undefined; + + /** + * Process QR code data through registered handlers + * + * Handlers are executed in priority order until one successfully + * processes the data (or all handlers are tried if stopOnFirstSuccess is false) + * + * @param data - Raw QR code data string + * @param options - Processing options + * @returns Result of the processing + */ + process(data: string, options?: ProcessOptions): Promise; + + /** + * Clear all registered handlers + */ + clearHandlers(): void; +} \ No newline at end of file