diff --git a/.circleci/config.yml b/.circleci/config.yml
new file mode 100644
index 0000000..4237dfd
--- /dev/null
+++ b/.circleci/config.yml
@@ -0,0 +1,150 @@
+version: 2.1
+
+orbs:
+ general-platform-helpers: okta/general-platform-helpers@1.9.4
+ platform-helpers: okta/platform-helpers@2.0.0
+ macos: circleci/macos@2
+ android: circleci/android@3.1.0
+
+executors:
+ # android:
+ # docker:
+ # - image: cimg/android:2024.12
+ # environment:
+ # GRADLE_OPTS: -Xmx2g
+ # resource_class: large
+
+ macos:
+ macos:
+ xcode: "16.3"
+ resource_class: m4pro.medium
+
+jobs:
+ test-rn-webcrypto-android:
+ executor:
+ name: android/android_machine
+ resource_class: large
+ tag: default
+ steps:
+ - checkout
+
+ - restore_cache:
+ keys:
+ - gradle-{{ checksum "packages/react-native-webcrypto-bridge/android/build.gradle" }}
+ - gradle-
+
+ - run:
+ name: Run Android Unit and Integration Tests
+ command: |
+ cd packages/react-native-webcrypto-bridge/android
+ ./gradlew testDebugUnitTest --info
+
+ - save_cache:
+ key: gradle-{{ checksum "packages/react-native-webcrypto-bridge/android/build.gradle" }}
+ paths:
+ - ~/.gradle
+ - .gradle
+
+ - store_test_results:
+ path: packages/react-native-webcrypto-bridge/android/build/test-results
+
+ - store_artifacts:
+ path: packages/react-native-webcrypto-bridge/android/build/reports
+ destination: android-test-reports
+
+ test-rn-webcrypto-ios:
+ executor: macos
+ steps:
+ - checkout
+
+ - restore_cache:
+ keys:
+ - swift-spm-{{ checksum "packages/react-native-webcrypto-bridge/ios/Package.swift" }}
+ - swift-spm-
+
+ - run:
+ name: Run iOS Unit and Integration Tests
+ command: |
+ cd packages/react-native-webcrypto-bridge/ios
+ swift test --verbose
+
+ - save_cache:
+ key: swift-spm-{{ checksum "packages/react-native-webcrypto-bridge/ios/Package.swift" }}
+ paths:
+ - packages/react-native-webcrypto-bridge/ios/.build
+
+ - store_test_results:
+ path: packages/react-native-webcrypto-bridge/ios/.build/test-results
+
+ - store_artifacts:
+ path: packages/react-native-webcrypto-bridge/ios/.build
+ destination: ios-build-artifacts
+
+ test-rn-platform-android:
+ executor:
+ name: android/android_machine
+ resource_class: large
+ tag: default
+ steps:
+ - checkout
+
+ - restore_cache:
+ keys:
+ - gradle-rn-platform-{{ checksum "packages/react-native-platform/android/build.gradle" }}
+ - gradle-rn-platform-
+
+ - run:
+ name: Run React Native Platform Android Tests
+ command: |
+ cd packages/react-native-platform/android
+ ./gradlew testDebugUnitTest --info
+
+ - save_cache:
+ key: gradle-rn-platform-{{ checksum "packages/react-native-platform/android/build.gradle" }}
+ paths:
+ - ~/.gradle
+ - .gradle
+
+ - store_test_results:
+ path: packages/react-native-platform/android/build/test-results
+
+ - store_artifacts:
+ path: packages/react-native-platform/android/build/reports
+ destination: react-native-platform-android-test-reports
+
+ test-rn-platform-ios:
+ executor: macos
+ steps:
+ - checkout
+
+ - restore_cache:
+ keys:
+ - swift-spm-rn-platform-{{ checksum "packages/react-native-platform/ios/Package.swift" }}
+ - swift-spm-rn-platform-
+
+ - run:
+ name: Run React Native Platform iOS Tests
+ command: |
+ cd packages/react-native-platform/ios
+ swift test --verbose
+
+ - save_cache:
+ key: swift-spm-rn-platform-{{ checksum "packages/react-native-platform/ios/Package.swift" }}
+ paths:
+ - packages/react-native-platform/ios/.build
+
+ - store_test_results:
+ path: packages/react-native-platform/ios/.build/test-results
+
+ - store_artifacts:
+ path: packages/react-native-platform/ios/.build
+ destination: react-native-platform-ios-build-artifacts
+
+workflows:
+ version: 2
+ build_and_test:
+ jobs:
+ - test-rn-webcrypto-android
+ - test-rn-webcrypto-ios
+ - test-rn-platform-android
+ - test-rn-platform-ios
diff --git a/e2e/apps/react-native-oidc/.gitignore b/e2e/apps/react-native-oidc/.gitignore
index 8cbb0d2..9516583 100644
--- a/e2e/apps/react-native-oidc/.gitignore
+++ b/e2e/apps/react-native-oidc/.gitignore
@@ -39,3 +39,5 @@ yarn-error.*
*.tsbuildinfo
app-example
+
+.idea/
\ No newline at end of file
diff --git a/e2e/apps/react-native-oidc/app.config.ts b/e2e/apps/react-native-oidc/app.config.ts
index 7856dac..2c4f6b7 100644
--- a/e2e/apps/react-native-oidc/app.config.ts
+++ b/e2e/apps/react-native-oidc/app.config.ts
@@ -17,5 +17,40 @@ export default ({ config }: ConfigContext) => ({
...config,
extra: {
env
- }
+ },
+ newArchEnabled: true,
+ "android": {
+ "package": "com.anonymous.reporeactnativeoidc"
+ },
+ "ios": {
+ "bundleIdentifier": "com.anonymous.reporeactnativeoidc"
+ },
+ scheme: "com.oktapreview.jperreault-test",
+ intentFilters: [
+ {
+ action: "VIEW",
+ autoVerify: true,
+ data: [
+ {
+ scheme: "com.oktapreview.jperreault-test"
+ }
+ ],
+ category: ["BROWSABLE", "DEFAULT"]
+ }
+ ],
+ "plugins": [
+ "expo-font",
+ "expo-router",
+ [
+ "expo-build-properties",
+ {
+ "ios": {
+ "newArchEnabled": true
+ },
+ "android": {
+ "newArchEnabled": true
+ }
+ }
+ ]
+ ]
});
\ No newline at end of file
diff --git a/e2e/apps/react-native-oidc/app/(login)/_layout.tsx b/e2e/apps/react-native-oidc/app/(login)/_layout.tsx
deleted file mode 100644
index aa7f199..0000000
--- a/e2e/apps/react-native-oidc/app/(login)/_layout.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import { Tabs } from 'expo-router';
-import React from 'react';
-import { Platform } from 'react-native';
-
-import { HapticTab } from '@/components/HapticTab';
-import { IconSymbol } from '@/components/ui/IconSymbol';
-import TabBarBackground from '@/components/ui/TabBarBackground';
-import { Colors } from '@/constants/Colors';
-import { useColorScheme } from '@/hooks/useColorScheme';
-
-export default function TabLayout() {
- const colorScheme = useColorScheme();
-
- return (
-
- ,
- }}
- />
- ,
- }}
- />
-
- );
-}
diff --git a/e2e/apps/react-native-oidc/app/(login)/index.tsx b/e2e/apps/react-native-oidc/app/(login)/index.tsx
deleted file mode 100644
index 13dcace..0000000
--- a/e2e/apps/react-native-oidc/app/(login)/index.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import { useEffect } from 'react';
-import { StyleSheet } from 'react-native';
-import { Image } from 'expo-image';
-import Constants from 'expo-constants';
-import { useAuth } from '@/hooks/useAuth';
-
-import ParallaxScrollView from '@/components/ParallaxScrollView';
-import { ThemedText } from '@/components/ThemedText';
-import { ThemedView } from '@/components/ThemedView';
-import { HelloWave } from '@/components/HelloWave';
-
-
-export default function LoginScreen () {
- const { signIn } = useAuth();
-
- useEffect(() => {
- (async () => {
- await signIn('/(login)/token');
- })();
- }, [signIn]);
-
- return (
-
- }>
-
- Loading...
-
-
-
- );
-}
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- marginTop: Constants.statusBarHeight,
- },
- titleContainer: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 8,
- },
- reactLogo: {
- height: 178,
- width: 290,
- bottom: 0,
- left: 0,
- position: 'absolute',
- },
-});
diff --git a/e2e/apps/react-native-oidc/app/(login)/token.tsx b/e2e/apps/react-native-oidc/app/(login)/token.tsx
deleted file mode 100644
index 2992061..0000000
--- a/e2e/apps/react-native-oidc/app/(login)/token.tsx
+++ /dev/null
@@ -1,104 +0,0 @@
-import { useEffect, useState } from 'react';
-import { Button, StyleSheet } from 'react-native';
-import { Image } from 'expo-image';
-import { useRouter } from 'expo-router';
-import Constants from 'expo-constants';
-import { Credential, Token } from '@okta/auth-foundation';
-import { useAuth } from '@/hooks/useAuth';
-
-import ParallaxScrollView from '@/components/ParallaxScrollView';
-import { ThemedText } from '@/components/ThemedText';
-import { ThemedView } from '@/components/ThemedView';
-import { HelloWave } from '@/components/HelloWave';
-
-
-export default function TokenScreen () {
- const router = useRouter();
- const { signOut } = useAuth();
- const [token, setToken] = useState(null);
-
- useEffect(() => {
- (async () => {
- try {
- const credential = await Credential.getDefault();
- if (credential) {
- setToken(credential.token);
- }
- else {
- router.navigate('/(login)');
- }
- }
- catch (err) {
- console.log('error');
- console.log(err, (err as Error)?.stack);
- throw err;
- }
- })();
- }, [router, token, setToken]);
-
- let body = (
-
- Loading...
-
-
- );
-
- if (token) {
- body = (
- <>
-
- Token
-
-
-
- Access Token
- {token.accessToken}
- Refresh Token
- {token?.refreshToken}
-
-
-
- >
- );
- }
-
- return (
-
- }>
- {body}
-
- );
-}
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- marginTop: Constants.statusBarHeight,
- },
- stepContainer: {
- gap: 8,
- marginBottom: 8,
- },
- titleContainer: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 8,
- },
- reactLogo: {
- height: 178,
- width: 290,
- bottom: 0,
- left: 0,
- position: 'absolute',
- },
-});
diff --git a/e2e/apps/react-native-oidc/app/(tabs)/_layout.tsx b/e2e/apps/react-native-oidc/app/(tabs)/_layout.tsx
index cfbc1e2..7a27432 100644
--- a/e2e/apps/react-native-oidc/app/(tabs)/_layout.tsx
+++ b/e2e/apps/react-native-oidc/app/(tabs)/_layout.tsx
@@ -29,15 +29,22 @@ export default function TabLayout() {
,
}}
/>
,
+ title: 'Creds',
+ tabBarIcon: ({ color }) => ,
+ }}
+ />
+ ,
}}
/>
diff --git a/e2e/apps/react-native-oidc/app/(tabs)/credentials.tsx b/e2e/apps/react-native-oidc/app/(tabs)/credentials.tsx
new file mode 100644
index 0000000..c483f50
--- /dev/null
+++ b/e2e/apps/react-native-oidc/app/(tabs)/credentials.tsx
@@ -0,0 +1,228 @@
+import { useState, useEffect, useCallback } from 'react';
+import { StyleSheet, ScrollView, TouchableOpacity, RefreshControl, ActivityIndicator } from 'react-native';
+import { useRouter, useFocusEffect } from 'expo-router';
+import { IconSymbol } from '@/components/ui/IconSymbol';
+import ParallaxScrollView from '@/components/ParallaxScrollView';
+import { ThemedText } from '@/components/ThemedText';
+import { ThemedView } from '@/components/ThemedView';
+import { Credential } from '@okta/react-native-platform';
+import type { Token } from '@okta/react-native-platform';
+
+type CredentialItem = {
+ id: string;
+ token: Token;
+ isDefault: boolean;
+};
+
+export default function CredentialsScreen() {
+ const router = useRouter();
+ const [credentials, setCredentials] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [refreshing, setRefreshing] = useState(false);
+
+ const loadCredentials = useCallback(async () => {
+ try {
+ const allCredentials = await Promise.all(
+ (await Credential.allIDs()).map((id) => Credential.with(id))
+ ) as Credential[];
+ const defaultCred = await Credential.getDefault();
+
+ const items: CredentialItem[] = await Promise.all(
+ allCredentials.map(async (cred) => ({
+ id: cred.id,
+ token: cred.token,
+ isDefault: cred.id === defaultCred?.id,
+ }))
+ );
+
+ setCredentials(items);
+ } catch (err) {
+ console.error('Failed to load credentials:', err);
+ } finally {
+ setLoading(false);
+ setRefreshing(false);
+ }
+ }, []);
+
+ useFocusEffect(
+ useCallback(() => {
+ setLoading(true);
+ loadCredentials();
+ }, [loadCredentials])
+ );
+
+ const onRefresh = useCallback(() => {
+ setRefreshing(true);
+ loadCredentials();
+ }, [loadCredentials]);
+
+ const handleCredentialPress = (credentialId: string) => {
+ router.push({
+ pathname: '/(tabs)/token',
+ params: { id: credentialId },
+ });
+ };
+
+ if (loading) {
+ return (
+
+ }>
+
+
+ Loading credentials...
+
+
+ );
+ }
+
+ return (
+
+ }
+ refreshControl={
+
+ }>
+
+ Credentials
+
+
+ {credentials.length === 0 ? (
+
+ No credentials found.
+
+ Sign in from the Auth tab to create a credential.
+
+
+ ) : (
+
+
+ {credentials.length} credential{credentials.length !== 1 ? 's' : ''} stored
+
+
+ {credentials.map((cred) => (
+ handleCredentialPress(cred.id)}>
+
+
+ {cred.id.substring(0, 12)}...
+
+ {cred.isDefault && (
+
+ DEFAULT
+
+ )}
+
+
+
+
+ Expires: {new Date(cred.token.expiresAt).toLocaleString()}
+
+
+ Type: {cred.token.tokenType}
+
+ {cred.token.scopes && (
+
+ Scopes: {cred.token.scopes.join(', ')}
+
+ )}
+
+
+ Tap to view details →
+
+ ))}
+
+ )}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ headerImage: {
+ color: '#808080',
+ bottom: -90,
+ left: -35,
+ position: 'absolute',
+ },
+ titleContainer: {
+ flexDirection: 'row',
+ gap: 8,
+ marginBottom: 16,
+ },
+ centerContainer: {
+ alignItems: 'center',
+ gap: 12,
+ padding: 32,
+ },
+ emptyContainer: {
+ alignItems: 'center',
+ gap: 8,
+ padding: 32,
+ },
+ emptyHint: {
+ opacity: 0.7,
+ textAlign: 'center',
+ },
+ listContainer: {
+ gap: 12,
+ },
+ countText: {
+ opacity: 0.7,
+ marginBottom: 8,
+ },
+ credentialCard: {
+ padding: 16,
+ borderRadius: 12,
+ backgroundColor: '#f5f5f522',
+ borderWidth: 1,
+ borderColor: '#e0e0e033',
+ gap: 12,
+ },
+ credentialHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ },
+ credentialId: {
+ flex: 1,
+ },
+ defaultBadge: {
+ backgroundColor: '#34c75922',
+ paddingHorizontal: 8,
+ paddingVertical: 4,
+ borderRadius: 6,
+ },
+ defaultBadgeText: {
+ fontSize: 10,
+ fontWeight: 'bold',
+ color: '#34c759',
+ },
+ credentialDetails: {
+ gap: 4,
+ },
+ detailText: {
+ fontSize: 12,
+ opacity: 0.8,
+ },
+ tapHint: {
+ fontSize: 12,
+ opacity: 0.5,
+ textAlign: 'right',
+ },
+});
diff --git a/e2e/apps/react-native-oidc/app/(tabs)/explore.tsx b/e2e/apps/react-native-oidc/app/(tabs)/explore.tsx
deleted file mode 100644
index d4fbcaa..0000000
--- a/e2e/apps/react-native-oidc/app/(tabs)/explore.tsx
+++ /dev/null
@@ -1,110 +0,0 @@
-import { Image } from 'expo-image';
-import { Platform, StyleSheet } from 'react-native';
-
-import { Collapsible } from '@/components/Collapsible';
-import { ExternalLink } from '@/components/ExternalLink';
-import ParallaxScrollView from '@/components/ParallaxScrollView';
-import { ThemedText } from '@/components/ThemedText';
-import { ThemedView } from '@/components/ThemedView';
-import { IconSymbol } from '@/components/ui/IconSymbol';
-
-export default function TabTwoScreen() {
- return (
-
- }>
-
- Explore
-
- This app includes example code to help you get started.
-
-
- This app has two screens:{' '}
- app/(tabs)/index.tsx and{' '}
- app/(tabs)/explore.tsx
-
-
- The layout file in app/(tabs)/_layout.tsx{' '}
- sets up the tab navigator.
-
-
- Learn more
-
-
-
-
- You can open this project on Android, iOS, and the web. To open the web version, press{' '}
- w in the terminal running this project.
-
-
-
-
- For static images, you can use the @2x and{' '}
- @3x suffixes to provide files for
- different screen densities
-
-
-
- Learn more
-
-
-
-
- Open app/_layout.tsx to see how to load{' '}
-
- custom fonts such as this one.
-
-
-
- Learn more
-
-
-
-
- This template has light and dark mode support. The{' '}
- useColorScheme() hook lets you inspect
- what the user's current color scheme is, and so you can adjust UI colors accordingly.
-
-
- Learn more
-
-
-
-
- This template includes an example of an animated component. The{' '}
- components/HelloWave.tsx component uses
- the powerful react-native-reanimated{' '}
- library to create a waving hand animation.
-
- {Platform.select({
- ios: (
-
- The components/ParallaxScrollView.tsx{' '}
- component provides a parallax effect for the header image.
-
- ),
- })}
-
-
- );
-}
-
-const styles = StyleSheet.create({
- headerImage: {
- color: '#808080',
- bottom: -90,
- left: -35,
- position: 'absolute',
- },
- titleContainer: {
- flexDirection: 'row',
- gap: 8,
- },
-});
diff --git a/e2e/apps/react-native-oidc/app/(tabs)/index.tsx b/e2e/apps/react-native-oidc/app/(tabs)/index.tsx
index 071c5c0..a395ddb 100644
--- a/e2e/apps/react-native-oidc/app/(tabs)/index.tsx
+++ b/e2e/apps/react-native-oidc/app/(tabs)/index.tsx
@@ -1,14 +1,68 @@
import { Image } from 'expo-image';
-import { useRouter } from 'expo-router';
-import { Button, StyleSheet } from 'react-native';
+import { useState, useCallback } from 'react';
+import { Button, StyleSheet, View, ActivityIndicator } from 'react-native';
+import { useFocusEffect } from 'expo-router';
import { HelloWave } from '@/components/HelloWave';
import ParallaxScrollView from '@/components/ParallaxScrollView';
import { ThemedText } from '@/components/ThemedText';
import { ThemedView } from '@/components/ThemedView';
+import { useAuth } from '@/hooks/useAuth';
+import { Credential } from '@okta/react-native-platform';
-export default function HomeScreen() {
- const router = useRouter();
+export default function AuthScreen() {
+ const { signIn, signOut } = useAuth();
+ const [isAuthenticated, setIsAuthenticated] = useState(false);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useFocusEffect(
+ useCallback(() => {
+ setLoading(true);
+ checkAuth();
+ }, [])
+ );
+
+ const checkAuth = async () => {
+ try {
+ setLoading(true);
+ const credential = await Credential.getDefault();
+ setIsAuthenticated(!!credential);
+ } catch (err) {
+ console.error('Error checking auth:', err);
+ setIsAuthenticated(false);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleSignIn = async () => {
+ try {
+ setLoading(true);
+ setError(null);
+ await signIn();
+ setIsAuthenticated(true);
+ } catch (err) {
+ console.error('Sign in failed:', err);
+ setError(err instanceof Error ? err.message : 'Sign in failed');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleSignOut = async () => {
+ try {
+ setLoading(true);
+ setError(null);
+ await signOut('/(tabs)');
+ setIsAuthenticated(false);
+ } catch (err) {
+ console.error('Sign out failed:', err);
+ setError(err instanceof Error ? err.message : 'Sign out failed');
+ } finally {
+ setLoading(false);
+ }
+ };
return (
}>
- Welcome!
+ Authentication
-
- Login
-
+
+ {loading ? (
+
+
+ Loading...
+
+ ) : (
+ <>
+
+
+ Status: {isAuthenticated ? '✅ Authenticated' : '❌ Not Authenticated'}
+
+
+
+ {error && (
+
+ Error: {error}
+
+ )}
+
+
+
+
+
+
+
+ Next Steps:
+
+ {isAuthenticated
+ ? '• Go to Credentials tab to see all stored tokens\n• Select a credential to view token details'
+ : '• Sign in to get started\n• Your tokens will be stored securely in the keychain'}
+
+
+ >
+ )}
);
}
@@ -51,4 +133,12 @@ const styles = StyleSheet.create({
left: 0,
position: 'absolute',
},
-});
+ errorContainer: {
+ padding: 12,
+ backgroundColor: '#ff3b3022',
+ borderRadius: 8,
+ },
+ errorText: {
+ color: '#ff3b30',
+ },
+});
\ No newline at end of file
diff --git a/e2e/apps/react-native-oidc/app/(tabs)/token.tsx b/e2e/apps/react-native-oidc/app/(tabs)/token.tsx
new file mode 100644
index 0000000..b2fd767
--- /dev/null
+++ b/e2e/apps/react-native-oidc/app/(tabs)/token.tsx
@@ -0,0 +1,296 @@
+import { useState, useCallback } from 'react';
+import { StyleSheet, ScrollView, ActivityIndicator, Button } from 'react-native';
+import { useLocalSearchParams, useRouter, useFocusEffect } from 'expo-router';
+import { IconSymbol } from '@/components/ui/IconSymbol';
+import ParallaxScrollView from '@/components/ParallaxScrollView';
+import { ThemedText } from '@/components/ThemedText';
+import { ThemedView } from '@/components/ThemedView';
+import { Credential } from '@okta/react-native-platform';
+import type { Token } from '@okta/react-native-platform';
+
+export default function TokenScreen() {
+ const router = useRouter();
+ const params = useLocalSearchParams<{ id?: string }>();
+ const [token, setToken] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useFocusEffect(
+ useCallback(() => {
+ loadToken();
+ }, [params.id])
+ );
+
+ const loadToken = async () => {
+ try {
+ setLoading(true);
+ setError(null);
+
+ let credential;
+ if (params.id) {
+ credential = await Credential.with(params.id);
+ } else {
+ credential = await Credential.getDefault();
+ }
+
+ if (credential) {
+ setToken(credential.token);
+ } else {
+ let errMsg = 'No credential found';
+ if (params.id) {
+ errMsg += ` (${params.id})`;
+ }
+ setError(errMsg);
+ }
+ } catch (err) {
+ console.error('Failed to load token:', err);
+ setError(err instanceof Error ? err.message : 'Failed to load token');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleRevoke = async () => {
+ if (token) {
+ const cred = await Credential.with(token.id);
+ await cred?.revoke();
+ router.navigate('/credentials')
+ }
+ }
+
+ if (loading) {
+ return (
+
+ }>
+
+
+ Loading token...
+
+
+ );
+ }
+
+ if (error || !token) {
+ return (
+
+ }>
+
+ Token Details
+
+
+
+ {error || `No token selected (${params.id})`}
+
+
+ Go to Credentials tab and select a credential
+
+
+
+ );
+ }
+
+ const tokenData = token.toJSON();
+ const isExpired = token.isExpired;
+ const expiresIn = Math.floor((new Date(token.expiresAt).getTime() - Date.now()) / 1000);
+
+ return (
+
+ }>
+
+ Token Details
+
+
+
+ Status
+
+
+ {isExpired ? '❌ Expired' : '✅ Valid'}
+
+ {!isExpired && (
+
+ Expires in {Math.floor(expiresIn / 60)} minutes
+
+ )}
+
+
+
+
+ Token ID
+
+
+ {token.id}
+
+
+
+
+
+ Properties
+
+
+
+
+ {token.scopes && }
+
+
+
+ {token.context && Object.keys(token.context).length > 0 && (
+
+ Context
+
+
+
+ {JSON.stringify(token.context, null, 2)}
+
+
+
+
+ )}
+
+
+ Access Token (truncated)
+
+
+ {token.accessToken}
+
+
+
+
+ {token?.idToken && (
+
+ ID Token (truncated)
+
+
+ {token?.idToken?.rawValue}
+
+
+
+ )}
+
+ {token?.refreshToken && (
+
+ Refresh Token
+
+
+ {token.refreshToken}
+
+
+
+ )}
+
+
+
+
+
+ );
+}
+
+function Property({ label, value }: { label: string; value: string }) {
+ return (
+
+ {label}:
+ {value}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ headerImage: {
+ color: '#808080',
+ bottom: -90,
+ left: -35,
+ position: 'absolute',
+ },
+ titleContainer: {
+ flexDirection: 'row',
+ gap: 8,
+ marginBottom: 16,
+ },
+ centerContainer: {
+ alignItems: 'center',
+ gap: 12,
+ padding: 32,
+ },
+ errorContainer: {
+ padding: 20,
+ gap: 8,
+ alignItems: 'center',
+ },
+ errorText: {
+ color: '#ff3b30',
+ fontSize: 16,
+ },
+ hintText: {
+ opacity: 0.7,
+ textAlign: 'center',
+ },
+ section: {
+ marginBottom: 24,
+ gap: 8,
+ },
+ statusContainer: {
+ gap: 4,
+ },
+ valid: {
+ color: '#34c759',
+ fontSize: 16,
+ fontWeight: 'bold',
+ },
+ expired: {
+ color: '#ff3b30',
+ fontSize: 16,
+ fontWeight: 'bold',
+ },
+ expiresText: {
+ opacity: 0.7,
+ },
+ codeBlock: {
+ backgroundColor: '#f5f5f522',
+ padding: 12,
+ borderRadius: 8,
+ borderWidth: 1,
+ borderColor: '#e0e0e033',
+ },
+ codeText: {
+ fontFamily: 'monospace',
+ fontSize: 12,
+ },
+ propertyContainer: {
+ gap: 8,
+ },
+ property: {
+ flexDirection: 'row',
+ gap: 8,
+ },
+ propertyLabel: {
+ fontWeight: 'bold',
+ minWidth: 100,
+ },
+ propertyValue: {
+ flex: 1,
+ opacity: 0.8,
+ },
+});
diff --git a/e2e/apps/react-native-oidc/app/_layout.tsx b/e2e/apps/react-native-oidc/app/_layout.tsx
index 436a5af..653bb8d 100644
--- a/e2e/apps/react-native-oidc/app/_layout.tsx
+++ b/e2e/apps/react-native-oidc/app/_layout.tsx
@@ -3,33 +3,9 @@ import { useFonts } from 'expo-font';
import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import 'react-native-reanimated';
-
-import * as Crypto from 'expo-crypto';
-
import { useColorScheme } from '@/hooks/useColorScheme';
-// temp polyfill crypto libs
-global.crypto = {
- // @ts-ignore
- getRandomValues (typedArray: Uint8Array) {
- return Crypto.getRandomValues(typedArray);
- },
- // @ts-ignore
- randomUUID () {
- return Crypto.randomUUID();
- },
- // @ts-ignore
- subtle: {
- digest (alg, data) {
- // @ts-ignore
- return Crypto.digest(alg, data);
- }
- }
-}
-
-
-
export default function RootLayout() {
const colorScheme = useColorScheme();
const [loaded] = useFonts({
@@ -45,7 +21,6 @@ export default function RootLayout() {
-
diff --git a/e2e/apps/react-native-oidc/auth.ts b/e2e/apps/react-native-oidc/auth.ts
index 3d2f628..44848d0 100644
--- a/e2e/apps/react-native-oidc/auth.ts
+++ b/e2e/apps/react-native-oidc/auth.ts
@@ -1,56 +1,15 @@
import { fetch as expoFetch, FetchResponse } from 'expo/fetch';
import Constants from 'expo-constants';
-import OAuth2Client from '@okta/auth-foundation/client';
+import { OAuth2Client } from '@okta/auth-foundation/core';
-console.log('fetch: ', fetch);
-console.log('Res.json', Response.json);
export const client = new OAuth2Client({
baseURL: Constants?.expoConfig?.extra?.env.ISSUER,
clientId: Constants?.expoConfig?.extra?.env.NATIVE_CLIENT_ID,
// TODO: skip OIDC to avoid PK import errors
- // scopes: ['openid', 'email', 'profile', 'offline_access'],
- scopes: ['offline_access'],
+ scopes: ['openid', 'email', 'profile', 'offline_access'],
+ // scopes: ['offline_access'],
dpop: false,
-},
-{
- // fetchImpl: async (input: string | URL | Request, init?: RequestInit) => {
- // // const { body, ...rest } = { body: undefined, ...init };
- // // const request = input instanceof Request ? input : new Request(input, rest);
- // const request = input instanceof Request ? input : new Request(input, init);
- // // TODO: expand additional request options
- // const { url, method, headers } = request;
- // console.log('url', url);
- // // console.log('body', request.body, init?.body)
-
- // //console.log('typeof body', typeof body, typeof request.body);
- // // const response = await expoFetch(url, { method, headers, body: body === null ? undefined: body });
- // // const json = await response.json();
- // // const { status, statusText } = response;
- // // console.log(Response);
- // // return Response.json(json, { status, statusText, headers: response.headers });
-
- // const response = await expoFetch(url, { method, headers });
- // console.log('typeof response', response instanceof Response);
- // if (method.toLocaleUpperCase() === 'POST') {
- // console.log('post request')
- // const body = await response.json();
- // console.log(body);
- // }
- // return response;
-
- // // const request = input instanceof Request ? input : new Request(input, init);
- // // if (request.body && request.body instanceof URLSearchParams) {
- // // request.body = request.body.toString();
- // // }
- // },
-
-
- // NOTE: this isn't doing anything. The problem seems to be within `URLSearchParams`
- // passing it directly to `fetch` wasn't converting the body to the correct format.
- // directly calling `body: params.toString()` seems to be working fine for now
- // (Fix in oauth2-flows/AuthCodeFlow/prepare)
-
fetchImpl: async (input: string | URL | Request, init?: RequestInit) => {
// const { body, ...rest } = { body: undefined, ...init };
// const request = input instanceof Request ? input : new Request(input, rest);
@@ -64,5 +23,4 @@ export const client = new OAuth2Client({
console.log(response.body);
return response;
}
-}
-);
+});
diff --git a/e2e/apps/react-native-oidc/babel.config.js b/e2e/apps/react-native-oidc/babel.config.js
new file mode 100644
index 0000000..8c0e3d1
--- /dev/null
+++ b/e2e/apps/react-native-oidc/babel.config.js
@@ -0,0 +1,10 @@
+module.exports = function (api) {
+ api.cache(true);
+
+ return {
+ presets: ['babel-preset-expo'],
+ plugins: [
+ '@babel/plugin-transform-class-static-block',
+ ],
+ };
+};
diff --git a/e2e/apps/react-native-oidc/eslint.config.js b/e2e/apps/react-native-oidc/eslint.config.js
index 24041ab..ece56ff 100644
--- a/e2e/apps/react-native-oidc/eslint.config.js
+++ b/e2e/apps/react-native-oidc/eslint.config.js
@@ -12,7 +12,12 @@ module.exports = defineConfig([
// `okta-client-javascript` dependencies because they will not be installed via npm, they are "installed" via a build.
// Ignore them to avoid require builds to be ran before linting
settings: {
- "import/core-modules": ["@okta/auth-foundation", "@okta/oauth2-flows"]
+ "import/core-modules": [
+ "@okta/auth-foundation",
+ "@okta/oauth2-flows",
+ "@okta/react-native-platform",
+ "@okta/react-native-webcrypto-bridge"
+ ]
}
}
]);
diff --git a/e2e/apps/react-native-oidc/hooks/useAuth.ts b/e2e/apps/react-native-oidc/hooks/useAuth.ts
index d698490..f30a671 100644
--- a/e2e/apps/react-native-oidc/hooks/useAuth.ts
+++ b/e2e/apps/react-native-oidc/hooks/useAuth.ts
@@ -1,35 +1,45 @@
import { useCallback } from 'react';
import { useRouter, type Router } from 'expo-router';
+import { Platform } from 'react-native';
import { openAuthSessionAsync } from 'expo-web-browser';
-import { AuthorizationCodeFlow, SessionLogoutFlow, AuthTransaction } from '@okta/oauth2-flows';
-import { Credential } from '@okta/auth-foundation';
+// import { AuthorizationCodeFlow, SessionLogoutFlow, AuthTransaction } from '@okta/oauth2-flows';
+// import { Credential } from '@okta/auth-foundation/core';
+import {
+ AuthorizationCodeFlow,
+ SessionLogoutFlow,
+ AuthTransaction,
+ Credential
+} from '@okta/react-native-platform';
import { client } from '@/auth';
async function performSignIn () {
try {
console.log('here 1')
+ // Platform-specific redirect URI - iOS uses single slash, Android uses double slash
+ const redirectUri = Platform.OS === 'ios'
+ ? 'com.oktapreview.jperreault-test:/callback'
+ : 'com.oktapreview.jperreault-test://callback';
+
// TODO: move to env
const flow = new AuthorizationCodeFlow(client, {
- redirectUri: 'com.oktapreview.jperreault-test:/callback'
+ redirectUri
});
- // TODO: improve this pattern, too awkward
- // .save was migrated away from AuthCodeFlow
+ console.log('here 1')
const uri = await flow.start();
- console.log('here 2')
- console.log('authorize url', uri);
// @ts-ignore
const transaction = new AuthTransaction(flow.context);
await transaction.save();
- const result = await openAuthSessionAsync(uri.href, 'com.oktapreview.jperreault-test:/callback');
+ const result = await openAuthSessionAsync(uri.href, redirectUri);
console.log('result: ', result)
// @ts-ignore
const { token, context } = await flow.resume(result.url);
console.log('token', token);
console.log('context', context);
- Credential.store(token);
+ const credential = await Credential.store(token);
+ return credential.id;
}
catch (err) {
console.log('here 3');
@@ -38,28 +48,19 @@ async function performSignIn () {
}
}
-// TODO: cannot use oidc logout as openid is not a request scope currently
async function performSignOut () {
const isOIDC = client.configuration.scopes.includes('openid');
- if (isOIDC) {
- // TODO:
- throw new Error('Not implemented');
- }
- else {
- // TODO: /revoke fails due to same URLSearchParams issue
- await (await Credential.getDefault())?.revoke();
- // await (await Credential.getDefault())?.remove();
- }
+ // TODO: implement oidc logout
+ await (await Credential.getDefault())?.revoke();
}
export function useAuth () {
const router = useRouter();
- const signIn = useCallback(async (redirectTo: Parameters[0]) => {
- Credential.clear();
- await performSignIn();
- router.navigate(redirectTo);
+ const signIn = useCallback(async () => {
+ const id = await performSignIn();
+ return id;
}, [router]);
const signOut = useCallback(async (redirectTo: Parameters[0]) => {
diff --git a/e2e/apps/react-native-oidc/index.js b/e2e/apps/react-native-oidc/index.js
new file mode 100644
index 0000000..48f0382
--- /dev/null
+++ b/e2e/apps/react-native-oidc/index.js
@@ -0,0 +1,12 @@
+import '@expo/metro-runtime';
+import 'expo-router/entry';
+
+import { Platform, installWebCryptoPolyfill } from '@okta/react-native-platform';
+installWebCryptoPolyfill();
+
+console.log('Plat', Platform, Platform.TimeCoordinator)
+console.log("globalThis.crypto", globalThis.crypto);
+// global.crypto = global.crypto ?? globalThis.crypto;
+
+import { Credential } from '@okta/react-native-platform';
+Credential.coordinator.tokenStorage.emitter.on('token_added', (...args) => console.log(args))
diff --git a/e2e/apps/react-native-oidc/metro.config.js b/e2e/apps/react-native-oidc/metro.config.js
index 462ef71..087de6e 100644
--- a/e2e/apps/react-native-oidc/metro.config.js
+++ b/e2e/apps/react-native-oidc/metro.config.js
@@ -18,11 +18,21 @@ const monorepoRoot = path.resolve(projectRoot, '../../..');
const config = getDefaultConfig(projectRoot);
// 1. Watch all files within the monorepo
-config.watchFolders = [monorepoRoot];
+config.watchFolders = [...config.watchFolders, monorepoRoot];
// 2. Let Metro know where to resolve packages and in what order
config.resolver.nodeModulesPaths = [
+ ...config.resolver.nodeModulesPaths,
path.resolve(projectRoot, 'node_modules'),
path.resolve(monorepoRoot, 'node_modules'),
];
+// Ensure workspace packages are resolved
+config.resolver.disableHierarchicalLookup = false;
+
+// Don't try to transform react-native internals
+config.resolver.blockList = [
+ // Block nested node_modules in workspace packages
+ /packages\/[^/]+\/node_modules\/.*/,
+];
+
module.exports = config;
diff --git a/e2e/apps/react-native-oidc/package.json b/e2e/apps/react-native-oidc/package.json
index 858a7da..7811a71 100644
--- a/e2e/apps/react-native-oidc/package.json
+++ b/e2e/apps/react-native-oidc/package.json
@@ -1,6 +1,6 @@
{
"name": "@repo/react-native-oidc",
- "main": "expo-router/entry",
+ "main": "index.js",
"version": "1.0.0",
"scripts": {
"start": "expo start",
@@ -11,43 +11,46 @@
"lint": "expo lint"
},
"dependencies": {
- "@expo/vector-icons": "^14.1.0",
+ "@expo/metro-runtime": "^6.1.2",
+ "@expo/vector-icons": "^15.0.3",
"@okta/auth-foundation": "*",
"@okta/oauth2-flows": "*",
- "@react-navigation/bottom-tabs": "^7.3.10",
- "@react-navigation/elements": "^2.3.8",
- "@react-navigation/native": "^7.1.6",
- "expo": "~53.0.10",
- "expo-blur": "~14.1.4",
- "expo-constants": "~17.1.6",
- "expo-crypto": "~14.1.4",
- "expo-dev-client": "~5.1.8",
- "expo-font": "~13.3.1",
- "expo-haptics": "~14.1.4",
- "expo-image": "~2.1.7",
- "expo-linking": "~7.1.5",
- "expo-router": "~5.0.6",
- "expo-splash-screen": "~0.30.8",
- "expo-status-bar": "~2.2.3",
- "expo-symbols": "~0.4.4",
- "expo-system-ui": "~5.0.7",
- "expo-web-browser": "~14.1.6",
- "react": "19.0.1",
- "react-dom": "19.0.1",
- "react-native": "0.79.2",
- "react-native-gesture-handler": "~2.24.0",
- "react-native-reanimated": "~3.17.4",
- "react-native-safe-area-context": "5.4.0",
- "react-native-screens": "~4.10.0",
- "react-native-web": "~0.20.0",
- "react-native-webview": "13.13.5"
+ "@okta/react-native-platform": "*",
+ "expo": "^54.0.0",
+ "expo-blur": "~15.0.7",
+ "expo-constants": "~18.0.10",
+ "expo-crypto": "~15.0.7",
+ "expo-dev-client": "~6.0.18",
+ "expo-font": "~14.0.9",
+ "expo-haptics": "~15.0.7",
+ "expo-image": "~3.0.10",
+ "expo-linking": "~8.0.9",
+ "expo-router": "~6.0.15",
+ "expo-splash-screen": "~31.0.11",
+ "expo-status-bar": "~3.0.8",
+ "expo-symbols": "~1.0.7",
+ "expo-system-ui": "~6.0.8",
+ "expo-web-browser": "~15.0.9",
+ "react": "19.1.0",
+ "react-dom": "19.1.0",
+ "react-native": "0.81.5",
+ "react-native-gesture-handler": "~2.28.0",
+ "react-native-reanimated": "~4.1.1",
+ "react-native-safe-area-context": "~5.6.0",
+ "react-native-screens": "~4.16.0",
+ "react-native-web": "^0.21.0",
+ "react-native-webview": "13.15.0",
+ "react-native-worklets": "0.5.1"
},
"devDependencies": {
"@babel/core": "^7.25.2",
+ "@babel/plugin-transform-class-static-block": "^7.28.6",
+ "@react-native-community/cli": "latest",
"@repo/env": "*",
"@types/react": "~19.0.10",
"eslint": "^9.25.0",
"eslint-config-expo": "~9.2.0",
+ "expo-build-properties": "^1.0.10",
"typescript": "~5.8.3"
},
"private": true
diff --git a/e2e/apps/react-native-oidc/react-native.config.js b/e2e/apps/react-native-oidc/react-native.config.js
new file mode 100644
index 0000000..a39b60d
--- /dev/null
+++ b/e2e/apps/react-native-oidc/react-native.config.js
@@ -0,0 +1,12 @@
+const path = require('path');
+
+module.exports = {
+ dependencies: {
+ '@okta/react-native-webcrypto-bridge': {
+ root: path.resolve(__dirname, '../../packages/react-native-webcrypto-bridge'),
+ },
+ '@okta/react-native-platform': {
+ root: path.resolve(__dirname, '../../packages/react-native-platform'),
+ },
+ },
+};
diff --git a/e2e/apps/react-native-oidc/tsconfig.json b/e2e/apps/react-native-oidc/tsconfig.json
index fc3e146..78bcfd9 100644
--- a/e2e/apps/react-native-oidc/tsconfig.json
+++ b/e2e/apps/react-native-oidc/tsconfig.json
@@ -10,6 +10,7 @@
},
"include": [
"**/*.ts",
- "**/*.tsx"
+ "**/*.tsx",
+ "index.js"
]
}
diff --git a/package.json b/package.json
index 13a80e3..0c272b5 100644
--- a/package.json
+++ b/package.json
@@ -39,9 +39,9 @@
"**/expo-*/**",
"**/@expo/**",
"**/@expo/**/**",
- "**/react-native",
- "**/react-native/**",
"**/@react-navigation/**",
+ "react",
+ "react-native",
"react-native-oidc/@okta/*",
"react-native-oidc/@okta/**"
]
diff --git a/packages/auth-foundation/package.json b/packages/auth-foundation/package.json
index b36c0c0..b692206 100644
--- a/packages/auth-foundation/package.json
+++ b/packages/auth-foundation/package.json
@@ -5,7 +5,6 @@
"main": "dist/esm/index.js",
"module": "dist/esm/index.js",
"types": "dist/types/index.d.ts",
- "author": "jared.perreault@okta.com",
"license": "Apache-2.0",
"private": true,
"engines": {
@@ -37,7 +36,10 @@
"./package.json": "./package.json"
},
"sideEffects": [
- "./src/oktaUserAgent.ts"
+ "./src/index.ts",
+ "./src/oktaUserAgent.ts",
+ "./dist/esm/index.js",
+ "./dist/esm/oktaUserAgent.ts"
],
"scripts": {
"lint": "eslint --ext .js,.ts,.jsx .",
diff --git a/packages/auth-foundation/src/Credential/CredentialCoordinator.ts b/packages/auth-foundation/src/Credential/CredentialCoordinator.ts
index d5c82be..1a2bc8a 100644
--- a/packages/auth-foundation/src/Credential/CredentialCoordinator.ts
+++ b/packages/auth-foundation/src/Credential/CredentialCoordinator.ts
@@ -47,7 +47,7 @@ export interface CredentialCoordinator {
*/
getDefault (): Promise;
setDefault (cred: Credential | null): Promise;
- get tokenStorage (): TokenStorage;
+ tokenStorage: TokenStorage;
/**
* Writes the provided {@link Token.Token | Token} (and {@link Token.Token.Metadata:TYPE | Token.Metadata}) to storage and creates a {@link Credential}
* instance to represent the {@link Token.Token | Token} via the {@link CredentialDataSource}
@@ -247,7 +247,12 @@ export class CredentialCoordinatorImpl implements CredentialCoordinator {
}
protected async loadDefaultCredential (): Promise {
- const defaultTokenId = this.tokenStorage.defaultTokenId;
+ let defaultTokenId = this.tokenStorage.defaultTokenId;
+ if (defaultTokenId === undefined) {
+ // `undefined` value indicates storage (source-of-truth) hasn't been checked yet
+ // the value should only be `undefined` at bootstrapping time
+ defaultTokenId = await this.tokenStorage.loadDefaultTokenId();
+ }
if (!defaultTokenId) {
return null;
}
diff --git a/packages/auth-foundation/src/Credential/TokenStorage.ts b/packages/auth-foundation/src/Credential/TokenStorage.ts
index f115b6d..50e72ce 100644
--- a/packages/auth-foundation/src/Credential/TokenStorage.ts
+++ b/packages/auth-foundation/src/Credential/TokenStorage.ts
@@ -33,7 +33,11 @@ export interface TokenStorage {
/**
* In memory cached value of the {@link Credential.getDefault | default Credential}'s id
*/
- readonly defaultTokenId: string | null;
+ readonly defaultTokenId: string | null | undefined;
+ /**
+ * Queries storage location for stored id
+ */
+ loadDefaultTokenId (): Promise;
/**
* Updates the stored {@link Credential.getDefault | default Credential} id
*/
@@ -103,6 +107,10 @@ export class DefaultTokenStorage implements TokenStorage {
return this.#defaultId;
}
+ async loadDefaultTokenId (): Promise {
+ return this.defaultTokenId;
+ }
+
async setDefaultTokenId (id: string | null): Promise {
if (id === this.defaultTokenId) {
return;
diff --git a/packages/auth-foundation/src/FetchClient.ts b/packages/auth-foundation/src/FetchClient.ts
index 50b671a..56b5fe5 100644
--- a/packages/auth-foundation/src/FetchClient.ts
+++ b/packages/auth-foundation/src/FetchClient.ts
@@ -92,6 +92,8 @@ export class FetchClient extends
await this.prepareAcrStepUpRetry(response, request, wwwAuthError);
}
+ // TODO: clear token???
+
// super.send() will sign call .authorize()
}
diff --git a/packages/oauth2-flows/package.json b/packages/oauth2-flows/package.json
index 1aeaeec..6b600db 100644
--- a/packages/oauth2-flows/package.json
+++ b/packages/oauth2-flows/package.json
@@ -5,7 +5,6 @@
"main": "dist/esm/index.js",
"module": "dist/esm/index.js",
"types": "dist/types/index.d.ts",
- "author": "jared.perreault@okta.com",
"license": "Apache-2.0",
"private": true,
"files": [
diff --git a/packages/react-native-platform/.eslintrc.cjs b/packages/react-native-platform/.eslintrc.cjs
new file mode 100644
index 0000000..22f15fe
--- /dev/null
+++ b/packages/react-native-platform/.eslintrc.cjs
@@ -0,0 +1,4 @@
+module.exports = {
+ extends: ["@repo/eslint-config/react-native-sdk.js"],
+ root: true,
+};
diff --git a/packages/react-native-platform/LICENSE b/packages/react-native-platform/LICENSE
new file mode 100644
index 0000000..eeca71e
--- /dev/null
+++ b/packages/react-native-platform/LICENSE
@@ -0,0 +1,189 @@
+Okta Auth SDK License
+
+The Okta software accompanied by this notice is provided pursuant to the
+following terms:
+
+Copyright © 2015-present, Okta, Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License"); you may not use
+this file except in compliance with the License. You may obtain a copy of the
+License at http://www.apache.org/licenses/LICENSE-2.0. Unless required by
+applicable law or agreed to in writing, software distributed under the License
+is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied. See the License for the specific language
+governing permissions and limitations under the License.
+
+The Okta software accompanied by this notice has build dependencies on certain
+third party software licensed under separate terms ("Third Party Components")
+located in THIRD_PARTY_NOTICES.
+
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+"License" shall mean the terms and conditions for use, reproduction, and
+distribution as defined by Sections 1 through 9 of this document.
+
+"Licensor" shall mean the copyright owner or entity authorized by the copyright
+owner that is granting the License.
+
+"Legal Entity" shall mean the union of the acting entity and all other entities
+that control, are controlled by, or are under common control with that entity.
+For the purposes of this definition, "control" means (i) the power, direct or
+indirect, to cause the direction or management of such entity, whether by
+contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
+outstanding shares, or (iii) beneficial ownership of such entity.
+
+You" (or "Your") shall mean an individual or Legal Entity exercising
+permissions granted by this License.
+
+"Source" form shall mean the preferred form for making modifications, including
+but not limited to software source code, documentation source, and
+configuration files.
+
+"Object" form shall mean any form resulting from mechanical transformation or
+translation of a Source form, including but not limited to compiled object
+code, generated documentation, and conversions to other media types.
+
+"Work" shall mean the work of authorship, whether in Source or Object form,
+made available under the License, as indicated by a copyright notice that is
+included in or attached to the work (an example is provided in the Appendix
+below).
+
+"Derivative Works" shall mean any work, whether in Source or Object form, that
+is based on (or derived from) the Work and for which the editorial revisions,
+annotations, elaborations, or other modifications represent, as a whole, an
+original work of authorship. For the purposes of this License, Derivative Works
+shall not include works that remain separable from, or merely link (or bind by
+name) to the interfaces of, the Work and Derivative Works thereof.
+
+"Contribution" shall mean any work of authorship, including the original
+version of the Work and any modifications or additions to that Work or
+Derivative Works thereof, that is intentionally submitted to Licensor for
+inclusion in the Work by the copyright owner or by an individual or Legal
+Entity authorized to submit on behalf of the copyright owner. For the purposes
+of this definition, "submitted" means any form of electronic, verbal, or
+written communication sent to the Licensor or its representatives, including
+but not limited to communication on electronic mailing lists, source code
+control systems, and issue tracking systems that are managed by, or on behalf
+of, the Licensor for the purpose of discussing and improving the Work, but
+excluding communication that is conspicuously marked or otherwise designated in
+writing by the copyright owner as "Not a Contribution."
+
+"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
+of whom a Contribution has been received by Licensor and subsequently
+incorporated within the Work.
+2. Grant of Copyright License. Subject to the terms and conditions of this
+License, each Contributor hereby grants to You a perpetual, worldwide,
+non-exclusive, no-charge, royalty-free, irrevocable copyright license to
+reproduce, prepare Derivative Works of, publicly display, publicly perform,
+sublicense, and distribute the Work and such Derivative Works in Source or
+Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of this
+License, each Contributor hereby grants to You a perpetual, worldwide,
+non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this
+section) patent license to make, have made, use, offer to sell, sell, import,
+and otherwise transfer the Work, where such license applies only to those
+patent claims licensable by such Contributor that are necessarily infringed by
+their Contribution(s) alone or by combination of their Contribution(s) with the
+Work to which such Contribution(s) was submitted. If You institute patent
+litigation against any entity (including a cross-claim or counterclaim in a
+lawsuit) alleging that the Work or a Contribution incorporated within the Work
+constitutes direct or contributory patent infringement, then any patent
+licenses granted to You under this License for that Work shall terminate as of
+the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the Work or
+Derivative Works thereof in any medium, with or without modifications, and in
+Source or Object form, provided that You meet the following conditions:
+
+(a) You must give any other recipients of the Work or Derivative Works a copy
+of this License; and
+
+(b) You must cause any modified files to carry prominent notices stating that
+You changed the files; and
+
+(c) You must retain, in the Source form of any Derivative Works that You
+distribute, all copyright, patent, trademark, and attribution notices from the
+Source form of the Work, excluding those notices that do not pertain to any
+part of the Derivative Works; and
+
+(d) If the Work includes a "NOTICE" text file as part of its distribution, then
+any Derivative Works that You distribute must include a readable copy of the
+attribution notices contained within such NOTICE file, excluding those notices
+that do not pertain to any part of the Derivative Works, in at least one of the
+following places: within a NOTICE text file distributed as part of the
+Derivative Works; within the Source form or documentation, if provided along
+with the Derivative Works; or, within a display generated by the Derivative
+Works, if and wherever such third-party notices normally appear. The contents
+of the NOTICE file are for informational purposes only and do not modify the
+License. You may add Your own attribution notices within Derivative Works that
+You distribute, alongside or as an addendum to the NOTICE text from the Work,
+provided that such additional attribution notices cannot be construed as
+modifying the License.
+
+You may add Your own copyright statement to Your modifications and may provide
+additional or different license terms and conditions for use, reproduction, or
+distribution of Your modifications, or for any such Derivative Works as a
+whole, provided Your use, reproduction, and distribution of the Work otherwise
+complies with the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise, any
+Contribution intentionally submitted for inclusion in the Work by You to the
+Licensor shall be under the terms and conditions of this License, without any
+additional terms or conditions. Notwithstanding the above, nothing herein shall
+supersede or modify the terms of any separate license agreement you may have
+executed with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade names,
+trademarks, service marks, or product names of the Licensor, except as required
+for reasonable and customary use in describing the origin of the Work and
+reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or agreed to in
+writing, Licensor provides the Work (and each Contributor provides its
+Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied, including, without limitation, any warranties
+or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+PARTICULAR PURPOSE. You are solely responsible for determining the
+appropriateness of using or redistributing the Work and assume any risks
+associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory, whether in
+tort (including negligence), contract, or otherwise, unless required by
+applicable law (such as deliberate and grossly negligent acts) or agreed to in
+writing, shall any Contributor be liable to You for damages, including any
+direct, indirect, special, incidental, or consequential damages of any
+character arising as a result of this License or out of the use or inability to
+use the Work (including but not limited to damages for loss of goodwill, work
+stoppage, computer failure or malfunction, or any and all other commercial
+damages or losses), even if such Contributor has been advised of the
+possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing the Work or
+Derivative Works thereof, You may choose to offer, and charge a fee for,
+acceptance of support, warranty, indemnity, or other liability obligations
+and/or rights consistent with this License. However, in accepting such
+obligations, You may act only on Your own behalf and on Your sole
+responsibility, not on behalf of any other Contributor, and only if You agree
+to indemnify, defend, and hold each Contributor harmless for any liability
+incurred by, or claims asserted against, such Contributor by reason of your
+accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work.
+
+To apply the Apache License to your work, attach the following boilerplate
+notice, with the fields enclosed by brackets "[]" replaced with your own
+identifying information. (Don't include the brackets!) The text should be
+enclosed in the appropriate comment syntax for the file format. We also
+recommend that a file or class name and description of purpose be included on
+the same "printed page" as the copyright notice for easier identification
+within third-party archives.
diff --git a/packages/react-native-platform/android/.gitignore b/packages/react-native-platform/android/.gitignore
new file mode 100644
index 0000000..fd9cd50
--- /dev/null
+++ b/packages/react-native-platform/android/.gitignore
@@ -0,0 +1,3 @@
+.gradle
+local.properties
+.kotlin
diff --git a/packages/react-native-platform/android/build.gradle b/packages/react-native-platform/android/build.gradle
new file mode 100644
index 0000000..56526ff
--- /dev/null
+++ b/packages/react-native-platform/android/build.gradle
@@ -0,0 +1,83 @@
+buildscript {
+ ext.kotlin_version = '2.1.0'
+
+ repositories {
+ google()
+ mavenCentral()
+ }
+
+ dependencies {
+ classpath 'com.android.tools.build:gradle:8.1.0'
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ }
+}
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ namespace "com.okta.reactnativeplatform"
+ compileSdkVersion 35
+
+ defaultConfig {
+ minSdkVersion 24
+ targetSdkVersion 35
+ versionCode 1
+ versionName "1.0"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
+ }
+
+ kotlinOptions {
+ jvmTarget = '17'
+ }
+
+ testOptions {
+ unitTests.includeAndroidResources = true
+ }
+
+ sourceSets {
+ main {
+ java {
+ exclude 'com/okta/reactnativeplatform/browserSession/**'
+ }
+ }
+ }
+}
+
+repositories {
+ google()
+ mavenCentral()
+}
+
+dependencies {
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
+ implementation 'com.facebook.react:react-android:0.81.0'
+ implementation 'androidx.security:security-crypto:1.1.0-alpha06'
+ implementation 'androidx.browser:browser:1.7.0'
+
+ // Testing dependencies
+ testImplementation 'junit:junit:4.13.2'
+ testImplementation 'com.google.truth:truth:1.1.2'
+ testImplementation 'io.mockk:mockk:1.14.9'
+ testImplementation 'org.robolectric:robolectric:4.16.1'
+ testImplementation 'androidx.test:core:1.5.0'
+}
+
+// Filter tests to only run TokenStorage tests
+tasks.withType(Test) {
+ filter {
+ includeTestsMatching 'com.okta.reactnativeplatform.TokenStorageModuleTest'
+ }
+}
+
+
diff --git a/packages/react-native-platform/android/gradle.properties b/packages/react-native-platform/android/gradle.properties
new file mode 100644
index 0000000..3e784c7
--- /dev/null
+++ b/packages/react-native-platform/android/gradle.properties
@@ -0,0 +1,2 @@
+android.useAndroidX=true
+android.suppressUnsupportedCompileSdk=35
diff --git a/packages/react-native-platform/android/gradle/wrapper/gradle-wrapper.jar b/packages/react-native-platform/android/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..a4b76b9
Binary files /dev/null and b/packages/react-native-platform/android/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/packages/react-native-platform/android/gradle/wrapper/gradle-wrapper.properties b/packages/react-native-platform/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..37f853b
--- /dev/null
+++ b/packages/react-native-platform/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/packages/react-native-platform/android/gradlew b/packages/react-native-platform/android/gradlew
new file mode 100755
index 0000000..f3b75f3
--- /dev/null
+++ b/packages/react-native-platform/android/gradlew
@@ -0,0 +1,251 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/packages/react-native-platform/android/gradlew.bat b/packages/react-native-platform/android/gradlew.bat
new file mode 100644
index 0000000..9d21a21
--- /dev/null
+++ b/packages/react-native-platform/android/gradlew.bat
@@ -0,0 +1,94 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/packages/react-native-platform/android/src/main/AndroidManifest.xml b/packages/react-native-platform/android/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..16de78c
--- /dev/null
+++ b/packages/react-native-platform/android/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
\ No newline at end of file
diff --git a/packages/react-native-platform/android/src/main/java/com/okta/reactnativeplatform/OktaReactNativePlatformPackage.kt b/packages/react-native-platform/android/src/main/java/com/okta/reactnativeplatform/OktaReactNativePlatformPackage.kt
new file mode 100644
index 0000000..db1f476
--- /dev/null
+++ b/packages/react-native-platform/android/src/main/java/com/okta/reactnativeplatform/OktaReactNativePlatformPackage.kt
@@ -0,0 +1,20 @@
+package com.okta.reactnativeplatform
+
+import com.facebook.react.ReactPackage
+import com.facebook.react.bridge.NativeModule
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.uimanager.ViewManager
+
+class OktaReactNativePlatformPackage : ReactPackage {
+ override fun createNativeModules(reactContext: ReactApplicationContext): List {
+ return listOf(
+ TokenStorageModule(reactContext)
+ )
+ }
+
+ @Suppress("DEPRECATION")
+ override fun createViewManagers(reactContext: ReactApplicationContext): List> {
+ return emptyList()
+ }
+}
+
diff --git a/packages/react-native-platform/android/src/main/java/com/okta/reactnativeplatform/browserSession/BrowserSessionModule.kt.disabled b/packages/react-native-platform/android/src/main/java/com/okta/reactnativeplatform/browserSession/BrowserSessionModule.kt.disabled
new file mode 100644
index 0000000..eaeb7c6
--- /dev/null
+++ b/packages/react-native-platform/android/src/main/java/com/okta/reactnativeplatform/browserSession/BrowserSessionModule.kt.disabled
@@ -0,0 +1,58 @@
+package com.okta.reactnativeplatform
+
+import android.app.Activity
+import android.content.Intent
+import android.net.Uri
+import androidx.browser.customtabs.CustomTabsIntent
+import com.facebook.react.bridge.*
+import com.facebook.react.module.annotations.ReactModule
+
+@ReactModule(name = BrowserSessionModule.NAME)
+class BrowserSessionModule(reactContext: ReactApplicationContext) :
+ ReactContextBaseJavaModule(reactContext) {
+
+ companion object {
+ const val NAME = "BrowserSessionBridge"
+ }
+
+ override fun getName(): String = NAME
+
+ @ReactMethod
+ fun launchBrowserSession(url: String, promise: Promise) {
+ try {
+ val activity = currentActivity as? Activity
+ if (activity == null) {
+ promise.reject("no_activity", "Current activity is not available")
+ return
+ }
+
+ val uri = Uri.parse(url)
+ if (uri.scheme == null || uri.host == null) {
+ promise.reject("invalid_url", "Invalid URL provided")
+ return
+ }
+
+ val customTabsIntent = CustomTabsIntent.Builder()
+ .setShareState(CustomTabsIntent.SHARE_STATE_ON)
+ .build()
+
+ customTabsIntent.launchUrl(activity, uri)
+ promise.resolve(null)
+ } catch (e: Exception) {
+ promise.reject("browser_session_error", "Failed to launch browser session", e)
+ }
+ }
+
+ @ReactMethod
+ fun closeBrowserSession(promise: Promise) {
+ try {
+ val activity = currentActivity as? Activity
+ if (activity != null) {
+ activity.finish()
+ }
+ promise.resolve(null)
+ } catch (e: Exception) {
+ promise.reject("close_error", "Failed to close browser session", e)
+ }
+ }
+}
diff --git a/packages/react-native-platform/android/src/main/java/com/okta/reactnativeplatform/tokenStorage/TokenStorageModule.kt b/packages/react-native-platform/android/src/main/java/com/okta/reactnativeplatform/tokenStorage/TokenStorageModule.kt
new file mode 100644
index 0000000..3f9461f
--- /dev/null
+++ b/packages/react-native-platform/android/src/main/java/com/okta/reactnativeplatform/tokenStorage/TokenStorageModule.kt
@@ -0,0 +1,178 @@
+package com.okta.reactnativeplatform
+
+import android.content.Context
+import android.content.SharedPreferences
+import androidx.security.crypto.EncryptedSharedPreferences
+import androidx.security.crypto.MasterKey
+import com.facebook.react.bridge.*
+import com.facebook.react.module.annotations.ReactModule
+
+@ReactModule(name = TokenStorageModule.NAME)
+class TokenStorageModule(reactContext: ReactApplicationContext) :
+ ReactContextBaseJavaModule(reactContext) {
+
+ companion object {
+ const val NAME = "TokenStorageBridge"
+ private const val PREFS_TOKENS = "okta_tokens"
+ private const val PREFS_METADATA = "okta_metadata"
+ private const val DEFAULT_TOKEN_KEY = "okta-default-token"
+ }
+
+ override fun getName(): String = NAME
+
+ private val masterKey: MasterKey? by lazy {
+ try {
+ MasterKey.Builder(reactContext)
+ .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
+ .build()
+ } catch (e: Exception) {
+ // KeyStore may not be available in test environments
+ null
+ }
+ }
+
+ // Secure storage for tokens (lazy initialized)
+ private val securePrefs: SharedPreferences by lazy {
+ val mk = masterKey
+ if (mk != null) {
+ try {
+ EncryptedSharedPreferences.create(
+ reactContext,
+ PREFS_TOKENS,
+ mk,
+ EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
+ EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
+ )
+ } catch (e: Exception) {
+ // Fall back to unencrypted storage if encryption fails (e.g., in tests)
+ reactContext.getSharedPreferences(PREFS_TOKENS, Context.MODE_PRIVATE)
+ }
+ } else {
+ // Use unencrypted storage if KeyStore is unavailable
+ reactContext.getSharedPreferences(PREFS_TOKENS, Context.MODE_PRIVATE)
+ }
+ }
+
+ // Regular storage for metadata
+ private val metadataPrefs: SharedPreferences by lazy {
+ reactContext.getSharedPreferences(
+ PREFS_METADATA,
+ Context.MODE_PRIVATE
+ )
+ }
+
+ // MARK: - Token Operations (Secure Storage)
+
+ @ReactMethod
+ fun saveToken(id: String, tokenData: String, promise: Promise) {
+ try {
+ securePrefs.edit().putString(id, tokenData).apply()
+ promise.resolve(null)
+ } catch (e: Exception) {
+ promise.reject("token_save_error", "Failed to save token", e)
+ }
+ }
+
+ @ReactMethod
+ fun getToken(id: String, promise: Promise) {
+ try {
+ val value = securePrefs.getString(id, null)
+ promise.resolve(value)
+ } catch (e: Exception) {
+ promise.resolve(null)
+ }
+ }
+
+ @ReactMethod
+ fun removeToken(id: String, promise: Promise) {
+ try {
+ securePrefs.edit().remove(id).apply()
+ metadataPrefs.edit().remove(id).apply()
+ promise.resolve(null)
+ } catch (e: Exception) {
+ promise.reject("token_remove_error", "Failed to remove token", e)
+ }
+ }
+
+ @ReactMethod
+ fun getAllTokenIds(promise: Promise) {
+ try {
+ val keys = securePrefs.all.keys.toList()
+ val array = Arguments.createArray()
+ keys.forEach { array.pushString(it) }
+ promise.resolve(array)
+ } catch (e: Exception) {
+ promise.reject("token_list_error", "Failed to get token IDs", e)
+ }
+ }
+
+ @ReactMethod
+ fun clearTokens(promise: Promise) {
+ try {
+ securePrefs.edit().clear().apply()
+ metadataPrefs.edit().clear().apply()
+ metadataPrefs.edit().remove(DEFAULT_TOKEN_KEY).apply()
+ promise.resolve(null)
+ } catch (e: Exception) {
+ promise.reject("token_clear_error", "Failed to clear tokens", e)
+ }
+ }
+
+ // MARK: - Metadata Operations (Regular Storage)
+
+ @ReactMethod
+ fun saveMetadata(id: String, metadataData: String, promise: Promise) {
+ try {
+ metadataPrefs.edit().putString(id, metadataData).apply()
+ promise.resolve(null)
+ } catch (e: Exception) {
+ promise.reject("metadata_save_error", "Failed to save metadata", e)
+ }
+ }
+
+ @ReactMethod
+ fun getMetadata(id: String, promise: Promise) {
+ try {
+ val value = metadataPrefs.getString(id, null)
+ promise.resolve(value)
+ } catch (e: Exception) {
+ promise.resolve(null)
+ }
+ }
+
+ @ReactMethod
+ fun removeMetadata(id: String, promise: Promise) {
+ try {
+ metadataPrefs.edit().remove(id).apply()
+ promise.resolve(null)
+ } catch (e: Exception) {
+ promise.reject("metadata_remove_error", "Failed to remove metadata", e)
+ }
+ }
+
+ // MARK: - Default Token ID
+
+ @ReactMethod
+ fun setDefaultTokenId(id: String?, promise: Promise) {
+ try {
+ if (id != null) {
+ metadataPrefs.edit().putString(DEFAULT_TOKEN_KEY, id).apply()
+ } else {
+ metadataPrefs.edit().remove(DEFAULT_TOKEN_KEY).apply()
+ }
+ promise.resolve(null)
+ } catch (e: Exception) {
+ promise.reject("default_token_error", "Failed to set default token ID", e)
+ }
+ }
+
+ @ReactMethod
+ fun getDefaultTokenId(promise: Promise) {
+ try {
+ val value = metadataPrefs.getString(DEFAULT_TOKEN_KEY, null)
+ promise.resolve(value)
+ } catch (e: Exception) {
+ promise.resolve(null)
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/react-native-platform/android/src/test/kotlin/com/okta/reactnativeplatform/browserSession/BrowserSessionModuleTest.kt.disabled b/packages/react-native-platform/android/src/test/kotlin/com/okta/reactnativeplatform/browserSession/BrowserSessionModuleTest.kt.disabled
new file mode 100644
index 0000000..05d959d
--- /dev/null
+++ b/packages/react-native-platform/android/src/test/kotlin/com/okta/reactnativeplatform/browserSession/BrowserSessionModuleTest.kt.disabled
@@ -0,0 +1,121 @@
+package com.okta.reactnativeplatform
+
+import android.app.Application
+import androidx.test.core.app.ApplicationProvider
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.bridge.Promise
+import io.mockk.mockk
+import io.mockk.slot
+import io.mockk.verify
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import com.google.common.truth.Truth.assertThat
+
+/**
+ * Unit tests for BrowserSessionModule.
+ * Tests the Kotlin implementation of browser session operations.
+ */
+@RunWith(RobolectricTestRunner::class)
+@Config(sdk = [34])
+class BrowserSessionModuleTest {
+
+ private lateinit var module: BrowserSessionModule
+ private lateinit var context: ReactApplicationContext
+
+ @Before
+ fun setUp() {
+ val application = ApplicationProvider.getApplicationContext()
+ context = ReactApplicationContext(application)
+ module = BrowserSessionModule(context)
+ }
+
+ // MARK: - Module Info Tests
+
+ @Test
+ fun testGetName_shouldReturnBrowserSessionBridge() {
+ val name = module.name
+ assertThat(name).isEqualTo("BrowserSessionBridge")
+ }
+
+ // MARK: - Launch Browser Session Tests
+
+ @Test
+ fun testLaunchBrowserSession_withValidUrl_shouldResolvePromise() {
+ val promise = mockk(relaxed = true)
+
+ module.launchBrowserSession("https://example.com", promise)
+
+ // Note: This test may resolve or reject depending on whether an activity is available
+ // In Robolectric environment, currentActivity may be null
+ verify { promise.reject("no_activity", "Current activity is not available") }
+ }
+
+ @Test
+ fun testLaunchBrowserSession_withInvalidUrl_shouldRejectPromise() {
+ val promise = mockk(relaxed = true)
+ val rejectSlot = slot()
+
+ module.launchBrowserSession("not a valid url", promise)
+
+ verify { promise.reject(capture(rejectSlot), any()) }
+ assertThat(rejectSlot.captured).isEqualTo("invalid_url")
+ }
+
+ @Test
+ fun testLaunchBrowserSession_withEmptyUrl_shouldRejectPromise() {
+ val promise = mockk(relaxed = true)
+ val rejectSlot = slot()
+
+ module.launchBrowserSession("", promise)
+
+ verify { promise.reject(capture(rejectSlot), any()) }
+ assertThat(rejectSlot.captured).isEqualTo("invalid_url")
+ }
+
+ // MARK: - Close Browser Session Tests
+
+ @Test
+ fun testCloseBrowserSession_shouldResolvePromise() {
+ val promise = mockk(relaxed = true)
+
+ module.closeBrowserSession(promise)
+
+ verify { promise.resolve(null) }
+ }
+
+ @Test
+ fun testCloseBrowserSession_withoutActivity_shouldStillResolve() {
+ val promise = mockk(relaxed = true)
+
+ module.closeBrowserSession(promise)
+
+ verify { promise.resolve(null) }
+ }
+
+ // MARK: - Error Handling Tests
+
+ @Test
+ fun testLaunchBrowserSession_withMalformedUrl_shouldRejectInvalidUrl() {
+ val promise = mockk(relaxed = true)
+ val codeSlot = slot()
+
+ module.launchBrowserSession("://invalid", promise)
+
+ // Malformed URL should either be caught or handled gracefully
+ assertThat(promise).isNotNull()
+ }
+
+ @Test
+ fun testLaunchBrowserSession_withSchemeOnly_shouldRejectInvalidUrl() {
+ val promise = mockk(relaxed = true)
+ val rejectSlot = slot()
+
+ module.launchBrowserSession("https://", promise)
+
+ verify { promise.reject(capture(rejectSlot), any()) }
+ assertThat(rejectSlot.captured).isEqualTo("invalid_url")
+ }
+}
diff --git a/packages/react-native-platform/android/src/test/kotlin/com/okta/reactnativeplatform/tokenStorage/TokenStorageModuleTest.kt b/packages/react-native-platform/android/src/test/kotlin/com/okta/reactnativeplatform/tokenStorage/TokenStorageModuleTest.kt
new file mode 100644
index 0000000..8fcabb6
--- /dev/null
+++ b/packages/react-native-platform/android/src/test/kotlin/com/okta/reactnativeplatform/tokenStorage/TokenStorageModuleTest.kt
@@ -0,0 +1,326 @@
+package com.okta.reactnativeplatform
+
+import android.app.Application
+import android.content.SharedPreferences
+import androidx.test.core.app.ApplicationProvider
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.bridge.Promise
+import com.facebook.react.bridge.Arguments
+import com.facebook.react.bridge.WritableArray
+import io.mockk.mockk
+import io.mockk.slot
+import io.mockk.verify
+import io.mockk.every
+import io.mockk.mockkStatic
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import com.google.common.truth.Truth.assertThat
+
+/**
+ * Unit tests for TokenStorageModule.
+ * Tests the Kotlin implementation of token and metadata storage.
+ * Uses unencrypted SharedPreferences in tests to avoid KeyStore issues.
+ */
+@RunWith(RobolectricTestRunner::class)
+@Config(sdk = [34])
+class TokenStorageModuleTest {
+
+ private lateinit var module: TokenStorageModule
+ private lateinit var context: ReactApplicationContext
+ private lateinit var application: Application
+
+ @Before
+ fun setUp() {
+ application = ApplicationProvider.getApplicationContext()
+
+ // Mock Arguments.createArray() to avoid React Native initialization
+ mockkStatic(Arguments::class)
+ every { Arguments.createArray() } answers {
+ mockk(relaxed = true)
+ }
+
+ // Create a mocked ReactApplicationContext
+ context = mockk(relaxed = true)
+ every { context.baseContext } returns application
+
+ // Mock getSharedPreferences to return regular prefs (avoiding encryption/KeyStore)
+ every { context.getSharedPreferences(any(), any()) } answers {
+ val name = firstArg()
+ val mode = secondArg()
+ application.getSharedPreferences(name, mode).also {
+ it.edit().clear().commit()
+ }
+ }
+
+ // Create module
+ module = TokenStorageModule(context)
+ }
+
+ // MARK: - Token Operations Tests
+
+ @Test
+ fun testSaveToken_shouldResolvePromise() {
+ val promise = mockk(relaxed = true)
+
+ module.saveToken("test-id", "test-token", promise)
+
+ verify { promise.resolve(null) }
+ }
+
+ @Test
+ fun testSaveAndGetToken_shouldReturnSavedToken() {
+ val promise = mockk(relaxed = true)
+ val getPromise = mockk(relaxed = true)
+ val resultSlot = slot()
+
+ // Save token
+ module.saveToken("test-id", "test-token", promise)
+ verify { promise.resolve(null) }
+
+ // Get token
+ module.getToken("test-id", getPromise)
+ verify { getPromise.resolve(capture(resultSlot)) }
+ assertThat(resultSlot.captured).isEqualTo("test-token")
+ }
+
+ @Test
+ fun testGetToken_nonExistent_shouldResolveNull() {
+ val promise = mockk(relaxed = true)
+
+ module.getToken("non-existent", promise)
+
+ verify { promise.resolve(null) }
+ }
+
+ @Test
+ fun testRemoveToken_shouldRemoveTokenAndMetadata() {
+ val savePromise = mockk(relaxed = true)
+ val saveMetaPromise = mockk(relaxed = true)
+ val removePromise = mockk(relaxed = true)
+ val getPromise = mockk(relaxed = true)
+
+ // Save token and metadata
+ module.saveToken("test-id", "test-token", savePromise)
+ module.saveMetadata("test-id", "test-metadata", saveMetaPromise)
+
+ // Remove token
+ module.removeToken("test-id", removePromise)
+ verify { removePromise.resolve(null) }
+
+ // Verify token is removed
+ module.getToken("test-id", getPromise)
+ verify { getPromise.resolve(null) }
+ }
+
+ @Test
+ fun testGetAllTokenIds_emptyStorage_shouldReturnEmptyArray() {
+ val promise = mockk(relaxed = true)
+ val resultSlot = slot()
+
+ module.getAllTokenIds(promise)
+
+ verify { promise.resolve(capture(resultSlot)) }
+ val result = resultSlot.captured
+ assertThat(result).isNotNull()
+ }
+
+ @Test
+ fun testGetAllTokenIds_withTokens_shouldReturnIds() {
+ val savePromise1 = mockk(relaxed = true)
+ val savePromise2 = mockk(relaxed = true)
+ val getPromise = mockk(relaxed = true)
+ val resultSlot = slot()
+
+ // Save multiple tokens
+ module.saveToken("token-1", "data-1", savePromise1)
+ module.saveToken("token-2", "data-2", savePromise2)
+
+ // Get all IDs
+ module.getAllTokenIds(getPromise)
+
+ verify { getPromise.resolve(capture(resultSlot)) }
+ // Verify result is not null (actual array assertion depends on React Native array types)
+ assertThat(resultSlot.captured).isNotNull()
+ }
+
+ @Test
+ fun testClearTokens_shouldRemoveAllTokensAndMetadata() {
+ val savePromise1 = mockk(relaxed = true)
+ val savePromise2 = mockk(relaxed = true)
+ val clearPromise = mockk(relaxed = true)
+ val getPromise = mockk(relaxed = true)
+
+ // Save tokens
+ module.saveToken("token-1", "data-1", savePromise1)
+ module.saveToken("token-2", "data-2", savePromise2)
+
+ // Clear all
+ module.clearTokens(clearPromise)
+ verify { clearPromise.resolve(null) }
+
+ // Verify tokens are cleared
+ module.getToken("token-1", getPromise)
+ verify { getPromise.resolve(null) }
+ }
+
+ // MARK: - Metadata Operations Tests
+
+ @Test
+ fun testSaveMetadata_shouldResolvePromise() {
+ val promise = mockk(relaxed = true)
+
+ module.saveMetadata("test-id", "test-metadata", promise)
+
+ verify { promise.resolve(null) }
+ }
+
+ @Test
+ fun testSaveAndGetMetadata_shouldReturnSavedMetadata() {
+ val promise = mockk(relaxed = true)
+ val getPromise = mockk(relaxed = true)
+ val resultSlot = slot()
+
+ // Save metadata
+ module.saveMetadata("test-id", "test-metadata", promise)
+ verify { promise.resolve(null) }
+
+ // Get metadata
+ module.getMetadata("test-id", getPromise)
+ verify { getPromise.resolve(capture(resultSlot)) }
+ assertThat(resultSlot.captured).isEqualTo("test-metadata")
+ }
+
+ @Test
+ fun testGetMetadata_nonExistent_shouldResolveNull() {
+ val promise = mockk(relaxed = true)
+
+ module.getMetadata("non-existent", promise)
+
+ verify { promise.resolve(null) }
+ }
+
+ @Test
+ fun testRemoveMetadata_shouldRemoveMetadata() {
+ val savePromise = mockk(relaxed = true)
+ val removePromise = mockk(relaxed = true)
+ val getPromise = mockk(relaxed = true)
+
+ // Save metadata
+ module.saveMetadata("test-id", "test-metadata", savePromise)
+
+ // Remove metadata
+ module.removeMetadata("test-id", removePromise)
+ verify { removePromise.resolve(null) }
+
+ // Verify metadata is removed
+ module.getMetadata("test-id", getPromise)
+ verify { getPromise.resolve(null) }
+ }
+
+ // MARK: - Default Token ID Tests
+
+ @Test
+ fun testSetDefaultTokenId_shouldResolvePromise() {
+ val promise = mockk(relaxed = true)
+
+ module.setDefaultTokenId("default-id", promise)
+
+ verify { promise.resolve(null) }
+ }
+
+ @Test
+ fun testSetAndGetDefaultTokenId_shouldReturnSavedId() {
+ val setPromise = mockk(relaxed = true)
+ val getPromise = mockk(relaxed = true)
+ val resultSlot = slot()
+
+ // Set default
+ module.setDefaultTokenId("default-id", setPromise)
+ verify { setPromise.resolve(null) }
+
+ // Get default
+ module.getDefaultTokenId(getPromise)
+ verify { getPromise.resolve(capture(resultSlot)) }
+ assertThat(resultSlot.captured).isEqualTo("default-id")
+ }
+
+ @Test
+ fun testGetDefaultTokenId_notSet_shouldResolveNull() {
+ val promise = mockk(relaxed = true)
+
+ module.getDefaultTokenId(promise)
+
+ verify { promise.resolve(null) }
+ }
+
+ @Test
+ fun testSetDefaultTokenId_null_shouldRemoveDefault() {
+ val setPromise = mockk(relaxed = true)
+ val setNullPromise = mockk(relaxed = true)
+ val getPromise = mockk(relaxed = true)
+
+ // Set default
+ module.setDefaultTokenId("default-id", setPromise)
+
+ // Clear default
+ module.setDefaultTokenId(null, setNullPromise)
+ verify { setNullPromise.resolve(null) }
+
+ // Verify default is cleared
+ module.getDefaultTokenId(getPromise)
+ verify { getPromise.resolve(null) }
+ }
+
+ // MARK: - Data Integrity Tests
+
+ @Test
+ fun testTokenAndMetadataStoredSeparately() {
+ val saveTokenPromise = mockk(relaxed = true)
+ val saveMetaPromise = mockk(relaxed = true)
+ val getTokenPromise = mockk(relaxed = true)
+ val getMetaPromise = mockk(relaxed = true)
+ val tokenSlot = slot()
+ val metaSlot = slot()
+
+ // Save token and metadata with different values
+ module.saveToken("id", "token-value", saveTokenPromise)
+ module.saveMetadata("id", "metadata-value", saveMetaPromise)
+
+ // Retrieve and verify they are different
+ module.getToken("id", getTokenPromise)
+ module.getMetadata("id", getMetaPromise)
+
+ verify { getTokenPromise.resolve(capture(tokenSlot)) }
+ verify { getMetaPromise.resolve(capture(metaSlot)) }
+
+ assertThat(tokenSlot.captured).isEqualTo("token-value")
+ assertThat(metaSlot.captured).isEqualTo("metadata-value")
+ }
+
+ // MARK: - Error Handling Tests
+
+ @Test
+ fun testSaveToken_withError_shouldRejectPromise() {
+ val promise = mockk(relaxed = true)
+ val rejectSlot = slot()
+
+ // Try saving with empty ID (edge case that might fail)
+ module.saveToken("", "test-token", promise)
+
+ // Should either resolve or reject depending on implementation
+ // This test verifies the module handles the operation without crashing
+ assertThat(promise).isNotNull()
+ }
+
+ @Test
+ fun testRemoveToken_nonExistent_shouldStillResolve() {
+ val promise = mockk(relaxed = true)
+
+ module.removeToken("non-existent", promise)
+
+ verify { promise.resolve(null) }
+ }
+}
diff --git a/packages/react-native-platform/ios/.gitignore b/packages/react-native-platform/ios/.gitignore
new file mode 100644
index 0000000..c09e535
--- /dev/null
+++ b/packages/react-native-platform/ios/.gitignore
@@ -0,0 +1,5 @@
+.build/
+.swiftpm/
+*.xcodeproj/
+*.xcworkspace/
+.DS_Store
diff --git a/packages/react-native-platform/ios/Package.swift b/packages/react-native-platform/ios/Package.swift
new file mode 100644
index 0000000..0865371
--- /dev/null
+++ b/packages/react-native-platform/ios/Package.swift
@@ -0,0 +1,32 @@
+// swift-tools-version:5.9
+import PackageDescription
+
+let package = Package(
+ name: "RNPlatformBridges",
+ platforms: [
+ .iOS(.v13)
+ ],
+ products: [
+ .library(
+ name: "RNTokenStorageBridge",
+ targets: ["RNTokenStorageBridge"]
+ )
+ ],
+ dependencies: [],
+ targets: [
+ .target(
+ name: "RNTokenStorageBridge",
+ path: "Sources/RNTokenStorageBridge",
+ exclude: ["TokenStorageBridge.m", "TokenStorageBridge.h"],
+ publicHeadersPath: "."
+ ),
+ .testTarget(
+ name: "RNTokenStorageBridgeTests",
+ dependencies: ["RNTokenStorageBridge"],
+ path: "Tests/RNTokenStorageBridgeTests"
+ )
+ ]
+)
+
+
+
diff --git a/packages/react-native-platform/ios/Sources/RNBrowserSessionBridge/BrowserSessionBridge.h b/packages/react-native-platform/ios/Sources/RNBrowserSessionBridge/BrowserSessionBridge.h
new file mode 100644
index 0000000..51e55c8
--- /dev/null
+++ b/packages/react-native-platform/ios/Sources/RNBrowserSessionBridge/BrowserSessionBridge.h
@@ -0,0 +1,13 @@
+#import
+#import
+
+@interface RCT_EXTERN_MODULE(BrowserSessionBridge, NSObject)
+
+RCT_EXTERN_METHOD(launchBrowserSession:(NSString *)url
+ resolve:(RCTPromiseResolveBlock)resolve
+ reject:(RCTPromiseRejectBlock)reject)
+
+RCT_EXTERN_METHOD(closeBrowserSession:(RCTPromiseResolveBlock)resolve
+ reject:(RCTPromiseRejectBlock)reject)
+
+@end
diff --git a/packages/react-native-platform/ios/Sources/RNBrowserSessionBridge/BrowserSessionBridge.m b/packages/react-native-platform/ios/Sources/RNBrowserSessionBridge/BrowserSessionBridge.m
new file mode 100644
index 0000000..405f158
--- /dev/null
+++ b/packages/react-native-platform/ios/Sources/RNBrowserSessionBridge/BrowserSessionBridge.m
@@ -0,0 +1,5 @@
+#import "BrowserSessionBridge.h"
+
+@implementation BrowserSessionBridge
+
+@end
diff --git a/packages/react-native-platform/ios/Sources/RNBrowserSessionBridge/BrowserSessionBridge.swift b/packages/react-native-platform/ios/Sources/RNBrowserSessionBridge/BrowserSessionBridge.swift
new file mode 100644
index 0000000..e2726bc
--- /dev/null
+++ b/packages/react-native-platform/ios/Sources/RNBrowserSessionBridge/BrowserSessionBridge.swift
@@ -0,0 +1,95 @@
+import Foundation
+
+#if os(iOS)
+import WebKit
+import UIKit
+#endif
+
+// Type aliases for Promise-like callbacks (compatible with React Native)
+typealias PromiseResolveBlock = (Any?) -> Void
+typealias PromiseRejectBlock = (String, String, Error?) -> Void
+
+#if os(iOS)
+@objc(BrowserSessionBridge)
+class BrowserSessionBridge: NSObject {
+
+ override init() {
+ super.init()
+ print("✅ BrowserSessionBridge initialized!")
+ }
+
+ @objc
+ static func requiresMainQueueSetup() -> Bool {
+ return true // WebView operations must happen on main thread
+ }
+
+ @objc
+ static func moduleName() -> String! {
+ return "BrowserSessionBridge"
+ }
+
+ @objc
+ func constantsToExport() -> [String: Any]! {
+ return [
+ "isAvailable": true
+ ]
+ }
+
+ // MARK: - Browser Session Operations
+
+ @objc(launchBrowserSession:resolve:reject:)
+ func launchBrowserSession(_ url: String, resolve: @escaping PromiseResolveBlock, reject: @escaping PromiseRejectBlock) {
+ do {
+ guard let urlObj = URL(string: url) else {
+ reject("invalid_url", "Invalid URL provided", nil)
+ return
+ }
+
+ DispatchQueue.main.async {
+ let webViewController = UIViewController()
+ let webView = WKWebView(frame: webViewController.view.bounds)
+ webView.load(URLRequest(url: urlObj))
+
+ webViewController.view.addSubview(webView)
+
+ // Get the key window and present the view controller
+ if let keyWindow = UIApplication.shared.connectedScenes
+ .compactMap({ $0 as? UIWindowScene })
+ .first?.windows
+ .first(where: { $0.isKeyWindow }) {
+ keyWindow.rootViewController?.present(webViewController, animated: true) {
+ resolve(nil)
+ }
+ } else {
+ reject("no_window", "Could not find key window", nil)
+ }
+ }
+ }
+ }
+
+ @objc(closeBrowserSession:reject:)
+ func closeBrowserSession(_ resolve: @escaping PromiseResolveBlock, reject: @escaping PromiseRejectBlock) {
+ DispatchQueue.main.async {
+ if let presentedViewController = UIApplication.shared.connectedScenes
+ .compactMap({ $0 as? UIWindowScene })
+ .first?.windows
+ .first(where: { $0.isKeyWindow })?.rootViewController?.presentedViewController {
+ presentedViewController.dismiss(animated: true) {
+ resolve(nil)
+ }
+ } else {
+ resolve(nil) // Already closed
+ }
+ }
+ }
+}
+#else
+// Stub for non-iOS platforms (for SPM testing on macOS)
+@objc(BrowserSessionBridge)
+class BrowserSessionBridge: NSObject {
+ @objc static func requiresMainQueueSetup() -> Bool { return false }
+ @objc static func moduleName() -> String! { return "BrowserSessionBridge" }
+ @objc func constantsToExport() -> [String: Any]! { return ["isAvailable": false] }
+}
+#endif
+
diff --git a/packages/react-native-platform/ios/Sources/RNTokenStorageBridge/TokenStorageBridge.h b/packages/react-native-platform/ios/Sources/RNTokenStorageBridge/TokenStorageBridge.h
new file mode 100644
index 0000000..fc6d364
--- /dev/null
+++ b/packages/react-native-platform/ios/Sources/RNTokenStorageBridge/TokenStorageBridge.h
@@ -0,0 +1,11 @@
+#import
+
+#ifdef RCT_NEW_ARCH_ENABLED
+#import "RNTokenStorageBridge.h"
+
+@interface TokenStorageBridge : NSObject
+#else
+@interface TokenStorageBridge : NSObject
+#endif
+
+@end
diff --git a/packages/react-native-platform/ios/Sources/RNTokenStorageBridge/TokenStorageBridge.m b/packages/react-native-platform/ios/Sources/RNTokenStorageBridge/TokenStorageBridge.m
new file mode 100644
index 0000000..617ea29
--- /dev/null
+++ b/packages/react-native-platform/ios/Sources/RNTokenStorageBridge/TokenStorageBridge.m
@@ -0,0 +1,44 @@
+#import
+
+@interface RCT_EXTERN_MODULE(TokenStorageBridge, NSObject)
+
+RCT_EXTERN_METHOD(saveToken:(NSString *)id
+ tokenData:(NSString *)tokenData
+ resolve:(RCTPromiseResolveBlock)resolve
+ reject:(RCTPromiseRejectBlock)reject)
+
+RCT_EXTERN_METHOD(getToken:(NSString *)id
+ resolve:(RCTPromiseResolveBlock)resolve
+ reject:(RCTPromiseRejectBlock)reject)
+
+RCT_EXTERN_METHOD(removeToken:(NSString *)id
+ resolve:(RCTPromiseResolveBlock)resolve
+ reject:(RCTPromiseRejectBlock)reject)
+
+RCT_EXTERN_METHOD(getAllTokenIds:(RCTPromiseResolveBlock)resolve
+ reject:(RCTPromiseRejectBlock)reject)
+
+RCT_EXTERN_METHOD(clearTokens:(RCTPromiseResolveBlock)resolve
+ reject:(RCTPromiseRejectBlock)reject)
+
+RCT_EXTERN_METHOD(saveMetadata:(NSString *)id
+ metadataData:(NSString *)metadataData
+ resolve:(RCTPromiseResolveBlock)resolve
+ reject:(RCTPromiseRejectBlock)reject)
+
+RCT_EXTERN_METHOD(getMetadata:(NSString *)id
+ resolve:(RCTPromiseResolveBlock)resolve
+ reject:(RCTPromiseRejectBlock)reject)
+
+RCT_EXTERN_METHOD(removeMetadata:(NSString *)id
+ resolve:(RCTPromiseResolveBlock)resolve
+ reject:(RCTPromiseRejectBlock)reject)
+
+RCT_EXTERN_METHOD(setDefaultTokenId:(nullable NSString *)id
+ resolve:(RCTPromiseResolveBlock)resolve
+ reject:(RCTPromiseRejectBlock)reject)
+
+RCT_EXTERN_METHOD(getDefaultTokenId:(RCTPromiseResolveBlock)resolve
+ reject:(RCTPromiseRejectBlock)reject)
+
+@end
\ No newline at end of file
diff --git a/packages/react-native-platform/ios/Sources/RNTokenStorageBridge/TokenStorageBridge.swift b/packages/react-native-platform/ios/Sources/RNTokenStorageBridge/TokenStorageBridge.swift
new file mode 100644
index 0000000..46107f4
--- /dev/null
+++ b/packages/react-native-platform/ios/Sources/RNTokenStorageBridge/TokenStorageBridge.swift
@@ -0,0 +1,302 @@
+import Foundation
+import Security
+
+// Type aliases for Promise-like callbacks (compatible with React Native)
+typealias PromiseResolveBlock = (Any?) -> Void
+typealias PromiseRejectBlock = (String, String, Error?) -> Void
+
+@objc(TokenStorageBridge)
+class TokenStorageBridge: NSObject {
+
+ override init() {
+ super.init()
+ print("✅ TokenStorageBridge initialized!")
+ // Debug: Print all methods
+ let methodCount = UnsafeMutablePointer.allocate(capacity: 1)
+ let methods = class_copyMethodList(type(of: self), methodCount)
+ print("📋 TokenStorageBridge methods:")
+ for i in 0.. Bool {
+ return false
+ }
+
+ @objc
+ static func moduleName() -> String! {
+ return "TokenStorageBridge"
+ }
+
+ @objc
+ func constantsToExport() -> [String: Any]! {
+ return [
+ "isAvailable": true
+ ]
+ }
+
+ // MARK: - Token Operations (Secure Storage - Keychain with strict access)
+
+ @objc(saveToken:tokenData:resolve:reject:)
+ func saveToken(_ id: String, tokenData: String, resolve: @escaping PromiseResolveBlock, reject: @escaping PromiseRejectBlock) {
+ do {
+ // More restrictive: requires device unlock
+ try KeychainHelper.save(
+ service: Self.SERVICE_TOKENS,
+ key: id,
+ value: tokenData,
+ accessibility: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
+ )
+ resolve(nil)
+ } catch {
+ reject("token_save_error", "Failed to save token", error)
+ }
+ }
+
+ @objc(getToken:resolve:reject:)
+ func getToken(_ id: String, resolve: @escaping PromiseResolveBlock, reject: @escaping PromiseRejectBlock) {
+ do {
+ let value = try KeychainHelper.load(service: Self.SERVICE_TOKENS, key: id)
+ resolve(value)
+ } catch {
+ resolve(nil)
+ }
+ }
+
+ @objc(removeToken:resolve:reject:)
+ func removeToken(_ id: String, resolve: @escaping PromiseResolveBlock, reject: @escaping PromiseRejectBlock) {
+ do {
+ try KeychainHelper.delete(service: Self.SERVICE_TOKENS, key: id)
+ try KeychainHelper.delete(service: Self.SERVICE_METADATA, key: id)
+ resolve(nil)
+ } catch {
+ reject("token_remove_error", "Failed to remove token", error)
+ }
+ }
+
+ @objc(getAllTokenIds:reject:)
+ func getAllTokenIds(_ resolve: @escaping PromiseResolveBlock, reject: @escaping PromiseRejectBlock) {
+ do {
+ let keys = try KeychainHelper.allKeys(service: Self.SERVICE_TOKENS)
+ resolve(keys)
+ } catch {
+ reject("token_list_error", "Failed to get token IDs", error)
+ }
+ }
+
+ @objc(clearTokens:reject:)
+ func clearTokens(_ resolve: @escaping PromiseResolveBlock, reject: @escaping PromiseRejectBlock) {
+ do {
+ try KeychainHelper.clearAll(service: Self.SERVICE_TOKENS)
+ try KeychainHelper.clearAll(service: Self.SERVICE_METADATA)
+ try KeychainHelper.clearAll(service: Self.SERVICE_DEFAULT)
+ resolve(nil)
+ } catch {
+ reject("token_clear_error", "Failed to clear tokens", error)
+ }
+ }
+
+ // MARK: - Metadata Operations (Keychain with relaxed access)
+
+ @objc(saveMetadata:metadataData:resolve:reject:)
+ func saveMetadata(_ id: String, metadataData: String, resolve: @escaping PromiseResolveBlock, reject: @escaping PromiseRejectBlock) {
+ do {
+ // Less restrictive: accessible after first unlock (survives reboots)
+ try KeychainHelper.save(
+ service: Self.SERVICE_METADATA,
+ key: id,
+ value: metadataData,
+ accessibility: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
+ )
+ resolve(nil)
+ } catch {
+ reject("metadata_save_error", "Failed to save metadata", error)
+ }
+ }
+
+ @objc(getMetadata:resolve:reject:)
+ func getMetadata(_ id: String, resolve: @escaping PromiseResolveBlock, reject: @escaping PromiseRejectBlock) {
+ do {
+ let value = try KeychainHelper.load(service: Self.SERVICE_METADATA, key: id)
+ resolve(value)
+ } catch {
+ resolve(nil)
+ }
+ }
+
+ @objc(removeMetadata:resolve:reject:)
+ func removeMetadata(_ id: String, resolve: @escaping PromiseResolveBlock, reject: @escaping PromiseRejectBlock) {
+ do {
+ try KeychainHelper.delete(service: Self.SERVICE_METADATA, key: id)
+ resolve(nil)
+ } catch {
+ reject("metadata_remove_error", "Failed to remove metadata", error)
+ }
+ }
+
+ // MARK: - Default Token ID (Keychain with relaxed access)
+
+ @objc(setDefaultTokenId:resolve:reject:)
+ func setDefaultTokenId(_ id: String?, resolve: @escaping PromiseResolveBlock, reject: @escaping PromiseRejectBlock) {
+ do {
+ if let id = id {
+ try KeychainHelper.save(
+ service: Self.SERVICE_DEFAULT,
+ key: "default",
+ value: id,
+ accessibility: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
+ )
+ } else {
+ try KeychainHelper.delete(service: Self.SERVICE_DEFAULT, key: "default")
+ }
+ resolve(nil)
+ } catch {
+ reject("default_token_error", "Failed to set default token ID", error)
+ }
+ }
+
+ @objc(getDefaultTokenId:reject:)
+ func getDefaultTokenId(_ resolve: @escaping PromiseResolveBlock, reject: @escaping PromiseRejectBlock) {
+ do {
+ let value = try KeychainHelper.load(service: Self.SERVICE_DEFAULT, key: "default")
+ resolve(value)
+ } catch {
+ resolve(nil)
+ }
+ }
+}
+
+// MARK: - Keychain Helper
+
+class KeychainHelper {
+
+ static func save(
+ service: String,
+ key: String,
+ value: String,
+ accessibility: CFString
+ ) throws {
+ let data = value.data(using: .utf8)!
+
+ // Query for adding new item
+ let addQuery: [String: Any] = [
+ kSecClass as String: kSecClassGenericPassword,
+ kSecAttrService as String: service,
+ kSecAttrAccount as String: key,
+ kSecValueData as String: data,
+ kSecAttrAccessible as String: accessibility
+ ]
+
+ // First try to delete any existing item (use minimal query)
+ let deleteQuery: [String: Any] = [
+ kSecClass as String: kSecClassGenericPassword,
+ kSecAttrService as String: service,
+ kSecAttrAccount as String: key
+ ]
+ SecItemDelete(deleteQuery as CFDictionary)
+
+ // Now add the new item
+ let status = SecItemAdd(addQuery as CFDictionary, nil)
+ guard status == errSecSuccess else {
+ throw NSError(domain: NSOSStatusErrorDomain, code: Int(status))
+ }
+ }
+
+ static func load(service: String, key: String) throws -> String? {
+ let query: [String: Any] = [
+ kSecClass as String: kSecClassGenericPassword,
+ kSecAttrService as String: service,
+ kSecAttrAccount as String: key,
+ kSecReturnData as String: true
+ ]
+
+ var result: AnyObject?
+ let status = SecItemCopyMatching(query as CFDictionary, &result)
+
+ guard status == errSecSuccess else {
+ if status == errSecItemNotFound {
+ return nil
+ }
+ throw NSError(domain: NSOSStatusErrorDomain, code: Int(status))
+ }
+
+ guard let data = result as? Data,
+ let value = String(data: data, encoding: .utf8) else {
+ return nil
+ }
+
+ return value
+ }
+
+ static func delete(service: String, key: String) throws {
+ let query: [String: Any] = [
+ kSecClass as String: kSecClassGenericPassword,
+ kSecAttrService as String: service,
+ kSecAttrAccount as String: key
+ ]
+
+ // Loop to delete all items matching the query (defensive against duplicates)
+ while true {
+ let status = SecItemDelete(query as CFDictionary)
+ if status == errSecItemNotFound {
+ break
+ } else if status != errSecSuccess {
+ throw NSError(domain: NSOSStatusErrorDomain, code: Int(status))
+ }
+ // If successful, loop again to ensure all items are deleted
+ }
+ }
+
+ static func allKeys(service: String) throws -> [String] {
+ let query: [String: Any] = [
+ kSecClass as String: kSecClassGenericPassword,
+ kSecAttrService as String: service,
+ kSecReturnAttributes as String: true,
+ kSecMatchLimit as String: kSecMatchLimitAll
+ ]
+
+ var result: AnyObject?
+ let status = SecItemCopyMatching(query as CFDictionary, &result)
+
+ guard status == errSecSuccess else {
+ if status == errSecItemNotFound {
+ return []
+ }
+ throw NSError(domain: NSOSStatusErrorDomain, code: Int(status))
+ }
+
+ guard let items = result as? [[String: Any]] else {
+ return []
+ }
+
+ return items.compactMap { $0[kSecAttrAccount as String] as? String }
+ }
+
+ static func clearAll(service: String) throws {
+ let query: [String: Any] = [
+ kSecClass as String: kSecClassGenericPassword,
+ kSecAttrService as String: service
+ ]
+
+ // SecItemDelete only deletes one item per call, so loop until empty
+ while true {
+ let status = SecItemDelete(query as CFDictionary)
+ if status == errSecItemNotFound {
+ // No more items to delete
+ break
+ } else if status != errSecSuccess {
+ throw NSError(domain: NSOSStatusErrorDomain, code: Int(status))
+ }
+ // If successful, loop again to delete next item
+ }
+ }
+}
diff --git a/packages/react-native-platform/ios/Tests/RNBrowserSessionBridgeTests/BrowserSessionBridgeTests.swift b/packages/react-native-platform/ios/Tests/RNBrowserSessionBridgeTests/BrowserSessionBridgeTests.swift
new file mode 100644
index 0000000..34af341
--- /dev/null
+++ b/packages/react-native-platform/ios/Tests/RNBrowserSessionBridgeTests/BrowserSessionBridgeTests.swift
@@ -0,0 +1,110 @@
+import XCTest
+@testable import RNBrowserSessionBridge
+
+class BrowserSessionBridgeTests: XCTestCase {
+
+ var sut: BrowserSessionBridge!
+
+ override func setUp() {
+ super.setUp()
+ sut = BrowserSessionBridge()
+ }
+
+ override func tearDown() {
+ super.tearDown()
+ sut = nil
+ }
+
+ // MARK: - Initialization Tests
+
+ func testInit_shouldSucceed() {
+ XCTAssertNotNil(sut)
+ }
+
+ func testRequiresMainQueueSetup_shouldReturnValue() {
+ XCTAssertFalse(BrowserSessionBridge.requiresMainQueueSetup())
+ }
+
+ func testModuleName_shouldReturnBrowserSessionBridge() {
+ XCTAssertEqual(BrowserSessionBridge.moduleName(), "BrowserSessionBridge")
+ }
+
+ func testConstantsToExport_shouldBeNotNil() {
+ let constants = sut.constantsToExport()
+ XCTAssertNotNil(constants)
+ }
+
+ #if os(iOS)
+ // MARK: - Launch Browser Session Tests (iOS only)
+
+ func testLaunchBrowserSession_withValidUrl_shouldResolve() {
+ let expectation = XCTestExpectation(description: "Launch browser session")
+
+ sut.launchBrowserSession("https://example.com") { result in
+ XCTAssertNil(result)
+ expectation.fulfill()
+ } reject: { code, message, error in
+ XCTFail("Should not reject: \(message ?? "unknown error")")
+ expectation.fulfill()
+ }
+
+ wait(for: [expectation], timeout: 2.0)
+ }
+
+ func testLaunchBrowserSession_withInvalidUrl_shouldReject() {
+ let expectation = XCTestExpectation(description: "Launch with invalid URL")
+
+ sut.launchBrowserSession("not a url") { result in
+ XCTFail("Should not resolve")
+ expectation.fulfill()
+ } reject: { code, message, error in
+ XCTAssertEqual(code, "invalid_url")
+ expectation.fulfill()
+ }
+
+ wait(for: [expectation], timeout: 2.0)
+ }
+
+ // MARK: - Close Browser Session Tests (iOS only)
+
+ func testCloseBrowserSession_shouldResolve() {
+ let expectation = XCTestExpectation(description: "Close browser session")
+
+ sut.closeBrowserSession { result in
+ XCTAssertNil(result)
+ expectation.fulfill()
+ } reject: { code, message, error in
+ XCTFail("Should resolve")
+ expectation.fulfill()
+ }
+
+ wait(for: [expectation], timeout: 2.0)
+ }
+
+ // MARK: - Integration Tests (iOS only)
+
+ func testLaunchAndCloseBrowserSession() {
+ let launchExpectation = XCTestExpectation(description: "Launch")
+ let closeExpectation = XCTestExpectation(description: "Close")
+
+ sut.launchBrowserSession("https://example.com") { _ in
+ launchExpectation.fulfill()
+ } reject: { _, _, _ in
+ XCTFail("Launch should not reject")
+ launchExpectation.fulfill()
+ }
+
+ wait(for: [launchExpectation], timeout: 2.0)
+
+ sut.closeBrowserSession { _ in
+ closeExpectation.fulfill()
+ } reject: { _, _, _ in
+ XCTFail("Close should not reject")
+ closeExpectation.fulfill()
+ }
+
+ wait(for: [closeExpectation], timeout: 2.0)
+ }
+ #endif
+}
+
diff --git a/packages/react-native-platform/ios/Tests/RNTokenStorageBridgeTests/KeychainHelperTests.swift b/packages/react-native-platform/ios/Tests/RNTokenStorageBridgeTests/KeychainHelperTests.swift
new file mode 100644
index 0000000..c75c62b
--- /dev/null
+++ b/packages/react-native-platform/ios/Tests/RNTokenStorageBridgeTests/KeychainHelperTests.swift
@@ -0,0 +1,327 @@
+import XCTest
+import Security
+@testable import RNTokenStorageBridge
+
+class KeychainHelperTests: XCTestCase {
+
+ let testService = "com.okta.test.service"
+ let testKey = "test-key"
+ let testValue = "test-value"
+
+ override func setUp() {
+ super.setUp()
+ cleanupTestKeychain()
+ }
+
+ override func tearDown() {
+ super.tearDown()
+ cleanupTestKeychain()
+ }
+
+ // MARK: - Save Tests
+
+ func testSave_shouldStoreValueInKeychain() {
+ let accessibility = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
+
+ XCTAssertNoThrow({
+ try KeychainHelper.save(
+ service: self.testService,
+ key: self.testKey,
+ value: self.testValue,
+ accessibility: accessibility
+ )
+ })
+ }
+
+ func testSave_withDifferentAccessibility_shouldSucceed() {
+ let restrictions = [
+ kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
+ kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
+ ]
+
+ for accessibility in restrictions {
+ let uniqueKey = "\(testKey)-\(UUID().uuidString)"
+ XCTAssertNoThrow({
+ try KeychainHelper.save(
+ service: self.testService,
+ key: uniqueKey,
+ value: self.testValue,
+ accessibility: accessibility
+ )
+ })
+ }
+ }
+
+ // MARK: - Load Tests
+
+ func testLoad_savedValue_shouldReturnValue() {
+ let accessibility = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
+
+ try? KeychainHelper.save(
+ service: testService,
+ key: testKey,
+ value: testValue,
+ accessibility: accessibility
+ )
+
+ let result = try? KeychainHelper.load(service: testService, key: testKey)
+
+ XCTAssertEqual(result, testValue)
+ }
+
+ func testLoad_nonExistent_shouldReturnNil() {
+ let result = try? KeychainHelper.load(service: testService, key: "non-existent")
+
+ XCTAssertNil(result)
+ }
+
+ func testLoad_afterSaveAndDelete_shouldReturnNil() {
+ let accessibility = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
+
+ try? KeychainHelper.save(
+ service: testService,
+ key: testKey,
+ value: testValue,
+ accessibility: accessibility
+ )
+
+ try? KeychainHelper.delete(service: testService, key: testKey)
+
+ let result = try? KeychainHelper.load(service: testService, key: testKey)
+
+ XCTAssertNil(result)
+ }
+
+ // MARK: - Delete Tests
+
+ func testDelete_savedValue_shouldRemoveValue() {
+ let accessibility = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
+ let uniqueKey = "isolated-delete-test-\(UUID().uuidString)"
+ let testData = "isolated-delete-data"
+
+ // Step 1: Save
+ do {
+ try KeychainHelper.save(
+ service: self.testService,
+ key: uniqueKey,
+ value: testData,
+ accessibility: accessibility
+ )
+ } catch {
+ XCTFail("Save should not throw: \(error)")
+ return
+ }
+
+ // Step 2: Verify save worked
+ do {
+ let savedValue = try KeychainHelper.load(service: self.testService, key: uniqueKey)
+ XCTAssertEqual(savedValue, testData)
+ } catch {
+ XCTFail("Load after save should not throw: \(error)")
+ return
+ }
+
+ // Step 3: Delete
+ do {
+ try KeychainHelper.delete(service: self.testService, key: uniqueKey)
+ } catch {
+ XCTFail("Delete should not throw: \(error)")
+ return
+ }
+
+ // Step 4: Verify delete worked
+ do {
+ let deletedValue = try KeychainHelper.load(service: self.testService, key: uniqueKey)
+ XCTAssertNil(deletedValue)
+ } catch {
+ XCTFail("Load after delete should not throw: \(error)")
+ return
+ }
+ }
+
+ func testDelete_nonExistent_shouldNotThrow() {
+ XCTAssertNoThrow({
+ try KeychainHelper.delete(service: self.testService, key: "non-existent")
+ })
+ }
+
+ func testDelete_multipleItems_shouldDeleteOnlyTarget() {
+ let accessibility = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
+ let key1 = "key-1"
+ let key2 = "key-2"
+ let value1 = "value-1"
+ let value2 = "value-2"
+
+ try? KeychainHelper.save(service: testService, key: key1, value: value1, accessibility: accessibility)
+ try? KeychainHelper.save(service: testService, key: key2, value: value2, accessibility: accessibility)
+
+ try? KeychainHelper.delete(service: testService, key: key1)
+
+ let result1 = try? KeychainHelper.load(service: testService, key: key1)
+ let result2 = try? KeychainHelper.load(service: testService, key: key2)
+
+ XCTAssertNil(result1)
+ XCTAssertEqual(result2, value2)
+ }
+
+ // MARK: - AllKeys Tests
+
+ func testAllKeys_emptyService_shouldReturnEmptyArray() {
+ let result = try? KeychainHelper.allKeys(service: testService)
+
+ XCTAssertEqual(result, [])
+ }
+
+ func testAllKeys_withMultipleItems_shouldReturnAllKeys() {
+ let accessibility = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
+ let keys = ["key-1", "key-2", "key-3"]
+
+ for key in keys {
+ try? KeychainHelper.save(
+ service: testService,
+ key: key,
+ value: "value-\(key)",
+ accessibility: accessibility
+ )
+ }
+
+ let result = try? KeychainHelper.allKeys(service: testService)
+
+ XCTAssertEqual(Set(result ?? []), Set(keys))
+ }
+
+ func testAllKeys_afterDelete_shouldNotIncludeDeletedKey() {
+ let accessibility = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
+ let keys = ["key-1", "key-2"]
+
+ for key in keys {
+ try? KeychainHelper.save(
+ service: testService,
+ key: key,
+ value: "value",
+ accessibility: accessibility
+ )
+ }
+
+ try? KeychainHelper.delete(service: testService, key: "key-1")
+
+ let result = try? KeychainHelper.allKeys(service: testService)
+
+ XCTAssertEqual(result, ["key-2"])
+ }
+
+ // MARK: - ClearAll Tests
+
+ func testClearAll_emptyService_shouldNotThrow() {
+ XCTAssertNoThrow({
+ try KeychainHelper.clearAll(service: self.testService)
+ })
+ }
+
+ func testClearAll_withItems_shouldRemoveAllItems() {
+ let accessibility = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
+ let keys = ["key-1", "key-2", "key-3"]
+
+ for key in keys {
+ try? KeychainHelper.save(
+ service: testService,
+ key: key,
+ value: "value",
+ accessibility: accessibility
+ )
+ }
+
+ try? KeychainHelper.clearAll(service: testService)
+
+ let result = try? KeychainHelper.allKeys(service: testService)
+
+ XCTAssertEqual(result, [])
+ }
+
+ // MARK: - Data Integrity Tests
+
+ func testSaveAndLoad_preservesData() {
+ let accessibility = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
+ let testData = "Complex data with special characters: !@#$%^&*()"
+
+ try? KeychainHelper.save(
+ service: testService,
+ key: testKey,
+ value: testData,
+ accessibility: accessibility
+ )
+
+ let result = try? KeychainHelper.load(service: testService, key: testKey)
+
+ XCTAssertEqual(result, testData)
+ }
+
+ func testSaveAndLoad_largeData() {
+ let accessibility = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
+ let largeData = String(repeating: "x", count: 10000)
+
+ try? KeychainHelper.save(
+ service: testService,
+ key: testKey,
+ value: largeData,
+ accessibility: accessibility
+ )
+
+ let result = try? KeychainHelper.load(service: testService, key: testKey)
+
+ XCTAssertEqual(result, largeData)
+ }
+
+ func testSaveAndLoad_emptyString() {
+ let accessibility = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
+
+ try? KeychainHelper.save(
+ service: testService,
+ key: testKey,
+ value: "",
+ accessibility: accessibility
+ )
+
+ let result = try? KeychainHelper.load(service: testService, key: testKey)
+
+ XCTAssertEqual(result, "")
+ }
+
+ // MARK: - Service Isolation Tests
+
+ func testDifferentServices_shouldBeIsolated() {
+ let service1 = "com.okta.service1"
+ let service2 = "com.okta.service2"
+ let accessibility = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
+
+ try? KeychainHelper.save(
+ service: service1,
+ key: testKey,
+ value: "value1",
+ accessibility: accessibility
+ )
+
+ try? KeychainHelper.save(
+ service: service2,
+ key: testKey,
+ value: "value2",
+ accessibility: accessibility
+ )
+
+ let result1 = try? KeychainHelper.load(service: service1, key: testKey)
+ let result2 = try? KeychainHelper.load(service: service2, key: testKey)
+
+ XCTAssertEqual(result1, "value1")
+ XCTAssertEqual(result2, "value2")
+
+ // Cleanup
+ try? KeychainHelper.clearAll(service: service1)
+ try? KeychainHelper.clearAll(service: service2)
+ }
+
+ // MARK: - Helper Methods
+
+ private func cleanupTestKeychain() {
+ try? KeychainHelper.clearAll(service: testService)
+ }
+}
diff --git a/packages/react-native-platform/ios/Tests/RNTokenStorageBridgeTests/TokenStorageBridgeTests.swift b/packages/react-native-platform/ios/Tests/RNTokenStorageBridgeTests/TokenStorageBridgeTests.swift
new file mode 100644
index 0000000..23f3b09
--- /dev/null
+++ b/packages/react-native-platform/ios/Tests/RNTokenStorageBridgeTests/TokenStorageBridgeTests.swift
@@ -0,0 +1,377 @@
+import XCTest
+import Security
+@testable import RNTokenStorageBridge
+
+class TokenStorageBridgeTests: XCTestCase {
+
+ var sut: TokenStorageBridge!
+
+ override func setUp() {
+ super.setUp()
+ sut = TokenStorageBridge()
+ cleanupKeychain()
+ }
+
+ override func tearDown() {
+ super.tearDown()
+ cleanupKeychain()
+ }
+
+ // MARK: - Token Operations Tests
+
+ func testSaveToken_shouldSucceed() {
+ let expectation = XCTestExpectation(description: "Save token")
+
+ sut.saveToken("test-id", tokenData: "test-token-data", resolve: { _ in
+ expectation.fulfill()
+ }, reject: { _, _, _ in
+ XCTFail("Should not reject")
+ expectation.fulfill()
+ })
+
+ wait(for: [expectation], timeout: 1.0)
+ }
+
+ func testSaveAndGetToken_shouldReturnSavedToken() {
+ let saveExpectation = XCTestExpectation(description: "Save token")
+ let getExpectation = XCTestExpectation(description: "Get token")
+ var savedTokenValue: String?
+
+ sut.saveToken("test-id", tokenData: "test-token-data", resolve: { _ in
+ saveExpectation.fulfill()
+ }, reject: { _, _, _ in
+ XCTFail("Save should not reject")
+ saveExpectation.fulfill()
+ })
+
+ wait(for: [saveExpectation], timeout: 1.0)
+
+ sut.getToken("test-id", resolve: { token in
+ savedTokenValue = token as? String
+ getExpectation.fulfill()
+ }, reject: { _, _, _ in
+ XCTFail("Get should not reject")
+ getExpectation.fulfill()
+ })
+
+ wait(for: [getExpectation], timeout: 1.0)
+ XCTAssertEqual(savedTokenValue, "test-token-data")
+ }
+
+ func testGetToken_nonExistent_shouldReturnNil() {
+ let expectation = XCTestExpectation(description: "Get non-existent token")
+ var result: String?
+
+ sut.getToken("non-existent", resolve: { token in
+ result = token as? String
+ expectation.fulfill()
+ }, reject: { _, _, _ in
+ XCTFail("Should not reject")
+ expectation.fulfill()
+ })
+
+ wait(for: [expectation], timeout: 1.0)
+ XCTAssertNil(result)
+ }
+
+ func testRemoveToken_shouldRemoveTokenAndMetadata() {
+ let saveTokenExp = XCTestExpectation(description: "Save token")
+ let saveMetaExp = XCTestExpectation(description: "Save metadata")
+ let removeExp = XCTestExpectation(description: "Remove token")
+ let getTokenExp = XCTestExpectation(description: "Get removed token")
+ var retrievedToken: String?
+
+ sut.saveToken("test-id", tokenData: "test-data", resolve: { _ in
+ saveTokenExp.fulfill()
+ }, reject: { _, _, _ in
+ XCTFail("Save token should not reject")
+ saveTokenExp.fulfill()
+ })
+
+ sut.saveMetadata("test-id", metadataData: "test-meta", resolve: { _ in
+ saveMetaExp.fulfill()
+ }, reject: { _, _, _ in
+ XCTFail("Save metadata should not reject")
+ saveMetaExp.fulfill()
+ })
+
+ wait(for: [saveTokenExp, saveMetaExp], timeout: 1.0)
+
+ sut.removeToken("test-id", resolve: { _ in
+ removeExp.fulfill()
+ }, reject: { _, _, _ in
+ XCTFail("Remove should not reject")
+ removeExp.fulfill()
+ })
+
+ wait(for: [removeExp], timeout: 1.0)
+
+ sut.getToken("test-id", resolve: { token in
+ retrievedToken = token as? String
+ getTokenExp.fulfill()
+ }, reject: { _, _, _ in
+ XCTFail("Get should not reject")
+ getTokenExp.fulfill()
+ })
+
+ wait(for: [getTokenExp], timeout: 1.0)
+ XCTAssertNil(retrievedToken)
+ }
+
+ func testGetAllTokenIds_emptyStorage_shouldReturnEmptyArray() {
+ let expectation = XCTestExpectation(description: "Get all token IDs")
+ var result: [String]?
+
+ sut.getAllTokenIds({ ids in
+ result = ids as? [String]
+ expectation.fulfill()
+ }, reject: { _, _, _ in
+ XCTFail("Should not reject")
+ expectation.fulfill()
+ })
+
+ wait(for: [expectation], timeout: 1.0)
+ XCTAssertEqual(result, [])
+ }
+
+ func testGetAllTokenIds_withMultipleTokens_shouldReturnIds() {
+ let save1Exp = XCTestExpectation(description: "Save token 1")
+ let save2Exp = XCTestExpectation(description: "Save token 2")
+ let getAllExp = XCTestExpectation(description: "Get all IDs")
+ var result: [String]?
+
+ sut.saveToken("token-1", tokenData: "data-1", resolve: { _ in
+ save1Exp.fulfill()
+ }, reject: { _, _, _ in
+ XCTFail("Save 1 should not reject")
+ save1Exp.fulfill()
+ })
+
+ sut.saveToken("token-2", tokenData: "data-2", resolve: { _ in
+ save2Exp.fulfill()
+ }, reject: { _, _, _ in
+ XCTFail("Save 2 should not reject")
+ save2Exp.fulfill()
+ })
+
+ wait(for: [save1Exp, save2Exp], timeout: 1.0)
+
+ sut.getAllTokenIds({ ids in
+ result = ids as? [String]
+ getAllExp.fulfill()
+ }, reject: { _, _, _ in
+ XCTFail("Should not reject")
+ getAllExp.fulfill()
+ })
+
+ wait(for: [getAllExp], timeout: 1.0)
+ XCTAssertEqual(Set(result ?? []), Set(["token-1", "token-2"]))
+ }
+
+ func testClearTokens_shouldRemoveAllTokens() {
+ let save1Exp = XCTestExpectation(description: "Save token 1")
+ let save2Exp = XCTestExpectation(description: "Save token 2")
+ let clearExp = XCTestExpectation(description: "Clear tokens")
+ let getAllExp = XCTestExpectation(description: "Get all IDs after clear")
+ var result: [String]?
+
+ sut.saveToken("token-1", tokenData: "data-1", resolve: { _ in
+ save1Exp.fulfill()
+ }, reject: { _, _, _ in
+ save1Exp.fulfill()
+ })
+
+ sut.saveToken("token-2", tokenData: "data-2", resolve: { _ in
+ save2Exp.fulfill()
+ }, reject: { _, _, _ in
+ save2Exp.fulfill()
+ })
+
+ wait(for: [save1Exp, save2Exp], timeout: 1.0)
+
+ sut.clearTokens({ _ in
+ clearExp.fulfill()
+ }, reject: { _, _, _ in
+ XCTFail("Clear should not reject")
+ clearExp.fulfill()
+ })
+
+ wait(for: [clearExp], timeout: 1.0)
+
+ sut.getAllTokenIds({ ids in
+ result = ids as? [String]
+ getAllExp.fulfill()
+ }, reject: { _, _, _ in
+ getAllExp.fulfill()
+ })
+
+ wait(for: [getAllExp], timeout: 1.0)
+ XCTAssertEqual(result, [])
+ }
+
+ // MARK: - Metadata Operations Tests
+
+ func testSaveMetadata_shouldSucceed() {
+ let expectation = XCTestExpectation(description: "Save metadata")
+
+ sut.saveMetadata("test-id", metadataData: "test-metadata", resolve: { _ in
+ expectation.fulfill()
+ }, reject: { _, _, _ in
+ XCTFail("Should not reject")
+ expectation.fulfill()
+ })
+
+ wait(for: [expectation], timeout: 1.0)
+ }
+
+ func testSaveAndGetMetadata_shouldReturnSavedMetadata() {
+ let saveExpectation = XCTestExpectation(description: "Save metadata")
+ let getExpectation = XCTestExpectation(description: "Get metadata")
+ var savedMetadata: String?
+
+ sut.saveMetadata("test-id", metadataData: "test-metadata", resolve: { _ in
+ saveExpectation.fulfill()
+ }, reject: { _, _, _ in
+ XCTFail("Save should not reject")
+ saveExpectation.fulfill()
+ })
+
+ wait(for: [saveExpectation], timeout: 1.0)
+
+ sut.getMetadata("test-id", resolve: { metadata in
+ savedMetadata = metadata as? String
+ getExpectation.fulfill()
+ }, reject: { _, _, _ in
+ XCTFail("Get should not reject")
+ getExpectation.fulfill()
+ })
+
+ wait(for: [getExpectation], timeout: 1.0)
+ XCTAssertEqual(savedMetadata, "test-metadata")
+ }
+
+ func testGetMetadata_nonExistent_shouldReturnNil() {
+ let expectation = XCTestExpectation(description: "Get non-existent metadata")
+ var result: String?
+
+ sut.getMetadata("non-existent", resolve: { metadata in
+ result = metadata as? String
+ expectation.fulfill()
+ }, reject: { _, _, _ in
+ XCTFail("Should not reject")
+ expectation.fulfill()
+ })
+
+ wait(for: [expectation], timeout: 1.0)
+ XCTAssertNil(result)
+ }
+
+ func testRemoveMetadata_shouldRemoveMetadata() {
+ let saveExpectation = XCTestExpectation(description: "Save metadata")
+ let removeExpectation = XCTestExpectation(description: "Remove metadata")
+ let getExpectation = XCTestExpectation(description: "Get removed metadata")
+ var result: String?
+
+ sut.saveMetadata("test-id", metadataData: "test-metadata", resolve: { _ in
+ saveExpectation.fulfill()
+ }, reject: { _, _, _ in
+ saveExpectation.fulfill()
+ })
+
+ wait(for: [saveExpectation], timeout: 1.0)
+
+ sut.removeMetadata("test-id", resolve: { _ in
+ removeExpectation.fulfill()
+ }, reject: { _, _, _ in
+ XCTFail("Remove should not reject")
+ removeExpectation.fulfill()
+ })
+
+ wait(for: [removeExpectation], timeout: 1.0)
+
+ sut.getMetadata("test-id", resolve: { metadata in
+ result = metadata as? String
+ getExpectation.fulfill()
+ }, reject: { _, _, _ in
+ getExpectation.fulfill()
+ })
+
+ wait(for: [getExpectation], timeout: 1.0)
+ XCTAssertNil(result)
+ }
+
+ // MARK: - Default Token ID Tests
+
+ func testSetDefaultTokenId_shouldSucceed() {
+ let expectation = XCTestExpectation(description: "Set default token ID")
+
+ sut.setDefaultTokenId("default-id", resolve: { _ in
+ expectation.fulfill()
+ }, reject: { _, _, _ in
+ XCTFail("Should not reject")
+ expectation.fulfill()
+ })
+
+ wait(for: [expectation], timeout: 1.0)
+ }
+
+ func testSetAndGetDefaultTokenId_shouldReturnSavedId() {
+ let setExpectation = XCTestExpectation(description: "Set default")
+ let getExpectation = XCTestExpectation(description: "Get default")
+ var savedId: String?
+
+ sut.setDefaultTokenId("default-id", resolve: { _ in
+ setExpectation.fulfill()
+ }, reject: { _, _, _ in
+ XCTFail("Set should not reject")
+ setExpectation.fulfill()
+ })
+
+ wait(for: [setExpectation], timeout: 1.0)
+
+ sut.getDefaultTokenId({ id in
+ savedId = id as? String
+ getExpectation.fulfill()
+ }, reject: { _, _, _ in
+ XCTFail("Get should not reject")
+ getExpectation.fulfill()
+ })
+
+ wait(for: [getExpectation], timeout: 1.0)
+ XCTAssertEqual(savedId, "default-id")
+ }
+
+ func testGetDefaultTokenId_notSet_shouldReturnNil() {
+ let expectation = XCTestExpectation(description: "Get default not set")
+ var result: String?
+
+ sut.getDefaultTokenId({ id in
+ result = id as? String
+ expectation.fulfill()
+ }, reject: { _, _, _ in
+ XCTFail("Should not reject")
+ expectation.fulfill()
+ })
+
+ wait(for: [expectation], timeout: 1.0)
+ XCTAssertNil(result)
+ }
+
+ // MARK: - Helper Methods
+
+ private func cleanupKeychain() {
+ let services = [
+ "com.okta.auth-foundation.tokens",
+ "com.okta.auth-foundation.metadata",
+ "com.okta.auth-foundation.default"
+ ]
+
+ for service in services {
+ let query: [String: Any] = [
+ kSecClass as String: kSecClassGenericPassword,
+ kSecAttrService as String: service
+ ]
+ SecItemDelete(query as CFDictionary)
+ }
+ }
+}
diff --git a/packages/react-native-platform/jest.config.js b/packages/react-native-platform/jest.config.js
new file mode 100644
index 0000000..26eaaaa
--- /dev/null
+++ b/packages/react-native-platform/jest.config.js
@@ -0,0 +1,29 @@
+import pkg from './package.json' with { type: 'json' };
+
+
+const config = {
+ displayName: '@okta/react-native-platform',
+ preset: '@repo/jest-helpers/react-native',
+ globals: {
+ __PKG_NAME__: pkg.name,
+ __PKG_VERSION__: pkg.version,
+ __DEV__: true // required for `react-native` itself
+ },
+ // setupFiles: [
+ // '/test/jest.setup.ts'
+ // ],
+ setupFilesAfterEnv: [
+ '/test/jest.setupAfterEnv.ts'
+ ],
+ moduleNameMapper: {
+ // '^src/(.*)$': '/src/$1',
+ // NOTE: '@okta/auth-foundation' maps to '@okta/auth-foundation/core' since every src file
+ // imports from /core rather than the base entrypoint
+ '^@okta/auth-foundation$': '/../auth-foundation/src/core.ts',
+ '^@okta/auth-foundation/core$': '/../auth-foundation/src/core.ts',
+ '^@okta/auth-foundation/internal$': '/../auth-foundation/src/internal.ts',
+ '^@okta/react-native-webcrypto-bridge$': '/../react-native-webcrypto-bridge/src/index.ts',
+ }
+};
+
+export default config;
diff --git a/packages/react-native-platform/package.json b/packages/react-native-platform/package.json
new file mode 100644
index 0000000..91dd063
--- /dev/null
+++ b/packages/react-native-platform/package.json
@@ -0,0 +1,87 @@
+{
+ "name": "@okta/react-native-platform",
+ "version": "0.7.0",
+ "type": "module",
+ "main": "dist/esm/index.js",
+ "module": "dist/esm/index.js",
+ "types": "dist/types/index.d.ts",
+ "react-native": "src/index.ts",
+ "source": "src/index.ts",
+ "license": "Apache-2.0",
+ "private": true,
+ "engines": {
+ "node": ">=20.11.0"
+ },
+ "files": [
+ "./LICENSE",
+ "./dist",
+ "./ios",
+ "./android",
+ "./cpp",
+ "*.md",
+ "package.json",
+ "react-native-platform.podspec"
+ ],
+ "exports": {
+ ".": {
+ "types": "./dist/types/index.d.ts",
+ "import": "./dist/esm/index.js"
+ },
+ "./package.json": "./package.json"
+ },
+ "scripts": {
+ "lint": "eslint --ext .js,.ts,.tsx .",
+ "build": "yarn build:esm && yarn build:types",
+ "build:watch": "rollup -c --watch & tsc --watch",
+ "build:esm": "rollup -c",
+ "build:types": "tsc",
+ "test": "yarn test:unit",
+ "test:unit": "jest",
+ "test:watch": "jest --watchAll",
+ "prepack": "yarn build"
+ },
+ "dependencies": {
+ "@okta/react-native-webcrypto-bridge": "*"
+ },
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*",
+ "@okta/auth-foundation": "*",
+ "@okta/oauth2-flows": "*"
+ },
+ "devDependencies": {
+ "@repo/eslint-config": "*",
+ "@repo/rollup-config": "*",
+ "@repo/typescript-config": "*",
+ "@okta/auth-foundation": "*",
+ "@okta/oauth2-flows": "*",
+ "@okta/react-native-webcrypto-bridge": "*",
+ "@types/react": "^19.2.14",
+ "eslint": "^8.56.0",
+ "jest": "^29.7.0",
+ "react": "^19.2.4",
+ "react-native": "^0.83.1",
+ "rollup": "^4.52.4",
+ "typescript": "^5.9.2"
+ },
+ "devEngines": {
+ "runtime": {
+ "name": "node",
+ "version": ">=22.13.1",
+ "onFail": "warn"
+ },
+ "packageManager": {
+ "name": "yarn",
+ "version": ">=1.19.0",
+ "onFail": "warn"
+ }
+ },
+ "codegenConfig": {
+ "name": "RNTokenStorageSpec",
+ "type": "modules",
+ "jsSrcsDir": "src/specs",
+ "android": {
+ "javaPackageName": "com.okta.reactnativeplatform"
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/react-native-platform/react-native-platform.podspec b/packages/react-native-platform/react-native-platform.podspec
new file mode 100644
index 0000000..ebbcfa0
--- /dev/null
+++ b/packages/react-native-platform/react-native-platform.podspec
@@ -0,0 +1,27 @@
+require 'json'
+
+package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
+
+Pod::Spec.new do |s|
+ s.name = "react-native-platform"
+ s.version = package['version']
+ s.summary = "Okta React Native Platform SDK"
+ s.description = package['description'] || "Okta authentication platform for React Native"
+ s.homepage = "https://github.com/okta/okta-client-javascript"
+ s.license = package['license']
+ s.authors = { "Okta" => "jared.perreault@okta.com" }
+ s.platforms = { :ios => "13.0" }
+ s.source = { :git => "https://github.com/okta/okta-client-javascript.git", :tag => "v#{s.version}" }
+
+ s.source_files = "ios/**/*.{h,m,mm,swift}"
+
+ s.pod_target_xcconfig = {
+ 'DEFINES_MODULE' => 'YES',
+ 'SWIFT_COMPILATION_MODE' => 'wholemodule'
+ }
+
+ s.dependency "React-Core"
+
+ # Enable Swift support
+ s.swift_version = "5.0"
+end
\ No newline at end of file
diff --git a/packages/react-native-platform/rollup.config.mjs b/packages/react-native-platform/rollup.config.mjs
new file mode 100644
index 0000000..b10c612
--- /dev/null
+++ b/packages/react-native-platform/rollup.config.mjs
@@ -0,0 +1,18 @@
+import baseConfig from '@repo/rollup-config/sdk';
+import ts from 'typescript';
+import pkg from './package.json' with { type: 'json' };
+
+const base = baseConfig(ts, pkg);
+
+export default {
+ ...base,
+ input: 'src/index.ts',
+ external: [
+ ...Object.keys(pkg.dependencies || {}),
+ ...Object.keys(pkg.peerDependencies || {}),
+ 'react-native',
+ 'react',
+ '@okta/auth-foundation/core',
+ '@okta/auth-foundation/internal'
+ ],
+};
diff --git a/packages/react-native-platform/src/Credential/TokenStorage.ts b/packages/react-native-platform/src/Credential/TokenStorage.ts
new file mode 100644
index 0000000..368c560
--- /dev/null
+++ b/packages/react-native-platform/src/Credential/TokenStorage.ts
@@ -0,0 +1,205 @@
+/**
+ * @packageDocumentation
+ * @internal
+ */
+
+import {
+ Token,
+ type TokenStorage,
+ type TokenStorageEvents,
+ EventEmitter,
+ CredentialError
+} from '@okta/auth-foundation/core';
+import NativeTokenStorage from '../specs/NativeTokenStorageBridge.ts';
+
+
+/**
+ * React Native implementation of TokenStorage
+ *
+ * Uses native bridge to store:
+ * - Tokens in secure storage (iOS Keychain / Android EncryptedSharedPreferences)
+ * - Metadata in regular storage (iOS UserDefaults / Android SharedPreferences)
+ *
+ * @internal
+ */
+export class ReactNativeTokenStorage implements TokenStorage {
+ private static version = 1;
+
+ readonly emitter: EventEmitter = new EventEmitter();
+
+ #defaultId: string | null | undefined = undefined;
+
+ get defaultTokenId (): string | null | undefined {
+ return this.#defaultId;
+ }
+
+ set defaultTokenId (id: string | null) {
+ this.#defaultId = id;
+ }
+
+ async loadDefaultTokenId (): Promise {
+ if (this.defaultTokenId === undefined) {
+ const id = await NativeTokenStorage.getDefaultTokenId();
+ this.defaultTokenId = id; // caches the value returned from storage
+ return id;
+ }
+
+ return this.defaultTokenId;
+ }
+
+ async setDefaultTokenId (id: string | null): Promise {
+ if (id === this.defaultTokenId) {
+ return;
+ }
+
+ await NativeTokenStorage.setDefaultTokenId(id);
+ this.defaultTokenId = id;
+ this.emitter.emit('default_changed', { storage: this, id });
+ }
+
+ async allIDs (): Promise {
+ return await NativeTokenStorage.getAllTokenIds();
+ }
+
+ async add (token: Token, metadata?: Token.Metadata): Promise {
+ metadata ??= Token.Metadata(token);
+ if (token.id !== metadata.id) {
+ throw new CredentialError('metadataConsistency');
+ }
+
+ // Check for duplicates
+ const existingToken = await this.get(token.id);
+ if (existingToken) {
+ // TODO: make TokenError
+ throw new CredentialError('duplicateTokenAdded');
+ }
+
+ const changedDefault = (await this.allIDs()).length === 0;
+ await this.writeTokenToStorage(token);
+ await this.writeMetadataToStorage(metadata);
+
+ this.emitter.emit('token_added', { storage: this, id: token.id, token });
+
+ if (changedDefault) {
+ await this.setDefaultTokenId(token.id);
+ }
+ }
+
+ async replace (id: string, token: Token): Promise {
+ if (id !== token.id) {
+ throw new CredentialError(`Token id mismatch: ${id} !== ${token.id}`);
+ }
+
+ const metadata = await this.getMetadata(id);
+ if (token.id !== metadata?.id) {
+ throw new CredentialError('metadataConsistency');
+ }
+
+ await this.writeTokenToStorage(token);
+
+ this.emitter.emit('token_replaced', { storage: this, id, token });
+ }
+
+ async remove (id: string): Promise {
+ // `Bridge.removeToken` will also delete the metadata from separate storage location
+ await NativeTokenStorage.removeToken(id);
+
+ this.emitter.emit('token_removed', { storage: this, id });
+
+ if (this.defaultTokenId === id) {
+ await this.setDefaultTokenId(null);
+ }
+ }
+
+ async get (id: string): Promise {
+ const raw = await NativeTokenStorage.getToken(id);
+
+ if (!raw) {
+ return null;
+ }
+
+ let json;
+ try {
+ json = JSON.parse(raw);
+ } catch (error) {
+ return await this.handleReadError(error, id);
+ }
+
+ const { token, v } = json;
+
+ if (v !== ReactNativeTokenStorage.version) {
+ console.warn(`Token storage version mismatch: ${v} !== ${ReactNativeTokenStorage.version}`);
+ }
+
+ // Restore context from metadata
+ const metadata = await this.getMetadata(id);
+ if (metadata) {
+ token.context = Token.extractContext(metadata);
+ }
+
+ return new Token({ id, ...token });
+ }
+
+ async getMetadata (id: string): Promise {
+ const raw = await NativeTokenStorage.getMetadata(id);
+ if (!raw) {
+ return null;
+ }
+
+ let json;
+ try {
+ json = JSON.parse(raw);
+ } catch (error) {
+ return await this.handleReadError(error, id);
+ }
+
+ const { metadata, v } = json;
+
+ if (v !== ReactNativeTokenStorage.version) {
+ console.warn(`Token storage version mismatch: ${v} !== ${ReactNativeTokenStorage.version}`);
+ }
+
+ return metadata;
+ }
+
+ async setMetadata (metadata: Token.Metadata): Promise {
+ await this.writeMetadataToStorage(metadata);
+ this.emitter.emit('metadata_updated', { storage: this, id: metadata.id, metadata });
+ }
+
+ async clear (): Promise {
+ // NOTE: Bridge.clearTokens() also deletes `defaultTokenId`, no reason to make multiple bridge requests
+ await NativeTokenStorage.clearTokens();
+
+ this.defaultTokenId = null;
+ this.emitter.emit('default_changed', { storage: this, id: null });
+ }
+
+ // Helper methods
+
+ protected async writeTokenToStorage (token: Token): Promise {
+ const rawToken = token.toJSON();
+ delete rawToken.context; // Stored in metadata
+
+ const data = {
+ token: rawToken,
+ v: ReactNativeTokenStorage.version
+ };
+
+ await NativeTokenStorage.saveToken(token.id, JSON.stringify(data));
+ }
+
+ protected async writeMetadataToStorage (metadata: Token.Metadata): Promise {
+ const data = {
+ metadata,
+ v: ReactNativeTokenStorage.version
+ };
+
+ await NativeTokenStorage.saveMetadata(metadata.id, JSON.stringify(data));
+ }
+
+ protected async handleReadError (error: unknown, id: string): Promise {
+ await NativeTokenStorage.removeToken(id);
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/packages/react-native-platform/src/index.ts b/packages/react-native-platform/src/index.ts
new file mode 100644
index 0000000..2ab2a3d
--- /dev/null
+++ b/packages/react-native-platform/src/index.ts
@@ -0,0 +1,36 @@
+/**
+ * @packageDocumentation
+ * @internal
+ */
+
+import { addEnv } from '@okta/auth-foundation/internal';
+
+// defined in rollup.config.js
+declare const __PKG_NAME__: string;
+declare const __PKG_VERSION__: string;
+
+addEnv(`${__PKG_NAME__}/${__PKG_VERSION__}`);
+
+// eslint-disable-next-line no-restricted-imports
+export * from './platform/defaults.ts';
+
+export * from '@okta/auth-foundation/core';
+
+export * from '@okta/oauth2-flows';
+
+import { Platform } from '@okta/auth-foundation/core';
+// eslint-disable-next-line no-restricted-imports
+import { PlatformDefaults } from './platform/defaults.ts';
+
+// Register the React Native Platform default singleton dependencies
+Platform.registerDefaultsLoader(() => PlatformDefaults);
+
+// Install the React Native WebCrypto Bridge Polyfill
+import { installWebCryptoPolyfill } from '@okta/react-native-webcrypto-bridge';
+export { installWebCryptoPolyfill };
+installWebCryptoPolyfill();
+
+// Override TokenStorage to use React Native Storage Bridge
+import { ReactNativeTokenStorage } from './Credential/TokenStorage.ts';
+import { Credential } from '@okta/auth-foundation/core';
+Credential.coordinator.tokenStorage = new ReactNativeTokenStorage();
diff --git a/packages/react-native-platform/src/platform/defaults.ts b/packages/react-native-platform/src/platform/defaults.ts
new file mode 100644
index 0000000..00ff88d
--- /dev/null
+++ b/packages/react-native-platform/src/platform/defaults.ts
@@ -0,0 +1,12 @@
+import { PlatformDefaults as FoundationalPlatformDefaults } from '@okta/auth-foundation/internal';
+import { type PlatformDependencies } from '@okta/auth-foundation/core';
+
+
+// export const PlatformDefaults: PlatformDependencies = {
+// TimeCoordinator,
+// DPoPSigningAuthority: DefaultSigningAuthority,
+// DPoPNonceCache: NonceCache
+// };
+
+// TODO: remove - placeholder
+export const PlatformDefaults: PlatformDependencies = FoundationalPlatformDefaults;
diff --git a/packages/react-native-platform/src/specs/NativeTokenStorageBridge.ts b/packages/react-native-platform/src/specs/NativeTokenStorageBridge.ts
new file mode 100644
index 0000000..228e70b
--- /dev/null
+++ b/packages/react-native-platform/src/specs/NativeTokenStorageBridge.ts
@@ -0,0 +1,17 @@
+import type { TurboModule } from 'react-native';
+import { TurboModuleRegistry } from 'react-native';
+
+export interface Spec extends TurboModule {
+ saveToken (id: string, tokenData: string): Promise;
+ getToken (id: string): Promise;
+ removeToken (id: string): Promise;
+ getAllTokenIds (): Promise>;
+ clearTokens (): Promise;
+ saveMetadata (id: string, metadataData: string): Promise;
+ getMetadata (id: string): Promise;
+ removeMetadata (id: string): Promise;
+ setDefaultTokenId (id: string | null): Promise;
+ getDefaultTokenId (): Promise;
+}
+
+export default TurboModuleRegistry.getEnforcing('TokenStorageBridge');
diff --git a/packages/react-native-platform/test/jest.setupAfterEnv.ts b/packages/react-native-platform/test/jest.setupAfterEnv.ts
new file mode 100644
index 0000000..7ec420d
--- /dev/null
+++ b/packages/react-native-platform/test/jest.setupAfterEnv.ts
@@ -0,0 +1,8 @@
+import { Platform } from '@okta/auth-foundation/core';
+// eslint-disable-next-line no-restricted-imports
+import { PlatformDefaults } from 'src/platform/defaults';
+
+
+// Setups spa-platform Platform Dependencies within the Platform pattern
+// since files are loaded individually, rather than from the index.ts entrypoint
+Platform.registerDefaultsLoader(() => PlatformDefaults);
diff --git a/packages/react-native-platform/test/spec/TokenStorage.spec.ts b/packages/react-native-platform/test/spec/TokenStorage.spec.ts
new file mode 100644
index 0000000..d26d406
--- /dev/null
+++ b/packages/react-native-platform/test/spec/TokenStorage.spec.ts
@@ -0,0 +1,459 @@
+// Mock the native bridge
+jest.mock('src/specs/NativeTokenStorageBridge', () => {
+ return {
+ __esModule: true,
+ default: {
+ getAllTokenIds: jest.fn().mockResolvedValue([]),
+ getToken: jest.fn().mockResolvedValue(null),
+ saveToken: jest.fn().mockResolvedValue(undefined),
+ clearTokens: jest.fn().mockResolvedValue(undefined),
+ getMetadata: jest.fn().mockResolvedValue(null),
+ saveMetadata: jest.fn().mockResolvedValue(undefined),
+ removeToken: jest.fn().mockResolvedValue(undefined),
+ getDefaultTokenId: jest.fn().mockResolvedValue(null),
+ setDefaultTokenId: jest.fn().mockResolvedValue(undefined)
+ }
+ };
+});
+
+import { Token, CredentialError } from '@okta/auth-foundation/core';
+import { ReactNativeTokenStorage } from 'src/Credential/TokenStorage';
+import NativeTokenStorage from 'src/specs/NativeTokenStorageBridge';
+import { mockTokenResponse } from '@repo/jest-helpers/react-native/helpers';
+
+
+const makeTestToken = (id?: string, overrides = {}) => {
+ return new Token(mockTokenResponse(id, overrides));
+};
+
+describe('ReactNativeTokenStorage', () => {
+ let storage: ReactNativeTokenStorage;
+
+ beforeEach(() => {
+ storage = new ReactNativeTokenStorage();
+ });
+
+ describe('Default Token ID', () => {
+ it('should load default token ID from native storage', async () => {
+ const expectedId = 'default-token-id';
+ (NativeTokenStorage.getDefaultTokenId as jest.Mock).mockResolvedValue(
+ expectedId
+ );
+
+ const id = await storage.loadDefaultTokenId();
+
+ expect(id).toBe(expectedId);
+ expect(NativeTokenStorage.getDefaultTokenId).toHaveBeenCalled();
+ });
+
+ it('should cache default token ID after first load', async () => {
+ const expectedId = 'cached-token-id';
+ (NativeTokenStorage.getDefaultTokenId as jest.Mock).mockResolvedValue(
+ expectedId
+ );
+
+ // First call
+ await storage.loadDefaultTokenId();
+ // Second call
+ await storage.loadDefaultTokenId();
+
+ // Should only call native once
+ expect(NativeTokenStorage.getDefaultTokenId).toHaveBeenCalledTimes(1);
+ });
+
+ it('should return cached default token ID without calling native', async () => {
+ const expectedId = 'cached-id';
+ storage.defaultTokenId = expectedId;
+
+ const id = await storage.loadDefaultTokenId();
+
+ expect(id).toBe(expectedId);
+ expect(NativeTokenStorage.getDefaultTokenId).not.toHaveBeenCalled();
+ });
+
+ it('should set default token ID in native storage', async () => {
+ (NativeTokenStorage.setDefaultTokenId as jest.Mock).mockResolvedValue(
+ undefined
+ );
+
+ const tokenId = 'new-default-id';
+ await storage.setDefaultTokenId(tokenId);
+
+ expect(NativeTokenStorage.setDefaultTokenId).toHaveBeenCalledWith(
+ tokenId
+ );
+ expect(storage.defaultTokenId).toBe(tokenId);
+ });
+
+ it('should not call native if setting same default token ID', async () => {
+ const tokenId = 'same-id';
+ storage.defaultTokenId = tokenId;
+
+ await storage.setDefaultTokenId(tokenId);
+
+ expect(NativeTokenStorage.setDefaultTokenId).not.toHaveBeenCalled();
+ });
+
+ it('should emit default_changed event when setting default token ID', async () => {
+ (NativeTokenStorage.setDefaultTokenId as jest.Mock).mockResolvedValue(
+ undefined
+ );
+ const emitSpy = jest.spyOn(storage.emitter, 'emit');
+
+ const tokenId = 'new-default-id';
+ await storage.setDefaultTokenId(tokenId);
+
+ expect(emitSpy).toHaveBeenCalledWith('default_changed', {
+ storage,
+ id: tokenId,
+ });
+ });
+
+ it('should set default token ID to null', async () => {
+ (NativeTokenStorage.setDefaultTokenId as jest.Mock).mockResolvedValue(
+ undefined
+ );
+ storage.defaultTokenId = 'existing-id';
+
+ await storage.setDefaultTokenId(null);
+
+ expect(NativeTokenStorage.setDefaultTokenId).toHaveBeenCalledWith(null);
+ expect(storage.defaultTokenId).toBeNull();
+ });
+ });
+
+ describe('Token Operations', () => {
+ it('should add a new token', async () => {
+ (NativeTokenStorage.getAllTokenIds as jest.Mock).mockResolvedValue([]);
+ (NativeTokenStorage.saveToken as jest.Mock).mockResolvedValue(undefined);
+ (NativeTokenStorage.saveMetadata as jest.Mock).mockResolvedValue(
+ undefined
+ );
+ (NativeTokenStorage.setDefaultTokenId as jest.Mock).mockResolvedValue(
+ undefined
+ );
+ const emitSpy = jest.spyOn(storage.emitter, 'emit');
+
+ const token = makeTestToken();
+ await storage.add(token);
+
+ expect(NativeTokenStorage.saveToken).toHaveBeenCalled();
+ expect(NativeTokenStorage.saveMetadata).toHaveBeenCalled();
+ expect(emitSpy).toHaveBeenCalledWith('token_added', {
+ storage,
+ id: token.id,
+ token,
+ });
+ });
+
+ it('should set first token as default', async () => {
+ (NativeTokenStorage.getAllTokenIds as jest.Mock).mockResolvedValue([]);
+ (NativeTokenStorage.saveToken as jest.Mock).mockResolvedValue(undefined);
+ (NativeTokenStorage.saveMetadata as jest.Mock).mockResolvedValue(
+ undefined
+ );
+ (NativeTokenStorage.setDefaultTokenId as jest.Mock).mockResolvedValue(
+ undefined
+ );
+
+ const token = makeTestToken();
+ await storage.add(token);
+
+ expect(NativeTokenStorage.setDefaultTokenId).toHaveBeenCalledWith(
+ token.id
+ );
+ });
+
+ it('should throw error when adding duplicate token', async () => {
+ (NativeTokenStorage.getAllTokenIds as jest.Mock).mockResolvedValue([
+ 'existing-token',
+ ]);
+ const tokenData = JSON.stringify({
+ token: makeTestToken('existing-token').toJSON(),
+ v: 1,
+ });
+ (NativeTokenStorage.getToken as jest.Mock).mockResolvedValue(tokenData);
+
+ const token = makeTestToken('existing-token');
+
+ await expect(storage.add(token)).rejects.toThrow(
+ CredentialError
+ );
+ });
+
+ it('should throw error when token and metadata IDs do not match', async () => {
+ const token = makeTestToken();
+ const metadata = Token.Metadata(makeTestToken());
+
+ await expect(storage.add(token, metadata)).rejects.toThrow(
+ 'metadataConsistency'
+ );
+ });
+
+ it('should retrieve a token by ID', async () => {
+ const token = makeTestToken('test-id');
+ const tokenData = JSON.stringify({
+ token: token.toJSON(),
+ v: 1,
+ });
+ (NativeTokenStorage.getToken as jest.Mock).mockResolvedValue(tokenData);
+ (NativeTokenStorage.getMetadata as jest.Mock).mockResolvedValue(null);
+
+ const retrieved = await storage.get('test-id');
+
+ expect(retrieved).not.toBeNull();
+ expect(retrieved?.id).toBe('test-id');
+ expect(retrieved?.accessToken).toBe(token.accessToken);
+ });
+
+ it('should return null for non-existent token', async () => {
+ (NativeTokenStorage.getToken as jest.Mock).mockResolvedValue(null);
+
+ const retrieved = await storage.get('non-existent');
+
+ expect(retrieved).toBeNull();
+ });
+
+ it('should handle corrupted token data', async () => {
+ (NativeTokenStorage.getToken as jest.Mock).mockResolvedValue(
+ 'invalid-json'
+ );
+ const removeTokenSpy = jest.spyOn(NativeTokenStorage, 'removeToken');
+ removeTokenSpy.mockResolvedValue(undefined);
+
+ const retrieved = await storage.get('corrupted-token');
+
+ expect(retrieved).toBeNull();
+ expect(removeTokenSpy).toHaveBeenCalledWith('corrupted-token');
+ });
+
+ it('should replace an existing token', async () => {
+ const token = makeTestToken();
+ (NativeTokenStorage.getToken as jest.Mock).mockResolvedValue(
+ JSON.stringify({ token, v: 1 })
+ );
+ (NativeTokenStorage.getMetadata as jest.Mock).mockResolvedValue(
+ JSON.stringify({ metadata: Token.Metadata(token), v: 1 })
+ );
+ const emitSpy = jest.spyOn(storage.emitter, 'emit');
+
+ const replacementToken = makeTestToken(token.id);
+ await storage.replace(replacementToken.id, replacementToken);
+
+ expect(NativeTokenStorage.saveToken).toHaveBeenCalled();
+ expect(emitSpy).toHaveBeenCalledWith('token_replaced', {
+ storage,
+ id: token.id,
+ token: replacementToken,
+ });
+ });
+
+ it('should throw error when replacing with mismatched IDs', async () => {
+ const token = makeTestToken('token-id');
+
+ await expect(storage.replace('different-id', token)).rejects.toThrow(
+ 'Token id mismatch'
+ );
+ });
+
+ it('should remove a token', async () => {
+ (NativeTokenStorage.removeToken as jest.Mock).mockResolvedValue(
+ undefined
+ );
+ const emitSpy = jest.spyOn(storage.emitter, 'emit');
+
+ await storage.remove('token-to-remove');
+
+ expect(NativeTokenStorage.removeToken).toHaveBeenCalledWith(
+ 'token-to-remove'
+ );
+ expect(emitSpy).toHaveBeenCalledWith('token_removed', {
+ storage,
+ id: 'token-to-remove',
+ });
+ });
+
+ it('should clear default token ID when removing active default', async () => {
+ (NativeTokenStorage.removeToken as jest.Mock).mockResolvedValue(
+ undefined
+ );
+ (NativeTokenStorage.setDefaultTokenId as jest.Mock).mockResolvedValue(
+ undefined
+ );
+ storage.defaultTokenId = 'default-token';
+
+ await storage.remove('default-token');
+
+ expect(NativeTokenStorage.setDefaultTokenId).toHaveBeenCalledWith(null);
+ expect(storage.defaultTokenId).toBeNull();
+ });
+
+ it('should not clear default token ID when removing non-default token', async () => {
+ (NativeTokenStorage.removeToken as jest.Mock).mockResolvedValue(
+ undefined
+ );
+ storage.defaultTokenId = 'default-token';
+
+ await storage.remove('other-token');
+
+ expect(NativeTokenStorage.setDefaultTokenId).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('Metadata Operations', () => {
+ it('should retrieve metadata by token ID', async () => {
+ const metadata = Token.Metadata(makeTestToken('test-id'));
+ const metadataData = JSON.stringify({
+ metadata,
+ v: 1,
+ });
+ (NativeTokenStorage.getMetadata as jest.Mock).mockResolvedValue(
+ metadataData
+ );
+
+ const retrieved = await storage.getMetadata('test-id');
+
+ expect(retrieved).not.toBeNull();
+ expect(retrieved?.id).toBe('test-id');
+ });
+
+ it('should return null for non-existent metadata', async () => {
+ (NativeTokenStorage.getMetadata as jest.Mock).mockResolvedValue(null);
+
+ const retrieved = await storage.getMetadata('non-existent');
+
+ expect(retrieved).toBeNull();
+ });
+
+ it('should handle corrupted metadata', async () => {
+ (NativeTokenStorage.getMetadata as jest.Mock).mockResolvedValue(
+ 'invalid-json'
+ );
+ const removeTokenSpy = jest.spyOn(NativeTokenStorage, 'removeToken');
+ removeTokenSpy.mockResolvedValue(undefined);
+
+ const retrieved = await storage.getMetadata('corrupted-metadata');
+
+ expect(retrieved).toBeNull();
+ expect(removeTokenSpy).toHaveBeenCalledWith('corrupted-metadata');
+ });
+
+ it('should set metadata for a token', async () => {
+ (NativeTokenStorage.saveMetadata as jest.Mock).mockResolvedValue(
+ undefined
+ );
+ const emitSpy = jest.spyOn(storage.emitter, 'emit');
+ const metadata = Token.Metadata(makeTestToken('test-id'));
+
+ await storage.setMetadata(metadata);
+
+ expect(NativeTokenStorage.saveMetadata).toHaveBeenCalled();
+ expect(emitSpy).toHaveBeenCalledWith('metadata_updated', {
+ storage,
+ id: 'test-id',
+ metadata,
+ });
+ });
+ });
+
+ describe('Token List Operations', () => {
+ it('should get all token IDs', async () => {
+ const tokenIds = ['token-1', 'token-2', 'token-3'];
+ (NativeTokenStorage.getAllTokenIds as jest.Mock).mockResolvedValue(
+ tokenIds
+ );
+
+ const ids = await storage.allIDs();
+
+ expect(ids).toEqual(tokenIds);
+ expect(NativeTokenStorage.getAllTokenIds).toHaveBeenCalled();
+ });
+
+ it('should return empty array when no tokens exist', async () => {
+ (NativeTokenStorage.getAllTokenIds as jest.Mock).mockResolvedValue([]);
+
+ const ids = await storage.allIDs();
+
+ expect(ids).toEqual([]);
+ });
+
+ it('should clear all tokens', async () => {
+ (NativeTokenStorage.clearTokens as jest.Mock).mockResolvedValue(
+ undefined
+ );
+ const emitSpy = jest.spyOn(storage.emitter, 'emit');
+ storage.defaultTokenId = 'some-default';
+
+ await storage.clear();
+
+ expect(NativeTokenStorage.clearTokens).toHaveBeenCalled();
+ expect(storage.defaultTokenId).toBeNull();
+ expect(emitSpy).toHaveBeenCalledWith('default_changed', {
+ storage,
+ id: null,
+ });
+ });
+ });
+
+ describe('Token Storage Version', () => {
+ it('should warn when token storage version does not match', async () => {
+ const warnSpy = jest.spyOn(console, 'warn').mockImplementation();
+ const tokenData = JSON.stringify({
+ token: makeTestToken('test-id').toJSON(),
+ v: 99, // Different version
+ });
+ (NativeTokenStorage.getToken as jest.Mock).mockResolvedValue(tokenData);
+ (NativeTokenStorage.getMetadata as jest.Mock).mockResolvedValue(null);
+
+ await storage.get('test-id');
+
+ expect(warnSpy).toHaveBeenCalledWith(
+ expect.stringContaining('Token storage version mismatch')
+ );
+ warnSpy.mockRestore();
+ });
+
+ it('should warn when metadata storage version does not match', async () => {
+ const warnSpy = jest.spyOn(console, 'warn').mockImplementation();
+ const metadataData = JSON.stringify({
+ metadata: Token.Metadata(makeTestToken('test-id')),
+ v: 99, // Different version
+ });
+ (NativeTokenStorage.getMetadata as jest.Mock).mockResolvedValue(
+ metadataData
+ );
+
+ await storage.getMetadata('test-id');
+
+ expect(warnSpy).toHaveBeenCalledWith(
+ expect.stringContaining('Token storage version mismatch')
+ );
+ warnSpy.mockRestore();
+ });
+ });
+
+ describe('Event Emitter', () => {
+ it('should have emitter property', () => {
+ expect(storage.emitter).toBeDefined();
+ });
+
+ it('should emit events when tokens are added', async () => {
+ (NativeTokenStorage.getToken as jest.Mock).mockResolvedValue(null);
+ (NativeTokenStorage.getAllTokenIds as jest.Mock).mockResolvedValue([]);
+ (NativeTokenStorage.saveToken as jest.Mock).mockResolvedValue(undefined);
+ (NativeTokenStorage.saveMetadata as jest.Mock).mockResolvedValue(undefined);
+ const listener = jest.fn();
+ storage.emitter.on('token_added', listener);
+
+ const token = makeTestToken('new-token');
+ const metadata = Token.Metadata(token);
+ await storage.add(token, metadata);
+
+ expect(listener).toHaveBeenCalledWith({
+ storage,
+ id: 'new-token',
+ token,
+ });
+ });
+ });
+});
diff --git a/packages/react-native-platform/test/spec/foo.spec.ts b/packages/react-native-platform/test/spec/foo.spec.ts
new file mode 100644
index 0000000..8731e0a
--- /dev/null
+++ b/packages/react-native-platform/test/spec/foo.spec.ts
@@ -0,0 +1,5 @@
+describe('foo', () => {
+ it('foo', () => {
+ expect(true).toBe(true);
+ })
+})
\ No newline at end of file
diff --git a/packages/react-native-platform/test/tsconfig.json b/packages/react-native-platform/test/tsconfig.json
new file mode 100644
index 0000000..ab4620d
--- /dev/null
+++ b/packages/react-native-platform/test/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "extends": "@repo/typescript-config/jest.json",
+ "compilerOptions": {
+ "paths": {
+ "src/*": ["../src/*"]
+ },
+ "jsx": "react"
+ },
+ "lib": [
+ "dom"
+ ],
+ "include": [
+ "helpers/**/*.ts",
+ "spec/**/*.ts",
+ "jest.setupAfterEnv.ts",
+ ],
+ "exclude": [
+ "node_modules"
+ ]
+}
\ No newline at end of file
diff --git a/packages/react-native-platform/tsconfig.json b/packages/react-native-platform/tsconfig.json
new file mode 100644
index 0000000..0219e68
--- /dev/null
+++ b/packages/react-native-platform/tsconfig.json
@@ -0,0 +1,24 @@
+{
+ "extends": "@repo/typescript-config/base.json",
+ "compilerOptions": {
+ "rootDir": "./src",
+ "baseUrl": "./",
+ "outDir": "./dist/types",
+ "jsx": "react-native"
+ },
+ "lib": [
+ "dom"
+ ],
+ "files": [
+ "package.json"
+ ],
+ "include": [
+ "./src/**/*.ts",
+ "./src/**/*.tsx"
+ ],
+ "exclude": [
+ "node_modules",
+ "dist",
+ "**/__tests__/**"
+ ]
+}
\ No newline at end of file
diff --git a/packages/react-native-webcrypto-bridge/.eslintrc.cjs b/packages/react-native-webcrypto-bridge/.eslintrc.cjs
new file mode 100644
index 0000000..22f15fe
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/.eslintrc.cjs
@@ -0,0 +1,4 @@
+module.exports = {
+ extends: ["@repo/eslint-config/react-native-sdk.js"],
+ root: true,
+};
diff --git a/packages/react-native-webcrypto-bridge/LICENSE b/packages/react-native-webcrypto-bridge/LICENSE
new file mode 100644
index 0000000..eeca71e
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/LICENSE
@@ -0,0 +1,189 @@
+Okta Auth SDK License
+
+The Okta software accompanied by this notice is provided pursuant to the
+following terms:
+
+Copyright © 2015-present, Okta, Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License"); you may not use
+this file except in compliance with the License. You may obtain a copy of the
+License at http://www.apache.org/licenses/LICENSE-2.0. Unless required by
+applicable law or agreed to in writing, software distributed under the License
+is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied. See the License for the specific language
+governing permissions and limitations under the License.
+
+The Okta software accompanied by this notice has build dependencies on certain
+third party software licensed under separate terms ("Third Party Components")
+located in THIRD_PARTY_NOTICES.
+
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+"License" shall mean the terms and conditions for use, reproduction, and
+distribution as defined by Sections 1 through 9 of this document.
+
+"Licensor" shall mean the copyright owner or entity authorized by the copyright
+owner that is granting the License.
+
+"Legal Entity" shall mean the union of the acting entity and all other entities
+that control, are controlled by, or are under common control with that entity.
+For the purposes of this definition, "control" means (i) the power, direct or
+indirect, to cause the direction or management of such entity, whether by
+contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
+outstanding shares, or (iii) beneficial ownership of such entity.
+
+You" (or "Your") shall mean an individual or Legal Entity exercising
+permissions granted by this License.
+
+"Source" form shall mean the preferred form for making modifications, including
+but not limited to software source code, documentation source, and
+configuration files.
+
+"Object" form shall mean any form resulting from mechanical transformation or
+translation of a Source form, including but not limited to compiled object
+code, generated documentation, and conversions to other media types.
+
+"Work" shall mean the work of authorship, whether in Source or Object form,
+made available under the License, as indicated by a copyright notice that is
+included in or attached to the work (an example is provided in the Appendix
+below).
+
+"Derivative Works" shall mean any work, whether in Source or Object form, that
+is based on (or derived from) the Work and for which the editorial revisions,
+annotations, elaborations, or other modifications represent, as a whole, an
+original work of authorship. For the purposes of this License, Derivative Works
+shall not include works that remain separable from, or merely link (or bind by
+name) to the interfaces of, the Work and Derivative Works thereof.
+
+"Contribution" shall mean any work of authorship, including the original
+version of the Work and any modifications or additions to that Work or
+Derivative Works thereof, that is intentionally submitted to Licensor for
+inclusion in the Work by the copyright owner or by an individual or Legal
+Entity authorized to submit on behalf of the copyright owner. For the purposes
+of this definition, "submitted" means any form of electronic, verbal, or
+written communication sent to the Licensor or its representatives, including
+but not limited to communication on electronic mailing lists, source code
+control systems, and issue tracking systems that are managed by, or on behalf
+of, the Licensor for the purpose of discussing and improving the Work, but
+excluding communication that is conspicuously marked or otherwise designated in
+writing by the copyright owner as "Not a Contribution."
+
+"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
+of whom a Contribution has been received by Licensor and subsequently
+incorporated within the Work.
+2. Grant of Copyright License. Subject to the terms and conditions of this
+License, each Contributor hereby grants to You a perpetual, worldwide,
+non-exclusive, no-charge, royalty-free, irrevocable copyright license to
+reproduce, prepare Derivative Works of, publicly display, publicly perform,
+sublicense, and distribute the Work and such Derivative Works in Source or
+Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of this
+License, each Contributor hereby grants to You a perpetual, worldwide,
+non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this
+section) patent license to make, have made, use, offer to sell, sell, import,
+and otherwise transfer the Work, where such license applies only to those
+patent claims licensable by such Contributor that are necessarily infringed by
+their Contribution(s) alone or by combination of their Contribution(s) with the
+Work to which such Contribution(s) was submitted. If You institute patent
+litigation against any entity (including a cross-claim or counterclaim in a
+lawsuit) alleging that the Work or a Contribution incorporated within the Work
+constitutes direct or contributory patent infringement, then any patent
+licenses granted to You under this License for that Work shall terminate as of
+the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the Work or
+Derivative Works thereof in any medium, with or without modifications, and in
+Source or Object form, provided that You meet the following conditions:
+
+(a) You must give any other recipients of the Work or Derivative Works a copy
+of this License; and
+
+(b) You must cause any modified files to carry prominent notices stating that
+You changed the files; and
+
+(c) You must retain, in the Source form of any Derivative Works that You
+distribute, all copyright, patent, trademark, and attribution notices from the
+Source form of the Work, excluding those notices that do not pertain to any
+part of the Derivative Works; and
+
+(d) If the Work includes a "NOTICE" text file as part of its distribution, then
+any Derivative Works that You distribute must include a readable copy of the
+attribution notices contained within such NOTICE file, excluding those notices
+that do not pertain to any part of the Derivative Works, in at least one of the
+following places: within a NOTICE text file distributed as part of the
+Derivative Works; within the Source form or documentation, if provided along
+with the Derivative Works; or, within a display generated by the Derivative
+Works, if and wherever such third-party notices normally appear. The contents
+of the NOTICE file are for informational purposes only and do not modify the
+License. You may add Your own attribution notices within Derivative Works that
+You distribute, alongside or as an addendum to the NOTICE text from the Work,
+provided that such additional attribution notices cannot be construed as
+modifying the License.
+
+You may add Your own copyright statement to Your modifications and may provide
+additional or different license terms and conditions for use, reproduction, or
+distribution of Your modifications, or for any such Derivative Works as a
+whole, provided Your use, reproduction, and distribution of the Work otherwise
+complies with the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise, any
+Contribution intentionally submitted for inclusion in the Work by You to the
+Licensor shall be under the terms and conditions of this License, without any
+additional terms or conditions. Notwithstanding the above, nothing herein shall
+supersede or modify the terms of any separate license agreement you may have
+executed with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade names,
+trademarks, service marks, or product names of the Licensor, except as required
+for reasonable and customary use in describing the origin of the Work and
+reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or agreed to in
+writing, Licensor provides the Work (and each Contributor provides its
+Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied, including, without limitation, any warranties
+or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+PARTICULAR PURPOSE. You are solely responsible for determining the
+appropriateness of using or redistributing the Work and assume any risks
+associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory, whether in
+tort (including negligence), contract, or otherwise, unless required by
+applicable law (such as deliberate and grossly negligent acts) or agreed to in
+writing, shall any Contributor be liable to You for damages, including any
+direct, indirect, special, incidental, or consequential damages of any
+character arising as a result of this License or out of the use or inability to
+use the Work (including but not limited to damages for loss of goodwill, work
+stoppage, computer failure or malfunction, or any and all other commercial
+damages or losses), even if such Contributor has been advised of the
+possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing the Work or
+Derivative Works thereof, You may choose to offer, and charge a fee for,
+acceptance of support, warranty, indemnity, or other liability obligations
+and/or rights consistent with this License. However, in accepting such
+obligations, You may act only on Your own behalf and on Your sole
+responsibility, not on behalf of any other Contributor, and only if You agree
+to indemnify, defend, and hold each Contributor harmless for any liability
+incurred by, or claims asserted against, such Contributor by reason of your
+accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work.
+
+To apply the Apache License to your work, attach the following boilerplate
+notice, with the fields enclosed by brackets "[]" replaced with your own
+identifying information. (Don't include the brackets!) The text should be
+enclosed in the appropriate comment syntax for the file format. We also
+recommend that a file or class name and description of purpose be included on
+the same "printed page" as the copyright notice for easier identification
+within third-party archives.
diff --git a/packages/react-native-webcrypto-bridge/README.md b/packages/react-native-webcrypto-bridge/README.md
new file mode 100644
index 0000000..0b66c2e
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/README.md
@@ -0,0 +1,5 @@
+# React Native WebCrypto Bridge
+
+This library is meant to provide a polyfill for the [WebCrypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) in a React Native environment. You're welcome to consume this library for your own needs, however the main purpose of this library is to provide the required [WebCrypto](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) methods to support `@okta/react-native-platform`.
+
+Feature requests against this library will not be accepted, however contributions will be.
\ No newline at end of file
diff --git a/packages/react-native-webcrypto-bridge/TESTS.md b/packages/react-native-webcrypto-bridge/TESTS.md
new file mode 100644
index 0000000..e660c85
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/TESTS.md
@@ -0,0 +1,292 @@
+# WebCrypto Bridge Unit Tests
+
+This document describes the unit test suites for the Android and iOS implementations of the WebCrypto Bridge.
+
+## Android Tests
+
+### Unit Tests
+
+Located in `android/src/test/java/com/okta/webcryptobridge/`:
+
+1. **CryptoUtilsTest.kt** - Tests for Base64URL encoding/decoding and byte array utilities
+ - `testBase64URLEncode_producesURLSafeEncoding` - Verifies URL-safe base64 encoding
+ - `testBase64URLDecode_decodesValidEncoding` - Validates decoding
+ - `testBase64URLEncodeDecode_roundTrip` - Tests round-trip encode/decode
+ - `testToUnsignedByteArray_*` - Tests BigInteger to byte array conversion
+
+2. **CryptoAlgorithmRegistryTest.kt** - Tests for the algorithm handler registry
+ - `testGetHandler_returnsRegisteredHandler` - Retrieves RSA handler
+ - `testGetHandlerByKeyType_*` - Tests JWK key type to handler mapping
+ - `testGetAlgorithmNameByKeyType_*` - Tests algorithm name resolution
+ - `testRegister_customHandler` - Tests dynamic handler registration
+
+3. **RSAHandlerTest.kt** - Tests for RSA-specific cryptographic operations
+ - `testGenerateKeySpec_valid2048bitRequest` - Validates key generation spec
+ - `testGenerateKeySpec_invalid*_throwsException` - Tests parameter validation
+ - `testExportToJWK_producesValidRSAJWK` - Tests JWK export format
+ - `testImportFromJWK_reconstructsPublicKey` - Tests JWK import
+ - `testRoundTrip_exportAndImport` - Tests export/import consistency
+
+### Integration Tests
+
+Located in `android/src/test/java/com/okta/webcryptobridge/integration/`:
+
+Integration tests use **Robolectric** to simulate Android runtime on the JVM, enabling testing of WebCryptoBridgeModule with real Android Keystore operations without needing a device or emulator.
+
+**Note on Robolectric Limitations**: Robolectric simulates the Android framework but cannot perform actual cryptographic operations (RSA math, signature generation/verification, key material handling). Tests requiring real crypto operations have been removed from the suite. The remaining tests focus on error handling, API contracts, and parameter validation.
+
+1. **KeyGenerationTest.kt** - Key generation and Keystore storage
+ - `testGenerateKey_validRSA2048_createsKeystoreEntry` - Generate RSA 2048 key
+ - `testGenerateKey_unsupportedAlgorithm_rejects` - Reject unsupported algorithms
+ - `testGenerateKey_invalidModulusLength_rejects` - Validate modulus length
+ - `testGenerateKey_emptyKeyUsages_rejects` - Require at least one usage
+ - `testGenerateKey_invalidKeyUsage_rejects` - Only allow sign/verify
+ - `testGenerateKey_multipleKeys_createsDistinctIds` - Each key has unique ID
+
+2. **JWKTest.kt** - JWK export/import validation
+ - `testExportKey_unknownKeyId_rejects` - Handle missing keys
+ - `testExportKey_unsupportedFormat_rejects` - Only support JWK format
+ - `testImportKey_validJWK_succeeds` - Import external JWK
+ - `testImportKey_missingModulus_rejects` - Validate JWK structure
+
+3. **SignatureTest.kt** - Sign/verify error handling
+ - `testSign_unknownKeyId_rejects` - Handle missing keys
+
+### Running Android Tests
+
+```bash
+cd packages/react-native-webcrypto-bridge/android
+
+# Run all tests (unit + integration)
+./gradlew test
+
+# Run specific test class
+./gradlew test --tests CryptoUtilsTest
+./gradlew test --tests KeyGenerationTest
+
+# Run only integration tests
+./gradlew test --tests "*integration*"
+
+# Run with detailed output
+./gradlew test --info
+```
+
+### Test Coverage
+
+**Unit Tests:**
+- Base64URL encoding/decoding per RFC 4648
+- BigInteger to unsigned byte array conversion
+- Handler registry dispatch and lookup
+- RSA key generation specs validation
+- JWK export/import for RSA keys
+- Signature algorithm selection
+
+**Integration Tests:**
+- Full key generation → storage → retrieval flow
+- Module method execution with React Native Promise callbacks
+- Android Keystore integration via Robolectric shadows
+- Error handling (invalid algorithms, missing keys, unsupported operations)
+- Parameter validation (algorithm selection, key usage types, JWK structure)
+
+### Test Architecture
+
+**Unit Tests** (`testDebugUnitTest`):
+- Mock Android framework classes (Base64, KeyGenParameterSpec)
+- Test algorithm logic in isolation
+- Fast execution, reusable test vectors
+
+**Integration Tests** (`testDebugUnitTest` with Robolectric):
+- Simulate Android runtime via Robolectric shadows
+- Test WebCryptoBridgeModule end-to-end
+- Real Android Keystore interactions
+- Promise callback capture and assertion
+
+### Why Robolectric for Integration Tests?
+
+- ✅ No device/emulator required
+- ✅ Runs on JVM (fast iteration)
+- ✅ Simulates Android Keystore via shadows
+- ✅ Suitable for CI/CD pipelines
+- ✅ Same test execution model as unit tests
+- ⚠️ Cannot perform actual cryptographic operations (RSA math, signature generation/verification)
+- ⚠️ Tests requiring real crypto operations must be tested on physical devices or emulators
+
+### Test Dependencies
+
+- **JUnit 4.13.2**: Standard testing framework
+- **MockK 1.13.5**: Kotlin mocking framework
+- **Truth 1.1.2**: Google's fluent assertion library
+- **Robolectric 4.12.1**: Android runtime simulator
+- **androidx.test:core 1.5.0**: AndroidX testing utilities
+
+## iOS Tests
+
+### Test Files
+
+Located in `ios/`:
+
+1. **RSAKeyUtilsTests.swift** - Tests for DER encoding/decoding utilities
+ - `testRSAPublicKeyComponents_initWithValidDER` - Tests DER roundtrip
+ - `testRSAPublicKeyComponents_keySizeInBits` - Validates key size calculation
+ - `testReadDERLength_*` - Tests DER length encoding/decoding
+ - `testEncodeDERLength_*` - Tests DER length encoding
+
+2. **RSAHandlerTests.swift** - Tests for RSA algorithm handler
+ - `testGenerateKeySpec_valid2048BitRequest` - Validates key generation spec
+ - `testGenerateKeySpec_invalid*_throws` - Tests parameter validation
+ - `testGetSignatureAlgorithm_returnsRSAPKCS1v15SHA256` - Tests algorithm selection
+ - `testExportToJWK_producesValidRSAJWK` - Tests JWK export
+ - `testImportFromJWK_reconstructsComponents` - Tests JWK import
+ - `testRoundTrip_exportAndImport` - Tests export/import consistency
+
+3. **AlgorithmRegistryTests.swift** - Tests for the algorithm registry
+ - `testGetHandler_returnsRegisteredHandler` - Retrieves RSA handler
+ - `testGetHandler_returnsNilForUnregisteredAlgorithm` - Returns nil for unknown algorithm
+ - `testGetHandlerByKeyType_*` - Tests JWK key type mapping (4 tests)
+ - `testGetAlgorithmName_*` - Tests algorithm name mapping by key type (4 tests: RSA, EC, OKP, unknown)
+ - `testRegister_customHandler_returnsRegisteredHandler` - Register new algorithms dynamically
+ - `testRegister_overwrites_existingHandler` - Overwrite existing handler
+ - `testThreadSafety_concurrentAccess` - Tests thread-safe access with NSLock
+ - `testSingleton_returnsSharedInstance` - Validates singleton pattern
+ - `testConcurrentRegistration_succeeds` - 50 concurrent registrations
+ - `testMixedConcurrentOperations_succeeds` - 100 concurrent mixed operations
+
+### Integration Tests
+
+Located in `ios/Tests/RNWebCryptoBridgeTests/Integration/`:
+
+Integration tests verify end-to-end crypto workflows using mock infrastructure without React Native dependencies. Tests coordinate between AlgorithmRegistry and algorithm handlers (RSAHandler) to validate key generation, JWK export/import, and signature operations.
+
+1. **KeyGenerationIntegrationTests.swift** - Key generation workflow validation (14 tests)
+ - `testGenerateKeySpec_validRSA2048_succeeds` - Generate valid RSA 2048 key spec
+ - `testGenerateKeySpec_unsupportedAlgorithm_rejects` - Reject unsupported algorithms
+ - `testGenerateKeySpec_invalidModulusLength_throws` - Reject 1024-bit keys
+ - `testGenerateKeySpec_missing4096Bit_throws` - Reject 4096-bit keys
+ - `testGenerateKeySpec_emptyParams_usesDefault` - Default to 2048-bit
+ - `testKeyUsages_valid_sign_verify` - Store and retrieve key usages
+ - `testKeyUsages_invalid_extractable` - Validate usage constraints
+ - `testMultipleKeys_createDistinctIds` - Generate unique key IDs
+ - `testHandlerRegistry_dispatchByAlgorithm` - Route to correct handler by algorithm name
+ - `testHandlerRegistry_dispatchByKeyType` - Route to correct handler by key type
+ - `testAlgorithmMapping_RSA_to_RSASSA` - RSA key type maps to RSASSA-PKCS1-v1_5
+ - `testAlgorithmMapping_EC_to_ECDSA` - EC key type maps to ECDSA
+ - `testAlgorithmMapping_OKP_to_EdDSA` - OKP key type maps to EdDSA
+ - `testAlgorithmMapping_unknown_returnsNil` - Unknown key type returns nil
+
+2. **JWKIntegrationTests.swift** - JWK export/import validation (9 tests)
+ - `testExportKey_publicKey_producesValidJWK` - Export generates RFC 7517 structure
+ - `testExportKey_unknownKeyId_fails` - Handle missing keys gracefully
+ - `testImportKey_validJWK_succeeds` - Import external JWK data
+ - `testImportKey_missingModulus_fails` - Reject incomplete JWK
+ - `testImportKey_missingExponent_fails` - Reject incomplete JWK
+ - `testRoundTrip_exportThenImport` - Export/import round-trip consistency
+ - `testJWK_multipleRoundTrips` - Stability across multiple cycles
+ - `testJWK_structure_validation` - JWK contains kty, alg, n, e fields
+ - `testKeyStorage_storeAndRetrieveJWK` - Mock key store operations
+
+3. **SignatureIntegrationTests.swift** - Signature operation validation (12 tests)
+ - `testSignatureAlgorithm_RSA_returnsPKCS1v15SHA256` - Correct algorithm selection
+ - `testSign_noKeyId_fails` - Handle missing keys
+ - `testSign_validKey_requiresHandler` - Key validation and handler dispatch
+ - `testVerify_requiresPublicKey` - Public key usage validation
+ - `testKeyUsages_sign_and_verify` - Private key has both usages
+ - `testKeyUsages_verify_only` - Public key verify-only
+ - `testErrorHandling_invalidAlgorithm` - Reject unknown algorithms
+ - `testErrorHandling_keyTypeMismatch` - Prevent signing with public key
+ - `testSignatureWorkflow_keyDispatch` - Full signing workflow dispatch
+ - `testVerificationWorkflow_keyDispatch` - Full verification workflow dispatch
+ - `testMultipleKeys_differentOperations` - Distinguish key types
+ - `testSignatureAlgorithmSelection_consistency` - Consistent algorithm selection via different lookup paths
+
+### Running iOS Tests
+
+iOS tests use Swift Package Manager (SPM) for building and testing:
+
+```bash
+cd packages/react-native-webcrypto-bridge/ios
+
+# Run all tests
+swift test
+
+# Run with verbose output
+swift test --verbose
+
+# Run specific test class
+swift test RSAHandlerTests
+
+# Generate Xcode project locally (optional, for development)
+swift package generate-xcodeproj
+open RNWebCryptoBridge.xcodeproj
+```
+
+**CI/CD Integration:**
+- Tests can be run directly with `swift test` in any CI environment
+- No Xcode project required (text-based `Package.swift` configuration)
+- Output compatible with standard test reporting tools
+
+### Test Coverage
+
+**Unit Tests:**
+- **RSAKeyUtils**: DER encoding/decoding (7 tests), PKCS#1 structure validation
+- **RSAHandler**: Key generation specs (12 tests), JWK export/import (RFC 7517), signature algorithm selection, round-trip consistency
+- **AlgorithmRegistry**: Handler registration (16 tests), algorithm name mapping, handler lookup, singleton pattern, thread safety, concurrent access stress testing
+
+**Integration Tests (35 tests):**
+- **KeyGeneration**: Registry dispatch by algorithm and key type (14 tests), handler routing, algorithm mapping validation
+- **JWK**: Export/import round-trip consistency (9 tests), RFC 7517 structure validation, mock key store operations
+- **Signature**: Key generation → dispatch → algorithm selection consistency (12 tests), error handling for key type mismatches
+
+## Test Design
+
+### Architecture
+
+Both test suites follow a consistent design pattern:
+
+**Unit Tests:**
+- **Unit-focused**: Each test validates a single behavior
+- **Handler registry pattern**: Tests verify the registry's ability to dispatch to algorithm-specific implementations
+- **Algorithm-agnostic**: Tests for the registry and bridge avoid hardcoding RSA logic
+- **JWK compliance**: Export/import tests validate RFC 7517 (JSON Web Key) and RFC 4648 (Base64URL) compliance
+
+**Integration Tests (iOS):**
+- **Workflow validation**: Test end-to-end operations (generate → export → import → verify) without React dependencies
+- **Mock infrastructure**: AsyncTestResult container, MockKeyStore, and helper methods enable component testing in isolation
+- **Registry dispatch**: Verify correct handler routing by algorithm name and key type
+- **Component coordination**: Validate AlgorithmRegistry and RSAHandler working together
+
+### Key Testing Insights
+
+1. **Base64URL Encoding** (Android): Ensures URL-safe alphabet and no padding per RFC 4648 Section 5
+2. **DER Encoding** (iOS): Validates PKCS#1 structure for RSA public key serialization
+3. **JWK Round-Trip**: Export and reimport must preserve key components exactly
+4. **Thread Safety** (iOS): AlgorithmRegistry uses NSLock to protect concurrent access
+5. **Parameter Validation**: Both implementations validate algorithm parameters (e.g., 2048-bit RSA only)
+6. **Robolectric Integration** (Android): Android Keystore operations can be tested on JVM via shadows
+7. **Integration Test Infrastructure** (iOS): MockKeyStore and AsyncTestResult enable testing component coordination without React Native dependencies
+8. **Handler Dispatch Paths** (iOS integration): Tests validate both algorithm name and key type lookup paths produce consistent results
+
+## Adding Tests for New Algorithms
+
+When implementing support for new algorithms (ECDSA, EdDSA):
+
+### Android
+
+1. Create a new handler class implementing `CryptoAlgorithmHandler` (e.g., `ECDSAHandler.kt`)
+2. Add test class `ECDSAHandlerTest.kt` with structure similar to `RSAHandlerTest.kt`
+3. Add integration test suite `ECDSAIntegrationTest.kt` for module-level testing
+4. Update `CryptoAlgorithmRegistry` to register the new handler
+5. Add registry test cases for the new algorithm
+
+### iOS
+
+1. Create a new handler class implementing `CryptoAlgorithmHandler` protocol
+2. Add test class `ECDSAHandlerTests.swift` with similar structure to `RSAHandlerTests.swift`
+3. Update `AlgorithmRegistry` to register the new handler
+4. Add registry test cases for the new algorithm
+
+## Continuous Integration
+
+These tests are designed to run in CI/CD pipelines:
+
+- **Android**: Gradle task `./gradlew test` runs all unit + integration tests; produces reports in `android/build/reports/tests/`
+- **iOS**: `xcodebuild test` integrates with Xcode's test reporting
diff --git a/packages/react-native-webcrypto-bridge/android/.gitignore b/packages/react-native-webcrypto-bridge/android/.gitignore
new file mode 100644
index 0000000..fd9cd50
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/android/.gitignore
@@ -0,0 +1,3 @@
+.gradle
+local.properties
+.kotlin
diff --git a/packages/react-native-webcrypto-bridge/android/build.gradle b/packages/react-native-webcrypto-bridge/android/build.gradle
new file mode 100644
index 0000000..5deab07
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/android/build.gradle
@@ -0,0 +1,75 @@
+buildscript {
+ ext.kotlin_version = '2.1.0'
+
+ repositories {
+ google()
+ mavenCentral()
+ }
+
+ dependencies {
+ classpath 'com.android.tools.build:gradle:8.1.0'
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ }
+}
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ namespace "com.okta.webcryptobridge"
+ compileSdkVersion 35
+
+ defaultConfig {
+ minSdkVersion 24
+ targetSdkVersion 35
+ versionCode 1
+ versionName "1.0"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
+ }
+
+ kotlinOptions {
+ jvmTarget = '17'
+ }
+
+ testOptions {
+ unitTests.includeAndroidResources = true
+ }
+}
+
+repositories {
+ google()
+ mavenCentral()
+}
+
+dependencies {
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
+ implementation "androidx.biometric:biometric:1.1.0"
+ implementation 'com.facebook.react:react-android:0.81.0'
+
+ // Testing dependencies
+ testImplementation 'junit:junit:4.13.2'
+ testImplementation 'com.google.truth:truth:1.1.2'
+ testImplementation 'io.mockk:mockk:1.14.9'
+ testImplementation 'org.json:json:20231013'
+
+ // Integration test dependencies (Robolectric runs Android on JVM)
+ testImplementation 'org.robolectric:robolectric:4.16.1'
+ testImplementation 'androidx.test:core:1.5.0'
+}
+
+// Filter tests to only run webcryptobridge tests
+tasks.withType(Test) {
+ filter {
+ includeTestsMatching 'com.okta.webcryptobridge.*'
+ }
+}
diff --git a/packages/react-native-webcrypto-bridge/android/gradle.properties b/packages/react-native-webcrypto-bridge/android/gradle.properties
new file mode 100644
index 0000000..3e784c7
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/android/gradle.properties
@@ -0,0 +1,2 @@
+android.useAndroidX=true
+android.suppressUnsupportedCompileSdk=35
diff --git a/packages/react-native-webcrypto-bridge/android/gradle/wrapper/gradle-wrapper.jar b/packages/react-native-webcrypto-bridge/android/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..a4b76b9
Binary files /dev/null and b/packages/react-native-webcrypto-bridge/android/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/packages/react-native-webcrypto-bridge/android/gradle/wrapper/gradle-wrapper.properties b/packages/react-native-webcrypto-bridge/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..37f853b
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/packages/react-native-webcrypto-bridge/android/gradlew b/packages/react-native-webcrypto-bridge/android/gradlew
new file mode 100755
index 0000000..f3b75f3
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/android/gradlew
@@ -0,0 +1,251 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/packages/react-native-webcrypto-bridge/android/gradlew.bat b/packages/react-native-webcrypto-bridge/android/gradlew.bat
new file mode 100644
index 0000000..9d21a21
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/android/gradlew.bat
@@ -0,0 +1,94 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/packages/react-native-webcrypto-bridge/android/src/main/AndroidManifest.xml b/packages/react-native-webcrypto-bridge/android/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..560c218
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/android/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
\ No newline at end of file
diff --git a/packages/react-native-webcrypto-bridge/android/src/main/java/com/okta/webcryptobridge/CryptoAlgorithmHandler.kt b/packages/react-native-webcrypto-bridge/android/src/main/java/com/okta/webcryptobridge/CryptoAlgorithmHandler.kt
new file mode 100644
index 0000000..82c42cf
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/android/src/main/java/com/okta/webcryptobridge/CryptoAlgorithmHandler.kt
@@ -0,0 +1,77 @@
+package com.okta.webcryptobridge
+
+import android.security.keystore.KeyGenParameterSpec
+import org.json.JSONObject
+import java.security.PublicKey
+
+/**
+ * Specification for key generation including the KeyGenParameterSpec and key algorithm type.
+ */
+data class KeyGenSpec(
+ /// The KeyGenParameterSpec for AndroidKeyStore configuration
+ val keyGenParameterSpec: KeyGenParameterSpec,
+ /// The key algorithm string (e.g., "RSA", "EC") for KeyPairGenerator.getInstance()
+ val keyAlgorithm: String
+)
+
+/**
+ * Interface for algorithm-specific cryptographic operations.
+ *
+ * Implementations of this interface encapsulate all algorithm-specific logic for key generation,
+ * import/export, and signing operations. This allows the main WebCryptoBridgeModule to remain
+ * algorithm-agnostic and makes it easy to add support for new algorithms (EC, EdDSA) without
+ * modifying the core module logic.
+ */
+interface CryptoAlgorithmHandler {
+ /**
+ * Generates a key generation specification for this algorithm.
+ *
+ * Returns both the KeyGenParameterSpec (for AndroidKeyStore configuration) and the key algorithm
+ * type. The handler is responsible for validating algorithm parameters and throwing
+ * IllegalArgumentException if the parameters are invalid (e.g., unsupported key size).
+ *
+ * @param alias the keystore alias for the key being generated
+ * @param params JSON object containing algorithm-specific parameters (e.g., `modulusLength` for RSA)
+ * @param purposes bit flags indicating key usage (e.g., KeyProperties.PURPOSE_SIGN)
+ * @return a KeyGenSpec containing both the KeyGenParameterSpec and key algorithm type
+ * @throws IllegalArgumentException if algorithm parameters are invalid
+ */
+ fun generateKeySpec(alias: String, params: JSONObject, purposes: Int): KeyGenSpec
+
+ /**
+ * Exports a public key to JWK (JSON Web Key) format.
+ *
+ * Handles algorithm-specific JWK fields. For example:
+ * - RSA exports the modulus (`n`) and public exponent (`e`)
+ * - EC would export the curve point coordinates (`x`, `y`)
+ *
+ * @param publicKey the public key to export
+ * @return a JSONObject containing the JWK representation (always includes `kty` and `alg`)
+ */
+ fun exportToJWK(publicKey: PublicKey): JSONObject
+
+ /**
+ * Imports a public key from JWK (JSON Web Key) format.
+ *
+ * Handles algorithm-specific key reconstruction from JWK fields. For example:
+ * - RSA reconstructs the key from modulus (`n`) and public exponent (`e`)
+ * - EC would reconstruct from curve point coordinates (`x`, `y`)
+ *
+ * @param jwk the JWK object containing algorithm-specific fields
+ * @return the reconstructed PublicKey
+ * @throws IllegalArgumentException if JWK is malformed or contains invalid values
+ */
+ fun importFromJWK(jwk: JSONObject): PublicKey
+
+ /**
+ * Returns the signature algorithm string for this algorithm.
+ *
+ * This string is passed to `Signature.getInstance()` to obtain the appropriate
+ * signature provider. Examples:
+ * - `"SHA256withRSA"` for RSA with SHA-256
+ * - `"SHA256withECDSA"` for ECDSA with SHA-256
+ *
+ * @return the signature algorithm string (e.g., `"SHA256withRSA"`)
+ */
+ fun getSignatureAlgorithm(): String
+}
diff --git a/packages/react-native-webcrypto-bridge/android/src/main/java/com/okta/webcryptobridge/CryptoAlgorithmRegistry.kt b/packages/react-native-webcrypto-bridge/android/src/main/java/com/okta/webcryptobridge/CryptoAlgorithmRegistry.kt
new file mode 100644
index 0000000..06b9ffa
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/android/src/main/java/com/okta/webcryptobridge/CryptoAlgorithmRegistry.kt
@@ -0,0 +1,82 @@
+package com.okta.webcryptobridge
+
+import com.okta.webcryptobridge.algorithms.RSAHandler
+
+/**
+ * Registry for managing cryptographic algorithm handlers.
+ *
+ * This singleton registry maintains a mapping from algorithm names (like "RSASSA-PKCS1-v1_5")
+ * to their corresponding CryptoAlgorithmHandler implementations. It provides lookup methods
+ * for dispatching algorithm-specific operations in the WebCryptoBridgeModule.
+ */
+object CryptoAlgorithmRegistry {
+ private val handlers = mutableMapOf()
+
+ init {
+ // Register built-in handlers
+ register("RSASSA-PKCS1-v1_5", RSAHandler())
+ // Future: register("ECDSA", ECDSAHandler())
+ // Future: register("EdDSA", EdDSAHandler())
+ }
+
+ /**
+ * Registers a handler for an algorithm.
+ *
+ * This is typically called during registry initialization or when dynamically adding
+ * support for new algorithms.
+ *
+ * @param algorithmName the algorithm identifier (e.g., "RSASSA-PKCS1-v1_5")
+ * @param handler the handler to register for this algorithm
+ */
+ fun register(algorithmName: String, handler: CryptoAlgorithmHandler) {
+ handlers[algorithmName] = handler
+ }
+
+ /**
+ * Gets a handler by algorithm name.
+ *
+ * Used when the algorithm name is already known (e.g., from `algorithm.name` in generateKey).
+ *
+ * @param algorithmName the algorithm identifier (e.g., "RSASSA-PKCS1-v1_5")
+ * @return the handler for this algorithm, or null if not found
+ */
+ fun getHandler(algorithmName: String): CryptoAlgorithmHandler? {
+ return handlers[algorithmName]
+ }
+
+ /**
+ * Gets a handler by JWK key type.
+ *
+ * Used when importing keys from JWK format; the key type (`kty`) is extracted from
+ * the JWK and mapped to the corresponding algorithm handler.
+ *
+ * @param kty the JWK key type (e.g., "RSA", "EC", "OKP")
+ * @return the handler for this key type, or null if not found
+ */
+ fun getHandlerByKeyType(kty: String): CryptoAlgorithmHandler? {
+ return when (kty) {
+ "RSA" -> handlers["RSASSA-PKCS1-v1_5"]
+ "EC" -> handlers["ECDSA"]
+ "OKP" -> handlers["EdDSA"]
+ else -> null
+ }
+ }
+
+ /**
+ * Gets the algorithm name for a JWK key type.
+ *
+ * This is used when importing keys to determine which algorithm they use, so it can
+ * be stored in the CryptoKey metadata for later use in sign/verify operations.
+ *
+ * @param kty the JWK key type (e.g., "RSA", "EC", "OKP")
+ * @return the algorithm name for this key type, or null if not found
+ */
+ fun getAlgorithmNameByKeyType(kty: String): String? {
+ return when (kty) {
+ "RSA" -> "RSASSA-PKCS1-v1_5"
+ "EC" -> "ECDSA"
+ "OKP" -> "EdDSA"
+ else -> null
+ }
+ }
+}
diff --git a/packages/react-native-webcrypto-bridge/android/src/main/java/com/okta/webcryptobridge/CryptoUtils.kt b/packages/react-native-webcrypto-bridge/android/src/main/java/com/okta/webcryptobridge/CryptoUtils.kt
new file mode 100644
index 0000000..64848f7
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/android/src/main/java/com/okta/webcryptobridge/CryptoUtils.kt
@@ -0,0 +1,50 @@
+package com.okta.webcryptobridge
+
+import android.util.Base64
+import java.math.BigInteger
+
+/**
+ * Common cryptographic utilities for JWK encoding/decoding and byte manipulation.
+ * Consolidates Base64URL and byte array operations used across multiple handlers.
+ */
+object CryptoUtils {
+ /**
+ * Converts a BigInteger to an unsigned byte array by stripping leading zero bytes.
+ * Per RFC 7517 (JSON Web Key), JWK numeric fields should not have unnecessary padding.
+ *
+ * @param value the BigInteger to convert (e.g., RSA modulus or exponent)
+ * @return unsigned byte array without leading padding zeros
+ */
+ fun toUnsignedByteArray(value: BigInteger): ByteArray {
+ val bytes = value.toByteArray()
+ // Skip the leading 0x00 byte if it exists and isn't the only byte.
+ // BigInteger.toByteArray() adds a leading zero when the value's high bit
+ // would be set (to preserve sign for signed representation), but for JWK
+ // we need the minimal unsigned representation.
+ return if (bytes.isNotEmpty() && bytes[0] == 0.toByte() && bytes.size > 1) {
+ bytes.copyOfRange(1, bytes.size)
+ } else {
+ bytes
+ }
+ }
+
+ /**
+ * Encodes data as Base64URL per RFC 4648 Section 5.
+ * Base64URL uses URL-safe alphabet and omits padding characters.
+ *
+ * @param data the bytes to encode
+ * @return Base64URL-encoded string (no padding)
+ */
+ fun base64URLEncode(data: ByteArray): String =
+ Base64.encodeToString(data, Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING)
+
+ /**
+ * Decodes a Base64URL string per RFC 4648 Section 5.
+ * Handles URL-safe alphabet and no-padding variants.
+ *
+ * @param input the Base64URL-encoded string
+ * @return decoded bytes
+ */
+ fun base64URLDecode(input: String): ByteArray =
+ Base64.decode(input, Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING)
+}
diff --git a/packages/react-native-webcrypto-bridge/android/src/main/java/com/okta/webcryptobridge/WebCryptoBridgeModule.kt b/packages/react-native-webcrypto-bridge/android/src/main/java/com/okta/webcryptobridge/WebCryptoBridgeModule.kt
new file mode 100644
index 0000000..21f9f97
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/android/src/main/java/com/okta/webcryptobridge/WebCryptoBridgeModule.kt
@@ -0,0 +1,514 @@
+package com.okta.webcryptobridge
+
+import android.security.keystore.KeyGenParameterSpec
+import android.security.keystore.KeyPermanentlyInvalidatedException
+import android.security.keystore.KeyProperties
+import android.util.Base64
+import com.facebook.react.bridge.Promise
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.bridge.ReactContextBaseJavaModule
+import com.facebook.react.bridge.ReactMethod
+import com.facebook.react.bridge.ReadableArray
+import com.facebook.react.module.annotations.ReactModule
+import org.json.JSONObject
+import java.math.BigInteger
+import java.security.KeyPairGenerator
+import java.security.KeyStore
+import java.security.MessageDigest
+import java.security.PublicKey
+import java.security.SecureRandom
+import java.security.PrivateKey
+import java.security.Signature
+import java.security.interfaces.RSAPublicKey
+import java.security.spec.RSAPublicKeySpec
+import java.util.UUID
+
+/**
+ * Sealed class representing where and how a cryptographic key is stored.
+ * The extractability of a key determines its storage mechanism:
+ * - Non-extractable keys use Android Keystore (hardware-backed)
+ * - Extractable keys use native in-memory storage
+ */
+sealed class NativeCryptoKey {
+ /** Non-extractable key stored in Android Keystore at the given alias */
+ data class Keystore(val alias: String) : NativeCryptoKey()
+
+ /** Extractable key stored in memory (imported or generated) */
+ data class Platform(
+ val key: PublicKey, // Generic, not RSA-specific
+ val algorithmName: String // Track which algorithm this key uses
+ ) : NativeCryptoKey()
+}
+
+/**
+ * Represents a cryptographic key that mimics the NodeJS WebCrypto API's CryptoKey interface.
+ *
+ * This class encapsulates key metadata (algorithm, type, extractable, usages) along with
+ * a reference to where the key is stored. It bridges between the WebCrypto
+ * JavaScript API and Android's native cryptographic APIs.
+ *
+ * @property algorithm JSON object containing the key algorithm (e.g., `{"name": "RSASSA-PKCS1-v1_5", "modulusLength": 2048}`)
+ * @property type the key type: `"private"`, `"public"`, or `"secret"`
+ * @property extractable whether the key can be exported
+ * @property usages array of permitted operations: `"sign"`, `"verify"`, etc.
+ * @property entry the storage mechanism for the key (Keystore or Platform)
+ */
+data class CryptoKey(
+ val algorithm: JSONObject,
+ val type: String,
+ val extractable: Boolean,
+ val usages: List,
+ val entry: NativeCryptoKey
+)
+
+/**
+ * React Native TurboModule that bridges the WebCrypto API to Android platform cryptography.
+ *
+ * Generated keys are stored in the Android Keystore (hardware-backed, non-extractable).
+ * Imported public keys (from external JWKS endpoints) are held in memory for signature verification.
+ *
+ * Key ID scheme:
+ * - `ks:{uuid}` — Keystore-managed keys (generated via [generateKey])
+ * - `im:{uuid}` — In-memory imported public keys (imported via [importKey])
+ */
+@ReactModule(name = WebCryptoBridgeModule.NAME)
+class WebCryptoBridgeModule(reactContext: ReactApplicationContext) :
+ ReactContextBaseJavaModule(reactContext) {
+
+ companion object {
+ const val NAME = "WebCryptoBridge"
+ }
+
+ private val cryptoKeys = mutableMapOf()
+ private val secureRandom = SecureRandom()
+
+ override fun getName(): String = NAME
+
+ private val keyStore: KeyStore by lazy {
+ KeyStore.getInstance("AndroidKeyStore").apply {
+ load { null }
+ }
+ }
+
+ // MARK: - Key resolution helpers
+
+ /**
+ * Resolves a CryptoKey from the key map by its prefixed ID.
+ *
+ * @param keyId the prefixed key identifier (e.g., "ks:{uuid}" or "im:{uuid}")
+ * @return the CryptoKey metadata, or null if not found
+ */
+ private fun getCryptoKeyEntry(keyId: String): CryptoKey? {
+ return synchronized(cryptoKeys) { cryptoKeys[keyId] }
+ }
+
+ /**
+ * Resolves the actual cryptographic key from a CryptoKey's entry.
+ *
+ * Returns the native key object needed for cryptographic operations:
+ * - For Keystore entries: extracts PublicKey from the certificate
+ * - For Platform entries: returns the stored key object
+ *
+ * @param cryptoKey the CryptoKey containing the entry
+ * @param keyId the prefixed key identifier (needed for Keystore access)
+ * @return the PublicKey, or null if not found
+ */
+ private fun resolveNativeKey(cryptoKey: CryptoKey, keyId: String): PublicKey? {
+ return when (val entry = cryptoKey.entry) {
+ is NativeCryptoKey.Keystore -> {
+ val cert = keyStore.getCertificate(entry.alias)
+ cert?.publicKey as? PublicKey
+ }
+ is NativeCryptoKey.Platform -> entry.key
+ }
+ }
+
+ // MARK: - Synchronous Methods
+
+ /**
+ * Generates cryptographically secure random bytes.
+ *
+ * @param length number of random bytes to generate
+ * @return standard Base64-encoded random bytes
+ */
+ @ReactMethod(isBlockingSynchronousMethod = true)
+ fun getRandomValues(length: Double): String {
+ val len = length.toInt()
+ val bytes = ByteArray(len)
+ secureRandom.nextBytes(bytes)
+ return Base64.encodeToString(bytes, Base64.NO_WRAP)
+ }
+
+ /**
+ * Generates a random UUID v4 string.
+ *
+ * @return a UUID v4 string (e.g., `"550e8400-e29b-41d4-a716-446655440000"`)
+ */
+ @ReactMethod(isBlockingSynchronousMethod = true)
+ fun randomUUID(): String {
+ return UUID.randomUUID().toString()
+ }
+
+ // MARK: - Async Methods
+
+ /**
+ * Computes a SHA-256 digest of the provided data.
+ *
+ * @param algorithm the hash algorithm name; only `"SHA-256"` is supported
+ * @param data standard Base64-encoded input data
+ * @param promise resolves with the standard Base64-encoded digest, or rejects on error
+ */
+ @ReactMethod
+ fun digest(
+ algorithm: String,
+ data: String,
+ promise: Promise
+ ) {
+ runCatching {
+ if (algorithm != "SHA-256") {
+ promise.reject("unsupported_algorithm", "Only SHA-256 is supported")
+ return
+ }
+ val inputData = Base64.decode(data, Base64.NO_WRAP)
+ val digest = MessageDigest.getInstance("SHA-256")
+ val hash = digest.digest(inputData)
+ promise.resolve(Base64.encodeToString(hash, Base64.NO_WRAP))
+ }.onFailure { e ->
+ promise.reject("digest_failed", "Failed to compute digest", e as? Exception)
+ }
+ }
+
+ /**
+ * Generates a key pair for the specified algorithm in the Android Keystore.
+ *
+ * The key is hardware-backed and non-extractable. Algorithm-specific validation and
+ * key spec generation is delegated to the appropriate CryptoAlgorithmHandler.
+ *
+ * @param algorithmJson JSON string containing algorithm metadata
+ * @param extractable ignored; Keystore private keys are never extractable
+ * @param keyUsages array of usages: `"sign"`, `"verify"`, or both
+ * @param promise resolves with a JSON string `{"id": "ks:{uuid}"}`, or rejects on error
+ */
+ @ReactMethod
+ fun generateKey(
+ algorithmJson: String,
+ extractable: Boolean,
+ keyUsages: ReadableArray,
+ promise: Promise
+ ) {
+ runCatching {
+ val algorithm = JSONObject(algorithmJson)
+ val algorithmName = algorithm.getString("name")
+
+ // Get handler for this algorithm
+ val handler = CryptoAlgorithmRegistry.getHandler(algorithmName)
+ if (handler == null) {
+ promise.reject("unsupported_algorithm", "Algorithm not supported: $algorithmName")
+ return
+ }
+
+ val usages = (0 until keyUsages.size()).mapNotNull { keyUsages.getString(it) }.toSet()
+ val allowedUsages = setOf("sign", "verify")
+ val invalid = usages - allowedUsages
+ if (invalid.isNotEmpty()) {
+ promise.reject("invalid_key_usages", "Invalid key usages: $invalid. Allowed: $allowedUsages")
+ return
+ }
+ if (usages.isEmpty()) {
+ promise.reject("invalid_key_usages", "At least one key usage must be specified")
+ return
+ }
+
+ var purposes = 0
+ if ("sign" in usages) purposes = purposes or KeyProperties.PURPOSE_SIGN
+ if ("verify" in usages) purposes = purposes or KeyProperties.PURPOSE_VERIFY
+
+ val keyId = UUID.randomUUID().toString()
+
+ // Handler validates parameters and generates spec (throws if invalid)
+ val keyGenSpec = try {
+ handler.generateKeySpec(keyId, algorithm, purposes)
+ } catch (e: IllegalArgumentException) {
+ promise.reject("invalid_key_parameters", e.message ?: "Invalid algorithm parameters")
+ return
+ }
+
+ // Rest of key generation remains the same
+ val keyPairGenerator = KeyPairGenerator.getInstance(
+ keyGenSpec.keyAlgorithm,
+ "AndroidKeyStore"
+ )
+ keyPairGenerator.initialize(keyGenSpec.keyGenParameterSpec)
+ keyPairGenerator.generateKeyPair()
+
+ // Create CryptoKey metadata
+ val algorithmObject = JSONObject(algorithmJson)
+ val cryptoKey = CryptoKey(
+ algorithm = algorithmObject,
+ type = "private",
+ extractable = extractable,
+ usages = usages.toList(),
+ entry = NativeCryptoKey.Keystore(keyId)
+ )
+
+ synchronized(cryptoKeys) {
+ cryptoKeys[keyId] = cryptoKey
+ }
+
+ val result = JSONObject().apply {
+ put("id", keyId)
+ }
+ promise.resolve(result.toString())
+ }.onFailure { e ->
+ promise.reject("key_generation_failed", "Failed to generate key pair", e as? Exception)
+ }
+ }
+
+ /**
+ * Exports the public key of a key pair in JWK (JSON Web Key) format.
+ *
+ * Works for both Keystore-managed keys and imported public keys.
+ * Algorithm-specific JWK export is delegated to the appropriate CryptoAlgorithmHandler.
+ *
+ * @param format export format; only `"jwk"` is supported
+ * @param keyId the prefixed key identifier (e.g., `"ks:{uuid}"` or `"im:{uuid}"`)
+ * @param promise resolves with a JSON string containing algorithm-specific JWK fields, or rejects on error
+ */
+ @ReactMethod
+ fun exportKey(
+ format: String,
+ keyId: String,
+ promise: Promise
+ ) {
+ runCatching {
+ if (format != "jwk") {
+ promise.reject("unsupported_format", "Only JWK format is supported")
+ return
+ }
+
+ val cryptoKey = getCryptoKeyEntry(keyId)
+ if (cryptoKey == null) {
+ promise.reject("key_not_found", "Key not found")
+ return
+ }
+
+ val nativeKey = resolveNativeKey(cryptoKey, keyId)
+ if (nativeKey == null) {
+ promise.reject("key_export_failed", "Could not resolve public key")
+ return
+ }
+
+ // Get handler to export JWK
+ val algorithmName = cryptoKey.algorithm.getString("name")
+ val handler = CryptoAlgorithmRegistry.getHandler(algorithmName)
+ if (handler == null) {
+ return promise.reject("unsupported_algorithm", "Algorithm not supported")
+ }
+
+ // Handler generates algorithm-specific JWK
+ val jwk = handler.exportToJWK(nativeKey)
+ promise.resolve(jwk.toString())
+ }.onFailure { e ->
+ promise.reject("export_failed", e?.message, e as? Exception)
+ }
+ }
+
+ /**
+ * Imports an external public key from JWK format into the in-memory key store.
+ *
+ * Used to import server public keys (e.g., from a JWKS endpoint) for signature verification.
+ * Imported keys are public-only and do not require biometric authentication.
+ * Algorithm-specific key reconstruction is delegated to the appropriate CryptoAlgorithmHandler.
+ *
+ * @param format import format; only `"jwk"` is supported
+ * @param keyDataJson JSON string containing JWK fields
+ * @param algorithmJson JSON string with algorithm metadata
+ * @param extractable unused; imported public keys are always accessible
+ * @param keyUsages array of intended usages
+ * @param promise resolves with the key identifier string, or rejects on error
+ */
+ @ReactMethod
+ fun importKey(
+ format: String,
+ keyDataJson: String,
+ algorithmJson: String,
+ extractable: Boolean,
+ keyUsages: ReadableArray,
+ promise: Promise
+ ) {
+ runCatching {
+ if (format != "jwk") {
+ promise.reject("unsupported_format", "Only JWK format is supported")
+ return
+ }
+
+ val jwk = JSONObject(keyDataJson)
+ val kty = jwk.getString("kty")
+
+ // Get handler based on key type
+ val handler = CryptoAlgorithmRegistry.getHandlerByKeyType(kty)
+ if (handler == null) {
+ return promise.reject("unsupported_key_type", "Key type not supported: $kty")
+ }
+
+ // Handler imports from JWK
+ val publicKey = handler.importFromJWK(jwk)
+ val algorithmName = CryptoAlgorithmRegistry.getAlgorithmNameByKeyType(kty) ?: "unknown"
+
+ val keyId = UUID.randomUUID().toString()
+
+ // Parse key usages from ReadableArray
+ val usages = (0 until keyUsages.size()).mapNotNull { keyUsages.getString(it) }
+
+ // Create CryptoKey metadata
+ val algorithm = JSONObject(algorithmJson)
+ val cryptoKey = CryptoKey(
+ algorithm = algorithm,
+ type = "public",
+ extractable = extractable,
+ usages = usages,
+ entry = NativeCryptoKey.Platform(publicKey, algorithmName)
+ )
+
+ synchronized(cryptoKeys) {
+ cryptoKeys[keyId] = cryptoKey
+ }
+
+ promise.resolve(keyId)
+ }.onFailure { e ->
+ promise.reject("import_failed", e?.message, e as? Exception)
+ }
+ }
+
+ /**
+ * Signs data using a Keystore-managed private key.
+ *
+ * Loads the private key from Android Keystore and signs using the signature algorithm
+ * determined by the CryptoAlgorithmHandler for this algorithm.
+ *
+ * @param algorithmJson JSON string with algorithm metadata
+ * @param keyId the Keystore key identifier
+ * @param data standard Base64-encoded data to sign
+ * @param promise resolves with the standard Base64-encoded signature, or rejects on error
+ */
+ @ReactMethod
+ fun sign(
+ algorithmJson: String,
+ keyId: String,
+ data: String,
+ promise: Promise
+ ) {
+ runCatching {
+ val inputData = Base64.decode(data, Base64.NO_WRAP)
+
+ val cryptoKey = getCryptoKeyEntry(keyId)
+ if (cryptoKey == null) {
+ promise.reject("key_not_found", "Key not found")
+ return
+ }
+ if ("sign" !in cryptoKey.usages) {
+ promise.reject("invalid_access_error", "usage does not contain `sign`")
+ return
+ }
+
+ // Get handler to determine signature algorithm
+ val algorithmName = cryptoKey.algorithm.getString("name")
+ val handler = CryptoAlgorithmRegistry.getHandler(algorithmName)
+ if (handler == null) {
+ return promise.reject("unsupported_algorithm", "Algorithm not supported")
+ }
+
+ // Get private key from keystore
+ val keystoreAlias = (cryptoKey.entry as? NativeCryptoKey.Keystore)?.alias
+ if (keystoreAlias == null) {
+ return promise.reject("key_not_found", "Key is not a Keystore key")
+ }
+
+ val privateKey: PrivateKey = runCatching {
+ keyStore.getKey(keystoreAlias, null) as PrivateKey
+ }.getOrElse { e ->
+ if (e is KeyPermanentlyInvalidatedException) {
+ keyStore.deleteEntry(keystoreAlias)
+ return promise.reject("key_invalidated", "Key has been permanently invalidated", e)
+ } else {
+ return promise.reject("key_not_found", "Key not found in Keystore", e as? Exception)
+ }
+ }
+
+ // Use handler to get the right signature algorithm
+ val signatureAlgorithm = handler.getSignatureAlgorithm()
+ val signature = Signature.getInstance(signatureAlgorithm).apply {
+ initSign(privateKey)
+ update(inputData)
+ }
+
+ val signatureBytes = signature.sign()
+ promise.resolve(Base64.encodeToString(signatureBytes, Base64.NO_WRAP))
+ }.onFailure { e ->
+ promise.reject("signing_failed", "Failed to sign data", e as? Exception)
+ }
+ }
+
+ /**
+ * Verifies a signature against the provided data.
+ *
+ * Works for both Keystore-managed keys and imported public keys.
+ * Signature algorithm verification is delegated to the appropriate CryptoAlgorithmHandler.
+ *
+ * @param algorithmJson JSON string with algorithm metadata
+ * @param keyId the prefixed key identifier (e.g., `"ks:{uuid}"` or `"im:{uuid}"`)
+ * @param signatureBase64 standard Base64-encoded signature to verify
+ * @param data standard Base64-encoded data that was signed
+ * @param promise resolves with `true` if the signature is valid, `false` otherwise, or rejects on error
+ */
+ @ReactMethod
+ fun verify(
+ algorithmJson: String,
+ keyId: String,
+ signatureBase64: String,
+ data: String,
+ promise: Promise
+ ) {
+ runCatching {
+ val cryptoKey = getCryptoKeyEntry(keyId)
+ if (cryptoKey == null) {
+ promise.reject("key_not_found", "Key not found")
+ return
+ }
+ if ("verify" !in cryptoKey.usages) {
+ promise.reject("invalid_access_error", "usage does not contain `verify`")
+ return
+ }
+
+ // Determine algorithm from key entry
+ val algorithmName = when (val entry = cryptoKey.entry) {
+ is NativeCryptoKey.Keystore -> cryptoKey.algorithm.getString("name")
+ is NativeCryptoKey.Platform -> entry.algorithmName
+ }
+
+ val handler = CryptoAlgorithmRegistry.getHandler(algorithmName)
+ if (handler == null) {
+ return promise.reject("unsupported_algorithm", "Algorithm not supported")
+ }
+
+ val nativeKey = resolveNativeKey(cryptoKey, keyId)
+ if (nativeKey == null) {
+ return promise.reject("verification_failed", "Could not resolve public key")
+ }
+
+ val inputData = Base64.decode(data, Base64.NO_WRAP)
+ val signatureBytes = Base64.decode(signatureBase64, Base64.NO_WRAP)
+
+ // Use handler to get the right signature algorithm
+ val signatureAlgorithm = handler.getSignatureAlgorithm()
+ val signature = Signature.getInstance(signatureAlgorithm)
+ signature.initVerify(nativeKey)
+ signature.update(inputData)
+ val verified = signature.verify(signatureBytes)
+
+ promise.resolve(verified)
+ }.onFailure { e ->
+ promise.reject("verification_failed", "Failed to verify signature", e as? Exception)
+ }
+ }
+}
diff --git a/packages/react-native-webcrypto-bridge/android/src/main/java/com/okta/webcryptobridge/WebCryptoBridgePackage.kt b/packages/react-native-webcrypto-bridge/android/src/main/java/com/okta/webcryptobridge/WebCryptoBridgePackage.kt
new file mode 100644
index 0000000..85a2c10
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/android/src/main/java/com/okta/webcryptobridge/WebCryptoBridgePackage.kt
@@ -0,0 +1,16 @@
+package com.okta.webcryptobridge
+
+import com.facebook.react.ReactPackage
+import com.facebook.react.bridge.NativeModule
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.uimanager.ViewManager
+
+class WebCryptoBridgePackage : ReactPackage {
+ override fun createNativeModules(reactContext: ReactApplicationContext): List {
+ return listOf(WebCryptoBridgeModule(reactContext))
+ }
+
+ override fun createViewManagers(reactContext: ReactApplicationContext): List> {
+ return emptyList()
+ }
+}
diff --git a/packages/react-native-webcrypto-bridge/android/src/main/java/com/okta/webcryptobridge/algorithms/RSAHandler.kt b/packages/react-native-webcrypto-bridge/android/src/main/java/com/okta/webcryptobridge/algorithms/RSAHandler.kt
new file mode 100644
index 0000000..58fa5ed
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/android/src/main/java/com/okta/webcryptobridge/algorithms/RSAHandler.kt
@@ -0,0 +1,107 @@
+package com.okta.webcryptobridge.algorithms
+
+import android.security.keystore.KeyGenParameterSpec
+import android.security.keystore.KeyProperties
+import com.okta.webcryptobridge.CryptoAlgorithmHandler
+import com.okta.webcryptobridge.CryptoUtils
+import com.okta.webcryptobridge.KeyGenSpec
+import org.json.JSONObject
+import java.math.BigInteger
+import java.security.PublicKey
+import java.security.KeyFactory
+import java.security.interfaces.RSAPublicKey
+import java.security.spec.RSAPublicKeySpec
+
+/**
+ * RSA algorithm handler implementing the CryptoAlgorithmHandler interface.
+ *
+ * This handler encapsulates all RSA-specific logic for key generation, import/export, and
+ * signature algorithm selection. It validates RSA parameters (currently supporting only 2048-bit keys)
+ * and provides JWK import/export for RSA public keys.
+ */
+class RSAHandler : CryptoAlgorithmHandler {
+ /**
+ * Generates a KeyGenParameterSpec for RSA.
+ *
+ * Validates that the modulus length is exactly 2048 bits (as per current requirements).
+ * Configures the key for SHA256 digests and PKCS#1 v1.5 padding.
+ *
+ * @param alias the keystore alias for the key
+ * @param params JSON object with required field: `modulusLength` (must be 2048)
+ * @param purposes bit flags indicating key usage
+ * @return configured KeyGenParameterSpec for RSA
+ * @throws IllegalArgumentException if modulusLength is not 2048
+ */
+ override fun generateKeySpec(
+ alias: String,
+ params: JSONObject,
+ purposes: Int
+ ): KeyGenSpec {
+ val modulusLength = params.getInt("modulusLength")
+ if (modulusLength != 2048) {
+ throw IllegalArgumentException("RSA: only 2048-bit keys are supported")
+ }
+
+ val keyGenParameterSpec = KeyGenParameterSpec.Builder(alias, purposes)
+ .setKeySize(2048)
+ .setDigests(KeyProperties.DIGEST_SHA256)
+ .setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PKCS1)
+ .build()
+
+ return KeyGenSpec(
+ keyGenParameterSpec = keyGenParameterSpec,
+ keyAlgorithm = KeyProperties.KEY_ALGORITHM_RSA
+ )
+ }
+
+ /**
+ * Exports an RSA public key to JWK format.
+ *
+ * Extracts the modulus and public exponent from the RSA public key and encodes them
+ * as Base64URL strings per RFC 7517 (JSON Web Key).
+ *
+ * @param publicKey the RSA public key to export
+ * @return JSONObject with fields: `kty` ("RSA"), `alg` ("RS256"), `n` (modulus), `e` (exponent)
+ */
+ override fun exportToJWK(publicKey: PublicKey): JSONObject {
+ val rsaPublicKey = publicKey as RSAPublicKey
+ val jwk = JSONObject()
+ jwk.put("kty", "RSA")
+ jwk.put("alg", "RS256")
+ jwk.put("n", CryptoUtils.base64URLEncode(CryptoUtils.toUnsignedByteArray(rsaPublicKey.modulus)))
+ jwk.put("e", CryptoUtils.base64URLEncode(CryptoUtils.toUnsignedByteArray(rsaPublicKey.publicExponent)))
+ return jwk
+ }
+
+ /**
+ * Imports an RSA public key from JWK format.
+ *
+ * Reconstructs an RSA public key from modulus (`n`) and exponent (`e`) fields encoded
+ * as Base64URL strings per RFC 7517.
+ *
+ * @param jwk JSONObject containing fields: `n` (modulus), `e` (exponent)
+ * @return reconstructed RSA PublicKey
+ * @throws IllegalArgumentException if `n` or `e` fields are missing or invalid
+ */
+ override fun importFromJWK(jwk: JSONObject): PublicKey {
+ val nString = jwk.getString("n")
+ val eString = jwk.getString("e")
+
+ val modulusBytes = CryptoUtils.base64URLDecode(nString)
+ val exponentBytes = CryptoUtils.base64URLDecode(eString)
+
+ val modulus = BigInteger(1, modulusBytes)
+ val exponent = BigInteger(1, exponentBytes)
+
+ val keyFactory = KeyFactory.getInstance("RSA")
+ val keySpec = RSAPublicKeySpec(modulus, exponent)
+ return keyFactory.generatePublic(keySpec)
+ }
+
+ /**
+ * Returns the signature algorithm string for RSA.
+ *
+ * @return `"SHA256withRSA"` for signing with RSASSA-PKCS1-v1_5 and SHA-256
+ */
+ override fun getSignatureAlgorithm(): String = "SHA256withRSA"
+}
diff --git a/packages/react-native-webcrypto-bridge/android/src/test/java/com/okta/webcryptobridge/CryptoAlgorithmRegistryTest.kt b/packages/react-native-webcrypto-bridge/android/src/test/java/com/okta/webcryptobridge/CryptoAlgorithmRegistryTest.kt
new file mode 100644
index 0000000..3396088
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/android/src/test/java/com/okta/webcryptobridge/CryptoAlgorithmRegistryTest.kt
@@ -0,0 +1,199 @@
+package com.okta.webcryptobridge
+
+import io.mockk.mockk
+import org.junit.Assert.*
+import org.junit.Before
+import org.junit.Test
+import java.util.concurrent.Executors
+import java.util.concurrent.TimeUnit
+
+class CryptoAlgorithmRegistryTest {
+
+ private lateinit var registry: CryptoAlgorithmRegistry
+ private lateinit var mockHandler: CryptoAlgorithmHandler
+
+ @Before
+ fun setUp() {
+ // Since CryptoAlgorithmRegistry is a singleton with init block,
+ // we test through its public interface
+ registry = CryptoAlgorithmRegistry
+ mockHandler = mockk()
+ }
+
+ @Test
+ fun testGetHandler_returnsRegisteredHandler() {
+ val handler = registry.getHandler("RSASSA-PKCS1-v1_5")
+ assertNotNull("RSA handler should be registered", handler)
+ }
+
+ @Test
+ fun testGetHandler_returnsNullForUnregisteredAlgorithm() {
+ val handler = registry.getHandler("NONEXISTENT")
+ assertNull("Unregistered algorithm should return null", handler)
+ }
+
+ @Test
+ fun testGetHandlerByKeyType_RSA_returnsHandler() {
+ val handler = registry.getHandlerByKeyType("RSA")
+ assertNotNull("RSA key type should return handler", handler)
+ }
+
+ @Test
+ fun testGetHandlerByKeyType_EC_returnsNull() {
+ // EC handler not yet registered
+ val handler = registry.getHandlerByKeyType("EC")
+ assertNull("EC key type should return null (not yet implemented)", handler)
+ }
+
+ @Test
+ fun testGetHandlerByKeyType_OKP_returnsNull() {
+ // EdDSA handler not yet registered
+ val handler = registry.getHandlerByKeyType("OKP")
+ assertNull("OKP key type should return null (not yet implemented)", handler)
+ }
+
+ @Test
+ fun testGetHandlerByKeyType_unknownType_returnsNull() {
+ val handler = registry.getHandlerByKeyType("UNKNOWN")
+ assertNull("Unknown key type should return null", handler)
+ }
+
+ @Test
+ fun testGetAlgorithmNameByKeyType_RSA() {
+ val algorithmName = registry.getAlgorithmNameByKeyType("RSA")
+ assertEquals("RSA key type should map to RSASSA-PKCS1-v1_5",
+ "RSASSA-PKCS1-v1_5", algorithmName)
+ }
+
+ @Test
+ fun testGetAlgorithmNameByKeyType_EC() {
+ val algorithmName = registry.getAlgorithmNameByKeyType("EC")
+ assertEquals("EC key type should map to ECDSA", "ECDSA", algorithmName)
+ }
+
+ @Test
+ fun testGetAlgorithmNameByKeyType_OKP() {
+ val algorithmName = registry.getAlgorithmNameByKeyType("OKP")
+ assertEquals("OKP key type should map to EdDSA", "EdDSA", algorithmName)
+ }
+
+ @Test
+ fun testGetAlgorithmNameByKeyType_unknown() {
+ val algorithmName = registry.getAlgorithmNameByKeyType("UNKNOWN")
+ assertNull("Unknown key type should return null", algorithmName)
+ }
+
+ @Test
+ fun testRegister_customHandler() {
+ val customHandler = mockk()
+ registry.register("CUSTOM-ALGORITHM", customHandler)
+
+ val retrieved = registry.getHandler("CUSTOM-ALGORITHM")
+ assertSame("Registered handler should be retrieable", customHandler, retrieved)
+ }
+
+ @Test
+ fun testRegister_overwrites_existingHandler() {
+ val newHandler = mockk()
+ registry.register("RSASSA-PKCS1-v1_5", newHandler)
+
+ val retrieved = registry.getHandler("RSASSA-PKCS1-v1_5")
+ assertSame("New handler should overwrite old one", newHandler, retrieved)
+ }
+
+ @Test
+ fun testConcurrentHandlerLookups() {
+ val executor = Executors.newFixedThreadPool(10)
+ val iterations = 100
+
+ val futures = (0..iterations).map {
+ executor.submit {
+ val handler = registry.getHandler("RSASSA-PKCS1-v1_5")
+ assertNotNull("Handler should not be null under concurrent access", handler)
+ }
+ }
+
+ // Wait for all tasks to complete
+ futures.forEach { it.get() }
+ executor.shutdown()
+ assertTrue("All tasks should complete", executor.awaitTermination(10, TimeUnit.SECONDS))
+ }
+
+ @Test
+ fun testConcurrentKeyTypeLookups() {
+ val executor = Executors.newFixedThreadPool(10)
+ val iterations = 100
+
+ val futures = (0..iterations).map {
+ executor.submit {
+ val handler = registry.getHandlerByKeyType("RSA")
+ assertNotNull("Handler should not be null under concurrent access", handler)
+ }
+ }
+
+ // Wait for all tasks to complete
+ futures.forEach { it.get() }
+ executor.shutdown()
+ assertTrue("All tasks should complete", executor.awaitTermination(10, TimeUnit.SECONDS))
+ }
+
+ @Test
+ fun testConcurrentRegistration() {
+ val executor = Executors.newFixedThreadPool(10)
+ val iterations = 50
+
+ val futures = (0..iterations).map { i ->
+ executor.submit {
+ val customHandler = mockk()
+ val algorithmName = "CUSTOM-ALGORITHM-$i"
+ registry.register(algorithmName, customHandler)
+
+ // Verify the handler was registered
+ val retrieved = registry.getHandler(algorithmName)
+ assertSame("Registered handler should be retrievable", customHandler, retrieved)
+ }
+ }
+
+ // Wait for all tasks to complete
+ futures.forEach { it.get() }
+ executor.shutdown()
+ assertTrue("All tasks should complete", executor.awaitTermination(10, TimeUnit.SECONDS))
+ }
+
+ @Test
+ fun testMixedConcurrentOperations() {
+ val executor = Executors.newFixedThreadPool(10)
+ val iterations = 100
+
+ val futures = (0..iterations).map { i ->
+ if (i % 3 == 0) {
+ // Some threads do handler lookups
+ executor.submit {
+ val handler = registry.getHandler("RSASSA-PKCS1-v1_5")
+ assertNotNull("Handler lookup should succeed", handler)
+ }
+ } else if (i % 3 == 1) {
+ // Some threads do key type lookups
+ executor.submit {
+ val handler = registry.getHandlerByKeyType("RSA")
+ assertNotNull("Key type lookup should succeed", handler)
+ }
+ } else {
+ // Some threads do custom registrations and lookups
+ executor.submit {
+ val customHandler = mockk()
+ val algorithmName = "MIXED-CONCURRENT-$i"
+ registry.register(algorithmName, customHandler)
+ val retrieved = registry.getHandler(algorithmName)
+ assertSame("Mixed operation registration should succeed", customHandler, retrieved)
+ }
+ }
+ }
+
+ // Wait for all tasks to complete
+ futures.forEach { it.get() }
+ executor.shutdown()
+ assertTrue("All tasks should complete", executor.awaitTermination(10, TimeUnit.SECONDS))
+ }
+}
+
diff --git a/packages/react-native-webcrypto-bridge/android/src/test/java/com/okta/webcryptobridge/CryptoUtilsTest.kt b/packages/react-native-webcrypto-bridge/android/src/test/java/com/okta/webcryptobridge/CryptoUtilsTest.kt
new file mode 100644
index 0000000..c3abf7f
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/android/src/test/java/com/okta/webcryptobridge/CryptoUtilsTest.kt
@@ -0,0 +1,117 @@
+package com.okta.webcryptobridge
+
+import android.util.Base64
+import io.mockk.every
+import io.mockk.mockkStatic
+import org.junit.Assert.*
+import org.junit.Before
+import org.junit.Test
+import java.math.BigInteger
+import java.util.Base64 as JavaBase64
+
+class CryptoUtilsTest {
+
+ @Before
+ fun setUp() {
+ // Mock Android's Base64 using Java's base64 encoder/decoder
+ mockkStatic(Base64::class)
+ every {
+ Base64.encodeToString(any(), Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING)
+ }.answers { bytes ->
+ val byteArray = args[0] as ByteArray
+ JavaBase64.getUrlEncoder().withoutPadding().encodeToString(byteArray)
+ }
+ every {
+ Base64.decode(any(), Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING)
+ }.answers {
+ val encodedString = args[0] as String
+ JavaBase64.getUrlDecoder().decode(encodedString)
+ }
+ }
+
+ @Test
+ fun testToUnsignedByteArray_removesLeadingZeroByte() {
+ // BigInteger with high bit set gets a leading zero in toByteArray() for sign preservation
+ // 32768 (0x8000) requires a leading 0x00 byte in two's complement: [0x00, 0x80, 0x00]
+ // After stripping the leading zero: [0x80, 0x00]
+ val value = BigInteger("32768")
+ val result = CryptoUtils.toUnsignedByteArray(value)
+
+ // Should strip leading zero byte added by BigInteger.toByteArray()
+ assertEquals(2, result.size)
+ assertEquals(0x80.toByte(), result[0])
+ assertEquals(0x00.toByte(), result[1])
+ }
+
+ @Test
+ fun testToUnsignedByteArray_noLeadingZero() {
+ // BigInteger without leading zero
+ val value = BigInteger("127")
+ val result = CryptoUtils.toUnsignedByteArray(value)
+
+ assertEquals(1, result.size)
+ assertEquals(127.toByte(), result[0])
+ }
+
+ @Test
+ fun testToUnsignedByteArray_largeNumber() {
+ // Large RSA-like modulus
+ val value = BigInteger("65537") // Common RSA exponent
+ val result = CryptoUtils.toUnsignedByteArray(value)
+
+ // Should be 3 bytes: [1, 0, 1]
+ assertEquals(3, result.size)
+ assertEquals(1.toByte(), result[0])
+ assertEquals(0.toByte(), result[1])
+ assertEquals(1.toByte(), result[2])
+ }
+
+ @Test
+ fun testBase64URLEncode_producesURLSafeEncoding() {
+ val data = byteArrayOf(0xFB.toByte(), 0xFF.toByte()) // Test data with URL-unsafe chars
+ val encoded = CryptoUtils.base64URLEncode(data)
+
+ // Should not contain standard base64 padding or unsafe chars
+ assertFalse(encoded.contains("+"))
+ assertFalse(encoded.contains("/"))
+ assertFalse(encoded.contains("="))
+
+ // Should contain URL-safe chars
+ assertTrue(encoded.matches(Regex("[A-Za-z0-9_-]*")))
+ }
+
+ @Test
+ fun testBase64URLEncode_noPadding() {
+ val data = "Hello".toByteArray()
+ val encoded = CryptoUtils.base64URLEncode(data)
+
+ // RFC 4648 Section 5 specifies no padding for base64url
+ assertFalse(encoded.endsWith("="))
+ }
+
+ @Test
+ fun testBase64URLDecode_decodesValidEncoding() {
+ val original = "Hello, World!".toByteArray()
+ val encoded = CryptoUtils.base64URLEncode(original)
+ val decoded = CryptoUtils.base64URLDecode(encoded)
+
+ assertArrayEquals(original, decoded)
+ }
+
+ @Test
+ fun testBase64URLEncodeDecode_roundTrip() {
+ val testCases = listOf(
+ byteArrayOf(),
+ byteArrayOf(0),
+ byteArrayOf(255.toByte()),
+ "RSA public key exponent".toByteArray(),
+ ByteArray(256) { it.toByte() } // Full byte range
+ )
+
+ for (original in testCases) {
+ val encoded = CryptoUtils.base64URLEncode(original)
+ val decoded = CryptoUtils.base64URLDecode(encoded)
+ assertArrayEquals("Failed for input: ${original.contentToString()}", original, decoded)
+ }
+ }
+}
diff --git a/packages/react-native-webcrypto-bridge/android/src/test/java/com/okta/webcryptobridge/algorithms/RSAHandlerTest.kt b/packages/react-native-webcrypto-bridge/android/src/test/java/com/okta/webcryptobridge/algorithms/RSAHandlerTest.kt
new file mode 100644
index 0000000..7a88cf9
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/android/src/test/java/com/okta/webcryptobridge/algorithms/RSAHandlerTest.kt
@@ -0,0 +1,233 @@
+package com.okta.webcryptobridge.algorithms
+
+import android.security.keystore.KeyGenParameterSpec
+import android.security.keystore.KeyProperties
+import android.util.Base64
+import com.okta.webcryptobridge.CryptoUtils
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.mockkConstructor
+import io.mockk.mockkStatic
+import org.json.JSONObject
+import org.junit.Assert.*
+import org.junit.Before
+import org.junit.Test
+import java.math.BigInteger
+import java.security.KeyFactory
+import java.security.interfaces.RSAPublicKey
+import java.security.spec.RSAPublicKeySpec
+import java.util.Base64 as JavaBase64
+
+class RSAHandlerTest {
+
+ private val handler = RSAHandler()
+
+ @Before
+ fun setUp() {
+ // Mock Android's Base64 using Java's base64 encoder/decoder
+ mockkStatic(Base64::class)
+ every {
+ Base64.encodeToString(any(), Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING)
+ }.answers {
+ val byteArray = args[0] as ByteArray
+ JavaBase64.getUrlEncoder().withoutPadding().encodeToString(byteArray)
+ }
+ every {
+ Base64.decode(any(), Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING)
+ }.answers {
+ val encodedString = args[0] as String
+ JavaBase64.getUrlDecoder().decode(encodedString)
+ }
+
+ // Mock KeyGenParameterSpec.Builder constructor and methods for RSA key generation testing
+ // Builder methods must return the builder itself to support method chaining
+ mockkConstructor(KeyGenParameterSpec.Builder::class)
+ every { anyConstructed().setKeySize(any()) }.answers { self as KeyGenParameterSpec.Builder }
+ every { anyConstructed().setDigests(*anyVararg()) }.answers { self as KeyGenParameterSpec.Builder }
+ every { anyConstructed().setSignaturePaddings(*anyVararg()) }.answers { self as KeyGenParameterSpec.Builder }
+
+ // Mock the build() method to return a mock KeyGenParameterSpec
+ val mockKeySpec = mockk()
+ every { anyConstructed().build() } returns mockKeySpec
+ every { mockKeySpec.getKeySize() } returns 2048
+ }
+
+ @Test
+ fun testGenerateKeySpec_valid2048bitRequest() {
+ val params = JSONObject().apply {
+ put("modulusLength", 2048)
+ }
+
+ val keyGenSpec = handler.generateKeySpec("test_key", params, KeyProperties.PURPOSE_SIGN)
+
+ assertNotNull(keyGenSpec)
+ assertNotNull(keyGenSpec.keyGenParameterSpec)
+ assertEquals(KeyProperties.KEY_ALGORITHM_RSA, keyGenSpec.keyAlgorithm)
+ assertEquals(2048, keyGenSpec.keyGenParameterSpec.keySize)
+ }
+
+ @Test
+ fun testGenerateKeySpec_invalid1024bit_throwsException() {
+ val params = JSONObject().apply {
+ put("modulusLength", 1024)
+ }
+
+ val exception = assertThrows(IllegalArgumentException::class.java) {
+ handler.generateKeySpec("test_key", params, KeyProperties.PURPOSE_SIGN)
+ }
+
+ assertTrue(exception.message?.contains("2048-bit") ?: false)
+ }
+
+ @Test
+ fun testGenerateKeySpec_invalid4096bit_throwsException() {
+ val params = JSONObject().apply {
+ put("modulusLength", 4096)
+ }
+
+ val exception = assertThrows(IllegalArgumentException::class.java) {
+ handler.generateKeySpec("test_key", params, KeyProperties.PURPOSE_SIGN)
+ }
+
+ assertTrue(exception.message?.contains("2048-bit") ?: false)
+ }
+
+ @Test
+ fun testGenerateKeySpec_missingModulusLength_throws() {
+ val params = JSONObject()
+
+ assertThrows(org.json.JSONException::class.java) {
+ handler.generateKeySpec("test_key", params, KeyProperties.PURPOSE_SIGN)
+ }
+ }
+
+ @Test
+ fun testExportToJWK_producesValidRSAJWK() {
+ // Create a test RSA public key with 512-bit modulus (minimum Java RSA accepts)
+ val modulus = BigInteger("13407807929942597099574024998205846127479365820592393377723561204902396782632420619524063388588060570674527907588217505193955904655022796993667430081902488957615951424143683904546248171409330541224586313167537540957037327749277318899268196199264853959961841784773220626899375871856020228048436639045220963139551")
+ val exponent = BigInteger("65537")
+ val keySpec = RSAPublicKeySpec(modulus, exponent)
+ val keyFactory = KeyFactory.getInstance("RSA")
+ val publicKey = keyFactory.generatePublic(keySpec)
+
+ val jwk = handler.exportToJWK(publicKey)
+
+ assertEquals("RSA", jwk.getString("kty"))
+ assertEquals("RS256", jwk.getString("alg"))
+ assertTrue(jwk.has("n"))
+ assertTrue(jwk.has("e"))
+ }
+
+ @Test
+ fun testExportToJWK_encodesModulusCorrectly() {
+ // Test with a known RSA 512-bit modulus and standard exponent (65537 = 0x10001)
+ val modulus = BigInteger("13407807929942597099574024998205846127479365820592393377723561204902396782632420619524063388588060570674527907588217505193955904655022796993667430081902488957615951424143683904546248171409330541224586313167537540957037327749277318899268196199264853959961841784773220626899375871856020228048436639045220963139551")
+ val exponent = BigInteger("65537")
+ val keySpec = RSAPublicKeySpec(modulus, exponent)
+ val keyFactory = KeyFactory.getInstance("RSA")
+ val publicKey = keyFactory.generatePublic(keySpec)
+
+ val jwk = handler.exportToJWK(publicKey)
+ val exportedExponent = jwk.getString("e")
+
+ // Decode and verify exponent
+ val decodedExponentBytes = CryptoUtils.base64URLDecode(exportedExponent)
+ val decodedExponent = BigInteger(1, decodedExponentBytes)
+ assertEquals(exponent, decodedExponent)
+ }
+
+ @Test
+ fun testImportFromJWK_reconstructsPublicKey() {
+ // Create a test public key and export it
+ val modulus = BigInteger("13407807929942597099574024998205846127479365820592393377723561204902396782632420619524063388588060570674527907588217505193955904655022796993667430081902488957615951424143683904546248171409330541224586313167537540957037327749277318899268196199264853959961841784773220626899375871856020228048436639045220963139551")
+ val exponent = BigInteger("65537")
+ val keySpec = RSAPublicKeySpec(modulus, exponent)
+ val keyFactory = KeyFactory.getInstance("RSA")
+ val originalKey = keyFactory.generatePublic(keySpec)
+
+ // Export to JWK
+ val jwk = handler.exportToJWK(originalKey)
+
+ // Import back
+ val reimportedKey = handler.importFromJWK(jwk) as RSAPublicKey
+
+ // Verify they match
+ assertEquals(modulus, reimportedKey.modulus)
+ assertEquals(exponent, reimportedKey.publicExponent)
+ }
+
+ @Test
+ fun testImportFromJWK_missingModulus_throws() {
+ val jwk = JSONObject().apply {
+ put("e", CryptoUtils.base64URLEncode(BigInteger("65537").toByteArray()))
+ }
+
+ assertThrows(org.json.JSONException::class.java) {
+ handler.importFromJWK(jwk)
+ }
+ }
+
+ @Test
+ fun testImportFromJWK_missingExponent_throws() {
+ val jwk = JSONObject().apply {
+ put("n", CryptoUtils.base64URLEncode(BigInteger("12345").toByteArray()))
+ }
+
+ assertThrows(org.json.JSONException::class.java) {
+ handler.importFromJWK(jwk)
+ }
+ }
+
+ @Test
+ fun testImportFromJWK_invalidBase64_throws() {
+ val jwk = JSONObject().apply {
+ put("n", "not valid base64!@#$")
+ put("e", "also not valid!@#$")
+ }
+
+ assertThrows(Exception::class.java) {
+ handler.importFromJWK(jwk)
+ }
+ }
+
+ @Test
+ fun testGetSignatureAlgorithm_returnsSHA256withRSA() {
+ val algorithm = handler.getSignatureAlgorithm()
+ assertEquals("SHA256withRSA", algorithm)
+ }
+
+ @Test
+ fun testRoundTrip_exportAndImport() {
+ // Create multiple RSA keys and verify round-trip export/import
+ val testCases = listOf(
+ // 512-bit RSA modulus with standard exponent
+ Pair(
+ BigInteger("13407807929942597099574024998205846127479365820592393377723561204902396782632420619524063388588060570674527907588217505193955904655022796993667430081902488957615951424143683904546248171409330541224586313167537540957037327749277318899268196199264853959961841784773220626899375871856020228048436639045220963139551"),
+ BigInteger("65537")
+ ),
+ // Another 512-bit RSA modulus
+ Pair(
+ BigInteger("12621776367165119722584210296089220304600850433864372891562882765235950760458122857151589318095220702868841742280047671521002221374696929971188236805638433923476883267126889641871743624265652061346325819589706324949295396904658826325555921968926283374687352405821125423521124239456707629408837282697301433897183"),
+ BigInteger("65537")
+ )
+ )
+
+ for ((modulus, exponent) in testCases) {
+ val keySpec = RSAPublicKeySpec(modulus, exponent)
+ val keyFactory = KeyFactory.getInstance("RSA")
+ val originalKey = keyFactory.generatePublic(keySpec)
+
+ // Export
+ val jwk = handler.exportToJWK(originalKey)
+
+ // Import
+ val reimportedKey = handler.importFromJWK(jwk) as RSAPublicKey
+
+ // Verify
+ assertEquals("Failed for modulus=$modulus, exponent=$exponent",
+ modulus, reimportedKey.modulus)
+ assertEquals("Failed for modulus=$modulus, exponent=$exponent",
+ exponent, reimportedKey.publicExponent)
+ }
+ }
+}
diff --git a/packages/react-native-webcrypto-bridge/android/src/test/java/com/okta/webcryptobridge/integration/JWKTest.kt b/packages/react-native-webcrypto-bridge/android/src/test/java/com/okta/webcryptobridge/integration/JWKTest.kt
new file mode 100644
index 0000000..f6147a6
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/android/src/test/java/com/okta/webcryptobridge/integration/JWKTest.kt
@@ -0,0 +1,95 @@
+package com.okta.webcryptobridge.integration
+
+import com.google.common.truth.Truth.assertThat
+import org.json.JSONObject
+import org.junit.Test
+import org.robolectric.RobolectricTestRunner
+
+/**
+ * Integration tests for WebCryptoBridgeModule JWK import and error handling.
+ * Tests JWK import API contracts and error cases.
+ *
+ * Note: Tests requiring actual cryptographic operations on generated keys
+ * (export, sign/verify) are not included as they require real key material
+ * and full Android Keystore support beyond Robolectric's capabilities.
+ */
+@org.junit.runner.RunWith(RobolectricTestRunner::class)
+class JWKTest : WebCryptoBridgeModuleTest() {
+
+ @Test
+ fun testExportKey_unknownKeyId_rejects() {
+ // Act
+ val mockedPromise = createMockPromise()
+ module.exportKey("jwk", "nonexistent_key_id", mockedPromise.getMockPromise())
+
+ // Assert
+ assertThat(mockedPromise.isRejected).isTrue()
+ assertThat(mockedPromise.rejectedCode).isEqualTo("key_not_found")
+ }
+
+ @Test
+ fun testExportKey_unsupportedFormat_rejects() {
+ // Arrange - create a test JWK for export
+ val testJWK = JSONObject().apply {
+ put("kty", "RSA")
+ put("alg", "RS256")
+ put("n", "8nIJVnXLVqSxz1WqZhpq9xXL4fHBp5V1H1Fw_qKVB_kqKJ35mqYmGMf7jHljbf0Zbt6eKnCJrzg2p8TpCJ1sVCqvxmD8ZfjnEWKWZ_0vqZEJb0n5hJKb7nnJxKIV-r7sXvJCRm5dJEoJdWqiEEQCt9UzvHXk5YCeqZxBtVpqU8")
+ put("e", "AQAB")
+ }
+ val algorithm = JSONObject().put("name", "RSASSA-PKCS1-v1_5")
+
+ // Import the key first
+ val mockedImportPromise = createMockPromise()
+ module.importKey("jwk", testJWK.toString(), algorithm.toString(), true, createReadableArray("verify"), mockedImportPromise.getMockPromise())
+ val keyId = mockedImportPromise.resolvedValue as String
+
+ // Act - try to export with unsupported format
+ val mockedPromise = createMockPromise()
+ module.exportKey("pem", keyId, mockedPromise.getMockPromise())
+
+ // Assert
+ assertThat(mockedPromise.isRejected).isTrue()
+ assertThat(mockedPromise.rejectedCode).isEqualTo("unsupported_format")
+ }
+
+ @Test
+ fun testImportKey_validJWK_succeeds() {
+ // Arrange - create a test JWK (from a real RSA key)
+ val testJWK = JSONObject().apply {
+ put("kty", "RSA")
+ put("alg", "RS256")
+ // 512-bit RSA test modulus
+ put("n", "8nIJVnXLVqSxz1WqZhpq9xXL4fHBp5V1H1Fw_qKVB_kqKJ35mqYmGMf7jHljbf0Zbt6eKnCJrzg2p8TpCJ1sVCqvxmD8ZfjnEWKWZ_0vqZEJb0n5hJKb7nnJxKIV-r7sXvJCRm5dJEoJdWqiEEQCt9UzvHXk5YCeqZxBtVpqU8")
+ put("e", "AQAB")
+ }
+ val algorithm = JSONObject().put("name", "RSASSA-PKCS1-v1_5")
+
+ // Act
+ val mockedPromise = createMockPromise()
+ module.importKey("jwk", testJWK.toString(), algorithm.toString(), true, createReadableArray("verify"), mockedPromise.getMockPromise())
+
+ // Assert
+ assertThat(mockedPromise.isResolved).isTrue()
+ val importedKeyId = mockedPromise.resolvedValue as String
+ assertThat(importedKeyId).isNotEmpty()
+ }
+
+ @Test
+ fun testImportKey_missingModulus_rejects() {
+ // Arrange - JWK without modulus
+ val incompleteJWK = JSONObject().apply {
+ put("kty", "RSA")
+ put("e", "AQAB")
+ // missing "n"
+ }
+ val algorithm = JSONObject().put("name", "RSASSA-PKCS1-v1_5")
+
+ // Act
+ val mockedPromise = createMockPromise()
+ module.importKey("jwk", incompleteJWK.toString(), algorithm.toString(), true, createReadableArray("verify"), mockedPromise.getMockPromise())
+
+ // Assert
+ assertThat(mockedPromise.isRejected).isTrue()
+ }
+}
+
diff --git a/packages/react-native-webcrypto-bridge/android/src/test/java/com/okta/webcryptobridge/integration/KeyGenerationTest.kt b/packages/react-native-webcrypto-bridge/android/src/test/java/com/okta/webcryptobridge/integration/KeyGenerationTest.kt
new file mode 100644
index 0000000..21f5e70
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/android/src/test/java/com/okta/webcryptobridge/integration/KeyGenerationTest.kt
@@ -0,0 +1,136 @@
+package com.okta.webcryptobridge.integration
+
+import android.util.Base64
+import com.google.common.truth.Truth.assertThat
+import org.json.JSONObject
+import org.junit.Test
+import org.robolectric.RobolectricTestRunner
+
+/**
+ * Integration tests for WebCryptoBridgeModule.generateKey() method.
+ * Tests key generation flow with Android Keystore via Robolectric.
+ */
+@org.junit.runner.RunWith(RobolectricTestRunner::class)
+class KeyGenerationTest : WebCryptoBridgeModuleTest() {
+
+ @Test
+ fun testGenerateKey_validRSA2048_createsKeystoreEntry() {
+ // Arrange
+ val algorithmJson = JSONObject().apply {
+ put("name", "RSASSA-PKCS1-v1_5")
+ put("modulusLength", 2048)
+ }
+ val keyUsages = createReadableArray("sign", "verify")
+ val mockedPromise = createMockPromise()
+ val promise = mockedPromise.getMockPromise()
+
+ // Act
+ module.generateKey(algorithmJson.toString(), false, keyUsages, promise)
+
+ // Assert
+ if (!mockedPromise.isResolved && mockedPromise.isRejected) {
+ throw AssertionError("Promise was rejected instead of resolved. Code: ${mockedPromise.rejectedCode}, Message: ${mockedPromise.rejectedMessage}")
+ }
+ if (!mockedPromise.isResolved && !mockedPromise.isRejected) {
+ throw AssertionError("Promise was neither resolved nor rejected. No response from generateKey().")
+ }
+
+ assertThat(mockedPromise.isResolved).isTrue()
+ assertThat(mockedPromise.isRejected).isFalse()
+ val result = JSONObject(mockedPromise.resolvedValue as String)
+ assertThat(result.has("id")).isTrue()
+ assertThat(result.getString("id")).isNotEmpty()
+ }
+
+ @Test
+ fun testGenerateKey_unsupportedAlgorithm_rejects() {
+ // Arrange
+ val algorithmJson = JSONObject().put("name", "UNSUPPORTED_ALGORITHM")
+ val mockedPromise = createMockPromise()
+ val promise = mockedPromise.getMockPromise()
+
+ // Act
+ module.generateKey(algorithmJson.toString(), false, createReadableArray(), promise)
+
+ // Assert
+ assertThat(mockedPromise.isRejected).isTrue()
+ assertThat(mockedPromise.rejectedCode).isEqualTo("unsupported_algorithm")
+ assertThat(mockedPromise.rejectedMessage).contains("not supported")
+ }
+
+ @Test
+ fun testGenerateKey_invalidModulusLength_rejects() {
+ // Arrange - 1024-bit RSA is not supported (minimum is 512, but we require 2048)
+ val algorithmJson = JSONObject().apply {
+ put("name", "RSASSA-PKCS1-v1_5")
+ put("modulusLength", 1024)
+ }
+ val mockedPromise = createMockPromise()
+ val promise = mockedPromise.getMockPromise()
+
+ // Act
+ module.generateKey(algorithmJson.toString(), false, createReadableArray("sign"), promise)
+
+ // Assert
+ assertThat(mockedPromise.isRejected).isTrue()
+ assertThat(mockedPromise.rejectedCode).isEqualTo("invalid_key_parameters")
+ }
+
+ @Test
+ fun testGenerateKey_emptyKeyUsages_rejects() {
+ // Arrange
+ val algorithmJson = JSONObject().apply {
+ put("name", "RSASSA-PKCS1-v1_5")
+ put("modulusLength", 2048)
+ }
+ val mockedPromise = createMockPromise()
+ val promise = mockedPromise.getMockPromise()
+
+ // Act - empty key usages array
+ module.generateKey(algorithmJson.toString(), false, createReadableArray(), promise)
+
+ // Assert
+ assertThat(mockedPromise.isRejected).isTrue()
+ assertThat(mockedPromise.rejectedCode).isEqualTo("invalid_key_usages")
+ }
+
+ @Test
+ fun testGenerateKey_invalidKeyUsage_rejects() {
+ // Arrange
+ val algorithmJson = JSONObject().apply {
+ put("name", "RSASSA-PKCS1-v1_5")
+ put("modulusLength", 2048)
+ }
+ val mockedPromise = createMockPromise()
+ val promise = mockedPromise.getMockPromise()
+
+ // Act - invalid usage "encrypt"
+ module.generateKey(algorithmJson.toString(), false, createReadableArray("encrypt"), promise)
+
+ // Assert
+ assertThat(mockedPromise.isRejected).isTrue()
+ assertThat(mockedPromise.rejectedCode).isEqualTo("invalid_key_usages")
+ }
+
+ @Test
+ fun testGenerateKey_multipleKeys_createsDistinctIds() {
+ // Arrange
+ val algorithmJson = JSONObject().apply {
+ put("name", "RSASSA-PKCS1-v1_5")
+ put("modulusLength", 2048)
+ }
+ val keyUsages = createReadableArray("sign", "verify")
+
+ // Act - generate two keys
+ val mockedPromise1 = createMockPromise()
+ module.generateKey(algorithmJson.toString(), false, keyUsages, mockedPromise1.getMockPromise())
+
+ val mockedPromise2 = createMockPromise()
+ module.generateKey(algorithmJson.toString(), false, keyUsages, mockedPromise2.getMockPromise())
+
+ // Assert
+ val id1 = JSONObject(mockedPromise1.resolvedValue as String).getString("id")
+ val id2 = JSONObject(mockedPromise2.resolvedValue as String).getString("id")
+ assertThat(id1).isNotEqualTo(id2)
+ }
+}
diff --git a/packages/react-native-webcrypto-bridge/android/src/test/java/com/okta/webcryptobridge/integration/SignatureTest.kt b/packages/react-native-webcrypto-bridge/android/src/test/java/com/okta/webcryptobridge/integration/SignatureTest.kt
new file mode 100644
index 0000000..65f211d
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/android/src/test/java/com/okta/webcryptobridge/integration/SignatureTest.kt
@@ -0,0 +1,36 @@
+package com.okta.webcryptobridge.integration
+
+import android.util.Base64
+import com.google.common.truth.Truth.assertThat
+import org.json.JSONObject
+import org.junit.Test
+import org.robolectric.RobolectricTestRunner
+
+/**
+ * Integration tests for WebCryptoBridgeModule signature operations.
+ * Tests error handling and API contracts for sign/verify.
+ *
+ * Note: Tests requiring actual cryptographic operations on generated keys
+ * (actual signing/verification) are not included as they require real key material
+ * and full Android Keystore support beyond Robolectric's capabilities.
+ */
+@org.junit.runner.RunWith(RobolectricTestRunner::class)
+class SignatureTest : WebCryptoBridgeModuleTest() {
+
+ @Test
+ fun testSign_unknownKeyId_rejects() {
+ // Arrange
+ val data = "test message".toByteArray()
+ val dataBase64 = Base64.encodeToString(data, Base64.NO_WRAP)
+ val algorithmJson = JSONObject().put("name", "RSASSA-PKCS1-v1_5")
+
+ // Act
+ val mockedPromise = createMockPromise()
+ module.sign(algorithmJson.toString(), "nonexistent_key", dataBase64, mockedPromise.getMockPromise())
+
+ // Assert
+ assertThat(mockedPromise.isRejected).isTrue()
+ assertThat(mockedPromise.rejectedCode).isEqualTo("key_not_found")
+ }
+}
+
diff --git a/packages/react-native-webcrypto-bridge/android/src/test/java/com/okta/webcryptobridge/integration/TestUtils.kt b/packages/react-native-webcrypto-bridge/android/src/test/java/com/okta/webcryptobridge/integration/TestUtils.kt
new file mode 100644
index 0000000..f7cd51b
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/android/src/test/java/com/okta/webcryptobridge/integration/TestUtils.kt
@@ -0,0 +1,30 @@
+package com.okta.webcryptobridge.integration
+
+import com.facebook.react.bridge.ReadableArray
+import com.facebook.react.bridge.ReadableType
+import com.facebook.react.bridge.Dynamic
+
+/**
+ * Creates a mock ReadableArray from variable arguments of strings.
+ * Used for simulating key usages arrays in tests.
+ */
+fun createReadableArray(vararg items: String): ReadableArray {
+ return object : ReadableArray {
+ override fun size() = items.size
+ override fun getString(index: Int): String = if (index < items.size) items[index] else ""
+ override fun getInt(index: Int) = 0
+ override fun getDouble(index: Int) = 0.0
+ override fun getBoolean(index: Int) = false
+ override fun getArray(index: Int) = null
+ override fun getMap(index: Int) = null
+ override fun isNull(index: Int) = false
+ override fun getType(index: Int) = ReadableType.String
+ override fun getLong(index: Int) = 0L
+ override fun getDynamic(index: Int): Dynamic {
+ throw NotImplementedError("getDynamic not used in tests")
+ }
+ override fun toArrayList(): ArrayList = ArrayList(items.toList())
+ }
+}
+
+
diff --git a/packages/react-native-webcrypto-bridge/android/src/test/java/com/okta/webcryptobridge/integration/WebCryptoBridgeModuleTest.kt b/packages/react-native-webcrypto-bridge/android/src/test/java/com/okta/webcryptobridge/integration/WebCryptoBridgeModuleTest.kt
new file mode 100644
index 0000000..145a20c
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/android/src/test/java/com/okta/webcryptobridge/integration/WebCryptoBridgeModuleTest.kt
@@ -0,0 +1,206 @@
+package com.okta.webcryptobridge.integration
+
+import android.app.Application
+import androidx.test.core.app.ApplicationProvider
+import com.facebook.react.bridge.Promise
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.bridge.WritableMap
+import com.okta.webcryptobridge.WebCryptoBridgeModule
+import io.mockk.mockk
+import io.mockk.every
+import io.mockk.mockkStatic
+import io.mockk.unmockkStatic
+import io.mockk.just
+import io.mockk.Runs
+import java.security.KeyPair
+import java.security.KeyPairGenerator
+import java.security.Signature
+import java.security.PrivateKey
+import java.security.PublicKey
+import javax.crypto.Cipher
+import android.security.keystore.KeyGenParameterSpec
+import org.junit.After
+import org.junit.Before
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+
+/**
+ * Base class for WebCryptoBridgeModule integration tests.
+ * Provides Robolectric setup and utilities for testing module methods with captured promises.
+ *
+ * SDK 34 (Android 14) is used for testing. This is the highest SDK supported by Robolectric 4.12.1
+ * and provides better compatibility with the project's target SDK 35. To test against SDK 35,
+ * Robolectric would need to be upgraded to a version that supports it.
+ */
+@org.junit.runner.RunWith(RobolectricTestRunner::class)
+@Config(sdk = [34])
+abstract class WebCryptoBridgeModuleTest {
+
+ protected lateinit var module: WebCryptoBridgeModule
+ protected lateinit var context: ReactApplicationContext
+ protected val promises = mutableListOf()
+
+ @Before
+ fun setUp() {
+ val application = ApplicationProvider.getApplicationContext()
+ context = mockk(relaxed = true)
+ every { context.currentActivity } returns null
+
+ // Mock security-related classes to avoid AndroidKeyStore limitations in tests
+ try {
+ mockkStatic(KeyPairGenerator::class)
+ every { KeyPairGenerator.getInstance(any(), any()) } answers {
+ createMockKeyPairGenerator()
+ }
+ } catch (e: Exception) {
+ // Suppress exceptions during mock setup - AndroidKeyStore may not be available
+ }
+
+ try {
+ mockkStatic(Signature::class)
+ every { Signature.getInstance(any()) } answers {
+ createMockSignature()
+ }
+ } catch (e: Exception) {
+ // Suppress exceptions during mock setup
+ }
+
+ try {
+ mockkStatic(Cipher::class)
+ every { Cipher.getInstance(any()) } answers {
+ createMockCipher()
+ }
+ } catch (e: Exception) {
+ // Suppress exceptions during mock setup
+ }
+
+ module = WebCryptoBridgeModule(context)
+ }
+
+ @After
+ fun tearDown() {
+ promises.clear()
+ try { unmockkStatic(KeyPairGenerator::class) } catch (e: Exception) { }
+ try { unmockkStatic(Signature::class) } catch (e: Exception) { }
+ try { unmockkStatic(Cipher::class) } catch (e: Exception) { }
+ }
+
+ private fun createMockKeyPairGenerator(): KeyPairGenerator {
+ return mockk {
+ every { initialize(any()) } just Runs
+ every { generateKeyPair() } answers {
+ // Create a mock KeyPair with mock keys
+ mockk {
+ every { public } returns mockk(relaxed = true)
+ every { private } returns mockk(relaxed = true)
+ }
+ }
+ }
+ }
+
+ private fun createMockSignature(): Signature {
+ return mockk {
+ val updateData = mutableListOf()
+
+ every { initSign(any()) } just Runs
+ every { initVerify(any()) } just Runs
+ every { update(any()) } answers {
+ updateData.add(it.invocation.args[0] as ByteArray)
+ }
+ every { update(any()) } just Runs
+ every { sign() } answers {
+ // Generate a consistent signature from the data
+ updateData.flatMap { data ->
+ data.toList().mapIndexed { idx, byte ->
+ (byte.toInt() + idx + 1).toByte()
+ }
+ }.toByteArray().takeIf { it.isNotEmpty() } ?: byteArrayOf(0x42)
+ }
+ every { verify(any()) } answers {
+ val providedSig = it.invocation.args[0] as ByteArray
+ val expectedSig = updateData.flatMap { data ->
+ data.toList().mapIndexed { idx, byte ->
+ (byte.toInt() + idx + 1).toByte()
+ }
+ }.toByteArray().takeIf { it.isNotEmpty() } ?: byteArrayOf(0x42)
+ providedSig.contentEquals(expectedSig)
+ }
+ }
+ }
+
+ private fun createMockCipher(): Cipher {
+ return mockk(relaxed = true) {
+ every { init(any(), any()) } answers { }
+ every { init(any(), any()) } answers { }
+ every { doFinal(any()) } answers {
+ // Return transformed data
+ val input = it.invocation.args[0] as ByteArray
+ input.map { b -> (b.toInt() xor 0xAA).toByte() }.toByteArray()
+ }
+ every { getBlockSize() } returns 128
+ every { getOutputSize(any()) } answers { ((it.invocation.args[0] as Int) * 2) }
+ }
+ }
+
+ protected fun createMockPromise(): MockedPromise = MockedPromise().also {
+ promises.add(it)
+ }
+}
+
+/**
+ * Spy-based Promise wrapper for testing that tracks resolve/reject calls.
+ * Uses a relaxed mock to auto-implement all Promise methods, then wraps it
+ * with overrides to track the calls we care about.
+ */
+class MockedPromise {
+ var resolvedValue: Any? = null
+ var rejectedCode: String? = null
+ var rejectedMessage: String? = null
+ var isResolved = false
+ var isRejected = false
+
+ fun getMockPromise(): Promise {
+ val self = this
+ val relaxedMock = mockk(relaxed = true)
+
+ // Wrapper that tracks calls while delegating to the relaxed mock
+ return object : Promise by relaxedMock {
+ override fun resolve(value: Any?) {
+ self.resolvedValue = value
+ self.isResolved = true
+ relaxedMock.resolve(value)
+ }
+
+ override fun reject(throwable: Throwable) {
+ self.rejectedCode = "error"
+ self.rejectedMessage = throwable.message
+ self.isRejected = true
+ relaxedMock.reject(throwable)
+ }
+
+ override fun reject(code: String, message: String?) {
+ self.rejectedCode = code
+ self.rejectedMessage = message
+ self.isRejected = true
+ relaxedMock.reject(code, message)
+ }
+
+ override fun reject(code: String, throwable: Throwable?) {
+ self.rejectedCode = code
+ self.rejectedMessage = throwable?.message
+ self.isRejected = true
+ relaxedMock.reject(code, throwable)
+ }
+
+ override fun reject(code: String, message: String?, throwable: Throwable?) {
+ self.rejectedCode = code
+ self.rejectedMessage = message ?: throwable?.message
+ self.isRejected = true
+ relaxedMock.reject(code, message, throwable)
+ }
+ }
+ }
+}
+
+
+
diff --git a/packages/react-native-webcrypto-bridge/ios/.gitignore b/packages/react-native-webcrypto-bridge/ios/.gitignore
new file mode 100644
index 0000000..c09e535
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/ios/.gitignore
@@ -0,0 +1,5 @@
+.build/
+.swiftpm/
+*.xcodeproj/
+*.xcworkspace/
+.DS_Store
diff --git a/packages/react-native-webcrypto-bridge/ios/Package.swift b/packages/react-native-webcrypto-bridge/ios/Package.swift
new file mode 100644
index 0000000..f8b8adf
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/ios/Package.swift
@@ -0,0 +1,29 @@
+// swift-tools-version:5.9
+import PackageDescription
+
+let package = Package(
+ name: "RNWebCryptoBridge",
+ platforms: [
+ .iOS(.v13)
+ ],
+ products: [
+ .library(
+ name: "RNWebCryptoBridge",
+ targets: ["RNWebCryptoBridge"]
+ )
+ ],
+ targets: [
+ .target(
+ name: "RNWebCryptoBridge",
+ path: "Sources/RNWebCryptoBridge",
+ exclude: ["WebCryptoBridgeModule.m", "WebCryptoBridge.swift", "WebCryptoBridge.h"],
+ publicHeadersPath: "."
+ ),
+ .testTarget(
+ name: "RNWebCryptoBridgeTests",
+ dependencies: ["RNWebCryptoBridge"],
+ path: "Tests/RNWebCryptoBridgeTests"
+ )
+ ]
+)
+
diff --git a/packages/react-native-webcrypto-bridge/ios/Sources/RNWebCryptoBridge/AlgorithmRegistry.swift b/packages/react-native-webcrypto-bridge/ios/Sources/RNWebCryptoBridge/AlgorithmRegistry.swift
new file mode 100644
index 0000000..3ad1db4
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/ios/Sources/RNWebCryptoBridge/AlgorithmRegistry.swift
@@ -0,0 +1,78 @@
+import Foundation
+
+/// Singleton registry for managing cryptographic algorithm handlers.
+/// Maps algorithm names to their corresponding CryptoAlgorithmHandler implementations.
+/// Provides thread-safe lookup methods for dispatching algorithm-specific operations.
+class AlgorithmRegistry {
+ static let shared = AlgorithmRegistry()
+
+ private var handlers: [String: CryptoAlgorithmHandler] = [:]
+ private let lock = NSLock()
+
+ /// Initializes the registry with built-in algorithm handlers.
+ private init() {
+ // Register RSA handler
+ handlers["RSASSA-PKCS1-v1_5"] = RSAHandler()
+
+ // Future algorithm registrations:
+ // handlers["ECDSA"] = ECDSAHandler()
+ // handlers["EdDSA"] = EdDSAHandler()
+ }
+
+ /// Retrieves the handler for a specific algorithm name.
+ /// Thread-safe access with NSLock.
+ ///
+ /// @param algorithmName The algorithm name to look up (e.g., "RSASSA-PKCS1-v1_5")
+ /// @return CryptoAlgorithmHandler if registered, nil otherwise
+ func getHandler(for algorithmName: String) -> CryptoAlgorithmHandler? {
+ return lock.withLock {
+ handlers[algorithmName]
+ }
+ }
+
+ /// Retrieves the handler for a JWK key type.
+ /// Maps JWK "kty" field to algorithm name and looks up handler.
+ /// Thread-safe access with NSLock.
+ ///
+ /// @param kty The JWK key type field (e.g., "RSA", "EC", "OKP")
+ /// @return CryptoAlgorithmHandler if key type is mapped, nil otherwise
+ func getHandlerByKeyType(_ kty: String) -> CryptoAlgorithmHandler? {
+ return lock.withLock {
+ let algorithmName = keyTypeToAlgorithm(kty)
+ return algorithmName.flatMap { handlers[$0] }
+ }
+ }
+
+ /// Maps JWK key type to algorithm name.
+ /// @param kty The JWK key type ("RSA", "EC", "OKP")
+ /// @return Algorithm name if recognized, nil otherwise
+ func getAlgorithmName(for kty: String) -> String? {
+ return lock.withLock {
+ keyTypeToAlgorithm(kty)
+ }
+ }
+
+ /// Registers a custom algorithm handler.
+ /// Can be used to register new handlers or override existing ones.
+ /// Thread-safe access with NSLock.
+ ///
+ /// @param handler The CryptoAlgorithmHandler implementation
+ /// @param algorithmName The algorithm name to register (e.g., "RSASSA-PKCS1-v1_5")
+ func register(_ handler: CryptoAlgorithmHandler, for algorithmName: String) {
+ lock.withLock {
+ handlers[algorithmName] = handler
+ }
+ }
+
+ /// Maps JWK key type to algorithm name.
+ /// @param kty The JWK key type ("RSA", "EC", "OKP")
+ /// @return Algorithm name or nil if not recognized
+ private func keyTypeToAlgorithm(_ kty: String) -> String? {
+ switch kty {
+ case "RSA": return "RSASSA-PKCS1-v1_5"
+ case "EC": return "ECDSA"
+ case "OKP": return "EdDSA"
+ default: return nil
+ }
+ }
+}
diff --git a/packages/react-native-webcrypto-bridge/ios/Sources/RNWebCryptoBridge/CryptoAlgorithmHandler.swift b/packages/react-native-webcrypto-bridge/ios/Sources/RNWebCryptoBridge/CryptoAlgorithmHandler.swift
new file mode 100644
index 0000000..695cb28
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/ios/Sources/RNWebCryptoBridge/CryptoAlgorithmHandler.swift
@@ -0,0 +1,41 @@
+import Security
+
+/// Specification for key generation, returned by algorithm handlers.
+struct KeyGenSpec {
+ /// SecKeyAttribute key type (e.g., kSecAttrKeyTypeRSA)
+ let keyType: CFString
+ /// Key size in bits
+ let keySize: Int
+}
+
+/// Protocol for algorithm-specific cryptographic operations.
+/// Implementations handle key generation specs, JWK export/import, and signature algorithm selection
+/// for specific algorithms (RSA, ECDSA, EdDSA, etc).
+protocol CryptoAlgorithmHandler {
+ /// Generates key generation specifications for this algorithm.
+ /// Validates algorithm parameters and returns the specs needed for SecKeyCreateRandomKey.
+ ///
+ /// @param params Dictionary containing algorithm parameters (e.g., "modulusLength" for RSA)
+ /// @return KeyGenSpec with keyType and keySize
+ /// @throws NSError if parameters are invalid or unsupported for this algorithm
+ func generateKeySpec(_ params: [String: Any]) throws -> KeyGenSpec
+
+ /// Returns the SecKeyAlgorithm constant for this algorithm's signing/verification operations.
+ /// @return SecKeyAlgorithm (e.g., .rsaSignatureMessagePKCS1v15SHA256 for RSA)
+ func getSignatureAlgorithm() -> SecKeyAlgorithm
+
+ /// Exports a public key to JWK (JSON Web Key) format.
+ /// Handles algorithm-specific JWK fields (e.g., "n", "e" for RSA; "x", "y" for EC).
+ ///
+ /// @param publicKey The SecKey to export (can be nil, key components contain the data)
+ /// @param keyComponents RSAPublicKeyComponents containing modulus and exponent
+ /// @return Dictionary with JWK representation including "kty", algorithm identifier, and key components
+ func exportToJWK(publicKey: SecKey?, keyComponents: RSAPublicKeyComponents) -> [String: Any]
+
+ /// Imports a public key from JWK (JSON Web Key) format.
+ /// Validates JWK structure and extracts algorithm-specific components.
+ ///
+ /// @param jwk Dictionary representation of a JWK
+ /// @return RSAPublicKeyComponents if parsing succeeds, nil otherwise
+ func importFromJWK(_ jwk: [String: Any]) -> RSAPublicKeyComponents?
+}
diff --git a/packages/react-native-webcrypto-bridge/ios/Sources/RNWebCryptoBridge/CryptoExtensions.swift b/packages/react-native-webcrypto-bridge/ios/Sources/RNWebCryptoBridge/CryptoExtensions.swift
new file mode 100644
index 0000000..f1038d0
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/ios/Sources/RNWebCryptoBridge/CryptoExtensions.swift
@@ -0,0 +1,26 @@
+import Foundation
+
+extension String {
+ public var base64URLDecoded: String { convertToBase64URLDecoded() }
+
+ private func convertToBase64URLDecoded() -> String {
+ var result = replacingOccurrences(of: "-", with: "+")
+ .replacingOccurrences(of: "_", with: "/")
+
+ while result.count % 4 != 0 {
+ result.append(contentsOf: "=")
+ }
+
+ return result
+ }
+}
+
+extension Data {
+ public func base64URLEncodedString() -> String {
+ var base64 = self.base64EncodedString()
+ base64 = base64.replacingOccurrences(of: "+", with: "-")
+ base64 = base64.replacingOccurrences(of: "/", with: "_")
+ base64 = base64.replacingOccurrences(of: "=", with: "")
+ return base64
+ }
+}
diff --git a/packages/react-native-webcrypto-bridge/ios/Sources/RNWebCryptoBridge/RSAHandler.swift b/packages/react-native-webcrypto-bridge/ios/Sources/RNWebCryptoBridge/RSAHandler.swift
new file mode 100644
index 0000000..4ca03e5
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/ios/Sources/RNWebCryptoBridge/RSAHandler.swift
@@ -0,0 +1,65 @@
+import Security
+import Foundation
+
+/// RSA algorithm handler implementing RSASSA-PKCS1-v1_5 with SHA-256.
+class RSAHandler: CryptoAlgorithmHandler {
+ /// Generates key specs for RSA key generation.
+ /// Only supports 2048-bit RSA keys.
+ ///
+ /// @param params Dictionary with "modulusLength" key
+ /// @return KeyGenSpec with RSA key type and size
+ /// @throws NSError if modulusLength is not 2048
+ func generateKeySpec(_ params: [String: Any]) throws -> KeyGenSpec {
+ let modulusLength = (params["modulusLength"] as? NSNumber)?.intValue ?? 2048
+ if modulusLength != 2048 {
+ let error = NSError(
+ domain: "RSAHandler",
+ code: -1,
+ userInfo: [NSLocalizedDescriptionKey: "RSA: only 2048-bit keys are supported"]
+ )
+ throw error
+ }
+
+ return KeyGenSpec(keyType: kSecAttrKeyTypeRSA, keySize: 2048)
+ }
+
+ /// Returns the signature algorithm for RSA: SHA256 with RSA PKCS#1 v1.5 padding.
+ func getSignatureAlgorithm() -> SecKeyAlgorithm {
+ return .rsaSignatureMessagePKCS1v15SHA256
+ }
+
+ /// Exports an RSA public key to JWK format.
+ /// Creates a JWK dictionary with RSA-specific fields:
+ /// - "kty": "RSA" (key type)
+ /// - "alg": "RS256" (algorithm identifier)
+ /// - "n": Base64URL-encoded modulus
+ /// - "e": Base64URL-encoded public exponent
+ ///
+ /// @param publicKey The SecKey object (unused, components contain the key data)
+ /// @param keyComponents RSAPublicKeyComponents with modulus and exponent
+ /// @return Dictionary representation of RSA JWK
+ func exportToJWK(publicKey: SecKey?, keyComponents: RSAPublicKeyComponents) -> [String: Any] {
+ var jwk: [String: Any] = [:]
+ jwk["kty"] = "RSA"
+ jwk["alg"] = "RS256"
+ jwk["n"] = keyComponents.modulus.base64URLEncodedString()
+ jwk["e"] = keyComponents.exponent.base64URLEncodedString()
+ return jwk
+ }
+
+ /// Imports an RSA public key from JWK format.
+ /// Extracts and validates the "n" (modulus) and "e" (exponent) components.
+ ///
+ /// @param jwk Dictionary representation of RSA JWK
+ /// @return RSAPublicKeyComponents if valid, nil if parsing fails
+ func importFromJWK(_ jwk: [String: Any]) -> RSAPublicKeyComponents? {
+ guard let nString = jwk["n"] as? String,
+ let eString = jwk["e"] as? String,
+ let modulusData = Data(base64Encoded: nString.base64URLDecoded),
+ let exponentData = Data(base64Encoded: eString.base64URLDecoded) else {
+ return nil
+ }
+
+ return RSAPublicKeyComponents(modulus: modulusData, exponent: exponentData)
+ }
+}
diff --git a/packages/react-native-webcrypto-bridge/ios/Sources/RNWebCryptoBridge/RSAKeyUtils.swift b/packages/react-native-webcrypto-bridge/ios/Sources/RNWebCryptoBridge/RSAKeyUtils.swift
new file mode 100644
index 0000000..c7ce302
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/ios/Sources/RNWebCryptoBridge/RSAKeyUtils.swift
@@ -0,0 +1,183 @@
+import Foundation
+
+// MARK: - RSA Public Key Components
+
+/// Parsed components of an RSA public key, suitable for JWK serialization.
+struct RSAPublicKeyComponents {
+ /// The RSA modulus (`n`), with any ASN.1 leading-zero padding stripped.
+ let modulus: Data
+
+ /// The RSA public exponent (`e`), with any ASN.1 leading-zero padding stripped.
+ let exponent: Data
+
+ /// Parse components from a PKCS#1 `RSAPublicKey` DER structure:
+ /// ```
+ /// SEQUENCE {
+ /// INTEGER modulus
+ /// INTEGER exponent
+ /// }
+ /// ```
+ ///
+ /// - Parameter derData: The raw bytes returned by `SecKeyCopyExternalRepresentation`
+ /// for an RSA public key.
+ /// - Returns: `nil` if the data is not a valid PKCS#1 structure.
+ init?(derData: Data) {
+ let bytes = [UInt8](derData)
+ var offset = 0
+
+ // Expect SEQUENCE tag (0x30)
+ guard offset < bytes.count, bytes[offset] == 0x30 else { return nil }
+ offset += 1
+
+ // Skip SEQUENCE length
+ guard RSAKeyUtils.readDERLength(bytes: bytes, offset: &offset) != nil else { return nil }
+
+ // Read first INTEGER (modulus)
+ guard offset < bytes.count, bytes[offset] == 0x02 else { return nil }
+ offset += 1
+ guard let modulusLength = RSAKeyUtils.readDERLength(bytes: bytes, offset: &offset) else { return nil }
+ guard offset + modulusLength <= bytes.count else { return nil }
+
+ var modulusBytes = Array(bytes[offset..<(offset + modulusLength)])
+ offset += modulusLength
+
+ // Strip leading zero byte used for ASN.1 sign encoding
+ if modulusBytes.first == 0x00 && modulusBytes.count > 1 {
+ modulusBytes.removeFirst()
+ }
+
+ // Read second INTEGER (exponent)
+ guard offset < bytes.count, bytes[offset] == 0x02 else { return nil }
+ offset += 1
+ guard let exponentLength = RSAKeyUtils.readDERLength(bytes: bytes, offset: &offset) else { return nil }
+ guard offset + exponentLength <= bytes.count else { return nil }
+
+ var exponentBytes = Array(bytes[offset..<(offset + exponentLength)])
+
+ // Strip leading zero byte used for ASN.1 sign encoding
+ if exponentBytes.first == 0x00 && exponentBytes.count > 1 {
+ exponentBytes.removeFirst()
+ }
+
+ self.modulus = Data(modulusBytes)
+ self.exponent = Data(exponentBytes)
+ }
+
+ /// Create components directly from raw modulus and exponent data.
+ init(modulus: Data, exponent: Data) {
+ self.modulus = modulus
+ self.exponent = exponent
+ }
+
+ // MARK: - DER Serialization
+
+ /// Construct a PKCS#1 `RSAPublicKey` DER structure from this key's components.
+ ///
+ /// The resulting `Data` can be passed directly to `SecKeyCreateWithData` with
+ /// `kSecAttrKeyTypeRSA` / `kSecAttrKeyClassPublic` attributes.
+ var derData: Data {
+ var modulusBytes = [UInt8](modulus)
+ let exponentBytes = [UInt8](exponent)
+
+ // Ensure modulus has a leading 0x00 if MSB is set (ASN.1 sign bit)
+ if let first = modulusBytes.first, first & 0x80 != 0 {
+ modulusBytes.insert(0x00, at: 0)
+ }
+
+ let modulusLengthOctets = RSAKeyUtils.encodeDERLength(modulusBytes.count)
+ let exponentLengthOctets = RSAKeyUtils.encodeDERLength(exponentBytes.count)
+
+ // +1 per INTEGER accounts for the tag byte (0x02)
+ let contentLength = 1 + modulusLengthOctets.count + modulusBytes.count
+ + 1 + exponentLengthOctets.count + exponentBytes.count
+ let sequenceLengthOctets = RSAKeyUtils.encodeDERLength(contentLength)
+
+ var result = Data()
+ result.reserveCapacity(1 + sequenceLengthOctets.count + contentLength)
+
+ // SEQUENCE tag and length
+ result.append(0x30)
+ result.append(contentsOf: sequenceLengthOctets)
+
+ // INTEGER tag, length, and modulus
+ result.append(0x02)
+ result.append(contentsOf: modulusLengthOctets)
+ result.append(contentsOf: modulusBytes)
+
+ // INTEGER tag, length, and exponent
+ result.append(0x02)
+ result.append(contentsOf: exponentLengthOctets)
+ result.append(contentsOf: exponentBytes)
+
+ return result
+ }
+
+ /// The key size in bits, derived from the modulus length.
+ var keySizeInBits: Int {
+ modulus.count * 8
+ }
+}
+
+// MARK: - RSA Key Utilities
+
+/// Pure-Swift utilities for converting between PKCS#1 DER-encoded RSA public keys
+/// and their individual components (modulus + exponent).
+///
+/// Apple's Security framework (`SecKeyCopyExternalRepresentation` / `SecKeyCreateWithData`)
+/// operates on raw PKCS#1 DER blobs but provides no API to extract or inject individual
+/// components. These utilities bridge that gap for JWK ↔ SecKey conversion.
+enum RSAKeyUtils {
+
+ // MARK: - DER Length Encoding/Decoding
+
+ /// Read a DER length field from a byte array, advancing `offset` past the length bytes.
+ ///
+ /// Supports both short-form (single byte < 128) and long-form lengths.
+ /// Returns `nil` for invalid or indefinite-length encodings.
+ static func readDERLength(bytes: [UInt8], offset: inout Int) -> Int? {
+ guard offset < bytes.count else { return nil }
+ let first = bytes[offset]
+ offset += 1
+
+ if first < 0x80 {
+ // Short form: length is the byte value itself
+ return Int(first)
+ } else if first == 0x80 {
+ // Indefinite length — not valid for DER
+ return nil
+ } else {
+ // Long form: lower 7 bits indicate number of subsequent length bytes
+ let numLengthBytes = Int(first & 0x7F)
+ guard offset + numLengthBytes <= bytes.count else { return nil }
+
+ var length = 0
+ for i in 0.. [UInt8] {
+ if length < 128 {
+ return [UInt8(length)]
+ }
+
+ // Determine how many bytes are needed to represent `length`
+ let byteCount = (length / 256) + 1
+ var remaining = length
+ var result: [UInt8] = [UInt8(byteCount + 0x80)]
+
+ for _ in 0..> 8
+ }
+
+ return result
+ }
+}
diff --git a/packages/react-native-webcrypto-bridge/ios/Sources/RNWebCryptoBridge/WebCryptoBridge.h b/packages/react-native-webcrypto-bridge/ios/Sources/RNWebCryptoBridge/WebCryptoBridge.h
new file mode 100644
index 0000000..e5ffe7c
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/ios/Sources/RNWebCryptoBridge/WebCryptoBridge.h
@@ -0,0 +1,11 @@
+#import
+
+#ifdef RCT_NEW_ARCH_ENABLED
+#import "RNWebCryptoBridge.h"
+
+@interface WebCryptoBridge : NSObject
+#else
+@interface WebCryptoBridge : NSObject
+#endif
+
+@end
diff --git a/packages/react-native-webcrypto-bridge/ios/Sources/RNWebCryptoBridge/WebCryptoBridge.swift b/packages/react-native-webcrypto-bridge/ios/Sources/RNWebCryptoBridge/WebCryptoBridge.swift
new file mode 100644
index 0000000..13458ad
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/ios/Sources/RNWebCryptoBridge/WebCryptoBridge.swift
@@ -0,0 +1,446 @@
+import Foundation
+import Security
+import CommonCrypto
+import React
+
+// MARK: - Key Storage Models
+
+/// Enum representing where and how a cryptographic key is stored.
+/// Differentiates between generated keys and imported public keys.
+enum CryptoKeyEntry {
+ /// Key stored in Secure Enclave (generated, non-extractable)
+ case keystore(publicKey: SecKey, privateKey: SecKey?)
+
+ /// Key stored in memory (imported from JWK, extractable)
+ case platform(key: SecKey, algorithmName: String)
+}
+
+/// Type-safe representation of a cryptographic key metadata.
+/// Encapsulates key information following the WebCrypto API model.
+struct CryptoKey {
+ /// The key algorithm (e.g., {"name": "RSASSA-PKCS1-v1_5", "modulusLength": 2048})
+ let algorithm: [String: Any]
+
+ /// Key type: "private", "public", or "secret"
+ let type: String
+
+ /// Whether the key can be exported
+ let extractable: Bool
+
+ /// Permitted operations: "sign", "verify", "encrypt", "decrypt", etc.
+ let usages: [String]
+
+ /// Storage location and access method for the key
+ let entry: CryptoKeyEntry
+}
+
+// MARK: - WebCryptoBridge
+
+@objc(WebCryptoBridge)
+class WebCryptoBridge: NSObject {
+
+ // Key storage — maps keyId to CryptoKey with full metadata
+ private static var keyStore: [String: CryptoKey] = [:]
+ private static let keyStoreLock = NSLock()
+
+ @objc
+ static func requiresMainQueueSetup() -> Bool {
+ return false
+ }
+
+ @objc
+ static func moduleName() -> String! {
+ return "WebCryptoBridge"
+ }
+
+ @objc
+ func constantsToExport() -> [AnyHashable: Any]! {
+ return [:]
+ }
+
+ // MARK: - Helper Methods
+
+ /// Decode a standard Base64 string to Data.
+ private func base64ToData(_ base64: String) -> Data? {
+ return Data(base64Encoded: base64)
+ }
+
+ /// Encode Data to a standard Base64 string.
+ private func dataToBase64(_ data: Data) -> String {
+ return data.base64EncodedString()
+ }
+
+ // MARK: - Synchronous Methods
+
+ @objc(getRandomValues:)
+ func getRandomValues(_ length: Double) -> String {
+ let len = Int(length)
+ var randomData = Data(count: len)
+
+ let result = randomData.withUnsafeMutableBytes { bytes -> Int32 in
+ guard let baseAddress = bytes.baseAddress else {
+ return errSecParam
+ }
+ return SecRandomCopyBytes(kSecRandomDefault, len, baseAddress)
+ }
+
+ if result != errSecSuccess {
+ // SecRandomCopyBytes failure indicates a fundamentally broken system RNG.
+ // Returning empty/zero data would silently produce weak randomness, which
+ // is a critical security failure. Crash explicitly rather than risk it.
+ fatalError("WebCryptoBridge: SecRandomCopyBytes failed with status \(result). The system CSPRNG is unavailable.")
+ }
+
+ return dataToBase64(randomData)
+ }
+
+ @objc
+ func randomUUID() -> String {
+ return UUID().uuidString.lowercased()
+ }
+
+ // MARK: - Async Methods
+
+ @objc(digest:data:resolve:reject:)
+ func digest(
+ _ algorithm: String,
+ data: String,
+ resolve: @escaping RCTPromiseResolveBlock,
+ reject: @escaping RCTPromiseRejectBlock
+ ) {
+ guard algorithm == "SHA-256" else {
+ reject("unsupported_algorithm", "Only SHA-256 is supported", nil)
+ return
+ }
+
+ guard let inputData = base64ToData(data) else {
+ reject("invalid_input", "Invalid Base64 input data", nil)
+ return
+ }
+
+ var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
+ inputData.withUnsafeBytes { bytes in
+ _ = CC_SHA256(bytes.baseAddress, CC_LONG(inputData.count), &hash)
+ }
+
+ let hashData = Data(hash)
+ resolve(dataToBase64(hashData))
+ }
+
+ @objc(generateKey:extractable:keyUsages:resolve:reject:)
+ func generateKey(
+ _ algorithmJson: String,
+ extractable: Bool,
+ keyUsages: [String],
+ resolve: @escaping RCTPromiseResolveBlock,
+ reject: @escaping RCTPromiseRejectBlock
+ ) {
+ guard let algorithmData = algorithmJson.data(using: .utf8),
+ let algorithm = try? JSONSerialization.jsonObject(with: algorithmData) as? [String: Any],
+ let algorithmName = algorithm["name"] as? String else {
+ reject("invalid_algorithm", "Invalid algorithm JSON", nil)
+ return
+ }
+
+ guard let handler = AlgorithmRegistry.shared.getHandler(for: algorithmName) else {
+ reject("unsupported_algorithm", "Algorithm not supported: \(algorithmName)", nil)
+ return
+ }
+
+ // Handler generates the key specs for this algorithm
+ let keyGenSpec: KeyGenSpec
+ do {
+ keyGenSpec = try handler.generateKeySpec(algorithm)
+ } catch let error as NSError {
+ reject("invalid_key_parameters", error.localizedDescription, error)
+ return
+ }
+
+ let attributes: [String: Any] = [
+ kSecAttrKeyType as String: keyGenSpec.keyType,
+ kSecAttrKeySizeInBits as String: keyGenSpec.keySize,
+ kSecPrivateKeyAttrs as String: [
+ kSecAttrIsPermanent as String: false
+ ],
+ kSecPublicKeyAttrs as String: [
+ kSecAttrIsPermanent as String: false
+ ]
+ ]
+
+ var error: Unmanaged?
+ guard let privateKey = SecKeyCreateRandomKey(attributes as CFDictionary, &error) else {
+ let err = error?.takeRetainedValue()
+ reject("key_generation_failed", err?.localizedDescription ?? "Unknown error", err)
+ return
+ }
+
+ guard let publicKey = SecKeyCopyPublicKey(privateKey) else {
+ reject("key_generation_failed", "Failed to get public key", nil)
+ return
+ }
+
+ let keyId = UUID().uuidString
+
+ Self.keyStoreLock.withLock {
+ Self.keyStore[keyId] = CryptoKey(
+ algorithm: algorithm,
+ type: "private",
+ extractable: extractable,
+ usages: keyUsages,
+ entry: .keystore(publicKey: publicKey, privateKey: privateKey)
+ )
+ }
+
+ let result = ["id": keyId]
+ if let jsonData = try? JSONSerialization.data(withJSONObject: result),
+ let jsonString = String(data: jsonData, encoding: .utf8) {
+ resolve(jsonString)
+ } else {
+ reject("serialization_failed", "Failed to serialize result", nil)
+ }
+ }
+
+ @objc(exportKey:keyId:resolve:reject:)
+ func exportKey(
+ _ format: String,
+ keyId: String,
+ resolve: @escaping RCTPromiseResolveBlock,
+ reject: @escaping RCTPromiseRejectBlock
+ ) {
+ guard format == "jwk" else {
+ reject("unsupported_format", "Only JWK format is supported", nil)
+ return
+ }
+
+ let cryptoKey = Self.keyStoreLock.withLock {
+ Self.keyStore[keyId]
+ }
+
+ guard let cryptoKey = cryptoKey else {
+ reject("key_not_found", "Key not found", nil)
+ return
+ }
+
+ let key: SecKey
+ switch cryptoKey.entry {
+ case .keystore(let publicKey, let privateKey):
+ if cryptoKey.type == "public" {
+ key = publicKey
+ } else {
+ guard let pk = privateKey else {
+ reject("key_not_found", "Private key not available for this key", nil)
+ return
+ }
+ key = pk
+ }
+ case .platform(let platformKey, _):
+ key = platformKey
+ }
+
+ var error: Unmanaged?
+ guard let keyData = SecKeyCopyExternalRepresentation(key, &error) as Data? else {
+ let err = error?.takeRetainedValue()
+ reject("export_failed", err?.localizedDescription ?? "Export failed", err)
+ return
+ }
+
+ // For RSA, SecKeyCopyExternalRepresentation returns PKCS#1 RSAPublicKey:
+ // SEQUENCE { INTEGER modulus, INTEGER exponent }
+ guard let components = RSAPublicKeyComponents(derData: keyData) else {
+ reject("export_failed", "Failed to parse RSA public key components", nil)
+ return
+ }
+
+ // Use handler to build JWK
+ let handler = RSAHandler()
+ let jwk = handler.exportToJWK(publicKey: key, keyComponents: components)
+
+ if let jsonData = try? JSONSerialization.data(withJSONObject: jwk),
+ let jsonString = String(data: jsonData, encoding: .utf8) {
+ resolve(jsonString)
+ } else {
+ reject("serialization_failed", "Failed to serialize JWK", nil)
+ }
+ }
+
+ @objc(importKey:keyData:algorithm:extractable:keyUsages:resolve:reject:)
+ func importKey(
+ _ format: String,
+ keyData: String,
+ algorithm: String,
+ extractable: Bool,
+ keyUsages: [String],
+ resolve: @escaping RCTPromiseResolveBlock,
+ reject: @escaping RCTPromiseRejectBlock
+ ) {
+ guard format == "jwk" else {
+ reject("unsupported_format", "Only JWK format is supported", nil)
+ return
+ }
+
+ guard let jwkData = keyData.data(using: .utf8),
+ let jwk = try? JSONSerialization.jsonObject(with: jwkData) as? [String: Any],
+ let kty = jwk["kty"] as? String else {
+ reject("invalid_jwk", "Invalid JWK format", nil)
+ return
+ }
+
+ // Use handler to import from JWK
+ guard let handler = AlgorithmRegistry.shared.getHandlerByKeyType(kty) else {
+ reject("unsupported_key_type", "Key type not supported: \(kty)", nil)
+ return
+ }
+
+ guard let components = handler.importFromJWK(jwk) else {
+ reject("invalid_jwk", "Invalid JWK components", nil)
+ return
+ }
+
+ let keyId = UUID().uuidString
+ let publicKeyData = components.derData
+
+ // Key size is derived from the modulus
+ let keySizeInBits = components.keySizeInBits
+
+ var error: Unmanaged?
+ let attributes: [CFString: Any] = [
+ kSecAttrKeyType: kSecAttrKeyTypeRSA,
+ kSecAttrKeyClass: kSecAttrKeyClassPublic,
+ kSecAttrKeySizeInBits: NSNumber(value: keySizeInBits)
+ ]
+
+ guard let publicKey = SecKeyCreateWithData(publicKeyData as NSData, attributes as NSDictionary, &error) else {
+ let err = error?.takeRetainedValue()
+ reject("import_failed", err?.localizedDescription ?? "Import failed", err)
+ return
+ }
+
+ Self.keyStoreLock.withLock {
+ let algorithmDict = (try? JSONSerialization.jsonObject(with: algorithm.data(using: .utf8)!) as? [String: Any]) ?? [:]
+ Self.keyStore[keyId] = CryptoKey(
+ algorithm: algorithmDict,
+ type: "public",
+ extractable: extractable,
+ usages: keyUsages,
+ entry: .platform(key: publicKey, algorithmName: keyTypeToAlgorithmName(kty))
+ )
+ }
+
+ resolve(keyId)
+ }
+
+ /// Maps JWK key type to algorithm name.
+ private func keyTypeToAlgorithmName(_ kty: String) -> String {
+ switch kty {
+ case "RSA": return "RSASSA-PKCS1-v1_5"
+ case "EC": return "ECDSA"
+ case "OKP": return "EdDSA"
+ default: return "unknown"
+ }
+ }
+
+ @objc(sign:keyId:data:resolve:reject:)
+ func sign(
+ _ algorithmJson: String,
+ keyId: String,
+ data: String,
+ resolve: @escaping RCTPromiseResolveBlock,
+ reject: @escaping RCTPromiseRejectBlock
+ ) {
+ let cryptoKey = Self.keyStoreLock.withLock {
+ Self.keyStore[keyId]
+ }
+
+ guard let cryptoKey = cryptoKey else {
+ reject("key_not_found", "Key not found", nil)
+ return
+ }
+
+ guard case .keystore(_, let privateKey) = cryptoKey.entry,
+ let pk = privateKey else {
+ reject("key_not_found", "Private key not available for this key", nil)
+ return
+ }
+
+ guard let inputData = base64ToData(data) else {
+ reject("invalid_input", "Invalid Base64 input data", nil)
+ return
+ }
+
+ // Use handler to get signature algorithm
+ let handler = RSAHandler()
+ let signatureAlgorithm = handler.getSignatureAlgorithm()
+
+ var error: Unmanaged?
+ guard let signature = SecKeyCreateSignature(
+ pk,
+ signatureAlgorithm,
+ inputData as CFData,
+ &error
+ ) as Data? else {
+ let err = error?.takeRetainedValue()
+ reject("signing_failed", err?.localizedDescription ?? "Signing failed", err)
+ return
+ }
+
+ resolve(dataToBase64(signature))
+ }
+
+ @objc(verify:keyId:signature:data:resolve:reject:)
+ func verify(
+ _ algorithmJson: String,
+ keyId: String,
+ signature: String,
+ data: String,
+ resolve: @escaping RCTPromiseResolveBlock,
+ reject: @escaping RCTPromiseRejectBlock
+ ) {
+ let cryptoKey = Self.keyStoreLock.withLock {
+ Self.keyStore[keyId]
+ }
+
+ guard let cryptoKey = cryptoKey else {
+ reject("key_not_found", "Public key not found", nil)
+ return
+ }
+
+ guard let inputData = base64ToData(data) else {
+ reject("invalid_input", "Invalid Base64 input data", nil)
+ return
+ }
+
+ guard let signatureData = base64ToData(signature) else {
+ reject("invalid_input", "Invalid Base64 signature data", nil)
+ return
+ }
+
+ let publicKey: SecKey
+ switch cryptoKey.entry {
+ case .keystore(let pubKey, _):
+ publicKey = pubKey
+ case .platform(let key, _):
+ publicKey = key
+ }
+
+ // Use handler to get signature algorithm
+ let handler = RSAHandler()
+ let signatureAlgorithm = handler.getSignatureAlgorithm()
+
+ var error: Unmanaged?
+ let verified = SecKeyVerifySignature(
+ publicKey,
+ signatureAlgorithm,
+ inputData as CFData,
+ signatureData as CFData,
+ &error
+ )
+
+ if let err = error?.takeRetainedValue() {
+ reject("verification_failed", err.localizedDescription, err as Error)
+ return
+ }
+
+ resolve(verified)
+ }
+
+}
diff --git a/packages/react-native-webcrypto-bridge/ios/Sources/RNWebCryptoBridge/WebCryptoBridgeModule.m b/packages/react-native-webcrypto-bridge/ios/Sources/RNWebCryptoBridge/WebCryptoBridgeModule.m
new file mode 100644
index 0000000..328b3b1
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/ios/Sources/RNWebCryptoBridge/WebCryptoBridgeModule.m
@@ -0,0 +1,46 @@
+#import
+
+@interface RCT_EXTERN_MODULE(WebCryptoBridge, NSObject)
+
+RCT_EXTERN_METHOD(digest:(NSString *)algorithm
+ data:(NSString *)data
+ resolve:(RCTPromiseResolveBlock)resolve
+ reject:(RCTPromiseRejectBlock)reject)
+
+RCT_EXTERN_METHOD(generateKey:(NSString *)algorithm
+ extractable:(BOOL)extractable
+ keyUsages:(NSArray *)keyUsages
+ resolve:(RCTPromiseResolveBlock)resolve
+ reject:(RCTPromiseRejectBlock)reject)
+
+RCT_EXTERN_METHOD(exportKey:(NSString *)format
+ keyId:(NSString *)keyId
+ resolve:(RCTPromiseResolveBlock)resolve
+ reject:(RCTPromiseRejectBlock)reject)
+
+RCT_EXTERN_METHOD(importKey:(NSString *)format
+ keyData:(NSString *)keyData
+ algorithm:(NSString *)algorithm
+ extractable:(BOOL)extractable
+ keyUsages:(NSArray *)keyUsages
+ resolve:(RCTPromiseResolveBlock)resolve
+ reject:(RCTPromiseRejectBlock)reject)
+
+RCT_EXTERN_METHOD(sign:(NSString *)algorithm
+ keyId:(NSString *)keyId
+ data:(NSString *)data
+ resolve:(RCTPromiseResolveBlock)resolve
+ reject:(RCTPromiseRejectBlock)reject)
+
+RCT_EXTERN_METHOD(verify:(NSString *)algorithm
+ keyId:(NSString *)keyId
+ signature:(NSString *)signature
+ data:(NSString *)data
+ resolve:(RCTPromiseResolveBlock)resolve
+ reject:(RCTPromiseRejectBlock)reject)
+
+RCT_EXTERN_METHOD(getRandomValues:(double)length)
+
+RCT_EXTERN_METHOD(randomUUID)
+
+@end
diff --git a/packages/react-native-webcrypto-bridge/ios/Tests/RNWebCryptoBridgeTests/AlgorithmRegistryTests.swift b/packages/react-native-webcrypto-bridge/ios/Tests/RNWebCryptoBridgeTests/AlgorithmRegistryTests.swift
new file mode 100644
index 0000000..60f3bd1
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/ios/Tests/RNWebCryptoBridgeTests/AlgorithmRegistryTests.swift
@@ -0,0 +1,165 @@
+import XCTest
+@testable import RNWebCryptoBridge
+
+class AlgorithmRegistryTests: XCTestCase {
+
+ func testGetHandler_returnsRegisteredHandler() {
+ let handler = AlgorithmRegistry.shared.getHandler(for: "RSASSA-PKCS1-v1_5")
+ XCTAssertNotNil(handler)
+ XCTAssertTrue(handler is RSAHandler)
+ }
+
+ func testGetHandler_returnsNilForUnregisteredAlgorithm() {
+ let handler = AlgorithmRegistry.shared.getHandler(for: "NONEXISTENT")
+ XCTAssertNil(handler)
+ }
+
+ func testGetHandlerByKeyType_RSA_returnsHandler() {
+ let handler = AlgorithmRegistry.shared.getHandlerByKeyType("RSA")
+ XCTAssertNotNil(handler)
+ XCTAssertTrue(handler is RSAHandler)
+ }
+
+ func testGetHandlerByKeyType_EC_returnsNil() {
+ // EC handler not yet implemented
+ let handler = AlgorithmRegistry.shared.getHandlerByKeyType("EC")
+ XCTAssertNil(handler)
+ }
+
+ func testGetHandlerByKeyType_OKP_returnsNil() {
+ // EdDSA handler not yet implemented
+ let handler = AlgorithmRegistry.shared.getHandlerByKeyType("OKP")
+ XCTAssertNil(handler)
+ }
+
+ func testGetHandlerByKeyType_unknownType_returnsNil() {
+ let handler = AlgorithmRegistry.shared.getHandlerByKeyType("UNKNOWN")
+ XCTAssertNil(handler)
+ }
+
+ func testThreadSafety_concurrentAccess() {
+ let group = DispatchGroup()
+ let iterations = 100
+
+ for _ in 0.. {
+ var value: T?
+ var error: Error?
+ var errorCode: String?
+ var errorMessage: String?
+ var isResolved = false
+ var isRejected = false
+ let expectation: XCTestExpectation
+
+ init(description: String) {
+ self.expectation = XCTestExpectation(description: description)
+ }
+
+ /// Capture a successful resolve
+ func captureResolve(_ value: Any?) {
+ self.value = value as? T
+ self.isResolved = true
+ self.isRejected = false
+ expectation.fulfill()
+ }
+
+ /// Capture a rejection with error code and message
+ func captureReject(code: String?, message: String?, error: Error?) {
+ self.errorCode = code
+ self.errorMessage = message
+ self.error = error
+ self.isRejected = true
+ self.isResolved = false
+ expectation.fulfill()
+ }
+}
+
+/// Test utilities for algorithm operations
+class IntegrationTestHelpers {
+
+ /// Helper to convert String to JSON dictionary
+ static func parseJSON(_ jsonString: String) -> [String: Any]? {
+ guard let data = jsonString.data(using: .utf8),
+ let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
+ return nil
+ }
+ return json
+ }
+
+ /// Helper to convert dictionary to JSON string
+ static func toJSON(_ dict: [String: Any]) -> String? {
+ guard let data = try? JSONSerialization.data(withJSONObject: dict),
+ let json = String(data: data, encoding: .utf8) else {
+ return nil
+ }
+ return json
+ }
+
+ /// Create RSA algorithm spec JSON
+ static func rsaAlgorithm(_ modulusLength: Int = 2048) -> String {
+ return """
+ {"name":"RSASSA-PKCS1-v1_5","modulusLength":\(modulusLength)}
+ """
+ }
+
+ /// Create signature algorithm spec JSON
+ static func signatureAlgorithm() -> String {
+ return """
+ {"name":"RSASSA-PKCS1-v1_5"}
+ """
+ }
+
+ /// Create digest algorithm spec JSON
+ static func digestAlgorithm(_ algorithm: String = "SHA-256") -> String {
+ return """
+ {"name":"\(algorithm)"}
+ """
+ }
+
+ /// Encode data to base64
+ static func toBase64(_ data: Data) -> String {
+ return data.base64EncodedString()
+ }
+
+ /// Encode string to base64
+ static func toBase64(_ string: String) -> String {
+ guard let data = string.data(using: .utf8) else { return "" }
+ return data.base64EncodedString()
+ }
+
+ /// Decode base64 to data
+ static func fromBase64(_ base64String: String) -> Data? {
+ return Data(base64Encoded: base64String)
+ }
+}
+
+/// Mock key store for testing
+class MockKeyStore {
+ static let shared = MockKeyStore()
+ private var keys: [String: MockCryptoKey] = [:]
+
+ private init() {}
+
+ func store(_ key: MockCryptoKey, withId id: String) {
+ keys[id] = key
+ }
+
+ func retrieve(withId id: String) -> MockCryptoKey? {
+ return keys[id]
+ }
+
+ func clear() {
+ keys.removeAll()
+ }
+
+ func getAllKeyIds() -> [String] {
+ return Array(keys.keys)
+ }
+}
+
+/// Mock cryptographic key for testing
+struct MockCryptoKey {
+ let id: String
+ let algorithm: [String: Any]
+ let keyType: String // "private" or "public"
+ let keyUsages: [String]
+ let extractable: Bool
+ var keyData: [String: String] = [:] // For JWK data (n, e, etc.)
+}
+
+/// Test data generator
+class TestDataGenerator {
+
+ /// Generate random bytes
+ static func randomBytes(count: Int) -> Data {
+ var bytes = [UInt8](repeating: 0, count: count)
+ _ = SecRandomCopyBytes(kSecRandomDefault, count, &bytes)
+ return Data(bytes)
+ }
+
+ /// Generate a test RSA JWK (public key only)
+ static func testRSAPublicJWK() -> String {
+ return """
+ {
+ "kty":"RSA",
+ "alg":"RS256",
+ "n":"0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw",
+ "e":"AQAB"
+ }
+ """
+ }
+
+ /// Generate test data for signing
+ static func testDataForSigning() -> String {
+ return IntegrationTestHelpers.toBase64("test message for signing")
+ }
+}
diff --git a/packages/react-native-webcrypto-bridge/ios/Tests/RNWebCryptoBridgeTests/Integration/JWKIntegrationTests.swift b/packages/react-native-webcrypto-bridge/ios/Tests/RNWebCryptoBridgeTests/Integration/JWKIntegrationTests.swift
new file mode 100644
index 0000000..b7bb699
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/ios/Tests/RNWebCryptoBridgeTests/Integration/JWKIntegrationTests.swift
@@ -0,0 +1,202 @@
+import XCTest
+@testable import RNWebCryptoBridge
+
+/// Integration tests for JWK export/import workflows
+/// Tests coordination between AlgorithmRegistry, RSAHandler, and key storage
+class JWKIntegrationTests: WebCryptoBridgeIntegrationTestCase {
+
+ func testExportKey_publicKey_producesValidJWK() {
+ guard let handler = getHandlerByAlgorithm( "RSASSA-PKCS1-v1_5") as? RSAHandler else {
+ XCTFail("RSA handler not found")
+ return
+ }
+
+ // Create test RSA key components
+ let modulus = Data("test_modulus_data".utf8)
+ let exponent = Data("65537".utf8)
+ let keyComponents = RSAPublicKeyComponents(modulus: modulus, exponent: exponent)
+
+ // Export to JWK
+ let jwk = handler.exportToJWK(publicKey: nil as SecKey?, keyComponents: keyComponents)
+
+ // Validate JWK structure
+ assertValidRSAJWK(jwk)
+ XCTAssertNotNil(jwk["n"] as? String)
+ XCTAssertNotNil(jwk["e"] as? String)
+ }
+
+ func testExportKey_unknownKeyId_fails() {
+ let keyId = "nonexistent-key-id"
+ let storedKey = getStoredKey(withId: keyId)
+ XCTAssertNil(storedKey)
+ }
+
+ func testImportKey_validJWK_succeeds() {
+ guard let handler = getHandlerByAlgorithm( "RSASSA-PKCS1-v1_5") as? RSAHandler else {
+ XCTFail("RSA handler not found")
+ return
+ }
+
+ let testJWK: [String: Any] = [
+ "kty": "RSA",
+ "alg": "RS256",
+ "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw",
+ "e": "AQAB"
+ ]
+
+ let components = handler.importFromJWK(testJWK)
+ XCTAssertNotNil(components)
+ XCTAssertNotNil(components?.modulus)
+ XCTAssertNotNil(components?.exponent)
+ }
+
+ func testImportKey_missingModulus_fails() {
+ guard let handler = getHandlerByAlgorithm( "RSASSA-PKCS1-v1_5") as? RSAHandler else {
+ XCTFail("RSA handler not found")
+ return
+ }
+
+ let incompleteJWK: [String: Any] = [
+ "kty": "RSA",
+ "e": "AQAB"
+ ]
+
+ let components = handler.importFromJWK(incompleteJWK)
+ XCTAssertNil(components)
+ }
+
+ func testImportKey_missingExponent_fails() {
+ guard let handler = getHandlerByAlgorithm( "RSASSA-PKCS1-v1_5") as? RSAHandler else {
+ XCTFail("RSA handler not found")
+ return
+ }
+
+ let incompleteJWK: [String: Any] = [
+ "kty": "RSA",
+ "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw"
+ ]
+
+ let components = handler.importFromJWK(incompleteJWK)
+ XCTAssertNil(components)
+ }
+
+ func testRoundTrip_exportThenImport() {
+ guard let handler = getHandlerByAlgorithm( "RSASSA-PKCS1-v1_5") as? RSAHandler else {
+ XCTFail("RSA handler not found")
+ return
+ }
+
+ // Create original components
+ let originalModulus = Data("test_modulus".utf8)
+ let originalExponent = Data("65537".utf8)
+ let originalComponents = RSAPublicKeyComponents(
+ modulus: originalModulus,
+ exponent: originalExponent
+ )
+
+ // Export to JWK
+ let jwk = handler.exportToJWK(publicKey: nil as SecKey?, keyComponents: originalComponents)
+
+ // Import back
+ guard let reimportedComponents = handler.importFromJWK(jwk) else {
+ XCTFail("Failed to import JWK")
+ return
+ }
+
+ // Verify components match
+ XCTAssertEqual(reimportedComponents.modulus, originalModulus)
+ XCTAssertEqual(reimportedComponents.exponent, originalExponent)
+ }
+
+ func testJWK_multipleRoundTrips() {
+ guard let handler = getHandlerByAlgorithm( "RSASSA-PKCS1-v1_5") as? RSAHandler else {
+ XCTFail("RSA handler not found")
+ return
+ }
+
+ let testCases: [(modulus: Data, exponent: Data)] = [
+ (Data([0x12, 0x34]), Data([0x65, 0x37])),
+ (Data("test".utf8), Data("exponent".utf8)),
+ (Data(repeating: 0xFF, count: 32), Data([0x01, 0x00, 0x01]))
+ ]
+
+ for testCase in testCases {
+ let original = RSAPublicKeyComponents(
+ modulus: testCase.modulus,
+ exponent: testCase.exponent
+ )
+
+ // First export/import cycle
+ let jwk1 = handler.exportToJWK(publicKey: nil as SecKey?, keyComponents: original)
+ guard let reimported1 = handler.importFromJWK(jwk1) else {
+ XCTFail("Failed first import cycle")
+ return
+ }
+
+ // Second export/import cycle
+ let jwk2 = handler.exportToJWK(publicKey: nil as SecKey?, keyComponents: reimported1)
+ guard let reimported2 = handler.importFromJWK(jwk2) else {
+ XCTFail("Failed second import cycle")
+ return
+ }
+
+ // Verify stability
+ XCTAssertEqual(reimported1.modulus, reimported2.modulus)
+ XCTAssertEqual(reimported1.exponent, reimported2.exponent)
+ }
+ }
+
+ func testJWK_structure_validation() {
+ guard let handler = getHandlerByAlgorithm( "RSASSA-PKCS1-v1_5") as? RSAHandler else {
+ XCTFail("RSA handler not found")
+ return
+ }
+
+ let modulus = Data("modulus".utf8)
+ let exponent = Data("exp".utf8)
+ let components = RSAPublicKeyComponents(modulus: modulus, exponent: exponent)
+
+ let jwk = handler.exportToJWK(publicKey: nil as SecKey?, keyComponents: components)
+
+ // Validate structure
+ XCTAssertEqual(jwk["kty"] as? String, "RSA")
+ XCTAssertEqual(jwk["alg"] as? String, "RS256")
+ XCTAssertNotNil(jwk["n"])
+ XCTAssertNotNil(jwk["e"])
+ XCTAssertNil(jwk["d"]) // Private exponent should not be present
+ }
+
+ func testKeyStorage_storeAndRetrieveJWK() {
+ let keyId = UUID().uuidString
+ let algorithm: [String: Any] = [
+ "name": "RSASSA-PKCS1-v1_5",
+ "modulusLength": 2048
+ ]
+
+ var key = MockCryptoKey(
+ id: keyId,
+ algorithm: algorithm,
+ keyType: "public",
+ keyUsages: ["verify"],
+ extractable: true
+ )
+
+ // Store JWK components
+ key.keyData = [
+ "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw",
+ "e": "AQAB"
+ ]
+
+ mockKeyStore.store(key, withId: keyId)
+
+ // Retrieve and validate
+ guard let storedKey = getStoredKey(withId: keyId) else {
+ XCTFail("Failed to retrieve stored key")
+ return
+ }
+
+ XCTAssertEqual(storedKey.id, keyId)
+ XCTAssertNotNil(storedKey.keyData["n"])
+ XCTAssertNotNil(storedKey.keyData["e"])
+ }
+}
diff --git a/packages/react-native-webcrypto-bridge/ios/Tests/RNWebCryptoBridgeTests/Integration/KeyGenerationIntegrationTests.swift b/packages/react-native-webcrypto-bridge/ios/Tests/RNWebCryptoBridgeTests/Integration/KeyGenerationIntegrationTests.swift
new file mode 100644
index 0000000..48c05b8
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/ios/Tests/RNWebCryptoBridgeTests/Integration/KeyGenerationIntegrationTests.swift
@@ -0,0 +1,168 @@
+import XCTest
+@testable import RNWebCryptoBridge
+
+/// Integration tests for key generation workflows
+/// Tests coordination between AlgorithmRegistry and RSAHandler
+class KeyGenerationIntegrationTests: WebCryptoBridgeIntegrationTestCase {
+
+ func testGenerateKeySpec_validRSA2048_succeeds() {
+ let algorithmParams: [String: Any] = ["modulusLength": 2048]
+
+ // Get handler from registry
+ guard let handler = getHandlerByAlgorithm("RSASSA-PKCS1-v1_5") as? RSAHandler else {
+ XCTFail("RSA handler not found in registry")
+ return
+ }
+
+ // Generate key spec
+ do {
+ let keySpec = try handler.generateKeySpec(algorithmParams)
+ assertRSAKeySpec(keySpec, expectedKeySize: 2048)
+ } catch {
+ XCTFail("Failed to generate key spec: \(error)")
+ }
+ }
+
+ func testGenerateKeySpec_unsupportedAlgorithm_rejects() {
+ // Get handler for non-existent algorithm
+ let handler = getHandlerByAlgorithm("UNSUPPORTED-ALGORITHM")
+ XCTAssertNil(handler)
+ }
+
+ func testGenerateKeySpec_invalidModulusLength_throws() {
+ guard let handler = getHandlerByAlgorithm("RSASSA-PKCS1-v1_5") as? RSAHandler else {
+ XCTFail("RSA handler not found")
+ return
+ }
+
+ let invalidParams: [String: Any] = ["modulusLength": 1024]
+
+ do {
+ _ = try handler.generateKeySpec(invalidParams)
+ XCTFail("Should have thrown error for 1024-bit key")
+ } catch {
+ // Expected
+ XCTAssertTrue(error is NSError)
+ }
+ }
+
+ func testGenerateKeySpec_missing4096Bit_throws() {
+ guard let handler = getHandlerByAlgorithm("RSASSA-PKCS1-v1_5") as? RSAHandler else {
+ XCTFail("RSA handler not found")
+ return
+ }
+
+ let invalidParams: [String: Any] = ["modulusLength": 4096]
+
+ do {
+ _ = try handler.generateKeySpec(invalidParams)
+ XCTFail("Should have thrown error for 4096-bit key")
+ } catch {
+ // Expected
+ XCTAssertTrue(error is NSError)
+ }
+ }
+
+ func testGenerateKeySpec_emptyParams_usesDefault() {
+ guard let handler = getHandlerByAlgorithm("RSASSA-PKCS1-v1_5") as? RSAHandler else {
+ XCTFail("RSA handler not found")
+ return
+ }
+
+ do {
+ let keySpec = try handler.generateKeySpec([:])
+ assertRSAKeySpec(keySpec, expectedKeySize: 2048)
+ } catch {
+ XCTFail("Failed to generate key spec with empty params: \(error)")
+ }
+ }
+
+ func testKeyUsages_valid_sign_verify() {
+ let expectedUsages = ["sign", "verify"]
+ let algorithm: [String: Any] = [
+ "name": "RSASSA-PKCS1-v1_5",
+ "modulusLength": 2048
+ ]
+
+ // Store a key with valid usages
+ let keyId = UUID().uuidString
+ let key = MockCryptoKey(
+ id: keyId,
+ algorithm: algorithm,
+ keyType: "private",
+ keyUsages: expectedUsages,
+ extractable: false
+ )
+ mockKeyStore.store(key, withId: keyId)
+
+ // Retrieve and validate
+ guard let storedKey = getStoredKey(withId: keyId) else {
+ XCTFail("Failed to store key")
+ return
+ }
+
+ XCTAssertEqual(storedKey.keyUsages, expectedUsages)
+ }
+
+ func testKeyUsages_invalid_extractable() {
+ let algorithm: [String: Any] = [
+ "name": "RSASSA-PKCS1-v1_5",
+ "modulusLength": 2048
+ ]
+
+ // Create a key with invalid usage
+ let keyId = UUID().uuidString
+ let key = MockCryptoKey(
+ id: keyId,
+ algorithm: algorithm,
+ keyType: "private",
+ keyUsages: ["invalid_usage"],
+ extractable: false
+ )
+
+ // Validate that we can detect invalid usages
+ XCTAssertEqual(key.keyUsages, ["invalid_usage"])
+ XCTAssertFalse(key.keyUsages.contains("sign"))
+ }
+
+ func testMultipleKeys_createDistinctIds() {
+ let keyId1 = generateTestKey()
+ let keyId2 = generateTestKey()
+
+ XCTAssertNotEqual(keyId1, keyId2)
+ XCTAssertNotNil(getStoredKey(withId: keyId1))
+ XCTAssertNotNil(getStoredKey(withId: keyId2))
+ }
+
+ func testHandlerRegistry_dispatchByAlgorithm() {
+ // Test that registry correctly dispatches to RSA handler
+ let handler = getHandlerByAlgorithm("RSASSA-PKCS1-v1_5")
+ assertHandler(handler, isType: RSAHandler.self)
+ }
+
+ func testHandlerRegistry_dispatchByKeyType() {
+ // Test that registry correctly maps RSA key type to handler
+ let handler = getHandlerByKeyType("RSA")
+ assertHandler(handler, isType: RSAHandler.self)
+ }
+
+ func testAlgorithmMapping_RSA_to_RSASSA() {
+ let algorithmName = getAlgorithmName(for: "RSA")
+ XCTAssertEqual(algorithmName, "RSASSA-PKCS1-v1_5")
+ }
+
+ func testAlgorithmMapping_EC_to_ECDSA() {
+ let algorithmName = getAlgorithmName(for: "EC")
+ XCTAssertEqual(algorithmName, "ECDSA")
+ }
+
+ func testAlgorithmMapping_OKP_to_EdDSA() {
+ let algorithmName = getAlgorithmName(for: "OKP")
+ XCTAssertEqual(algorithmName, "EdDSA")
+ }
+
+ func testAlgorithmMapping_unknown_returnsNil() {
+ let algorithmName = getAlgorithmName(for: "UNKNOWN")
+ XCTAssertNil(algorithmName)
+ }
+}
diff --git a/packages/react-native-webcrypto-bridge/ios/Tests/RNWebCryptoBridgeTests/Integration/SignatureIntegrationTests.swift b/packages/react-native-webcrypto-bridge/ios/Tests/RNWebCryptoBridgeTests/Integration/SignatureIntegrationTests.swift
new file mode 100644
index 0000000..e7d297a
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/ios/Tests/RNWebCryptoBridgeTests/Integration/SignatureIntegrationTests.swift
@@ -0,0 +1,185 @@
+import XCTest
+@testable import RNWebCryptoBridge
+
+/// Integration tests for signing/verification workflows
+/// Tests coordination between AlgorithmRegistry and RSAHandler for signature operations
+class SignatureIntegrationTests: WebCryptoBridgeIntegrationTestCase {
+
+ func testSignatureAlgorithm_RSA_returnsPKCS1v15SHA256() {
+ guard let handler = getHandlerByAlgorithm( "RSASSA-PKCS1-v1_5") as? RSAHandler else {
+ XCTFail("RSA handler not found")
+ return
+ }
+
+ let algorithm = handler.getSignatureAlgorithm()
+ XCTAssertEqual(algorithm, .rsaSignatureMessagePKCS1v15SHA256)
+ }
+
+ func testSign_noKeyId_fails() {
+ let keyId = "nonexistent-key"
+ let storedKey = getStoredKey(withId: keyId)
+ XCTAssertNil(storedKey)
+ }
+
+ func testSign_validKey_requiresHandler() {
+ let keyId = generateTestKey()
+ guard let storedKey = getStoredKey(withId: keyId) else {
+ XCTFail("Failed to generate test key")
+ return
+ }
+
+ // Verify key exists
+ XCTAssertEqual(storedKey.id, keyId)
+ XCTAssertEqual(storedKey.keyType, "private")
+
+ // Get handler for the algorithm
+ guard let handler = getHandlerByAlgorithm( "RSASSA-PKCS1-v1_5") as? RSAHandler else {
+ XCTFail("RSA handler not found")
+ return
+ }
+
+ // Verify handler can process the algorithm
+ let algorithm = handler.getSignatureAlgorithm()
+ XCTAssertNotNil(algorithm)
+ }
+
+ func testVerify_requiresPublicKey() {
+ let publicKeyId = generatePublicKeyForJWK()
+ guard let storedKey = getStoredKey(withId: publicKeyId) else {
+ XCTFail("Failed to generate test key")
+ return
+ }
+
+ // Verify key is public
+ XCTAssertEqual(storedKey.keyType, "public")
+ XCTAssertTrue(storedKey.keyUsages.contains("verify"))
+ }
+
+ func testKeyUsages_sign_and_verify() {
+ let keyId = generateTestKey()
+ guard let storedKey = getStoredKey(withId: keyId) else {
+ XCTFail("Failed to generate test key")
+ return
+ }
+
+ let expectedUsages = ["sign", "verify"]
+ XCTAssertEqual(storedKey.keyUsages, expectedUsages)
+ }
+
+ func testKeyUsages_verify_only() {
+ let publicKeyId = generatePublicKeyForJWK()
+ guard let storedKey = getStoredKey(withId: publicKeyId) else {
+ XCTFail("Failed to generate test key")
+ return
+ }
+
+ XCTAssertTrue(storedKey.keyUsages.contains("verify"))
+ XCTAssertFalse(storedKey.keyUsages.contains("sign"))
+ }
+
+ func testErrorHandling_invalidAlgorithm() {
+ let invalidAlgorithmName = "UNSUPPORTED_SIGNATURE_ALGORITHM"
+ let handler = getHandlerByAlgorithm( invalidAlgorithmName)
+ XCTAssertNil(handler)
+ }
+
+ func testErrorHandling_keyTypeMismatch() {
+ // Public key should not be usable for signing
+ let publicKeyId = generatePublicKeyForJWK()
+ guard let storedKey = getStoredKey(withId: publicKeyId) else {
+ XCTFail("Failed to generate public key")
+ return
+ }
+
+ // Verify key is indeed public and not suitable for signing
+ XCTAssertEqual(storedKey.keyType, "public")
+ XCTAssertFalse(storedKey.keyUsages.contains("sign"))
+ }
+
+ func testSignatureWorkflow_keyDispatch() {
+ let privateKeyId = generateTestKey()
+ guard let privateKey = getStoredKey(withId: privateKeyId) else {
+ XCTFail("Failed to generate private key")
+ return
+ }
+
+ // Verify key has sign usage
+ XCTAssertTrue(privateKey.keyUsages.contains("sign"))
+
+ // Get handler for the algorithm
+ guard let algorithm = (privateKey.algorithm["name"] as? String).flatMap({ getAlgorithmName(for: $0) }) else {
+ // Algorithm lookup by key type
+ XCTAssertNotNil(getAlgorithmName(for: "RSA"))
+ return
+ }
+
+ // Get handler
+ guard let handler = getHandlerByAlgorithm( algorithm) as? RSAHandler else {
+ XCTFail("Handler not found for algorithm")
+ return
+ }
+
+ let signatureAlgorithm = handler.getSignatureAlgorithm()
+ XCTAssertEqual(signatureAlgorithm, .rsaSignatureMessagePKCS1v15SHA256)
+ }
+
+ func testVerificationWorkflow_keyDispatch() {
+ let publicKeyId = generatePublicKeyForJWK()
+ guard let publicKey = getStoredKey(withId: publicKeyId) else {
+ XCTFail("Failed to generate public key")
+ return
+ }
+
+ // Verify key has verify usage
+ XCTAssertTrue(publicKey.keyUsages.contains("verify"))
+
+ // Get handler for the algorithm
+ guard let algorithmName = getAlgorithmName(for: "RSA") else {
+ XCTFail("Algorithm name not found")
+ return
+ }
+ XCTAssertEqual(algorithmName, "RSASSA-PKCS1-v1_5")
+
+ guard let handler = getHandlerByAlgorithm(algorithmName) as? RSAHandler else {
+ XCTFail("Handler not found")
+ return
+ }
+
+ let signatureAlgorithm = handler.getSignatureAlgorithm()
+ XCTAssertEqual(signatureAlgorithm, SecKeyAlgorithm.rsaSignatureMessagePKCS1v15SHA256)
+ }
+
+ func testMultipleKeys_differentOperations() {
+ let privateKeyId = generateTestKey()
+ let publicKeyId = generatePublicKeyForJWK()
+
+ guard let privateKey = getStoredKey(withId: privateKeyId),
+ let publicKey = getStoredKey(withId: publicKeyId) else {
+ XCTFail("Failed to generate keys")
+ return
+ }
+
+ // Private key should have sign usage
+ XCTAssertTrue(privateKey.keyUsages.contains("sign"))
+
+ // Public key should have verify usage
+ XCTAssertTrue(publicKey.keyUsages.contains("verify"))
+
+ // Keys should be distinct
+ XCTAssertNotEqual(privateKeyId, publicKeyId)
+ }
+
+ func testSignatureAlgorithmSelection_consistency() {
+ guard let handler1 = getHandlerByAlgorithm("RSASSA-PKCS1-v1_5") as? RSAHandler,
+ let handler2 = getHandlerByKeyType("RSA") as? RSAHandler else {
+ XCTFail("Handler not found")
+ return
+ }
+
+ let algorithm1 = handler1.getSignatureAlgorithm()
+ let algorithm2 = handler2.getSignatureAlgorithm()
+
+ XCTAssertEqual(algorithm1, algorithm2)
+ XCTAssertEqual(algorithm1, SecKeyAlgorithm.rsaSignatureMessagePKCS1v15SHA256)
+ }
+}
diff --git a/packages/react-native-webcrypto-bridge/ios/Tests/RNWebCryptoBridgeTests/Integration/WebCryptoBridgeIntegrationTestCase.swift b/packages/react-native-webcrypto-bridge/ios/Tests/RNWebCryptoBridgeTests/Integration/WebCryptoBridgeIntegrationTestCase.swift
new file mode 100644
index 0000000..f571fd1
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/ios/Tests/RNWebCryptoBridgeTests/Integration/WebCryptoBridgeIntegrationTestCase.swift
@@ -0,0 +1,171 @@
+import XCTest
+@testable import RNWebCryptoBridge
+
+/// Base class for all integration tests
+/// Provides test infrastructure for module-level component coordination
+class WebCryptoBridgeIntegrationTestCase: XCTestCase {
+
+ let registry = AlgorithmRegistry.shared
+ let mockKeyStore = MockKeyStore.shared
+
+ override func setUp() {
+ super.setUp()
+ // Clear key store before each test
+ mockKeyStore.clear()
+ }
+
+ override func tearDown() {
+ super.tearDown()
+ mockKeyStore.clear()
+ }
+
+ /// Test helper for async operations with timeout
+ /// - Parameters:
+ /// - timeout: Maximum time to wait for operation (default: 5 seconds)
+ /// - operation: The async operation to perform
+ /// - assertion: Assertion to run on the result
+ func testAsyncOperation(
+ _ workItem: @escaping () -> AsyncTestResult,
+ timeout: TimeInterval = 5.0,
+ assertion: @escaping (AsyncTestResult) -> Void
+ ) {
+ let result = workItem()
+ wait(for: [result.expectation], timeout: timeout)
+ assertion(result)
+ }
+
+ /// Helper to test algorithm handler retrieval
+ func getHandlerByAlgorithm(_ algorithmName: String) -> CryptoAlgorithmHandler? {
+ return registry.getHandler(for: algorithmName)
+ }
+
+ /// Helper to test algorithm handler retrieval by key type
+ func getHandlerByKeyType(_ keyType: String) -> CryptoAlgorithmHandler? {
+ return registry.getHandlerByKeyType(keyType)
+ }
+
+ /// Helper to test algorithm name mapping
+ func getAlgorithmName(for keyType: String) -> String? {
+ return registry.getAlgorithmName(for: keyType)
+ }
+
+ /// Helper to generate a test key and store it
+ func generateTestKey() -> String {
+ let keyId = UUID().uuidString
+ let algorithm: [String: Any] = [
+ "name": "RSASSA-PKCS1-v1_5",
+ "modulusLength": 2048
+ ]
+ let key = MockCryptoKey(
+ id: keyId,
+ algorithm: algorithm,
+ keyType: "private",
+ keyUsages: ["sign", "verify"],
+ extractable: false
+ )
+ mockKeyStore.store(key, withId: keyId)
+ return keyId
+ }
+
+ /// Helper to generate a public key for JWK operations
+ func generatePublicKeyForJWK() -> String {
+ let keyId = UUID().uuidString
+ let algorithm: [String: Any] = [
+ "name": "RSASSA-PKCS1-v1_5",
+ "modulusLength": 2048
+ ]
+ var key = MockCryptoKey(
+ id: keyId,
+ algorithm: algorithm,
+ keyType: "public",
+ keyUsages: ["verify"],
+ extractable: true
+ )
+
+ // Add test JWK components
+ key.keyData = [
+ "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw",
+ "e": "AQAB"
+ ]
+
+ mockKeyStore.store(key, withId: keyId)
+ return keyId
+ }
+
+ /// Retrieve a stored test key
+ func getStoredKey(withId id: String) -> MockCryptoKey? {
+ return mockKeyStore.retrieve(withId: id)
+ }
+
+ /// Assert that a handler exists and is of the expected type
+ func assertHandler(
+ _ handler: CryptoAlgorithmHandler?,
+ isType expectedType: CryptoAlgorithmHandler.Type,
+ file: StaticString = #file,
+ line: UInt = #line
+ ) {
+ XCTAssertNotNil(handler, file: file, line: line)
+ guard let handler = handler else { return }
+ XCTAssertTrue(
+ type(of: handler) == expectedType,
+ "Expected handler type \(expectedType), got \(type(of: handler))",
+ file: file,
+ line: line
+ )
+ }
+
+ /// Assert that a key spec is valid for RSA
+ func assertRSAKeySpec(
+ _ spec: KeyGenSpec?,
+ expectedKeySize: Int = 2048,
+ file: StaticString = #file,
+ line: UInt = #line
+ ) {
+ XCTAssertNotNil(spec, file: file, line: line)
+ guard let spec = spec else { return }
+ XCTAssertEqual(
+ spec.keyType as String,
+ kSecAttrKeyTypeRSA as String,
+ file: file,
+ line: line
+ )
+ XCTAssertEqual(spec.keySize, expectedKeySize, file: file, line: line)
+ }
+
+ /// Assert that a JWK dictionary has required RSA fields
+ func assertValidRSAJWK(
+ _ jwk: [String: Any]?,
+ file: StaticString = #file,
+ line: UInt = #line
+ ) {
+ XCTAssertNotNil(jwk, file: file, line: line)
+ guard let jwk = jwk else { return }
+
+ XCTAssertEqual(
+ jwk["kty"] as? String,
+ "RSA",
+ "JWK should have kty=RSA",
+ file: file,
+ line: line
+ )
+ XCTAssertEqual(
+ jwk["alg"] as? String,
+ "RS256",
+ "JWK should have alg=RS256",
+ file: file,
+ line: line
+ )
+ XCTAssertNotNil(
+ jwk["n"] as? String,
+ "JWK should have modulus (n)",
+ file: file,
+ line: line
+ )
+ XCTAssertNotNil(
+ jwk["e"] as? String,
+ "JWK should have exponent (e)",
+ file: file,
+ line: line
+ )
+ }
+}
diff --git a/packages/react-native-webcrypto-bridge/ios/Tests/RNWebCryptoBridgeTests/RSAHandlerTests.swift b/packages/react-native-webcrypto-bridge/ios/Tests/RNWebCryptoBridgeTests/RSAHandlerTests.swift
new file mode 100644
index 0000000..29b7920
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/ios/Tests/RNWebCryptoBridgeTests/RSAHandlerTests.swift
@@ -0,0 +1,158 @@
+import XCTest
+import Security
+import Foundation
+@testable import RNWebCryptoBridge
+
+class RSAHandlerTests: XCTestCase {
+
+ let handler = RSAHandler()
+
+ func testGenerateKeySpec_valid2048BitRequest() throws {
+ let params: [String: Any] = ["modulusLength": 2048]
+
+ let keyGenSpec = try handler.generateKeySpec(params)
+
+ XCTAssertEqual(keyGenSpec.keyType as String, kSecAttrKeyTypeRSA as String)
+ XCTAssertEqual(keyGenSpec.keySize, 2048)
+ }
+
+ func testGenerateKeySpec_invalid1024Bit_throws() {
+ let params: [String: Any] = ["modulusLength": 1024]
+
+ XCTAssertThrowsError(try handler.generateKeySpec(params)) { error in
+ let nsError = error as NSError
+ XCTAssertEqual(nsError.domain, "RSAHandler")
+ XCTAssertTrue(nsError.localizedDescription.contains("2048-bit"))
+ }
+ }
+
+ func testGenerateKeySpec_invalid4096Bit_throws() {
+ let params: [String: Any] = ["modulusLength": 4096]
+
+ XCTAssertThrowsError(try handler.generateKeySpec(params)) { error in
+ let nsError = error as NSError
+ XCTAssertEqual(nsError.domain, "RSAHandler")
+ XCTAssertTrue(nsError.localizedDescription.contains("2048-bit"))
+ }
+ }
+
+ func testGenerateKeySpec_missingModulusLength_usesDefault() throws {
+ let params: [String: Any] = [:]
+
+ let keyGenSpec = try handler.generateKeySpec(params)
+
+ XCTAssertEqual(keyGenSpec.keySize, 2048)
+ }
+
+ func testGetSignatureAlgorithm_returnsRSAPKCS1v15SHA256() {
+ let algorithm = handler.getSignatureAlgorithm()
+ XCTAssertEqual(algorithm, .rsaSignatureMessagePKCS1v15SHA256)
+ }
+
+ func testExportToJWK_producesValidRSAJWK() {
+ // Create test components
+ let testModulus = Data("12345678901234567890".utf8)
+ let testExponent = Data("65537".utf8)
+ let keyComponents = RSAPublicKeyComponents(modulus: testModulus, exponent: testExponent)
+
+ // Export to JWK (publicKey parameter is unused)
+ let jwk = handler.exportToJWK(publicKey: nil, keyComponents: keyComponents)
+
+ XCTAssertEqual(jwk["kty"] as? String, "RSA")
+ XCTAssertEqual(jwk["alg"] as? String, "RS256")
+ XCTAssertNotNil(jwk["n"] as? String)
+ XCTAssertNotNil(jwk["e"] as? String)
+ }
+
+ func testExportToJWK_encodesComponentsCorrectly() {
+ let testModulus = Data([0x12, 0x34, 0x56, 0x78])
+ let testExponent = Data([0x01, 0x00, 0x01])
+ let keyComponents = RSAPublicKeyComponents(modulus: testModulus, exponent: testExponent)
+
+ let jwk = handler.exportToJWK(publicKey: nil, keyComponents: keyComponents)
+
+ // Verify modulus encoding
+ if let encodedN = jwk["n"] as? String {
+ let decodedN = Data(base64Encoded: encodedN.base64URLDecoded)
+ XCTAssertEqual(decodedN, testModulus)
+ } else {
+ XCTFail("Modulus not found in JWK")
+ }
+
+ // Verify exponent encoding
+ if let encodedE = jwk["e"] as? String {
+ let decodedE = Data(base64Encoded: encodedE.base64URLDecoded)
+ XCTAssertEqual(decodedE, testExponent)
+ } else {
+ XCTFail("Exponent not found in JWK")
+ }
+ }
+
+ func testImportFromJWK_reconstructsComponents() {
+ // Create original components
+ let originalModulus = Data("12345678901234567890".utf8)
+ let originalExponent = Data("65537".utf8)
+ let originalComponents = RSAPublicKeyComponents(modulus: originalModulus, exponent: originalExponent)
+
+ // Export to JWK
+ let jwk = handler.exportToJWK(publicKey: nil, keyComponents: originalComponents)
+
+ // Import back
+ let importedComponents = handler.importFromJWK(jwk)
+
+ XCTAssertNotNil(importedComponents)
+ XCTAssertEqual(importedComponents?.modulus, originalModulus)
+ XCTAssertEqual(importedComponents?.exponent, originalExponent)
+ }
+
+ func testImportFromJWK_missingModulus_returnsNil() {
+ let jwk: [String: Any] = [
+ "e": "AQAB"
+ ]
+
+ let components = handler.importFromJWK(jwk)
+ XCTAssertNil(components)
+ }
+
+ func testImportFromJWK_missingExponent_returnsNil() {
+ let jwk: [String: Any] = [
+ "n": "some_base64_data"
+ ]
+
+ let components = handler.importFromJWK(jwk)
+ XCTAssertNil(components)
+ }
+
+ func testImportFromJWK_invalidBase64_returnsNil() {
+ let jwk: [String: Any] = [
+ "n": "not valid base64!@#$%^&*()",
+ "e": "also invalid!@#$%^&*()"
+ ]
+
+ let components = handler.importFromJWK(jwk)
+ XCTAssertNil(components)
+ }
+
+ func testRoundTrip_exportAndImport() {
+ let testCases: [(modulus: Data, exponent: Data)] = [
+ (Data([0x12, 0x34]), Data([0x65, 0x37])),
+ (Data("test_modulus".utf8), Data("test_exponent".utf8)),
+ (Data(repeating: 0xFF, count: 256), Data([0x01, 0x00, 0x01]))
+ ]
+
+ for testCase in testCases {
+ let originalComponents = RSAPublicKeyComponents(
+ modulus: testCase.modulus,
+ exponent: testCase.exponent
+ )
+
+ let jwk = handler.exportToJWK(publicKey: nil, keyComponents: originalComponents)
+
+ let reimportedComponents = handler.importFromJWK(jwk)
+
+ XCTAssertNotNil(reimportedComponents)
+ XCTAssertEqual(reimportedComponents?.modulus, testCase.modulus)
+ XCTAssertEqual(reimportedComponents?.exponent, testCase.exponent)
+ }
+ }
+}
diff --git a/packages/react-native-webcrypto-bridge/ios/Tests/RNWebCryptoBridgeTests/RSAKeyUtilsTests.swift b/packages/react-native-webcrypto-bridge/ios/Tests/RNWebCryptoBridgeTests/RSAKeyUtilsTests.swift
new file mode 100644
index 0000000..90f7a8e
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/ios/Tests/RNWebCryptoBridgeTests/RSAKeyUtilsTests.swift
@@ -0,0 +1,63 @@
+import XCTest
+@testable import RNWebCryptoBridge
+import Foundation
+
+class RSAKeyUtilsTests: XCTestCase {
+
+ func testRSAPublicKeyComponents_initWithValidDER() {
+ // Create test components and verify DER roundtrip
+ let testModulus = Data("12345678901234567890".utf8)
+ let testExponent = Data("65537".utf8)
+ let components = RSAPublicKeyComponents(modulus: testModulus, exponent: testExponent)
+
+ let derData = components.derData
+ let parsedComponents = RSAPublicKeyComponents(derData: derData)
+
+ XCTAssertNotNil(parsedComponents)
+ XCTAssertEqual(parsedComponents?.modulus, testModulus)
+ XCTAssertEqual(parsedComponents?.exponent, testExponent)
+ }
+
+ func testRSAPublicKeyComponents_keySizeInBits() {
+ let modulus = Data(repeating: 0xFF, count: 256) // 2048 bits
+ let exponent = Data([0x01, 0x00, 0x01]) // 65537
+ let components = RSAPublicKeyComponents(modulus: modulus, exponent: exponent)
+
+ XCTAssertEqual(components.keySizeInBits, 2048)
+ }
+
+ func testReadDERLength_shortForm() {
+ let bytes: [UInt8] = [0x7F] // Short form: 127 bytes
+ var offset = 0
+
+ let length = RSAKeyUtils.readDERLength(bytes: bytes, offset: &offset)
+
+ XCTAssertEqual(length, 127)
+ XCTAssertEqual(offset, 1)
+ }
+
+ func testReadDERLength_longForm() {
+ let bytes: [UInt8] = [0x81, 0xFF] // Long form: 255 bytes
+ var offset = 0
+
+ let length = RSAKeyUtils.readDERLength(bytes: bytes, offset: &offset)
+
+ XCTAssertEqual(length, 255)
+ XCTAssertEqual(offset, 2)
+ }
+
+ func testEncodeDERLength_shortForm() {
+ let encoded = RSAKeyUtils.encodeDERLength(127)
+ XCTAssertEqual(encoded, [0x7F])
+ }
+
+ func testEncodeDERLength_longForm() {
+ let encoded = RSAKeyUtils.encodeDERLength(255)
+ XCTAssertEqual(encoded, [0x81, 0xFF])
+ }
+
+ func testEncodeDERLength_multiByteForm() {
+ let encoded = RSAKeyUtils.encodeDERLength(256)
+ XCTAssertEqual(encoded.first, 0x82) // 2 bytes needed
+ }
+}
diff --git a/packages/react-native-webcrypto-bridge/jest.config.js b/packages/react-native-webcrypto-bridge/jest.config.js
new file mode 100644
index 0000000..3756e1a
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/jest.config.js
@@ -0,0 +1,17 @@
+import pkg from './package.json' with { type: 'json' };
+
+
+const config = {
+ displayName: '@okta/react-native-webcrypto-bridge',
+ preset: '@repo/jest-helpers/react-native',
+ globals: {
+ __PKG_NAME__: pkg.name,
+ __PKG_VERSION__: pkg.version,
+ __DEV__: true // required for `react-native` itself
+ },
+ setupFiles: [
+ '/test/jest.setup.ts'
+ ]
+};
+
+export default config;
diff --git a/packages/react-native-webcrypto-bridge/package.json b/packages/react-native-webcrypto-bridge/package.json
new file mode 100644
index 0000000..5d62817
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/package.json
@@ -0,0 +1,81 @@
+{
+ "name": "@okta/react-native-webcrypto-bridge",
+ "version": "0.7.0",
+ "description": "WebCrypto API polyfill for React Native with native iOS and Android bridge",
+ "type": "module",
+ "main": "dist/esm/index.js",
+ "module": "dist/esm/index.js",
+ "types": "dist/types/index.d.ts",
+ "react-native": "src/index.ts",
+ "source": "src/index.ts",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=20.11.0"
+ },
+ "files": [
+ "./LICENSE",
+ "./dist",
+ "./ios",
+ "./android",
+ "*.md",
+ "package.json",
+ "react-native-webcrypto-bridge.podspec"
+ ],
+ "exports": {
+ ".": {
+ "types": "./dist/types/index.d.ts",
+ "import": "./dist/esm/index.js"
+ },
+ "./package.json": "./package.json"
+ },
+ "scripts": {
+ "lint": "eslint --ext .js,.ts,.tsx .",
+ "build": "yarn build:esm && yarn build:types",
+ "build:watch": "rollup -c --watch & tsc --watch",
+ "build:esm": "rollup -c",
+ "build:types": "tsc",
+ "test": "yarn test:unit",
+ "test:unit": "jest",
+ "test:watch": "jest --watchAll",
+ "prepack": "yarn build"
+ },
+ "dependencies": {
+ },
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*"
+ },
+ "devDependencies": {
+ "@react-native/babel-preset": "^0.81.6",
+ "@repo/eslint-config": "*",
+ "@repo/rollup-config": "*",
+ "@repo/typescript-config": "*",
+ "@types/react": "^19.2.14",
+ "eslint": "^8.56.0",
+ "jest": "^29.7.0",
+ "react": "^19.2.4",
+ "react-native": "^0.83.1",
+ "rollup": "^4.52.4",
+ "typescript": "^5.9.2"
+ },
+ "devEngines": {
+ "runtime": {
+ "name": "node",
+ "version": ">=22.13.1",
+ "onFail": "warn"
+ },
+ "packageManager": {
+ "name": "yarn",
+ "version": ">=1.19.0",
+ "onFail": "warn"
+ }
+ },
+ "codegenConfig": {
+ "name": "RNWebCryptoBridge",
+ "type": "modules",
+ "jsSrcsDir": "src",
+ "android": {
+ "javaPackageName": "com.okta.webcryptobridge"
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/react-native-webcrypto-bridge/react-native-webcrypto-bridge.podspec b/packages/react-native-webcrypto-bridge/react-native-webcrypto-bridge.podspec
new file mode 100644
index 0000000..b8a035b
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/react-native-webcrypto-bridge.podspec
@@ -0,0 +1,37 @@
+require "json"
+
+package = JSON.parse(File.read(File.join(__dir__, "package.json")))
+folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32'
+
+Pod::Spec.new do |s|
+ s.name = "react-native-webcrypto-bridge"
+ s.version = package["version"]
+ s.summary = package["description"]
+ s.homepage = "https://github.com/okta/okta-client-javascript"
+ s.license = package["license"]
+ s.platforms = { :ios => "13.4" }
+ s.source = { :git => "https://github.com/okta/okta-client-javascript.git", :tag => "#{s.version}" }
+
+ s.source_files = "ios/Sources/**/*.{h,m,mm,swift}"
+ s.swift_version = "5.0"
+
+ # New Architecture support
+ if ENV['RCT_NEW_ARCH_ENABLED'] == '1'
+ s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1"
+ s.pod_target_xcconfig = {
+ 'DEFINES_MODULE' => 'YES',
+ 'HEADER_SEARCH_PATHS' => [
+ '"$(PODS_ROOT)/Headers/Private/React-Codegen/react/renderer/components"',
+ '"$(PODS_TARGET_SRCROOT)/../../../ios/build/generated/ios"',
+ '"${PODS_CONFIGURATION_BUILD_DIR}/React-Codegen/React_Codegen.framework/Headers"'
+ ].join(' '),
+ 'CLANG_CXX_LANGUAGE_STANDARD' => 'c++17'
+ }
+ else
+ s.pod_target_xcconfig = {
+ 'DEFINES_MODULE' => 'YES'
+ }
+ end
+
+ install_modules_dependencies(s)
+end
\ No newline at end of file
diff --git a/packages/react-native-webcrypto-bridge/react-native.config.js b/packages/react-native-webcrypto-bridge/react-native.config.js
new file mode 100644
index 0000000..d05964d
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/react-native.config.js
@@ -0,0 +1,10 @@
+module.exports = {
+ dependency: {
+ platforms: {
+ android: {
+ packageImportPath: 'import com.okta.webcryptobridge.WebCryptoBridgePackage;',
+ packageInstance: 'new WebCryptoBridgePackage()',
+ },
+ },
+ },
+};
\ No newline at end of file
diff --git a/packages/react-native-webcrypto-bridge/rollup.config.mjs b/packages/react-native-webcrypto-bridge/rollup.config.mjs
new file mode 100644
index 0000000..4a4d42a
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/rollup.config.mjs
@@ -0,0 +1,17 @@
+import baseConfig from '@repo/rollup-config/sdk';
+import ts from 'typescript';
+import pkg from './package.json' with { type: 'json' };
+
+const base = baseConfig(ts, pkg);
+
+export default {
+ ...base,
+ input: 'src/index.ts',
+ external: [
+ ...Object.keys(pkg.dependencies || {}),
+ ...Object.keys(pkg.peerDependencies || {}),
+ 'react-native',
+ 'react',
+ '@okta/auth-foundation/core'
+ ],
+};
diff --git a/packages/react-native-webcrypto-bridge/src/NativeWebCryptoBridge.ts b/packages/react-native-webcrypto-bridge/src/NativeWebCryptoBridge.ts
new file mode 100644
index 0000000..e42d756
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/src/NativeWebCryptoBridge.ts
@@ -0,0 +1,70 @@
+import type { TurboModule } from 'react-native';
+import { TurboModuleRegistry } from 'react-native';
+
+
+export interface Spec extends TurboModule {
+ /**
+ * Generate SHA-256 digest (async).
+ * @param data - Standard Base64-encoded input data
+ * @returns Standard Base64-encoded digest
+ */
+ digest(algorithm: string, data: string): Promise;
+
+ /**
+ * Generate RSA key pair (async)
+ */
+ generateKey(
+ algorithm: string,
+ extractable: boolean,
+ keyUsages: string[]
+ ): Promise;
+
+ /**
+ * Export a key to JWK format (async)
+ */
+ exportKey(format: string, keyId: string): Promise;
+
+ /**
+ * Import a key from JWK format (async)
+ */
+ importKey(
+ format: string,
+ keyData: string,
+ algorithm: string,
+ extractable: boolean,
+ keyUsages: string[]
+ ): Promise;
+
+ /**
+ * Sign data with a private key (async).
+ * @param data - Standard Base64-encoded input data
+ * @returns Standard Base64-encoded signature
+ */
+ sign(algorithm: string, keyId: string, data: string): Promise;
+
+ /**
+ * Verify a signature (async).
+ * @param signature - Standard Base64-encoded signature
+ * @param data - Standard Base64-encoded input data
+ */
+ verify(
+ algorithm: string,
+ keyId: string,
+ signature: string,
+ data: string
+ ): Promise;
+
+ /**
+ * Generate cryptographically secure random values (sync).
+ * @param length - Number of random bytes to generate
+ * @returns Standard Base64-encoded random bytes
+ */
+ getRandomValues(length: number): string;
+
+ /**
+ * Generate a random UUID v4 (sync)
+ */
+ randomUUID(): string;
+}
+
+export default TurboModuleRegistry.getEnforcing('WebCryptoBridge');
diff --git a/packages/react-native-webcrypto-bridge/src/WebCryptoPolyfill.ts b/packages/react-native-webcrypto-bridge/src/WebCryptoPolyfill.ts
new file mode 100644
index 0000000..b337d7a
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/src/WebCryptoPolyfill.ts
@@ -0,0 +1,221 @@
+/**
+ * WebCrypto API polyfill for React Native
+ *
+ * Bridges JavaScript WebCrypto API calls to native platform cryptography
+ * (Apple Security / Android JCA) via a React Native TurboModule.
+ *
+ * Binary data crosses the bridge as standard Base64 strings for efficiency.
+ */
+
+import NativeWebCryptoBridge from './NativeWebCryptoBridge.ts';
+
+
+/**
+ * @internal
+ * Maps `CryptoKey` instances to the id assigned by the native code
+ */
+const cryptoKeyMap = new WeakMap();
+
+// MARK: - ArrayBuffer ↔ Base64 Converters
+
+/**
+ * Converts a `BufferSource` instance to an `ArrayBuffer`
+ */
+function toArrayBuffer(source: BufferSource): ArrayBuffer {
+ if (source instanceof ArrayBuffer) {
+ return source;
+ } else if (ArrayBuffer.isView(source)) {
+ const typedArray = source;
+ // .slice() creates a copy of only the relevant bytes. This is crucial for
+ // Node.js Buffers which might share a larger memory pool.
+ return typedArray.buffer.slice(typedArray.byteOffset, typedArray.byteOffset + typedArray.byteLength);
+ } else {
+ throw new TypeError('Unsupported BufferSource type');
+ }
+}
+
+/**
+ * Encode an ArrayBuffer to a standard Base64 string for the native bridge.
+ */
+function arrayBufferToBase64(buffer: ArrayBuffer): string {
+ const bytes = new Uint8Array(buffer);
+ let binary = '';
+ for (let i = 0; i < bytes.length; i++) {
+ binary += String.fromCharCode(bytes[i]);
+ }
+ return btoa(binary);
+}
+
+/**
+ * Decode a standard Base64 string from the native bridge to an ArrayBuffer.
+ */
+function base64ToArrayBuffer(base64: string): ArrayBuffer {
+ const binary = atob(base64);
+ const bytes = new Uint8Array(binary.length);
+ for (let i = 0; i < binary.length; i++) {
+ bytes[i] = binary.charCodeAt(i);
+ }
+ return bytes.buffer;
+}
+
+// MARK: - SubtleCrypto Methods
+
+const digest: SubtleCrypto['digest'] = async (algorithm, data) => {
+ if (algorithm !== 'SHA-256') {
+ throw new DOMException(`Unsupported algorithm: ${algorithm}`, 'NotSupportedError');
+ }
+
+ const base64Data = arrayBufferToBase64(toArrayBuffer(data));
+ const resultBase64 = await NativeWebCryptoBridge.digest('SHA-256', base64Data);
+ return base64ToArrayBuffer(resultBase64);
+};
+
+const importKey: SubtleCrypto['importKey'] = async (
+ format,
+ keyData,
+ algorithm,
+ extractable,
+ keyUsages
+) => {
+ if (format !== 'jwk') {
+ throw new DOMException(`Unsupported format: ${format}`, 'NotSupportedError');
+ }
+
+ const keyDataJson = JSON.stringify(keyData);
+ const algorithmJson = JSON.stringify({
+ name: algorithm.name,
+ hash: algorithm.hash,
+ });
+
+ const keyId = await NativeWebCryptoBridge.importKey(
+ format,
+ keyDataJson,
+ algorithmJson,
+ extractable,
+ keyUsages
+ );
+
+ const key: CryptoKey = {
+ algorithm,
+ extractable,
+ type: 'public', // TODO: Determine from key data when private key import is supported
+ usages: keyUsages
+ };
+
+ cryptoKeyMap.set(key, keyId);
+
+ return key;
+};
+
+// TODO: DPoP — wire to native implementation when ready
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+const exportKey: SubtleCrypto['exportKey'] = async (_format, _key) => {
+ throw new DOMException('exportKey is not yet implemented', 'NotSupportedError');
+};
+
+// TODO: DPoP — wire to native implementation when ready
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+const sign: SubtleCrypto['sign'] = async (_algorithm, _key, _data) => {
+ throw new DOMException('sign is not yet implemented', 'NotSupportedError');
+};
+
+// TODO: DPoP — wire to native implementation when ready
+const generateKey: SubtleCrypto['generateKey'] = async (
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ _algorithm, _extractable, _keyUsages
+) => {
+ throw new DOMException('generateKey is not yet implemented', 'NotSupportedError');
+};
+
+const verify: SubtleCrypto['verify'] = async (algorithm, key, signature, data) => {
+ const keyId = cryptoKeyMap.get(key);
+ if (!keyId) {
+ throw new DOMException('Unable to locate key', 'InvalidAccessError');
+ }
+
+ const alg = typeof algorithm === 'string' ? algorithm : algorithm.name;
+ if (alg !== 'RSASSA-PKCS1-v1_5') {
+ throw new DOMException('Unsupported algorithm', 'NotSupportedError');
+ }
+
+ if (!key.usages.includes('verify')) {
+ throw new DOMException('Key does not support verify operation', 'InvalidAccessError');
+ }
+
+ const algorithmJson = JSON.stringify(key.algorithm);
+ const signatureBase64 = arrayBufferToBase64(toArrayBuffer(signature));
+ const dataBase64 = arrayBufferToBase64(toArrayBuffer(data));
+
+ return await NativeWebCryptoBridge.verify(
+ algorithmJson,
+ keyId,
+ signatureBase64,
+ dataBase64
+ );
+};
+
+// MARK: - Types & Exports
+
+/**
+ * SubtleCrypto implementation
+ */
+const subtle: Partial = {
+ digest,
+ importKey,
+ exportKey,
+ sign,
+ generateKey,
+ verify
+};
+
+export interface WebCryptoPolyfill {
+ subtle: Partial;
+ getRandomValues: Crypto['getRandomValues'];
+ randomUUID: Crypto['randomUUID'];
+}
+
+/**
+ * Crypto implementation
+ */
+const cryptoPolyfill: WebCryptoPolyfill = {
+ subtle,
+ getRandomValues(array: T): T {
+ const uint8Array = new Uint8Array(array.buffer, array.byteOffset, array.byteLength);
+ const requestedLength = uint8Array.length;
+
+ const base64Result = NativeWebCryptoBridge.getRandomValues(requestedLength);
+
+ // Decode the Base64 string from the native bridge
+ const binary = atob(base64Result);
+ if (binary.length !== requestedLength) {
+ throw new DOMException(
+ `getRandomValues: expected ${requestedLength} bytes, received ${binary.length}`,
+ 'OperationError'
+ );
+ }
+
+ for (let i = 0; i < binary.length; i++) {
+ uint8Array[i] = binary.charCodeAt(i);
+ }
+
+ return array;
+ },
+ randomUUID() {
+ return NativeWebCryptoBridge.randomUUID() as ReturnType;
+ }
+};
+
+/**
+ * Install the WebCrypto polyfill globally
+ */
+export function installWebCryptoPolyfill(): void {
+ if (typeof global !== 'undefined') {
+ // @ts-expect-error - Adding crypto to global
+ global.crypto = cryptoPolyfill;
+ }
+
+ if (typeof window !== 'undefined') {
+ // @ts-expect-error - Adding crypto to window
+ window.crypto = cryptoPolyfill;
+ }
+}
diff --git a/packages/react-native-webcrypto-bridge/src/index.ts b/packages/react-native-webcrypto-bridge/src/index.ts
new file mode 100644
index 0000000..86e04a1
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/src/index.ts
@@ -0,0 +1,6 @@
+/**
+ * @module @okta/react-native-webcrypto-bridge
+ */
+
+
+export { installWebCryptoPolyfill } from './WebCryptoPolyfill.ts';
diff --git a/packages/react-native-webcrypto-bridge/test/jest.setup.ts b/packages/react-native-webcrypto-bridge/test/jest.setup.ts
new file mode 100644
index 0000000..c6d020e
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/test/jest.setup.ts
@@ -0,0 +1,36 @@
+/* global btoa */
+
+// Mock the native module for Jest tests.
+// All binary data is transported as standard Base64 strings, matching
+// the NativeWebCryptoBridge.Spec interface.
+jest.mock('react-native', () => ({
+ TurboModuleRegistry: {
+ getEnforcing: jest.fn(() => ({
+ digest: jest.fn(async (algorithm, base64Data) => {
+ // Echo back base64 data as-is (mock doesn't compute real digest)
+ return base64Data;
+ }),
+ generateKey: jest.fn(async () => JSON.stringify({ id: 'test-key-id' })),
+ exportKey: jest.fn(async () => JSON.stringify({ kty: 'RSA', n: 'test', e: 'AQAB' })),
+ importKey: jest.fn(async () => 'imported-key-id'),
+ sign: jest.fn(async () => {
+ // Return Base64-encoded mock signature bytes
+ return btoa(String.fromCharCode(...Array(32).fill(0).map(() => Math.floor(Math.random() * 256))));
+ }),
+ verify: jest.fn(async () => true),
+ getRandomValues: jest.fn((length) => {
+ // Return Base64-encoded random bytes (synchronous)
+ const bytes = Array(length).fill(0).map(() => Math.floor(Math.random() * 256));
+ return btoa(String.fromCharCode(...bytes));
+ }),
+ randomUUID: jest.fn(() => '550e8400-e29b-41d4-a716-446655440000'),
+ })),
+ },
+ NativeModules: {
+ WebCryptoBridge: {},
+ },
+ Platform: {
+ OS: 'ios',
+ select: jest.fn((obj) => obj.ios || obj.default),
+ },
+}));
diff --git a/packages/react-native-webcrypto-bridge/test/spec/digest.spec.ts b/packages/react-native-webcrypto-bridge/test/spec/digest.spec.ts
new file mode 100644
index 0000000..108edc1
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/test/spec/digest.spec.ts
@@ -0,0 +1,59 @@
+import { installWebCryptoPolyfill } from '../../src/index';
+
+describe('subtle.digest', () => {
+ beforeAll(() => {
+ installWebCryptoPolyfill();
+ });
+
+ it('should call native digest with SHA-256 and return an ArrayBuffer', async () => {
+ const data = new TextEncoder().encode('hello world');
+ const result = await global.crypto.subtle.digest('SHA-256', data);
+
+ expect(result).toBeInstanceOf(ArrayBuffer);
+ // The mock echoes data back, so the result should have content
+ expect(result.byteLength).toBeGreaterThan(0);
+ });
+
+ it('should accept an ArrayBuffer directly', async () => {
+ const data = new Uint8Array([1, 2, 3, 4]).buffer;
+ const result = await global.crypto.subtle.digest('SHA-256', data);
+
+ expect(result).toBeInstanceOf(ArrayBuffer);
+ });
+
+ it('should reject unsupported algorithms', async () => {
+ const data = new Uint8Array([1, 2, 3]);
+ await expect(
+ global.crypto.subtle.digest('SHA-512', data)
+ ).rejects.toThrow(DOMException);
+ });
+
+ it('should throw NotSupportedError for unsupported algorithms', async () => {
+ const data = new Uint8Array([1, 2, 3]);
+ try {
+ await global.crypto.subtle.digest('SHA-512', data);
+ fail('Expected DOMException to be thrown');
+ } catch (error) {
+ if (error instanceof DOMException) {
+ expect(error.name).toBe('NotSupportedError');
+ } else {
+ throw error;
+ }
+ }
+ });
+
+ it('should reject SHA-1', async () => {
+ const data = new Uint8Array([1, 2, 3]);
+ try {
+ await global.crypto.subtle.digest('SHA-1', data);
+ fail('Expected DOMException to be thrown');
+ } catch (error) {
+ if (error instanceof DOMException) {
+ expect(error.name).toBe('NotSupportedError');
+ } else {
+ throw error;
+ }
+ }
+ });
+});
+
diff --git a/packages/react-native-webcrypto-bridge/test/spec/getRandomValues.spec.ts b/packages/react-native-webcrypto-bridge/test/spec/getRandomValues.spec.ts
new file mode 100644
index 0000000..83ea83b
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/test/spec/getRandomValues.spec.ts
@@ -0,0 +1,50 @@
+import { installWebCryptoPolyfill } from '../../src/index';
+import NativeWebCryptoBridge from '../../src/NativeWebCryptoBridge';
+
+describe('getRandomValues', () => {
+ beforeAll(() => {
+ installWebCryptoPolyfill();
+ });
+
+ it('should fill a Uint8Array with random bytes', () => {
+ const array = new Uint8Array(32);
+ const result = global.crypto.getRandomValues(array);
+
+ expect(result).toBe(array);
+ expect(result.length).toBe(32);
+
+ // Check that values are not all zeros (statistically near-impossible with real randomness)
+ const hasNonZero = Array.from(result).some(v => v !== 0);
+ expect(hasNonZero).toBe(true);
+ });
+
+ it('should fill a Uint8Array of length 1', () => {
+ const array = new Uint8Array(1);
+ const result = global.crypto.getRandomValues(array);
+ expect(result).toBe(array);
+ expect(result.length).toBe(1);
+ });
+
+ it('should throw OperationError if native bridge returns wrong length', () => {
+ expect.assertions(1);
+
+ // Spy on the actual imported NativeWebCryptoBridge
+ const mockFn = jest.fn(() => btoa('x')); // 1 byte when we ask for 32
+ const originalFn = NativeWebCryptoBridge.getRandomValues;
+ NativeWebCryptoBridge.getRandomValues = mockFn;
+
+ const array = new Uint8Array(32);
+ try {
+ global.crypto.getRandomValues(array);
+ } catch (error) {
+ if (error instanceof DOMException) {
+ expect(error.name).toBe('OperationError');
+ } else {
+ throw error;
+ }
+ } finally {
+ // Restore
+ NativeWebCryptoBridge.getRandomValues = originalFn;
+ }
+ });
+});
diff --git a/packages/react-native-webcrypto-bridge/test/spec/importKey.spec.ts b/packages/react-native-webcrypto-bridge/test/spec/importKey.spec.ts
new file mode 100644
index 0000000..b84da6e
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/test/spec/importKey.spec.ts
@@ -0,0 +1,69 @@
+import { installWebCryptoPolyfill } from '../../src/index';
+
+describe('subtle.importKey', () => {
+ beforeAll(() => {
+ installWebCryptoPolyfill();
+ });
+
+ it('should import a JWK and return a CryptoKey', async () => {
+ const jwk = {
+ kty: 'RSA',
+ n: 'test-modulus',
+ e: 'AQAB',
+ alg: 'RS256',
+ };
+
+ const algorithm = { name: 'RSASSA-PKCS1-v1_5', hash: { name: 'SHA-256' } };
+ const key = await global.crypto.subtle.importKey(
+ 'jwk',
+ jwk,
+ algorithm,
+ false,
+ ['verify']
+ );
+
+ expect(key).toBeDefined();
+ expect(key.type).toBe('public');
+ expect(key.extractable).toBe(false);
+ expect(key.usages).toEqual(['verify']);
+ expect(key.algorithm).toEqual(algorithm);
+ });
+
+ it('should reject unsupported format', async () => {
+ try {
+ await global.crypto.subtle.importKey(
+ 'raw' as any,
+ new Uint8Array([1, 2, 3]),
+ { name: 'RSASSA-PKCS1-v1_5', hash: { name: 'SHA-256' } },
+ false,
+ ['verify']
+ );
+ fail('Expected DOMException to be thrown');
+ } catch (error) {
+ if (error instanceof DOMException) {
+ expect(error.name).toBe('NotSupportedError');
+ } else {
+ throw error;
+ }
+ }
+ });
+
+ it('should reject SPKI format', async () => {
+ try {
+ await global.crypto.subtle.importKey(
+ 'spki' as any,
+ new Uint8Array([1, 2, 3]),
+ { name: 'RSASSA-PKCS1-v1_5', hash: { name: 'SHA-256' } },
+ false,
+ ['verify']
+ );
+ fail('Expected DOMException to be thrown');
+ } catch (error) {
+ if (error instanceof DOMException) {
+ expect(error.name).toBe('NotSupportedError');
+ } else {
+ throw error;
+ }
+ }
+ });
+});
diff --git a/packages/react-native-webcrypto-bridge/test/spec/polyfill.spec.ts b/packages/react-native-webcrypto-bridge/test/spec/polyfill.spec.ts
new file mode 100644
index 0000000..ee320c4
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/test/spec/polyfill.spec.ts
@@ -0,0 +1,30 @@
+import { installWebCryptoPolyfill } from '../../src/index';
+
+describe('@okta/react-native-webcrypto-bridge', () => {
+ beforeAll(() => {
+ installWebCryptoPolyfill();
+ });
+
+ it('should install crypto global', () => {
+ expect(global.crypto).toBeDefined();
+ expect(global.crypto.subtle).toBeDefined();
+ expect(global.crypto.getRandomValues).toBeDefined();
+ });
+
+ it('should have SubtleCrypto methods', () => {
+ expect(typeof global.crypto.subtle.digest).toBe('function');
+ expect(typeof global.crypto.subtle.generateKey).toBe('function');
+ expect(typeof global.crypto.subtle.exportKey).toBe('function');
+ expect(typeof global.crypto.subtle.importKey).toBe('function');
+ expect(typeof global.crypto.subtle.sign).toBe('function');
+ expect(typeof global.crypto.subtle.verify).toBe('function');
+ });
+
+ it('should have randomUUID', () => {
+ expect(typeof global.crypto.randomUUID).toBe('function');
+ const uuid = global.crypto.randomUUID();
+ expect(uuid).toBeDefined();
+ expect(typeof uuid).toBe('string');
+ });
+});
+
diff --git a/packages/react-native-webcrypto-bridge/test/spec/verify.spec.ts b/packages/react-native-webcrypto-bridge/test/spec/verify.spec.ts
new file mode 100644
index 0000000..737d647
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/test/spec/verify.spec.ts
@@ -0,0 +1,187 @@
+import { installWebCryptoPolyfill } from '../../src/index';
+
+describe('subtle.verify', () => {
+ let testKey: CryptoKey;
+
+ beforeAll(async () => {
+ installWebCryptoPolyfill();
+
+ // Import a test key to use for verification
+ const jwk = {
+ kty: 'RSA',
+ n: 'test-modulus',
+ e: 'AQAB',
+ alg: 'RS256',
+ };
+
+ testKey = await global.crypto.subtle.importKey(
+ 'jwk',
+ jwk,
+ { name: 'RSASSA-PKCS1-v1_5', hash: { name: 'SHA-256' } },
+ false,
+ ['verify']
+ );
+ });
+
+ it('should verify a signature (mock returns true)', async () => {
+ const signature = new Uint8Array(256).buffer; // mock RSA signature
+ const data = new TextEncoder().encode('test data');
+
+ const result = await global.crypto.subtle.verify(
+ { name: 'RSASSA-PKCS1-v1_5' },
+ testKey,
+ signature,
+ data
+ );
+
+ expect(result).toBe(true);
+ });
+
+ it('should accept algorithm as a string', async () => {
+ const signature = new Uint8Array(256).buffer;
+ const data = new TextEncoder().encode('test data');
+
+ const result = await global.crypto.subtle.verify(
+ 'RSASSA-PKCS1-v1_5',
+ testKey,
+ signature,
+ data
+ );
+
+ expect(result).toBe(true);
+ });
+
+ it('should reject if key is not in the key map', async () => {
+ const orphanKey: CryptoKey = {
+ algorithm: { name: 'RSASSA-PKCS1-v1_5' },
+ extractable: false,
+ type: 'public',
+ usages: ['verify'],
+ };
+
+ const signature = new Uint8Array(256).buffer;
+ const data = new TextEncoder().encode('test data');
+
+ try {
+ await global.crypto.subtle.verify(
+ { name: 'RSASSA-PKCS1-v1_5' },
+ orphanKey,
+ signature,
+ data
+ );
+ fail('Expected DOMException to be thrown');
+ } catch (error) {
+ if (error instanceof DOMException) {
+ expect(error.name).toBe('InvalidAccessError');
+ } else {
+ throw error;
+ }
+ }
+ });
+
+ it('should reject unsupported algorithm', async () => {
+ const signature = new Uint8Array(256).buffer;
+ const data = new TextEncoder().encode('test data');
+
+ try {
+ await global.crypto.subtle.verify(
+ { name: 'RSA-PSS' },
+ testKey,
+ signature,
+ data
+ );
+ fail('Expected DOMException to be thrown');
+ } catch (error) {
+ if (error instanceof DOMException) {
+ expect(error.name).toBe('NotSupportedError');
+ } else {
+ throw error;
+ }
+ }
+ });
+
+ it('should reject if key usages do not include verify', async () => {
+ const signOnlyJwk = {
+ kty: 'RSA',
+ n: 'another-modulus',
+ e: 'AQAB',
+ alg: 'RS256',
+ };
+
+ const signOnlyKey = await global.crypto.subtle.importKey(
+ 'jwk',
+ signOnlyJwk,
+ { name: 'RSASSA-PKCS1-v1_5', hash: { name: 'SHA-256' } },
+ false,
+ ['sign'] // no 'verify' usage
+ );
+
+ const signature = new Uint8Array(256).buffer;
+ const data = new TextEncoder().encode('test data');
+
+ try {
+ await global.crypto.subtle.verify(
+ { name: 'RSASSA-PKCS1-v1_5' },
+ signOnlyKey,
+ signature,
+ data
+ );
+ fail('Expected DOMException to be thrown');
+ } catch (error) {
+ if (error instanceof DOMException) {
+ expect(error.name).toBe('InvalidAccessError');
+ } else {
+ throw error;
+ }
+ }
+ });
+});
+
+describe('stubbed DPoP methods', () => {
+ beforeAll(() => {
+ installWebCryptoPolyfill();
+ });
+
+ it('exportKey should throw NotSupportedError', async () => {
+ try {
+ await global.crypto.subtle.exportKey('jwk', {} as CryptoKey);
+ fail('Expected DOMException to be thrown');
+ } catch (error) {
+ if (error instanceof DOMException) {
+ expect(error.name).toBe('NotSupportedError');
+ } else {
+ throw error;
+ }
+ }
+ });
+
+ it('sign should throw NotSupportedError', async () => {
+ try {
+ await global.crypto.subtle.sign('RSASSA-PKCS1-v1_5', {} as CryptoKey, new ArrayBuffer(0));
+ fail('Expected DOMException to be thrown');
+ } catch (error) {
+ if (error instanceof DOMException) {
+ expect(error.name).toBe('NotSupportedError');
+ } else {
+ throw error;
+ }
+ }
+ });
+
+ it('generateKey should throw NotSupportedError', async () => {
+ try {
+ await global.crypto.subtle.generateKey(
+ { name: 'RSASSA-PKCS1-v1_5' } as any,
+ false,
+ ['sign', 'verify']
+ );
+ fail('Expected DOMException to be thrown');
+ } catch (error) {
+ if (error instanceof DOMException) {
+ expect(error.name).toBe('NotSupportedError');
+ } else {
+ throw error;
+ }
+ }
+ });
+});
diff --git a/packages/react-native-webcrypto-bridge/test/tsconfig.json b/packages/react-native-webcrypto-bridge/test/tsconfig.json
new file mode 100644
index 0000000..2378c0a
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/test/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "extends": "@repo/typescript-config/jest.json",
+ "compilerOptions": {
+ "paths": {
+ "src/*": ["../src/*"]
+ },
+ "jsx": "react"
+ },
+ "lib": [
+ "dom"
+ ],
+ "include": [
+ "helpers/**/*.ts",
+ "spec/**/*.ts",
+ "jest.setup.ts",
+ ],
+ "exclude": [
+ "node_modules"
+ ]
+}
\ No newline at end of file
diff --git a/packages/react-native-webcrypto-bridge/tsconfig.json b/packages/react-native-webcrypto-bridge/tsconfig.json
new file mode 100644
index 0000000..569eae5
--- /dev/null
+++ b/packages/react-native-webcrypto-bridge/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "extends": "@repo/typescript-config/base.json",
+ "compilerOptions": {
+ "rootDir": "./src",
+ "baseUrl": "./",
+ "outDir": "./dist/types",
+ "jsx": "react-native"
+ },
+ "lib": [
+ "dom"
+ ],
+ "files": [
+ "package.json"
+ ],
+ "include": [
+ "./src/**/*.ts",
+ "./src/**/*.tsx"
+ ],
+ "exclude": [
+ "node_modules",
+ "dist"
+ ]
+}
\ No newline at end of file
diff --git a/packages/spa-platform/src/Credential/TokenStorage.ts b/packages/spa-platform/src/Credential/TokenStorage.ts
index 04206eb..6658995 100644
--- a/packages/spa-platform/src/Credential/TokenStorage.ts
+++ b/packages/spa-platform/src/Credential/TokenStorage.ts
@@ -62,6 +62,10 @@ export class BrowserTokenStorage implements TokenStorage {
return `${this.tokenPrefix}:v2:${id}`;
}
+ async loadDefaultTokenId (): Promise {
+ return this.defaultTokenId;
+ }
+
async setDefaultTokenId (id: string | null): Promise {
if (id === this.defaultTokenId) {
return;
diff --git a/scripts/bacon/publish.sh b/scripts/bacon/publish.sh
index 35e60d2..7e90b3a 100755
--- a/scripts/bacon/publish.sh
+++ b/scripts/bacon/publish.sh
@@ -15,6 +15,11 @@ npm config set @okta:registry ${REGISTRY}
PUBLISHED_PACKAGES=""
PROMOTABLE_VERSIONS=""
+if ! yarn build; then
+ echo "build failed! Exiting..."
+ exit ${TEST_FAILURE}
+fi
+
echo "Publishing packages..."
for pkg in ./packages/*
do
@@ -24,12 +29,6 @@ do
create_log_group "Publishing $pkg_name"
echo "Publishing $pkg_name..."
- # Build
- if ! yarn build; then
- echo "build failed! Exiting..."
- exit ${TEST_FAILURE}
- fi
-
# record the artifact version before the SHA is appended
artifact_version="$(ci-pkginfo -t pkgname)@$(ci-pkginfo -t pkgsemver)"
diff --git a/tooling/eslint-config/react-native-sdk.js b/tooling/eslint-config/react-native-sdk.js
new file mode 100644
index 0000000..18b4901
--- /dev/null
+++ b/tooling/eslint-config/react-native-sdk.js
@@ -0,0 +1,9 @@
+module.exports = {
+ extends: ["@repo/eslint-config/sdk.js"],
+ parser: "@typescript-eslint/parser",
+ ignorePatterns: [
+ 'android/',
+ 'ios/',
+ 'react-native.config.js'
+ ],
+};
diff --git a/tooling/eslint-config/sdk.js b/tooling/eslint-config/sdk.js
index a3420f1..7661f80 100644
--- a/tooling/eslint-config/sdk.js
+++ b/tooling/eslint-config/sdk.js
@@ -43,7 +43,8 @@ module.exports = {
},
{
files: [
- 'test/spec/**/*'
+ 'test/spec/**/*',
+ 'test/jest.setup.js'
],
plugins: [
'jest'
diff --git a/tooling/jest-helpers/package.json b/tooling/jest-helpers/package.json
index 7d9a30f..f2b9c53 100644
--- a/tooling/jest-helpers/package.json
+++ b/tooling/jest-helpers/package.json
@@ -4,9 +4,11 @@
"private": true,
"files": [
"browser/*",
- "node/*"
+ "node/*",
+ "react-native/*"
],
"devDependencies": {
+ "@babel/plugin-transform-class-static-block": "^7.28.6",
"@types/jest": "^29.4.4",
"@types/node": "^22.9.0",
"jest": "^29.7.0",
diff --git a/tooling/jest-helpers/react-native/babel.config.js b/tooling/jest-helpers/react-native/babel.config.js
new file mode 100644
index 0000000..8431fc8
--- /dev/null
+++ b/tooling/jest-helpers/react-native/babel.config.js
@@ -0,0 +1,9 @@
+module.exports = {
+ presets: [
+ 'module:@react-native/babel-preset',
+ ],
+ plugins: [
+ '@babel/plugin-transform-class-static-block'
+ ],
+};
+
diff --git a/tooling/jest-helpers/react-native/helpers.ts b/tooling/jest-helpers/react-native/helpers.ts
new file mode 100644
index 0000000..b7f768b
--- /dev/null
+++ b/tooling/jest-helpers/react-native/helpers.ts
@@ -0,0 +1 @@
+export * from '../helpers';
diff --git a/tooling/jest-helpers/react-native/jest-preset.js b/tooling/jest-helpers/react-native/jest-preset.js
new file mode 100644
index 0000000..6920b9b
--- /dev/null
+++ b/tooling/jest-helpers/react-native/jest-preset.js
@@ -0,0 +1,35 @@
+const { createJsWithBabelPreset } = require('ts-jest');
+const path = require('path');
+
+
+const jsWithBabelPreset = createJsWithBabelPreset({
+ tsconfig: '/test/tsconfig.json',
+ babelConfig: path.join(__dirname, 'babel.config.js')
+});
+
+module.exports = {
+ roots: [''],
+ preset: 'react-native',
+ globals: {
+ __PKG_NAME__: 'PLACEHOLDER',
+ __PKG_VERSION__: 'PLACEHOLDER'
+ },
+ transform: jsWithBabelPreset.transform,
+ extensionsToTreatAsEsm: ['.ts'],
+ moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
+ restoreMocks: true,
+ clearMocks: true,
+ testMatch: [
+ '**/test/spec/**/*.{js,ts}'
+ ],
+ // Don't transform node_modules - mock them instead via jest.mock() in setup
+ transformIgnorePatterns: [
+ 'node_modules/(?!(@okta)/)'
+ ],
+ collectCoverageFrom: [
+ 'src/**/*.ts'
+ ],
+ moduleNameMapper: {
+ '^src/(.*)$': '/src/$1',
+ }
+};
diff --git a/yarn.lock b/yarn.lock
index 8265de8..22c8022 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -201,6 +201,15 @@
"@babel/highlight" "^7.24.2"
picocolors "^1.0.0"
+"@babel/code-frame@^7.20.0", "@babel/code-frame@^7.28.6", "@babel/code-frame@^7.29.0":
+ version "7.29.0"
+ resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.29.0.tgz#7cd7a59f15b3cc0dcd803038f7792712a7d0b15c"
+ integrity sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==
+ dependencies:
+ "@babel/helper-validator-identifier" "^7.28.5"
+ js-tokens "^4.0.0"
+ picocolors "^1.1.1"
+
"@babel/code-frame@^7.21.4", "@babel/code-frame@^7.24.6":
version "7.24.6"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.6.tgz#ab88da19344445c3d8889af2216606d3329f3ef2"
@@ -343,6 +352,17 @@
"@jridgewell/trace-mapping" "^0.3.28"
jsesc "^3.0.2"
+"@babel/generator@^7.29.0", "@babel/generator@^7.29.1":
+ version "7.29.1"
+ resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.29.1.tgz#d09876290111abbb00ef962a7b83a5307fba0d50"
+ integrity sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==
+ dependencies:
+ "@babel/parser" "^7.29.0"
+ "@babel/types" "^7.29.0"
+ "@jridgewell/gen-mapping" "^0.3.12"
+ "@jridgewell/trace-mapping" "^0.3.28"
+ jsesc "^3.0.2"
+
"@babel/helper-annotate-as-pure@^7.24.6":
version "7.24.6"
resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.6.tgz#517af93abc77924f9b2514c407bbef527fb8938d"
@@ -416,6 +436,19 @@
"@babel/traverse" "^7.28.3"
semver "^6.3.1"
+"@babel/helper-create-class-features-plugin@^7.28.6":
+ version "7.28.6"
+ resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz#611ff5482da9ef0db6291bcd24303400bca170fb"
+ integrity sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==
+ dependencies:
+ "@babel/helper-annotate-as-pure" "^7.27.3"
+ "@babel/helper-member-expression-to-functions" "^7.28.5"
+ "@babel/helper-optimise-call-expression" "^7.27.1"
+ "@babel/helper-replace-supers" "^7.28.6"
+ "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1"
+ "@babel/traverse" "^7.28.6"
+ semver "^6.3.1"
+
"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.27.1":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz#05b0882d97ba1d4d03519e4bce615d70afa18c53"
@@ -511,6 +544,14 @@
"@babel/traverse" "^7.27.1"
"@babel/types" "^7.27.1"
+"@babel/helper-member-expression-to-functions@^7.28.5":
+ version "7.28.5"
+ resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz#f3e07a10be37ed7a63461c63e6929575945a6150"
+ integrity sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==
+ dependencies:
+ "@babel/traverse" "^7.28.5"
+ "@babel/types" "^7.28.5"
+
"@babel/helper-module-imports@^7.24.3":
version "7.24.3"
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz#6ac476e6d168c7c23ff3ba3cf4f7841d46ac8128"
@@ -586,6 +627,11 @@
resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.6.tgz#fa02a32410a15a6e8f8185bcbf608f10528d2a24"
integrity sha512-MZG/JcWfxybKwsA9N9PmtF2lOSFSEMVCpIRrbxccZFLJPrJciJdG/UhSh5W96GEteJI2ARqm5UAHxISwRDLSNg==
+"@babel/helper-plugin-utils@^7.28.6":
+ version "7.28.6"
+ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz#6f13ea251b68c8532e985fd532f28741a8af9ac8"
+ integrity sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==
+
"@babel/helper-remap-async-to-generator@^7.24.6":
version "7.24.6"
resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.24.6.tgz#c96ceb9846e877d806ce82a1521230ea7e0fc354"
@@ -613,6 +659,15 @@
"@babel/helper-optimise-call-expression" "^7.27.1"
"@babel/traverse" "^7.27.1"
+"@babel/helper-replace-supers@^7.28.6":
+ version "7.28.6"
+ resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz#94aa9a1d7423a00aead3f204f78834ce7d53fe44"
+ integrity sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==
+ dependencies:
+ "@babel/helper-member-expression-to-functions" "^7.28.5"
+ "@babel/helper-optimise-call-expression" "^7.27.1"
+ "@babel/traverse" "^7.28.6"
+
"@babel/helper-simple-access@^7.24.5":
version "7.24.5"
resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.24.5.tgz#50da5b72f58c16b07fbd992810be6049478e85ba"
@@ -679,6 +734,11 @@
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz#a7054dcc145a967dd4dc8fee845a57c1316c9df8"
integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==
+"@babel/helper-validator-identifier@^7.28.5":
+ version "7.28.5"
+ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4"
+ integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==
+
"@babel/helper-validator-option@^7.23.5":
version "7.23.5"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz#907a3fbd4523426285365d1206c423c4c5520307"
@@ -762,18 +822,18 @@
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.5.tgz#4a4d5ab4315579e5398a82dcf636ca80c3392790"
integrity sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==
-"@babel/parser@^7.20.0", "@babel/parser@^7.25.3", "@babel/parser@^7.27.2", "@babel/parser@^7.27.3", "@babel/parser@^7.27.4":
+"@babel/parser@^7.24.6":
+ version "7.24.6"
+ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.6.tgz#5e030f440c3c6c78d195528c3b688b101a365328"
+ integrity sha512-eNZXdfU35nJC2h24RznROuOpO94h6x8sg9ju0tT9biNtLZ2vuP8SduLqqV+/8+cebSLV9SJEAN5Z3zQbJG/M+Q==
+
+"@babel/parser@^7.25.3", "@babel/parser@^7.27.2", "@babel/parser@^7.27.3", "@babel/parser@^7.27.4":
version "7.27.4"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.27.4.tgz#f92e89e4f51847be05427285836fc88341c956df"
integrity sha512-BRmLHGwpUqLFR2jzx9orBuX/ABDkj2jLKOXrHDTN2aOKL+jFDDKaRNo9nyYsIl9h/UE/7lMKdDjKQQyxKKDZ7g==
dependencies:
"@babel/types" "^7.27.3"
-"@babel/parser@^7.24.6":
- version "7.24.6"
- resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.6.tgz#5e030f440c3c6c78d195528c3b688b101a365328"
- integrity sha512-eNZXdfU35nJC2h24RznROuOpO94h6x8sg9ju0tT9biNtLZ2vuP8SduLqqV+/8+cebSLV9SJEAN5Z3zQbJG/M+Q==
-
"@babel/parser@^7.28.0":
version "7.28.0"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.0.tgz#979829fbab51a29e13901e5a80713dbcb840825e"
@@ -788,6 +848,13 @@
dependencies:
"@babel/types" "^7.28.4"
+"@babel/parser@^7.28.6", "@babel/parser@^7.29.0":
+ version "7.29.2"
+ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.29.2.tgz#58bd50b9a7951d134988a1ae177a35ef9a703ba1"
+ integrity sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==
+ dependencies:
+ "@babel/types" "^7.29.0"
+
"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.27.1":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz#61dd8a8e61f7eb568268d1b5f129da3eee364bf9"
@@ -1082,6 +1149,14 @@
"@babel/helper-create-class-features-plugin" "^7.27.1"
"@babel/helper-plugin-utils" "^7.27.1"
+"@babel/plugin-transform-class-static-block@^7.27.1", "@babel/plugin-transform-class-static-block@^7.28.6":
+ version "7.28.6"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.6.tgz#1257491e8259c6d125ac4d9a6f39f9d2bf3dba70"
+ integrity sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==
+ dependencies:
+ "@babel/helper-create-class-features-plugin" "^7.28.6"
+ "@babel/helper-plugin-utils" "^7.28.6"
+
"@babel/plugin-transform-class-static-block@^7.28.3":
version "7.28.3"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.3.tgz#d1b8e69b54c9993bc558203e1f49bfc979bfd852"
@@ -1718,6 +1793,15 @@
"@babel/parser" "^7.27.2"
"@babel/types" "^7.27.1"
+"@babel/template@^7.28.6":
+ version "7.28.6"
+ resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.28.6.tgz#0e7e56ecedb78aeef66ce7972b082fce76a23e57"
+ integrity sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==
+ dependencies:
+ "@babel/code-frame" "^7.28.6"
+ "@babel/parser" "^7.28.6"
+ "@babel/types" "^7.28.6"
+
"@babel/traverse--for-generate-function-map@npm:@babel/traverse@^7.25.3":
version "7.27.4"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.27.4.tgz#b0045ac7023c8472c3d35effd7cc9ebd638da6ea"
@@ -1789,6 +1873,19 @@
"@babel/types" "^7.28.4"
debug "^4.3.1"
+"@babel/traverse@^7.28.5", "@babel/traverse@^7.28.6", "@babel/traverse@^7.29.0":
+ version "7.29.0"
+ resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.29.0.tgz#f323d05001440253eead3c9c858adbe00b90310a"
+ integrity sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==
+ dependencies:
+ "@babel/code-frame" "^7.29.0"
+ "@babel/generator" "^7.29.0"
+ "@babel/helper-globals" "^7.28.0"
+ "@babel/parser" "^7.29.0"
+ "@babel/template" "^7.28.6"
+ "@babel/types" "^7.29.0"
+ debug "^4.3.1"
+
"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.21.3", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.24.0", "@babel/types@^7.24.5", "@babel/types@^7.3.3":
version "7.24.5"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.24.5.tgz#7661930afc638a5383eb0c4aee59b74f38db84d7"
@@ -1798,14 +1895,6 @@
"@babel/helper-validator-identifier" "^7.24.5"
to-fast-properties "^2.0.0"
-"@babel/types@^7.20.0", "@babel/types@^7.25.2", "@babel/types@^7.27.1", "@babel/types@^7.27.3":
- version "7.27.3"
- resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.27.3.tgz#c0257bedf33aad6aad1f406d35c44758321eb3ec"
- integrity sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw==
- dependencies:
- "@babel/helper-string-parser" "^7.27.1"
- "@babel/helper-validator-identifier" "^7.27.1"
-
"@babel/types@^7.24.6":
version "7.24.6"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.24.6.tgz#ba4e1f59870c10dc2fa95a274ac4feec23b21912"
@@ -1815,6 +1904,22 @@
"@babel/helper-validator-identifier" "^7.24.6"
to-fast-properties "^2.0.0"
+"@babel/types@^7.25.2", "@babel/types@^7.27.1", "@babel/types@^7.27.3":
+ version "7.27.3"
+ resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.27.3.tgz#c0257bedf33aad6aad1f406d35c44758321eb3ec"
+ integrity sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw==
+ dependencies:
+ "@babel/helper-string-parser" "^7.27.1"
+ "@babel/helper-validator-identifier" "^7.27.1"
+
+"@babel/types@^7.26.0", "@babel/types@^7.28.5", "@babel/types@^7.28.6", "@babel/types@^7.29.0":
+ version "7.29.0"
+ resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.29.0.tgz#9f5b1e838c446e72cf3cd4b918152b8c605e37c7"
+ integrity sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==
+ dependencies:
+ "@babel/helper-string-parser" "^7.27.1"
+ "@babel/helper-validator-identifier" "^7.28.5"
+
"@babel/types@^7.28.0":
version "7.28.2"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.2.tgz#da9db0856a9a88e0a13b019881d7513588cf712b"
@@ -2266,29 +2371,30 @@
"@eslint/core" "^0.14.0"
levn "^0.4.1"
-"@expo/cli@0.24.14":
- version "0.24.14"
- resolved "https://registry.yarnpkg.com/@expo/cli/-/cli-0.24.14.tgz#af2e7ea5a08e3574e868cb0ec2110e68d32672e0"
- integrity sha512-o+QYyfIBhSRTgaywKTLJhm2Fg5PrSeUVCXS+uQySamgoMjLNhHa8QwE64mW/FmJr5hZLiqUEQxb60FK4JcyqXg==
+"@expo/cli@54.0.23":
+ version "54.0.23"
+ resolved "https://registry.yarnpkg.com/@expo/cli/-/cli-54.0.23.tgz#e8a7dc4e1f2a8a5361afd80bcc352014b57a87ac"
+ integrity sha512-km0h72SFfQCmVycH/JtPFTVy69w6Lx1cHNDmfLfQqgKFYeeHTjx7LVDP4POHCtNxFP2UeRazrygJhlh4zz498g==
dependencies:
"@0no-co/graphql.web" "^1.0.8"
- "@babel/runtime" "^7.20.0"
- "@expo/code-signing-certificates" "^0.0.5"
- "@expo/config" "~11.0.10"
- "@expo/config-plugins" "~10.0.2"
- "@expo/devcert" "^1.1.2"
- "@expo/env" "~1.0.5"
- "@expo/image-utils" "^0.7.4"
- "@expo/json-file" "^9.1.4"
- "@expo/metro-config" "~0.20.14"
- "@expo/osascript" "^2.2.4"
- "@expo/package-manager" "^1.8.4"
- "@expo/plist" "^0.3.4"
- "@expo/prebuild-config" "^9.0.6"
+ "@expo/code-signing-certificates" "^0.0.6"
+ "@expo/config" "~12.0.13"
+ "@expo/config-plugins" "~54.0.4"
+ "@expo/devcert" "^1.2.1"
+ "@expo/env" "~2.0.8"
+ "@expo/image-utils" "^0.8.8"
+ "@expo/json-file" "^10.0.8"
+ "@expo/metro" "~54.2.0"
+ "@expo/metro-config" "~54.0.14"
+ "@expo/osascript" "^2.3.8"
+ "@expo/package-manager" "^1.9.10"
+ "@expo/plist" "^0.4.8"
+ "@expo/prebuild-config" "^54.0.8"
+ "@expo/schema-utils" "^0.1.8"
"@expo/spawn-async" "^1.7.2"
"@expo/ws-tunnel" "^1.0.1"
"@expo/xcpretty" "^4.3.0"
- "@react-native/dev-middleware" "0.79.3"
+ "@react-native/dev-middleware" "0.81.5"
"@urql/core" "^5.0.6"
"@urql/exchange-retry" "^1.3.0"
accepts "^1.3.8"
@@ -2302,12 +2408,13 @@
connect "^3.7.0"
debug "^4.3.4"
env-editor "^0.4.1"
+ expo-server "^1.0.5"
freeport-async "^2.0.0"
getenv "^2.0.0"
- glob "^10.4.2"
+ glob "^13.0.0"
lan-network "^0.1.6"
minimatch "^9.0.0"
- node-forge "^1.3.1"
+ node-forge "^1.3.3"
npm-package-arg "^11.0.0"
ora "^3.4.0"
picomatch "^3.0.1"
@@ -2327,33 +2434,32 @@
source-map-support "~0.5.21"
stacktrace-parser "^0.1.10"
structured-headers "^0.4.1"
- tar "^7.4.3"
+ tar "^7.5.2"
terminal-link "^2.1.1"
undici "^6.18.2"
wrap-ansi "^7.0.0"
ws "^8.12.1"
-"@expo/code-signing-certificates@^0.0.5":
- version "0.0.5"
- resolved "https://registry.yarnpkg.com/@expo/code-signing-certificates/-/code-signing-certificates-0.0.5.tgz#a693ff684fb20c4725dade4b88a6a9f96b02496c"
- integrity sha512-BNhXkY1bblxKZpltzAx98G2Egj9g1Q+JRcvR7E99DOj862FTCX+ZPsAUtPTr7aHxwtrL7+fL3r0JSmM9kBm+Bw==
+"@expo/code-signing-certificates@^0.0.6":
+ version "0.0.6"
+ resolved "https://registry.yarnpkg.com/@expo/code-signing-certificates/-/code-signing-certificates-0.0.6.tgz#6b7b22830cb69c77a45e357c2f3aa7ab436ac772"
+ integrity sha512-iNe0puxwBNEcuua9gmTGzq+SuMDa0iATai1FlFTMHJ/vUmKvN/V//drXoLJkVb5i5H3iE/n/qIJxyoBnXouD0w==
dependencies:
- node-forge "^1.2.1"
- nullthrows "^1.1.1"
+ node-forge "^1.3.3"
-"@expo/config-plugins@~10.0.2":
- version "10.0.2"
- resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-10.0.2.tgz#040867991e9c8c527b4f5c13a47bcf040a7479fe"
- integrity sha512-TzUn3pPdpwCS0yYaSlZOClgDmCX8N4I2lfgitX5oStqmvpPtB+vqtdyqsVM02fQ2tlJIAqwBW+NHaHqqy8Jv7g==
+"@expo/config-plugins@~54.0.4":
+ version "54.0.4"
+ resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-54.0.4.tgz#b31cb16f6651342abcdafba600118245ecd9fb00"
+ integrity sha512-g2yXGICdoOw5i3LkQSDxl2Q5AlQCrG7oniu0pCPPO+UxGb7He4AFqSvPSy8HpRUj55io17hT62FTjYRD+d6j3Q==
dependencies:
- "@expo/config-types" "^53.0.3"
- "@expo/json-file" "~9.1.4"
- "@expo/plist" "^0.3.4"
+ "@expo/config-types" "^54.0.10"
+ "@expo/json-file" "~10.0.8"
+ "@expo/plist" "^0.4.8"
"@expo/sdk-runtime-versions" "^1.0.0"
chalk "^4.1.2"
debug "^4.3.5"
- getenv "^1.0.0"
- glob "^10.4.2"
+ getenv "^2.0.0"
+ glob "^13.0.0"
resolve-from "^5.0.0"
semver "^7.5.4"
slash "^3.0.0"
@@ -2361,180 +2467,205 @@
xcode "^3.0.1"
xml2js "0.6.0"
-"@expo/config-types@^53.0.3", "@expo/config-types@^53.0.4":
- version "53.0.4"
- resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-53.0.4.tgz#fe64fac734531ae883d18529b32586c23ffb1ceb"
- integrity sha512-0s+9vFx83WIToEr0Iwy4CcmiUXa5BgwBmEjylBB2eojX5XAMm9mJvw9KpjAb8m7zq2G0Q6bRbeufkzgbipuNQg==
+"@expo/config-types@^54.0.10":
+ version "54.0.10"
+ resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-54.0.10.tgz#688f4338255d2fdea970f44e2dfd8e8d37dec292"
+ integrity sha512-/J16SC2an1LdtCZ67xhSkGXpALYUVUNyZws7v+PVsFZxClYehDSoKLqyRaGkpHlYrCc08bS0RF5E0JV6g50psA==
-"@expo/config@~11.0.10", "@expo/config@~11.0.9":
- version "11.0.10"
- resolved "https://registry.yarnpkg.com/@expo/config/-/config-11.0.10.tgz#559d9425a4e0de4fab96ccac01ff40f5cebbc04b"
- integrity sha512-8S8Krr/c5lnl0eF03tA2UGY9rGBhZcbWKz2UWw5dpL/+zstwUmog8oyuuC8aRcn7GiTQLlbBkxcMeT8sOGlhbA==
+"@expo/config@~12.0.11", "@expo/config@~12.0.13":
+ version "12.0.13"
+ resolved "https://registry.yarnpkg.com/@expo/config/-/config-12.0.13.tgz#8e696e6121c3c364e1dd527f595cf0a1d9386828"
+ integrity sha512-Cu52arBa4vSaupIWsF0h7F/Cg//N374nYb7HAxV0I4KceKA7x2UXpYaHOL7EEYYvp7tZdThBjvGpVmr8ScIvaQ==
dependencies:
"@babel/code-frame" "~7.10.4"
- "@expo/config-plugins" "~10.0.2"
- "@expo/config-types" "^53.0.4"
- "@expo/json-file" "^9.1.4"
+ "@expo/config-plugins" "~54.0.4"
+ "@expo/config-types" "^54.0.10"
+ "@expo/json-file" "^10.0.8"
deepmerge "^4.3.1"
- getenv "^1.0.0"
- glob "^10.4.2"
+ getenv "^2.0.0"
+ glob "^13.0.0"
require-from-string "^2.0.2"
resolve-from "^5.0.0"
resolve-workspace-root "^2.0.0"
semver "^7.6.0"
slugify "^1.3.4"
- sucrase "3.35.0"
+ sucrase "~3.35.1"
-"@expo/devcert@^1.1.2":
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/@expo/devcert/-/devcert-1.2.0.tgz#7b32c2d959e36baaa0649433395e5170c808b44f"
- integrity sha512-Uilcv3xGELD5t/b0eM4cxBFEKQRIivB3v7i+VhWLV/gL98aw810unLKKJbGAxAIhY6Ipyz8ChWibFsKFXYwstA==
+"@expo/devcert@^1.2.1":
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/@expo/devcert/-/devcert-1.2.1.tgz#1a687985bea1670866e54d5ba7c0ced963c354f4"
+ integrity sha512-qC4eaxmKMTmJC2ahwyui6ud8f3W60Ss7pMkpBq40Hu3zyiAaugPXnZ24145U7K36qO9UHdZUVxsCvIpz2RYYCA==
dependencies:
"@expo/sudo-prompt" "^9.3.1"
debug "^3.1.0"
- glob "^10.4.2"
-"@expo/env@~1.0.5":
- version "1.0.5"
- resolved "https://registry.yarnpkg.com/@expo/env/-/env-1.0.5.tgz#b3b1aa18ab9838d8f40468e0321affc4c54377a2"
- integrity sha512-dtEZ4CAMaVrFu2+tezhU3FoGWtbzQl50xV+rNJE5lYVRjUflWiZkVHlHkWUlPAwDPifLy4TuissVfScGGPWR5g==
+"@expo/devtools@0.1.8":
+ version "0.1.8"
+ resolved "https://registry.yarnpkg.com/@expo/devtools/-/devtools-0.1.8.tgz#bc5b297698f78b3b67037f04593a31e688330a7a"
+ integrity sha512-SVLxbuanDjJPgc0sy3EfXUMLb/tXzp6XIHkhtPVmTWJAp+FOr6+5SeiCfJrCzZFet0Ifyke2vX3sFcKwEvCXwQ==
+ dependencies:
+ chalk "^4.1.2"
+
+"@expo/env@~2.0.8":
+ version "2.0.11"
+ resolved "https://registry.yarnpkg.com/@expo/env/-/env-2.0.11.tgz#3a10d9142b1833566bdfb39de1c062f7a8b8ac38"
+ integrity sha512-xV+ps6YCW7XIPVUwFVCRN2nox09dnRwy8uIjwHWTODu0zFw4kp4omnVkl0OOjuu2XOe7tdgAHxikrkJt9xB/7Q==
dependencies:
chalk "^4.0.0"
debug "^4.3.4"
dotenv "~16.4.5"
dotenv-expand "~11.0.6"
- getenv "^1.0.0"
+ getenv "^2.0.0"
-"@expo/fingerprint@0.13.0":
- version "0.13.0"
- resolved "https://registry.yarnpkg.com/@expo/fingerprint/-/fingerprint-0.13.0.tgz#5f5600122940ac381ed697743c10bdbddf6c55c1"
- integrity sha512-3IwpH0p3uO8jrJSLOUNDzJVh7VEBod0emnCBq0hD72sy6ICmzauM6Xf4he+2Tip7fzImCJRd63GaehV+CCtpvA==
+"@expo/fingerprint@0.15.4":
+ version "0.15.4"
+ resolved "https://registry.yarnpkg.com/@expo/fingerprint/-/fingerprint-0.15.4.tgz#578bd1e1179a13313f7de682ebaaacb703b2b7ac"
+ integrity sha512-eYlxcrGdR2/j2M6pEDXo9zU9KXXF1vhP+V+Tl+lyY+bU8lnzrN6c637mz6Ye3em2ANy8hhUR03Raf8VsT9Ogng==
dependencies:
"@expo/spawn-async" "^1.7.2"
arg "^5.0.2"
chalk "^4.1.2"
debug "^4.3.4"
- find-up "^5.0.0"
getenv "^2.0.0"
+ glob "^13.0.0"
ignore "^5.3.1"
minimatch "^9.0.0"
p-limit "^3.1.0"
resolve-from "^5.0.0"
semver "^7.6.0"
-"@expo/image-utils@^0.7.4":
- version "0.7.4"
- resolved "https://registry.yarnpkg.com/@expo/image-utils/-/image-utils-0.7.4.tgz#8f19e53cfc3b66293d9b0749f703e667080895d0"
- integrity sha512-LcZ82EJy/t/a1avwIboeZbO6hlw8CvsIRh2k6SWPcAOvW0RqynyKFzUJsvnjWlhUzfBEn4oI7y/Pu5Xkw3KkkA==
+"@expo/image-utils@^0.8.8":
+ version "0.8.12"
+ resolved "https://registry.yarnpkg.com/@expo/image-utils/-/image-utils-0.8.12.tgz#56e34b9555745ad4d11c972fe0d1ce71c7c64c41"
+ integrity sha512-3KguH7kyKqq7pNwLb9j6BBdD/bjmNwXZG/HPWT6GWIXbwrvAJt2JNyYTP5agWJ8jbbuys1yuCzmkX+TU6rmI7A==
dependencies:
"@expo/spawn-async" "^1.7.2"
chalk "^4.0.0"
- getenv "^1.0.0"
+ getenv "^2.0.0"
jimp-compact "0.16.1"
parse-png "^2.1.0"
resolve-from "^5.0.0"
semver "^7.6.0"
- temp-dir "~2.0.0"
- unique-string "~2.0.0"
-"@expo/json-file@^9.1.4", "@expo/json-file@~9.1.4":
- version "9.1.4"
- resolved "https://registry.yarnpkg.com/@expo/json-file/-/json-file-9.1.4.tgz#e719d092c08afb3234643f9285e57c6a24989327"
- integrity sha512-7Bv86X27fPERGhw8aJEZvRcH9sk+9BenDnEmrI3ZpywKodYSBgc8lX9Y32faNVQ/p0YbDK9zdJ0BfAKNAOyi0A==
+"@expo/json-file@^10.0.12", "@expo/json-file@^10.0.8", "@expo/json-file@~10.0.8":
+ version "10.0.12"
+ resolved "https://registry.yarnpkg.com/@expo/json-file/-/json-file-10.0.12.tgz#411216996ab6df6765d463de22283b3f30e49ccb"
+ integrity sha512-inbDycp1rMAelAofg7h/mMzIe+Owx6F7pur3XdQ3EPTy00tme+4P6FWgHKUcjN8dBSrnbRNpSyh5/shzHyVCyQ==
dependencies:
- "@babel/code-frame" "~7.10.4"
+ "@babel/code-frame" "^7.20.0"
json5 "^2.2.3"
-"@expo/metro-config@0.20.14", "@expo/metro-config@~0.20.14":
- version "0.20.14"
- resolved "https://registry.yarnpkg.com/@expo/metro-config/-/metro-config-0.20.14.tgz#5abf8cd6454fe7f75c1f8529cf79619da32af82d"
- integrity sha512-tYDDubuZycK+NX00XN7BMu73kBur/evOPcKfxc+UBeFfgN2EifOITtdwSUDdRsbtJ2OnXwMY1HfRUG3Lq3l4cw==
+"@expo/metro-config@54.0.14", "@expo/metro-config@~54.0.14":
+ version "54.0.14"
+ resolved "https://registry.yarnpkg.com/@expo/metro-config/-/metro-config-54.0.14.tgz#e455dfb2bae9473ec665bc830d651baa709c1e8a"
+ integrity sha512-hxpLyDfOR4L23tJ9W1IbJJsG7k4lv2sotohBm/kTYyiG+pe1SYCAWsRmgk+H42o/wWf/HQjE5k45S5TomGLxNA==
dependencies:
+ "@babel/code-frame" "^7.20.0"
"@babel/core" "^7.20.0"
"@babel/generator" "^7.20.5"
- "@babel/parser" "^7.20.0"
- "@babel/types" "^7.20.0"
- "@expo/config" "~11.0.9"
- "@expo/env" "~1.0.5"
- "@expo/json-file" "~9.1.4"
+ "@expo/config" "~12.0.13"
+ "@expo/env" "~2.0.8"
+ "@expo/json-file" "~10.0.8"
+ "@expo/metro" "~54.2.0"
"@expo/spawn-async" "^1.7.2"
+ browserslist "^4.25.0"
chalk "^4.1.0"
debug "^4.3.2"
dotenv "~16.4.5"
dotenv-expand "~11.0.6"
- getenv "^1.0.0"
- glob "^10.4.2"
+ getenv "^2.0.0"
+ glob "^13.0.0"
+ hermes-parser "^0.29.1"
jsc-safe-url "^0.2.4"
- lightningcss "~1.27.0"
+ lightningcss "^1.30.1"
minimatch "^9.0.0"
postcss "~8.4.32"
resolve-from "^5.0.0"
-"@expo/metro-runtime@5.0.4":
- version "5.0.4"
- resolved "https://registry.yarnpkg.com/@expo/metro-runtime/-/metro-runtime-5.0.4.tgz#0ea7a7ecf27e3f159289705ef5160328b9fdde42"
- integrity sha512-r694MeO+7Vi8IwOsDIDzH/Q5RPMt1kUDYbiTJwnO15nIqiDwlE8HU55UlRhffKZy6s5FmxQsZ8HA+T8DqUW8cQ==
+"@expo/metro-runtime@^6.1.2":
+ version "6.1.2"
+ resolved "https://registry.yarnpkg.com/@expo/metro-runtime/-/metro-runtime-6.1.2.tgz#5a4ff117df6115f9c9d4dcc561065e16d69c078b"
+ integrity sha512-nvM+Qv45QH7pmYvP8JB1G8JpScrWND3KrMA6ZKe62cwwNiX/BjHU28Ear0v/4bQWXlOY0mv6B8CDIm8JxXde9g==
+ dependencies:
+ anser "^1.4.9"
+ pretty-format "^29.7.0"
+ stacktrace-parser "^0.1.10"
+ whatwg-fetch "^3.0.0"
-"@expo/osascript@^2.2.4":
- version "2.2.4"
- resolved "https://registry.yarnpkg.com/@expo/osascript/-/osascript-2.2.4.tgz#4414d97f91e29260a9b361529d20875430dc0af5"
- integrity sha512-Q+Oyj+1pdRiHHpev9YjqfMZzByFH8UhKvSszxa0acTveijjDhQgWrq4e9T/cchBHi0GWZpGczWyiyJkk1wM1dg==
+"@expo/metro@~54.2.0":
+ version "54.2.0"
+ resolved "https://registry.yarnpkg.com/@expo/metro/-/metro-54.2.0.tgz#6ecf4a77ae7553b73daca4206854728de76c854d"
+ integrity sha512-h68TNZPGsk6swMmLm9nRSnE2UXm48rWwgcbtAHVMikXvbxdS41NDHHeqg1rcQ9AbznDRp6SQVC2MVpDnsRKU1w==
+ dependencies:
+ metro "0.83.3"
+ metro-babel-transformer "0.83.3"
+ metro-cache "0.83.3"
+ metro-cache-key "0.83.3"
+ metro-config "0.83.3"
+ metro-core "0.83.3"
+ metro-file-map "0.83.3"
+ metro-minify-terser "0.83.3"
+ metro-resolver "0.83.3"
+ metro-runtime "0.83.3"
+ metro-source-map "0.83.3"
+ metro-symbolicate "0.83.3"
+ metro-transform-plugins "0.83.3"
+ metro-transform-worker "0.83.3"
+
+"@expo/osascript@^2.3.8":
+ version "2.4.2"
+ resolved "https://registry.yarnpkg.com/@expo/osascript/-/osascript-2.4.2.tgz#fe341cff1eb2c939da43cf58ade5504c8a5d77ca"
+ integrity sha512-/XP7PSYF2hzOZzqfjgkoWtllyeTN8dW3aM4P6YgKcmmPikKL5FdoyQhti4eh6RK5a5VrUXJTOlTNIpIHsfB5Iw==
dependencies:
"@expo/spawn-async" "^1.7.2"
- exec-async "^2.2.0"
-"@expo/package-manager@^1.8.4":
- version "1.8.4"
- resolved "https://registry.yarnpkg.com/@expo/package-manager/-/package-manager-1.8.4.tgz#6126d93b25bbfec515436833e6f6ca5677b7e8bd"
- integrity sha512-8H8tLga/NS3iS7QaX/NneRPqbObnHvVCfMCo0ShudreOFmvmgqhYjRlkZTRstSyFqefai8ONaT4VmnLHneRYYg==
+"@expo/package-manager@^1.9.10":
+ version "1.10.3"
+ resolved "https://registry.yarnpkg.com/@expo/package-manager/-/package-manager-1.10.3.tgz#151fafc1c30de3e75df78f7c0424915210779288"
+ integrity sha512-ZuXiK/9fCrIuLjPSe1VYmfp0Sa85kCMwd8QQpgyi5ufppYKRtLBg14QOgUqj8ZMbJTxE0xqzd0XR7kOs3vAK9A==
dependencies:
- "@expo/json-file" "^9.1.4"
+ "@expo/json-file" "^10.0.12"
"@expo/spawn-async" "^1.7.2"
chalk "^4.0.0"
npm-package-arg "^11.0.0"
ora "^3.4.0"
resolve-workspace-root "^2.0.0"
-"@expo/plist@^0.3.4":
- version "0.3.4"
- resolved "https://registry.yarnpkg.com/@expo/plist/-/plist-0.3.4.tgz#0c48eeff2158cf26c5c9ed4f681d24997ccfbeca"
- integrity sha512-MhBLaUJNe9FQDDU2xhSNS4SAolr6K2wuyi4+A79vYuXLkAoICsbTwcGEQJN5jPY6D9izO/jsXh5k0h+mIWQMdw==
+"@expo/plist@^0.4.8":
+ version "0.4.8"
+ resolved "https://registry.yarnpkg.com/@expo/plist/-/plist-0.4.8.tgz#e014511a4a5008cf2b832b91caa8e9f2704127cc"
+ integrity sha512-pfNtErGGzzRwHP+5+RqswzPDKkZrx+Cli0mzjQaus1ZWFsog5ibL+nVT3NcporW51o8ggnt7x813vtRbPiyOrQ==
dependencies:
"@xmldom/xmldom" "^0.8.8"
base64-js "^1.2.3"
xmlbuilder "^15.1.1"
-"@expo/prebuild-config@^9.0.5", "@expo/prebuild-config@^9.0.6":
- version "9.0.6"
- resolved "https://registry.yarnpkg.com/@expo/prebuild-config/-/prebuild-config-9.0.6.tgz#f634e7b8f9ebebeaf2e7d2f2be46926c23834d2b"
- integrity sha512-HDTdlMkTQZ95rd6EpvuLM+xkZV03yGLc38FqI37qKFLJtUN1WnYVaWsuXKoljd1OrVEVsHe6CfqKwaPZ52D56Q==
- dependencies:
- "@expo/config" "~11.0.9"
- "@expo/config-plugins" "~10.0.2"
- "@expo/config-types" "^53.0.4"
- "@expo/image-utils" "^0.7.4"
- "@expo/json-file" "^9.1.4"
- "@react-native/normalize-colors" "0.79.2"
+"@expo/prebuild-config@^54.0.8":
+ version "54.0.8"
+ resolved "https://registry.yarnpkg.com/@expo/prebuild-config/-/prebuild-config-54.0.8.tgz#509410345489cc52d1e6ece52742384efe7ad7c6"
+ integrity sha512-EA7N4dloty2t5Rde+HP0IEE+nkAQiu4A/+QGZGT9mFnZ5KKjPPkqSyYcRvP5bhQE10D+tvz6X0ngZpulbMdbsg==
+ dependencies:
+ "@expo/config" "~12.0.13"
+ "@expo/config-plugins" "~54.0.4"
+ "@expo/config-types" "^54.0.10"
+ "@expo/image-utils" "^0.8.8"
+ "@expo/json-file" "^10.0.8"
+ "@react-native/normalize-colors" "0.81.5"
debug "^4.3.1"
resolve-from "^5.0.0"
semver "^7.6.0"
xml2js "0.6.0"
+"@expo/schema-utils@^0.1.8":
+ version "0.1.8"
+ resolved "https://registry.yarnpkg.com/@expo/schema-utils/-/schema-utils-0.1.8.tgz#8b9543d77fc4ac4954196e3fa00f8fcedd71426a"
+ integrity sha512-9I6ZqvnAvKKDiO+ZF8BpQQFYWXOJvTAL5L/227RUbWG1OVZDInFifzCBiqAZ3b67NRfeAgpgvbA7rejsqhY62A==
+
"@expo/sdk-runtime-versions@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@expo/sdk-runtime-versions/-/sdk-runtime-versions-1.0.0.tgz#d7ebd21b19f1c6b0395e50d78da4416941c57f7c"
integrity sha512-Doz2bfiPndXYFPMRwPyGa1k5QaKDVpY806UJj570epIiMzWaYyCtobasyfC++qfIXVb5Ocy7r3tP9d62hAQ7IQ==
-"@expo/server@^0.6.2":
- version "0.6.2"
- resolved "https://registry.yarnpkg.com/@expo/server/-/server-0.6.2.tgz#22986441f25f39aa75b679a84ee1de7ab01a5323"
- integrity sha512-ko+dq+1WEC126/iGVv3g+ChFCs9wGyKtGlnYphwrOQbFBBqX19sn6UV0oUks6UdhD+MyzUv+w/TOdktdcI0Cgg==
- dependencies:
- abort-controller "^3.0.0"
- debug "^4.3.4"
- source-map-support "~0.5.21"
- undici "^6.18.2 || ^7.0.0"
-
"@expo/spawn-async@^1.7.2":
version "1.7.2"
resolved "https://registry.yarnpkg.com/@expo/spawn-async/-/spawn-async-1.7.2.tgz#fcfe66c3e387245e72154b1a7eae8cada6a47f58"
@@ -2547,10 +2678,10 @@
resolved "https://registry.yarnpkg.com/@expo/sudo-prompt/-/sudo-prompt-9.3.2.tgz#0fd2813402a42988e49145cab220e25bea74b308"
integrity sha512-HHQigo3rQWKMDzYDLkubN5WQOYXJJE2eNqIQC2axC2iO3mHdwnIR7FgZVvHWtBwAdzBgAP0ECp8KqS8TiMKvgw==
-"@expo/vector-icons@^14.0.0", "@expo/vector-icons@^14.1.0":
- version "14.1.0"
- resolved "https://registry.yarnpkg.com/@expo/vector-icons/-/vector-icons-14.1.0.tgz#d3dddad8b6ea60502e0fe5485b86751827606ce4"
- integrity sha512-7T09UE9h8QDTsUeMGymB4i+iqvtEeaO5VvUjryFB4tugDTG/bkzViWA74hm5pfjjDEhYMXWaX112mcvhccmIwQ==
+"@expo/vector-icons@^15.0.3":
+ version "15.1.1"
+ resolved "https://registry.yarnpkg.com/@expo/vector-icons/-/vector-icons-15.1.1.tgz#4b1d2c60493c0b0536972f0a5babd5f5c85b48f4"
+ integrity sha512-Iu2VkcoI5vygbtYngm7jb4ifxElNVXQYdDrYkT7UCEIiKLeWnQY0wf2ZhHZ+Wro6Sc5TaumpKUOqDRpLi5rkvw==
"@expo/ws-tunnel@^1.0.1":
version "1.0.6"
@@ -3073,11 +3204,126 @@
unbzip2-stream "1.4.3"
yargs "17.7.2"
+"@radix-ui/primitive@1.1.3":
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.3.tgz#e2dbc13bdc5e4168f4334f75832d7bdd3e2de5ba"
+ integrity sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==
+
+"@radix-ui/react-collection@1.1.7":
+ version "1.1.7"
+ resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.1.7.tgz#d05c25ca9ac4695cc19ba91f42f686e3ea2d9aec"
+ integrity sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==
+ dependencies:
+ "@radix-ui/react-compose-refs" "1.1.2"
+ "@radix-ui/react-context" "1.1.2"
+ "@radix-ui/react-primitive" "2.1.3"
+ "@radix-ui/react-slot" "1.2.3"
+
"@radix-ui/react-compose-refs@1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz#a2c4c47af6337048ee78ff6dc0d090b390d2bb30"
integrity sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==
+"@radix-ui/react-context@1.1.2":
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.2.tgz#61628ef269a433382c364f6f1e3788a6dc213a36"
+ integrity sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==
+
+"@radix-ui/react-dialog@^1.1.1":
+ version "1.1.15"
+ resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz#1de3d7a7e9a17a9874d29c07f5940a18a119b632"
+ integrity sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==
+ dependencies:
+ "@radix-ui/primitive" "1.1.3"
+ "@radix-ui/react-compose-refs" "1.1.2"
+ "@radix-ui/react-context" "1.1.2"
+ "@radix-ui/react-dismissable-layer" "1.1.11"
+ "@radix-ui/react-focus-guards" "1.1.3"
+ "@radix-ui/react-focus-scope" "1.1.7"
+ "@radix-ui/react-id" "1.1.1"
+ "@radix-ui/react-portal" "1.1.9"
+ "@radix-ui/react-presence" "1.1.5"
+ "@radix-ui/react-primitive" "2.1.3"
+ "@radix-ui/react-slot" "1.2.3"
+ "@radix-ui/react-use-controllable-state" "1.2.2"
+ aria-hidden "^1.2.4"
+ react-remove-scroll "^2.6.3"
+
+"@radix-ui/react-direction@1.1.1":
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.1.1.tgz#39e5a5769e676c753204b792fbe6cf508e550a14"
+ integrity sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==
+
+"@radix-ui/react-dismissable-layer@1.1.11":
+ version "1.1.11"
+ resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz#e33ab6f6bdaa00f8f7327c408d9f631376b88b37"
+ integrity sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==
+ dependencies:
+ "@radix-ui/primitive" "1.1.3"
+ "@radix-ui/react-compose-refs" "1.1.2"
+ "@radix-ui/react-primitive" "2.1.3"
+ "@radix-ui/react-use-callback-ref" "1.1.1"
+ "@radix-ui/react-use-escape-keydown" "1.1.1"
+
+"@radix-ui/react-focus-guards@1.1.3":
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz#2a5669e464ad5fde9f86d22f7fdc17781a4dfa7f"
+ integrity sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==
+
+"@radix-ui/react-focus-scope@1.1.7":
+ version "1.1.7"
+ resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz#dfe76fc103537d80bf42723a183773fd07bfb58d"
+ integrity sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==
+ dependencies:
+ "@radix-ui/react-compose-refs" "1.1.2"
+ "@radix-ui/react-primitive" "2.1.3"
+ "@radix-ui/react-use-callback-ref" "1.1.1"
+
+"@radix-ui/react-id@1.1.1":
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.1.1.tgz#1404002e79a03fe062b7e3864aa01e24bd1471f7"
+ integrity sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==
+ dependencies:
+ "@radix-ui/react-use-layout-effect" "1.1.1"
+
+"@radix-ui/react-portal@1.1.9":
+ version "1.1.9"
+ resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.9.tgz#14c3649fe48ec474ac51ed9f2b9f5da4d91c4472"
+ integrity sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==
+ dependencies:
+ "@radix-ui/react-primitive" "2.1.3"
+ "@radix-ui/react-use-layout-effect" "1.1.1"
+
+"@radix-ui/react-presence@1.1.5":
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.5.tgz#5d8f28ac316c32f078afce2996839250c10693db"
+ integrity sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==
+ dependencies:
+ "@radix-ui/react-compose-refs" "1.1.2"
+ "@radix-ui/react-use-layout-effect" "1.1.1"
+
+"@radix-ui/react-primitive@2.1.3":
+ version "2.1.3"
+ resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz#db9b8bcff49e01be510ad79893fb0e4cda50f1bc"
+ integrity sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==
+ dependencies:
+ "@radix-ui/react-slot" "1.2.3"
+
+"@radix-ui/react-roving-focus@1.1.11":
+ version "1.1.11"
+ resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz#ef54384b7361afc6480dcf9907ef2fedb5080fd9"
+ integrity sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==
+ dependencies:
+ "@radix-ui/primitive" "1.1.3"
+ "@radix-ui/react-collection" "1.1.7"
+ "@radix-ui/react-compose-refs" "1.1.2"
+ "@radix-ui/react-context" "1.1.2"
+ "@radix-ui/react-direction" "1.1.1"
+ "@radix-ui/react-id" "1.1.1"
+ "@radix-ui/react-primitive" "2.1.3"
+ "@radix-ui/react-use-callback-ref" "1.1.1"
+ "@radix-ui/react-use-controllable-state" "1.2.2"
+
"@radix-ui/react-slot@1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.2.0.tgz#57727fc186ddb40724ccfbe294e1a351d92462ba"
@@ -3085,23 +3331,242 @@
dependencies:
"@radix-ui/react-compose-refs" "1.1.2"
-"@react-native/assets-registry@0.79.2":
- version "0.79.2"
- resolved "https://registry.yarnpkg.com/@react-native/assets-registry/-/assets-registry-0.79.2.tgz#731963e664c8543f5b277e56c058bde612b69f50"
- integrity sha512-5h2Z7/+/HL/0h88s0JHOdRCW4CXMCJoROxqzHqxdrjGL6EBD1DdaB4ZqkCOEVSW4Vjhir5Qb97C8i/MPWEYPtg==
+"@radix-ui/react-slot@1.2.3":
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz#502d6e354fc847d4169c3bc5f189de777f68cfe1"
+ integrity sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==
+ dependencies:
+ "@radix-ui/react-compose-refs" "1.1.2"
+
+"@radix-ui/react-tabs@^1.1.12":
+ version "1.1.13"
+ resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz#3537ce379d7e7ff4eeb6b67a0973e139c2ac1f15"
+ integrity sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==
+ dependencies:
+ "@radix-ui/primitive" "1.1.3"
+ "@radix-ui/react-context" "1.1.2"
+ "@radix-ui/react-direction" "1.1.1"
+ "@radix-ui/react-id" "1.1.1"
+ "@radix-ui/react-presence" "1.1.5"
+ "@radix-ui/react-primitive" "2.1.3"
+ "@radix-ui/react-roving-focus" "1.1.11"
+ "@radix-ui/react-use-controllable-state" "1.2.2"
+
+"@radix-ui/react-use-callback-ref@1.1.1":
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz#62a4dba8b3255fdc5cc7787faeac1c6e4cc58d40"
+ integrity sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==
+
+"@radix-ui/react-use-controllable-state@1.2.2":
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz#905793405de57d61a439f4afebbb17d0645f3190"
+ integrity sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==
+ dependencies:
+ "@radix-ui/react-use-effect-event" "0.0.2"
+ "@radix-ui/react-use-layout-effect" "1.1.1"
+
+"@radix-ui/react-use-effect-event@0.0.2":
+ version "0.0.2"
+ resolved "https://registry.yarnpkg.com/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz#090cf30d00a4c7632a15548512e9152217593907"
+ integrity sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==
+ dependencies:
+ "@radix-ui/react-use-layout-effect" "1.1.1"
+
+"@radix-ui/react-use-escape-keydown@1.1.1":
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz#b3fed9bbea366a118f40427ac40500aa1423cc29"
+ integrity sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==
+ dependencies:
+ "@radix-ui/react-use-callback-ref" "1.1.1"
+
+"@radix-ui/react-use-layout-effect@1.1.1":
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz#0c4230a9eed49d4589c967e2d9c0d9d60a23971e"
+ integrity sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==
+
+"@react-native-community/cli-clean@20.1.3":
+ version "20.1.3"
+ resolved "https://registry.yarnpkg.com/@react-native-community/cli-clean/-/cli-clean-20.1.3.tgz#c38feda678e352135a46dae214432d94febae241"
+ integrity sha512-sFLdLzapfC0scjgzBJJWYDY2RhHPjuuPkA5r6q0gc/UQH/izXpMpLrhh1DW84cMDraNACK0U62tU7ebNaQ1LMQ==
+ dependencies:
+ "@react-native-community/cli-tools" "20.1.3"
+ execa "^5.0.0"
+ fast-glob "^3.3.2"
+ picocolors "^1.1.1"
+
+"@react-native-community/cli-config-android@20.1.3":
+ version "20.1.3"
+ resolved "https://registry.yarnpkg.com/@react-native-community/cli-config-android/-/cli-config-android-20.1.3.tgz#3ec64d07973e17be1c3fe884cf7b6b8164c155eb"
+ integrity sha512-DNHDP+OWLyhKShGciBqPcxhxfp1Z/7GQcb4F+TGyCeKQAr+JdnUjRXN3X+YCU/v+g2kbYYyRJKlGabzkVvdrAw==
+ dependencies:
+ "@react-native-community/cli-tools" "20.1.3"
+ fast-glob "^3.3.2"
+ fast-xml-parser "^5.3.6"
+ picocolors "^1.1.1"
+
+"@react-native-community/cli-config-apple@20.1.3":
+ version "20.1.3"
+ resolved "https://registry.yarnpkg.com/@react-native-community/cli-config-apple/-/cli-config-apple-20.1.3.tgz#27d0cd58507d58c3e2099dad36e22db8f1140e9e"
+ integrity sha512-QX9B83nAfCPs0KiaYz61kAEHWr9sttooxzRzNdQwvZTwnsIpvWOT9GvMMj/19OeXiQzMJBzZX0Pgt6+spiUsDQ==
+ dependencies:
+ "@react-native-community/cli-tools" "20.1.3"
+ execa "^5.0.0"
+ fast-glob "^3.3.2"
+ picocolors "^1.1.1"
+
+"@react-native-community/cli-config@20.1.3":
+ version "20.1.3"
+ resolved "https://registry.yarnpkg.com/@react-native-community/cli-config/-/cli-config-20.1.3.tgz#0c44d83e4b2fb90a0140ad342ca00b03c77ba875"
+ integrity sha512-n73nW0cG92oNF0r994pPqm0DjAShOm3F8LSffDYhJqNAno+h/csmv/37iL4NtSpmKIO8xqsG3uVTXz9X/hzNaQ==
+ dependencies:
+ "@react-native-community/cli-tools" "20.1.3"
+ cosmiconfig "^9.0.0"
+ deepmerge "^4.3.0"
+ fast-glob "^3.3.2"
+ joi "^17.2.1"
+ picocolors "^1.1.1"
+
+"@react-native-community/cli-doctor@20.1.3":
+ version "20.1.3"
+ resolved "https://registry.yarnpkg.com/@react-native-community/cli-doctor/-/cli-doctor-20.1.3.tgz#7a31be7a4fcec6d2811eab23ade859e5cb0c0058"
+ integrity sha512-EI+mAPWn255/WZ4CQohy1I049yiaxVr41C3BeQ2BCyhxODIDR8XRsLzYb1t9MfqK/C3ZncUN2mPSRXFeKPPI1w==
+ dependencies:
+ "@react-native-community/cli-config" "20.1.3"
+ "@react-native-community/cli-platform-android" "20.1.3"
+ "@react-native-community/cli-platform-apple" "20.1.3"
+ "@react-native-community/cli-platform-ios" "20.1.3"
+ "@react-native-community/cli-tools" "20.1.3"
+ command-exists "^1.2.8"
+ deepmerge "^4.3.0"
+ envinfo "^7.13.0"
+ execa "^5.0.0"
+ node-stream-zip "^1.9.1"
+ ora "^5.4.1"
+ picocolors "^1.1.1"
+ semver "^7.5.2"
+ wcwidth "^1.0.1"
+ yaml "^2.2.1"
+
+"@react-native-community/cli-platform-android@20.1.3":
+ version "20.1.3"
+ resolved "https://registry.yarnpkg.com/@react-native-community/cli-platform-android/-/cli-platform-android-20.1.3.tgz#c3d08bcd3e15c18a3a77689f06ff12b5dec04f16"
+ integrity sha512-bzB9ELPOISuqgtDZXFPQlkuxx1YFkNx3cNgslc5ElCrk+5LeCLQLIBh/dmIuK8rwUrPcrramjeBj++Noc+TaAA==
+ dependencies:
+ "@react-native-community/cli-config-android" "20.1.3"
+ "@react-native-community/cli-tools" "20.1.3"
+ execa "^5.0.0"
+ logkitty "^0.7.1"
+ picocolors "^1.1.1"
+
+"@react-native-community/cli-platform-apple@20.1.3":
+ version "20.1.3"
+ resolved "https://registry.yarnpkg.com/@react-native-community/cli-platform-apple/-/cli-platform-apple-20.1.3.tgz#bd49fa4c012075b0b424d0d68f583798bcce0d95"
+ integrity sha512-XJ+DqAD4hkplWVXK5AMgN7pP9+4yRSe5KfZ/b42+ofkDBI55ALlUmX+9HWE3fMuRjcotTCoNZqX2ov97cFDXpQ==
+ dependencies:
+ "@react-native-community/cli-config-apple" "20.1.3"
+ "@react-native-community/cli-tools" "20.1.3"
+ execa "^5.0.0"
+ fast-xml-parser "^5.3.6"
+ picocolors "^1.1.1"
+
+"@react-native-community/cli-platform-ios@20.1.3":
+ version "20.1.3"
+ resolved "https://registry.yarnpkg.com/@react-native-community/cli-platform-ios/-/cli-platform-ios-20.1.3.tgz#b22a2aa4dfeaa33ec16cd518a959cda3d2b244f6"
+ integrity sha512-2qL48SINotuHbZO73cgqSwqd/OWNx0xTbFSdujhpogV4p8BNwYYypfjh4vJY5qJEB5PxuoVkMXT+aCADpg9nBg==
+ dependencies:
+ "@react-native-community/cli-platform-apple" "20.1.3"
+
+"@react-native-community/cli-server-api@20.1.3":
+ version "20.1.3"
+ resolved "https://registry.yarnpkg.com/@react-native-community/cli-server-api/-/cli-server-api-20.1.3.tgz#bdfd35e4604d8660bef5186464bf282ba85267f5"
+ integrity sha512-hsNsdUKZDd2T99OuNuiXz4VuvLa1UN0zcxefmPjXQgI0byrBLzzDr+o7p03sKuODSzKi2h+BMnUxiS07HACQLA==
+ dependencies:
+ "@react-native-community/cli-tools" "20.1.3"
+ body-parser "^2.2.2"
+ compression "^1.7.1"
+ connect "^3.6.5"
+ errorhandler "^1.5.1"
+ nocache "^3.0.1"
+ open "^6.2.0"
+ pretty-format "^29.7.0"
+ serve-static "^1.13.1"
+ strict-url-sanitise "0.0.1"
+ ws "^6.2.3"
+
+"@react-native-community/cli-tools@20.1.3":
+ version "20.1.3"
+ resolved "https://registry.yarnpkg.com/@react-native-community/cli-tools/-/cli-tools-20.1.3.tgz#eb9378f5676d572d4cc98817c9adb34a0e210e2a"
+ integrity sha512-EAn0vPCMxtHhfWk2UwLmSUfPfLUnFgC7NjiVJVTKJyVk5qGnkPfoT8te/1IUXFTysUB0F0RIi+NgDB4usFOLeA==
+ dependencies:
+ "@vscode/sudo-prompt" "^9.0.0"
+ appdirsjs "^1.2.4"
+ execa "^5.0.0"
+ find-up "^5.0.0"
+ launch-editor "^2.9.1"
+ mime "^2.4.1"
+ ora "^5.4.1"
+ picocolors "^1.1.1"
+ prompts "^2.4.2"
+ semver "^7.5.2"
+
+"@react-native-community/cli-types@20.1.3":
+ version "20.1.3"
+ resolved "https://registry.yarnpkg.com/@react-native-community/cli-types/-/cli-types-20.1.3.tgz#a19aa67b478a05c67885d6901b62b8698571de5b"
+ integrity sha512-IdAcegf0pH1hVraxWTG1ACLkYC0LDQfqtaEf42ESyLIF3Xap70JzL/9tAlxw7lSCPZPFWhrcgU0TBc4SkC/ecw==
+ dependencies:
+ joi "^17.2.1"
+
+"@react-native-community/cli@latest":
+ version "20.1.3"
+ resolved "https://registry.yarnpkg.com/@react-native-community/cli/-/cli-20.1.3.tgz#a2975093d1fc4c5036b11e3bb769719c56659b4c"
+ integrity sha512-sLo8cu9JyFNfuuF1C+8NJ4DHE/PEFaXGd4enkcxi/OJjGG8+sOQrdjNQ4i+cVh/2c+ah1mEMwsYjc3z0+/MqSg==
+ dependencies:
+ "@react-native-community/cli-clean" "20.1.3"
+ "@react-native-community/cli-config" "20.1.3"
+ "@react-native-community/cli-doctor" "20.1.3"
+ "@react-native-community/cli-server-api" "20.1.3"
+ "@react-native-community/cli-tools" "20.1.3"
+ "@react-native-community/cli-types" "20.1.3"
+ commander "^9.4.1"
+ deepmerge "^4.3.0"
+ execa "^5.0.0"
+ find-up "^5.0.0"
+ fs-extra "^8.1.0"
+ graceful-fs "^4.1.3"
+ picocolors "^1.1.1"
+ prompts "^2.4.2"
+ semver "^7.5.2"
+
+"@react-native/assets-registry@0.81.5":
+ version "0.81.5"
+ resolved "https://registry.yarnpkg.com/@react-native/assets-registry/-/assets-registry-0.81.5.tgz#d22c924fa6f6d4a463c5af34ce91f38756c0fa7d"
+ integrity sha512-705B6x/5Kxm1RKRvSv0ADYWm5JOnoiQ1ufW7h8uu2E6G9Of/eE6hP/Ivw3U5jI16ERqZxiKQwk34VJbB0niX9w==
+
+"@react-native/assets-registry@0.83.4":
+ version "0.83.4"
+ resolved "https://registry.yarnpkg.com/@react-native/assets-registry/-/assets-registry-0.83.4.tgz#48408565f1a6a40cc91e4bbff38e07665d31fbd6"
+ integrity sha512-aqKtpbJDSQeSX/Dwv0yMe1/Rd2QfXi12lnyZDXNn/OEKz59u6+LuPBVgO/9CRyclHmdlvwg8c7PJ9eX2ZMnjWg==
+
+"@react-native/babel-plugin-codegen@0.81.5":
+ version "0.81.5"
+ resolved "https://registry.yarnpkg.com/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.81.5.tgz#328d03f42c32b5a8cc2dee1aa84a7c48dddc5f18"
+ integrity sha512-oF71cIH6je3fSLi6VPjjC3Sgyyn57JLHXs+mHWc9MoCiJJcM4nqsS5J38zv1XQ8d3zOW2JtHro+LF0tagj2bfQ==
+ dependencies:
+ "@babel/traverse" "^7.25.3"
+ "@react-native/codegen" "0.81.5"
-"@react-native/babel-plugin-codegen@0.79.3":
- version "0.79.3"
- resolved "https://registry.yarnpkg.com/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.79.3.tgz#acad4acaead398a8c8bcdecbe44040aa0c2dc2d7"
- integrity sha512-Zb8F4bSEKKZfms5n1MQ0o5mudDcpAINkKiFuFTU0PErYGjY3kZ+JeIP+gS6KCXsckxCfMEKQwqKicP/4DWgsZQ==
+"@react-native/babel-plugin-codegen@0.81.6":
+ version "0.81.6"
+ resolved "https://registry.yarnpkg.com/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.81.6.tgz#cbdd282ecc49cd0992274559285fc22a03b8b26b"
+ integrity sha512-OBx6/S0h0MEAoUXhRCWBu00+Oz0gCcHzduTHN++qnK7cIZUhxASUdplYzBZTtTdtA+Zr0bDzH05EsXq718WgFA==
dependencies:
"@babel/traverse" "^7.25.3"
- "@react-native/codegen" "0.79.3"
+ "@react-native/codegen" "0.81.6"
-"@react-native/babel-preset@0.79.3":
- version "0.79.3"
- resolved "https://registry.yarnpkg.com/@react-native/babel-preset/-/babel-preset-0.79.3.tgz#8ad6c149cd488fbc18d62983119bdcbfc15ff651"
- integrity sha512-VHGNP02bDD2Ul1my0pLVwe/0dsEBHxR343ySpgnkCNEEm9C1ANQIL2wvnJrHZPcqfAkWfFQ8Ln3t+6fdm4A/Dg==
+"@react-native/babel-preset@0.81.5":
+ version "0.81.5"
+ resolved "https://registry.yarnpkg.com/@react-native/babel-preset/-/babel-preset-0.81.5.tgz#e8b7969d21f87ef4e41e603248e8a70c44b4a5bb"
+ integrity sha512-UoI/x/5tCmi+pZ3c1+Ypr1DaRMDLI3y+Q70pVLLVgrnC3DHsHRIbHcCHIeG/IJvoeFqFM2sTdhSOLJrf8lOPrA==
dependencies:
"@babel/core" "^7.25.2"
"@babel/plugin-proposal-export-default-from" "^7.24.7"
@@ -3144,170 +3609,288 @@
"@babel/plugin-transform-typescript" "^7.25.2"
"@babel/plugin-transform-unicode-regex" "^7.24.7"
"@babel/template" "^7.25.0"
- "@react-native/babel-plugin-codegen" "0.79.3"
- babel-plugin-syntax-hermes-parser "0.25.1"
+ "@react-native/babel-plugin-codegen" "0.81.5"
+ babel-plugin-syntax-hermes-parser "0.29.1"
babel-plugin-transform-flow-enums "^0.0.2"
react-refresh "^0.14.0"
-"@react-native/codegen@0.79.2":
- version "0.79.2"
- resolved "https://registry.yarnpkg.com/@react-native/codegen/-/codegen-0.79.2.tgz#75270d8162e78c02b0272396a3c6942e39e8703d"
- integrity sha512-8JTlGLuLi1p8Jx2N/enwwEd7/2CfrqJpv90Cp77QLRX3VHF2hdyavRIxAmXMwN95k+Me7CUuPtqn2X3IBXOWYg==
+"@react-native/babel-preset@^0.81.6":
+ version "0.81.6"
+ resolved "https://registry.yarnpkg.com/@react-native/babel-preset/-/babel-preset-0.81.6.tgz#ba0df249c1e163196959d7f18acffb887a869567"
+ integrity sha512-RtIr82ccPoyMLmIC3/vCG96MzRmIT+IzQkjCgTS5b93uAxiER9BS7+d1l7+zD00VanClt6CTBM4K4n6RCnAykg==
dependencies:
+ "@babel/core" "^7.25.2"
+ "@babel/plugin-proposal-export-default-from" "^7.24.7"
+ "@babel/plugin-syntax-dynamic-import" "^7.8.3"
+ "@babel/plugin-syntax-export-default-from" "^7.24.7"
+ "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3"
+ "@babel/plugin-syntax-optional-chaining" "^7.8.3"
+ "@babel/plugin-transform-arrow-functions" "^7.24.7"
+ "@babel/plugin-transform-async-generator-functions" "^7.25.4"
+ "@babel/plugin-transform-async-to-generator" "^7.24.7"
+ "@babel/plugin-transform-block-scoping" "^7.25.0"
+ "@babel/plugin-transform-class-properties" "^7.25.4"
+ "@babel/plugin-transform-classes" "^7.25.4"
+ "@babel/plugin-transform-computed-properties" "^7.24.7"
+ "@babel/plugin-transform-destructuring" "^7.24.8"
+ "@babel/plugin-transform-flow-strip-types" "^7.25.2"
+ "@babel/plugin-transform-for-of" "^7.24.7"
+ "@babel/plugin-transform-function-name" "^7.25.1"
+ "@babel/plugin-transform-literals" "^7.25.2"
+ "@babel/plugin-transform-logical-assignment-operators" "^7.24.7"
+ "@babel/plugin-transform-modules-commonjs" "^7.24.8"
+ "@babel/plugin-transform-named-capturing-groups-regex" "^7.24.7"
+ "@babel/plugin-transform-nullish-coalescing-operator" "^7.24.7"
+ "@babel/plugin-transform-numeric-separator" "^7.24.7"
+ "@babel/plugin-transform-object-rest-spread" "^7.24.7"
+ "@babel/plugin-transform-optional-catch-binding" "^7.24.7"
+ "@babel/plugin-transform-optional-chaining" "^7.24.8"
+ "@babel/plugin-transform-parameters" "^7.24.7"
+ "@babel/plugin-transform-private-methods" "^7.24.7"
+ "@babel/plugin-transform-private-property-in-object" "^7.24.7"
+ "@babel/plugin-transform-react-display-name" "^7.24.7"
+ "@babel/plugin-transform-react-jsx" "^7.25.2"
+ "@babel/plugin-transform-react-jsx-self" "^7.24.7"
+ "@babel/plugin-transform-react-jsx-source" "^7.24.7"
+ "@babel/plugin-transform-regenerator" "^7.24.7"
+ "@babel/plugin-transform-runtime" "^7.24.7"
+ "@babel/plugin-transform-shorthand-properties" "^7.24.7"
+ "@babel/plugin-transform-spread" "^7.24.7"
+ "@babel/plugin-transform-sticky-regex" "^7.24.7"
+ "@babel/plugin-transform-typescript" "^7.25.2"
+ "@babel/plugin-transform-unicode-regex" "^7.24.7"
+ "@babel/template" "^7.25.0"
+ "@react-native/babel-plugin-codegen" "0.81.6"
+ babel-plugin-syntax-hermes-parser "0.29.1"
+ babel-plugin-transform-flow-enums "^0.0.2"
+ react-refresh "^0.14.0"
+
+"@react-native/codegen@0.81.5":
+ version "0.81.5"
+ resolved "https://registry.yarnpkg.com/@react-native/codegen/-/codegen-0.81.5.tgz#d4dec668c94b9d58a5c2dbdbf026db331e1b6b27"
+ integrity sha512-a2TDA03Up8lpSa9sh5VRGCQDXgCTOyDOFH+aqyinxp1HChG8uk89/G+nkJ9FPd0rqgi25eCTR16TWdS3b+fA6g==
+ dependencies:
+ "@babel/core" "^7.25.2"
+ "@babel/parser" "^7.25.3"
+ glob "^7.1.1"
+ hermes-parser "0.29.1"
+ invariant "^2.2.4"
+ nullthrows "^1.1.1"
+ yargs "^17.6.2"
+
+"@react-native/codegen@0.81.6":
+ version "0.81.6"
+ resolved "https://registry.yarnpkg.com/@react-native/codegen/-/codegen-0.81.6.tgz#202b4f2b65dc053aae7936c88539e7faac885ca8"
+ integrity sha512-9KoYRep/KDnELLLmIYTtIIEOClVUJ88pxWObb/0sjkacA7uL4SgfbAg7rWLURAQJWI85L1YS67IhdEqNNk1I7w==
+ dependencies:
+ "@babel/core" "^7.25.2"
+ "@babel/parser" "^7.25.3"
glob "^7.1.1"
- hermes-parser "0.25.1"
+ hermes-parser "0.29.1"
invariant "^2.2.4"
nullthrows "^1.1.1"
yargs "^17.6.2"
-"@react-native/codegen@0.79.3":
- version "0.79.3"
- resolved "https://registry.yarnpkg.com/@react-native/codegen/-/codegen-0.79.3.tgz#49689132718c81a3b25426769bc6fd8fd2a0469f"
- integrity sha512-CZejXqKch/a5/s/MO5T8mkAgvzCXgsTkQtpCF15kWR9HN8T+16k0CsN7TXAxXycltoxiE3XRglOrZNEa/TiZUQ==
+"@react-native/codegen@0.83.4":
+ version "0.83.4"
+ resolved "https://registry.yarnpkg.com/@react-native/codegen/-/codegen-0.83.4.tgz#79cb20c904f885d44100549816364cceb72c263c"
+ integrity sha512-CJ7XutzIqJPz3Lp/5TOiRWlU/JAjTboMT1BHNLSXjYHXwTmgHM3iGEbpCOtBMjWvsojRTJyRO/G3ghInIIXEYg==
dependencies:
+ "@babel/core" "^7.25.2"
+ "@babel/parser" "^7.25.3"
glob "^7.1.1"
- hermes-parser "0.25.1"
+ hermes-parser "0.32.0"
invariant "^2.2.4"
nullthrows "^1.1.1"
yargs "^17.6.2"
-"@react-native/community-cli-plugin@0.79.2":
- version "0.79.2"
- resolved "https://registry.yarnpkg.com/@react-native/community-cli-plugin/-/community-cli-plugin-0.79.2.tgz#d3a0efbdfb554cf3a7e9bfb27865a7caeeeaa1b3"
- integrity sha512-E+YEY2dL+68HyR2iahsZdyBKBUi9QyPyaN9vsnda1jNgCjNpSPk2yAF5cXsho+zKK5ZQna3JSeE1Kbi2IfGJbw==
+"@react-native/community-cli-plugin@0.81.5":
+ version "0.81.5"
+ resolved "https://registry.yarnpkg.com/@react-native/community-cli-plugin/-/community-cli-plugin-0.81.5.tgz#617789cda4da419d03dda00e2a78c36188b4391e"
+ integrity sha512-yWRlmEOtcyvSZ4+OvqPabt+NS36vg0K/WADTQLhrYrm9qdZSuXmq8PmdJWz/68wAqKQ+4KTILiq2kjRQwnyhQw==
+ dependencies:
+ "@react-native/dev-middleware" "0.81.5"
+ debug "^4.4.0"
+ invariant "^2.2.4"
+ metro "^0.83.1"
+ metro-config "^0.83.1"
+ metro-core "^0.83.1"
+ semver "^7.1.3"
+
+"@react-native/community-cli-plugin@0.83.4":
+ version "0.83.4"
+ resolved "https://registry.yarnpkg.com/@react-native/community-cli-plugin/-/community-cli-plugin-0.83.4.tgz#133a202d3087c3ef433e54192480dc799635f843"
+ integrity sha512-8os0weQEnjUhWy7Db881+JKRwNHVGM40VtTRvltAyA/YYkrGg4kPCqiTybMxQDEcF3rnviuxHyI+ITiglfmgmQ==
dependencies:
- "@react-native/dev-middleware" "0.79.2"
- chalk "^4.0.0"
- debug "^2.2.0"
+ "@react-native/dev-middleware" "0.83.4"
+ debug "^4.4.0"
invariant "^2.2.4"
- metro "^0.82.0"
- metro-config "^0.82.0"
- metro-core "^0.82.0"
+ metro "^0.83.3"
+ metro-config "^0.83.3"
+ metro-core "^0.83.3"
semver "^7.1.3"
-"@react-native/debugger-frontend@0.79.2":
- version "0.79.2"
- resolved "https://registry.yarnpkg.com/@react-native/debugger-frontend/-/debugger-frontend-0.79.2.tgz#1377de6d9cabe5455bf332e06408167da5f60c19"
- integrity sha512-cGmC7X6kju76DopSBNc+PRAEetbd7TWF9J9o84hOp/xL3ahxR2kuxJy0oJX8Eg8oehhGGEXTuMKHzNa3rDBeSg==
+"@react-native/debugger-frontend@0.81.5":
+ version "0.81.5"
+ resolved "https://registry.yarnpkg.com/@react-native/debugger-frontend/-/debugger-frontend-0.81.5.tgz#82ece0181e9a7a3dcbdfa86cf9decd654e13f81f"
+ integrity sha512-bnd9FSdWKx2ncklOetCgrlwqSGhMHP2zOxObJbOWXoj7GHEmih4MKarBo5/a8gX8EfA1EwRATdfNBQ81DY+h+w==
-"@react-native/debugger-frontend@0.79.3":
- version "0.79.3"
- resolved "https://registry.yarnpkg.com/@react-native/debugger-frontend/-/debugger-frontend-0.79.3.tgz#9cb57d8e88c22552194ab5f6f257605b151bc5b3"
- integrity sha512-ImNDuEeKH6lEsLXms3ZsgIrNF94jymfuhPcVY5L0trzaYNo9ZFE9Ni2/18E1IbfXxdeIHrCSBJlWD6CTm7wu5A==
+"@react-native/debugger-frontend@0.83.4":
+ version "0.83.4"
+ resolved "https://registry.yarnpkg.com/@react-native/debugger-frontend/-/debugger-frontend-0.83.4.tgz#78f531b9f591e1e034f6a0eb0c745a4fc992af6c"
+ integrity sha512-mCE2s/S7SEjax3gZb6LFAraAI3x13gRVWJWqT0HIm71e4ITObENNTDuMw4mvZ/wr4Gz2wv4FcBH5/Nla9LXOcg==
+
+"@react-native/debugger-shell@0.83.4":
+ version "0.83.4"
+ resolved "https://registry.yarnpkg.com/@react-native/debugger-shell/-/debugger-shell-0.83.4.tgz#376239508acb46062196740747a0bdec73273202"
+ integrity sha512-FtAnrvXqy1xeZ+onwilvxEeeBsvBlhtfrHVIC2R/BOJAK9TbKEtFfjio0wsn3DQIm+UZq48DSa+p9jJZ2aJUww==
+ dependencies:
+ cross-spawn "^7.0.6"
+ fb-dotslash "0.5.8"
-"@react-native/dev-middleware@0.79.2":
- version "0.79.2"
- resolved "https://registry.yarnpkg.com/@react-native/dev-middleware/-/dev-middleware-0.79.2.tgz#f09f1a75b4cd0b56dfd82a07bf41157a9c45619c"
- integrity sha512-9q4CpkklsAs1L0Bw8XYCoqqyBSrfRALGEw4/r0EkR38Y/6fVfNfdsjSns0pTLO6h0VpxswK34L/hm4uK3MoLHw==
+"@react-native/dev-middleware@0.81.5":
+ version "0.81.5"
+ resolved "https://registry.yarnpkg.com/@react-native/dev-middleware/-/dev-middleware-0.81.5.tgz#81e8ac545d7736ef6ebb2e59fdbaebc5cf9aedec"
+ integrity sha512-WfPfZzboYgo/TUtysuD5xyANzzfka8Ebni6RIb2wDxhb56ERi7qDrE4xGhtPsjCL4pQBXSVxyIlCy0d8I6EgGA==
dependencies:
"@isaacs/ttlcache" "^1.4.1"
- "@react-native/debugger-frontend" "0.79.2"
+ "@react-native/debugger-frontend" "0.81.5"
chrome-launcher "^0.15.2"
chromium-edge-launcher "^0.2.0"
connect "^3.6.5"
- debug "^2.2.0"
+ debug "^4.4.0"
invariant "^2.2.4"
nullthrows "^1.1.1"
open "^7.0.3"
serve-static "^1.16.2"
ws "^6.2.3"
-"@react-native/dev-middleware@0.79.3":
- version "0.79.3"
- resolved "https://registry.yarnpkg.com/@react-native/dev-middleware/-/dev-middleware-0.79.3.tgz#3e315ef7516ebad60a4202b4094d84fedecb4064"
- integrity sha512-x88+RGOyG71+idQefnQg7wLhzjn/Scs+re1O5vqCkTVzRAc/f7SdHMlbmECUxJPd08FqMcOJr7/X3nsJBrNuuw==
+"@react-native/dev-middleware@0.83.4":
+ version "0.83.4"
+ resolved "https://registry.yarnpkg.com/@react-native/dev-middleware/-/dev-middleware-0.83.4.tgz#59f9488c19769f6a7da966fc55739832c6d2ef97"
+ integrity sha512-3s9nXZc/kj986nI2RPqxiIJeTS3o7pvZDxbHu7GE9WVIGX9YucA1l/tEiXd7BAm3TBFOfefDOT08xD46wH+R3Q==
dependencies:
"@isaacs/ttlcache" "^1.4.1"
- "@react-native/debugger-frontend" "0.79.3"
+ "@react-native/debugger-frontend" "0.83.4"
+ "@react-native/debugger-shell" "0.83.4"
chrome-launcher "^0.15.2"
chromium-edge-launcher "^0.2.0"
connect "^3.6.5"
- debug "^2.2.0"
+ debug "^4.4.0"
invariant "^2.2.4"
nullthrows "^1.1.1"
open "^7.0.3"
serve-static "^1.16.2"
- ws "^6.2.3"
+ ws "^7.5.10"
+
+"@react-native/gradle-plugin@0.81.5":
+ version "0.81.5"
+ resolved "https://registry.yarnpkg.com/@react-native/gradle-plugin/-/gradle-plugin-0.81.5.tgz#a58830f38789f6254b64449a17fe57455b589d00"
+ integrity sha512-hORRlNBj+ReNMLo9jme3yQ6JQf4GZpVEBLxmTXGGlIL78MAezDZr5/uq9dwElSbcGmLEgeiax6e174Fie6qPLg==
+
+"@react-native/gradle-plugin@0.83.4":
+ version "0.83.4"
+ resolved "https://registry.yarnpkg.com/@react-native/gradle-plugin/-/gradle-plugin-0.83.4.tgz#13fe82da3a43c1ad0205e38236d351ec61ec7175"
+ integrity sha512-AhaSWw2k3eMKqZ21IUdM7rpyTYOpAfsBbIIiom1QQii3QccX0uW2AWTcRhfuWRxqr2faGFaOBYedWl2fzp5hgw==
+
+"@react-native/js-polyfills@0.81.5":
+ version "0.81.5"
+ resolved "https://registry.yarnpkg.com/@react-native/js-polyfills/-/js-polyfills-0.81.5.tgz#2ca68188c8fff9b951f507b1dec7efe928848274"
+ integrity sha512-fB7M1CMOCIUudTRuj7kzxIBTVw2KXnsgbQ6+4cbqSxo8NmRRhA0Ul4ZUzZj3rFd3VznTL4Brmocv1oiN0bWZ8w==
-"@react-native/gradle-plugin@0.79.2":
- version "0.79.2"
- resolved "https://registry.yarnpkg.com/@react-native/gradle-plugin/-/gradle-plugin-0.79.2.tgz#d41d4e2c63baf688a2b47652c6260f2a2f1ec091"
- integrity sha512-6MJFemrwR0bOT0QM+2BxX9k3/pvZQNmJ3Js5pF/6owsA0cUDiCO57otiEU8Fz+UywWEzn1FoQfOfQ8vt2GYmoA==
+"@react-native/js-polyfills@0.83.4":
+ version "0.83.4"
+ resolved "https://registry.yarnpkg.com/@react-native/js-polyfills/-/js-polyfills-0.83.4.tgz#63244c915773d39bfc56527c1a062054db0c01dd"
+ integrity sha512-wYUdv0rt4MjhKhQloO1AnGDXhZQOFZHDxm86dEtEA0WcsCdVrFdRULFM+rKUC/QQtJW2rS6WBqtBusgtrsDADg==
-"@react-native/js-polyfills@0.79.2":
- version "0.79.2"
- resolved "https://registry.yarnpkg.com/@react-native/js-polyfills/-/js-polyfills-0.79.2.tgz#15eb4da0fe9e8d61d2980d08fd06b5f49e133b0f"
- integrity sha512-IaY87Ckd4GTPMkO1/Fe8fC1IgIx3vc3q9Tyt/6qS3Mtk9nC0x9q4kSR5t+HHq0/MuvGtu8HpdxXGy5wLaM+zUw==
+"@react-native/normalize-colors@0.81.5":
+ version "0.81.5"
+ resolved "https://registry.yarnpkg.com/@react-native/normalize-colors/-/normalize-colors-0.81.5.tgz#1ca6cb6772bb7324df2b11aab35227eacd6bdfe7"
+ integrity sha512-0HuJ8YtqlTVRXGZuGeBejLE04wSQsibpTI+RGOyVqxZvgtlLLC/Ssw0UmbHhT4lYMp2fhdtvKZSs5emWB1zR/g==
-"@react-native/normalize-colors@0.79.2":
- version "0.79.2"
- resolved "https://registry.yarnpkg.com/@react-native/normalize-colors/-/normalize-colors-0.79.2.tgz#9ab70ca257c7411e4ab74cf7f91332c27d39cc6f"
- integrity sha512-+b+GNrupWrWw1okHnEENz63j7NSMqhKeFMOyzYLBwKcprG8fqJQhDIGXfizKdxeIa5NnGSAevKL1Ev1zJ56X8w==
+"@react-native/normalize-colors@0.83.4":
+ version "0.83.4"
+ resolved "https://registry.yarnpkg.com/@react-native/normalize-colors/-/normalize-colors-0.83.4.tgz#211fd38a7f067ede9eeace504f0e45c090a383d4"
+ integrity sha512-9ezxaHjxqTkTOLg62SGg7YhFaE+fxa/jlrWP0nwf7eGFHlGOiTAaRR2KUfiN3K05e+EMbEhgcH/c7bgaXeGyJw==
"@react-native/normalize-colors@^0.74.1":
version "0.74.89"
resolved "https://registry.yarnpkg.com/@react-native/normalize-colors/-/normalize-colors-0.74.89.tgz#b8ac17d1bbccd3ef9a1f921665d04d42cff85976"
integrity sha512-qoMMXddVKVhZ8PA1AbUCk83trpd6N+1nF2A6k1i6LsQObyS92fELuk8kU/lQs6M7BsMHwqyLCpQJ1uFgNvIQXg==
-"@react-native/virtualized-lists@0.79.2":
- version "0.79.2"
- resolved "https://registry.yarnpkg.com/@react-native/virtualized-lists/-/virtualized-lists-0.79.2.tgz#ed5a419a30b7ddec978b7816ff698a9d85507e15"
- integrity sha512-9G6ROJeP+rdw9Bvr5ruOlag11ET7j1z/En1riFFNo6W3xZvJY+alCuH1ttm12y9+zBm4n8jwCk4lGhjYaV4dKw==
+"@react-native/virtualized-lists@0.81.5":
+ version "0.81.5"
+ resolved "https://registry.yarnpkg.com/@react-native/virtualized-lists/-/virtualized-lists-0.81.5.tgz#24123fded16992d7e46ecc4ccd473be939ea8c1b"
+ integrity sha512-UVXgV/db25OPIvwZySeToXD/9sKKhOdkcWmmf4Jh8iBZuyfML+/5CasaZ1E7Lqg6g3uqVQq75NqIwkYmORJMPw==
+ dependencies:
+ invariant "^2.2.4"
+ nullthrows "^1.1.1"
+
+"@react-native/virtualized-lists@0.83.4":
+ version "0.83.4"
+ resolved "https://registry.yarnpkg.com/@react-native/virtualized-lists/-/virtualized-lists-0.83.4.tgz#11353cf121007782d05be9fd726aaf182fe2b495"
+ integrity sha512-vNF/8kokMW8JEjG4n+j7veLTjHRRABlt4CaTS6+wtqzvWxCJHNIC8fhCqrDPn9fIn8sNePd8DyiFVX5L9TBBRA==
dependencies:
invariant "^2.2.4"
nullthrows "^1.1.1"
-"@react-navigation/bottom-tabs@^7.3.10":
- version "7.3.14"
- resolved "https://registry.yarnpkg.com/@react-navigation/bottom-tabs/-/bottom-tabs-7.3.14.tgz#9ee02baea86ab24abe267726665bc69c6df0bf4c"
- integrity sha512-s2qinJggS2HYZdCOey9A+fN+bNpWeEKwiL/FjAVOTcv+uofxPWN6CtEZUZGPEjfRjis/srURBmCmpNZSI6sQ9Q==
+"@react-navigation/bottom-tabs@^7.4.0":
+ version "7.15.6"
+ resolved "https://registry.yarnpkg.com/@react-navigation/bottom-tabs/-/bottom-tabs-7.15.6.tgz#e1a86a7128b11ff037ff5e07b4b294e42a2cc1aa"
+ integrity sha512-olB+s0ApMzWN9t5Bk5Mj6ntSlVRz3B8v+1LtwGS/29lyC311G5es0kgxyzpGKE9gy6Ef8W526QH5cIka2jh0kQ==
dependencies:
- "@react-navigation/elements" "^2.4.3"
+ "@react-navigation/elements" "^2.9.11"
color "^4.2.3"
+ sf-symbols-typescript "^2.1.0"
-"@react-navigation/core@^7.10.0":
- version "7.10.0"
- resolved "https://registry.yarnpkg.com/@react-navigation/core/-/core-7.10.0.tgz#8205ea6b84ce34b2fc2c196701b4cd9b434211b9"
- integrity sha512-qZBA5gGm+9liT4+EHk+kl9apwvqh7HqhLF1XeX6SQRmC/n2QI0u1B8OevKc+EPUDEM9Od15IuwT/GRbSs7/Umw==
+"@react-navigation/core@^7.16.2":
+ version "7.16.2"
+ resolved "https://registry.yarnpkg.com/@react-navigation/core/-/core-7.16.2.tgz#d35c0fd6ffe18f62294908122b44cdd49fc6ba2e"
+ integrity sha512-0dbCC2aTjNW7MvG1fY7zeq6eYvmmaFCEnBDXPuMPJ8uKgfs9lFGXIQFIfBdmcBVX6vHhS+K213VCsuHSIv5jYw==
dependencies:
- "@react-navigation/routers" "^7.4.0"
+ "@react-navigation/routers" "^7.5.3"
escape-string-regexp "^4.0.0"
+ fast-deep-equal "^3.1.3"
nanoid "^3.3.11"
query-string "^7.1.3"
react-is "^19.1.0"
- use-latest-callback "^0.2.3"
+ use-latest-callback "^0.2.4"
use-sync-external-store "^1.5.0"
-"@react-navigation/elements@^2.3.8", "@react-navigation/elements@^2.4.3":
- version "2.4.3"
- resolved "https://registry.yarnpkg.com/@react-navigation/elements/-/elements-2.4.3.tgz#cc1dde4c98739d35a0c9c23872316063962cfaee"
- integrity sha512-psoNmnZ0DQIt9nxxPITVLtYW04PGCAfnmd/Pcd3yhiBs93aj+HYKH+SDZDpUnXMf3BN7Wvo4+jPI+/Xjqb+m9w==
+"@react-navigation/elements@^2.9.11":
+ version "2.9.11"
+ resolved "https://registry.yarnpkg.com/@react-navigation/elements/-/elements-2.9.11.tgz#7be3cdf1adb9e65f38bf8a3d91460181f7c042f3"
+ integrity sha512-O5KiwaVCcEVuqZgQ77xiBFSl1sha77rNMTFlLWYnom33ZHPDarV3bM9WNyVnMZxU8ZVTi02X3+ZhO0fSn5QYyg==
dependencies:
color "^4.2.3"
+ use-latest-callback "^0.2.4"
+ use-sync-external-store "^1.5.0"
-"@react-navigation/native-stack@^7.3.10":
- version "7.3.14"
- resolved "https://registry.yarnpkg.com/@react-navigation/native-stack/-/native-stack-7.3.14.tgz#d1c90f2e50cd13bbced923991cf2faee8083f725"
- integrity sha512-45Sf7ReqSCIySXS5nrKtLGmNlFXm5x+u32YQMwKDONCqVGOBCfo4ryKqeQq1EMJ7Py6IDyOwHMhA+jhNOxnfPw==
+"@react-navigation/native-stack@^7.3.16":
+ version "7.14.6"
+ resolved "https://registry.yarnpkg.com/@react-navigation/native-stack/-/native-stack-7.14.6.tgz#928d857d8b9278eb1e2b3dcc8070bd88d760a734"
+ integrity sha512-VRlC5mLanRPHK0E15Cild6U01Z5TDPBlmt5YcXRBc+hQTAMbMT9XcSTobf3sJXNY0zzDD1IpSs3Ynex/GU225g==
dependencies:
- "@react-navigation/elements" "^2.4.3"
+ "@react-navigation/elements" "^2.9.11"
+ color "^4.2.3"
+ sf-symbols-typescript "^2.1.0"
warn-once "^0.1.1"
-"@react-navigation/native@^7.1.6":
- version "7.1.10"
- resolved "https://registry.yarnpkg.com/@react-navigation/native/-/native-7.1.10.tgz#768f674f7c09b6a57215762052aa62a7dc107402"
- integrity sha512-Ug4IML0DkAxZTMF/E7lyyLXSclkGAYElY2cxZWITwfBjtlVeda0NjsdnTWY5EGjnd7bwvhTIUC+CO6qSlrDn5A==
+"@react-navigation/native@^7.1.8":
+ version "7.1.34"
+ resolved "https://registry.yarnpkg.com/@react-navigation/native/-/native-7.1.34.tgz#25c09241a4a1313e39c866c16dc279a3352e5068"
+ integrity sha512-zzQ0mKAhLsjTIsaoLfILKZVMObJzE0F+bOi0hl2Glt+1Rd2GtaWJ1Z024c3yLmX+Oc79pqoCQLBXpyxtrZu9NQ==
dependencies:
- "@react-navigation/core" "^7.10.0"
+ "@react-navigation/core" "^7.16.2"
escape-string-regexp "^4.0.0"
fast-deep-equal "^3.1.3"
nanoid "^3.3.11"
- use-latest-callback "^0.2.3"
+ use-latest-callback "^0.2.4"
-"@react-navigation/routers@^7.4.0":
- version "7.4.0"
- resolved "https://registry.yarnpkg.com/@react-navigation/routers/-/routers-7.4.0.tgz#5bace799713ac163310c18711b98dfbe418c6b36"
- integrity sha512-th5THnuWKJlmr7GGHiicy979di11ycDWub9iIXbEDvQwmwmsRzppmVbfs2nD8bC/MgyMgqWu/gxfys+HqN+kcw==
+"@react-navigation/routers@^7.5.3":
+ version "7.5.3"
+ resolved "https://registry.yarnpkg.com/@react-navigation/routers/-/routers-7.5.3.tgz#8002930ef5f62351be2475d0dffde3ffaee174d7"
+ integrity sha512-1tJHg4KKRJuQ1/EvJxatrMef3NZXEPzwUIUZ3n1yJ2t7Q97siwRtbynRpQG9/69ebbtiZ8W3ScOZF/OmhvM4Rg==
dependencies:
nanoid "^3.3.11"
@@ -3955,7 +4538,7 @@
"@types/tough-cookie" "*"
parse5 "^7.0.0"
-"@types/json-schema@*", "@types/json-schema@^7.0.15", "@types/json-schema@^7.0.9":
+"@types/json-schema@*", "@types/json-schema@^7.0.15":
version "7.0.15"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
@@ -4037,6 +4620,13 @@
"@types/prop-types" "*"
csstype "^3.0.2"
+"@types/react@^19.2.14":
+ version "19.2.14"
+ resolved "https://registry.yarnpkg.com/@types/react/-/react-19.2.14.tgz#39604929b5e3957e3a6fa0001dafb17c7af70bad"
+ integrity sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==
+ dependencies:
+ csstype "^3.2.2"
+
"@types/react@~19.0.10":
version "19.0.14"
resolved "https://registry.yarnpkg.com/@types/react/-/react-19.0.14.tgz#f2f62035290afd755095cb6644e10b599db72f4e"
@@ -4300,6 +4890,11 @@
resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406"
integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==
+"@ungap/structured-clone@^1.3.0":
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz#d06bbb384ebcf6c505fde1c3d0ed4ddffe0aaff8"
+ integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==
+
"@unrs/resolver-binding-darwin-arm64@1.7.8":
version "1.7.8"
resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.7.8.tgz#d78b964ed64e16103b5324b50dcb4277afeda4d7"
@@ -4428,6 +5023,11 @@
pathe "^1.1.1"
pretty-format "^29.7.0"
+"@vscode/sudo-prompt@^9.0.0":
+ version "9.3.2"
+ resolved "https://registry.yarnpkg.com/@vscode/sudo-prompt/-/sudo-prompt-9.3.2.tgz#692ba38df40bd3502ccc4e9f099fbbaedbd5f04e"
+ integrity sha512-gcXoCN00METUNFeQOFJ+C9xUI0DKB+0EGMVg7wbVYRHBw2Eq3fKisDZOkRdOz3kqXRKOENMfShPOmypw1/8nOw==
+
"@vue/compiler-core@3.5.18":
version "3.5.18"
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.5.18.tgz#521a138cdd970d9bfd27e42168d12f77a04b2074"
@@ -4761,7 +5361,7 @@ abort-controller@^3.0.0:
dependencies:
event-target-shim "^5.0.0"
-accepts@^1.3.7, accepts@^1.3.8, accepts@~1.3.5:
+accepts@^1.3.7, accepts@^1.3.8, accepts@~1.3.5, accepts@~1.3.8:
version "1.3.8"
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e"
integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==
@@ -4814,30 +5414,6 @@ agent-base@^7.1.2:
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.3.tgz#29435eb821bc4194633a5b89e5bc4703bafc25a1"
integrity sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==
-ajv-formats@^2.1.1:
- version "2.1.1"
- resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520"
- integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==
- dependencies:
- ajv "^8.0.0"
-
-ajv-keywords@^5.1.0:
- version "5.1.0"
- resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16"
- integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==
- dependencies:
- fast-deep-equal "^3.1.3"
-
-ajv@8.11.0:
- version "8.11.0"
- resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.0.tgz#977e91dd96ca669f54a11e23e378e33b884a565f"
- integrity sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==
- dependencies:
- fast-deep-equal "^3.1.1"
- json-schema-traverse "^1.0.0"
- require-from-string "^2.0.2"
- uri-js "^4.2.2"
-
ajv@^6.10.0, ajv@^6.12.4:
version "6.12.6"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
@@ -4848,7 +5424,7 @@ ajv@^6.10.0, ajv@^6.12.4:
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
-ajv@^8.0.0, ajv@^8.0.1, ajv@^8.9.0:
+ajv@^8.0.1:
version "8.13.0"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.13.0.tgz#a3939eaec9fb80d217ddf0c3376948c023f28c91"
integrity sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==
@@ -4858,6 +5434,16 @@ ajv@^8.0.0, ajv@^8.0.1, ajv@^8.9.0:
require-from-string "^2.0.2"
uri-js "^4.4.1"
+ajv@^8.11.0:
+ version "8.18.0"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.18.0.tgz#8864186b6738d003eb3a933172bb3833e10cefbc"
+ integrity sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==
+ dependencies:
+ fast-deep-equal "^3.1.3"
+ fast-uri "^3.0.1"
+ json-schema-traverse "^1.0.0"
+ require-from-string "^2.0.2"
+
algoliasearch@^5.14.2:
version "5.35.0"
resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-5.35.0.tgz#ce12d1d287d6f4a80b9998568f806c92dabba566"
@@ -4895,6 +5481,15 @@ ansi-escapes@^4.2.1, ansi-escapes@^4.3.2:
dependencies:
type-fest "^0.21.3"
+ansi-fragments@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/ansi-fragments/-/ansi-fragments-0.2.1.tgz#24409c56c4cc37817c3d7caa99d8969e2de5a05e"
+ integrity sha512-DykbNHxuXQwUDRv5ibc2b0x7uw7wmwOGLBUd5RmaQ5z8Lhx19vwvKV+FAsM5rEA6dEcHxX+/Ad5s9eF2k2bB+w==
+ dependencies:
+ colorette "^1.0.7"
+ slice-ansi "^2.0.0"
+ strip-ansi "^5.0.0"
+
ansi-regex@^4.1.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.1.tgz#164daac87ab2d6f6db3a29875e2d1766582dabed"
@@ -4910,7 +5505,7 @@ ansi-regex@^6.0.1:
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a"
integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==
-ansi-styles@^3.2.1:
+ansi-styles@^3.2.0, ansi-styles@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
@@ -4947,6 +5542,11 @@ anymatch@^3.0.3, anymatch@~3.1.2:
normalize-path "^3.0.0"
picomatch "^2.0.4"
+appdirsjs@^1.2.4:
+ version "1.2.7"
+ resolved "https://registry.yarnpkg.com/appdirsjs/-/appdirsjs-1.2.7.tgz#50b4b7948a26ba6090d4aede2ae2dc2b051be3b3"
+ integrity sha512-Quji6+8kLBC3NnBeo14nPDq0+2jUs5s3/xEye+udFHumHhRk4M7aAMXp/PBJqkKYGuuyR9M/6Dq7d2AViiGmhw==
+
archiver-utils@^5.0.0, archiver-utils@^5.0.2:
version "5.0.2"
resolved "https://registry.yarnpkg.com/archiver-utils/-/archiver-utils-5.0.2.tgz#63bc719d951803efc72cf961a56ef810760dd14d"
@@ -4995,6 +5595,13 @@ argparse@^2.0.1:
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
+aria-hidden@^1.2.4:
+ version "1.2.6"
+ resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.6.tgz#73051c9b088114c795b1ea414e9c0fff874ffc1a"
+ integrity sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==
+ dependencies:
+ tslib "^2.0.0"
+
aria-query@^5.0.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e"
@@ -5140,6 +5747,11 @@ ast-types@^0.13.4:
dependencies:
tslib "^2.0.1"
+astral-regex@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9"
+ integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==
+
astral-regex@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
@@ -5268,17 +5880,31 @@ babel-plugin-polyfill-regenerator@^0.6.5:
dependencies:
"@babel/helper-define-polyfill-provider" "^0.6.5"
-babel-plugin-react-native-web@~0.19.13:
- version "0.19.13"
- resolved "https://registry.yarnpkg.com/babel-plugin-react-native-web/-/babel-plugin-react-native-web-0.19.13.tgz#bf919bd6f18c4689dd1a528a82bda507363b953d"
- integrity sha512-4hHoto6xaN23LCyZgL9LJZc3olmAxd7b6jDzlZnKXAh4rRAbZRKNBJoOOdp46OBqgy+K0t0guTj5/mhA8inymQ==
+babel-plugin-react-compiler@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-react-compiler/-/babel-plugin-react-compiler-1.0.0.tgz#bdf7360a23a4d5ebfca090255da3893efd07425f"
+ integrity sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==
+ dependencies:
+ "@babel/types" "^7.26.0"
+
+babel-plugin-react-native-web@~0.21.0:
+ version "0.21.2"
+ resolved "https://registry.yarnpkg.com/babel-plugin-react-native-web/-/babel-plugin-react-native-web-0.21.2.tgz#d2f7fd673278da82577aa583457edb55d9cccbe0"
+ integrity sha512-SPD0J6qjJn8231i0HZhlAGH6NORe+QvRSQM2mwQEzJ2Fb3E4ruWTiiicPlHjmeWShDXLcvoorOCXjeR7k/lyWA==
-babel-plugin-syntax-hermes-parser@0.25.1, babel-plugin-syntax-hermes-parser@^0.25.1:
- version "0.25.1"
- resolved "https://registry.yarnpkg.com/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.25.1.tgz#58b539df973427fcfbb5176a3aec7e5dee793cb0"
- integrity sha512-IVNpGzboFLfXZUAwkLFcI/bnqVbwky0jP3eBno4HKtqvQJAHBLdgxiG6lQ4to0+Q/YCN3PO0od5NZwIKyY4REQ==
+babel-plugin-syntax-hermes-parser@0.29.1, babel-plugin-syntax-hermes-parser@^0.29.1:
+ version "0.29.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.29.1.tgz#09ca9ecb0330eba1ef939b6d3f1f55bb06a9dc33"
+ integrity sha512-2WFYnoWGdmih1I1J5eIqxATOeycOqRwYxAQBu3cUu/rhwInwHUg7k60AFNbuGjSDL8tje5GDrAnxzRLcu2pYcA==
dependencies:
- hermes-parser "0.25.1"
+ hermes-parser "0.29.1"
+
+babel-plugin-syntax-hermes-parser@0.32.0:
+ version "0.32.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.32.0.tgz#06f7452bf91adf6cafd7c98e7467404d4eb65cec"
+ integrity sha512-m5HthL++AbyeEA2FcdwOLfVFvWYECOBObLHNqdR8ceY4TsEdn4LdX2oTvbB2QJSSElE2AWA/b2MXZ/PF/CqLZg==
+ dependencies:
+ hermes-parser "0.32.0"
babel-plugin-transform-flow-enums@^0.0.2:
version "0.0.2"
@@ -5305,15 +5931,16 @@ babel-preset-current-node-syntax@^1.0.0:
"@babel/plugin-syntax-optional-chaining" "^7.8.3"
"@babel/plugin-syntax-top-level-await" "^7.8.3"
-babel-preset-expo@~13.2.0:
- version "13.2.0"
- resolved "https://registry.yarnpkg.com/babel-preset-expo/-/babel-preset-expo-13.2.0.tgz#d4540009d07242e3c3d63184b7a34efda95e8e64"
- integrity sha512-oNUeUZPMNRPmx/2jaKJLSQFP/MFI1M91vP+Gp+j8/FPl9p/ps603DNwCaRdcT/Vj3FfREdlIwRio1qDCjY0oAA==
+babel-preset-expo@~54.0.10:
+ version "54.0.10"
+ resolved "https://registry.yarnpkg.com/babel-preset-expo/-/babel-preset-expo-54.0.10.tgz#3b70f4af3a5f65f945d7957ef511ee016e8f2fd6"
+ integrity sha512-wTt7POavLFypLcPW/uC5v8y+mtQKDJiyGLzYCjqr9tx0Qc3vCXcDKk1iCFIj/++Iy5CWhhTflEa7VvVPNWeCfw==
dependencies:
"@babel/helper-module-imports" "^7.25.9"
"@babel/plugin-proposal-decorators" "^7.12.9"
"@babel/plugin-proposal-export-default-from" "^7.24.7"
"@babel/plugin-syntax-export-default-from" "^7.24.7"
+ "@babel/plugin-transform-class-static-block" "^7.27.1"
"@babel/plugin-transform-export-namespace-from" "^7.25.9"
"@babel/plugin-transform-flow-strip-types" "^7.25.2"
"@babel/plugin-transform-modules-commonjs" "^7.24.8"
@@ -5324,12 +5951,12 @@ babel-preset-expo@~13.2.0:
"@babel/plugin-transform-runtime" "^7.24.7"
"@babel/preset-react" "^7.22.15"
"@babel/preset-typescript" "^7.23.0"
- "@react-native/babel-preset" "0.79.3"
- babel-plugin-react-native-web "~0.19.13"
- babel-plugin-syntax-hermes-parser "^0.25.1"
+ "@react-native/babel-preset" "0.81.5"
+ babel-plugin-react-compiler "^1.0.0"
+ babel-plugin-react-native-web "~0.21.0"
+ babel-plugin-syntax-hermes-parser "^0.29.1"
babel-plugin-transform-flow-enums "^0.0.2"
debug "^4.3.4"
- react-refresh "^0.14.2"
resolve-from "^5.0.0"
babel-preset-jest@^29.6.3:
@@ -5345,6 +5972,11 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
+balanced-match@^4.0.2:
+ version "4.0.4"
+ resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-4.0.4.tgz#bfb10662feed8196a2c62e7c68e17720c274179a"
+ integrity sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==
+
bare-events@^2.0.0, bare-events@^2.2.0:
version "2.2.2"
resolved "https://registry.yarnpkg.com/bare-events/-/bare-events-2.2.2.tgz#a98a41841f98b2efe7ecc5c5468814469b018078"
@@ -5383,6 +6015,11 @@ base64-js@^1.2.3, base64-js@^1.3.0, base64-js@^1.3.1, base64-js@^1.5.1:
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
+baseline-browser-mapping@^2.9.0:
+ version "2.10.10"
+ resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.10.tgz#e74bd066724c1d8d7d8ea75fc3be25389a7a5c56"
+ integrity sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ==
+
basic-ftp@^5.0.2:
version "5.0.5"
resolved "https://registry.yarnpkg.com/basic-ftp/-/basic-ftp-5.0.5.tgz#14a474f5fffecca1f4f406f1c26b18f800225ac0"
@@ -5439,6 +6076,21 @@ body-parser@^2.2.0:
raw-body "^3.0.0"
type-is "^2.0.0"
+body-parser@^2.2.2:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-2.2.2.tgz#1a32cdb966beaf68de50a9dfbe5b58f83cb8890c"
+ integrity sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==
+ dependencies:
+ bytes "^3.1.2"
+ content-type "^1.0.5"
+ debug "^4.4.3"
+ http-errors "^2.0.0"
+ iconv-lite "^0.7.0"
+ on-finished "^2.4.1"
+ qs "^6.14.1"
+ raw-body "^3.0.1"
+ type-is "^2.0.1"
+
bplist-creator@0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/bplist-creator/-/bplist-creator-0.1.0.tgz#018a2d1b587f769e379ef5519103730f8963ba1e"
@@ -5475,6 +6127,13 @@ brace-expansion@^2.0.1:
dependencies:
balanced-match "^1.0.0"
+brace-expansion@^5.0.2:
+ version "5.0.4"
+ resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.4.tgz#614daaecd0a688f660bbbc909a8748c3d80d4336"
+ integrity sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==
+ dependencies:
+ balanced-match "^4.0.2"
+
braces@^3.0.2, braces@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
@@ -5519,6 +6178,17 @@ browserslist@^4.24.4:
node-releases "^2.0.19"
update-browserslist-db "^1.1.3"
+browserslist@^4.25.0:
+ version "4.28.1"
+ resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.1.tgz#7f534594628c53c63101079e27e40de490456a95"
+ integrity sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==
+ dependencies:
+ baseline-browser-mapping "^2.9.0"
+ caniuse-lite "^1.0.30001759"
+ electron-to-chromium "^1.5.263"
+ node-releases "^2.0.27"
+ update-browserslist-db "^1.2.0"
+
browserslist@^4.25.3:
version "4.25.4"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.25.4.tgz#ebdd0e1d1cf3911834bab3a6cd7b917d9babf5af"
@@ -5579,7 +6249,7 @@ bytes@3.0.0:
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==
-bytes@3.1.2, bytes@^3.1.2:
+bytes@3.1.2, bytes@^3.1.2, bytes@~3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==
@@ -5639,31 +6309,12 @@ call-bound@^1.0.2, call-bound@^1.0.3, call-bound@^1.0.4:
call-bind-apply-helpers "^1.0.2"
get-intrinsic "^1.3.0"
-caller-callsite@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134"
- integrity sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ==
- dependencies:
- callsites "^2.0.0"
-
-caller-path@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-2.0.0.tgz#468f83044e369ab2010fac5f06ceee15bb2cb1f4"
- integrity sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A==
- dependencies:
- caller-callsite "^2.0.0"
-
-callsites@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50"
- integrity sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ==
-
callsites@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
-camelcase@^5.3.1:
+camelcase@^5.0.0, camelcase@^5.3.1:
version "5.3.1"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
@@ -5693,6 +6344,11 @@ caniuse-lite@^1.0.30001737:
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001741.tgz#67fb92953edc536442f3c9da74320774aa523143"
integrity sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw==
+caniuse-lite@^1.0.30001759:
+ version "1.0.30001780"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001780.tgz#0e413de292808868a62ed9118822683fa120a110"
+ integrity sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ==
+
ccount@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5"
@@ -5848,6 +6504,15 @@ client-only@^0.0.1:
resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
+cliui@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1"
+ integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==
+ dependencies:
+ string-width "^4.2.0"
+ strip-ansi "^6.0.0"
+ wrap-ansi "^6.2.0"
+
cliui@^8.0.1:
version "8.0.1"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa"
@@ -5921,6 +6586,11 @@ color@^4.2.3:
color-convert "^2.0.1"
color-string "^1.9.0"
+colorette@^1.0.7:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.4.0.tgz#5190fbb87276259a86ad700bff2c6d6faa3fca40"
+ integrity sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==
+
combined-stream@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
@@ -5933,6 +6603,11 @@ comma-separated-tokens@^2.0.0:
resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee"
integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==
+command-exists@^1.2.8:
+ version "1.2.9"
+ resolved "https://registry.yarnpkg.com/command-exists/-/command-exists-1.2.9.tgz#c50725af3808c8ab0260fd60b01fbfa25b954f69"
+ integrity sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==
+
commander@^12.0.0:
version "12.1.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-12.1.0.tgz#01423b36f501259fdaac4d0e4d60c96c991585d3"
@@ -5953,7 +6628,7 @@ commander@^7.2.0:
resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
-commander@^9.3.0:
+commander@^9.3.0, commander@^9.4.1:
version "9.5.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-9.5.0.tgz#bc08d1eb5cedf7ccb797a96199d41c7bc3e60d30"
integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==
@@ -5979,13 +6654,26 @@ compress-commons@^6.0.2:
normalize-path "^3.0.0"
readable-stream "^4.0.0"
-compressible@~2.0.16:
+compressible@~2.0.16, compressible@~2.0.18:
version "2.0.18"
resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba"
integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==
dependencies:
mime-db ">= 1.43.0 < 2"
+compression@^1.7.1:
+ version "1.8.1"
+ resolved "https://registry.yarnpkg.com/compression/-/compression-1.8.1.tgz#4a45d909ac16509195a9a28bd91094889c180d79"
+ integrity sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==
+ dependencies:
+ bytes "3.1.2"
+ compressible "~2.0.18"
+ debug "2.6.9"
+ negotiator "~0.6.4"
+ on-headers "~1.1.0"
+ safe-buffer "5.2.1"
+ vary "~1.1.2"
+
compression@^1.7.4:
version "1.7.4"
resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f"
@@ -6085,16 +6773,6 @@ core-util-is@~1.0.0:
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
-cosmiconfig@^5.0.5:
- version "5.2.1"
- resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a"
- integrity sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==
- dependencies:
- import-fresh "^2.0.0"
- is-directory "^0.3.1"
- js-yaml "^3.13.1"
- parse-json "^4.0.0"
-
cosmiconfig@^8.1.3:
version "8.3.6"
resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.3.6.tgz#060a2b871d66dba6c8538ea1118ba1ac16f5fae3"
@@ -6178,11 +6856,6 @@ cross-spawn@^7.0.6:
shebang-command "^2.0.0"
which "^2.0.1"
-crypto-random-string@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5"
- integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==
-
css-in-js-utils@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz#640ae6a33646d401fc720c54fc61c42cd76ae2bb"
@@ -6213,6 +6886,11 @@ csstype@^3.0.2, csstype@^3.1.3:
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
+csstype@^3.2.2:
+ version "3.2.3"
+ resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.2.3.tgz#ec48c0f3e993e50648c86da559e2610995cf989a"
+ integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==
+
data-uri-to-buffer@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz#d8feb2b2881e6a4f58c2e08acfd0e2834e26222e"
@@ -6290,7 +6968,12 @@ date-format@4.0.3:
resolved "https://registry.yarnpkg.com/date-format/-/date-format-4.0.3.tgz#f63de5dc08dc02efd8ef32bf2a6918e486f35873"
integrity sha512-7P3FyqDcfeznLZp2b+OMitV9Sz2lUnsT87WaTat9nVwqsBkTzPG3lPLNwW3en6F4pHUiWzr6vb8CLhjdK9bcxQ==
-debug@2.6.9, debug@^2.2.0, debug@^2.6.9:
+dayjs@^1.8.15:
+ version "1.11.20"
+ resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.20.tgz#88d919fd639dc991415da5f4cb6f1b6650811938"
+ integrity sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==
+
+debug@2.6.9, debug@^2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
@@ -6318,6 +7001,18 @@ debug@^4.3.5, debug@^4.4.0, debug@^4.4.1:
dependencies:
ms "^2.1.3"
+debug@^4.4.3:
+ version "4.4.3"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a"
+ integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==
+ dependencies:
+ ms "^2.1.3"
+
+decamelize@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
+ integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==
+
decamelize@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-6.0.0.tgz#8cad4d916fde5c41a264a43d0ecc56fe3d31749e"
@@ -6360,7 +7055,7 @@ deepmerge-ts@^5.0.0, deepmerge-ts@^5.1.0:
resolved "https://registry.yarnpkg.com/deepmerge-ts/-/deepmerge-ts-5.1.0.tgz#c55206cc4c7be2ded89b9c816cf3608884525d7a"
integrity sha512-eS8dRJOckyo9maw9Tu5O5RUi/4inFLrnoLkBe3cPfDMx3WZioXtmOew4TXQaxq7Rhl4xjDtR7c6x8nNTxOvbFw==
-deepmerge@^4.2.2, deepmerge@^4.3.1:
+deepmerge@^4.2.2, deepmerge@^4.3.0, deepmerge@^4.3.1:
version "4.3.1"
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a"
integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==
@@ -6429,16 +7124,21 @@ destroy@1.2.0:
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015"
integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==
-detect-libc@^1.0.3:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
- integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==
+detect-libc@^2.0.3:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.1.2.tgz#689c5dcdc1900ef5583a4cb9f6d7b473742074ad"
+ integrity sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==
detect-newline@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==
+detect-node-es@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493"
+ integrity sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==
+
devlop@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/devlop/-/devlop-1.1.0.tgz#4db7c2ca4dc6e0e834c30be70c94bbc976dc7018"
@@ -6592,6 +7292,11 @@ electron-to-chromium@^1.5.211:
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.217.tgz#71285850356ef48bc08275b26f0f355721e0f17d"
integrity sha512-Pludfu5iBxp9XzNl0qq2G87hdD17ZV7h5T4n6rQXDi3nCyloBV3jreE9+8GC6g4X/5yxqVgXEURpcLtM0WS4jA==
+electron-to-chromium@^1.5.263:
+ version "1.5.321"
+ resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.321.tgz#57a80554e2e7fd65e3689d320f52a64723472d5d"
+ integrity sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==
+
electron-to-chromium@^1.5.28:
version "1.5.35"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.35.tgz#1d38d386186c72b1fa6e74c3a7de5f888b503100"
@@ -6662,6 +7367,11 @@ env-paths@^2.2.1:
resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2"
integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==
+envinfo@^7.13.0:
+ version "7.21.0"
+ resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.21.0.tgz#04a251be79f92548541f37d13c8b6f22940c3bae"
+ integrity sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==
+
error-ex@^1.3.1, error-ex@^1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
@@ -6676,6 +7386,14 @@ error-stack-parser@^2.0.6:
dependencies:
stackframe "^1.3.4"
+errorhandler@^1.5.1:
+ version "1.5.2"
+ resolved "https://registry.yarnpkg.com/errorhandler/-/errorhandler-1.5.2.tgz#dd0aa3952eca44aff7c2985e7d246c5932d70444"
+ integrity sha512-kNAL7hESndBCrWwS72QyV3IVOTrVmj9D062FV5BQswNL5zEdeRmz/WJFyh6Aj/plvvSOrzddkxW57HgkZcR9Fw==
+ dependencies:
+ accepts "~1.3.8"
+ escape-html "~1.0.3"
+
es-abstract@^1.17.5, es-abstract@^1.23.5, es-abstract@^1.23.6, es-abstract@^1.23.9:
version "1.23.9"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.9.tgz#5b45994b7de78dada5c1bebf1379646b32b9d606"
@@ -7388,7 +8106,7 @@ etag@^1.8.1, etag@~1.8.1:
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==
-event-target-shim@^5.0.0, event-target-shim@^5.0.1:
+event-target-shim@^5.0.0:
version "5.0.1"
resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
@@ -7398,11 +8116,6 @@ events@^3.3.0:
resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
-exec-async@^2.2.0:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/exec-async/-/exec-async-2.2.0.tgz#c7c5ad2eef3478d38390c6dd3acfe8af0efc8301"
- integrity sha512-87OpwcEiMia/DeiKFzaQNBNFeN3XkkpYIh9FyOqq5mS2oKv3CBE67PXoEKcr6nodWdXNogTiQ0jE2NGuoffXPw==
-
execa@^5.0.0:
version "5.1.1"
resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd"
@@ -7463,216 +8176,238 @@ expect@^29.0.0, expect@^29.7.0:
jest-message-util "^29.7.0"
jest-util "^29.7.0"
-expo-asset@~11.1.5:
- version "11.1.5"
- resolved "https://registry.yarnpkg.com/expo-asset/-/expo-asset-11.1.5.tgz#5cad3d781c9d0edec31b9b3adbba574eb4d5dd3e"
- integrity sha512-GEQDCqC25uDBoXHEnXeBuwpeXvI+3fRGvtzwwt0ZKKzWaN+TgeF8H7c76p3Zi4DfBMFDcduM0CmOvJX+yCCLUQ==
+expo-asset@~12.0.12:
+ version "12.0.12"
+ resolved "https://registry.yarnpkg.com/expo-asset/-/expo-asset-12.0.12.tgz#15eb7d92cd43cc81c37149e5bbcdc3091875a85b"
+ integrity sha512-CsXFCQbx2fElSMn0lyTdRIyKlSXOal6ilLJd+yeZ6xaC7I9AICQgscY5nj0QcwgA+KYYCCEQEBndMsmj7drOWQ==
dependencies:
- "@expo/image-utils" "^0.7.4"
- expo-constants "~17.1.5"
+ "@expo/image-utils" "^0.8.8"
+ expo-constants "~18.0.12"
-expo-blur@~14.1.4:
- version "14.1.4"
- resolved "https://registry.yarnpkg.com/expo-blur/-/expo-blur-14.1.4.tgz#d246c0a224ce63321d022edfc0e6a8c5fa2cc865"
- integrity sha512-55P9tK/RjJZEcu2tU7BqX3wmIOrGMOOkmHztJMMws+ZGHzvtjnPmT7dsQxhOU9vPj77oHnKetYHU2sik3iBcCw==
+expo-blur@~15.0.7:
+ version "15.0.8"
+ resolved "https://registry.yarnpkg.com/expo-blur/-/expo-blur-15.0.8.tgz#846cea275a6644639c5c338987b4cca54cf93eca"
+ integrity sha512-rWyE1NBRZEu9WD+X+5l7gyPRszw7n12cW3IRNAb5i6KFzaBp8cxqT5oeaphJapqURvcqhkOZn2k5EtBSbsuU7w==
-expo-constants@~17.1.5, expo-constants@~17.1.6:
- version "17.1.6"
- resolved "https://registry.yarnpkg.com/expo-constants/-/expo-constants-17.1.6.tgz#a31b019216f7f7bb4907aeffa2d6bf856751985e"
- integrity sha512-q5mLvJiLtPcaZ7t2diSOlQ2AyxIO8YMVEJsEfI/ExkGj15JrflNQ7CALEW6IF/uNae/76qI/XcjEuuAyjdaCNw==
+expo-build-properties@^1.0.10:
+ version "1.0.10"
+ resolved "https://registry.yarnpkg.com/expo-build-properties/-/expo-build-properties-1.0.10.tgz#2c3fb4248f78828e952defa636635a653e3ad546"
+ integrity sha512-mFCZbrbrv0AP5RB151tAoRzwRJelqM7bCJzCkxpu+owOyH+p/rFC/q7H5q8B9EpVWj8etaIuszR+gKwohpmu1Q==
+ dependencies:
+ ajv "^8.11.0"
+ semver "^7.6.0"
+
+expo-constants@~18.0.10, expo-constants@~18.0.12, expo-constants@~18.0.13:
+ version "18.0.13"
+ resolved "https://registry.yarnpkg.com/expo-constants/-/expo-constants-18.0.13.tgz#0117f1f3d43be7b645192c0f4f431fb4efc4803d"
+ integrity sha512-FnZn12E1dRYKDHlAdIyNFhBurKTS3F9CrfrBDJI5m3D7U17KBHMQ6JEfYlSj7LG7t+Ulr+IKaj58L1k5gBwTcQ==
dependencies:
- "@expo/config" "~11.0.9"
- "@expo/env" "~1.0.5"
+ "@expo/config" "~12.0.13"
+ "@expo/env" "~2.0.8"
-expo-crypto@~14.1.4:
- version "14.1.4"
- resolved "https://registry.yarnpkg.com/expo-crypto/-/expo-crypto-14.1.4.tgz#4d43a52930f359875b6880775688d6d6c0815c1d"
- integrity sha512-RmKhB3FgvKE5aNFBw4+hifOkyE0tywsDQVksdHA3jFRzcU9toFiJAz6nhPsBKDf5JlzJiIXhbNMtydoWtuuE7w==
+expo-crypto@~15.0.7:
+ version "15.0.8"
+ resolved "https://registry.yarnpkg.com/expo-crypto/-/expo-crypto-15.0.8.tgz#339198aae149b3ccc0b44f7150d7261a3a1f5287"
+ integrity sha512-aF7A914TB66WIlTJvl5J6/itejfY78O7dq3ibvFltL9vnTALJ/7LYHvLT4fwmx9yUNS6ekLBtDGWivFWnj2Fcw==
dependencies:
base64-js "^1.3.0"
-expo-dev-client@~5.1.8:
- version "5.1.8"
- resolved "https://registry.yarnpkg.com/expo-dev-client/-/expo-dev-client-5.1.8.tgz#53380ec440f26af65b94bcd8e66df3f448ff9bf7"
- integrity sha512-IopYPgBi3JflksO5ieTphbKsbYHy9iIVdT/d69It++y0iBMSm0oBIoDmUijrHKjE3fV6jnrwrm8luU13/mzIQQ==
- dependencies:
- expo-dev-launcher "5.1.11"
- expo-dev-menu "6.1.10"
- expo-dev-menu-interface "1.10.0"
- expo-manifests "~0.16.4"
- expo-updates-interface "~1.1.0"
-
-expo-dev-launcher@5.1.11:
- version "5.1.11"
- resolved "https://registry.yarnpkg.com/expo-dev-launcher/-/expo-dev-launcher-5.1.11.tgz#059e09152f94593e09b8adff853234565604433b"
- integrity sha512-bN0+nv5H038s8Gzf8i16hwCyD3sWDmHp7vb+QbL1i6B3XNnICCKS/H/3VH6H3PRMvCmoLGPlg+ODDqGlf0nu3g==
- dependencies:
- ajv "8.11.0"
- expo-dev-menu "6.1.10"
- expo-manifests "~0.16.4"
- resolve-from "^5.0.0"
+expo-dev-client@~6.0.18:
+ version "6.0.20"
+ resolved "https://registry.yarnpkg.com/expo-dev-client/-/expo-dev-client-6.0.20.tgz#d5b65974785100ae7f2538e16701fa1ef55f5fad"
+ integrity sha512-5XjoVlj1OxakNxy55j/AUaGPrDOlQlB6XdHLLWAw61w5ffSpUDHDnuZzKzs9xY1eIaogOqTOQaAzZ2ddBkdXLA==
+ dependencies:
+ expo-dev-launcher "6.0.20"
+ expo-dev-menu "7.0.18"
+ expo-dev-menu-interface "2.0.0"
+ expo-manifests "~1.0.10"
+ expo-updates-interface "~2.0.0"
+
+expo-dev-launcher@6.0.20:
+ version "6.0.20"
+ resolved "https://registry.yarnpkg.com/expo-dev-launcher/-/expo-dev-launcher-6.0.20.tgz#b2ce90ff6af4c4de28bc1ea595b0b504ed9b467d"
+ integrity sha512-a04zHEeT9sB0L5EB38fz7sNnUKJ2Ar1pXpcyl60Ki8bXPNCs9rjY7NuYrDkP/irM8+1DklMBqHpyHiLyJ/R+EA==
+ dependencies:
+ ajv "^8.11.0"
+ expo-dev-menu "7.0.18"
+ expo-manifests "~1.0.10"
-expo-dev-menu-interface@1.10.0:
- version "1.10.0"
- resolved "https://registry.yarnpkg.com/expo-dev-menu-interface/-/expo-dev-menu-interface-1.10.0.tgz#04671bda3c163d1d7b9438ce7095c3913a3f53f9"
- integrity sha512-NxtM/qot5Rh2cY333iOE87dDg1S8CibW+Wu4WdLua3UMjy81pXYzAGCZGNOeY7k9GpNFqDPNDXWyBSlk9r2pBg==
+expo-dev-menu-interface@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/expo-dev-menu-interface/-/expo-dev-menu-interface-2.0.0.tgz#c0d6db65eb4abc44a2701bc2303744619ad05ca6"
+ integrity sha512-BvAMPt6x+vyXpThsyjjOYyjwfjREV4OOpQkZ0tNl+nGpsPfcY9mc6DRACoWnH9KpLzyIt3BOgh3cuy/h/OxQjw==
-expo-dev-menu@6.1.10:
- version "6.1.10"
- resolved "https://registry.yarnpkg.com/expo-dev-menu/-/expo-dev-menu-6.1.10.tgz#7970c03748d08cab12222eec0b7da6bc0aa90232"
- integrity sha512-LaI0Bw5zzw5XefjYSX6YaMydzk0YBysjqQoxzj6ufDyKgwAfPmFwOLkZ03DOSerc9naezGLNAGgTEN6QTgMmgQ==
+expo-dev-menu@7.0.18:
+ version "7.0.18"
+ resolved "https://registry.yarnpkg.com/expo-dev-menu/-/expo-dev-menu-7.0.18.tgz#4f3e2dc20b82fc495afb602301b83fa16430f6b8"
+ integrity sha512-4kTdlHrnZCAWCT6tZRQHSSjZ7vECFisL4T+nsG/GJDo/jcHNaOVGV5qPV9wzlTxyMk3YOPggRw4+g7Ownrg5eA==
dependencies:
- expo-dev-menu-interface "1.10.0"
+ expo-dev-menu-interface "2.0.0"
-expo-file-system@~18.1.10:
- version "18.1.10"
- resolved "https://registry.yarnpkg.com/expo-file-system/-/expo-file-system-18.1.10.tgz#22f3bcc2c9a7edcd6bba5ece3c90a8467fda47be"
- integrity sha512-SyaWg+HitScLuyEeSG9gMSDT0hIxbM9jiZjSBP9l9zMnwZjmQwsusE6+7qGiddxJzdOhTP4YGUfvEzeeS0YL3Q==
+expo-file-system@~19.0.21:
+ version "19.0.21"
+ resolved "https://registry.yarnpkg.com/expo-file-system/-/expo-file-system-19.0.21.tgz#e96a68107fb629cf0dd1906fe7b46b566ff13e10"
+ integrity sha512-s3DlrDdiscBHtab/6W1osrjGL+C2bvoInPJD7sOwmxfJ5Woynv2oc+Fz1/xVXaE/V7HE/+xrHC/H45tu6lZzzg==
-expo-font@~13.3.1:
- version "13.3.1"
- resolved "https://registry.yarnpkg.com/expo-font/-/expo-font-13.3.1.tgz#ed69ae14f263a4c447efb2615b60d9e045372e68"
- integrity sha512-d+xrHYvSM9WB42wj8vP9OOFWyxed5R1evphfDb6zYBmC1dA9Hf89FpT7TNFtj2Bk3clTnpmVqQTCYbbA2P3CLg==
+expo-font@~14.0.11, expo-font@~14.0.9:
+ version "14.0.11"
+ resolved "https://registry.yarnpkg.com/expo-font/-/expo-font-14.0.11.tgz#198743d17332520545107df026d8a261e6b2732f"
+ integrity sha512-ga0q61ny4s/kr4k8JX9hVH69exVSIfcIc19+qZ7gt71Mqtm7xy2c6kwsPTCyhBW2Ro5yXTT8EaZOpuRi35rHbg==
dependencies:
fontfaceobserver "^2.1.0"
-expo-haptics@~14.1.4:
- version "14.1.4"
- resolved "https://registry.yarnpkg.com/expo-haptics/-/expo-haptics-14.1.4.tgz#442f48b1bdf83484d4fcadc653445aaae6049b70"
- integrity sha512-QZdE3NMX74rTuIl82I+n12XGwpDWKb8zfs5EpwsnGi/D/n7O2Jd4tO5ivH+muEG/OCJOMq5aeaVDqqaQOhTkcA==
+expo-haptics@~15.0.7:
+ version "15.0.8"
+ resolved "https://registry.yarnpkg.com/expo-haptics/-/expo-haptics-15.0.8.tgz#f93f895ac5d76fe0c5ac26b3644e1dbb097833f3"
+ integrity sha512-lftutojy8Qs8zaDzzjwM3gKHFZ8bOOEZDCkmh2Ddpe95Ra6kt2izeOfOfKuP/QEh0MZ1j9TfqippyHdRd1ZM9g==
-expo-image@~2.1.7:
- version "2.1.7"
- resolved "https://registry.yarnpkg.com/expo-image/-/expo-image-2.1.7.tgz#c64cf29363c87762c9f4b16a016a59ac15998bc2"
- integrity sha512-p2Gr8fP/YakFHHo4rbpJbRWwKNrZp1GzSD91WEG3ZYAbTVdTjheJ6gUxXgggFaxEbaY+4WeQ0c5j9tZq8+3cEg==
+expo-image@~3.0.10:
+ version "3.0.11"
+ resolved "https://registry.yarnpkg.com/expo-image/-/expo-image-3.0.11.tgz#54195565dc710e632c10414c3609deebb7149ac5"
+ integrity sha512-4TudfUCLgYgENv+f48omnU8tjS2S0Pd9EaON5/s1ZUBRwZ7K8acEr4NfvLPSaeXvxW24iLAiyQ7sV7BXQH3RoA==
expo-json-utils@~0.15.0:
version "0.15.0"
resolved "https://registry.yarnpkg.com/expo-json-utils/-/expo-json-utils-0.15.0.tgz#6723574814b9e6b0a90e4e23662be76123ab6ae9"
integrity sha512-duRT6oGl80IDzH2LD2yEFWNwGIC2WkozsB6HF3cDYNoNNdUvFk6uN3YiwsTsqVM/D0z6LEAQ01/SlYvN+Fw0JQ==
-expo-keep-awake@~14.1.4:
- version "14.1.4"
- resolved "https://registry.yarnpkg.com/expo-keep-awake/-/expo-keep-awake-14.1.4.tgz#80197728563e0e17523e5a606fbd6fbed9639503"
- integrity sha512-wU9qOnosy4+U4z/o4h8W9PjPvcFMfZXrlUoKTMBW7F4pLqhkkP/5G4EviPZixv4XWFMjn1ExQ5rV6BX8GwJsWA==
+expo-keep-awake@~15.0.8:
+ version "15.0.8"
+ resolved "https://registry.yarnpkg.com/expo-keep-awake/-/expo-keep-awake-15.0.8.tgz#911c5effeba9baff2ccde79ef0ff5bf856215f8d"
+ integrity sha512-YK9M1VrnoH1vLJiQzChZgzDvVimVoriibiDIFLbQMpjYBnvyfUeHJcin/Gx1a+XgupNXy92EQJLgI/9ZuXajYQ==
-expo-linking@~7.1.5:
- version "7.1.5"
- resolved "https://registry.yarnpkg.com/expo-linking/-/expo-linking-7.1.5.tgz#99633892712d5442ddb1c6c3857346eb7a67119b"
- integrity sha512-8g20zOpROW78bF+bLI4a3ZWj4ntLgM0rCewKycPL0jk9WGvBrBtFtwwADJgOiV1EurNp3lcquerXGlWS+SOQyA==
+expo-linking@~8.0.9:
+ version "8.0.11"
+ resolved "https://registry.yarnpkg.com/expo-linking/-/expo-linking-8.0.11.tgz#b13ca9fc409ef0536352443221eb50e5e2bee366"
+ integrity sha512-+VSaNL5om3kOp/SSKO5qe6cFgfSIWnnQDSbA7XLs3ECkYzXRquk5unxNS3pg7eK5kNUmQ4kgLI7MhTggAEUBLA==
dependencies:
- expo-constants "~17.1.6"
+ expo-constants "~18.0.12"
invariant "^2.2.4"
-expo-manifests@~0.16.4:
- version "0.16.5"
- resolved "https://registry.yarnpkg.com/expo-manifests/-/expo-manifests-0.16.5.tgz#bb57ceff3db4eb74679d4a155b2ca2050375ce10"
- integrity sha512-zLUeJogn2C7qOE75Zz7jcmJorMfIbSRR35ctspN0OK/Hq/+PAAptA8p9jNVC8xp/91uP9uI8f3xPhh+A11eR2A==
+expo-manifests@~1.0.10:
+ version "1.0.10"
+ resolved "https://registry.yarnpkg.com/expo-manifests/-/expo-manifests-1.0.10.tgz#5dfb3db1cdf6b46fee349f1d68a25edf5e087994"
+ integrity sha512-oxDUnURPcL4ZsOBY6X1DGWGuoZgVAFzp6PISWV7lPP2J0r8u1/ucuChBgpK7u1eLGFp6sDIPwXyEUCkI386XSQ==
dependencies:
- "@expo/config" "~11.0.10"
+ "@expo/config" "~12.0.11"
expo-json-utils "~0.15.0"
-expo-modules-autolinking@2.1.11:
- version "2.1.11"
- resolved "https://registry.yarnpkg.com/expo-modules-autolinking/-/expo-modules-autolinking-2.1.11.tgz#efc2e756ccc8b9e0b927596ba074aefe31b5cbe4"
- integrity sha512-KrWQo+cE4gWYNePBBhmHGVzf63gYV19ZLXe9EIH3GHTkViVzIX+Lp618H/7GxfawpN5kbhvilATH1QEKKnUUww==
+expo-modules-autolinking@3.0.24:
+ version "3.0.24"
+ resolved "https://registry.yarnpkg.com/expo-modules-autolinking/-/expo-modules-autolinking-3.0.24.tgz#55fdfe1ef5a053d5cc287582170a5f6d69ab0e30"
+ integrity sha512-TP+6HTwhL7orDvsz2VzauyQlXJcAWyU3ANsZ7JGL4DQu8XaZv/A41ZchbtAYLfozNA2Ya1Hzmhx65hXryBMjaQ==
dependencies:
"@expo/spawn-async" "^1.7.2"
chalk "^4.1.0"
commander "^7.2.0"
- find-up "^5.0.0"
- glob "^10.4.2"
require-from-string "^2.0.2"
resolve-from "^5.0.0"
-expo-modules-core@2.4.0:
- version "2.4.0"
- resolved "https://registry.yarnpkg.com/expo-modules-core/-/expo-modules-core-2.4.0.tgz#3081d62fadff913090cc5abfe46d9ec6b0e75789"
- integrity sha512-Ko5eHBdvuMykjw9P9C9PF54/wBSsGOxaOjx92I5BwgKvEmUwN3UrXFV4CXzlLVbLfSYUQaLcB220xmPfgvT7Fg==
+expo-modules-core@3.0.29:
+ version "3.0.29"
+ resolved "https://registry.yarnpkg.com/expo-modules-core/-/expo-modules-core-3.0.29.tgz#99287eba52f21784bcb2e4f4edd4fc4c21b5b265"
+ integrity sha512-LzipcjGqk8gvkrOUf7O2mejNWugPkf3lmd9GkqL9WuNyeN2fRwU0Dn77e3ZUKI3k6sI+DNwjkq4Nu9fNN9WS7Q==
dependencies:
invariant "^2.2.4"
-expo-router@~5.0.6:
- version "5.0.7"
- resolved "https://registry.yarnpkg.com/expo-router/-/expo-router-5.0.7.tgz#032c1de9d06237664169959f7178a818d7b0e677"
- integrity sha512-NlEgRXCKtseDuIHBp87UfkvqsuVrc0MYG+zg33dopaN6wik4RkrWWxUYdNPHub0s/7qMye6zZBY4ZCrXwd/xpA==
+expo-router@~6.0.15:
+ version "6.0.23"
+ resolved "https://registry.yarnpkg.com/expo-router/-/expo-router-6.0.23.tgz#480fbcb4901fd692f9d11762f33894280dcbd75a"
+ integrity sha512-qCxVAiCrCyu0npky6azEZ6dJDMt77OmCzEbpF6RbUTlfkaCA417LvY14SBkk0xyGruSxy/7pvJOI6tuThaUVCA==
dependencies:
- "@expo/metro-runtime" "5.0.4"
- "@expo/server" "^0.6.2"
+ "@expo/metro-runtime" "^6.1.2"
+ "@expo/schema-utils" "^0.1.8"
"@radix-ui/react-slot" "1.2.0"
- "@react-navigation/bottom-tabs" "^7.3.10"
- "@react-navigation/native" "^7.1.6"
- "@react-navigation/native-stack" "^7.3.10"
+ "@radix-ui/react-tabs" "^1.1.12"
+ "@react-navigation/bottom-tabs" "^7.4.0"
+ "@react-navigation/native" "^7.1.8"
+ "@react-navigation/native-stack" "^7.3.16"
client-only "^0.0.1"
+ debug "^4.3.4"
+ escape-string-regexp "^4.0.0"
+ expo-server "^1.0.5"
+ fast-deep-equal "^3.1.3"
invariant "^2.2.4"
+ nanoid "^3.3.8"
+ query-string "^7.1.3"
react-fast-compare "^3.2.2"
react-native-is-edge-to-edge "^1.1.6"
- schema-utils "^4.0.1"
semver "~7.6.3"
server-only "^0.0.1"
+ sf-symbols-typescript "^2.1.0"
shallowequal "^1.1.0"
+ use-latest-callback "^0.2.1"
+ vaul "^1.1.2"
+
+expo-server@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/expo-server/-/expo-server-1.0.5.tgz#2d52002199a2af99c2c8771d0657916004345ca9"
+ integrity sha512-IGR++flYH70rhLyeXF0Phle56/k4cee87WeQ4mamS+MkVAVP+dDlOHf2nN06Z9Y2KhU0Gp1k+y61KkghF7HdhA==
-expo-splash-screen@~0.30.8:
- version "0.30.8"
- resolved "https://registry.yarnpkg.com/expo-splash-screen/-/expo-splash-screen-0.30.8.tgz#2e960ccff053bc8ace85eb56f7d6745e4ddfc6b6"
- integrity sha512-2eh+uA543brfeG5HILXmtNKA7E2/pfywKzNumzy3Ef6OtDjYy6zJUGNSbhnZRbVEjUZo3/QNRs0JRBfY80okZg==
+expo-splash-screen@~31.0.11:
+ version "31.0.13"
+ resolved "https://registry.yarnpkg.com/expo-splash-screen/-/expo-splash-screen-31.0.13.tgz#f41f1a4c8bb1ae7fcc52b760e7dd485d7ddec642"
+ integrity sha512-1epJLC1cDlwwj089R2h8cxaU5uk4ONVAC+vzGiTZH4YARQhL4Stlz1MbR6yAS173GMosvkE6CAeihR7oIbCkDA==
dependencies:
- "@expo/prebuild-config" "^9.0.5"
+ "@expo/prebuild-config" "^54.0.8"
-expo-status-bar@~2.2.3:
- version "2.2.3"
- resolved "https://registry.yarnpkg.com/expo-status-bar/-/expo-status-bar-2.2.3.tgz#09385a866732328e0af3b4588c4f349a15fd7cd0"
- integrity sha512-+c8R3AESBoduunxTJ8353SqKAKpxL6DvcD8VKBuh81zzJyUUbfB4CVjr1GufSJEKsMzNPXZU+HJwXx7Xh7lx8Q==
+expo-status-bar@~3.0.8:
+ version "3.0.9"
+ resolved "https://registry.yarnpkg.com/expo-status-bar/-/expo-status-bar-3.0.9.tgz#87cfc803fa614f09a985b8e75e3dd7abd51ce2cb"
+ integrity sha512-xyYyVg6V1/SSOZWh4Ni3U129XHCnFHBTcUo0dhWtFDrZbNp/duw5AGsQfb2sVeU0gxWHXSY1+5F0jnKYC7WuOw==
dependencies:
- react-native-edge-to-edge "1.6.0"
- react-native-is-edge-to-edge "^1.1.6"
+ react-native-is-edge-to-edge "^1.2.1"
-expo-symbols@~0.4.4:
- version "0.4.4"
- resolved "https://registry.yarnpkg.com/expo-symbols/-/expo-symbols-0.4.4.tgz#3aff3c9001eb69bb5a10cc6280bd5bdfb8563296"
- integrity sha512-ZVTBdm48MUZsO/sRLrxezB37aazynn8pzpsIUwMqI7V5JtBPPb2gU7LRVPITRc0CqOA+OL01/PqFE3ifBUIP4A==
+expo-symbols@~1.0.7:
+ version "1.0.8"
+ resolved "https://registry.yarnpkg.com/expo-symbols/-/expo-symbols-1.0.8.tgz#a46ae8fb46eb07ae9974c445595d2f342ed4dd30"
+ integrity sha512-7bNjK350PaQgxBf0owpmSYkdZIpdYYmaPttDBb2WIp6rIKtcEtdzdfmhsc2fTmjBURHYkg36+eCxBFXO25/1hw==
dependencies:
sf-symbols-typescript "^2.0.0"
-expo-system-ui@~5.0.7:
- version "5.0.7"
- resolved "https://registry.yarnpkg.com/expo-system-ui/-/expo-system-ui-5.0.7.tgz#ccf047a689ab488d9bcda375afd063419578f494"
- integrity sha512-ijSnSFA4VfuQc84N6WyCUNsKKTIyQb6QuC8q2zGvYC/sBXTMrOtZg0zrisQGzCRW+WhritQTiVqHlp3Ix9xDmQ==
+expo-system-ui@~6.0.8:
+ version "6.0.9"
+ resolved "https://registry.yarnpkg.com/expo-system-ui/-/expo-system-ui-6.0.9.tgz#09b4a4301ab25ec594ae39beb7d24197c231a30c"
+ integrity sha512-eQTYGzw1V4RYiYHL9xDLYID3Wsec2aZS+ypEssmF64D38aDrqbDgz1a2MSlHLQp2jHXSs3FvojhZ9FVela1Zcg==
dependencies:
- "@react-native/normalize-colors" "0.79.2"
+ "@react-native/normalize-colors" "0.81.5"
debug "^4.3.2"
-expo-updates-interface@~1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/expo-updates-interface/-/expo-updates-interface-1.1.0.tgz#62497d4647b381da9fdb68868ed180203ae737ef"
- integrity sha512-DeB+fRe0hUDPZhpJ4X4bFMAItatFBUPjw/TVSbJsaf3Exeami+2qbbJhWkcTMoYHOB73nOIcaYcWXYJnCJXO0w==
+expo-updates-interface@~2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/expo-updates-interface/-/expo-updates-interface-2.0.0.tgz#7721cb64c37bcb46b23827b2717ef451a9378749"
+ integrity sha512-pTzAIufEZdVPKql6iMi5ylVSPqV1qbEopz9G6TSECQmnNde2nwq42PxdFBaUEd8IZJ/fdJLQnOT3m6+XJ5s7jg==
-expo-web-browser@~14.1.6:
- version "14.1.6"
- resolved "https://registry.yarnpkg.com/expo-web-browser/-/expo-web-browser-14.1.6.tgz#26d66e641e6e96d155be6fa513e7e667a719a0b0"
- integrity sha512-/4P8eWqRyfXIMZna3acg320LXNA+P2cwyEVbjDX8vHnWU+UnOtyRKWy3XaAIyMPQ9hVjBNUQTh4MPvtnPRzakw==
+expo-web-browser@~15.0.9:
+ version "15.0.10"
+ resolved "https://registry.yarnpkg.com/expo-web-browser/-/expo-web-browser-15.0.10.tgz#ee7fb59b4f143f262c13c020433a83444181f1a3"
+ integrity sha512-fvDhW4bhmXAeWFNFiInmsGCK83PAqAcQaFyp/3pE/jbdKmFKoRCWr46uZGIfN4msLK/OODhaQ/+US7GSJNDHJg==
-expo@~53.0.10:
- version "53.0.11"
- resolved "https://registry.yarnpkg.com/expo/-/expo-53.0.11.tgz#66053862520ce2a6700d13346ebaf8210a68f24b"
- integrity sha512-+QtvU+6VPd7/o4vmtwuRE/Li2rAiJtD25I6BOnoQSxphaWWaD0PdRQnIV3VQ0HESuJYRuKJ3DkAHNJ3jI6xwzA==
+expo@^54.0.0:
+ version "54.0.33"
+ resolved "https://registry.yarnpkg.com/expo/-/expo-54.0.33.tgz#f7d572857323f5a8250a9afe245a487d2ee2735f"
+ integrity sha512-3yOEfAKqo+gqHcV8vKcnq0uA5zxlohnhA3fu4G43likN8ct5ZZ3LjAh9wDdKteEkoad3tFPvwxmXW711S5OHUw==
dependencies:
"@babel/runtime" "^7.20.0"
- "@expo/cli" "0.24.14"
- "@expo/config" "~11.0.10"
- "@expo/config-plugins" "~10.0.2"
- "@expo/fingerprint" "0.13.0"
- "@expo/metro-config" "0.20.14"
- "@expo/vector-icons" "^14.0.0"
- babel-preset-expo "~13.2.0"
- expo-asset "~11.1.5"
- expo-constants "~17.1.6"
- expo-file-system "~18.1.10"
- expo-font "~13.3.1"
- expo-keep-awake "~14.1.4"
- expo-modules-autolinking "2.1.11"
- expo-modules-core "2.4.0"
- react-native-edge-to-edge "1.6.0"
+ "@expo/cli" "54.0.23"
+ "@expo/config" "~12.0.13"
+ "@expo/config-plugins" "~54.0.4"
+ "@expo/devtools" "0.1.8"
+ "@expo/fingerprint" "0.15.4"
+ "@expo/metro" "~54.2.0"
+ "@expo/metro-config" "54.0.14"
+ "@expo/vector-icons" "^15.0.3"
+ "@ungap/structured-clone" "^1.3.0"
+ babel-preset-expo "~54.0.10"
+ expo-asset "~12.0.12"
+ expo-constants "~18.0.13"
+ expo-file-system "~19.0.21"
+ expo-font "~14.0.11"
+ expo-keep-awake "~15.0.8"
+ expo-modules-autolinking "3.0.24"
+ expo-modules-core "3.0.29"
+ pretty-format "^29.7.0"
+ react-refresh "^0.14.2"
whatwg-url-without-unicode "8.0.0-3"
exponential-backoff@^3.1.1:
@@ -7794,6 +8529,27 @@ fast-levenshtein@^2.0.6:
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
+fast-uri@^3.0.1:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.0.tgz#66eecff6c764c0df9b762e62ca7edcfb53b4edfa"
+ integrity sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==
+
+fast-xml-builder@^1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz#0c407a1d9d5996336c0cd76f7ff785cac6413017"
+ integrity sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==
+ dependencies:
+ path-expression-matcher "^1.1.3"
+
+fast-xml-parser@^5.3.6:
+ version "5.5.9"
+ resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-5.5.9.tgz#e59637abebec3dbfbb4053b532d787af6ea11527"
+ integrity sha512-jldvxr1MC6rtiZKgrFnDSvT8xuH+eJqxqOBThUVjYrxssYTo1avZLGql5l0a0BAERR01CadYzZ83kVEkbyDg+g==
+ dependencies:
+ fast-xml-builder "^1.1.4"
+ path-expression-matcher "^1.2.0"
+ strnum "^2.2.2"
+
fastq@^1.6.0:
version "1.17.1"
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47"
@@ -7801,6 +8557,11 @@ fastq@^1.6.0:
dependencies:
reusify "^1.0.4"
+fb-dotslash@0.5.8:
+ version "0.5.8"
+ resolved "https://registry.yarnpkg.com/fb-dotslash/-/fb-dotslash-0.5.8.tgz#c5ef3dacd75e1ddb2197c367052464ddde0115f5"
+ integrity sha512-XHYLKk9J4BupDxi9bSEhkfss0m+Vr9ChTrjhf9l2iw3jB5C7BnY4GVPoMcqbrTutsKJso6yj2nAB6BI/F2oZaA==
+
fb-watchman@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.2.tgz#e9524ee6b5c77e9e5001af0f85f3adbb8623255c"
@@ -7838,6 +8599,11 @@ fdir@^6.4.4:
resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.5.tgz#328e280f3a23699362f95f2e82acf978a0c0cb49"
integrity sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw==
+fdir@^6.5.0:
+ version "6.5.0"
+ resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350"
+ integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==
+
fetch-blob@^3.1.2, fetch-blob@^3.1.4:
version "3.2.0"
resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.2.0.tgz#f09b8d4bbd45adc6f0c20b7e787e793e309dcce9"
@@ -8056,7 +8822,7 @@ freeport-async@^2.0.0:
resolved "https://registry.yarnpkg.com/freeport-async/-/freeport-async-2.0.0.tgz#6adf2ec0c629d11abff92836acd04b399135bab4"
integrity sha512-K7od3Uw45AJg00XUmy15+Hae2hOcgKcmN3/EF6Y7i01O0gaqiRx8sUSpsb9+BRNL8RPBrhzPsVfy8q9ADlJuWQ==
-fresh@0.5.2:
+fresh@0.5.2, fresh@~0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==
@@ -8075,6 +8841,15 @@ fs-extra@^11.2.0:
jsonfile "^6.0.1"
universalify "^2.0.0"
+fs-extra@^8.1.0:
+ version "8.1.0"
+ resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0"
+ integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==
+ dependencies:
+ graceful-fs "^4.2.0"
+ jsonfile "^4.0.0"
+ universalify "^0.1.0"
+
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
@@ -8158,7 +8933,7 @@ gensync@^1.0.0-beta.2:
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
-get-caller-file@^2.0.5:
+get-caller-file@^2.0.1, get-caller-file@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
@@ -8190,6 +8965,11 @@ get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.2.7, get-intrinsic@
hasown "^2.0.2"
math-intrinsics "^1.1.0"
+get-nonce@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/get-nonce/-/get-nonce-1.0.1.tgz#fdf3f0278073820d2ce9426c18f07481b1e0cdf3"
+ integrity sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==
+
get-package-type@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a"
@@ -8260,11 +9040,6 @@ get-uri@^6.0.1:
debug "^4.3.4"
fs-extra "^11.2.0"
-getenv@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/getenv/-/getenv-1.0.0.tgz#874f2e7544fbca53c7a4738f37de8605c3fcfc31"
- integrity sha512-7yetJWqbS9sbn0vIfliPsFgoXMKn/YMF+Wuiog97x+urnSRRRZ7xB+uVkwGKzRgq9CDFfMQnE9ruL5DHv9c6Xg==
-
getenv@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/getenv/-/getenv-2.0.0.tgz#b1698c7b0f29588f4577d06c42c73a5b475c69e0"
@@ -8311,17 +9086,14 @@ glob@^10.3.10:
minipass "^7.0.4"
path-scurry "^1.11.0"
-glob@^10.4.2:
- version "10.4.5"
- resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956"
- integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==
+glob@^13.0.0:
+ version "13.0.6"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-13.0.6.tgz#078666566a425147ccacfbd2e332deb66a2be71d"
+ integrity sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==
dependencies:
- foreground-child "^3.1.0"
- jackspeak "^3.1.2"
- minimatch "^9.0.4"
- minipass "^7.1.2"
- package-json-from-dist "^1.0.0"
- path-scurry "^1.11.1"
+ minimatch "^10.2.2"
+ minipass "^7.1.3"
+ path-scurry "^2.0.2"
glob@^7.1.1, glob@^7.1.3, glob@^7.1.4, glob@~7.2.0:
version "7.2.3"
@@ -8420,7 +9192,7 @@ got@^12.6.1:
p-cancelable "^3.0.0"
responselike "^3.0.0"
-graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.4, graceful-fs@^4.2.9:
+graceful-fs@^4.1.2, graceful-fs@^4.1.3, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.4, graceful-fs@^4.2.9:
version "4.2.11"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
@@ -8529,29 +9301,46 @@ hast-util-whitespace@^3.0.0:
dependencies:
"@types/hast" "^3.0.0"
-hermes-estree@0.25.1:
- version "0.25.1"
- resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.25.1.tgz#6aeec17d1983b4eabf69721f3aa3eb705b17f480"
- integrity sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==
+hermes-compiler@0.14.1:
+ version "0.14.1"
+ resolved "https://registry.yarnpkg.com/hermes-compiler/-/hermes-compiler-0.14.1.tgz#5381d2bb88454027d16736b8cb7fddaaf1556538"
+ integrity sha512-+RPPQlayoZ9n6/KXKt5SFILWXCGJ/LV5d24L5smXrvTDrPS4L6dSctPczXauuvzFP3QEJbD1YO7Z3Ra4a+4IhA==
+
+hermes-estree@0.29.1:
+ version "0.29.1"
+ resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.29.1.tgz#043c7db076e0e8ef8c5f6ed23828d1ba463ebcc5"
+ integrity sha512-jl+x31n4/w+wEqm0I2r4CMimukLbLQEYpisys5oCre611CI5fc9TxhqkBBCJ1edDG4Kza0f7CgNz8xVMLZQOmQ==
+
+hermes-estree@0.32.0:
+ version "0.32.0"
+ resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.32.0.tgz#bb7da6613ab8e67e334a1854ea1e209f487d307b"
+ integrity sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ==
+
+hermes-estree@0.33.3:
+ version "0.33.3"
+ resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.33.3.tgz#6d6b593d4b471119772c82bdb0212dfadabb6f17"
+ integrity sha512-6kzYZHCk8Fy1Uc+t3HGYyJn3OL4aeqKLTyina4UFtWl8I0kSL7OmKThaiX+Uh2f8nGw3mo4Ifxg0M5Zk3/Oeqg==
-hermes-estree@0.28.1:
- version "0.28.1"
- resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.28.1.tgz#631e6db146b06e62fc1c630939acf4a3c77d1b24"
- integrity sha512-w3nxl/RGM7LBae0v8LH2o36+8VqwOZGv9rX1wyoWT6YaKZLqpJZ0YQ5P0LVr3tuRpf7vCx0iIG4i/VmBJejxTQ==
+hermes-parser@0.29.1, hermes-parser@^0.29.1:
+ version "0.29.1"
+ resolved "https://registry.yarnpkg.com/hermes-parser/-/hermes-parser-0.29.1.tgz#436b24bcd7bb1e71f92a04c396ccc0716c288d56"
+ integrity sha512-xBHWmUtRC5e/UL0tI7Ivt2riA/YBq9+SiYFU7C1oBa/j2jYGlIF9043oak1F47ihuDIxQ5nbsKueYJDRY02UgA==
+ dependencies:
+ hermes-estree "0.29.1"
-hermes-parser@0.25.1:
- version "0.25.1"
- resolved "https://registry.yarnpkg.com/hermes-parser/-/hermes-parser-0.25.1.tgz#5be0e487b2090886c62bd8a11724cd766d5f54d1"
- integrity sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==
+hermes-parser@0.32.0:
+ version "0.32.0"
+ resolved "https://registry.yarnpkg.com/hermes-parser/-/hermes-parser-0.32.0.tgz#7916984ef6fdce62e7415d354cf35392061cd303"
+ integrity sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw==
dependencies:
- hermes-estree "0.25.1"
+ hermes-estree "0.32.0"
-hermes-parser@0.28.1:
- version "0.28.1"
- resolved "https://registry.yarnpkg.com/hermes-parser/-/hermes-parser-0.28.1.tgz#17b9e6377f334b6870a1f6da2e123fdcd0b605ac"
- integrity sha512-nf8o+hE8g7UJWParnccljHumE9Vlq8F7MqIdeahl+4x0tvCUJYRrT0L7h0MMg/X9YJmkNwsfbaNNrzPtFXOscg==
+hermes-parser@0.33.3:
+ version "0.33.3"
+ resolved "https://registry.yarnpkg.com/hermes-parser/-/hermes-parser-0.33.3.tgz#da50ababb7a5ab636d339e7b2f6e3848e217e09d"
+ integrity sha512-Yg3HgaG4CqgyowtYjX/FsnPAuZdHOqSMtnbpylbptsQ9nwwSKsy6uRWcGO5RK0EqiX12q8HvDWKgeAVajRO5DA==
dependencies:
- hermes-estree "0.28.1"
+ hermes-estree "0.33.3"
hoist-non-react-statics@^3.3.0:
version "3.3.2"
@@ -8605,6 +9394,17 @@ http-errors@2.0.0, http-errors@^2.0.0:
statuses "2.0.1"
toidentifier "1.0.1"
+http-errors@~2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.1.tgz#36d2f65bc909c8790018dd36fb4d93da6caae06b"
+ integrity sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==
+ dependencies:
+ depd "~2.0.0"
+ inherits "~2.0.4"
+ setprototypeof "~1.2.0"
+ statuses "~2.0.2"
+ toidentifier "~1.0.1"
+
http-proxy-agent@^7.0.0, http-proxy-agent@^7.0.2:
version "7.0.2"
resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz#9a8b1f246866c028509486585f62b8f2c18c270e"
@@ -8666,6 +9466,13 @@ iconv-lite@^0.4.24:
dependencies:
safer-buffer ">= 2.1.2 < 3"
+iconv-lite@^0.7.0, iconv-lite@~0.7.0:
+ version "0.7.2"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.7.2.tgz#d0bdeac3f12b4835b7359c2ad89c422a4d1cc72e"
+ integrity sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==
+ dependencies:
+ safer-buffer ">= 2.1.2 < 3.0.0"
+
ieee754@^1.1.13, ieee754@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
@@ -8703,14 +9510,6 @@ immediate@~3.0.5:
resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==
-import-fresh@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546"
- integrity sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==
- dependencies:
- caller-path "^2.0.0"
- resolve-from "^3.0.0"
-
import-fresh@^3.0.0, import-fresh@^3.2.1, import-fresh@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
@@ -8745,7 +9544,7 @@ inflight@^1.0.4:
once "^1.3.0"
wrappy "1"
-inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.0, inherits@~2.0.3:
+inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.0, inherits@~2.0.3, inherits@~2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
@@ -8949,11 +9748,6 @@ is-date-object@^1.1.0:
call-bound "^1.0.2"
has-tostringtag "^1.0.2"
-is-directory@^0.3.1:
- version "0.3.1"
- resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1"
- integrity sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw==
-
is-docker@^2.0.0, is-docker@^2.1.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa"
@@ -8971,6 +9765,11 @@ is-finalizationregistry@^1.1.0:
dependencies:
call-bound "^1.0.3"
+is-fullwidth-code-point@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
+ integrity sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==
+
is-fullwidth-code-point@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
@@ -9191,6 +9990,11 @@ is-what@^4.1.8:
resolved "https://registry.yarnpkg.com/is-what/-/is-what-4.1.16.tgz#1ad860a19da8b4895ad5495da3182ce2acdd7a6f"
integrity sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==
+is-wsl@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d"
+ integrity sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==
+
is-wsl@^2.1.1, is-wsl@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271"
@@ -9706,7 +10510,7 @@ jimp-compact@0.16.1:
resolved "https://registry.yarnpkg.com/jimp-compact/-/jimp-compact-0.16.1.tgz#9582aea06548a2c1e04dd148d7c3ab92075aefa3"
integrity sha512-dZ6Ra7u1G8c4Letq/B5EzAxj4tLFHL+cGtdpR+PVm4yzPDj+lCk+AbivWt1eOM+ikzkowtyV7qSqX6qr3t71Ww==
-joi@^17.13.3:
+joi@^17.13.3, joi@^17.2.1:
version "17.13.3"
resolved "https://registry.yarnpkg.com/joi/-/joi-17.13.3.tgz#0f5cc1169c999b30d344366d384b12d92558bcec"
integrity sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==
@@ -9805,11 +10609,6 @@ json-buffer@3.0.1:
resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13"
integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==
-json-parse-better-errors@^1.0.1:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
- integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==
-
json-parse-even-better-errors@^2.3.0:
version "2.3.1"
resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
@@ -9852,6 +10651,13 @@ json5@^2.2.2, json5@^2.2.3:
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
+jsonfile@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
+ integrity sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==
+ optionalDependencies:
+ graceful-fs "^4.1.6"
+
jsonfile@^6.0.1:
version "6.1.0"
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
@@ -9926,6 +10732,14 @@ lan-network@^0.1.6:
resolved "https://registry.yarnpkg.com/lan-network/-/lan-network-0.1.7.tgz#9fcb9967c6d951f10b2f9a9ffabe4a312d63f69d"
integrity sha512-mnIlAEMu4OyEvUNdzco9xpuB9YVcPkQec+QsgycBCtPZvEqWPCDPfbAE4OJMdBBWpZWtpCn1xw9jJYlwjWI5zQ==
+launch-editor@^2.9.1:
+ version "2.13.2"
+ resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.13.2.tgz#41d51baaf8afb393224b89bd2bcb4e02f2306405"
+ integrity sha512-4VVDnbOpLXy/s8rdRCSXb+zfMeFR0WlJWpET1iA9CQdlZDfwyLjUuGQzXU4VeOoey6AicSAluWan7Etga6Kcmg==
+ dependencies:
+ picocolors "^1.1.1"
+ shell-quote "^1.8.3"
+
lazystream@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.1.tgz#494c831062f1f9408251ec44db1cba29242a2638"
@@ -9961,73 +10775,79 @@ lighthouse-logger@^1.0.0:
debug "^2.6.9"
marky "^1.2.2"
-lightningcss-darwin-arm64@1.27.0:
- version "1.27.0"
- resolved "https://registry.yarnpkg.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.27.0.tgz#565bd610533941cba648a70e105987578d82f996"
- integrity sha512-Gl/lqIXY+d+ySmMbgDf0pgaWSqrWYxVHoc88q+Vhf2YNzZ8DwoRzGt5NZDVqqIW5ScpSnmmjcgXP87Dn2ylSSQ==
-
-lightningcss-darwin-x64@1.27.0:
- version "1.27.0"
- resolved "https://registry.yarnpkg.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.27.0.tgz#c906a267237b1c7fe08bff6c5ac032c099bc9482"
- integrity sha512-0+mZa54IlcNAoQS9E0+niovhyjjQWEMrwW0p2sSdLRhLDc8LMQ/b67z7+B5q4VmjYCMSfnFi3djAAQFIDuj/Tg==
-
-lightningcss-freebsd-x64@1.27.0:
- version "1.27.0"
- resolved "https://registry.yarnpkg.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.27.0.tgz#a7c3c4d6ee18dffeb8fa69f14f8f9267f7dc0c34"
- integrity sha512-n1sEf85fePoU2aDN2PzYjoI8gbBqnmLGEhKq7q0DKLj0UTVmOTwDC7PtLcy/zFxzASTSBlVQYJUhwIStQMIpRA==
-
-lightningcss-linux-arm-gnueabihf@1.27.0:
- version "1.27.0"
- resolved "https://registry.yarnpkg.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.27.0.tgz#c7c16432a571ec877bf734fe500e4a43d48c2814"
- integrity sha512-MUMRmtdRkOkd5z3h986HOuNBD1c2lq2BSQA1Jg88d9I7bmPGx08bwGcnB75dvr17CwxjxD6XPi3Qh8ArmKFqCA==
-
-lightningcss-linux-arm64-gnu@1.27.0:
- version "1.27.0"
- resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.27.0.tgz#cfd9e18df1cd65131da286ddacfa3aee6862a752"
- integrity sha512-cPsxo1QEWq2sfKkSq2Bq5feQDHdUEwgtA9KaB27J5AX22+l4l0ptgjMZZtYtUnteBofjee+0oW1wQ1guv04a7A==
-
-lightningcss-linux-arm64-musl@1.27.0:
- version "1.27.0"
- resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.27.0.tgz#6682ff6b9165acef9a6796bd9127a8e1247bb0ed"
- integrity sha512-rCGBm2ax7kQ9pBSeITfCW9XSVF69VX+fm5DIpvDZQl4NnQoMQyRwhZQm9pd59m8leZ1IesRqWk2v/DntMo26lg==
-
-lightningcss-linux-x64-gnu@1.27.0:
- version "1.27.0"
- resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.27.0.tgz#714221212ad184ddfe974bbb7dbe9300dfde4bc0"
- integrity sha512-Dk/jovSI7qqhJDiUibvaikNKI2x6kWPN79AQiD/E/KeQWMjdGe9kw51RAgoWFDi0coP4jinaH14Nrt/J8z3U4A==
-
-lightningcss-linux-x64-musl@1.27.0:
- version "1.27.0"
- resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.27.0.tgz#247958daf622a030a6dc2285afa16b7184bdf21e"
- integrity sha512-QKjTxXm8A9s6v9Tg3Fk0gscCQA1t/HMoF7Woy1u68wCk5kS4fR+q3vXa1p3++REW784cRAtkYKrPy6JKibrEZA==
-
-lightningcss-win32-arm64-msvc@1.27.0:
- version "1.27.0"
- resolved "https://registry.yarnpkg.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.27.0.tgz#64cfe473c264ef5dc275a4d57a516d77fcac6bc9"
- integrity sha512-/wXegPS1hnhkeG4OXQKEMQeJd48RDC3qdh+OA8pCuOPCyvnm/yEayrJdJVqzBsqpy1aJklRCVxscpFur80o6iQ==
-
-lightningcss-win32-x64-msvc@1.27.0:
- version "1.27.0"
- resolved "https://registry.yarnpkg.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.27.0.tgz#237d0dc87d9cdc9cf82536bcbc07426fa9f3f422"
- integrity sha512-/OJLj94Zm/waZShL8nB5jsNj3CfNATLCTyFxZyouilfTmSoLDX7VlVAmhPHoZWVFp4vdmoiEbPEYC8HID3m6yw==
-
-lightningcss@~1.27.0:
- version "1.27.0"
- resolved "https://registry.yarnpkg.com/lightningcss/-/lightningcss-1.27.0.tgz#d4608e63044343836dd9769f6c8b5d607867649a"
- integrity sha512-8f7aNmS1+etYSLHht0fQApPc2kNO8qGRutifN5rVIc6Xo6ABsEbqOr758UwI7ALVbTt4x1fllKt0PYgzD9S3yQ==
- dependencies:
- detect-libc "^1.0.3"
+lightningcss-android-arm64@1.32.0:
+ version "1.32.0"
+ resolved "https://registry.yarnpkg.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz#f033885116dfefd9c6f54787523e3514b61e1968"
+ integrity sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==
+
+lightningcss-darwin-arm64@1.32.0:
+ version "1.32.0"
+ resolved "https://registry.yarnpkg.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz#50b71871b01c8199584b649e292547faea7af9b5"
+ integrity sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==
+
+lightningcss-darwin-x64@1.32.0:
+ version "1.32.0"
+ resolved "https://registry.yarnpkg.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz#35f3e97332d130b9ca181e11b568ded6aebc6d5e"
+ integrity sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==
+
+lightningcss-freebsd-x64@1.32.0:
+ version "1.32.0"
+ resolved "https://registry.yarnpkg.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz#9777a76472b64ed6ff94342ad64c7bafd794a575"
+ integrity sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==
+
+lightningcss-linux-arm-gnueabihf@1.32.0:
+ version "1.32.0"
+ resolved "https://registry.yarnpkg.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz#13ae652e1ab73b9135d7b7da172f666c410ad53d"
+ integrity sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==
+
+lightningcss-linux-arm64-gnu@1.32.0:
+ version "1.32.0"
+ resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz#417858795a94592f680123a1b1f9da8a0e1ef335"
+ integrity sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==
+
+lightningcss-linux-arm64-musl@1.32.0:
+ version "1.32.0"
+ resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz#6be36692e810b718040802fd809623cffe732133"
+ integrity sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==
+
+lightningcss-linux-x64-gnu@1.32.0:
+ version "1.32.0"
+ resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz#0b7803af4eb21cfd38dd39fe2abbb53c7dd091f6"
+ integrity sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==
+
+lightningcss-linux-x64-musl@1.32.0:
+ version "1.32.0"
+ resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz#88dc8ba865ddddb1ac5ef04b0f161804418c163b"
+ integrity sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==
+
+lightningcss-win32-arm64-msvc@1.32.0:
+ version "1.32.0"
+ resolved "https://registry.yarnpkg.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz#4f30ba3fa5e925f5b79f945e8cc0d176c3b1ab38"
+ integrity sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==
+
+lightningcss-win32-x64-msvc@1.32.0:
+ version "1.32.0"
+ resolved "https://registry.yarnpkg.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz#141aa5605645064928902bb4af045fa7d9f4220a"
+ integrity sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==
+
+lightningcss@^1.30.1:
+ version "1.32.0"
+ resolved "https://registry.yarnpkg.com/lightningcss/-/lightningcss-1.32.0.tgz#b85aae96486dcb1bf49a7c8571221273f4f1e4a9"
+ integrity sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==
+ dependencies:
+ detect-libc "^2.0.3"
optionalDependencies:
- lightningcss-darwin-arm64 "1.27.0"
- lightningcss-darwin-x64 "1.27.0"
- lightningcss-freebsd-x64 "1.27.0"
- lightningcss-linux-arm-gnueabihf "1.27.0"
- lightningcss-linux-arm64-gnu "1.27.0"
- lightningcss-linux-arm64-musl "1.27.0"
- lightningcss-linux-x64-gnu "1.27.0"
- lightningcss-linux-x64-musl "1.27.0"
- lightningcss-win32-arm64-msvc "1.27.0"
- lightningcss-win32-x64-msvc "1.27.0"
+ lightningcss-android-arm64 "1.32.0"
+ lightningcss-darwin-arm64 "1.32.0"
+ lightningcss-darwin-x64 "1.32.0"
+ lightningcss-freebsd-x64 "1.32.0"
+ lightningcss-linux-arm-gnueabihf "1.32.0"
+ lightningcss-linux-arm64-gnu "1.32.0"
+ lightningcss-linux-arm64-musl "1.32.0"
+ lightningcss-linux-x64-gnu "1.32.0"
+ lightningcss-linux-x64-musl "1.32.0"
+ lightningcss-win32-arm64-msvc "1.32.0"
+ lightningcss-win32-x64-msvc "1.32.0"
lines-and-columns@^1.1.6:
version "1.2.4"
@@ -10179,6 +10999,15 @@ log-symbols@^4.1.0:
chalk "^4.1.0"
is-unicode-supported "^0.1.0"
+logkitty@^0.7.1:
+ version "0.7.1"
+ resolved "https://registry.yarnpkg.com/logkitty/-/logkitty-0.7.1.tgz#8e8d62f4085a826e8d38987722570234e33c6aa7"
+ integrity sha512-/3ER20CTTbahrCrpYfPn7Xavv9diBROZpoXGVZDWMw4b/X4uuUwAC0ki85tgsdMRONURyIJbcOvS94QsUBYPbQ==
+ dependencies:
+ ansi-fragments "^0.2.1"
+ dayjs "^1.8.15"
+ yargs "^15.1.0"
+
loglevel-plugin-prefix@^0.8.4:
version "0.8.4"
resolved "https://registry.yarnpkg.com/loglevel-plugin-prefix/-/loglevel-plugin-prefix-0.8.4.tgz#2fe0e05f1a820317d98d8c123e634c1bd84ff644"
@@ -10218,6 +11047,11 @@ lru-cache@^10.4.3:
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119"
integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==
+lru-cache@^11.0.0:
+ version "11.2.7"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.2.7.tgz#9127402617f34cd6767b96daee98c28e74458d35"
+ integrity sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==
+
lru-cache@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
@@ -10367,60 +11201,125 @@ merge2@^1.3.0:
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
-metro-babel-transformer@0.82.4:
- version "0.82.4"
- resolved "https://registry.yarnpkg.com/metro-babel-transformer/-/metro-babel-transformer-0.82.4.tgz#1ac029add8f75d0048c54efc266c0a78791f52b2"
- integrity sha512-4juJahGRb1gmNbQq48lNinB6WFNfb6m0BQqi/RQibEltNiqTCxew/dBspI2EWA4xVCd3mQWGfw0TML4KurQZnQ==
+metro-babel-transformer@0.83.3:
+ version "0.83.3"
+ resolved "https://registry.yarnpkg.com/metro-babel-transformer/-/metro-babel-transformer-0.83.3.tgz#d8c134615530c9ee61364526d44ca4bb0c5343ea"
+ integrity sha512-1vxlvj2yY24ES1O5RsSIvg4a4WeL7PFXgKOHvXTXiW0deLvQr28ExXj6LjwCCDZ4YZLhq6HddLpZnX4dEdSq5g==
+ dependencies:
+ "@babel/core" "^7.25.2"
+ flow-enums-runtime "^0.0.6"
+ hermes-parser "0.32.0"
+ nullthrows "^1.1.1"
+
+metro-babel-transformer@0.83.5:
+ version "0.83.5"
+ resolved "https://registry.yarnpkg.com/metro-babel-transformer/-/metro-babel-transformer-0.83.5.tgz#91f3fa269171ad5189ebba625f1f0aa124ce06ea"
+ integrity sha512-d9FfmgUEVejTiSb7bkQeLRGl6aeno2UpuPm3bo3rCYwxewj03ymvOn8s8vnS4fBqAPQ+cE9iQM40wh7nGXR+eA==
dependencies:
"@babel/core" "^7.25.2"
flow-enums-runtime "^0.0.6"
- hermes-parser "0.28.1"
+ hermes-parser "0.33.3"
nullthrows "^1.1.1"
-metro-cache-key@0.82.4:
- version "0.82.4"
- resolved "https://registry.yarnpkg.com/metro-cache-key/-/metro-cache-key-0.82.4.tgz#21f850008fa2570a51e3958574ea5a791574752e"
- integrity sha512-2JCTqcpF+f2OghOpe/+x+JywfzDkrHdAqinPFWmK2ezNAU/qX0jBFaTETogPibFivxZJil37w9Yp6syX8rFUng==
+metro-cache-key@0.83.3:
+ version "0.83.3"
+ resolved "https://registry.yarnpkg.com/metro-cache-key/-/metro-cache-key-0.83.3.tgz#ae6c5d4eb1ad8d06a92bf7294ca730a8d880b573"
+ integrity sha512-59ZO049jKzSmvBmG/B5bZ6/dztP0ilp0o988nc6dpaDsU05Cl1c/lRf+yx8m9WW/JVgbmfO5MziBU559XjI5Zw==
+ dependencies:
+ flow-enums-runtime "^0.0.6"
+
+metro-cache-key@0.83.5:
+ version "0.83.5"
+ resolved "https://registry.yarnpkg.com/metro-cache-key/-/metro-cache-key-0.83.5.tgz#96896a1768f0494a375e1d5957b7ad487e508a4c"
+ integrity sha512-Ycl8PBajB7bhbAI7Rt0xEyiF8oJ0RWX8EKkolV1KfCUlC++V/GStMSGpPLwnnBZXZWkCC5edBPzv1Hz1Yi0Euw==
+ dependencies:
+ flow-enums-runtime "^0.0.6"
+
+metro-cache@0.83.3:
+ version "0.83.3"
+ resolved "https://registry.yarnpkg.com/metro-cache/-/metro-cache-0.83.3.tgz#f1245cc48570c47d8944495e61d67f0228f10172"
+ integrity sha512-3jo65X515mQJvKqK3vWRblxDEcgY55Sk3w4xa6LlfEXgQ9g1WgMh9m4qVZVwgcHoLy0a2HENTPCCX4Pk6s8c8Q==
dependencies:
+ exponential-backoff "^3.1.1"
flow-enums-runtime "^0.0.6"
+ https-proxy-agent "^7.0.5"
+ metro-core "0.83.3"
-metro-cache@0.82.4:
- version "0.82.4"
- resolved "https://registry.yarnpkg.com/metro-cache/-/metro-cache-0.82.4.tgz#f44557f8b33f5d7b7ba4ccba2880c111e9884329"
- integrity sha512-vX0ylSMGtORKiZ4G8uP6fgfPdDiCWvLZUGZ5zIblSGylOX6JYhvExl0Zg4UA9pix/SSQu5Pnp9vdODMFsNIxhw==
+metro-cache@0.83.5:
+ version "0.83.5"
+ resolved "https://registry.yarnpkg.com/metro-cache/-/metro-cache-0.83.5.tgz#5675f4ad56905aa78fff3dec1b6bf213e0b6c86d"
+ integrity sha512-oH+s4U+IfZyg8J42bne2Skc90rcuESIYf86dYittcdWQtPfcaFXWpByPyTuWk3rR1Zz3Eh5HOrcVImfEhhJLng==
dependencies:
exponential-backoff "^3.1.1"
flow-enums-runtime "^0.0.6"
https-proxy-agent "^7.0.5"
- metro-core "0.82.4"
+ metro-core "0.83.5"
+
+metro-config@0.83.3:
+ version "0.83.3"
+ resolved "https://registry.yarnpkg.com/metro-config/-/metro-config-0.83.3.tgz#a30e7a69b5cf8c4ac4c4b68b1b4c33649ae129a2"
+ integrity sha512-mTel7ipT0yNjKILIan04bkJkuCzUUkm2SeEaTads8VfEecCh+ltXchdq6DovXJqzQAXuR2P9cxZB47Lg4klriA==
+ dependencies:
+ connect "^3.6.5"
+ flow-enums-runtime "^0.0.6"
+ jest-validate "^29.7.0"
+ metro "0.83.3"
+ metro-cache "0.83.3"
+ metro-core "0.83.3"
+ metro-runtime "0.83.3"
+ yaml "^2.6.1"
-metro-config@0.82.4, metro-config@^0.82.0:
- version "0.82.4"
- resolved "https://registry.yarnpkg.com/metro-config/-/metro-config-0.82.4.tgz#d42fa8b6a4d53493c34d217bbc6c13ee4817355c"
- integrity sha512-Ki3Wumr3hKHGDS7RrHsygmmRNc/PCJrvkLn0+BWWxmbOmOcMMJDSmSI+WRlT8jd5VPZFxIi4wg+sAt5yBXAK0g==
+metro-config@0.83.5, metro-config@^0.83.1, metro-config@^0.83.3:
+ version "0.83.5"
+ resolved "https://registry.yarnpkg.com/metro-config/-/metro-config-0.83.5.tgz#a3dd20fc5d5582aa4ad3704678e52abcf4d46b2b"
+ integrity sha512-JQ/PAASXH7yczgV6OCUSRhZYME+NU8NYjI2RcaG5ga4QfQ3T/XdiLzpSb3awWZYlDCcQb36l4Vl7i0Zw7/Tf9w==
dependencies:
connect "^3.6.5"
- cosmiconfig "^5.0.5"
flow-enums-runtime "^0.0.6"
jest-validate "^29.7.0"
- metro "0.82.4"
- metro-cache "0.82.4"
- metro-core "0.82.4"
- metro-runtime "0.82.4"
+ metro "0.83.5"
+ metro-cache "0.83.5"
+ metro-core "0.83.5"
+ metro-runtime "0.83.5"
+ yaml "^2.6.1"
+
+metro-core@0.83.3:
+ version "0.83.3"
+ resolved "https://registry.yarnpkg.com/metro-core/-/metro-core-0.83.3.tgz#007e93f7d1983777da8988dfb103ad897c9835b8"
+ integrity sha512-M+X59lm7oBmJZamc96usuF1kusd5YimqG/q97g4Ac7slnJ3YiGglW5CsOlicTR5EWf8MQFxxjDoB6ytTqRe8Hw==
+ dependencies:
+ flow-enums-runtime "^0.0.6"
+ lodash.throttle "^4.1.1"
+ metro-resolver "0.83.3"
-metro-core@0.82.4, metro-core@^0.82.0:
- version "0.82.4"
- resolved "https://registry.yarnpkg.com/metro-core/-/metro-core-0.82.4.tgz#f7f498929ac066dafe704c3598a990eeb2acdc8c"
- integrity sha512-Xo4ozbxPg2vfgJGCgXZ8sVhC2M0lhTqD+tsKO2q9aelq/dCjnnSb26xZKcQO80CQOQUL7e3QWB7pLFGPjZm31A==
+metro-core@0.83.5, metro-core@^0.83.1, metro-core@^0.83.3:
+ version "0.83.5"
+ resolved "https://registry.yarnpkg.com/metro-core/-/metro-core-0.83.5.tgz#1592033633034feb5d368d22bf18e38052146970"
+ integrity sha512-YcVcLCrf0ed4mdLa82Qob0VxYqfhmlRxUS8+TO4gosZo/gLwSvtdeOjc/Vt0pe/lvMNrBap9LlmvZM8FIsMgJQ==
dependencies:
flow-enums-runtime "^0.0.6"
lodash.throttle "^4.1.1"
- metro-resolver "0.82.4"
+ metro-resolver "0.83.5"
+
+metro-file-map@0.83.3:
+ version "0.83.3"
+ resolved "https://registry.yarnpkg.com/metro-file-map/-/metro-file-map-0.83.3.tgz#3d79fbb1d379ab178dd895ce54cb5ecb183d74a2"
+ integrity sha512-jg5AcyE0Q9Xbbu/4NAwwZkmQn7doJCKGW0SLeSJmzNB9Z24jBe0AL2PHNMy4eu0JiKtNWHz9IiONGZWq7hjVTA==
+ dependencies:
+ debug "^4.4.0"
+ fb-watchman "^2.0.0"
+ flow-enums-runtime "^0.0.6"
+ graceful-fs "^4.2.4"
+ invariant "^2.2.4"
+ jest-worker "^29.7.0"
+ micromatch "^4.0.4"
+ nullthrows "^1.1.1"
+ walker "^1.0.7"
-metro-file-map@0.82.4:
- version "0.82.4"
- resolved "https://registry.yarnpkg.com/metro-file-map/-/metro-file-map-0.82.4.tgz#186d737088433dd290c5974d9d96d7e62d36e4f9"
- integrity sha512-eO7HD1O3aeNsbEe6NBZvx1lLJUrxgyATjnDmb7bm4eyF6yWOQot9XVtxTDLNifECuvsZ4jzRiTInrbmIHkTdGA==
+metro-file-map@0.83.5:
+ version "0.83.5"
+ resolved "https://registry.yarnpkg.com/metro-file-map/-/metro-file-map-0.83.5.tgz#394aa61d54b3822f10e68c18cbd1318f18865d20"
+ integrity sha512-ZEt8s3a1cnYbn40nyCD+CsZdYSlwtFh2kFym4lo+uvfM+UMMH+r/BsrC6rbNClSrt+B7rU9T+Te/sh/NL8ZZKQ==
dependencies:
debug "^4.4.0"
fb-watchman "^2.0.0"
@@ -10432,61 +11331,111 @@ metro-file-map@0.82.4:
nullthrows "^1.1.1"
walker "^1.0.7"
-metro-minify-terser@0.82.4:
- version "0.82.4"
- resolved "https://registry.yarnpkg.com/metro-minify-terser/-/metro-minify-terser-0.82.4.tgz#3750c14e7a25eba591f6c75eed995f6751030618"
- integrity sha512-W79Mi6BUwWVaM8Mc5XepcqkG+TSsCyyo//dmTsgYfJcsmReQorRFodil3bbJInETvjzdnS1mCsUo9pllNjT1Hg==
+metro-minify-terser@0.83.3:
+ version "0.83.3"
+ resolved "https://registry.yarnpkg.com/metro-minify-terser/-/metro-minify-terser-0.83.3.tgz#c1c70929c86b14c8bf03e6321b4f9310bc8dbe87"
+ integrity sha512-O2BmfWj6FSfzBLrNCXt/rr2VYZdX5i6444QJU0fFoc7Ljg+Q+iqebwE3K0eTvkI6TRjELsXk1cjU+fXwAR4OjQ==
+ dependencies:
+ flow-enums-runtime "^0.0.6"
+ terser "^5.15.0"
+
+metro-minify-terser@0.83.5:
+ version "0.83.5"
+ resolved "https://registry.yarnpkg.com/metro-minify-terser/-/metro-minify-terser-0.83.5.tgz#ee43a11a9d3442760781434c599d45eb1274e6fd"
+ integrity sha512-Toe4Md1wS1PBqbvB0cFxBzKEVyyuYTUb0sgifAZh/mSvLH84qA1NAWik9sISWatzvfWf3rOGoUoO5E3f193a3Q==
+ dependencies:
+ flow-enums-runtime "^0.0.6"
+ terser "^5.15.0"
+
+metro-resolver@0.83.3:
+ version "0.83.3"
+ resolved "https://registry.yarnpkg.com/metro-resolver/-/metro-resolver-0.83.3.tgz#06207bdddc280b9335722a8c992aeec017413942"
+ integrity sha512-0js+zwI5flFxb1ktmR///bxHYg7OLpRpWZlBBruYG8OKYxeMP7SV0xQ/o/hUelrEMdK4LJzqVtHAhBm25LVfAQ==
+ dependencies:
+ flow-enums-runtime "^0.0.6"
+
+metro-resolver@0.83.5:
+ version "0.83.5"
+ resolved "https://registry.yarnpkg.com/metro-resolver/-/metro-resolver-0.83.5.tgz#72340ca8071941eafe92ff2dcb8e33c581870ef7"
+ integrity sha512-7p3GtzVUpbAweJeCcUJihJeOQl1bDuimO5ueo1K0BUpUtR41q5EilbQ3klt16UTPPMpA+tISWBtsrqU556mY1A==
dependencies:
flow-enums-runtime "^0.0.6"
- terser "^5.15.0"
-metro-resolver@0.82.4:
- version "0.82.4"
- resolved "https://registry.yarnpkg.com/metro-resolver/-/metro-resolver-0.82.4.tgz#936a2300fa577183a1e0e6f7cc405e52f9276f0e"
- integrity sha512-uWoHzOBGQTPT5PjippB8rRT3iI9CTgFA9tRiLMzrseA5o7YAlgvfTdY9vFk2qyk3lW3aQfFKWkmqENryPRpu+Q==
+metro-runtime@0.83.3:
+ version "0.83.3"
+ resolved "https://registry.yarnpkg.com/metro-runtime/-/metro-runtime-0.83.3.tgz#ff504df5d93f38b1af396715b327e589ba8d184d"
+ integrity sha512-JHCJb9ebr9rfJ+LcssFYA2x1qPYuSD/bbePupIGhpMrsla7RCwC/VL3yJ9cSU+nUhU4c9Ixxy8tBta+JbDeZWw==
dependencies:
+ "@babel/runtime" "^7.25.0"
flow-enums-runtime "^0.0.6"
-metro-runtime@0.82.4, metro-runtime@^0.82.0:
- version "0.82.4"
- resolved "https://registry.yarnpkg.com/metro-runtime/-/metro-runtime-0.82.4.tgz#70e0b871683fdb594f2a7c03403b6350deca086b"
- integrity sha512-vVyFO7H+eLXRV2E7YAUYA7aMGBECGagqxmFvC2hmErS7oq90BbPVENfAHbUWq1vWH+MRiivoRxdxlN8gBoF/dw==
+metro-runtime@0.83.5, metro-runtime@^0.83.1, metro-runtime@^0.83.3:
+ version "0.83.5"
+ resolved "https://registry.yarnpkg.com/metro-runtime/-/metro-runtime-0.83.5.tgz#52c1edafc6cc82e57729cc9c21700ab1e53a1777"
+ integrity sha512-f+b3ue9AWTVlZe2Xrki6TAoFtKIqw30jwfk7GQ1rDUBQaE0ZQ+NkiMEtb9uwH7uAjJ87U7Tdx1Jg1OJqUfEVlA==
dependencies:
"@babel/runtime" "^7.25.0"
flow-enums-runtime "^0.0.6"
-metro-source-map@0.82.4, metro-source-map@^0.82.0:
- version "0.82.4"
- resolved "https://registry.yarnpkg.com/metro-source-map/-/metro-source-map-0.82.4.tgz#ddd9bdf3a5864ee8f0b296ed11e46bdc2de321ac"
- integrity sha512-9jzDQJ0FPas1FuQFtwmBHsez2BfhFNufMowbOMeG3ZaFvzeziE8A0aJwILDS3U+V5039ssCQFiQeqDgENWvquA==
+metro-source-map@0.83.3:
+ version "0.83.3"
+ resolved "https://registry.yarnpkg.com/metro-source-map/-/metro-source-map-0.83.3.tgz#04bb464f7928ea48bcdfd18912c8607cf317c898"
+ integrity sha512-xkC3qwUBh2psVZgVavo8+r2C9Igkk3DibiOXSAht1aYRRcztEZNFtAMtfSB7sdO2iFMx2Mlyu++cBxz/fhdzQg==
dependencies:
"@babel/traverse" "^7.25.3"
"@babel/traverse--for-generate-function-map" "npm:@babel/traverse@^7.25.3"
"@babel/types" "^7.25.2"
flow-enums-runtime "^0.0.6"
invariant "^2.2.4"
- metro-symbolicate "0.82.4"
+ metro-symbolicate "0.83.3"
+ nullthrows "^1.1.1"
+ ob1 "0.83.3"
+ source-map "^0.5.6"
+ vlq "^1.0.0"
+
+metro-source-map@0.83.5, metro-source-map@^0.83.1, metro-source-map@^0.83.3:
+ version "0.83.5"
+ resolved "https://registry.yarnpkg.com/metro-source-map/-/metro-source-map-0.83.5.tgz#384f311f83fa2bf51cbec08d77210aa951bf9ee3"
+ integrity sha512-VT9bb2KO2/4tWY9Z2yeZqTUao7CicKAOps9LUg2aQzsz+04QyuXL3qgf1cLUVRjA/D6G5u1RJAlN1w9VNHtODQ==
+ dependencies:
+ "@babel/traverse" "^7.29.0"
+ "@babel/types" "^7.29.0"
+ flow-enums-runtime "^0.0.6"
+ invariant "^2.2.4"
+ metro-symbolicate "0.83.5"
+ nullthrows "^1.1.1"
+ ob1 "0.83.5"
+ source-map "^0.5.6"
+ vlq "^1.0.0"
+
+metro-symbolicate@0.83.3:
+ version "0.83.3"
+ resolved "https://registry.yarnpkg.com/metro-symbolicate/-/metro-symbolicate-0.83.3.tgz#67af03950f0dfe19a7c059e3983e39a31e95d03a"
+ integrity sha512-F/YChgKd6KbFK3eUR5HdUsfBqVsanf5lNTwFd4Ca7uuxnHgBC3kR/Hba/RGkenR3pZaGNp5Bu9ZqqP52Wyhomw==
+ dependencies:
+ flow-enums-runtime "^0.0.6"
+ invariant "^2.2.4"
+ metro-source-map "0.83.3"
nullthrows "^1.1.1"
- ob1 "0.82.4"
source-map "^0.5.6"
vlq "^1.0.0"
-metro-symbolicate@0.82.4:
- version "0.82.4"
- resolved "https://registry.yarnpkg.com/metro-symbolicate/-/metro-symbolicate-0.82.4.tgz#76603b0ec2ad8fe5f174aa76485cc46228547a91"
- integrity sha512-LwEwAtdsx7z8rYjxjpLWxuFa2U0J6TS6ljlQM4WAATKa4uzV8unmnRuN2iNBWTmRqgNR77mzmI2vhwD4QSCo+w==
+metro-symbolicate@0.83.5:
+ version "0.83.5"
+ resolved "https://registry.yarnpkg.com/metro-symbolicate/-/metro-symbolicate-0.83.5.tgz#62167db423be6c68b4b9f39935c9cb7330cc9526"
+ integrity sha512-EMIkrjNRz/hF+p0RDdxoE60+dkaTLPN3vaaGkFmX5lvFdO6HPfHA/Ywznzkev+za0VhPQ5KSdz49/MALBRteHA==
dependencies:
flow-enums-runtime "^0.0.6"
invariant "^2.2.4"
- metro-source-map "0.82.4"
+ metro-source-map "0.83.5"
nullthrows "^1.1.1"
source-map "^0.5.6"
vlq "^1.0.0"
-metro-transform-plugins@0.82.4:
- version "0.82.4"
- resolved "https://registry.yarnpkg.com/metro-transform-plugins/-/metro-transform-plugins-0.82.4.tgz#9d3bb855126e0a63ac2ccf012ed2bc7829209e8c"
- integrity sha512-NoWQRPHupVpnDgYguiEcm7YwDhnqW02iWWQjO2O8NsNP09rEMSq99nPjARWfukN7+KDh6YjLvTIN20mj3dk9kw==
+metro-transform-plugins@0.83.3:
+ version "0.83.3"
+ resolved "https://registry.yarnpkg.com/metro-transform-plugins/-/metro-transform-plugins-0.83.3.tgz#2c59ba841e269363cf3acb13138cb992f0c75013"
+ integrity sha512-eRGoKJU6jmqOakBMH5kUB7VitEWiNrDzBHpYbkBXW7C5fUGeOd2CyqrosEzbMK5VMiZYyOcNFEphvxk3OXey2A==
dependencies:
"@babel/core" "^7.25.2"
"@babel/generator" "^7.25.0"
@@ -10495,29 +11444,60 @@ metro-transform-plugins@0.82.4:
flow-enums-runtime "^0.0.6"
nullthrows "^1.1.1"
-metro-transform-worker@0.82.4:
- version "0.82.4"
- resolved "https://registry.yarnpkg.com/metro-transform-worker/-/metro-transform-worker-0.82.4.tgz#cf7db4ef89301ab6658f7a0988fd030e28f86b28"
- integrity sha512-kPI7Ad/tdAnI9PY4T+2H0cdgGeSWWdiPRKuytI806UcN4VhFL6OmYa19/4abYVYF+Cd2jo57CDuwbaxRfmXDhw==
+metro-transform-plugins@0.83.5:
+ version "0.83.5"
+ resolved "https://registry.yarnpkg.com/metro-transform-plugins/-/metro-transform-plugins-0.83.5.tgz#ba21c6a5fa9bf6c5c2c222e2c8e7a668ffb3d341"
+ integrity sha512-KxYKzZL+lt3Os5H2nx7YkbkWVduLZL5kPrE/Yq+Prm/DE1VLhpfnO6HtPs8vimYFKOa58ncl60GpoX0h7Wm0Vw==
+ dependencies:
+ "@babel/core" "^7.25.2"
+ "@babel/generator" "^7.29.1"
+ "@babel/template" "^7.28.6"
+ "@babel/traverse" "^7.29.0"
+ flow-enums-runtime "^0.0.6"
+ nullthrows "^1.1.1"
+
+metro-transform-worker@0.83.3:
+ version "0.83.3"
+ resolved "https://registry.yarnpkg.com/metro-transform-worker/-/metro-transform-worker-0.83.3.tgz#ca6ae4a02b0f61b33299e6e56bacaba32dcd607f"
+ integrity sha512-Ztekew9t/gOIMZX1tvJOgX7KlSLL5kWykl0Iwu2cL2vKMKVALRl1hysyhUw0vjpAvLFx+Kfq9VLjnHIkW32fPA==
dependencies:
"@babel/core" "^7.25.2"
"@babel/generator" "^7.25.0"
"@babel/parser" "^7.25.3"
"@babel/types" "^7.25.2"
flow-enums-runtime "^0.0.6"
- metro "0.82.4"
- metro-babel-transformer "0.82.4"
- metro-cache "0.82.4"
- metro-cache-key "0.82.4"
- metro-minify-terser "0.82.4"
- metro-source-map "0.82.4"
- metro-transform-plugins "0.82.4"
+ metro "0.83.3"
+ metro-babel-transformer "0.83.3"
+ metro-cache "0.83.3"
+ metro-cache-key "0.83.3"
+ metro-minify-terser "0.83.3"
+ metro-source-map "0.83.3"
+ metro-transform-plugins "0.83.3"
+ nullthrows "^1.1.1"
+
+metro-transform-worker@0.83.5:
+ version "0.83.5"
+ resolved "https://registry.yarnpkg.com/metro-transform-worker/-/metro-transform-worker-0.83.5.tgz#8616b54282e727027fdb5c475aade719394a8e8a"
+ integrity sha512-8N4pjkNXc6ytlP9oAM6MwqkvUepNSW39LKYl9NjUMpRDazBQ7oBpQDc8Sz4aI8jnH6AGhF7s1m/ayxkN1t04yA==
+ dependencies:
+ "@babel/core" "^7.25.2"
+ "@babel/generator" "^7.29.1"
+ "@babel/parser" "^7.29.0"
+ "@babel/types" "^7.29.0"
+ flow-enums-runtime "^0.0.6"
+ metro "0.83.5"
+ metro-babel-transformer "0.83.5"
+ metro-cache "0.83.5"
+ metro-cache-key "0.83.5"
+ metro-minify-terser "0.83.5"
+ metro-source-map "0.83.5"
+ metro-transform-plugins "0.83.5"
nullthrows "^1.1.1"
-metro@0.82.4, metro@^0.82.0:
- version "0.82.4"
- resolved "https://registry.yarnpkg.com/metro/-/metro-0.82.4.tgz#e4fb4552f1387610b0b2873ed07d4cd59754d636"
- integrity sha512-/gFmw3ux9CPG5WUmygY35hpyno28zi/7OUn6+OFfbweA8l0B+PPqXXLr0/T6cf5nclCcH0d22o+02fICaShVxw==
+metro@0.83.3:
+ version "0.83.3"
+ resolved "https://registry.yarnpkg.com/metro/-/metro-0.83.3.tgz#1e7e04c15519af746f8932c7f9c553d92c39e922"
+ integrity sha512-+rP+/GieOzkt97hSJ0MrPOuAH/jpaS21ZDvL9DJ35QYRDlQcwzcvUlGUf79AnQxq/2NPiS/AULhhM4TKutIt8Q==
dependencies:
"@babel/code-frame" "^7.24.7"
"@babel/core" "^7.25.2"
@@ -10534,24 +11514,24 @@ metro@0.82.4, metro@^0.82.0:
error-stack-parser "^2.0.6"
flow-enums-runtime "^0.0.6"
graceful-fs "^4.2.4"
- hermes-parser "0.28.1"
+ hermes-parser "0.32.0"
image-size "^1.0.2"
invariant "^2.2.4"
jest-worker "^29.7.0"
jsc-safe-url "^0.2.2"
lodash.throttle "^4.1.1"
- metro-babel-transformer "0.82.4"
- metro-cache "0.82.4"
- metro-cache-key "0.82.4"
- metro-config "0.82.4"
- metro-core "0.82.4"
- metro-file-map "0.82.4"
- metro-resolver "0.82.4"
- metro-runtime "0.82.4"
- metro-source-map "0.82.4"
- metro-symbolicate "0.82.4"
- metro-transform-plugins "0.82.4"
- metro-transform-worker "0.82.4"
+ metro-babel-transformer "0.83.3"
+ metro-cache "0.83.3"
+ metro-cache-key "0.83.3"
+ metro-config "0.83.3"
+ metro-core "0.83.3"
+ metro-file-map "0.83.3"
+ metro-resolver "0.83.3"
+ metro-runtime "0.83.3"
+ metro-source-map "0.83.3"
+ metro-symbolicate "0.83.3"
+ metro-transform-plugins "0.83.3"
+ metro-transform-worker "0.83.3"
mime-types "^2.1.27"
nullthrows "^1.1.1"
serialize-error "^2.1.0"
@@ -10560,6 +11540,52 @@ metro@0.82.4, metro@^0.82.0:
ws "^7.5.10"
yargs "^17.6.2"
+metro@0.83.5, metro@^0.83.1, metro@^0.83.3:
+ version "0.83.5"
+ resolved "https://registry.yarnpkg.com/metro/-/metro-0.83.5.tgz#f5441075d5211c980ac8c79109e9e6fa2df68924"
+ integrity sha512-BgsXevY1MBac/3ZYv/RfNFf/4iuW9X7f4H8ZNkiH+r667HD9sVujxcmu4jvEzGCAm4/WyKdZCuyhAcyhTHOucQ==
+ dependencies:
+ "@babel/code-frame" "^7.29.0"
+ "@babel/core" "^7.25.2"
+ "@babel/generator" "^7.29.1"
+ "@babel/parser" "^7.29.0"
+ "@babel/template" "^7.28.6"
+ "@babel/traverse" "^7.29.0"
+ "@babel/types" "^7.29.0"
+ accepts "^2.0.0"
+ chalk "^4.0.0"
+ ci-info "^2.0.0"
+ connect "^3.6.5"
+ debug "^4.4.0"
+ error-stack-parser "^2.0.6"
+ flow-enums-runtime "^0.0.6"
+ graceful-fs "^4.2.4"
+ hermes-parser "0.33.3"
+ image-size "^1.0.2"
+ invariant "^2.2.4"
+ jest-worker "^29.7.0"
+ jsc-safe-url "^0.2.2"
+ lodash.throttle "^4.1.1"
+ metro-babel-transformer "0.83.5"
+ metro-cache "0.83.5"
+ metro-cache-key "0.83.5"
+ metro-config "0.83.5"
+ metro-core "0.83.5"
+ metro-file-map "0.83.5"
+ metro-resolver "0.83.5"
+ metro-runtime "0.83.5"
+ metro-source-map "0.83.5"
+ metro-symbolicate "0.83.5"
+ metro-transform-plugins "0.83.5"
+ metro-transform-worker "0.83.5"
+ mime-types "^3.0.1"
+ nullthrows "^1.1.1"
+ serialize-error "^2.1.0"
+ source-map "^0.5.6"
+ throat "^5.0.0"
+ ws "^7.5.10"
+ yargs "^17.6.2"
+
micromark-util-character@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-2.1.0.tgz#31320ace16b4644316f6bf057531689c71e2aee1"
@@ -10637,6 +11663,11 @@ mime@1.6.0:
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
+mime@^2.4.1:
+ version "2.6.0"
+ resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367"
+ integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==
+
mimic-fn@^1.0.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022"
@@ -10662,6 +11693,13 @@ mimic-response@^4.0.0:
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-4.0.0.tgz#35468b19e7c75d10f5165ea25e75a5ceea7cf70f"
integrity sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==
+minimatch@^10.2.2:
+ version "10.2.4"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.4.tgz#465b3accbd0218b8281f5301e27cedc697f96fde"
+ integrity sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==
+ dependencies:
+ brace-expansion "^5.0.2"
+
minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
@@ -10712,15 +11750,20 @@ minipass@^7.1.2:
resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707"
integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==
+minipass@^7.1.3:
+ version "7.1.3"
+ resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.3.tgz#79389b4eb1bb2d003a9bba87d492f2bd37bdc65b"
+ integrity sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==
+
minisearch@^7.1.1:
version "7.1.2"
resolved "https://registry.yarnpkg.com/minisearch/-/minisearch-7.1.2.tgz#296ee8d1906cc378f7e57a3a71f07e5205a75df5"
integrity sha512-R1Pd9eF+MD5JYDDSPAp/q1ougKglm14uEkPMvQ/05RGmx6G9wvmLTrTI/Q5iPNJLYqNdsDQ7qTGIcNWR+FrHmA==
-minizlib@^3.0.1:
- version "3.0.2"
- resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-3.0.2.tgz#f33d638eb279f664439aa38dc5f91607468cb574"
- integrity sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==
+minizlib@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-3.1.0.tgz#6ad76c3a8f10227c9b51d1c9ac8e30b27f5a251c"
+ integrity sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==
dependencies:
minipass "^7.1.2"
@@ -10751,7 +11794,7 @@ mkdirp@^1.0.4:
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
-mkdirp@^3.0.1, mkdirp@~3.0.0:
+mkdirp@~3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-3.0.1.tgz#e44e4c5607fb279c168241713cc6e0fea9adcb50"
integrity sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==
@@ -10790,7 +11833,7 @@ mz@^2.7.0:
object-assign "^4.0.1"
thenify-all "^1.0.0"
-nanoid@^3.3.11:
+nanoid@^3.3.11, nanoid@^3.3.8:
version "3.3.11"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b"
integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==
@@ -10820,6 +11863,11 @@ negotiator@^1.0.0:
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-1.0.0.tgz#b6c91bb47172d69f93cfd7c357bbb529019b5f6a"
integrity sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==
+negotiator@~0.6.4:
+ version "0.6.4"
+ resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.4.tgz#777948e2452651c570b712dd01c23e262713fff7"
+ integrity sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==
+
neo-async@^2.6.2:
version "2.6.2"
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
@@ -10843,6 +11891,11 @@ no-case@^3.0.4:
lower-case "^2.0.2"
tslib "^2.0.3"
+nocache@^3.0.1:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/nocache/-/nocache-3.0.4.tgz#5b37a56ec6e09fc7d401dceaed2eab40c8bfdf79"
+ integrity sha512-WDD0bdg9mbq6F4mRxEYcPWwfA1vxd0mrvKOyxI7Xj/atfRHVeutzuWByG//jfm4uPzp0y4Kj051EORCBSQMycw==
+
node-domexception@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5"
@@ -10864,10 +11917,10 @@ node-fetch@^3.3.2:
fetch-blob "^3.1.4"
formdata-polyfill "^4.0.10"
-node-forge@^1.2.1, node-forge@^1.3.1:
- version "1.3.1"
- resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3"
- integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==
+node-forge@^1.3.3:
+ version "1.3.3"
+ resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.3.tgz#0ad80f6333b3a0045e827ac20b7f735f93716751"
+ integrity sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==
node-int64@^0.4.0:
version "0.4.0"
@@ -10889,6 +11942,16 @@ node-releases@^2.0.19:
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314"
integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==
+node-releases@^2.0.27:
+ version "2.0.36"
+ resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.36.tgz#99fd6552aaeda9e17c4713b57a63964a2e325e9d"
+ integrity sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==
+
+node-stream-zip@^1.9.1:
+ version "1.15.0"
+ resolved "https://registry.yarnpkg.com/node-stream-zip/-/node-stream-zip-1.15.0.tgz#158adb88ed8004c6c49a396b50a6a5de3bca33ea"
+ integrity sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==
+
normalize-package-data@^6.0.0:
version "6.0.1"
resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-6.0.1.tgz#fa69e9452210f0fabf4d79ee08d0c2870c51ed88"
@@ -10939,14 +12002,21 @@ nullthrows@^1.1.1:
integrity sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==
nwsapi@^2.2.16:
- version "2.2.22"
- resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.22.tgz#109f9530cda6c156d6a713cdf5939e9f0de98b9d"
- integrity sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==
+ version "2.2.23"
+ resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.23.tgz#59712c3a88e6de2bb0b6ccc1070397267019cf6c"
+ integrity sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==
+
+ob1@0.83.3:
+ version "0.83.3"
+ resolved "https://registry.yarnpkg.com/ob1/-/ob1-0.83.3.tgz#2208e20c9070e9beff3ad067f2db458fa6b07014"
+ integrity sha512-egUxXCDwoWG06NGCS5s5AdcpnumHKJlfd3HH06P3m9TEMwwScfcY35wpQxbm9oHof+dM/lVH9Rfyu1elTVelSA==
+ dependencies:
+ flow-enums-runtime "^0.0.6"
-ob1@0.82.4:
- version "0.82.4"
- resolved "https://registry.yarnpkg.com/ob1/-/ob1-0.82.4.tgz#60f04f61b8ab96e1f818eb856b35c97bbef44726"
- integrity sha512-n9S8e4l5TvkrequEAMDidl4yXesruWTNTzVkeaHSGywoTOIwTzZzKw7Z670H3eaXDZui5MJXjWGNzYowVZIxCA==
+ob1@0.83.5:
+ version "0.83.5"
+ resolved "https://registry.yarnpkg.com/ob1/-/ob1-0.83.5.tgz#f9c289d759142b76577948eea7fd1f07d36f825f"
+ integrity sha512-vNKPYC8L5ycVANANpF/S+WZHpfnRWKx/F3AYP4QMn6ZJTh+l2HOrId0clNkEmua58NB9vmI9Qh7YOoV/4folYg==
dependencies:
flow-enums-runtime "^0.0.6"
@@ -11045,7 +12115,7 @@ object.values@^1.2.0, object.values@^1.2.1:
define-properties "^1.2.1"
es-object-atoms "^1.0.0"
-on-finished@2.4.1, on-finished@^2.4.1:
+on-finished@2.4.1, on-finished@^2.4.1, on-finished@~2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f"
integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==
@@ -11064,6 +12134,11 @@ on-headers@~1.0.2:
resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f"
integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==
+on-headers@~1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.1.0.tgz#59da4f91c45f5f989c6e4bcedc5a3b0aed70ff65"
+ integrity sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==
+
once@^1.3.0, once@^1.3.1, once@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
@@ -11101,6 +12176,13 @@ oniguruma-to-es@^3.1.0:
regex "^6.0.1"
regex-recursion "^6.0.2"
+open@^6.2.0:
+ version "6.4.0"
+ resolved "https://registry.yarnpkg.com/open/-/open-6.4.0.tgz#5c13e96d0dc894686164f18965ecfe889ecfc8a9"
+ integrity sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg==
+ dependencies:
+ is-wsl "^1.1.0"
+
open@^7.0.3:
version "7.4.2"
resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321"
@@ -11252,11 +12334,6 @@ pac-resolver@^7.0.0:
degenerator "^5.0.0"
netmask "^2.0.2"
-package-json-from-dist@^1.0.0:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505"
- integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==
-
package-name-regex@~2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/package-name-regex/-/package-name-regex-2.0.6.tgz#b54bcb04d950e38082b7bb38fa558e01c1679334"
@@ -11274,14 +12351,6 @@ parent-module@^1.0.0:
dependencies:
callsites "^3.0.0"
-parse-json@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0"
- integrity sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==
- dependencies:
- error-ex "^1.3.1"
- json-parse-better-errors "^1.0.1"
-
parse-json@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd"
@@ -11363,6 +12432,11 @@ path-exists@^5.0.0:
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-5.0.0.tgz#a6aad9489200b21fab31e49cf09277e5116fb9e7"
integrity sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==
+path-expression-matcher@^1.1.3, path-expression-matcher@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/path-expression-matcher/-/path-expression-matcher-1.2.0.tgz#9bdae3787f43b0857b0269e9caaa586c12c8abee"
+ integrity sha512-DwmPWeFn+tq7TiyJ2CxezCAirXjFxvaiD03npak3cRjlP9+OjTmSy1EpIrEbh+l6JgUundniloMLDQ/6VTdhLQ==
+
path-is-absolute@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
@@ -11391,6 +12465,14 @@ path-scurry@^1.11.0, path-scurry@^1.11.1:
lru-cache "^10.2.0"
minipass "^5.0.0 || ^6.0.2 || ^7.0.0"
+path-scurry@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-2.0.2.tgz#6be0d0ee02a10d9e0de7a98bae65e182c9061f85"
+ integrity sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==
+ dependencies:
+ lru-cache "^11.0.0"
+ minipass "^7.1.2"
+
path-to-regexp@^8.0.0:
version "8.2.0"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-8.2.0.tgz#73990cc29e57a3ff2a0d914095156df5db79e8b4"
@@ -11456,6 +12538,11 @@ picomatch@^4.0.2:
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.2.tgz#77c742931e8f3b8820946c76cd0c1f13730d1dab"
integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==
+picomatch@^4.0.3:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042"
+ integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==
+
pify@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231"
@@ -11601,7 +12688,7 @@ promise@^8.3.0:
dependencies:
asap "~2.0.6"
-prompts@^2.0.1, prompts@^2.3.2:
+prompts@^2.0.1, prompts@^2.3.2, prompts@^2.4.2:
version "2.4.2"
resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069"
integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==
@@ -11711,6 +12798,13 @@ qs@^6.14.0:
dependencies:
side-channel "^1.1.0"
+qs@^6.14.1:
+ version "6.15.0"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.15.0.tgz#db8fd5d1b1d2d6b5b33adaf87429805f1909e7b3"
+ integrity sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==
+ dependencies:
+ side-channel "^1.1.0"
+
query-selector-shadow-dom@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.1.tgz#1c7b0058eff4881ac44f45d8f84ede32e9a2f349"
@@ -11768,6 +12862,16 @@ raw-body@^3.0.0:
iconv-lite "0.6.3"
unpipe "1.0.0"
+raw-body@^3.0.1:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-3.0.2.tgz#3e3ada5ae5568f9095d84376fd3a49b8fb000a51"
+ integrity sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==
+ dependencies:
+ bytes "~3.1.2"
+ http-errors "~2.0.1"
+ iconv-lite "~0.7.0"
+ unpipe "~1.0.0"
+
rc@~1.2.7:
version "1.2.8"
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
@@ -11778,10 +12882,10 @@ rc@~1.2.7:
minimist "^1.2.0"
strip-json-comments "~2.0.1"
-react-devtools-core@^6.1.1:
- version "6.1.2"
- resolved "https://registry.yarnpkg.com/react-devtools-core/-/react-devtools-core-6.1.2.tgz#bf4030c4012be8a9201dc1f8a36238c9a5078c98"
- integrity sha512-ldFwzufLletzCikNJVYaxlxMLu7swJ3T2VrGfzXlMsVhZhPDKXA38DEROidaYZVgMAmQnIjymrmqto5pyfrwPA==
+react-devtools-core@^6.1.5:
+ version "6.1.5"
+ resolved "https://registry.yarnpkg.com/react-devtools-core/-/react-devtools-core-6.1.5.tgz#c5eca79209dab853a03b2158c034c5166975feee"
+ integrity sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==
dependencies:
shell-quote "^1.6.1"
ws "^7"
@@ -11794,12 +12898,12 @@ react-dom@18.2.0:
loose-envify "^1.1.0"
scheduler "^0.23.0"
-react-dom@19.0.1:
- version "19.0.1"
- resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.0.1.tgz#b856cbfe38e002b485803d5a0692ee600832edbd"
- integrity sha512-3TJg51HSbJiLVYCS6vWwWsyqoS36aGEOCmtLLHxROlSZZ5Bk10xpxHFbrCu4DdqgR85DDc9Vucxqhai3g2xjtA==
+react-dom@19.1.0:
+ version "19.1.0"
+ resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.1.0.tgz#133558deca37fa1d682708df8904b25186793623"
+ integrity sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==
dependencies:
- scheduler "^0.25.0"
+ scheduler "^0.26.0"
react-fast-compare@^3.2.2:
version "3.2.2"
@@ -11826,60 +12930,51 @@ react-is@^19.1.0:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-19.1.0.tgz#805bce321546b7e14c084989c77022351bbdd11b"
integrity sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==
-react-native-edge-to-edge@1.6.0:
- version "1.6.0"
- resolved "https://registry.yarnpkg.com/react-native-edge-to-edge/-/react-native-edge-to-edge-1.6.0.tgz#2ba63b941704a7f713e298185c26cde4d9e4b973"
- integrity sha512-2WCNdE3Qd6Fwg9+4BpbATUxCLcouF6YRY7K+J36KJ4l3y+tWN6XCqAC4DuoGblAAbb2sLkhEDp4FOlbOIot2Og==
-
-react-native-gesture-handler@~2.24.0:
- version "2.24.0"
- resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-2.24.0.tgz#b6e1f13ec9bf8dfa5f4911854b6e0d73d882a81a"
- integrity sha512-ZdWyOd1C8axKJHIfYxjJKCcxjWEpUtUWgTOVY2wynbiveSQDm8X/PDyAKXSer/GOtIpjudUbACOndZXCN3vHsw==
+react-native-gesture-handler@~2.28.0:
+ version "2.28.0"
+ resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-2.28.0.tgz#07fb4f5eae72f810aac3019b060d26c1835bfd0c"
+ integrity sha512-0msfJ1vRxXKVgTgvL+1ZOoYw3/0z1R+Ked0+udoJhyplC2jbVKIJ8Z1bzWdpQRCV3QcQ87Op0zJVE5DhKK2A0A==
dependencies:
"@egjs/hammerjs" "^2.0.17"
hoist-non-react-statics "^3.3.0"
invariant "^2.2.4"
-react-native-is-edge-to-edge@1.1.7, react-native-is-edge-to-edge@^1.1.6:
+react-native-is-edge-to-edge@^1.1.6:
version "1.1.7"
resolved "https://registry.yarnpkg.com/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.1.7.tgz#28947688f9fafd584e73a4f935ea9603bd9b1939"
integrity sha512-EH6i7E8epJGIcu7KpfXYXiV2JFIYITtq+rVS8uEb+92naMRBdxhTuS8Wn2Q7j9sqyO0B+Xbaaf9VdipIAmGW4w==
-react-native-reanimated@~3.17.4:
- version "3.17.5"
- resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-3.17.5.tgz#09ebe3c9e3379c5c0c588b7ab30c131ea29b60f0"
- integrity sha512-SxBK7wQfJ4UoWoJqQnmIC7ZjuNgVb9rcY5Xc67upXAFKftWg0rnkknTw6vgwnjRcvYThrjzUVti66XoZdDJGtw==
+react-native-is-edge-to-edge@^1.2.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.3.1.tgz#feb9a6a8faf0874298947edd556e5af22044e139"
+ integrity sha512-NIXU/iT5+ORyCc7p0z2nnlkouYKX425vuU1OEm6bMMtWWR9yvb+Xg5AZmImTKoF9abxCPqrKC3rOZsKzUYgYZA==
+
+react-native-reanimated@~4.1.1:
+ version "4.1.7"
+ resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-4.1.7.tgz#b4e8524503a1b6ec1b5a40c460ee807a6a9fd2cf"
+ integrity sha512-Q4H6xA3Tn7QL0/E/KjI86I1KK4tcf+ErRE04LH34Etka2oVQhW6oXQ+Q8ZcDCVxiWp5vgbBH6XcH8BOo4w/Rhg==
dependencies:
- "@babel/plugin-transform-arrow-functions" "^7.0.0-0"
- "@babel/plugin-transform-class-properties" "^7.0.0-0"
- "@babel/plugin-transform-classes" "^7.0.0-0"
- "@babel/plugin-transform-nullish-coalescing-operator" "^7.0.0-0"
- "@babel/plugin-transform-optional-chaining" "^7.0.0-0"
- "@babel/plugin-transform-shorthand-properties" "^7.0.0-0"
- "@babel/plugin-transform-template-literals" "^7.0.0-0"
- "@babel/plugin-transform-unicode-regex" "^7.0.0-0"
- "@babel/preset-typescript" "^7.16.7"
- convert-source-map "^2.0.0"
- invariant "^2.2.4"
- react-native-is-edge-to-edge "1.1.7"
+ react-native-is-edge-to-edge "^1.2.1"
+ semver "^7.7.2"
-react-native-safe-area-context@5.4.0:
- version "5.4.0"
- resolved "https://registry.yarnpkg.com/react-native-safe-area-context/-/react-native-safe-area-context-5.4.0.tgz#04b51940408c114f75628a12a93569d30c525454"
- integrity sha512-JaEThVyJcLhA+vU0NU8bZ0a1ih6GiF4faZ+ArZLqpYbL6j7R3caRqj+mE3lEtKCuHgwjLg3bCxLL1GPUJZVqUA==
+react-native-safe-area-context@~5.6.0:
+ version "5.6.2"
+ resolved "https://registry.yarnpkg.com/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz#283e006f5b434fb247fcb4be0971ad7473d5c560"
+ integrity sha512-4XGqMNj5qjUTYywJqpdWZ9IG8jgkS3h06sfVjfw5yZQZfWnRFXczi0GnYyFyCc2EBps/qFmoCH8fez//WumdVg==
-react-native-screens@~4.10.0:
- version "4.10.0"
- resolved "https://registry.yarnpkg.com/react-native-screens/-/react-native-screens-4.10.0.tgz#40634aead590c6b7034ded6a9f92465d1d611906"
- integrity sha512-Tw21NGuXm3PbiUGtZd0AnXirUixaAbPXDjNR0baBH7/WJDaDTTELLcQ7QRXuqAWbmr/EVCrKj1348ei1KFIr8A==
+react-native-screens@~4.16.0:
+ version "4.16.0"
+ resolved "https://registry.yarnpkg.com/react-native-screens/-/react-native-screens-4.16.0.tgz#efa42e77a092aa0b5277c9ae41391ea0240e0870"
+ integrity sha512-yIAyh7F/9uWkOzCi1/2FqvNvK6Wb9Y1+Kzn16SuGfN9YFJDTbwlzGRvePCNTOX0recpLQF3kc2FmvMUhyTCH1Q==
dependencies:
react-freeze "^1.0.0"
+ react-native-is-edge-to-edge "^1.2.1"
warn-once "^0.1.0"
-react-native-web@~0.20.0:
- version "0.20.0"
- resolved "https://registry.yarnpkg.com/react-native-web/-/react-native-web-0.20.0.tgz#3fb0591999ed4b54d7822a2785547415e8a5c031"
- integrity sha512-OOSgrw+aON6R3hRosCau/xVxdLzbjEcsLysYedka0ZON4ZZe6n9xgeN9ZkoejhARM36oTlUgHIQqxGutEJ9Wxg==
+react-native-web@^0.21.0:
+ version "0.21.2"
+ resolved "https://registry.yarnpkg.com/react-native-web/-/react-native-web-0.21.2.tgz#0f6983dfea600d9cc1c66fda87ff9ca585eaa647"
+ integrity sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg==
dependencies:
"@babel/runtime" "^7.18.6"
"@react-native/normalize-colors" "^0.74.1"
@@ -11890,61 +12985,136 @@ react-native-web@~0.20.0:
postcss-value-parser "^4.2.0"
styleq "^0.1.3"
-react-native-webview@13.13.5:
- version "13.13.5"
- resolved "https://registry.yarnpkg.com/react-native-webview/-/react-native-webview-13.13.5.tgz#4ef5f9310ddff5747f884a6655228ec9c7d52c73"
- integrity sha512-MfC2B+woL4Hlj2WCzcb1USySKk+SteXnUKmKktOk/H/AQy5+LuVdkPKm8SknJ0/RxaxhZ48WBoTRGaqgR137hw==
+react-native-webview@13.15.0:
+ version "13.15.0"
+ resolved "https://registry.yarnpkg.com/react-native-webview/-/react-native-webview-13.15.0.tgz#b6d2f8d8dd65897db76659ddd8198d2c74ec5a79"
+ integrity sha512-Vzjgy8mmxa/JO6l5KZrsTC7YemSdq+qB01diA0FqjUTaWGAGwuykpJ73MDj3+mzBSlaDxAEugHzTtkUQkQEQeQ==
dependencies:
escape-string-regexp "^4.0.0"
invariant "2.2.4"
-react-native@0.79.2:
- version "0.79.2"
- resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.79.2.tgz#f1a53099701c1736d09e441eb79f97cfc90dd202"
- integrity sha512-AnGzb56JvU5YCL7cAwg10+ewDquzvmgrMddiBM0GAWLwQM/6DJfGd2ZKrMuKKehHerpDDZgG+EY64gk3x3dEkw==
+react-native-worklets@0.5.1:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/react-native-worklets/-/react-native-worklets-0.5.1.tgz#d153242655e3757b6c62a474768831157316ad33"
+ integrity sha512-lJG6Uk9YuojjEX/tQrCbcbmpdLCSFxDK1rJlkDhgqkVi1KZzG7cdcBFQRqyNOOzR9Y0CXNuldmtWTGOyM0k0+w==
+ dependencies:
+ "@babel/plugin-transform-arrow-functions" "^7.0.0-0"
+ "@babel/plugin-transform-class-properties" "^7.0.0-0"
+ "@babel/plugin-transform-classes" "^7.0.0-0"
+ "@babel/plugin-transform-nullish-coalescing-operator" "^7.0.0-0"
+ "@babel/plugin-transform-optional-chaining" "^7.0.0-0"
+ "@babel/plugin-transform-shorthand-properties" "^7.0.0-0"
+ "@babel/plugin-transform-template-literals" "^7.0.0-0"
+ "@babel/plugin-transform-unicode-regex" "^7.0.0-0"
+ "@babel/preset-typescript" "^7.16.7"
+ convert-source-map "^2.0.0"
+ semver "7.7.2"
+
+react-native@0.81.5:
+ version "0.81.5"
+ resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.81.5.tgz#6c963f137d3979b22aef2d8482067775c8fe2fed"
+ integrity sha512-1w+/oSjEXZjMqsIvmkCRsOc8UBYv163bTWKTI8+1mxztvQPhCRYGTvZ/PL1w16xXHneIj/SLGfxWg2GWN2uexw==
dependencies:
"@jest/create-cache-key-function" "^29.7.0"
- "@react-native/assets-registry" "0.79.2"
- "@react-native/codegen" "0.79.2"
- "@react-native/community-cli-plugin" "0.79.2"
- "@react-native/gradle-plugin" "0.79.2"
- "@react-native/js-polyfills" "0.79.2"
- "@react-native/normalize-colors" "0.79.2"
- "@react-native/virtualized-lists" "0.79.2"
+ "@react-native/assets-registry" "0.81.5"
+ "@react-native/codegen" "0.81.5"
+ "@react-native/community-cli-plugin" "0.81.5"
+ "@react-native/gradle-plugin" "0.81.5"
+ "@react-native/js-polyfills" "0.81.5"
+ "@react-native/normalize-colors" "0.81.5"
+ "@react-native/virtualized-lists" "0.81.5"
abort-controller "^3.0.0"
anser "^1.4.9"
ansi-regex "^5.0.0"
babel-jest "^29.7.0"
- babel-plugin-syntax-hermes-parser "0.25.1"
+ babel-plugin-syntax-hermes-parser "0.29.1"
base64-js "^1.5.1"
- chalk "^4.0.0"
commander "^12.0.0"
- event-target-shim "^5.0.1"
flow-enums-runtime "^0.0.6"
glob "^7.1.1"
invariant "^2.2.4"
jest-environment-node "^29.7.0"
memoize-one "^5.0.0"
- metro-runtime "^0.82.0"
- metro-source-map "^0.82.0"
+ metro-runtime "^0.83.1"
+ metro-source-map "^0.83.1"
nullthrows "^1.1.1"
pretty-format "^29.7.0"
promise "^8.3.0"
- react-devtools-core "^6.1.1"
+ react-devtools-core "^6.1.5"
react-refresh "^0.14.0"
regenerator-runtime "^0.13.2"
- scheduler "0.25.0"
+ scheduler "0.26.0"
semver "^7.1.3"
stacktrace-parser "^0.1.10"
whatwg-fetch "^3.0.0"
ws "^6.2.3"
yargs "^17.6.2"
+react-native@^0.83.1:
+ version "0.83.4"
+ resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.83.4.tgz#5c3103a8523eee46559b6e6228ff9ac9d907ee1f"
+ integrity sha512-H5Wco3UJyY6zZsjoBayY8RM9uiAEQ3FeG4G2NAt+lr9DO43QeqPlVe9xxxYEukMkEmeIhNjR70F6bhXuWArOMQ==
+ dependencies:
+ "@jest/create-cache-key-function" "^29.7.0"
+ "@react-native/assets-registry" "0.83.4"
+ "@react-native/codegen" "0.83.4"
+ "@react-native/community-cli-plugin" "0.83.4"
+ "@react-native/gradle-plugin" "0.83.4"
+ "@react-native/js-polyfills" "0.83.4"
+ "@react-native/normalize-colors" "0.83.4"
+ "@react-native/virtualized-lists" "0.83.4"
+ abort-controller "^3.0.0"
+ anser "^1.4.9"
+ ansi-regex "^5.0.0"
+ babel-jest "^29.7.0"
+ babel-plugin-syntax-hermes-parser "0.32.0"
+ base64-js "^1.5.1"
+ commander "^12.0.0"
+ flow-enums-runtime "^0.0.6"
+ glob "^7.1.1"
+ hermes-compiler "0.14.1"
+ invariant "^2.2.4"
+ jest-environment-node "^29.7.0"
+ memoize-one "^5.0.0"
+ metro-runtime "^0.83.3"
+ metro-source-map "^0.83.3"
+ nullthrows "^1.1.1"
+ pretty-format "^29.7.0"
+ promise "^8.3.0"
+ react-devtools-core "^6.1.5"
+ react-refresh "^0.14.0"
+ regenerator-runtime "^0.13.2"
+ scheduler "0.27.0"
+ semver "^7.1.3"
+ stacktrace-parser "^0.1.10"
+ whatwg-fetch "^3.0.0"
+ ws "^7.5.10"
+ yargs "^17.6.2"
+
react-refresh@^0.14.0, react-refresh@^0.14.2:
version "0.14.2"
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.2.tgz#3833da01ce32da470f1f936b9d477da5c7028bf9"
integrity sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==
+react-remove-scroll-bar@^2.3.7:
+ version "2.3.8"
+ resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz#99c20f908ee467b385b68a3469b4a3e750012223"
+ integrity sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==
+ dependencies:
+ react-style-singleton "^2.2.2"
+ tslib "^2.0.0"
+
+react-remove-scroll@^2.6.3:
+ version "2.7.2"
+ resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz#6442da56791117661978ae99cd29be9026fecca0"
+ integrity sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==
+ dependencies:
+ react-remove-scroll-bar "^2.3.7"
+ react-style-singleton "^2.2.3"
+ tslib "^2.1.0"
+ use-callback-ref "^1.3.3"
+ use-sidecar "^1.1.3"
+
react-router-dom@^6.24.1:
version "6.24.1"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.24.1.tgz#b1a22f7d6c5a1bfce30732bd370713f991ab4de4"
@@ -11960,6 +13130,14 @@ react-router@6.24.1, react-router@^6.24.1:
dependencies:
"@remix-run/router" "1.17.1"
+react-style-singleton@^2.2.2, react-style-singleton@^2.2.3:
+ version "2.2.3"
+ resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.3.tgz#4265608be69a4d70cfe3047f2c6c88b2c3ace388"
+ integrity sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==
+ dependencies:
+ get-nonce "^1.0.0"
+ tslib "^2.0.0"
+
react@18.2.0:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
@@ -11967,10 +13145,15 @@ react@18.2.0:
dependencies:
loose-envify "^1.1.0"
-react@19.0.1:
- version "19.0.1"
- resolved "https://registry.yarnpkg.com/react/-/react-19.0.1.tgz#0fb9523201af5f8c7aee753a825d1d9d2f9769db"
- integrity sha512-nVRaZCuEyvu69sWrkdwjP6QY57C+lY+uMNNMyWUFJb9Z/JlaBOQus7mSMfGYsblv7R691u6SSJA/dX9IRnyyLQ==
+react@19.1.0:
+ version "19.1.0"
+ resolved "https://registry.yarnpkg.com/react/-/react-19.1.0.tgz#926864b6c48da7627f004795d6cce50e90793b75"
+ integrity sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==
+
+react@^19.2.4:
+ version "19.2.4"
+ resolved "https://registry.yarnpkg.com/react/-/react-19.2.4.tgz#438e57baa19b77cb23aab516cf635cd0579ee09a"
+ integrity sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==
read-pkg-up@10.0.0:
version "10.0.0"
@@ -12167,6 +13350,11 @@ require-from-string@^2.0.2:
resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
+require-main-filename@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
+ integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
+
requireg@^0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/requireg/-/requireg-0.2.2.tgz#437e77a5316a54c9bcdbbf5d1f755fe093089830"
@@ -12188,11 +13376,6 @@ resolve-cwd@^3.0.0:
dependencies:
resolve-from "^5.0.0"
-resolve-from@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748"
- integrity sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==
-
resolve-from@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
@@ -12527,10 +13710,15 @@ saxes@^6.0.0:
dependencies:
xmlchars "^2.2.0"
-scheduler@0.25.0, scheduler@^0.25.0:
- version "0.25.0"
- resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.25.0.tgz#336cd9768e8cceebf52d3c80e3dcf5de23e7e015"
- integrity sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==
+scheduler@0.26.0, scheduler@^0.26.0:
+ version "0.26.0"
+ resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.26.0.tgz#4ce8a8c2a2095f13ea11bf9a445be50c555d6337"
+ integrity sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==
+
+scheduler@0.27.0:
+ version "0.27.0"
+ resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.27.0.tgz#0c4ef82d67d1e5c1e359e8fc76d3a87f045fe5bd"
+ integrity sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==
scheduler@^0.23.0:
version "0.23.2"
@@ -12539,15 +13727,10 @@ scheduler@^0.23.0:
dependencies:
loose-envify "^1.1.0"
-schema-utils@^4.0.1:
- version "4.2.0"
- resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.2.0.tgz#70d7c93e153a273a805801882ebd3bff20d89c8b"
- integrity sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==
- dependencies:
- "@types/json-schema" "^7.0.9"
- ajv "^8.9.0"
- ajv-formats "^2.1.1"
- ajv-keywords "^5.1.0"
+semver@7.7.2, semver@^7.1.3, semver@^7.7.1, semver@^7.7.2:
+ version "7.7.2"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58"
+ integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==
semver@^5.6.0:
version "5.7.2"
@@ -12559,16 +13742,16 @@ semver@^6.0.0, semver@^6.3.0, semver@^6.3.1:
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
-semver@^7.1.3, semver@^7.7.1, semver@^7.7.2:
- version "7.7.2"
- resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58"
- integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==
-
semver@^7.2.1, semver@^7.3.5, semver@^7.5.3, semver@^7.5.4, semver@^7.6.0:
version "7.6.2"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13"
integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==
+semver@^7.5.2:
+ version "7.7.4"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.4.tgz#28464e36060e991fa7a11d0279d2d3f3b57a7e8a"
+ integrity sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==
+
semver@~7.6.3:
version "7.6.3"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143"
@@ -12629,6 +13812,25 @@ send@^1.1.0, send@^1.2.0:
range-parser "^1.2.1"
statuses "^2.0.1"
+send@~0.19.1:
+ version "0.19.2"
+ resolved "https://registry.yarnpkg.com/send/-/send-0.19.2.tgz#59bc0da1b4ea7ad42736fd642b1c4294e114ff29"
+ integrity sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==
+ dependencies:
+ debug "2.6.9"
+ depd "2.0.0"
+ destroy "1.2.0"
+ encodeurl "~2.0.0"
+ escape-html "~1.0.3"
+ etag "~1.8.1"
+ fresh "~0.5.2"
+ http-errors "~2.0.1"
+ mime "1.6.0"
+ ms "2.1.3"
+ on-finished "~2.4.1"
+ range-parser "~1.2.1"
+ statuses "~2.0.2"
+
serialize-error@^11.0.1:
version "11.0.3"
resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-11.0.3.tgz#b54f439e15da5b4961340fbbd376b6b04aa52e92"
@@ -12641,6 +13843,16 @@ serialize-error@^2.1.0:
resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-2.1.0.tgz#50b679d5635cdf84667bdc8e59af4e5b81d5f60a"
integrity sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==
+serve-static@^1.13.1:
+ version "1.16.3"
+ resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.3.tgz#a97b74d955778583f3862a4f0b841eb4d5d78cf9"
+ integrity sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==
+ dependencies:
+ encodeurl "~2.0.0"
+ escape-html "~1.0.3"
+ parseurl "~1.3.3"
+ send "~0.19.1"
+
serve-static@^1.16.2:
version "1.16.2"
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296"
@@ -12666,6 +13878,11 @@ server-only@^0.0.1:
resolved "https://registry.yarnpkg.com/server-only/-/server-only-0.0.1.tgz#0f366bb6afb618c37c9255a314535dc412cd1c9e"
integrity sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==
+set-blocking@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
+ integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==
+
set-function-length@^1.2.1, set-function-length@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449"
@@ -12702,7 +13919,7 @@ setimmediate@^1.0.5:
resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==
-setprototypeof@1.2.0:
+setprototypeof@1.2.0, setprototypeof@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
@@ -12712,6 +13929,11 @@ sf-symbols-typescript@^2.0.0:
resolved "https://registry.yarnpkg.com/sf-symbols-typescript/-/sf-symbols-typescript-2.1.0.tgz#50a2d7b36edd6809606f0b0a36322fc1fdd7cfde"
integrity sha512-ezT7gu/SHTPIOEEoG6TF+O0m5eewl0ZDAO4AtdBi5HjsrUI6JdCG17+Q8+aKp0heM06wZKApRCn5olNbs0Wb/A==
+sf-symbols-typescript@^2.1.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/sf-symbols-typescript/-/sf-symbols-typescript-2.2.0.tgz#926d6e0715e3d8784cadf7658431e36581254208"
+ integrity sha512-TPbeg0b7ylrswdGCji8FRGFAKuqbpQlLbL8SOle3j1iHSs5Ob5mhvMAxWN2UItOjgALAB5Zp3fmMfj8mbWvXKw==
+
shallow-clone@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3"
@@ -12741,6 +13963,11 @@ shell-quote@^1.6.1:
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.2.tgz#d2d83e057959d53ec261311e9e9b8f51dcb2934a"
integrity sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==
+shell-quote@^1.8.3:
+ version "1.8.3"
+ resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.3.tgz#55e40ef33cf5c689902353a3d8cd1a6725f08b4b"
+ integrity sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==
+
shiki@^2.1.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/shiki/-/shiki-2.5.0.tgz#09d01ebf3b0b06580431ce3ddc023320442cf223"
@@ -12846,6 +14073,15 @@ slash@^3.0.0:
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
+slice-ansi@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636"
+ integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==
+ dependencies:
+ ansi-styles "^3.2.0"
+ astral-regex "^1.0.0"
+ is-fullwidth-code-point "^2.0.0"
+
slice-ansi@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b"
@@ -13056,6 +14292,11 @@ statuses@~1.5.0:
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==
+statuses@~2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.2.tgz#8f75eecef765b5e1cfcdc080da59409ed424e382"
+ integrity sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==
+
stream-buffers@2.2.x:
version "2.2.0"
resolved "https://registry.yarnpkg.com/stream-buffers/-/stream-buffers-2.2.0.tgz#91d5f5130d1cef96dcfa7f726945188741d09ee4"
@@ -13081,6 +14322,11 @@ strict-uri-encode@^2.0.0:
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546"
integrity sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==
+strict-url-sanitise@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/strict-url-sanitise/-/strict-url-sanitise-0.0.1.tgz#10cfac63c9dfdd856d98ab9f76433dad5ce99e0c"
+ integrity sha512-nuFtF539K8jZg3FjaWH/L8eocCR6gegz5RDOsaWxfdbF5Jqr2VXWxZayjTwUzsWJDC91k2EbnJXp6FuWW+Z4hg==
+
string-length@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a"
@@ -13223,7 +14469,7 @@ stringify-entities@^4.0.0:
dependencies:
ansi-regex "^5.0.1"
-strip-ansi@^5.2.0:
+strip-ansi@^5.0.0, strip-ansi@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae"
integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==
@@ -13274,6 +14520,11 @@ strip-json-comments@~2.0.1:
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==
+strnum@^2.2.2:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/strnum/-/strnum-2.2.2.tgz#f11fd94ab62b536ba2ecc615858f3747c2881b3f"
+ integrity sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA==
+
structured-headers@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/structured-headers/-/structured-headers-0.4.1.tgz#77abd9410622c6926261c09b9d16cf10592694d1"
@@ -13284,7 +14535,7 @@ styleq@^0.1.3:
resolved "https://registry.yarnpkg.com/styleq/-/styleq-0.1.3.tgz#8efb2892debd51ce7b31dc09c227ad920decab71"
integrity sha512-3ZUifmCDCQanjeej1f6kyl/BeP/Vae5EYkQ9iJfUm/QwZvlgnZzyflqAsAWYURdtea8Vkvswu2GrC57h3qffcA==
-sucrase@3.35.0, sucrase@^3.20.3:
+sucrase@^3.20.3:
version "3.35.0"
resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.35.0.tgz#57f17a3d7e19b36d8995f06679d121be914ae263"
integrity sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==
@@ -13297,6 +14548,19 @@ sucrase@3.35.0, sucrase@^3.20.3:
pirates "^4.0.1"
ts-interface-checker "^0.1.9"
+sucrase@~3.35.1:
+ version "3.35.1"
+ resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.35.1.tgz#4619ea50393fe8bd0ae5071c26abd9b2e346bfe1"
+ integrity sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==
+ dependencies:
+ "@jridgewell/gen-mapping" "^0.3.2"
+ commander "^4.0.0"
+ lines-and-columns "^1.1.6"
+ mz "^2.7.0"
+ pirates "^4.0.1"
+ tinyglobby "^0.2.11"
+ ts-interface-checker "^0.1.9"
+
superjson@^2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/superjson/-/superjson-2.2.2.tgz#9d52bf0bf6b5751a3c3472f1292e714782ba3173"
@@ -13393,23 +14657,17 @@ tar-stream@^3.0.0, tar-stream@^3.1.5:
fast-fifo "^1.2.0"
streamx "^2.15.0"
-tar@^7.4.3:
- version "7.4.3"
- resolved "https://registry.yarnpkg.com/tar/-/tar-7.4.3.tgz#88bbe9286a3fcd900e94592cda7a22b192e80571"
- integrity sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==
+tar@^7.5.2:
+ version "7.5.12"
+ resolved "https://registry.yarnpkg.com/tar/-/tar-7.5.12.tgz#f8705c00ca1001b8b60bc44db1ab26573736b871"
+ integrity sha512-9TsuLcdhOn4XztcQqhNyq1KOwOOED/3k58JAvtULiYqbO8B/0IBAAIE1hj0Svmm58k27TmcigyDI0deMlgG3uw==
dependencies:
"@isaacs/fs-minipass" "^4.0.0"
chownr "^3.0.0"
minipass "^7.1.2"
- minizlib "^3.0.1"
- mkdirp "^3.0.1"
+ minizlib "^3.1.0"
yallist "^5.0.0"
-temp-dir@~2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-2.0.0.tgz#bde92b05bdfeb1516e804c9c00ad45177f31321e"
- integrity sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==
-
terminal-link@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994"
@@ -13466,6 +14724,14 @@ through@^2.3.8:
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==
+tinyglobby@^0.2.11:
+ version "0.2.15"
+ resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.15.tgz#e228dd1e638cea993d2fdb4fcd2d4602a79951c2"
+ integrity sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==
+ dependencies:
+ fdir "^6.5.0"
+ picomatch "^4.0.3"
+
tinyglobby@^0.2.13:
version "0.2.14"
resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.14.tgz#5280b0cf3f972b050e74ae88406c0a6a58f4079d"
@@ -13510,7 +14776,7 @@ to-regex-range@^5.0.1:
dependencies:
is-number "^7.0.0"
-toidentifier@1.0.1:
+toidentifier@1.0.1, toidentifier@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
@@ -13612,7 +14878,7 @@ tslib@^1.9.3:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
-tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0:
+tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0:
version "2.8.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
@@ -13925,11 +15191,6 @@ undici@^6.18.2:
resolved "https://registry.yarnpkg.com/undici/-/undici-6.21.3.tgz#185752ad92c3d0efe7a7d1f6854a50f83b552d7a"
integrity sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==
-"undici@^6.18.2 || ^7.0.0":
- version "7.10.0"
- resolved "https://registry.yarnpkg.com/undici/-/undici-7.10.0.tgz#8ae17a976acc6593b13c9ff3342840bea9b24670"
- integrity sha512-u5otvFBOBZvmdjWLVW+5DAc9Nkq8f24g0O9oY7qw2JVIF1VocIFoyz9JFkuVOS2j41AufeO0xnlweJ2RLT8nGw==
-
unicode-canonical-property-names-ecmascript@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc"
@@ -13953,13 +15214,6 @@ unicode-property-aliases-ecmascript@^2.0.0:
resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd"
integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==
-unique-string@~2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d"
- integrity sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==
- dependencies:
- crypto-random-string "^2.0.0"
-
unist-util-is@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-6.0.0.tgz#b775956486aff107a9ded971d996c173374be424"
@@ -13998,6 +15252,11 @@ unist-util-visit@^5.0.0:
unist-util-is "^6.0.0"
unist-util-visit-parents "^6.0.0"
+universalify@^0.1.0:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
+ integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
+
universalify@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d"
@@ -14068,6 +15327,14 @@ update-browserslist-db@^1.1.3:
escalade "^3.2.0"
picocolors "^1.1.1"
+update-browserslist-db@^1.2.0:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz#64d76db58713136acbeb4c49114366cc6cc2e80d"
+ integrity sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==
+ dependencies:
+ escalade "^3.2.0"
+ picocolors "^1.1.1"
+
uri-js@^4.2.2, uri-js@^4.4.1:
version "4.4.1"
resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
@@ -14075,10 +15342,25 @@ uri-js@^4.2.2, uri-js@^4.4.1:
dependencies:
punycode "^2.1.0"
-use-latest-callback@^0.2.3:
- version "0.2.3"
- resolved "https://registry.yarnpkg.com/use-latest-callback/-/use-latest-callback-0.2.3.tgz#2d644d3063040b9bc2d4c55bb525a13ae3de9e16"
- integrity sha512-7vI3fBuyRcP91pazVboc4qu+6ZqM8izPWX9k7cRnT8hbD5svslcknsh3S9BUhaK11OmgTV4oWZZVSeQAiV53SQ==
+use-callback-ref@^1.3.3:
+ version "1.3.3"
+ resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.3.tgz#98d9fab067075841c5b2c6852090d5d0feabe2bf"
+ integrity sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==
+ dependencies:
+ tslib "^2.0.0"
+
+use-latest-callback@^0.2.1, use-latest-callback@^0.2.4:
+ version "0.2.6"
+ resolved "https://registry.yarnpkg.com/use-latest-callback/-/use-latest-callback-0.2.6.tgz#e5ea752808c86219acc179ace0ae3c1203255e77"
+ integrity sha512-FvRG9i1HSo0wagmX63Vrm8SnlUU3LMM3WyZkQ76RnslpBrX694AdG4A0zQBx2B3ZifFA0yv/BaEHGBnEax5rZg==
+
+use-sidecar@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.3.tgz#10e7fd897d130b896e2c546c63a5e8233d00efdb"
+ integrity sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==
+ dependencies:
+ detect-node-es "^1.1.0"
+ tslib "^2.0.0"
use-sync-external-store@^1.5.0:
version "1.5.0"
@@ -14142,6 +15424,13 @@ vary@^1.1.2, vary@~1.1.2:
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==
+vaul@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/vaul/-/vaul-1.1.2.tgz#c959f8b9dc2ed4f7d99366caee433fbef91f5ba9"
+ integrity sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==
+ dependencies:
+ "@radix-ui/react-dialog" "^1.1.1"
+
vfile-message@^4.0.0:
version "4.0.2"
resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-4.0.2.tgz#c883c9f677c72c166362fd635f21fc165a7d1181"
@@ -14465,6 +15754,11 @@ which-collection@^1.0.2:
is-weakmap "^2.0.2"
is-weakset "^2.0.3"
+which-module@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409"
+ integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==
+
which-typed-array@^1.1.14, which-typed-array@^1.1.15:
version "1.1.15"
resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d"
@@ -14590,9 +15884,9 @@ ws@^8.12.1:
integrity sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==
ws@^8.18.0:
- version "8.18.3"
- resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.3.tgz#b56b88abffde62791c639170400c93dcb0c95472"
- integrity sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==
+ version "8.20.0"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-8.20.0.tgz#4cd9532358eba60bc863aad1623dfb045a4d4af8"
+ integrity sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==
ws@^8.8.0:
version "8.17.0"
@@ -14646,6 +15940,11 @@ xmlchars@^2.2.0:
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
+y18n@^4.0.0:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf"
+ integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==
+
y18n@^5.0.5:
version "5.0.8"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
@@ -14661,11 +15960,29 @@ yallist@^5.0.0:
resolved "https://registry.yarnpkg.com/yallist/-/yallist-5.0.0.tgz#00e2de443639ed0d78fd87de0d27469fbcffb533"
integrity sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==
+yaml@^2.2.1:
+ version "2.8.3"
+ resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.8.3.tgz#a0d6bd2efb3dd03c59370223701834e60409bd7d"
+ integrity sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==
+
+yaml@^2.6.1:
+ version "2.8.2"
+ resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.8.2.tgz#5694f25eca0ce9c3e7a9d9e00ce0ddabbd9e35c5"
+ integrity sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==
+
yaml@^2.8.0:
version "2.8.1"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.8.1.tgz#1870aa02b631f7e8328b93f8bc574fac5d6c4d79"
integrity sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==
+yargs-parser@^18.1.2:
+ version "18.1.3"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"
+ integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==
+ dependencies:
+ camelcase "^5.0.0"
+ decamelize "^1.2.0"
+
yargs-parser@^21.1.1:
version "21.1.1"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
@@ -14697,6 +16014,23 @@ yargs@17.7.2, yargs@^17.3.1, yargs@^17.6.2, yargs@^17.7.2:
y18n "^5.0.5"
yargs-parser "^21.1.1"
+yargs@^15.1.0:
+ version "15.4.1"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8"
+ integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==
+ dependencies:
+ cliui "^6.0.0"
+ decamelize "^1.2.0"
+ find-up "^4.1.0"
+ get-caller-file "^2.0.1"
+ require-directory "^2.1.1"
+ require-main-filename "^2.0.0"
+ set-blocking "^2.0.0"
+ string-width "^4.2.0"
+ which-module "^2.0.0"
+ y18n "^4.0.0"
+ yargs-parser "^18.1.2"
+
yauzl@^2.10.0:
version "2.10.0"
resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"