@wakastellar/ui 2.0.0 → 2.1.0
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.
- package/dist/cli/commands/add.d.ts +7 -0
- package/dist/cli/commands/init.d.ts +6 -0
- package/dist/cli/commands/list.d.ts +5 -0
- package/dist/cli/commands/search.d.ts +1 -0
- package/dist/cli/index.cjs +4844 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/utils/config.d.ts +29 -0
- package/dist/cli/utils/logger.d.ts +20 -0
- package/dist/cli/utils/registry.d.ts +23 -0
- package/package.json +14 -3
- package/src/blocks/activity-timeline/index.tsx +586 -0
- package/src/blocks/calendar-view/index.tsx +756 -0
- package/src/blocks/chat/index.tsx +1018 -0
- package/src/blocks/chat/widget.tsx +504 -0
- package/src/blocks/dashboard/index.tsx +522 -0
- package/src/blocks/empty-states/index.tsx +452 -0
- package/src/blocks/error-pages/index.tsx +426 -0
- package/src/blocks/faq/index.tsx +479 -0
- package/src/blocks/file-manager/index.tsx +890 -0
- package/src/blocks/footer/index.tsx +133 -0
- package/src/blocks/header/index.tsx +357 -0
- package/src/blocks/headtab/index.tsx +139 -0
- package/src/blocks/i18n-editor/index.tsx +1016 -0
- package/src/blocks/index.ts +80 -0
- package/src/blocks/kanban-board/index.tsx +779 -0
- package/src/blocks/landing/index.tsx +677 -0
- package/src/blocks/language-selector/index.tsx +88 -0
- package/src/blocks/layout/index.tsx +159 -0
- package/src/blocks/login/index.tsx +339 -0
- package/src/blocks/login/types.ts +131 -0
- package/src/blocks/pricing/index.tsx +564 -0
- package/src/blocks/profile/index.tsx +746 -0
- package/src/blocks/settings/index.tsx +558 -0
- package/src/blocks/sidebar/index.tsx +713 -0
- package/src/blocks/theme-creator-block/index.tsx +835 -0
- package/src/blocks/user-management/index.tsx +1037 -0
- package/src/blocks/wizard/index.tsx +719 -0
- package/src/components/DataTable/DataTable.tsx +406 -0
- package/src/components/DataTable/DataTableAdvanced.tsx +720 -0
- package/src/components/DataTable/DataTableBody.tsx +216 -0
- package/src/components/DataTable/DataTableCell.tsx +172 -0
- package/src/components/DataTable/DataTableColumnResizer.tsx +62 -0
- package/src/components/DataTable/DataTableConflictResolver.tsx +478 -0
- package/src/components/DataTable/DataTableContextMenu.tsx +219 -0
- package/src/components/DataTable/DataTableEditCell.tsx +279 -0
- package/src/components/DataTable/DataTableFilterBuilder.tsx +519 -0
- package/src/components/DataTable/DataTableFilters.tsx +535 -0
- package/src/components/DataTable/DataTableGrouping.tsx +147 -0
- package/src/components/DataTable/DataTableHeader.tsx +172 -0
- package/src/components/DataTable/DataTablePagination.tsx +125 -0
- package/src/components/DataTable/DataTableSelection.tsx +269 -0
- package/src/components/DataTable/DataTableSyncStatus.tsx +281 -0
- package/src/components/DataTable/DataTableToolbar.tsx +262 -0
- package/src/components/DataTable/README.md +446 -0
- package/src/components/DataTable/__tests__/DataTableAdvanced.test.tsx +426 -0
- package/src/components/DataTable/__tests__/DataTableEdit.test.tsx +329 -0
- package/src/components/DataTable/__tests__/useDataTableAdvanced.test.ts +455 -0
- package/src/components/DataTable/examples/EditExample.tsx +166 -0
- package/src/components/DataTable/formatters/index.ts +335 -0
- package/src/components/DataTable/hooks/__tests__/useDataTableEdit.test.ts +239 -0
- package/src/components/DataTable/hooks/useDataTable.ts +145 -0
- package/src/components/DataTable/hooks/useDataTableAdvanced.ts +342 -0
- package/src/components/DataTable/hooks/useDataTableAdvancedFilters.ts +637 -0
- package/src/components/DataTable/hooks/useDataTableColumnTemplates.ts +186 -0
- package/src/components/DataTable/hooks/useDataTableEdit.ts +167 -0
- package/src/components/DataTable/hooks/useDataTableExport.ts +227 -0
- package/src/components/DataTable/hooks/useDataTableImport.ts +216 -0
- package/src/components/DataTable/hooks/useDataTableOffline.ts +481 -0
- package/src/components/DataTable/hooks/useDataTableTheme.ts +213 -0
- package/src/components/DataTable/hooks/useDataTableVirtualization.ts +99 -0
- package/src/components/DataTable/hooks/useTableLayout.ts +85 -0
- package/src/components/DataTable/index.ts +81 -0
- package/src/components/DataTable/services/IndexedDBService.ts +504 -0
- package/src/components/DataTable/templates/index.tsx +803 -0
- package/src/components/DataTable/types.ts +504 -0
- package/src/components/DataTable/utils.ts +164 -0
- package/src/components/DataTable/workers/exportWorker.ts +213 -0
- package/src/components/accordion/index.tsx +61 -0
- package/src/components/alert/index.tsx +61 -0
- package/src/components/alert-dialog/index.tsx +146 -0
- package/src/components/aspect-ratio/index.tsx +12 -0
- package/src/components/avatar/index.tsx +54 -0
- package/src/components/badge/Badge.stories.tsx +64 -0
- package/src/components/badge/index.tsx +38 -0
- package/src/components/button/Button.stories.tsx +173 -0
- package/src/components/button/index.tsx +56 -0
- package/src/components/calendar/index.tsx +73 -0
- package/src/components/card/index.tsx +78 -0
- package/src/components/checkbox/index.tsx +34 -0
- package/src/components/code/index.tsx +229 -0
- package/src/components/collapsible/index.tsx +16 -0
- package/src/components/command/index.tsx +162 -0
- package/src/components/context-menu/index.tsx +204 -0
- package/src/components/dialog/index.tsx +126 -0
- package/src/components/dropdown-menu/index.tsx +204 -0
- package/src/components/error-boundary/ErrorBoundary.tsx +281 -0
- package/src/components/error-boundary/index.ts +7 -0
- package/src/components/form/index.tsx +183 -0
- package/src/components/hover-card/index.tsx +33 -0
- package/src/components/index.ts +368 -0
- package/src/components/input/Input.stories.tsx +100 -0
- package/src/components/input/index.tsx +27 -0
- package/src/components/input-otp/index.tsx +277 -0
- package/src/components/label/index.tsx +30 -0
- package/src/components/language-selector/index.tsx +341 -0
- package/src/components/menubar/index.tsx +240 -0
- package/src/components/navigation-menu/index.tsx +134 -0
- package/src/components/popover/index.tsx +35 -0
- package/src/components/progress/index.tsx +32 -0
- package/src/components/radio-group/index.tsx +48 -0
- package/src/components/scroll-area/index.tsx +52 -0
- package/src/components/select/index.tsx +164 -0
- package/src/components/separator/index.tsx +35 -0
- package/src/components/sheet/index.tsx +147 -0
- package/src/components/skeleton/index.tsx +22 -0
- package/src/components/slider/index.tsx +32 -0
- package/src/components/switch/index.tsx +33 -0
- package/src/components/table/index.tsx +117 -0
- package/src/components/tabs/index.tsx +59 -0
- package/src/components/textarea/index.tsx +30 -0
- package/src/components/theme-selector/index.tsx +327 -0
- package/src/components/toast/index.tsx +133 -0
- package/src/components/toaster/index.tsx +34 -0
- package/src/components/toggle/index.tsx +49 -0
- package/src/components/tooltip/index.tsx +34 -0
- package/src/components/typography/index.tsx +276 -0
- package/src/components/waka-3d-pie-chart/index.tsx +486 -0
- package/src/components/waka-achievement-unlock/index.tsx +716 -0
- package/src/components/waka-activity-feed/index.tsx +686 -0
- package/src/components/waka-address-autocomplete/index.tsx +1202 -0
- package/src/components/waka-admincrumb/index.tsx +349 -0
- package/src/components/waka-alert-stack/index.tsx +827 -0
- package/src/components/waka-allocation-matrix/index.tsx +1278 -0
- package/src/components/waka-approval-chain/index.tsx +766 -0
- package/src/components/waka-audit-log/index.tsx +1475 -0
- package/src/components/waka-autocomplete/index.tsx +358 -0
- package/src/components/waka-badge-showcase/index.tsx +704 -0
- package/src/components/waka-barcode/index.tsx +260 -0
- package/src/components/waka-biometric-prompt/index.tsx +765 -0
- package/src/components/waka-bottom-sheet/index.tsx +495 -0
- package/src/components/waka-breadcrumb/index.tsx +376 -0
- package/src/components/waka-breadcrumb-path/index.tsx +513 -0
- package/src/components/waka-budget-burn/index.tsx +1234 -0
- package/src/components/waka-capacity-planner/index.tsx +1107 -0
- package/src/components/waka-carousel/index.tsx +893 -0
- package/src/components/waka-cart-summary/index.tsx +1055 -0
- package/src/components/waka-challenge-timer/index.tsx +1044 -0
- package/src/components/waka-charts/WakaAreaChart.tsx +251 -0
- package/src/components/waka-charts/WakaBarChart.tsx +222 -0
- package/src/components/waka-charts/WakaChart.tsx +124 -0
- package/src/components/waka-charts/WakaLineChart.tsx +219 -0
- package/src/components/waka-charts/WakaMiniChart.tsx +133 -0
- package/src/components/waka-charts/WakaPieChart.tsx +214 -0
- package/src/components/waka-charts/WakaSparkline.tsx +229 -0
- package/src/components/waka-charts/dataTableHelpers.ts +109 -0
- package/src/components/waka-charts/hooks/useChartTheme.ts +123 -0
- package/src/components/waka-charts/hooks/useRechartsLoader.ts +234 -0
- package/src/components/waka-charts/index.ts +90 -0
- package/src/components/waka-charts/types.ts +330 -0
- package/src/components/waka-chat-bubble/index.tsx +1060 -0
- package/src/components/waka-checklist/index.tsx +1067 -0
- package/src/components/waka-checkout-stepper/index.tsx +976 -0
- package/src/components/waka-cohort-table/index.tsx +1011 -0
- package/src/components/waka-color-picker/index.tsx +447 -0
- package/src/components/waka-combo-counter/index.tsx +864 -0
- package/src/components/waka-combobox/index.tsx +497 -0
- package/src/components/waka-command-bar/index.tsx +403 -0
- package/src/components/waka-compare-period/index.tsx +1230 -0
- package/src/components/waka-connection-matrix/index.tsx +1053 -0
- package/src/components/waka-contribution-graph/index.tsx +552 -0
- package/src/components/waka-cost-breakdown/index.tsx +1065 -0
- package/src/components/waka-coupon-input/index.tsx +592 -0
- package/src/components/waka-credit-card-input/index.tsx +982 -0
- package/src/components/waka-daily-reward/index.tsx +762 -0
- package/src/components/waka-date-range-picker/index.tsx +378 -0
- package/src/components/waka-datetime-picker/index.tsx +793 -0
- package/src/components/waka-datetime-picker.form-integration/index.tsx +402 -0
- package/src/components/waka-deployment-lane/index.tsx +673 -0
- package/src/components/waka-device-trust/index.tsx +1259 -0
- package/src/components/waka-dock/index.tsx +285 -0
- package/src/components/waka-drawer/index.tsx +319 -0
- package/src/components/waka-empty-state/index.tsx +545 -0
- package/src/components/waka-error-shake/index.tsx +398 -0
- package/src/components/waka-feature-announcement/index.tsx +991 -0
- package/src/components/waka-file-upload/index.tsx +437 -0
- package/src/components/waka-floating-nav/index.tsx +413 -0
- package/src/components/waka-flow-diagram/index.tsx +508 -0
- package/src/components/waka-funnel-chart/index.tsx +823 -0
- package/src/components/waka-glow-card/index.tsx +246 -0
- package/src/components/waka-goal-progress/index.tsx +1025 -0
- package/src/components/waka-haptic-button/index.tsx +388 -0
- package/src/components/waka-health-pulse/index.tsx +451 -0
- package/src/components/waka-heatmap/index.tsx +1026 -0
- package/src/components/waka-hotspot/index.tsx +682 -0
- package/src/components/waka-image/index.tsx +373 -0
- package/src/components/waka-incident-timeline/index.tsx +686 -0
- package/src/components/waka-invoice-preview/index.tsx +829 -0
- package/src/components/waka-kanban/index.tsx +646 -0
- package/src/components/waka-kpi-dashboard/index.tsx +755 -0
- package/src/components/waka-leaderboard/index.tsx +746 -0
- package/src/components/waka-level-progress/index.tsx +665 -0
- package/src/components/waka-liquid-button/index.tsx +520 -0
- package/src/components/waka-loading-orbit/index.tsx +478 -0
- package/src/components/waka-loot-box/index.tsx +1091 -0
- package/src/components/waka-magic-link/index.tsx +321 -0
- package/src/components/waka-magnetic-button/index.tsx +567 -0
- package/src/components/waka-mention-input/index.tsx +953 -0
- package/src/components/waka-metric-sparkline/index.tsx +627 -0
- package/src/components/waka-milestone-road/index.tsx +1064 -0
- package/src/components/waka-modal/index.tsx +374 -0
- package/src/components/waka-morph-button/index.tsx +495 -0
- package/src/components/waka-network-topology/index.tsx +801 -0
- package/src/components/waka-notifications/index.tsx +414 -0
- package/src/components/waka-number-input/index.tsx +373 -0
- package/src/components/waka-orbital-menu/index.tsx +445 -0
- package/src/components/waka-order-tracker/index.tsx +1041 -0
- package/src/components/waka-pagination/index.tsx +393 -0
- package/src/components/waka-password-strength/index.tsx +824 -0
- package/src/components/waka-payment-method-picker/index.tsx +715 -0
- package/src/components/waka-permission-matrix/index.tsx +1302 -0
- package/src/components/waka-phone-input/index.tsx +801 -0
- package/src/components/waka-pipeline-view/index.tsx +604 -0
- package/src/components/waka-player-card/index.tsx +691 -0
- package/src/components/waka-points-popup/index.tsx +366 -0
- package/src/components/waka-power-up/index.tsx +1155 -0
- package/src/components/waka-presence-indicator/index.tsx +1181 -0
- package/src/components/waka-pricing-table/index.tsx +755 -0
- package/src/components/waka-product-card/index.tsx +786 -0
- package/src/components/waka-progress-onboarding/index.tsx +878 -0
- package/src/components/waka-pull-to-refresh/index.tsx +451 -0
- package/src/components/waka-qrcode/index.tsx +232 -0
- package/src/components/waka-quest-card/index.tsx +1275 -0
- package/src/components/waka-quota-bar/index.tsx +693 -0
- package/src/components/waka-radar-score/index.tsx +512 -0
- package/src/components/waka-rank-badge/index.tsx +813 -0
- package/src/components/waka-rating-input/index.tsx +560 -0
- package/src/components/waka-reaction-picker/index.tsx +1062 -0
- package/src/components/waka-region-map/index.tsx +730 -0
- package/src/components/waka-resource-gauge/index.tsx +654 -0
- package/src/components/waka-resource-pool/index.tsx +1035 -0
- package/src/components/waka-rich-text-editor/index.tsx +594 -0
- package/src/components/waka-rollback-slider/index.tsx +891 -0
- package/src/components/waka-sankey-diagram/index.tsx +1032 -0
- package/src/components/waka-schedule-picker/index.tsx +1060 -0
- package/src/components/waka-scratch-card/index.tsx +914 -0
- package/src/components/waka-season-pass/index.tsx +886 -0
- package/src/components/waka-security-score/index.tsx +1126 -0
- package/src/components/waka-segmented-control/index.tsx +238 -0
- package/src/components/waka-server-rack/index.tsx +764 -0
- package/src/components/waka-session-manager/index.tsx +815 -0
- package/src/components/waka-signature-pad/index.tsx +744 -0
- package/src/components/waka-skeleton-wave/index.tsx +454 -0
- package/src/components/waka-skill-tree/index.tsx +1031 -0
- package/src/components/waka-sla-tracker/index.tsx +798 -0
- package/src/components/waka-slider-range/index.tsx +765 -0
- package/src/components/waka-spin-wheel/index.tsx +671 -0
- package/src/components/waka-spinner/index.tsx +284 -0
- package/src/components/waka-spotlight/index.tsx +410 -0
- package/src/components/waka-stat/index.tsx +428 -0
- package/src/components/waka-stats-hexagon/index.tsx +824 -0
- package/src/components/waka-status-matrix/index.tsx +565 -0
- package/src/components/waka-stepper/index.tsx +489 -0
- package/src/components/waka-streak-counter/index.tsx +334 -0
- package/src/components/waka-success-explosion/index.tsx +453 -0
- package/src/components/waka-swipe-card/index.tsx +574 -0
- package/src/components/waka-tabs-morph/index.tsx +509 -0
- package/src/components/waka-tag-input/index.tsx +877 -0
- package/src/components/waka-team-banner/index.tsx +1183 -0
- package/src/components/waka-terminal-output/index.tsx +836 -0
- package/src/components/waka-theme-creator/index.tsx +762 -0
- package/src/components/waka-theme-manager/index.tsx +654 -0
- package/src/components/waka-thread-view/index.tsx +874 -0
- package/src/components/waka-tilt-card/index.tsx +250 -0
- package/src/components/waka-time-picker/index.tsx +479 -0
- package/src/components/waka-timeline/index.tsx +385 -0
- package/src/components/waka-tooltip-tour/index.tsx +855 -0
- package/src/components/waka-tour-guide/index.tsx +920 -0
- package/src/components/waka-tournament-bracket/index.tsx +1276 -0
- package/src/components/waka-tree/index.tsx +557 -0
- package/src/components/waka-treemap-chart/index.tsx +1031 -0
- package/src/components/waka-two-factor-setup/index.tsx +995 -0
- package/src/components/waka-typewriter/index.tsx +566 -0
- package/src/components/waka-typing-indicator/index.tsx +649 -0
- package/src/components/waka-versus-card/index.tsx +1026 -0
- package/src/components/waka-video/index.tsx +557 -0
- package/src/components/waka-video-call/index.tsx +1087 -0
- package/src/components/waka-virtual-list/index.tsx +327 -0
- package/src/components/waka-voice-message/index.tsx +1019 -0
- package/src/components/waka-welcome-modal/index.tsx +790 -0
- package/src/components/waka-xp-bar/index.tsx +799 -0
|
@@ -0,0 +1,765 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cn } from "../../utils/cn"
|
|
5
|
+
import {
|
|
6
|
+
Fingerprint,
|
|
7
|
+
ScanFace,
|
|
8
|
+
Check,
|
|
9
|
+
X,
|
|
10
|
+
KeyRound,
|
|
11
|
+
AlertCircle,
|
|
12
|
+
RefreshCw,
|
|
13
|
+
Loader2,
|
|
14
|
+
} from "lucide-react"
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Types
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
export type BiometricType = "fingerprint" | "face" | "iris" | "voice"
|
|
21
|
+
|
|
22
|
+
export type BiometricState = "idle" | "scanning" | "success" | "error"
|
|
23
|
+
|
|
24
|
+
export interface BiometricPromptConfig {
|
|
25
|
+
/** Type of biometric authentication */
|
|
26
|
+
type: BiometricType
|
|
27
|
+
/** Custom label for the biometric type */
|
|
28
|
+
label?: string
|
|
29
|
+
/** Custom icon component */
|
|
30
|
+
icon?: React.ReactNode
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface WakaBiometricPromptProps {
|
|
34
|
+
/** Available biometric types */
|
|
35
|
+
biometricTypes?: BiometricPromptConfig[]
|
|
36
|
+
/** Currently selected biometric type */
|
|
37
|
+
selectedType?: BiometricType
|
|
38
|
+
/** Callback when biometric type changes */
|
|
39
|
+
onTypeChange?: (type: BiometricType) => void
|
|
40
|
+
/** Title text */
|
|
41
|
+
title?: string
|
|
42
|
+
/** Description text */
|
|
43
|
+
description?: string
|
|
44
|
+
/** Current authentication state */
|
|
45
|
+
state?: BiometricState
|
|
46
|
+
/** Error message when state is "error" */
|
|
47
|
+
errorMessage?: string
|
|
48
|
+
/** Callback when authentication is initiated */
|
|
49
|
+
onAuthenticate?: () => void | Promise<void>
|
|
50
|
+
/** Callback when retry is clicked */
|
|
51
|
+
onRetry?: () => void
|
|
52
|
+
/** Callback when cancel is clicked */
|
|
53
|
+
onCancel?: () => void
|
|
54
|
+
/** Callback when fallback to password is clicked */
|
|
55
|
+
onFallbackToPassword?: () => void
|
|
56
|
+
/** Show fallback to password option */
|
|
57
|
+
showPasswordFallback?: boolean
|
|
58
|
+
/** Show cancel button */
|
|
59
|
+
showCancel?: boolean
|
|
60
|
+
/** Custom scanning duration in ms (for demo purposes) */
|
|
61
|
+
scanningDuration?: number
|
|
62
|
+
/** Success message */
|
|
63
|
+
successMessage?: string
|
|
64
|
+
/** Additional CSS classes */
|
|
65
|
+
className?: string
|
|
66
|
+
/** Size variant */
|
|
67
|
+
size?: "sm" | "md" | "lg"
|
|
68
|
+
/** Color scheme */
|
|
69
|
+
colorScheme?: "default" | "primary" | "dark"
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface UseBiometricPromptOptions {
|
|
73
|
+
/** Initial biometric type */
|
|
74
|
+
initialType?: BiometricType
|
|
75
|
+
/** Simulated scanning duration */
|
|
76
|
+
scanningDuration?: number
|
|
77
|
+
/** Simulated success rate (0-1, for demo) */
|
|
78
|
+
successRate?: number
|
|
79
|
+
/** Callback on successful authentication */
|
|
80
|
+
onSuccess?: () => void
|
|
81
|
+
/** Callback on authentication error */
|
|
82
|
+
onError?: (error: string) => void
|
|
83
|
+
/** Actual authentication function */
|
|
84
|
+
authenticate?: () => Promise<boolean>
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface UseBiometricPromptReturn {
|
|
88
|
+
/** Current state */
|
|
89
|
+
state: BiometricState
|
|
90
|
+
/** Current biometric type */
|
|
91
|
+
selectedType: BiometricType
|
|
92
|
+
/** Error message if any */
|
|
93
|
+
errorMessage: string | null
|
|
94
|
+
/** Set biometric type */
|
|
95
|
+
setSelectedType: (type: BiometricType) => void
|
|
96
|
+
/** Start authentication */
|
|
97
|
+
startAuthentication: () => Promise<void>
|
|
98
|
+
/** Retry authentication */
|
|
99
|
+
retry: () => void
|
|
100
|
+
/** Reset to idle state */
|
|
101
|
+
reset: () => void
|
|
102
|
+
/** Cancel authentication */
|
|
103
|
+
cancel: () => void
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ============================================================================
|
|
107
|
+
// Hook
|
|
108
|
+
// ============================================================================
|
|
109
|
+
|
|
110
|
+
export function useBiometricPrompt(
|
|
111
|
+
options: UseBiometricPromptOptions = {}
|
|
112
|
+
): UseBiometricPromptReturn {
|
|
113
|
+
const {
|
|
114
|
+
initialType = "fingerprint",
|
|
115
|
+
scanningDuration = 2000,
|
|
116
|
+
successRate = 0.9,
|
|
117
|
+
onSuccess,
|
|
118
|
+
onError,
|
|
119
|
+
authenticate,
|
|
120
|
+
} = options
|
|
121
|
+
|
|
122
|
+
const [state, setState] = React.useState<BiometricState>("idle")
|
|
123
|
+
const [selectedType, setSelectedType] = React.useState<BiometricType>(initialType)
|
|
124
|
+
const [errorMessage, setErrorMessage] = React.useState<string | null>(null)
|
|
125
|
+
const timeoutRef = React.useRef<NodeJS.Timeout | null>(null)
|
|
126
|
+
|
|
127
|
+
const clearTimeouts = React.useCallback(() => {
|
|
128
|
+
if (timeoutRef.current) {
|
|
129
|
+
clearTimeout(timeoutRef.current)
|
|
130
|
+
timeoutRef.current = null
|
|
131
|
+
}
|
|
132
|
+
}, [])
|
|
133
|
+
|
|
134
|
+
const reset = React.useCallback(() => {
|
|
135
|
+
clearTimeouts()
|
|
136
|
+
setState("idle")
|
|
137
|
+
setErrorMessage(null)
|
|
138
|
+
}, [clearTimeouts])
|
|
139
|
+
|
|
140
|
+
const cancel = React.useCallback(() => {
|
|
141
|
+
clearTimeouts()
|
|
142
|
+
setState("idle")
|
|
143
|
+
setErrorMessage(null)
|
|
144
|
+
}, [clearTimeouts])
|
|
145
|
+
|
|
146
|
+
const startAuthentication = React.useCallback(async () => {
|
|
147
|
+
clearTimeouts()
|
|
148
|
+
setState("scanning")
|
|
149
|
+
setErrorMessage(null)
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
if (authenticate) {
|
|
153
|
+
// Use actual authentication function
|
|
154
|
+
const success = await authenticate()
|
|
155
|
+
if (success) {
|
|
156
|
+
setState("success")
|
|
157
|
+
onSuccess?.()
|
|
158
|
+
} else {
|
|
159
|
+
setState("error")
|
|
160
|
+
const message = "Authentication failed. Please try again."
|
|
161
|
+
setErrorMessage(message)
|
|
162
|
+
onError?.(message)
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
// Simulated authentication for demo
|
|
166
|
+
await new Promise<void>((resolve) => {
|
|
167
|
+
timeoutRef.current = setTimeout(() => {
|
|
168
|
+
const success = Math.random() < successRate
|
|
169
|
+
if (success) {
|
|
170
|
+
setState("success")
|
|
171
|
+
onSuccess?.()
|
|
172
|
+
} else {
|
|
173
|
+
setState("error")
|
|
174
|
+
const message = "Authentication failed. Please try again."
|
|
175
|
+
setErrorMessage(message)
|
|
176
|
+
onError?.(message)
|
|
177
|
+
}
|
|
178
|
+
resolve()
|
|
179
|
+
}, scanningDuration)
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
} catch (error) {
|
|
183
|
+
setState("error")
|
|
184
|
+
const message = error instanceof Error ? error.message : "An unexpected error occurred."
|
|
185
|
+
setErrorMessage(message)
|
|
186
|
+
onError?.(message)
|
|
187
|
+
}
|
|
188
|
+
}, [authenticate, clearTimeouts, onError, onSuccess, scanningDuration, successRate])
|
|
189
|
+
|
|
190
|
+
const retry = React.useCallback(() => {
|
|
191
|
+
startAuthentication()
|
|
192
|
+
}, [startAuthentication])
|
|
193
|
+
|
|
194
|
+
React.useEffect(() => {
|
|
195
|
+
return () => {
|
|
196
|
+
clearTimeouts()
|
|
197
|
+
}
|
|
198
|
+
}, [clearTimeouts])
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
state,
|
|
202
|
+
selectedType,
|
|
203
|
+
errorMessage,
|
|
204
|
+
setSelectedType,
|
|
205
|
+
startAuthentication,
|
|
206
|
+
retry,
|
|
207
|
+
reset,
|
|
208
|
+
cancel,
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ============================================================================
|
|
213
|
+
// Default Configs
|
|
214
|
+
// ============================================================================
|
|
215
|
+
|
|
216
|
+
const defaultBiometricConfigs: BiometricPromptConfig[] = [
|
|
217
|
+
{ type: "fingerprint", label: "Fingerprint" },
|
|
218
|
+
{ type: "face", label: "Face ID" },
|
|
219
|
+
]
|
|
220
|
+
|
|
221
|
+
const biometricIcons: Record<BiometricType, React.ReactNode> = {
|
|
222
|
+
fingerprint: <Fingerprint className="h-full w-full" />,
|
|
223
|
+
face: <ScanFace className="h-full w-full" />,
|
|
224
|
+
iris: <ScanFace className="h-full w-full" />,
|
|
225
|
+
voice: <ScanFace className="h-full w-full" />,
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ============================================================================
|
|
229
|
+
// Size Configs
|
|
230
|
+
// ============================================================================
|
|
231
|
+
|
|
232
|
+
const sizeConfig = {
|
|
233
|
+
sm: {
|
|
234
|
+
container: "p-4 max-w-xs",
|
|
235
|
+
icon: "h-12 w-12",
|
|
236
|
+
iconInner: "h-8 w-8",
|
|
237
|
+
title: "text-base",
|
|
238
|
+
description: "text-xs",
|
|
239
|
+
button: "h-8 px-3 text-xs",
|
|
240
|
+
typeButton: "h-8 w-8",
|
|
241
|
+
},
|
|
242
|
+
md: {
|
|
243
|
+
container: "p-6 max-w-sm",
|
|
244
|
+
icon: "h-20 w-20",
|
|
245
|
+
iconInner: "h-12 w-12",
|
|
246
|
+
title: "text-lg",
|
|
247
|
+
description: "text-sm",
|
|
248
|
+
button: "h-10 px-4 text-sm",
|
|
249
|
+
typeButton: "h-10 w-10",
|
|
250
|
+
},
|
|
251
|
+
lg: {
|
|
252
|
+
container: "p-8 max-w-md",
|
|
253
|
+
icon: "h-28 w-28",
|
|
254
|
+
iconInner: "h-16 w-16",
|
|
255
|
+
title: "text-xl",
|
|
256
|
+
description: "text-base",
|
|
257
|
+
button: "h-12 px-6 text-base",
|
|
258
|
+
typeButton: "h-12 w-12",
|
|
259
|
+
},
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ============================================================================
|
|
263
|
+
// Color Scheme Configs
|
|
264
|
+
// ============================================================================
|
|
265
|
+
|
|
266
|
+
const colorSchemeConfig = {
|
|
267
|
+
default: {
|
|
268
|
+
background: "bg-background",
|
|
269
|
+
text: "text-foreground",
|
|
270
|
+
muted: "text-muted-foreground",
|
|
271
|
+
primary: "text-primary",
|
|
272
|
+
success: "text-green-500",
|
|
273
|
+
error: "text-destructive",
|
|
274
|
+
iconBg: "bg-muted",
|
|
275
|
+
iconBgScanning: "bg-primary/10",
|
|
276
|
+
iconBgSuccess: "bg-green-500/10",
|
|
277
|
+
iconBgError: "bg-destructive/10",
|
|
278
|
+
button: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
279
|
+
buttonOutline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
|
280
|
+
buttonGhost: "hover:bg-accent hover:text-accent-foreground",
|
|
281
|
+
},
|
|
282
|
+
primary: {
|
|
283
|
+
background: "bg-primary",
|
|
284
|
+
text: "text-primary-foreground",
|
|
285
|
+
muted: "text-primary-foreground/70",
|
|
286
|
+
primary: "text-primary-foreground",
|
|
287
|
+
success: "text-green-300",
|
|
288
|
+
error: "text-red-300",
|
|
289
|
+
iconBg: "bg-primary-foreground/10",
|
|
290
|
+
iconBgScanning: "bg-primary-foreground/20",
|
|
291
|
+
iconBgSuccess: "bg-green-400/20",
|
|
292
|
+
iconBgError: "bg-red-400/20",
|
|
293
|
+
button: "bg-primary-foreground text-primary hover:bg-primary-foreground/90",
|
|
294
|
+
buttonOutline: "border border-primary-foreground/30 bg-transparent hover:bg-primary-foreground/10",
|
|
295
|
+
buttonGhost: "hover:bg-primary-foreground/10",
|
|
296
|
+
},
|
|
297
|
+
dark: {
|
|
298
|
+
background: "bg-zinc-900",
|
|
299
|
+
text: "text-zinc-100",
|
|
300
|
+
muted: "text-zinc-400",
|
|
301
|
+
primary: "text-blue-400",
|
|
302
|
+
success: "text-green-400",
|
|
303
|
+
error: "text-red-400",
|
|
304
|
+
iconBg: "bg-zinc-800",
|
|
305
|
+
iconBgScanning: "bg-blue-500/20",
|
|
306
|
+
iconBgSuccess: "bg-green-500/20",
|
|
307
|
+
iconBgError: "bg-red-500/20",
|
|
308
|
+
button: "bg-blue-500 text-white hover:bg-blue-600",
|
|
309
|
+
buttonOutline: "border border-zinc-700 bg-transparent hover:bg-zinc-800",
|
|
310
|
+
buttonGhost: "hover:bg-zinc-800",
|
|
311
|
+
},
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ============================================================================
|
|
315
|
+
// Scanning Animation Component
|
|
316
|
+
// ============================================================================
|
|
317
|
+
|
|
318
|
+
interface ScanningAnimationProps {
|
|
319
|
+
type: BiometricType
|
|
320
|
+
size: "sm" | "md" | "lg"
|
|
321
|
+
colorScheme: "default" | "primary" | "dark"
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function ScanningAnimation({ type, size, colorScheme }: ScanningAnimationProps) {
|
|
325
|
+
const colors = colorSchemeConfig[colorScheme]
|
|
326
|
+
const sizes = sizeConfig[size]
|
|
327
|
+
|
|
328
|
+
return (
|
|
329
|
+
<div className="absolute inset-0 flex items-center justify-center">
|
|
330
|
+
{/* Pulsing rings */}
|
|
331
|
+
<div className={cn(
|
|
332
|
+
"absolute rounded-full animate-ping",
|
|
333
|
+
colors.iconBgScanning,
|
|
334
|
+
sizes.icon
|
|
335
|
+
)} style={{ animationDuration: "1.5s" }} />
|
|
336
|
+
<div className={cn(
|
|
337
|
+
"absolute rounded-full animate-ping",
|
|
338
|
+
colors.iconBgScanning,
|
|
339
|
+
sizes.icon
|
|
340
|
+
)} style={{ animationDuration: "1.5s", animationDelay: "0.5s" }} />
|
|
341
|
+
|
|
342
|
+
{/* Scanning line for fingerprint */}
|
|
343
|
+
{type === "fingerprint" && (
|
|
344
|
+
<div
|
|
345
|
+
className={cn(
|
|
346
|
+
"absolute h-0.5 rounded-full animate-scan-line",
|
|
347
|
+
colorScheme === "default" ? "bg-primary" :
|
|
348
|
+
colorScheme === "primary" ? "bg-primary-foreground" : "bg-blue-400"
|
|
349
|
+
)}
|
|
350
|
+
style={{ width: "70%" }}
|
|
351
|
+
/>
|
|
352
|
+
)}
|
|
353
|
+
|
|
354
|
+
{/* Scanning frame for face */}
|
|
355
|
+
{type === "face" && (
|
|
356
|
+
<div className="absolute inset-2 border-2 border-dashed rounded-lg animate-pulse"
|
|
357
|
+
style={{
|
|
358
|
+
borderColor: colorScheme === "default" ? "hsl(var(--primary))" :
|
|
359
|
+
colorScheme === "primary" ? "hsl(var(--primary-foreground))" : "#60a5fa"
|
|
360
|
+
}}
|
|
361
|
+
/>
|
|
362
|
+
)}
|
|
363
|
+
</div>
|
|
364
|
+
)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// ============================================================================
|
|
368
|
+
// Success Animation Component
|
|
369
|
+
// ============================================================================
|
|
370
|
+
|
|
371
|
+
interface SuccessAnimationProps {
|
|
372
|
+
size: "sm" | "md" | "lg"
|
|
373
|
+
colorScheme: "default" | "primary" | "dark"
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function SuccessAnimation({ size, colorScheme }: SuccessAnimationProps) {
|
|
377
|
+
const colors = colorSchemeConfig[colorScheme]
|
|
378
|
+
const sizes = sizeConfig[size]
|
|
379
|
+
|
|
380
|
+
return (
|
|
381
|
+
<div className={cn(
|
|
382
|
+
"flex items-center justify-center rounded-full animate-success-pop",
|
|
383
|
+
colors.iconBgSuccess,
|
|
384
|
+
sizes.icon
|
|
385
|
+
)}>
|
|
386
|
+
<Check className={cn(sizes.iconInner, colors.success)} strokeWidth={3} />
|
|
387
|
+
</div>
|
|
388
|
+
)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ============================================================================
|
|
392
|
+
// Error Animation Component
|
|
393
|
+
// ============================================================================
|
|
394
|
+
|
|
395
|
+
interface ErrorAnimationProps {
|
|
396
|
+
size: "sm" | "md" | "lg"
|
|
397
|
+
colorScheme: "default" | "primary" | "dark"
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function ErrorAnimation({ size, colorScheme }: ErrorAnimationProps) {
|
|
401
|
+
const colors = colorSchemeConfig[colorScheme]
|
|
402
|
+
const sizes = sizeConfig[size]
|
|
403
|
+
|
|
404
|
+
return (
|
|
405
|
+
<div className={cn(
|
|
406
|
+
"flex items-center justify-center rounded-full animate-error-shake",
|
|
407
|
+
colors.iconBgError,
|
|
408
|
+
sizes.icon
|
|
409
|
+
)}>
|
|
410
|
+
<AlertCircle className={cn(sizes.iconInner, colors.error)} strokeWidth={2} />
|
|
411
|
+
</div>
|
|
412
|
+
)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// ============================================================================
|
|
416
|
+
// Biometric Type Selector
|
|
417
|
+
// ============================================================================
|
|
418
|
+
|
|
419
|
+
interface BiometricTypeSelectorProps {
|
|
420
|
+
types: BiometricPromptConfig[]
|
|
421
|
+
selectedType: BiometricType
|
|
422
|
+
onTypeChange: (type: BiometricType) => void
|
|
423
|
+
disabled?: boolean
|
|
424
|
+
size: "sm" | "md" | "lg"
|
|
425
|
+
colorScheme: "default" | "primary" | "dark"
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function BiometricTypeSelector({
|
|
429
|
+
types,
|
|
430
|
+
selectedType,
|
|
431
|
+
onTypeChange,
|
|
432
|
+
disabled,
|
|
433
|
+
size,
|
|
434
|
+
colorScheme,
|
|
435
|
+
}: BiometricTypeSelectorProps) {
|
|
436
|
+
const colors = colorSchemeConfig[colorScheme]
|
|
437
|
+
const sizes = sizeConfig[size]
|
|
438
|
+
|
|
439
|
+
if (types.length <= 1) return null
|
|
440
|
+
|
|
441
|
+
return (
|
|
442
|
+
<div className="flex items-center justify-center gap-2 mt-4" role="radiogroup" aria-label="Biometric type selection">
|
|
443
|
+
{types.map((config) => {
|
|
444
|
+
const isSelected = config.type === selectedType
|
|
445
|
+
const icon = config.icon || biometricIcons[config.type]
|
|
446
|
+
|
|
447
|
+
return (
|
|
448
|
+
<button
|
|
449
|
+
key={config.type}
|
|
450
|
+
type="button"
|
|
451
|
+
role="radio"
|
|
452
|
+
aria-checked={isSelected}
|
|
453
|
+
aria-label={config.label || config.type}
|
|
454
|
+
disabled={disabled}
|
|
455
|
+
onClick={() => onTypeChange(config.type)}
|
|
456
|
+
className={cn(
|
|
457
|
+
"flex items-center justify-center rounded-full transition-all",
|
|
458
|
+
"focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
459
|
+
sizes.typeButton,
|
|
460
|
+
isSelected
|
|
461
|
+
? cn(colors.button)
|
|
462
|
+
: cn(colors.buttonGhost, colors.muted),
|
|
463
|
+
disabled && "opacity-50 cursor-not-allowed"
|
|
464
|
+
)}
|
|
465
|
+
>
|
|
466
|
+
<div className={cn(
|
|
467
|
+
size === "sm" ? "h-4 w-4" : size === "md" ? "h-5 w-5" : "h-6 w-6"
|
|
468
|
+
)}>
|
|
469
|
+
{icon}
|
|
470
|
+
</div>
|
|
471
|
+
</button>
|
|
472
|
+
)
|
|
473
|
+
})}
|
|
474
|
+
</div>
|
|
475
|
+
)
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// ============================================================================
|
|
479
|
+
// Main Component
|
|
480
|
+
// ============================================================================
|
|
481
|
+
|
|
482
|
+
export const WakaBiometricPrompt = React.forwardRef<
|
|
483
|
+
HTMLDivElement,
|
|
484
|
+
WakaBiometricPromptProps
|
|
485
|
+
>(
|
|
486
|
+
(
|
|
487
|
+
{
|
|
488
|
+
biometricTypes = defaultBiometricConfigs,
|
|
489
|
+
selectedType: controlledSelectedType,
|
|
490
|
+
onTypeChange,
|
|
491
|
+
title = "Use biometrics",
|
|
492
|
+
description = "Authenticate using your biometric data",
|
|
493
|
+
state = "idle",
|
|
494
|
+
errorMessage,
|
|
495
|
+
onAuthenticate,
|
|
496
|
+
onRetry,
|
|
497
|
+
onCancel,
|
|
498
|
+
onFallbackToPassword,
|
|
499
|
+
showPasswordFallback = true,
|
|
500
|
+
showCancel = true,
|
|
501
|
+
successMessage = "Authentication successful",
|
|
502
|
+
className,
|
|
503
|
+
size = "md",
|
|
504
|
+
colorScheme = "default",
|
|
505
|
+
},
|
|
506
|
+
ref
|
|
507
|
+
) => {
|
|
508
|
+
const [internalSelectedType, setInternalSelectedType] = React.useState<BiometricType>(
|
|
509
|
+
biometricTypes[0]?.type || "fingerprint"
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
const selectedType = controlledSelectedType ?? internalSelectedType
|
|
513
|
+
const handleTypeChange = onTypeChange ?? setInternalSelectedType
|
|
514
|
+
|
|
515
|
+
const colors = colorSchemeConfig[colorScheme]
|
|
516
|
+
const sizes = sizeConfig[size]
|
|
517
|
+
|
|
518
|
+
const currentConfig = biometricTypes.find((c) => c.type === selectedType) || biometricTypes[0]
|
|
519
|
+
const icon = currentConfig?.icon || biometricIcons[selectedType]
|
|
520
|
+
|
|
521
|
+
const isDisabled = state === "scanning" || state === "success"
|
|
522
|
+
|
|
523
|
+
// Handle keyboard navigation
|
|
524
|
+
const handleKeyDown = (event: React.KeyboardEvent) => {
|
|
525
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
526
|
+
event.preventDefault()
|
|
527
|
+
if (state === "idle") {
|
|
528
|
+
onAuthenticate?.()
|
|
529
|
+
} else if (state === "error") {
|
|
530
|
+
onRetry?.()
|
|
531
|
+
}
|
|
532
|
+
} else if (event.key === "Escape") {
|
|
533
|
+
event.preventDefault()
|
|
534
|
+
onCancel?.()
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return (
|
|
539
|
+
<div
|
|
540
|
+
ref={ref}
|
|
541
|
+
className={cn(
|
|
542
|
+
"flex flex-col items-center rounded-2xl shadow-lg",
|
|
543
|
+
colors.background,
|
|
544
|
+
sizes.container,
|
|
545
|
+
className
|
|
546
|
+
)}
|
|
547
|
+
role="dialog"
|
|
548
|
+
aria-labelledby="biometric-title"
|
|
549
|
+
aria-describedby="biometric-description"
|
|
550
|
+
onKeyDown={handleKeyDown}
|
|
551
|
+
>
|
|
552
|
+
{/* Icon Container */}
|
|
553
|
+
<div className="relative flex items-center justify-center mb-4">
|
|
554
|
+
{state === "idle" && (
|
|
555
|
+
<button
|
|
556
|
+
type="button"
|
|
557
|
+
onClick={onAuthenticate}
|
|
558
|
+
disabled={isDisabled}
|
|
559
|
+
className={cn(
|
|
560
|
+
"flex items-center justify-center rounded-full transition-all",
|
|
561
|
+
"focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
562
|
+
"hover:scale-105 active:scale-95",
|
|
563
|
+
colors.iconBg,
|
|
564
|
+
colors.primary,
|
|
565
|
+
sizes.icon
|
|
566
|
+
)}
|
|
567
|
+
aria-label={`Authenticate with ${currentConfig?.label || selectedType}`}
|
|
568
|
+
>
|
|
569
|
+
<div className={sizes.iconInner}>{icon}</div>
|
|
570
|
+
</button>
|
|
571
|
+
)}
|
|
572
|
+
|
|
573
|
+
{state === "scanning" && (
|
|
574
|
+
<div className={cn(
|
|
575
|
+
"relative flex items-center justify-center rounded-full",
|
|
576
|
+
colors.iconBgScanning,
|
|
577
|
+
colors.primary,
|
|
578
|
+
sizes.icon
|
|
579
|
+
)}>
|
|
580
|
+
<div className={cn(sizes.iconInner, "animate-pulse")}>{icon}</div>
|
|
581
|
+
<ScanningAnimation type={selectedType} size={size} colorScheme={colorScheme} />
|
|
582
|
+
</div>
|
|
583
|
+
)}
|
|
584
|
+
|
|
585
|
+
{state === "success" && (
|
|
586
|
+
<SuccessAnimation size={size} colorScheme={colorScheme} />
|
|
587
|
+
)}
|
|
588
|
+
|
|
589
|
+
{state === "error" && (
|
|
590
|
+
<ErrorAnimation size={size} colorScheme={colorScheme} />
|
|
591
|
+
)}
|
|
592
|
+
</div>
|
|
593
|
+
|
|
594
|
+
{/* Title */}
|
|
595
|
+
<h2
|
|
596
|
+
id="biometric-title"
|
|
597
|
+
className={cn("font-semibold text-center", colors.text, sizes.title)}
|
|
598
|
+
>
|
|
599
|
+
{state === "success" ? successMessage : state === "error" ? "Authentication failed" : title}
|
|
600
|
+
</h2>
|
|
601
|
+
|
|
602
|
+
{/* Description */}
|
|
603
|
+
<p
|
|
604
|
+
id="biometric-description"
|
|
605
|
+
className={cn("text-center mt-1", colors.muted, sizes.description)}
|
|
606
|
+
>
|
|
607
|
+
{state === "scanning" && `Scanning ${currentConfig?.label || selectedType}...`}
|
|
608
|
+
{state === "error" && (errorMessage || "Please try again")}
|
|
609
|
+
{state === "success" && "You can now proceed"}
|
|
610
|
+
{state === "idle" && description}
|
|
611
|
+
</p>
|
|
612
|
+
|
|
613
|
+
{/* Biometric Type Selector */}
|
|
614
|
+
{state === "idle" && (
|
|
615
|
+
<BiometricTypeSelector
|
|
616
|
+
types={biometricTypes}
|
|
617
|
+
selectedType={selectedType}
|
|
618
|
+
onTypeChange={handleTypeChange}
|
|
619
|
+
disabled={isDisabled}
|
|
620
|
+
size={size}
|
|
621
|
+
colorScheme={colorScheme}
|
|
622
|
+
/>
|
|
623
|
+
)}
|
|
624
|
+
|
|
625
|
+
{/* Loading indicator for scanning state */}
|
|
626
|
+
{state === "scanning" && (
|
|
627
|
+
<div className={cn("flex items-center gap-2 mt-4", colors.muted)}>
|
|
628
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
629
|
+
<span className={sizes.description}>Processing...</span>
|
|
630
|
+
</div>
|
|
631
|
+
)}
|
|
632
|
+
|
|
633
|
+
{/* Action Buttons */}
|
|
634
|
+
<div className="flex flex-col items-center gap-2 mt-6 w-full">
|
|
635
|
+
{state === "error" && onRetry && (
|
|
636
|
+
<button
|
|
637
|
+
type="button"
|
|
638
|
+
onClick={onRetry}
|
|
639
|
+
className={cn(
|
|
640
|
+
"inline-flex items-center justify-center gap-2 rounded-lg font-medium transition-colors",
|
|
641
|
+
"focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
642
|
+
colors.button,
|
|
643
|
+
sizes.button,
|
|
644
|
+
"w-full"
|
|
645
|
+
)}
|
|
646
|
+
aria-label="Retry authentication"
|
|
647
|
+
>
|
|
648
|
+
<RefreshCw className="h-4 w-4" />
|
|
649
|
+
Try again
|
|
650
|
+
</button>
|
|
651
|
+
)}
|
|
652
|
+
|
|
653
|
+
{state === "idle" && showPasswordFallback && onFallbackToPassword && (
|
|
654
|
+
<button
|
|
655
|
+
type="button"
|
|
656
|
+
onClick={onFallbackToPassword}
|
|
657
|
+
className={cn(
|
|
658
|
+
"inline-flex items-center justify-center gap-2 rounded-lg font-medium transition-colors",
|
|
659
|
+
"focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
660
|
+
colors.buttonOutline,
|
|
661
|
+
sizes.button,
|
|
662
|
+
"w-full"
|
|
663
|
+
)}
|
|
664
|
+
aria-label="Use password instead"
|
|
665
|
+
>
|
|
666
|
+
<KeyRound className="h-4 w-4" />
|
|
667
|
+
Use password instead
|
|
668
|
+
</button>
|
|
669
|
+
)}
|
|
670
|
+
|
|
671
|
+
{(state === "idle" || state === "error") && showCancel && onCancel && (
|
|
672
|
+
<button
|
|
673
|
+
type="button"
|
|
674
|
+
onClick={onCancel}
|
|
675
|
+
className={cn(
|
|
676
|
+
"inline-flex items-center justify-center gap-2 rounded-lg font-medium transition-colors",
|
|
677
|
+
"focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
678
|
+
colors.buttonGhost,
|
|
679
|
+
colors.muted,
|
|
680
|
+
sizes.button
|
|
681
|
+
)}
|
|
682
|
+
aria-label="Cancel authentication"
|
|
683
|
+
>
|
|
684
|
+
<X className="h-4 w-4" />
|
|
685
|
+
Cancel
|
|
686
|
+
</button>
|
|
687
|
+
)}
|
|
688
|
+
</div>
|
|
689
|
+
</div>
|
|
690
|
+
)
|
|
691
|
+
}
|
|
692
|
+
)
|
|
693
|
+
|
|
694
|
+
WakaBiometricPrompt.displayName = "WakaBiometricPrompt"
|
|
695
|
+
|
|
696
|
+
// ============================================================================
|
|
697
|
+
// CSS Animations (injected dynamically)
|
|
698
|
+
// ============================================================================
|
|
699
|
+
|
|
700
|
+
if (typeof document !== "undefined") {
|
|
701
|
+
const styleId = "waka-biometric-prompt-styles"
|
|
702
|
+
if (!document.getElementById(styleId)) {
|
|
703
|
+
const style = document.createElement("style")
|
|
704
|
+
style.id = styleId
|
|
705
|
+
style.textContent = `
|
|
706
|
+
@keyframes scan-line {
|
|
707
|
+
0%, 100% {
|
|
708
|
+
transform: translateY(-200%);
|
|
709
|
+
opacity: 0;
|
|
710
|
+
}
|
|
711
|
+
10%, 90% {
|
|
712
|
+
opacity: 1;
|
|
713
|
+
}
|
|
714
|
+
50% {
|
|
715
|
+
transform: translateY(200%);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
@keyframes success-pop {
|
|
720
|
+
0% {
|
|
721
|
+
transform: scale(0);
|
|
722
|
+
opacity: 0;
|
|
723
|
+
}
|
|
724
|
+
50% {
|
|
725
|
+
transform: scale(1.2);
|
|
726
|
+
}
|
|
727
|
+
100% {
|
|
728
|
+
transform: scale(1);
|
|
729
|
+
opacity: 1;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
@keyframes error-shake {
|
|
734
|
+
0%, 100% {
|
|
735
|
+
transform: translateX(0);
|
|
736
|
+
}
|
|
737
|
+
10%, 30%, 50%, 70%, 90% {
|
|
738
|
+
transform: translateX(-4px);
|
|
739
|
+
}
|
|
740
|
+
20%, 40%, 60%, 80% {
|
|
741
|
+
transform: translateX(4px);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
.animate-scan-line {
|
|
746
|
+
animation: scan-line 2s ease-in-out infinite;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
.animate-success-pop {
|
|
750
|
+
animation: success-pop 0.4s ease-out forwards;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
.animate-error-shake {
|
|
754
|
+
animation: error-shake 0.5s ease-in-out;
|
|
755
|
+
}
|
|
756
|
+
`
|
|
757
|
+
document.head.appendChild(style)
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// ============================================================================
|
|
762
|
+
// Exports
|
|
763
|
+
// ============================================================================
|
|
764
|
+
|
|
765
|
+
export default WakaBiometricPrompt
|