diff --git a/.gitleaks.toml b/.gitleaks.toml index 365f24f99..b5a0874b3 100644 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -24,6 +24,7 @@ paths = [ '''common/src/constants/mockCertificates.ts''', '''Database.refactorlog''', '''vendor''', + '''.*tamagui-components\.config\.cjs$''', ] [[rules]] diff --git a/app/.eslintrc.cjs b/app/.eslintrc.cjs index 77e949e2e..96a45d26d 100644 --- a/app/.eslintrc.cjs +++ b/app/.eslintrc.cjs @@ -11,6 +11,8 @@ module.exports = { 'android/', 'deployments/', 'node_modules/', + 'web/dist/', + '.tamagui/*', 'metro.config.cjs', ], rules: { diff --git a/app/.gitignore b/app/.gitignore index d065a83f6..06170ee32 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -89,3 +89,6 @@ yarn-error.log # Bundle analyzer source maps *-sourcemap.jsonandroid/.kotlin/errors/ + +# web app +.tamagui/* diff --git a/app/.prettierignore b/app/.prettierignore index 5d6c3d38e..1d4b544a7 100644 --- a/app/.prettierignore +++ b/app/.prettierignore @@ -6,4 +6,6 @@ src/assets/animations/ witnesscalc/ vendor/ android/ +.tamagui/ +web/dist/ *.md diff --git a/app/env.ts b/app/env.ts new file mode 100644 index 000000000..2ebb85beb --- /dev/null +++ b/app/env.ts @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11 + +/* This file provides compatiblity between how web expects env variables to be and how native does. + * on web it is aliased to @env on native it is not used + */ + +export const IS_TEST_BUILD = process.env.IS_TEST_BUILD === 'true'; +export const GOOGLE_SIGNIN_WEB_CLIENT_ID = + process.env.GOOGLE_SIGNIN_WEB_CLIENT_ID; +export const SENTRY_DSN = process.env.SENTRY_DSN; +export const SEGMENT_KEY = process.env.SEGMENT_KEY; +export const ENABLE_DEBUG_LOGS = process.env.ENABLE_DEBUG_LOGS === 'true'; +export const DEFAULT_PNUMBER = undefined; +export const DEFAULT_DOB = undefined; +export const DEFAULT_DOE = undefined; +export const MIXPANEL_NFC_PROJECT_TOKEN = undefined; diff --git a/app/package.json b/app/package.json index b45420640..b28bfd3e5 100644 --- a/app/package.json +++ b/app/package.json @@ -46,7 +46,10 @@ "tag:remove": "node scripts/tag.js remove", "test": "jest --passWithNoTests", "test:fastlane": "bundle exec ruby -Itest fastlane/test/helpers_test.rb", - "types": "tsc --noEmit" + "types": "tsc --noEmit", + "web": "vite", + "web:build": "vite build", + "web:preview": "vite preview" }, "dependencies": { "@babel/runtime": "^7.27.4", @@ -66,11 +69,16 @@ "@segment/analytics-react-native": "^2.21.0", "@segment/sovran-react-native": "^1.1.3", "@selfxyz/common": "workspace:^", + "@sentry/react": "^9.32.0", "@sentry/react-native": "6.10.0", "@stablelib/cbor": "^2.0.1", + "@tamagui/animations-css": "^1.129.3", + "@tamagui/animations-react-native": "^1.129.3", "@tamagui/config": "1.126.14", "@tamagui/lucide-icons": "1.126.14", "@tamagui/toast": "^1.127.2", + "@tamagui/vite-plugin": "1.126.14", + "@types/react-dom": "^19.1.6", "@xstate/react": "^5.0.3", "add": "^2.0.6", "asn1js": "^3.0.5", @@ -80,6 +88,7 @@ "ethers": "^6.11.0", "expo-modules-core": "^2.2.1", "js-sha512": "^0.9.0", + "lottie-react": "^2.4.1", "lottie-react-native": "7.2.2", "msgpack-lite": "^0.1.26", "node-forge": "^1.3.1", @@ -87,6 +96,7 @@ "pkijs": "^3.2.4", "poseidon-lite": "^0.2.0", "react": "^18.3.1", + "react-dom": "^18.3.1", "react-native": "0.75.4", "react-native-app-auth": "^8.0.3", "react-native-biometrics": "^3.0.1", @@ -106,9 +116,13 @@ "react-native-screens": "4.9.0", "react-native-sqlite-storage": "^6.0.1", "react-native-svg": "^15.11.1", + "react-native-svg-web": "^1.0.9", + "react-native-web": "^0.19.0", + "react-qr-barcode-scanner": "^2.1.7", "socket.io-client": "^4.7.5", "tamagui": "1.126.14", "uuid": "^11.0.5", + "vite-plugin-svgr": "^4.3.0", "xstate": "^5.19.2", "zustand": "^4.5.2" }, @@ -135,7 +149,9 @@ "@types/react-native": "^0.73.0", "@types/react-native-dotenv": "^0.2.0", "@types/react-native-sqlite-storage": "^6.0.5", + "@types/react-native-web": "^0", "@types/react-test-renderer": "^18", + "@vitejs/plugin-react-swc": "^3.10.2", "eslint": "^8.19.0", "eslint-config-prettier": "^10.1.2", "eslint-plugin-header": "^3.1.1", @@ -148,7 +164,8 @@ "react-native-svg-transformer": "^1.5.0", "react-test-renderer": "^18.3.1", "stream-browserify": "^3.0.0", - "typescript": "^5.8.3" + "typescript": "^5.8.3", + "vite": "^7.0.0" }, "engines": { "node": ">=18" diff --git a/app/src/RemoteConfig.shared.ts b/app/src/RemoteConfig.shared.ts new file mode 100644 index 000000000..49240d1d5 --- /dev/null +++ b/app/src/RemoteConfig.shared.ts @@ -0,0 +1,267 @@ +// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11 + +// Shared types and constants for RemoteConfig + +export type FeatureFlagValue = string | boolean | number; + +export interface LocalOverride { + [key: string]: FeatureFlagValue; +} + +export const LOCAL_OVERRIDES_KEY = 'feature_flag_overrides'; + +export const defaultFlags: Record = { + aesop: false, +}; + +export interface FeatureFlagInfo { + key: string; + remoteValue?: FeatureFlagValue; + overrideValue?: FeatureFlagValue; + value: FeatureFlagValue; + source: string; + type: 'boolean' | 'string' | 'number'; +} + +// Shared interfaces for platform-specific implementations +export interface StorageBackend { + getItem(key: string): Promise; + setItem(key: string, value: string): Promise; + removeItem(key: string): Promise; +} + +export interface RemoteConfigBackend { + getValue(key: string): { + asBoolean(): boolean; + asNumber(): number; + asString(): string; + getSource(): string; + }; + getAll(): Record; + setDefaults(defaults: Record): Promise | void; + setConfigSettings(settings: any): Promise | void; + fetchAndActivate(): Promise; +} + +// Helper function to detect and parse remote config values +export const getRemoteConfigValue = ( + remoteConfig: RemoteConfigBackend, + key: string, + defaultValue: FeatureFlagValue, +): FeatureFlagValue => { + const configValue = remoteConfig.getValue(key); + + if (typeof defaultValue === 'boolean') { + return configValue.asBoolean(); + } else if (typeof defaultValue === 'number') { + return configValue.asNumber(); + } else if (typeof defaultValue === 'string') { + return configValue.asString(); + } + + // Fallback: try to infer type from the remote config value + const stringValue = configValue.asString(); + if (stringValue === 'true' || stringValue === 'false') { + return configValue.asBoolean(); + } + if (!Number.isNaN(Number(stringValue)) && stringValue !== '') { + return configValue.asNumber(); + } + return stringValue; +}; + +// Local override management +export const getLocalOverrides = async ( + storage: StorageBackend, +): Promise => { + try { + const overrides = await storage.getItem(LOCAL_OVERRIDES_KEY); + if (!overrides) { + return {}; + } + return JSON.parse(overrides); + } catch (error) { + console.error('Failed to get local overrides:', error); + + // If JSON parsing fails, clear the corrupt data + if (error instanceof SyntaxError) { + try { + await storage.removeItem(LOCAL_OVERRIDES_KEY); + } catch (removeError) { + console.error('Failed to clear corrupt local overrides:', removeError); + } + } + + return {}; + } +}; + +export const setLocalOverride = async ( + storage: StorageBackend, + flag: string, + value: FeatureFlagValue, +): Promise => { + try { + const overrides = await getLocalOverrides(storage); + overrides[flag] = value; + await storage.setItem(LOCAL_OVERRIDES_KEY, JSON.stringify(overrides)); + } catch (error) { + console.error('Failed to set local override:', error); + } +}; + +export const clearLocalOverride = async ( + storage: StorageBackend, + flag: string, +): Promise => { + try { + const overrides = await getLocalOverrides(storage); + delete overrides[flag]; + await storage.setItem(LOCAL_OVERRIDES_KEY, JSON.stringify(overrides)); + } catch (error) { + console.error('Failed to clear local override:', error); + } +}; + +export const clearAllLocalOverrides = async ( + storage: StorageBackend, +): Promise => { + try { + await storage.removeItem(LOCAL_OVERRIDES_KEY); + } catch (error) { + console.error('Failed to clear all local overrides:', error); + } +}; + +export const initRemoteConfig = async ( + remoteConfig: RemoteConfigBackend, +): Promise => { + await remoteConfig.setDefaults(defaultFlags); + await remoteConfig.setConfigSettings({ + minimumFetchIntervalMillis: __DEV__ ? 0 : 3600000, + }); + try { + await remoteConfig.fetchAndActivate(); + } catch (err) { + console.log('Remote config fetch failed', err); + } +}; + +export const getFeatureFlag = async ( + remoteConfig: RemoteConfigBackend, + storage: StorageBackend, + flag: string, + defaultValue: T, +): Promise => { + try { + // Check local overrides first + const localOverrides = await getLocalOverrides(storage); + if (Object.prototype.hasOwnProperty.call(localOverrides, flag)) { + return localOverrides[flag] as T; + } + + // Return default value for string flags + if (typeof defaultValue === 'string') { + return defaultValue; + } + + // Fall back to remote config for number and boolean flags + return getRemoteConfigValue(remoteConfig, flag, defaultValue) as T; + } catch (error) { + console.error('Failed to get feature flag:', error); + return defaultValue; + } +}; + +export const getAllFeatureFlags = async ( + remoteConfig: RemoteConfigBackend, + storage: StorageBackend, +): Promise => { + try { + const keys = remoteConfig.getAll(); + const localOverrides = await getLocalOverrides(storage); + + // Get all remote/default flags + const remoteFlags = Object.keys(keys).map(key => { + const configValue = keys[key]; + + // Try to determine the type from default flags or infer from value + const defaultValue = defaultFlags[key]; + const remoteVal = + defaultValue !== undefined + ? getRemoteConfigValue(remoteConfig, key, defaultValue) + : configValue.asString(); // Default to string if no default defined + + const hasLocalOverride = Object.prototype.hasOwnProperty.call( + localOverrides, + key, + ); + const overrideVal = hasLocalOverride ? localOverrides[key] : undefined; + const effectiveVal = hasLocalOverride ? overrideVal! : remoteVal; + + // Determine type + const type = + typeof effectiveVal === 'boolean' + ? 'boolean' + : typeof effectiveVal === 'number' + ? 'number' + : 'string'; + + return { + key, + remoteValue: remoteVal, + overrideValue: overrideVal, + value: effectiveVal, + type: type as 'boolean' | 'string' | 'number', + source: hasLocalOverride + ? 'Local Override' + : configValue.getSource() === 'remote' + ? 'Remote Config' + : configValue.getSource() === 'default' + ? 'Default' + : configValue.getSource() === 'static' + ? 'Static' + : 'Unknown', + }; + }); + + // Add any local overrides that don't exist in remote config + const localOnlyFlags = Object.keys(localOverrides) + .filter(key => !Object.prototype.hasOwnProperty.call(keys, key)) + .map(key => { + const value = localOverrides[key]; + const type = + typeof value === 'boolean' + ? 'boolean' + : typeof value === 'number' + ? 'number' + : 'string'; + + return { + key, + remoteValue: undefined, + overrideValue: value, + value: value, + type: type as 'boolean' | 'string' | 'number', + source: 'Local Override', + }; + }); + + return [...remoteFlags, ...localOnlyFlags].sort((a, b) => + a.key.localeCompare(b.key), + ); + } catch (error) { + console.error('Failed to get all feature flags:', error); + return []; + } +}; + +export const refreshRemoteConfig = async ( + remoteConfig: RemoteConfigBackend, +): Promise => { + try { + await remoteConfig.fetchAndActivate(); + } catch (err) { + console.log('Remote config refresh failed', err); + } +}; diff --git a/app/src/RemoteConfig.ts b/app/src/RemoteConfig.ts index 2a8bdec11..53ac0be44 100644 --- a/app/src/RemoteConfig.ts +++ b/app/src/RemoteConfig.ts @@ -3,228 +3,77 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; import remoteConfig from '@react-native-firebase/remote-config'; -export type FeatureFlagValue = string | boolean | number; +import { + clearAllLocalOverrides as clearAllLocalOverridesShared, + clearLocalOverride as clearLocalOverrideShared, + FeatureFlagValue, + getAllFeatureFlags as getAllFeatureFlagsShared, + getFeatureFlag as getFeatureFlagShared, + getLocalOverrides as getLocalOverridesShared, + initRemoteConfig as initRemoteConfigShared, + refreshRemoteConfig as refreshRemoteConfigShared, + RemoteConfigBackend, + setLocalOverride as setLocalOverrideShared, + StorageBackend, +} from './RemoteConfig.shared'; -interface LocalOverride { - [key: string]: FeatureFlagValue; -} - -const LOCAL_OVERRIDES_KEY = 'feature_flag_overrides'; - -const defaultFlags: Record = { - aesop: false, +// Mobile-specific storage backend using AsyncStorage +const mobileStorageBackend: StorageBackend = { + getItem: async (key: string): Promise => { + return await AsyncStorage.getItem(key); + }, + setItem: async (key: string, value: string): Promise => { + await AsyncStorage.setItem(key, value); + }, + removeItem: async (key: string): Promise => { + await AsyncStorage.removeItem(key); + }, }; -// Helper function to detect and parse remote config values -const getRemoteConfigValue = ( - key: string, - defaultValue: FeatureFlagValue, -): FeatureFlagValue => { - const configValue = remoteConfig().getValue(key); - - if (typeof defaultValue === 'boolean') { - return configValue.asBoolean(); - } else if (typeof defaultValue === 'number') { - return configValue.asNumber(); - } else if (typeof defaultValue === 'string') { - return configValue.asString(); - } - - // Fallback: try to infer type from the remote config value - const stringValue = configValue.asString(); - if (stringValue === 'true' || stringValue === 'false') { - return configValue.asBoolean(); - } - if (!Number.isNaN(Number(stringValue)) && stringValue !== '') { - return configValue.asNumber(); - } - return stringValue; +// Mobile-specific remote config backend using React Native Firebase +const mobileRemoteConfigBackend: RemoteConfigBackend = { + getValue: (key: string) => { + return remoteConfig().getValue(key); + }, + getAll: () => { + return remoteConfig().getAll(); + }, + setDefaults: async (defaults: Record) => { + await remoteConfig().setDefaults(defaults); + }, + setConfigSettings: async (settings: any) => { + await remoteConfig().setConfigSettings(settings); + }, + fetchAndActivate: async (): Promise => { + return await remoteConfig().fetchAndActivate(); + }, }; -// Local override management -export const getLocalOverrides = async (): Promise => { - try { - const overrides = await AsyncStorage.getItem(LOCAL_OVERRIDES_KEY); - if (!overrides) { - return {}; - } - return JSON.parse(overrides); - } catch (error) { - console.error('Failed to get local overrides:', error); - - // If JSON parsing fails, clear the corrupt data - if (error instanceof SyntaxError) { - try { - await AsyncStorage.removeItem(LOCAL_OVERRIDES_KEY); - } catch (removeError) { - console.error('Failed to clear corrupt local overrides:', removeError); - } - } - - return {}; - } -}; - -export const setLocalOverride = async ( - flag: string, - value: FeatureFlagValue, -): Promise => { - try { - const overrides = await getLocalOverrides(); - overrides[flag] = value; - await AsyncStorage.setItem(LOCAL_OVERRIDES_KEY, JSON.stringify(overrides)); - } catch (error) { - console.error('Failed to set local override:', error); - } -}; - -export const clearLocalOverride = async (flag: string): Promise => { - try { - const overrides = await getLocalOverrides(); - delete overrides[flag]; - await AsyncStorage.setItem(LOCAL_OVERRIDES_KEY, JSON.stringify(overrides)); - } catch (error) { - console.error('Failed to clear local override:', error); - } -}; - -export const clearAllLocalOverrides = async (): Promise => { - try { - await AsyncStorage.removeItem(LOCAL_OVERRIDES_KEY); - } catch (error) { - console.error('Failed to clear all local overrides:', error); - } -}; - -export const initRemoteConfig = async () => { - await remoteConfig().setDefaults(defaultFlags); - await remoteConfig().setConfigSettings({ - minimumFetchIntervalMillis: __DEV__ ? 0 : 3600000, - }); - try { - await remoteConfig().fetchAndActivate(); - } catch (err) { - console.log('Remote config fetch failed', err); - } -}; - -export const getFeatureFlag = async ( +// Export the shared functions with mobile-specific backends +export const getLocalOverrides = () => + getLocalOverridesShared(mobileStorageBackend); +export const setLocalOverride = (flag: string, value: FeatureFlagValue) => + setLocalOverrideShared(mobileStorageBackend, flag, value); +export const clearLocalOverride = (flag: string) => + clearLocalOverrideShared(mobileStorageBackend, flag); +export const clearAllLocalOverrides = () => + clearAllLocalOverridesShared(mobileStorageBackend); +export const initRemoteConfig = () => + initRemoteConfigShared(mobileRemoteConfigBackend); +export const getFeatureFlag = ( flag: string, defaultValue: T, -): Promise => { - try { - // Check local overrides first - const localOverrides = await getLocalOverrides(); - if (Object.prototype.hasOwnProperty.call(localOverrides, flag)) { - return localOverrides[flag] as T; - } +) => + getFeatureFlagShared( + mobileRemoteConfigBackend, + mobileStorageBackend, + flag, + defaultValue, + ); +export const getAllFeatureFlags = () => + getAllFeatureFlagsShared(mobileRemoteConfigBackend, mobileStorageBackend); +export const refreshRemoteConfig = () => + refreshRemoteConfigShared(mobileRemoteConfigBackend); - // Return default value for string flags - if (typeof defaultValue === 'string') { - return defaultValue; - } - - // Fall back to remote config for number and boolean flags - return getRemoteConfigValue(flag, defaultValue) as T; - } catch (error) { - console.error('Failed to get feature flag:', error); - return defaultValue; - } -}; - -export const getAllFeatureFlags = async (): Promise< - Array<{ - key: string; - remoteValue?: FeatureFlagValue; - overrideValue?: FeatureFlagValue; - value: FeatureFlagValue; - source: string; - type: 'boolean' | 'string' | 'number'; - }> -> => { - try { - const keys = remoteConfig().getAll(); - const localOverrides = await getLocalOverrides(); - - // Get all remote/default flags - const remoteFlags = Object.keys(keys).map(key => { - const configValue = keys[key]; - - // Try to determine the type from default flags or infer from value - const defaultValue = defaultFlags[key]; - const remoteVal = - defaultValue !== undefined - ? getRemoteConfigValue(key, defaultValue) - : configValue.asString(); // Default to string if no default defined - - const hasLocalOverride = Object.prototype.hasOwnProperty.call( - localOverrides, - key, - ); - const overrideVal = hasLocalOverride ? localOverrides[key] : undefined; - const effectiveVal = hasLocalOverride ? overrideVal! : remoteVal; - - // Determine type - const type = - typeof effectiveVal === 'boolean' - ? 'boolean' - : typeof effectiveVal === 'number' - ? 'number' - : 'string'; - - return { - key, - remoteValue: remoteVal, - overrideValue: overrideVal, - value: effectiveVal, - type: type as 'boolean' | 'string' | 'number', - source: hasLocalOverride - ? 'Local Override' - : configValue.getSource() === 'remote' - ? 'Remote Config' - : configValue.getSource() === 'default' - ? 'Default' - : configValue.getSource() === 'static' - ? 'Static' - : 'Unknown', - }; - }); - - // Add any local overrides that don't exist in remote config - const localOnlyFlags = Object.keys(localOverrides) - .filter(key => !Object.prototype.hasOwnProperty.call(keys, key)) - .map(key => { - const value = localOverrides[key]; - const type = - typeof value === 'boolean' - ? 'boolean' - : typeof value === 'number' - ? 'number' - : 'string'; - - return { - key, - remoteValue: undefined, - overrideValue: value, - value: value, - type: type as 'boolean' | 'string' | 'number', - source: 'Local Override', - }; - }); - - return [...remoteFlags, ...localOnlyFlags].sort((a, b) => - a.key.localeCompare(b.key), - ); - } catch (error) { - console.error('Failed to get all feature flags:', error); - return []; - } -}; - -export const refreshRemoteConfig = async () => { - try { - await remoteConfig().fetchAndActivate(); - } catch (err) { - console.log('Remote config refresh failed', err); - } -}; +// Re-export types for convenience +export type { FeatureFlagValue } from './RemoteConfig.shared'; diff --git a/app/src/RemoteConfig.web.ts b/app/src/RemoteConfig.web.ts new file mode 100644 index 000000000..f33240afe --- /dev/null +++ b/app/src/RemoteConfig.web.ts @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11 + +// Web-compatible version using LocalStorage and Firebase Web SDK +// This file provides the same API as RemoteConfig.ts but for web environments + +import { + clearAllLocalOverrides as clearAllLocalOverridesShared, + clearLocalOverride as clearLocalOverrideShared, + FeatureFlagValue, + getAllFeatureFlags as getAllFeatureFlagsShared, + getFeatureFlag as getFeatureFlagShared, + getLocalOverrides as getLocalOverridesShared, + initRemoteConfig as initRemoteConfigShared, + refreshRemoteConfig as refreshRemoteConfigShared, + RemoteConfigBackend, + setLocalOverride as setLocalOverrideShared, + StorageBackend, +} from './RemoteConfig.shared'; + +// Web-specific storage backend using LocalStorage +const webStorageBackend: StorageBackend = { + getItem: async (key: string): Promise => { + return localStorage.getItem(key); + }, + setItem: async (key: string, value: string): Promise => { + localStorage.setItem(key, value); + }, + removeItem: async (key: string): Promise => { + localStorage.removeItem(key); + }, +}; + +// Mock Firebase Remote Config for web (since Firebase Web SDK for Remote Config is not installed) +// In a real implementation, you would import and use the Firebase Web SDK +class MockFirebaseRemoteConfig implements RemoteConfigBackend { + private config: Record = {}; + private settings: any = {}; + + setDefaults(defaults: Record) { + this.config = { ...defaults }; + } + + setConfigSettings(settings: any) { + this.settings = settings; + } + + async fetchAndActivate(): Promise { + // Simulate network delay + await new Promise(resolve => setTimeout(resolve, 100)); + return true; + } + + getValue(key: string) { + const value = this.config[key] || ''; + return { + asBoolean: () => { + if (typeof value === 'boolean') return value; + if (typeof value === 'string') return value === 'true'; + return false; + }, + asNumber: () => { + if (typeof value === 'number') return value; + if (typeof value === 'string') { + const num = Number(value); + return isNaN(num) ? 0 : num; + } + return 0; + }, + asString: () => { + if (typeof value === 'string') return value; + return String(value); + }, + getSource: () => { + return value; + }, + }; + } + + getAll() { + return this.config; + } +} + +// Web-specific remote config backend using mock Firebase +const webRemoteConfigBackend: RemoteConfigBackend = + new MockFirebaseRemoteConfig(); + +// Export the shared functions with web-specific backends +export const getLocalOverrides = () => + getLocalOverridesShared(webStorageBackend); +export const setLocalOverride = (flag: string, value: FeatureFlagValue) => + setLocalOverrideShared(webStorageBackend, flag, value); +export const clearLocalOverride = (flag: string) => + clearLocalOverrideShared(webStorageBackend, flag); +export const clearAllLocalOverrides = () => + clearAllLocalOverridesShared(webStorageBackend); +export const initRemoteConfig = () => + initRemoteConfigShared(webRemoteConfigBackend); +export const getFeatureFlag = ( + flag: string, + defaultValue: T, +) => + getFeatureFlagShared( + webRemoteConfigBackend, + webStorageBackend, + flag, + defaultValue, + ); +export const getAllFeatureFlags = () => + getAllFeatureFlagsShared(webRemoteConfigBackend, webStorageBackend); +export const refreshRemoteConfig = () => + refreshRemoteConfigShared(webRemoteConfigBackend); + +// Re-export types for convenience +export type { FeatureFlagValue } from './RemoteConfig.shared'; diff --git a/app/src/Sentry.web.ts b/app/src/Sentry.web.ts new file mode 100644 index 000000000..c2534eb9c --- /dev/null +++ b/app/src/Sentry.web.ts @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11 + +import { SENTRY_DSN } from '@env'; +import * as Sentry from '@sentry/react'; + +export const isSentryDisabled = !SENTRY_DSN; + +export const initSentry = () => { + if (isSentryDisabled) { + return null; + } + + Sentry.init({ + dsn: SENTRY_DSN, + debug: false, + // Performance Monitoring + tracesSampleRate: 1.0, + // Session Replay + replaysSessionSampleRate: 0.1, + replaysOnErrorSampleRate: 1.0, + // Disable collection of PII data + beforeSend(event) { + // Remove PII data + if (event.user) { + event.user.ip_address = undefined; + event.user.id = undefined; + } + return event; + }, + }); + return Sentry; +}; + +export const captureException = ( + error: Error, + context?: Record, +) => { + if (isSentryDisabled) { + return; + } + Sentry.captureException(error, { + extra: context, + }); +}; + +export const captureMessage = ( + message: string, + context?: Record, +) => { + if (isSentryDisabled) { + return; + } + Sentry.captureMessage(message, { + extra: context, + }); +}; + +export const wrapWithSentry = (App: React.ComponentType) => { + return isSentryDisabled ? App : Sentry.withProfiler(App); +}; diff --git a/app/src/components/ErrorBoundary.tsx b/app/src/components/ErrorBoundary.tsx index 66af35a31..0d6444d34 100644 --- a/app/src/components/ErrorBoundary.tsx +++ b/app/src/components/ErrorBoundary.tsx @@ -28,6 +28,9 @@ class ErrorBoundary extends React.Component { componentDidCatch() { // Flush analytics before the app crashes flushAnalytics(); + // TODO Sentry React docs recommend Sentry.captureReactException(error, info); + // https://docs.sentry.io/platforms/javascript/guides/react/features/error-boundary/ + // but ill wait so as to have few changes on native app } render() { diff --git a/app/src/components/buttons/PrimaryButtonLongHold.shared.ts b/app/src/components/buttons/PrimaryButtonLongHold.shared.ts new file mode 100644 index 000000000..533e3a33f --- /dev/null +++ b/app/src/components/buttons/PrimaryButtonLongHold.shared.ts @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11 + +import { ButtonProps } from './AbstractButton'; + +export type RGBA = `rgba(${number}, ${number}, ${number}, ${number})`; + +export const ACTION_TIMER = 600; // time in ms +//slate400 to slate800 but in rgb +export const COLORS: RGBA[] = ['rgba(30, 41, 59, 0.3)', 'rgba(30, 41, 59, 1)']; + +export interface HeldPrimaryButtonProps extends ButtonProps { + onLongPress: () => void; +} diff --git a/app/src/components/buttons/PrimaryButtonLongHold.tsx b/app/src/components/buttons/PrimaryButtonLongHold.tsx index 28b72e6aa..e12562e03 100644 --- a/app/src/components/buttons/PrimaryButtonLongHold.tsx +++ b/app/src/components/buttons/PrimaryButtonLongHold.tsx @@ -8,23 +8,24 @@ import { useAnimatedValue, } from 'react-native'; -import { ButtonProps } from './AbstractButton'; import { PrimaryButton } from './PrimaryButton'; +import { + ACTION_TIMER, + COLORS, + HeldPrimaryButtonProps, +} from './PrimaryButtonLongHold.shared'; -type RGBA = `rgba(${number}, ${number}, ${number}, ${number})`; - -const ACTION_TIMER = 600; // time in ms -//slate400 to slate800 but in rgb -const COLORS: RGBA[] = ['rgba(30, 41, 59, 0.3)', 'rgba(30, 41, 59, 1)']; export function HeldPrimaryButton({ children, onLongPress, ...props -}: ButtonProps & { onLongPress: () => void }) { - const animation = useAnimatedValue(0); +}: HeldPrimaryButtonProps) { const [hasTriggered, setHasTriggered] = useState(false); const [size, setSize] = useState({ width: 0, height: 0 }); + // React Native animation setup + const animation = useAnimatedValue(0); + const onPressIn = () => { setHasTriggered(false); Animated.timing(animation, { @@ -50,23 +51,8 @@ export function HeldPrimaryButton({ setSize({ width, height }); }; - const getProgressStyles = () => { - const scaleX = animation.interpolate({ - inputRange: [0, 1], - outputRange: [0, 1], - }); - const bgColor = animation.interpolate({ - inputRange: [0, 1], - outputRange: COLORS, - }); - return { - transform: [{ scaleX }], - backgroundColor: bgColor, - height: size.height, - }; - }; - useEffect(() => { + // Mobile: Use React Native animation listener animation.addListener(({ value }) => { if (value >= 0.95 && !hasTriggered) { setHasTriggered(true); @@ -78,6 +64,32 @@ export function HeldPrimaryButton({ }; }, [animation, hasTriggered, onLongPress]); + const renderAnimatedComponent = () => { + // Mobile: Use React Native Animated.View + const scaleX = animation.interpolate({ + inputRange: [0, 1], + outputRange: [0, 1], + }); + const bgColor = animation.interpolate({ + inputRange: [0, 1], + outputRange: COLORS, + }); + + return ( + + ); + }; + return ( - } + animatedComponent={renderAnimatedComponent()} > {children} ); } + const styles = StyleSheet.create({ fill: { transformOrigin: 'left', diff --git a/app/src/components/buttons/PrimaryButtonLongHold.web.tsx b/app/src/components/buttons/PrimaryButtonLongHold.web.tsx new file mode 100644 index 000000000..509b782a5 --- /dev/null +++ b/app/src/components/buttons/PrimaryButtonLongHold.web.tsx @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11 + +import React, { useEffect, useState } from 'react'; +import { LayoutChangeEvent } from 'react-native'; +// Tamagui imports for web +import { AnimatePresence, YStack } from 'tamagui'; + +import { PrimaryButton } from './PrimaryButton'; +import { + ACTION_TIMER, + COLORS, + HeldPrimaryButtonProps, +} from './PrimaryButtonLongHold.shared'; + +export function HeldPrimaryButton({ + children, + onLongPress, + ...props +}: HeldPrimaryButtonProps) { + const [hasTriggered, setHasTriggered] = useState(false); + const [size, setSize] = useState({ width: 0, height: 0 }); + const [isPressed, setIsPressed] = useState(false); + + const onPressIn = () => { + setHasTriggered(false); + setIsPressed(true); + }; + + const onPressOut = () => { + setIsPressed(false); + }; + + const getButtonSize = (e: LayoutChangeEvent) => { + const width = e.nativeEvent.layout.width - 1; + const height = e.nativeEvent.layout.height - 1; + setSize({ width, height }); + }; + + useEffect(() => { + // Web: Use setTimeout to trigger onLongPress + let timeoutId: NodeJS.Timeout; + if (isPressed && !hasTriggered) { + timeoutId = setTimeout(() => { + if (isPressed && !hasTriggered) { + setHasTriggered(true); + onLongPress(); + } + }, ACTION_TIMER); + } + return () => { + if (timeoutId) clearTimeout(timeoutId); + }; + }, [hasTriggered, onLongPress, isPressed]); + + const renderAnimatedComponent = () => { + // Web: Use Tamagui AnimatePresence with CSS transitions + return ( + + {isPressed && ( + + )} + + ); + }; + + return ( + + {children} + + ); +} diff --git a/app/src/components/native/PassportCamera.web.tsx b/app/src/components/native/PassportCamera.web.tsx new file mode 100644 index 000000000..2b11e6af0 --- /dev/null +++ b/app/src/components/native/PassportCamera.web.tsx @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11 + +import React, { useCallback } from 'react'; + +import { extractMRZInfo } from '../../utils/utils'; + +// TODO: Web find a lightweight ocr or mrz scanner. + +export interface PassportCameraProps { + isMounted: boolean; + onPassportRead: ( + error: Error | null, + mrzData?: ReturnType, + ) => void; +} + +export const PassportCamera: React.FC = ({ + onPassportRead, + isMounted, +}) => { + const handleError = useCallback(() => { + if (!isMounted) { + return; + } + const error = new Error('Passport camera not implemented for web yet'); + onPassportRead(error); + }, [onPassportRead, isMounted]); + + // Web stub - no functionality yet + React.useEffect(() => { + // Simulate that the component is not ready for web + if (isMounted) { + console.warn('PassportCamera: Web implementation not yet available'); + // Optionally trigger an error after a short delay to indicate not implemented + const timer = setTimeout(() => { + handleError(); + }, 100); + return () => clearTimeout(timer); + } + }, [isMounted, handleError]); + + return ( +
+
+
📷 Passport Camera
+
+ Web implementation coming soon +
+
+
+ ); +}; diff --git a/app/src/components/native/QrCodeScanner.web.tsx b/app/src/components/native/QrCodeScanner.web.tsx new file mode 100644 index 000000000..a785b08ef --- /dev/null +++ b/app/src/components/native/QrCodeScanner.web.tsx @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11 + +import React from 'react'; +import BarcodeScanner, { BarcodeFormat } from 'react-qr-barcode-scanner'; + +export interface QRCodeScannerViewProps { + isMounted: boolean; + onQRData: (error: Error | null, uri?: string) => void; +} + +export function QRCodeScannerView({ + onQRData, + isMounted, +}: QRCodeScannerViewProps) { + if (!isMounted) { + return null; + } + return ( + { + if (result) { + const url = result.getText(); + console.log('SELF URL', url); + onQRData(null, url); + } else if (err && err instanceof Error) { + // it will give NotFoundException2 every frame until a QR code is found so we ignore it because thats just noisy + if (err.name !== 'NotFoundException2') { + onQRData(err); + } + } + }} + /> + ); +} + +export default QRCodeScannerView; diff --git a/app/src/hooks/useAppUpdates.web.ts b/app/src/hooks/useAppUpdates.web.ts new file mode 100644 index 000000000..01af69987 --- /dev/null +++ b/app/src/hooks/useAppUpdates.web.ts @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11 + +import { useNavigation } from '@react-navigation/native'; +import { useState } from 'react'; + +import { AppEvents } from '../consts/analytics'; +import analytics from '../utils/analytics'; +import { registerModalCallbacks } from '../utils/modalCallbackRegistry'; + +const { trackEvent } = analytics(); + +export const useAppUpdates = (): [boolean, () => void, boolean] => { + const navigation = useNavigation(); + const [isModalDismissed, setIsModalDismissed] = useState(false); + + const showAppUpdateModal = () => { + const callbackId = registerModalCallbacks({ + onButtonPress: async () => { + window.location.reload(); + trackEvent(AppEvents.UPDATE_STARTED); + }, + onModalDismiss: () => { + setIsModalDismissed(true); + trackEvent(AppEvents.UPDATE_MODAL_CLOSED); + }, + }); + + navigation.navigate('Modal', { + titleText: 'New Version Available', + bodyText: + "We've improved performance, fixed bugs, and added new features. Update now to install the latest version of Self.", + buttonText: 'Update and restart', + callbackId, + }); + trackEvent(AppEvents.UPDATE_MODAL_OPENED); + }; + + const newVersionAvailable = false; + + return [newVersionAvailable, showAppUpdateModal, isModalDismissed]; +}; diff --git a/app/src/hooks/useConnectionModal.ts b/app/src/hooks/useConnectionModal.ts index 178cff384..cd99ddd60 100644 --- a/app/src/hooks/useConnectionModal.ts +++ b/app/src/hooks/useConnectionModal.ts @@ -1,6 +1,5 @@ // SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11 -import { useNetInfo } from '@react-native-community/netinfo'; import { useEffect } from 'react'; import { Linking, Platform } from 'react-native'; @@ -9,6 +8,7 @@ import { navigationRef } from '../navigation'; import { useSettingStore } from '../stores/settingStore'; import analytics from '../utils/analytics'; import { useModal } from './useModal'; +import { useNetInfo } from './useNetInfo'; const { trackEvent } = analytics(); diff --git a/app/src/hooks/useNetInfo.ts b/app/src/hooks/useNetInfo.ts new file mode 100644 index 000000000..3d7931ef2 --- /dev/null +++ b/app/src/hooks/useNetInfo.ts @@ -0,0 +1,3 @@ +// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11 + +export { useNetInfo } from '@react-native-community/netinfo'; diff --git a/app/src/hooks/useNetInfo.web.ts b/app/src/hooks/useNetInfo.web.ts new file mode 100644 index 000000000..55234fb0b --- /dev/null +++ b/app/src/hooks/useNetInfo.web.ts @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11 + +export function useNetInfo() { + // when implementing this for real be ware that Network information API + // is not available on webview on ios https://developer.mozilla.org/en-US/docs/Web/API/Network_Information_API + return { isConnected: true, isInternetReachable: true }; +} diff --git a/app/src/mocks/react-native-gesture-handler.ts b/app/src/mocks/react-native-gesture-handler.ts new file mode 100644 index 000000000..fafabfc8d --- /dev/null +++ b/app/src/mocks/react-native-gesture-handler.ts @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11 + +/* + * Web-compatible mock for react-native-gesture-handler + */ + +import React from 'react'; + +// Mock GestureHandlerRootView as a simple wrapper +export const GestureHandlerRootView: React.FC<{ + children: React.ReactNode; + [key: string]: any; +}> = ({ children, ...props }) => { + return React.createElement('div', props, children); +}; + +const returnValue = { + numberOfTaps: () => returnValue, + onStart: () => returnValue, + onEnd: () => returnValue, + onCancel: () => returnValue, + onFail: () => returnValue, + onUpdate: () => returnValue, + onFinalize: () => returnValue, +}; + +// Mock Gesture and GestureDetector for web +export const Gesture = { + Pan: () => returnValue, + Tap: () => returnValue, + LongPress: () => returnValue, + Pinch: () => returnValue, + Rotation: () => returnValue, + Fling: () => returnValue, + Force: () => returnValue, + Native: () => returnValue, + Race: () => returnValue, + Simultaneous: () => returnValue, + Exclusive: () => returnValue, + Composed: () => returnValue, +}; + +export const GestureDetector: React.FC<{ + children: React.ReactNode; + gesture?: any; +}> = ({ children, gesture: _gesture }) => { + return React.createElement('div', {}, children); +}; + +// Mock other commonly used exports +export const State = { + UNDETERMINED: 0, + FAILED: 1, + BEGAN: 2, + CANCELLED: 3, + ACTIVE: 4, + END: 5, +}; + +export const Directions = { + RIGHT: 1, + LEFT: 2, + UP: 4, + DOWN: 8, +}; + +// Mock the jest setup +export const jestSetup = () => {}; + +// Default export for the main import +export default { + GestureHandlerRootView, + Gesture, + GestureDetector, + State, + Directions, + jestSetup, +}; diff --git a/app/src/mocks/react-native-safe-area-context.js b/app/src/mocks/react-native-safe-area-context.js new file mode 100644 index 000000000..2b455d7a6 --- /dev/null +++ b/app/src/mocks/react-native-safe-area-context.js @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11 + +import React from 'react'; +// On web we dont need safe context area since we will be inside another app. (and it doesnt work) + +export function SafeAreaProvider({ children }) { + return React.createElement(React.Fragment, null, children); +} + +export function useSafeAreaInsets() { + return { top: 0, bottom: 0, left: 0, right: 0 }; +} + +export function useSafeAreaFrame() { + return { x: 0, y: 0, width: 0, height: 0 }; +} + +export function SafeAreaView(props) { + return React.createElement('div', props, props.children); +} + +export const initialWindowMetrics = { + insets: { + top: 0, + bottom: 0, + left: 0, + right: 0, + }, + frame: { + x: 0, + y: 0, + width: 0, + height: 0, + }, +}; + +export const SafeAreaContext = React.createContext(initialWindowMetrics); + +export const SafeAreaInsetsContext = React.createContext({ + top: 0, + bottom: 0, + left: 0, + right: 0, +}); diff --git a/app/src/navigation/home.ts b/app/src/navigation/home.ts index 556486822..cf5cb4f0b 100644 --- a/app/src/navigation/home.ts +++ b/app/src/navigation/home.ts @@ -8,7 +8,6 @@ import HomeScreen from '../screens/home/HomeScreen'; import ProofHistoryDetailScreen from '../screens/home/ProofHistoryDetailScreen'; import ProofHistoryScreen from '../screens/home/ProofHistoryScreen'; import { black } from '../utils/colors'; - const homeScreens = { Disclaimer: { screen: DisclaimerScreen, diff --git a/app/src/navigation/index.tsx b/app/src/navigation/index.tsx index 4dbc832c4..a66e8a665 100644 --- a/app/src/navigation/index.tsx +++ b/app/src/navigation/index.tsx @@ -9,6 +9,7 @@ import { } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import React from 'react'; +import { Platform } from 'react-native'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { DefaultNavBar } from '../components/NavBar'; @@ -38,7 +39,8 @@ export const navigationScreens = { }; const AppNavigation = createNativeStackNavigator({ - initialRouteName: 'Splash', + id: undefined, + initialRouteName: Platform.OS === 'web' ? 'Home' : 'Splash', screenOptions: { header: DefaultNavBar, navigationBarColor: white, diff --git a/app/src/navigation/recovery.web.ts b/app/src/navigation/recovery.web.ts new file mode 100644 index 000000000..27dadc629 --- /dev/null +++ b/app/src/navigation/recovery.web.ts @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11 + +import { NativeStackNavigationOptions } from '@react-navigation/native-stack'; + +import PassportDataNotFound from '../screens/recovery/PassportDataNotFoundScreen'; + +const recoveryScreens = { + PassportDataNotFound: { + screen: PassportDataNotFound, + options: { + headerShown: false, + gestureEnabled: false, + animation: 'slide_from_bottom', + // presentation: 'modal', + } as NativeStackNavigationOptions, + }, +}; + +export default recoveryScreens; diff --git a/app/src/navigation/settings.web.ts b/app/src/navigation/settings.web.ts new file mode 100644 index 000000000..0ee69cd64 --- /dev/null +++ b/app/src/navigation/settings.web.ts @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11 + +import { NativeStackNavigationOptions } from '@react-navigation/native-stack'; + +import ManageDocumentsScreen from '../screens/settings/ManageDocumentsScreen'; +import PassportDataInfoScreen from '../screens/settings/PassportDataInfoScreen'; +import SettingsScreen from '../screens/settings/SettingsScreen'; +import { black, white } from '../utils/colors'; + +const settingsScreens = { + ManageDocuments: { + screen: ManageDocumentsScreen, + options: { + title: 'Manage Documents', + headerStyle: { + backgroundColor: white, + }, + headerTitleStyle: { + color: black, + }, + } as NativeStackNavigationOptions, + }, + PassportDataInfo: { + screen: PassportDataInfoScreen, + options: { + title: 'Passport Data Info', + headerStyle: { + backgroundColor: white, + }, + } as NativeStackNavigationOptions, + }, + Settings: { + screen: SettingsScreen, + options: { + animation: 'slide_from_bottom', + title: 'Settings', + headerStyle: { + backgroundColor: white, + }, + headerTitleStyle: { + color: black, + }, + navigationBarColor: black, + } as NativeStackNavigationOptions, + config: { + screens: {}, + }, + }, +}; + +export default settingsScreens; diff --git a/app/src/providers/authProvider.web.tsx b/app/src/providers/authProvider.web.tsx new file mode 100644 index 000000000..e25e0f649 --- /dev/null +++ b/app/src/providers/authProvider.web.tsx @@ -0,0 +1,284 @@ +// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11 + +/* + * This entire file is a stub and MUST be replaced + * + */ + +import React, { + createContext, + PropsWithChildren, + useCallback, + useContext, + useMemo, + useState, +} from 'react'; + +import { AuthEvents } from '../consts/analytics'; +import { Mnemonic } from '../types/mnemonic'; +import analytics from '../utils/analytics'; + +const { trackEvent } = analytics(); + +type SignedPayload = { signature: string; data: T }; + +// Check if Android bridge is available +interface AndroidBridge { + getPrivateKey(): Promise; +} + +declare global { + interface Window { + // TODO ios Bridge + Android?: AndroidBridge; + } +} + +const isAndroidBridgeAvailable = (): boolean => { + return typeof window !== 'undefined' && 'Android' in window; +}; + +// Get private key from Android bridge or prompt user +const getPrivateKeyFromAndroidBridge = async (): Promise => { + if (!isAndroidBridgeAvailable()) { + return null; + } + + try { + const privateKey = await window.Android!.getPrivateKey(); + + // Validate the returned private key + if (typeof privateKey !== 'string' || privateKey.length === 0) { + throw new Error('Invalid private key received from Android bridge'); + } + + return privateKey; + } catch (error) { + console.error('Failed to get private key from Android bridge:', error); + return null; + } +}; + +// Prompt user for private key input +const promptUserForPrivateKey = async (): Promise => { + // TODO: Implement secure key input mechanism + throw new Error('Secure key input not yet implemented for web'); +}; + +// Get private key from Android bridge or prompt user +const getPrivateKey = async (): Promise => { + // Try Android bridge first + const key = await getPrivateKeyFromAndroidBridge(); + if (key) { + return key; + } + return promptUserForPrivateKey(); +}; + +/* + * This function is not implemented yet + * and is only a placeholder for the web implementation. + * it doesnt do anything + */ +const _getSecurely = async function ( + fn: () => Promise, + formatter: (dataString: string) => T, +): Promise | null> { + console.log('Starting _getSecurely (web)'); + + console.warn( + 'This is a stub for _getSecurely on web. It does not implement secure storage or biometric authentication.', + ); + const dataString = await fn(); + console.log('Got data string:', dataString ? 'exists' : 'not found'); + + if (dataString === false) { + console.log('No data string available'); + return null; + } + + try { + // For web, we need to figure out exactly how this will interact with the + // Android bridge or any other secure storage mechanism. + trackEvent(AuthEvents.BIOMETRIC_AUTH_SUCCESS); + return { + signature: 'authenticated', + data: formatter(dataString), + }; + } catch (error: any) { + console.error('Error in _getSecurely:', error); + trackEvent(AuthEvents.BIOMETRIC_AUTH_FAILED, { + reason: 'unknown_error', + error: error.message, + }); + throw error; + } +}; + +async function checkBiometricsAvailable(): Promise { + // On web, biometrics are not available in the same way as mobile + // We'll return false to indicate biometrics are not available + trackEvent(AuthEvents.BIOMETRIC_CHECK, { available: false }); + return false; +} + +async function restoreFromMnemonic(_mnemonic: string): Promise { + // No-op on web since we don't have access to mnemonics + console.log('restoreFromMnemonic: No-op on web'); + trackEvent(AuthEvents.MNEMONIC_RESTORE_FAILED, { + reason: 'not_supported_on_web', + }); + return false; +} + +async function loadOrCreateMnemonic(): Promise { + // No-op on web since we don't have access to mnemonics + console.log('loadOrCreateMnemonic: No-op on web'); + return false; +} + +interface AuthProviderProps extends PropsWithChildren { + authenticationTimeoutinMs?: number; +} + +interface IAuthContext { + isAuthenticated: boolean; + isAuthenticating: boolean; + loginWithBiometrics: () => Promise; + _getSecurely: typeof _getSecurely; + getOrCreateMnemonic: () => Promise | null>; + restoreAccountFromMnemonic: ( + mnemonic: string, + ) => Promise | null>; + checkBiometricsAvailable: () => Promise; +} + +export const AuthContext = createContext({ + isAuthenticated: false, + isAuthenticating: false, + loginWithBiometrics: () => Promise.resolve(), + _getSecurely, + getOrCreateMnemonic: () => Promise.resolve(null), + restoreAccountFromMnemonic: () => Promise.resolve(null), + checkBiometricsAvailable: () => Promise.resolve(false), +}); + +export const AuthProvider = ({ + children, + authenticationTimeoutinMs = 15 * 60 * 1000, +}: AuthProviderProps) => { + const [_, setAuthenticatedTimeout] = + useState>(); + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [isAuthenticatingPromise, setIsAuthenticatingPromise] = + useState | null>(null); + + const loginWithBiometrics = useCallback(async () => { + if (isAuthenticatingPromise) { + await isAuthenticatingPromise; + return; + } + + trackEvent(AuthEvents.BIOMETRIC_LOGIN_ATTEMPT); + + // On web, we'll simulate biometric authentication by checking if we can get the private key + const promise = (async () => { + try { + const privateKey = await getPrivateKey(); + if (privateKey) { + return { success: true }; + } else { + return { success: false, error: 'No private key provided' }; + } + } catch (error: any) { + return { success: false, error: error.message }; + } + })(); + + setIsAuthenticatingPromise(promise); + const { success, error } = await promise; + + if (error) { + setIsAuthenticatingPromise(null); + trackEvent(AuthEvents.BIOMETRIC_LOGIN_FAILED, { error }); + throw new Error(error); + } + if (!success) { + setIsAuthenticatingPromise(null); + trackEvent(AuthEvents.BIOMETRIC_LOGIN_CANCELLED); + throw new Error('Canceled by user'); + } + + setIsAuthenticatingPromise(null); + setIsAuthenticated(true); + trackEvent(AuthEvents.BIOMETRIC_LOGIN_SUCCESS); + setAuthenticatedTimeout(previousTimeout => { + if (previousTimeout) { + clearTimeout(previousTimeout); + } + return setTimeout(() => { + setIsAuthenticated(false); + trackEvent(AuthEvents.AUTHENTICATION_TIMEOUT); + }, authenticationTimeoutinMs); + }); + }, [isAuthenticatingPromise, authenticationTimeoutinMs]); + + const getOrCreateMnemonic = useCallback( + () => _getSecurely(loadOrCreateMnemonic, str => JSON.parse(str)), + [], + ); + + const restoreAccountFromMnemonic = useCallback( + (mnemonic: string) => + _getSecurely( + () => restoreFromMnemonic(mnemonic), + str => !!str, + ), + [], + ); + + const state: IAuthContext = useMemo( + () => ({ + isAuthenticated, + isAuthenticating: !!isAuthenticatingPromise, + loginWithBiometrics, + getOrCreateMnemonic, + restoreAccountFromMnemonic, + checkBiometricsAvailable, + _getSecurely, + }), + [ + isAuthenticated, + isAuthenticatingPromise, + loginWithBiometrics, + getOrCreateMnemonic, + restoreAccountFromMnemonic, + ], + ); + + return {children}; +}; + +export const useAuth = () => { + return useContext(AuthContext); +}; + +export async function hasSecretStored() { + // TODO implement a way to check if the private key is stored + return true; +} + +/** + * The only reason this is exported without being locked behind user biometrics is to allow `loadPassportDataAndSecret` + * to access both the privatekey and the passport data with the user only authenticating once + */ +export async function unsafe_getPrivateKey() { + return getPrivateKey(); +} + +export async function unsafe_clearSecrets() { + if (__DEV__) { + console.warn('unsafe_clearSecrets is not implemented for web'); + // In a real implementation, you would clear any stored secrets here + } +} diff --git a/app/src/providers/notificationTrackingProvider.web.tsx b/app/src/providers/notificationTrackingProvider.web.tsx new file mode 100644 index 000000000..4715a259c --- /dev/null +++ b/app/src/providers/notificationTrackingProvider.web.tsx @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11 + +import React, { PropsWithChildren } from 'react'; + +//TODO:WEB Stubbed out for now on web +export const NotificationTrackingProvider: React.FC = ({ + children, +}) => { + return <>{children}; +}; diff --git a/app/src/screens/home/ProofHistoryDetailScreen.tsx b/app/src/screens/home/ProofHistoryDetailScreen.tsx index 34300b9ce..07126525f 100644 --- a/app/src/screens/home/ProofHistoryDetailScreen.tsx +++ b/app/src/screens/home/ProofHistoryDetailScreen.tsx @@ -5,7 +5,7 @@ import React, { useMemo } from 'react'; import { ScrollView, StyleSheet } from 'react-native'; import { Card, Image, Text, XStack, YStack } from 'tamagui'; -import { ProofHistory, ProofStatus } from '../../stores/proofHistoryStore'; +import { ProofHistory, ProofStatus } from '../../stores/proof-types'; import { black, blue100, diff --git a/app/src/screens/home/ProofHistoryScreen.tsx b/app/src/screens/home/ProofHistoryScreen.tsx index ed2fa105f..f5513836c 100644 --- a/app/src/screens/home/ProofHistoryScreen.tsx +++ b/app/src/screens/home/ProofHistoryScreen.tsx @@ -13,11 +13,8 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { Card, Image, Text, View, XStack, YStack } from 'tamagui'; import { BodyText } from '../../components/typography/BodyText'; -import { - ProofHistory, - ProofStatus, - useProofHistoryStore, -} from '../../stores/proofHistoryStore'; +import { ProofHistory, ProofStatus } from '../../stores/proof-types'; +import { useProofHistoryStore } from '../../stores/proofHistoryStore'; import { black, blue100, diff --git a/app/src/screens/passport/PassportNFCScanScreen.web.tsx b/app/src/screens/passport/PassportNFCScanScreen.web.tsx new file mode 100644 index 000000000..16c8a786a --- /dev/null +++ b/app/src/screens/passport/PassportNFCScanScreen.web.tsx @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11 + +import React from 'react'; +import { Image } from 'tamagui'; + +import { SecondaryButton } from '../../components/buttons/SecondaryButton'; +import ButtonsContainer from '../../components/ButtonsContainer'; +import TextsContainer from '../../components/TextsContainer'; +import { BodyText } from '../../components/typography/BodyText'; +import { Title } from '../../components/typography/Title'; +import { PassportEvents } from '../../consts/analytics'; +import useHapticNavigation from '../../hooks/useHapticNavigation'; +import NFC_IMAGE from '../../images/nfc.png'; +import { ExpandableBottomLayout } from '../../layouts/ExpandableBottomLayout'; +import { black, slate100, white } from '../../utils/colors'; + +interface PassportNFCScanScreenProps {} + +const PassportNFCScanScreen: React.FC = ({}) => { + const onCancelPress = useHapticNavigation('Launch', { + action: 'cancel', + }); + + return ( + + + <>Animation Goes Here + + + <> + + + <BodyText textAlign="center">TODO implement</BodyText> + </TextsContainer> + <Image + h="$8" + w="$8" + alignSelf="center" + borderRadius={1000} + source={{ + uri: NFC_IMAGE, + }} + margin={20} + /> + </> + <ButtonsContainer> + <SecondaryButton + trackEvent={PassportEvents.CANCEL_PASSPORT_NFC} + onPress={onCancelPress} + > + Cancel + </SecondaryButton> + </ButtonsContainer> + </ExpandableBottomLayout.BottomSection> + </ExpandableBottomLayout.Layout> + ); +}; + +export default PassportNFCScanScreen; diff --git a/app/src/screens/prove/ProofRequestStatusScreen.tsx b/app/src/screens/prove/ProofRequestStatusScreen.tsx index e06a19c89..134dd5a6c 100644 --- a/app/src/screens/prove/ProofRequestStatusScreen.tsx +++ b/app/src/screens/prove/ProofRequestStatusScreen.tsx @@ -17,10 +17,8 @@ import { Title } from '../../components/typography/Title'; import { ProofEvents } from '../../consts/analytics'; import useHapticNavigation from '../../hooks/useHapticNavigation'; import { ExpandableBottomLayout } from '../../layouts/ExpandableBottomLayout'; -import { - ProofStatus, - useProofHistoryStore, -} from '../../stores/proofHistoryStore'; +import { ProofStatus } from '../../stores/proof-types'; +import { useProofHistoryStore } from '../../stores/proofHistoryStore'; import { useSelfAppStore } from '../../stores/selfAppStore'; import analytics from '../../utils/analytics'; import { black, white } from '../../utils/colors'; diff --git a/app/src/screens/prove/ProveScreen.tsx b/app/src/screens/prove/ProveScreen.tsx index cb5ba114b..9c9d84aa7 100644 --- a/app/src/screens/prove/ProveScreen.tsx +++ b/app/src/screens/prove/ProveScreen.tsx @@ -28,10 +28,8 @@ import { Caption } from '../../components/typography/Caption'; import { ProofEvents } from '../../consts/analytics'; import { ExpandableBottomLayout } from '../../layouts/ExpandableBottomLayout'; import { setDefaultDocumentTypeIfNeeded } from '../../providers/passportDataProvider'; -import { - ProofStatus, - useProofHistoryStore, -} from '../../stores/proofHistoryStore'; +import { ProofStatus } from '../../stores/proof-types'; +import { useProofHistoryStore } from '../../stores/proofHistoryStore'; import { useSelfAppStore } from '../../stores/selfAppStore'; import analytics from '../../utils/analytics'; import { black, slate300, white } from '../../utils/colors'; diff --git a/app/src/screens/settings/SettingsScreen.tsx b/app/src/screens/settings/SettingsScreen.tsx index 292426617..212e2e21e 100644 --- a/app/src/screens/settings/SettingsScreen.tsx +++ b/app/src/screens/settings/SettingsScreen.tsx @@ -6,7 +6,6 @@ import { FileText } from '@tamagui/lucide-icons'; import React, { PropsWithChildren, useCallback, useMemo } from 'react'; import { Linking, Platform, Share } from 'react-native'; import { Gesture, GestureDetector } from 'react-native-gesture-handler'; -import { getCountry, getLocales, getTimeZone } from 'react-native-localize'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { SvgProps } from 'react-native-svg'; import { Button, ScrollView, View, XStack, YStack } from 'tamagui'; @@ -41,6 +40,7 @@ import { } from '../../utils/colors'; import { extraYPadding } from '../../utils/constants'; import { impactLight } from '../../utils/haptic'; +import { getCountry, getLocales, getTimeZone } from '../../utils/locale'; interface SettingsScreenProps {} interface MenuButtonProps extends PropsWithChildren { @@ -70,14 +70,29 @@ const goToStore = () => { Linking.openURL(storeURL); }; -const routes = [ - [Data, 'View passport info', 'PassportDataInfo'], - [Lock, 'Reveal recovery phrase', 'ShowRecoveryPhrase'], - [Cloud, 'Cloud backup', 'CloudBackupSettings'], - [Feedback, 'Send feeback', 'email_feedback'], - [ShareIcon, 'Share Self app', 'share'], - [FileText as React.FC<SvgProps>, 'Manage ID documents', 'ManageDocuments'], -] satisfies [React.FC<SvgProps>, string, RouteOption][]; +const routes = + Platform.OS !== 'web' + ? ([ + [Data, 'View passport info', 'PassportDataInfo'], + [Lock, 'Reveal recovery phrase', 'ShowRecoveryPhrase'], + [Cloud, 'Cloud backup', 'CloudBackupSettings'], + [Feedback, 'Send feedback', 'email_feedback'], + [ShareIcon, 'Share Self app', 'share'], + [ + FileText as React.FC<SvgProps>, + 'Manage ID documents', + 'ManageDocuments', + ], + ] satisfies [React.FC<SvgProps>, string, RouteOption][]) + : ([ + [Data, 'View passport info', 'PassportDataInfo'], + [Feedback, 'Send feeback', 'email_feedback'], + [ + FileText as React.FC<SvgProps>, + 'Manage ID documents', + 'ManageDocuments', + ], + ] satisfies [React.FC<SvgProps>, string, RouteOption][]); // get the actual type of the routes so we can use in the onMenuPress function so it // doesnt worry about us linking to screens with required props which we dont want to go to anyway diff --git a/app/src/stores/database.ts b/app/src/stores/database.ts new file mode 100644 index 000000000..d771e5bcc --- /dev/null +++ b/app/src/stores/database.ts @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11 + +import SQLite from 'react-native-sqlite-storage'; + +import { + ProofDB, + ProofDBResult, + ProofHistory, + ProofStatus, +} from './proof-types'; + +const PAGE_SIZE = 20; +const DB_NAME = 'proof_history.db'; +const TABLE_NAME = 'proof_history'; +const STALE_PROOF_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes + +SQLite.enablePromise(true); + +async function openDatabase() { + return SQLite.openDatabase({ + name: DB_NAME, + location: 'default', + }); +} + +export const database: ProofDB = { + updateStaleProofs: async ( + setProofStatus: (id: string, status: ProofStatus) => Promise<void>, + ) => { + const db = await openDatabase(); + const staleTimestamp = Date.now() - STALE_PROOF_TIMEOUT_MS; + const [stalePending] = await db.executeSql( + `SELECT sessionId FROM ${TABLE_NAME} WHERE status = ? AND timestamp <= ?`, + [ProofStatus.PENDING, staleTimestamp], + ); + + // Improved error handling - wrap each setProofStatus call in try-catch + let successfulUpdates = 0; + let failedUpdates = 0; + + for (let i = 0; i < stalePending.rows.length; i++) { + const { sessionId } = stalePending.rows.item(i); + try { + await setProofStatus(sessionId, ProofStatus.FAILURE); + successfulUpdates++; + } catch (error) { + console.error( + `Failed to update proof status for session ${sessionId}:`, + error, + ); + failedUpdates++; + // Continue with the next iteration instead of stopping the entire loop + } + } + + if (stalePending.rows.length > 0) { + console.log( + `Stale proof cleanup: ${successfulUpdates} successful, ${failedUpdates} failed`, + ); + } + }, + getPendingProofs: async (): Promise<ProofDBResult> => { + const db = await openDatabase(); + + const [pendingProofs] = await db.executeSql(` + SELECT * FROM ${TABLE_NAME} WHERE status = '${ProofStatus.PENDING}' + `); + + return { + rows: pendingProofs.rows.raw(), + total_count: pendingProofs.rows.item(0)?.total_count, + }; + }, + getHistory: async (page: number = 1): Promise<ProofDBResult> => { + const db = await openDatabase(); + const offset = (page - 1) * PAGE_SIZE; + + const [results] = await db.executeSql( + `WITH data AS ( + SELECT *, COUNT(*) OVER() as total_count + FROM ${TABLE_NAME} + ORDER BY timestamp DESC + LIMIT ? OFFSET ? + ) + SELECT * FROM data`, + [PAGE_SIZE, offset], + ); + return { + rows: results.rows.raw(), + total_count: results.rows.item(0)?.total_count, + }; + }, + init: async () => { + const db = await openDatabase(); + await db.executeSql(` + CREATE TABLE IF NOT EXISTS ${TABLE_NAME} ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + appName TEXT NOT NULL, + sessionId TEXT NOT NULL UNIQUE, + userId TEXT NOT NULL, + userIdType TEXT NOT NULL, + endpointType TEXT NOT NULL, + status TEXT NOT NULL, + errorCode TEXT, + errorReason TEXT, + timestamp INTEGER NOT NULL, + disclosures TEXT NOT NULL, + logoBase64 TEXT + ) + `); + + await db.executeSql(` + CREATE INDEX IF NOT EXISTS idx_proof_history_timestamp ON ${TABLE_NAME} (timestamp) + `); + }, + async insertProof(proof: Omit<ProofHistory, 'id' | 'timestamp'>) { + const db = await openDatabase(); + const timestamp = Date.now(); + + const [insertResult] = await db.executeSql( + `INSERT OR IGNORE INTO ${TABLE_NAME} (appName, endpointType, status, errorCode, errorReason, timestamp, disclosures, logoBase64, userId, userIdType, sessionId) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [ + proof.appName, + proof.endpointType, + proof.status, + proof.errorCode || null, + proof.errorReason || null, + timestamp, + proof.disclosures, + proof.logoBase64 || null, + proof.userId, + proof.userIdType, + proof.sessionId, + ], + ); + return { + id: insertResult.insertId.toString(), + timestamp, + rowsAffected: insertResult.rowsAffected, + }; + }, + async updateProofStatus( + status: ProofStatus, + errorCode: string | undefined, + errorReason: string | undefined, + sessionId: string, + ) { + const db = await openDatabase(); + await db.executeSql( + ` + UPDATE ${TABLE_NAME} SET status = ?, errorCode = ?, errorReason = ? WHERE sessionId = ? + `, + [status, errorCode, errorReason, sessionId], + ); + }, +}; diff --git a/app/src/stores/database.web.ts b/app/src/stores/database.web.ts new file mode 100644 index 000000000..2bbd192fd --- /dev/null +++ b/app/src/stores/database.web.ts @@ -0,0 +1,256 @@ +// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11 + +import { + ProofDB, + ProofDBResult, + ProofHistory, + ProofStatus, +} from './proof-types'; + +export const DB_NAME = 'proof_history_db'; +const STORE_NAME = 'proof_history'; +const DB_VERSION = 1; +const PAGE_SIZE = 20; + +class IndexedDBDatabase implements ProofDB { + private db: IDBDatabase | null = null; + + private async openDatabase(): Promise<IDBDatabase> { + if (this.db) { + return this.db; + } + + return new Promise((resolve, reject) => { + const request = indexedDB.open(DB_NAME, DB_VERSION); + + request.onerror = () => reject(request.error); + request.onsuccess = () => { + this.db = request.result; + resolve(request.result); + }; + + request.onupgradeneeded = event => { + const db = (event.target as IDBOpenDBRequest).result; + + // Create the object store + if (!db.objectStoreNames.contains(STORE_NAME)) { + const store = db.createObjectStore(STORE_NAME, { + keyPath: 'id', + autoIncrement: true, + }); + + // Create indexes + store.createIndex('sessionId', 'sessionId', { unique: true }); + store.createIndex('status', 'status', { unique: false }); + store.createIndex('timestamp', 'timestamp', { unique: false }); + } + }; + }); + } + + async updateStaleProofs( + setProofStatus: (id: string, status: ProofStatus) => Promise<void>, + ): Promise<void> { + const db = await this.openDatabase(); + + const staleTimestamp = Date.now() - 10 * 60 * 1000; // 10 minutes + + return new Promise((resolve, reject) => { + const transaction = db.transaction([STORE_NAME], 'readonly'); + const store = transaction.objectStore(STORE_NAME); + const statusIndex = store.index('status'); + const request = statusIndex.getAll(ProofStatus.PENDING); + + request.onerror = () => reject(request.error); + request.onsuccess = async () => { + const staleProofs = request.result.filter( + proof => proof.timestamp <= staleTimestamp, + ); + + for (const proof of staleProofs) { + try { + await setProofStatus(proof.sessionId, ProofStatus.FAILURE); + } catch (error) { + console.error( + `Failed to update proof status for session ${proof.sessionId}:`, + error, + ); + } + } + resolve(); + }; + }); + } + + async getPendingProofs(): Promise<ProofDBResult> { + const db = await this.openDatabase(); + + return new Promise((resolve, reject) => { + const transaction = db.transaction([STORE_NAME], 'readonly'); + const store = transaction.objectStore(STORE_NAME); + const statusIndex = store.index('status'); + const request = statusIndex.getAll(ProofStatus.PENDING); + + request.onerror = () => reject(request.error); + request.onsuccess = () => { + resolve({ rows: request.result }); + }; + }); + } + + async getHistory(page: number = 1): Promise<ProofDBResult> { + const db = await this.openDatabase(); + const offset = (page - 1) * PAGE_SIZE; + + return new Promise((resolve, reject) => { + const transaction = db.transaction([STORE_NAME], 'readonly'); + const store = transaction.objectStore(STORE_NAME); + const timestampIndex = store.index('timestamp'); + + // Get total count + const countRequest = store.count(); + countRequest.onerror = () => reject(countRequest.error); + + countRequest.onsuccess = () => { + const totalCount = countRequest.result; + + // Get paginated results (IndexedDB doesn't have OFFSET, so we need to handle pagination manually) + const request = timestampIndex.openCursor(null, 'prev'); + const results: ProofHistory[] = []; + let skipped = 0; + let returned = 0; + + request.onerror = () => reject(request.error); + request.onsuccess = event => { + const cursor = (event.target as IDBRequest).result; + + if (cursor) { + if (skipped < offset) { + skipped++; + cursor.continue(); + } else if (returned < PAGE_SIZE) { + results.push(cursor.value); + returned++; + cursor.continue(); + } else { + // Add total count to the first result for compatibility + const resultWithCount = results.map((item, index) => + index === 0 ? { ...item, total_count: totalCount } : item, + ); + resolve({ rows: resultWithCount, total_count: totalCount }); + } + } else { + // Add total count to the first result for compatibility + const resultWithCount = results.map((item, index) => + index === 0 ? { ...item, total_count: totalCount } : item, + ); + resolve({ rows: resultWithCount, total_count: totalCount }); + } + }; + }; + }); + } + + async init(): Promise<void> { + // Database initialization is handled in openDatabase + await this.openDatabase(); + } + + async insertProof( + proof: Omit<ProofHistory, 'id' | 'timestamp'>, + ): Promise<{ id: string; timestamp: number; rowsAffected: number }> { + const db = await this.openDatabase(); + const timestamp = Date.now(); + + return new Promise((resolve, reject) => { + const transaction = db.transaction([STORE_NAME], 'readwrite'); + const store = transaction.objectStore(STORE_NAME); + + const proofWithId = { + ...proof, + timestamp, + }; + + const request = store.add(proofWithId); + + request.onerror = () => { + // Handle unique constraint violation for sessionId + if (request.error?.name === 'ConstraintError') { + // Find existing record by sessionId and update it + const sessionIdIndex = store.index('sessionId'); + const getRequest = sessionIdIndex.get(proof.sessionId); + getRequest.onsuccess = () => { + const existing = getRequest.result; + if (existing) { + const updateRequest = store.put({ + ...existing, + ...proofWithId, + id: existing.id, // Preserve the original ID + }); + updateRequest.onerror = () => reject(updateRequest.error); + updateRequest.onsuccess = () => { + resolve({ + id: existing.id.toString(), + timestamp, + rowsAffected: 1, + }); + }; + } else { + reject(new Error('Constraint error but record not found')); + } + }; + getRequest.onerror = () => reject(getRequest.error); + } else { + reject(request.error); + } + }; + + request.onsuccess = () => { + resolve({ + id: request.result?.toString() || '', + timestamp, + rowsAffected: 1, + }); + }; + }); + } + + async updateProofStatus( + status: ProofStatus, + errorCode: string | undefined, + errorReason: string | undefined, + sessionId: string, + ): Promise<void> { + const db = await this.openDatabase(); + + return new Promise((resolve, reject) => { + const transaction = db.transaction([STORE_NAME], 'readwrite'); + const store = transaction.objectStore(STORE_NAME); + const sessionIdIndex = store.index('sessionId'); + + // First find the record by sessionId + const getRequest = sessionIdIndex.get(sessionId); + + getRequest.onerror = () => reject(getRequest.error); + getRequest.onsuccess = () => { + const existingProof = getRequest.result; + if (existingProof) { + const updatedProof = { + ...existingProof, + status, + errorCode, + errorReason, + }; + + const updateRequest = store.put(updatedProof); + updateRequest.onerror = () => reject(updateRequest.error); + updateRequest.onsuccess = () => resolve(); + } else { + resolve(); // No record found, nothing to update + } + }; + }); + } +} + +export const database: ProofDB = new IndexedDBDatabase(); diff --git a/app/src/stores/proof-types.ts b/app/src/stores/proof-types.ts new file mode 100644 index 000000000..95d9314f4 --- /dev/null +++ b/app/src/stores/proof-types.ts @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11 + +import { type EndpointType, UserIdType } from '@selfxyz/common'; + +export interface ProofHistory { + id: string; + appName: string; + sessionId: string; + userId: string; + userIdType: UserIdType; + endpointType: EndpointType; + status: ProofStatus; + errorCode?: string; + errorReason?: string; + timestamp: number; + disclosures: string; + logoBase64?: string; +} + +export enum ProofStatus { + PENDING = 'pending', + SUCCESS = 'success', + FAILURE = 'failure', +} + +export interface ProofDBResult { + rows: ProofHistory[]; + rowsAffected?: number; + insertId?: string; + total_count?: number; +} + +export interface ProofDB { + updateStaleProofs: ( + updateProofStatus: (id: string, status: ProofStatus) => Promise<void>, + ) => Promise<void>; + getPendingProofs: () => Promise<ProofDBResult>; + getHistory: (page?: number) => Promise<ProofDBResult>; + init: () => Promise<void>; + insertProof: ( + proof: Omit<ProofHistory, 'id' | 'timestamp'>, + ) => Promise<{ id: string; timestamp: number; rowsAffected: number }>; + updateProofStatus: ( + status: ProofStatus, + errorCode: string | undefined, + errorReason: string | undefined, + sessionId: string, + ) => Promise<void>; +} diff --git a/app/src/stores/proofHistoryStore.ts b/app/src/stores/proofHistoryStore.ts index b3dfd9d8d..54dd00537 100644 --- a/app/src/stores/proofHistoryStore.ts +++ b/app/src/stores/proofHistoryStore.ts @@ -1,35 +1,11 @@ // SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11 -import type { EndpointType } from '@selfxyz/common'; import { WS_DB_RELAYER } from '@selfxyz/common'; -import { UserIdType } from '@selfxyz/common'; -import { Platform } from 'react-native'; -import SQLite from 'react-native-sqlite-storage'; import { io } from 'socket.io-client'; import { create } from 'zustand'; -SQLite.enablePromise(true); - -export interface ProofHistory { - id: string; - appName: string; - sessionId: string; - userId: string; - userIdType: UserIdType; - endpointType: EndpointType; - status: ProofStatus; - errorCode?: string; - errorReason?: string; - timestamp: number; - disclosures: string; - logoBase64?: string; -} - -export enum ProofStatus { - PENDING = 'pending', - SUCCESS = 'success', - FAILURE = 'failure', -} +import { database } from './database'; +import { ProofHistory, ProofStatus } from './proof-types'; interface ProofHistoryState { proofHistory: ProofHistory[]; @@ -50,10 +26,6 @@ interface ProofHistoryState { resetHistory: () => void; } -const PAGE_SIZE = 20; -const DB_NAME = Platform.OS === 'ios' ? 'proof_history.db' : 'proof_history.db'; -const TABLE_NAME = 'proof_history'; -const STALE_PROOF_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes const SYNC_THROTTLE_MS = 30 * 1000; // 30 seconds throttle for sync calls export const useProofHistoryStore = create<ProofHistoryState>()((set, get) => { @@ -70,45 +42,10 @@ export const useProofHistoryStore = create<ProofHistoryState>()((set, get) => { lastSyncTime = now; set({ isLoading: true }); - const db = await SQLite.openDatabase({ - name: DB_NAME, - location: 'default', - }); - const tenMinutesAgo = Date.now() - STALE_PROOF_TIMEOUT_MS; - const [stalePending] = await db.executeSql( - `SELECT sessionId FROM ${TABLE_NAME} WHERE status = ? AND timestamp <= ?`, - [ProofStatus.PENDING, tenMinutesAgo], - ); + await database.updateStaleProofs(get().updateProofStatus); - // Improved error handling - wrap each updateProofStatus call in try-catch - let successfulUpdates = 0; - let failedUpdates = 0; - - for (let i = 0; i < stalePending.rows.length; i++) { - const { sessionId } = stalePending.rows.item(i); - try { - await get().updateProofStatus(sessionId, ProofStatus.FAILURE); - successfulUpdates++; - } catch (error) { - console.error( - `Failed to update proof status for session ${sessionId}:`, - error, - ); - failedUpdates++; - // Continue with the next iteration instead of stopping the entire loop - } - } - - if (stalePending.rows.length > 0) { - console.log( - `Stale proof cleanup: ${successfulUpdates} successful, ${failedUpdates} failed`, - ); - } - - const [pendingProofs] = await db.executeSql(` - SELECT * FROM ${TABLE_NAME} WHERE status = '${ProofStatus.PENDING}' - `); + const pendingProofs = await database.getPendingProofs(); if (pendingProofs.rows.length === 0) { console.log('No pending proofs to sync'); @@ -119,13 +56,18 @@ export const useProofHistoryStore = create<ProofHistoryState>()((set, get) => { path: '/', transports: ['websocket'], }); + setTimeout(() => { + websocket.connected && websocket.disconnect(); + console.log('WebSocket disconnected after timeout'); + // disconnect after 2 minutes + }, SYNC_THROTTLE_MS * 4); for (let i = 0; i < pendingProofs.rows.length; i++) { - const proof = pendingProofs.rows.item(i); + const proof = pendingProofs.rows[i]; websocket.emit('subscribe', proof.sessionId); } - websocket.on('status', message => { + websocket.timeout(SYNC_THROTTLE_MS * 3).on('status', message => { const data = typeof message === 'string' ? JSON.parse(message) : message; @@ -139,6 +81,7 @@ export const useProofHistoryStore = create<ProofHistoryState>()((set, get) => { console.log('Failed to verify proof'); get().updateProofStatus(data.request_id, ProofStatus.FAILURE); } + websocket.emit('unsubscribe', data.request_id); }); } catch (error) { console.error('Error syncing proof status', error); @@ -155,31 +98,7 @@ export const useProofHistoryStore = create<ProofHistoryState>()((set, get) => { initDatabase: async () => { try { - const db = await SQLite.openDatabase({ - name: DB_NAME, - location: 'default', - }); - - await db.executeSql(` - CREATE TABLE IF NOT EXISTS ${TABLE_NAME} ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - appName TEXT NOT NULL, - sessionId TEXT NOT NULL UNIQUE, - userId TEXT NOT NULL, - userIdType TEXT NOT NULL, - endpointType TEXT NOT NULL, - status TEXT NOT NULL, - errorCode TEXT, - errorReason TEXT, - timestamp INTEGER NOT NULL, - disclosures TEXT NOT NULL, - logoBase64 TEXT - ) - `); - - await db.executeSql(` - CREATE INDEX IF NOT EXISTS idx_proof_history_timestamp ON ${TABLE_NAME} (timestamp) - `); + await database.init(); // Load initial data const state = get(); @@ -196,33 +115,10 @@ export const useProofHistoryStore = create<ProofHistoryState>()((set, get) => { addProofHistory: async proof => { try { - const db = await SQLite.openDatabase({ - name: DB_NAME, - location: 'default', - }); + const insertResult = await database.insertProof(proof); - const timestamp = Date.now(); - - const [insertResult] = await db.executeSql( - `INSERT OR IGNORE INTO ${TABLE_NAME} (appName, endpointType, status, errorCode, errorReason, timestamp, disclosures, logoBase64, userId, userIdType, sessionId) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, - [ - proof.appName, - proof.endpointType, - proof.status, - proof.errorCode || null, - proof.errorReason || null, - timestamp, - proof.disclosures, - proof.logoBase64 || null, - proof.userId, - proof.userIdType, - proof.sessionId, - ], - ); - - if (insertResult.rowsAffected > 0 && insertResult.insertId) { - const id = insertResult.insertId.toString(); + if (insertResult.rowsAffected > 0 && insertResult.id) { + const { id, timestamp } = insertResult; set(state => ({ proofHistory: [ { @@ -242,17 +138,12 @@ export const useProofHistoryStore = create<ProofHistoryState>()((set, get) => { updateProofStatus: async (sessionId, status, errorCode, errorReason) => { try { - const db = await SQLite.openDatabase({ - name: DB_NAME, - location: 'default', - }); - await db.executeSql( - ` - UPDATE ${TABLE_NAME} SET status = ?, errorCode = ?, errorReason = ? WHERE sessionId = ? - `, - [status, errorCode, errorReason, sessionId], + await database.updateProofStatus( + status, + errorCode, + errorReason, + sessionId, ); - // Update the status in the state set(state => ({ proofHistory: state.proofHistory.map(proof => @@ -273,29 +164,12 @@ export const useProofHistoryStore = create<ProofHistoryState>()((set, get) => { set({ isLoading: true }); try { - const db = await SQLite.openDatabase({ - name: DB_NAME, - location: 'default', - }); - const offset = (state.currentPage - 1) * PAGE_SIZE; - - const [results] = await db.executeSql( - `WITH data AS ( - SELECT *, COUNT(*) OVER() as total_count - FROM ${TABLE_NAME} - ORDER BY timestamp DESC - LIMIT ? OFFSET ? - ) - SELECT * FROM data`, - [PAGE_SIZE, offset], - ); + const results = await database.getHistory(state.currentPage); const proofs: ProofHistory[] = []; - let totalCount = 0; - + let totalCount = results.total_count || 0; for (let i = 0; i < results.rows.length; i++) { - const row = results.rows.item(i); - totalCount = row.total_count; // same for all rows + const row = results.rows[i]; proofs.push({ id: row.id.toString(), sessionId: row.sessionId, diff --git a/app/src/utils/deeplinks.ts b/app/src/utils/deeplinks.ts index cdabfb9e3..6bc20c543 100644 --- a/app/src/utils/deeplinks.ts +++ b/app/src/utils/deeplinks.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11 import queryString from 'query-string'; -import { Linking } from 'react-native'; +import { Linking, Platform } from 'react-native'; import { navigationRef } from '../navigation'; import { useSelfAppStore } from '../stores/selfAppStore'; @@ -75,6 +75,9 @@ export const handleUrl = (uri: string) => { } navigationRef.navigate('QRCodeTrouble'); } + } else if (Platform.OS === 'web') { + // TODO: web handle links if we need to idk if we do + // For web, we can handle the URL some other way if we dont do this loading app in web always navigates to QRCodeTrouble } else { if (typeof __DEV__ !== 'undefined' && __DEV__) { console.error('No sessionId or selfApp found in the data'); diff --git a/app/src/utils/haptic.ts b/app/src/utils/haptic/index.ts similarity index 74% rename from app/src/utils/haptic.ts rename to app/src/utils/haptic/index.ts index 061e09e32..8b19ff9bb 100644 --- a/app/src/utils/haptic.ts +++ b/app/src/utils/haptic/index.ts @@ -1,30 +1,8 @@ // SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11 import { Platform, Vibration } from 'react-native'; -import ReactNativeHapticFeedback from 'react-native-haptic-feedback'; -export type HapticType = - | 'selection' - | 'impactLight' - | 'impactMedium' - | 'impactHeavy' - | 'notificationSuccess' - | 'notificationWarning' - | 'notificationError'; - -export type HapticOptions = { - enableVibrateFallback?: boolean; - ignoreAndroidSystemSettings?: boolean; - pattern?: number[]; - increaseIosIntensity?: boolean; -}; - -const defaultOptions: HapticOptions = { - enableVibrateFallback: true, - ignoreAndroidSystemSettings: false, - pattern: [50, 100, 50], - increaseIosIntensity: true, -}; +import { triggerFeedback } from './trigger'; // Keep track of the loading screen interval let loadingScreenInterval: NodeJS.Timeout | null = null; @@ -181,34 +159,4 @@ export const feedbackUnsuccessful = () => { }, 1000); }; -/** - * Triggers haptic feedback or vibration based on platform. - * @param type - The haptic feedback type. - * @param options - Custom options (optional). - */ -export const triggerFeedback = ( - type: HapticType | 'custom', - options: HapticOptions = {}, -) => { - const mergedOptions = { ...defaultOptions, ...options }; - if (Platform.OS === 'ios' && type !== 'custom') { - if (mergedOptions.increaseIosIntensity) { - if (type === 'impactLight') { - type = 'impactMedium'; - } else if (type === 'impactMedium') { - type = 'impactHeavy'; - } - } - - ReactNativeHapticFeedback.trigger(type, { - enableVibrateFallback: mergedOptions.enableVibrateFallback, - ignoreAndroidSystemSettings: mergedOptions.ignoreAndroidSystemSettings, - }); - } else { - if (mergedOptions.pattern) { - Vibration.vibrate(mergedOptions.pattern, false); - } else { - Vibration.vibrate(100); - } - } -}; +export { triggerFeedback } from './trigger'; diff --git a/app/src/utils/haptic/shared.ts b/app/src/utils/haptic/shared.ts new file mode 100644 index 000000000..5ba016650 --- /dev/null +++ b/app/src/utils/haptic/shared.ts @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11 + +export type HapticType = + | 'selection' + | 'impactLight' + | 'impactMedium' + | 'impactHeavy' + | 'notificationSuccess' + | 'notificationWarning' + | 'notificationError'; + +export type HapticOptions = { + enableVibrateFallback?: boolean; + ignoreAndroidSystemSettings?: boolean; + pattern?: number[]; + increaseIosIntensity?: boolean; +}; + +export const defaultOptions: HapticOptions = { + enableVibrateFallback: true, + ignoreAndroidSystemSettings: false, + pattern: [50, 100, 50], + increaseIosIntensity: true, +}; diff --git a/app/src/utils/haptic/trigger.ts b/app/src/utils/haptic/trigger.ts new file mode 100644 index 000000000..4938800cb --- /dev/null +++ b/app/src/utils/haptic/trigger.ts @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11 + +import { Platform, Vibration } from 'react-native'; +import ReactNativeHapticFeedback from 'react-native-haptic-feedback'; + +import { defaultOptions, HapticOptions, HapticType } from './shared'; +/** + * Triggers haptic feedback or vibration based on platform. + * @param type - The haptic feedback type. + * @param options - Custom options (optional). + */ +export const triggerFeedback = ( + type: HapticType | 'custom', + options: HapticOptions = {}, +) => { + const mergedOptions = { ...defaultOptions, ...options }; + if (Platform.OS === 'ios' && type !== 'custom') { + if (mergedOptions.increaseIosIntensity) { + if (type === 'impactLight') { + type = 'impactMedium'; + } else if (type === 'impactMedium') { + type = 'impactHeavy'; + } + } + + ReactNativeHapticFeedback.trigger(type, { + enableVibrateFallback: mergedOptions.enableVibrateFallback, + ignoreAndroidSystemSettings: mergedOptions.ignoreAndroidSystemSettings, + }); + } else { + if (mergedOptions.pattern) { + Vibration.vibrate(mergedOptions.pattern, false); + } else { + Vibration.vibrate(100); + } + } +}; diff --git a/app/src/utils/haptic/trigger.web.ts b/app/src/utils/haptic/trigger.web.ts new file mode 100644 index 000000000..1d1a5bb0f --- /dev/null +++ b/app/src/utils/haptic/trigger.web.ts @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11 + +import { defaultOptions, HapticOptions, HapticType } from './shared'; + +/** + * Triggers haptic feedback or vibration based on platform. + * @param type - The haptic feedback type. (only here for compatibility, not used in web) + * @param options - Custom options (optional). + */ +export const triggerFeedback = ( + _type: HapticType | 'custom', + options: HapticOptions = {}, +) => { + const mergedOptions = { ...defaultOptions, ...options }; + + // Check if Vibration API is available + if (!navigator.vibrate) { + console.warn('Vibration API not supported in this browser'); + return; + } + + if (mergedOptions.pattern) { + navigator.vibrate(mergedOptions.pattern); + } else { + navigator.vibrate(100); + } +}; diff --git a/app/src/utils/locale.ts b/app/src/utils/locale.ts new file mode 100644 index 000000000..caea57a57 --- /dev/null +++ b/app/src/utils/locale.ts @@ -0,0 +1,3 @@ +// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11 + +export { getCountry, getLocales, getTimeZone } from 'react-native-localize'; diff --git a/app/src/utils/locale.web.ts b/app/src/utils/locale.web.ts new file mode 100644 index 000000000..adc8d9112 --- /dev/null +++ b/app/src/utils/locale.web.ts @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11 + +export function getCountry() { + const locale = new Intl.Locale(navigator.language); + return locale.region ?? ''; +} + +type Locale = { + languageCode: string; + scriptCode?: string; + countryCode: string; + languageTag: string; + isRTL: boolean; +}; + +export function getLocales(): Locale[] { + return navigator.languages.map(lang => { + const locale = new Intl.Locale(lang); + return { + languageCode: locale.language, + countryCode: locale.region ?? '', + scriptCode: locale.script, + languageTag: lang, + // @ts-expect-error this not in type but appears to be in browsers + isRTL: locale.textInfo?.direction === 'rtl', + }; + }); +} + +export function getTimeZone(): string { + return Intl.DateTimeFormat().resolvedOptions().timeZone; +} diff --git a/app/src/utils/notifications/notificationService.shared.ts b/app/src/utils/notifications/notificationService.shared.ts new file mode 100644 index 000000000..f33316e4f --- /dev/null +++ b/app/src/utils/notifications/notificationService.shared.ts @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11 + +export const getStateMessage = (state: string): string => { + switch (state) { + case 'idle': + return 'Getting ready...'; + case 'fetching_data': + return 'Fetching data...'; + case 'validating_document': + return 'Validating document...'; + case 'init_tee_connexion': + return 'Preparing secure environment...'; + case 'ready_to_prove': + return 'Ready to prove...'; + case 'proving': + return 'Generating proof...'; + case 'post_proving': + return 'Finalizing...'; + case 'completed': + return 'Verification completed!'; + case 'error': + return 'Error occurred'; + case 'passport_not_supported': + return 'Passport not supported'; + case 'account_recovery_choice': + return 'Account recovery needed'; + case 'passport_data_not_found': + return 'Passport data not found'; + case 'failure': + return 'Verification failed'; + default: + return 'Processing...'; + } +}; + +export interface RemoteMessage { + messageId?: string; + data?: { [key: string]: string | object }; + notification?: { + title?: string; + body?: string; + }; + [key: string]: any; +} + +export interface DeviceTokenRegistration { + session_id: string; + device_token: string; + platform: 'ios' | 'android' | 'web'; +} + +export const API_URL = 'https://notification.self.xyz'; +export const API_URL_STAGING = 'https://notification.staging.self.xyz'; diff --git a/app/src/utils/notifications/notificationService.ts b/app/src/utils/notifications/notificationService.ts index 72222ecc5..d97b867a2 100644 --- a/app/src/utils/notifications/notificationService.ts +++ b/app/src/utils/notifications/notificationService.ts @@ -3,9 +3,15 @@ import messaging from '@react-native-firebase/messaging'; import { PermissionsAndroid, Platform } from 'react-native'; -const API_URL = 'https://notification.self.xyz'; -const API_URL_STAGING = 'https://notification.staging.self.xyz'; +import { + API_URL, + API_URL_STAGING, + DeviceTokenRegistration, + getStateMessage, + RemoteMessage, +} from './notificationService.shared'; +export { getStateMessage }; // Determine if running in test environment const isTestEnv = process.env.NODE_ENV === 'test'; const log = (...args: any[]) => { @@ -15,39 +21,6 @@ const error = (...args: any[]) => { if (!isTestEnv) console.error(...args); }; -export const getStateMessage = (state: string): string => { - switch (state) { - case 'idle': - return 'Getting ready...'; - case 'fetching_data': - return 'Fetching data...'; - case 'validating_document': - return 'Validating document...'; - case 'init_tee_connexion': - return 'Preparing secure environment...'; - case 'ready_to_prove': - return 'Ready to prove...'; - case 'proving': - return 'Generating proof...'; - case 'post_proving': - return 'Finalizing...'; - case 'completed': - return 'Verification completed!'; - case 'error': - return 'Error occurred'; - case 'passport_not_supported': - return 'Passport not supported'; - case 'account_recovery_choice': - return 'Account recovery needed'; - case 'passport_data_not_found': - return 'Passport data not found'; - case 'failure': - return 'Verification failed'; - default: - return 'Processing...'; - } -}; - export async function requestNotificationPermission(): Promise<boolean> { try { if (Platform.OS === 'android') { @@ -61,7 +34,6 @@ export async function requestNotificationPermission(): Promise<boolean> { } } } - const authStatus = await messaging().requestPermission(); const enabled = authStatus === messaging.AuthorizationStatus.AUTHORIZED || @@ -108,7 +80,7 @@ export async function registerDeviceToken( const cleanedToken = token.trim(); const baseUrl = isMockPassport ? API_URL_STAGING : API_URL; - const deviceTokenRegistration = { + const deviceTokenRegistration: DeviceTokenRegistration = { session_id: sessionId, device_token: cleanedToken, platform: Platform.OS === 'ios' ? 'ios' : 'android', @@ -143,16 +115,6 @@ export async function registerDeviceToken( } } -export interface RemoteMessage { - messageId?: string; - data?: { [key: string]: string | object }; - notification?: { - title?: string; - body?: string; - }; - [key: string]: any; -} - export function setupNotifications(): () => void { messaging().setBackgroundMessageHandler( async (remoteMessage: RemoteMessage) => { diff --git a/app/src/utils/notifications/notificationService.web.ts b/app/src/utils/notifications/notificationService.web.ts new file mode 100644 index 000000000..a66d95a02 --- /dev/null +++ b/app/src/utils/notifications/notificationService.web.ts @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11 + +import { + API_URL, + API_URL_STAGING, + DeviceTokenRegistration, + getStateMessage, +} from './notificationService.shared'; + +// TODO: web handle notifications better. this file is more or less a fancy placeholder + +export { getStateMessage }; + +export async function requestNotificationPermission(): Promise<boolean> { + try { + if (!('Notification' in window)) { + console.log('This browser does not support notifications'); + return false; + } + + if (Notification.permission === 'granted') { + console.log('Notification permission already granted'); + return true; + } + + if (Notification.permission === 'denied') { + console.log('Notification permission denied'); + return false; + } + + const permission = await Notification.requestPermission(); + const enabled = permission === 'granted'; + + console.log('Notification permission status:', enabled); + return enabled; + } catch (error) { + console.error('Failed to request notification permission:', error); + return false; + } +} + +export async function getFCMToken(): Promise<string | null> { + try { + // For web, we'll generate a simple token or use a service worker registration + // In a real implementation, you might want to use Firebase Web SDK or a custom solution + if ('serviceWorker' in navigator) { + const registration = await navigator.serviceWorker + .register('/sw.js') + .catch(() => null); + if (registration) { + // Generate a simple token based on registration + const token = `web_${registration.active?.scriptURL || Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + console.log('Web FCM Token generated'); + return token; + } + } + + // Fallback: generate a simple token + const token = `web_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + console.log('Web FCM Token generated (fallback)'); + return token; + } catch (error) { + console.error('Failed to get FCM token:', error); + return null; + } +} + +export async function registerDeviceToken( + sessionId: string, + deviceToken?: string, + isMockPassport?: boolean, +): Promise<void> { + try { + let token = deviceToken; + if (!token) { + const fcmToken = await getFCMToken(); + if (!fcmToken) { + console.log('No FCM token available'); + return; + } + token = fcmToken; + } + + const cleanedToken = token.trim(); + const baseUrl = isMockPassport ? API_URL_STAGING : API_URL; + + const deviceTokenRegistration: DeviceTokenRegistration = { + session_id: sessionId, + device_token: cleanedToken, + platform: 'web', + }; + + if (cleanedToken.length > 10) { + console.log( + 'Registering device token:', + `${cleanedToken.substring(0, 5)}...${cleanedToken.substring( + cleanedToken.length - 5, + )}`, + ); + } + + const response = await fetch(`${baseUrl}/register-token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify(deviceTokenRegistration), + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error( + 'Failed to register device token:', + response.status, + errorText, + ); + } else { + console.log( + 'Device token registered successfully with session_id:', + sessionId, + ); + } + } catch (error) { + console.error('Error registering device token:', error); + } +} + +export function setupNotifications(): () => void { + // For web, we'll set up service worker for background notifications + // and handle foreground notifications directly + + if ('serviceWorker' in navigator) { + navigator.serviceWorker.register('/sw.js').catch(error => { + console.error('Service Worker registration failed:', error); + }); + } + + // For web, we don't have a direct equivalent to Firebase messaging + // You might want to implement WebSocket or Server-Sent Events for real-time notifications + // For now, we'll return a no-op unsubscribe function + return () => { + console.log('Web notification service cleanup'); + }; +} diff --git a/app/tests/__setup__/databaseMocks.ts b/app/tests/__setup__/databaseMocks.ts new file mode 100644 index 000000000..595483fb8 --- /dev/null +++ b/app/tests/__setup__/databaseMocks.ts @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11 + +/* global jest */ + +// Mock for react-native-sqlite-storage +export const SQLite = { + enablePromise: jest.fn(), + openDatabase: jest.fn(), +}; diff --git a/app/tests/src/stores/database.test.ts b/app/tests/src/stores/database.test.ts new file mode 100644 index 000000000..a10f04a6c --- /dev/null +++ b/app/tests/src/stores/database.test.ts @@ -0,0 +1,443 @@ +// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11 + +import SQLite from 'react-native-sqlite-storage'; + +import { database } from '../../../src/stores/database'; +import { ProofStatus } from '../../../src/stores/proof-types'; + +// Mock react-native-sqlite-storage +jest.mock('react-native-sqlite-storage', () => ({ + enablePromise: jest.fn(), + openDatabase: jest.fn(), +})); + +const mockSQLite = SQLite as any; + +describe('database (SQLite)', () => { + let mockDb: any; + + beforeEach(() => { + // Reset all mocks + jest.clearAllMocks(); + + // Create mock database + mockDb = { + executeSql: jest.fn(() => Promise.resolve()), + }; + + mockSQLite.openDatabase.mockResolvedValue(mockDb); + }); + + describe('init', () => { + it('creates table and index if they do not exist', async () => { + await database.init(); + + expect(mockSQLite.openDatabase).toHaveBeenCalledWith({ + name: 'proof_history.db', + location: 'default', + }); + + expect(mockDb.executeSql).toHaveBeenCalledTimes(2); + + // Check table creation + expect(mockDb.executeSql).toHaveBeenCalledWith( + expect.stringContaining(`CREATE TABLE IF NOT EXISTS proof_history`), + ); + + // Check index creation + expect(mockDb.executeSql).toHaveBeenCalledWith( + expect.stringContaining( + 'CREATE INDEX IF NOT EXISTS idx_proof_history_timestamp', + ), + ); + }); + + it('handles initialization errors gracefully', async () => { + const initError = new Error('Table creation failed'); + mockDb.executeSql.mockRejectedValueOnce(initError); + + await expect(database.init()).rejects.toThrow('Table creation failed'); + expect(mockDb.executeSql).toHaveBeenCalledWith( + expect.stringContaining('CREATE TABLE IF NOT EXISTS proof_history'), + ); + }); + }); + + describe('insertProof', () => { + it('inserts a new proof successfully', async () => { + const mockProof = { + appName: 'TestApp', + sessionId: 'session-123', + userId: 'user-456', + userIdType: 'uuid' as const, + endpointType: 'https' as const, + status: ProofStatus.PENDING, + disclosures: '{"test": "data"}', + logoBase64: 'base64-logo', + }; + + const mockInsertResult = { + insertId: 1, + rowsAffected: 1, + }; + + mockDb.executeSql.mockResolvedValueOnce([mockInsertResult]); + + const result = await database.insertProof(mockProof); + + expect(mockDb.executeSql).toHaveBeenCalledWith( + expect.stringContaining('INSERT OR IGNORE INTO proof_history'), + [ + mockProof.appName, + mockProof.endpointType, + mockProof.status, + null, // errorCode + null, // errorReason + expect.any(Number), // timestamp + mockProof.disclosures, + mockProof.logoBase64, + mockProof.userId, + mockProof.userIdType, + mockProof.sessionId, + ], + ); + + expect(result).toEqual({ + id: '1', + timestamp: expect.any(Number), + rowsAffected: 1, + }); + }); + + it('handles proof with error information', async () => { + const mockProof = { + appName: 'TestApp', + sessionId: 'session-123', + userId: 'user-456', + userIdType: 'uuid' as const, + endpointType: 'https' as const, + status: ProofStatus.FAILURE, + errorCode: 'ERROR_001', + errorReason: 'Test error', + disclosures: '{"test": "data"}', + }; + + const mockInsertResult = { + insertId: 2, + rowsAffected: 1, + }; + + mockDb.executeSql.mockResolvedValueOnce([mockInsertResult]); + + const result = await database.insertProof(mockProof); + + expect(mockDb.executeSql).toHaveBeenCalledWith( + expect.stringContaining('INSERT OR IGNORE INTO proof_history'), + [ + mockProof.appName, + mockProof.endpointType, + mockProof.status, + mockProof.errorCode, + mockProof.errorReason, + expect.any(Number), + mockProof.disclosures, + null, // logoBase64 + mockProof.userId, + mockProof.userIdType, + mockProof.sessionId, + ], + ); + + expect(result).toEqual({ + id: '2', + timestamp: expect.any(Number), + rowsAffected: 1, + }); + }); + }); + + describe('updateProofStatus', () => { + it('updates proof status successfully', async () => { + const sessionId = 'session-123'; + const status = ProofStatus.SUCCESS; + const errorCode = 'SUCCESS_001'; + const errorReason = 'Operation completed'; + + mockDb.executeSql.mockResolvedValueOnce([{}]); + + await database.updateProofStatus( + status, + errorCode, + errorReason, + sessionId, + ); + + expect(mockDb.executeSql).toHaveBeenCalledWith( + expect.stringContaining( + 'UPDATE proof_history SET status = ?, errorCode = ?, errorReason = ? WHERE sessionId = ?', + ), + [status, errorCode, errorReason, sessionId], + ); + }); + + it('updates proof status with undefined error information', async () => { + const sessionId = 'session-123'; + const status = ProofStatus.SUCCESS; + + mockDb.executeSql.mockResolvedValueOnce([{}]); + + await database.updateProofStatus(status, undefined, undefined, sessionId); + + expect(mockDb.executeSql).toHaveBeenCalledWith( + expect.stringContaining( + 'UPDATE proof_history SET status = ?, errorCode = ?, errorReason = ? WHERE sessionId = ?', + ), + [status, undefined, undefined, sessionId], + ); + }); + }); + + describe('getPendingProofs', () => { + it('returns pending proofs successfully', async () => { + const mockRows = [ + { + id: 1, + appName: 'TestApp', + sessionId: 'session-123', + userId: 'user-456', + userIdType: 'email', + endpointType: 'register', + status: ProofStatus.PENDING, + timestamp: Date.now(), + disclosures: '{"test": "data"}', + }, + ]; + + const mockResult = { + rows: { + raw: jest.fn().mockReturnValue(mockRows), + item: jest.fn().mockReturnValue({ total_count: 1 }), + }, + }; + + mockDb.executeSql.mockResolvedValueOnce([mockResult]); + + const result = await database.getPendingProofs(); + + expect(mockDb.executeSql).toHaveBeenCalledWith( + expect.stringContaining( + "SELECT * FROM proof_history WHERE status = 'pending'", + ), + ); + + expect(result).toEqual({ + rows: mockRows, + total_count: 1, + }); + }); + + it('returns empty array when no pending proofs exist', async () => { + const mockResult = { + rows: { + raw: jest.fn().mockReturnValue([]), + item: jest.fn().mockReturnValue(undefined), + }, + }; + + mockDb.executeSql.mockResolvedValueOnce([mockResult]); + + const result = await database.getPendingProofs(); + + expect(result).toEqual({ + rows: [], + total_count: undefined, + }); + }); + }); + + describe('getHistory', () => { + it('returns paginated history successfully', async () => { + const mockRows = [ + { + id: 1, + appName: 'TestApp', + sessionId: 'session-123', + userId: 'user-456', + userIdType: 'email', + endpointType: 'register', + status: ProofStatus.SUCCESS, + timestamp: Date.now(), + disclosures: '{"test": "data"}', + total_count: 5, + }, + ]; + + const mockResult = { + rows: { + raw: jest.fn().mockReturnValue(mockRows), + item: jest.fn().mockReturnValue({ total_count: 5 }), + }, + }; + + mockDb.executeSql.mockResolvedValueOnce([mockResult]); + + const result = await database.getHistory(1); + + expect(mockDb.executeSql).toHaveBeenCalledWith( + expect.stringContaining('WITH data AS'), + [20, 0], // PAGE_SIZE, offset + ); + + expect(result).toEqual({ + rows: mockRows, + total_count: 5, + }); + }); + + it('handles second page correctly', async () => { + const mockResult = { + rows: { + raw: jest.fn().mockReturnValue([]), + item: jest.fn().mockReturnValue({ total_count: 5 }), + }, + }; + + mockDb.executeSql.mockResolvedValueOnce([mockResult]); + + await database.getHistory(2); + + expect(mockDb.executeSql).toHaveBeenCalledWith( + expect.stringContaining('WITH data AS'), + [20, 20], // PAGE_SIZE, offset for page 2 + ); + }); + + it('defaults to page 1 when no page is provided', async () => { + const mockResult = { + rows: { + raw: jest.fn().mockReturnValue([]), + item: jest.fn().mockReturnValue({ total_count: 0 }), + }, + }; + + mockDb.executeSql.mockResolvedValueOnce([mockResult]); + + await database.getHistory(); + + expect(mockDb.executeSql).toHaveBeenCalledWith( + expect.stringContaining('WITH data AS'), + [20, 0], // PAGE_SIZE, offset for page 1 + ); + }); + }); + + describe('updateStaleProofs', () => { + it('updates stale pending proofs to failure', async () => { + const mockSetProofStatus = jest.fn().mockResolvedValue(undefined); + + const mockStaleRows = [ + { sessionId: 'session-123' }, + { sessionId: 'session-456' }, + ]; + + const mockResult = { + rows: { + length: 2, + item: jest.fn((index: number) => mockStaleRows[index]), + }, + }; + + mockDb.executeSql.mockResolvedValueOnce([mockResult]); + + await database.updateStaleProofs(mockSetProofStatus); + + expect(mockDb.executeSql).toHaveBeenCalledWith( + expect.stringContaining( + 'SELECT sessionId FROM proof_history WHERE status = ? AND timestamp <= ?', + ), + [ProofStatus.PENDING, expect.any(Number)], + ); + + expect(mockSetProofStatus).toHaveBeenCalledTimes(2); + expect(mockSetProofStatus).toHaveBeenCalledWith( + 'session-123', + ProofStatus.FAILURE, + ); + expect(mockSetProofStatus).toHaveBeenCalledWith( + 'session-456', + ProofStatus.FAILURE, + ); + }); + + it('handles errors during status updates gracefully', async () => { + const mockSetProofStatus = jest + .fn() + .mockResolvedValueOnce(undefined) // First call succeeds + .mockRejectedValueOnce(new Error('Update failed')); // Second call fails + + const mockStaleRows = [ + { sessionId: 'session-123' }, + { sessionId: 'session-456' }, + ]; + + const mockResult = { + rows: { + length: 2, + item: jest.fn((index: number) => mockStaleRows[index]), + }, + }; + + mockDb.executeSql.mockResolvedValueOnce([mockResult]); + + // Should not throw error + await database.updateStaleProofs(mockSetProofStatus); + + expect(mockSetProofStatus).toHaveBeenCalledTimes(2); + expect(mockSetProofStatus).toHaveBeenCalledWith( + 'session-123', + ProofStatus.FAILURE, + ); + expect(mockSetProofStatus).toHaveBeenCalledWith( + 'session-456', + ProofStatus.FAILURE, + ); + }); + + it('handles no stale proofs gracefully', async () => { + const mockSetProofStatus = jest.fn().mockResolvedValue(undefined); + + const mockResult = { + rows: { + length: 0, + item: jest.fn(), + }, + }; + + mockDb.executeSql.mockResolvedValueOnce([mockResult]); + + await database.updateStaleProofs(mockSetProofStatus); + + expect(mockSetProofStatus).not.toHaveBeenCalled(); + }); + }); + + describe('error handling', () => { + it('handles database errors gracefully', async () => { + const dbError = new Error('Database connection failed'); + mockSQLite.openDatabase.mockRejectedValueOnce(dbError); + + await expect(database.init()).rejects.toThrow( + 'Database connection failed', + ); + }); + + it('handles SQL execution errors', async () => { + const sqlError = new Error('SQL execution failed'); + mockDb.executeSql.mockRejectedValueOnce(sqlError); + + await expect(database.getPendingProofs()).rejects.toThrow( + 'SQL execution failed', + ); + }); + }); +}); diff --git a/app/tests/src/stores/proofHistoryStore.test.ts b/app/tests/src/stores/proofHistoryStore.test.ts new file mode 100644 index 000000000..667abf064 --- /dev/null +++ b/app/tests/src/stores/proofHistoryStore.test.ts @@ -0,0 +1,313 @@ +// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11 + +import { act } from '@testing-library/react-native'; +import { io } from 'socket.io-client'; + +import { database } from '../../../src/stores/database'; +import { ProofStatus } from '../../../src/stores/proof-types'; +import { useProofHistoryStore } from '../../../src/stores/proofHistoryStore'; + +// Mock socket.io-client +jest.mock('socket.io-client', () => ({ + io: jest.fn(), +})); + +// Mock database +jest.mock('../../../src/stores/database', () => ({ + database: { + init: jest.fn(), + insertProof: jest.fn(), + updateProofStatus: jest.fn(), + getHistory: jest.fn(), + getPendingProofs: jest.fn(), + updateStaleProofs: jest.fn(), + }, +})); + +const mockDatabase = database as any; +const mockIo = io as any; + +describe('proofHistoryStore', () => { + let mockSocket: any; + + beforeEach(() => { + jest.clearAllMocks(); + + act(() => { + useProofHistoryStore.setState({ + proofHistory: [], + isLoading: false, + hasMore: true, + currentPage: 1, + }); + }); + + mockSocket = { + emit: jest.fn(), + on: jest.fn(), + }; + mockIo.mockReturnValue(mockSocket); + }); + + describe('initDatabase', () => { + it('initializes database and loads initial data', async () => { + const mockHistoryResult = { + rows: [ + { + id: '1', + sessionId: 'session-123', + appName: 'TestApp', + endpointType: 'celo', + status: ProofStatus.SUCCESS, + errorCode: null, + errorReason: null, + timestamp: Date.now(), + disclosures: '{"test": "data"}', + logoBase64: 'base64-logo', + userId: 'user-456', + userIdType: 'uuid', + }, + ], + total_count: 1, + }; + + mockDatabase.init.mockResolvedValue(); + mockDatabase.getHistory.mockResolvedValue(mockHistoryResult); + mockDatabase.getPendingProofs.mockResolvedValue({ rows: [] }); + + await act(async () => { + await useProofHistoryStore.getState().initDatabase(); + }); + + expect(mockDatabase.init).toHaveBeenCalled(); + expect(mockDatabase.getHistory).toHaveBeenCalledWith(1); + expect(useProofHistoryStore.getState().proofHistory).toHaveLength(1); + }); + + it('handles initialization errors gracefully', async () => { + mockDatabase.init.mockRejectedValue(new Error('Database error')); + + await act(async () => { + await useProofHistoryStore.getState().initDatabase(); + }); + + expect(mockDatabase.init).toHaveBeenCalled(); + expect(useProofHistoryStore.getState().proofHistory).toHaveLength(0); + }); + }); + + describe('addProofHistory', () => { + it('adds a new proof to the store', async () => { + const mockProof = { + appName: 'TestApp', + sessionId: 'session-123', + userId: 'user-456', + userIdType: 'uuid', + endpointType: 'celo', + status: ProofStatus.PENDING, + disclosures: '{"test": "data"}', + logoBase64: 'base64-logo', + } as const; + + const mockInsertResult = { + id: '1', + timestamp: Date.now(), + rowsAffected: 1, + }; + + mockDatabase.insertProof.mockResolvedValue(mockInsertResult); + + await act(async () => { + await useProofHistoryStore.getState().addProofHistory(mockProof); + }); + + expect(mockDatabase.insertProof).toHaveBeenCalledWith(mockProof); + expect(useProofHistoryStore.getState().proofHistory).toHaveLength(1); + }); + + it('handles insertion errors gracefully', async () => { + const mockProof = { + appName: 'TestApp', + sessionId: 'session-123', + userId: 'user-456', + userIdType: 'uuid', + endpointType: 'celo', + status: ProofStatus.PENDING, + disclosures: '{"test": "data"}', + } as const; + + mockDatabase.insertProof.mockRejectedValue(new Error('Insert error')); + + await act(async () => { + await useProofHistoryStore.getState().addProofHistory(mockProof); + }); + + expect(mockDatabase.insertProof).toHaveBeenCalledWith(mockProof); + expect(useProofHistoryStore.getState().proofHistory).toHaveLength(0); + }); + }); + + describe('updateProofStatus', () => { + it('updates proof status in database and store', async () => { + const mockProof = { + appName: 'TestApp', + sessionId: 'session-123', + userId: 'user-456', + userIdType: 'uuid', + endpointType: 'celo', + status: ProofStatus.PENDING, + disclosures: '{"test": "data"}', + } as const; + + mockDatabase.insertProof.mockResolvedValue({ + id: '1', + timestamp: Date.now(), + rowsAffected: 1, + }); + + await act(async () => { + await useProofHistoryStore.getState().addProofHistory(mockProof); + }); + + await act(async () => { + await useProofHistoryStore + .getState() + .updateProofStatus( + 'session-123', + ProofStatus.SUCCESS, + 'SUCCESS_001', + 'Operation completed', + ); + }); + + expect(mockDatabase.updateProofStatus).toHaveBeenCalledWith( + ProofStatus.SUCCESS, + 'SUCCESS_001', + 'Operation completed', + 'session-123', + ); + }); + + it('handles status update errors gracefully', async () => { + const mockProof = { + appName: 'TestApp', + sessionId: 'session-123', + userId: 'user-456', + userIdType: 'uuid', + endpointType: 'https', + status: ProofStatus.PENDING, + disclosures: '{"test": "data"}', + } as const; + + mockDatabase.insertProof.mockResolvedValue({ + id: '1', + timestamp: Date.now(), + rowsAffected: 1, + }); + + await act(async () => { + await useProofHistoryStore.getState().addProofHistory(mockProof); + }); + + mockDatabase.updateProofStatus.mockRejectedValue( + new Error('Update failed'), + ); + + await act(async () => { + await useProofHistoryStore + .getState() + .updateProofStatus( + 'session-123', + ProofStatus.SUCCESS, + 'SUCCESS_001', + 'Operation completed', + ); + }); + + expect(mockDatabase.updateProofStatus).toHaveBeenCalled(); + // Store should handle the error gracefully without crashing + }); + }); + + describe('loadMoreHistory', () => { + it('loads more history successfully', async () => { + const mockHistoryResult = { + rows: [ + { + id: '1', + sessionId: 'session-1', + appName: 'TestApp1', + endpointType: 'celo', + status: ProofStatus.SUCCESS, + errorCode: null, + errorReason: null, + timestamp: Date.now(), + disclosures: '{"test": "data1"}', + logoBase64: 'base64-logo1', + userId: 'user-1', + userIdType: 'uuid', + }, + ], + total_count: 5, + }; + + mockDatabase.getHistory.mockResolvedValue(mockHistoryResult); + + await act(async () => { + await useProofHistoryStore.getState().loadMoreHistory(); + }); + + expect(mockDatabase.getHistory).toHaveBeenCalledWith(1); + expect(useProofHistoryStore.getState().proofHistory).toHaveLength(1); + expect(useProofHistoryStore.getState().currentPage).toBe(2); + expect(useProofHistoryStore.getState().hasMore).toBe(true); + expect(useProofHistoryStore.getState().isLoading).toBe(false); + }); + + it('prevents loading when already loading', async () => { + act(() => { + useProofHistoryStore.setState({ isLoading: true }); + }); + + await act(async () => { + await useProofHistoryStore.getState().loadMoreHistory(); + }); + + expect(mockDatabase.getHistory).not.toHaveBeenCalled(); + }); + }); + + describe('resetHistory', () => { + it('resets history state to initial values', async () => { + const mockProof = { + appName: 'TestApp', + sessionId: 'session-123', + userId: 'user-456', + userIdType: 'uuid', + endpointType: 'celo', + status: ProofStatus.PENDING, + disclosures: '{"test": "data"}', + } as const; + + mockDatabase.insertProof.mockResolvedValue({ + id: '1', + timestamp: Date.now(), + rowsAffected: 1, + }); + + await act(async () => { + await useProofHistoryStore.getState().addProofHistory(mockProof); + }); + + expect(useProofHistoryStore.getState().proofHistory).toHaveLength(1); + + act(() => { + useProofHistoryStore.getState().resetHistory(); + }); + + expect(useProofHistoryStore.getState().proofHistory).toHaveLength(0); + expect(useProofHistoryStore.getState().currentPage).toBe(1); + expect(useProofHistoryStore.getState().hasMore).toBe(true); + }); + }); +}); diff --git a/app/tsconfig.json b/app/tsconfig.json index 136717c6e..8e99a77d1 100644 --- a/app/tsconfig.json +++ b/app/tsconfig.json @@ -4,5 +4,6 @@ "lib": ["dom", "esnext"], "resolveJsonModule": true, "esModuleInterop": true - } + }, + "exclude": ["node_modules", "vite.config.ts", ".tamagui/*"] } diff --git a/app/vite.config.ts b/app/vite.config.ts new file mode 100644 index 000000000..e74829754 --- /dev/null +++ b/app/vite.config.ts @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11 + +import { tamaguiPlugin } from '@tamagui/vite-plugin'; +import react from '@vitejs/plugin-react-swc'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { defineConfig } from 'vite'; +import svgr from 'vite-plugin-svgr'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +export default defineConfig({ + root: 'web', + resolve: { + extensions: [ + '.web.tsx', + '.web.js', + '.web.jsx', + '.web.ts', + '.tsx', + '.ts', + '.jsx', + '.js', + ], + alias: { + '@env': path.resolve(__dirname, 'env.ts'), + '/src': path.resolve(__dirname, 'src'), + 'react-native-svg': 'react-native-svg-web', + 'lottie-react-native': 'lottie-react', + 'react-native-safe-area-context': path.resolve( + __dirname, + 'src/mocks/react-native-safe-area-context.js', + ), + 'react-native-gesture-handler': path.resolve( + __dirname, + 'src/mocks/react-native-gesture-handler.ts', + ), + }, + }, + plugins: [ + react(), + svgr({ + include: '**/*.svg', + }), + tamaguiPlugin({ + config: path.resolve(__dirname, 'tamagui.config.ts'), + components: ['tamagui'], + excludeReactNativeWebExports: [ + 'Switch', + 'ProgressBar', + 'Picker', + 'CheckBox', + 'Touchable', + ], + platform: 'web', + optimize: true, + }), + ].filter(Boolean), + define: { + global: 'globalThis', + }, +}); diff --git a/app/web/index.html b/app/web/index.html new file mode 100644 index 000000000..82d06b41d --- /dev/null +++ b/app/web/index.html @@ -0,0 +1,13 @@ +<!doctype html> +<html lang="en"> + <head> + <meta charset="UTF-8" /> + <link rel="icon" type="image/svg+xml" href="/vite.svg" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <title>Self App + + +
+ + + diff --git a/app/web/main.tsx b/app/web/main.tsx new file mode 100644 index 000000000..ac98a8870 --- /dev/null +++ b/app/web/main.tsx @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11 + +import 'react-native-get-random-values'; + +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import { TamaguiProvider, View } from 'tamagui'; + +import App from '../App'; +import { black } from '../src/utils/colors'; +import tamaguiConfig from '../tamagui.config.ts'; + +const Root = () => ( + + + + + +); + +// Create root element and render the app +const container = document.getElementById('root'); +if (container) { + const root = createRoot(container); + root.render(); +} diff --git a/yarn.lock b/yarn.lock index 4702e3cdf..6373da2d4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -93,7 +93,7 @@ __metadata: languageName: node linkType: hard -"@babel/generator@npm:^7.20.0, @babel/generator@npm:^7.25.0, @babel/generator@npm:^7.27.3, @babel/generator@npm:^7.7.2": +"@babel/generator@npm:^7.20.0, @babel/generator@npm:^7.25.0, @babel/generator@npm:^7.25.5, @babel/generator@npm:^7.27.3, @babel/generator@npm:^7.7.2": version: 7.27.5 resolution: "@babel/generator@npm:7.27.5" dependencies: @@ -215,7 +215,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.18.6, @babel/helper-plugin-utils@npm:^7.20.2, @babel/helper-plugin-utils@npm:^7.22.5, @babel/helper-plugin-utils@npm:^7.27.1, @babel/helper-plugin-utils@npm:^7.8.0": +"@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.18.6, @babel/helper-plugin-utils@npm:^7.20.2, @babel/helper-plugin-utils@npm:^7.22.5, @babel/helper-plugin-utils@npm:^7.24.8, @babel/helper-plugin-utils@npm:^7.27.1, @babel/helper-plugin-utils@npm:^7.8.0": version: 7.27.1 resolution: "@babel/helper-plugin-utils@npm:7.27.1" checksum: 10c0/94cf22c81a0c11a09b197b41ab488d416ff62254ce13c57e62912c85700dc2e99e555225787a4099ff6bae7a1812d622c80fbaeda824b79baa10a6c5ac4cf69b @@ -300,7 +300,7 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.13.16, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.0, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.23.9, @babel/parser@npm:^7.25.3, @babel/parser@npm:^7.27.2, @babel/parser@npm:^7.27.4, @babel/parser@npm:^7.27.5": +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.1.6, @babel/parser@npm:^7.13.16, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.0, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.23.9, @babel/parser@npm:^7.25.3, @babel/parser@npm:^7.25.4, @babel/parser@npm:^7.27.2, @babel/parser@npm:^7.27.4, @babel/parser@npm:^7.27.5": version: 7.27.5 resolution: "@babel/parser@npm:7.27.5" dependencies: @@ -889,7 +889,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-react-jsx@npm:^7.0.0": +"@babel/plugin-transform-react-jsx@npm:^7.0.0, @babel/plugin-transform-react-jsx@npm:^7.25.2": version: 7.27.1 resolution: "@babel/plugin-transform-react-jsx@npm:7.27.1" dependencies: @@ -1035,7 +1035,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.25.0, @babel/runtime@npm:^7.27.4": +"@babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.25.0, @babel/runtime@npm:^7.25.4, @babel/runtime@npm:^7.27.4": version: 7.27.6 resolution: "@babel/runtime@npm:7.27.6" checksum: 10c0/89726be83f356f511dcdb74d3ea4d873a5f0cf0017d4530cb53aa27380c01ca102d573eff8b8b77815e624b1f8c24e7f0311834ad4fb632c90a770fda00bd4c8 @@ -1053,7 +1053,7 @@ __metadata: languageName: node linkType: hard -"@babel/traverse--for-generate-function-map@npm:@babel/traverse@^7.25.3, @babel/traverse@npm:^7.20.0, @babel/traverse@npm:^7.25.3, @babel/traverse@npm:^7.27.1, @babel/traverse@npm:^7.27.3, @babel/traverse@npm:^7.27.4": +"@babel/traverse--for-generate-function-map@npm:@babel/traverse@^7.25.3, @babel/traverse@npm:^7.1.6, @babel/traverse@npm:^7.20.0, @babel/traverse@npm:^7.25.3, @babel/traverse@npm:^7.25.4, @babel/traverse@npm:^7.27.1, @babel/traverse@npm:^7.27.3, @babel/traverse@npm:^7.27.4": version: 7.27.4 resolution: "@babel/traverse@npm:7.27.4" dependencies: @@ -1068,7 +1068,7 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.21.3, @babel/types@npm:^7.25.2, @babel/types@npm:^7.27.1, @babel/types@npm:^7.27.3, @babel/types@npm:^7.27.6, @babel/types@npm:^7.3.3": +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.1.6, @babel/types@npm:^7.20.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.21.3, @babel/types@npm:^7.25.2, @babel/types@npm:^7.25.4, @babel/types@npm:^7.27.1, @babel/types@npm:^7.27.3, @babel/types@npm:^7.27.6, @babel/types@npm:^7.3.3": version: 7.27.6 resolution: "@babel/types@npm:7.27.6" dependencies: @@ -2981,6 +2981,62 @@ __metadata: languageName: node linkType: hard +"@oxc-transform/binding-darwin-arm64@npm:0.47.1": + version: 0.47.1 + resolution: "@oxc-transform/binding-darwin-arm64@npm:0.47.1" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@oxc-transform/binding-darwin-x64@npm:0.47.1": + version: 0.47.1 + resolution: "@oxc-transform/binding-darwin-x64@npm:0.47.1" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@oxc-transform/binding-linux-arm64-gnu@npm:0.47.1": + version: 0.47.1 + resolution: "@oxc-transform/binding-linux-arm64-gnu@npm:0.47.1" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@oxc-transform/binding-linux-arm64-musl@npm:0.47.1": + version: 0.47.1 + resolution: "@oxc-transform/binding-linux-arm64-musl@npm:0.47.1" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@oxc-transform/binding-linux-x64-gnu@npm:0.47.1": + version: 0.47.1 + resolution: "@oxc-transform/binding-linux-x64-gnu@npm:0.47.1" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@oxc-transform/binding-linux-x64-musl@npm:0.47.1": + version: 0.47.1 + resolution: "@oxc-transform/binding-linux-x64-musl@npm:0.47.1" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@oxc-transform/binding-win32-arm64-msvc@npm:0.47.1": + version: 0.47.1 + resolution: "@oxc-transform/binding-win32-arm64-msvc@npm:0.47.1" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@oxc-transform/binding-win32-x64-msvc@npm:0.47.1": + version: 0.47.1 + resolution: "@oxc-transform/binding-win32-x64-msvc@npm:0.47.1" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@peculiar/asn1-cms@npm:^2.3.15": version: 2.3.15 resolution: "@peculiar/asn1-cms@npm:2.3.15" @@ -3893,6 +3949,13 @@ __metadata: languageName: node linkType: hard +"@react-native/normalize-colors@npm:^0.74.1": + version: 0.74.89 + resolution: "@react-native/normalize-colors@npm:0.74.89" + checksum: 10c0/6d0e5c91793ca5a66b4a0e5995361f474caacac56bde4772ac02b8ab470bd323076c567bd8856b0b097816d2b890e73a4040a3df01fd284adee683f5ba89d5ba + languageName: node + linkType: hard + "@react-native/typescript-config@npm:0.75.4": version: 0.75.4 resolution: "@react-native/typescript-config@npm:0.75.4" @@ -4033,6 +4096,29 @@ __metadata: languageName: node linkType: hard +"@rolldown/pluginutils@npm:1.0.0-beta.11": + version: 1.0.0-beta.11 + resolution: "@rolldown/pluginutils@npm:1.0.0-beta.11" + checksum: 10c0/140088e33a4dd3bc21d06fa0cbe79b52e95487c9737d425aa5729e52446dc70f066fbce632489a53e45bb567f1e86c19835677c98fe5d4123ae1e2fef53f8d97 + languageName: node + linkType: hard + +"@rollup/pluginutils@npm:^5.1.3": + version: 5.2.0 + resolution: "@rollup/pluginutils@npm:5.2.0" + dependencies: + "@types/estree": "npm:^1.0.0" + estree-walker: "npm:^2.0.2" + picomatch: "npm:^4.0.2" + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + checksum: 10c0/794890d512751451bcc06aa112366ef47ea8f9125dac49b1abf72ff8b079518b09359de9c60a013b33266541634e765ae61839c749fae0edb59a463418665c55 + languageName: node + linkType: hard + "@rollup/rollup-android-arm-eabi@npm:4.44.0": version: 4.44.0 resolution: "@rollup/rollup-android-arm-eabi@npm:4.44.0" @@ -4492,12 +4578,16 @@ __metadata: "@segment/analytics-react-native": "npm:^2.21.0" "@segment/sovran-react-native": "npm:^1.1.3" "@selfxyz/common": "workspace:^" + "@sentry/react": "npm:^9.32.0" "@sentry/react-native": "npm:6.10.0" "@stablelib/cbor": "npm:^2.0.1" + "@tamagui/animations-css": "npm:^1.129.3" + "@tamagui/animations-react-native": "npm:^1.129.3" "@tamagui/config": "npm:1.126.14" "@tamagui/lucide-icons": "npm:1.126.14" "@tamagui/toast": "npm:^1.127.2" "@tamagui/types": "npm:1.126.14" + "@tamagui/vite-plugin": "npm:1.126.14" "@testing-library/react-hooks": "npm:^8.0.1" "@testing-library/react-native": "npm:^13.2.0" "@tsconfig/react-native": "npm:^3.0.0" @@ -4508,10 +4598,13 @@ __metadata: "@types/node-forge": "npm:^1.3.3" "@types/pako": "npm:^2.0.3" "@types/react": "npm:^18.2.6" + "@types/react-dom": "npm:^19.1.6" "@types/react-native": "npm:^0.73.0" "@types/react-native-dotenv": "npm:^0.2.0" "@types/react-native-sqlite-storage": "npm:^6.0.5" + "@types/react-native-web": "npm:^0" "@types/react-test-renderer": "npm:^18" + "@vitejs/plugin-react-swc": "npm:^3.10.2" "@xstate/react": "npm:^5.0.3" add: "npm:^2.0.6" asn1js: "npm:^3.0.5" @@ -4528,6 +4621,7 @@ __metadata: expo-modules-core: "npm:^2.2.1" jest: "npm:^29.6.3" js-sha512: "npm:^0.9.0" + lottie-react: "npm:^2.4.1" lottie-react-native: "npm:7.2.2" msgpack-lite: "npm:^0.1.26" node-forge: "npm:^1.3.1" @@ -4536,6 +4630,7 @@ __metadata: poseidon-lite: "npm:^0.2.0" prettier: "npm:^3.5.3" react: "npm:^18.3.1" + react-dom: "npm:^18.3.1" react-native: "npm:0.75.4" react-native-app-auth: "npm:^8.0.3" react-native-biometrics: "npm:^3.0.1" @@ -4557,12 +4652,17 @@ __metadata: react-native-sqlite-storage: "npm:^6.0.1" react-native-svg: "npm:^15.11.1" react-native-svg-transformer: "npm:^1.5.0" + react-native-svg-web: "npm:^1.0.9" + react-native-web: "npm:^0.19.0" + react-qr-barcode-scanner: "npm:^2.1.7" react-test-renderer: "npm:^18.3.1" socket.io-client: "npm:^4.7.5" stream-browserify: "npm:^3.0.0" tamagui: "npm:1.126.14" typescript: "npm:^5.8.3" uuid: "npm:^11.0.5" + vite: "npm:^7.0.0" + vite-plugin-svgr: "npm:^4.3.0" xstate: "npm:^5.19.2" zustand: "npm:^4.5.2" languageName: unknown @@ -4615,6 +4715,15 @@ __metadata: languageName: node linkType: hard +"@sentry-internal/browser-utils@npm:9.32.0": + version: 9.32.0 + resolution: "@sentry-internal/browser-utils@npm:9.32.0" + dependencies: + "@sentry/core": "npm:9.32.0" + checksum: 10c0/ebbfca8002b2775eaf29e4c53509bc9faabff79bf17fc380875057e88d707106cf45c0940c175ba4f5578d551a6a0803c985b68d344645304e3db6b9fc2e66f3 + languageName: node + linkType: hard + "@sentry-internal/feedback@npm:8.54.0": version: 8.54.0 resolution: "@sentry-internal/feedback@npm:8.54.0" @@ -4624,6 +4733,15 @@ __metadata: languageName: node linkType: hard +"@sentry-internal/feedback@npm:9.32.0": + version: 9.32.0 + resolution: "@sentry-internal/feedback@npm:9.32.0" + dependencies: + "@sentry/core": "npm:9.32.0" + checksum: 10c0/6b81f7742c4e512d22c1ebbda7d5b5a4aadeef811ce37daee4f9800d75075701f345096efedf6ebfd089b05fbb3b049db38d87c8e5c81b6192de17d06bcb126a + languageName: node + linkType: hard + "@sentry-internal/replay-canvas@npm:8.54.0": version: 8.54.0 resolution: "@sentry-internal/replay-canvas@npm:8.54.0" @@ -4634,6 +4752,16 @@ __metadata: languageName: node linkType: hard +"@sentry-internal/replay-canvas@npm:9.32.0": + version: 9.32.0 + resolution: "@sentry-internal/replay-canvas@npm:9.32.0" + dependencies: + "@sentry-internal/replay": "npm:9.32.0" + "@sentry/core": "npm:9.32.0" + checksum: 10c0/248774bd967091c902e986cf6c24d7aac11846cd7ac9c80dfc9076453a168813e9ddafbb2d079c0361366aae82cfe01a02e47e28d716f209b5612e2de4891318 + languageName: node + linkType: hard + "@sentry-internal/replay@npm:8.54.0": version: 8.54.0 resolution: "@sentry-internal/replay@npm:8.54.0" @@ -4644,6 +4772,16 @@ __metadata: languageName: node linkType: hard +"@sentry-internal/replay@npm:9.32.0": + version: 9.32.0 + resolution: "@sentry-internal/replay@npm:9.32.0" + dependencies: + "@sentry-internal/browser-utils": "npm:9.32.0" + "@sentry/core": "npm:9.32.0" + checksum: 10c0/ca01deff29135d36841ec99d504ec0b0cd4d15f3d9efef55358bf1be1ed4f2f1ab89789db649fc85a8acc1b6e671a3bf8aa396999ca39bcce5ee02717aa55dd1 + languageName: node + linkType: hard + "@sentry/babel-plugin-component-annotate@npm:3.2.2": version: 3.2.2 resolution: "@sentry/babel-plugin-component-annotate@npm:3.2.2" @@ -4664,6 +4802,19 @@ __metadata: languageName: node linkType: hard +"@sentry/browser@npm:9.32.0": + version: 9.32.0 + resolution: "@sentry/browser@npm:9.32.0" + dependencies: + "@sentry-internal/browser-utils": "npm:9.32.0" + "@sentry-internal/feedback": "npm:9.32.0" + "@sentry-internal/replay": "npm:9.32.0" + "@sentry-internal/replay-canvas": "npm:9.32.0" + "@sentry/core": "npm:9.32.0" + checksum: 10c0/997bb8f89a6a0df9b26e45d92558839f169f0a090d44594962e55903e8dd5bdd8cfd6755d4ce0479102805e73a4f1b1b9123d37835dd7118389787d70a9bb59a + languageName: node + linkType: hard + "@sentry/cli-darwin@npm:2.42.4": version: 2.42.4 resolution: "@sentry/cli-darwin@npm:2.42.4" @@ -4770,6 +4921,13 @@ __metadata: languageName: node linkType: hard +"@sentry/core@npm:9.32.0": + version: 9.32.0 + resolution: "@sentry/core@npm:9.32.0" + checksum: 10c0/0336f1c4cfdf2fe2cf2a1c3ab286566ee53178998665a7f55cee8ecc9582796827fd6c67431337ae0b07be5726fcdfef5c3c908e2a29b56ce4be1eaabcda40e2 + languageName: node + linkType: hard + "@sentry/hub@npm:5.30.0": version: 5.30.0 resolution: "@sentry/hub@npm:5.30.0" @@ -4846,6 +5004,19 @@ __metadata: languageName: node linkType: hard +"@sentry/react@npm:^9.32.0": + version: 9.32.0 + resolution: "@sentry/react@npm:9.32.0" + dependencies: + "@sentry/browser": "npm:9.32.0" + "@sentry/core": "npm:9.32.0" + hoist-non-react-statics: "npm:^3.3.2" + peerDependencies: + react: ^16.14.0 || 17.x || 18.x || 19.x + checksum: 10c0/0fa0c5d14651942d2f2bb6ccde068b010b66c80556c82c8a107a6b931853d6f374dff06100d0c6fe178cabe03af90f250b0dd6cebc8f12e889bd45004bb994bf + languageName: node + linkType: hard + "@sentry/tracing@npm:5.30.0": version: 5.30.0 resolution: "@sentry/tracing@npm:5.30.0" @@ -6444,6 +6615,147 @@ __metadata: languageName: node linkType: hard +"@swc/core-darwin-arm64@npm:1.12.6": + version: 1.12.6 + resolution: "@swc/core-darwin-arm64@npm:1.12.6" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@swc/core-darwin-x64@npm:1.12.6": + version: 1.12.6 + resolution: "@swc/core-darwin-x64@npm:1.12.6" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@swc/core-linux-arm-gnueabihf@npm:1.12.6": + version: 1.12.6 + resolution: "@swc/core-linux-arm-gnueabihf@npm:1.12.6" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@swc/core-linux-arm64-gnu@npm:1.12.6": + version: 1.12.6 + resolution: "@swc/core-linux-arm64-gnu@npm:1.12.6" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@swc/core-linux-arm64-musl@npm:1.12.6": + version: 1.12.6 + resolution: "@swc/core-linux-arm64-musl@npm:1.12.6" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@swc/core-linux-x64-gnu@npm:1.12.6": + version: 1.12.6 + resolution: "@swc/core-linux-x64-gnu@npm:1.12.6" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@swc/core-linux-x64-musl@npm:1.12.6": + version: 1.12.6 + resolution: "@swc/core-linux-x64-musl@npm:1.12.6" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@swc/core-win32-arm64-msvc@npm:1.12.6": + version: 1.12.6 + resolution: "@swc/core-win32-arm64-msvc@npm:1.12.6" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@swc/core-win32-ia32-msvc@npm:1.12.6": + version: 1.12.6 + resolution: "@swc/core-win32-ia32-msvc@npm:1.12.6" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@swc/core-win32-x64-msvc@npm:1.12.6": + version: 1.12.6 + resolution: "@swc/core-win32-x64-msvc@npm:1.12.6" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@swc/core@npm:^1.11.31, @swc/core@npm:^1.5.25, @swc/core@npm:^1.7.21": + version: 1.12.6 + resolution: "@swc/core@npm:1.12.6" + dependencies: + "@swc/core-darwin-arm64": "npm:1.12.6" + "@swc/core-darwin-x64": "npm:1.12.6" + "@swc/core-linux-arm-gnueabihf": "npm:1.12.6" + "@swc/core-linux-arm64-gnu": "npm:1.12.6" + "@swc/core-linux-arm64-musl": "npm:1.12.6" + "@swc/core-linux-x64-gnu": "npm:1.12.6" + "@swc/core-linux-x64-musl": "npm:1.12.6" + "@swc/core-win32-arm64-msvc": "npm:1.12.6" + "@swc/core-win32-ia32-msvc": "npm:1.12.6" + "@swc/core-win32-x64-msvc": "npm:1.12.6" + "@swc/counter": "npm:^0.1.3" + "@swc/types": "npm:^0.1.23" + peerDependencies: + "@swc/helpers": ">=0.5.17" + dependenciesMeta: + "@swc/core-darwin-arm64": + optional: true + "@swc/core-darwin-x64": + optional: true + "@swc/core-linux-arm-gnueabihf": + optional: true + "@swc/core-linux-arm64-gnu": + optional: true + "@swc/core-linux-arm64-musl": + optional: true + "@swc/core-linux-x64-gnu": + optional: true + "@swc/core-linux-x64-musl": + optional: true + "@swc/core-win32-arm64-msvc": + optional: true + "@swc/core-win32-ia32-msvc": + optional: true + "@swc/core-win32-x64-msvc": + optional: true + peerDependenciesMeta: + "@swc/helpers": + optional: true + checksum: 10c0/8a7fdc5c14fc497eb81940721aab999c9dd32bbfaab041c80849d84ca9040ecc7eee46856af9849dd4f34404542cbe51dcef6ac9242afc891376a5e00bc307c9 + languageName: node + linkType: hard + +"@swc/counter@npm:^0.1.3": + version: 0.1.3 + resolution: "@swc/counter@npm:0.1.3" + checksum: 10c0/8424f60f6bf8694cfd2a9bca45845bce29f26105cda8cf19cdb9fd3e78dc6338699e4db77a89ae449260bafa1cc6bec307e81e7fb96dbf7dcfce0eea55151356 + languageName: node + linkType: hard + +"@swc/helpers@npm:^0.5.11": + version: 0.5.17 + resolution: "@swc/helpers@npm:0.5.17" + dependencies: + tslib: "npm:^2.8.0" + checksum: 10c0/fe1f33ebb968558c5a0c595e54f2e479e4609bff844f9ca9a2d1ffd8dd8504c26f862a11b031f48f75c95b0381c2966c3dd156e25942f90089badd24341e7dbb + languageName: node + linkType: hard + +"@swc/types@npm:^0.1.23": + version: 0.1.23 + resolution: "@swc/types@npm:0.1.23" + dependencies: + "@swc/counter": "npm:^0.1.3" + checksum: 10c0/edbfe4a72257f40137e27b537bc17d47ccab28de7727471b859c00a1e67f5feac5e01e4b4e0a2365907ce024bb8c3de4b26b6260733e1b601094db54ae9b7477 + languageName: node + linkType: hard + "@szmarczak/http-timer@npm:^4.0.5": version: 4.0.6 resolution: "@szmarczak/http-timer@npm:4.0.6" @@ -6563,6 +6875,21 @@ __metadata: languageName: node linkType: hard +"@tamagui/animations-css@npm:^1.129.3": + version: 1.129.3 + resolution: "@tamagui/animations-css@npm:1.129.3" + dependencies: + "@tamagui/constants": "npm:1.129.3" + "@tamagui/cubic-bezier-animator": "npm:1.129.3" + "@tamagui/use-presence": "npm:1.129.3" + "@tamagui/web": "npm:1.129.3" + peerDependencies: + react: "*" + react-dom: "*" + checksum: 10c0/cddc6be43eff69feb98a7f9a9ad19a07a07e30c9b97591b4bcb0c3eac735137fe5f30beb83abdd8f934958823cf584067f42c262fc7596a276304f43e14de6bc + languageName: node + linkType: hard + "@tamagui/animations-moti@npm:1.126.14": version: 1.126.14 resolution: "@tamagui/animations-moti@npm:1.126.14" @@ -6589,6 +6916,19 @@ __metadata: languageName: node linkType: hard +"@tamagui/animations-react-native@npm:^1.129.3": + version: 1.129.3 + resolution: "@tamagui/animations-react-native@npm:1.129.3" + dependencies: + "@tamagui/constants": "npm:1.129.3" + "@tamagui/use-presence": "npm:1.129.3" + "@tamagui/web": "npm:1.129.3" + peerDependencies: + react: "*" + checksum: 10c0/efd74f7109cd1eeb35bca24ac0b3274c547668867fc23dc32ee9577db8372f93204bd9928c507bd421b142ce54849e2cb79851c39c34bb0ea6b1bbc5f8705c31 + languageName: node + linkType: hard + "@tamagui/aria-hidden@npm:1.126.14": version: 1.126.14 resolution: "@tamagui/aria-hidden@npm:1.126.14" @@ -6615,6 +6955,40 @@ __metadata: languageName: node linkType: hard +"@tamagui/babel-plugin-fully-specified@npm:1.126.14": + version: 1.126.14 + resolution: "@tamagui/babel-plugin-fully-specified@npm:1.126.14" + dependencies: + "@babel/core": "npm:^7.25.2" + checksum: 10c0/1f653e59ee0debcbfde5873a2efaf0470a8bc0c23d2225e2bb1b6ab1a90d0b4681563b27b187eb2d27f3a9ee23b14417f8c332b82282fef4d42fba94e6b56441 + languageName: node + linkType: hard + +"@tamagui/build@npm:1.126.14": + version: 1.126.14 + resolution: "@tamagui/build@npm:1.126.14" + dependencies: + "@babel/core": "npm:^7.25.2" + "@swc/core": "npm:^1.7.21" + "@tamagui/babel-plugin-fully-specified": "npm:1.126.14" + "@types/fs-extra": "npm:^9.0.13" + chokidar: "npm:^3.5.2" + esbuild: "npm:^0.25.0" + esbuild-plugin-es5: "npm:^2.1.1" + esbuild-register: "npm:^3.6.0" + execa: "npm:^5.0.0" + fast-glob: "npm:^3.2.11" + fs-extra: "npm:^11.2.0" + lodash.debounce: "npm:^4.0.8" + oxc-transform: "npm:^0.47.1" + typescript: "npm:^5.8.2" + bin: + tamagui-build: tamagui-build.js + teesx: teesx.sh + checksum: 10c0/55533154a6b0bb7c466fb680df600c4806954842bd9f788c00ad7f84115cde640bc836a2a57e59f36d0721b66a990cfbf62aac6d8e06392a09095293886d0cb0 + languageName: node + linkType: hard + "@tamagui/button@npm:1.126.14": version: 1.126.14 resolution: "@tamagui/button@npm:1.126.14" @@ -6688,6 +7062,13 @@ __metadata: languageName: node linkType: hard +"@tamagui/cli-color@npm:1.126.14": + version: 1.126.14 + resolution: "@tamagui/cli-color@npm:1.126.14" + checksum: 10c0/e82454fda153d34f10ef9b3683280e209e59e2683ddd723223cf08684c2f332b70fca0719c92039f0553bdd75dae415511556bffc10e1fda3f9ad12e2b3d8f33 + languageName: node + linkType: hard + "@tamagui/collapsible@npm:1.126.14": version: 1.126.14 resolution: "@tamagui/collapsible@npm:1.126.14" @@ -6748,6 +7129,26 @@ __metadata: languageName: node linkType: hard +"@tamagui/compose-refs@npm:1.129.3": + version: 1.129.3 + resolution: "@tamagui/compose-refs@npm:1.129.3" + peerDependencies: + react: "*" + checksum: 10c0/c6d41ee1f4c47b24379bab31b65ee092147f4f3bd4f7bf7acd5117a533eb9804b179685ddf0ded1a0986c9145291d1cd8d7869912be09123ac07de782d3e6c51 + languageName: node + linkType: hard + +"@tamagui/config-default@npm:1.126.14": + version: 1.126.14 + resolution: "@tamagui/config-default@npm:1.126.14" + dependencies: + "@tamagui/animations-css": "npm:1.126.14" + "@tamagui/core": "npm:1.126.14" + "@tamagui/shorthands": "npm:1.126.14" + checksum: 10c0/ef7b1068209e3fb579a541db05e2900757c5f5377153a82d5e4cb52d5352804301d77472d83c7aed615ce712b397a0a8ca6d03d073729dbe04bfa0f1232b28d9 + languageName: node + linkType: hard + "@tamagui/config@npm:1.126.14": version: 1.126.14 resolution: "@tamagui/config@npm:1.126.14" @@ -6784,6 +7185,15 @@ __metadata: languageName: node linkType: hard +"@tamagui/constants@npm:1.129.3": + version: 1.129.3 + resolution: "@tamagui/constants@npm:1.129.3" + peerDependencies: + react: "*" + checksum: 10c0/2f303f573a05126dd1ab54f4daf14454ac30487cebc1aae881f857cc692a6559c4852e0e8ec15e2c24ed1692bd376ae91930b74267f1dafab0205c0480d6e38e + languageName: node + linkType: hard + "@tamagui/core@npm:1.126.14": version: 1.126.14 resolution: "@tamagui/core@npm:1.126.14" @@ -6844,6 +7254,13 @@ __metadata: languageName: node linkType: hard +"@tamagui/cubic-bezier-animator@npm:1.129.3": + version: 1.129.3 + resolution: "@tamagui/cubic-bezier-animator@npm:1.129.3" + checksum: 10c0/fbb225941fd852b70b0a943a227f62bedcf08a1fe79e5b7ff1142ae768d7bda47f4769952225e39f30c2eac2f76bced1afe339d1c397436c6e024d14e62354ce + languageName: node + linkType: hard + "@tamagui/dialog@npm:1.126.14": version: 1.126.14 resolution: "@tamagui/dialog@npm:1.126.14" @@ -7005,6 +7422,19 @@ __metadata: languageName: node linkType: hard +"@tamagui/generate-themes@npm:1.126.14": + version: 1.126.14 + resolution: "@tamagui/generate-themes@npm:1.126.14" + dependencies: + "@tamagui/create-theme": "npm:1.126.14" + "@tamagui/theme-builder": "npm:1.126.14" + "@tamagui/types": "npm:1.126.14" + esbuild-register: "npm:^3.6.0" + fs-extra: "npm:^11.2.0" + checksum: 10c0/4c12eea2fb8d365ffbdd71a1a7232eb2b837a1f9ea8b0a71acfc59b12d494cebac86f0b97758abfeae45fe66f2cbf7c3396846a40dc7e90e1bc6b4b2b3dc3e7e + languageName: node + linkType: hard + "@tamagui/get-button-sized@npm:1.126.14": version: 1.126.14 resolution: "@tamagui/get-button-sized@npm:1.126.14" @@ -7079,6 +7509,15 @@ __metadata: languageName: node linkType: hard +"@tamagui/helpers-node@npm:1.126.14": + version: 1.126.14 + resolution: "@tamagui/helpers-node@npm:1.126.14" + dependencies: + "@tamagui/types": "npm:1.126.14" + checksum: 10c0/63a8f8bb941ea0305b0046f960533fe6bf48655b1dc3796516667679466b3bf7fed49f23a1a668073b5b196093e28c11e99c01254fa973a6e85d85246c22f1b7 + languageName: node + linkType: hard + "@tamagui/helpers-tamagui@npm:1.126.14": version: 1.126.14 resolution: "@tamagui/helpers-tamagui@npm:1.126.14" @@ -7123,6 +7562,16 @@ __metadata: languageName: node linkType: hard +"@tamagui/helpers@npm:1.129.3": + version: 1.129.3 + resolution: "@tamagui/helpers@npm:1.129.3" + dependencies: + "@tamagui/constants": "npm:1.129.3" + "@tamagui/simple-hash": "npm:1.129.3" + checksum: 10c0/d12fb80606beeb0604fab3f92cdf352918ea67a9070f8cf2b8de96b3d37ebd4552fccb7ad2b47373414ee09ff61ede073897a65bb5e32f56fbdc38b935af58cc + languageName: node + linkType: hard + "@tamagui/image@npm:1.126.14": version: 1.126.14 resolution: "@tamagui/image@npm:1.126.14" @@ -7215,6 +7664,15 @@ __metadata: languageName: node linkType: hard +"@tamagui/normalize-css-color@npm:1.129.3": + version: 1.129.3 + resolution: "@tamagui/normalize-css-color@npm:1.129.3" + dependencies: + "@react-native/normalize-color": "npm:^2.1.0" + checksum: 10c0/962770534b57ad0117257f9ae7bd59b58cd3b1c2ab79bce374d08054571b43e4f2096a7021895f1ee83cd2f2bcc44ca61134b5a609c9641a1babaed5eff60f7c + languageName: node + linkType: hard + "@tamagui/polyfill-dev@npm:1.126.14": version: 1.126.14 resolution: "@tamagui/polyfill-dev@npm:1.126.14" @@ -7323,6 +7781,13 @@ __metadata: languageName: node linkType: hard +"@tamagui/proxy-worm@npm:1.126.14": + version: 1.126.14 + resolution: "@tamagui/proxy-worm@npm:1.126.14" + checksum: 10c0/8a36b725ff8535843651a101c76ecadd45b8c241fe9fe24105e98f94551b66877b3af867e4f4793328fd48490b59a08cc34d9788beadae7cffb8f4b489fdf2fd + languageName: node + linkType: hard + "@tamagui/radio-group@npm:1.126.14": version: 1.126.14 resolution: "@tamagui/radio-group@npm:1.126.14" @@ -7386,6 +7851,13 @@ __metadata: languageName: node linkType: hard +"@tamagui/react-native-svg@npm:1.126.14": + version: 1.126.14 + resolution: "@tamagui/react-native-svg@npm:1.126.14" + checksum: 10c0/bea990e3f1d9ec8416dfd81c987562b7ed2b539e66182a709e2600cf4b6989b843a3fd9bc1c2eaed56f9cdc931ebb7222834ae6f71b2136d4e9b27a488a48453 + languageName: node + linkType: hard + "@tamagui/react-native-use-pressable@npm:1.126.14": version: 1.126.14 resolution: "@tamagui/react-native-use-pressable@npm:1.126.14" @@ -7422,6 +7894,35 @@ __metadata: languageName: node linkType: hard +"@tamagui/react-native-web-internals@npm:1.126.14": + version: 1.126.14 + resolution: "@tamagui/react-native-web-internals@npm:1.126.14" + dependencies: + "@tamagui/normalize-css-color": "npm:1.126.14" + "@tamagui/react-native-use-pressable": "npm:1.126.14" + "@tamagui/react-native-use-responder-events": "npm:1.126.14" + "@tamagui/simple-hash": "npm:1.126.14" + "@tamagui/web": "npm:1.126.14" + react: "npm:*" + checksum: 10c0/4289cdfc881039e1fcaa5d7f3e55762cb05369512c53d5f5abe61447dd203148becc08fddd0e2955fa89969e191a2d041b84be0e0f80923329e69980066421a0 + languageName: node + linkType: hard + +"@tamagui/react-native-web-lite@npm:1.126.14": + version: 1.126.14 + resolution: "@tamagui/react-native-web-lite@npm:1.126.14" + dependencies: + "@tamagui/normalize-css-color": "npm:1.126.14" + "@tamagui/react-native-use-pressable": "npm:1.126.14" + "@tamagui/react-native-use-responder-events": "npm:1.126.14" + "@tamagui/react-native-web-internals": "npm:1.126.14" + invariant: "npm:^2.2.4" + peerDependencies: + react: "*" + checksum: 10c0/7c6954d44c0ef9fdc90fdde6b7dc30d2bae713b1e0d8fca725175033664297f9c10c26d30897354f612e26139fc96359b7d924f0078013fbaaafa8365b087b20 + languageName: node + linkType: hard + "@tamagui/remove-scroll@npm:1.126.14": version: 1.126.14 resolution: "@tamagui/remove-scroll@npm:1.126.14" @@ -7572,6 +8073,13 @@ __metadata: languageName: node linkType: hard +"@tamagui/simple-hash@npm:1.129.3": + version: 1.129.3 + resolution: "@tamagui/simple-hash@npm:1.129.3" + checksum: 10c0/fa876f1853b5032375cc11f42b978d36c8648cc4d0d7ea6680124ec7ea816d20e90c73b2279edf6b83267dcc02c0c0cfcc97a9848a8dc7fcf876c79bd7a2bfce + languageName: node + linkType: hard + "@tamagui/slider@npm:1.126.14": version: 1.126.14 resolution: "@tamagui/slider@npm:1.126.14" @@ -7628,6 +8136,50 @@ __metadata: languageName: node linkType: hard +"@tamagui/static@npm:1.126.14": + version: 1.126.14 + resolution: "@tamagui/static@npm:1.126.14" + dependencies: + "@babel/core": "npm:^7.25.2" + "@babel/generator": "npm:^7.25.5" + "@babel/helper-plugin-utils": "npm:^7.24.8" + "@babel/parser": "npm:^7.25.4" + "@babel/plugin-transform-react-jsx": "npm:^7.25.2" + "@babel/runtime": "npm:^7.25.4" + "@babel/traverse": "npm:^7.25.4" + "@babel/types": "npm:^7.25.4" + "@tamagui/build": "npm:1.126.14" + "@tamagui/cli-color": "npm:1.126.14" + "@tamagui/config-default": "npm:1.126.14" + "@tamagui/core": "npm:1.126.14" + "@tamagui/fake-react-native": "npm:1.126.14" + "@tamagui/generate-themes": "npm:1.126.14" + "@tamagui/helpers": "npm:1.126.14" + "@tamagui/helpers-node": "npm:1.126.14" + "@tamagui/proxy-worm": "npm:1.126.14" + "@tamagui/react-native-web-internals": "npm:1.126.14" + "@tamagui/react-native-web-lite": "npm:1.126.14" + "@tamagui/shorthands": "npm:1.126.14" + "@tamagui/types": "npm:1.126.14" + babel-literal-to-ast: "npm:^2.1.0" + browserslist: "npm:^4.22.2" + check-dependency-version-consistency: "npm:^4.1.0" + esbuild: "npm:^0.25.0" + esbuild-register: "npm:^3.6.0" + fast-glob: "npm:^3.2.11" + find-cache-dir: "npm:^3.3.2" + find-root: "npm:^1.1.0" + fs-extra: "npm:^11.2.0" + invariant: "npm:^2.2.4" + js-yaml: "npm:^4.1.0" + lodash: "npm:^4.17.21" + react-native-web: "npm:^0.20.0" + peerDependencies: + react: "*" + checksum: 10c0/3f468e63a8f81e9974f9e1e52543c0973a6adba7f622d621e194df21a44a00011167b0a67de24e35b9d2f9b67ccb110d932552fe9b170dbaeb81ee8b438d9bad + languageName: node + linkType: hard + "@tamagui/switch-headless@npm:1.126.14": version: 1.126.14 resolution: "@tamagui/switch-headless@npm:1.126.14" @@ -7760,6 +8312,13 @@ __metadata: languageName: node linkType: hard +"@tamagui/timer@npm:1.129.3": + version: 1.129.3 + resolution: "@tamagui/timer@npm:1.129.3" + checksum: 10c0/5bb4af2e98615e7d9a054d4935e42eae0beefb805fd8d87ce2e4de8841e082e04af30da0e8ab4e55a99c0035a74d5ac0adbb6e0c858900caded52de623455752 + languageName: node + linkType: hard + "@tamagui/toast@npm:^1.127.2": version: 1.127.2 resolution: "@tamagui/toast@npm:1.127.2" @@ -7844,6 +8403,13 @@ __metadata: languageName: node linkType: hard +"@tamagui/types@npm:1.129.3": + version: 1.129.3 + resolution: "@tamagui/types@npm:1.129.3" + checksum: 10c0/39621fcdfceac3275660bd7ce01b0801b7ec066815628c3640dc6919aacf2d1cfe1797f6ca43f752c9b08c511862d28e543ebfdbd22d05f35baf203444a55480 + languageName: node + linkType: hard + "@tamagui/use-callback-ref@npm:1.126.14": version: 1.126.14 resolution: "@tamagui/use-callback-ref@npm:1.126.14" @@ -7927,6 +8493,15 @@ __metadata: languageName: node linkType: hard +"@tamagui/use-did-finish-ssr@npm:1.129.3": + version: 1.129.3 + resolution: "@tamagui/use-did-finish-ssr@npm:1.129.3" + peerDependencies: + react: "*" + checksum: 10c0/189003a2573d500acd83cf752a5b250e27cc14c13e79ac754ce28e602681ca6b549b23301a9d347b77afbcba8fe2bc7801f36043b016817cf990da6fdebec895 + languageName: node + linkType: hard + "@tamagui/use-direction@npm:1.126.14": version: 1.126.14 resolution: "@tamagui/use-direction@npm:1.126.14" @@ -7976,6 +8551,17 @@ __metadata: languageName: node linkType: hard +"@tamagui/use-event@npm:1.129.3": + version: 1.129.3 + resolution: "@tamagui/use-event@npm:1.129.3" + dependencies: + "@tamagui/constants": "npm:1.129.3" + peerDependencies: + react: "*" + checksum: 10c0/921740935d08d91f8829a52e80e1ac9de549f98d6625e81cb536e055c6cd80f4ef377309a890566d18a50d585c17f27d10783b1194c9b3a8932eb5c73fdec784 + languageName: node + linkType: hard + "@tamagui/use-force-update@npm:1.126.14": version: 1.126.14 resolution: "@tamagui/use-force-update@npm:1.126.14" @@ -7994,6 +8580,15 @@ __metadata: languageName: node linkType: hard +"@tamagui/use-force-update@npm:1.129.3": + version: 1.129.3 + resolution: "@tamagui/use-force-update@npm:1.129.3" + peerDependencies: + react: "*" + checksum: 10c0/925c4d697372e858d8c1cb263411b8e0a3a0a3338dbbdcedb750395f9dc4f9dbb9b4fd84f478426b8fd32d8fc019cacedb7fda5b84d9c174053feafbe5e1cccd + languageName: node + linkType: hard + "@tamagui/use-keyboard-visible@npm:1.126.14": version: 1.126.14 resolution: "@tamagui/use-keyboard-visible@npm:1.126.14" @@ -8025,6 +8620,17 @@ __metadata: languageName: node linkType: hard +"@tamagui/use-presence@npm:1.129.3": + version: 1.129.3 + resolution: "@tamagui/use-presence@npm:1.129.3" + dependencies: + "@tamagui/web": "npm:1.129.3" + peerDependencies: + react: "*" + checksum: 10c0/acff4350100835ab412eb54d044c085876b65a6de2d8b4152d780cbba483bc9056101c9e32d9e39ac2571852853e1f18e1d2865929b865e077f4d53910e21829 + languageName: node + linkType: hard + "@tamagui/use-previous@npm:1.126.14": version: 1.126.14 resolution: "@tamagui/use-previous@npm:1.126.14" @@ -8065,6 +8671,25 @@ __metadata: languageName: node linkType: hard +"@tamagui/vite-plugin@npm:1.126.14": + version: 1.126.14 + resolution: "@tamagui/vite-plugin@npm:1.126.14" + dependencies: + "@tamagui/fake-react-native": "npm:1.126.14" + "@tamagui/proxy-worm": "npm:1.126.14" + "@tamagui/react-native-svg": "npm:1.126.14" + "@tamagui/react-native-web-lite": "npm:1.126.14" + "@tamagui/static": "npm:1.126.14" + esm-resolve: "npm:^1.0.8" + fs-extra: "npm:^11.2.0" + outdent: "npm:^0.8.0" + react-native-web: "npm:^0.20.0" + peerDependencies: + vite: "*" + checksum: 10c0/51a19432692ef9fad5289259029ba9827e16c085a4f840b6033a177505ff24c354289f55d3921cc204d29b07b2efa242d96a9ac425d7124e736da0c18f4a6410 + languageName: node + linkType: hard + "@tamagui/web@npm:1.126.14": version: 1.126.14 resolution: "@tamagui/web@npm:1.126.14" @@ -8105,6 +8730,26 @@ __metadata: languageName: node linkType: hard +"@tamagui/web@npm:1.129.3": + version: 1.129.3 + resolution: "@tamagui/web@npm:1.129.3" + dependencies: + "@tamagui/compose-refs": "npm:1.129.3" + "@tamagui/constants": "npm:1.129.3" + "@tamagui/helpers": "npm:1.129.3" + "@tamagui/normalize-css-color": "npm:1.129.3" + "@tamagui/timer": "npm:1.129.3" + "@tamagui/types": "npm:1.129.3" + "@tamagui/use-did-finish-ssr": "npm:1.129.3" + "@tamagui/use-event": "npm:1.129.3" + "@tamagui/use-force-update": "npm:1.129.3" + peerDependencies: + react: "*" + react-dom: "*" + checksum: 10c0/d402da54e012f20e501927b186af81254329c6e04c62300795fffa4f01e72bbc7c1b40196059c8e7e695c9e2c21adbe2b6d026733f447f8fa0c50927fd9391f9 + languageName: node + linkType: hard + "@tamagui/z-index-stack@npm:1.126.14": version: 1.126.14 resolution: "@tamagui/z-index-stack@npm:1.126.14" @@ -8412,7 +9057,7 @@ __metadata: languageName: node linkType: hard -"@types/estree@npm:*, @types/estree@npm:1.0.8, @types/estree@npm:^1.0.6": +"@types/estree@npm:*, @types/estree@npm:1.0.8, @types/estree@npm:^1.0.0, @types/estree@npm:^1.0.6": version: 1.0.8 resolution: "@types/estree@npm:1.0.8" checksum: 10c0/39d34d1afaa338ab9763f37ad6066e3f349444f9052b9676a7cc0252ef9485a41c6d81c9c4e0d26e9077993354edf25efc853f3224dd4b447175ef62bdcc86a5 @@ -8437,6 +9082,15 @@ __metadata: languageName: node linkType: hard +"@types/fs-extra@npm:^9.0.13": + version: 9.0.13 + resolution: "@types/fs-extra@npm:9.0.13" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/576d4e9d382393316ed815c593f7f5c157408ec5e184521d077fcb15d514b5a985245f153ef52142b9b976cb9bd8f801850d51238153ebd0dc9e96b7a7548588 + languageName: node + linkType: hard + "@types/glob@npm:^7.1.1": version: 7.2.0 resolution: "@types/glob@npm:7.2.0" @@ -8512,6 +9166,13 @@ __metadata: languageName: node linkType: hard +"@types/js-yaml@npm:^4.0.5": + version: 4.0.9 + resolution: "@types/js-yaml@npm:4.0.9" + checksum: 10c0/24de857aa8d61526bbfbbaa383aa538283ad17363fcd5bb5148e2c7f604547db36646440e739d78241ed008702a8920665d1add5618687b6743858fae00da211 + languageName: node + linkType: hard + "@types/json-schema@npm:*, @types/json-schema@npm:^7.0.15, @types/json-schema@npm:^7.0.9": version: 7.0.15 resolution: "@types/json-schema@npm:7.0.15" @@ -8661,6 +9322,15 @@ __metadata: languageName: node linkType: hard +"@types/react-dom@npm:^19.1.6": + version: 19.1.6 + resolution: "@types/react-dom@npm:19.1.6" + peerDependencies: + "@types/react": ^19.0.0 + checksum: 10c0/7ba74eee2919e3f225e898b65fdaa16e54952aaf9e3472a080ddc82ca54585e46e60b3c52018d21d4b7053f09d27b8293e9f468b85f9932ff452cd290cc131e8 + languageName: node + linkType: hard + "@types/react-native-dotenv@npm:^0.2.0": version: 0.2.2 resolution: "@types/react-native-dotenv@npm:0.2.2" @@ -8675,6 +9345,16 @@ __metadata: languageName: node linkType: hard +"@types/react-native-web@npm:^0": + version: 0.19.1 + resolution: "@types/react-native-web@npm:0.19.1" + dependencies: + "@types/react": "npm:*" + react-native: "npm:*" + checksum: 10c0/227db57fd299ea1effd7372e277b1b74d3e5a6ae59069ad1e17ecc4c5e436e8f600e79eb054f9822107667486733246ab4dfc016928091fc5254aca005426226 + languageName: node + linkType: hard + "@types/react-native@npm:^0.73.0": version: 0.73.0 resolution: "@types/react-native@npm:0.73.0" @@ -8693,6 +9373,15 @@ __metadata: languageName: node linkType: hard +"@types/react@npm:*": + version: 19.1.8 + resolution: "@types/react@npm:19.1.8" + dependencies: + csstype: "npm:^3.0.2" + checksum: 10c0/4908772be6dc941df276931efeb0e781777fa76e4d5d12ff9f75eb2dcc2db3065e0100efde16fde562c5bafa310cc8f50c1ee40a22640459e066e72cd342143e + languageName: node + linkType: hard + "@types/react@npm:^18, @types/react@npm:^18.2.6, @types/react@npm:^18.3.4": version: 18.3.23 resolution: "@types/react@npm:18.3.23" @@ -9062,6 +9751,18 @@ __metadata: languageName: node linkType: hard +"@vitejs/plugin-react-swc@npm:^3.10.2": + version: 3.10.2 + resolution: "@vitejs/plugin-react-swc@npm:3.10.2" + dependencies: + "@rolldown/pluginutils": "npm:1.0.0-beta.11" + "@swc/core": "npm:^1.11.31" + peerDependencies: + vite: ^4 || ^5 || ^6 || ^7.0.0-beta.0 + checksum: 10c0/3d1c10ed03f9ef5ee633453dec99f36a8d697b66bdb2edc4532352d4e43ec8fc01ea01258f4cd9c316925994c89fb02bedc396ee0f2d0953b6a5719f355f6c47 + languageName: node + linkType: hard + "@webassemblyjs/ast@npm:1.14.1, @webassemblyjs/ast@npm:^1.14.1": version: 1.14.1 resolution: "@webassemblyjs/ast@npm:1.14.1" @@ -9453,6 +10154,26 @@ __metadata: languageName: node linkType: hard +"@zxing/library@npm:^0.21.3": + version: 0.21.3 + resolution: "@zxing/library@npm:0.21.3" + dependencies: + "@zxing/text-encoding": "npm:~0.9.0" + ts-custom-error: "npm:^3.2.1" + dependenciesMeta: + "@zxing/text-encoding": + optional: true + checksum: 10c0/c6b33998847ad2fda0bce2f07f5b625e33e9dd98f84818ceb976b1193936b69b38ac3120418cca24b38441c711cfd21208b50851967bf8f301567e7f70197268 + languageName: node + linkType: hard + +"@zxing/text-encoding@npm:~0.9.0": + version: 0.9.0 + resolution: "@zxing/text-encoding@npm:0.9.0" + checksum: 10c0/d15bff181d46c2ab709e7242801a8d40408aa8c19b44462e5f60e766bf59105b44957914ab6baab60d10d466a5e965f21fe890c67dfdb7d5c7f940df457b4d0d + languageName: node + linkType: hard + "abbrev@npm:1": version: 1.1.1 resolution: "abbrev@npm:1.1.1" @@ -9948,7 +10669,7 @@ __metadata: languageName: node linkType: hard -"asap@npm:~2.0.6": +"asap@npm:~2.0.3, asap@npm:~2.0.6": version: 2.0.6 resolution: "asap@npm:2.0.6" checksum: 10c0/c6d5e39fe1f15e4b87677460bd66b66050cd14c772269cee6688824c1410a08ab20254bb6784f9afb75af9144a9f9a7692d49547f4d19d715aeb7c0318f3136d @@ -10137,6 +10858,19 @@ __metadata: languageName: node linkType: hard +"babel-literal-to-ast@npm:^2.1.0": + version: 2.1.0 + resolution: "babel-literal-to-ast@npm:2.1.0" + dependencies: + "@babel/parser": "npm:^7.1.6" + "@babel/traverse": "npm:^7.1.6" + "@babel/types": "npm:^7.1.6" + peerDependencies: + "@babel/core": ^7.1.2 + checksum: 10c0/58e41540f9727b981d5adb684f3927a423054f77740045e9c5e136de7cc8909afa56110445070bde7b00b8cb75e2c81e7925710f59aacb6549aee9ff89c7afe1 + languageName: node + linkType: hard + "babel-plugin-istanbul@npm:^6.1.1": version: 6.1.1 resolution: "babel-plugin-istanbul@npm:6.1.1" @@ -10557,6 +11291,20 @@ __metadata: languageName: node linkType: hard +"browserslist@npm:^4.22.2": + version: 4.25.1 + resolution: "browserslist@npm:4.25.1" + dependencies: + caniuse-lite: "npm:^1.0.30001726" + electron-to-chromium: "npm:^1.5.173" + node-releases: "npm:^2.0.19" + update-browserslist-db: "npm:^1.1.3" + bin: + browserslist: cli.js + checksum: 10c0/acba5f0bdbd5e72dafae1e6ec79235b7bad305ed104e082ed07c34c38c7cb8ea1bc0f6be1496958c40482e40166084458fc3aee15111f15faa79212ad9081b2a + languageName: node + linkType: hard + "browserslist@npm:^4.24.0, browserslist@npm:^4.25.0": version: 4.25.0 resolution: "browserslist@npm:4.25.0" @@ -10816,6 +11564,13 @@ __metadata: languageName: node linkType: hard +"caniuse-lite@npm:^1.0.30001726": + version: 1.0.30001726 + resolution: "caniuse-lite@npm:1.0.30001726" + checksum: 10c0/2c5f91da7fd9ebf8c6b432818b1498ea28aca8de22b30dafabe2a2a6da1e014f10e67e14f8e68e872a0867b6b4cd6001558dde04e3ab9770c9252ca5c8849d0e + languageName: node + linkType: hard + "caseless@npm:^0.12.0, caseless@npm:~0.12.0": version: 0.12.0 resolution: "caseless@npm:0.12.0" @@ -10916,6 +11671,13 @@ __metadata: languageName: node linkType: hard +"chalk@npm:^5.2.0": + version: 5.4.1 + resolution: "chalk@npm:5.4.1" + checksum: 10c0/b23e88132c702f4855ca6d25cb5538b1114343e41472d5263ee8a37cccfccd9c4216d111e1097c6a27830407a1dc81fecdf2a56f2c63033d4dbbd88c10b0dcef + languageName: node + linkType: hard + "char-regex@npm:^1.0.2": version: 1.0.2 resolution: "char-regex@npm:1.0.2" @@ -10930,6 +11692,25 @@ __metadata: languageName: node linkType: hard +"check-dependency-version-consistency@npm:^4.1.0": + version: 4.1.1 + resolution: "check-dependency-version-consistency@npm:4.1.1" + dependencies: + "@types/js-yaml": "npm:^4.0.5" + chalk: "npm:^5.2.0" + commander: "npm:^11.0.0" + edit-json-file: "npm:^1.7.0" + globby: "npm:^13.1.4" + js-yaml: "npm:^4.1.0" + semver: "npm:^7.5.1" + table: "npm:^6.8.1" + type-fest: "npm:^4.30.0" + bin: + check-dependency-version-consistency: dist/bin/check-dependency-version-consistency.js + checksum: 10c0/395e0d367a92481345e80f37badff2cfc2d2f37985d96f9a5e58a1d0b6e6ec924cf798ab688b268ea792714c0010293d5a49a0a40588045695b52ba3c31c19a5 + languageName: node + linkType: hard + "check-error@npm:^1.0.2, check-error@npm:^1.0.3": version: 1.0.3 resolution: "check-error@npm:1.0.3" @@ -10953,7 +11734,7 @@ __metadata: languageName: node linkType: hard -"chokidar@npm:^3.5.3": +"chokidar@npm:^3.5.2, chokidar@npm:^3.5.3": version: 3.6.0 resolution: "chokidar@npm:3.6.0" dependencies: @@ -11752,6 +12533,15 @@ __metadata: languageName: node linkType: hard +"cross-fetch@npm:^3.1.5": + version: 3.2.0 + resolution: "cross-fetch@npm:3.2.0" + dependencies: + node-fetch: "npm:^2.7.0" + checksum: 10c0/d8596adf0269130098a676f6739a0922f3cc7b71cc89729925411ebe851a87026171c82ea89154c4811c9867c01c44793205a52e618ce2684650218c7fbeeb9f + languageName: node + linkType: hard + "cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3, cross-spawn@npm:^7.0.6": version: 7.0.6 resolution: "cross-spawn@npm:7.0.6" @@ -11777,6 +12567,15 @@ __metadata: languageName: node linkType: hard +"css-in-js-utils@npm:^3.1.0": + version: 3.1.0 + resolution: "css-in-js-utils@npm:3.1.0" + dependencies: + hyphenate-style-name: "npm:^1.0.3" + checksum: 10c0/8bb042e8f7701a7edadc3cce5ce2d5cf41189631d7e2aed194d5a7059b25776dded2a0466cb9da1d1f3fc6c99dcecb51e45671148d073b8a2a71e34755152e52 + languageName: node + linkType: hard + "css-select@npm:^5.1.0": version: 5.1.0 resolution: "css-select@npm:5.1.0" @@ -12319,6 +13118,19 @@ __metadata: languageName: node linkType: hard +"edit-json-file@npm:^1.7.0": + version: 1.8.1 + resolution: "edit-json-file@npm:1.8.1" + dependencies: + find-value: "npm:^1.0.12" + iterate-object: "npm:^1.3.4" + r-json: "npm:^1.2.10" + set-value: "npm:^4.1.0" + w-json: "npm:^1.3.10" + checksum: 10c0/62358a310d409eaf67108c57c0fc27287ecb41a72870b327327c6f2251efd73974d1a542ea4c1afb464fcbb646a83704326bd775013c8dcf9293af01f6cfb02a + languageName: node + linkType: hard + "ee-first@npm:1.1.1": version: 1.1.1 resolution: "ee-first@npm:1.1.1" @@ -12344,6 +13156,13 @@ __metadata: languageName: node linkType: hard +"electron-to-chromium@npm:^1.5.173": + version: 1.5.173 + resolution: "electron-to-chromium@npm:1.5.173" + checksum: 10c0/3242129332438ddc34c30dc218241e837fd87e8db54ba5d22a2e3e789115ce15932b5989d91b14be304081446a4c169bc1e573db6edd6eb3e859a7dba44e6c0a + languageName: node + linkType: hard + "elliptic@npm:6.6.1, elliptic@npm:^6.5.5, elliptic@npm:^6.5.7, elliptic@npm:^6.6.1": version: 6.6.1 resolution: "elliptic@npm:6.6.1" @@ -12689,6 +13508,30 @@ __metadata: languageName: node linkType: hard +"esbuild-plugin-es5@npm:^2.1.1": + version: 2.1.1 + resolution: "esbuild-plugin-es5@npm:2.1.1" + dependencies: + "@swc/core": "npm:^1.5.25" + "@swc/helpers": "npm:^0.5.11" + deepmerge: "npm:^4.3.1" + peerDependencies: + esbuild: "*" + checksum: 10c0/6d00df632e9c31919c1fca6d33919c19940409899597d0eb44527a2fcf364d5185e7371c4f6f390640a5b042b5e3aa5caad5fb21dc2fd09d638f3b57f0db20b2 + languageName: node + linkType: hard + +"esbuild-register@npm:^3.6.0": + version: 3.6.0 + resolution: "esbuild-register@npm:3.6.0" + dependencies: + debug: "npm:^4.3.4" + peerDependencies: + esbuild: ">=0.12 <1" + checksum: 10c0/77193b7ca32ba9f81b35ddf3d3d0138efb0b1429d71b39480cfee932e1189dd2e492bd32bf04a4d0bc3adfbc7ec7381ceb5ffd06efe35f3e70904f1f686566d5 + languageName: node + linkType: hard + "esbuild@npm:^0.25.0, esbuild@npm:~0.25.0": version: 0.25.5 resolution: "esbuild@npm:0.25.5" @@ -13131,6 +13974,13 @@ __metadata: languageName: node linkType: hard +"esm-resolve@npm:^1.0.8": + version: 1.0.11 + resolution: "esm-resolve@npm:1.0.11" + checksum: 10c0/c57bba8a2156e99f76433f24687da710fc366024870e2d2a5aa5c357973956d63c306595d97028eeb329a90ee6e6e9f4a4ca26a529c4f949ed3d66aff3d1d2ee + languageName: node + linkType: hard + "espree@npm:^9.6.0, espree@npm:^9.6.1": version: 9.6.1 resolution: "espree@npm:9.6.1" @@ -13226,6 +14076,13 @@ __metadata: languageName: node linkType: hard +"estree-walker@npm:^2.0.2": + version: 2.0.2 + resolution: "estree-walker@npm:2.0.2" + checksum: 10c0/53a6c54e2019b8c914dc395890153ffdc2322781acf4bd7d1a32d7aedc1710807bdcd866ac133903d5629ec601fbb50abe8c2e5553c7f5a0afdd9b6af6c945af + languageName: node + linkType: hard + "esutils@npm:^2.0.2": version: 2.0.3 resolution: "esutils@npm:2.0.3" @@ -13542,7 +14399,7 @@ __metadata: languageName: node linkType: hard -"fast-glob@npm:^3.0.3, fast-glob@npm:^3.2.2, fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.2": +"fast-glob@npm:^3.0.3, fast-glob@npm:^3.2.11, fast-glob@npm:^3.2.2, fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.0, fast-glob@npm:^3.3.2": version: 3.3.3 resolution: "fast-glob@npm:3.3.3" dependencies: @@ -13569,6 +14426,13 @@ __metadata: languageName: node linkType: hard +"fast-loops@npm:^1.1.3": + version: 1.1.4 + resolution: "fast-loops@npm:1.1.4" + checksum: 10c0/25e8a608fccc0d84c1d037efa715ab1e6f21576e1070931b3ed966657204c47ed2b1cba16e5c46ddde2d62aba0b4100d86616d995318b7367fa0a902a78ed885 + languageName: node + linkType: hard + "fast-uri@npm:^3.0.1": version: 3.0.6 resolution: "fast-uri@npm:3.0.6" @@ -13612,6 +14476,28 @@ __metadata: languageName: node linkType: hard +"fbjs-css-vars@npm:^1.0.0": + version: 1.0.2 + resolution: "fbjs-css-vars@npm:1.0.2" + checksum: 10c0/dfb64116b125a64abecca9e31477b5edb9a2332c5ffe74326fe36e0a72eef7fc8a49b86adf36c2c293078d79f4524f35e80f5e62546395f53fb7c9e69821f54f + languageName: node + linkType: hard + +"fbjs@npm:^3.0.4": + version: 3.0.5 + resolution: "fbjs@npm:3.0.5" + dependencies: + cross-fetch: "npm:^3.1.5" + fbjs-css-vars: "npm:^1.0.0" + loose-envify: "npm:^1.0.0" + object-assign: "npm:^4.1.0" + promise: "npm:^7.1.1" + setimmediate: "npm:^1.0.5" + ua-parser-js: "npm:^1.0.35" + checksum: 10c0/66d0a2fc9a774f9066e35ac2ac4bf1245931d27f3ac287c7d47e6aa1fc152b243c2109743eb8f65341e025621fb51a12038fadb9fd8fda2e3ddae04ebab06f91 + languageName: node + linkType: hard + "fd-slicer@npm:~1.1.0": version: 1.1.0 resolution: "fd-slicer@npm:1.1.0" @@ -13621,7 +14507,7 @@ __metadata: languageName: node linkType: hard -"fdir@npm:^6.4.4": +"fdir@npm:^6.4.4, fdir@npm:^6.4.6": version: 6.4.6 resolution: "fdir@npm:6.4.6" peerDependencies: @@ -13760,6 +14646,17 @@ __metadata: languageName: node linkType: hard +"find-cache-dir@npm:^3.3.2": + version: 3.3.2 + resolution: "find-cache-dir@npm:3.3.2" + dependencies: + commondir: "npm:^1.0.1" + make-dir: "npm:^3.0.2" + pkg-dir: "npm:^4.1.0" + checksum: 10c0/92747cda42bff47a0266b06014610981cfbb71f55d60f2c8216bc3108c83d9745507fb0b14ecf6ab71112bed29cd6fb1a137ee7436179ea36e11287e3159e587 + languageName: node + linkType: hard + "find-chrome-bin@npm:2.0.2": version: 2.0.2 resolution: "find-chrome-bin@npm:2.0.2" @@ -13778,6 +14675,13 @@ __metadata: languageName: node linkType: hard +"find-root@npm:^1.1.0": + version: 1.1.0 + resolution: "find-root@npm:1.1.0" + checksum: 10c0/1abc7f3bf2f8d78ff26d9e00ce9d0f7b32e5ff6d1da2857bcdf4746134c422282b091c672cde0572cac3840713487e0a7a636af9aa1b74cb11894b447a521efa + languageName: node + linkType: hard + "find-up@npm:^3.0.0": version: 3.0.0 resolution: "find-up@npm:3.0.0" @@ -13807,6 +14711,13 @@ __metadata: languageName: node linkType: hard +"find-value@npm:^1.0.12": + version: 1.0.13 + resolution: "find-value@npm:1.0.13" + checksum: 10c0/54846d5ed6925d1aaba8f95b82537e0aad23e48ca779a0bd4e11a1de7fd55c09bf9c5efcaa9037a7d7d865e969409143bf6ad248369b7149c7849ea2d9d62d15 + languageName: node + linkType: hard + "find-yarn-workspace-root@npm:^2.0.0": version: 2.0.0 resolution: "find-yarn-workspace-root@npm:2.0.0" @@ -14015,6 +14926,17 @@ __metadata: languageName: node linkType: hard +"fs-extra@npm:^11.2.0": + version: 11.3.0 + resolution: "fs-extra@npm:11.3.0" + dependencies: + graceful-fs: "npm:^4.2.0" + jsonfile: "npm:^6.0.1" + universalify: "npm:^2.0.0" + checksum: 10c0/5f95e996186ff45463059feb115a22fb048bdaf7e487ecee8a8646c78ed8fdca63630e3077d4c16ce677051f5e60d3355a06f3cd61f3ca43f48cc58822a44d0a + languageName: node + linkType: hard + "fs-extra@npm:^7.0.0, fs-extra@npm:^7.0.1": version: 7.0.1 resolution: "fs-extra@npm:7.0.1" @@ -14449,6 +15371,19 @@ __metadata: languageName: node linkType: hard +"globby@npm:^13.1.4": + version: 13.2.2 + resolution: "globby@npm:13.2.2" + dependencies: + dir-glob: "npm:^3.0.1" + fast-glob: "npm:^3.3.0" + ignore: "npm:^5.2.4" + merge2: "npm:^1.4.1" + slash: "npm:^4.0.0" + checksum: 10c0/a8d7cc7cbe5e1b2d0f81d467bbc5bc2eac35f74eaded3a6c85fc26d7acc8e6de22d396159db8a2fc340b8a342e74cac58de8f4aee74146d3d146921a76062664 + languageName: node + linkType: hard + "gopd@npm:^1.0.1, gopd@npm:^1.2.0": version: 1.2.0 resolution: "gopd@npm:1.2.0" @@ -14925,6 +15860,13 @@ __metadata: languageName: node linkType: hard +"hyphenate-style-name@npm:^1.0.3": + version: 1.1.0 + resolution: "hyphenate-style-name@npm:1.1.0" + checksum: 10c0/bfe88deac2414a41a0d08811e277c8c098f23993d6a1eb17f14a0f11b54c4d42865a63d3cfe1914668eefb9a188e2de58f38b55a179a238fd1fef606893e194f + languageName: node + linkType: hard + "i18n-iso-countries@npm:^7.13.0": version: 7.14.0 resolution: "i18n-iso-countries@npm:7.14.0" @@ -14959,7 +15901,7 @@ __metadata: languageName: node linkType: hard -"ignore@npm:^5.0.5, ignore@npm:^5.1.1, ignore@npm:^5.2.0, ignore@npm:^5.3.1": +"ignore@npm:^5.0.5, ignore@npm:^5.1.1, ignore@npm:^5.2.0, ignore@npm:^5.2.4, ignore@npm:^5.3.1": version: 5.3.2 resolution: "ignore@npm:5.3.2" checksum: 10c0/f9f652c957983634ded1e7f02da3b559a0d4cc210fca3792cb67f1b153623c9c42efdc1c4121af171e295444459fc4a9201101fb041b1104a3c000bccb188337 @@ -15075,6 +16017,25 @@ __metadata: languageName: node linkType: hard +"inline-style-prefixer@npm:^6.0.1": + version: 6.0.4 + resolution: "inline-style-prefixer@npm:6.0.4" + dependencies: + css-in-js-utils: "npm:^3.1.0" + fast-loops: "npm:^1.1.3" + checksum: 10c0/d3d42bf0c48d621ea4bcfb077b5d370b106995422300a3a472674f96c9b489d96b4aac6f29dea3bb26ff2dfd7293e4752098bc2b53407769eafdb66c6c4c1764 + languageName: node + linkType: hard + +"inline-style-prefixer@npm:^7.0.1": + version: 7.0.1 + resolution: "inline-style-prefixer@npm:7.0.1" + dependencies: + css-in-js-utils: "npm:^3.1.0" + checksum: 10c0/15da5a396b7f286b5b6742efe315218cd577bc96b43de08aeb76af7697d9f1ab3bfc66cf19fad2173957dd5d617a790240b9d51898bdcf4c2efb40d3f8bcb370 + languageName: node + linkType: hard + "int64-buffer@npm:^0.1.9": version: 0.1.10 resolution: "int64-buffer@npm:0.1.10" @@ -15390,6 +16351,13 @@ __metadata: languageName: node linkType: hard +"is-primitive@npm:^3.0.1": + version: 3.0.1 + resolution: "is-primitive@npm:3.0.1" + checksum: 10c0/2e3b6f029fabbdda467ea51ea4fdd00e6552434108b863a08f296638072c506a7c195089e3e31f83e7fc14bebbd1c5c9f872fe127c9284a7665c8227b47ffdd6 + languageName: node + linkType: hard + "is-regex@npm:^1.2.1": version: 1.2.1 resolution: "is-regex@npm:1.2.1" @@ -15604,6 +16572,13 @@ __metadata: languageName: node linkType: hard +"iterate-object@npm:^1.3.4": + version: 1.3.5 + resolution: "iterate-object@npm:1.3.5" + checksum: 10c0/ee24365493f5ec812906b8cb851c9342040e15419138f8b70b2d38b1759f5f3c016943da11c2d83ad0328de455861280d64ad3bee8a2f1a597f3bd12b4806a71 + languageName: node + linkType: hard + "iterator.prototype@npm:^1.1.4": version: 1.1.5 resolution: "iterator.prototype@npm:1.1.5" @@ -16859,7 +17834,7 @@ __metadata: languageName: node linkType: hard -"lottie-react@npm:^2.4.0": +"lottie-react@npm:^2.4.0, lottie-react@npm:^2.4.1": version: 2.4.1 resolution: "lottie-react@npm:2.4.1" dependencies: @@ -16968,6 +17943,15 @@ __metadata: languageName: node linkType: hard +"make-dir@npm:^3.0.2": + version: 3.1.0 + resolution: "make-dir@npm:3.1.0" + dependencies: + semver: "npm:^6.0.0" + checksum: 10c0/56aaafefc49c2dfef02c5c95f9b196c4eb6988040cf2c712185c7fe5c99b4091591a7fc4d4eafaaefa70ff763a26f6ab8c3ff60b9e75ea19876f49b18667ecaa + languageName: node + linkType: hard + "make-dir@npm:^4.0.0": version: 4.0.0 resolution: "make-dir@npm:4.0.0" @@ -17072,6 +18056,13 @@ __metadata: languageName: node linkType: hard +"memoize-one@npm:^6.0.0": + version: 6.0.0 + resolution: "memoize-one@npm:6.0.0" + checksum: 10c0/45c88e064fd715166619af72e8cf8a7a17224d6edf61f7a8633d740ed8c8c0558a4373876c9b8ffc5518c2b65a960266adf403cc215cb1e90f7e262b58991f54 + languageName: node + linkType: hard + "memorystream@npm:^0.3.1": version: 0.3.1 resolution: "memorystream@npm:0.3.1" @@ -18181,7 +19172,7 @@ __metadata: languageName: node linkType: hard -"node-fetch@npm:^2.2.0, node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.7": +"node-fetch@npm:^2.2.0, node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.7, node-fetch@npm:^2.7.0": version: 2.7.0 resolution: "node-fetch@npm:2.7.0" dependencies: @@ -18600,6 +19591,13 @@ __metadata: languageName: node linkType: hard +"outdent@npm:^0.8.0": + version: 0.8.0 + resolution: "outdent@npm:0.8.0" + checksum: 10c0/d8a6c38b838b7ac23ebf1cc50442312f4efe286b211dbe5c71fa84d5daa2512fb94a8f2df1389313465acb0b4e5fa72270dd78f519f3d4db5bc22b2762c86827 + languageName: node + linkType: hard + "own-keys@npm:^1.0.1": version: 1.0.1 resolution: "own-keys@npm:1.0.1" @@ -18611,6 +19609,39 @@ __metadata: languageName: node linkType: hard +"oxc-transform@npm:^0.47.1": + version: 0.47.1 + resolution: "oxc-transform@npm:0.47.1" + dependencies: + "@oxc-transform/binding-darwin-arm64": "npm:0.47.1" + "@oxc-transform/binding-darwin-x64": "npm:0.47.1" + "@oxc-transform/binding-linux-arm64-gnu": "npm:0.47.1" + "@oxc-transform/binding-linux-arm64-musl": "npm:0.47.1" + "@oxc-transform/binding-linux-x64-gnu": "npm:0.47.1" + "@oxc-transform/binding-linux-x64-musl": "npm:0.47.1" + "@oxc-transform/binding-win32-arm64-msvc": "npm:0.47.1" + "@oxc-transform/binding-win32-x64-msvc": "npm:0.47.1" + dependenciesMeta: + "@oxc-transform/binding-darwin-arm64": + optional: true + "@oxc-transform/binding-darwin-x64": + optional: true + "@oxc-transform/binding-linux-arm64-gnu": + optional: true + "@oxc-transform/binding-linux-arm64-musl": + optional: true + "@oxc-transform/binding-linux-x64-gnu": + optional: true + "@oxc-transform/binding-linux-x64-musl": + optional: true + "@oxc-transform/binding-win32-arm64-msvc": + optional: true + "@oxc-transform/binding-win32-x64-msvc": + optional: true + checksum: 10c0/b33ce8c54d9deb8827c35eefd022eac2522ddd486e8f3b9e07ef8f448b063094246f329207f4b631622734e3025c3bd128a208d1e6b38ba08e98282ec7312855 + languageName: node + linkType: hard + "p-cancelable@npm:^2.0.0": version: 2.1.1 resolution: "p-cancelable@npm:2.1.1" @@ -18951,7 +19982,7 @@ __metadata: languageName: node linkType: hard -"pkg-dir@npm:^4.2.0": +"pkg-dir@npm:^4.1.0, pkg-dir@npm:^4.2.0": version: 4.2.0 resolution: "pkg-dir@npm:4.2.0" dependencies: @@ -19055,6 +20086,24 @@ __metadata: languageName: node linkType: hard +"postcss-value-parser@npm:^4.2.0": + version: 4.2.0 + resolution: "postcss-value-parser@npm:4.2.0" + checksum: 10c0/f4142a4f56565f77c1831168e04e3effd9ffcc5aebaf0f538eee4b2d465adfd4b85a44257bb48418202a63806a7da7fe9f56c330aebb3cac898e46b4cbf49161 + languageName: node + linkType: hard + +"postcss@npm:^8.5.6": + version: 8.5.6 + resolution: "postcss@npm:8.5.6" + dependencies: + nanoid: "npm:^3.3.11" + picocolors: "npm:^1.1.1" + source-map-js: "npm:^1.2.1" + checksum: 10c0/5127cc7c91ed7a133a1b7318012d8bfa112da9ef092dddf369ae699a1f10ebbd89b1b9f25f3228795b84585c72aabd5ced5fc11f2ba467eedf7b081a66fad024 + languageName: node + linkType: hard + "postinstall-postinstall@npm:^2.1.0": version: 2.1.0 resolution: "postinstall-postinstall@npm:2.1.0" @@ -19188,6 +20237,15 @@ __metadata: languageName: node linkType: hard +"promise@npm:^7.1.1": + version: 7.3.1 + resolution: "promise@npm:7.3.1" + dependencies: + asap: "npm:~2.0.3" + checksum: 10c0/742e5c0cc646af1f0746963b8776299701ad561ce2c70b49365d62c8db8ea3681b0a1bf0d4e2fe07910bf72f02d39e51e8e73dc8d7503c3501206ac908be107f + languageName: node + linkType: hard + "promise@npm:^8.0.0, promise@npm:^8.3.0": version: 8.3.0 resolution: "promise@npm:8.3.0" @@ -19372,6 +20430,15 @@ __metadata: languageName: node linkType: hard +"r-json@npm:^1.2.10": + version: 1.3.1 + resolution: "r-json@npm:1.3.1" + dependencies: + w-json: "npm:1.3.10" + checksum: 10c0/af0196bb4ff3371ee1dc3671a10eed722e89579930e3a647fb334d5f8d5d4b3e8f4a309bbd08e4741a2b33c311713f8904f2a8400c0b7737b5556dd367cc8c3f + languageName: node + linkType: hard + "r1csfile@npm:0.0.40": version: 0.0.40 resolution: "r1csfile@npm:0.0.40" @@ -19475,7 +20542,7 @@ __metadata: languageName: node linkType: hard -"react-dom@npm:^18.0.0": +"react-dom@npm:^18.0.0, react-dom@npm:^18.3.1": version: 18.3.1 resolution: "react-dom@npm:18.3.1" dependencies: @@ -19767,6 +20834,17 @@ __metadata: languageName: node linkType: hard +"react-native-svg-web@npm:^1.0.9": + version: 1.0.9 + resolution: "react-native-svg-web@npm:1.0.9" + peerDependencies: + prop-types: "*" + react: "*" + react-native-web: ">= 0.10.1" + checksum: 10c0/9c1d76f7dd8892106dcb15b47c2f005a7c4b33f4cada19a224b54edcc5f63069f52fe51ac307d4dedc241b71ff42e91e5e15a04c46925c04170154044b458fe4 + languageName: node + linkType: hard + "react-native-svg@npm:^15.11.1": version: 15.12.0 resolution: "react-native-svg@npm:15.12.0" @@ -19781,6 +20859,44 @@ __metadata: languageName: node linkType: hard +"react-native-web@npm:^0.19.0": + version: 0.19.13 + resolution: "react-native-web@npm:0.19.13" + dependencies: + "@babel/runtime": "npm:^7.18.6" + "@react-native/normalize-colors": "npm:^0.74.1" + fbjs: "npm:^3.0.4" + inline-style-prefixer: "npm:^6.0.1" + memoize-one: "npm:^6.0.0" + nullthrows: "npm:^1.1.1" + postcss-value-parser: "npm:^4.2.0" + styleq: "npm:^0.1.3" + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + checksum: 10c0/55e82a6f656843b2b4f6e4c4006a82ae8feed548e880e9fa3c2623da415d3abd9399c91c5360b71d5f24f47c5cbe30872a3ad785fa1a32cf152383d595f8ebd5 + languageName: node + linkType: hard + +"react-native-web@npm:^0.20.0": + version: 0.20.0 + resolution: "react-native-web@npm:0.20.0" + dependencies: + "@babel/runtime": "npm:^7.18.6" + "@react-native/normalize-colors": "npm:^0.74.1" + fbjs: "npm:^3.0.4" + inline-style-prefixer: "npm:^7.0.1" + memoize-one: "npm:^6.0.0" + nullthrows: "npm:^1.1.1" + postcss-value-parser: "npm:^4.2.0" + styleq: "npm:^0.1.3" + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + checksum: 10c0/266c16c67ccc4114864cf4facac14c3736412c937af8cf031eaaa618e801723f2c4aac5bf2d680536bbbe95602b97c13a819e775602884e900dd1362bbe2f3f5 + languageName: node + linkType: hard + "react-native@npm:*": version: 0.80.0 resolution: "react-native@npm:0.80.0" @@ -19887,6 +21003,19 @@ __metadata: languageName: node linkType: hard +"react-qr-barcode-scanner@npm:^2.1.7": + version: 2.1.7 + resolution: "react-qr-barcode-scanner@npm:2.1.7" + dependencies: + "@zxing/library": "npm:^0.21.3" + react-webcam: "npm:^7.2.0" + peerDependencies: + react: ">=18.0.0" + react-dom: ">=18.0.0" + checksum: 10c0/6b2e63dbbcbfd0f2ab9c05686bf7220a600cc1ab0130250b26b86bdcfa639e7bae49e7dbe0e8a5a6a8d709f0cd7ac529409dd4795344b60d42c07bdba2b07e94 + languageName: node + linkType: hard + "react-refresh@npm:^0.14.0": version: 0.14.2 resolution: "react-refresh@npm:0.14.2" @@ -19980,6 +21109,23 @@ __metadata: languageName: node linkType: hard +"react-webcam@npm:^7.2.0": + version: 7.2.0 + resolution: "react-webcam@npm:7.2.0" + peerDependencies: + react: ">=16.2.0" + react-dom: ">=16.2.0" + checksum: 10c0/d639a9e4cd545f66a5ecbfdc6c38344f6ae40e6609ba5fb26db730b48723158dedeeecdecba94e4d117517f4db0be828169db270c329b9b71d847e54aba6f7f0 + languageName: node + linkType: hard + +"react@npm:*": + version: 19.1.0 + resolution: "react@npm:19.1.0" + checksum: 10c0/530fb9a62237d54137a13d2cfb67a7db6a2156faed43eecc423f4713d9b20c6f2728b026b45e28fcd72e8eadb9e9ed4b089e99f5e295d2f0ad3134251bdd3698 + languageName: node + linkType: hard + "react@npm:^18.0.0, react@npm:^18.3.1": version: 18.3.1 resolution: "react@npm:18.3.1" @@ -20468,7 +21614,7 @@ __metadata: languageName: node linkType: hard -"rollup@npm:^4.34.8": +"rollup@npm:^4.34.8, rollup@npm:^4.40.0": version: 4.44.0 resolution: "rollup@npm:4.44.0" dependencies: @@ -20728,7 +21874,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^6.3.0, semver@npm:^6.3.1": +"semver@npm:^6.0.0, semver@npm:^6.3.0, semver@npm:^6.3.1": version: 6.3.1 resolution: "semver@npm:6.3.1" bin: @@ -20737,7 +21883,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.1.2, semver@npm:^7.1.3, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.7.1, semver@npm:^7.7.2": +"semver@npm:^7.1.2, semver@npm:^7.1.3, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.5.1, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.7.1, semver@npm:^7.7.2": version: 7.7.2 resolution: "semver@npm:7.7.2" bin: @@ -20839,6 +21985,16 @@ __metadata: languageName: node linkType: hard +"set-value@npm:^4.1.0": + version: 4.1.0 + resolution: "set-value@npm:4.1.0" + dependencies: + is-plain-object: "npm:^2.0.4" + is-primitive: "npm:^3.0.1" + checksum: 10c0/dc186676b6cc0cfcf1656b8acdfe7a68591f0645dd2872250100817fb53e5e9298dc1727a95605ac03f82110e9b3820c90a0a02d84e0fb89f210922b08b37e02 + languageName: node + linkType: hard + "setimmediate@npm:^1.0.5": version: 1.0.5 resolution: "setimmediate@npm:1.0.5" @@ -21047,6 +22203,13 @@ __metadata: languageName: node linkType: hard +"slash@npm:^4.0.0": + version: 4.0.0 + resolution: "slash@npm:4.0.0" + checksum: 10c0/b522ca75d80d107fd30d29df0549a7b2537c83c4c4ecd12cd7d4ea6c8aaca2ab17ada002e7a1d78a9d736a0261509f26ea5b489082ee443a3a810586ef8eff18 + languageName: node + linkType: hard + "slice-ansi@npm:^2.0.0": version: 2.1.0 resolution: "slice-ansi@npm:2.1.0" @@ -21261,7 +22424,7 @@ __metadata: languageName: node linkType: hard -"source-map-js@npm:^1.0.1": +"source-map-js@npm:^1.0.1, source-map-js@npm:^1.2.1": version: 1.2.1 resolution: "source-map-js@npm:1.2.1" checksum: 10c0/7bda1fc4c197e3c6ff17de1b8b2c20e60af81b63a52cb32ec5a5d67a20a7d42651e2cb34ebe93833c5a2a084377e17455854fee3e21e7925c64a51b6a52b0faf @@ -21754,6 +22917,13 @@ __metadata: languageName: node linkType: hard +"styleq@npm:^0.1.3": + version: 0.1.3 + resolution: "styleq@npm:0.1.3" + checksum: 10c0/975d951792e65052f1f6e41aaad46492642ce4922b3dc36d4b49b37c8509f9a776794d8f275360f00116a5e6ab1e31514bdcd5840656c4e3213da6803fa12941 + languageName: node + linkType: hard + "sucrase@npm:^3.35.0": version: 3.35.0 resolution: "sucrase@npm:3.35.0" @@ -21904,7 +23074,7 @@ __metadata: languageName: node linkType: hard -"table@npm:^6.8.0": +"table@npm:^6.8.0, table@npm:^6.8.1": version: 6.9.0 resolution: "table@npm:6.9.0" dependencies: @@ -22242,7 +23412,7 @@ __metadata: languageName: node linkType: hard -"tinyglobby@npm:^0.2.11, tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.6": +"tinyglobby@npm:^0.2.11, tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.14, tinyglobby@npm:^0.2.6": version: 0.2.14 resolution: "tinyglobby@npm:0.2.14" dependencies: @@ -22389,6 +23559,13 @@ __metadata: languageName: node linkType: hard +"ts-custom-error@npm:^3.2.1": + version: 3.3.1 + resolution: "ts-custom-error@npm:3.3.1" + checksum: 10c0/67cc807d03406d7eeb2b908408d455d253cc43b480d669e2e3ea028b9aaa2c78ad1b392425b86368bbbc3c43a15e4abe304312d87ed845091646ec1937bab982 + languageName: node + linkType: hard + "ts-essentials@npm:^7.0.1": version: 7.0.3 resolution: "ts-essentials@npm:7.0.3" @@ -22531,7 +23708,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.3.1, tslib@npm:^2.4.0, tslib@npm:^2.6.3, tslib@npm:^2.8.1": +"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.3.1, tslib@npm:^2.4.0, tslib@npm:^2.6.3, tslib@npm:^2.8.0, tslib@npm:^2.8.1": version: 2.8.1 resolution: "tslib@npm:2.8.1" checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62 @@ -22683,6 +23860,13 @@ __metadata: languageName: node linkType: hard +"type-fest@npm:^4.30.0": + version: 4.41.0 + resolution: "type-fest@npm:4.41.0" + checksum: 10c0/f5ca697797ed5e88d33ac8f1fec21921839871f808dc59345c9cf67345bfb958ce41bd821165dbf3ae591cedec2bf6fe8882098dfdd8dc54320b859711a2c1e4 + languageName: node + linkType: hard + "typechain@npm:^8.3.2": version: 8.3.2 resolution: "typechain@npm:8.3.2" @@ -22787,7 +23971,7 @@ __metadata: languageName: node linkType: hard -"typescript@npm:^5.1.6, typescript@npm:^5.3.3, typescript@npm:^5.4.5, typescript@npm:^5.8.3": +"typescript@npm:^5.1.6, typescript@npm:^5.3.3, typescript@npm:^5.4.5, typescript@npm:^5.8.2, typescript@npm:^5.8.3": version: 5.8.3 resolution: "typescript@npm:5.8.3" bin: @@ -22807,7 +23991,7 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@npm%3A^5.1.6#optional!builtin, typescript@patch:typescript@npm%3A^5.3.3#optional!builtin, typescript@patch:typescript@npm%3A^5.4.5#optional!builtin, typescript@patch:typescript@npm%3A^5.8.3#optional!builtin": +"typescript@patch:typescript@npm%3A^5.1.6#optional!builtin, typescript@patch:typescript@npm%3A^5.3.3#optional!builtin, typescript@patch:typescript@npm%3A^5.4.5#optional!builtin, typescript@patch:typescript@npm%3A^5.8.2#optional!builtin, typescript@patch:typescript@npm%3A^5.8.3#optional!builtin": version: 5.8.3 resolution: "typescript@patch:typescript@npm%3A5.8.3#optional!builtin::version=5.8.3&hash=5786d5" bin: @@ -22831,6 +24015,15 @@ __metadata: languageName: node linkType: hard +"ua-parser-js@npm:^1.0.35": + version: 1.0.40 + resolution: "ua-parser-js@npm:1.0.40" + bin: + ua-parser-js: script/cli.js + checksum: 10c0/2b6ac642c74323957dae142c31f72287f2420c12dced9603d989b96c132b80232779c429b296d7de4012ef8b64e0d8fadc53c639ef06633ce13d785a78b5be6c + languageName: node + linkType: hard + "ufo@npm:^1.5.4": version: 1.6.1 resolution: "ufo@npm:1.6.1" @@ -23178,6 +24371,74 @@ __metadata: languageName: node linkType: hard +"vite-plugin-svgr@npm:^4.3.0": + version: 4.3.0 + resolution: "vite-plugin-svgr@npm:4.3.0" + dependencies: + "@rollup/pluginutils": "npm:^5.1.3" + "@svgr/core": "npm:^8.1.0" + "@svgr/plugin-jsx": "npm:^8.1.0" + peerDependencies: + vite: ">=2.6.0" + checksum: 10c0/a73f10d319f72cd8c16bf9701cf18170f2300f98c72c6bf939565de0b1e93916bd70c6f5a446dc034b4405c72d382655c7c16be4bd1cbf35bbcde5febf7aeffc + languageName: node + linkType: hard + +"vite@npm:^7.0.0": + version: 7.0.0 + resolution: "vite@npm:7.0.0" + dependencies: + esbuild: "npm:^0.25.0" + fdir: "npm:^6.4.6" + fsevents: "npm:~2.3.3" + picomatch: "npm:^4.0.2" + postcss: "npm:^8.5.6" + rollup: "npm:^4.40.0" + tinyglobby: "npm:^0.2.14" + peerDependencies: + "@types/node": ^20.19.0 || >=22.12.0 + jiti: ">=1.21.0" + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: ">=0.54.8" + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + dependenciesMeta: + fsevents: + optional: true + peerDependenciesMeta: + "@types/node": + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + bin: + vite: bin/vite.js + checksum: 10c0/860838d223f877dd8e04bd2b8f33cf67a38706643bdf07e3153e2857d7c0d33c3ee94cea7e86e60937cc91b3793272912cc7af14565641476f814bd61b3a1374 + languageName: node + linkType: hard + "vlq@npm:^1.0.0": version: 1.0.1 resolution: "vlq@npm:1.0.1" @@ -23185,6 +24446,20 @@ __metadata: languageName: node linkType: hard +"w-json@npm:1.3.10": + version: 1.3.10 + resolution: "w-json@npm:1.3.10" + checksum: 10c0/441bf7685d8c8d9ff787066d75c66f7a66794a90ea7577d9e94103089427f04117176b40bdf4def505c5f90ee3e65a4f569765754e866f688d68e4521da1fa94 + languageName: node + linkType: hard + +"w-json@npm:^1.3.10": + version: 1.3.11 + resolution: "w-json@npm:1.3.11" + checksum: 10c0/6c17753971668a3de9fa6c41a5cce31419643c7ccf7d0ffdf7e0f758ec767e20ea8cc91d08f619b88e3fef2dc0ee1d280241ffcc77800e1b042d8e645ebe5c02 + languageName: node + linkType: hard + "walker@npm:^1.0.7, walker@npm:^1.0.8": version: 1.0.8 resolution: "walker@npm:1.0.8"