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 = () => (
+
+ );
+
+ 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 = () => (
+
+ );
+
+ const renderWarningMessage = () => (
+
+
+ Note: Test could run for up to 2-5 minutes for larger models and cannot
+ be interrupted once started.
+
+
+ );
+
+ const renderShareDialog = () => (
+
+ );
+
+ 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) => (
+
+
+
+ ))}
+
+ )}
+
+
+
+
+
+ {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==