{"version":3,"sources":["../../src/react/QRScanner.tsx","../../src/upi.ts","../../src/scanner.ts"],"sourcesContent":["import React, { useEffect, useRef, useState, useCallback } from \"react\";\nimport type { UPIData } from \"../types\";\nimport { UPIScanner } from \"../scanner\";\n\nexport interface QRScannerProps {\n  /** Called when a valid UPI QR is successfully scanned. */\n  onScan: (data: UPIData) => void;\n  /** Called on scanner errors (camera permission, library load, etc). */\n  onError?: (error: Error) => void;\n  /** Called when the user clicks the close button (if shown). */\n  onClose?: () => void;\n  /** Width of the scanner viewport in pixels. */\n  width?: number;\n  /** Height of the scanner viewport in pixels. */\n  height?: number;\n  /** FPS for the camera feed. */\n  fps?: number;\n  /** Whether to show a close button. */\n  showCloseButton?: boolean;\n  /** CSS class for the container. */\n  className?: string;\n  /** Inline styles for the container. */\n  style?: React.CSSProperties;\n  /** Debounce interval in ms to prevent duplicate scans. */\n  debounceMs?: number;\n}\n\n/**\n * React component that renders a camera-based QR scanner.\n *\n * Only UPI QR codes are emitted via `onScan`; non-UPI codes are ignored.\n *\n * @example\n * ```tsx\n * import { QRScanner } from \"@vouch/sdk/react\";\n *\n * <QRScanner\n *   onScan={(upi) => console.log(upi.upiId)}\n *   onError={(e) => toast.error(e.message)}\n * />\n * ```\n */\nexport const QRScanner: React.FC<QRScannerProps> = ({\n  onScan,\n  onError,\n  onClose,\n  width = 320,\n  height = 320,\n  fps = 10,\n  showCloseButton = true,\n  className,\n  style,\n  debounceMs = 2000,\n}) => {\n  const containerRef = useRef<HTMLDivElement | null>(null);\n  const scannerRef = useRef<UPIScanner | null>(null);\n  const onScanRef = useRef(onScan);\n  const onErrorRef = useRef(onError);\n  const [loading, setLoading] = useState(true);\n  const [cameraError, setCameraError] = useState<string | null>(null);\n\n  onScanRef.current = onScan;\n  onErrorRef.current = onError;\n\n  const stableOnScan = useCallback((data: UPIData) => onScanRef.current(data), []);\n  const stableOnError = useCallback((err: Error) => {\n    setCameraError(err.message);\n    setLoading(false);\n    onErrorRef.current?.(err);\n  }, []);\n\n  useEffect(() => {\n    if (!containerRef.current) return;\n\n    const scanner = new UPIScanner({\n      container: containerRef.current,\n      onScan: stableOnScan,\n      onError: stableOnError,\n      width,\n      height,\n      fps,\n      debounceMs,\n    });\n\n    scannerRef.current = scanner;\n\n    scanner\n      .start()\n      .then(() => setLoading(false))\n      .catch(() => setLoading(false));\n\n    return () => {\n      scanner.stop().catch(() => {});\n    };\n  }, [width, height, fps, debounceMs, stableOnScan, stableOnError]);\n\n  return (\n    <div\n      className={className}\n      style={{\n        position: \"relative\",\n        width,\n        height,\n        background: \"#000\",\n        borderRadius: 8,\n        overflow: \"hidden\",\n        ...style,\n      }}\n    >\n      {loading && (\n        <div\n          style={{\n            position: \"absolute\",\n            inset: 0,\n            display: \"flex\",\n            alignItems: \"center\",\n            justifyContent: \"center\",\n            color: \"#fff\",\n            zIndex: 2,\n          }}\n        >\n          <span>Starting camera…</span>\n        </div>\n      )}\n\n      {cameraError && (\n        <div\n          style={{\n            position: \"absolute\",\n            inset: 0,\n            display: \"flex\",\n            flexDirection: \"column\",\n            alignItems: \"center\",\n            justifyContent: \"center\",\n            color: \"#ff6b6b\",\n            padding: 16,\n            textAlign: \"center\",\n            zIndex: 2,\n          }}\n        >\n          <p>{cameraError}</p>\n          {showCloseButton && onClose && (\n            <button onClick={onClose} style={{ marginTop: 12 }}>\n              Close\n            </button>\n          )}\n        </div>\n      )}\n\n      <div ref={containerRef} style={{ width: \"100%\", height: \"100%\" }} />\n\n      {showCloseButton && onClose && !cameraError && (\n        <button\n          onClick={onClose}\n          aria-label=\"Close scanner\"\n          style={{\n            position: \"absolute\",\n            top: 8,\n            right: 8,\n            zIndex: 3,\n            background: \"rgba(0,0,0,0.5)\",\n            color: \"#fff\",\n            border: \"none\",\n            borderRadius: 4,\n            padding: \"4px 8px\",\n            cursor: \"pointer\",\n          }}\n        >\n          ✕\n        </button>\n      )}\n    </div>\n  );\n};\n","import type { UPIData, UPIScheme } from \"./types\";\n\n/**\n * Parse a UPI payment scheme string.\n *\n * Supports:\n * - `upi://pay?pa=merchant@upi&pn=Merchant+Name&am=100.00`\n * - Raw UPI IDs like `merchant@upi`\n *\n * Returns `null` if the input is not recognisable as UPI.\n */\nexport function parseUPIScheme(raw: string): UPIData | null {\n  const trimmed = raw.trim();\n  if (!trimmed) return null;\n\n  // Standard UPI URI scheme\n  if (trimmed.toLowerCase().startsWith(\"upi://\")) {\n    try {\n      const url = new URL(trimmed);\n      const upiId = url.searchParams.get(\"pa\") || undefined;\n      if (!upiId) return null;\n\n      return {\n        type: \"upi\",\n        raw: trimmed,\n        upiId,\n        merchantName: url.searchParams.get(\"pn\")?.replace(/\\+/g, \" \") || undefined,\n        amount: url.searchParams.get(\"am\") || undefined,\n        mcc: url.searchParams.get(\"mc\") || undefined,\n      };\n    } catch {\n      return null;\n    }\n  }\n\n  // Raw UPI ID fallback (anything containing @)\n  if (trimmed.includes(\"@\")) {\n    return {\n      type: \"upi\",\n      raw: trimmed,\n      upiId: trimmed,\n    };\n  }\n\n  return null;\n}\n\n/**\n * Extract all UPI query parameters from a raw UPI scheme string.\n * Useful if you need fields beyond the common ones (e.g. `mc`, `tr`, `tn`).\n */\nexport function parseUPISchemeFull(raw: string): UPIScheme | null {\n  const trimmed = raw.trim();\n  if (!trimmed.toLowerCase().startsWith(\"upi://\")) return null;\n\n  try {\n    const url = new URL(trimmed);\n    const get = (key: string) => url.searchParams.get(key) || undefined;\n    return {\n      pa: get(\"pa\"),\n      pn: get(\"pn\"),\n      am: get(\"am\"),\n      cu: get(\"cu\"),\n      tn: get(\"tn\"),\n      tr: get(\"tr\"),\n      url: get(\"url\"),\n      mc: get(\"mc\"),\n    };\n  } catch {\n    return null;\n  }\n}\n","import type { UPIData, UPIScannerOptions } from \"./types\";\nimport { parseUPIScheme } from \"./upi\";\n\n/**\n * Framework-agnostic UPI QR scanner.\n *\n * Works in any browser environment — vanilla JS, React, Vue, Svelte, etc.\n * Dynamically loads `html5-qrcode` on first use.\n *\n * @example\n * ```ts\n * const scanner = new UPIScanner({\n *   container: \"#scanner\",\n *   onScan: (upi) => console.log(upi.upiId),\n *   onError: (err) => alert(err.message),\n * });\n * await scanner.start();\n * // later...\n * await scanner.stop();\n * ```\n */\nexport class UPIScanner {\n  private options: Required<UPIScannerOptions>;\n  private scanner: { stop: () => Promise<void> } | null = null;\n  private lastScan = 0;\n  private _started = false;\n  private containerEl: HTMLElement | null = null;\n\n  constructor(options: UPIScannerOptions) {\n    this.options = {\n      onError: () => {},\n      width: 320,\n      height: 320,\n      fps: 10,\n      debounceMs: 2000,\n      ...options,\n    };\n  }\n\n  private resolveContainer(): HTMLElement | null {\n    const { container } = this.options;\n    if (typeof container === \"string\") {\n      return document.querySelector(container);\n    }\n    return container;\n  }\n\n  /**\n   * Start the camera and begin scanning for UPI QR codes.\n   */\n  async start(): Promise<void> {\n    if (this._started) return;\n\n    this.containerEl = this.resolveContainer();\n    if (!this.containerEl) {\n      const err = new Error(`UPIScanner: container not found`);\n      this.options.onError(err);\n      throw err;\n    }\n\n    try {\n      const { Html5Qrcode } = await import(\"html5-qrcode\");\n\n      // Ensure the container has an ID for html5-qrcode\n      let scannerId = this.containerEl.id;\n      if (!scannerId) {\n        scannerId = `vouch-scanner-${Math.random().toString(36).slice(2, 9)}`;\n        this.containerEl.id = scannerId;\n      }\n\n      // Clear any previous content\n      this.containerEl.innerHTML = \"\";\n      this.containerEl.style.width = `${this.options.width}px`;\n      this.containerEl.style.height = `${this.options.height}px`;\n\n      const scanner = new Html5Qrcode(scannerId);\n      this.scanner = scanner;\n\n      const qrboxSize = Math.min(this.options.width, this.options.height) * 0.7;\n\n      await scanner.start(\n        { facingMode: \"environment\" },\n        { fps: this.options.fps, qrbox: { width: qrboxSize, height: qrboxSize } },\n        (decodedText: string) => {\n          this.handleScan(decodedText);\n        },\n        () => {\n          // QR not found in this frame — no-op\n        }\n      );\n\n      this._started = true;\n    } catch (err) {\n      const error = err instanceof Error ? err : new Error(String(err));\n      this.options.onError(error);\n      throw error;\n    }\n  }\n\n  /**\n   * Stop the camera and release resources.\n   */\n  async stop(): Promise<void> {\n    if (!this._started || !this.scanner) return;\n    try {\n      await this.scanner.stop();\n    } catch {\n      // ignore cleanup errors\n    }\n    this.scanner = null;\n    this._started = false;\n  }\n\n  /** Whether the scanner is currently active. */\n  isStarted(): boolean {\n    return this._started;\n  }\n\n  private handleScan(decodedText: string): void {\n    const now = Date.now();\n    if (now - this.lastScan < this.options.debounceMs) return;\n\n    const parsed = parseUPIScheme(decodedText);\n    if (!parsed) return; // ignore non-UPI codes\n\n    this.lastScan = now;\n    this.options.onScan(parsed);\n  }\n}\n"],"mappings":"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","names":["useEffect","useRef","useState","useCallback","parseUPIScheme","raw","trimmed","url","upiId","UPIScanner","options","container","err","Html5Qrcode","scannerId","scanner","qrboxSize","decodedText","error","now","parsed","parseUPIScheme","jsx","jsxs","QRScanner","onScan","onError","onClose","width","height","fps","showCloseButton","className","style","debounceMs","containerRef","useRef","scannerRef","onScanRef","onErrorRef","loading","setLoading","useState","cameraError","setCameraError","stableOnScan","useCallback","data","stableOnError","err","useEffect","scanner","UPIScanner"]}