diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..aa05e0f --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +APP_RELEASE_STORE_PASSWORD= +APP_RELEASE_KEY_PASSWORD= +FIREBASE_FUNCTIONS_URL= + +# Debug tokens (only for local development) +APPCHECK_DEBUG_TOKEN_ANDROID= +APPCHECK_DEBUG_TOKEN_IOS= \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2ce29e3..1817ba1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -90,6 +90,11 @@ jobs: workload_identity_provider: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }} create_credentials_file: true + # Add this step before the Android build + - name: Create .env file + run: | + echo "FIREBASE_FUNCTIONS_URL=${{ vars.FIREBASE_FUNCTIONS_URL }}" > .env + # Step 10: Build and upload Android app to Alpha track (includes building APK and Bundle) - name: Build and upload Android app working-directory: android @@ -100,6 +105,7 @@ jobs: GRADLE_USER_HOME: ${{ runner.temp }}/.gradle # GOOGLE_APPLICATION_CREDENTIALS: ${{ steps.auth.outputs.credentials_file_path }} # This is not supported by fastlane, we need to replace it with PLAY_STORE_JSON_KEY in the future. PLAY_STORE_JSON_KEY: ${{ secrets.PLAY_STORE_SERVICE_ACCOUNT_JSON }} + GOOGLE_SERVICES_JSON: ${{ secrets.GOOGLE_SERVICES_JSON }} run: | echo "$PLAY_STORE_JSON_KEY" > play-store-key.json bundle exec fastlane release_android_alpha @@ -151,6 +157,11 @@ jobs: working-directory: ios run: pod install + # Add this step before the iOS build + - name: Create .env file + run: | + echo "FIREBASE_FUNCTIONS_URL=${{ vars.FIREBASE_FUNCTIONS_URL }}" > .env + # Step 6: Build and upload iOS app to TestFlight - name: Build and upload iOS app working-directory: ios @@ -162,4 +173,5 @@ jobs: APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_ISSUER_ID }} APP_STORE_CONNECT_API_KEY_CONTENT: ${{ secrets.APP_STORE_CONNECT_API_KEY_CONTENT }} APP_STORE_CONNECT_USER_ID: ${{ secrets.APP_STORE_CONNECT_USER_ID }} - run: bundle exec fastlane release_ios + GOOGLE_SERVICES_PLIST: ${{ secrets.GOOGLE_SERVICES_PLIST }} + run: bundle exec fastlane release_ios_testflight diff --git a/.gitignore b/.gitignore index f7951bc..ea6e9e2 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,12 @@ package-lock.json # testing /coverage .env + +# Firebase config files +android/app/google-services.json +ios/GoogleService-Info.plist + +# Environment files +.env +.env.* +!.env.example diff --git a/App.tsx b/App.tsx index 966c58a..f38b442 100644 --- a/App.tsx +++ b/App.tsx @@ -15,9 +15,13 @@ import { import {useTheme} from './src/hooks'; import {modelStore} from './src/store'; -import {HeaderRight, SidebarContent} from './src/components'; -import {ChatScreen, ModelsScreen, SettingsScreen} from './src/screens'; -import {ModelsHeaderRight} from './src/components'; +import {HeaderRight, SidebarContent, ModelsHeaderRight} from './src/components'; +import { + ChatScreen, + ModelsScreen, + SettingsScreen, + BenchmarkScreen, +} from './src/screens'; const Drawer = createDrawerNavigator(); @@ -74,6 +78,10 @@ const App = observer(() => { name="Settings" component={gestureHandlerRootHOC(SettingsScreen)} /> + diff --git a/__mocks__/external/@react-native-firebase/app-check.js b/__mocks__/external/@react-native-firebase/app-check.js new file mode 100644 index 0000000..2abe733 --- /dev/null +++ b/__mocks__/external/@react-native-firebase/app-check.js @@ -0,0 +1,11 @@ +const mockAppCheck = { + newReactNativeFirebaseAppCheckProvider: jest.fn().mockReturnValue({ + configure: jest.fn(), + }), + initializeAppCheck: jest.fn(), + getToken: jest.fn().mockResolvedValue({token: 'mock-token'}), +}; + +export default () => ({ + appCheck: () => mockAppCheck, +}); diff --git a/__mocks__/external/@react-native-firebase/app.js b/__mocks__/external/@react-native-firebase/app.js new file mode 100644 index 0000000..35ac590 --- /dev/null +++ b/__mocks__/external/@react-native-firebase/app.js @@ -0,0 +1,11 @@ +const mockFirebase = { + appCheck: jest.fn().mockReturnValue({ + newReactNativeFirebaseAppCheckProvider: jest.fn().mockReturnValue({ + configure: jest.fn(), + }), + initializeAppCheck: jest.fn(), + getToken: jest.fn().mockResolvedValue({token: 'mock-token'}), + }), +}; + +export default mockFirebase; diff --git a/__mocks__/external/react-native-device-info.js b/__mocks__/external/react-native-device-info.js index 1d204ed..2bcd605 100644 --- a/__mocks__/external/react-native-device-info.js +++ b/__mocks__/external/react-native-device-info.js @@ -10,5 +10,11 @@ export default { getUsedMemory: jest.fn(() => deviceInfo.usedMemory), getVersion: jest.fn(() => deviceInfo.version), getBuildNumber: jest.fn(() => deviceInfo.buildNumber), + isEmulator: jest.fn(() => false), + getBrand: jest.fn(() => 'Apple'), + getDevice: jest.fn(() => 'iPhone 12'), + getDeviceId: jest.fn(() => 'test-device-id'), + supportedAbis: jest.fn(() => ['arm64', 'arm64-v8a']), + // Not all methods are mocked, add any other methods from react-native-device-info that you use in your code }; diff --git a/__mocks__/stores/benchmarkStore.ts b/__mocks__/stores/benchmarkStore.ts new file mode 100644 index 0000000..9a942d8 --- /dev/null +++ b/__mocks__/stores/benchmarkStore.ts @@ -0,0 +1,39 @@ +import {BenchmarkResult} from '../../src/utils/types'; +import {mockResult} from '../../jest/fixtures/benchmark'; + +// Create mock functions +const mockRemoveResult = jest.fn((timestamp: string) => { + benchmarkStore.results = benchmarkStore.results.filter( + result => result.timestamp !== timestamp, + ); +}); + +const mockClearResults = jest.fn(() => { + benchmarkStore.results = []; +}); + +const mockAddResult = jest.fn((result: BenchmarkResult) => { + benchmarkStore.results.unshift(result); +}); + +const mockMarkAsSubmitted = jest.fn((uuid: string) => { + const result = benchmarkStore.results.find(r => r.uuid === uuid); + if (result) { + result.submitted = true; + } +}); + +const mockGetResultsByModel = jest.fn((modelId: string): BenchmarkResult[] => { + return benchmarkStore.results.filter(result => result.modelId === modelId); +}); + +// Define the mockBenchmarkStore +export const benchmarkStore = { + results: [mockResult], + addResult: mockAddResult, + removeResult: mockRemoveResult, + clearResults: mockClearResults, + markAsSubmitted: mockMarkAsSubmitted, + getResultsByModel: mockGetResultsByModel, + latestResult: mockResult, +}; diff --git a/__mocks__/stores/modelStore.ts b/__mocks__/stores/modelStore.ts index f15a2d8..0b45776 100644 --- a/__mocks__/stores/modelStore.ts +++ b/__mocks__/stores/modelStore.ts @@ -95,6 +95,10 @@ class MockModelStore { get activeModel() { return this.models.find(model => model.id === this.activeModelId); } + + get availableModels() { + return this.models.filter(model => model.isDownloaded); + } } export const mockModelStore = new MockModelStore(); diff --git a/__mocks__/stores/uiStore.ts b/__mocks__/stores/uiStore.ts index 80c9074..1e1b799 100644 --- a/__mocks__/stores/uiStore.ts +++ b/__mocks__/stores/uiStore.ts @@ -8,6 +8,9 @@ export class UIStore { export const mockUiStore = { colorScheme: 'light', autoNavigatetoChat: false, + benchmarkShareDialog: { + shouldShow: true, + }, pageStates: { modelsScreen: { filters: [], @@ -21,4 +24,5 @@ export const mockUiStore = { setAutoNavigateToChat: jest.fn(), setColorScheme: jest.fn(), setDisplayMemUsage: jest.fn(), + setBenchmarkShareDialogPreference: jest.fn(), }; diff --git a/android/app/build.gradle b/android/app/build.gradle index ce4f38f..89dff61 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -2,6 +2,11 @@ apply plugin: "com.android.application" apply plugin: "org.jetbrains.kotlin.android" apply plugin: "com.facebook.react" +// Add the Google Services plugin dependency +// This is used exclusively for sending benchmarks (with user consent) to Hugging Face Spaces via Firebase. +// Firebase is used for App Check functionality, allowing unauthenticated users to submit their benchmark data securely. +apply plugin: "com.google.gms.google-services" + apply from: "../../node_modules/react-native-config/android/dotenv.gradle" /** @@ -128,6 +133,10 @@ dependencies { } else { implementation jscFlavor } + + implementation platform('com.google.firebase:firebase-bom:32.7.0') + implementation 'com.google.firebase:firebase-appcheck-playintegrity' + implementation 'com.google.firebase:firebase-appcheck-debug' } // apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project) diff --git a/android/app/src/main/java/com/pocketpalai/DeviceInfoModule.kt b/android/app/src/main/java/com/pocketpalai/DeviceInfoModule.kt new file mode 100644 index 0000000..6c04326 --- /dev/null +++ b/android/app/src/main/java/com/pocketpalai/DeviceInfoModule.kt @@ -0,0 +1,87 @@ +package com.pocketpal + +import com.facebook.react.bridge.* +import android.os.Build +import java.io.File + +class DeviceInfoModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { + + override fun getName(): String = "DeviceInfoModule" + + @ReactMethod + fun getChipset(promise: Promise) { + try { + val chipset = Build.HARDWARE.takeUnless { it.isNullOrEmpty() } ?: Build.BOARD + promise.resolve(chipset) + } catch (e: Exception) { + promise.reject("ERROR", e.message) + } + } + + @ReactMethod + fun getCPUInfo(promise: Promise) { + try { + val cpuInfo = Arguments.createMap() + cpuInfo.putInt("cores", Runtime.getRuntime().availableProcessors()) + + val processors = Arguments.createArray() + val features = mutableSetOf() + val cpuInfoFile = File("/proc/cpuinfo") + + if (cpuInfoFile.exists()) { + val cpuInfoLines = cpuInfoFile.readLines() + var currentProcessor = Arguments.createMap() + var hasData = false + + for (line in cpuInfoLines) { + if (line.isEmpty() && hasData) { + processors.pushMap(currentProcessor) + currentProcessor = Arguments.createMap() + hasData = false + continue + } + + val parts = line.split(":") + if (parts.size >= 2) { + val key = parts[0].trim() + val value = parts[1].trim() + when (key) { + "processor", "model name", "cpu MHz", "vendor_id" -> { + currentProcessor.putString(key, value) + hasData = true + } + "flags", "Features" -> { // "flags" for x86, "Features" for ARM + features.addAll(value.split(" ").filter { it.isNotEmpty() }) + } + } + } + } + + if (hasData) { + processors.pushMap(currentProcessor) + } + + cpuInfo.putArray("processors", processors) + + // Convert features set to array + val featuresArray = Arguments.createArray() + features.forEach { featuresArray.pushString(it) } + cpuInfo.putArray("features", featuresArray) + + // ML-related CPU features detection + cpuInfo.putBoolean("hasFp16", features.any { it in setOf("fphp", "fp16") }) + cpuInfo.putBoolean("hasDotProd", features.any { it in setOf("dotprod", "asimddp") }) + cpuInfo.putBoolean("hasSve", features.any { it == "sve" }) + cpuInfo.putBoolean("hasI8mm", features.any { it == "i8mm" }) + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + cpuInfo.putString("socModel", Build.SOC_MODEL) + } + + promise.resolve(cpuInfo) + } catch (e: Exception) { + promise.reject("ERROR", e.message) + } + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/pocketpalai/DeviceInfoPackage.kt b/android/app/src/main/java/com/pocketpalai/DeviceInfoPackage.kt new file mode 100644 index 0000000..5bde93f --- /dev/null +++ b/android/app/src/main/java/com/pocketpalai/DeviceInfoPackage.kt @@ -0,0 +1,18 @@ +package com.pocketpal + +import android.view.View +import com.facebook.react.ReactPackage +import com.facebook.react.bridge.NativeModule +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.uimanager.ReactShadowNode +import com.facebook.react.uimanager.ViewManager + +class DeviceInfoPackage : ReactPackage { + override fun createNativeModules(reactContext: ReactApplicationContext): List { + return listOf(DeviceInfoModule(reactContext)) + } + + override fun createViewManagers(reactContext: ReactApplicationContext): List>> { + return emptyList() + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/pocketpalai/MainApplication.kt b/android/app/src/main/java/com/pocketpalai/MainApplication.kt index 29ae317..3b41d7f 100644 --- a/android/app/src/main/java/com/pocketpalai/MainApplication.kt +++ b/android/app/src/main/java/com/pocketpalai/MainApplication.kt @@ -20,6 +20,7 @@ class MainApplication : Application(), ReactApplication { PackageList(this).packages.apply { // Packages that cannot be autolinked yet can be added manually here, for example: // add(MyReactNativePackage()) + add(DeviceInfoPackage()) } override fun getJSMainModuleName(): String = "index" diff --git a/android/build.gradle b/android/build.gradle index 2320d03..cef1f61 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -15,6 +15,11 @@ buildscript { classpath("com.android.tools.build:gradle") classpath("com.facebook.react:react-native-gradle-plugin") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin") + + // Add the Google Services plugin dependency + // This is used exclusively for sending benchmarks (with user consent) to Hugging Face Spaces via Firebase. + // Firebase is used for App Check functionality, allowing unauthenticated users to submit their benchmark data securely. + classpath 'com.google.gms:google-services:4.4.2' } } diff --git a/android/fastlane/Fastfile b/android/fastlane/Fastfile index 105bda9..d23affe 100644 --- a/android/fastlane/Fastfile +++ b/android/fastlane/Fastfile @@ -5,6 +5,12 @@ platform :android do lane :release_android_alpha do android_dir = File.expand_path('..', Dir.pwd) + # Create google-services.json from GitHub secret + File.write( + File.join(android_dir, "app/google-services.json"), + ENV["GOOGLE_SERVICES_JSON"] + ) + # Build APK for GitHub Release gradle( task: "assemble", diff --git a/babel.config.js b/babel.config.js index 02c7d13..0121cb3 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,4 +1,7 @@ module.exports = { presets: ['module:@react-native/babel-preset'], - plugins: ['react-native-reanimated/plugin'], + plugins: [ + 'react-native-reanimated/plugin', + ['module:react-native-dotenv', {moduleName: '@env'}], + ], }; diff --git a/env.d.ts b/env.d.ts new file mode 100644 index 0000000..b9b6935 --- /dev/null +++ b/env.d.ts @@ -0,0 +1,5 @@ +declare module '@env' { + export const FIREBASE_FUNCTIONS_URL: string; + export const APPCHECK_DEBUG_TOKEN_ANDROID: string; + export const APPCHECK_DEBUG_TOKEN_IOS: string; +} diff --git a/ios/PocketPal.xcodeproj/project.pbxproj b/ios/PocketPal.xcodeproj/project.pbxproj index a2905b5..a5b5c1b 100644 --- a/ios/PocketPal.xcodeproj/project.pbxproj +++ b/ios/PocketPal.xcodeproj/project.pbxproj @@ -12,9 +12,12 @@ 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 1C5077855E738169D7580281 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 56D1BD968C32BD43D7C02874 /* PrivacyInfo.xcprivacy */; }; - 36B03A5A5CA4CC82C7DB800B /* libPods-PocketPal.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 2F375E195CC1974E75C558FD /* libPods-PocketPal.a */; }; - 6F48BC249E5E59B72A562202 /* libPods-PocketPal-PocketPalTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D27E0293B9D8C211B881E4C4 /* libPods-PocketPal-PocketPalTests.a */; }; + 727402CF6DC5EB1F1535F1CD /* Pods_PocketPal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 896B2626BF410A9AA45D7B31 /* Pods_PocketPal.framework */; }; 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; + A8894CE22D0E02AF00FA6CAC /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = A8894CE12D0E02AF00FA6CAC /* GoogleService-Info.plist */; }; + A88DF87F2D0B6F7400239E77 /* DeviceInfoModule.h in Sources */ = {isa = PBXBuildFile; fileRef = A88DF87D2D0B6F7400239E77 /* DeviceInfoModule.h */; }; + A88DF8802D0B6F7400239E77 /* DeviceInfoModule.m in Sources */ = {isa = PBXBuildFile; fileRef = A88DF87E2D0B6F7400239E77 /* DeviceInfoModule.m */; }; + D43BDE0A12091D9D4BAD3976 /* Pods_PocketPal_PocketPalTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F19633F294D822DA3DDFEA88 /* Pods_PocketPal_PocketPalTests.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -39,13 +42,16 @@ 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = PocketPal/Images.xcassets; sourceTree = ""; }; 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = PocketPal/Info.plist; sourceTree = ""; }; 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = PocketPal/main.m; sourceTree = ""; }; - 2F375E195CC1974E75C558FD /* libPods-PocketPal.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-PocketPal.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - 56D1BD968C32BD43D7C02874 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = PocketPal/PrivacyInfo.xcprivacy; sourceTree = ""; }; + 56D1BD968C32BD43D7C02874 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = PocketPal/PrivacyInfo.xcprivacy; sourceTree = ""; }; 7ED25E60B577DABD90D17D15 /* Pods-PocketPal.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PocketPal.release.xcconfig"; path = "Target Support Files/Pods-PocketPal/Pods-PocketPal.release.xcconfig"; sourceTree = ""; }; 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = PocketPal/LaunchScreen.storyboard; sourceTree = ""; }; - D27E0293B9D8C211B881E4C4 /* libPods-PocketPal-PocketPalTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-PocketPal-PocketPalTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 896B2626BF410A9AA45D7B31 /* Pods_PocketPal.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_PocketPal.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A8894CE12D0E02AF00FA6CAC /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; + A88DF87D2D0B6F7400239E77 /* DeviceInfoModule.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = DeviceInfoModule.h; path = PocketPal/DeviceInfoModule.h; sourceTree = ""; }; + A88DF87E2D0B6F7400239E77 /* DeviceInfoModule.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = DeviceInfoModule.m; path = PocketPal/DeviceInfoModule.m; sourceTree = ""; }; EB8DF3A7DF5AEC7AECAC8163 /* Pods-PocketPal-PocketPalTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PocketPal-PocketPalTests.release.xcconfig"; path = "Target Support Files/Pods-PocketPal-PocketPalTests/Pods-PocketPal-PocketPalTests.release.xcconfig"; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; + F19633F294D822DA3DDFEA88 /* Pods_PocketPal_PocketPalTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_PocketPal_PocketPalTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; F6E78A942BFFAF1A00968174 /* MaterialCommunityIcons.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = MaterialCommunityIcons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/MaterialCommunityIcons.ttf"; sourceTree = ""; }; F6FDF0F62BF10215004D619B /* MaterialIcons.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = MaterialIcons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/MaterialIcons.ttf"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -55,7 +61,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 6F48BC249E5E59B72A562202 /* libPods-PocketPal-PocketPalTests.a in Frameworks */, + D43BDE0A12091D9D4BAD3976 /* Pods_PocketPal_PocketPalTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -63,7 +69,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 36B03A5A5CA4CC82C7DB800B /* libPods-PocketPal.a in Frameworks */, + 727402CF6DC5EB1F1535F1CD /* Pods_PocketPal.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -98,6 +104,9 @@ 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */, 13B07FB71A68108700A75B9A /* main.m */, 56D1BD968C32BD43D7C02874 /* PrivacyInfo.xcprivacy */, + A88DF87E2D0B6F7400239E77 /* DeviceInfoModule.m */, + A88DF87D2D0B6F7400239E77 /* DeviceInfoModule.h */, + A8894CE12D0E02AF00FA6CAC /* GoogleService-Info.plist */, ); name = PocketPal; sourceTree = ""; @@ -106,8 +115,8 @@ isa = PBXGroup; children = ( ED297162215061F000B7C4FE /* JavaScriptCore.framework */, - 2F375E195CC1974E75C558FD /* libPods-PocketPal.a */, - D27E0293B9D8C211B881E4C4 /* libPods-PocketPal-PocketPalTests.a */, + 896B2626BF410A9AA45D7B31 /* Pods_PocketPal.framework */, + F19633F294D822DA3DDFEA88 /* Pods_PocketPal_PocketPalTests.framework */, ); name = Frameworks; sourceTree = ""; @@ -198,6 +207,7 @@ 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, D6CD7534C543272FC89109BC /* [CP] Embed Pods Frameworks */, E4C990C8DEAE1FDF91912E22 /* [CP] Copy Pods Resources */, + C32649D6F9C183EF9AB4414B /* [CP-User] [RNFB] Core Configuration */, ); buildRules = ( ); @@ -257,6 +267,7 @@ buildActionMask = 2147483647; files = ( 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */, + A8894CE22D0E02AF00FA6CAC /* GoogleService-Info.plist in Resources */, 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, 1C5077855E738169D7580281 /* PrivacyInfo.xcprivacy in Resources */, ); @@ -320,6 +331,19 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; + C32649D6F9C183EF9AB4414B /* [CP-User] [RNFB] Core Configuration */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)", + ); + name = "[CP-User] [RNFB] Core Configuration"; + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "#!/usr/bin/env bash\n#\n# Copyright (c) 2016-present Invertase Limited & Contributors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this library except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n##########################################################################\n##########################################################################\n#\n# NOTE THAT IF YOU CHANGE THIS FILE YOU MUST RUN pod install AFTERWARDS\n#\n# This file is installed as an Xcode build script in the project file\n# by cocoapods, and you will not see your changes until you pod install\n#\n##########################################################################\n##########################################################################\n\nset -e\n\n_MAX_LOOKUPS=2;\n_SEARCH_RESULT=''\n_RN_ROOT_EXISTS=''\n_CURRENT_LOOKUPS=1\n_JSON_ROOT=\"'react-native'\"\n_JSON_FILE_NAME='firebase.json'\n_JSON_OUTPUT_BASE64='e30=' # { }\n_CURRENT_SEARCH_DIR=${PROJECT_DIR}\n_PLIST_BUDDY=/usr/libexec/PlistBuddy\n_TARGET_PLIST=\"${BUILT_PRODUCTS_DIR}/${INFOPLIST_PATH}\"\n_DSYM_PLIST=\"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist\"\n\n# plist arrays\n_PLIST_ENTRY_KEYS=()\n_PLIST_ENTRY_TYPES=()\n_PLIST_ENTRY_VALUES=()\n\nfunction setPlistValue {\n echo \"info: setting plist entry '$1' of type '$2' in file '$4'\"\n ${_PLIST_BUDDY} -c \"Add :$1 $2 '$3'\" $4 || echo \"info: '$1' already exists\"\n}\n\nfunction getFirebaseJsonKeyValue () {\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n ruby -Ku -e \"require 'rubygems';require 'json'; output=JSON.parse('$1'); puts output[$_JSON_ROOT]['$2']\"\n else\n echo \"\"\n fi;\n}\n\nfunction jsonBoolToYesNo () {\n if [[ $1 == \"false\" ]]; then\n echo \"NO\"\n elif [[ $1 == \"true\" ]]; then\n echo \"YES\"\n else echo \"NO\"\n fi\n}\n\necho \"info: -> RNFB build script started\"\necho \"info: 1) Locating ${_JSON_FILE_NAME} file:\"\n\nif [[ -z ${_CURRENT_SEARCH_DIR} ]]; then\n _CURRENT_SEARCH_DIR=$(pwd)\nfi;\n\nwhile true; do\n _CURRENT_SEARCH_DIR=$(dirname \"$_CURRENT_SEARCH_DIR\")\n if [[ \"$_CURRENT_SEARCH_DIR\" == \"/\" ]] || [[ ${_CURRENT_LOOKUPS} -gt ${_MAX_LOOKUPS} ]]; then break; fi;\n echo \"info: ($_CURRENT_LOOKUPS of $_MAX_LOOKUPS) Searching in '$_CURRENT_SEARCH_DIR' for a ${_JSON_FILE_NAME} file.\"\n _SEARCH_RESULT=$(find \"$_CURRENT_SEARCH_DIR\" -maxdepth 2 -name ${_JSON_FILE_NAME} -print | /usr/bin/head -n 1)\n if [[ ${_SEARCH_RESULT} ]]; then\n echo \"info: ${_JSON_FILE_NAME} found at $_SEARCH_RESULT\"\n break;\n fi;\n _CURRENT_LOOKUPS=$((_CURRENT_LOOKUPS+1))\ndone\n\nif [[ ${_SEARCH_RESULT} ]]; then\n _JSON_OUTPUT_RAW=$(cat \"${_SEARCH_RESULT}\")\n _RN_ROOT_EXISTS=$(ruby -Ku -e \"require 'rubygems';require 'json'; output=JSON.parse('$_JSON_OUTPUT_RAW'); puts output[$_JSON_ROOT]\" || echo '')\n\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n if ! python3 --version >/dev/null 2>&1; then echo \"python3 not found, firebase.json file processing error.\" && exit 1; fi\n _JSON_OUTPUT_BASE64=$(python3 -c 'import json,sys,base64;print(base64.b64encode(bytes(json.dumps(json.loads(open('\"'${_SEARCH_RESULT}'\"', '\"'rb'\"').read())['${_JSON_ROOT}']), '\"'utf-8'\"')).decode())' || echo \"e30=\")\n fi\n\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n\n # config.app_data_collection_default_enabled\n _APP_DATA_COLLECTION_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"app_data_collection_default_enabled\")\n if [[ $_APP_DATA_COLLECTION_ENABLED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseDataCollectionDefaultEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_APP_DATA_COLLECTION_ENABLED\")\")\n fi\n\n # config.analytics_auto_collection_enabled\n _ANALYTICS_AUTO_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_auto_collection_enabled\")\n if [[ $_ANALYTICS_AUTO_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AUTO_COLLECTION\")\")\n fi\n\n # config.analytics_collection_deactivated\n _ANALYTICS_DEACTIVATED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_collection_deactivated\")\n if [[ $_ANALYTICS_DEACTIVATED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_DEACTIVATED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_DEACTIVATED\")\")\n fi\n\n # config.analytics_idfv_collection_enabled\n _ANALYTICS_IDFV_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_idfv_collection_enabled\")\n if [[ $_ANALYTICS_IDFV_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_IDFV_COLLECTION_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_IDFV_COLLECTION\")\")\n fi\n\n # config.analytics_default_allow_analytics_storage\n _ANALYTICS_STORAGE=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_analytics_storage\")\n if [[ $_ANALYTICS_STORAGE ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_ANALYTICS_STORAGE\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_STORAGE\")\")\n fi\n\n # config.analytics_default_allow_ad_storage\n _ANALYTICS_AD_STORAGE=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_storage\")\n if [[ $_ANALYTICS_AD_STORAGE ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_STORAGE\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AD_STORAGE\")\")\n fi\n\n # config.analytics_default_allow_ad_user_data\n _ANALYTICS_AD_USER_DATA=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_user_data\")\n if [[ $_ANALYTICS_AD_USER_DATA ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_USER_DATA\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AD_USER_DATA\")\")\n fi\n\n # config.analytics_default_allow_ad_personalization_signals\n _ANALYTICS_PERSONALIZATION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_personalization_signals\")\n if [[ $_ANALYTICS_PERSONALIZATION ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_PERSONALIZATION_SIGNALS\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_PERSONALIZATION\")\")\n fi\n\n # config.analytics_registration_with_ad_network_enabled\n _ANALYTICS_REGISTRATION_WITH_AD_NETWORK=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"google_analytics_registration_with_ad_network_enabled\")\n if [[ $_ANALYTICS_REGISTRATION_WITH_AD_NETWORK ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_REGISTRATION_WITH_AD_NETWORK_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_REGISTRATION_WITH_AD_NETWORK\")\")\n fi\n\n # config.google_analytics_automatic_screen_reporting_enabled\n _ANALYTICS_AUTO_SCREEN_REPORTING=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"google_analytics_automatic_screen_reporting_enabled\")\n if [[ $_ANALYTICS_AUTO_SCREEN_REPORTING ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseAutomaticScreenReportingEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AUTO_SCREEN_REPORTING\")\")\n fi\n\n # config.perf_auto_collection_enabled\n _PERF_AUTO_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"perf_auto_collection_enabled\")\n if [[ $_PERF_AUTO_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"firebase_performance_collection_enabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_PERF_AUTO_COLLECTION\")\")\n fi\n\n # config.perf_collection_deactivated\n _PERF_DEACTIVATED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"perf_collection_deactivated\")\n if [[ $_PERF_DEACTIVATED ]]; then\n _PLIST_ENTRY_KEYS+=(\"firebase_performance_collection_deactivated\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_PERF_DEACTIVATED\")\")\n fi\n\n # config.messaging_auto_init_enabled\n _MESSAGING_AUTO_INIT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"messaging_auto_init_enabled\")\n if [[ $_MESSAGING_AUTO_INIT ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseMessagingAutoInitEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_MESSAGING_AUTO_INIT\")\")\n fi\n\n # config.in_app_messaging_auto_colllection_enabled\n _FIAM_AUTO_INIT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"in_app_messaging_auto_collection_enabled\")\n if [[ $_FIAM_AUTO_INIT ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseInAppMessagingAutomaticDataCollectionEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_FIAM_AUTO_INIT\")\")\n fi\n\n # config.app_check_token_auto_refresh\n _APP_CHECK_TOKEN_AUTO_REFRESH=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"app_check_token_auto_refresh\")\n if [[ $_APP_CHECK_TOKEN_AUTO_REFRESH ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseAppCheckTokenAutoRefreshEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_APP_CHECK_TOKEN_AUTO_REFRESH\")\")\n fi\n\n # config.crashlytics_disable_auto_disabler - undocumented for now - mainly for debugging, document if becomes useful\n _CRASHLYTICS_AUTO_DISABLE_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"crashlytics_disable_auto_disabler\")\n if [[ $_CRASHLYTICS_AUTO_DISABLE_ENABLED == \"true\" ]]; then\n echo \"Disabled Crashlytics auto disabler.\" # do nothing\n else\n _PLIST_ENTRY_KEYS+=(\"FirebaseCrashlyticsCollectionEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"NO\")\n fi\nelse\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n echo \"warning: A firebase.json file was not found, whilst this file is optional it is recommended to include it to configure firebase services in React Native Firebase.\"\nfi;\n\necho \"info: 2) Injecting Info.plist entries: \"\n\n# Log out the keys we're adding\nfor i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n echo \" -> $i) ${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\"\ndone\n\nfor plist in \"${_TARGET_PLIST}\" \"${_DSYM_PLIST}\" ; do\n if [[ -f \"${plist}\" ]]; then\n\n # paths with spaces break the call to setPlistValue. temporarily modify\n # the shell internal field separator variable (IFS), which normally\n # includes spaces, to consist only of line breaks\n oldifs=$IFS\n IFS=\"\n\"\n\n for i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n setPlistValue \"${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\" \"${plist}\"\n done\n\n # restore the original internal field separator value\n IFS=$oldifs\n else\n echo \"warning: A Info.plist build output file was not found (${plist})\"\n fi\ndone\n\necho \"info: <- RNFB build script finished\"\n"; + }; CBCE96C73D0E43A5B779B642 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -408,6 +432,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + A88DF87F2D0B6F7400239E77 /* DeviceInfoModule.h in Sources */, + A88DF8802D0B6F7400239E77 /* DeviceInfoModule.m in Sources */, 13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */, 13B07FC11A68108700A75B9A /* main.m in Sources */, ); @@ -624,6 +650,17 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers/react/nativemodule/core", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers/platform/ios", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", + "${PODS_CONFIGURATION_BUILD_DIR}/React-NativeModulesApple/React_NativeModulesApple.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers/react/renderer/graphics/platform/ios", + ); IPHONEOS_DEPLOYMENT_TARGET = 13.4; LD_RUNPATH_SEARCH_PATHS = ( /usr/lib/swift, @@ -700,6 +737,17 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers/react/nativemodule/core", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers/platform/ios", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", + "${PODS_CONFIGURATION_BUILD_DIR}/React-NativeModulesApple/React_NativeModulesApple.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers/react/renderer/graphics/platform/ios", + ); IPHONEOS_DEPLOYMENT_TARGET = 13.4; LD_RUNPATH_SEARCH_PATHS = ( /usr/lib/swift, diff --git a/ios/PocketPal/AppDelegate.mm b/ios/PocketPal/AppDelegate.mm index 97929b9..f4c2dff 100644 --- a/ios/PocketPal/AppDelegate.mm +++ b/ios/PocketPal/AppDelegate.mm @@ -1,5 +1,10 @@ #import "AppDelegate.h" +// App Check +#import "RNFBAppCheckModule.h" +#import + + #import #import @@ -7,6 +12,12 @@ @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + // Initialize Firebase + // This is used exclusively for sending model benchmarks (with user consent) to Firebase. + // Firebase is used for App Check functionality, allowing unauthenticated users to submit their benchmark data securely. + [RNFBAppCheckModule sharedInstance]; + [FIRApp configure]; + self.moduleName = @"PocketPal"; // You can add your custom initial props in the dictionary below. // They will be passed down to the ViewController used by React Native. diff --git a/ios/PocketPal/DeviceInfoModule.h b/ios/PocketPal/DeviceInfoModule.h new file mode 100644 index 0000000..69ec73f --- /dev/null +++ b/ios/PocketPal/DeviceInfoModule.h @@ -0,0 +1,4 @@ +#import + +@interface DeviceInfoModule : NSObject +@end diff --git a/ios/PocketPal/DeviceInfoModule.m b/ios/PocketPal/DeviceInfoModule.m new file mode 100644 index 0000000..dd4d8c1 --- /dev/null +++ b/ios/PocketPal/DeviceInfoModule.m @@ -0,0 +1,20 @@ +#import "DeviceInfoModule.h" +#import + +@implementation DeviceInfoModule + +RCT_EXPORT_MODULE(DeviceInfoModule); + +RCT_EXPORT_METHOD(getCPUInfo:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + @try { + NSUInteger numberOfCPUCores = [[NSProcessInfo processInfo] activeProcessorCount]; + NSDictionary *result = @{@"cores": @(numberOfCPUCores)}; + resolve(result); + } @catch (NSException *exception) { + reject(@"error_getting_cpu_info", @"Could not retrieve CPU info", nil); + } +} + +@end diff --git a/ios/PocketPal/PrivacyInfo.xcprivacy b/ios/PocketPal/PrivacyInfo.xcprivacy index 540332e..ee76a47 100644 --- a/ios/PocketPal/PrivacyInfo.xcprivacy +++ b/ios/PocketPal/PrivacyInfo.xcprivacy @@ -6,27 +6,29 @@ NSPrivacyAccessedAPIType - NSPrivacyAccessedAPICategoryFileTimestamp + NSPrivacyAccessedAPICategoryUserDefaults NSPrivacyAccessedAPITypeReasons - C617.1 - 0A2A.1 + CA92.1 + 1C8F.1 + C56D.1 NSPrivacyAccessedAPIType - NSPrivacyAccessedAPICategorySystemBootTime + NSPrivacyAccessedAPICategoryFileTimestamp NSPrivacyAccessedAPITypeReasons - 35F9.1 + C617.1 + 0A2A.1 NSPrivacyAccessedAPIType - NSPrivacyAccessedAPICategoryUserDefaults + NSPrivacyAccessedAPICategorySystemBootTime NSPrivacyAccessedAPITypeReasons - CA92.1 + 35F9.1 diff --git a/ios/Podfile b/ios/Podfile index ea05634..e8deb7f 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -12,8 +12,16 @@ linkage = ENV['USE_FRAMEWORKS'] if linkage != nil Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green use_frameworks! :linkage => linkage.to_sym +else + # Configure CocoaPods to use static frameworks required for Firebase. + # Firebase is used exclusively for sending benchmarks (with user consent) to Hugging Face Spaces. + # Firebase enables App Check functionality, ensuring unauthenticated users can securely submit their benchmark data. + use_frameworks! :linkage => :static end +# Enable Firebase as a static framework +$RNFirebaseAsStaticFramework = true + ENV['RCT_NEW_ARCH_ENABLED'] = '0' target 'PocketPal' do @@ -39,4 +47,9 @@ target 'PocketPal' do # :ccache_enabled => true ) end + + # Firebase (App Check) + pod 'Firebase', :modular_headers => true + pod 'FirebaseCore', :modular_headers => true + pod 'FirebaseAppCheck', :modular_headers => true end diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 65ba00e..7ca648d 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,4 +1,8 @@ PODS: + - AppCheckCore (11.2.0): + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/UserDefaults (~> 8.0) + - PromisesObjC (~> 2.4) - boost (1.84.0) - BVLinearGradient (2.8.3): - React-Core @@ -25,13 +29,112 @@ PODS: - ReactCommon/turbomodule/core - Yoga - FBLazyVector (0.76.3) + - Firebase (11.5.0): + - Firebase/Core (= 11.5.0) + - Firebase/AppCheck (11.5.0): + - Firebase/CoreOnly + - FirebaseAppCheck (~> 11.5.0) + - Firebase/Core (11.5.0): + - Firebase/CoreOnly + - FirebaseAnalytics (~> 11.5.0) + - Firebase/CoreOnly (11.5.0): + - FirebaseCore (= 11.5.0) + - FirebaseAnalytics (11.5.0): + - FirebaseAnalytics/AdIdSupport (= 11.5.0) + - FirebaseCore (= 11.5) + - FirebaseInstallations (~> 11.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.0) + - GoogleUtilities/MethodSwizzler (~> 8.0) + - GoogleUtilities/Network (~> 8.0) + - "GoogleUtilities/NSData+zlib (~> 8.0)" + - nanopb (~> 3.30910.0) + - FirebaseAnalytics/AdIdSupport (11.5.0): + - FirebaseCore (= 11.5) + - FirebaseInstallations (~> 11.0) + - GoogleAppMeasurement (= 11.5.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.0) + - GoogleUtilities/MethodSwizzler (~> 8.0) + - GoogleUtilities/Network (~> 8.0) + - "GoogleUtilities/NSData+zlib (~> 8.0)" + - nanopb (~> 3.30910.0) + - FirebaseAppCheck (11.5.0): + - AppCheckCore (~> 11.0) + - FirebaseAppCheckInterop (~> 11.0) + - FirebaseCore (= 11.5) + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/UserDefaults (~> 8.0) + - FirebaseAppCheckInterop (11.6.0) + - FirebaseCore (11.5.0): + - FirebaseCoreInternal (= 11.5) + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/Logger (~> 8.0) + - FirebaseCoreInternal (11.5.0): + - "GoogleUtilities/NSData+zlib (~> 8.0)" + - FirebaseInstallations (11.5.0): + - FirebaseCore (= 11.5) + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/UserDefaults (~> 8.0) + - PromisesObjC (~> 2.4) - fmt (9.1.0) - glog (0.3.5) + - GoogleAppMeasurement (11.5.0): + - GoogleAppMeasurement/AdIdSupport (= 11.5.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.0) + - GoogleUtilities/MethodSwizzler (~> 8.0) + - GoogleUtilities/Network (~> 8.0) + - "GoogleUtilities/NSData+zlib (~> 8.0)" + - nanopb (~> 3.30910.0) + - GoogleAppMeasurement/AdIdSupport (11.5.0): + - GoogleAppMeasurement/WithoutAdIdSupport (= 11.5.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.0) + - GoogleUtilities/MethodSwizzler (~> 8.0) + - GoogleUtilities/Network (~> 8.0) + - "GoogleUtilities/NSData+zlib (~> 8.0)" + - nanopb (~> 3.30910.0) + - GoogleAppMeasurement/WithoutAdIdSupport (11.5.0): + - GoogleUtilities/AppDelegateSwizzler (~> 8.0) + - GoogleUtilities/MethodSwizzler (~> 8.0) + - GoogleUtilities/Network (~> 8.0) + - "GoogleUtilities/NSData+zlib (~> 8.0)" + - nanopb (~> 3.30910.0) + - GoogleUtilities/AppDelegateSwizzler (8.0.2): + - GoogleUtilities/Environment + - GoogleUtilities/Logger + - GoogleUtilities/Network + - GoogleUtilities/Privacy + - GoogleUtilities/Environment (8.0.2): + - GoogleUtilities/Privacy + - GoogleUtilities/Logger (8.0.2): + - GoogleUtilities/Environment + - GoogleUtilities/Privacy + - GoogleUtilities/MethodSwizzler (8.0.2): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - GoogleUtilities/Network (8.0.2): + - GoogleUtilities/Logger + - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Privacy + - GoogleUtilities/Reachability + - "GoogleUtilities/NSData+zlib (8.0.2)": + - GoogleUtilities/Privacy + - GoogleUtilities/Privacy (8.0.2) + - GoogleUtilities/Reachability (8.0.2): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - GoogleUtilities/UserDefaults (8.0.2): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy - hermes-engine (0.76.3): - hermes-engine/Pre-built (= 0.76.3) - hermes-engine/Pre-built (0.76.3) - llama-rn (0.4.3-1): - React-Core + - nanopb (3.30910.0): + - nanopb/decode (= 3.30910.0) + - nanopb/encode (= 3.30910.0) + - nanopb/decode (3.30910.0) + - nanopb/encode (3.30910.0) + - PromisesObjC (2.4.0) - RCT-Folly (2024.01.01.00): - boost - DoubleConversion @@ -1636,6 +1739,13 @@ PODS: - React-Core - RNDeviceInfo (13.2.0): - React-Core + - RNFBApp (21.6.1): + - Firebase/CoreOnly (= 11.5.0) + - React-Core + - RNFBAppCheck (21.6.1): + - Firebase/AppCheck (= 11.5.0) + - React-Core + - RNFBApp - RNGestureHandler (2.21.2): - DoubleConversion - glog @@ -1819,6 +1929,9 @@ DEPENDENCIES: - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - "dr-pogodin-react-native-fs (from `../node_modules/@dr.pogodin/react-native-fs`)" - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) + - Firebase + - FirebaseAppCheck + - FirebaseCore - fmt (from `../node_modules/react-native/third-party-podspecs/fmt.podspec`) - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`) - hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) @@ -1893,6 +2006,8 @@ DEPENDENCIES: - "RNCMaskedView (from `../node_modules/@react-native-masked-view/masked-view`)" - "RNCPicker (from `../node_modules/@react-native-picker/picker`)" - RNDeviceInfo (from `../node_modules/react-native-device-info`) + - "RNFBApp (from `../node_modules/@react-native-firebase/app`)" + - "RNFBAppCheck (from `../node_modules/@react-native-firebase/app-check`)" - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) - RNReactNativeHapticFeedback (from `../node_modules/react-native-haptic-feedback`) - RNReanimated (from `../node_modules/react-native-reanimated`) @@ -1903,6 +2018,18 @@ DEPENDENCIES: SPEC REPOS: trunk: + - AppCheckCore + - Firebase + - FirebaseAnalytics + - FirebaseAppCheck + - FirebaseAppCheckInterop + - FirebaseCore + - FirebaseCoreInternal + - FirebaseInstallations + - GoogleAppMeasurement + - GoogleUtilities + - nanopb + - PromisesObjC - SocketRocket EXTERNAL SOURCES: @@ -2061,6 +2188,10 @@ EXTERNAL SOURCES: :path: "../node_modules/@react-native-picker/picker" RNDeviceInfo: :path: "../node_modules/react-native-device-info" + RNFBApp: + :path: "../node_modules/@react-native-firebase/app" + RNFBAppCheck: + :path: "../node_modules/@react-native-firebase/app-check" RNGestureHandler: :path: "../node_modules/react-native-gesture-handler" RNReactNativeHapticFeedback: @@ -2077,16 +2208,28 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/yoga" SPEC CHECKSUMS: + AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f boost: 1dca942403ed9342f98334bf4c3621f011aa7946 BVLinearGradient: cb006ba232a1f3e4f341bb62c42d1098c284da70 DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 - dr-pogodin-react-native-fs: 6c198fc213e6a8d6679885d4df5868feed77db78 + dr-pogodin-react-native-fs: 6472f94270d0a5720221bd0ffa377dad303f6e29 FBLazyVector: be7314029d6ec6b90f0f75ce1195b8130ed9ac4f + Firebase: 7a56fe4f56b5ab81b86a6822f5b8f909ae6fc7e2 + FirebaseAnalytics: 2f4a11eeb7a0e9c6fcf642d4e6aaca7fa4d38c28 + FirebaseAppCheck: 1c4adb8028cc5ec6a8d3d10f18b60293cddc45a4 + FirebaseAppCheckInterop: 347aa09a805219a31249b58fc956888e9fcb314b + FirebaseCore: 93abc05437f8064cd2bc0a53b768fb0bc5a1d006 + FirebaseCoreInternal: f47dd28ae7782e6a4738aad3106071a8fe0af604 + FirebaseInstallations: d8063d302a426d114ac531cd82b1e335a0565745 fmt: 10c6e61f4be25dc963c36bd73fc7b1705fe975be glog: 08b301085f15bcbb6ff8632a8ebaf239aae04e6a + GoogleAppMeasurement: ee5c2d2242816773fbf79e5b0563f5355ef1c315 + GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d hermes-engine: 0555a84ea495e8e3b4bde71b597cd87fbb382888 llama-rn: 740c802dbb28961bf4a94e658963bc22d81150a7 - RCT-Folly: bf5c0376ffe4dd2cf438dcf86db385df9fdce648 + nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 + PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 + RCT-Folly: 84578c8756030547307e4572ab1947de1685c599 RCTDeprecation: 2c5e1000b04ab70b53956aa498bf7442c3c6e497 RCTRequired: 5f785a001cf68a551c5f5040fb4c415672dbb481 RCTTypeSafety: 6b98db8965005d32449605c0d005ecb4fee8a0f7 @@ -2095,74 +2238,76 @@ SPEC CHECKSUMS: React-Core: 14708d0ebf0f961647347a7ae9cebcfaa75c99aa React-CoreModules: 42b62e50cb4ac9a8ca92b48b263355cebcc1a523 React-cxxreact: 846f1d9eb37a3dec19738d195d313deb7e9ee36b - React-debug: 931ca94abd6b1bcab539e356e20df788afecae8f - React-defaultsnativemodule: 6f56c95a8cd7b7fcfa6f983b19270753000c1a4e - React-domnativemodule: 42711869923d77713e8d090f489580c330ff2033 - React-Fabric: 2709bcae28c5b567337b3bad4ec1aa8b81aa1241 - React-FabricComponents: f35e150acf873109d244bd8c4535425d51f26672 - React-FabricImage: 6dd7af0496dae7924553f8b3cd2b134d86256a48 - React-featureflags: 7c7a74b65ee5a228f520b387ebfe0e8d9cecc622 - React-featureflagsnativemodule: 2e98d69110ccee1cf10acafad34041940457545c - React-graphics: ba13cb82b71ec2e0ad9dac2ae31867b5656d5a28 + React-debug: ccf1390c0984edf6f9601eb3b13383d54beb6348 + React-defaultsnativemodule: 808ea6f3dc257a6ffa3e1dbf214be4d12e56066a + React-domnativemodule: f76fdf0da6c0504fa70341a6a4e65447241c1561 + React-Fabric: ed269fc1b32f4bc15bb171083465e0ccfa38d7cd + React-FabricComponents: 20e59d87b3ed9960c26792fb7d3226dbb9831f77 + React-FabricImage: 3d179c86131f5ea09a00a6b9344919233e8139e5 + React-featureflags: ad8c56c9dd770ea485bd4f564af2878dd77ed623 + React-featureflagsnativemodule: 197f04601054c8c23940405ceb9c9ae07bf53d55 + React-graphics: 845d105c042837f6eb7fb806cf16e95901191884 React-hermes: 4bfb5c90304c4d2aa36b3404ddd00df591dcae8a - React-idlecallbacksnativemodule: 4a77976e6b6d74ebe2008ade2e46c69484a69f45 - React-ImageManager: 0464b2fac74d9d06d44abc3e85035424fc0d1a70 - React-jserrorhandler: 4c8ee0547d89d1bfe9cd740ca83d229934cc94e4 + React-idlecallbacksnativemodule: cb04ed83ee5e70bb2c5e342d8538f4c31001af01 + React-ImageManager: 4e2ead3d25b77d08e48f3c263884e8a549ce0528 + React-jserrorhandler: 6576ec21bce9547fd91fa838593c2339cd448cc0 React-jsi: 927e83b5aef299ca42842cd1ad696c4cf301d5eb React-jsiexecutor: ba6b1fbaa388a99fcfb750529fed3ce65efbda4f - React-jsinspector: 1f9f161bf0961df50d2843802a431eb4fd4059cf - React-jsitracing: f6f65398e2d58bd24523ba210b277681e9cf2ee0 + React-jsinspector: 1538d8147957b2ac38fcb0e0cc3fd3ea7d7ef179 + React-jsitracing: f61ecea3cf2e5a08a30274fa9fdf29fd637de7f3 React-logger: 2736a90a3fdaed3dab1e2e9c5a5e9b3be00c287d - React-Mapbuffer: bd6a2ffbf401f11cfc8b5e8f38acab9fe117620c - React-microtasksnativemodule: e3e161b03573a22cf4e05434fe5309c7847ef197 - react-native-blur: 3d5dd1ed2dd810b304ac3bcee9cf7d460757c89b + React-Mapbuffer: 12bbf447b326f0de8a472998eceafbdc4f43ca4e + React-microtasksnativemodule: 2841fe62a6d2fdfdd1051286394186fd1d210f99 + react-native-blur: aeda95624971b20d19c6c0fb291c411056e6d270 react-native-config: ea75335a7cca1d3326de1da384227e580a7c082e - react-native-document-picker: 530879d9e89b490f0954bcc4ab697c5b5e35d659 + react-native-document-picker: 302837be22e5e557340d1d510bddb1c658bd4475 react-native-get-random-values: d16467cf726c618e9c7a8c3c39c31faa2244bbba react-native-render-html: 5afc4751f1a98621b3009432ef84c47019dcb2bd react-native-safe-area-context: b13be9714d9771fbde0120bc519c963484de3a71 - react-native-slider: fc7f35c082abec47e341dfe43657a1c26f38db2f - React-nativeconfig: aeed6e2a8ac02b2df54476afcc7c663416c12bf7 - React-NativeModulesApple: c0783e5e21c71aa2764ac33120abc96208466fa6 + react-native-slider: 6750dca1fcfa1403dc1b4841f3f8c87c2d19f054 + React-nativeconfig: f2bddc1e6c2eb793c065b37e12c513fdea88afef + React-NativeModulesApple: 43be3b84598af78efe8fcdbad90033caef83af43 React-perflogger: 2991d4258277af250eb7a2705980ebb3ef9ba85e - React-performancetimeline: 76a55d0be7293adfce8603eafb8dbae92234352d + React-performancetimeline: d81ada298f4246ed1e093d1bfd989be707a34902 React-RCTActionSheet: c940a35d71686941ac2b96dd07bde11ea0f0c34f React-RCTAnimation: db10ffa5b463794089b5319818f1df1e0b996422 React-RCTAppDelegate: c398e067ecde4909eab8b48adf06bf03783cd335 React-RCTBlob: 2ede907cfc8039c5f5da32b3ba9e144b663c3c98 - React-RCTFabric: 9612452b193ce3daa3a05fdbeba4ffea756b17f8 + React-RCTFabric: dcb63d1d85cf6c66e85a3b827a71129de5a3dbc1 React-RCTImage: 3d6b5cc404c62d2b2cd767061a56bd48b4300f5e React-RCTLinking: 548cf5c3fe935f3d0c8df5dc3c234283df48cea6 React-RCTNetwork: 59f683fee79f9187b8ee4c270343b474ad37b42c React-RCTSettings: b14bd94b2ac87c803375a0873746ef31c7b0eead React-RCTText: 24e7a9b64341d6a1c98e1eabc5471b88c8bc61f1 React-RCTVibration: 2830b19dca45d23dc61b3c93c25fbc7b85979202 - React-rendererconsistency: 52b471890a1946991f2db81aa6867b14d93f4ea5 - React-rendererdebug: c2496eb27c25d367b7e80a8a4a1babc21b6cc64b - React-rncore: 33ea67bfd2eeaa4f4a0c9e0e8bd55e9b7ccb9faa - React-RuntimeApple: 28bcdbce4517b9428ab127d42d7b6bfeb27eb69c - React-RuntimeCore: d629b37225984a26c57803d857c8357c839f3fef + React-rendererconsistency: 678c479227d4be6b97a5cebeb891f0af66481b44 + React-rendererdebug: 73a2e636671885c26518088d7ff0e49c729270af + React-rncore: 2087c670978028c09b87e767a95a91997ddb00ab + React-RuntimeApple: ed413531c98c3437ec0d7eff82b1d8ef5f4da60e + React-RuntimeCore: 783c8e32a5281091675ac6eecbfc09b993accb77 React-runtimeexecutor: db3f17084ee7b71ab84912c527d428cc3a137841 - React-RuntimeHermes: 5d857268954776a1c6f0e0ecff1f7860806bb43b - React-runtimescheduler: 00be5844aa2c7d72fd47293e0798bb41462ecfee - React-timing: 54693ad0872f64127f7cb41675b1be4fd28ea4dc - React-utils: ee370a52b08a000963af1a60c31e6c87a70620a5 - ReactCodegen: 5148a0102fc8f0a1f9b05d955da886b44447679d - ReactCommon: b8485556b596ef2f44f59bc586113bda138fb804 + React-RuntimeHermes: 58b31fa6f2aea69fb1c0657320aca09a9eb87678 + React-runtimescheduler: a3cfaf71458952e2a9e8854cd6c79a6dc1ebe25e + React-timing: 854b47632fdd9f7d2690ea6444d01b7071d5da69 + React-utils: 5bb4b8442d3f1d068e84a9d7afdb6130035fff2f + ReactCodegen: b91032e6eb7dc1019852a3c4fe366e730299b68d + ReactCommon: 49e601e49039dc5795658db8c407b87152a6b401 RNCAsyncStorage: c91d753ede6dc21862c4922cd13f98f7cfde578e RNCClipboard: dbcf25b8f666b4685c02eeb65be981d30198e505 - RNCMaskedView: e2e87cbd6366581943892b44176e7976ff988dfd + RNCMaskedView: 972c098900ec45a5684a6785a7af405e8b0fc1e1 RNCPicker: d8662eb6615e3401acb590c44b97b2af3beb1e53 RNDeviceInfo: ae26ae45db3f9937f038a284bcd0a1db8d70db96 - RNGestureHandler: 5b24d10761754ad271b714e536c457fd89b17c54 - RNReactNativeHapticFeedback: 00ba111b82aa266bb3ee1aa576831c2ea9a9dfad - RNReanimated: 929c26a706dfe1af8feee9f2cf78004394e4dd04 - RNScreens: e21c8d32fe97737ecc30f1f21e7b6f69f341a1f5 + RNFBApp: 72b96921c64702d51eca9f3ed579c1777efd0d7e + RNFBAppCheck: 9e3bbada601e8c37fd066c04e46afdef5e4a3fed + RNGestureHandler: fc04a2ca677e1d661d94716ef96a441aa98d6c68 + RNReactNativeHapticFeedback: c873497ad3f9fa80447baa18daa9474e671d24bf + RNReanimated: f858cf8b4a9a47c8b190347c1a8dacbeab4a3255 + RNScreens: f770034511e60c71feee26f9f50a6e1cc617ef9e RNSVG: 6a529f4faed8be4ebfb00f1a29e25cb046d95e61 - RNVectorIcons: 182892e7d1a2f27b52d3c627eca5d2665a22ee28 + RNVectorIcons: 55221459225ae0fe7a5ab6b029378c79f1a82d8f SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 - Yoga: 3deb2471faa9916c8a82dda2a22d3fba2620ad37 + Yoga: 748f06daed00132bf0a051dbb2f4347660ac1a98 -PODFILE CHECKSUM: d2a83d0d0d40239616686e11a41f91da64d8831b +PODFILE CHECKSUM: e7d96885936750d384ed051dda70a66b2604e73a COCOAPODS: 1.16.2 diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index 431c6ca..cd246fd 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -2,7 +2,13 @@ default_platform :ios platform :ios do desc "Release iOS app to TestFlight" - lane :release_ios do + lane :release_ios_testflight do + # Create GoogleService-Info.plist from GitHub secret + File.write( + File.join(Dir.pwd, "../GoogleService-Info.plist"), + ENV["GOOGLE_SERVICES_PLIST"] + ) + setup_ci(force: true) api_key = app_store_connect_api_key( diff --git a/jest.config.js b/jest.config.js index 2e5e1d8..3ca5e92 100644 --- a/jest.config.js +++ b/jest.config.js @@ -41,5 +41,9 @@ module.exports = { '/__mocks__/external/@dr.pogodin/react-native-fs.js', 'react-native-haptic-feedback': '/__mocks__/external/react-native-haptic-feedback.js', + '@react-native-firebase/app': + '/__mocks__/external/@react-native-firebase/app.js', + '@react-native-firebase/app-check': + '/__mocks__/external/@react-native-firebase/app-check.js', }, }; diff --git a/jest/fixtures/benchmark.ts b/jest/fixtures/benchmark.ts new file mode 100644 index 0000000..69c143f --- /dev/null +++ b/jest/fixtures/benchmark.ts @@ -0,0 +1,70 @@ +import {BenchmarkResult, DeviceInfo} from '../../src/utils/types'; + +export const mockResult: BenchmarkResult = { + config: { + pp: 512, + tg: 256, + pl: 1, + nr: 3, + label: 'Test Config', + }, + modelDesc: 'Test Model', + modelSize: 1000000, + modelNParams: 7000000000, + ppAvg: 20.5, + ppStd: 1.2, + tgAvg: 30.5, + tgStd: 2.1, + timestamp: '2024-03-20T10:00:00.000Z', + modelId: 'test-model-id', + modelName: 'Test Model', + filename: 'test-model.gguf', + uuid: 'test-uuid', + oid: 'test-oid', + rfilename: 'test-rfilename', + peakMemoryUsage: { + total: 1000000, + used: 500000, + percentage: 50, + }, + wallTimeMs: 10000, + submitted: false, +}; + +export const mockSubmittedResult: BenchmarkResult = { + ...mockResult, + submitted: true, +}; + +export const mockDeviceInfo: DeviceInfo = { + model: 'Test Phone', + systemName: 'iOS', + systemVersion: '16.0', + brand: 'Apple', + cpuArch: ['arm64'], + isEmulator: false, + version: '16.0', + buildNumber: '20A362', + device: 'iPhone14,2', + deviceId: 'test-device-id', + totalMemory: 6144, + chipset: 'Apple A15', + cpu: 'hexa-core', + cpuDetails: { + cores: 6, + processors: [ + { + processor: '0', + 'model name': 'Apple A15', + 'cpu MHz': '3200', + vendor_id: 'Apple', + }, + ], + socModel: 'Apple A15', + features: ['fp16', 'neon'], + hasFp16: true, + hasDotProd: true, + hasSve: false, + hasI8mm: true, + }, +}; diff --git a/jest/setup.ts b/jest/setup.ts index 100b221..ccf2fa7 100644 --- a/jest/setup.ts +++ b/jest/setup.ts @@ -32,6 +32,7 @@ import {mockUiStore} from '../__mocks__/stores/uiStore'; import {mockHFStore} from '../__mocks__/stores/hfStore'; import {mockModelStore} from '../__mocks__/stores/modelStore'; import {mockChatSessionStore} from '../__mocks__/stores/chatSessionStore'; +import {benchmarkStore as mockBenchmarkStore} from '../__mocks__/stores/benchmarkStore'; jest.mock('@react-native-clipboard/clipboard', () => mockClipboard); @@ -56,6 +57,7 @@ jest.mock('../src/store', () => { uiStore: mockUiStore, chatSessionStore: mockChatSessionStore, hfStore: mockHFStore, + benchmarkStore: mockBenchmarkStore, }; }); diff --git a/package.json b/package.json index e22fe31..f16e4e3 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,8 @@ "@react-native-clipboard/clipboard": "^1.15.0", "@react-native-community/blur": "^4.4.1", "@react-native-community/slider": "^4.5.5", + "@react-native-firebase/app": "^21.6.1", + "@react-native-firebase/app-check": "^21.6.1", "@react-native-masked-view/masked-view": "^0.3.1", "@react-native-picker/picker": "^2.10.2", "@react-navigation/bottom-tabs": "^6.6.1", @@ -95,6 +97,7 @@ "patch-package": "^8.0.0", "postinstall-postinstall": "^2.1.0", "prettier": "2.8.8", + "react-native-dotenv": "^3.4.11", "react-test-renderer": "18.3.1", "typescript": "5.0.4" }, diff --git a/src/api/__tests__/benchmark.test.ts b/src/api/__tests__/benchmark.test.ts new file mode 100644 index 0000000..798ee43 --- /dev/null +++ b/src/api/__tests__/benchmark.test.ts @@ -0,0 +1,131 @@ +import axios from 'axios'; +import {submitBenchmark} from '../benchmark'; +import * as fb from '../../utils/fb'; +import {urls} from '../../config'; +import {DeviceInfo, BenchmarkResult} from '../../utils/types'; + +jest.mock('axios'); +jest.mock('../../utils/fb'); + +const mockedAxios = axios as jest.Mocked; +const mockedFb = fb as jest.Mocked; + +describe('submitBenchmark', () => { + const mockDeviceInfo: DeviceInfo = { + model: 'Test Phone', + systemName: 'iOS', + systemVersion: '16.0', + brand: 'Apple', + cpuArch: ['arm64'], + isEmulator: false, + version: '16.0', + buildNumber: '20A362', + device: 'iPhone14,2', + deviceId: 'test-device-id', + totalMemory: 6144, + chipset: 'Apple A15', + cpu: 'hexa-core', + cpuDetails: { + cores: 6, + processors: [ + { + processor: '0', + 'model name': 'Apple A15', + 'cpu MHz': '3200', + vendor_id: 'Apple', + }, + ], + socModel: 'Apple A15', + features: ['fp16', 'neon'], + hasFp16: true, + hasDotProd: true, + hasSve: false, + hasI8mm: true, + }, + }; + + const mockBenchmarkResult: BenchmarkResult = { + config: { + pp: 1, + tg: 1, + pl: 512, + nr: 3, + label: 'Test Config', + }, + modelDesc: 'Test Model', + modelSize: 1000000, + modelNParams: 7000000000, + ppAvg: 20.5, + ppStd: 1.2, + tgAvg: 30.5, + tgStd: 2.1, + timestamp: new Date().toISOString(), + modelId: 'test-model-id', + modelName: 'Test Model', + filename: 'test-model.gguf', + uuid: 'test-uuid', + }; + + const mockAppCheckToken = 'mock-app-check-token'; + const mockResponse = { + data: { + message: 'Success', + id: 123, + }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + + mockedFb.getAppCheckToken.mockResolvedValue(mockAppCheckToken); + mockedAxios.post.mockResolvedValue(mockResponse); + }); + + it('should successfully submit benchmark data', async () => { + const result = await submitBenchmark(mockDeviceInfo, mockBenchmarkResult); + + // Verify AppCheck initialization and token retrieval + expect(mockedFb.initializeAppCheck).toHaveBeenCalled(); + expect(mockedFb.getAppCheckToken).toHaveBeenCalled(); + + // Verify API call + expect(mockedAxios.post).toHaveBeenCalledWith( + urls.benchmarkSubmit(), + { + deviceInfo: mockDeviceInfo, + benchmarkResult: mockBenchmarkResult, + }, + { + headers: { + 'X-Firebase-AppCheck': mockAppCheckToken, + 'Content-Type': 'application/json', + }, + }, + ); + + // Verify response + expect(result).toEqual({ + message: 'Success', + id: 123, + }); + }); + + it('should throw error when AppCheck token is not available', async () => { + mockedFb.getAppCheckToken.mockResolvedValue(''); + + await expect( + submitBenchmark(mockDeviceInfo, mockBenchmarkResult), + ).rejects.toThrow('Failed to obtain App Check token'); + + expect(mockedAxios.post).not.toHaveBeenCalled(); + }); + + it('should handle API errors', async () => { + const error = new Error('Network error'); + mockedAxios.post.mockRejectedValue(error); + + await expect( + submitBenchmark(mockDeviceInfo, mockBenchmarkResult), + ).rejects.toThrow('Network error'); + }); +}); diff --git a/src/api/benchmark.ts b/src/api/benchmark.ts new file mode 100644 index 0000000..3cbe086 --- /dev/null +++ b/src/api/benchmark.ts @@ -0,0 +1,42 @@ +import axios from 'axios'; +import {urls} from '../config'; +import {getAppCheckToken, initializeAppCheck} from '../utils/fb'; +import {BenchmarkResult, DeviceInfo} from '../utils/types'; + +type SubmissionData = { + deviceInfo: DeviceInfo; + benchmarkResult: BenchmarkResult; +}; + +export async function submitBenchmark( + deviceInfo: DeviceInfo, + benchmarkResult: BenchmarkResult, +): Promise<{message: string; id: number}> { + try { + initializeAppCheck(); + const appCheckToken = await getAppCheckToken(); + + if (!appCheckToken) { + throw new Error('Failed to obtain App Check token'); + } + + const data: SubmissionData = { + deviceInfo, + benchmarkResult, + }; + + const response = await axios.post(urls.benchmarkSubmit(), data, { + headers: { + 'X-Firebase-AppCheck': appCheckToken, + 'Content-Type': 'application/json', + }, + }); + return response.data; + } catch (error) { + console.error('Error submitting benchmark:', error); + if (error instanceof Error) { + console.error('Error details:', error.message); + } + throw error; + } +} diff --git a/src/components/Checkbox/Checkbox.tsx b/src/components/Checkbox/Checkbox.tsx new file mode 100644 index 0000000..5e0f8b6 --- /dev/null +++ b/src/components/Checkbox/Checkbox.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import {View, TouchableOpacity} from 'react-native'; + +import {Icon} from 'react-native-paper'; + +import {useTheme} from '../../hooks'; + +import {createStyles} from './styles'; + +type Props = { + testID?: string; + checked: boolean; + onPress: () => void; + size?: number; + disabled?: boolean; +}; + +export const Checkbox: React.FC = ({ + testID, + checked, + onPress, + size = 20, + disabled = false, +}) => { + const theme = useTheme(); + const styles = createStyles(theme); + + return ( + + + {checked && ( + + )} + + + ); +}; diff --git a/src/components/Checkbox/__tests__/Checkbox.test.tsx b/src/components/Checkbox/__tests__/Checkbox.test.tsx new file mode 100644 index 0000000..bc67814 --- /dev/null +++ b/src/components/Checkbox/__tests__/Checkbox.test.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import {Checkbox} from '../Checkbox'; +import {fireEvent, render, waitFor} from '../../../../jest/test-utils'; + +describe('Checkbox', () => { + it('renders correctly in unchecked state', () => { + const onPress = jest.fn(); + const {queryByTestId} = render( + , + ); + + expect(queryByTestId('check-icon')).toBeNull(); + }); + + it('renders correctly in checked state', async () => { + const onPress = jest.fn(); + const {findByTestId} = render( + , + ); + + await waitFor(() => { + const checkIcon = findByTestId('check-icon'); + expect(checkIcon).toBeDefined(); + }); + }); + + it('calls onPress when clicked and not disabled', () => { + const onPress = jest.fn(); + const {getByTestId} = render( + , + ); + + fireEvent.press(getByTestId('checkbox')); + expect(onPress).toHaveBeenCalledTimes(1); + }); + + it('does not call onPress when disabled', () => { + const onPress = jest.fn(); + const {getByTestId} = render( + , + ); + + fireEvent.press(getByTestId('checkbox')); + expect(onPress).not.toHaveBeenCalled(); + }); +}); diff --git a/src/components/Checkbox/index.ts b/src/components/Checkbox/index.ts new file mode 100644 index 0000000..f5c939f --- /dev/null +++ b/src/components/Checkbox/index.ts @@ -0,0 +1 @@ +export * from './Checkbox'; diff --git a/src/components/Checkbox/styles.ts b/src/components/Checkbox/styles.ts new file mode 100644 index 0000000..704e350 --- /dev/null +++ b/src/components/Checkbox/styles.ts @@ -0,0 +1,21 @@ +import {StyleSheet} from 'react-native'; + +import type {Theme} from '../../utils/types'; + +export const createStyles = (theme: Theme) => + StyleSheet.create({ + checkbox: { + borderWidth: 2, + borderRadius: 4, + justifyContent: 'center', + alignItems: 'center', + }, + checkedBox: { + borderColor: theme.colors.primary, + backgroundColor: theme.colors.primary, + }, + uncheckedBox: { + borderColor: theme.colors.outline, + backgroundColor: 'transparent', + }, + }); diff --git a/src/components/Dialog/Dialog.tsx b/src/components/Dialog/Dialog.tsx index e4694cb..81381ee 100644 --- a/src/components/Dialog/Dialog.tsx +++ b/src/components/Dialog/Dialog.tsx @@ -20,9 +20,13 @@ export interface DialogAction { label: string; onPress: () => void; mode?: 'text' | 'contained' | 'outlined'; + loading?: boolean; + disabled?: boolean; + testID?: string; } interface CustomDialogProps { + testID?: string; visible: boolean; onDismiss: () => void; title: string; @@ -32,12 +36,14 @@ interface CustomDialogProps { contentStyle?: ViewStyle; scrollAreaStyle?: ViewStyle; scrollable?: boolean; + scrollableBorderShown?: boolean; dismissableBackButton?: boolean; dismissable?: boolean; avoidKeyboard?: boolean; } export const Dialog: React.FC = ({ + testID, visible, onDismiss, title, @@ -47,12 +53,13 @@ export const Dialog: React.FC = ({ contentStyle, scrollAreaStyle, scrollable = false, + scrollableBorderShown = false, dismissableBackButton = true, dismissable = true, avoidKeyboard = false, }) => { const theme = useTheme(); - const styles = createStyles(theme); + const styles = createStyles(theme, scrollableBorderShown); const [bottom, setBottom] = React.useState(0); React.useEffect(() => { @@ -144,6 +151,7 @@ export const Dialog: React.FC = ({ return ( = ({ {actions.map(action => ( diff --git a/src/components/Dialog/__tests__/Dialog.test.tsx b/src/components/Dialog/__tests__/Dialog.test.tsx new file mode 100644 index 0000000..2e239e4 --- /dev/null +++ b/src/components/Dialog/__tests__/Dialog.test.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import {fireEvent, render} from '../../../../jest/test-utils'; +import {Dialog} from '../Dialog'; +import {Text} from 'react-native'; + +describe('Dialog', () => { + const defaultProps = { + visible: true, + onDismiss: jest.fn(), + title: 'Test Dialog', + actions: [ + { + label: 'Cancel', + onPress: jest.fn(), + }, + { + label: 'OK', + onPress: jest.fn(), + }, + ], + children: Dialog content, + }; + + it('renders dialog with title and content', () => { + const {getByText} = render(); + + expect(getByText('Test Dialog')).toBeDefined(); + expect(getByText('Dialog content')).toBeDefined(); + }); + + it('renders action buttons and handles presses', () => { + const {getByText} = render(); + + const cancelButton = getByText('Cancel'); + const okButton = getByText('OK'); + + fireEvent.press(cancelButton); + expect(defaultProps.actions[0].onPress).toHaveBeenCalled(); + + fireEvent.press(okButton); + expect(defaultProps.actions[1].onPress).toHaveBeenCalled(); + }); + + it('does not render when visible is false', () => { + const {queryByText} = render(); + + expect(queryByText('Test Dialog')).toBeNull(); + }); +}); diff --git a/src/components/Dialog/styles.ts b/src/components/Dialog/styles.ts index d2db3bb..fad7fd0 100644 --- a/src/components/Dialog/styles.ts +++ b/src/components/Dialog/styles.ts @@ -4,7 +4,7 @@ import {Theme} from '../../utils/types'; const dialogHeight = Dimensions.get('window').height * 0.65; -export const createStyles = (theme: Theme) => +export const createStyles = (theme: Theme, scrollableBorderShown?: boolean) => StyleSheet.create({ dialog: { //maxHeight: '90%', @@ -20,6 +20,8 @@ export const createStyles = (theme: Theme) => dialogContent: { maxHeight: dialogHeight, paddingHorizontal: 16, + borderTopWidth: scrollableBorderShown ? 1 : 0, + borderBottomWidth: scrollableBorderShown ? 1 : 0, backgroundColor: theme.colors.surface, }, dialogScrollArea: {}, @@ -30,6 +32,6 @@ export const createStyles = (theme: Theme) => flexDirection: 'row', justifyContent: 'space-around', paddingHorizontal: 16, - paddingBottom: 8, + paddingBottom: 16, }, }); diff --git a/src/components/SidebarContent/SidebarContent.tsx b/src/components/SidebarContent/SidebarContent.tsx index e1f5453..042c404 100644 --- a/src/components/SidebarContent/SidebarContent.tsx +++ b/src/components/SidebarContent/SidebarContent.tsx @@ -62,6 +62,11 @@ export const SidebarContent: React.FC = observer( icon={'view-grid'} onPress={() => props.navigation.navigate('Models')} /> + props.navigation.navigate('Benchmark')} + /> `${HF_DOMAIN}/${modelId}/resolve/main/${filename}`, modelWebPage: (modelId: string) => `${HF_DOMAIN}/${modelId}`, + + // Benchmark Endpoint + benchmarkSubmit: () => `${FIREBASE_FUNCTIONS_URL}/api/v1/submit`, }; diff --git a/src/screens/BenchmarkScreen/BenchResultCard/BenchResultCard.tsx b/src/screens/BenchmarkScreen/BenchResultCard/BenchResultCard.tsx new file mode 100644 index 0000000..ec08f32 --- /dev/null +++ b/src/screens/BenchmarkScreen/BenchResultCard/BenchResultCard.tsx @@ -0,0 +1,194 @@ +import {View, Linking} from 'react-native'; +import React, {useState} from 'react'; + +import {Card, Text, Button, Tooltip} from 'react-native-paper'; + +import {useTheme} from '../../../hooks'; + +import {createStyles} from './styles'; + +import {formatBytes, formatNumber} from '../../../utils'; +import {BenchmarkResult} from '../../../utils/types'; + +type Props = { + result: BenchmarkResult; + onDelete: (timestamp: string) => void; + onShare: (result: BenchmarkResult) => Promise; +}; + +export const BenchResultCard = ({result, onDelete, onShare}: Props) => { + const theme = useTheme(); + const styles = createStyles(theme); + const [isSubmitting, setIsSubmitting] = useState(false); + const [submitError, setSubmitError] = useState(null); + + const handleSubmit = async () => { + setIsSubmitting(true); + setSubmitError(null); + try { + await onShare(result); + } catch (error) { + setSubmitError( + error instanceof Error ? error.message : 'Failed to submit benchmark', + ); + } finally { + setIsSubmitting(false); + } + }; + + const formatDuration = (ms: number) => { + if (ms < 1000) { + return `${ms}ms`; + } + const seconds = Math.floor(ms / 1000); + const minutes = Math.floor(seconds / 60); + if (minutes > 0) { + const remainingSeconds = seconds % 60; + return `${minutes}m ${remainingSeconds}s`; + } + return `${seconds}s`; + }; + + const openLeaderboard = () => { + Linking.openURL( + 'https://huggingface.co/spaces/a-ghorbani/ai-phone-leaderboard', + ); + }; + + return ( + + + + + + {result.modelName} + + + {formatBytes(result.modelSize)} •{' '} + {formatNumber(result.modelNParams, 2, true, false)} params + + + + + + + Config + + PP: {result.config.pp} • TG: {result.config.tg} • PL:{' '} + {result.config.pl} • Rep: {result.config.nr} + + + + + + + + {result.ppAvg.toFixed(2)} + t/s + + Prompt Processing + ±{result.ppStd.toFixed(2)} + + + + {result.tgAvg.toFixed(2)} + t/s + + Token Generation + ±{result.tgStd.toFixed(2)} + + + + {(result.wallTimeMs || result.peakMemoryUsage) && ( + + {result.wallTimeMs && ( + + + {formatDuration(result.wallTimeMs)} + + Total Time + + )} + {result.peakMemoryUsage && ( + + + {result.peakMemoryUsage.percentage.toFixed(1)}% + + Peak Memory + + {formatBytes(result.peakMemoryUsage.used, 0)} /{' '} + {formatBytes(result.peakMemoryUsage.total, 0)} + + + )} + + )} + + {new Date(result.timestamp).toLocaleString()} + + + + + {result.submitted ? ( + + + ✓ Shared to{' '} + + AI Phone Leaderboard ↗ + + + + ) : !result.oid ? ( + + + + Cannot share + + ⓘ + + + ) : result.config.label === 'Custom' ? ( + + + + Cannot share + + ⓘ + + + ) : ( + + + + View leaderboard ↗ + + + )} + + + {submitError && {submitError}} + + + ); +}; diff --git a/src/screens/BenchmarkScreen/BenchResultCard/__tests__/BenchResultCard.test.tsx b/src/screens/BenchmarkScreen/BenchResultCard/__tests__/BenchResultCard.test.tsx new file mode 100644 index 0000000..2845273 --- /dev/null +++ b/src/screens/BenchmarkScreen/BenchResultCard/__tests__/BenchResultCard.test.tsx @@ -0,0 +1,53 @@ +import React from 'react'; + +import {render} from '../../../../../jest/test-utils'; +import {mockResult} from '../../../../../jest/fixtures/benchmark'; + +import {BenchResultCard} from '../BenchResultCard'; + +import {formatNumber} from '../../../../utils'; + +describe('BenchResultCard', () => { + it('renders benchmark results correctly', () => { + const {getByText} = render( + {}} + onShare={() => Promise.resolve()} + />, + ); + + expect(getByText(mockResult.modelName)).toBeDefined(); + expect(getByText(`${mockResult.ppAvg.toFixed(2)} t/s`)).toBeDefined(); + expect(getByText(`${mockResult.tgAvg.toFixed(2)} t/s`)).toBeDefined(); + }); + + it('shows standard deviations', () => { + const {getByText} = render( + {}} + onShare={() => Promise.resolve()} + />, + ); + + expect(getByText(`±${mockResult.ppStd.toFixed(2)}`)).toBeDefined(); + expect(getByText(`±${mockResult.tgStd.toFixed(2)}`)).toBeDefined(); + }); + + it('displays model parameters and size', () => { + const {getByText} = render( + {}} + onShare={() => Promise.resolve()} + />, + ); + + expect( + getByText(formatNumber(mockResult.modelNParams, 2, true, false), { + exact: false, + }), + ).toBeDefined(); + }); +}); diff --git a/src/screens/BenchmarkScreen/BenchResultCard/index.ts b/src/screens/BenchmarkScreen/BenchResultCard/index.ts new file mode 100644 index 0000000..0ca0bbb --- /dev/null +++ b/src/screens/BenchmarkScreen/BenchResultCard/index.ts @@ -0,0 +1 @@ +export * from './BenchResultCard'; diff --git a/src/screens/BenchmarkScreen/BenchResultCard/styles.ts b/src/screens/BenchmarkScreen/BenchResultCard/styles.ts new file mode 100644 index 0000000..1c64160 --- /dev/null +++ b/src/screens/BenchmarkScreen/BenchResultCard/styles.ts @@ -0,0 +1,153 @@ +import {StyleSheet} from 'react-native'; +import type {Theme} from '../../../utils/types'; + +export const createStyles = (theme: Theme) => + StyleSheet.create({ + resultCard: { + backgroundColor: theme.colors.surface, + borderRadius: 16, + borderWidth: 1, + borderColor: theme.colors.surfaceVariant, + }, + resultHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'flex-start', + marginBottom: 16, + }, + headerLeft: { + flex: 1, + marginRight: 16, + }, + modelName: { + color: theme.colors.onSurface, + marginBottom: 4, + //fontSize: 18, + //fontWeight: '500', + }, + modelMeta: { + fontSize: 12, + color: theme.colors.onSurfaceVariant, + }, + configBar: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingVertical: 8, + marginBottom: 16, + borderTopWidth: 1, + borderBottomWidth: 1, + borderColor: theme.colors.surfaceVariant, + }, + configText: { + fontSize: 12, + color: theme.colors.onSurfaceVariant, + textAlign: 'center', + }, + resultsContainer: { + marginBottom: 16, + backgroundColor: theme.colors.surfaceVariant, + borderRadius: 12, + padding: 16, + }, + resultRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'stretch', + marginBottom: 16, + }, + resultItem: { + flex: 1, + paddingHorizontal: 8, + }, + resultValue: { + fontSize: 16, + color: theme.colors.onSurface, + fontWeight: '500', + marginBottom: 2, + }, + resultUnit: { + fontSize: 13, + color: theme.colors.onSurfaceVariant, + fontWeight: 'normal', + }, + resultLabel: { + fontSize: 11, + color: theme.colors.onSurfaceVariant, + marginBottom: 1, + letterSpacing: 0.1, + }, + resultStd: { + fontSize: 10, + color: theme.colors.onSurfaceVariant, + }, + deleteButton: { + marginTop: -8, + marginRight: -8, + }, + footer: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingTop: 12, + borderTopWidth: 1, + borderColor: theme.colors.surfaceVariant, + }, + timestamp: { + fontSize: 11, + color: theme.colors.onSurfaceVariant, + }, + submitButton: { + borderColor: theme.colors.primary, + borderRadius: 16, + }, + errorText: { + color: theme.colors.error, + marginTop: 8, + fontSize: 12, + }, + submittedText: { + color: theme.colors.primary, + fontSize: 12, + }, + tooltipContainer: { + flexDirection: 'row', + alignItems: 'center', + gap: 4, + }, + infoIcon: { + fontSize: 14, + opacity: 0.6, + }, + disabledText: { + color: theme.colors.onSurfaceVariant, + fontSize: 12, + fontStyle: 'italic', + }, + shareContainer: { + alignItems: 'center', + gap: 8, + }, + actionContainer: { + flex: 1, + alignItems: 'center', + gap: 8, + }, + shareTextContainer: { + flex: 1, + marginRight: 16, + }, + sharePrompt: { + color: theme.colors.primary, + fontWeight: '500', + marginBottom: 2, + }, + shareSubtext: { + color: theme.colors.onSurfaceVariant, + fontSize: 11, + }, + leaderboardLink: { + color: theme.colors.primary, + textDecorationLine: 'underline', + }, + }); diff --git a/src/screens/BenchmarkScreen/BenchmarkScreen.tsx b/src/screens/BenchmarkScreen/BenchmarkScreen.tsx new file mode 100644 index 0000000..4657afe --- /dev/null +++ b/src/screens/BenchmarkScreen/BenchmarkScreen.tsx @@ -0,0 +1,611 @@ +import {View, ScrollView} from 'react-native'; +import React, {useState, useCallback} from 'react'; + +import {v4 as uuidv4} from 'uuid'; +import {observer} from 'mobx-react'; +import RNDeviceInfo from 'react-native-device-info'; +import Slider from '@react-native-community/slider'; +import {SafeAreaView} from 'react-native-safe-area-context'; +import {Text, Button, Card, ActivityIndicator, Icon} from 'react-native-paper'; + +import {submitBenchmark} from '../../api/benchmark'; + +import {Menu, Dialog, Checkbox} from '../../components'; + +import {useTheme} from '../../hooks'; + +import {createStyles} from './styles'; +import {DeviceInfoCard} from './DeviceInfoCard'; +import {BenchResultCard} from './BenchResultCard'; + +import {modelStore, benchmarkStore, uiStore} from '../../store'; + +import type {DeviceInfo, Model} from '../../utils/types'; +import {BenchmarkConfig, BenchmarkResult} from '../../utils/types'; + +const DEFAULT_CONFIGS: BenchmarkConfig[] = [ + {pp: 512, tg: 128, pl: 1, nr: 3, label: 'Default'}, + {pp: 128, tg: 32, pl: 1, nr: 3, label: 'Fast'}, +]; + +const BENCHMARK_PARAMS_METADATA = { + pp: { + validation: {min: 64, max: 512}, + descriptionKey: 'Number of prompt processing tokens', + }, + tg: { + validation: {min: 32, max: 512}, + descriptionKey: 'Number of text generation tokens', + }, + pl: { + validation: {min: 1, max: 4}, + descriptionKey: 'Pipeline parallel size', + }, + nr: { + validation: {min: 1, max: 10}, + descriptionKey: 'Number of repetitions', + }, +}; + +export const BenchmarkScreen: React.FC = observer(() => { + const [isRunning, setIsRunning] = useState(false); + const [selectedConfig, setSelectedConfig] = useState( + DEFAULT_CONFIGS[0], + ); + const [showModelMenu, setShowModelMenu] = useState(false); + const [selectedModel, setSelectedModel] = useState(null); + const [localSliderValues, setLocalSliderValues] = useState<{ + [key: string]: number; + }>({}); + const [showAdvancedDialog, setShowAdvancedDialog] = useState(false); + const [deleteConfirmVisible, setDeleteConfirmVisible] = useState(false); + const [pendingDeleteTimestamp, setPendingDeleteTimestamp] = useState< + string | null + >(null); + const [deleteAllConfirmVisible, setDeleteAllConfirmVisible] = useState(false); + const [deviceInfo, setDeviceInfo] = useState(null); + const [showShareDialog, setShowShareDialog] = useState(false); + const [showDetails, setShowDetails] = useState(false); + const [dontShowAgain, setDontShowAgain] = useState(false); + const [pendingShareResult, setPendingShareResult] = + useState(null); + const [shareError, setShareError] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + const theme = useTheme(); + const styles = createStyles(theme); + + const handleModelSelect = async (model: Model) => { + setShowModelMenu(false); + if (model.id !== modelStore.activeModelId) { + try { + await modelStore.initContext(model); + setSelectedModel(model); + } catch (error) { + if (error instanceof Error) { + console.error('Model initialization error:', error); + } + } + } else { + setSelectedModel(model); + } + }; + + const handleSliderChange = (name: string, value: number) => { + setSelectedConfig(prev => ({ + ...prev, + [name]: value, + label: 'Custom', + })); + }; + + const trackPeakMemoryUsage = async () => { + try { + const total = await RNDeviceInfo.getTotalMemory(); + const used = await RNDeviceInfo.getUsedMemory(); + const percentage = (used / total) * 100; + return {total, used, percentage}; + } catch (error) { + console.error('Failed to fetch memory stats:', error); + return null; + } + }; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const stopBenchmark = async () => { + if (modelStore.context) { + try { + // TODO: This is not working for bench. + await modelStore.context.stopCompletion(); + } catch (error) { + console.error('Error stopping benchmark:', error); + } + } + }; + + const runBenchmark = async () => { + if (!modelStore.context || !modelStore.activeModel) { + return; + } + + setIsRunning(true); + let peakMemoryUsage: NonNullable< + BenchmarkResult['peakMemoryUsage'] + > | null = null; + let memoryCheckInterval: ReturnType | undefined; + const startTime = Date.now(); + + try { + // Start memory tracking + memoryCheckInterval = setInterval(async () => { + const currentUsage = await trackPeakMemoryUsage(); + if ( + currentUsage && + (!peakMemoryUsage || + currentUsage.percentage > peakMemoryUsage.percentage) + ) { + peakMemoryUsage = currentUsage; + } + }, 1000); + + const {modelDesc, modelSize, modelNParams, ppAvg, ppStd, tgAvg, tgStd} = + await modelStore.context.bench( + selectedConfig.pp, + selectedConfig.tg, + selectedConfig.pl, + selectedConfig.nr, + ); + + const wallTimeMs = Date.now() - startTime; + + const result: BenchmarkResult = { + config: selectedConfig, + modelDesc, + modelSize, + modelNParams, + ppAvg, + ppStd, + tgAvg, + tgStd, + timestamp: new Date().toISOString(), + modelId: modelStore.activeModel.id, + modelName: modelStore.activeModel.name, + oid: modelStore.activeModel.hfModelFile?.oid, + rfilename: modelStore.activeModel.hfModelFile?.rfilename, + filename: modelStore.activeModel.filename, + peakMemoryUsage: peakMemoryUsage || undefined, + wallTimeMs, + uuid: uuidv4(), + }; + + benchmarkStore.addResult(result); + } catch (error) { + if (error instanceof Error) { + console.error('Benchmark error:', error); + } + } finally { + clearInterval(memoryCheckInterval); + setIsRunning(false); + } + }; + + const handlePresetSelect = (config: BenchmarkConfig) => { + setSelectedConfig(config); + setLocalSliderValues({}); + }; + + const handleDeleteResult = (timestamp: string) => { + setPendingDeleteTimestamp(timestamp); + setDeleteConfirmVisible(true); + }; + + const handleConfirmDelete = () => { + if (pendingDeleteTimestamp) { + benchmarkStore.removeResult(pendingDeleteTimestamp); + } + setDeleteConfirmVisible(false); + setPendingDeleteTimestamp(null); + }; + + const handleDeleteAll = () => { + setDeleteAllConfirmVisible(true); + }; + + const handleConfirmDeleteAll = () => { + benchmarkStore.clearResults(); + setDeleteAllConfirmVisible(false); + }; + + const handleDeviceInfo = useCallback((info: DeviceInfo) => { + setDeviceInfo(info); + }, []); + + const handleShareResult = async (result: BenchmarkResult) => { + if (!deviceInfo) { + throw new Error('Device information not available'); + } + if (result.submitted) { + throw new Error('This benchmark has already been submitted'); + } + try { + const response = await submitBenchmark(deviceInfo, result); + console.log('Benchmark submitted successfully:', response); + benchmarkStore.markAsSubmitted(result.uuid); + } catch (error) { + console.error('Failed to submit benchmark:', error); + throw error; + } + }; + + const handleSharePress = async (result: BenchmarkResult) => { + if (!uiStore.benchmarkShareDialog.shouldShow) { + await handleShareResult(result); + return; + } + setPendingShareResult(result); + setShowShareDialog(true); + }; + + const handleConfirmShare = async () => { + if (dontShowAgain) { + uiStore.setBenchmarkShareDialogPreference(false); + } + setIsSubmitting(true); + try { + if (pendingShareResult) { + await handleShareResult(pendingShareResult); + } + setShowShareDialog(false); + setPendingShareResult(null); + } catch (error) { + setShareError( + error instanceof Error ? error.message : 'Failed to share benchmark', + ); + } finally { + setIsSubmitting(false); + } + }; + + const renderModelSelector = () => ( + setShowModelMenu(false)} + anchorPosition="bottom" + anchor={ + + }> + {modelStore.availableModels.map(model => ( + handleModelSelect(model)} + label={model.name} + leadingIcon={ + model.id === modelStore.activeModelId ? 'check' : undefined + } + /> + ))} + + ); + + const renderSlider = ({ + name, + step = 1, + testId, + }: { + name: keyof typeof BENCHMARK_PARAMS_METADATA; + step?: number; + testId?: string; + }) => ( + + + {name.toUpperCase()} + + { + setLocalSliderValues(prev => ({...prev, [name]: value})); + }} + onSlidingComplete={value => { + handleSliderChange(name, value); + }} + thumbTintColor={theme.colors.primary} + minimumTrackTintColor={theme.colors.primary} + /> + + + {BENCHMARK_PARAMS_METADATA[name].descriptionKey} + + + {Number.isInteger(step) + ? Math.round( + localSliderValues[name] ?? selectedConfig[name], + ).toString() + : (localSliderValues[name] ?? selectedConfig[name]).toFixed(2)} + + + + ); + + const renderAdvancedSettings = () => ( + setShowAdvancedDialog(false)} + title="Advanced Settings" + actions={[ + { + label: 'Done', + onPress: () => setShowAdvancedDialog(false), + }, + ]}> + + + Test Profile + + + {DEFAULT_CONFIGS.map((config, index) => ( + + ))} + + + + Custom Parameters + + + Fine-tune the benchmark parameters for specific testing scenarios. + + + {renderSlider({name: 'pp'})} + {renderSlider({name: 'tg'})} + {renderSlider({name: 'nr'})} + + + + ); + + const renderWarningMessage = () => ( + + + Note: Test could run for up to 2-5 minutes for larger models and cannot + be interrupted once started. + + + ); + + const renderShareDialog = () => ( + { + setShowShareDialog(false); + setPendingShareResult(null); + }} + title="Share Benchmark Results" + scrollable + actions={[ + { + testID: 'share-benchmark-dialog-cancel-button', + label: 'Cancel', + onPress: () => { + setShowShareDialog(false); + setPendingShareResult(null); + }, + disabled: isSubmitting, + }, + { + testID: 'share-benchmark-dialog-confirm-button', + label: isSubmitting ? 'Sharing...' : 'Share', + onPress: handleConfirmShare, + mode: 'contained', + loading: isSubmitting, + disabled: isSubmitting, + }, + ]}> + + Shared data includes: + + + • Device specs & model info + • Performance metrics + + + + + {showDetails && pendingShareResult && deviceInfo && ( + + + {JSON.stringify( + { + deviceInfo, + benchmark: pendingShareResult, + }, + null, + 2, + )} + + + )} + + {shareError && {shareError}} + + + setDontShowAgain(!dontShowAgain)} + /> + setDontShowAgain(!dontShowAgain)}> + Don't show this message again + + + + ); + + return ( + + + + + + {renderModelSelector()} + + {modelStore.loadingModel ? ( + + + Initializing model... + + ) : ( + <> + {!modelStore.context ? ( + + Please select and initialize a model first + + ) : ( + <> + + + {!isRunning && renderWarningMessage()} + + + + {isRunning && ( + + + + Please keep this screen open. + + + )} + + {renderAdvancedSettings()} + + )} + + )} + + {benchmarkStore.results.length > 0 && ( + + + Test Results + + + {benchmarkStore.results.map((result, index) => ( + + + + ))} + + )} + + setDeleteConfirmVisible(false)} + title="Delete Result" + actions={[ + { + label: 'Cancel', + onPress: () => setDeleteConfirmVisible(false), + }, + { + label: 'Delete', + onPress: handleConfirmDelete, + }, + ]}> + + Are you sure you want to delete this benchmark result? + + + + setDeleteAllConfirmVisible(false)} + title="Clear All Results" + actions={[ + { + testID: 'clear-all-dialog-cancel-button', + label: 'Cancel', + onPress: () => setDeleteAllConfirmVisible(false), + }, + { + testID: 'clear-all-dialog-confirm-button', + label: 'Clear All', + onPress: handleConfirmDeleteAll, + }, + ]}> + + Are you sure you want to delete all benchmark results? + + + + {renderShareDialog()} + + + + + ); +}); diff --git a/src/screens/BenchmarkScreen/DeviceInfoCard/DeviceInfoCard.tsx b/src/screens/BenchmarkScreen/DeviceInfoCard/DeviceInfoCard.tsx new file mode 100644 index 0000000..6ec1b65 --- /dev/null +++ b/src/screens/BenchmarkScreen/DeviceInfoCard/DeviceInfoCard.tsx @@ -0,0 +1,283 @@ +import React, {useEffect, useState} from 'react'; +import {View, TouchableOpacity} from 'react-native'; +import {Platform, NativeModules} from 'react-native'; + +import {Card, Text, Icon} from 'react-native-paper'; +import RNDeviceInfo from 'react-native-device-info'; + +import {useTheme} from '../../../hooks'; + +import {createStyles} from './styles'; + +import {DeviceInfo} from '../../../utils/types'; + +const {DeviceInfoModule} = NativeModules; + +const getChipsetInfo = async () => { + if (Platform.OS !== 'android' || !DeviceInfoModule) { + return ''; + } + try { + return await DeviceInfoModule.getChipset(); + } catch (e) { + console.warn('Failed to get chipset info:', e); + return ''; + } +}; + +const getCPUInfo = async () => { + if (!DeviceInfoModule) { + console.warn('DeviceInfoModule not available'); + return { + cores: 0, + processors: [], + features: [], + socModel: '', + hasFp16: false, + hasDotProd: false, + hasSve: false, + hasI8mm: false, + }; + } + try { + const info = await DeviceInfoModule.getCPUInfo(); + if (!info) { + return null; + } + + return Platform.OS === 'ios' + ? { + cores: info.cores || 0, + processors: [], + features: [], + socModel: '', + hasFp16: false, + hasDotProd: false, + hasSve: false, + hasI8mm: false, + } + : info; + } catch (e) { + console.warn('Failed to get CPU info:', e); + return null; + } +}; + +type Props = { + onDeviceInfo?: (info: DeviceInfo) => void; + testId?: string; +}; + +export const DeviceInfoCard = ({onDeviceInfo, testId}: Props) => { + const theme = useTheme(); + const styles = createStyles(theme); + const [deviceInfo, setDeviceInfo] = useState({ + model: RNDeviceInfo.getModel(), + systemName: Platform.OS === 'ios' ? 'iOS' : 'Android', + systemVersion: String(Platform.Version || ''), + brand: RNDeviceInfo.getBrand(), + cpuArch: [] as string[], + isEmulator: false, + version: RNDeviceInfo.getVersion(), + buildNumber: RNDeviceInfo.getBuildNumber(), + device: '', + deviceId: '', + totalMemory: 0, + chipset: '', + cpu: '', + cpuDetails: { + cores: 0, + processors: [] as Array<{ + processor: string; + 'model name': string; + 'cpu MHz': string; + vendor_id: string; + }>, + socModel: '', + features: [] as string[], + hasFp16: false, + hasDotProd: false, + hasSve: false, + hasI8mm: false, + }, + }); + const [expanded, setExpanded] = useState(false); + + useEffect(() => { + Promise.all([ + RNDeviceInfo.supportedAbis(), + RNDeviceInfo.isEmulator(), + RNDeviceInfo.getDevice(), + RNDeviceInfo.getDeviceId(), + RNDeviceInfo.getTotalMemory(), + getChipsetInfo(), + getCPUInfo(), + ]).then( + ([abis, emulator, device, deviceId, totalMem, chipset, cpuInfo]) => { + const newDeviceInfo = { + model: RNDeviceInfo.getModel(), + systemName: Platform.OS === 'ios' ? 'iOS' : 'Android', + systemVersion: String(Platform.Version || ''), + brand: RNDeviceInfo.getBrand(), + version: RNDeviceInfo.getVersion(), + buildNumber: RNDeviceInfo.getBuildNumber(), + cpuArch: abis, + isEmulator: emulator, + device, + deviceId, + totalMemory: totalMem, + chipset, + cpu: '', + cpuDetails: + typeof cpuInfo === 'object' + ? cpuInfo + : { + cores: 0, + processors: [], + socModel: '', + features: [], + hasFp16: false, + hasDotProd: false, + hasSve: false, + hasI8mm: false, + }, + }; + + setDeviceInfo(newDeviceInfo); + onDeviceInfo?.(newDeviceInfo); + }, + ); + }, [onDeviceInfo]); + + const formatBytes = (bytes: number) => { + const gb = bytes / (1024 * 1024 * 1024); + return `${gb.toFixed(1)} GB`; + }; + + return ( + + setExpanded(!expanded)}> + + + Device Information + + {deviceInfo.brand} {deviceInfo.model} • {deviceInfo.systemName}{' '} + {deviceInfo.systemVersion} + + + {deviceInfo.cpuDetails.cores} cores •{' '} + {formatBytes(deviceInfo.totalMemory)} + + + + + + + {expanded && ( + + + + Basic Info + + + + Architecture + + + {Array.isArray(deviceInfo.cpuArch) + ? deviceInfo.cpuArch.join(', ') + : deviceInfo.cpuArch} + + + + + Total Memory + + + {formatBytes(deviceInfo.totalMemory)} + + + + + Device ID + + + {Platform.OS === 'ios' + ? deviceInfo.deviceId + : `${deviceInfo.device} (${deviceInfo.deviceId})`} + + + + + + + CPU Details + + + + CPU Cores + + + {deviceInfo.cpuDetails.cores} + + + {deviceInfo.cpuDetails.processors[0]?.['model name'] && ( + + + CPU Model + + + {deviceInfo.cpuDetails.processors[0]['model name']} + + + )} + {Platform.OS === 'android' && deviceInfo.chipset && ( + + + Chipset + + + {deviceInfo.chipset} + + + )} + {Platform.OS === 'android' && ( + + + ML Instructions + + + FP16: {deviceInfo.cpuDetails.hasFp16 ? '✓' : '✗'}, DotProd:{' '} + {deviceInfo.cpuDetails.hasDotProd ? '✓' : '✗'}, SVE:{' '} + {deviceInfo.cpuDetails.hasSve ? '✓' : '✗'}, I8MM:{' '} + {deviceInfo.cpuDetails.hasI8mm ? '✓' : '✗'} + + + )} + + + + + App Info + + + + Version + + + {deviceInfo.version} ({deviceInfo.buildNumber}) + + + + + )} + + ); +}; diff --git a/src/screens/BenchmarkScreen/DeviceInfoCard/index.ts b/src/screens/BenchmarkScreen/DeviceInfoCard/index.ts new file mode 100644 index 0000000..52c8848 --- /dev/null +++ b/src/screens/BenchmarkScreen/DeviceInfoCard/index.ts @@ -0,0 +1 @@ +export * from './DeviceInfoCard'; diff --git a/src/screens/BenchmarkScreen/DeviceInfoCard/styles.ts b/src/screens/BenchmarkScreen/DeviceInfoCard/styles.ts new file mode 100644 index 0000000..20e584c --- /dev/null +++ b/src/screens/BenchmarkScreen/DeviceInfoCard/styles.ts @@ -0,0 +1,49 @@ +import {StyleSheet} from 'react-native'; + +import type {Theme} from '../../../utils/types'; + +export const createStyles = (theme: Theme) => + StyleSheet.create({ + deviceInfoCard: { + marginBottom: 16, + backgroundColor: theme.colors.surface, + borderWidth: 1, + borderColor: theme.colors.outline, + borderRadius: 15, + }, + deviceInfoRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 4, + }, + deviceInfoLabel: { + color: theme.colors.onSurfaceVariant, + }, + deviceInfoValue: { + color: theme.colors.onSurface, + }, + headerRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + padding: 16, + borderBottomWidth: 1, + borderBottomColor: theme.colors.outline, + }, + headerContent: { + flex: 1, + }, + headerSummary: { + color: theme.colors.onSurfaceVariant, + marginTop: 4, + }, + section: { + marginVertical: 8, + }, + sectionTitle: { + color: theme.colors.primary, + marginBottom: 8, + textTransform: 'uppercase', + }, + }); diff --git a/src/screens/BenchmarkScreen/__tests__/BenchmarkScreen.test.tsx b/src/screens/BenchmarkScreen/__tests__/BenchmarkScreen.test.tsx new file mode 100644 index 0000000..8eb44b9 --- /dev/null +++ b/src/screens/BenchmarkScreen/__tests__/BenchmarkScreen.test.tsx @@ -0,0 +1,470 @@ +import React from 'react'; +import {NativeModules} from 'react-native'; + +import {cloneDeep} from 'lodash'; +import {LlamaContext} from '@pocketpalai/llama.rn'; + +import {submitBenchmark} from '../../../api/benchmark'; + +import {fireEvent, render, waitFor} from '../../../../jest/test-utils'; +import { + mockResult, + mockSubmittedResult, +} from '../../../../jest/fixtures/benchmark'; + +import {BenchmarkScreen} from '../BenchmarkScreen'; + +import {benchmarkStore, modelStore, uiStore} from '../../../store'; + +jest.mock('../../../api/benchmark', () => ({ + submitBenchmark: jest.fn().mockResolvedValue(undefined), +})); + +describe('BenchmarkScreen', () => { + beforeEach(() => { + benchmarkStore.results = [ + cloneDeep(mockResult), + cloneDeep(mockSubmittedResult), + ]; + jest.clearAllMocks(); + }); + + describe('Model Initialization', () => { + it('should show loading indicator during model initialization', async () => { + const initPromise = new Promise(resolve => setTimeout(resolve, 100)); + (modelStore.initContext as jest.Mock).mockReturnValue(initPromise); + modelStore.isContextLoading = true; + modelStore.loadingModel = modelStore.models[0]; + + const {getByTestId} = render(); + + // Verify loading indicator is shown + expect(getByTestId('loading-indicator-model-init')).toBeDefined(); + + // Wait for initialization to complete + await initPromise; + }); + + it('should hide loading indicator after model initialization completes', async () => { + // Loading context + modelStore.isContextLoading = true; + modelStore.loadingModel = modelStore.models[0]; + + const {getByTestId, queryByTestId} = render(); + expect(getByTestId('loading-indicator-model-init')).toBeDefined(); + + // Complete loading + modelStore.isContextLoading = false; + modelStore.loadingModel = undefined; + + // Verify loading indicator is removed + await waitFor(() => { + expect(queryByTestId('loading-indicator-model-init')).toBeNull(); + }); + }); + + it('should show model selector with available models', () => { + const {getByText} = render(); + + // Open model selector + fireEvent.press(getByText('Select Model')); + + // Verify available models are shown + modelStore.availableModels.forEach(model => { + expect(getByText(model.name)).toBeDefined(); + }); + }); + + it('should initialize model when selected', async () => { + const {getByText} = render(); + const modelToSelect = modelStore.availableModels[0]; + + // Open model selector and select a model + fireEvent.press(getByText('Select Model')); + fireEvent.press(getByText(modelToSelect.name)); + + // Verify initContext was called + expect(modelStore.initContext).toHaveBeenCalledWith(modelToSelect); + }); + }); + + describe('Benchmark Execution', () => { + it('handles submission of benchmark results', async () => { + const {getByTestId} = render(); + + const submitButton = getByTestId('submit-benchmark-button'); + fireEvent.press(submitButton); + + await waitFor(() => { + expect(getByTestId('share-benchmark-dialog')).toBeDefined(); + }); + + const confirmButton = getByTestId( + 'share-benchmark-dialog-confirm-button', + ); + fireEvent.press(confirmButton); + + await waitFor(() => { + expect(submitBenchmark).toHaveBeenCalled(); + }); + }); + + it('should show benchmark loading indicator during execution', async () => { + modelStore.activeModelId = modelStore.models[0].id; + modelStore.context = new LlamaContext({ + contextId: 1, + gpu: false, + reasonNoGPU: '', + model: {}, + }); + + const {getByText, getByTestId} = render(); + + // Start benchmark + fireEvent.press(getByTestId('start-test-button')); + + expect(getByTestId('loading-indicator-benchmark')).toBeDefined(); + expect(getByText('Please keep this screen open.')).toBeDefined(); + }); + + it('should disable start button during benchmark execution', async () => { + modelStore.activeModelId = modelStore.models[0].id; + modelStore.context = new LlamaContext({ + contextId: 1, + gpu: false, + reasonNoGPU: '', + model: {}, + }); + const {getByTestId} = render(); + const startButton = getByTestId('start-test-button'); + + // Start benchmark + fireEvent.press(startButton); + + console.log(startButton.props.accessibilityState.disabled); + await waitFor(() => { + expect(startButton.props.accessibilityState.disabled).toBe(true); + }); + }); + }); + + describe('Memory Usage Tracking', () => { + beforeAll(() => { + // Mock DeviceInfoModule + NativeModules.DeviceInfoModule = { + getCPUInfo: jest.fn().mockResolvedValue({ + cores: 8, + processors: ['CPU1', 'CPU2', 'CPU3', 'CPU4'], + }), + }; + }); + + it('should display memory usage in results', async () => { + const result = { + ...mockResult, + peakMemoryUsage: { + total: 8 * 1000 * 1000 * 1000, + used: 4 * 1000 * 1000 * 1000, + percentage: 50, + }, + }; + benchmarkStore.results = [result]; + + const {getByText} = render(); + + // Verify memory usage display + expect(getByText('Peak Memory')).toBeDefined(); + expect(getByText('50.0%')).toBeDefined(); + expect(getByText('4 GB / 8 GB')).toBeDefined(); + }); + }); + + describe('Advanced Settings', () => { + it('should apply preset configurations correctly', () => { + modelStore.activeModelId = modelStore.models[0].id; + modelStore.context = new LlamaContext({ + contextId: 1, + gpu: false, + reasonNoGPU: '', + model: {}, + }); + + const {getByText, getByTestId} = render(); + + // Open advanced settings + fireEvent.press(getByTestId('advanced-settings-button')); + + // Select Fast preset + fireEvent.press(getByText('Fast')); + + // Verify preset values + expect(getByTestId('pp-slider').props.value).toBe(128); + expect(getByTestId('tg-slider').props.value).toBe(32); + }); + }); + + describe('Device Info Integration', () => { + it('renders device info card', () => { + const {getByText} = render(); + expect(getByText('Device Information')).toBeDefined(); + }); + + it('should include device info in benchmark submission', async () => { + const {getByTestId} = render(); + + // Wait for device info to be collected + await waitFor(() => { + expect(getByTestId('device-info-card')).toBeDefined(); + }); + + // Trigger benchmark submission + const submitButton = getByTestId('submit-benchmark-button'); + fireEvent.press(submitButton); + + const confirmButton = getByTestId( + 'share-benchmark-dialog-confirm-button', + ); + fireEvent.press(confirmButton); + + // Verify device info is included in submission + await waitFor(() => { + expect(submitBenchmark).toHaveBeenCalledWith( + expect.objectContaining({ + model: expect.any(String), + systemName: expect.any(String), + systemVersion: expect.any(String), + }), + expect.any(Object), + ); + }); + }); + }); + + describe('Share Dialog Preferences', () => { + it('should respect "dont show again" preference when is false', async () => { + benchmarkStore.results = [ + cloneDeep(mockResult), + cloneDeep(mockSubmittedResult), + cloneDeep(mockResult), + ]; + // Force to show confirm dialog + uiStore.benchmarkShareDialog.shouldShow = true; + + const {getByTestId, queryByTestId, getAllByTestId} = render( + , + ); + + // Trigger share + const submitButton = getAllByTestId('submit-benchmark-button')[0]; + fireEvent.press(submitButton); + + // Wait for the dialog to appear + await waitFor(() => { + expect(getByTestId('share-benchmark-dialog')).toBeDefined(); + }); + + // Set "don't show again" + const checkbox = getByTestId('dont-show-again-checkbox'); + fireEvent.press(checkbox); + + // Confirm share + const confirmButton = getByTestId( + 'share-benchmark-dialog-confirm-button', + ); + fireEvent.press(confirmButton); + + // wait for the submission to be called + await waitFor(() => { + expect(submitBenchmark).toHaveBeenCalled(); + }); + + // wait for the dialog to be closed + await waitFor(() => { + expect(queryByTestId('share-benchmark-dialog')).toBeNull(); + }); + + // Verify preference was saved + expect(uiStore.setBenchmarkShareDialogPreference).toHaveBeenCalledWith( + false, + ); + + // Since the store is mock we need to manually set the state + uiStore.benchmarkShareDialog.shouldShow = false; + + // Share another result + const submitButton2 = getByTestId('submit-benchmark-button'); + fireEvent.press(submitButton2); + + expect(queryByTestId('share-benchmark-dialog')).toBeNull(); + }); + + it('should respect "dont show again" preference when is true', async () => { + benchmarkStore.results = [ + cloneDeep(mockResult), + cloneDeep(mockSubmittedResult), + cloneDeep(mockResult), + ]; + uiStore.benchmarkShareDialog.shouldShow = true; + const {getByTestId, queryByTestId, getAllByTestId} = render( + , + ); + + // Trigger share + const submitButton = getAllByTestId('submit-benchmark-button')[0]; + fireEvent.press(submitButton); + + // Wait for the dialog to appear + await waitFor(() => { + expect(getByTestId('share-benchmark-dialog')).toBeDefined(); + }); + + // Confirm share + const confirmButton = getByTestId( + 'share-benchmark-dialog-confirm-button', + ); + fireEvent.press(confirmButton); + + // wait for the submission to be called + await waitFor(() => { + expect(submitBenchmark).toHaveBeenCalled(); + }); + + benchmarkStore.results = [mockResult, mockSubmittedResult]; + + // wait for the dialog to be closed + await waitFor(() => { + expect(queryByTestId('share-benchmark-dialog')).toBeNull(); + }); + + // Since the store is mock we need to manually set the state + uiStore.benchmarkShareDialog.shouldShow = true; + + // Share another result + const submitButton2 = getByTestId('submit-benchmark-button'); + fireEvent.press(submitButton2); + + await waitFor(() => { + expect(getByTestId('share-benchmark-dialog')).toBeDefined(); + }); + }); + + it('should show raw data in share dialog', async () => { + const {getByTestId, getByText} = render(); + + // Trigger share + const submitButton = getByTestId('submit-benchmark-button'); + fireEvent.press(submitButton); + + // Show raw data + const viewRawDataButton = getByTestId( + 'share-benchmark-dialog-view-raw-data-button', + ); + fireEvent.press(viewRawDataButton); + + // Verify raw data is shown + await waitFor(() => { + expect( + getByTestId('share-benchmark-dialog-raw-data-container'), + ).toBeDefined(); + }); + expect(getByText(/"deviceInfo":/)).toBeDefined(); + expect(getByText(/"benchmark":/)).toBeDefined(); + }); + }); + + describe('Result Management', () => { + it('renders benchmark results when available', async () => { + benchmarkStore.results = [mockResult]; + const {getByText} = render(); + + await waitFor(() => { + expect(getByText('Test Results')).toBeDefined(); + expect(getByText(mockResult.modelName)).toBeDefined(); + }); + }); + + it('should delete individual result', async () => { + // Add results to store + benchmarkStore.results = [mockResult, mockSubmittedResult]; + + const {getAllByTestId, getByText} = render(); + + // Delete first result + const deleteButtons = getAllByTestId('delete-result-button'); + fireEvent.press(deleteButtons[0]); + + // Confirm deletion + fireEvent.press(getByText('Delete')); + + // Verify deletion + expect(benchmarkStore.removeResult).toHaveBeenCalledWith( + mockResult.timestamp, + ); + }); + + it('should cancel result deletion', async () => { + // Add results to store + benchmarkStore.results = [mockResult]; + + const {getAllByTestId, getByText} = render(); + + // Attempt to delete result + const deleteButtons = getAllByTestId('delete-result-button'); + fireEvent.press(deleteButtons[0]); + + // Cancel deletion + fireEvent.press(getByText('Cancel')); + + // Verify result was not deleted + expect(benchmarkStore.removeResult).not.toHaveBeenCalled(); + }); + + it('allows clearing all results after confirmation', async () => { + benchmarkStore.results = [mockResult]; + const {getByTestId} = render(); + + // Click clear all button + const clearButton = getByTestId('clear-all-button'); + fireEvent.press(clearButton); + + // Confirm in the dialog + const confirmButton = getByTestId('clear-all-dialog-confirm-button'); + fireEvent.press(confirmButton); + + expect(benchmarkStore.results.length).toBe(0); + }); + + it('should clear all results', async () => { + // Add results to store + benchmarkStore.results = [mockResult, mockSubmittedResult]; + + const {getByTestId} = render(); + + // Clear all results + const clearAllButton = getByTestId('clear-all-button'); + fireEvent.press(clearAllButton); + + // Confirm clear all + const confirmButton = getByTestId('clear-all-dialog-confirm-button'); + fireEvent.press(confirmButton); + + // Verify all results were cleared + expect(benchmarkStore.clearResults).toHaveBeenCalled(); + }); + + it('keeps results if clear all is cancelled', async () => { + benchmarkStore.results = [mockResult]; + const {getByTestId} = render(); + + // Click clear all button + const clearButton = getByTestId('clear-all-button'); + fireEvent.press(clearButton); + + // Cancel in the dialog + const cancelButton = getByTestId('clear-all-dialog-cancel-button'); + fireEvent.press(cancelButton); + + expect(benchmarkStore.results.length).toBe(1); + }); + }); +}); diff --git a/src/screens/BenchmarkScreen/index.ts b/src/screens/BenchmarkScreen/index.ts new file mode 100644 index 0000000..baf70f3 --- /dev/null +++ b/src/screens/BenchmarkScreen/index.ts @@ -0,0 +1 @@ +export * from './BenchmarkScreen'; diff --git a/src/screens/BenchmarkScreen/styles.ts b/src/screens/BenchmarkScreen/styles.ts new file mode 100644 index 0000000..d1a447f --- /dev/null +++ b/src/screens/BenchmarkScreen/styles.ts @@ -0,0 +1,162 @@ +import {Platform, StyleSheet} from 'react-native'; +import type {Theme} from '../../utils/types'; + +export const createStyles = (theme: Theme) => + StyleSheet.create({ + container: { + flex: 1, + backgroundColor: theme.colors.surface, + }, + scrollView: { + flex: 1, + padding: 16, + }, + card: { + marginBottom: 16, + backgroundColor: theme.colors.surface, + }, + description: { + marginBottom: 16, + color: theme.colors.onSurfaceVariant, + }, + warning: { + color: theme.colors.error, + marginVertical: 8, + textAlign: 'center', + }, + button: { + marginVertical: 6, + }, + loadingContainer: { + alignItems: 'center', + marginVertical: 8, + }, + loadingText: { + marginTop: 8, + color: theme.colors.onSurfaceVariant, + }, + modelSelectorContent: { + justifyContent: 'space-between', + flexDirection: 'row-reverse', + alignItems: 'center', + }, + presetContainer: { + flexDirection: 'row', + marginBottom: 16, + justifyContent: 'space-around', + flexWrap: 'wrap', + gap: 8, + }, + presetButton: { + flex: 1, + minWidth: 100, + marginHorizontal: 4, + }, + slidersContainer: { + marginTop: 16, + }, + sliderDescriptionContainer: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + settingItem: { + marginBottom: 16, + }, + settingLabel: { + color: theme.colors.primary, + marginBottom: 0, + }, + settingValue: { + textAlign: 'right', + color: theme.colors.onSurface, + marginTop: 0, + }, + slider: { + height: 40, + }, + sectionTitle: { + color: theme.colors.primary, + marginBottom: 8, + }, + advancedButton: { + marginBottom: 6, + }, + advancedDescription: { + marginBottom: 16, + color: theme.colors.onSurfaceVariant, + fontSize: 12, + }, + warningContainer: { + backgroundColor: theme.colors.errorContainer, + padding: 16, + borderRadius: 8, + marginBottom: 16, + }, + warningList: { + marginTop: 8, + paddingLeft: 8, + }, + warningText: { + color: theme.colors.error, + marginVertical: 4, + }, + resultsHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingHorizontal: 8, + }, + resultsCard: { + marginTop: 16, + padding: 0, + backgroundColor: theme.colors.surface, + }, + resultItem: { + marginBottom: 16, + }, + errorText: { + marginTop: 16, + color: theme.colors.error, + }, + dialogList: { + marginVertical: 10, + paddingLeft: 8, + }, + dialogSection: { + marginTop: 16, + marginBottom: 8, + fontWeight: '600', + }, + link: { + textDecorationLine: 'underline', + }, + detailsButton: { + marginTop: 16, + alignSelf: 'flex-start', + }, + detailsContainer: { + backgroundColor: theme.colors.surfaceVariant, + borderRadius: 8, + padding: 12, + marginTop: 8, + }, + codeBlock: { + fontFamily: Platform.select({ios: 'Menlo', android: 'monospace'}), + fontSize: 11, + }, + checkboxContainer: { + flexDirection: 'row', + alignItems: 'center', + marginTop: 24, + paddingTop: 16, + borderTopWidth: 1, + borderTopColor: theme.colors.surfaceVariant, + }, + checkboxLabel: { + color: theme.colors.onSurfaceVariant, + fontSize: 12, + marginLeft: 12, + flex: 1, + }, + }); diff --git a/src/screens/ModelsScreen/HFModelSearch/DetailsView/__tests__/DetailsView.test.tsx b/src/screens/ModelsScreen/HFModelSearch/DetailsView/__tests__/DetailsView.test.tsx new file mode 100644 index 0000000..7a5ffcf --- /dev/null +++ b/src/screens/ModelsScreen/HFModelSearch/DetailsView/__tests__/DetailsView.test.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import {render} from '../../../../../../jest/test-utils'; +import {DetailsView} from '../DetailsView'; +import { + mockHFModel1, + mockHFModel2, +} from '../../../../../../jest/fixtures/models'; +import {formatNumber, timeAgo} from '../../../../../utils'; + +describe('DetailsView', () => { + it('renders basic model information', () => { + const {getByText} = render(); + + // Check author and model name are displayed + expect(getByText(mockHFModel1.author)).toBeDefined(); + expect(getByText('hf-model-name-1')).toBeDefined(); + }); + + it('renders model statistics', () => { + const {getByText} = render(); + + // Check stats are displayed with correct formatting + expect(getByText(timeAgo(mockHFModel1.lastModified))).toBeDefined(); + expect(getByText(formatNumber(mockHFModel1.downloads, 0))).toBeDefined(); + expect(getByText(formatNumber(mockHFModel1.likes, 0))).toBeDefined(); + }); + + it('shows trending indicator for high trending score', () => { + const {getByText} = render( + , + ); + + // mockHFModel2 has trendingScore > 20 + expect(getByText('🔥')).toBeDefined(); + }); + + it('renders model files section', () => { + const {getByText} = render(); + + expect(getByText('Available GGUF Files')).toBeDefined(); + // Check if file names are displayed + mockHFModel1.siblings.forEach(file => { + expect(getByText(file.rfilename)).toBeDefined(); + }); + }); +}); diff --git a/src/screens/index.ts b/src/screens/index.ts index a135d82..7989ef6 100644 --- a/src/screens/index.ts +++ b/src/screens/index.ts @@ -1,3 +1,4 @@ +export * from './BenchmarkScreen'; export * from './ChatScreen'; export * from './ModelsScreen'; export * from './SettingsScreen'; diff --git a/src/store/BenchmarkStore.ts b/src/store/BenchmarkStore.ts new file mode 100644 index 0000000..1a61c7e --- /dev/null +++ b/src/store/BenchmarkStore.ts @@ -0,0 +1,56 @@ +import {makeAutoObservable, runInAction} from 'mobx'; +import {makePersistable} from 'mobx-persist-store'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import {BenchmarkResult} from '../utils/types'; + +export class BenchmarkStore { + results: BenchmarkResult[] = []; + + constructor() { + makeAutoObservable(this); + makePersistable(this, { + name: 'BenchmarkStore', + properties: ['results'], + storage: AsyncStorage, + }); + } + + addResult(result: BenchmarkResult) { + runInAction(() => { + this.results.unshift(result); // Add new result at the beginning + }); + } + + removeResult(timestamp: string) { + runInAction(() => { + this.results = this.results.filter( + result => result.timestamp !== timestamp, + ); + }); + } + + clearResults() { + runInAction(() => { + this.results = []; + }); + } + + getResultsByModel(modelId: string): BenchmarkResult[] { + return this.results.filter(result => result.modelId === modelId); + } + + get latestResult(): BenchmarkResult | undefined { + return this.results[0]; + } + + markAsSubmitted(uuid: string) { + runInAction(() => { + const result = this.results.find(r => r.uuid === uuid); + if (result) { + result.submitted = true; + } + }); + } +} + +export const benchmarkStore = new BenchmarkStore(); diff --git a/src/store/UIStore.ts b/src/store/UIStore.ts index a7cb6af..6ad1ad8 100644 --- a/src/store/UIStore.ts +++ b/src/store/UIStore.ts @@ -29,6 +29,10 @@ export class UIStore { iOSBackgroundDownloading = false; + benchmarkShareDialog = { + shouldShow: true, + }; + constructor() { makeAutoObservable(this); makePersistable(this, { @@ -39,7 +43,8 @@ export class UIStore { 'autoNavigatetoChat', 'displayMemUsage', 'iOSBackgroundDownloading', - ], // Properties to persist + 'benchmarkShareDialog', + ], storage: AsyncStorage, }); } @@ -81,6 +86,12 @@ export class UIStore { this.iOSBackgroundDownloading = value; }); } + + setBenchmarkShareDialogPreference(shouldShow: boolean) { + runInAction(() => { + this.benchmarkShareDialog.shouldShow = shouldShow; + }); + } } export const uiStore = new UIStore(); diff --git a/src/store/__tests__/BenchmarkStore.test.ts b/src/store/__tests__/BenchmarkStore.test.ts new file mode 100644 index 0000000..06774a3 --- /dev/null +++ b/src/store/__tests__/BenchmarkStore.test.ts @@ -0,0 +1,83 @@ +import {BenchmarkStore} from '../BenchmarkStore'; +import {BenchmarkResult} from '../../utils/types'; + +describe('BenchmarkStore', () => { + let store: BenchmarkStore; + const mockResult: BenchmarkResult = { + config: { + pp: 1, + tg: 1, + pl: 512, + nr: 3, + label: 'Test Config', + }, + modelDesc: 'Test Model', + modelSize: 1000000, + modelNParams: 7000000000, + ppAvg: 20.5, + ppStd: 1.2, + tgAvg: 30.5, + tgStd: 2.1, + timestamp: '2024-03-20T10:00:00.000Z', + modelId: 'test-model-id', + modelName: 'Test Model', + filename: 'test-model.gguf', + uuid: 'test-uuid', + }; + + beforeEach(() => { + store = new BenchmarkStore(); + }); + + it('adds new result to the beginning of results array', () => { + store.addResult(mockResult); + expect(store.results[0]).toEqual(mockResult); + }); + + it('removes result by timestamp', () => { + store.addResult(mockResult); + store.removeResult(mockResult.timestamp); + expect(store.results.length).toBe(0); + }); + + it('clears all results', () => { + store.addResult(mockResult); + store.addResult({...mockResult, uuid: 'test-uuid-2'}); + store.clearResults(); + expect(store.results.length).toBe(0); + }); + + it('gets results by model ID', () => { + const differentModelResult = { + ...mockResult, + modelId: 'different-model', + uuid: 'different-uuid', + }; + store.addResult(mockResult); + store.addResult(differentModelResult); + + const results = store.getResultsByModel(mockResult.modelId); + expect(results.length).toBe(1); + expect(results[0].modelId).toBe(mockResult.modelId); + }); + + it('returns latest result', () => { + const olderResult = { + ...mockResult, + timestamp: '2024-03-19T10:00:00.000Z', + uuid: 'older-uuid', + }; + store.addResult(mockResult); + store.addResult(olderResult); + + expect(store.latestResult).toEqual(olderResult); + }); + + it('marks result as submitted', () => { + store.addResult(mockResult); + store.markAsSubmitted(mockResult.uuid); + + const result = store.results.find(r => r.uuid === mockResult.uuid); + expect(result?.submitted).toBe(true); + }); +}); diff --git a/src/store/index.ts b/src/store/index.ts index 3f76a78..a9ca96f 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -2,3 +2,4 @@ export * from './ChatSessionStore'; export * from './ModelStore'; export * from './UIStore'; export * from './HFStore'; +export * from './BenchmarkStore'; diff --git a/src/utils/fb.ts b/src/utils/fb.ts new file mode 100644 index 0000000..e7c6ec7 --- /dev/null +++ b/src/utils/fb.ts @@ -0,0 +1,51 @@ +import '@react-native-firebase/app-check'; +import firebase from '@react-native-firebase/app'; +import {APPCHECK_DEBUG_TOKEN_ANDROID, APPCHECK_DEBUG_TOKEN_IOS} from '@env'; + +// Track initialization status +let isAppCheckInitialized = false; + +export const initializeAppCheck = () => { + if (isAppCheckInitialized) { + return; + } + + try { + const rnfbProvider = firebase + .appCheck() + .newReactNativeFirebaseAppCheckProvider(); + + rnfbProvider.configure({ + android: { + provider: __DEV__ ? 'debug' : 'playIntegrity', + debugToken: APPCHECK_DEBUG_TOKEN_ANDROID, + }, + apple: { + provider: __DEV__ ? 'debug' : 'appAttestWithDeviceCheckFallback', + debugToken: APPCHECK_DEBUG_TOKEN_IOS, + }, + }); + firebase.appCheck().initializeAppCheck({ + provider: rnfbProvider, + isTokenAutoRefreshEnabled: true, + }); + + isAppCheckInitialized = true; + } catch (error) { + console.error('Failed to initialize Firebase App Check:', error); + } +}; + +// Get a fresh App Check token +export const getAppCheckToken = async () => { + try { + if (!firebase.appCheck) { + throw new Error('Firebase App Check module is not available'); + } + const {token} = await firebase.appCheck().getToken(true); + return token; + } catch (error) { + console.error('Failed to get App Check token:', error); + throw error; + } +}; diff --git a/src/utils/types.ts b/src/utils/types.ts index f79f981..ab3c6ea 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -379,3 +379,66 @@ export interface GGUFSpecs { eos_token?: string; }; } +export type BenchmarkConfig = { + pp: number; + tg: number; + pl: number; + nr: number; + label: string; +}; + +export type BenchmarkResult = { + config: BenchmarkConfig; + modelDesc: string; + modelSize: number; + modelNParams: number; + ppAvg: number; + ppStd: number; + tgAvg: number; + tgStd: number; + timestamp: string; + modelId: string; + modelName: string; + oid?: string; + rfilename?: string; + filename: string; + peakMemoryUsage?: { + total: number; + used: number; + percentage: number; + }; + wallTimeMs?: number; + uuid: string; + submitted?: boolean; +}; + +export type DeviceInfo = { + model: string; + systemName: string; + systemVersion: string; + brand: string; + cpuArch: string[]; + isEmulator: boolean; + version: string; + buildNumber: string; + device: string; + deviceId: string; + totalMemory: number; + chipset: string; + cpu: string; + cpuDetails: { + cores: number; + processors: Array<{ + processor: string; + 'model name': string; + 'cpu MHz': string; + vendor_id: string; + }>; + socModel: string; + features: string[]; + hasFp16: boolean; + hasDotProd: boolean; + hasSve: boolean; + hasI8mm: boolean; + }; +}; diff --git a/yarn.lock b/yarn.lock index 1a1f4da..d3c41a0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1313,6 +1313,390 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2" integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q== +"@firebase/analytics-compat@0.2.14": + version "0.2.14" + resolved "https://registry.yarnpkg.com/@firebase/analytics-compat/-/analytics-compat-0.2.14.tgz#7e85a245317394a36523d08bccf5dd5bbe91b72d" + integrity sha512-unRVY6SvRqfNFIAA/kwl4vK+lvQAL2HVcgu9zTrUtTyYDmtIt/lOuHJynBMYEgLnKm39YKBDhtqdapP2e++ASw== + dependencies: + "@firebase/analytics" "0.10.8" + "@firebase/analytics-types" "0.8.2" + "@firebase/component" "0.6.9" + "@firebase/util" "1.10.0" + tslib "^2.1.0" + +"@firebase/analytics-types@0.8.2": + version "0.8.2" + resolved "https://registry.yarnpkg.com/@firebase/analytics-types/-/analytics-types-0.8.2.tgz#947f85346e404332aac6c996d71fd4a89cd7f87a" + integrity sha512-EnzNNLh+9/sJsimsA/FGqzakmrAUKLeJvjRHlg8df1f97NLUlFidk9600y0ZgWOp3CAxn6Hjtk+08tixlUOWyw== + +"@firebase/analytics@0.10.8": + version "0.10.8" + resolved "https://registry.yarnpkg.com/@firebase/analytics/-/analytics-0.10.8.tgz#73d4bfa1bdae5140907a94817cfdddf00d1dae22" + integrity sha512-CVnHcS4iRJPqtIDc411+UmFldk0ShSK3OB+D0bKD8Ck5Vro6dbK5+APZpkuWpbfdL359DIQUnAaMLE+zs/PVyA== + dependencies: + "@firebase/component" "0.6.9" + "@firebase/installations" "0.6.9" + "@firebase/logger" "0.4.2" + "@firebase/util" "1.10.0" + tslib "^2.1.0" + +"@firebase/app-check-compat@0.3.15": + version "0.3.15" + resolved "https://registry.yarnpkg.com/@firebase/app-check-compat/-/app-check-compat-0.3.15.tgz#78babc0575c34c9bb550601d2563438597dc56c2" + integrity sha512-zFIvIFFNqDXpOT2huorz9cwf56VT3oJYRFjSFYdSbGYEJYEaXjLJbfC79lx/zjx4Fh+yuN8pry3TtvwaevrGbg== + dependencies: + "@firebase/app-check" "0.8.8" + "@firebase/app-check-types" "0.5.2" + "@firebase/component" "0.6.9" + "@firebase/logger" "0.4.2" + "@firebase/util" "1.10.0" + tslib "^2.1.0" + +"@firebase/app-check-interop-types@0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.2.tgz#455b6562c7a3de3ef75ea51f72dfec5829ad6997" + integrity sha512-LMs47Vinv2HBMZi49C09dJxp0QT5LwDzFaVGf/+ITHe3BlIhUiLNttkATSXplc89A2lAaeTqjgqVkiRfUGyQiQ== + +"@firebase/app-check-types@0.5.2": + version "0.5.2" + resolved "https://registry.yarnpkg.com/@firebase/app-check-types/-/app-check-types-0.5.2.tgz#1221bd09b471e11bb149252f16640a0a51043cbc" + integrity sha512-FSOEzTzL5bLUbD2co3Zut46iyPWML6xc4x+78TeaXMSuJap5QObfb+rVvZJtla3asN4RwU7elaQaduP+HFizDA== + +"@firebase/app-check@0.8.8": + version "0.8.8" + resolved "https://registry.yarnpkg.com/@firebase/app-check/-/app-check-0.8.8.tgz#78bdd5ba1745c5eecf284c3687a8b2902bfcb08c" + integrity sha512-O49RGF1xj7k6BuhxGpHmqOW5hqBIAEbt2q6POW0lIywx7emYtzPDeQI+ryQpC4zbKX646SoVZ711TN1DBLNSOQ== + dependencies: + "@firebase/component" "0.6.9" + "@firebase/logger" "0.4.2" + "@firebase/util" "1.10.0" + tslib "^2.1.0" + +"@firebase/app-compat@0.2.41": + version "0.2.41" + resolved "https://registry.yarnpkg.com/@firebase/app-compat/-/app-compat-0.2.41.tgz#6cdc0b5c3248e8e04dcfa373e4895d5785e94f2b" + integrity sha512-ktJcObWKjlIWq31kXu6sHoqWlhQD5rx0a2F2ZC2JVuEE5A5f7F43VO1Z6lfeRZXMFZbGG/aqIfXqgsP3zD2JYg== + dependencies: + "@firebase/app" "0.10.11" + "@firebase/component" "0.6.9" + "@firebase/logger" "0.4.2" + "@firebase/util" "1.10.0" + tslib "^2.1.0" + +"@firebase/app-types@0.9.2": + version "0.9.2" + resolved "https://registry.yarnpkg.com/@firebase/app-types/-/app-types-0.9.2.tgz#8cbcceba784753a7c0066a4809bc22f93adee080" + integrity sha512-oMEZ1TDlBz479lmABwWsWjzHwheQKiAgnuKxE0pz0IXCVx7/rtlkx1fQ6GfgK24WCrxDKMplZrT50Kh04iMbXQ== + +"@firebase/app@0.10.11": + version "0.10.11" + resolved "https://registry.yarnpkg.com/@firebase/app/-/app-0.10.11.tgz#25547f5bf815896dc08023f97138487948522092" + integrity sha512-DuI8c+p/ndPmV6V0i+mcSuaU9mK9Pi9h76WOYFkPNsbmkblEy8bpTOazjG7tnfar6Of1Wn5ohvyOHSRqnN6flQ== + dependencies: + "@firebase/component" "0.6.9" + "@firebase/logger" "0.4.2" + "@firebase/util" "1.10.0" + idb "7.1.1" + tslib "^2.1.0" + +"@firebase/auth-compat@0.5.14": + version "0.5.14" + resolved "https://registry.yarnpkg.com/@firebase/auth-compat/-/auth-compat-0.5.14.tgz#d3bcb8e1bd992eb1850a025240397d94461ea179" + integrity sha512-2eczCSqBl1KUPJacZlFpQayvpilg3dxXLy9cSMTKtQMTQSmondUtPI47P3ikH3bQAXhzKLOE+qVxJ3/IRtu9pw== + dependencies: + "@firebase/auth" "1.7.9" + "@firebase/auth-types" "0.12.2" + "@firebase/component" "0.6.9" + "@firebase/util" "1.10.0" + tslib "^2.1.0" + undici "6.19.7" + +"@firebase/auth-interop-types@0.2.3": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@firebase/auth-interop-types/-/auth-interop-types-0.2.3.tgz#927f1f2139a680b55fef0bddbff2c982b08587e8" + integrity sha512-Fc9wuJGgxoxQeavybiuwgyi+0rssr76b+nHpj+eGhXFYAdudMWyfBHvFL/I5fEHniUM/UQdFzi9VXJK2iZF7FQ== + +"@firebase/auth-types@0.12.2": + version "0.12.2" + resolved "https://registry.yarnpkg.com/@firebase/auth-types/-/auth-types-0.12.2.tgz#f12d890585866e53b6ab18b16fa4d425c52eee6e" + integrity sha512-qsEBaRMoGvHO10unlDJhaKSuPn4pyoTtlQuP1ghZfzB6rNQPuhp/N/DcFZxm9i4v0SogjCbf9reWupwIvfmH6w== + +"@firebase/auth@1.7.9": + version "1.7.9" + resolved "https://registry.yarnpkg.com/@firebase/auth/-/auth-1.7.9.tgz#00d40fbf49474a235bb1152ba5833074115300dd" + integrity sha512-yLD5095kVgDw965jepMyUrIgDklD6qH/BZNHeKOgvu7pchOKNjVM+zQoOVYJIKWMWOWBq8IRNVU6NXzBbozaJg== + dependencies: + "@firebase/component" "0.6.9" + "@firebase/logger" "0.4.2" + "@firebase/util" "1.10.0" + tslib "^2.1.0" + undici "6.19.7" + +"@firebase/component@0.6.9": + version "0.6.9" + resolved "https://registry.yarnpkg.com/@firebase/component/-/component-0.6.9.tgz#4248cfeab222245ada0d7f78ece95a87574532b4" + integrity sha512-gm8EUEJE/fEac86AvHn8Z/QW8BvR56TBw3hMW0O838J/1mThYQXAIQBgUv75EqlCZfdawpWLrKt1uXvp9ciK3Q== + dependencies: + "@firebase/util" "1.10.0" + tslib "^2.1.0" + +"@firebase/database-compat@1.0.8": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@firebase/database-compat/-/database-compat-1.0.8.tgz#69ab03d00e27a89f65486896ea219094aa38c27f" + integrity sha512-OpeWZoPE3sGIRPBKYnW9wLad25RaWbGyk7fFQe4xnJQKRzlynWeFBSRRAoLE2Old01WXwskUiucNqUUVlFsceg== + dependencies: + "@firebase/component" "0.6.9" + "@firebase/database" "1.0.8" + "@firebase/database-types" "1.0.5" + "@firebase/logger" "0.4.2" + "@firebase/util" "1.10.0" + tslib "^2.1.0" + +"@firebase/database-types@1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@firebase/database-types/-/database-types-1.0.5.tgz#2d923f42e3d9911b9eec537ed8b5ecaa0ce95c37" + integrity sha512-fTlqCNwFYyq/C6W7AJ5OCuq5CeZuBEsEwptnVxlNPkWCo5cTTyukzAHRSO/jaQcItz33FfYrrFk1SJofcu2AaQ== + dependencies: + "@firebase/app-types" "0.9.2" + "@firebase/util" "1.10.0" + +"@firebase/database@1.0.8": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@firebase/database/-/database-1.0.8.tgz#01bb0d0cb5653ae6a6641523f6f085b4c1be9c2f" + integrity sha512-dzXALZeBI1U5TXt6619cv0+tgEhJiwlUtQ55WNZY7vGAjv7Q1QioV969iYwt1AQQ0ovHnEW0YW9TiBfefLvErg== + dependencies: + "@firebase/app-check-interop-types" "0.3.2" + "@firebase/auth-interop-types" "0.2.3" + "@firebase/component" "0.6.9" + "@firebase/logger" "0.4.2" + "@firebase/util" "1.10.0" + faye-websocket "0.11.4" + tslib "^2.1.0" + +"@firebase/firestore-compat@0.3.37": + version "0.3.37" + resolved "https://registry.yarnpkg.com/@firebase/firestore-compat/-/firestore-compat-0.3.37.tgz#eab17138fc1a7484000807ad9921fbe1b686831a" + integrity sha512-YwjJePx+m2OGnpKTGFTkcRXQZ+z0+8t7/zuwyOsTmKERobn0kekOv8VAQQmITcC+3du8Ul98O2a0vMH3xwt7jQ== + dependencies: + "@firebase/component" "0.6.9" + "@firebase/firestore" "4.7.2" + "@firebase/firestore-types" "3.0.2" + "@firebase/util" "1.10.0" + tslib "^2.1.0" + +"@firebase/firestore-types@3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@firebase/firestore-types/-/firestore-types-3.0.2.tgz#75c301acc5fa33943eaaa9570b963c55398cad2a" + integrity sha512-wp1A+t5rI2Qc/2q7r2ZpjUXkRVPtGMd6zCLsiWurjsQpqPgFin3AhNibKcIzoF2rnToNa/XYtyWXuifjOOwDgg== + +"@firebase/firestore@4.7.2": + version "4.7.2" + resolved "https://registry.yarnpkg.com/@firebase/firestore/-/firestore-4.7.2.tgz#7fa7f6aaa844b53695daaf509da274b945372f65" + integrity sha512-WPkL/DEHuJg1PZPyHn81pNUhitG+7WkpLVdXmoYB23Za3eoM8VzuIn7zcD4Cji6wDCGA6eI1rvGYLtsXmE1OaQ== + dependencies: + "@firebase/component" "0.6.9" + "@firebase/logger" "0.4.2" + "@firebase/util" "1.10.0" + "@firebase/webchannel-wrapper" "1.0.1" + "@grpc/grpc-js" "~1.9.0" + "@grpc/proto-loader" "^0.7.8" + tslib "^2.1.0" + undici "6.19.7" + +"@firebase/functions-compat@0.3.14": + version "0.3.14" + resolved "https://registry.yarnpkg.com/@firebase/functions-compat/-/functions-compat-0.3.14.tgz#0997de9c799912dd171758273238234b1b5a700d" + integrity sha512-dZ0PKOKQFnOlMfcim39XzaXonSuPPAVuzpqA4ONTIdyaJK/OnBaIEVs/+BH4faa1a2tLeR+Jy15PKqDRQoNIJw== + dependencies: + "@firebase/component" "0.6.9" + "@firebase/functions" "0.11.8" + "@firebase/functions-types" "0.6.2" + "@firebase/util" "1.10.0" + tslib "^2.1.0" + +"@firebase/functions-types@0.6.2": + version "0.6.2" + resolved "https://registry.yarnpkg.com/@firebase/functions-types/-/functions-types-0.6.2.tgz#03b4ec9259d2f57548a3909d6a35ae35ad243552" + integrity sha512-0KiJ9lZ28nS2iJJvimpY4nNccV21rkQyor5Iheu/nq8aKXJqtJdeSlZDspjPSBBiHRzo7/GMUttegnsEITqR+w== + +"@firebase/functions@0.11.8": + version "0.11.8" + resolved "https://registry.yarnpkg.com/@firebase/functions/-/functions-0.11.8.tgz#a85dcc843882dba8b17b974155b036da04f59576" + integrity sha512-Lo2rTPDn96naFIlSZKVd1yvRRqqqwiJk7cf9TZhUerwnPKgBzXy+aHE22ry+6EjCaQusUoNai6mU6p+G8QZT1g== + dependencies: + "@firebase/app-check-interop-types" "0.3.2" + "@firebase/auth-interop-types" "0.2.3" + "@firebase/component" "0.6.9" + "@firebase/messaging-interop-types" "0.2.2" + "@firebase/util" "1.10.0" + tslib "^2.1.0" + undici "6.19.7" + +"@firebase/installations-compat@0.2.9": + version "0.2.9" + resolved "https://registry.yarnpkg.com/@firebase/installations-compat/-/installations-compat-0.2.9.tgz#0b169ad292d6ef4e1fdef453164d60c2d883eaa1" + integrity sha512-2lfdc6kPXR7WaL4FCQSQUhXcPbI7ol3wF+vkgtU25r77OxPf8F/VmswQ7sgIkBBWtymn5ZF20TIKtnOj9rjb6w== + dependencies: + "@firebase/component" "0.6.9" + "@firebase/installations" "0.6.9" + "@firebase/installations-types" "0.5.2" + "@firebase/util" "1.10.0" + tslib "^2.1.0" + +"@firebase/installations-types@0.5.2": + version "0.5.2" + resolved "https://registry.yarnpkg.com/@firebase/installations-types/-/installations-types-0.5.2.tgz#4d4949e0e83ced7f36cbee009355cd305a36e158" + integrity sha512-que84TqGRZJpJKHBlF2pkvc1YcXrtEDOVGiDjovP/a3s6W4nlbohGXEsBJo0JCeeg/UG9A+DEZVDUV9GpklUzA== + +"@firebase/installations@0.6.9": + version "0.6.9" + resolved "https://registry.yarnpkg.com/@firebase/installations/-/installations-0.6.9.tgz#eb696577b4c5fb0a68836e167edd46fb4a39b7b2" + integrity sha512-hlT7AwCiKghOX3XizLxXOsTFiFCQnp/oj86zp1UxwDGmyzsyoxtX+UIZyVyH/oBF5+XtblFG9KZzZQ/h+dpy+Q== + dependencies: + "@firebase/component" "0.6.9" + "@firebase/util" "1.10.0" + idb "7.1.1" + tslib "^2.1.0" + +"@firebase/logger@0.4.2": + version "0.4.2" + resolved "https://registry.yarnpkg.com/@firebase/logger/-/logger-0.4.2.tgz#74dfcfeedee810deb8a7080d5b7eba56aa16ffa2" + integrity sha512-Q1VuA5M1Gjqrwom6I6NUU4lQXdo9IAQieXlujeHZWvRt1b7qQ0KwBaNAjgxG27jgF9/mUwsNmO8ptBCGVYhB0A== + dependencies: + tslib "^2.1.0" + +"@firebase/messaging-compat@0.2.11": + version "0.2.11" + resolved "https://registry.yarnpkg.com/@firebase/messaging-compat/-/messaging-compat-0.2.11.tgz#7a4c18bdb5b071bf762bd609e0dc14b0754b2f59" + integrity sha512-2NCkfE1L9jSn5OC+2n5rGAz5BEAQreK2lQGdPYQEJlAbKB2efoF+2FdiQ+LD8SlioSXz66REfeaEdesoLPFQcw== + dependencies: + "@firebase/component" "0.6.9" + "@firebase/messaging" "0.12.11" + "@firebase/util" "1.10.0" + tslib "^2.1.0" + +"@firebase/messaging-interop-types@0.2.2": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.2.tgz#81042f7e9739733fa4571d17f6eb6869522754d0" + integrity sha512-l68HXbuD2PPzDUOFb3aG+nZj5KA3INcPwlocwLZOzPp9rFM9yeuI9YLl6DQfguTX5eAGxO0doTR+rDLDvQb5tA== + +"@firebase/messaging@0.12.11": + version "0.12.11" + resolved "https://registry.yarnpkg.com/@firebase/messaging/-/messaging-0.12.11.tgz#a9e5c50210f20919ce6b4eafe27fcac38967fa33" + integrity sha512-zn5zGhF46BmiZ7W9yAUoHlqzJGakmWn1FNp//roXHN62dgdEFIKfXY7IODA2iQiXpmUO3sBdI/Tf+Hsft1mVkw== + dependencies: + "@firebase/component" "0.6.9" + "@firebase/installations" "0.6.9" + "@firebase/messaging-interop-types" "0.2.2" + "@firebase/util" "1.10.0" + idb "7.1.1" + tslib "^2.1.0" + +"@firebase/performance-compat@0.2.9": + version "0.2.9" + resolved "https://registry.yarnpkg.com/@firebase/performance-compat/-/performance-compat-0.2.9.tgz#f7f603ef9116162ccbe24ea9b00abc9b0de84faa" + integrity sha512-dNl95IUnpsu3fAfYBZDCVhXNkASE0uo4HYaEPd2/PKscfTvsgqFAOxfAXzBEDOnynDWiaGUnb5M1O00JQ+3FXA== + dependencies: + "@firebase/component" "0.6.9" + "@firebase/logger" "0.4.2" + "@firebase/performance" "0.6.9" + "@firebase/performance-types" "0.2.2" + "@firebase/util" "1.10.0" + tslib "^2.1.0" + +"@firebase/performance-types@0.2.2": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@firebase/performance-types/-/performance-types-0.2.2.tgz#7b78cd2ab2310bac89a63348d93e67e01eb06dd7" + integrity sha512-gVq0/lAClVH5STrIdKnHnCo2UcPLjJlDUoEB/tB4KM+hAeHUxWKnpT0nemUPvxZ5nbdY/pybeyMe8Cs29gEcHA== + +"@firebase/performance@0.6.9": + version "0.6.9" + resolved "https://registry.yarnpkg.com/@firebase/performance/-/performance-0.6.9.tgz#e8fc4ecc7c5be21acd3ed1ef1e0e123ea2e3b05f" + integrity sha512-PnVaak5sqfz5ivhua+HserxTJHtCar/7zM0flCX6NkzBNzJzyzlH4Hs94h2Il0LQB99roBqoE5QT1JqWqcLJHQ== + dependencies: + "@firebase/component" "0.6.9" + "@firebase/installations" "0.6.9" + "@firebase/logger" "0.4.2" + "@firebase/util" "1.10.0" + tslib "^2.1.0" + +"@firebase/remote-config-compat@0.2.9": + version "0.2.9" + resolved "https://registry.yarnpkg.com/@firebase/remote-config-compat/-/remote-config-compat-0.2.9.tgz#2c8ca1c0cf86051df6998f3f7051065804dccaaa" + integrity sha512-AxzGpWfWFYejH2twxfdOJt5Cfh/ATHONegTd/a0p5flEzsD5JsxXgfkFToop+mypEL3gNwawxrxlZddmDoNxyA== + dependencies: + "@firebase/component" "0.6.9" + "@firebase/logger" "0.4.2" + "@firebase/remote-config" "0.4.9" + "@firebase/remote-config-types" "0.3.2" + "@firebase/util" "1.10.0" + tslib "^2.1.0" + +"@firebase/remote-config-types@0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@firebase/remote-config-types/-/remote-config-types-0.3.2.tgz#a5d1009c6fd08036c5cd4f28764e3cd694f966d5" + integrity sha512-0BC4+Ud7y2aPTyhXJTMTFfrGGLqdYXrUB9sJVAB8NiqJswDTc4/2qrE/yfUbnQJhbSi6ZaTTBKyG3n1nplssaA== + +"@firebase/remote-config@0.4.9": + version "0.4.9" + resolved "https://registry.yarnpkg.com/@firebase/remote-config/-/remote-config-0.4.9.tgz#280d5ad2ed35e86187f058ecdd4bfdd2cf798e3e" + integrity sha512-EO1NLCWSPMHdDSRGwZ73kxEEcTopAxX1naqLJFNApp4hO8WfKfmEpmjxmP5TrrnypjIf2tUkYaKsfbEA7+AMmA== + dependencies: + "@firebase/component" "0.6.9" + "@firebase/installations" "0.6.9" + "@firebase/logger" "0.4.2" + "@firebase/util" "1.10.0" + tslib "^2.1.0" + +"@firebase/storage-compat@0.3.12": + version "0.3.12" + resolved "https://registry.yarnpkg.com/@firebase/storage-compat/-/storage-compat-0.3.12.tgz#e24d004bb28b1c0fae9adccf120b71c371491c30" + integrity sha512-hA4VWKyGU5bWOll+uwzzhEMMYGu9PlKQc1w4DWxB3aIErWYzonrZjF0icqNQZbwKNIdh8SHjZlFeB2w6OSsjfg== + dependencies: + "@firebase/component" "0.6.9" + "@firebase/storage" "0.13.2" + "@firebase/storage-types" "0.8.2" + "@firebase/util" "1.10.0" + tslib "^2.1.0" + +"@firebase/storage-types@0.8.2": + version "0.8.2" + resolved "https://registry.yarnpkg.com/@firebase/storage-types/-/storage-types-0.8.2.tgz#edb321b8a3872a9f74e1f27de046f160021c8e1f" + integrity sha512-0vWu99rdey0g53lA7IShoA2Lol1jfnPovzLDUBuon65K7uKG9G+L5uO05brD9pMw+l4HRFw23ah3GwTGpEav6g== + +"@firebase/storage@0.13.2": + version "0.13.2" + resolved "https://registry.yarnpkg.com/@firebase/storage/-/storage-0.13.2.tgz#33cd113a8c0904f7d2ab16142112046826f7ef00" + integrity sha512-fxuJnHshbhVwuJ4FuISLu+/76Aby2sh+44ztjF2ppoe0TELIDxPW6/r1KGlWYt//AD0IodDYYA8ZTN89q8YqUw== + dependencies: + "@firebase/component" "0.6.9" + "@firebase/util" "1.10.0" + tslib "^2.1.0" + undici "6.19.7" + +"@firebase/util@1.10.0": + version "1.10.0" + resolved "https://registry.yarnpkg.com/@firebase/util/-/util-1.10.0.tgz#9ec8ab54da82bfc31baff0c43cb281998cbeddab" + integrity sha512-xKtx4A668icQqoANRxyDLBLz51TAbDP9KRfpbKGxiCAW346d0BeJe5vN6/hKxxmWwnZ0mautyv39JxviwwQMOQ== + dependencies: + tslib "^2.1.0" + +"@firebase/vertexai-preview@0.0.4": + version "0.0.4" + resolved "https://registry.yarnpkg.com/@firebase/vertexai-preview/-/vertexai-preview-0.0.4.tgz#14327cb69e2f72462d1a32366c71aa0836ffc39e" + integrity sha512-EBSqyu9eg8frQlVU9/HjKtHN7odqbh9MtAcVz3WwHj4gLCLOoN9F/o+oxlq3CxvFrd3CNTZwu6d2mZtVlEInng== + dependencies: + "@firebase/app-check-interop-types" "0.3.2" + "@firebase/component" "0.6.9" + "@firebase/logger" "0.4.2" + "@firebase/util" "1.10.0" + tslib "^2.1.0" + +"@firebase/webchannel-wrapper@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@firebase/webchannel-wrapper/-/webchannel-wrapper-1.0.1.tgz#0b62c9f47f557a5b4adc073bb0a47542ce6af4c4" + integrity sha512-jmEnr/pk0yVkA7mIlHNnxCi+wWzOFUg0WyIotgkKAb2u1J7fAeDBcVNSTjTihbAYNusCLQdW5s9IJ5qwnEufcQ== + "@flyerhq/react-native-link-preview@^1.6.0": version "1.6.0" resolved "https://registry.yarnpkg.com/@flyerhq/react-native-link-preview/-/react-native-link-preview-1.6.0.tgz#c7285ab93a133328edc5d69b011822581bb4414b" @@ -1335,6 +1719,24 @@ dependencies: nanoid "^3.3.1" +"@grpc/grpc-js@~1.9.0": + version "1.9.15" + resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.9.15.tgz#433d7ac19b1754af690ea650ab72190bd700739b" + integrity sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ== + dependencies: + "@grpc/proto-loader" "^0.7.8" + "@types/node" ">=12.12.47" + +"@grpc/proto-loader@^0.7.8": + version "0.7.13" + resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.7.13.tgz#f6a44b2b7c9f7b609f5748c6eac2d420e37670cf" + integrity sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw== + dependencies: + lodash.camelcase "^4.3.0" + long "^5.0.0" + protobufjs "^7.2.5" + yargs "^17.7.2" + "@hapi/hoek@^9.0.0", "@hapi/hoek@^9.3.0": version "9.3.0" resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb" @@ -1702,6 +2104,59 @@ resolved "https://registry.yarnpkg.com/@pocketpalai/llama.rn/-/llama.rn-0.4.3-1.tgz#e8e965b55effd9071f900b7dac25396fff9b34b9" integrity sha512-7L7gou7m91s3fAP9jlKf17De86XRyLUSEw1RTopmSQKuengZHxWcgMESCLIT5y6/u69IzlM6DVjrQUsBb0Om9A== +"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" + integrity sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ== + +"@protobufjs/base64@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" + integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== + +"@protobufjs/codegen@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb" + integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg== + +"@protobufjs/eventemitter@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70" + integrity sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q== + +"@protobufjs/fetch@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45" + integrity sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ== + dependencies: + "@protobufjs/aspromise" "^1.1.1" + "@protobufjs/inquire" "^1.1.0" + +"@protobufjs/float@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" + integrity sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ== + +"@protobufjs/inquire@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089" + integrity sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q== + +"@protobufjs/path@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" + integrity sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA== + +"@protobufjs/pool@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" + integrity sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw== + +"@protobufjs/utf8@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" + integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== + "@react-native-async-storage/async-storage@^2.1.0": version "2.1.0" resolved "https://registry.yarnpkg.com/@react-native-async-storage/async-storage/-/async-storage-2.1.0.tgz#84ca82af320c16d3d8e617508ea523fe786b6781" @@ -1876,6 +2331,18 @@ resolved "https://registry.yarnpkg.com/@react-native-community/slider/-/slider-4.5.5.tgz#d70fc5870477760033769bbd6625d57e7d7678b2" integrity sha512-x2N415pg4ZxIltArOKczPwn7JEYh+1OxQ4+hTnafomnMsqs65HZuEWcX+Ch8c5r8V83DiunuQUf5hWGWlw8hQQ== +"@react-native-firebase/app-check@^21.6.1": + version "21.6.1" + resolved "https://registry.yarnpkg.com/@react-native-firebase/app-check/-/app-check-21.6.1.tgz#c526c110d668b3252aec2f09fe0d22edcd8866d5" + integrity sha512-aJCqePi2nbZIL6YVi9EI0UE5e92EHithAB4vR4pid/aDa+dpYc0epTsNLWboWRCgRBFnJw21NLfmbXjOqcyH0A== + +"@react-native-firebase/app@^21.6.1": + version "21.6.1" + resolved "https://registry.yarnpkg.com/@react-native-firebase/app/-/app-21.6.1.tgz#2f3a5f9bc9436ba3a0a76b9d32eb685e0a379340" + integrity sha512-o1i3aQMtJeofue+73j/8KO2l+/bRHaSHqOl7FtIeZfajQZaulaROugD2Rr2Mlc1o7LfEUyoDOoij9nGdrydEyw== + dependencies: + firebase "10.13.2" + "@react-native-masked-view/masked-view@^0.3.1": version "0.3.2" resolved "https://registry.yarnpkg.com/@react-native-masked-view/masked-view/-/masked-view-0.3.2.tgz#7064533a573e3539ec912f59c1f457371bf49dd9" @@ -2291,6 +2758,13 @@ dependencies: undici-types "~6.20.0" +"@types/node@>=12.12.47", "@types/node@>=13.7.0": + version "22.10.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.2.tgz#a485426e6d1fdafc7b0d4c7b24e2c78182ddabb9" + integrity sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ== + dependencies: + undici-types "~6.20.0" + "@types/prop-types@*": version "15.7.13" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.13.tgz#2af91918ee12d9d32914feb13f5326658461b451" @@ -3594,6 +4068,11 @@ dot-prop@^5.1.0: dependencies: is-obj "^2.0.0" +dotenv@^16.4.5: + version "16.4.7" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.7.tgz#0e20c5b82950140aa99be360a8a5f52335f53c26" + integrity sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ== + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -4103,6 +4582,13 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" +faye-websocket@0.11.4: + version "0.11.4" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da" + integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g== + dependencies: + websocket-driver ">=0.5.1" + fb-watchman@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.2.tgz#e9524ee6b5c77e9e5001af0f85f3adbb8623255c" @@ -4190,6 +4676,39 @@ find-yarn-workspace-root@^2.0.0: dependencies: micromatch "^4.0.2" +firebase@10.13.2: + version "10.13.2" + resolved "https://registry.yarnpkg.com/firebase/-/firebase-10.13.2.tgz#f0adf725c74bf346386b4a50b7c00823028b13f9" + integrity sha512-YeI+TO5rJsoyZsVFx9WiN5ibdVCIigYTWwldRTMfCzrSPrJFVGao4acYj3x0EYGKDIgSgEyVBayD5BffD4Eyow== + dependencies: + "@firebase/analytics" "0.10.8" + "@firebase/analytics-compat" "0.2.14" + "@firebase/app" "0.10.11" + "@firebase/app-check" "0.8.8" + "@firebase/app-check-compat" "0.3.15" + "@firebase/app-compat" "0.2.41" + "@firebase/app-types" "0.9.2" + "@firebase/auth" "1.7.9" + "@firebase/auth-compat" "0.5.14" + "@firebase/database" "1.0.8" + "@firebase/database-compat" "1.0.8" + "@firebase/firestore" "4.7.2" + "@firebase/firestore-compat" "0.3.37" + "@firebase/functions" "0.11.8" + "@firebase/functions-compat" "0.3.14" + "@firebase/installations" "0.6.9" + "@firebase/installations-compat" "0.2.9" + "@firebase/messaging" "0.12.11" + "@firebase/messaging-compat" "0.2.11" + "@firebase/performance" "0.6.9" + "@firebase/performance-compat" "0.2.9" + "@firebase/remote-config" "0.4.9" + "@firebase/remote-config-compat" "0.2.9" + "@firebase/storage" "0.13.2" + "@firebase/storage-compat" "0.3.12" + "@firebase/util" "1.10.0" + "@firebase/vertexai-preview" "0.0.4" + flat-cache@^3.0.4: version "3.2.0" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee" @@ -4537,6 +5056,11 @@ http-errors@2.0.0: statuses "2.0.1" toidentifier "1.0.1" +http-parser-js@>=0.5.1: + version "0.5.8" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3" + integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q== + http-status-codes@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/http-status-codes/-/http-status-codes-2.3.0.tgz#987fefb28c69f92a43aecc77feec2866349a8bfc" @@ -4552,6 +5076,11 @@ husky@^9.1.7: resolved "https://registry.yarnpkg.com/husky/-/husky-9.1.7.tgz#d46a38035d101b46a70456a850ff4201344c0b2d" integrity sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA== +idb@7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/idb/-/idb-7.1.1.tgz#d910ded866d32c7ced9befc5bfdf36f572ced72b" + integrity sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ== + ieee754@^1.1.13, ieee754@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" @@ -5669,6 +6198,11 @@ logkitty@^0.7.1: dayjs "^1.8.15" yargs "^15.1.0" +long@^5.0.0: + version "5.2.3" + resolved "https://registry.yarnpkg.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1" + integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q== + loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" @@ -6550,6 +7084,24 @@ prop-types@^15.5.7, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.7.x, object-assign "^4.1.1" react-is "^16.13.1" +protobufjs@^7.2.5: + version "7.4.0" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.4.0.tgz#7efe324ce9b3b61c82aae5de810d287bc08a248a" + integrity sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/node" ">=13.7.0" + long "^5.0.0" + proxy-from-env@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" @@ -6656,6 +7208,13 @@ react-native-document-picker@^9.1.2: dependencies: invariant "^2.2.4" +react-native-dotenv@^3.4.11: + version "3.4.11" + resolved "https://registry.yarnpkg.com/react-native-dotenv/-/react-native-dotenv-3.4.11.tgz#2e6c4eabd55d5f1bf109b3dd9141dadf9c55cdd4" + integrity sha512-6vnIE+WHABSeHCaYP6l3O1BOEhWxKH6nHAdV7n/wKn/sciZ64zPPp2NUdEUf1m7g4uuzlLbjgr+6uDt89q2DOg== + dependencies: + dotenv "^16.4.5" + react-native-gesture-handler@^2.20.2: version "2.21.2" resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-2.21.2.tgz#824a098d7397212fbe51aa3a9df84833a4ea1c3f" @@ -7113,7 +7672,7 @@ safe-array-concat@^1.1.2: has-symbols "^1.0.3" isarray "^2.0.5" -safe-buffer@5.2.1, safe-buffer@~5.2.0: +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -7647,7 +8206,7 @@ tslib@^1.8.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.1: +tslib@^2.0.1, tslib@^2.1.0: version "2.8.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== @@ -7751,6 +8310,11 @@ undici-types@~6.20.0: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433" integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== +undici@6.19.7: + version "6.19.7" + resolved "https://registry.yarnpkg.com/undici/-/undici-6.19.7.tgz#7d4cf26dc689838aa8b6753a3c5c4288fc1e0216" + integrity sha512-HR3W/bMGPSr90i8AAp2C4DM3wChFdJPLrWYpIS++LxS8K+W535qftjt+4MyjNYHeWabMj1nvtmLIi7l++iq91A== + unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz#cb3173fe47ca743e228216e4a3ddc4c84d628cc2" @@ -7887,6 +8451,20 @@ webidl-conversions@^3.0.0: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== +websocket-driver@>=0.5.1: + version "0.7.4" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" + integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== + dependencies: + http-parser-js ">=0.5.1" + safe-buffer ">=5.1.0" + websocket-extensions ">=0.1.1" + +websocket-extensions@>=0.1.1: + version "0.1.4" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" + integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== + whatwg-fetch@^3.0.0: version "3.6.20" resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz#580ce6d791facec91d37c72890995a0b48d31c70" @@ -8093,7 +8671,7 @@ yargs@^16.1.1: y18n "^5.0.5" yargs-parser "^20.2.2" -yargs@^17.0.0, yargs@^17.3.1, yargs@^17.6.2: +yargs@^17.0.0, yargs@^17.3.1, yargs@^17.6.2, yargs@^17.7.2: version "17.7.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==