diff --git a/.gitignore b/.gitignore index 2557c855..ffbbad20 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,6 @@ fastlane/report.xml .expo .vercel .env*.local + +# Claude +.claude diff --git a/Gemfile.lock b/Gemfile.lock index ecd9b444..59ddc16f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -296,6 +296,7 @@ PLATFORMS arm64-darwin-22 arm64-darwin-23 arm64-darwin-24 + arm64-darwin-25 DEPENDENCIES cocoapods (= 1.14.3) diff --git a/dapps/poc-pos-app/app/scan.tsx b/dapps/poc-pos-app/app/scan.tsx index e5e3c66e..80f910f1 100644 --- a/dapps/poc-pos-app/app/scan.tsx +++ b/dapps/poc-pos-app/app/scan.tsx @@ -5,6 +5,7 @@ import QRCode from "@/components/qr-code"; import { ThemedText } from "@/components/themed-text"; import { WalletConnectLoading } from "@/components/walletconnect-loading"; import { Spacing } from "@/constants/spacing"; +import { useNfcPayment } from "@/hooks/use-nfc-payment"; import { useTheme } from "@/hooks/use-theme-color"; import { useLogsStore } from "@/store/useLogsStore"; import { useSettingsStore } from "@/store/useSettingsStore"; @@ -33,12 +34,28 @@ export default function ScanScreen() { const [qrUri, setQrUri] = useState(""); const [paymentId, setPaymentId] = useState(null); - const { deviceId, merchantId } = useSettingsStore((state) => state); + const { deviceId, merchantId, nfcEnabled } = useSettingsStore((state) => state); const addLog = useLogsStore((state) => state.addLog); const Theme = useTheme(); const { amount } = params; + // NFC integration - Android HCE or iOS VAS + const { isNfcActive, nfcMode } = useNfcPayment({ + paymentUrl: qrUri, + enabled: !!qrUri && nfcEnabled, + onNfcReady: () => { + const modeLabel = nfcMode === "hce" ? "HCE" : nfcMode === "vas" ? "VAS" : "NFC"; + addLog("info", `NFC ${modeLabel} activated`, "scan", "useNfcPayment", { + paymentUrl: qrUri, + nfcMode, + }); + }, + onNfcError: (error) => { + addLog("error", error.message, "scan", "useNfcPayment", { error, nfcMode }); + }, + }); + const onSuccess = useCallback( (data: PaymentStatusResponse) => { const { @@ -167,6 +184,18 @@ export default function ScanScreen() { const isProcessing = paymentStatusData?.status === "processing"; + // Determine instruction text based on NFC availability + // Show "Tap or scan" when NFC is active (HCE on Android or VAS on iOS) + const showNfcOption = nfcMode !== "none" && isNfcActive; + const instructionText = showNfcOption ? "Tap or scan to pay" : "Scan to pay"; + + // Get NFC mode label for display + const getNfcModeLabel = () => { + if (nfcMode === "hce") return "NFC Ready (HCE)"; + if (nfcMode === "vas") return "NFC Ready (VAS)"; + return "NFC Ready"; + }; + return ( {isProcessing ? ( @@ -186,8 +215,19 @@ export default function ScanScreen() { - Scan to pay + {instructionText} + {/* NFC Status Indicator (when HCE or VAS is active) */} + {showNfcOption && ( + + + + {getNfcModeLabel()} + + + )} state.getVariantPrinterLogo, ); + const nfcEnabled = useSettingsStore((state) => state.nfcEnabled); + const setNfcEnabled = useSettingsStore((state) => state.setNfcEnabled); const addLog = useLogsStore((state) => state.addLog); const theme = useTheme(); + // NFC capabilities for toggle + const nfcCapabilities = useNfcCapabilities(); + // Custom hooks for biometrics and merchant flow const { biometricStatus, @@ -99,6 +105,42 @@ export default function SettingsScreen() { setVariant(value); }; + const handleNfcToggle = (value: boolean) => { + // Only check NFC support when trying to enable + if (value) { + // Check platform-specific NFC support + if (Platform.OS === "android" && !nfcCapabilities.isHceSupported) { + showErrorToast("NFC not supported on this device"); + return; + } + if (Platform.OS === "ios" && !nfcCapabilities.isVasAvailable) { + showErrorToast(nfcCapabilities.vasReason || "NFC not available"); + return; + } + } + setNfcEnabled(value); + }; + + // Show NFC toggle on both platforms when mode is available + const isNfcModeAvailable = nfcCapabilities.nfcMode !== "none"; + const showNfcToggle = Platform.OS === "android" || Platform.OS === "ios"; + + // Get NFC support description + const getNfcDescription = () => { + if (nfcCapabilities.isLoading) return "Checking NFC support..."; + if (Platform.OS === "android") { + return nfcCapabilities.isHceSupported + ? "Tap to pay via NFC (HCE)" + : "HCE not supported"; + } + if (Platform.OS === "ios") { + return nfcCapabilities.isVasAvailable + ? "Tap to pay via NFC (VAS)" + : "VAS not available"; + } + return "NFC not supported"; + }; + const handleTestPrinterPress = async () => { try { const isBluetoothPermissionGranted = await requestBluetoothPermission(); @@ -196,6 +238,28 @@ export default function SettingsScreen() { /> + {/* NFC Support toggle - Android HCE / iOS VAS */} + {showNfcToggle && ( + + + + + NFC Support + + + {getNfcDescription()} + + + + + + )} + Merchant ID diff --git a/dapps/poc-pos-app/hooks/use-nfc-capabilities.ts b/dapps/poc-pos-app/hooks/use-nfc-capabilities.ts new file mode 100644 index 00000000..fa2121d8 --- /dev/null +++ b/dapps/poc-pos-app/hooks/use-nfc-capabilities.ts @@ -0,0 +1,190 @@ +import { useEffect, useState } from "react"; +import { NativeModules, Platform } from "react-native"; + +const { HceModule, VasModule } = NativeModules; + +export type NfcMode = "hce" | "vas" | "none"; + +export interface NfcCapabilities { + /** Device has NFC hardware */ + isNfcSupported: boolean; + /** NFC is turned on in settings */ + isNfcEnabled: boolean; + /** Device supports Host Card Emulation (Android only) */ + isHceSupported: boolean; + /** Device supports Apple VAS via Tap to Pay (iOS only) */ + isVasSupported: boolean; + /** VAS is available and ready to use (entitlement configured) */ + isVasAvailable: boolean; + /** The active NFC mode for this device */ + nfcMode: NfcMode; + /** Still checking capabilities */ + isLoading: boolean; + /** Error message if check failed */ + error: string | null; + /** Reason for VAS unavailability (iOS only) */ + vasReason: string | null; +} + +/** + * Hook to check NFC capabilities of the device + * + * Returns platform-specific NFC capabilities: + * - Android: HCE (Host Card Emulation) for NDEF tag emulation + * - iOS: VAS (Value Added Services) via Tap to Pay on iPhone + * + * The `nfcMode` indicates which mode is available: + * - "hce": Android HCE mode + * - "vas": iOS VAS mode (Tap to Pay) + * - "none": No NFC transmission available + */ +export function useNfcCapabilities(): NfcCapabilities { + const [capabilities, setCapabilities] = useState({ + isNfcSupported: false, + isNfcEnabled: false, + isHceSupported: false, + isVasSupported: false, + isVasAvailable: false, + nfcMode: "none", + isLoading: true, + error: null, + vasReason: null, + }); + + useEffect(() => { + async function checkCapabilities() { + try { + // iOS: Check VAS support via Tap to Pay + if (Platform.OS === "ios") { + if (!VasModule) { + // VasModule not available - gracefully disable NFC features + setCapabilities({ + isNfcSupported: true, // Modern iOS has NFC + isNfcEnabled: true, + isHceSupported: false, // iOS never supports HCE + isVasSupported: false, + isVasAvailable: false, + nfcMode: "none", + isLoading: false, + error: null, + vasReason: "VAS module not available", + }); + return; + } + + try { + const result = await VasModule.getVasCapabilities(); + const isVasAvailable = result.isVasAvailable ?? false; + + setCapabilities({ + isNfcSupported: true, + isNfcEnabled: true, + isHceSupported: false, + isVasSupported: result.isVasSupported ?? false, + isVasAvailable, + nfcMode: isVasAvailable ? "vas" : "none", + isLoading: false, + error: null, + vasReason: result.reason ?? null, + }); + } catch (error) { + console.warn("VAS capabilities check failed:", error); + setCapabilities({ + isNfcSupported: true, + isNfcEnabled: true, + isHceSupported: false, + isVasSupported: false, + isVasAvailable: false, + nfcMode: "none", + isLoading: false, + error: null, + vasReason: "Failed to check VAS capabilities", + }); + } + return; + } + + // Android: Check HCE support + if (Platform.OS === "android") { + if (!HceModule) { + // HceModule not available - gracefully disable NFC features + setCapabilities({ + isNfcSupported: false, + isNfcEnabled: false, + isHceSupported: false, + isVasSupported: false, + isVasAvailable: false, + nfcMode: "none", + isLoading: false, + error: null, + vasReason: null, + }); + return; + } + + try { + const result = await HceModule.getNfcCapabilities(); + const isHceSupported = result.isHceSupported ?? false; + + setCapabilities({ + isNfcSupported: result.isNfcSupported ?? false, + isNfcEnabled: result.isNfcEnabled ?? false, + isHceSupported, + isVasSupported: false, // Android doesn't support VAS + isVasAvailable: false, + nfcMode: isHceSupported ? "hce" : "none", + isLoading: false, + error: null, + vasReason: null, + }); + } catch (error) { + console.warn("NFC capabilities check failed:", error); + setCapabilities({ + isNfcSupported: false, + isNfcEnabled: false, + isHceSupported: false, + isVasSupported: false, + isVasAvailable: false, + nfcMode: "none", + isLoading: false, + error: null, + vasReason: null, + }); + } + return; + } + + // Other platforms (web, etc.) + setCapabilities({ + isNfcSupported: false, + isNfcEnabled: false, + isHceSupported: false, + isVasSupported: false, + isVasAvailable: false, + nfcMode: "none", + isLoading: false, + error: null, + vasReason: null, + }); + } catch (error) { + // Catch-all for any unexpected errors + console.warn("Unexpected error checking NFC capabilities:", error); + setCapabilities({ + isNfcSupported: false, + isNfcEnabled: false, + isHceSupported: false, + isVasSupported: false, + isVasAvailable: false, + nfcMode: "none", + isLoading: false, + error: null, + vasReason: null, + }); + } + } + + checkCapabilities(); + }, []); + + return capabilities; +} diff --git a/dapps/poc-pos-app/hooks/use-nfc-payment.ts b/dapps/poc-pos-app/hooks/use-nfc-payment.ts new file mode 100644 index 00000000..04421dfe --- /dev/null +++ b/dapps/poc-pos-app/hooks/use-nfc-payment.ts @@ -0,0 +1,235 @@ +import { useEffect, useRef, useCallback, useState } from "react"; +import { NativeModules, Platform, AppState, AppStateStatus } from "react-native"; +import { useNfcCapabilities, NfcMode } from "./use-nfc-capabilities"; + +const { HceModule, VasModule } = NativeModules; + +interface UseNfcPaymentOptions { + /** The payment URL to transmit via NFC */ + paymentUrl: string; + /** Whether NFC transmission should be active */ + enabled: boolean; + /** Callback when NFC is successfully set up */ + onNfcReady?: () => void; + /** Callback when an error occurs */ + onNfcError?: (error: Error) => void; +} + +interface UseNfcPaymentReturn { + /** Whether NFC is currently active and transmitting */ + isNfcActive: boolean; + /** Whether the device supports HCE mode (Android) */ + isHceMode: boolean; + /** Whether the device supports VAS mode (iOS) */ + isVasMode: boolean; + /** The active NFC mode */ + nfcMode: NfcMode; + /** Whether NFC capabilities are still loading */ + isLoading: boolean; + /** Start NFC transmission manually */ + startNfc: () => Promise; + /** Stop NFC transmission manually */ + stopNfc: () => Promise; + /** Update the payment URL while active */ + updateUrl: (url: string) => Promise; +} + +/** + * Hook to manage NFC payment transmission + * + * This hook handles platform-specific NFC transmission: + * - Android: HCE (Host Card Emulation) - emulates NFC tag with NDEF URL + * - iOS: VAS (Value Added Services) - pushes URL via Tap to Pay + * + * Usage: + * ```tsx + * const { isNfcActive, nfcMode } = useNfcPayment({ + * paymentUrl: 'https://pay.example.com/abc123', + * enabled: true, + * }); + * ``` + */ +export function useNfcPayment(options: UseNfcPaymentOptions): UseNfcPaymentReturn { + const { paymentUrl, enabled, onNfcReady, onNfcError } = options; + const capabilities = useNfcCapabilities(); + const [isNfcActive, setIsNfcActive] = useState(false); + const currentUrlRef = useRef(paymentUrl); + + // Update URL ref when it changes + useEffect(() => { + currentUrlRef.current = paymentUrl; + }, [paymentUrl]); + + // Start NFC for Android HCE + const startHce = useCallback(async () => { + if (Platform.OS !== "android" || !HceModule) { + return false; + } + + if (!capabilities.isHceSupported) { + console.log("HCE not supported on this device"); + return false; + } + + try { + await HceModule.setPaymentUrl(currentUrlRef.current); + console.log("NFC HCE started with URL:", currentUrlRef.current); + return true; + } catch (error) { + console.error("Failed to start HCE:", error); + throw error; + } + }, [capabilities.isHceSupported]); + + // Start NFC for iOS VAS + const startVas = useCallback(async () => { + if (Platform.OS !== "ios" || !VasModule) { + return false; + } + + if (!capabilities.isVasAvailable) { + console.log("VAS not available on this device:", capabilities.vasReason); + return false; + } + + try { + await VasModule.setPaymentUrl(currentUrlRef.current); + console.log("NFC VAS started with URL:", currentUrlRef.current); + return true; + } catch (error) { + console.error("Failed to start VAS:", error); + throw error; + } + }, [capabilities.isVasAvailable, capabilities.vasReason]); + + // Unified start NFC function + const startNfc = useCallback(async () => { + try { + let success = false; + + if (Platform.OS === "android") { + success = await startHce(); + } else if (Platform.OS === "ios") { + success = await startVas(); + } + + if (success) { + setIsNfcActive(true); + onNfcReady?.(); + } + } catch (error) { + onNfcError?.(error instanceof Error ? error : new Error("Failed to start NFC")); + } + }, [startHce, startVas, onNfcReady, onNfcError]); + + // Stop NFC for Android HCE + const stopHce = useCallback(async () => { + if (Platform.OS !== "android" || !HceModule) { + return; + } + + try { + await HceModule.clearPaymentUrl(); + console.log("NFC HCE stopped"); + } catch (error) { + console.error("Failed to stop HCE:", error); + } + }, []); + + // Stop NFC for iOS VAS + const stopVas = useCallback(async () => { + if (Platform.OS !== "ios" || !VasModule) { + return; + } + + try { + await VasModule.clearPaymentUrl(); + console.log("NFC VAS stopped"); + } catch (error) { + console.error("Failed to stop VAS:", error); + } + }, []); + + // Unified stop NFC function + const stopNfc = useCallback(async () => { + if (Platform.OS === "android") { + await stopHce(); + } else if (Platform.OS === "ios") { + await stopVas(); + } + setIsNfcActive(false); + }, [stopHce, stopVas]); + + // Update URL while NFC is active + const updateUrl = useCallback(async (url: string) => { + currentUrlRef.current = url; + + if (!isNfcActive) { + return; + } + + try { + if (Platform.OS === "android" && HceModule) { + await HceModule.setPaymentUrl(url); + console.log("HCE URL updated:", url); + } else if (Platform.OS === "ios" && VasModule) { + await VasModule.setPaymentUrl(url); + console.log("VAS URL updated:", url); + } + } catch (error) { + console.error("Failed to update NFC URL:", error); + } + }, [isNfcActive]); + + // Handle app state changes + useEffect(() => { + const handleAppStateChange = (nextAppState: AppStateStatus) => { + if (nextAppState === "active" && enabled && capabilities.nfcMode !== "none") { + // Re-activate NFC when app comes to foreground + startNfc(); + } + // Note: We don't stop NFC when going to background + // HCE continues to work in background on Android + // VAS requires app to be in foreground on iOS + }; + + const subscription = AppState.addEventListener("change", handleAppStateChange); + return () => subscription.remove(); + }, [enabled, capabilities.nfcMode, startNfc]); + + // Start/stop based on enabled state and URL availability + useEffect(() => { + const canStart = !capabilities.isLoading && + enabled && + paymentUrl && + capabilities.nfcMode !== "none"; + + if (canStart) { + startNfc(); + } else if (!enabled || !paymentUrl) { + stopNfc(); + } + + return () => { + stopNfc(); + }; + }, [enabled, paymentUrl, capabilities.isLoading, capabilities.nfcMode, startNfc, stopNfc]); + + // Update URL when it changes while active + useEffect(() => { + if (isNfcActive && paymentUrl) { + updateUrl(paymentUrl); + } + }, [paymentUrl, isNfcActive, updateUrl]); + + return { + isNfcActive, + isHceMode: capabilities.isHceSupported, + isVasMode: capabilities.isVasAvailable, + nfcMode: capabilities.nfcMode, + isLoading: capabilities.isLoading, + startNfc, + stopNfc, + updateUrl, + }; +} diff --git a/dapps/poc-pos-app/package-lock.json b/dapps/poc-pos-app/package-lock.json index d3d7b714..ba41166c 100644 --- a/dapps/poc-pos-app/package-lock.json +++ b/dapps/poc-pos-app/package-lock.json @@ -38,7 +38,9 @@ "react-native-device-info": "^14.1.1", "react-native-gesture-handler": "~2.28.0", "react-native-get-random-values": "~1.11.0", + "react-native-hce": "^0.3.0", "react-native-mmkv": "^4.0.0", + "react-native-nfc-manager": "^3.17.2", "react-native-nitro-modules": "^0.31.5", "react-native-permissions": "^5.4.4", "react-native-qrcode-skia": "^0.3.1", @@ -97,12 +99,12 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", + "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -111,9 +113,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", - "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz", + "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -151,13 +153,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", - "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", + "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -179,12 +181,12 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.27.2", + "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", @@ -195,17 +197,17 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.3.tgz", - "integrity": "sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", + "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==", "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/helper-replace-supers": "^7.27.1", + "@babel/helper-replace-supers": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/traverse": "^7.28.3", + "@babel/traverse": "^7.28.6", "semver": "^6.3.1" }, "engines": { @@ -216,13 +218,13 @@ } }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz", - "integrity": "sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz", + "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "regexpu-core": "^6.2.0", + "@babel/helper-annotate-as-pure": "^7.27.3", + "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "engines": { @@ -258,40 +260,40 @@ } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", - "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -313,9 +315,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -339,14 +341,14 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", - "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", + "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", "license": "MIT", "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -509,12 +511,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", "license": "MIT", "dependencies": { - "@babel/types": "^7.28.5" + "@babel/types": "^7.28.6" }, "bin": { "parser": "bin/babel-parser.js" @@ -523,6 +525,85 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz", + "integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", + "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", + "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", + "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.6.tgz", + "integrity": "sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/@babel/plugin-proposal-decorators": { "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.28.0.tgz", @@ -555,6 +636,18 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-async-generators": { "version": "7.8.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", @@ -663,13 +756,28 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz", + "integrity": "sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", - "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -834,6 +942,22 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/@babel/plugin-transform-arrow-functions": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", @@ -850,14 +974,14 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.0.tgz", - "integrity": "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.6.tgz", + "integrity": "sha512-9knsChgsMzBV5Yh3kkhrZNxH3oCYAfMBkNNaVN4cP2RVlFPe8wYdwwcnOsAbkdDoV9UjFtOXWrWB52M8W4jNeA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-remap-async-to-generator": "^7.27.1", - "@babel/traverse": "^7.28.0" + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -867,13 +991,13 @@ } }, "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz", - "integrity": "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.28.6.tgz", + "integrity": "sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==", "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-remap-async-to-generator": "^7.27.1" }, "engines": { @@ -883,10 +1007,10 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.5.tgz", - "integrity": "sha512-45DmULpySVvmq9Pj3X9B+62Xe+DJGov27QravQJU1LLcapR6/10i+gYVAucGGJpHBp5mYxIMK4nDAT/QDLr47g==", + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", + "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -898,14 +1022,29 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.6.tgz", + "integrity": "sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz", - "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.28.6.tgz", + "integrity": "sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==", "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -915,13 +1054,13 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.3.tgz", - "integrity": "sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.6.tgz", + "integrity": "sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==", "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.28.3", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -931,17 +1070,17 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz", - "integrity": "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.6.tgz", + "integrity": "sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==", "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-globals": "^7.28.0", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-replace-supers": "^7.27.1", - "@babel/traverse": "^7.28.4" + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -951,13 +1090,13 @@ } }, "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz", - "integrity": "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.28.6.tgz", + "integrity": "sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/template": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/template": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -982,6 +1121,99 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.28.6.tgz", + "integrity": "sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", + "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.28.6.tgz", + "integrity": "sha512-5suVoXjC14lUN6ZL9OLKIHCNVWCrqGqlmEp/ixdXjvgnEl/kauLvvMO/Xw9NyMc95Joj1AeLVPVMvibBgSoFlA==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", + "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-explicit-resource-management": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.6.tgz", + "integrity": "sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.6.tgz", + "integrity": "sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-export-namespace-from": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", @@ -1046,13 +1278,90 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.28.6.tgz", + "integrity": "sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-literals": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.6.tgz", + "integrity": "sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", + "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", + "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", + "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1061,13 +1370,16 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "node_modules/@babel/plugin-transform-modules-systemjs": { "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.5.tgz", - "integrity": "sha512-axUuqnUTBuXyHGcJEVVh9pORaN6wC5bYfE7FGzPiaWa3syib9m7g+/IT/4VgCOe2Upef43PHzeAvcrVek6QuuA==", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.28.5.tgz", + "integrity": "sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -1076,10 +1388,10 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-modules-commonjs": { + "node_modules/@babel/plugin-transform-modules-umd": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", - "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", + "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", "license": "MIT", "dependencies": { "@babel/helper-module-transforms": "^7.27.1", @@ -1108,10 +1420,10 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "node_modules/@babel/plugin-transform-new-target": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz", - "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", + "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1123,13 +1435,28 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.28.6.tgz", + "integrity": "sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz", - "integrity": "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.28.6.tgz", + "integrity": "sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1139,16 +1466,16 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.4.tgz", - "integrity": "sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.6.tgz", + "integrity": "sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==", "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.28.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5", "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/traverse": "^7.28.4" + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1157,13 +1484,29 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-optional-catch-binding": { + "node_modules/@babel/plugin-transform-object-super": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz", - "integrity": "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", + "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.28.6.tgz", + "integrity": "sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1173,12 +1516,12 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz", - "integrity": "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.6.tgz", + "integrity": "sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "engines": { @@ -1204,13 +1547,13 @@ } }, "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz", - "integrity": "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.28.6.tgz", + "integrity": "sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==", "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1220,13 +1563,28 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.28.6.tgz", + "integrity": "sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz", - "integrity": "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", + "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { @@ -1332,9 +1690,40 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.4.tgz", - "integrity": "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.6.tgz", + "integrity": "sha512-eZhoEZHYQLL5uc1gS5e9/oTknS0sSSAtd5TkKMUp3J+S/CaUjagc0kOUPsEbDmMeva0nC3WWl4SxVY6+OBuxfw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.28.6.tgz", + "integrity": "sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", + "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1382,12 +1771,12 @@ } }, "node_modules/@babel/plugin-transform-spread": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz", - "integrity": "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.28.6.tgz", + "integrity": "sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "engines": { @@ -1427,6 +1816,21 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", + "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-typescript": { "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.0.tgz", @@ -1446,6 +1850,37 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", + "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.28.6.tgz", + "integrity": "sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-unicode-regex": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", @@ -1462,6 +1897,120 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.28.6.tgz", + "integrity": "sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.6.tgz", + "integrity": "sha512-GaTI4nXDrs7l0qaJ6Rg06dtOXTBCG6TMDB44zbqofCIC4PqC7SEvmFFtpxzCDw9W5aJ7RKVshgXTLvLdBFV/qw==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.6", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.28.6", + "@babel/plugin-syntax-import-attributes": "^7.28.6", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.27.1", + "@babel/plugin-transform-async-generator-functions": "^7.28.6", + "@babel/plugin-transform-async-to-generator": "^7.28.6", + "@babel/plugin-transform-block-scoped-functions": "^7.27.1", + "@babel/plugin-transform-block-scoping": "^7.28.6", + "@babel/plugin-transform-class-properties": "^7.28.6", + "@babel/plugin-transform-class-static-block": "^7.28.6", + "@babel/plugin-transform-classes": "^7.28.6", + "@babel/plugin-transform-computed-properties": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5", + "@babel/plugin-transform-dotall-regex": "^7.28.6", + "@babel/plugin-transform-duplicate-keys": "^7.27.1", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.28.6", + "@babel/plugin-transform-dynamic-import": "^7.27.1", + "@babel/plugin-transform-explicit-resource-management": "^7.28.6", + "@babel/plugin-transform-exponentiation-operator": "^7.28.6", + "@babel/plugin-transform-export-namespace-from": "^7.27.1", + "@babel/plugin-transform-for-of": "^7.27.1", + "@babel/plugin-transform-function-name": "^7.27.1", + "@babel/plugin-transform-json-strings": "^7.28.6", + "@babel/plugin-transform-literals": "^7.27.1", + "@babel/plugin-transform-logical-assignment-operators": "^7.28.6", + "@babel/plugin-transform-member-expression-literals": "^7.27.1", + "@babel/plugin-transform-modules-amd": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.28.6", + "@babel/plugin-transform-modules-systemjs": "^7.28.5", + "@babel/plugin-transform-modules-umd": "^7.27.1", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-new-target": "^7.27.1", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6", + "@babel/plugin-transform-numeric-separator": "^7.28.6", + "@babel/plugin-transform-object-rest-spread": "^7.28.6", + "@babel/plugin-transform-object-super": "^7.27.1", + "@babel/plugin-transform-optional-catch-binding": "^7.28.6", + "@babel/plugin-transform-optional-chaining": "^7.28.6", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/plugin-transform-private-methods": "^7.28.6", + "@babel/plugin-transform-private-property-in-object": "^7.28.6", + "@babel/plugin-transform-property-literals": "^7.27.1", + "@babel/plugin-transform-regenerator": "^7.28.6", + "@babel/plugin-transform-regexp-modifiers": "^7.28.6", + "@babel/plugin-transform-reserved-words": "^7.27.1", + "@babel/plugin-transform-shorthand-properties": "^7.27.1", + "@babel/plugin-transform-spread": "^7.28.6", + "@babel/plugin-transform-sticky-regex": "^7.27.1", + "@babel/plugin-transform-template-literals": "^7.27.1", + "@babel/plugin-transform-typeof-symbol": "^7.27.1", + "@babel/plugin-transform-unicode-escapes": "^7.27.1", + "@babel/plugin-transform-unicode-property-regex": "^7.28.6", + "@babel/plugin-transform-unicode-regex": "^7.27.1", + "@babel/plugin-transform-unicode-sets-regex": "^7.28.6", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.14", + "babel-plugin-polyfill-corejs3": "^0.13.0", + "babel-plugin-polyfill-regenerator": "^0.6.5", + "core-js-compat": "^3.43.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, "node_modules/@babel/preset-react": { "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.28.5.tgz", @@ -1512,31 +2061,31 @@ } }, "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", - "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", + "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6", "debug": "^4.3.1" }, "engines": { @@ -1563,9 +2112,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -1579,7 +2128,6 @@ "version": "0.2.3", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true, "license": "MIT" }, "node_modules/@egjs/hammerjs": { @@ -2491,7 +3039,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", - "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -2509,7 +3056,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", - "dev": true, "license": "MIT", "dependencies": { "@jest/console": "^29.7.0", @@ -2557,7 +3103,6 @@ "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "dev": true, "funding": [ { "type": "github", @@ -2573,7 +3118,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -2613,7 +3157,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", - "dev": true, "license": "MIT", "dependencies": { "expect": "^29.7.0", @@ -2627,7 +3170,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", - "dev": true, "license": "MIT", "dependencies": { "jest-get-type": "^29.6.3" @@ -2657,7 +3199,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", - "dev": true, "license": "MIT", "dependencies": { "@jest/environment": "^29.7.0", @@ -2673,7 +3214,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", - "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^0.2.3", @@ -2717,7 +3257,6 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "@babel/core": "^7.23.9", @@ -2734,7 +3273,6 @@ "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -2747,7 +3285,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -2772,7 +3309,6 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.18", @@ -2787,7 +3323,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", - "dev": true, "license": "MIT", "dependencies": { "@jest/console": "^29.7.0", @@ -2803,7 +3338,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", - "dev": true, "license": "MIT", "dependencies": { "@jest/test-result": "^29.7.0", @@ -4010,7 +4544,6 @@ "version": "29.5.14", "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", - "dev": true, "license": "MIT", "dependencies": { "expect": "^29.0.0", @@ -5522,7 +6055,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -5589,7 +6121,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -5646,7 +6177,6 @@ "version": "1.4.3", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", - "dev": true, "license": "MIT" }, "node_modules/cli-cursor": { @@ -5755,7 +6285,6 @@ "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", - "dev": true, "license": "MIT", "engines": { "iojs": ">= 1.0.0", @@ -5766,7 +6295,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", - "dev": true, "license": "MIT" }, "node_modules/color": { @@ -5932,7 +6460,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", - "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -6091,7 +6618,6 @@ "version": "1.7.1", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz", "integrity": "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==", - "dev": true, "license": "MIT", "peerDependencies": { "babel-plugin-macros": "^3.1.0" @@ -6216,7 +6742,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6232,7 +6757,6 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, "license": "MIT", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -6321,7 +6845,6 @@ "version": "0.13.1", "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -6358,7 +6881,6 @@ "version": "1.3.4", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" @@ -6368,7 +6890,6 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, "license": "MIT" }, "node_modules/error-stack-parser": { @@ -7026,7 +7547,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" @@ -7060,7 +7580,6 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, "license": "MIT", "dependencies": { "cross-spawn": "^7.0.3", @@ -7084,7 +7603,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -7094,7 +7612,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, "license": "MIT", "dependencies": { "mimic-fn": "^2.1.0" @@ -7110,14 +7627,12 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, "license": "ISC" }, "node_modules/exit": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", - "dev": true, "engines": { "node": ">= 0.8.0" } @@ -7126,7 +7641,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", - "dev": true, "license": "MIT", "dependencies": { "@jest/expect-utils": "^29.7.0", @@ -8340,7 +8854,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -8658,7 +9171,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true, "license": "MIT" }, "node_modules/http-errors": { @@ -8703,7 +9215,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=10.17.0" @@ -8790,7 +9301,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", - "dev": true, "license": "MIT", "dependencies": { "pkg-dir": "^4.2.0", @@ -9077,7 +9587,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -9220,7 +9729,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -9380,7 +9888,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "istanbul-lib-coverage": "^3.0.0", @@ -9395,7 +9902,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "debug": "^4.1.1", @@ -9410,7 +9916,6 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -9420,7 +9925,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "html-escaper": "^2.0.0", @@ -9467,7 +9971,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", - "dev": true, "license": "MIT", "dependencies": { "@jest/core": "^29.7.0", @@ -9494,7 +9997,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", - "dev": true, "license": "MIT", "dependencies": { "execa": "^5.0.0", @@ -9509,7 +10011,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", - "dev": true, "license": "MIT", "dependencies": { "@jest/environment": "^29.7.0", @@ -9541,7 +10042,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", - "dev": true, "license": "MIT", "dependencies": { "@jest/core": "^29.7.0", @@ -9575,7 +10075,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.11.6", @@ -9621,7 +10120,6 @@ "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "dev": true, "funding": [ { "type": "github", @@ -9637,7 +10135,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", - "dev": true, "license": "MIT", "dependencies": { "chalk": "^4.0.0", @@ -9653,7 +10150,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", - "dev": true, "license": "MIT", "dependencies": { "detect-newline": "^3.0.0" @@ -9666,7 +10162,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", - "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -9734,7 +10229,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", - "dev": true, "license": "MIT", "dependencies": { "jest-get-type": "^29.6.3", @@ -9748,7 +10242,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", - "dev": true, "license": "MIT", "dependencies": { "chalk": "^4.0.0", @@ -9798,7 +10291,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -9825,7 +10317,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", - "dev": true, "license": "MIT", "dependencies": { "chalk": "^4.0.0", @@ -9846,7 +10337,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", - "dev": true, "license": "MIT", "dependencies": { "jest-regex-util": "^29.6.3", @@ -9860,7 +10350,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", - "dev": true, "license": "MIT", "dependencies": { "@jest/console": "^29.7.0", @@ -9893,7 +10382,6 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -9903,7 +10391,6 @@ "version": "0.5.13", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", - "dev": true, "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", @@ -9914,7 +10401,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", - "dev": true, "license": "MIT", "dependencies": { "@jest/environment": "^29.7.0", @@ -9948,7 +10434,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -9958,7 +10443,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.11.6", @@ -9990,7 +10474,6 @@ "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -10052,7 +10535,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", - "dev": true, "license": "MIT", "dependencies": { "@jest/test-result": "^29.7.0", @@ -10151,7 +10633,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, "license": "MIT" }, "node_modules/json-schema-traverse": { @@ -10722,7 +11203,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, "license": "MIT", "dependencies": { "semver": "^7.5.3" @@ -10738,7 +11218,6 @@ "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -11257,7 +11736,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, "license": "MIT" }, "node_modules/negotiator": { @@ -11356,7 +11834,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.0.0" @@ -11761,7 +12238,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", @@ -11943,7 +12419,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, "license": "MIT", "dependencies": { "find-up": "^4.0.0" @@ -11956,7 +12431,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, "license": "MIT", "dependencies": { "locate-path": "^5.0.0", @@ -11970,7 +12444,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, "license": "MIT", "dependencies": { "p-locate": "^4.1.0" @@ -11983,7 +12456,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, "license": "MIT", "dependencies": { "p-try": "^2.0.0" @@ -11999,7 +12471,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, "license": "MIT", "dependencies": { "p-limit": "^2.2.0" @@ -12254,7 +12725,6 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", - "dev": true, "funding": [ { "type": "individual", @@ -12709,6 +13179,22 @@ "react-native": ">=0.56" } }, + "node_modules/react-native-hce": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/react-native-hce/-/react-native-hce-0.3.0.tgz", + "integrity": "sha512-/1hm9nXRlwep6XJFLsVnDNskyNtRYLHv6sq0P0mPapW3LaQNLIzaTX4PnZol4AIBQAqvlaENT81dCdHroGaq6A==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.18.13", + "@babel/preset-env": "^7.1.6", + "@types/jest": "^29.5.12", + "jest": "^29.7.0" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/react-native-is-edge-to-edge": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.2.1.tgz", @@ -12731,6 +13217,20 @@ "react-native-nitro-modules": "*" } }, + "node_modules/react-native-nfc-manager": { + "version": "3.17.2", + "resolved": "https://registry.npmjs.org/react-native-nfc-manager/-/react-native-nfc-manager-3.17.2.tgz", + "integrity": "sha512-0NryP/Iw2hzw4MVH5KCngoRerNUrnRok6VfLrlFcFZRKyTQ7KTgpsdDxCB6cR33qYNyEDrWGBayfAI+ym5gt8Q==", + "license": "MIT", + "peerDependencies": { + "@expo/config-plugins": "*" + }, + "peerDependenciesMeta": { + "@expo/config-plugins": { + "optional": true + } + } + }, "node_modules/react-native-nitro-modules": { "version": "0.31.5", "resolved": "https://registry.npmjs.org/react-native-nitro-modules/-/react-native-nitro-modules-0.31.5.tgz", @@ -13256,7 +13756,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, "license": "MIT", "dependencies": { "resolve-from": "^5.0.0" @@ -13960,7 +14459,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "dev": true, "license": "MIT", "dependencies": { "char-regex": "^1.0.2", @@ -13974,7 +14472,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -14185,7 +14682,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -14195,7 +14691,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -14975,7 +15470,6 @@ "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", - "dev": true, "license": "ISC", "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", diff --git a/dapps/poc-pos-app/package.json b/dapps/poc-pos-app/package.json index 93ab0bf0..f7b49300 100644 --- a/dapps/poc-pos-app/package.json +++ b/dapps/poc-pos-app/package.json @@ -48,7 +48,9 @@ "react-native-device-info": "^14.1.1", "react-native-gesture-handler": "~2.28.0", "react-native-get-random-values": "~1.11.0", + "react-native-hce": "^0.3.0", "react-native-mmkv": "^4.0.0", + "react-native-nfc-manager": "^3.17.2", "react-native-nitro-modules": "^0.31.5", "react-native-permissions": "^5.4.4", "react-native-qrcode-skia": "^0.3.1", diff --git a/dapps/poc-pos-app/roadmap.md b/dapps/poc-pos-app/roadmap.md new file mode 100644 index 00000000..f3b9f6c8 --- /dev/null +++ b/dapps/poc-pos-app/roadmap.md @@ -0,0 +1,1847 @@ +# NFC Implementation Roadmap for POS App + +This document provides a comprehensive implementation plan for adding NFC capabilities to the React Native POS application. It is designed to be self-contained with all technical details needed for implementation. + +**Target Repository:** https://github.com/reown-com/react-native-examples/tree/main/dapps/poc-pos-app + +--- + +## Table of Contents + +1. [Executive Summary](#executive-summary) +2. [Phase 1: Repository Analysis](#phase-1-repository-analysis) +3. [Phase 2: HCE Detection and NDEF Implementation](#phase-2-hce-detection-and-ndef-implementation) +4. [Phase 3: VAS/Smart Tap Fallback](#phase-3-vasmart-tap-fallback) +5. [Phase 4: UI Updates](#phase-4-ui-updates) +6. [Technical Reference: NDEF Protocol](#technical-reference-ndef-protocol) +7. [Technical Reference: Apple VAS Protocol](#technical-reference-apple-vas-protocol) +8. [Technical Reference: Google Smart Tap Protocol](#technical-reference-google-smart-tap-protocol) +9. [Testing Strategy](#testing-strategy) + +--- + +## Executive Summary + +### Goal + +Add NFC capability to transmit payment URLs from the POS terminal (React Native app) to customer phones (iOS/Android), with QR code as fallback. + +### Strategy (Three-Tier Fallback) + +```text +┌──────────────────────────────────────────────────────────────────────────┐ +│ NFC IMPLEMENTATION STRATEGY │ +├──────────────────────────────────────────────────────────────────────────┤ +│ │ +│ TIER 1: HCE/NDEF Tag Emulation (PREFERRED - Android Only) │ +│ └─► Emulate NFC Type 4 Tag containing URL │ +│ └─► Works with: iOS (XS+) and Android (5.0+) │ +│ └─► Response time: ~200ms │ +│ └─► No certification required │ +│ │ +│ TIER 2: VAS/Smart Tap (FALLBACK - If HCE unavailable) │ +│ └─► Use Universal VAS AID to detect device type │ +│ └─► Route to Apple VAS or Google Smart Tap │ +│ └─► Works with: iOS (6+) and Android with Google Wallet │ +│ └─► Response time: ~400-600ms │ +│ └─► Requires Apple + Google certification │ +│ │ +│ TIER 3: QR Code (ALWAYS VISIBLE) │ +│ └─► Display QR code with payment URL │ +│ └─► Works with: ANY phone with camera │ +│ └─► Response time: ~2-5 seconds (user-dependent) │ +│ │ +└──────────────────────────────────────────────────────────────────────────┘ +``` + +### Key Constraint + +React Native on iOS does NOT support Host Card Emulation (HCE). Therefore: +- **Android POS device**: Can use Tier 1 (HCE) as primary +- **iOS POS device**: Must use Tier 2 (VAS/Smart Tap) or Tier 3 (QR) only + +--- + +## Phase 1: Repository Analysis + +### Objective + +Understand the existing codebase structure, locate QR code implementation, and identify integration points for NFC. + +### Tasks + +#### 1.1 Analyze Project Structure + +```text +EXPECTED STRUCTURE: +poc-pos-app/ +├── api/ # Backend API integration +├── app/ # Application screens/routes (Expo Router) +├── assets/ # Images and static resources +├── components/ # Reusable UI components +├── constants/ # Configuration and constants +├── hooks/ # Custom React hooks +├── store/ # State management (likely Zustand or Redux) +├── utils/ # Helper functions +├── package.json # Dependencies +├── app.json # Expo configuration +└── index.ts # Entry point +``` + +#### 1.2 Locate QR Code Implementation + +Search for: + +```text +SEARCH PATTERNS: +- Files: **/QR*.tsx, **/qr*.tsx, **/*Payment*.tsx, **/*Checkout*.tsx +- Imports: 'react-native-qrcode', 'qrcode', 'react-qr-code' +- Components: , , qrcode +- Functions: generateQR, createQR, showQR +``` + +Expected findings: +- A component that generates QR code from payment URL +- A screen/page that displays the QR code to the merchant +- URL generation logic (likely in utils/ or hooks/) + +#### 1.3 Identify Payment URL Structure + +**IMPORTANT:** The payment URL base is configured via environment variable: + +```text +┌──────────────────────────────────────────────────────────────────────────┐ +│ PAYMENT URL CONFIGURATION │ +├──────────────────────────────────────────────────────────────────────────┤ +│ │ +│ Environment Variable: EXPO_PUBLIC_GATEWAY_URL │ +│ │ +│ Location: .env file (see .env.example for template) │ +│ │ +│ ┌────────────────────────────────────────────────────────────────────┐ │ +│ │ EXACT URL CONSTRUCTION (use this pattern): │ │ +│ │ │ │ +│ │ const url = `${process.env.EXPO_PUBLIC_GATEWAY_URL}/?pid=${data.paymentId}`; │ +│ │ │ │ +│ │ Example result: │ │ +│ │ https://gateway.example.com/?pid=abc123-def456-ghi789 │ │ +│ │ │ │ +│ └────────────────────────────────────────────────────────────────────┘ │ +│ │ +│ Key variables: │ +│ • EXPO_PUBLIC_GATEWAY_URL - Base gateway URL from environment │ +│ • data.paymentId - Payment/transaction identifier │ +│ │ +│ The NFC implementation should use this SAME URL that is currently │ +│ displayed in the QR code. │ +│ │ +└──────────────────────────────────────────────────────────────────────────┘ +``` + +#### 1.4 Check Existing NFC Dependencies + +Search `package.json` for: + +```json +{ + "dependencies": { + "react-native-nfc-manager": "...", // Primary NFC library + "react-native-hce": "...", // HCE support + "@anthropic/nfc": "..." // Any other NFC libs + } +} +``` + +#### 1.5 Document Platform Configuration + +Check for existing native configuration: +- `android/app/src/main/AndroidManifest.xml` - NFC permissions +- `ios/*/Info.plist` - NFC entitlements +- `app.json` or `expo-plugins` - Expo NFC configuration + +### Deliverables for Phase 1 + +```text +□ File structure map of the repository +□ Location of QR code generation/display component +□ Verify URL pattern: ${EXPO_PUBLIC_GATEWAY_URL}/?pid=${data.paymentId} +□ Locate where 'data.paymentId' is generated/stored +□ List of existing NFC-related dependencies (if any) +□ Platform configuration status (Android/iOS) +□ Identification of state management approach +□ Entry points for NFC integration +``` + +--- + +## Phase 2: HCE Detection and NDEF Implementation + +### Objective + +Implement Host Card Emulation (HCE) to emit NDEF URL records when supported by the device. + +### Prerequisites + +This phase applies to **Android POS devices only**. iOS does not expose HCE APIs. + +### 2.1 Install Dependencies + +```bash +# Primary NFC library +npm install react-native-nfc-manager + +# HCE support (Android only) +npm install react-native-hce + +# TypeScript types +npm install --save-dev @types/react-native-nfc-manager +``` + +### 2.2 Configure Android Manifest + +Add to `android/app/src/main/AndroidManifest.xml`: + +```xml + + + + + + + + + + + + + + + + + + + + + +``` + +### 2.3 Create APDU Service Configuration + +Create `android/app/src/main/res/xml/apduservice.xml`: + +```xml + + + + + + + + + + +``` + +### 2.4 Implement HCE Detection Hook + +Create `hooks/useNfcCapabilities.ts`: + +```typescript +import { useEffect, useState } from 'react'; +import { Platform } from 'react-native'; +import NfcManager, { NfcTech } from 'react-native-nfc-manager'; + +interface NfcCapabilities { + isNfcSupported: boolean; + isNfcEnabled: boolean; + isHceSupported: boolean; + isLoading: boolean; + error: string | null; +} + +export function useNfcCapabilities(): NfcCapabilities { + const [capabilities, setCapabilities] = useState({ + isNfcSupported: false, + isNfcEnabled: false, + isHceSupported: false, + isLoading: true, + error: null, + }); + + useEffect(() => { + async function checkCapabilities() { + try { + // iOS does not support HCE + if (Platform.OS === 'ios') { + setCapabilities({ + isNfcSupported: true, // Assume true for iOS 11+ + isNfcEnabled: true, + isHceSupported: false, // iOS never supports HCE + isLoading: false, + error: null, + }); + return; + } + + // Android NFC checks + const isSupported = await NfcManager.isSupported(); + + if (!isSupported) { + setCapabilities({ + isNfcSupported: false, + isNfcEnabled: false, + isHceSupported: false, + isLoading: false, + error: null, + }); + return; + } + + const isEnabled = await NfcManager.isEnabled(); + + // Check HCE support via package manager feature + // This requires native module or react-native-device-info + const hasHceFeature = await checkHceFeature(); + + setCapabilities({ + isNfcSupported: isSupported, + isNfcEnabled: isEnabled, + isHceSupported: hasHceFeature, + isLoading: false, + error: null, + }); + + } catch (error) { + setCapabilities({ + isNfcSupported: false, + isNfcEnabled: false, + isHceSupported: false, + isLoading: false, + error: error instanceof Error ? error.message : 'Unknown error', + }); + } + } + + checkCapabilities(); + }, []); + + return capabilities; +} + +async function checkHceFeature(): Promise { + // Implementation depends on available libraries + // Option 1: Use react-native-device-info + // Option 2: Create native module to check PackageManager.FEATURE_NFC_HOST_CARD_EMULATION + // Option 3: Try to initialize HCE and catch failure + + try { + // Attempt to import and check HCE + const HCE = require('react-native-hce').default; + return HCE !== null; + } catch { + return false; + } +} +``` + +### 2.5 Implement NDEF URL Record Builder + +Create `utils/ndef.ts`: + +```typescript +/** + * NDEF URL Record Builder + * + * NDEF (NFC Data Exchange Format) is a binary format for NFC data. + * This module builds NDEF messages containing URL records. + * + * NDEF Record Structure: + * ┌──────────────────────────────────────────────────────────────────┐ + * │ Byte 0: Header (MB|ME|CF|SR|IL|TNF) │ + * │ Byte 1: Type Length │ + * │ Byte 2: Payload Length (1 byte if SR=1, else 4 bytes) │ + * │ Byte 3: Type ("U" = 0x55 for URI) │ + * │ Byte 4: URI Prefix Code │ + * │ Byte 5+: URI String (without prefix) │ + * └──────────────────────────────────────────────────────────────────┘ + */ + +// URI Prefix Codes (saves space by abbreviating common prefixes) +const URI_PREFIXES: Record = { + '': 0x00, // No prefix (raw URI) + 'http://www.': 0x01, + 'https://www.': 0x02, + 'http://': 0x03, + 'https://': 0x04, + 'tel:': 0x05, + 'mailto:': 0x06, + 'ftp://anonymous:anonymous@': 0x07, + 'ftp://ftp.': 0x08, + 'ftps://': 0x09, + 'sftp://': 0x0A, +}; + +// Header flags +const MB = 0x80; // Message Begin +const ME = 0x40; // Message End +const SR = 0x10; // Short Record (payload < 256 bytes) +const TNF_WELL_KNOWN = 0x01; // Type Name Format: NFC Forum well-known type + +/** + * Build an NDEF message containing a single URI record + */ +export function buildNdefUrlMessage(url: string): Uint8Array { + // Find matching prefix + let prefixCode = 0x00; + let urlWithoutPrefix = url; + + for (const [prefix, code] of Object.entries(URI_PREFIXES)) { + if (prefix && url.startsWith(prefix)) { + prefixCode = code; + urlWithoutPrefix = url.slice(prefix.length); + break; + } + } + + // Convert URL to bytes + const urlBytes = new TextEncoder().encode(urlWithoutPrefix); + + // Payload = prefix code + URL bytes + const payloadLength = 1 + urlBytes.length; + + if (payloadLength > 255) { + throw new Error('URL too long for short record format'); + } + + // Build NDEF record + const header = MB | ME | SR | TNF_WELL_KNOWN; + const typeLength = 1; // "U" is 1 byte + const type = 0x55; // "U" for URI + + // Assemble record + const record = new Uint8Array(4 + payloadLength); + record[0] = header; + record[1] = typeLength; + record[2] = payloadLength; + record[3] = type; + record[4] = prefixCode; + record.set(urlBytes, 5); + + return record; +} + +/** + * Build complete NDEF file content (with 2-byte length prefix) + * This is what gets stored in the Type 4 Tag NDEF file + */ +export function buildNdefFile(url: string): Uint8Array { + const ndefMessage = buildNdefUrlMessage(url); + + // NDEF file format: 2-byte length + NDEF message + const ndefFile = new Uint8Array(2 + ndefMessage.length); + + // Big-endian length + ndefFile[0] = (ndefMessage.length >> 8) & 0xFF; + ndefFile[1] = ndefMessage.length & 0xFF; + + // NDEF message + ndefFile.set(ndefMessage, 2); + + return ndefFile; +} + +/** + * Example: + * + * URL: "https://pay.example.com/s/ABC123" + * + * Prefix: 0x04 (https://) + * Remaining: "pay.example.com/s/ABC123" + * + * NDEF Record (hex): + * D1 01 19 55 04 70 61 79 2E 65 78 61 6D 70 6C 65 + * 2E 63 6F 6D 2F 73 2F 41 42 43 31 32 33 + * + * Breakdown: + * D1 = Header (MB=1, ME=1, SR=1, TNF=01) + * 01 = Type Length (1 byte) + * 19 = Payload Length (25 bytes) + * 55 = Type "U" (URI) + * 04 = Prefix code (https://) + * 70... = "pay.example.com/s/ABC123" + */ +``` + +### 2.6 Implement HCE Service + +Create `services/HceService.ts`: + +```typescript +import { Platform } from 'react-native'; +import { buildNdefFile } from '../utils/ndef'; + +// Type 4 Tag constants +const NDEF_APP_AID = 'D2760000850101'; +const CC_FILE_ID = 'E103'; +const NDEF_FILE_ID = 'E104'; + +// Capability Container (15 bytes) +// Describes tag capabilities to the reader +const CAPABILITY_CONTAINER = new Uint8Array([ + 0x00, 0x0F, // CC length (15 bytes) + 0x20, // Mapping version 2.0 + 0x00, 0xFF, // MLe (max read length) = 255 + 0x00, 0xFF, // MLc (max write length) = 255 + 0x04, // NDEF File Control TLV - Type + 0x06, // NDEF File Control TLV - Length + 0xE1, 0x04, // NDEF File ID + 0x08, 0x00, // Max NDEF size (2048 bytes) + 0x00, // Read access: no security + 0xFF, // Write access: no write allowed +]); + +// Status words +const SW_OK = [0x90, 0x00]; +const SW_NOT_FOUND = [0x6A, 0x82]; +const SW_WRONG_PARAMS = [0x6A, 0x86]; + +interface HceState { + selectedFile: 'none' | 'cc' | 'ndef'; + ndefFile: Uint8Array | null; +} + +class HceServiceClass { + private state: HceState = { + selectedFile: 'none', + ndefFile: null, + }; + + private hceInstance: any = null; + private isRunning = false; + + /** + * Start HCE with the given payment URL + */ + async start(paymentUrl: string): Promise { + if (Platform.OS !== 'android') { + console.warn('HCE is only supported on Android'); + return false; + } + + try { + // Build NDEF file with URL + this.state.ndefFile = buildNdefFile(paymentUrl); + this.state.selectedFile = 'none'; + + // Initialize HCE + const HCE = require('react-native-hce').default; + + this.hceInstance = await HCE.getInstance(); + + // Set up APDU handler + this.hceInstance.on('apdu', this.handleApdu.bind(this)); + + // Start emulation + await this.hceInstance.start(); + this.isRunning = true; + + console.log('HCE started with URL:', paymentUrl); + return true; + + } catch (error) { + console.error('Failed to start HCE:', error); + return false; + } + } + + /** + * Stop HCE emulation + */ + async stop(): Promise { + if (this.hceInstance && this.isRunning) { + await this.hceInstance.stop(); + this.isRunning = false; + this.state.selectedFile = 'none'; + console.log('HCE stopped'); + } + } + + /** + * Update the payment URL while running + */ + updateUrl(paymentUrl: string): void { + this.state.ndefFile = buildNdefFile(paymentUrl); + console.log('HCE URL updated:', paymentUrl); + } + + /** + * Handle incoming APDU commands from NFC reader (iPhone/Android phone) + * + * Type 4 Tag command flow: + * 1. SELECT NDEF Application (AID: D2760000850101) + * 2. SELECT Capability Container (File: E103) + * 3. READ BINARY (read CC) + * 4. SELECT NDEF File (File: E104) + * 5. READ BINARY (read NDEF length, then NDEF message) + */ + private handleApdu(apdu: number[]): number[] { + const command = this.parseApdu(apdu); + + switch (command.type) { + case 'SELECT_AID': + return this.handleSelectAid(command.data); + + case 'SELECT_FILE': + return this.handleSelectFile(command.data); + + case 'READ_BINARY': + return this.handleReadBinary(command.offset, command.length); + + default: + return SW_WRONG_PARAMS; + } + } + + private parseApdu(apdu: number[]): { + type: string; + data: number[]; + offset: number; + length: number; + } { + const cla = apdu[0]; + const ins = apdu[1]; + const p1 = apdu[2]; + const p2 = apdu[3]; + const lc = apdu[4] || 0; + const data = apdu.slice(5, 5 + lc); + + // SELECT command + if (cla === 0x00 && ins === 0xA4) { + if (p1 === 0x04 && p2 === 0x00) { + return { type: 'SELECT_AID', data, offset: 0, length: 0 }; + } + if (p1 === 0x00 && p2 === 0x0C) { + return { type: 'SELECT_FILE', data, offset: 0, length: 0 }; + } + } + + // READ BINARY command + if (cla === 0x00 && ins === 0xB0) { + const offset = (p1 << 8) | p2; + const length = apdu[apdu.length - 1] || 0; + return { type: 'READ_BINARY', data: [], offset, length }; + } + + return { type: 'UNKNOWN', data: [], offset: 0, length: 0 }; + } + + private handleSelectAid(data: number[]): number[] { + const aidHex = Array.from(data) + .map(b => b.toString(16).padStart(2, '0')) + .join('') + .toUpperCase(); + + if (aidHex === NDEF_APP_AID) { + this.state.selectedFile = 'none'; + return SW_OK; + } + + return SW_NOT_FOUND; + } + + private handleSelectFile(data: number[]): number[] { + const fileId = ((data[0] << 8) | data[1]) + .toString(16) + .toUpperCase() + .padStart(4, '0'); + + if (fileId === CC_FILE_ID) { + this.state.selectedFile = 'cc'; + return SW_OK; + } + + if (fileId === NDEF_FILE_ID) { + this.state.selectedFile = 'ndef'; + return SW_OK; + } + + return SW_NOT_FOUND; + } + + private handleReadBinary(offset: number, length: number): number[] { + let fileData: Uint8Array; + + switch (this.state.selectedFile) { + case 'cc': + fileData = CAPABILITY_CONTAINER; + break; + + case 'ndef': + if (!this.state.ndefFile) { + return SW_NOT_FOUND; + } + fileData = this.state.ndefFile; + break; + + default: + return SW_NOT_FOUND; + } + + // Handle read request + const end = Math.min(offset + length, fileData.length); + const responseData = Array.from(fileData.slice(offset, end)); + + return [...responseData, ...SW_OK]; + } +} + +export const HceService = new HceServiceClass(); +``` + +### 2.7 Create NFC Payment Hook + +Create `hooks/useNfcPayment.ts`: + +```typescript +import { useEffect, useRef, useCallback } from 'react'; +import { Platform, AppState } from 'react-native'; +import { useNfcCapabilities } from './useNfcCapabilities'; +import { HceService } from '../services/HceService'; + +interface UseNfcPaymentOptions { + paymentUrl: string; + enabled: boolean; + onNfcRead?: () => void; + onNfcError?: (error: Error) => void; +} + +interface UseNfcPaymentReturn { + isNfcActive: boolean; + isHceMode: boolean; + startNfc: () => Promise; + stopNfc: () => Promise; + updateUrl: (url: string) => void; +} + +export function useNfcPayment(options: UseNfcPaymentOptions): UseNfcPaymentReturn { + const { paymentUrl, enabled, onNfcRead, onNfcError } = options; + const capabilities = useNfcCapabilities(); + const isActiveRef = useRef(false); + const currentUrlRef = useRef(paymentUrl); + + // Update URL ref when it changes + useEffect(() => { + currentUrlRef.current = paymentUrl; + if (isActiveRef.current && capabilities.isHceSupported) { + HceService.updateUrl(paymentUrl); + } + }, [paymentUrl, capabilities.isHceSupported]); + + const startNfc = useCallback(async () => { + if (!enabled || capabilities.isLoading) return; + + try { + if (capabilities.isHceSupported) { + // Use HCE (Tier 1) + const success = await HceService.start(currentUrlRef.current); + if (success) { + isActiveRef.current = true; + } else { + throw new Error('Failed to start HCE'); + } + } else { + // HCE not available - will need Tier 2 (VAS/Smart Tap) + // or rely on QR code (Tier 3) + console.log('HCE not supported, using fallback'); + } + } catch (error) { + onNfcError?.(error instanceof Error ? error : new Error('NFC error')); + } + }, [enabled, capabilities, onNfcError]); + + const stopNfc = useCallback(async () => { + if (capabilities.isHceSupported) { + await HceService.stop(); + } + isActiveRef.current = false; + }, [capabilities.isHceSupported]); + + const updateUrl = useCallback((url: string) => { + currentUrlRef.current = url; + if (isActiveRef.current && capabilities.isHceSupported) { + HceService.updateUrl(url); + } + }, [capabilities.isHceSupported]); + + // Handle app state changes + useEffect(() => { + const subscription = AppState.addEventListener('change', (state) => { + if (state === 'active' && enabled) { + startNfc(); + } else if (state === 'background') { + // Keep HCE running in background for Android + // iOS will suspend automatically + } + }); + + return () => subscription.remove(); + }, [enabled, startNfc]); + + // Start/stop based on enabled state + useEffect(() => { + if (enabled && !capabilities.isLoading) { + startNfc(); + } else { + stopNfc(); + } + + return () => { + stopNfc(); + }; + }, [enabled, capabilities.isLoading, startNfc, stopNfc]); + + return { + isNfcActive: isActiveRef.current, + isHceMode: capabilities.isHceSupported, + startNfc, + stopNfc, + updateUrl, + }; +} +``` + +### Deliverables for Phase 2 + +```text +□ react-native-nfc-manager and react-native-hce installed +□ Android manifest configured with NFC permissions and HCE service +□ APDU service XML configuration created +□ useNfcCapabilities hook implemented +□ NDEF URL record builder (utils/ndef.ts) implemented +□ HCE service with APDU handler implemented +□ useNfcPayment hook created +□ HCE tested on Android device +``` + +--- + +## Phase 3: VAS/Smart Tap Fallback + +### Objective + +Implement Apple VAS and Google Smart Tap as fallback when HCE is not available. + +### Important Note on Feasibility + +```text +┌──────────────────────────────────────────────────────────────────────────┐ +│ ⚠️ CRITICAL LIMITATION │ +├──────────────────────────────────────────────────────────────────────────┤ +│ │ +│ Apple VAS and Google Smart Tap require the device to act as an │ +│ NFC READER, not a tag/card. │ +│ │ +│ React Native limitations: │ +│ • iOS: CoreNFC can only READ tags, not act as reader for VAS │ +│ • Android: Can read tags but VAS requires ECP polling support │ +│ │ +│ To implement Tier 2 (VAS/Smart Tap), you need: │ +│ 1. External VAS-certified USB NFC reader (WalletMate, VTAP, etc.) │ +│ OR │ +│ 2. POS hardware with VAS SDK (Zebra, Ingenico, etc.) │ +│ OR │ +│ 3. Native module with low-level NFC controller access │ +│ │ +│ For a React Native app running on standard phone/tablet: │ +│ → Tier 2 is NOT directly implementable │ +│ → Skip to Tier 3 (QR Code) as fallback │ +│ │ +└──────────────────────────────────────────────────────────────────────────┘ +``` + +### 3.1 External NFC Reader Integration (Optional) + +If using an external VAS-certified USB NFC reader: + +```typescript +/** + * External VAS Reader Integration + * + * This would require a native module to communicate with USB NFC reader. + * The reader handles ECP polling and VAS protocol. + * + * Supported readers: + * - ACS WalletMate / WalletMate II + * - VTAP VAS Readers + * - ID TECH PiP + */ + +interface VasReaderConfig { + applePassTypeId: string; // Your registered Pass Type ID + applePrivateKey: string; // P-256 private key (PEM) + googleCollectorId: string; // Google Smart Tap Collector ID + googlePrivateKey: string; // P-256 private key (PEM) + signupUrl: string; // URL to send in VAS URL mode +} + +// This would be implemented as a native module +// communicating with USB reader via serial/USB API +``` + +### 3.2 VAS Protocol Reference (For Native Implementation) + +If implementing at the native level or with external reader: + +```text +┌──────────────────────────────────────────────────────────────────────────┐ +│ UNIVERSAL VAS AID DETECTION │ +├──────────────────────────────────────────────────────────────────────────┤ +│ │ +│ STEP 1: SELECT Universal VAS AID │ +│ Command: 00 A4 04 00 0A 4F53452E5641532E3031 00 │ +│ ││ ││ ││ ││ ││ └─────────────────┘ └── Le │ +│ ││ ││ ││ ││ └── Lc (10 bytes) │ +│ ││ ││ ││ └── P2 (first or only occurrence) │ +│ ││ ││ └── P1 (select by name) │ +│ ││ └── INS (SELECT) │ +│ └── CLA │ +│ │ +│ AID: 4F 53 45 2E 56 41 53 2E 30 31 = "OSE.VAS.01" │ +│ │ +│ STEP 2: Check Response Tag 50 for Wallet Type │ +│ │ +│ Apple Device: │ +│ Tag 50: 41 70 70 6C 65 50 61 79 = "ApplePay" │ +│ → Proceed with Apple VAS protocol │ +│ │ +│ Android Device: │ +│ Tag 50: 41 6E 64 72 6F 69 64 50 61 79 = "AndroidPay" │ +│ → Proceed with Google Smart Tap protocol │ +│ │ +└──────────────────────────────────────────────────────────────────────────┘ +``` + +### 3.3 Simplified Approach: Skip to QR Fallback + +For standard React Native app without external hardware: + +```typescript +/** + * NFC Strategy Decision Hook + * + * Determines the best available NFC strategy based on device capabilities. + */ + +type NfcStrategy = 'hce' | 'qr-only'; + +export function useNfcStrategy(): { + strategy: NfcStrategy; + reason: string; +} { + const capabilities = useNfcCapabilities(); + + if (capabilities.isLoading) { + return { strategy: 'qr-only', reason: 'Loading...' }; + } + + if (capabilities.isHceSupported) { + return { + strategy: 'hce', + reason: 'HCE supported - using NDEF tag emulation' + }; + } + + // VAS/Smart Tap requires external reader or native SDK + // Fall back to QR code + return { + strategy: 'qr-only', + reason: 'HCE not supported - using QR code fallback' + }; +} +``` + +### Deliverables for Phase 3 + +```text +□ Document VAS/Smart Tap limitation for React Native +□ Implement NFC strategy decision hook +□ (Optional) Research external USB reader integration +□ (Optional) Implement native module for external reader +□ Ensure graceful fallback to QR code +``` + +--- + +## Phase 4: UI Updates + +### Objective + +Update the payment UI to show both NFC tap option and QR code, with appropriate messaging based on device capabilities. + +### 4.1 Payment Screen Component + +Create or update `components/PaymentScreen.tsx`: + +```tsx +import React from 'react'; +import { + View, + Text, + StyleSheet, + Animated, + Easing, +} from 'react-native'; +import QRCode from 'react-native-qrcode-svg'; // or your existing QR library +import { useNfcPayment } from '../hooks/useNfcPayment'; +import { useNfcCapabilities } from '../hooks/useNfcCapabilities'; + +interface PaymentScreenProps { + paymentUrl: string; + amount: string; + currency: string; + onPaymentComplete?: () => void; +} + +export function PaymentScreen({ + paymentUrl, + amount, + currency, + onPaymentComplete, +}: PaymentScreenProps) { + const capabilities = useNfcCapabilities(); + const { isNfcActive, isHceMode } = useNfcPayment({ + paymentUrl, + enabled: true, + }); + + // Pulse animation for NFC icon + const pulseAnim = React.useRef(new Animated.Value(1)).current; + + React.useEffect(() => { + if (isNfcActive) { + const pulse = Animated.loop( + Animated.sequence([ + Animated.timing(pulseAnim, { + toValue: 1.2, + duration: 1000, + easing: Easing.inOut(Easing.ease), + useNativeDriver: true, + }), + Animated.timing(pulseAnim, { + toValue: 1, + duration: 1000, + easing: Easing.inOut(Easing.ease), + useNativeDriver: true, + }), + ]) + ); + pulse.start(); + return () => pulse.stop(); + } + }, [isNfcActive, pulseAnim]); + + const showNfcOption = capabilities.isNfcSupported && !capabilities.isLoading; + + return ( + + {/* Header */} + + Payment Request + + {currency} {amount} + + + + {/* Main Content */} + + {/* Instruction */} + + {showNfcOption + ? 'Tap your phone or scan the QR code' + : 'Scan the QR code to pay'} + + + {/* NFC + QR Side by Side (or stacked on small screens) */} + + {/* NFC Option */} + {showNfcOption && ( + + + + + Tap here + {isNfcActive && ( + + {isHceMode ? 'NFC Ready' : 'Waiting...'} + + )} + + )} + + {/* Divider */} + {showNfcOption && ( + + or + + )} + + {/* QR Code Option */} + + + + + Scan QR code + + + + + {/* Footer */} + + + Waiting for payment... + + + + ); +} + +// NFC Icon Component +function NfcIcon({ active }: { active: boolean }) { + return ( + + {/* Replace with actual NFC icon SVG or image */} + 📱 + + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#f5f5f5', + }, + header: { + padding: 24, + backgroundColor: '#1a1a2e', + alignItems: 'center', + }, + title: { + fontSize: 18, + color: '#ffffff', + marginBottom: 8, + }, + amount: { + fontSize: 36, + fontWeight: 'bold', + color: '#ffffff', + }, + content: { + flex: 1, + padding: 24, + alignItems: 'center', + }, + instruction: { + fontSize: 20, + fontWeight: '600', + color: '#333', + marginBottom: 32, + textAlign: 'center', + }, + optionsContainer: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + flexWrap: 'wrap', + }, + nfcOption: { + alignItems: 'center', + padding: 16, + }, + nfcIconContainer: { + width: 120, + height: 120, + borderRadius: 60, + backgroundColor: '#e8f4f8', + justifyContent: 'center', + alignItems: 'center', + marginBottom: 12, + }, + nfcIcon: { + alignItems: 'center', + }, + nfcIconActive: { + // Active state styling + }, + nfcIconText: { + fontSize: 48, + }, + nfcWaves: { + position: 'absolute', + width: '100%', + height: '100%', + }, + wave: { + position: 'absolute', + borderWidth: 2, + borderColor: '#0066cc', + borderRadius: 100, + opacity: 0.3, + }, + wave1: { + width: '60%', + height: '60%', + top: '20%', + left: '20%', + }, + wave2: { + width: '80%', + height: '80%', + top: '10%', + left: '10%', + }, + wave3: { + width: '100%', + height: '100%', + top: 0, + left: 0, + }, + optionLabel: { + fontSize: 16, + color: '#666', + marginTop: 8, + }, + nfcStatus: { + fontSize: 14, + color: '#0066cc', + marginTop: 4, + }, + divider: { + paddingHorizontal: 24, + paddingVertical: 16, + }, + dividerText: { + fontSize: 16, + color: '#999', + }, + qrOption: { + alignItems: 'center', + padding: 16, + }, + qrContainer: { + padding: 16, + backgroundColor: 'white', + borderRadius: 12, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 8, + elevation: 4, + }, + footer: { + padding: 24, + alignItems: 'center', + }, + footerText: { + fontSize: 14, + color: '#666', + }, +}); +``` + +### 4.2 Status Indicator Component + +Create `components/NfcStatusIndicator.tsx`: + +```tsx +import React from 'react'; +import { View, Text, StyleSheet } from 'react-native'; +import { useNfcCapabilities } from '../hooks/useNfcCapabilities'; + +export function NfcStatusIndicator() { + const capabilities = useNfcCapabilities(); + + if (capabilities.isLoading) { + return ( + + Checking NFC... + + ); + } + + if (!capabilities.isNfcSupported) { + return ( + + NFC not available + + ); + } + + if (!capabilities.isNfcEnabled) { + return ( + + NFC disabled - enable in settings + + ); + } + + if (capabilities.isHceSupported) { + return ( + + NFC Ready (HCE) + + ); + } + + return ( + + NFC Limited (QR fallback) + + ); +} + +const styles = StyleSheet.create({ + container: { + paddingHorizontal: 12, + paddingVertical: 6, + borderRadius: 16, + }, + loading: { + backgroundColor: '#e0e0e0', + }, + unsupported: { + backgroundColor: '#ffebee', + }, + disabled: { + backgroundColor: '#fff3e0', + }, + ready: { + backgroundColor: '#e8f5e9', + }, + limited: { + backgroundColor: '#e3f2fd', + }, + text: { + fontSize: 12, + fontWeight: '600', + }, +}); +``` + +### 4.3 Integration with Existing Flow + +Identify where in the app the payment/checkout screen is shown and integrate: + +```tsx +// Example integration in existing payment flow + +import { PaymentScreen } from '../components/PaymentScreen'; + +function CheckoutPage() { + const { data, amount, currency } = usePaymentSession(); + + // Construct payment URL using the EXACT same pattern as existing QR code + // URL format: ${EXPO_PUBLIC_GATEWAY_URL}/?pid=${paymentId} + const paymentUrl = `${process.env.EXPO_PUBLIC_GATEWAY_URL}/?pid=${data.paymentId}`; + + // ... existing checkout logic + + return ( + { + // Handle successful payment + navigation.navigate('Success'); + }} + /> + ); +} +``` + +### Deliverables for Phase 4 + +```text +□ PaymentScreen component with NFC + QR display +□ NfcStatusIndicator component for showing NFC state +□ Pulse animation for NFC icon when active +□ Responsive layout for different screen sizes +□ Integration with existing checkout flow +□ Proper handling of NFC capability states +□ User-friendly error messages +``` + +--- + +## Technical Reference: NDEF Protocol + +### NDEF Message Structure + +```text +┌──────────────────────────────────────────────────────────────────────────┐ +│ NDEF MESSAGE FORMAT │ +├──────────────────────────────────────────────────────────────────────────┤ +│ │ +│ An NDEF message contains one or more NDEF records: │ +│ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ Record 1 │ │ Record 2 │ ... │ Record N │ │ +│ │ (MB=1) │ │ │ │ (ME=1) │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +│ │ +│ MB = Message Begin (set on first record) │ +│ ME = Message End (set on last record) │ +│ │ +└──────────────────────────────────────────────────────────────────────────┘ +``` + +### NDEF Record Header + +```text +┌──────────────────────────────────────────────────────────────────────────┐ +│ NDEF RECORD HEADER BYTE │ +├──────────────────────────────────────────────────────────────────────────┤ +│ │ +│ Bit 7: MB (Message Begin) - 1 if first record in message │ +│ Bit 6: ME (Message End) - 1 if last record in message │ +│ Bit 5: CF (Chunk Flag) - 1 if chunked (rarely used) │ +│ Bit 4: SR (Short Record) - 1 if payload length is 1 byte │ +│ Bit 3: IL (ID Length present) - 1 if ID field is present │ +│ Bits 2-0: TNF (Type Name Format) │ +│ │ +│ ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐ │ +│ │ MB │ ME │ CF │ SR │ IL │ T N F │ │ +│ │ b7 │ b6 │ b5 │ b4 │ b3 │ b2 │ b1 │ b0 │ │ +│ └─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘ │ +│ │ +│ TNF Values: │ +│ 0x00 = Empty │ +│ 0x01 = NFC Forum well-known type (used for URI) │ +│ 0x02 = Media-type (RFC 2046) │ +│ 0x03 = Absolute URI │ +│ 0x04 = External type │ +│ 0x05 = Unknown │ +│ 0x06 = Unchanged (for chunks) │ +│ │ +└──────────────────────────────────────────────────────────────────────────┘ +``` + +### URI Record Format + +```text +┌──────────────────────────────────────────────────────────────────────────┐ +│ NDEF URI RECORD FORMAT │ +├──────────────────────────────────────────────────────────────────────────┤ +│ │ +│ For a single URI record (most common case): │ +│ │ +│ Byte 0: Header = 0xD1 │ +│ (MB=1, ME=1, CF=0, SR=1, IL=0, TNF=0x01) │ +│ │ +│ Byte 1: Type Length = 0x01 (type is "U", 1 byte) │ +│ │ +│ Byte 2: Payload Length (1 byte for SR=1) │ +│ │ +│ Byte 3: Type = 0x55 ("U" in ASCII) │ +│ │ +│ Byte 4: URI Prefix Code (see table below) │ +│ │ +│ Byte 5+: URI string (without the prefix) │ +│ │ +│ ┌────────────────────────────────────────────────────────────────────┐ │ +│ │ URI PREFIX CODES │ │ +│ ├──────┬─────────────────────────────────────────────────────────────┤ │ +│ │ Code │ Prefix │ │ +│ ├──────┼─────────────────────────────────────────────────────────────┤ │ +│ │ 0x00 │ (none - raw URI) │ │ +│ │ 0x01 │ http://www. │ │ +│ │ 0x02 │ https://www. │ │ +│ │ 0x03 │ http:// │ │ +│ │ 0x04 │ https:// │ │ +│ │ 0x05 │ tel: │ │ +│ │ 0x06 │ mailto: │ │ +│ └──────┴─────────────────────────────────────────────────────────────┘ │ +│ │ +└──────────────────────────────────────────────────────────────────────────┘ +``` + +### Example: Building URL Record + +```text +URL: https://pay.example.com/session/ABC123 + +Step 1: Find prefix + - URL starts with "https://" + - Prefix code: 0x04 + - Remaining: "pay.example.com/session/ABC123" (30 bytes) + +Step 2: Build record + - Header: 0xD1 (MB=1, ME=1, SR=1, TNF=01) + - Type Length: 0x01 + - Payload Length: 0x1F (31 = 1 prefix + 30 chars) + - Type: 0x55 ("U") + - Prefix: 0x04 + - Data: "pay.example.com/session/ABC123" + +Step 3: Hex result + D1 01 1F 55 04 70 61 79 2E 65 78 61 6D 70 6C 65 + 2E 63 6F 6D 2F 73 65 73 73 69 6F 6E 2F 41 42 43 + 31 32 33 + +Step 4: Add NDEF file length prefix (for Type 4 Tag) + 00 22 D1 01 1F 55 04 70 61 79 2E 65 78 61 6D 70 + 6C 65 2E 63 6F 6D 2F 73 65 73 73 69 6F 6E 2F 41 + 42 43 31 32 33 + + (00 22 = length of 34 bytes) +``` + +### Type 4 Tag APDU Commands + +```text +┌──────────────────────────────────────────────────────────────────────────┐ +│ TYPE 4 TAG APDU COMMANDS │ +├──────────────────────────────────────────────────────────────────────────┤ +│ │ +│ 1. SELECT NDEF Application │ +│ Command: 00 A4 04 00 07 D2760000850101 00 │ +│ Response: 90 00 (OK) │ +│ │ +│ 2. SELECT Capability Container │ +│ Command: 00 A4 00 0C 02 E103 │ +│ Response: 90 00 (OK) │ +│ │ +│ 3. READ BINARY (Capability Container) │ +│ Command: 00 B0 00 00 0F │ +│ Response: [15 bytes CC data] 90 00 │ +│ │ +│ 4. SELECT NDEF File │ +│ Command: 00 A4 00 0C 02 E104 │ +│ Response: 90 00 (OK) │ +│ │ +│ 5. READ BINARY (NDEF Length) │ +│ Command: 00 B0 00 00 02 │ +│ Response: [2 bytes length] 90 00 │ +│ │ +│ 6. READ BINARY (NDEF Message) │ +│ Command: 00 B0 00 02 [length] │ +│ Response: [NDEF message] 90 00 │ +│ │ +└──────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Technical Reference: Apple VAS Protocol + +### Overview + +Apple VAS (Value Added Services) is Apple's proprietary NFC protocol for wallet passes and loyalty programs. + +**Important:** VAS requires the device to be an NFC **reader** with ECP (Enhanced Contactless Polling) support. Standard phones cannot act as VAS readers. + +### VAS Application Selection + +```text +┌──────────────────────────────────────────────────────────────────────────┐ +│ APPLE VAS PROTOCOL │ +├──────────────────────────────────────────────────────────────────────────┤ +│ │ +│ VAS AID: 4F 53 45 2E 56 41 53 2E 30 31 ("OSE.VAS.01") │ +│ │ +│ SELECT Command: │ +│ 00 A4 04 00 0A 4F53452E5641532E3031 00 │ +│ ││ ││ ││ ││ ││ └─────────────────┘ └── Le │ +│ ││ ││ ││ ││ └── Lc (10 bytes) │ +│ ││ ││ ││ └── P2 │ +│ ││ ││ └── P1 (select by name) │ +│ ││ └── INS (SELECT) │ +│ └── CLA │ +│ │ +│ Response includes: │ +│ - Tag 50: Application label ("ApplePay" = 4170706C65506179) │ +│ - Tag 9F21: Mobile capabilities │ +│ - SW: 90 00 (success) │ +│ │ +└──────────────────────────────────────────────────────────────────────────┘ +``` + +### VAS URL Mode + +```text +┌──────────────────────────────────────────────────────────────────────────┐ +│ VAS URL MODE (P2=0x00) │ +├──────────────────────────────────────────────────────────────────────────┤ +│ │ +│ GET VAS DATA Command: │ +│ 80 CA 01 00 [Lc] [Data] │ +│ ││ ││ ││ ││ │ +│ ││ ││ ││ └── P2 = 0x00 (URL Mode) │ +│ ││ ││ └── P1 = 0x01 (Version 1) │ +│ ││ └── INS (GET DATA) │ +│ └── CLA (proprietary) │ +│ │ +│ Data TLV Structure: │ +│ - Tag 9F25: Merchant ID (SHA-256 of Pass Type ID, 32 bytes) │ +│ - Tag 9F22: Signup URL (ASCII string) │ +│ - Tag 9F26: Terminal ephemeral public key (65 bytes, uncompressed) │ +│ │ +│ Response: │ +│ - SW: 90 00 (success, notification displayed) │ +│ - SW: 6A 83 (pass not available) │ +│ │ +└──────────────────────────────────────────────────────────────────────────┘ +``` + +### ECP (Enhanced Contactless Polling) + +```text +┌──────────────────────────────────────────────────────────────────────────┐ +│ ECP FRAME FORMAT │ +├──────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ECP enables frictionless "tap and go" by broadcasting terminal │ +│ capabilities during NFC polling. │ +│ │ +│ Frame Structure: │ +│ ┌────────┬─────────┬────────────────────────────────────┬──────────┐ │ +│ │ Header │ Version │ Payload │ CRC │ │ +│ │ 0x6A │ 01 / 02 │ TCI + Terminal Info │ ISO14443 │ │ +│ └────────┴─────────┴────────────────────────────────────┴──────────┘ │ +│ │ +│ TCI (Terminal Capabilities Identifier): │ +│ - 00 00 01: VAS and Payment │ +│ - 00 00 02: VAS Only │ +│ - 00 00 03: Payment Only │ +│ │ +│ Timing: │ +│ - Polling interval: 100-250ms optimal │ +│ - Guard time: 5ms between frames │ +│ - Processing delay: ~76ms │ +│ - Grace period: 300ms (payment), 750ms (transit) │ +│ │ +└──────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Technical Reference: Google Smart Tap Protocol + +### Overview + +Google Smart Tap is Google's proprietary NFC protocol for Google Wallet passes. + +### Smart Tap Detection + +```text +┌──────────────────────────────────────────────────────────────────────────┐ +│ GOOGLE SMART TAP PROTOCOL │ +├──────────────────────────────────────────────────────────────────────────┤ +│ │ +│ Smart Tap also responds to Universal VAS AID: │ +│ AID: 4F 53 45 2E 56 41 53 2E 30 31 ("OSE.VAS.01") │ +│ │ +│ Detection: │ +│ - SELECT OSE.VAS.01 │ +│ - Check Tag 50 in response │ +│ - If "AndroidPay" (416E64726F6964506179) → Smart Tap device │ +│ │ +│ Protocol Flow: │ +│ 1. SELECT applet │ +│ 2. NEGOTIATE SECURE CHANNEL (ECDH key exchange) │ +│ 3. GET DATA (retrieve pass information) │ +│ 4. GET MORE DATA (if continuation needed, SW=91 00) │ +│ │ +│ Security: │ +│ - ECDH key derivation for session keys │ +│ - AES encryption for pass data │ +│ - Collector ID identifies the reader/merchant │ +│ │ +└──────────────────────────────────────────────────────────────────────────┘ +``` + +### Smart Tap vs Apple VAS + +```text +┌──────────────────────────────────────────────────────────────────────────┐ +│ PROTOCOL COMPARISON │ +├──────────────────────────────────────────────────────────────────────────┤ +│ │ +│ Feature │ Apple VAS │ Google Smart Tap │ +│ ─────────────────────┼─────────────────────┼────────────────────────────│ +│ AID │ OSE.VAS.01 │ OSE.VAS.01 (same!) │ +│ Detection │ Tag 50 = "ApplePay" │ Tag 50 = "AndroidPay" │ +│ ECP Required │ Yes (mandatory) │ No │ +│ URL Push Mode │ Yes (P2=0x00) │ No direct equivalent │ +│ Encryption │ ECDH + AES-GCM │ ECDH + AES │ +│ Key Size │ P-256 │ P-256 │ +│ Certification │ Apple NFC cert │ Google enrollment │ +│ │ +└──────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Testing Strategy + +### 9.1 Unit Tests + +```typescript +// Test NDEF URL building +describe('NDEF URL Builder', () => { + it('should build correct NDEF record for https URL', () => { + const url = 'https://pay.example.com/s/123'; + const record = buildNdefUrlMessage(url); + + expect(record[0]).toBe(0xD1); // Header + expect(record[3]).toBe(0x55); // Type "U" + expect(record[4]).toBe(0x04); // https:// prefix + }); + + it('should handle URL without known prefix', () => { + const url = 'custom://app/path'; + const record = buildNdefUrlMessage(url); + + expect(record[4]).toBe(0x00); // No prefix + }); +}); +``` + +### 9.2 Integration Tests + +```text +TEST SCENARIOS: + +1. HCE Available (Android) + - Start HCE with URL + - Verify APDU responses + - Test URL update while running + - Test stop/restart + +2. HCE Not Available (iOS or restricted Android) + - Verify graceful fallback to QR + - Verify UI shows correct state + +3. QR Code Fallback + - Verify QR contains correct URL + - Verify QR is readable by camera apps + +4. UI States + - Loading state + - NFC ready state + - NFC disabled state + - Error states +``` + +### 9.3 Device Testing Matrix + +```text +┌──────────────────────────────────────────────────────────────────────────┐ +│ DEVICE TESTING MATRIX │ +├──────────────────────────────────────────────────────────────────────────┤ +│ │ +│ POS Device │ Customer Device │ Expected Behavior │ +│ ────────────────────────┼──────────────────────┼────────────────────────│ +│ Android (HCE enabled) │ iPhone XS+ │ NDEF notification │ +│ Android (HCE enabled) │ iPhone 6-X │ QR fallback only │ +│ Android (HCE enabled) │ Android 5.0+ │ NDEF notification │ +│ Android (no HCE) │ Any │ QR fallback only │ +│ iOS │ Any │ QR fallback only │ +│ │ +│ Notes: │ +│ - iPhone XS+ = A12 chip, supports Background Tag Reading │ +│ - iPhone 6-X can read NDEF but requires app to be open │ +│ - Android NDEF reading works in background with NFC enabled │ +│ │ +└──────────────────────────────────────────────────────────────────────────┘ +``` + +### 9.4 Test Commands + +```bash +# Run unit tests +npm test + +# Run on Android device +npm run android + +# Run on iOS device (for QR fallback testing) +npm run ios + +# Check NFC hardware (Android only) +adb shell dumpsys nfc +``` + +--- + +## Summary Checklist + +```text +PHASE 1: Repository Analysis +□ Clone and explore repository structure +□ Locate QR code implementation +□ Document payment URL format +□ Check existing NFC dependencies +□ Review platform configurations + +PHASE 2: HCE Implementation +□ Install react-native-nfc-manager and react-native-hce +□ Configure Android manifest +□ Create APDU service configuration +□ Implement useNfcCapabilities hook +□ Implement NDEF URL builder (utils/ndef.ts) +□ Implement HCE service +□ Create useNfcPayment hook +□ Test on Android device + +PHASE 3: Fallback Strategy +□ Document VAS/Smart Tap limitations +□ Implement strategy decision hook +□ Ensure QR code fallback works + +PHASE 4: UI Updates +□ Create PaymentScreen component +□ Create NfcStatusIndicator component +□ Add NFC icon with animation +□ Integrate with existing flow +□ Test on multiple devices +``` + +--- + +## References + +- [react-native-nfc-manager](https://github.com/revtel/react-native-nfc-manager) +- [react-native-hce](https://github.com/nicklockwood/react-native-hce) +- [NFC Forum Type 4 Tag Specification](https://nfc-forum.org/our-work/specification-releases/) +- [Apple VAS Research (kormax)](https://github.com/kormax/apple-vas) +- [Google Smart Tap Research (kormax)](https://github.com/kormax/google-smart-tap) +- [Apple ECP Research (kormax)](https://github.com/kormax/apple-enhanced-contactless-polling) diff --git a/dapps/poc-pos-app/store/useSettingsStore.ts b/dapps/poc-pos-app/store/useSettingsStore.ts index 7cb5a265..e8c40e77 100644 --- a/dapps/poc-pos-app/store/useSettingsStore.ts +++ b/dapps/poc-pos-app/store/useSettingsStore.ts @@ -32,6 +32,9 @@ interface SettingsStore { pinLockoutUntil: number | null; biometricEnabled: boolean; + // NFC settings + nfcEnabled: boolean; + // Actions setThemeMode: (themeMode: "light" | "dark") => void; setDeviceId: (deviceId: string) => void; @@ -49,6 +52,9 @@ interface SettingsStore { resetPinAttempts: () => void; setBiometricEnabled: (enabled: boolean) => void; + // NFC actions + setNfcEnabled: (enabled: boolean) => void; + getVariantPrinterLogo: () => string; } @@ -64,6 +70,7 @@ export const useSettingsStore = create()( pinFailedAttempts: 0, pinLockoutUntil: null, biometricEnabled: false, + nfcEnabled: true, setThemeMode: (themeMode: "light" | "dark") => set({ themeMode }), setDeviceId: (deviceId: string) => set({ deviceId }), setHasHydrated: (state: boolean) => set({ _hasHydrated: state }), @@ -138,6 +145,7 @@ export const useSettingsStore = create()( set({ pinFailedAttempts: 0, pinLockoutUntil: null }), setBiometricEnabled: (enabled: boolean) => set({ biometricEnabled: enabled }), + setNfcEnabled: (enabled: boolean) => set({ nfcEnabled: enabled }), getVariantPrinterLogo: () => { return Variants[get().variant]?.printerLogo ?? DEFAULT_LOGO_BASE64; @@ -145,7 +153,7 @@ export const useSettingsStore = create()( }), { name: "settings", - version: 6, + version: 7, storage, migrate: (persistedState: any, version: number) => { if (!persistedState || typeof persistedState !== "object") { @@ -168,6 +176,10 @@ export const useSettingsStore = create()( persistedState.biometricEnabled = persistedState.biometricEnabled ?? false; } + if (version < 7) { + // Initialize NFC settings - defaults to true (enabled) + persistedState.nfcEnabled = persistedState.nfcEnabled ?? true; + } return persistedState; }, onRehydrateStorage: () => (state, error) => {