diff --git a/apps/mobile/.gitignore b/apps/mobile/.gitignore
new file mode 100644
index 00000000000..6dd4bbef84d
--- /dev/null
+++ b/apps/mobile/.gitignore
@@ -0,0 +1,13 @@
+/node_modules
+.expo
+/ios
+/android
+dist
+coverage
+*.tsbuildinfo
+
+# @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb
+# The following patterns were generated by expo-cli
+
+expo-env.d.ts
+# @end expo-cli
\ No newline at end of file
diff --git a/apps/mobile/app.config.js b/apps/mobile/app.config.js
new file mode 100644
index 00000000000..7d6f2acd26f
--- /dev/null
+++ b/apps/mobile/app.config.js
@@ -0,0 +1,67 @@
+const associatedDomains =
+ process.env.CAP_MOBILE_DISABLE_ASSOCIATED_DOMAINS === "1"
+ ? []
+ : process.env.CAP_MOBILE_ASSOCIATED_DOMAINS
+ ? process.env.CAP_MOBILE_ASSOCIATED_DOMAINS.split(",")
+ .map((domain) => domain.trim())
+ .filter(Boolean)
+ : ["applinks:cap.so"];
+const bundleIdentifier = "so.cap.mobile";
+const ios = {
+ bundleIdentifier,
+ supportsTablet: false,
+ infoPlist: {
+ NSPhotoLibraryUsageDescription:
+ "Cap imports videos from Photos for upload.",
+ NSPhotoLibraryAddUsageDescription: "Cap saves downloaded videos to Photos.",
+ UIBackgroundModes: ["processing"],
+ },
+};
+
+if (associatedDomains.length > 0) {
+ ios.associatedDomains = associatedDomains;
+}
+
+module.exports = ({ config }) => ({
+ ...config,
+ name: "Cap",
+ slug: "cap-mobile",
+ scheme: "cap",
+ owner: "cap",
+ version: "0.1.0",
+ orientation: "portrait",
+ platforms: ["ios"],
+ userInterfaceStyle: "light",
+ icon: "./assets/icon.png",
+ splash: {
+ image: "./assets/splash-icon.png",
+ resizeMode: "contain",
+ backgroundColor: "#f9f9f9",
+ },
+ ios,
+ experiments: {
+ typedRoutes: true,
+ },
+ plugins: [
+ "expo-router",
+ [
+ "expo-font",
+ {
+ fonts: [
+ "../web/public/fonts/NeueMontreal-Regular.otf",
+ "../web/public/fonts/NeueMontreal-Medium.otf",
+ "../web/public/fonts/NeueMontreal-Bold.otf",
+ ],
+ },
+ ],
+ [
+ "expo-secure-store",
+ {
+ faceIDPermission: "Allow Cap to protect your account key.",
+ },
+ ],
+ ],
+ extra: {
+ apiBaseUrl: process.env.EXPO_PUBLIC_CAP_WEB_URL ?? "https://cap.so",
+ },
+});
diff --git a/apps/mobile/app/(tabs)/_layout.tsx b/apps/mobile/app/(tabs)/_layout.tsx
new file mode 100644
index 00000000000..3985c71fe94
--- /dev/null
+++ b/apps/mobile/app/(tabs)/_layout.tsx
@@ -0,0 +1,53 @@
+import { NativeTabs } from "expo-router/unstable-native-tabs";
+import { colors, fonts } from "@/theme";
+
+export default function TabsLayout() {
+ return (
+
+
+ My Caps
+
+
+
+ Import
+
+
+
+ Account
+
+
+
+ );
+}
diff --git a/apps/mobile/app/(tabs)/account.tsx b/apps/mobile/app/(tabs)/account.tsx
new file mode 100644
index 00000000000..7bb19f2b05e
--- /dev/null
+++ b/apps/mobile/app/(tabs)/account.tsx
@@ -0,0 +1,495 @@
+import Constants from "expo-constants";
+import { Image } from "expo-image";
+import { type SFSymbol, SymbolView } from "expo-symbols";
+import * as WebBrowser from "expo-web-browser";
+import { type ReactNode, useRef, useState } from "react";
+import {
+ ActionSheetIOS,
+ ActivityIndicator,
+ Alert,
+ Linking,
+ Platform,
+ Pressable,
+ StyleSheet,
+ Text,
+ View,
+} from "react-native";
+import { apiBaseUrl, useAuth } from "@/auth/AuthContext";
+import { SignInPanel } from "@/auth/SignInPanel";
+import { GlassSurface } from "@/components/GlassSurface";
+import { OrgSwitcher } from "@/components/OrgSwitcher";
+import { Screen } from "@/components/Screen";
+import { colors, fonts, radius, squircle } from "@/theme";
+
+type SettingsRowProps = {
+ label: string;
+ symbol: SFSymbol;
+ onPress?: () => void;
+ tintColor?: string;
+ destructive?: boolean;
+ value?: string;
+ accessibilityValueText?: string;
+ showChevron?: boolean;
+ accessibilityHint?: string;
+ busy?: boolean;
+ disabled?: boolean;
+};
+
+function SettingsRow({
+ label,
+ symbol,
+ onPress,
+ tintColor = colors.gray12,
+ destructive = false,
+ value,
+ accessibilityValueText,
+ showChevron = true,
+ accessibilityHint,
+ busy = false,
+ disabled = false,
+}: SettingsRowProps) {
+ const accessibilityValue = accessibilityValueText
+ ? { text: accessibilityValueText }
+ : value
+ ? { text: value }
+ : undefined;
+ const isAction = Boolean(onPress);
+ const isDisabled = disabled || busy;
+ const content = (
+ <>
+
+ {busy ? (
+
+ ) : (
+
+ )}
+
+
+ {label}
+
+ {value ? (
+
+ {value}
+
+ ) : null}
+ {showChevron && isAction ? (
+
+ ) : null}
+ >
+ );
+
+ if (!onPress) {
+ return (
+
+ {content}
+
+ );
+ }
+
+ return (
+ [
+ styles.settingsRow,
+ isDisabled && styles.settingsRowDisabled,
+ pressed && !isDisabled && styles.pressed,
+ ]}
+ >
+ {content}
+
+ );
+}
+
+function SettingsSection({
+ children,
+ title,
+}: {
+ children: ReactNode;
+ title: string;
+}) {
+ return (
+
+ {title}
+
+ {children}
+
+
+ );
+}
+
+type AccountAction =
+ | "appSettings"
+ | "organizationSettings"
+ | "refresh"
+ | "signOut";
+
+export default function AccountScreen() {
+ const auth = useAuth();
+ const appVersion = Constants.expoConfig?.version ?? "0.1.0";
+ const [accountAction, setAccountAction] = useState(
+ null,
+ );
+ const accountActionRef = useRef(null);
+ const accountActionHint =
+ accountAction === "refresh"
+ ? "Refresh is in progress"
+ : accountAction === "signOut"
+ ? "Sign out is in progress"
+ : accountAction !== null
+ ? "Settings are opening"
+ : null;
+ const accountActionDisabled = accountAction !== null;
+
+ const runAccountAction = async (
+ action: AccountAction,
+ operation: () => Promise,
+ ) => {
+ if (accountActionRef.current !== null) return;
+ accountActionRef.current = action;
+ setAccountAction(action);
+ try {
+ await operation();
+ } finally {
+ accountActionRef.current = null;
+ setAccountAction(null);
+ }
+ };
+
+ const confirmSignOut = () => {
+ if (accountActionRef.current !== null) return;
+
+ if (Platform.OS === "ios") {
+ ActionSheetIOS.showActionSheetWithOptions(
+ {
+ cancelButtonIndex: 1,
+ destructiveButtonIndex: 0,
+ message: "Remove this Cap session from your device?",
+ options: ["Sign out", "Cancel"],
+ title: "Sign out",
+ tintColor: colors.blue11,
+ userInterfaceStyle: "light",
+ },
+ (index) => {
+ if (index === 0) {
+ void runAccountAction("signOut", auth.signOut);
+ }
+ },
+ );
+ return;
+ }
+
+ Alert.alert("Sign out", "Remove this Cap session from your device?", [
+ { text: "Cancel", style: "cancel" },
+ {
+ text: "Sign out",
+ style: "destructive",
+ onPress: () => {
+ void runAccountAction("signOut", auth.signOut);
+ },
+ },
+ ]);
+ };
+
+ if (auth.status === "loading") {
+ return ;
+ }
+
+ if (auth.status === "signedOut") {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ {auth.bootstrap ? (
+
+
+
+ {auth.bootstrap.user.imageUrl ? (
+
+ ) : (
+
+ {(auth.bootstrap.user.name ?? auth.bootstrap.user.email)
+ .slice(0, 1)
+ .toUpperCase()}
+
+ )}
+
+
+
+ {auth.bootstrap.user.name ?? "Cap user"}
+
+
+ {auth.bootstrap.user.email}
+
+
+
+
+
+ ) : null}
+
+ {
+ void runAccountAction("organizationSettings", () =>
+ WebBrowser.openBrowserAsync(
+ new URL(
+ "/dashboard/settings/organization",
+ apiBaseUrl,
+ ).toString(),
+ ),
+ );
+ }}
+ value={
+ accountAction === "organizationSettings" ? "Opening..." : undefined
+ }
+ />
+
+
+ {
+ void runAccountAction("refresh", auth.refresh);
+ }}
+ value={accountAction === "refresh" ? "Refreshing..." : undefined}
+ />
+
+ {
+ void runAccountAction("appSettings", Linking.openSettings);
+ }}
+ value={accountAction === "appSettings" ? "Opening..." : undefined}
+ />
+
+
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ card: {
+ borderRadius: radius.md,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray3,
+ padding: 16,
+ gap: 16,
+ ...squircle,
+ },
+ cardFallback: {
+ backgroundColor: colors.gray1,
+ },
+ identityRow: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 12,
+ },
+ avatar: {
+ width: 48,
+ height: 48,
+ borderRadius: radius.sm,
+ overflow: "hidden",
+ alignItems: "center",
+ justifyContent: "center",
+ backgroundColor: colors.blue3,
+ ...squircle,
+ },
+ avatarImage: {
+ width: "100%",
+ height: "100%",
+ },
+ avatarText: {
+ fontFamily: fonts.medium,
+ fontSize: 18,
+ color: colors.blue11,
+ },
+ identityText: {
+ flex: 1,
+ minWidth: 0,
+ },
+ name: {
+ fontFamily: fonts.medium,
+ fontSize: 19,
+ color: colors.gray12,
+ },
+ email: {
+ fontFamily: fonts.regular,
+ fontSize: 14,
+ color: colors.gray10,
+ marginTop: 2,
+ },
+ settingsGroup: {
+ borderRadius: radius.md,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray3,
+ overflow: "hidden",
+ ...squircle,
+ },
+ section: {
+ marginTop: 16,
+ gap: 8,
+ },
+ sectionTitle: {
+ fontFamily: fonts.medium,
+ fontSize: 13,
+ lineHeight: 18,
+ color: colors.gray10,
+ paddingHorizontal: 4,
+ },
+ settingsFallback: {
+ backgroundColor: colors.gray1,
+ },
+ settingsRow: {
+ minHeight: 54,
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 12,
+ paddingHorizontal: 14,
+ },
+ settingsRowDisabled: {
+ backgroundColor: colors.gray2,
+ },
+ pressed: {
+ backgroundColor: colors.gray3,
+ },
+ settingsIcon: {
+ width: 30,
+ height: 30,
+ borderRadius: radius.sm,
+ alignItems: "center",
+ justifyContent: "center",
+ backgroundColor: colors.gray3,
+ ...squircle,
+ },
+ settingsLabel: {
+ flex: 1,
+ fontFamily: fonts.medium,
+ fontSize: 16,
+ color: colors.gray12,
+ },
+ settingsLabelDisabled: {
+ color: colors.gray9,
+ },
+ settingsValue: {
+ fontFamily: fonts.regular,
+ fontSize: 15,
+ color: colors.gray10,
+ },
+ settingsValueDisabled: {
+ color: colors.gray9,
+ },
+ dangerLabel: {
+ color: colors.red9,
+ },
+ separator: {
+ height: StyleSheet.hairlineWidth,
+ backgroundColor: colors.gray4,
+ marginLeft: 56,
+ },
+});
diff --git a/apps/mobile/app/(tabs)/index.tsx b/apps/mobile/app/(tabs)/index.tsx
new file mode 100644
index 00000000000..817f7e3c7f2
--- /dev/null
+++ b/apps/mobile/app/(tabs)/index.tsx
@@ -0,0 +1,947 @@
+import { FlashList } from "@shopify/flash-list";
+import * as Clipboard from "expo-clipboard";
+import { router } from "expo-router";
+import { SymbolView } from "expo-symbols";
+import * as WebBrowser from "expo-web-browser";
+import { useCallback, useEffect, useMemo, useState } from "react";
+import {
+ ActionSheetIOS,
+ Alert,
+ Linking,
+ Platform,
+ Pressable,
+ Share,
+ StyleSheet,
+ Text,
+ View,
+} from "react-native";
+import type {
+ MobileCapSummary,
+ MobileCapsListResponse,
+ MobileFolder,
+} from "@/api/mobile";
+import { MobileApiError } from "@/api/mobile";
+import { apiBaseUrl, useAuth } from "@/auth/AuthContext";
+import { SignInPanel } from "@/auth/SignInPanel";
+import { CapSettingsSheet } from "@/caps/CapSettingsSheet";
+import { showCapPasswordActions } from "@/caps/passwordActions";
+import {
+ PhotosPermissionDeniedError,
+ saveCapVideoToPhotos,
+} from "@/caps/saveCapVideo";
+import { showCapTitleActions } from "@/caps/titleActions";
+import { ActionButton } from "@/components/ActionButton";
+import { CapCard } from "@/components/CapCard";
+import { CapLogoBadge } from "@/components/CapLogoBadge";
+import { CapRefreshControl } from "@/components/CapRefreshControl";
+import { OrgSwitcher } from "@/components/OrgSwitcher";
+import { Screen } from "@/components/Screen";
+import { colors, fonts, radius, squircle } from "@/theme";
+
+type ListItem =
+ | { type: "section"; id: "folders" | "videos"; title: string }
+ | { type: "folder"; folder: MobileFolder }
+ | { type: "cap"; cap: MobileCapSummary };
+
+const folderColorOptions: Array<{
+ label: string;
+ color: MobileFolder["color"];
+}> = [
+ { label: "Normal", color: "normal" },
+ { label: "Blue", color: "blue" },
+ { label: "Red", color: "red" },
+ { label: "Yellow", color: "yellow" },
+];
+
+const folderTintByColor = {
+ normal: colors.gray12,
+ blue: colors.blue9,
+ red: colors.red9,
+ yellow: colors.yellow9,
+} as const;
+
+const getCapsErrorMessage = (error: unknown) => {
+ if (error instanceof MobileApiError) {
+ if (error.status === 401) return "Your session expired. Sign in again.";
+ return "Cap could not load your library. Try again.";
+ }
+ return error instanceof Error
+ ? error.message
+ : "Cap could not load your library. Try again.";
+};
+
+const showPhotosSettingsAlert = () => {
+ if (Platform.OS === "ios") {
+ ActionSheetIOS.showActionSheetWithOptions(
+ {
+ cancelButtonIndex: 1,
+ message: "Allow Cap to save videos to Photos from Settings.",
+ options: ["Open Settings", "Cancel"],
+ title: "Photos access needed",
+ tintColor: colors.blue11,
+ userInterfaceStyle: "light",
+ },
+ (index) => {
+ if (index === 0) void Linking.openSettings();
+ },
+ );
+ return;
+ }
+
+ Alert.alert(
+ "Photos access needed",
+ "Allow Cap to save videos to Photos from Settings.",
+ [
+ { text: "Cancel", style: "cancel" },
+ {
+ text: "Open Settings",
+ onPress: () => {
+ void Linking.openSettings();
+ },
+ },
+ ],
+ );
+};
+
+export default function CapsScreen() {
+ const auth = useAuth();
+ const [folder, setFolder] = useState(null);
+ const [result, setResult] = useState(null);
+ const [refreshing, setRefreshing] = useState(false);
+ const [loading, setLoading] = useState(false);
+ const [loadError, setLoadError] = useState(null);
+ const [savingId, setSavingId] = useState(null);
+ const [updatingSharingId, setUpdatingSharingId] = useState(
+ null,
+ );
+ const [settingsCap, setSettingsCap] = useState(null);
+ const [creatingFolder, setCreatingFolder] = useState(false);
+ const [creatingFolderName, setCreatingFolderName] = useState(
+ null,
+ );
+
+ const load = useCallback(async () => {
+ if (auth.status !== "signedIn") return;
+ setLoading(true);
+ try {
+ const response = await auth.client.listCaps({
+ folderId: folder?.id ?? null,
+ page: 1,
+ limit: 30,
+ });
+ setResult(response);
+ setLoadError(null);
+ } catch (error) {
+ setLoadError(getCapsErrorMessage(error));
+ } finally {
+ setLoading(false);
+ }
+ }, [auth, folder?.id]);
+
+ useEffect(() => {
+ void load();
+ }, [load]);
+
+ const refresh = useCallback(async () => {
+ setRefreshing(true);
+ try {
+ await Promise.all([auth.refresh(), load()]);
+ } catch (error) {
+ setLoadError(getCapsErrorMessage(error));
+ } finally {
+ setRefreshing(false);
+ }
+ }, [auth, load]);
+
+ const confirmDeleteCap = useCallback(
+ (cap: MobileCapSummary) => {
+ if (auth.status !== "signedIn") return;
+ const deleteCap = () => {
+ void (async () => {
+ setSettingsCap(null);
+ await auth.client.deleteCap(cap.id);
+ await Promise.all([auth.refresh(), load()]);
+ })();
+ };
+
+ if (Platform.OS === "ios") {
+ ActionSheetIOS.showActionSheetWithOptions(
+ {
+ cancelButtonIndex: 1,
+ destructiveButtonIndex: 0,
+ message: `${cap.title} will be removed from your library.`,
+ options: ["Delete Cap", "Cancel"],
+ title: "Delete Cap",
+ tintColor: colors.blue11,
+ userInterfaceStyle: "light",
+ },
+ (index) => {
+ if (index === 0) deleteCap();
+ },
+ );
+ return;
+ }
+
+ Alert.alert(
+ "Delete Cap",
+ `${cap.title} will be removed from your library.`,
+ [
+ { text: "Cancel", style: "cancel" },
+ {
+ text: "Delete",
+ style: "destructive",
+ onPress: deleteCap,
+ },
+ ],
+ );
+ },
+ [auth, load],
+ );
+
+ const copyCapLink = useCallback((cap: MobileCapSummary) => {
+ void Clipboard.setStringAsync(cap.shareUrl);
+ }, []);
+
+ const shareCapLink = useCallback((cap: MobileCapSummary) => {
+ void Share.share({ url: cap.shareUrl, message: cap.shareUrl });
+ }, []);
+
+ const updateCapVisibility = useCallback(
+ async (cap: MobileCapSummary, isPublic: boolean) => {
+ if (auth.status !== "signedIn" || updatingSharingId !== null) return;
+ setUpdatingSharingId(cap.id);
+ try {
+ const updated = await auth.client.updateCapSharing(cap.id, {
+ public: isPublic,
+ });
+ setSettingsCap((current) =>
+ current?.id === updated.id ? updated : current,
+ );
+ await Promise.all([auth.refresh(), load()]);
+ } catch (error) {
+ Alert.alert(
+ "Sharing update failed",
+ error instanceof Error
+ ? error.message
+ : "Unable to update sharing for this Cap.",
+ );
+ } finally {
+ setUpdatingSharingId(null);
+ }
+ },
+ [auth, load, updatingSharingId],
+ );
+
+ const saveCapVideo = useCallback(
+ async (cap: MobileCapSummary) => {
+ if (auth.status !== "signedIn" || savingId !== null) return;
+ setSavingId(cap.id);
+ try {
+ await saveCapVideoToPhotos(auth.client, cap.id);
+ } catch (error) {
+ if (error instanceof PhotosPermissionDeniedError) {
+ showPhotosSettingsAlert();
+ return;
+ }
+ Alert.alert(
+ "Save failed",
+ error instanceof Error ? error.message : "Unable to save this video.",
+ );
+ } finally {
+ setSavingId(null);
+ }
+ },
+ [auth, savingId],
+ );
+
+ const showPasswordActions = useCallback(
+ (cap: MobileCapSummary) => {
+ if (auth.status !== "signedIn") return;
+ showCapPasswordActions({
+ cap,
+ client: auth.client,
+ onUpdated: async (updated) => {
+ setSettingsCap((current) =>
+ current?.id === updated.id ? updated : current,
+ );
+ await Promise.all([auth.refresh(), load()]);
+ },
+ });
+ },
+ [auth, load],
+ );
+
+ const showTitleActions = useCallback(
+ (cap: MobileCapSummary) => {
+ if (auth.status !== "signedIn") return;
+ showCapTitleActions({
+ cap,
+ client: auth.client,
+ onUpdated: async (updated) => {
+ setSettingsCap((current) =>
+ current?.id === updated.id ? updated : current,
+ );
+ await Promise.all([auth.refresh(), load()]);
+ },
+ });
+ },
+ [auth, load],
+ );
+
+ const showCapSettings = useCallback((cap: MobileCapSummary) => {
+ setSettingsCap(cap);
+ }, []);
+
+ const viewAnalytics = useCallback((cap: MobileCapSummary) => {
+ const url = new URL("/dashboard/analytics", apiBaseUrl);
+ url.searchParams.set("capId", cap.id);
+ void WebBrowser.openBrowserAsync(url.toString());
+ }, []);
+
+ const createFolder = useCallback(
+ async (name: string, color: MobileFolder["color"]) => {
+ if (auth.status !== "signedIn" || creatingFolder) return;
+ const trimmedName = name.trim();
+ if (!trimmedName) {
+ Alert.alert("Folder name required", "Enter a folder name to continue.");
+ return;
+ }
+
+ setCreatingFolder(true);
+ setCreatingFolderName(trimmedName);
+ try {
+ await auth.client.createFolder({ name: trimmedName, color });
+ setFolder(null);
+ await Promise.all([auth.refresh(), load()]);
+ } catch (error) {
+ Alert.alert(
+ "Folder creation failed",
+ error instanceof Error
+ ? error.message
+ : "Unable to create this folder.",
+ );
+ } finally {
+ setCreatingFolder(false);
+ setCreatingFolderName(null);
+ }
+ },
+ [auth, creatingFolder, load],
+ );
+
+ const showFolderColorSheet = useCallback(
+ (name: string) => {
+ if (Platform.OS !== "ios") {
+ void createFolder(name, "normal");
+ return;
+ }
+
+ const cancelButtonIndex = folderColorOptions.length;
+ ActionSheetIOS.showActionSheetWithOptions(
+ {
+ cancelButtonIndex,
+ message: name,
+ options: [
+ ...folderColorOptions.map((option) => option.label),
+ "Cancel",
+ ],
+ title: "Folder color",
+ tintColor: colors.blue11,
+ userInterfaceStyle: "light",
+ },
+ (index) => {
+ const option = folderColorOptions[index];
+ if (option) void createFolder(name, option.color);
+ },
+ );
+ },
+ [createFolder],
+ );
+
+ const showNewFolderPrompt = useCallback(() => {
+ if (auth.status !== "signedIn" || creatingFolder) return;
+
+ if (Platform.OS === "ios") {
+ Alert.prompt(
+ "New Folder",
+ "Name this folder.",
+ [
+ { text: "Cancel", style: "cancel" },
+ {
+ text: "Next",
+ onPress: (value?: string) => {
+ const name = value?.trim() ?? "";
+ if (!name) {
+ Alert.alert(
+ "Folder name required",
+ "Enter a folder name to continue.",
+ );
+ return;
+ }
+ showFolderColorSheet(name);
+ },
+ },
+ ],
+ "plain-text",
+ );
+ return;
+ }
+
+ Alert.alert("New Folder", "Create a folder named Untitled?", [
+ { text: "Cancel", style: "cancel" },
+ {
+ text: "Create",
+ onPress: () => {
+ void createFolder("Untitled", "normal");
+ },
+ },
+ ]);
+ }, [auth.status, createFolder, creatingFolder, showFolderColorSheet]);
+
+ const showSharingActions = useCallback(
+ (cap: MobileCapSummary) => {
+ if (updatingSharingId !== null) return;
+ const visibilityAction = cap.public ? "Make private" : "Make public";
+
+ if (Platform.OS === "ios") {
+ ActionSheetIOS.showActionSheetWithOptions(
+ {
+ cancelButtonIndex: 3,
+ message: cap.shareUrl,
+ options: [visibilityAction, "Copy link", "Share link", "Cancel"],
+ title: cap.public ? "Shared" : "Not shared",
+ tintColor: colors.blue11,
+ userInterfaceStyle: "light",
+ },
+ (index) => {
+ if (index === 0) void updateCapVisibility(cap, !cap.public);
+ if (index === 1) copyCapLink(cap);
+ if (index === 2) shareCapLink(cap);
+ },
+ );
+ return;
+ }
+
+ Alert.alert(cap.public ? "Shared" : "Not shared", cap.shareUrl, [
+ {
+ text: visibilityAction,
+ onPress: () => void updateCapVisibility(cap, !cap.public),
+ },
+ { text: "Copy link", onPress: () => copyCapLink(cap) },
+ { text: "Share link", onPress: () => shareCapLink(cap) },
+ { text: "Cancel", style: "cancel" },
+ ]);
+ },
+ [copyCapLink, shareCapLink, updateCapVisibility, updatingSharingId],
+ );
+
+ const items = useMemo(() => {
+ if (!result) return [];
+ const nextItems: ListItem[] = [];
+ if (result.folders.length > 0) {
+ nextItems.push({ type: "section", id: "folders", title: "Folders" });
+ nextItems.push(
+ ...result.folders.map((item) => ({
+ type: "folder" as const,
+ folder: item,
+ })),
+ );
+ }
+ if (result.caps.length > 0) {
+ nextItems.push({ type: "section", id: "videos", title: "Videos" });
+ nextItems.push(
+ ...result.caps.map((item) => ({ type: "cap" as const, cap: item })),
+ );
+ }
+ return nextItems;
+ }, [result]);
+
+ const userName = auth.bootstrap?.user.name?.split(" ")[0];
+ const folderCreationHint = creatingFolder
+ ? "Folder creation is in progress"
+ : "Creates a folder for organizing Caps";
+ const folderCreationStatus = creatingFolder
+ ? `Creating folder ${creatingFolderName ?? ""}`.trim()
+ : null;
+ const folderCreationAccessibilityLabel = "New Folder";
+ const folderCreationAccessibilityValue = folderCreationStatus
+ ? { text: folderCreationStatus }
+ : undefined;
+ const dashboardActionHint = creatingFolder
+ ? "Folder creation is in progress"
+ : null;
+ const savingCap =
+ savingId !== null
+ ? settingsCap?.id === savingId
+ ? settingsCap
+ : (result?.caps.find((cap) => cap.id === savingId) ?? null)
+ : null;
+ const updatingSharingCap =
+ updatingSharingId !== null
+ ? settingsCap?.id === updatingSharingId
+ ? settingsCap
+ : (result?.caps.find((cap) => cap.id === updatingSharingId) ?? null)
+ : null;
+ const isLibraryActionInProgress =
+ savingId !== null || updatingSharingId !== null;
+ const saveDisabledHint =
+ savingId !== null
+ ? "Save is in progress"
+ : "Current Cap action is in progress";
+ const visibilityDisabledHint =
+ updatingSharingId !== null
+ ? "Sharing update is in progress"
+ : "Current Cap action is in progress";
+ const saveDisabledAccessibilityValue = savingCap
+ ? `Saving video for ${savingCap.title}`
+ : undefined;
+ const visibilityDisabledAccessibilityValue = updatingSharingCap
+ ? `Updating sharing for ${updatingSharingCap.title}`
+ : undefined;
+
+ if (auth.status === "loading") {
+ return ;
+ }
+
+ if (auth.status === "signedOut") {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ {auth.bootstrap ? (
+
+ {
+ setFolder(null);
+ await auth.setActiveOrganization(organizationId);
+ await load();
+ }}
+ />
+
+ ) : null}
+
+
+ router.push("/upload")}
+ disabled={creatingFolder}
+ size="sm"
+ style={styles.actionButton}
+ symbol="square.and.arrow.up"
+ variant="dark"
+ />
+
+ {folder ? (
+ setFolder(null)}
+ style={styles.folderCrumb}
+ >
+ My Caps
+
+
+
+
+
+ {folder.name}
+
+
+ ) : null}
+ {loadError ? (
+
+
+
+
+
+ Unable to load Caps
+ {loadError}
+
+
+
+ ) : null}
+ {loadError && !result ? null : (
+
+ item.type === "section"
+ ? `section-${item.id}`
+ : item.type === "folder"
+ ? `folder-${item.folder.id}`
+ : `cap-${item.cap.id}`
+ }
+ refreshControl={
+
+ }
+ showsVerticalScrollIndicator={false}
+ contentContainerStyle={styles.listContent}
+ getItemType={(item) => item.type}
+ ListEmptyComponent={
+
+
+
+
+
+
+
+
+
+ Hey{userName ? ` ${userName}` : ""}! Import your first Cap
+
+
+ Bring videos into Cap and share them instantly.
+
+
+ router.push("/upload")}
+ disabled={creatingFolder}
+ style={styles.emptyButton}
+ symbol="square.and.arrow.up"
+ variant="dark"
+ />
+
+
+ }
+ renderItem={({ item }) =>
+ item.type === "section" ? (
+
+ {item.title}
+
+ ) : item.type === "folder" ? (
+ setFolder(item.folder)}
+ style={({ pressed }) => [
+ styles.folderRow,
+ pressed ? styles.folderRowPressed : null,
+ ]}
+ >
+
+
+
+
+
+ {item.folder.name}
+
+
+ {item.folder.videoCount}{" "}
+ {item.folder.videoCount === 1 ? "video" : "videos"}
+
+
+
+
+ ) : (
+ viewAnalytics(item.cap)}
+ onCopyPress={() => copyCapLink(item.cap)}
+ onPress={() => router.push(`/caps/${item.cap.id}`)}
+ onSharePress={() => shareCapLink(item.cap)}
+ onVisibilityPress={() => showSharingActions(item.cap)}
+ onMenuPress={() => showCapSettings(item.cap)}
+ visibilityBusy={updatingSharingId === item.cap.id}
+ visibilityDisabled={updatingSharingId !== null}
+ visibilityDisabledHint={
+ updatingSharingId === item.cap.id
+ ? "Sharing update is in progress"
+ : "Another sharing update is in progress"
+ }
+ visibilityAccessibilityValue={
+ updatingSharingId === item.cap.id
+ ? `Updating sharing for ${item.cap.title}`
+ : undefined
+ }
+ />
+ )
+ }
+ />
+ )}
+ setSettingsCap(null)}
+ onCopyLink={copyCapLink}
+ onDelete={confirmDeleteCap}
+ onPassword={showPasswordActions}
+ onRename={showTitleActions}
+ onSaveVideo={(cap) => {
+ void saveCapVideo(cap);
+ }}
+ onShareLink={shareCapLink}
+ onViewAnalytics={viewAnalytics}
+ onVisibilityChange={(cap, isPublic) => {
+ void updateCapVisibility(cap, isPublic);
+ }}
+ saveDisabled={isLibraryActionInProgress}
+ saveDisabledHint={saveDisabledHint}
+ saveDisabledValue={savingId !== null ? undefined : "Unavailable"}
+ saveDisabledAccessibilityValue={saveDisabledAccessibilityValue}
+ visibilityDisabled={isLibraryActionInProgress}
+ visibilityDisabledHint={visibilityDisabledHint}
+ visibilityDisabledAccessibilityValue={
+ visibilityDisabledAccessibilityValue
+ }
+ />
+
+ );
+}
+
+const styles = StyleSheet.create({
+ topBar: {
+ marginBottom: 12,
+ },
+ actions: {
+ flexDirection: "row",
+ flexWrap: "wrap",
+ gap: 8,
+ marginBottom: 40,
+ },
+ actionButton: {
+ flexGrow: 1,
+ flexBasis: 104,
+ paddingHorizontal: 12,
+ },
+ listContent: {
+ paddingBottom: 22,
+ },
+ folderCrumb: {
+ minHeight: 40,
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 7,
+ marginBottom: 14,
+ },
+ folderCrumbText: {
+ fontFamily: fonts.medium,
+ color: colors.gray9,
+ fontSize: 20,
+ lineHeight: 26,
+ },
+ folderCrumbIcon: {
+ width: 24,
+ height: 24,
+ alignItems: "center",
+ justifyContent: "center",
+ },
+ folderCurrent: {
+ flex: 1,
+ fontFamily: fonts.medium,
+ color: colors.gray12,
+ fontSize: 20,
+ lineHeight: 26,
+ },
+ folderRow: {
+ minHeight: 82,
+ flexDirection: "row",
+ alignItems: "center",
+ borderRadius: radius.sm,
+ borderWidth: StyleSheet.hairlineWidth,
+ paddingHorizontal: 16,
+ paddingVertical: 16,
+ gap: 12,
+ marginBottom: 12,
+ backgroundColor: colors.gray3,
+ borderColor: colors.gray5,
+ ...squircle,
+ },
+ folderRowPressed: {
+ backgroundColor: colors.gray4,
+ borderColor: colors.gray6,
+ },
+ sectionHeader: {
+ paddingTop: 8,
+ paddingBottom: 24,
+ },
+ sectionTitle: {
+ fontFamily: fonts.medium,
+ fontSize: 24,
+ lineHeight: 30,
+ color: colors.gray12,
+ },
+ folderIcon: {
+ width: 50,
+ height: 50,
+ alignItems: "center",
+ justifyContent: "center",
+ },
+ folderText: {
+ flex: 1,
+ minWidth: 0,
+ },
+ folderName: {
+ fontFamily: fonts.regular,
+ fontSize: 15,
+ lineHeight: 22,
+ color: colors.gray12,
+ },
+ folderMeta: {
+ fontFamily: fonts.regular,
+ fontSize: 13,
+ lineHeight: 18,
+ color: colors.gray10,
+ },
+ errorCard: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 12,
+ backgroundColor: colors.gray1,
+ borderRadius: radius.md,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray3,
+ padding: 14,
+ marginBottom: 14,
+ ...squircle,
+ },
+ errorIcon: {
+ width: 36,
+ height: 36,
+ borderRadius: radius.full,
+ alignItems: "center",
+ justifyContent: "center",
+ backgroundColor: colors.gray3,
+ ...squircle,
+ },
+ errorCopy: {
+ flex: 1,
+ minWidth: 0,
+ },
+ errorTitle: {
+ fontFamily: fonts.medium,
+ fontSize: 15,
+ lineHeight: 20,
+ color: colors.gray12,
+ },
+ errorText: {
+ fontFamily: fonts.regular,
+ fontSize: 13,
+ lineHeight: 18,
+ color: colors.gray10,
+ marginTop: 2,
+ },
+ errorButton: {
+ paddingHorizontal: 14,
+ },
+ emptyState: {
+ alignItems: "center",
+ paddingTop: 42,
+ gap: 12,
+ paddingHorizontal: 8,
+ },
+ emptyArt: {
+ width: 180,
+ height: 112,
+ alignItems: "center",
+ justifyContent: "center",
+ marginBottom: 10,
+ },
+ emptyArtCard: {
+ position: "absolute",
+ width: 152,
+ height: 86,
+ borderRadius: radius.md,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray3,
+ backgroundColor: colors.gray1,
+ transform: [{ rotate: "-4deg" }],
+ ...squircle,
+ },
+ emptyArtCardBack: {
+ backgroundColor: colors.gray3,
+ borderColor: colors.gray4,
+ transform: [{ translateX: 12 }, { translateY: 7 }, { rotate: "5deg" }],
+ },
+ emptyLogo: {
+ width: 72,
+ height: 72,
+ borderRadius: radius.lg,
+ alignItems: "center",
+ justifyContent: "center",
+ backgroundColor: colors.white,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray3,
+ ...squircle,
+ },
+ emptyTitle: {
+ fontFamily: fonts.medium,
+ fontSize: 20,
+ color: colors.gray12,
+ textAlign: "center",
+ },
+ emptyText: {
+ fontFamily: fonts.regular,
+ fontSize: 15,
+ lineHeight: 22,
+ color: colors.gray10,
+ textAlign: "center",
+ },
+ emptyActions: {
+ width: "100%",
+ flexDirection: "row",
+ gap: 10,
+ marginTop: 4,
+ },
+ emptyButton: {
+ flex: 1,
+ },
+});
diff --git a/apps/mobile/app/(tabs)/upload.tsx b/apps/mobile/app/(tabs)/upload.tsx
new file mode 100644
index 00000000000..aa790e62a9e
--- /dev/null
+++ b/apps/mobile/app/(tabs)/upload.tsx
@@ -0,0 +1,1254 @@
+import * as DocumentPicker from "expo-document-picker";
+import * as ImagePicker from "expo-image-picker";
+import { router } from "expo-router";
+import { SymbolView } from "expo-symbols";
+import * as WebBrowser from "expo-web-browser";
+import { useEffect, useReducer, useRef, useState } from "react";
+import {
+ ActionSheetIOS,
+ ActivityIndicator,
+ Alert,
+ Linking,
+ Platform,
+ Pressable,
+ StyleSheet,
+ Text,
+ View,
+} from "react-native";
+import Svg, { Path } from "react-native-svg";
+import type { UploadFile } from "@/api/mobile";
+import { apiBaseUrl, useAuth } from "@/auth/AuthContext";
+import { SignInPanel } from "@/auth/SignInPanel";
+import { ActionButton } from "@/components/ActionButton";
+import { GlassSurface } from "@/components/GlassSurface";
+import { Screen } from "@/components/Screen";
+import { colors, fonts, radius, squircle } from "@/theme";
+import { contentTypeForUpload } from "@/uploads/fileTypes";
+import { runMobileUpload } from "@/uploads/runMobileUpload";
+import {
+ emptyUploadQueue,
+ isTerminalUploadQueueAction,
+ type UploadQueueItem,
+ uploadProgressPercent,
+ uploadQueueActionFromCapUpload,
+ uploadQueueReducer,
+ uploadQueueStatusText,
+} from "@/uploads/uploadQueue";
+import { formatDuration, formatFileSize } from "@/utils/format";
+
+const processingPollDelaysMs = [1500, 3000, 5000, 8000] as const;
+const photosAccessNeededMessage =
+ "Allow Cap to read videos from Photos before uploading.";
+const uploadAcceptedFormats = "MP4, MOV, AVI, MKV, WebM, or M4V";
+type UploadSourceLoading = "files" | "loom" | "photos" | null;
+type UploadSource = Exclude;
+type UploadSourceError = {
+ message: string;
+ source: UploadSource;
+};
+
+const queueItemFromFile = (
+ file: UploadFile,
+ organizationId: string | null,
+): Omit => ({
+ id: `${Date.now()}-${file.name}`,
+ localUri: file.uri,
+ fileName: file.name,
+ contentType: file.type,
+ size: file.size ?? 0,
+ durationSeconds: file.durationSeconds,
+ width: file.width,
+ height: file.height,
+ folderId: null,
+ organizationId,
+ status: "queued",
+ progress: 0,
+ error: null,
+ capId: null,
+ rawFileKey: null,
+ processingMessage: null,
+});
+
+const showPhotosSettingsAlert = () => {
+ if (Platform.OS === "ios") {
+ ActionSheetIOS.showActionSheetWithOptions(
+ {
+ cancelButtonIndex: 1,
+ message: photosAccessNeededMessage,
+ options: ["Open Settings", "Cancel"],
+ title: "Photos access needed",
+ tintColor: colors.blue11,
+ userInterfaceStyle: "light",
+ },
+ (index) => {
+ if (index === 0) void Linking.openSettings();
+ },
+ );
+ return;
+ }
+
+ Alert.alert("Photos access needed", photosAccessNeededMessage, [
+ { text: "Cancel", style: "cancel" },
+ {
+ text: "Open Settings",
+ onPress: () => {
+ void Linking.openSettings();
+ },
+ },
+ ]);
+};
+
+const getUploadSourceErrorMessage = (error: unknown, source: UploadSource) =>
+ error instanceof Error
+ ? error.message
+ : source === "loom"
+ ? "Unable to open Loom import"
+ : "Unable to open the picker";
+
+const uploadQueueMetadataText = (
+ item: UploadQueueItem,
+ statusText = uploadQueueStatusText(item),
+) => {
+ const failureReason =
+ item.status === "failed" && item.error?.trim() ? item.error.trim() : null;
+ return [
+ statusText,
+ failureReason,
+ formatFileSize(item.size),
+ formatDuration(item.durationSeconds ?? null),
+ ]
+ .filter(Boolean)
+ .join(" ยท ");
+};
+
+const uploadQueueMenuHint = (item: UploadQueueItem) => {
+ if (item.status === "failed") return "Opens retry and remove actions";
+ if (
+ (item.status === "processing" || item.status === "complete") &&
+ item.capId
+ ) {
+ return "Opens view and remove actions";
+ }
+ return "Opens remove action";
+};
+
+const uploadQueueHasProgress = (item: UploadQueueItem) =>
+ item.status === "uploading" ||
+ item.status === "processing" ||
+ item.status === "complete";
+
+const progressAccessibilityValue = (percent: number) => ({
+ max: 100,
+ min: 0,
+ now: percent,
+ text: `${percent}%`,
+});
+
+const LoomMark = () => (
+
+);
+
+const idleLoomImportLabel = "Import from Loom";
+
+export default function UploadScreen() {
+ const auth = useAuth();
+ const [queue, dispatch] = useReducer(uploadQueueReducer, emptyUploadQueue);
+ const [activeId, setActiveId] = useState(null);
+ const [activeUploadName, setActiveUploadName] = useState(null);
+ const [sourceError, setSourceError] = useState(
+ null,
+ );
+ const [sourceLoading, setSourceLoading] = useState(null);
+ const mountedRef = useRef(true);
+ const activeIdRef = useRef(null);
+ const sourceBusyRef = useRef(false);
+ const uploadSourceError =
+ sourceError?.source === "files" || sourceError?.source === "photos"
+ ? sourceError.message
+ : null;
+ const loomImportError =
+ sourceError?.source === "loom" ? sourceError.message : null;
+ const activeItem = activeId
+ ? (queue.items.find((item) => item.id === activeId) ?? null)
+ : null;
+ const activeUploadFileName = activeItem?.fileName ?? activeUploadName;
+ const activeUploadPreparing =
+ activeId !== null &&
+ (activeItem === null || activeItem.status === "queued");
+ const activeProgress =
+ activeItem !== null && uploadQueueHasProgress(activeItem)
+ ? uploadProgressPercent(activeItem.progress)
+ : null;
+ const activeUploadHint = activeUploadPreparing
+ ? "Preparing upload"
+ : "Upload is in progress";
+ const uploadSourceBusy = activeId !== null || sourceLoading !== null;
+ const sourcePending =
+ sourceLoading !== null && sourceLoading !== "loom" && activeId === null;
+ const sourceLoadingTitle =
+ sourcePending && sourceLoading === "files"
+ ? "Opening Files"
+ : sourcePending && sourceLoading === "photos"
+ ? "Opening Photos"
+ : null;
+ const sourceLoadingSubtitle =
+ sourcePending && sourceLoading === "files"
+ ? "Choose a video from Files."
+ : sourcePending && sourceLoading === "photos"
+ ? "Choose a video from Photos."
+ : null;
+ const sourceLoadingAccessibilityText =
+ sourceLoading === "files"
+ ? "Opening native file picker"
+ : sourceLoading === "photos"
+ ? "Opening native photo picker"
+ : sourceLoading === "loom"
+ ? "Opening Loom import"
+ : null;
+ const importTitle = sourceLoadingTitle ?? "Upload File";
+ const importSubtitle =
+ uploadSourceError ??
+ sourceLoadingSubtitle ??
+ (activeId !== null
+ ? activeUploadPreparing
+ ? "Preparing your video for upload."
+ : "Keep Cap open while your video uploads."
+ : "Upload a video file from your device");
+ const loomImportTitle = loomImportError
+ ? "Loom import unavailable"
+ : sourceLoading === "loom"
+ ? "Opening Loom"
+ : idleLoomImportLabel;
+ const loomImportSubtitle =
+ loomImportError ??
+ (sourceLoading === "loom"
+ ? "Continue in the browser sheet to import from Loom."
+ : activeId !== null
+ ? activeUploadPreparing
+ ? "Finish preparing this upload before importing from Loom."
+ : "Finish the current upload before importing from Loom."
+ : "Import a Loom share link or bulk import from CSV");
+ const activeUploadAccessibilityLabel = activeUploadFileName
+ ? activeUploadPreparing
+ ? `Preparing upload ${activeUploadFileName}`
+ : activeProgress !== null
+ ? `Uploading ${activeUploadFileName} ${activeProgress}%`
+ : `Uploading ${activeUploadFileName}`
+ : null;
+ const activeUploadAccessibilityValue = activeUploadAccessibilityLabel
+ ? { text: activeUploadAccessibilityLabel }
+ : undefined;
+ const showUploadFormats =
+ !uploadSourceError && !sourcePending && activeId === null;
+ const uploadSourceAccessibilityLabel = sourcePending
+ ? (sourceLoadingTitle ?? "Upload source opening")
+ : uploadSourceError
+ ? "Upload source unavailable"
+ : "Choose upload source";
+ const loomImportAccessibilityLabel = loomImportError
+ ? "Loom import unavailable"
+ : sourceLoading === "loom"
+ ? loomImportTitle
+ : "Open Loom import";
+ const uploadSourceAccessibilityValue = uploadSourceError
+ ? { text: uploadSourceError }
+ : sourcePending && sourceLoadingAccessibilityText
+ ? { text: sourceLoadingAccessibilityText }
+ : sourceLoading === "loom" && sourceLoadingAccessibilityText
+ ? { text: sourceLoadingAccessibilityText }
+ : (activeUploadAccessibilityValue ?? { text: uploadAcceptedFormats });
+ const loomImportAccessibilityValue = loomImportError
+ ? { text: loomImportError }
+ : sourceLoading !== null && sourceLoadingAccessibilityText
+ ? { text: sourceLoadingAccessibilityText }
+ : activeUploadAccessibilityValue;
+ const sourceOpeningHint =
+ sourceLoading === "loom"
+ ? "Loom import is opening"
+ : "Upload source picker is opening";
+ const uploadSourceActionHint = (
+ source: Exclude,
+ idleHint: string,
+ ) => {
+ if (activeId !== null) return activeUploadHint;
+ if (sourceLoading === source) return sourceOpeningHint;
+ if (sourceLoading !== null) {
+ return sourceLoading === "loom"
+ ? "Loom import is opening"
+ : "Another upload source is opening";
+ }
+ if (sourceError?.source === source) return sourceError.message;
+ return idleHint;
+ };
+ const uploadSourceActionValue = (
+ source: Exclude,
+ ) => {
+ if (sourceLoading !== null && sourceLoadingAccessibilityText) {
+ return { text: sourceLoadingAccessibilityText };
+ }
+ if (sourceError?.source === source) return { text: sourceError.message };
+ if (activeId !== null) {
+ return activeUploadAccessibilityValue;
+ }
+ return undefined;
+ };
+ const browseFilesLabel =
+ sourceError?.source === "files" ? "Retry Files" : "Browse Files";
+ const photosLabel =
+ sourceError?.source === "photos" ? "Retry Photos" : "Photos";
+ const loomActionLabel =
+ sourceError?.source === "loom" ? "Retry Loom" : "Loom";
+ const loomActionAccessibilityLabel =
+ sourceError?.source === "loom" ? undefined : idleLoomImportLabel;
+ const uploadSourceCardBusy = activeId !== null || sourcePending;
+
+ useEffect(
+ () => () => {
+ mountedRef.current = false;
+ },
+ [],
+ );
+
+ const dispatchIfMounted = (action: Parameters[0]) => {
+ if (mountedRef.current) dispatch(action);
+ };
+
+ const setActiveUploadId = (id: string | null, fileName?: string) => {
+ activeIdRef.current = id;
+ setActiveId(id);
+ setActiveUploadName(id ? (fileName ?? null) : null);
+ };
+
+ const isUploadSourceBusy = () =>
+ sourceBusyRef.current || activeIdRef.current !== null;
+
+ const beginUploadSource = (source: UploadSource) => {
+ if (isUploadSourceBusy()) return false;
+ sourceBusyRef.current = true;
+ setSourceError(null);
+ setSourceLoading(source);
+ return true;
+ };
+
+ const endUploadSource = () => {
+ sourceBusyRef.current = false;
+ setSourceLoading(null);
+ };
+
+ const waitForProcessing = async (queueItemId: string, capId: string) => {
+ if (auth.status !== "signedIn") return;
+
+ for (const delayMs of processingPollDelaysMs) {
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
+ if (!mountedRef.current) return;
+
+ try {
+ const detail = await auth.client.getCap(capId);
+ const action = uploadQueueActionFromCapUpload(
+ queueItemId,
+ detail.cap.upload,
+ );
+ if (action) {
+ dispatchIfMounted(action);
+ if (isTerminalUploadQueueAction(action)) {
+ if (action.type === "complete") {
+ await auth.refresh().catch(() => undefined);
+ }
+ return;
+ }
+ }
+ } catch {
+ return;
+ }
+ }
+ };
+
+ const uploadQueueItem = async (
+ item: Omit | UploadQueueItem,
+ file: UploadFile,
+ ) => {
+ if (auth.status !== "signedIn") return;
+
+ setActiveUploadId(item.id, item.fileName);
+ try {
+ const created = await runMobileUpload({
+ client: auth.client,
+ file,
+ organizationId:
+ item.organizationId ?? auth.bootstrap?.activeOrganizationId,
+ folderId: item.folderId,
+ onCreated: (capId, rawFileKey) =>
+ dispatch({
+ type: "start",
+ id: item.id,
+ capId,
+ rawFileKey,
+ }),
+ onProgress: (progress) =>
+ dispatch({ type: "progress", id: item.id, progress }),
+ });
+ dispatch({ type: "processing", id: item.id, progress: 0 });
+ await auth.refresh().catch(() => undefined);
+ void waitForProcessing(item.id, created.id);
+ } catch (error) {
+ dispatch({
+ type: "fail",
+ id: item.id,
+ error: error instanceof Error ? error.message : "Upload failed",
+ });
+ } finally {
+ setActiveUploadId(null);
+ }
+ };
+
+ const uploadFile = async (file: UploadFile) => {
+ if (auth.status !== "signedIn") return;
+ setSourceError(null);
+
+ const item = queueItemFromFile(
+ file,
+ auth.bootstrap?.activeOrganizationId ?? null,
+ );
+ dispatch({ type: "enqueue", item });
+ await uploadQueueItem(item, file);
+ };
+
+ const pickFile = async () => {
+ if (!beginUploadSource("files")) return;
+ try {
+ const result = await DocumentPicker.getDocumentAsync({
+ type: "video/*",
+ copyToCacheDirectory: true,
+ });
+ if (result.canceled || !result.assets[0]) return;
+ const asset = result.assets[0];
+ endUploadSource();
+ await uploadFile({
+ uri: asset.uri,
+ name: asset.name,
+ type: contentTypeForUpload(asset.name, asset.mimeType),
+ size: asset.size,
+ });
+ } catch (error) {
+ setSourceError({
+ message: getUploadSourceErrorMessage(error, "files"),
+ source: "files",
+ });
+ } finally {
+ endUploadSource();
+ }
+ };
+
+ const pickPhoto = async () => {
+ if (!beginUploadSource("photos")) return;
+ try {
+ const permission =
+ await ImagePicker.requestMediaLibraryPermissionsAsync();
+ if (!permission.granted) {
+ setSourceError({
+ message: photosAccessNeededMessage,
+ source: "photos",
+ });
+ showPhotosSettingsAlert();
+ return;
+ }
+ const result = await ImagePicker.launchImageLibraryAsync({
+ mediaTypes: ["videos"],
+ allowsEditing: false,
+ });
+ if (result.canceled || !result.assets[0]) return;
+ const asset = result.assets[0];
+ const name = asset.fileName ?? `Cap Upload ${Date.now()}.mov`;
+ endUploadSource();
+ await uploadFile({
+ uri: asset.uri,
+ name,
+ type: contentTypeForUpload(name, asset.mimeType),
+ size: asset.fileSize,
+ durationSeconds:
+ typeof asset.duration === "number" && asset.duration > 0
+ ? asset.duration / 1000
+ : undefined,
+ width: asset.width > 0 ? asset.width : undefined,
+ height: asset.height > 0 ? asset.height : undefined,
+ });
+ } catch (error) {
+ setSourceError({
+ message: getUploadSourceErrorMessage(error, "photos"),
+ source: "photos",
+ });
+ } finally {
+ endUploadSource();
+ }
+ };
+
+ const retry = async (item: UploadQueueItem) => {
+ if (activeIdRef.current !== null) return;
+ dispatch({ type: "retry", id: item.id });
+ await uploadQueueItem(item, {
+ uri: item.localUri,
+ name: item.fileName,
+ type: item.contentType,
+ size: item.size,
+ durationSeconds: item.durationSeconds,
+ width: item.width,
+ height: item.height,
+ });
+ };
+
+ const viewCap = (capId: string | null) => {
+ if (activeIdRef.current !== null) return;
+ if (!capId) return;
+ router.push(`/caps/${capId}`);
+ };
+
+ const removeQueueItem = (item: UploadQueueItem) => {
+ if (activeIdRef.current !== null) return;
+ dispatch({ type: "remove", id: item.id });
+ };
+
+ const showQueueItemActions = (item: UploadQueueItem) => {
+ if (activeIdRef.current !== null) return;
+ const actions: Array<{
+ label: string;
+ destructive?: boolean;
+ onPress: () => void;
+ }> = [];
+
+ if (item.status === "failed") {
+ actions.push({
+ label: "Retry",
+ onPress: () => {
+ void retry(item);
+ },
+ });
+ }
+
+ if (
+ (item.status === "processing" || item.status === "complete") &&
+ item.capId
+ ) {
+ actions.push({
+ label: "View",
+ onPress: () => viewCap(item.capId),
+ });
+ }
+
+ actions.push({
+ label: "Remove from Queue",
+ destructive: true,
+ onPress: () => removeQueueItem(item),
+ });
+
+ if (Platform.OS === "ios") {
+ const cancelButtonIndex = actions.length;
+ const destructiveButtonIndex = actions.findIndex(
+ (action) => action.destructive,
+ );
+ ActionSheetIOS.showActionSheetWithOptions(
+ {
+ cancelButtonIndex,
+ destructiveButtonIndex:
+ destructiveButtonIndex >= 0 ? destructiveButtonIndex : undefined,
+ message: uploadQueueMetadataText(item),
+ options: [...actions.map((action) => action.label), "Cancel"],
+ title: item.fileName,
+ tintColor: colors.blue11,
+ userInterfaceStyle: "light",
+ },
+ (index) => {
+ actions[index]?.onPress();
+ },
+ );
+ return;
+ }
+
+ Alert.alert(item.fileName, uploadQueueMetadataText(item), [
+ ...actions.map((action) => ({
+ text: action.label,
+ style: action.destructive ? ("destructive" as const) : undefined,
+ onPress: action.onPress,
+ })),
+ { text: "Cancel", style: "cancel" },
+ ]);
+ };
+
+ const showUploadSources = () => {
+ if (isUploadSourceBusy()) return;
+
+ if (Platform.OS === "ios") {
+ ActionSheetIOS.showActionSheetWithOptions(
+ {
+ options: ["Browse Files", "Photos", idleLoomImportLabel, "Cancel"],
+ cancelButtonIndex: 3,
+ message: uploadAcceptedFormats,
+ title: "Upload File",
+ tintColor: colors.blue11,
+ userInterfaceStyle: "light",
+ },
+ (index) => {
+ if (index === 0) void pickFile();
+ if (index === 1) void pickPhoto();
+ if (index === 2) void openLoomImport();
+ },
+ );
+ return;
+ }
+
+ Alert.alert("Upload File", "Choose a video source.", [
+ { text: "Browse Files", onPress: () => void pickFile() },
+ { text: "Photos", onPress: () => void pickPhoto() },
+ { text: idleLoomImportLabel, onPress: () => void openLoomImport() },
+ { text: "Cancel", style: "cancel" },
+ ]);
+ };
+
+ const openLoomImport = async () => {
+ if (!beginUploadSource("loom")) return;
+
+ try {
+ const url = new URL("/dashboard/import/loom", apiBaseUrl);
+ await WebBrowser.openBrowserAsync(url.toString());
+ } catch (error) {
+ setSourceError({
+ message: getUploadSourceErrorMessage(error, "loom"),
+ source: "loom",
+ });
+ } finally {
+ endUploadSource();
+ }
+ };
+
+ if (auth.status === "loading") {
+ return ;
+ }
+
+ if (auth.status === "signedOut") {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+ [
+ styles.importPressable,
+ uploadSourceBusy && styles.importPressableDisabled,
+ pressed && !uploadSourceBusy && styles.importPressablePressed,
+ ]}
+ >
+
+
+ {uploadSourceCardBusy ? (
+
+ ) : uploadSourceError ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ {uploadSourceError ? "Upload source unavailable" : importTitle}
+
+
+ {importSubtitle}
+
+ {showUploadFormats ? (
+ {uploadAcceptedFormats}
+ ) : null}
+ {activeProgress !== null ? (
+
+
+
+ ) : null}
+
+
+
+
+
+
+ {
+ void openLoomImport();
+ }}
+ variant="gray"
+ loading={sourceLoading === "loom"}
+ disabled={uploadSourceBusy && sourceLoading !== "loom"}
+ style={styles.actionButton}
+ size="sm"
+ leading={}
+ />
+
+
+
+ {
+ void openLoomImport();
+ }}
+ style={({ pressed }) => [
+ styles.importPressable,
+ uploadSourceBusy && styles.importPressableDisabled,
+ pressed && !uploadSourceBusy && styles.importPressablePressed,
+ ]}
+ >
+
+
+ {loomImportError ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {loomImportTitle}
+
+ {loomImportSubtitle}
+
+
+
+
+
+
+ Queue
+ {queue.items.length === 0 ? (
+
+
+ No uploads yet
+
+ ) : (
+
+ {queue.items
+ .slice()
+ .reverse()
+ .map((item, index, items) => {
+ const isActiveQueueItem = activeId === item.id;
+ const queueActionsDisabled = activeId !== null;
+ const queueProgress = uploadProgressPercent(item.progress);
+ const showQueueProgress = uploadQueueHasProgress(item);
+ const queueStatus = uploadQueueStatusText(item);
+ const queueDisplayStatus =
+ isActiveQueueItem && item.status === "queued"
+ ? "Preparing upload"
+ : queueStatus;
+ const queueMetadata = uploadQueueMetadataText(
+ item,
+ queueDisplayStatus,
+ );
+ const queueAccessibilityValue =
+ queueActionsDisabled && activeUploadAccessibilityValue
+ ? activeUploadAccessibilityValue
+ : { text: queueMetadata };
+ const queueHint = isActiveQueueItem
+ ? item.status === "queued"
+ ? "Preparing upload"
+ : "Upload is in progress"
+ : queueActionsDisabled
+ ? "Another upload is in progress"
+ : `${queueStatus}. Opens upload actions`;
+ const queueMenuHint = queueActionsDisabled
+ ? queueHint
+ : uploadQueueMenuHint(item);
+ return (
+
+ showQueueItemActions(item)}
+ onPress={() => showQueueItemActions(item)}
+ style={({ pressed }) => [
+ styles.queueItem,
+ queueActionsDisabled &&
+ !isActiveQueueItem &&
+ styles.queueItemDisabled,
+ pressed && activeId === null && styles.queueItemPressed,
+ ]}
+ >
+
+
+ {item.fileName}
+
+ {queueMetadata}
+ {showQueueProgress ? (
+
+
+
+ ) : null}
+ {item.error ? (
+
+ {item.error}
+
+ ) : null}
+
+ {item.status === "failed" ? (
+ {
+ event?.stopPropagation();
+ void retry(item);
+ }}
+ disabled={queueActionsDisabled}
+ size="sm"
+ style={styles.viewButton}
+ symbol="arrow.clockwise"
+ variant="secondary"
+ />
+ ) : (item.status === "processing" ||
+ item.status === "complete") &&
+ item.capId ? (
+ {
+ event?.stopPropagation();
+ viewCap(item.capId);
+ }}
+ disabled={queueActionsDisabled}
+ size="sm"
+ style={styles.viewButton}
+ symbol="play.rectangle"
+ variant="secondary"
+ />
+ ) : null}
+ {
+ event.stopPropagation();
+ if (queueActionsDisabled) return;
+ showQueueItemActions(item);
+ }}
+ style={({ pressed }) => [
+ styles.queueMenuButton,
+ queueActionsDisabled &&
+ styles.queueMenuButtonDisabled,
+ pressed &&
+ !queueActionsDisabled &&
+ styles.queueMenuButtonPressed,
+ ]}
+ >
+
+
+
+ {index < items.length - 1 ? (
+
+ ) : null}
+
+ );
+ })}
+
+ )}
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ importCard: {
+ borderRadius: radius.md,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray3,
+ overflow: "hidden",
+ marginBottom: 20,
+ ...squircle,
+ },
+ importCardFallback: {
+ backgroundColor: colors.gray1,
+ },
+ importPressable: {
+ width: "100%",
+ },
+ importPressablePressed: {
+ backgroundColor: colors.gray2,
+ },
+ importPressableDisabled: {
+ opacity: 0.58,
+ },
+ importPreview: {
+ height: 128,
+ alignItems: "center",
+ justifyContent: "center",
+ backgroundColor: colors.gray3,
+ },
+ importIcon: {
+ width: 56,
+ height: 56,
+ borderRadius: radius.full,
+ alignItems: "center",
+ justifyContent: "center",
+ backgroundColor: colors.gray1,
+ ...squircle,
+ },
+ importBody: {
+ padding: 16,
+ },
+ importCopy: {
+ gap: 4,
+ },
+ importTitle: {
+ fontFamily: fonts.medium,
+ fontSize: 14,
+ lineHeight: 20,
+ color: colors.gray12,
+ },
+ importSubtitle: {
+ fontFamily: fonts.regular,
+ fontSize: 12,
+ lineHeight: 16,
+ color: colors.gray10,
+ },
+ importMeta: {
+ fontFamily: fonts.regular,
+ fontSize: 12,
+ lineHeight: 16,
+ color: colors.gray9,
+ },
+ importErrorSubtitle: {
+ color: colors.red9,
+ },
+ importProgressTrack: {
+ height: 5,
+ borderRadius: radius.full,
+ backgroundColor: colors.gray4,
+ overflow: "hidden",
+ marginTop: 10,
+ ...squircle,
+ },
+ importProgressFill: {
+ height: "100%",
+ borderRadius: radius.full,
+ backgroundColor: colors.buttonBlue,
+ },
+ actions: {
+ flexDirection: "row",
+ gap: 10,
+ width: "100%",
+ borderTopWidth: StyleSheet.hairlineWidth,
+ borderTopColor: colors.gray3,
+ padding: 12,
+ },
+ actionButton: {
+ flex: 1,
+ },
+ queue: {
+ gap: 10,
+ },
+ sectionTitle: {
+ fontFamily: fonts.medium,
+ fontSize: 18,
+ color: colors.gray12,
+ },
+ empty: {
+ height: 124,
+ borderRadius: radius.md,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray3,
+ backgroundColor: colors.gray1,
+ alignItems: "center",
+ justifyContent: "center",
+ gap: 8,
+ ...squircle,
+ },
+ emptyText: {
+ fontFamily: fonts.medium,
+ color: colors.gray10,
+ },
+ queueGroup: {
+ borderRadius: radius.md,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray3,
+ overflow: "hidden",
+ ...squircle,
+ },
+ queueGroupFallback: {
+ backgroundColor: colors.gray1,
+ },
+ queueItem: {
+ minHeight: 78,
+ padding: 12,
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 10,
+ },
+ queueItemPressed: {
+ backgroundColor: colors.gray2,
+ },
+ queueItemDisabled: {
+ opacity: 0.58,
+ },
+ queueSeparator: {
+ height: StyleSheet.hairlineWidth,
+ backgroundColor: colors.gray4,
+ marginLeft: 12,
+ },
+ queueText: {
+ flex: 1,
+ minWidth: 0,
+ gap: 3,
+ },
+ fileName: {
+ fontFamily: fonts.medium,
+ fontSize: 16,
+ color: colors.gray12,
+ },
+ fileMeta: {
+ fontFamily: fonts.regular,
+ fontSize: 13,
+ color: colors.gray10,
+ },
+ errorText: {
+ fontFamily: fonts.regular,
+ fontSize: 12,
+ color: colors.red9,
+ },
+ viewButton: {
+ width: 88,
+ },
+ queueMenuButton: {
+ width: 42,
+ height: 42,
+ borderRadius: radius.full,
+ alignItems: "center",
+ justifyContent: "center",
+ backgroundColor: colors.gray2,
+ ...squircle,
+ },
+ queueMenuButtonPressed: {
+ backgroundColor: colors.gray4,
+ },
+ queueMenuButtonDisabled: {
+ backgroundColor: colors.gray3,
+ },
+ progressTrack: {
+ height: 4,
+ borderRadius: radius.full,
+ backgroundColor: colors.gray4,
+ overflow: "hidden",
+ marginTop: 5,
+ },
+ progressFill: {
+ height: "100%",
+ borderRadius: radius.full,
+ backgroundColor: colors.buttonBlue,
+ },
+});
diff --git a/apps/mobile/app/_layout.tsx b/apps/mobile/app/_layout.tsx
new file mode 100644
index 00000000000..b5794d72fac
--- /dev/null
+++ b/apps/mobile/app/_layout.tsx
@@ -0,0 +1,110 @@
+import "react-native-gesture-handler";
+import "react-native-reanimated";
+
+import { useFonts } from "expo-font";
+import { Stack, useSegments } from "expo-router";
+import {
+ ActivityIndicator,
+ KeyboardAvoidingView,
+ Platform,
+ ScrollView,
+ StatusBar,
+ StyleSheet,
+ View,
+} from "react-native";
+import { GestureHandlerRootView } from "react-native-gesture-handler";
+import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context";
+import { AuthProvider, useAuth } from "@/auth/AuthContext";
+import { SignInPanel } from "@/auth/SignInPanel";
+import { signInTitleForSegments } from "@/auth/signInDestination";
+import { colors } from "@/theme";
+
+function AppShell() {
+ const auth = useAuth();
+ const segments = useSegments();
+
+ if (auth.status === "loading") {
+ return (
+
+
+
+ );
+ }
+
+ if (auth.status === "signedOut") {
+ return (
+
+
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ );
+}
+
+export default function RootLayout() {
+ const [fontsLoaded] = useFonts({
+ "NeueMontreal-Regular": require("../../web/public/fonts/NeueMontreal-Regular.otf"),
+ "NeueMontreal-Medium": require("../../web/public/fonts/NeueMontreal-Medium.otf"),
+ "NeueMontreal-Bold": require("../../web/public/fonts/NeueMontreal-Bold.otf"),
+ });
+
+ if (!fontsLoaded) return null;
+
+ return (
+
+
+
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ loadingScreen: {
+ flex: 1,
+ alignItems: "center",
+ justifyContent: "center",
+ backgroundColor: colors.appBackground,
+ },
+ authScreen: {
+ flex: 1,
+ backgroundColor: colors.appBackground,
+ },
+ authKeyboard: {
+ flex: 1,
+ },
+ authScroll: {
+ flex: 1,
+ },
+ authContent: {
+ flexGrow: 1,
+ justifyContent: "center",
+ paddingHorizontal: 20,
+ paddingVertical: 28,
+ },
+});
diff --git a/apps/mobile/app/caps/[id].tsx b/apps/mobile/app/caps/[id].tsx
new file mode 100644
index 00000000000..4fb96c80900
--- /dev/null
+++ b/apps/mobile/app/caps/[id].tsx
@@ -0,0 +1,1105 @@
+import * as Clipboard from "expo-clipboard";
+import { router, Stack, useLocalSearchParams } from "expo-router";
+import { type SFSymbol, SymbolView } from "expo-symbols";
+import { useVideoPlayer, VideoView } from "expo-video";
+import * as WebBrowser from "expo-web-browser";
+import { useCallback, useEffect, useMemo, useState } from "react";
+import {
+ ActionSheetIOS,
+ Alert,
+ KeyboardAvoidingView,
+ Linking,
+ Platform,
+ Pressable,
+ Share,
+ StyleSheet,
+ Text,
+ TextInput,
+ View,
+} from "react-native";
+import type { MobileCapDetail, MobilePlaybackResponse } from "@/api/mobile";
+import { apiBaseUrl, useAuth } from "@/auth/AuthContext";
+import { SignInPanel } from "@/auth/SignInPanel";
+import { CapSettingsSheet } from "@/caps/CapSettingsSheet";
+import { showCapPasswordActions } from "@/caps/passwordActions";
+import {
+ PhotosPermissionDeniedError,
+ saveCapVideoToPhotos,
+} from "@/caps/saveCapVideo";
+import { showCapTitleActions } from "@/caps/titleActions";
+import { ActionButton } from "@/components/ActionButton";
+import { GlassSurface } from "@/components/GlassSurface";
+import { Screen } from "@/components/Screen";
+import { colors, fonts, radius, squircle } from "@/theme";
+import { formatRelativeDate } from "@/utils/format";
+
+const showPhotosSettingsAlert = () => {
+ if (Platform.OS === "ios") {
+ ActionSheetIOS.showActionSheetWithOptions(
+ {
+ cancelButtonIndex: 1,
+ message: "Allow Cap to save videos to Photos from Settings.",
+ options: ["Open Settings", "Cancel"],
+ title: "Photos access needed",
+ tintColor: colors.blue11,
+ userInterfaceStyle: "light",
+ },
+ (index) => {
+ if (index === 0) void Linking.openSettings();
+ },
+ );
+ return;
+ }
+
+ Alert.alert(
+ "Photos access needed",
+ "Allow Cap to save videos to Photos from Settings.",
+ [
+ { text: "Cancel", style: "cancel" },
+ {
+ text: "Open Settings",
+ onPress: () => {
+ void Linking.openSettings();
+ },
+ },
+ ],
+ );
+};
+
+const getCapDetailErrorMessage = (error: unknown) =>
+ error instanceof Error ? error.message : "Unable to load this Cap";
+
+type CapDetailOperation = "comment" | "save" | "visibility";
+
+type AnalyticsMetricProps = {
+ symbol: SFSymbol;
+ value: number;
+};
+
+function AnalyticsMetric({ symbol, value }: AnalyticsMetricProps) {
+ return (
+
+
+ {value}
+
+ );
+}
+
+export default function CapDetailScreen() {
+ const { id } = useLocalSearchParams<{ id: string }>();
+ const auth = useAuth();
+ const [detail, setDetail] = useState(null);
+ const [playback, setPlayback] = useState(null);
+ const [comment, setComment] = useState("");
+ const [loading, setLoading] = useState(true);
+ const [activeOperation, setActiveOperation] =
+ useState(null);
+ const [loadError, setLoadError] = useState(null);
+ const [copied, setCopied] = useState(false);
+ const [saved, setSaved] = useState(false);
+ const [settingsVisible, setSettingsVisible] = useState(false);
+ const player = useVideoPlayer(null);
+
+ const load = useCallback(async () => {
+ if (auth.status !== "signedIn" || typeof id !== "string") return;
+ setLoading(true);
+ setLoadError(null);
+ try {
+ const [nextDetail, nextPlayback] = await Promise.all([
+ auth.client.getCap(id),
+ auth.client.getPlayback(id),
+ ]);
+ setDetail(nextDetail);
+ setPlayback(nextPlayback);
+ } catch (error) {
+ setDetail(null);
+ setPlayback(null);
+ setLoadError(getCapDetailErrorMessage(error));
+ } finally {
+ setLoading(false);
+ }
+ }, [auth, id]);
+
+ useEffect(() => {
+ load().catch(() => {});
+ }, [load]);
+
+ useEffect(() => {
+ if (!playback?.url) return;
+ player.replace(playback.url);
+ }, [playback?.url, player]);
+
+ useEffect(() => {
+ if (!copied) return;
+ const timeout = setTimeout(() => setCopied(false), 1600);
+ return () => clearTimeout(timeout);
+ }, [copied]);
+
+ useEffect(() => {
+ if (!saved) return;
+ const timeout = setTimeout(() => setSaved(false), 1600);
+ return () => clearTimeout(timeout);
+ }, [saved]);
+
+ const textComments = useMemo(
+ () => detail?.comments.filter((item) => item.type === "text") ?? [],
+ [detail],
+ );
+ const reactions = useMemo(
+ () => detail?.comments.filter((item) => item.type === "emoji") ?? [],
+ [detail],
+ );
+ const isActionInProgress = activeOperation !== null;
+ const isPostingComment = activeOperation === "comment";
+ const isSavingVideo = activeOperation === "save";
+ const isUpdatingVisibility = activeOperation === "visibility";
+ const actionInProgressHint = "Current Cap action is in progress";
+ const saveVideoLabel = saved ? "Saved" : "Save video";
+ const saveVideoAccessibilityText =
+ isSavingVideo && detail
+ ? `Saving video for ${detail.cap.title}`
+ : saved && detail
+ ? `Saved video for ${detail.cap.title}`
+ : undefined;
+ const saveVideoAccessibilityLabel = saved
+ ? saveVideoAccessibilityText
+ : undefined;
+ const saveVideoAccessibilityValue =
+ isSavingVideo && saveVideoAccessibilityText
+ ? { text: saveVideoAccessibilityText }
+ : undefined;
+ const saveVideoHint = isSavingVideo
+ ? "Save is in progress"
+ : isActionInProgress
+ ? actionInProgressHint
+ : "Saves this video to Photos";
+ const sharingStatusHint = isUpdatingVisibility
+ ? "Sharing update is in progress"
+ : isActionInProgress
+ ? actionInProgressHint
+ : "Opens sharing settings";
+ const sharingStatusLabel = detail?.cap.public ? "Shared" : "Not shared";
+ const sharingStatusAccessibilityValue =
+ isUpdatingVisibility && detail
+ ? `Updating sharing for ${detail.cap.title}`
+ : undefined;
+ const commentHint = isPostingComment
+ ? "Comment is being sent"
+ : isActionInProgress
+ ? actionInProgressHint
+ : "Add a comment to this Cap";
+ const sendCommentHint = isPostingComment
+ ? "Comment is being sent"
+ : isActionInProgress
+ ? actionInProgressHint
+ : comment.trim().length > 0
+ ? "Adds this comment"
+ : "Enter a comment before sending";
+ const sendCommentLabel = isPostingComment ? "Sending..." : "Send";
+ const sendCommentAccessibilityLabel =
+ isPostingComment && detail
+ ? `Sending comment on ${detail.cap.title}`
+ : "Send comment";
+ const canSendComment = comment.trim().length > 0 && !isActionInProgress;
+
+ const createComment = async () => {
+ const trimmed = comment.trim();
+ if (!trimmed || !detail || isActionInProgress) return;
+ setActiveOperation("comment");
+ try {
+ const created = await auth.client.createComment(detail.cap.id, {
+ content: trimmed,
+ timestamp: null,
+ });
+ setDetail({
+ ...detail,
+ comments: [...detail.comments, created],
+ cap: {
+ ...detail.cap,
+ commentCount: detail.cap.commentCount + 1,
+ },
+ });
+ setComment("");
+ } catch (error) {
+ Alert.alert(
+ "Comment failed",
+ error instanceof Error ? error.message : "Unable to add that comment.",
+ );
+ } finally {
+ setActiveOperation(null);
+ }
+ };
+
+ const createReaction = async (emoji: string) => {
+ if (!detail) return;
+ try {
+ const created = await auth.client.createReaction(detail.cap.id, {
+ content: emoji,
+ timestamp: null,
+ });
+ setDetail({
+ ...detail,
+ comments: [...detail.comments, created],
+ cap: {
+ ...detail.cap,
+ reactionCount: detail.cap.reactionCount + 1,
+ },
+ });
+ } catch (error) {
+ Alert.alert(
+ "Reaction failed",
+ error instanceof Error ? error.message : "Unable to add that reaction.",
+ );
+ }
+ };
+
+ const copyLink = async () => {
+ if (!detail) return;
+ try {
+ await Clipboard.setStringAsync(detail.shareUrl);
+ setCopied(true);
+ } catch (error) {
+ Alert.alert(
+ "Copy failed",
+ error instanceof Error ? error.message : "Unable to copy this link.",
+ );
+ }
+ };
+
+ const shareLink = async () => {
+ if (!detail) return;
+ await Share.share({ url: detail.shareUrl, message: detail.shareUrl });
+ };
+
+ const updateVisibility = async (isPublic: boolean) => {
+ if (!detail || isActionInProgress) return;
+ setActiveOperation("visibility");
+ try {
+ const cap = await auth.client.updateCapSharing(detail.cap.id, {
+ public: isPublic,
+ });
+ setDetail((current) => (current ? { ...current, cap } : current));
+ await auth.refresh();
+ } catch (error) {
+ Alert.alert(
+ "Sharing update failed",
+ error instanceof Error
+ ? error.message
+ : "Unable to update sharing for this Cap.",
+ );
+ } finally {
+ setActiveOperation(null);
+ }
+ };
+
+ const showPasswordActions = () => {
+ if (!detail || auth.status !== "signedIn") return;
+ showCapPasswordActions({
+ cap: detail.cap,
+ client: auth.client,
+ onUpdated: async (cap) => {
+ setDetail((current) => (current ? { ...current, cap } : current));
+ await auth.refresh();
+ },
+ });
+ };
+
+ const showTitleActions = () => {
+ if (!detail || auth.status !== "signedIn") return;
+ showCapTitleActions({
+ cap: detail.cap,
+ client: auth.client,
+ onUpdated: async (cap) => {
+ setDetail((current) => (current ? { ...current, cap } : current));
+ await auth.refresh();
+ },
+ });
+ };
+
+ const downloadVideo = async () => {
+ if (!detail || isActionInProgress) return;
+ setActiveOperation("save");
+ try {
+ await saveCapVideoToPhotos(auth.client, detail.cap.id);
+ setSaved(true);
+ } catch (error) {
+ if (error instanceof PhotosPermissionDeniedError) {
+ showPhotosSettingsAlert();
+ return;
+ }
+ Alert.alert(
+ "Save failed",
+ error instanceof Error ? error.message : "Unable to save this video.",
+ );
+ } finally {
+ setActiveOperation(null);
+ }
+ };
+
+ const deleteCap = () => {
+ if (!detail || isActionInProgress) return;
+ const confirmDelete = () => {
+ void (async () => {
+ setSettingsVisible(false);
+ await auth.client.deleteCap(detail.cap.id);
+ await auth.refresh();
+ router.back();
+ })();
+ };
+
+ if (Platform.OS === "ios") {
+ ActionSheetIOS.showActionSheetWithOptions(
+ {
+ cancelButtonIndex: 1,
+ destructiveButtonIndex: 0,
+ message: "This Cap will be removed from your library.",
+ options: ["Delete Cap", "Cancel"],
+ title: "Delete Cap",
+ tintColor: colors.blue11,
+ userInterfaceStyle: "light",
+ },
+ (index) => {
+ if (index === 0) confirmDelete();
+ },
+ );
+ return;
+ }
+
+ Alert.alert("Delete Cap", "This Cap will be removed from your library.", [
+ { text: "Cancel", style: "cancel" },
+ {
+ text: "Delete",
+ style: "destructive",
+ onPress: confirmDelete,
+ },
+ ]);
+ };
+
+ const showMoreActions = () => {
+ setSettingsVisible(true);
+ };
+
+ const viewAnalytics = () => {
+ if (!detail || isActionInProgress) return;
+ const url = new URL("/dashboard/analytics", apiBaseUrl);
+ url.searchParams.set("capId", detail.cap.id);
+ void WebBrowser.openBrowserAsync(url.toString());
+ };
+
+ if (auth.status === "signedOut") {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+ detail ? (
+ [
+ styles.headerAction,
+ pressed && !isActionInProgress
+ ? styles.headerActionPressed
+ : null,
+ isActionInProgress ? styles.headerActionDisabled : null,
+ ]}
+ >
+
+
+ ) : null,
+ title: detail?.cap.title ?? "Cap",
+ }}
+ />
+
+ {loadError ? (
+
+
+ Unable to load Cap
+ {loadError}
+ {
+ void load();
+ }}
+ symbol="arrow.clockwise"
+ style={styles.retryButton}
+ />
+
+ ) : detail ? (
+ <>
+
+ {playback?.url ? (
+
+ ) : (
+
+ Processing video
+
+ )}
+
+
+ {detail.cap.title}
+
+ {formatRelativeDate(detail.cap.createdAt)} ยท{" "}
+ {detail.cap.ownerName}
+
+
+ setSettingsVisible(true)}
+ style={({ pressed }) => [
+ styles.shareStatusButton,
+ pressed && !isActionInProgress
+ ? styles.shareStatusButtonPressed
+ : null,
+ isActionInProgress
+ ? styles.shareStatusButtonDisabled
+ : null,
+ ]}
+ >
+
+
+ {sharingStatusLabel}
+
+
+
+ {detail.cap.protected ? (
+
+
+
+ Password protected
+
+
+ ) : null}
+
+
+
+
+
+
+
+ [
+ styles.analyticsPanel,
+ pressed && styles.analyticsPanelPressed,
+ ]}
+ >
+
+
+
+
+
+ View analytics
+
+ {detail.summary ? (
+
+ Summary
+ {detail.summary}
+
+ ) : null}
+ {detail.chapters.length > 0 ? (
+
+ Chapters
+ {detail.chapters.map((chapter) => (
+
+
+ {Math.floor(chapter.start / 60)}:
+ {Math.floor(chapter.start % 60)
+ .toString()
+ .padStart(2, "0")}
+
+
+ {chapter.title}
+
+
+ ))}
+
+ ) : null}
+
+
+ Reactions
+ {reactions.length}
+
+
+ {["๐", "๐", "๐ฅ", "๐"].map((emoji) => (
+ createReaction(emoji)}
+ style={styles.reactionButton}
+ >
+ {emoji}
+
+ ))}
+
+
+
+
+ Comments
+ {textComments.length}
+
+
+ {
+ void createComment();
+ }}
+ placeholder="Add a comment"
+ placeholderTextColor={colors.gray9}
+ returnKeyType="send"
+ selectionColor={colors.blue11}
+ style={[
+ styles.commentInput,
+ isActionInProgress ? styles.commentInputDisabled : null,
+ ]}
+ submitBehavior="blurAndSubmit"
+ value={comment}
+ multiline
+ />
+
+
+ {textComments.map((item) => (
+
+
+
+
+
+
+ {item.author.name ?? "Cap user"}
+
+ {item.content}
+
+
+ ))}
+
+ >
+ ) : null}
+
+ setSettingsVisible(false)}
+ onCopyLink={() => {
+ void copyLink();
+ }}
+ onDelete={() => deleteCap()}
+ onPassword={() => showPasswordActions()}
+ onRename={() => showTitleActions()}
+ onSaveVideo={() => {
+ void downloadVideo();
+ }}
+ onShareLink={() => {
+ void shareLink();
+ }}
+ onViewAnalytics={() => viewAnalytics()}
+ onVisibilityChange={(_cap, isPublic) => {
+ void updateVisibility(isPublic);
+ }}
+ saveDisabled={isActionInProgress}
+ saveDisabledHint={saveVideoHint}
+ saveDisabledValue={isSavingVideo ? undefined : "Unavailable"}
+ saveDisabledAccessibilityValue={
+ isSavingVideo ? saveVideoAccessibilityText : undefined
+ }
+ visibilityDisabled={isActionInProgress}
+ visibilityDisabledHint={sharingStatusHint}
+ visibilityDisabledAccessibilityValue={sharingStatusAccessibilityValue}
+ />
+
+ );
+}
+
+const styles = StyleSheet.create({
+ keyboard: {
+ flex: 1,
+ },
+ headerAction: {
+ width: 36,
+ height: 36,
+ borderRadius: radius.full,
+ alignItems: "center",
+ justifyContent: "center",
+ },
+ headerActionPressed: {
+ backgroundColor: colors.gray3,
+ },
+ headerActionDisabled: {
+ opacity: 0.55,
+ },
+ videoFrame: {
+ width: "100%",
+ aspectRatio: 16 / 9,
+ borderRadius: radius.md,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray3,
+ overflow: "hidden",
+ backgroundColor: colors.black,
+ marginBottom: 14,
+ ...squircle,
+ },
+ video: {
+ width: "100%",
+ height: "100%",
+ },
+ videoPlaceholder: {
+ flex: 1,
+ alignItems: "center",
+ justifyContent: "center",
+ },
+ placeholderText: {
+ fontFamily: fonts.medium,
+ color: colors.gray10,
+ },
+ titleBlock: {
+ gap: 4,
+ marginBottom: 14,
+ },
+ title: {
+ fontFamily: fonts.medium,
+ fontSize: 24,
+ lineHeight: 30,
+ color: colors.gray12,
+ },
+ meta: {
+ fontFamily: fonts.regular,
+ fontSize: 14,
+ lineHeight: 20,
+ color: colors.gray10,
+ },
+ statusRow: {
+ flexDirection: "row",
+ alignItems: "center",
+ flexWrap: "wrap",
+ gap: 8,
+ marginTop: 4,
+ },
+ shareStatusButton: {
+ minHeight: 30,
+ maxWidth: "100%",
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 6,
+ borderRadius: radius.full,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray4,
+ backgroundColor: colors.gray1,
+ paddingHorizontal: 11,
+ ...squircle,
+ },
+ shareStatusButtonPressed: {
+ backgroundColor: colors.gray3,
+ borderColor: colors.gray5,
+ },
+ shareStatusButtonDisabled: {
+ backgroundColor: colors.gray2,
+ borderColor: colors.gray3,
+ },
+ shareStatusText: {
+ fontFamily: fonts.regular,
+ fontSize: 14,
+ lineHeight: 19,
+ color: colors.gray10,
+ },
+ shareStatusTextDisabled: {
+ color: colors.gray9,
+ },
+ passwordPill: {
+ minHeight: 30,
+ maxWidth: "100%",
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 6,
+ borderRadius: radius.full,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray4,
+ backgroundColor: colors.gray1,
+ paddingHorizontal: 11,
+ ...squircle,
+ },
+ passwordPillText: {
+ fontFamily: fonts.regular,
+ fontSize: 14,
+ lineHeight: 19,
+ color: colors.gray10,
+ },
+ actions: {
+ flexDirection: "row",
+ flexWrap: "wrap",
+ gap: 8,
+ marginBottom: 18,
+ },
+ actionButton: {
+ flexBasis: 112,
+ flexGrow: 1,
+ },
+ analyticsPanel: {
+ minHeight: 42,
+ flexDirection: "row",
+ alignItems: "center",
+ justifyContent: "space-between",
+ gap: 12,
+ borderRadius: radius.md,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray3,
+ backgroundColor: colors.gray1,
+ paddingHorizontal: 14,
+ paddingVertical: 10,
+ marginBottom: 20,
+ ...squircle,
+ },
+ analyticsPanelPressed: {
+ backgroundColor: colors.gray2,
+ borderColor: colors.blue10,
+ },
+ analyticsMetrics: {
+ flexDirection: "row",
+ alignItems: "center",
+ flexWrap: "wrap",
+ gap: 16,
+ flexShrink: 1,
+ },
+ metric: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 7,
+ },
+ metricText: {
+ fontFamily: fonts.regular,
+ fontSize: 14,
+ color: colors.gray12,
+ },
+ analyticsLink: {
+ fontFamily: fonts.regular,
+ fontSize: 12,
+ lineHeight: 17,
+ color: colors.blue11,
+ },
+ errorCard: {
+ borderRadius: radius.md,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray3,
+ backgroundColor: colors.gray1,
+ alignItems: "center",
+ gap: 10,
+ paddingHorizontal: 18,
+ paddingVertical: 24,
+ ...squircle,
+ },
+ errorTitle: {
+ fontFamily: fonts.medium,
+ fontSize: 19,
+ color: colors.gray12,
+ textAlign: "center",
+ },
+ errorBody: {
+ fontFamily: fonts.regular,
+ fontSize: 14,
+ lineHeight: 20,
+ color: colors.gray10,
+ textAlign: "center",
+ },
+ retryButton: {
+ marginTop: 4,
+ minWidth: 150,
+ },
+ section: {
+ borderRadius: radius.md,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray3,
+ gap: 10,
+ padding: 16,
+ marginBottom: 20,
+ ...squircle,
+ },
+ sectionFallback: {
+ backgroundColor: colors.gray1,
+ },
+ sectionHeader: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 8,
+ },
+ sectionTitle: {
+ fontFamily: fonts.medium,
+ fontSize: 18,
+ lineHeight: 23,
+ color: colors.gray12,
+ },
+ countText: {
+ fontFamily: fonts.medium,
+ fontSize: 14,
+ color: colors.gray10,
+ },
+ bodyText: {
+ fontFamily: fonts.regular,
+ fontSize: 15,
+ lineHeight: 23,
+ color: colors.gray11,
+ },
+ chapter: {
+ minHeight: 48,
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 10,
+ borderRadius: radius.sm,
+ backgroundColor: colors.gray2,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray3,
+ padding: 10,
+ ...squircle,
+ },
+ chapterTime: {
+ width: 44,
+ fontFamily: fonts.medium,
+ fontSize: 13,
+ color: colors.blue11,
+ },
+ chapterTitle: {
+ flex: 1,
+ fontFamily: fonts.medium,
+ fontSize: 14,
+ color: colors.gray12,
+ },
+ reactions: {
+ flexDirection: "row",
+ gap: 8,
+ },
+ reactionButton: {
+ width: 52,
+ },
+ commentInputRow: {
+ flexDirection: "row",
+ alignItems: "flex-end",
+ gap: 8,
+ },
+ commentInput: {
+ flex: 1,
+ minHeight: 46,
+ maxHeight: 120,
+ borderRadius: radius.md,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray4,
+ backgroundColor: colors.gray2,
+ paddingHorizontal: 12,
+ paddingVertical: 10,
+ fontFamily: fonts.regular,
+ fontSize: 15,
+ color: colors.gray12,
+ ...squircle,
+ },
+ commentInputDisabled: {
+ backgroundColor: colors.gray3,
+ color: colors.gray10,
+ },
+ sendButton: {
+ width: 92,
+ },
+ comment: {
+ flexDirection: "row",
+ gap: 10,
+ backgroundColor: colors.gray2,
+ borderRadius: radius.md,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray3,
+ padding: 12,
+ ...squircle,
+ },
+ commentIcon: {
+ width: 30,
+ height: 30,
+ borderRadius: radius.sm,
+ alignItems: "center",
+ justifyContent: "center",
+ backgroundColor: colors.blue3,
+ ...squircle,
+ },
+ commentBody: {
+ flex: 1,
+ minWidth: 0,
+ gap: 3,
+ },
+ commentAuthor: {
+ fontFamily: fonts.medium,
+ fontSize: 14,
+ color: colors.gray12,
+ },
+ commentText: {
+ fontFamily: fonts.regular,
+ fontSize: 14,
+ lineHeight: 20,
+ color: colors.gray11,
+ },
+});
diff --git a/apps/mobile/assets/icon.png b/apps/mobile/assets/icon.png
new file mode 100644
index 00000000000..b81ddeeef92
Binary files /dev/null and b/apps/mobile/assets/icon.png differ
diff --git a/apps/mobile/assets/icon.svg b/apps/mobile/assets/icon.svg
new file mode 100644
index 00000000000..5e6e3521098
--- /dev/null
+++ b/apps/mobile/assets/icon.svg
@@ -0,0 +1,6 @@
+
diff --git a/apps/mobile/assets/splash-icon.png b/apps/mobile/assets/splash-icon.png
new file mode 100644
index 00000000000..b480ea2b880
Binary files /dev/null and b/apps/mobile/assets/splash-icon.png differ
diff --git a/apps/mobile/assets/splash-icon.svg b/apps/mobile/assets/splash-icon.svg
new file mode 100644
index 00000000000..2a3ccf24753
--- /dev/null
+++ b/apps/mobile/assets/splash-icon.svg
@@ -0,0 +1,5 @@
+
diff --git a/apps/mobile/babel.config.js b/apps/mobile/babel.config.js
new file mode 100644
index 00000000000..8f92bed32c1
--- /dev/null
+++ b/apps/mobile/babel.config.js
@@ -0,0 +1,7 @@
+module.exports = (api) => {
+ api.cache(true);
+ return {
+ presets: ["babel-preset-expo"],
+ plugins: ["react-native-reanimated/plugin"],
+ };
+};
diff --git a/apps/mobile/metro.config.js b/apps/mobile/metro.config.js
new file mode 100644
index 00000000000..9c8cf0a0e8d
--- /dev/null
+++ b/apps/mobile/metro.config.js
@@ -0,0 +1,14 @@
+const path = require("node:path");
+const { getDefaultConfig } = require("expo/metro-config");
+
+const projectRoot = __dirname;
+const workspaceRoot = path.resolve(projectRoot, "../..");
+const config = getDefaultConfig(projectRoot);
+
+config.watchFolders = [workspaceRoot];
+config.resolver.nodeModulesPaths = [
+ path.resolve(projectRoot, "node_modules"),
+ path.resolve(workspaceRoot, "node_modules"),
+];
+
+module.exports = config;
diff --git a/apps/mobile/package.json b/apps/mobile/package.json
new file mode 100644
index 00000000000..868773eb458
--- /dev/null
+++ b/apps/mobile/package.json
@@ -0,0 +1,60 @@
+{
+ "name": "@cap/mobile",
+ "version": "0.1.0",
+ "private": true,
+ "main": "expo-router/entry",
+ "scripts": {
+ "dev": "CAP_MOBILE_DISABLE_ASSOCIATED_DOMAINS=1 node scripts/run-ios-simulator.mjs",
+ "dev:device": "expo run:ios --device",
+ "start": "expo start --dev-client",
+ "typecheck": "tsc --noEmit",
+ "test": "vitest run",
+ "prebuild:ios": "expo prebuild --platform ios --no-install",
+ "ios": "expo run:ios"
+ },
+ "dependencies": {
+ "@cap/web-domain": "workspace:*",
+ "@expo/config-plugins": "~55.0.9",
+ "@expo/metro-runtime": "~55.0.11",
+ "@shopify/flash-list": "2.0.2",
+ "effect": "^3.18.4",
+ "expo": "~55.0.24",
+ "expo-clipboard": "~55.0.13",
+ "expo-constants": "~55.0.16",
+ "expo-dev-client": "~55.0.34",
+ "expo-document-picker": "~55.0.13",
+ "expo-file-system": "~55.0.20",
+ "expo-font": "~55.0.7",
+ "expo-glass-effect": "~55.0.11",
+ "expo-image": "~55.0.10",
+ "expo-image-picker": "~55.0.20",
+ "expo-linking": "~55.0.15",
+ "expo-media-library": "~55.0.17",
+ "expo-modules-core": "~55.0.25",
+ "expo-router": "~55.0.14",
+ "expo-secure-store": "~55.0.14",
+ "expo-sharing": "~55.0.19",
+ "expo-symbols": "~55.0.8",
+ "expo-video": "~55.0.17",
+ "expo-web-browser": "~55.0.16",
+ "react": "19.2.0",
+ "react-dom": "19.2.0",
+ "react-native": "0.83.6",
+ "react-native-gesture-handler": "~2.30.0",
+ "react-native-reanimated": "4.2.1",
+ "react-native-safe-area-context": "~5.6.2",
+ "react-native-screens": "~4.23.0",
+ "react-native-svg": "15.15.5",
+ "react-native-web": "~0.21.0",
+ "react-native-worklets": "0.7.4"
+ },
+ "devDependencies": {
+ "@testing-library/react-native": "^13.3.3",
+ "@types/react": "19.2.14",
+ "@types/react-test-renderer": "^19.1.0",
+ "babel-preset-expo": "~55.0.21",
+ "react-test-renderer": "19.2.0",
+ "typescript": "~5.9.2",
+ "vitest": "^3.2.0"
+ }
+}
diff --git a/apps/mobile/scripts/run-ios-simulator.mjs b/apps/mobile/scripts/run-ios-simulator.mjs
new file mode 100644
index 00000000000..1ea40fa0d75
--- /dev/null
+++ b/apps/mobile/scripts/run-ios-simulator.mjs
@@ -0,0 +1,107 @@
+import { spawnSync } from "node:child_process";
+import { existsSync, readFileSync } from "node:fs";
+import { join } from "node:path";
+
+const readSimulators = () => {
+ const result = spawnSync(
+ "xcrun",
+ ["simctl", "list", "devices", "available", "--json"],
+ {
+ encoding: "utf8",
+ },
+ );
+ if (result.status !== 0) {
+ throw new Error(result.stderr || "Unable to list iOS simulators");
+ }
+
+ return JSON.parse(result.stdout);
+};
+
+const findSimulator = () => {
+ const requestedUdid = process.env.IOS_SIMULATOR_UDID;
+ const requestedName = process.env.IOS_SIMULATOR_DEVICE;
+ const data = readSimulators();
+ const devices = Object.values(data.devices ?? {})
+ .flat()
+ .filter(
+ (device) => device?.isAvailable && device?.name?.includes("iPhone"),
+ );
+
+ if (requestedUdid) {
+ const requested = devices.find((device) => device.udid === requestedUdid);
+ if (requested) return requested;
+ throw new Error(`No available iPhone simulator found for ${requestedUdid}`);
+ }
+
+ if (requestedName) {
+ const requested = devices.find((device) => device.name === requestedName);
+ if (requested) return requested;
+ throw new Error(`No available iPhone simulator named ${requestedName}`);
+ }
+
+ const booted = devices.find((device) => device.state === "Booted");
+ if (booted) return booted;
+
+ const preferred = devices.find((device) => device.name.includes("Pro"));
+ return preferred ?? devices[0] ?? null;
+};
+
+const simulator = findSimulator();
+if (!simulator) {
+ throw new Error("No available iPhone simulators found");
+}
+
+const needsDevPrebuild = () => {
+ if (existsSync(join(process.cwd(), "ios", "CapBroadcastExtension"))) {
+ return true;
+ }
+ const entitlementsPath = join(
+ process.cwd(),
+ "ios",
+ "Cap",
+ "Cap.entitlements",
+ );
+ if (!existsSync(entitlementsPath)) return true;
+ const entitlements = readFileSync(entitlementsPath, "utf8");
+ return entitlements.includes("com.apple.developer.associated-domains");
+};
+
+const command = ["exec", "expo", "run:ios", "--device", simulator.udid];
+console.log(`Using iOS simulator: ${simulator.name} (${simulator.udid})`);
+
+if (process.env.CAP_MOBILE_DRY_RUN === "1") {
+ console.log(`pnpm ${command.join(" ")}`);
+ process.exit(0);
+}
+
+if (
+ process.env.CAP_MOBILE_DISABLE_ASSOCIATED_DOMAINS === "1" &&
+ needsDevPrebuild()
+) {
+ const prebuild = spawnSync(
+ "pnpm",
+ [
+ "exec",
+ "expo",
+ "prebuild",
+ "--platform",
+ "ios",
+ "--no-install",
+ "--clean",
+ ],
+ {
+ stdio: "inherit",
+ env: process.env,
+ },
+ );
+ if (prebuild.status !== 0) {
+ process.exit(prebuild.status ?? 1);
+ }
+}
+
+const result = spawnSync("pnpm", command, {
+ stdio: "inherit",
+ env: process.env,
+});
+
+process.exit(result.status ?? 1);
diff --git a/apps/mobile/src/api/mobile.test.ts b/apps/mobile/src/api/mobile.test.ts
new file mode 100644
index 00000000000..1a66a6a955e
--- /dev/null
+++ b/apps/mobile/src/api/mobile.test.ts
@@ -0,0 +1,469 @@
+import { describe, expect, it, vi } from "vitest";
+import {
+ createMobileApiClient,
+ createSessionRequestUrl,
+ uploadToTarget,
+} from "./mobile";
+
+const fileSystemMock = vi.hoisted(() => ({
+ FileSystemUploadType: {
+ BINARY_CONTENT: 0,
+ MULTIPART: 1,
+ },
+ createUploadTask: vi.fn(),
+ getInfoAsync: vi.fn(),
+}));
+
+vi.mock("expo-file-system/legacy", () => fileSystemMock);
+
+describe("createMobileApiClient", () => {
+ it("decodes bootstrap responses through shared schemas", async () => {
+ const calls: RequestInfo[] = [];
+ const originalFetch = globalThis.fetch;
+ globalThis.fetch = (async (input: RequestInfo | URL) => {
+ calls.push(input as RequestInfo);
+ return new Response(
+ JSON.stringify({
+ user: {
+ id: "user_123",
+ name: "Richie",
+ email: "richie@example.com",
+ imageUrl: null,
+ activeOrganizationId: "org_123",
+ },
+ organizations: [
+ {
+ id: "org_123",
+ name: "Cap",
+ iconUrl: null,
+ role: "owner",
+ },
+ ],
+ activeOrganizationId: "org_123",
+ rootFolders: [],
+ }),
+ { status: 200 },
+ );
+ }) as typeof fetch;
+
+ try {
+ const client = createMobileApiClient({
+ baseUrl: "https://cap.so/",
+ getToken: () => "api-key",
+ });
+ const result = await client.bootstrap();
+ expect(result.user.email).toBe("richie@example.com");
+ expect(String(calls[0])).toBe("https://cap.so/api/mobile/bootstrap");
+ } finally {
+ globalThis.fetch = originalFetch;
+ }
+ });
+
+ it("decodes public auth provider config", async () => {
+ const calls: RequestInfo[] = [];
+ const originalFetch = globalThis.fetch;
+ globalThis.fetch = (async (input: RequestInfo | URL) => {
+ calls.push(input as RequestInfo);
+ return new Response(
+ JSON.stringify({
+ googleAuthAvailable: false,
+ workosAuthAvailable: true,
+ }),
+ { status: 200 },
+ );
+ }) as typeof fetch;
+
+ try {
+ const client = createMobileApiClient({
+ baseUrl: "https://cap.so/",
+ getToken: () => null,
+ });
+ const result = await client.getAuthConfig();
+ expect(result.googleAuthAvailable).toBe(false);
+ expect(result.workosAuthAvailable).toBe(true);
+ expect(String(calls[0])).toBe("https://cap.so/api/mobile/session/config");
+ } finally {
+ globalThis.fetch = originalFetch;
+ }
+ });
+
+ it("keeps non-JSON error responses in the API error payload", async () => {
+ const originalFetch = globalThis.fetch;
+ globalThis.fetch = (async () =>
+ new Response("bad gateway", {
+ status: 502,
+ })) as typeof fetch;
+
+ try {
+ const client = createMobileApiClient({
+ baseUrl: "https://cap.so/",
+ getToken: () => "api-key",
+ });
+
+ await expect(client.bootstrap()).rejects.toMatchObject({
+ status: 502,
+ payload: "bad gateway",
+ });
+ } finally {
+ globalThis.fetch = originalFetch;
+ }
+ });
+
+ it("builds Google session request URLs", () => {
+ expect(
+ createSessionRequestUrl("https://cap.so/", "cap://auth", "google"),
+ ).toBe(
+ "https://cap.so/api/mobile/session/request?redirectUri=cap%3A%2F%2Fauth&provider=google",
+ );
+ });
+
+ it("builds WorkOS session request URLs", () => {
+ expect(
+ createSessionRequestUrl(
+ "https://cap.so/",
+ "cap://auth",
+ "workos",
+ "org_123",
+ ),
+ ).toBe(
+ "https://cap.so/api/mobile/session/request?redirectUri=cap%3A%2F%2Fauth&provider=workos&organizationId=org_123",
+ );
+ });
+
+ it("updates Cap sharing with the authenticated PATCH endpoint", async () => {
+ const calls: Array<{ input: RequestInfo | URL; init?: RequestInit }> = [];
+ const originalFetch = globalThis.fetch;
+ globalThis.fetch = (async (
+ input: RequestInfo | URL,
+ init?: RequestInit,
+ ) => {
+ calls.push({ input, init });
+ return new Response(
+ JSON.stringify({
+ id: "video_123",
+ shareUrl: "https://cap.so/s/video_123",
+ title: "Launch review",
+ createdAt: "2026-05-18T10:00:00.000Z",
+ updatedAt: "2026-05-18T10:30:00.000Z",
+ ownerName: "Richie",
+ durationSeconds: null,
+ thumbnailUrl: null,
+ folderId: null,
+ public: false,
+ protected: false,
+ viewCount: 0,
+ commentCount: 0,
+ reactionCount: 0,
+ upload: null,
+ }),
+ { status: 200 },
+ );
+ }) as typeof fetch;
+
+ try {
+ const client = createMobileApiClient({
+ baseUrl: "https://cap.so/",
+ getToken: () => "api-key",
+ });
+ const result = await client.updateCapSharing("video_123", {
+ public: false,
+ });
+ const body = calls[0]?.init?.body;
+
+ expect(result.public).toBe(false);
+ expect(String(calls[0]?.input)).toBe(
+ "https://cap.so/api/mobile/caps/video_123/sharing",
+ );
+ expect(calls[0]?.init?.method).toBe("PATCH");
+ expect(calls[0]?.init?.headers).toBeInstanceOf(Headers);
+ expect((calls[0]?.init?.headers as Headers).get("authorization")).toBe(
+ "Bearer api-key",
+ );
+ expect(typeof body).toBe("string");
+ expect(JSON.parse(body as string)).toEqual({ public: false });
+ } finally {
+ globalThis.fetch = originalFetch;
+ }
+ });
+
+ it("creates folders with the authenticated POST endpoint", async () => {
+ const calls: Array<{ input: RequestInfo | URL; init?: RequestInit }> = [];
+ const originalFetch = globalThis.fetch;
+ globalThis.fetch = (async (
+ input: RequestInfo | URL,
+ init?: RequestInit,
+ ) => {
+ calls.push({ input, init });
+ return new Response(
+ JSON.stringify({
+ id: "folder_123",
+ name: "Product",
+ color: "blue",
+ parentId: null,
+ videoCount: 0,
+ }),
+ { status: 200 },
+ );
+ }) as typeof fetch;
+
+ try {
+ const client = createMobileApiClient({
+ baseUrl: "https://cap.so/",
+ getToken: () => "api-key",
+ });
+ const result = await client.createFolder({
+ name: "Product",
+ color: "blue",
+ });
+ const body = calls[0]?.init?.body;
+
+ expect(result.name).toBe("Product");
+ expect(String(calls[0]?.input)).toBe("https://cap.so/api/mobile/folders");
+ expect(calls[0]?.init?.method).toBe("POST");
+ expect(typeof body).toBe("string");
+ expect(JSON.parse(body as string)).toEqual({
+ name: "Product",
+ color: "blue",
+ });
+ } finally {
+ globalThis.fetch = originalFetch;
+ }
+ });
+
+ it("updates Cap titles with the authenticated PATCH endpoint", async () => {
+ const calls: Array<{ input: RequestInfo | URL; init?: RequestInit }> = [];
+ const originalFetch = globalThis.fetch;
+ globalThis.fetch = (async (
+ input: RequestInfo | URL,
+ init?: RequestInit,
+ ) => {
+ calls.push({ input, init });
+ return new Response(
+ JSON.stringify({
+ id: "video_123",
+ shareUrl: "https://cap.so/s/video_123",
+ title: "Roadmap review",
+ createdAt: "2026-05-18T10:00:00.000Z",
+ updatedAt: "2026-05-18T10:30:00.000Z",
+ ownerName: "Richie",
+ durationSeconds: null,
+ thumbnailUrl: null,
+ folderId: null,
+ public: true,
+ protected: false,
+ viewCount: 0,
+ commentCount: 0,
+ reactionCount: 0,
+ upload: null,
+ }),
+ { status: 200 },
+ );
+ }) as typeof fetch;
+
+ try {
+ const client = createMobileApiClient({
+ baseUrl: "https://cap.so/",
+ getToken: () => "api-key",
+ });
+ const result = await client.updateCapTitle("video_123", {
+ title: "Roadmap review",
+ });
+ const body = calls[0]?.init?.body;
+
+ expect(result.title).toBe("Roadmap review");
+ expect(String(calls[0]?.input)).toBe(
+ "https://cap.so/api/mobile/caps/video_123/title",
+ );
+ expect(calls[0]?.init?.method).toBe("PATCH");
+ expect(typeof body).toBe("string");
+ expect(JSON.parse(body as string)).toEqual({ title: "Roadmap review" });
+ } finally {
+ globalThis.fetch = originalFetch;
+ }
+ });
+
+ it("updates Cap passwords with the authenticated PATCH endpoint", async () => {
+ const calls: Array<{ input: RequestInfo | URL; init?: RequestInit }> = [];
+ const originalFetch = globalThis.fetch;
+ globalThis.fetch = (async (
+ input: RequestInfo | URL,
+ init?: RequestInit,
+ ) => {
+ calls.push({ input, init });
+ return new Response(
+ JSON.stringify({
+ id: "video_123",
+ shareUrl: "https://cap.so/s/video_123",
+ title: "Launch review",
+ createdAt: "2026-05-18T10:00:00.000Z",
+ updatedAt: "2026-05-18T10:30:00.000Z",
+ ownerName: "Richie",
+ durationSeconds: null,
+ thumbnailUrl: null,
+ folderId: null,
+ public: true,
+ protected: true,
+ viewCount: 0,
+ commentCount: 0,
+ reactionCount: 0,
+ upload: null,
+ }),
+ { status: 200 },
+ );
+ }) as typeof fetch;
+
+ try {
+ const client = createMobileApiClient({
+ baseUrl: "https://cap.so/",
+ getToken: () => "api-key",
+ });
+ const result = await client.updateCapPassword("video_123", {
+ password: "secret",
+ });
+ const body = calls[0]?.init?.body;
+
+ expect(result.protected).toBe(true);
+ expect(String(calls[0]?.input)).toBe(
+ "https://cap.so/api/mobile/caps/video_123/password",
+ );
+ expect(calls[0]?.init?.method).toBe("PATCH");
+ expect(typeof body).toBe("string");
+ expect(JSON.parse(body as string)).toEqual({ password: "secret" });
+ } finally {
+ globalThis.fetch = originalFetch;
+ }
+ });
+
+ it("uploads local files with native transfer progress", async () => {
+ const uploadAsync = vi.fn(() =>
+ Promise.resolve({
+ body: "",
+ headers: {},
+ mimeType: null,
+ status: 200,
+ }),
+ );
+ const onProgress = vi.fn();
+
+ fileSystemMock.createUploadTask.mockImplementation(
+ (
+ url: string,
+ fileUri: string,
+ options: unknown,
+ callback?: (data: {
+ totalBytesExpectedToSend: number;
+ totalBytesSent: number;
+ }) => void,
+ ) => {
+ callback?.({
+ totalBytesExpectedToSend: 3,
+ totalBytesSent: 2,
+ });
+ return { uploadAsync, url, fileUri, options };
+ },
+ );
+
+ await uploadToTarget(
+ {
+ type: "driveResumable",
+ url: "https://uploads.example/drive",
+ headers: {
+ "Content-Type": "video/mp4",
+ },
+ },
+ {
+ uri: "file:///tmp/video.mp4",
+ name: "video.mp4",
+ type: "video/mp4",
+ size: 3,
+ },
+ onProgress,
+ );
+
+ expect(fileSystemMock.createUploadTask).toHaveBeenCalledWith(
+ "https://uploads.example/drive",
+ "file:///tmp/video.mp4",
+ {
+ headers: {
+ "Content-Range": "bytes 0-2/3",
+ "Content-Type": "video/mp4",
+ },
+ httpMethod: "PUT",
+ uploadType: fileSystemMock.FileSystemUploadType.BINARY_CONTENT,
+ },
+ expect.any(Function),
+ );
+ expect(uploadAsync).toHaveBeenCalled();
+ expect(onProgress).toHaveBeenCalledWith({ loaded: 2, total: 3 });
+ });
+
+ it("sets the Drive resumable upload byte range for remote blobs", async () => {
+ class MockXMLHttpRequest {
+ static instances: MockXMLHttpRequest[] = [];
+ upload: {
+ onprogress:
+ | ((event: ProgressEvent) => void)
+ | null;
+ } = { onprogress: null };
+ status = 200;
+ responseText = "";
+ onload: (() => void) | null = null;
+ onerror: (() => void) | null = null;
+ method = "";
+ url = "";
+ headers = new Map();
+ body: BodyInit | null = null;
+
+ constructor() {
+ MockXMLHttpRequest.instances.push(this);
+ }
+
+ open(method: string, url: string) {
+ this.method = method;
+ this.url = url;
+ }
+
+ setRequestHeader(key: string, value: string) {
+ this.headers.set(key, value);
+ }
+
+ send(body: BodyInit) {
+ this.body = body;
+ this.onload?.();
+ }
+ }
+
+ const originalFetch = globalThis.fetch;
+ const originalXhr = globalThis.XMLHttpRequest;
+ globalThis.fetch = (async () =>
+ new Response(new Uint8Array([1, 2, 3]))) as typeof fetch;
+ globalThis.XMLHttpRequest =
+ MockXMLHttpRequest as unknown as typeof XMLHttpRequest;
+
+ try {
+ await uploadToTarget(
+ {
+ type: "driveResumable",
+ url: "https://uploads.example/drive",
+ headers: {
+ "Content-Type": "video/mp4",
+ },
+ },
+ {
+ uri: "https://cache.example/video.mp4",
+ name: "video.mp4",
+ type: "video/mp4",
+ size: 3,
+ },
+ );
+
+ const request = MockXMLHttpRequest.instances[0];
+ expect(request?.method).toBe("PUT");
+ expect(request?.headers.get("content-type")).toBe("video/mp4");
+ expect(request?.headers.get("content-range")).toBe("bytes 0-2/3");
+ } finally {
+ globalThis.fetch = originalFetch;
+ globalThis.XMLHttpRequest = originalXhr;
+ }
+ });
+});
diff --git a/apps/mobile/src/api/mobile.ts b/apps/mobile/src/api/mobile.ts
new file mode 100644
index 00000000000..da87e70c68c
--- /dev/null
+++ b/apps/mobile/src/api/mobile.ts
@@ -0,0 +1,483 @@
+import { Mobile, type Storage } from "@cap/web-domain";
+import { Schema } from "effect";
+import * as FileSystem from "expo-file-system/legacy";
+
+export type MobileApiKeyResponse = typeof Mobile.MobileApiKeyResponse.Type;
+export type MobileSuccessResponse = typeof Mobile.MobileSuccessResponse.Type;
+export type MobileAuthConfigResponse =
+ typeof Mobile.MobileAuthConfigResponse.Type;
+export type MobileBootstrapResponse =
+ typeof Mobile.MobileBootstrapResponse.Type;
+export type MobileCapsListResponse = typeof Mobile.MobileCapsListResponse.Type;
+export type MobileCapSummary = typeof Mobile.MobileCapSummary.Type;
+export type MobileFolder = typeof Mobile.MobileFolder.Type;
+export type MobileCapDetail = typeof Mobile.MobileCapDetail.Type;
+export type MobileComment = typeof Mobile.MobileComment.Type;
+export type MobilePlaybackResponse = typeof Mobile.MobilePlaybackResponse.Type;
+export type MobileDownloadResponse = typeof Mobile.MobileDownloadResponse.Type;
+export type MobileCapSharingInput = typeof Mobile.MobileCapSharingInput.Type;
+export type MobileCapTitleInput = typeof Mobile.MobileCapTitleInput.Type;
+export type MobileCapPasswordInput = typeof Mobile.MobileCapPasswordInput.Type;
+export type MobileFolderCreateInput =
+ typeof Mobile.MobileFolderCreateInput.Type;
+export type MobileUploadCreateInput =
+ typeof Mobile.MobileUploadCreateInput.Type;
+export type MobileUploadCreateResponse =
+ typeof Mobile.MobileUploadCreateResponse.Type;
+
+export type MobileApiClient = ReturnType;
+
+export type UploadFile = {
+ uri: string;
+ name: string;
+ type: string;
+ size?: number;
+ durationSeconds?: number;
+ width?: number;
+ height?: number;
+};
+
+export type UploadProgress = {
+ loaded: number;
+ total: number;
+};
+
+type ClientOptions = {
+ baseUrl: string;
+ getToken: () => string | Promise | null;
+};
+
+type RequestOptions = {
+ method?: "GET" | "POST" | "PATCH" | "DELETE";
+ query?: Record;
+ body?: unknown;
+};
+
+export class MobileApiError extends Error {
+ constructor(
+ message: string,
+ readonly status: number,
+ readonly payload: unknown,
+ ) {
+ super(message);
+ this.name = "MobileApiError";
+ }
+}
+
+const trimBaseUrl = (baseUrl: string) => baseUrl.replace(/\/+$/, "");
+
+const decode = async (
+ schema: Schema.Schema,
+ value: unknown,
+): Promise => Schema.decodeUnknownPromise(schema)(value);
+
+const appendQuery = (
+ url: URL,
+ query: Record | undefined,
+) => {
+ if (!query) return;
+ for (const [key, value] of Object.entries(query)) {
+ if (value !== null && value !== undefined && value !== "") {
+ url.searchParams.set(key, String(value));
+ }
+ }
+};
+
+const parseJson = async (response: Response) => {
+ const text = await response.text();
+ if (text.length === 0) return null;
+ try {
+ return JSON.parse(text) as unknown;
+ } catch {
+ return text;
+ }
+};
+
+export const createSessionRequestUrl = (
+ baseUrl: string,
+ redirectUri: string,
+ provider?: "google" | "workos",
+ organizationId?: string,
+) => {
+ const url = new URL("/api/mobile/session/request", trimBaseUrl(baseUrl));
+ url.searchParams.set("redirectUri", redirectUri);
+ if (provider) url.searchParams.set("provider", provider);
+ if (organizationId) url.searchParams.set("organizationId", organizationId);
+ return url.toString();
+};
+
+export const createMobileApiClient = ({ baseUrl, getToken }: ClientOptions) => {
+ const origin = trimBaseUrl(baseUrl);
+
+ const request = async (
+ path: string,
+ schema: Schema.Schema,
+ options: RequestOptions = {},
+ ): Promise => {
+ const token = await getToken();
+ if (!token) {
+ throw new MobileApiError("Missing mobile session", 401, null);
+ }
+
+ const url = new URL(path, origin);
+ appendQuery(url, options.query);
+ const headers = new Headers({
+ Authorization: `Bearer ${token}`,
+ });
+ let body: BodyInit | undefined;
+ if (options.body !== undefined) {
+ headers.set("Content-Type", "application/json");
+ body = JSON.stringify(options.body);
+ }
+
+ const response = await fetch(url.toString(), {
+ method: options.method ?? "GET",
+ headers,
+ body,
+ });
+ const payload = await parseJson(response);
+ if (!response.ok) {
+ throw new MobileApiError(
+ `Mobile API request failed with ${response.status}`,
+ response.status,
+ payload,
+ );
+ }
+ return decode(schema, payload);
+ };
+
+ const publicRequest = async (
+ path: string,
+ schema: Schema.Schema,
+ options: Omit = {},
+ ): Promise => {
+ const url = new URL(path, origin);
+ const headers = new Headers();
+ let body: BodyInit | undefined;
+ if (options.body !== undefined) {
+ headers.set("Content-Type", "application/json");
+ body = JSON.stringify(options.body);
+ }
+
+ const response = await fetch(url.toString(), {
+ method: options.method ?? "GET",
+ headers,
+ body,
+ });
+ const payload = await parseJson(response);
+ if (!response.ok) {
+ throw new MobileApiError(
+ `Mobile API request failed with ${response.status}`,
+ response.status,
+ payload,
+ );
+ }
+ return decode(schema, payload);
+ };
+
+ return {
+ getAuthConfig: () =>
+ publicRequest(
+ "/api/mobile/session/config",
+ Mobile.MobileAuthConfigResponse,
+ ),
+ requestEmailCode: (email: string) =>
+ publicRequest(
+ "/api/mobile/session/email/request",
+ Mobile.MobileSuccessResponse,
+ {
+ method: "POST",
+ body: { email },
+ },
+ ),
+ verifyEmailCode: (input: { email: string; code: string }) =>
+ publicRequest(
+ "/api/mobile/session/email/verify",
+ Mobile.MobileApiKeyResponse,
+ {
+ method: "POST",
+ body: input,
+ },
+ ),
+ bootstrap: () =>
+ request("/api/mobile/bootstrap", Mobile.MobileBootstrapResponse),
+ setActiveOrganization: (organizationId: string) =>
+ request(
+ "/api/mobile/user/active-organization",
+ Mobile.MobileBootstrapResponse,
+ {
+ method: "PATCH",
+ body: { organizationId },
+ },
+ ),
+ listCaps: (params: {
+ folderId?: string | null;
+ page?: number;
+ limit?: number;
+ }) =>
+ request("/api/mobile/caps", Mobile.MobileCapsListResponse, {
+ query: params,
+ }),
+ createFolder: (input: MobileFolderCreateInput) =>
+ request("/api/mobile/folders", Mobile.MobileFolder, {
+ method: "POST",
+ body: input,
+ }),
+ getCap: (id: string) =>
+ request(`/api/mobile/caps/${id}`, Mobile.MobileCapDetail),
+ updateCapSharing: (id: string, input: MobileCapSharingInput) =>
+ request(`/api/mobile/caps/${id}/sharing`, Mobile.MobileCapSummary, {
+ method: "PATCH",
+ body: input,
+ }),
+ updateCapTitle: (id: string, input: MobileCapTitleInput) =>
+ request(`/api/mobile/caps/${id}/title`, Mobile.MobileCapSummary, {
+ method: "PATCH",
+ body: input,
+ }),
+ updateCapPassword: (id: string, input: MobileCapPasswordInput) =>
+ request(`/api/mobile/caps/${id}/password`, Mobile.MobileCapSummary, {
+ method: "PATCH",
+ body: input,
+ }),
+ deleteCap: (id: string) =>
+ request(`/api/mobile/caps/${id}`, Mobile.MobileSuccessResponse, {
+ method: "DELETE",
+ }),
+ getPlayback: (id: string) =>
+ request(`/api/mobile/caps/${id}/playback`, Mobile.MobilePlaybackResponse),
+ getDownload: (id: string) =>
+ request(`/api/mobile/caps/${id}/download`, Mobile.MobileDownloadResponse),
+ createComment: (
+ id: string,
+ input: { content: string; timestamp: number | null },
+ ) =>
+ request(`/api/mobile/caps/${id}/comments`, Mobile.MobileComment, {
+ method: "POST",
+ body: input,
+ }),
+ deleteComment: (id: string) =>
+ request(`/api/mobile/comments/${id}`, Mobile.MobileSuccessResponse, {
+ method: "DELETE",
+ }),
+ createReaction: (
+ id: string,
+ input: { content: string; timestamp: number | null },
+ ) =>
+ request(`/api/mobile/caps/${id}/reactions`, Mobile.MobileComment, {
+ method: "POST",
+ body: input,
+ }),
+ createUpload: (input: MobileUploadCreateInput) =>
+ request("/api/mobile/uploads", Mobile.MobileUploadCreateResponse, {
+ method: "POST",
+ body: input,
+ }),
+ updateUploadProgress: (
+ id: string,
+ input: { uploaded: number; total: number },
+ ) =>
+ request(
+ `/api/mobile/uploads/${id}/progress`,
+ Mobile.MobileSuccessResponse,
+ {
+ method: "POST",
+ body: input,
+ },
+ ),
+ completeUpload: (
+ id: string,
+ input: { rawFileKey: string; contentLength?: number },
+ ) =>
+ request(
+ `/api/mobile/uploads/${id}/complete`,
+ Mobile.MobileSuccessResponse,
+ {
+ method: "POST",
+ body: input,
+ },
+ ),
+ revokeSession: () =>
+ request("/api/mobile/session/revoke", Mobile.MobileSuccessResponse, {
+ method: "POST",
+ }),
+ };
+};
+
+const targetHeaders = (headers: Record) => {
+ const result = new Headers();
+ for (const [key, value] of Object.entries(headers)) {
+ result.set(key, value);
+ }
+ return result;
+};
+
+const isNativeUploadUri = (uri: string) =>
+ uri.startsWith("file://") || uri.startsWith("content://");
+
+const getLocalFileSize = async (file: UploadFile) => {
+ if (typeof file.size === "number" && file.size > 0) return file.size;
+
+ const info = await FileSystem.getInfoAsync(file.uri);
+ if (!info.exists || info.isDirectory) return 0;
+ return info.size;
+};
+
+const uploadNativeFile = async (
+ method: "POST" | "PUT",
+ url: string,
+ file: UploadFile,
+ options: FileSystem.FileSystemUploadOptions,
+ onProgress?: (progress: UploadProgress) => void,
+) => {
+ const task = FileSystem.createUploadTask(
+ url,
+ file.uri,
+ {
+ ...options,
+ httpMethod: method,
+ },
+ (data) => {
+ onProgress?.({
+ loaded: data.totalBytesSent,
+ total: data.totalBytesExpectedToSend,
+ });
+ },
+ );
+ const response = await task.uploadAsync();
+ if (!response || response.status < 200 || response.status >= 300) {
+ throw new MobileApiError(
+ "Upload target rejected the file",
+ response?.status ?? 0,
+ response?.body ?? null,
+ );
+ }
+};
+
+const uploadWithXhr = (
+ method: "POST" | "PUT",
+ url: string,
+ headers: Headers,
+ body: FormData | Blob,
+ onProgress?: (progress: UploadProgress) => void,
+) =>
+ new Promise((resolve, reject) => {
+ const xhr = new XMLHttpRequest();
+ xhr.open(method, url);
+ headers.forEach((value, key) => {
+ xhr.setRequestHeader(key, value);
+ });
+ xhr.upload.onprogress = (event) => {
+ onProgress?.({
+ loaded: event.loaded,
+ total: event.lengthComputable ? event.total : 0,
+ });
+ };
+ xhr.onload = () => {
+ if (xhr.status >= 200 && xhr.status < 300) {
+ resolve();
+ return;
+ }
+ reject(
+ new MobileApiError(
+ "Upload target rejected the file",
+ xhr.status,
+ xhr.responseText,
+ ),
+ );
+ };
+ xhr.onerror = () => {
+ reject(new Error("Upload failed"));
+ };
+ xhr.send(body);
+ });
+
+const fileBlob = async (file: UploadFile) => {
+ const response = await fetch(file.uri);
+ return response.blob();
+};
+
+export const uploadToTarget = async (
+ target: Storage.UploadTarget,
+ file: UploadFile,
+ onProgress?: (progress: UploadProgress) => void,
+) => {
+ if (target.type === "s3Post") {
+ if (isNativeUploadUri(file.uri)) {
+ await uploadNativeFile(
+ "POST",
+ target.url,
+ file,
+ {
+ fieldName: "file",
+ mimeType: file.type,
+ parameters: target.fields,
+ uploadType: FileSystem.FileSystemUploadType.MULTIPART,
+ },
+ onProgress,
+ );
+ return;
+ }
+
+ const formData = new FormData();
+ for (const [key, value] of Object.entries(target.fields)) {
+ formData.append(key, value);
+ }
+ formData.append("file", {
+ uri: file.uri,
+ name: file.name,
+ type: file.type,
+ } as unknown as Blob);
+ await uploadWithXhr(
+ "POST",
+ target.url,
+ new Headers(),
+ formData,
+ onProgress,
+ );
+ return;
+ }
+
+ const headers = { ...target.headers };
+ let size = file.size;
+ if (
+ target.type === "driveResumable" &&
+ typeof size === "number" &&
+ size > 0
+ ) {
+ headers["Content-Range"] = `bytes 0-${size - 1}/${size}`;
+ }
+
+ if (isNativeUploadUri(file.uri)) {
+ if (target.type === "driveResumable" && !size) {
+ size = await getLocalFileSize(file);
+ if (size > 0) {
+ headers["Content-Range"] = `bytes 0-${size - 1}/${size}`;
+ }
+ }
+
+ await uploadNativeFile(
+ "PUT",
+ target.url,
+ file,
+ {
+ headers,
+ uploadType: FileSystem.FileSystemUploadType.BINARY_CONTENT,
+ },
+ onProgress,
+ );
+ return;
+ }
+
+ const blob = await fileBlob(file);
+ if (target.type === "driveResumable" && !size && blob.size > 0) {
+ headers["Content-Range"] = `bytes 0-${blob.size - 1}/${blob.size}`;
+ }
+ await uploadWithXhr(
+ "PUT",
+ target.url,
+ targetHeaders(headers),
+ blob,
+ onProgress,
+ );
+};
diff --git a/apps/mobile/src/auth/AuthContext.test.ts b/apps/mobile/src/auth/AuthContext.test.ts
new file mode 100644
index 00000000000..e0bc13805f3
--- /dev/null
+++ b/apps/mobile/src/auth/AuthContext.test.ts
@@ -0,0 +1,31 @@
+import { describe, expect, it } from "vitest";
+import { parseAuthRedirect, requireAuthRedirectSession } from "./session";
+
+describe("parseAuthRedirect", () => {
+ it("extracts the issued API key and user id", () => {
+ expect(
+ parseAuthRedirect("cap://auth?api_key=key_123&user_id=user_123"),
+ ).toEqual({
+ apiKey: "key_123",
+ userId: "user_123",
+ });
+ });
+
+ it("rejects redirects without an API key", () => {
+ expect(parseAuthRedirect("cap://auth?user_id=user_123")).toBeNull();
+ });
+
+ it("throws a usable message for failed auth callbacks", () => {
+ expect(() =>
+ requireAuthRedirectSession(
+ "cap://auth?error_description=Organization%20not%20found",
+ ),
+ ).toThrow("Organization not found");
+ });
+
+ it("throws when an auth callback omits the mobile API key", () => {
+ expect(() =>
+ requireAuthRedirectSession("cap://auth?user_id=user_123"),
+ ).toThrow("Sign in did not return a mobile session.");
+ });
+});
diff --git a/apps/mobile/src/auth/AuthContext.tsx b/apps/mobile/src/auth/AuthContext.tsx
new file mode 100644
index 00000000000..9c97d7ec2e5
--- /dev/null
+++ b/apps/mobile/src/auth/AuthContext.tsx
@@ -0,0 +1,256 @@
+import Constants from "expo-constants";
+import * as Linking from "expo-linking";
+import * as SecureStore from "expo-secure-store";
+import * as WebBrowser from "expo-web-browser";
+import {
+ createContext,
+ type ReactNode,
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useState,
+} from "react";
+import {
+ createMobileApiClient,
+ createSessionRequestUrl,
+ type MobileApiClient,
+ type MobileAuthConfigResponse,
+ type MobileBootstrapResponse,
+} from "@/api/mobile";
+import { requireAuthRedirectSession } from "./session";
+
+WebBrowser.maybeCompleteAuthSession();
+
+const sessionKey = "cap.mobile.apiKey";
+const userIdKey = "cap.mobile.userId";
+
+type AuthState = {
+ status: "loading" | "signedOut" | "signedIn";
+ apiKey: string | null;
+ userId: string | null;
+ authConfig: MobileAuthConfigResponse;
+ bootstrap: MobileBootstrapResponse | null;
+ client: MobileApiClient;
+ requestEmailCode: (email: string) => Promise;
+ verifyEmailCode: (email: string, code: string) => Promise;
+ signInWithGoogle: () => Promise;
+ signInWithSso: (organizationId: string) => Promise;
+ signOut: () => Promise;
+ refresh: () => Promise;
+ setActiveOrganization: (organizationId: string) => Promise;
+};
+
+const AuthContext = createContext(null);
+const fallbackAuthConfig: MobileAuthConfigResponse = {
+ googleAuthAvailable: true,
+ workosAuthAvailable: true,
+};
+
+const getExtraString = (key: string, fallback: string) => {
+ const extra = Constants.expoConfig?.extra;
+ if (!extra || typeof extra !== "object") return fallback;
+ const value = (extra as Record)[key];
+ return typeof value === "string" ? value : fallback;
+};
+
+export const apiBaseUrl = getExtraString("apiBaseUrl", "https://cap.so");
+
+export function AuthProvider({ children }: { children: ReactNode }) {
+ const [apiKey, setApiKey] = useState(null);
+ const [userId, setUserId] = useState(null);
+ const [authConfig, setAuthConfig] =
+ useState(fallbackAuthConfig);
+ const [bootstrap, setBootstrap] = useState(
+ null,
+ );
+ const [loading, setLoading] = useState(true);
+
+ const client = useMemo(
+ () =>
+ createMobileApiClient({
+ baseUrl: apiBaseUrl,
+ getToken: () => apiKey,
+ }),
+ [apiKey],
+ );
+ const publicClient = useMemo(
+ () =>
+ createMobileApiClient({
+ baseUrl: apiBaseUrl,
+ getToken: () => null,
+ }),
+ [],
+ );
+
+ const refresh = useCallback(async () => {
+ const response = await client.bootstrap();
+ setBootstrap(response);
+ }, [client]);
+
+ useEffect(() => {
+ let active = true;
+ const load = async () => {
+ try {
+ const [storedKey, storedUserId, nextAuthConfig] = await Promise.all([
+ SecureStore.getItemAsync(sessionKey),
+ SecureStore.getItemAsync(userIdKey),
+ publicClient.getAuthConfig().catch(() => fallbackAuthConfig),
+ ]);
+ if (!active) return;
+ setApiKey(storedKey);
+ setUserId(storedKey ? storedUserId : null);
+ setAuthConfig(nextAuthConfig);
+ if (!storedKey && storedUserId) {
+ SecureStore.deleteItemAsync(userIdKey).catch(() => {});
+ }
+ } finally {
+ if (active) setLoading(false);
+ }
+ };
+ load();
+ return () => {
+ active = false;
+ };
+ }, [publicClient]);
+
+ useEffect(() => {
+ if (!apiKey) {
+ setUserId(null);
+ setBootstrap(null);
+ return;
+ }
+
+ refresh().catch(() => {
+ setApiKey(null);
+ setUserId(null);
+ setBootstrap(null);
+ SecureStore.deleteItemAsync(sessionKey).catch(() => {});
+ SecureStore.deleteItemAsync(userIdKey).catch(() => {});
+ });
+ }, [apiKey, refresh]);
+
+ const storeSession = useCallback(
+ async (session: { apiKey: string; userId: string | null }) => {
+ await SecureStore.setItemAsync(sessionKey, session.apiKey);
+ if (session.userId) {
+ await SecureStore.setItemAsync(userIdKey, session.userId);
+ } else {
+ await SecureStore.deleteItemAsync(userIdKey);
+ }
+ setApiKey(session.apiKey);
+ setUserId(session.userId);
+ },
+ [],
+ );
+
+ const requestEmailCode = useCallback(
+ async (email: string) => {
+ await client.requestEmailCode(email);
+ },
+ [client],
+ );
+
+ const verifyEmailCode = useCallback(
+ async (email: string, code: string) => {
+ const session = await client.verifyEmailCode({ email, code });
+ await storeSession({
+ apiKey: session.apiKey,
+ userId: session.userId,
+ });
+ },
+ [client, storeSession],
+ );
+
+ const signInWithGoogle = useCallback(async () => {
+ const redirectUri = Linking.createURL("auth");
+ const result = await WebBrowser.openAuthSessionAsync(
+ createSessionRequestUrl(apiBaseUrl, redirectUri, "google"),
+ redirectUri,
+ );
+ if (result.type !== "success") return;
+
+ await storeSession(requireAuthRedirectSession(result.url));
+ }, [storeSession]);
+
+ const signInWithSso = useCallback(
+ async (organizationId: string) => {
+ const redirectUri = Linking.createURL("auth");
+ const result = await WebBrowser.openAuthSessionAsync(
+ createSessionRequestUrl(
+ apiBaseUrl,
+ redirectUri,
+ "workos",
+ organizationId,
+ ),
+ redirectUri,
+ );
+ if (result.type !== "success") return;
+
+ await storeSession(requireAuthRedirectSession(result.url));
+ },
+ [storeSession],
+ );
+
+ const signOut = useCallback(async () => {
+ if (apiKey) {
+ await client.revokeSession().catch(() => {});
+ }
+ await Promise.all([
+ SecureStore.deleteItemAsync(sessionKey),
+ SecureStore.deleteItemAsync(userIdKey),
+ ]);
+ setApiKey(null);
+ setUserId(null);
+ setBootstrap(null);
+ }, [apiKey, client]);
+
+ const setActiveOrganization = useCallback(
+ async (organizationId: string) => {
+ const nextBootstrap = await client.setActiveOrganization(organizationId);
+ setBootstrap(nextBootstrap);
+ },
+ [client],
+ );
+
+ const value = useMemo(
+ () => ({
+ status: loading ? "loading" : apiKey ? "signedIn" : "signedOut",
+ apiKey,
+ userId,
+ authConfig,
+ bootstrap,
+ client,
+ requestEmailCode,
+ verifyEmailCode,
+ signInWithGoogle,
+ signInWithSso,
+ signOut,
+ refresh,
+ setActiveOrganization,
+ }),
+ [
+ loading,
+ apiKey,
+ userId,
+ authConfig,
+ bootstrap,
+ client,
+ requestEmailCode,
+ verifyEmailCode,
+ signInWithGoogle,
+ signInWithSso,
+ signOut,
+ refresh,
+ setActiveOrganization,
+ ],
+ );
+
+ return {children};
+}
+
+export const useAuth = () => {
+ const value = useContext(AuthContext);
+ if (!value) throw new Error("useAuth must be used inside AuthProvider");
+ return value;
+};
diff --git a/apps/mobile/src/auth/AuthProvider.test.tsx b/apps/mobile/src/auth/AuthProvider.test.tsx
new file mode 100644
index 00000000000..0d9c232f2b3
--- /dev/null
+++ b/apps/mobile/src/auth/AuthProvider.test.tsx
@@ -0,0 +1,172 @@
+import React, { type ReactNode } from "react";
+import TestRenderer, { act } from "react-test-renderer";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import { AuthProvider, useAuth } from "./AuthContext";
+
+type HostProps = {
+ children?: ReactNode;
+};
+
+const secureStoreMock = vi.hoisted(() => ({
+ deleteItemAsync: vi.fn((_key: string) => Promise.resolve()),
+ getItemAsync: vi.fn((_key: string) => Promise.resolve(null as string | null)),
+ setItemAsync: vi.fn((_key: string, _value: string) => Promise.resolve()),
+}));
+
+const apiMock = vi.hoisted(() => ({
+ bootstrap: vi.fn(() =>
+ Promise.resolve({
+ activeOrganizationId: "org_123",
+ user: {
+ email: "richie@cap.so",
+ name: "Richie",
+ },
+ }),
+ ),
+ getAuthConfig: vi.fn(() =>
+ Promise.resolve({
+ googleAuthAvailable: true,
+ workosAuthAvailable: true,
+ }),
+ ),
+}));
+
+vi.mock("react-native", async () => {
+ const React = await import("react");
+
+ return {
+ View: ({ children }: HostProps) =>
+ React.createElement("View", null, children),
+ };
+});
+
+vi.mock("expo-constants", () => ({
+ default: {
+ expoConfig: {
+ extra: {},
+ },
+ },
+}));
+
+vi.mock("expo-linking", () => ({
+ createURL: vi.fn(() => "cap://auth"),
+}));
+
+vi.mock("expo-secure-store", () => secureStoreMock);
+
+vi.mock("expo-web-browser", () => ({
+ maybeCompleteAuthSession: vi.fn(),
+ openAuthSessionAsync: vi.fn(),
+}));
+
+vi.mock("@/api/mobile", () => ({
+ createMobileApiClient: vi.fn(() => ({
+ bootstrap: apiMock.bootstrap,
+ getAuthConfig: apiMock.getAuthConfig,
+ revokeSession: vi.fn(() => Promise.resolve({ success: true })),
+ setActiveOrganization: vi.fn(),
+ })),
+ createSessionRequestUrl: vi.fn(
+ () => "https://cap.so/api/mobile/session/request",
+ ),
+}));
+
+(
+ globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
+).IS_REACT_ACT_ENVIRONMENT = true;
+
+const flushMicrotasks = async () => {
+ await Promise.resolve();
+ await Promise.resolve();
+ await Promise.resolve();
+ await Promise.resolve();
+};
+
+const states: Array<{
+ apiKey: string | null;
+ status: string;
+ userId: string | null;
+}> = [];
+
+const Probe = () => {
+ const auth = useAuth();
+ states.push({
+ apiKey: auth.apiKey,
+ status: auth.status,
+ userId: auth.userId,
+ });
+ return null;
+};
+
+describe("AuthProvider", () => {
+ beforeEach(() => {
+ states.length = 0;
+ secureStoreMock.deleteItemAsync.mockClear();
+ secureStoreMock.getItemAsync.mockReset();
+ secureStoreMock.getItemAsync.mockResolvedValue(null);
+ secureStoreMock.setItemAsync.mockClear();
+ apiMock.bootstrap.mockReset();
+ apiMock.bootstrap.mockResolvedValue({
+ activeOrganizationId: "org_123",
+ user: {
+ email: "richie@cap.so",
+ name: "Richie",
+ },
+ });
+ apiMock.getAuthConfig.mockReset();
+ apiMock.getAuthConfig.mockResolvedValue({
+ googleAuthAvailable: true,
+ workosAuthAvailable: true,
+ });
+ });
+
+ it("clears an orphaned stored user id when no API key is stored", async () => {
+ secureStoreMock.getItemAsync.mockImplementation((key: string) =>
+ Promise.resolve(key === "cap.mobile.userId" ? "user_123" : null),
+ );
+
+ await act(async () => {
+ TestRenderer.create(
+ React.createElement(AuthProvider, null, React.createElement(Probe)),
+ );
+ await flushMicrotasks();
+ });
+
+ expect(states.at(-1)).toMatchObject({
+ apiKey: null,
+ status: "signedOut",
+ userId: null,
+ });
+ expect(secureStoreMock.deleteItemAsync).toHaveBeenCalledWith(
+ "cap.mobile.userId",
+ );
+ });
+
+ it("clears the stored user id when bootstrapping a stored session fails", async () => {
+ secureStoreMock.getItemAsync.mockImplementation((key: string) => {
+ if (key === "cap.mobile.apiKey") return Promise.resolve("key_123");
+ if (key === "cap.mobile.userId") return Promise.resolve("user_123");
+ return Promise.resolve(null);
+ });
+ apiMock.bootstrap.mockRejectedValueOnce(new Error("Session expired"));
+
+ await act(async () => {
+ TestRenderer.create(
+ React.createElement(AuthProvider, null, React.createElement(Probe)),
+ );
+ await flushMicrotasks();
+ });
+
+ expect(states.at(-1)).toMatchObject({
+ apiKey: null,
+ status: "signedOut",
+ userId: null,
+ });
+ expect(secureStoreMock.deleteItemAsync).toHaveBeenCalledWith(
+ "cap.mobile.apiKey",
+ );
+ expect(secureStoreMock.deleteItemAsync).toHaveBeenCalledWith(
+ "cap.mobile.userId",
+ );
+ });
+});
diff --git a/apps/mobile/src/auth/SignInPanel.test.tsx b/apps/mobile/src/auth/SignInPanel.test.tsx
new file mode 100644
index 00000000000..9970dfb9cb4
--- /dev/null
+++ b/apps/mobile/src/auth/SignInPanel.test.tsx
@@ -0,0 +1,1234 @@
+import React, { type ReactElement, type ReactNode } from "react";
+import TestRenderer, {
+ act,
+ type ReactTestRenderer,
+ type ReactTestRendererJSON,
+} from "react-test-renderer";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import { SignInPanel } from "./SignInPanel";
+
+type HostProps = {
+ children?: ReactNode;
+ [key: string]: unknown;
+};
+
+type JsonNode = ReactTestRendererJSON | ReactTestRendererJSON[] | string | null;
+
+const authFns = vi.hoisted(() => ({
+ authConfig: {
+ googleAuthAvailable: true,
+ workosAuthAvailable: true,
+ },
+ requestEmailCode: vi.fn(() => Promise.resolve()),
+ signInWithGoogle: vi.fn(),
+ signInWithSso: vi.fn(),
+ verifyEmailCode: vi.fn(),
+}));
+
+(
+ globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
+).IS_REACT_ACT_ENVIRONMENT = true;
+
+const renderTree = async (node: ReactElement): Promise => {
+ let renderer: ReactTestRenderer | null = null;
+ await act(async () => {
+ renderer = TestRenderer.create(node);
+ });
+ return (renderer as ReactTestRenderer | null)?.toJSON() ?? null;
+};
+
+const renderPanel = async (): Promise => {
+ let renderer: ReactTestRenderer | null = null;
+ await act(async () => {
+ renderer = TestRenderer.create(React.createElement(SignInPanel));
+ });
+ return renderer as unknown as ReactTestRenderer;
+};
+
+const getTextNodes = (node: JsonNode): string[] => {
+ if (!node) return [];
+ if (typeof node === "string") return [node];
+ if (Array.isArray(node)) return node.flatMap(getTextNodes);
+ return node.children?.flatMap(getTextNodes) ?? [];
+};
+
+const hasProp = (node: JsonNode, prop: string, value: unknown): boolean => {
+ if (!node || typeof node === "string") return false;
+ if (Array.isArray(node))
+ return node.some((item) => hasProp(item, prop, value));
+ if (node.props[prop] === value) return true;
+ return node.children?.some((child) => hasProp(child, prop, value)) ?? false;
+};
+
+const hasStyle = (
+ node: JsonNode,
+ expected: Record,
+): boolean => {
+ if (!node || typeof node === "string") return false;
+ if (Array.isArray(node)) return node.some((item) => hasStyle(item, expected));
+ const styles = Array.isArray(node.props.style)
+ ? node.props.style
+ : [node.props.style];
+ const resolved = Object.assign({}, ...styles.filter(Boolean));
+ if (
+ Object.entries(expected).every(([key, value]) => resolved[key] === value)
+ ) {
+ return true;
+ }
+ return node.children?.some((child) => hasStyle(child, expected)) ?? false;
+};
+
+const resolveStyle = (
+ style: unknown,
+ pressed = false,
+): Record => {
+ const resolved = typeof style === "function" ? style({ pressed }) : style;
+ const styles = Array.isArray(resolved) ? resolved : [resolved];
+ return Object.assign({}, ...styles.filter(Boolean));
+};
+
+vi.mock("react-native", async () => {
+ const React = await import("react");
+ const createHost =
+ (name: string) =>
+ ({ children, ...props }: HostProps) =>
+ React.createElement(name, props, children);
+ const TextInput = React.forwardRef(
+ ({ children, ...props }, ref) =>
+ React.createElement(
+ "TextInput",
+ { ...props, ref },
+ children as ReactNode,
+ ),
+ );
+
+ return {
+ ActivityIndicator: createHost("ActivityIndicator"),
+ Pressable: createHost("Pressable"),
+ StyleSheet: {
+ create: >(styles: T) => styles,
+ hairlineWidth: 1,
+ },
+ Text: createHost("Text"),
+ TextInput,
+ View: createHost("View"),
+ };
+});
+
+vi.mock("expo-symbols", () => ({
+ SymbolView: () => null,
+}));
+
+vi.mock("expo-web-browser", () => ({
+ openBrowserAsync: vi.fn(),
+}));
+
+vi.mock("@/components/GlassSurface", async () => {
+ const React = await import("react");
+ return {
+ GlassSurface: ({ children }: { children?: ReactNode }) =>
+ React.createElement("GlassSurface", null, children),
+ };
+});
+
+vi.mock("react-native-svg", async () => {
+ const React = await import("react");
+ const Svg = ({ children, ...props }: HostProps) =>
+ React.createElement("Svg", props, children);
+
+ return {
+ default: Svg,
+ Path: (props: HostProps) => React.createElement("Path", props),
+ Rect: (props: HostProps) => React.createElement("Rect", props),
+ };
+});
+
+vi.mock("@/auth/AuthContext", () => ({
+ apiBaseUrl: "https://cap.so",
+ useAuth: () => ({
+ authConfig: authFns.authConfig,
+ requestEmailCode: authFns.requestEmailCode,
+ signInWithGoogle: authFns.signInWithGoogle,
+ signInWithSso: authFns.signInWithSso,
+ verifyEmailCode: authFns.verifyEmailCode,
+ }),
+}));
+
+vi.mock("@/api/mobile", () => ({
+ MobileApiError: class MobileApiError extends Error {
+ status: number;
+ payload: unknown;
+
+ constructor(message: string, status: number, payload: unknown) {
+ super(message);
+ this.status = status;
+ this.payload = payload;
+ }
+ },
+}));
+
+describe("SignInPanel", () => {
+ beforeEach(() => {
+ authFns.authConfig.googleAuthAvailable = true;
+ authFns.authConfig.workosAuthAvailable = true;
+ authFns.requestEmailCode.mockReset();
+ authFns.requestEmailCode.mockResolvedValue(undefined);
+ authFns.verifyEmailCode.mockReset();
+ authFns.verifyEmailCode.mockResolvedValue(undefined);
+ authFns.signInWithGoogle.mockReset();
+ authFns.signInWithGoogle.mockResolvedValue(undefined);
+ authFns.signInWithSso.mockReset();
+ authFns.signInWithSso.mockResolvedValue(undefined);
+ });
+
+ it("renders the Cap web login surface", async () => {
+ const tree = await renderTree(React.createElement(SignInPanel));
+ const text = getTextNodes(tree);
+
+ expect(text).toContain("Sign in to Cap");
+ expect(text).toContain("Your videos, organized and ready to share.");
+ expect(hasProp(tree, "viewBox", "0 0 40 40")).toBe(true);
+ expect(hasProp(tree, "rx", 8)).toBe(true);
+ expect(hasProp(tree, "placeholder", "tim@apple.com")).toBe(true);
+ expect(hasProp(tree, "accessibilityLabel", "Email address")).toBe(true);
+ expect(
+ hasProp(
+ tree,
+ "accessibilityHint",
+ "Enter your email to request a verification code",
+ ),
+ ).toBe(true);
+ expect(hasProp(tree, "clearButtonMode", "while-editing")).toBe(true);
+ expect(hasProp(tree, "enablesReturnKeyAutomatically", true)).toBe(true);
+ expect(hasProp(tree, "selectionColor", "#0090ff")).toBe(true);
+ expect(
+ hasProp(
+ tree,
+ "accessibilityHint",
+ "Enter a valid email address to continue",
+ ),
+ ).toBe(true);
+ expect(text).toContain("Login with email");
+ expect(text).toContain("Sign up here");
+ expect(
+ hasProp(tree, "accessibilityHint", "Opens sign up in a browser sheet"),
+ ).toBe(true);
+ expect(hasProp(tree, "accessibilityRole", "link")).toBe(true);
+ expect(text).toContain("OR");
+ expect(text).toContain("Login with Google");
+ expect(text).toContain("Login with SAML SSO");
+ expect(text).toContain("Terms of Service");
+ expect(text).toContain("Privacy Policy");
+ expect(
+ hasProp(
+ tree,
+ "accessibilityHint",
+ "Opens Terms of Service in a browser sheet",
+ ),
+ ).toBe(true);
+ expect(
+ hasProp(
+ tree,
+ "accessibilityHint",
+ "Opens Privacy Policy in a browser sheet",
+ ),
+ ).toBe(true);
+ });
+
+ it("hides unavailable provider options", async () => {
+ authFns.authConfig.googleAuthAvailable = false;
+ authFns.authConfig.workosAuthAvailable = false;
+
+ const tree = await renderTree(React.createElement(SignInPanel));
+ const text = getTextNodes(tree);
+
+ expect(text).toContain("Login with email");
+ expect(text).not.toContain("OR");
+ expect(text).not.toContain("Login with Google");
+ expect(text).not.toContain("Login with SAML SSO");
+ });
+
+ it("shows the native SSO organization step", async () => {
+ const renderer = await renderPanel();
+ const [ssoButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Login with SAML SSO",
+ });
+ if (!ssoButton) throw new Error("SSO button was not rendered");
+
+ await act(async () => {
+ ssoButton.props.onPress();
+ });
+
+ const tree = renderer.toJSON();
+ const text = getTextNodes(tree);
+
+ expect(hasProp(tree, "placeholder", "Enter your Organization ID...")).toBe(
+ true,
+ );
+ expect(hasProp(tree, "accessibilityLabel", "Organization ID")).toBe(true);
+ expect(
+ hasProp(
+ tree,
+ "accessibilityHint",
+ "Enter your organization ID to continue with SSO",
+ ),
+ ).toBe(true);
+ expect(hasProp(tree, "clearButtonMode", "while-editing")).toBe(true);
+ expect(hasProp(tree, "selectionColor", "#0090ff")).toBe(true);
+ expect(
+ hasProp(
+ tree,
+ "accessibilityHint",
+ "Enter your organization ID to continue",
+ ),
+ ).toBe(true);
+ const [continueButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Continue with SSO",
+ });
+ expect(continueButton?.props.accessibilityValue).toEqual({
+ text: "Organization ID required",
+ });
+ expect(text).toContain("Continue with SSO");
+ expect(text).toContain("Back");
+ });
+
+ it("locks the SSO back button while starting sign in", async () => {
+ let resolveSso: (() => void) | null = null;
+ authFns.signInWithSso.mockImplementationOnce(
+ () =>
+ new Promise((resolve) => {
+ resolveSso = resolve;
+ }),
+ );
+ const renderer = await renderPanel();
+ const [ssoButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Login with SAML SSO",
+ });
+ if (!ssoButton) throw new Error("SSO button was not rendered");
+
+ await act(async () => {
+ ssoButton.props.onPress();
+ });
+
+ const [organizationInput] = renderer.root.findAllByProps({
+ accessibilityLabel: "Organization ID",
+ });
+ if (!organizationInput)
+ throw new Error("Organization ID input was not rendered");
+ await act(async () => {
+ organizationInput.props.onChangeText("acme");
+ });
+
+ const [continueButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Continue with SSO",
+ });
+ if (!continueButton)
+ throw new Error("SSO continue button was not rendered");
+ expect(continueButton.props.accessibilityHint).toBe(
+ "Starts SAML SSO for this organization",
+ );
+ await act(async () => {
+ void continueButton.props.onPress();
+ await Promise.resolve();
+ });
+
+ const [loadingBackButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Back",
+ });
+ const [loadingOrganizationInput] = renderer.root.findAllByProps({
+ accessibilityLabel: "Organization ID",
+ });
+ const [loadingContinueButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Continue with SSO",
+ });
+ expect(loadingBackButton?.props.disabled).toBe(true);
+ expect(loadingBackButton?.props.accessibilityState).toEqual({
+ disabled: true,
+ });
+ expect(loadingBackButton?.props.accessibilityHint).toBe(
+ "Sign in is in progress",
+ );
+ expect(loadingBackButton?.props.accessibilityValue).toEqual({
+ text: "Starting SAML SSO sign in",
+ });
+ expect(loadingOrganizationInput?.props.editable).toBe(false);
+ expect(loadingOrganizationInput?.props.accessibilityState).toEqual({
+ disabled: true,
+ });
+ expect(loadingOrganizationInput?.props.accessibilityValue).toEqual({
+ text: "Starting SAML SSO sign in",
+ });
+ expect(loadingContinueButton?.props.accessibilityHint).toBe(
+ "SAML SSO sign in is starting",
+ );
+ expect(loadingContinueButton?.props.accessibilityState).toEqual({
+ busy: true,
+ disabled: true,
+ });
+ expect(loadingContinueButton?.props.accessibilityValue).toEqual({
+ text: "Starting SAML SSO sign in",
+ });
+ expect(getTextNodes(renderer.toJSON())).toContain("Continue with SSO");
+ expect(getTextNodes(renderer.toJSON())).not.toContain("Starting SSO...");
+
+ await act(async () => {
+ organizationInput.props.onChangeText("changed");
+ });
+
+ const [unchangedOrganizationInput] = renderer.root.findAllByProps({
+ accessibilityLabel: "Organization ID",
+ });
+ expect(unchangedOrganizationInput?.props.value).toBe("acme");
+
+ await act(async () => {
+ resolveSso?.();
+ await Promise.resolve();
+ });
+ });
+
+ it("does not request an email code for an invalid email address", async () => {
+ const renderer = await renderPanel();
+ const [emailInput] = renderer.root.findAllByProps({
+ placeholder: "tim@apple.com",
+ });
+ const [emailButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Login with email",
+ });
+ if (!emailInput || !emailButton) {
+ throw new Error("Email sign in controls were not rendered");
+ }
+
+ await act(async () => {
+ emailInput.props.onChangeText("richie");
+ });
+ expect(emailButton.props.accessibilityHint).toBe(
+ "Enter a valid email address to continue",
+ );
+ const [invalidEmailButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Login with email",
+ });
+ expect(invalidEmailButton?.props.accessibilityValue).toEqual({
+ text: "Email address is not valid",
+ });
+ expect(emailButton.props.accessibilityState).toEqual({
+ busy: false,
+ disabled: true,
+ });
+ await act(async () => {
+ await emailButton.props.onPress();
+ });
+
+ expect(authFns.requestEmailCode).not.toHaveBeenCalled();
+ });
+
+ it("locks the email field while requesting a verification code", async () => {
+ let resolveRequest: (() => void) | null = null;
+ authFns.requestEmailCode.mockImplementationOnce(
+ () =>
+ new Promise((resolve) => {
+ resolveRequest = resolve;
+ }),
+ );
+ const renderer = await renderPanel();
+ const [emailInput] = renderer.root.findAllByProps({
+ placeholder: "tim@apple.com",
+ });
+ const [emailButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Login with email",
+ });
+ if (!emailInput || !emailButton) {
+ throw new Error("Email sign in controls were not rendered");
+ }
+
+ await act(async () => {
+ emailInput.props.onChangeText("richie@cap.so");
+ });
+ await act(async () => {
+ void emailButton.props.onPress();
+ await Promise.resolve();
+ });
+
+ const [loadingEmailInput] = renderer.root.findAllByProps({
+ placeholder: "tim@apple.com",
+ });
+ const [loadingEmailButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Login with email",
+ });
+ expect(loadingEmailInput?.props.editable).toBe(false);
+ expect(loadingEmailInput?.props.accessibilityState).toEqual({
+ disabled: true,
+ });
+ expect(loadingEmailInput?.props.accessibilityValue).toEqual({
+ text: "Sending verification code",
+ });
+ expect(loadingEmailButton?.props.accessibilityHint).toBe(
+ "Sending verification code",
+ );
+ expect(loadingEmailButton?.props.accessibilityState).toEqual({
+ busy: true,
+ disabled: true,
+ });
+ expect(loadingEmailButton?.props.accessibilityValue).toEqual({
+ text: "Sending verification code",
+ });
+ expect(getTextNodes(renderer.toJSON())).toContain("Login with email");
+ expect(getTextNodes(renderer.toJSON())).not.toContain("Sending...");
+
+ await act(async () => {
+ emailInput.props.onChangeText("changed@cap.so");
+ });
+
+ const [unchangedEmailInput] = renderer.root.findAllByProps({
+ placeholder: "tim@apple.com",
+ });
+ expect(unchangedEmailInput?.props.value).toBe("richie@cap.so");
+
+ await act(async () => {
+ resolveRequest?.();
+ await Promise.resolve();
+ });
+ });
+
+ it("locks browser links while requesting a verification code", async () => {
+ let resolveRequest: (() => void) | null = null;
+ authFns.requestEmailCode.mockImplementationOnce(
+ () =>
+ new Promise((resolve) => {
+ resolveRequest = resolve;
+ }),
+ );
+ const renderer = await renderPanel();
+ const [emailInput] = renderer.root.findAllByProps({
+ placeholder: "tim@apple.com",
+ });
+ const [emailButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Login with email",
+ });
+ if (!emailInput || !emailButton) {
+ throw new Error("Email sign in controls were not rendered");
+ }
+
+ await act(async () => {
+ emailInput.props.onChangeText("richie@cap.so");
+ });
+ await act(async () => {
+ void emailButton.props.onPress();
+ await Promise.resolve();
+ });
+
+ const WebBrowser = await import("expo-web-browser");
+ const openBrowserAsync = vi.mocked(WebBrowser.openBrowserAsync);
+ openBrowserAsync.mockClear();
+ const [signupLink, termsLink, privacyLink] = renderer.root.findAllByProps({
+ accessibilityRole: "link",
+ });
+ if (!signupLink || !termsLink || !privacyLink) {
+ throw new Error("Browser links were not rendered");
+ }
+
+ expect(signupLink.props.accessibilityState).toEqual({ disabled: true });
+ expect(signupLink.props.accessibilityHint).toBe("Sign in is in progress");
+ expect(signupLink.props.accessibilityValue).toEqual({
+ text: "Sending verification code",
+ });
+ expect(termsLink.props.accessibilityState).toEqual({ disabled: true });
+ expect(termsLink.props.accessibilityHint).toBe("Sign in is in progress");
+ expect(termsLink.props.accessibilityValue).toEqual({
+ text: "Sending verification code",
+ });
+ expect(privacyLink.props.accessibilityState).toEqual({ disabled: true });
+ expect(privacyLink.props.accessibilityHint).toBe("Sign in is in progress");
+ expect(privacyLink.props.accessibilityValue).toEqual({
+ text: "Sending verification code",
+ });
+
+ await act(async () => {
+ signupLink.props.onPress();
+ termsLink.props.onPress();
+ privacyLink.props.onPress();
+ });
+
+ expect(openBrowserAsync).not.toHaveBeenCalled();
+
+ await act(async () => {
+ resolveRequest?.();
+ await Promise.resolve();
+ });
+ });
+
+ it("deduplicates sign-in actions while a provider request is pending", async () => {
+ let resolveGoogle: (() => void) | null = null;
+ authFns.signInWithGoogle.mockImplementationOnce(
+ () =>
+ new Promise((resolve) => {
+ resolveGoogle = resolve;
+ }),
+ );
+ const renderer = await renderPanel();
+ const [emailInput] = renderer.root.findAllByProps({
+ placeholder: "tim@apple.com",
+ });
+ if (!emailInput) throw new Error("Email input was not rendered");
+
+ await act(async () => {
+ emailInput.props.onChangeText("richie@cap.so");
+ });
+
+ const [emailButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Login with email",
+ });
+ const [googleButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Login with Google",
+ });
+ const [ssoButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Login with SAML SSO",
+ });
+ const [signupLink] = renderer.root.findAllByProps({
+ accessibilityHint: "Opens sign up in a browser sheet",
+ });
+ const [termsLink] = renderer.root.findAllByProps({
+ accessibilityHint: "Opens Terms of Service in a browser sheet",
+ });
+ const [privacyLink] = renderer.root.findAllByProps({
+ accessibilityHint: "Opens Privacy Policy in a browser sheet",
+ });
+ if (
+ !emailButton ||
+ !googleButton ||
+ !ssoButton ||
+ !signupLink ||
+ !termsLink ||
+ !privacyLink
+ ) {
+ throw new Error("Sign-in actions were not rendered");
+ }
+
+ await act(async () => {
+ void googleButton.props.onPress();
+ await Promise.resolve();
+ });
+
+ const [loadingEmailButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Login with email",
+ });
+ const [loadingGoogleButton] = renderer.root.findAll(
+ (node) =>
+ node.props.accessibilityLabel === "Login with Google" &&
+ node.props.accessibilityHint === "Google sign in is starting",
+ );
+ const [loadingSsoButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Login with SAML SSO",
+ });
+ expect(loadingEmailButton?.props.disabled).toBe(true);
+ expect(loadingEmailButton?.props.accessibilityHint).toBe(
+ "Sign in is in progress",
+ );
+ expect(loadingEmailButton?.props.accessibilityValue).toEqual({
+ text: "Starting Google sign in",
+ });
+ expect(loadingGoogleButton?.props.accessibilityHint).toBe(
+ "Google sign in is starting",
+ );
+ expect(loadingGoogleButton?.props.accessibilityState).toEqual({
+ busy: true,
+ disabled: true,
+ });
+ expect(loadingGoogleButton?.props.accessibilityValue).toEqual({
+ text: "Starting Google sign in",
+ });
+ expect(getTextNodes(renderer.toJSON())).toContain("Login with Google");
+ expect(getTextNodes(renderer.toJSON())).not.toContain("Starting Google...");
+ expect(loadingSsoButton?.props.disabled).toBe(true);
+ expect(loadingSsoButton?.props.accessibilityHint).toBe(
+ "Sign in is in progress",
+ );
+ expect(loadingSsoButton?.props.accessibilityValue).toEqual({
+ text: "Starting Google sign in",
+ });
+
+ const WebBrowser = await import("expo-web-browser");
+ const openBrowserAsync = vi.mocked(WebBrowser.openBrowserAsync);
+ openBrowserAsync.mockClear();
+
+ await act(async () => {
+ googleButton.props.onPress();
+ emailButton.props.onPress();
+ ssoButton.props.onPress();
+ signupLink.props.onPress();
+ termsLink.props.onPress();
+ privacyLink.props.onPress();
+ await Promise.resolve();
+ });
+
+ expect(authFns.signInWithGoogle).toHaveBeenCalledTimes(1);
+ expect(authFns.requestEmailCode).not.toHaveBeenCalled();
+ expect(openBrowserAsync).not.toHaveBeenCalled();
+ expect(getTextNodes(renderer.toJSON())).not.toContain("Continue with SSO");
+
+ await act(async () => {
+ resolveGoogle?.();
+ await Promise.resolve();
+ });
+ });
+
+ it("marks a failed Google sign-in as retryable", async () => {
+ authFns.signInWithGoogle.mockRejectedValueOnce(
+ new Error("Google unavailable"),
+ );
+ const renderer = await renderPanel();
+ const [googleButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Login with Google",
+ });
+ if (!googleButton) throw new Error("Google button was not rendered");
+
+ await act(async () => {
+ await googleButton.props.onPress();
+ });
+
+ expect(getTextNodes(renderer.toJSON())).toContain("Google unavailable");
+ const [retryGoogleButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Retry Google sign in",
+ });
+ const [errorAlert] = renderer.root.findAllByProps({
+ accessibilityRole: "alert",
+ });
+ expect(retryGoogleButton?.props.accessibilityHint).toBe(
+ "Google unavailable",
+ );
+ expect(retryGoogleButton?.props.accessibilityValue).toEqual({
+ text: "Google unavailable",
+ });
+ expect(errorAlert?.props.accessibilityLabel).toBe(
+ "Sign-in error: Google unavailable",
+ );
+
+ await act(async () => {
+ await retryGoogleButton?.props.onPress();
+ });
+
+ expect(authFns.signInWithGoogle).toHaveBeenCalledTimes(2);
+ expect(getTextNodes(renderer.toJSON())).not.toContain("Google unavailable");
+ expect(
+ renderer.root.findAllByProps({
+ accessibilityLabel: "Retry Google sign in",
+ }),
+ ).toHaveLength(0);
+ });
+
+ it("marks a failed SSO sign-in on the organization step", async () => {
+ authFns.signInWithSso.mockRejectedValueOnce(new Error("SSO unavailable"));
+ const renderer = await renderPanel();
+ const [ssoButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Login with SAML SSO",
+ });
+ if (!ssoButton) throw new Error("SSO button was not rendered");
+
+ await act(async () => {
+ ssoButton.props.onPress();
+ });
+
+ const [organizationInput] = renderer.root.findAllByProps({
+ accessibilityLabel: "Organization ID",
+ });
+ if (!organizationInput)
+ throw new Error("Organization ID input was not rendered");
+ await act(async () => {
+ organizationInput.props.onChangeText("acme");
+ });
+
+ const [continueButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Continue with SSO",
+ });
+ if (!continueButton)
+ throw new Error("SSO continue button was not rendered");
+ await act(async () => {
+ await continueButton.props.onPress();
+ });
+
+ expect(getTextNodes(renderer.toJSON())).toContain("SSO unavailable");
+ const [organizationInputAfterError] = renderer.root.findAllByProps({
+ accessibilityLabel: "Organization ID",
+ });
+ const [retrySsoButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Retry SAML SSO sign in",
+ });
+ const [errorAlert] = renderer.root.findAllByProps({
+ accessibilityRole: "alert",
+ });
+ expect(organizationInputAfterError?.props.accessibilityHint).toBe(
+ "SSO unavailable",
+ );
+ expect(retrySsoButton?.props.accessibilityHint).toBe("SSO unavailable");
+ expect(retrySsoButton?.props.accessibilityValue).toEqual({
+ text: "SSO unavailable",
+ });
+ expect(errorAlert?.props.accessibilityLabel).toBe(
+ "Sign-in error: SSO unavailable",
+ );
+ expect(
+ hasStyle(renderer.toJSON(), {
+ borderColor: "#f4a9aa",
+ }),
+ ).toBe(true);
+
+ await act(async () => {
+ await retrySsoButton?.props.onPress();
+ });
+
+ expect(authFns.signInWithSso).toHaveBeenCalledTimes(2);
+ expect(getTextNodes(renderer.toJSON())).not.toContain("SSO unavailable");
+ expect(
+ renderer.root.findAllByProps({
+ accessibilityLabel: "Retry SAML SSO sign in",
+ }),
+ ).toHaveLength(0);
+ });
+
+ it("shows the right error when an email is not allowed", async () => {
+ const { MobileApiError } = await import("@/api/mobile");
+ authFns.requestEmailCode.mockRejectedValueOnce(
+ new MobileApiError("Forbidden", 403, null),
+ );
+ const renderer = await renderPanel();
+ const [emailInput] = renderer.root.findAllByProps({
+ placeholder: "tim@apple.com",
+ });
+ const [emailButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Login with email",
+ });
+ if (!emailInput || !emailButton) {
+ throw new Error("Email sign in controls were not rendered");
+ }
+
+ await act(async () => {
+ emailInput.props.onChangeText("blocked@example.com");
+ });
+ await act(async () => {
+ await emailButton.props.onPress();
+ });
+
+ expect(getTextNodes(renderer.toJSON())).toContain(
+ "This email cannot be used to sign in to Cap.",
+ );
+ const [emailInputAfterError] = renderer.root.findAllByProps({
+ accessibilityLabel: "Email address",
+ });
+ const [errorAlert] = renderer.root.findAllByProps({
+ accessibilityRole: "alert",
+ });
+ expect(emailInputAfterError?.props.accessibilityHint).toBe(
+ "This email cannot be used to sign in to Cap.",
+ );
+ const [emailButtonAfterError] = renderer.root.findAllByProps({
+ accessibilityLabel: "Login with email",
+ });
+ expect(emailButtonAfterError?.props.accessibilityValue).toEqual({
+ text: "This email cannot be used to sign in to Cap.",
+ });
+ expect(errorAlert?.props.accessibilityLabel).toBe(
+ "Sign-in error: This email cannot be used to sign in to Cap.",
+ );
+ expect(errorAlert?.props.accessibilityLiveRegion).toBe("polite");
+ expect(
+ hasStyle(renderer.toJSON(), {
+ borderColor: "#f4a9aa",
+ }),
+ ).toBe(true);
+ expect(
+ hasStyle(renderer.toJSON(), {
+ backgroundColor: "#fffcfc",
+ borderColor: "#fdbdbe",
+ }),
+ ).toBe(true);
+ });
+
+ it("switches to a web-like verification code step after requesting email", async () => {
+ const renderer = await renderPanel();
+ const [emailInput] = renderer.root.findAllByProps({
+ placeholder: "tim@apple.com",
+ });
+ const [emailButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Login with email",
+ });
+ if (!emailInput || !emailButton) {
+ throw new Error("Email sign in controls were not rendered");
+ }
+
+ await act(async () => {
+ emailInput.props.onChangeText("richie@cap.so");
+ });
+ await act(async () => {
+ await emailButton.props.onPress();
+ });
+
+ const tree = renderer.toJSON();
+ const text = getTextNodes(tree);
+
+ expect(authFns.requestEmailCode).toHaveBeenCalledWith("richie@cap.so");
+ expect(text).toContain("Back");
+ expect(text).toContain("Enter verification code");
+ expect(text).toContain("We sent a 6-digit code to richie@cap.so");
+ expect(text).toContain("Verify Code");
+ expect(text).toContain("Resend in 30s");
+ expect(text).toContain("Terms of Service");
+ expect(hasProp(tree, "accessibilityLabel", "Verification code")).toBe(true);
+ const [codeTarget] = renderer.root.findAllByProps({
+ accessibilityLabel: "Verification code",
+ });
+ const [verifyButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Verify Code",
+ });
+ const [resendButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Resend in 30s",
+ });
+ const [codeInput] = renderer.root.findAllByProps({
+ textContentType: "oneTimeCode",
+ });
+ if (!codeInput) throw new Error("One-time code input was not rendered");
+ expect(codeTarget?.props.accessibilityValue).toEqual({
+ text: "0 of 6 digits entered",
+ });
+ expect(verifyButton?.props.accessibilityValue).toEqual({
+ text: "0 of 6 digits entered",
+ });
+ expect(resolveStyle(verifyButton?.props.style)).toMatchObject({
+ backgroundColor: "#d9d9d9",
+ borderColor: "#d9d9d9",
+ });
+ expect(resendButton?.props.accessibilityValue).toEqual({
+ text: "Wait 30 seconds",
+ });
+ expect(hasStyle(tree, { gap: 20, paddingTop: 2 })).toBe(true);
+ expect(
+ hasProp(tree, "accessibilityHint", "Tap to enter the 6-digit code"),
+ ).toBe(true);
+ expect(
+ hasProp(tree, "accessibilityHint", "Enter the 6-digit code to continue"),
+ ).toBe(true);
+ expect(hasProp(tree, "accessibilityHint", "Returns to email sign in")).toBe(
+ true,
+ );
+ expect(hasProp(tree, "accessibilityElementsHidden", true)).toBe(true);
+ expect(hasProp(tree, "accessible", false)).toBe(true);
+ expect(
+ hasProp(tree, "importantForAccessibility", "no-hide-descendants"),
+ ).toBe(true);
+ expect(hasProp(tree, "selectionColor", "#0090ff")).toBe(true);
+ await act(async () => {
+ codeInput.props.onFocus();
+ });
+ expect(
+ hasStyle(renderer.toJSON(), {
+ backgroundColor: "#f9f9f9",
+ borderColor: "#0090ff",
+ shadowOpacity: 0.12,
+ }),
+ ).toBe(true);
+ await act(async () => {
+ codeInput.props.onBlur();
+ });
+ expect(
+ hasStyle(renderer.toJSON(), {
+ backgroundColor: "#f9f9f9",
+ borderColor: "#0090ff",
+ shadowOpacity: 0.12,
+ }),
+ ).toBe(false);
+ expect(text).not.toContain("Login with Google");
+ });
+
+ it("verifies an autofilled one-time code when all six digits are entered", async () => {
+ const renderer = await renderPanel();
+ const [emailInput] = renderer.root.findAllByProps({
+ placeholder: "tim@apple.com",
+ });
+ const [emailButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Login with email",
+ });
+ if (!emailInput || !emailButton) {
+ throw new Error("Email sign in controls were not rendered");
+ }
+
+ await act(async () => {
+ emailInput.props.onChangeText("richie@cap.so");
+ });
+ await act(async () => {
+ await emailButton.props.onPress();
+ });
+
+ const [codeInput] = renderer.root.findAllByProps({
+ textContentType: "oneTimeCode",
+ });
+ if (!codeInput) throw new Error("One-time code input was not rendered");
+
+ await act(async () => {
+ codeInput.props.onChangeText("123-456");
+ await Promise.resolve();
+ });
+
+ expect(authFns.verifyEmailCode).toHaveBeenCalledWith(
+ "richie@cap.so",
+ "123456",
+ );
+ });
+
+ it("marks invalid verification codes on the visible code target", async () => {
+ const { MobileApiError } = await import("@/api/mobile");
+ authFns.verifyEmailCode.mockRejectedValueOnce(
+ new MobileApiError("Forbidden", 403, null),
+ );
+ const renderer = await renderPanel();
+ const [emailInput] = renderer.root.findAllByProps({
+ placeholder: "tim@apple.com",
+ });
+ const [emailButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Login with email",
+ });
+ if (!emailInput || !emailButton) {
+ throw new Error("Email sign in controls were not rendered");
+ }
+
+ await act(async () => {
+ emailInput.props.onChangeText("richie@cap.so");
+ });
+ await act(async () => {
+ await emailButton.props.onPress();
+ });
+
+ const [codeInput] = renderer.root.findAllByProps({
+ textContentType: "oneTimeCode",
+ });
+ if (!codeInput) throw new Error("One-time code input was not rendered");
+
+ await act(async () => {
+ codeInput.props.onChangeText("123456");
+ await Promise.resolve();
+ });
+
+ expect(getTextNodes(renderer.toJSON())).toContain(
+ "That code is invalid or expired.",
+ );
+ const [codeTarget] = renderer.root.findAllByProps({
+ accessibilityLabel: "Verification code",
+ });
+ const [errorAlert] = renderer.root.findAllByProps({
+ accessibilityRole: "alert",
+ });
+ expect(codeTarget?.props.accessibilityHint).toBe(
+ "That code is invalid or expired.",
+ );
+ expect(codeTarget?.props.accessibilityValue).toEqual({
+ text: "0 of 6 digits entered",
+ });
+ expect(errorAlert?.props.accessibilityLabel).toBe(
+ "Sign-in error: That code is invalid or expired.",
+ );
+ expect(
+ hasStyle(renderer.toJSON(), {
+ backgroundColor: "#fffcfc",
+ borderColor: "#f4a9aa",
+ }),
+ ).toBe(true);
+ });
+
+ it("locks the visible code entry target while verifying", async () => {
+ let resolveVerify: (() => void) | null = null;
+ authFns.verifyEmailCode.mockImplementationOnce(
+ () =>
+ new Promise((resolve) => {
+ resolveVerify = resolve;
+ }),
+ );
+ const renderer = await renderPanel();
+ const [emailInput] = renderer.root.findAllByProps({
+ placeholder: "tim@apple.com",
+ });
+ const [emailButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Login with email",
+ });
+ if (!emailInput || !emailButton) {
+ throw new Error("Email sign in controls were not rendered");
+ }
+
+ await act(async () => {
+ emailInput.props.onChangeText("richie@cap.so");
+ });
+ await act(async () => {
+ await emailButton.props.onPress();
+ });
+
+ const [codeInput] = renderer.root.findAllByProps({
+ textContentType: "oneTimeCode",
+ });
+ if (!codeInput) throw new Error("One-time code input was not rendered");
+
+ await act(async () => {
+ codeInput.props.onChangeText("123456");
+ await Promise.resolve();
+ });
+
+ const [codeTarget] = renderer.root.findAllByProps({
+ accessibilityLabel: "Verification code",
+ });
+ const [loadingBackButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Back",
+ });
+ const [loadingCodeInput] = renderer.root.findAllByProps({
+ textContentType: "oneTimeCode",
+ });
+ const [loadingVerifyButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Verify Code",
+ });
+ expect(codeTarget?.props.disabled).toBe(true);
+ expect(codeTarget?.props.accessibilityState).toEqual({
+ busy: true,
+ disabled: true,
+ });
+ expect(codeTarget?.props.accessibilityHint).toBe("Verifying code");
+ expect(codeTarget?.props.accessibilityValue).toEqual({
+ text: "Verifying code",
+ });
+ expect(loadingBackButton?.props.accessibilityHint).toBe(
+ "Sign in is in progress",
+ );
+ expect(loadingBackButton?.props.accessibilityValue).toEqual({
+ text: "Verifying code",
+ });
+ expect(loadingCodeInput?.props.editable).toBe(false);
+ expect(loadingVerifyButton?.props.accessibilityHint).toBe("Verifying code");
+ expect(loadingVerifyButton?.props.accessibilityValue).toEqual({
+ text: "Verifying code",
+ });
+ expect(loadingVerifyButton?.props.accessibilityState).toEqual({
+ busy: true,
+ disabled: true,
+ });
+ expect(getTextNodes(renderer.toJSON())).not.toContain("Verifying...");
+
+ await act(async () => {
+ codeInput.props.onChangeText("654321");
+ await Promise.resolve();
+ });
+
+ const [unchangedCodeInput] = renderer.root.findAllByProps({
+ textContentType: "oneTimeCode",
+ });
+ expect(unchangedCodeInput?.props.value).toBe("123456");
+ expect(authFns.verifyEmailCode).toHaveBeenCalledTimes(1);
+
+ await act(async () => {
+ resolveVerify?.();
+ await Promise.resolve();
+ });
+ });
+
+ it("prevents repeated email code requests during the resend cooldown", async () => {
+ const renderer = await renderPanel();
+ const [emailInput] = renderer.root.findAllByProps({
+ placeholder: "tim@apple.com",
+ });
+ const [emailButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Login with email",
+ });
+ if (!emailInput || !emailButton) {
+ throw new Error("Email sign in controls were not rendered");
+ }
+
+ await act(async () => {
+ emailInput.props.onChangeText("richie@cap.so");
+ });
+ await act(async () => {
+ await emailButton.props.onPress();
+ });
+
+ const [resendButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Resend in 30s",
+ });
+ if (!resendButton) throw new Error("Resend control was not rendered");
+
+ expect(resendButton.props.accessibilityState).toEqual({
+ busy: false,
+ disabled: true,
+ });
+ expect(resendButton.props.accessibilityHint).toBe(
+ "Wait 30 seconds before requesting a new code",
+ );
+ expect(resendButton.props.accessibilityValue).toEqual({
+ text: "Wait 30 seconds",
+ });
+ expect(resendButton.props.hitSlop).toBe(6);
+ expect(
+ hasStyle(renderer.toJSON(), {
+ color: "#8d8d8d",
+ textDecorationLine: "none",
+ }),
+ ).toBe(true);
+
+ await act(async () => {
+ await resendButton.props.onPress();
+ });
+
+ expect(authFns.requestEmailCode).toHaveBeenCalledTimes(1);
+ expect(getTextNodes(renderer.toJSON())).toContain(
+ "Please wait 30 seconds before requesting a new code.",
+ );
+ });
+
+ it("allows a corrected email to request a code without waiting for the previous cooldown", async () => {
+ const renderer = await renderPanel();
+ const [emailInput] = renderer.root.findAllByProps({
+ placeholder: "tim@apple.com",
+ });
+ const [emailButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Login with email",
+ });
+ if (!emailInput || !emailButton) {
+ throw new Error("Email sign in controls were not rendered");
+ }
+
+ await act(async () => {
+ emailInput.props.onChangeText("wrong@cap.so");
+ });
+ await act(async () => {
+ await emailButton.props.onPress();
+ });
+
+ const [backButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Back",
+ });
+ if (!backButton) throw new Error("Back button was not rendered");
+
+ await act(async () => {
+ backButton.props.onPress();
+ });
+
+ const [correctedEmailInput] = renderer.root.findAllByProps({
+ placeholder: "tim@apple.com",
+ });
+ const [correctedEmailButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Login with email",
+ });
+ if (!correctedEmailInput || !correctedEmailButton) {
+ throw new Error("Corrected email sign in controls were not rendered");
+ }
+
+ await act(async () => {
+ correctedEmailInput.props.onChangeText("right@cap.so");
+ });
+
+ const [readyEmailButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Login with email",
+ });
+ expect(readyEmailButton?.props.accessibilityState).toEqual({
+ busy: false,
+ disabled: false,
+ });
+
+ await act(async () => {
+ await readyEmailButton?.props.onPress();
+ });
+
+ expect(authFns.requestEmailCode).toHaveBeenNthCalledWith(1, "wrong@cap.so");
+ expect(authFns.requestEmailCode).toHaveBeenNthCalledWith(2, "right@cap.so");
+ });
+});
diff --git a/apps/mobile/src/auth/SignInPanel.tsx b/apps/mobile/src/auth/SignInPanel.tsx
new file mode 100644
index 00000000000..27f6f3eae0a
--- /dev/null
+++ b/apps/mobile/src/auth/SignInPanel.tsx
@@ -0,0 +1,1053 @@
+import { SymbolView } from "expo-symbols";
+import * as WebBrowser from "expo-web-browser";
+import { useEffect, useRef, useState } from "react";
+import { Pressable, StyleSheet, Text, TextInput, View } from "react-native";
+import Svg, { Path } from "react-native-svg";
+import { MobileApiError } from "@/api/mobile";
+import { ActionButton } from "@/components/ActionButton";
+import { CapLogoBadge } from "@/components/CapLogoBadge";
+import { GlassSurface } from "@/components/GlassSurface";
+import { colors, fonts, radius, squircle } from "@/theme";
+import { apiBaseUrl, useAuth } from "./AuthContext";
+
+type SignInPanelProps = {
+ title?: string;
+ subtitle?: string;
+};
+
+const codePattern = /^\d{6}$/;
+const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+const emailCodeCooldownMs = 30_000;
+const codeSlots = ["code-0", "code-1", "code-2", "code-3", "code-4", "code-5"];
+type FocusedInput = "code" | "email" | "sso" | null;
+type LoadingKind = "email" | "code" | "google" | "sso";
+type SignInError = {
+ message: string;
+ source: LoadingKind | "resend";
+};
+
+const getEmailRequestErrorMessage = (error: unknown) => {
+ if (error instanceof MobileApiError) {
+ if (error.status === 400) return "Enter a valid email address.";
+ if (error.status === 403) {
+ return "This email cannot be used to sign in to Cap.";
+ }
+ }
+ return error instanceof Error
+ ? error.message
+ : "Unable to send a code. Try again.";
+};
+
+const getCodeVerificationErrorMessage = (error: unknown) => {
+ if (error instanceof MobileApiError) {
+ if (error.status === 400) return "Enter a valid email and 6-digit code.";
+ if (error.status === 403) return "That code is invalid or expired.";
+ }
+ return error instanceof Error ? error.message : "Unable to verify that code.";
+};
+
+const getProviderErrorMessage = (error: unknown, fallback: string) => {
+ return error instanceof Error ? error.message : fallback;
+};
+
+const openWebPath = (path: string) => {
+ void WebBrowser.openBrowserAsync(new URL(path, apiBaseUrl).toString());
+};
+
+function GoogleMark() {
+ return (
+
+ );
+}
+
+export function SignInPanel({
+ title = "Sign in to Cap",
+ subtitle = "Your videos, organized and ready to share.",
+}: SignInPanelProps) {
+ const auth = useAuth();
+ const codeInputRef = useRef(null);
+ const loadingRef = useRef(false);
+ const [email, setEmail] = useState("");
+ const [code, setCode] = useState("");
+ const [organizationId, setOrganizationId] = useState("");
+ const [codeSent, setCodeSent] = useState(false);
+ const [lastCodeRequestedAt, setLastCodeRequestedAt] = useState(
+ null,
+ );
+ const [lastCodeRequestedEmail, setLastCodeRequestedEmail] = useState<
+ string | null
+ >(null);
+ const [nowMs, setNowMs] = useState(() => Date.now());
+ const [showSso, setShowSso] = useState(false);
+ const [focusedInput, setFocusedInput] = useState(null);
+ const [loading, setLoading] = useState(null);
+ const [error, setError] = useState(null);
+
+ const normalizedEmail = email.trim().toLowerCase();
+ const normalizedOrganizationId = organizationId.trim();
+ const cooldownEndsAt =
+ lastCodeRequestedAt !== null && lastCodeRequestedEmail === normalizedEmail
+ ? lastCodeRequestedAt + emailCodeCooldownMs
+ : null;
+ const cooldownRemainingMs =
+ cooldownEndsAt !== null ? Math.max(0, cooldownEndsAt - nowMs) : 0;
+ const cooldownRemainingSeconds = Math.ceil(cooldownRemainingMs / 1000);
+ const isCodeRequestCoolingDown = cooldownRemainingSeconds > 0;
+ const isEmailReady = emailPattern.test(normalizedEmail);
+ const isCodeReady = codePattern.test(code);
+ const isSsoReady = normalizedOrganizationId.length > 0;
+ const canRequestCode =
+ isEmailReady && loading === null && !isCodeRequestCoolingDown;
+ const canVerifyCode = isCodeReady && loading === null;
+ const canStartSso = isSsoReady && loading === null;
+ const isCodeStep = codeSent && !showSso;
+ const showBackButton = showSso || isCodeStep;
+ const showGoogle = auth.authConfig.googleAuthAvailable;
+ const showSaml = auth.authConfig.workosAuthAvailable;
+ const showProviderOptions = showGoogle || showSaml;
+ const errorMessage = error?.message ?? null;
+ const emailInputHasError =
+ error?.source === "email" && !showSso && !isCodeStep;
+ const ssoInputHasError = error?.source === "sso" && showSso;
+ const codeEntryHasError = error?.source === "code" && isCodeStep;
+ const googleActionHasError =
+ error?.source === "google" && !showSso && !isCodeStep;
+ const ssoActionHasError = error?.source === "sso" && showSso;
+ const backDisabled = loading !== null;
+ const codeEntryDisabled = loading !== null;
+ const activeCodeSlotIndex = Math.min(code.length, codeSlots.length - 1);
+ const linkDisabled = loading !== null;
+ const resendDisabled = loading !== null || isCodeRequestCoolingDown;
+ const headerTitle = isCodeStep ? "Enter verification code" : title;
+ const headerSubtitle = isCodeStep
+ ? `We sent a 6-digit code to ${normalizedEmail}`
+ : subtitle;
+ const resendLabel = isCodeRequestCoolingDown
+ ? `Resend in ${cooldownRemainingSeconds}s`
+ : "Didn't receive the code? Resend";
+ const emailButtonLabel = "Login with email";
+ const verifyButtonLabel = "Verify Code";
+ const googleButtonLabel = googleActionHasError
+ ? "Retry Google"
+ : "Login with Google";
+ const ssoContinueButtonLabel = ssoActionHasError
+ ? "Retry SSO"
+ : "Continue with SSO";
+ const googleButtonAccessibilityLabel = googleActionHasError
+ ? "Retry Google sign in"
+ : undefined;
+ const ssoContinueButtonAccessibilityLabel = ssoActionHasError
+ ? "Retry SAML SSO sign in"
+ : undefined;
+ const activeSignInAccessibilityText =
+ loading === "email"
+ ? "Sending verification code"
+ : loading === "code"
+ ? "Verifying code"
+ : loading === "google"
+ ? "Starting Google sign in"
+ : loading === "sso"
+ ? "Starting SAML SSO sign in"
+ : null;
+ const activeSignInAccessibilityValue = activeSignInAccessibilityText
+ ? { text: activeSignInAccessibilityText }
+ : undefined;
+ const emailButtonAccessibilityValue =
+ loading === "email"
+ ? activeSignInAccessibilityValue
+ : emailInputHasError && errorMessage
+ ? { text: errorMessage }
+ : normalizedEmail.length > 0 && !isEmailReady
+ ? { text: "Email address is not valid" }
+ : loading !== null
+ ? activeSignInAccessibilityValue
+ : undefined;
+ const verifyButtonAccessibilityValue =
+ loading === "code"
+ ? activeSignInAccessibilityValue
+ : isCodeStep
+ ? { text: `${code.length} of 6 digits entered` }
+ : undefined;
+ const googleButtonAccessibilityValue =
+ loading === "google"
+ ? activeSignInAccessibilityValue
+ : googleActionHasError && errorMessage
+ ? { text: errorMessage }
+ : loading !== null
+ ? activeSignInAccessibilityValue
+ : undefined;
+ const samlButtonAccessibilityValue =
+ loading !== null ? activeSignInAccessibilityValue : undefined;
+ const ssoContinueButtonAccessibilityValue =
+ loading === "sso"
+ ? activeSignInAccessibilityValue
+ : ssoActionHasError && errorMessage
+ ? { text: errorMessage }
+ : normalizedOrganizationId.length > 0
+ ? undefined
+ : { text: "Organization ID required" };
+ const resendAccessibilityLabel =
+ loading === "email" ? "Didn't receive the code? Resend" : resendLabel;
+ const resendAccessibilityValue =
+ loading === "email"
+ ? activeSignInAccessibilityValue
+ : loading !== null
+ ? activeSignInAccessibilityValue
+ : isCodeRequestCoolingDown
+ ? { text: `Wait ${cooldownRemainingSeconds} seconds` }
+ : undefined;
+ const codeEntryAccessibilityValue =
+ loading === "code"
+ ? activeSignInAccessibilityValue
+ : { text: `${code.length} of 6 digits entered` };
+ const linkAccessibilityValue =
+ loading !== null ? activeSignInAccessibilityValue : undefined;
+ const emailInputAccessibilityValue =
+ loading !== null ? activeSignInAccessibilityValue : undefined;
+ const ssoInputAccessibilityValue =
+ loading !== null
+ ? activeSignInAccessibilityValue
+ : ssoInputHasError && errorMessage
+ ? { text: errorMessage }
+ : undefined;
+ const backButtonAccessibilityValue =
+ loading !== null ? activeSignInAccessibilityValue : undefined;
+ const resendHint =
+ loading !== null
+ ? "Sign in is in progress"
+ : isCodeRequestCoolingDown
+ ? `Wait ${cooldownRemainingSeconds} seconds before requesting a new code`
+ : "Requests a new verification code";
+ const emailButtonHint =
+ loading === "email"
+ ? "Sending verification code"
+ : loading !== null
+ ? "Sign in is in progress"
+ : !isEmailReady
+ ? "Enter a valid email address to continue"
+ : "Sends a verification code to this email";
+ const verifyButtonHint =
+ loading === "code"
+ ? "Verifying code"
+ : loading !== null
+ ? "Sign in is in progress"
+ : !isCodeReady
+ ? "Enter the 6-digit code to continue"
+ : "Verifies the 6-digit code";
+ const googleButtonHint =
+ loading === "google"
+ ? "Google sign in is starting"
+ : loading !== null
+ ? "Sign in is in progress"
+ : googleActionHasError && errorMessage
+ ? errorMessage
+ : "Starts Google sign in";
+ const samlButtonHint =
+ loading !== null
+ ? "Sign in is in progress"
+ : "Shows the SSO organization step";
+ const ssoButtonHint =
+ loading === "sso"
+ ? "SAML SSO sign in is starting"
+ : loading !== null
+ ? "Sign in is in progress"
+ : ssoActionHasError && errorMessage
+ ? errorMessage
+ : !isSsoReady
+ ? "Enter your organization ID to continue"
+ : "Starts SAML SSO for this organization";
+ const emailInputHint =
+ emailInputHasError && errorMessage
+ ? errorMessage
+ : "Enter your email to request a verification code";
+ const ssoInputHint =
+ ssoInputHasError && errorMessage
+ ? errorMessage
+ : "Enter your organization ID to continue with SSO";
+ const codeEntryHint =
+ codeEntryHasError && errorMessage
+ ? errorMessage
+ : loading === "code"
+ ? "Verifying code"
+ : loading !== null
+ ? "Sign in is in progress"
+ : "Tap to enter the 6-digit code";
+ const signupLinkHint =
+ loading !== null
+ ? "Sign in is in progress"
+ : "Opens sign up in a browser sheet";
+ const termsLinkHint =
+ loading !== null
+ ? "Sign in is in progress"
+ : "Opens Terms of Service in a browser sheet";
+ const privacyLinkHint =
+ loading !== null
+ ? "Sign in is in progress"
+ : "Opens Privacy Policy in a browser sheet";
+ const backButtonHint =
+ loading !== null
+ ? "Sign in is in progress"
+ : isCodeStep
+ ? "Returns to email sign in"
+ : "Returns to sign in options";
+
+ const beginLoading = (nextLoading: LoadingKind) => {
+ if (loadingRef.current || loading !== null) return false;
+ loadingRef.current = true;
+ setLoading(nextLoading);
+ return true;
+ };
+
+ const endLoading = () => {
+ loadingRef.current = false;
+ setLoading(null);
+ };
+
+ const isAuthBusy = () => loadingRef.current || loading !== null;
+
+ useEffect(() => {
+ if (!isCodeRequestCoolingDown) return;
+ const interval = setInterval(() => setNowMs(Date.now()), 1000);
+ return () => clearInterval(interval);
+ }, [isCodeRequestCoolingDown]);
+
+ const goBack = () => {
+ if (isAuthBusy()) return;
+ if (showSso) setShowSso(false);
+ if (isCodeStep) {
+ setCodeSent(false);
+ setCode("");
+ }
+ setFocusedInput(null);
+ setError(null);
+ };
+
+ const updateOrganizationId = (value: string) => {
+ if (isAuthBusy()) return;
+ setOrganizationId(value.trim());
+ setError(null);
+ };
+
+ const updateEmail = (value: string) => {
+ if (isAuthBusy()) return;
+ setEmail(value.toLowerCase());
+ setCodeSent(false);
+ setCode("");
+ setError(null);
+ };
+
+ const requestCode = async () => {
+ if (!emailPattern.test(normalizedEmail)) return;
+ if (isCodeRequestCoolingDown) {
+ setError({
+ message: `Please wait ${cooldownRemainingSeconds} seconds before requesting a new code.`,
+ source: "resend",
+ });
+ return;
+ }
+ if (!beginLoading("email")) return;
+ setError(null);
+ try {
+ await auth.requestEmailCode(normalizedEmail);
+ const requestedAt = Date.now();
+ setEmail(normalizedEmail);
+ setCode("");
+ setCodeSent(true);
+ setLastCodeRequestedAt(requestedAt);
+ setLastCodeRequestedEmail(normalizedEmail);
+ setNowMs(requestedAt);
+ } catch (requestError) {
+ setError({
+ message: getEmailRequestErrorMessage(requestError),
+ source: "email",
+ });
+ } finally {
+ endLoading();
+ }
+ };
+
+ const verifyCode = async (codeToVerify = code) => {
+ if (!codePattern.test(codeToVerify)) return;
+ if (!beginLoading("code")) return;
+ setError(null);
+ try {
+ await auth.verifyEmailCode(normalizedEmail, codeToVerify);
+ } catch (verifyError) {
+ setError({
+ message: getCodeVerificationErrorMessage(verifyError),
+ source: "code",
+ });
+ setCode("");
+ } finally {
+ endLoading();
+ }
+ };
+
+ const updateCode = (value: string) => {
+ if (isAuthBusy()) return;
+ const nextCode = value.replace(/\D/g, "").slice(0, 6);
+ setCode(nextCode);
+ setError(null);
+ if (codePattern.test(nextCode)) void verifyCode(nextCode);
+ };
+
+ const submitCode = () => {
+ void verifyCode();
+ };
+
+ const focusCodeInput = () => {
+ if (codeEntryDisabled) return;
+ setFocusedInput("code");
+ codeInputRef.current?.focus();
+ };
+
+ const openWebPathIfIdle = (path: string) => {
+ if (isAuthBusy()) return;
+ openWebPath(path);
+ };
+
+ const signInWithGoogle = async () => {
+ if (!beginLoading("google")) return;
+ setError(null);
+ try {
+ await auth.signInWithGoogle();
+ } catch (googleError) {
+ setError({
+ message: getProviderErrorMessage(
+ googleError,
+ "Unable to start Google sign in.",
+ ),
+ source: "google",
+ });
+ } finally {
+ endLoading();
+ }
+ };
+
+ const signInWithSso = async () => {
+ if (normalizedOrganizationId.length === 0) return;
+ if (!beginLoading("sso")) return;
+ setError(null);
+ try {
+ await auth.signInWithSso(normalizedOrganizationId);
+ } catch (ssoError) {
+ setError({
+ message: getProviderErrorMessage(
+ ssoError,
+ "Unable to start SSO sign in.",
+ ),
+ source: "sso",
+ });
+ } finally {
+ endLoading();
+ }
+ };
+
+ const showSsoStep = () => {
+ if (isAuthBusy()) return;
+ setShowSso(true);
+ setCodeSent(false);
+ setCode("");
+ setError(null);
+ };
+
+ return (
+
+
+ {showBackButton ? (
+ [
+ styles.backPill,
+ pressed && !backDisabled && styles.backPillPressed,
+ backDisabled && styles.backPillDisabled,
+ ]}
+ >
+
+
+ Back
+
+
+ ) : null}
+
+
+
+
+
+ {headerTitle}
+
+
+ {headerSubtitle}
+
+
+
+ {showSso ? (
+
+ setFocusedInput(null)}
+ onFocus={() => setFocusedInput("sso")}
+ onSubmitEditing={signInWithSso}
+ style={[
+ styles.input,
+ focusedInput === "sso" && styles.inputFocused,
+ ssoInputHasError && styles.inputError,
+ ]}
+ />
+
+
+ ) : isCodeStep ? (
+
+
+ {codeSlots.map((slot, index) => (
+
+ {code[index] ?? ""}
+
+ ))}
+ setFocusedInput(null)}
+ onFocus={() => setFocusedInput("code")}
+ onSubmitEditing={submitCode}
+ maxLength={6}
+ importantForAccessibility="no-hide-descendants"
+ selectionColor={colors.blue9}
+ textContentType="oneTimeCode"
+ style={styles.codeInput}
+ />
+
+
+
+ ) : (
+ <>
+ setFocusedInput(null)}
+ onFocus={() => setFocusedInput("email")}
+ onSubmitEditing={requestCode}
+ style={[
+ styles.input,
+ focusedInput === "email" && styles.inputFocused,
+ emailInputHasError && styles.inputError,
+ ]}
+ />
+
+ >
+ )}
+ {errorMessage ? (
+
+
+ {errorMessage}
+
+ ) : null}
+ {isCodeStep ? (
+
+
+
+ {resendLabel}
+
+
+
+ ) : null}
+ {showSso || isCodeStep ? null : (
+ <>
+
+ Don't have an account?{" "}
+ openWebPathIfIdle("signup")}
+ >
+ Sign up here
+
+
+ {showProviderOptions ? (
+ <>
+
+
+ OR
+
+
+ {showGoogle ? (
+ }
+ onPress={signInWithGoogle}
+ loading={loading === "google"}
+ disabled={loading !== null}
+ variant="gray"
+ size="md"
+ />
+ ) : null}
+ {showSaml ? (
+
+ ) : null}
+ >
+ ) : null}
+ >
+ )}
+
+ {isCodeStep
+ ? "By entering your email, you acknowledge that you have both read and agree to Cap's "
+ : "By typing your email and clicking continue, you acknowledge that you have both read and agree to Cap's "}
+ openWebPathIfIdle("terms")}
+ >
+ Terms of Service
+ {" "}
+ and{" "}
+ openWebPathIfIdle("privacy")}
+ >
+ Privacy Policy
+
+ .
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ shell: {
+ flex: 1,
+ justifyContent: "center",
+ paddingVertical: 22,
+ },
+ card: {
+ width: "100%",
+ maxWidth: 432,
+ alignSelf: "center",
+ borderRadius: radius.lg,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray5,
+ paddingHorizontal: 28,
+ paddingVertical: 28,
+ gap: 28,
+ ...squircle,
+ },
+ cardFallback: {
+ backgroundColor: colors.gray3,
+ },
+ backPill: {
+ position: "absolute",
+ left: 20,
+ top: 20,
+ zIndex: 2,
+ minHeight: 30,
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 8,
+ borderRadius: radius.full,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray4,
+ paddingHorizontal: 12,
+ backgroundColor: "transparent",
+ ...squircle,
+ },
+ backPillPressed: {
+ backgroundColor: colors.gray1,
+ },
+ backPillDisabled: {
+ opacity: 0.55,
+ },
+ backPillText: {
+ fontFamily: fonts.regular,
+ fontSize: 12,
+ lineHeight: 17,
+ color: colors.gray12,
+ },
+ backPillTextDisabled: {
+ color: colors.gray9,
+ },
+ brandBlock: {
+ alignItems: "center",
+ },
+ header: {
+ alignItems: "center",
+ gap: 8,
+ },
+ title: {
+ fontFamily: fonts.medium,
+ fontSize: 24,
+ lineHeight: 30,
+ color: colors.gray12,
+ textAlign: "center",
+ },
+ codeTitle: {
+ fontSize: 20,
+ lineHeight: 26,
+ },
+ subtitle: {
+ fontFamily: fonts.regular,
+ fontSize: 16,
+ lineHeight: 22,
+ color: colors.gray10,
+ textAlign: "center",
+ },
+ codeSubtitle: {
+ fontSize: 14,
+ lineHeight: 20,
+ },
+ formStack: {
+ gap: 12,
+ },
+ input: {
+ minHeight: 44,
+ borderRadius: radius.md,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray4,
+ backgroundColor: colors.gray1,
+ paddingHorizontal: 14,
+ fontFamily: fonts.regular,
+ fontSize: 16,
+ color: colors.gray12,
+ ...squircle,
+ },
+ inputFocused: {
+ backgroundColor: colors.gray2,
+ borderWidth: 1,
+ borderColor: colors.gray5,
+ shadowColor: colors.gray12,
+ shadowOffset: { width: 0, height: 0 },
+ shadowOpacity: 0.12,
+ shadowRadius: 1,
+ },
+ inputError: {
+ backgroundColor: colors.red1,
+ borderWidth: 1,
+ borderColor: colors.red7,
+ },
+ codeSection: {
+ gap: 20,
+ paddingTop: 2,
+ },
+ codeBoxes: {
+ flexDirection: "row",
+ gap: 8,
+ justifyContent: "space-between",
+ position: "relative",
+ },
+ codeBoxesDisabled: {
+ opacity: 0.68,
+ },
+ codeBox: {
+ flex: 1,
+ height: 52,
+ borderRadius: radius.sm,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray5,
+ backgroundColor: colors.gray1,
+ alignItems: "center",
+ justifyContent: "center",
+ ...squircle,
+ },
+ codeBoxActive: {
+ borderColor: colors.blue9,
+ },
+ codeBoxFocused: {
+ backgroundColor: colors.gray2,
+ borderWidth: 1,
+ shadowColor: colors.gray12,
+ shadowOffset: { width: 0, height: 0 },
+ shadowOpacity: 0.12,
+ shadowRadius: 1,
+ },
+ codeBoxError: {
+ backgroundColor: colors.red1,
+ borderColor: colors.red7,
+ },
+ codeDigit: {
+ fontFamily: fonts.medium,
+ fontSize: 22,
+ lineHeight: 27,
+ color: colors.gray12,
+ textAlign: "center",
+ },
+ ssoStack: {
+ gap: 10,
+ },
+ codeInput: {
+ position: "absolute",
+ width: 1,
+ height: 1,
+ opacity: 0,
+ },
+ codeLinks: {
+ alignItems: "center",
+ marginTop: 2,
+ },
+ resendButton: {
+ minHeight: 30,
+ justifyContent: "center",
+ paddingHorizontal: 4,
+ },
+ resendText: {
+ fontFamily: fonts.regular,
+ fontSize: 14,
+ lineHeight: 20,
+ color: colors.gray10,
+ textDecorationLine: "underline",
+ },
+ resendTextDisabled: {
+ color: colors.gray9,
+ textDecorationLine: "none",
+ },
+ errorBanner: {
+ minHeight: 42,
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 8,
+ borderRadius: radius.md,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.red6,
+ backgroundColor: colors.red1,
+ paddingHorizontal: 12,
+ paddingVertical: 10,
+ ...squircle,
+ },
+ dividerRow: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 10,
+ paddingVertical: 3,
+ },
+ divider: {
+ flex: 1,
+ height: StyleSheet.hairlineWidth,
+ backgroundColor: colors.gray5,
+ },
+ dividerText: {
+ fontFamily: fonts.medium,
+ fontSize: 12,
+ textTransform: "uppercase",
+ color: colors.gray9,
+ },
+ legalText: {
+ fontFamily: fonts.regular,
+ fontSize: 12,
+ lineHeight: 18,
+ color: colors.gray9,
+ textAlign: "center",
+ },
+ legalLink: {
+ fontFamily: fonts.medium,
+ color: colors.gray12,
+ },
+ linkDisabled: {
+ opacity: 0.55,
+ },
+ signupText: {
+ fontFamily: fonts.regular,
+ fontSize: 12,
+ lineHeight: 18,
+ color: colors.gray9,
+ textAlign: "center",
+ },
+ signupLink: {
+ fontFamily: fonts.medium,
+ color: colors.blue9,
+ },
+ errorText: {
+ flex: 1,
+ fontFamily: fonts.medium,
+ fontSize: 14,
+ lineHeight: 20,
+ color: colors.red9,
+ },
+});
diff --git a/apps/mobile/src/auth/session.ts b/apps/mobile/src/auth/session.ts
new file mode 100644
index 00000000000..be8f9b87fe3
--- /dev/null
+++ b/apps/mobile/src/auth/session.ts
@@ -0,0 +1,20 @@
+export const parseAuthRedirect = (url: string) => {
+ const parsed = new URL(url);
+ const error = parsed.searchParams.get("error_description");
+ if (error) throw new Error(error);
+
+ const apiKey = parsed.searchParams.get("api_key");
+ const userId = parsed.searchParams.get("user_id");
+
+ if (!apiKey) return null;
+ return {
+ apiKey,
+ userId,
+ };
+};
+
+export const requireAuthRedirectSession = (url: string) => {
+ const session = parseAuthRedirect(url);
+ if (!session) throw new Error("Sign in did not return a mobile session.");
+ return session;
+};
diff --git a/apps/mobile/src/auth/signInDestination.test.ts b/apps/mobile/src/auth/signInDestination.test.ts
new file mode 100644
index 00000000000..8ca3f9b70e4
--- /dev/null
+++ b/apps/mobile/src/auth/signInDestination.test.ts
@@ -0,0 +1,12 @@
+import { describe, expect, it } from "vitest";
+import { signInTitleForSegments } from "./signInDestination";
+
+describe("signInTitleForSegments", () => {
+ it("uses contextual auth titles for deep-linked mobile surfaces", () => {
+ expect(signInTitleForSegments(["(tabs)", "upload"])).toBe(
+ "Sign in to import",
+ );
+ expect(signInTitleForSegments(["caps", "[id]"])).toBe("Sign in to view");
+ expect(signInTitleForSegments(["(tabs)"])).toBe("Sign in to Cap");
+ });
+});
diff --git a/apps/mobile/src/auth/signInDestination.ts b/apps/mobile/src/auth/signInDestination.ts
new file mode 100644
index 00000000000..033439d1463
--- /dev/null
+++ b/apps/mobile/src/auth/signInDestination.ts
@@ -0,0 +1,5 @@
+export const signInTitleForSegments = (segments: readonly string[]) => {
+ if (segments.includes("upload")) return "Sign in to import";
+ if (segments.includes("caps")) return "Sign in to view";
+ return "Sign in to Cap";
+};
diff --git a/apps/mobile/src/caps/CapSettingsSheet.test.tsx b/apps/mobile/src/caps/CapSettingsSheet.test.tsx
new file mode 100644
index 00000000000..535558bc5a7
--- /dev/null
+++ b/apps/mobile/src/caps/CapSettingsSheet.test.tsx
@@ -0,0 +1,319 @@
+import { Video } from "@cap/web-domain";
+import type { ReactElement, ReactNode } from "react";
+import { Switch } from "react-native";
+import TestRenderer, {
+ act,
+ type ReactTestInstance,
+ type ReactTestRenderer,
+ type ReactTestRendererJSON,
+} from "react-test-renderer";
+import { describe, expect, it, vi } from "vitest";
+import type { MobileCapSummary } from "@/api/mobile";
+import { CapSettingsSheet } from "./CapSettingsSheet";
+
+type HostProps = {
+ children?: ReactNode;
+ [key: string]: unknown;
+};
+
+type JsonNode = ReactTestRendererJSON | ReactTestRendererJSON[] | string | null;
+
+(
+ globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
+).IS_REACT_ACT_ENVIRONMENT = true;
+
+const renderComponent = async (
+ node: ReactElement,
+): Promise => {
+ let renderer: ReactTestRenderer | null = null;
+ await act(async () => {
+ renderer = TestRenderer.create(node);
+ });
+ return renderer as unknown as ReactTestRenderer;
+};
+
+const getTextNodes = (node: JsonNode): string[] => {
+ if (!node) return [];
+ if (typeof node === "string") return [node];
+ if (Array.isArray(node)) return node.flatMap(getTextNodes);
+ return node.children?.flatMap(getTextNodes) ?? [];
+};
+
+const getInstanceText = (node: ReactTestInstance): string[] =>
+ node.children.flatMap((child) =>
+ typeof child === "string" ? [child] : getInstanceText(child),
+ );
+
+const getNodeType = (node: ReactTestInstance) => String(node.type);
+const resolveStyle = (
+ style: unknown,
+ pressed = false,
+): Record => {
+ const resolved = typeof style === "function" ? style({ pressed }) : style;
+ const styles = Array.isArray(resolved) ? resolved : [resolved];
+ return Object.assign({}, ...styles.filter(Boolean));
+};
+
+vi.mock("react-native", async () => {
+ const React = await import("react");
+ const createHost =
+ (name: string) =>
+ ({ children, ...props }: HostProps) =>
+ React.createElement(name, props, children);
+
+ return {
+ Modal: createHost("Modal"),
+ Pressable: createHost("Pressable"),
+ ScrollView: createHost("ScrollView"),
+ StyleSheet: {
+ create: >(styles: T) => styles,
+ hairlineWidth: 1,
+ },
+ Switch: createHost("Switch"),
+ Text: createHost("Text"),
+ View: createHost("View"),
+ };
+});
+
+vi.mock("expo-symbols", async () => {
+ const React = await import("react");
+ return {
+ SymbolView: (props: Record) =>
+ React.createElement("SymbolView", props),
+ };
+});
+
+vi.mock("@/components/GlassSurface", async () => {
+ const React = await import("react");
+ return {
+ GlassSurface: ({ children, ...props }: HostProps) =>
+ React.createElement("GlassSurface", props, children),
+ };
+});
+
+const cap: MobileCapSummary = {
+ id: Video.VideoId.make("video_123"),
+ shareUrl: "https://cap.so/s/video_123",
+ title: "Launch review",
+ createdAt: "2026-05-18T10:00:00.000Z",
+ updatedAt: "2026-05-18T10:30:00.000Z",
+ ownerName: "Richie",
+ durationSeconds: null,
+ thumbnailUrl: null,
+ folderId: null,
+ public: true,
+ protected: true,
+ viewCount: 0,
+ commentCount: 0,
+ reactionCount: 0,
+ upload: null,
+};
+
+describe("CapSettingsSheet", () => {
+ it("renders native settings rows for Cap actions", async () => {
+ const renderer = await renderComponent(
+ ,
+ );
+
+ expect(getTextNodes(renderer.toJSON())).toEqual(
+ expect.arrayContaining([
+ "Settings",
+ "Launch review",
+ "Title",
+ "View analytics",
+ "Public link",
+ "Password",
+ "Protected",
+ "Copy link",
+ "Share",
+ "Save video",
+ "Delete Cap",
+ ]),
+ );
+ expect(
+ renderer.root.findAll((node) => getNodeType(node) === "GlassSurface"),
+ ).toHaveLength(4);
+ expect(
+ renderer.root.find((node) => getNodeType(node) === "Modal").props
+ .allowSwipeDismissal,
+ ).toBe(true);
+ const closeButton = renderer.root.findByProps({
+ accessibilityLabel: "Close Cap settings",
+ });
+ expect(closeButton.props.accessibilityHint).toBe("Dismisses Cap settings");
+ expect(closeButton.props.hitSlop).toBe(8);
+ expect(
+ renderer.root.findByProps({ accessibilityHint: "Renames this Cap" }),
+ ).toBeTruthy();
+ expect(
+ renderer.root.findByProps({
+ accessibilityHint: "Copies this Cap link",
+ }),
+ ).toBeTruthy();
+ expect(
+ renderer.root.findByProps({
+ accessibilityHint: "Opens the native share sheet",
+ }),
+ ).toBeTruthy();
+ expect(
+ renderer.root.findByProps({ accessibilityHint: "Deletes this Cap" }),
+ ).toBeTruthy();
+ });
+
+ it("updates public link with the native switch", async () => {
+ const onVisibilityChange = vi.fn();
+ const renderer = await renderComponent(
+ ,
+ );
+
+ const switchNode = renderer.root.findByType(Switch);
+ expect(switchNode.props).toMatchObject({
+ accessibilityLabel: "Public link",
+ accessibilityHint: "Toggles public link sharing",
+ accessibilityRole: "switch",
+ accessibilityState: {
+ checked: true,
+ disabled: false,
+ },
+ ios_backgroundColor: "#e0e0e0",
+ trackColor: {
+ false: "#e0e0e0",
+ true: "#8ec8f6",
+ },
+ });
+
+ switchNode.props.onValueChange(false);
+
+ expect(onVisibilityChange).toHaveBeenCalledWith(cap, false);
+ });
+
+ it("marks disabled save actions as unavailable in the native settings sheet", async () => {
+ const onSaveVideo = vi.fn();
+ const renderer = await renderComponent(
+ ,
+ );
+
+ const saveRow = renderer.root
+ .findAllByProps({ accessibilityRole: "button" })
+ .find((node) => getInstanceText(node).includes("Save video"));
+ if (!saveRow) throw new Error("Save video row was not rendered");
+
+ expect(saveRow.props.accessibilityState).toEqual({ disabled: true });
+ expect(saveRow.props.disabled).toBe(true);
+ expect(saveRow.props.accessibilityHint).toBe("Save is in progress");
+ expect(saveRow.props.accessibilityValue).toEqual({
+ text: "Saving video for Launch review",
+ });
+ expect(getInstanceText(saveRow)).not.toContain("Saving...");
+ expect(resolveStyle(saveRow.props.style)).toMatchObject({
+ backgroundColor: "#f9f9f9",
+ });
+ });
+
+ it("marks disabled sharing updates as in progress", async () => {
+ const onVisibilityChange = vi.fn();
+ const renderer = await renderComponent(
+ ,
+ );
+
+ const publicLinkRow = renderer.root
+ .findAllByProps({ accessibilityLabel: "Public link" })
+ .find((node) => getInstanceText(node).includes("Public link"));
+ if (!publicLinkRow) throw new Error("Public link row was not rendered");
+ const switchNode = renderer.root.findByType(Switch);
+ expect(publicLinkRow.props.accessibilityValue).toEqual({
+ text: "Updating sharing for Launch review",
+ });
+ expect(getInstanceText(publicLinkRow)).not.toContain("Updating...");
+ expect(switchNode.props.accessibilityState).toEqual({
+ checked: true,
+ disabled: true,
+ });
+ expect(switchNode.props.accessibilityHint).toBe(
+ "Sharing update is in progress",
+ );
+ expect(switchNode.props.disabled).toBe(true);
+ });
+
+ it("opens analytics from the native settings sheet", async () => {
+ const onViewAnalytics = vi.fn();
+ const renderer = await renderComponent(
+ ,
+ );
+
+ const analyticsRow = renderer.root
+ .findAllByProps({ accessibilityRole: "button" })
+ .find((node) => getInstanceText(node).includes("View analytics"));
+ if (!analyticsRow) throw new Error("Analytics row was not rendered");
+ expect(analyticsRow.props.accessibilityHint).toBe(
+ "Opens analytics in a browser sheet",
+ );
+
+ await act(async () => {
+ analyticsRow.props.onPress();
+ });
+
+ expect(onViewAnalytics).toHaveBeenCalledWith(cap);
+ });
+});
diff --git a/apps/mobile/src/caps/CapSettingsSheet.tsx b/apps/mobile/src/caps/CapSettingsSheet.tsx
new file mode 100644
index 00000000000..ceb1ab03bac
--- /dev/null
+++ b/apps/mobile/src/caps/CapSettingsSheet.tsx
@@ -0,0 +1,471 @@
+import { type SFSymbol, SymbolView } from "expo-symbols";
+import type { ReactNode } from "react";
+import {
+ Modal,
+ Pressable,
+ ScrollView,
+ StyleSheet,
+ Switch,
+ Text,
+ View,
+} from "react-native";
+import type { MobileCapSummary } from "@/api/mobile";
+import { GlassSurface } from "@/components/GlassSurface";
+import { colors, fonts, radius, squircle } from "@/theme";
+
+type CapSettingsSheetProps = {
+ cap: MobileCapSummary | null;
+ visible: boolean;
+ onClose: () => void;
+ onCopyLink: (cap: MobileCapSummary) => void;
+ onShareLink: (cap: MobileCapSummary) => void;
+ onRename: (cap: MobileCapSummary) => void;
+ onPassword: (cap: MobileCapSummary) => void;
+ onViewAnalytics?: (cap: MobileCapSummary) => void;
+ onVisibilityChange: (cap: MobileCapSummary, isPublic: boolean) => void;
+ onSaveVideo: (cap: MobileCapSummary) => void;
+ onDelete: (cap: MobileCapSummary) => void;
+ visibilityDisabled?: boolean;
+ visibilityDisabledHint?: string;
+ visibilityDisabledValue?: string;
+ visibilityDisabledAccessibilityValue?: string;
+ saveDisabled?: boolean;
+ saveDisabledHint?: string;
+ saveDisabledValue?: string;
+ saveDisabledAccessibilityValue?: string;
+};
+
+type SettingsRowProps = {
+ label: string;
+ value?: string;
+ accessibilityValueText?: string;
+ symbol: SFSymbol;
+ accessibilityHint?: string;
+ danger?: boolean;
+ disabled?: boolean;
+ onPress?: () => void;
+ children?: ReactNode;
+};
+
+function SettingsRow({
+ label,
+ value,
+ accessibilityValueText,
+ symbol,
+ accessibilityHint,
+ danger = false,
+ disabled = false,
+ onPress,
+ children,
+}: SettingsRowProps) {
+ const accessibilityValue = accessibilityValueText
+ ? { text: accessibilityValueText }
+ : value
+ ? { text: value }
+ : undefined;
+ const isAction = Boolean(onPress) || disabled;
+ const content = (
+ <>
+
+
+
+
+ {label}
+
+ {value ? (
+
+ {value}
+
+ ) : null}
+ {children}
+ {isAction ? (
+
+ ) : null}
+ >
+ );
+
+ if (!isAction) {
+ return (
+
+ {content}
+
+ );
+ }
+
+ return (
+ [
+ styles.row,
+ pressed && !disabled ? styles.rowPressed : null,
+ disabled ? styles.rowDisabled : null,
+ ]}
+ >
+ {content}
+
+ );
+}
+
+function SettingsSection({
+ children,
+ title,
+}: {
+ children: ReactNode;
+ title: string;
+}) {
+ return (
+
+ {title}
+
+ {children}
+
+
+ );
+}
+
+export function CapSettingsSheet({
+ cap,
+ visible,
+ onClose,
+ onCopyLink,
+ onShareLink,
+ onRename,
+ onPassword,
+ onViewAnalytics,
+ onVisibilityChange,
+ onSaveVideo,
+ onDelete,
+ visibilityDisabled = false,
+ visibilityDisabledHint,
+ visibilityDisabledValue,
+ visibilityDisabledAccessibilityValue,
+ saveDisabled = false,
+ saveDisabledHint,
+ saveDisabledValue,
+ saveDisabledAccessibilityValue,
+}: CapSettingsSheetProps) {
+ if (!cap) return null;
+
+ return (
+
+
+
+
+ Settings
+
+ {cap.title}
+
+
+ {cap.shareUrl}
+
+
+ [
+ styles.closeButton,
+ pressed ? styles.closeButtonPressed : null,
+ ]}
+ >
+
+
+
+
+
+ onRename(cap)}
+ symbol="textformat"
+ value={cap.title}
+ />
+ {onViewAnalytics ? (
+ <>
+
+ onViewAnalytics(cap)}
+ symbol="chart.bar"
+ />
+ >
+ ) : null}
+
+
+
+
+ onVisibilityChange(cap, value)}
+ trackColor={{ false: colors.gray5, true: colors.blue7 }}
+ thumbColor={colors.white}
+ value={cap.public}
+ />
+
+
+ onPassword(cap)}
+ symbol={cap.protected ? "lock.fill" : "lock.open"}
+ value={cap.protected ? "Protected" : "Off"}
+ />
+
+
+
+ onCopyLink(cap)}
+ symbol="doc.on.doc"
+ />
+
+ onShareLink(cap)}
+ symbol="square.and.arrow.up"
+ />
+
+ onSaveVideo(cap)}
+ symbol="square.and.arrow.down"
+ accessibilityValueText={saveDisabledAccessibilityValue}
+ value={saveDisabled ? saveDisabledValue : undefined}
+ />
+
+
+
+ onDelete(cap)}
+ symbol="trash"
+ />
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ sheet: {
+ flex: 1,
+ backgroundColor: colors.appBackground,
+ },
+ sheetContent: {
+ paddingHorizontal: 20,
+ paddingTop: 20,
+ paddingBottom: 28,
+ },
+ header: {
+ flexDirection: "row",
+ alignItems: "flex-start",
+ gap: 16,
+ paddingTop: 8,
+ paddingBottom: 18,
+ },
+ headerCopy: {
+ flex: 1,
+ minWidth: 0,
+ },
+ eyebrow: {
+ fontFamily: fonts.medium,
+ fontSize: 13,
+ lineHeight: 18,
+ color: colors.gray10,
+ marginBottom: 4,
+ },
+ title: {
+ fontFamily: fonts.medium,
+ fontSize: 24,
+ lineHeight: 30,
+ color: colors.gray12,
+ },
+ shareUrl: {
+ fontFamily: fonts.regular,
+ fontSize: 14,
+ lineHeight: 20,
+ color: colors.gray10,
+ marginTop: 4,
+ },
+ closeButton: {
+ width: 34,
+ height: 34,
+ borderRadius: radius.full,
+ alignItems: "center",
+ justifyContent: "center",
+ backgroundColor: colors.gray3,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray5,
+ ...squircle,
+ },
+ closeButtonPressed: {
+ backgroundColor: colors.gray5,
+ },
+ section: {
+ gap: 8,
+ marginBottom: 18,
+ },
+ sectionTitle: {
+ fontFamily: fonts.medium,
+ fontSize: 13,
+ lineHeight: 18,
+ color: colors.gray10,
+ paddingHorizontal: 4,
+ },
+ group: {
+ overflow: "hidden",
+ borderRadius: radius.md,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray3,
+ ...squircle,
+ },
+ groupFallback: {
+ backgroundColor: colors.gray1,
+ },
+ row: {
+ minHeight: 54,
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 12,
+ paddingHorizontal: 14,
+ paddingVertical: 10,
+ },
+ rowPressed: {
+ backgroundColor: colors.gray2,
+ },
+ rowDisabled: {
+ backgroundColor: colors.gray2,
+ },
+ rowIcon: {
+ width: 28,
+ height: 28,
+ borderRadius: radius.sm,
+ backgroundColor: colors.gray3,
+ alignItems: "center",
+ justifyContent: "center",
+ ...squircle,
+ },
+ dangerIcon: {
+ backgroundColor: colors.red3,
+ },
+ rowIconDisabled: {
+ backgroundColor: colors.gray3,
+ },
+ rowLabel: {
+ flex: 1,
+ fontFamily: fonts.regular,
+ fontSize: 16,
+ lineHeight: 22,
+ color: colors.gray12,
+ },
+ rowValue: {
+ maxWidth: "42%",
+ fontFamily: fonts.regular,
+ fontSize: 14,
+ lineHeight: 20,
+ color: colors.gray10,
+ },
+ rowLabelDisabled: {
+ color: colors.gray9,
+ },
+ rowValueDisabled: {
+ color: colors.gray9,
+ },
+ dangerText: {
+ color: colors.red11,
+ },
+ separator: {
+ height: StyleSheet.hairlineWidth,
+ backgroundColor: colors.gray3,
+ marginLeft: 54,
+ },
+});
diff --git a/apps/mobile/src/caps/passwordActions.test.ts b/apps/mobile/src/caps/passwordActions.test.ts
new file mode 100644
index 00000000000..95d0511aaeb
--- /dev/null
+++ b/apps/mobile/src/caps/passwordActions.test.ts
@@ -0,0 +1,116 @@
+import { Video } from "@cap/web-domain";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import type { MobileApiClient, MobileCapSummary } from "@/api/mobile";
+import { showCapPasswordActions } from "./passwordActions";
+
+const reactNativeMock = vi.hoisted(() => ({
+ ActionSheetIOS: {
+ showActionSheetWithOptions: vi.fn(),
+ },
+ Alert: {
+ alert: vi.fn(),
+ prompt: vi.fn(),
+ },
+ Platform: {
+ OS: "ios",
+ },
+ StyleSheet: {
+ create: >(styles: T) => styles,
+ },
+}));
+
+vi.mock("react-native", () => reactNativeMock);
+
+const cap: MobileCapSummary = {
+ id: Video.VideoId.make("video_123"),
+ shareUrl: "https://cap.so/s/video_123",
+ title: "Launch review",
+ createdAt: "2026-05-18T10:00:00.000Z",
+ updatedAt: "2026-05-18T10:30:00.000Z",
+ ownerName: "Richie",
+ durationSeconds: null,
+ thumbnailUrl: null,
+ folderId: null,
+ public: true,
+ protected: false,
+ viewCount: 0,
+ commentCount: 0,
+ reactionCount: 0,
+ upload: null,
+};
+
+describe("showCapPasswordActions", () => {
+ beforeEach(() => {
+ reactNativeMock.ActionSheetIOS.showActionSheetWithOptions.mockClear();
+ reactNativeMock.Alert.alert.mockClear();
+ reactNativeMock.Alert.prompt.mockClear();
+ });
+
+ it("uses a native secure prompt to add a Cap password", async () => {
+ const updated = { ...cap, protected: true };
+ const updateCapPassword = vi.fn(async () => updated);
+ const onUpdated = vi.fn();
+
+ showCapPasswordActions({
+ cap,
+ client: { updateCapPassword } as unknown as MobileApiClient,
+ onUpdated,
+ });
+
+ expect(reactNativeMock.Alert.prompt).toHaveBeenCalledWith(
+ "Add password",
+ "Set a password for this Cap link.",
+ expect.any(Array),
+ "secure-text",
+ );
+
+ const buttons = reactNativeMock.Alert.prompt.mock.calls[0]?.[2];
+ const saveButton = Array.isArray(buttons) ? buttons[1] : undefined;
+ saveButton?.onPress?.(" secret ");
+ await Promise.resolve();
+ await Promise.resolve();
+
+ expect(updateCapPassword).toHaveBeenCalledWith("video_123", {
+ password: "secret",
+ });
+ expect(onUpdated).toHaveBeenCalledWith(updated);
+ });
+
+ it("uses a native action sheet to remove an existing password", async () => {
+ const protectedCap = { ...cap, protected: true };
+ const updated = { ...protectedCap, protected: false };
+ const updateCapPassword = vi.fn(async () => updated);
+ const onUpdated = vi.fn();
+
+ showCapPasswordActions({
+ cap: protectedCap,
+ client: { updateCapPassword } as unknown as MobileApiClient,
+ onUpdated,
+ });
+
+ expect(
+ reactNativeMock.ActionSheetIOS.showActionSheetWithOptions,
+ ).toHaveBeenCalledWith(
+ expect.objectContaining({
+ cancelButtonIndex: 2,
+ destructiveButtonIndex: 1,
+ options: ["Change password", "Remove password", "Cancel"],
+ title: "Password protected",
+ userInterfaceStyle: "light",
+ }),
+ expect.any(Function),
+ );
+
+ const callback =
+ reactNativeMock.ActionSheetIOS.showActionSheetWithOptions.mock
+ .calls[0]?.[1];
+ callback?.(1);
+ await Promise.resolve();
+ await Promise.resolve();
+
+ expect(updateCapPassword).toHaveBeenCalledWith("video_123", {
+ password: null,
+ });
+ expect(onUpdated).toHaveBeenCalledWith(updated);
+ });
+});
diff --git a/apps/mobile/src/caps/passwordActions.ts b/apps/mobile/src/caps/passwordActions.ts
new file mode 100644
index 00000000000..62eb4e0f811
--- /dev/null
+++ b/apps/mobile/src/caps/passwordActions.ts
@@ -0,0 +1,97 @@
+import {
+ ActionSheetIOS,
+ Alert,
+ type AlertButton,
+ Platform,
+} from "react-native";
+import type { MobileApiClient, MobileCapSummary } from "@/api/mobile";
+import { colors } from "@/theme";
+
+type CapPasswordActionsInput = {
+ cap: MobileCapSummary;
+ client: MobileApiClient;
+ onUpdated: (cap: MobileCapSummary) => void | Promise;
+};
+
+const getPasswordErrorMessage = (error: unknown) =>
+ error instanceof Error ? error.message : "Unable to update this password.";
+
+const savePassword = async ({
+ cap,
+ client,
+ onUpdated,
+ password,
+}: CapPasswordActionsInput & { password: string | null }) => {
+ try {
+ const updated = await client.updateCapPassword(cap.id, { password });
+ await onUpdated(updated);
+ } catch (error) {
+ Alert.alert("Password update failed", getPasswordErrorMessage(error));
+ }
+};
+
+const promptForPassword = (input: CapPasswordActionsInput) => {
+ const title = input.cap.protected ? "Change password" : "Add password";
+
+ if (Platform.OS !== "ios") {
+ Alert.alert("Password", "Password editing is available on iOS.");
+ return;
+ }
+
+ Alert.prompt(
+ title,
+ "Set a password for this Cap link.",
+ [
+ { text: "Cancel", style: "cancel" },
+ {
+ text: "Save",
+ onPress: (value?: string) => {
+ const password = value?.trim() ?? "";
+ if (!password) {
+ Alert.alert("Password required", "Enter a password for this Cap.");
+ return;
+ }
+ void savePassword({ ...input, password });
+ },
+ },
+ ],
+ "secure-text",
+ );
+};
+
+export const showCapPasswordActions = (input: CapPasswordActionsInput) => {
+ if (!input.cap.protected) {
+ promptForPassword(input);
+ return;
+ }
+
+ if (Platform.OS === "ios") {
+ ActionSheetIOS.showActionSheetWithOptions(
+ {
+ cancelButtonIndex: 2,
+ destructiveButtonIndex: 1,
+ options: ["Change password", "Remove password", "Cancel"],
+ title: "Password protected",
+ tintColor: colors.blue11,
+ userInterfaceStyle: "light",
+ },
+ (index) => {
+ if (index === 0) promptForPassword(input);
+ if (index === 1) void savePassword({ ...input, password: null });
+ },
+ );
+ return;
+ }
+
+ const buttons: AlertButton[] = [
+ {
+ text: "Remove password",
+ style: "destructive",
+ onPress: () => {
+ void savePassword({ ...input, password: null });
+ },
+ },
+ { text: "Cancel", style: "cancel" },
+ ];
+ Alert.alert("Password protected", undefined, buttons);
+};
diff --git a/apps/mobile/src/caps/saveCapVideo.ts b/apps/mobile/src/caps/saveCapVideo.ts
new file mode 100644
index 00000000000..dc52b4a4248
--- /dev/null
+++ b/apps/mobile/src/caps/saveCapVideo.ts
@@ -0,0 +1,30 @@
+import * as FileSystem from "expo-file-system/legacy";
+import * as MediaLibrary from "expo-media-library";
+import type { MobileApiClient } from "@/api/mobile";
+
+export class PhotosPermissionDeniedError extends Error {
+ constructor() {
+ super("Photos access needed");
+ this.name = "PhotosPermissionDeniedError";
+ }
+}
+
+const safeFileName = (fileName: string) =>
+ fileName.replace(/[^\w.\- ]+/g, "").trim() || "Cap.mp4";
+
+export const saveCapVideoToPhotos = async (
+ client: MobileApiClient,
+ capId: string,
+) => {
+ const permission = await MediaLibrary.requestPermissionsAsync();
+ if (!permission.granted) throw new PhotosPermissionDeniedError();
+
+ const download = await client.getDownload(capId);
+ const target = `${FileSystem.documentDirectory}${safeFileName(
+ download.fileName,
+ )}`;
+ const result = await FileSystem.downloadAsync(download.url, target);
+ await MediaLibrary.saveToLibraryAsync(result.uri);
+
+ return download.fileName;
+};
diff --git a/apps/mobile/src/caps/titleActions.test.ts b/apps/mobile/src/caps/titleActions.test.ts
new file mode 100644
index 00000000000..0d9622a5a36
--- /dev/null
+++ b/apps/mobile/src/caps/titleActions.test.ts
@@ -0,0 +1,93 @@
+import { Video } from "@cap/web-domain";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import type { MobileApiClient, MobileCapSummary } from "@/api/mobile";
+import { showCapTitleActions } from "./titleActions";
+
+const reactNativeMock = vi.hoisted(() => ({
+ Alert: {
+ alert: vi.fn(),
+ prompt: vi.fn(),
+ },
+ Platform: {
+ OS: "ios",
+ },
+}));
+
+vi.mock("react-native", () => reactNativeMock);
+
+const cap: MobileCapSummary = {
+ id: Video.VideoId.make("video_123"),
+ shareUrl: "https://cap.so/s/video_123",
+ title: "Launch review",
+ createdAt: "2026-05-18T10:00:00.000Z",
+ updatedAt: "2026-05-18T10:30:00.000Z",
+ ownerName: "Richie",
+ durationSeconds: null,
+ thumbnailUrl: null,
+ folderId: null,
+ public: true,
+ protected: false,
+ viewCount: 0,
+ commentCount: 0,
+ reactionCount: 0,
+ upload: null,
+};
+
+describe("showCapTitleActions", () => {
+ beforeEach(() => {
+ reactNativeMock.Alert.alert.mockClear();
+ reactNativeMock.Alert.prompt.mockClear();
+ reactNativeMock.Platform.OS = "ios";
+ });
+
+ it("uses a native prompt to rename a Cap", async () => {
+ const updated = { ...cap, title: "Roadmap review" };
+ const updateCapTitle = vi.fn(async () => updated);
+ const onUpdated = vi.fn();
+
+ showCapTitleActions({
+ cap,
+ client: { updateCapTitle } as unknown as MobileApiClient,
+ onUpdated,
+ });
+
+ expect(reactNativeMock.Alert.prompt).toHaveBeenCalledWith(
+ "Rename Cap",
+ undefined,
+ expect.any(Array),
+ "plain-text",
+ "Launch review",
+ );
+
+ const buttons = reactNativeMock.Alert.prompt.mock.calls[0]?.[2];
+ const saveButton = Array.isArray(buttons) ? buttons[1] : undefined;
+ saveButton?.onPress?.(" Roadmap review ");
+ await Promise.resolve();
+ await Promise.resolve();
+
+ expect(updateCapTitle).toHaveBeenCalledWith("video_123", {
+ title: "Roadmap review",
+ });
+ expect(onUpdated).toHaveBeenCalledWith(updated);
+ });
+
+ it("rejects blank Cap titles before calling the API", () => {
+ const updateCapTitle = vi.fn();
+
+ showCapTitleActions({
+ cap,
+ client: { updateCapTitle } as unknown as MobileApiClient,
+ onUpdated: vi.fn(),
+ });
+
+ const buttons = reactNativeMock.Alert.prompt.mock.calls[0]?.[2];
+ const saveButton = Array.isArray(buttons) ? buttons[1] : undefined;
+ saveButton?.onPress?.(" ");
+
+ expect(updateCapTitle).not.toHaveBeenCalled();
+ expect(reactNativeMock.Alert.alert).toHaveBeenCalledWith(
+ "Title required",
+ "Enter a title for this Cap.",
+ );
+ });
+});
diff --git a/apps/mobile/src/caps/titleActions.ts b/apps/mobile/src/caps/titleActions.ts
new file mode 100644
index 00000000000..245243a836c
--- /dev/null
+++ b/apps/mobile/src/caps/titleActions.ts
@@ -0,0 +1,53 @@
+import { Alert, Platform } from "react-native";
+import type { MobileApiClient, MobileCapSummary } from "@/api/mobile";
+
+type CapTitleActionsInput = {
+ cap: MobileCapSummary;
+ client: MobileApiClient;
+ onUpdated: (cap: MobileCapSummary) => void | Promise;
+};
+
+const getTitleErrorMessage = (error: unknown) =>
+ error instanceof Error ? error.message : "Unable to rename this Cap.";
+
+const saveTitle = async ({
+ cap,
+ client,
+ onUpdated,
+ title,
+}: CapTitleActionsInput & { title: string }) => {
+ try {
+ const updated = await client.updateCapTitle(cap.id, { title });
+ await onUpdated(updated);
+ } catch (error) {
+ Alert.alert("Rename failed", getTitleErrorMessage(error));
+ }
+};
+
+export const showCapTitleActions = (input: CapTitleActionsInput) => {
+ if (Platform.OS !== "ios") {
+ Alert.alert("Rename Cap", "Title editing is available on iOS.");
+ return;
+ }
+
+ Alert.prompt(
+ "Rename Cap",
+ undefined,
+ [
+ { text: "Cancel", style: "cancel" },
+ {
+ text: "Save",
+ onPress: (value?: string) => {
+ const title = value?.trim() ?? "";
+ if (!title) {
+ Alert.alert("Title required", "Enter a title for this Cap.");
+ return;
+ }
+ void saveTitle({ ...input, title });
+ },
+ },
+ ],
+ "plain-text",
+ input.cap.title,
+ );
+};
diff --git a/apps/mobile/src/components/ActionButton.test.tsx b/apps/mobile/src/components/ActionButton.test.tsx
new file mode 100644
index 00000000000..c735962be44
--- /dev/null
+++ b/apps/mobile/src/components/ActionButton.test.tsx
@@ -0,0 +1,147 @@
+import type { ReactElement, ReactNode } from "react";
+import TestRenderer, { act, type ReactTestRenderer } from "react-test-renderer";
+import { describe, expect, it, vi } from "vitest";
+import { ActionButton } from "./ActionButton";
+
+type HostProps = {
+ children?: ReactNode;
+ [key: string]: unknown;
+};
+
+const renderComponent = async (
+ node: ReactElement,
+): Promise => {
+ let renderer: ReactTestRenderer | null = null;
+ await act(async () => {
+ renderer = TestRenderer.create(node);
+ });
+ return renderer as unknown as ReactTestRenderer;
+};
+
+(
+ globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
+).IS_REACT_ACT_ENVIRONMENT = true;
+
+const resolveStyle = (style: unknown): Record => {
+ const resolved =
+ typeof style === "function" ? style({ pressed: false }) : style;
+ const styles = Array.isArray(resolved) ? resolved : [resolved];
+ return Object.assign({}, ...styles.filter(Boolean));
+};
+
+vi.mock("react-native", async () => {
+ const React = await import("react");
+ const createHost =
+ (name: string) =>
+ ({ children, ...props }: HostProps) =>
+ React.createElement(name, props, children);
+
+ return {
+ ActivityIndicator: createHost("ActivityIndicator"),
+ Pressable: createHost("Pressable"),
+ StyleSheet: {
+ create: >(styles: T) => styles,
+ hairlineWidth: 1,
+ },
+ Text: createHost("Text"),
+ View: createHost("View"),
+ };
+});
+
+vi.mock("expo-symbols", async () => {
+ const React = await import("react");
+ return {
+ SymbolView: (props: Record) =>
+ React.createElement("SymbolView", props),
+ };
+});
+
+describe("ActionButton", () => {
+ it("matches the Cap web dark button surface and clips the inset highlight", async () => {
+ const renderer = await renderComponent(
+ ,
+ );
+ const button = renderer.root.findByProps({
+ accessibilityLabel: "Upload",
+ });
+
+ expect(button.props.android_ripple).toEqual({
+ color: "rgba(18, 22, 31, 0.05)",
+ });
+ expect(button.props.hitSlop).toEqual({
+ bottom: 4,
+ left: 4,
+ right: 4,
+ top: 4,
+ });
+ expect(button.props.accessibilityState).toEqual({
+ busy: false,
+ disabled: false,
+ });
+ expect(button.props.accessibilityHint).toBe("Opens upload options");
+ expect(resolveStyle(button.props.style)).toMatchObject({
+ backgroundColor: "#202020",
+ borderColor: "#202020",
+ borderRadius: 999,
+ height: 44,
+ overflow: "hidden",
+ });
+ });
+
+ it("uses the Cap web gray button token pair", async () => {
+ const renderer = await renderComponent(
+ ,
+ );
+ const button = renderer.root.findByProps({
+ accessibilityLabel: "Photos",
+ });
+
+ expect(resolveStyle(button.props.style)).toMatchObject({
+ backgroundColor: "#e0e0e0",
+ borderColor: "#bbbbbb",
+ });
+ });
+
+ it("allows a specific native label while keeping short visible text", async () => {
+ const renderer = await renderComponent(
+ ,
+ );
+ const button = renderer.root.findByProps({
+ accessibilityLabel: "Retry upload failed-upload.mp4",
+ });
+
+ expect(button.findByProps({ children: "Retry" }).props.children).toBe(
+ "Retry",
+ );
+ expect(button.props.accessibilityValue).toEqual({
+ text: "Upload failed",
+ });
+ });
+
+ it("exposes native disabled and busy state while loading", async () => {
+ const renderer = await renderComponent(
+ ,
+ );
+ const button = renderer.root.findByProps({
+ accessibilityLabel: "Upload",
+ });
+
+ expect(button.props.disabled).toBe(true);
+ expect(button.props.accessibilityState).toEqual({
+ busy: true,
+ disabled: true,
+ });
+ });
+});
diff --git a/apps/mobile/src/components/ActionButton.tsx b/apps/mobile/src/components/ActionButton.tsx
new file mode 100644
index 00000000000..d12855f239b
--- /dev/null
+++ b/apps/mobile/src/components/ActionButton.tsx
@@ -0,0 +1,320 @@
+import { type SFSymbol, SymbolView } from "expo-symbols";
+import type { ReactNode } from "react";
+import {
+ type AccessibilityValue,
+ ActivityIndicator,
+ type GestureResponderEvent,
+ Pressable,
+ StyleSheet,
+ Text,
+ View,
+ type ViewStyle,
+} from "react-native";
+import { colors, fonts, radius, squircle } from "@/theme";
+
+type ActionButtonProps = {
+ label: string;
+ onPress: (event?: GestureResponderEvent) => void;
+ accessibilityLabel?: string;
+ accessibilityHint?: string;
+ accessibilityValue?: AccessibilityValue;
+ symbol?: SFSymbol;
+ leading?: ReactNode;
+ variant?:
+ | "primary"
+ | "blue"
+ | "secondary"
+ | "gray"
+ | "dark"
+ | "danger"
+ | "ghost";
+ size?: "sm" | "md" | "lg";
+ disabled?: boolean;
+ loading?: boolean;
+ style?: ViewStyle;
+ children?: ReactNode;
+};
+
+const labelBySize = {
+ sm: "labelSm",
+ md: "labelMd",
+ lg: "labelLg",
+} as const;
+
+const iconColor = (
+ variant: NonNullable,
+ isDisabled: boolean,
+) => {
+ if (isDisabled) {
+ if (variant === "primary") return colors.gray9;
+ if (variant === "blue" || variant === "dark" || variant === "danger") {
+ return colors.gray10;
+ }
+ if (variant === "gray") return colors.gray11;
+ }
+
+ return variant === "primary" ||
+ variant === "blue" ||
+ variant === "dark" ||
+ variant === "danger"
+ ? colors.white
+ : colors.gray12;
+};
+
+const usesInsetHighlight = (
+ variant: NonNullable,
+) =>
+ variant === "primary" ||
+ variant === "blue" ||
+ variant === "gray" ||
+ variant === "dark";
+
+const buttonHitSlop = { bottom: 4, left: 4, right: 4, top: 4 };
+const androidRipple = { color: colors.blackAlpha5 };
+
+export function ActionButton({
+ label,
+ onPress,
+ accessibilityLabel,
+ accessibilityHint,
+ accessibilityValue,
+ symbol,
+ leading,
+ variant = "primary",
+ size = "md",
+ disabled = false,
+ loading = false,
+ style,
+ children,
+}: ActionButtonProps) {
+ const isDisabled = disabled || loading;
+ const showInsetHighlight = usesInsetHighlight(variant);
+
+ return (
+ [
+ styles.base,
+ styles[size],
+ styles[variant],
+ isDisabled && styles[`${variant}Disabled`],
+ pressed && !isDisabled && pressedStyles[variant],
+ style,
+ ]}
+ >
+ {showInsetHighlight ? (
+
+ ) : null}
+ {loading ? (
+
+ ) : leading ? (
+ leading
+ ) : symbol ? (
+
+ ) : null}
+
+ {children ?? label}
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ base: {
+ alignItems: "center",
+ justifyContent: "center",
+ flexDirection: "row",
+ gap: 4,
+ position: "relative",
+ borderWidth: StyleSheet.hairlineWidth,
+ overflow: "hidden",
+ ...squircle,
+ },
+ sm: {
+ height: 40,
+ borderRadius: radius.full,
+ paddingHorizontal: 20,
+ },
+ md: {
+ height: 44,
+ borderRadius: radius.full,
+ paddingHorizontal: 20,
+ },
+ lg: {
+ height: 48,
+ borderRadius: radius.full,
+ paddingHorizontal: 20,
+ },
+ primary: {
+ backgroundColor: colors.gray12,
+ borderColor: colors.gray12,
+ },
+ primaryPressed: {
+ backgroundColor: colors.gray11,
+ borderColor: colors.gray11,
+ },
+ blue: {
+ backgroundColor: colors.buttonBlue,
+ borderColor: colors.buttonBlueBorder,
+ },
+ bluePressed: {
+ backgroundColor: colors.buttonBlueHover,
+ borderColor: colors.buttonBlueBorder,
+ },
+ dark: {
+ backgroundColor: colors.gray12,
+ borderColor: colors.gray12,
+ },
+ darkPressed: {
+ backgroundColor: colors.gray11,
+ borderColor: colors.gray11,
+ },
+ secondary: {
+ backgroundColor: colors.gray3,
+ borderColor: colors.gray5,
+ },
+ secondaryPressed: {
+ backgroundColor: colors.gray5,
+ borderColor: colors.gray6,
+ },
+ gray: {
+ backgroundColor: colors.gray5,
+ borderColor: colors.gray8,
+ },
+ grayPressed: {
+ backgroundColor: colors.gray7,
+ borderColor: colors.gray8,
+ },
+ danger: {
+ backgroundColor: colors.red9,
+ borderColor: colors.red9,
+ },
+ dangerPressed: {
+ backgroundColor: colors.red10,
+ borderColor: colors.red10,
+ },
+ ghost: {
+ backgroundColor: "transparent",
+ borderColor: "transparent",
+ },
+ ghostPressed: {
+ backgroundColor: colors.blackAlpha5,
+ borderColor: "transparent",
+ },
+ primaryDisabled: {
+ backgroundColor: colors.gray6,
+ borderColor: colors.gray6,
+ },
+ blueDisabled: {
+ backgroundColor: colors.gray7,
+ borderColor: colors.gray8,
+ },
+ darkDisabled: {
+ backgroundColor: colors.gray7,
+ borderColor: colors.gray8,
+ },
+ secondaryDisabled: {
+ backgroundColor: colors.gray8,
+ borderColor: colors.gray8,
+ },
+ grayDisabled: {
+ backgroundColor: colors.gray8,
+ borderColor: colors.gray7,
+ },
+ dangerDisabled: {
+ backgroundColor: colors.gray7,
+ borderColor: colors.gray8,
+ },
+ ghostDisabled: {
+ backgroundColor: "transparent",
+ borderColor: "transparent",
+ },
+ label: {
+ fontFamily: fonts.medium,
+ },
+ labelSm: {
+ fontSize: 14,
+ lineHeight: 18,
+ },
+ labelMd: {
+ fontSize: 14,
+ lineHeight: 20,
+ },
+ labelLg: {
+ fontSize: 16,
+ lineHeight: 22,
+ },
+ primaryLabel: {
+ color: colors.white,
+ },
+ defaultLabel: {
+ color: colors.gray12,
+ },
+ primaryDisabledLabel: {
+ color: colors.gray9,
+ },
+ blueDisabledLabel: {
+ color: colors.gray10,
+ },
+ darkDisabledLabel: {
+ color: colors.gray10,
+ },
+ secondaryDisabledLabel: {
+ color: colors.gray11,
+ },
+ grayDisabledLabel: {
+ color: colors.gray11,
+ },
+ dangerDisabledLabel: {
+ color: colors.gray10,
+ },
+ ghostDisabledLabel: {
+ color: colors.gray9,
+ },
+ insetHighlight: {
+ position: "absolute",
+ top: 0,
+ left: 0,
+ right: 0,
+ height: 1.5,
+ backgroundColor: "rgba(255, 255, 255, 0.4)",
+ },
+});
+
+const pressedStyles = {
+ primary: styles.primaryPressed,
+ blue: styles.bluePressed,
+ secondary: styles.secondaryPressed,
+ gray: styles.grayPressed,
+ dark: styles.darkPressed,
+ danger: styles.dangerPressed,
+ ghost: styles.ghostPressed,
+} as const;
diff --git a/apps/mobile/src/components/CapCard.test.ts b/apps/mobile/src/components/CapCard.test.ts
new file mode 100644
index 00000000000..fc441d203f2
--- /dev/null
+++ b/apps/mobile/src/components/CapCard.test.ts
@@ -0,0 +1,521 @@
+import { Video } from "@cap/web-domain";
+import React, { type ReactElement, type ReactNode } from "react";
+import TestRenderer, {
+ act,
+ type ReactTestRenderer,
+ type ReactTestRendererJSON,
+} from "react-test-renderer";
+import { describe, expect, it, vi } from "vitest";
+import type { MobileCapSummary } from "@/api/mobile";
+import { CapCard } from "./CapCard";
+import { getCapCardViewModel } from "./capCardViewModel";
+
+type HostProps = {
+ children?: ReactNode;
+ [key: string]: unknown;
+};
+
+type JsonNode = ReactTestRendererJSON | ReactTestRendererJSON[] | string | null;
+
+(
+ globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
+).IS_REACT_ACT_ENVIRONMENT = true;
+
+const renderTree = async (node: ReactElement): Promise => {
+ let renderer: ReactTestRenderer | null = null;
+ await act(async () => {
+ renderer = TestRenderer.create(node);
+ });
+ return (renderer as ReactTestRenderer | null)?.toJSON() ?? null;
+};
+
+const renderComponent = async (
+ node: ReactElement,
+): Promise => {
+ let renderer: ReactTestRenderer | null = null;
+ await act(async () => {
+ renderer = TestRenderer.create(node);
+ });
+ return renderer as unknown as ReactTestRenderer;
+};
+
+const getTextNodes = (node: JsonNode): string[] => {
+ if (!node) return [];
+ if (typeof node === "string") return [node];
+ if (Array.isArray(node)) return node.flatMap(getTextNodes);
+ return node.children?.flatMap(getTextNodes) ?? [];
+};
+
+const hasProp = (node: JsonNode, prop: string, value: unknown): boolean => {
+ if (!node || typeof node === "string") return false;
+ if (Array.isArray(node))
+ return node.some((item) => hasProp(item, prop, value));
+ if (node.props[prop] === value) return true;
+ return node.children?.some((child) => hasProp(child, prop, value)) ?? false;
+};
+
+const resolveStyle = (
+ style: unknown,
+ pressed = false,
+): Record => {
+ const resolved = typeof style === "function" ? style({ pressed }) : style;
+ const styles = Array.isArray(resolved) ? resolved : [resolved];
+ return Object.assign({}, ...styles.filter(Boolean));
+};
+
+vi.mock("react-native", async () => {
+ const React = await import("react");
+ const createHost =
+ (name: string) =>
+ ({ children, ...props }: HostProps) =>
+ React.createElement(name, props, children);
+
+ return {
+ ActivityIndicator: createHost("ActivityIndicator"),
+ Pressable: createHost("Pressable"),
+ StyleSheet: {
+ absoluteFillObject: {
+ bottom: 0,
+ left: 0,
+ position: "absolute",
+ right: 0,
+ top: 0,
+ },
+ create: >(styles: T) => styles,
+ hairlineWidth: 1,
+ },
+ Text: createHost("Text"),
+ View: createHost("View"),
+ };
+});
+
+vi.mock("expo-image", async () => {
+ const React = await import("react");
+ return {
+ Image: (props: Record) =>
+ React.createElement("Image", props),
+ };
+});
+
+vi.mock("expo-symbols", async () => {
+ const React = await import("react");
+ return {
+ SymbolView: (props: Record) =>
+ React.createElement("SymbolView", props),
+ };
+});
+
+vi.mock("react-native-svg", async () => {
+ const React = await import("react");
+ const createHost =
+ (name: string) =>
+ ({ children, ...props }: HostProps) =>
+ React.createElement(name, props, children);
+
+ return {
+ default: createHost("Svg"),
+ Circle: createHost("Circle"),
+ };
+});
+
+const cap: MobileCapSummary = {
+ id: Video.VideoId.make("video_123"),
+ shareUrl: "https://cap.so/s/video_123",
+ title: "Launch review",
+ createdAt: "2026-05-18T10:00:00.000Z",
+ updatedAt: "2026-05-18T10:30:00.000Z",
+ ownerName: "Richie",
+ durationSeconds: 125,
+ thumbnailUrl: null,
+ folderId: null,
+ public: true,
+ protected: false,
+ viewCount: 7,
+ commentCount: 2,
+ reactionCount: 3,
+ upload: null,
+};
+
+describe("getCapCardViewModel", () => {
+ it("formats card rendering state", () => {
+ expect(
+ getCapCardViewModel(cap, new Date("2026-05-18T11:00:00.000Z")),
+ ).toMatchObject({
+ date: "an hour ago",
+ duration: "2 mins",
+ visibility: "Shared",
+ accessibilityLabel: "Launch review, an hour ago, Shared",
+ });
+ });
+
+ it("formats active upload state for the thumbnail overlay", () => {
+ expect(
+ getCapCardViewModel(
+ {
+ ...cap,
+ upload: {
+ uploaded: 25,
+ total: 100,
+ phase: "uploading",
+ processingProgress: 0,
+ processingMessage: null,
+ processingError: null,
+ },
+ },
+ new Date("2026-05-18T11:00:00.000Z"),
+ ),
+ ).toMatchObject({
+ uploadStatusText: "25% uploaded",
+ uploadProgress: 25,
+ uploadFailed: false,
+ accessibilityLabel: "Launch review, an hour ago, Shared, 25% uploaded",
+ });
+ });
+
+ it("keeps password protection separate from sharing state", () => {
+ expect(
+ getCapCardViewModel(
+ {
+ ...cap,
+ public: false,
+ protected: true,
+ },
+ new Date("2026-05-18T11:00:00.000Z"),
+ ),
+ ).toMatchObject({
+ date: "an hour ago",
+ visibility: "Not shared",
+ accessibilityLabel: "Launch review, an hour ago, Not shared",
+ });
+ });
+
+ it("uses processing progress as a percent value", () => {
+ expect(
+ getCapCardViewModel({
+ ...cap,
+ upload: {
+ uploaded: 100,
+ total: 100,
+ phase: "processing",
+ processingProgress: 42,
+ processingMessage: "Processing",
+ processingError: null,
+ },
+ }).uploadProgress,
+ ).toBe(42);
+ });
+
+ it("keeps non-finite upload progress display-safe", () => {
+ const uploading = getCapCardViewModel(
+ {
+ ...cap,
+ upload: {
+ uploaded: Number.NaN,
+ total: Number.NaN,
+ phase: "uploading",
+ processingProgress: 0,
+ processingMessage: null,
+ processingError: null,
+ },
+ },
+ new Date("2026-05-18T11:00:00.000Z"),
+ );
+ const processing = getCapCardViewModel({
+ ...cap,
+ upload: {
+ uploaded: 100,
+ total: 100,
+ phase: "processing",
+ processingProgress: Number.POSITIVE_INFINITY,
+ processingMessage: "Processing",
+ processingError: null,
+ },
+ });
+
+ expect(uploading).toMatchObject({
+ uploadStatusText: "0% uploaded",
+ uploadProgress: 0,
+ accessibilityLabel: "Launch review, an hour ago, Shared, 0% uploaded",
+ });
+ expect(processing.uploadProgress).toBe(0);
+ });
+
+ it("matches the web finishing state for completed processing records", () => {
+ expect(
+ getCapCardViewModel({
+ ...cap,
+ upload: {
+ uploaded: 100,
+ total: 100,
+ phase: "complete",
+ processingProgress: 100,
+ processingMessage: null,
+ processingError: null,
+ },
+ }).uploadStatusText,
+ ).toBe("Finishing up");
+ });
+});
+
+describe("CapCard", () => {
+ it("uses a branded thumbnail placeholder when a Cap has no thumbnail", async () => {
+ const tree = await renderTree(
+ React.createElement(CapCard, {
+ cap,
+ onPress: vi.fn(),
+ now: new Date("2026-05-18T11:00:00.000Z"),
+ }),
+ );
+
+ expect(hasProp(tree, "fill", "#cecece")).toBe(true);
+ expect(hasProp(tree, "name", "play.fill")).toBe(false);
+ });
+
+ it("exposes active upload progress as a native progressbar", async () => {
+ const renderer = await renderComponent(
+ React.createElement(CapCard, {
+ cap: {
+ ...cap,
+ upload: {
+ uploaded: 25,
+ total: 100,
+ phase: "uploading",
+ processingProgress: 0,
+ processingMessage: null,
+ processingError: null,
+ },
+ },
+ onPress: vi.fn(),
+ now: new Date("2026-05-18T11:00:00.000Z"),
+ }),
+ );
+ const [progress] = renderer.root.findAllByProps({
+ accessibilityLabel: "Upload progress",
+ });
+ if (!progress) throw new Error("Upload progress was not rendered");
+
+ expect(progress.props.accessibilityRole).toBe("progressbar");
+ expect(progress.props.accessibilityValue).toEqual({
+ max: 100,
+ min: 0,
+ now: 25,
+ text: "25%",
+ });
+ });
+
+ it("exposes processing upload state as an indeterminate progressbar", async () => {
+ const renderer = await renderComponent(
+ React.createElement(CapCard, {
+ cap: {
+ ...cap,
+ upload: {
+ uploaded: 100,
+ total: 100,
+ phase: "processing",
+ processingProgress: 0,
+ processingMessage: "Processing",
+ processingError: null,
+ },
+ },
+ onPress: vi.fn(),
+ now: new Date("2026-05-18T11:00:00.000Z"),
+ }),
+ );
+ const [progress] = renderer.root.findAllByProps({
+ accessibilityLabel: "Upload progress",
+ });
+ if (!progress) throw new Error("Upload progress was not rendered");
+
+ expect(progress.props.accessibilityRole).toBe("progressbar");
+ expect(progress.props.accessibilityValue).toEqual({
+ text: "Processing",
+ });
+ });
+
+ it("shows copy, share, and more actions together", async () => {
+ const tree = await renderTree(
+ React.createElement(CapCard, {
+ cap,
+ onPress: vi.fn(),
+ onCopyPress: vi.fn(),
+ onSharePress: vi.fn(),
+ onMenuPress: vi.fn(),
+ now: new Date("2026-05-18T11:00:00.000Z"),
+ }),
+ );
+
+ expect(
+ hasProp(tree, "accessibilityLabel", "Copy link for Launch review"),
+ ).toBe(true);
+ expect(hasProp(tree, "accessibilityHint", "Copies this Cap link")).toBe(
+ true,
+ );
+ expect(hasProp(tree, "accessibilityLabel", "Share Launch review")).toBe(
+ true,
+ );
+ expect(
+ hasProp(tree, "accessibilityHint", "Opens the native share sheet"),
+ ).toBe(true);
+ expect(
+ hasProp(tree, "accessibilityLabel", "More actions for Launch review"),
+ ).toBe(true);
+ expect(hasProp(tree, "accessibilityHint", "Opens Cap actions")).toBe(true);
+ });
+
+ it("uses the Cap web neutral button surface for card actions", async () => {
+ const renderer = await renderComponent(
+ React.createElement(CapCard, {
+ cap,
+ onPress: vi.fn(),
+ onCopyPress: vi.fn(),
+ onSharePress: vi.fn(),
+ onMenuPress: vi.fn(),
+ now: new Date("2026-05-18T11:00:00.000Z"),
+ }),
+ );
+ const [copyButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Copy link for Launch review",
+ });
+ if (!copyButton) throw new Error("Copy action was not rendered");
+
+ expect(resolveStyle(copyButton.props.style)).toMatchObject({
+ width: 32,
+ height: 32,
+ backgroundColor: "#f0f0f0",
+ borderColor: "#e0e0e0",
+ });
+ expect(resolveStyle(copyButton.props.style, true)).toMatchObject({
+ backgroundColor: "#e0e0e0",
+ borderColor: "#cecece",
+ });
+ expect(copyButton.props.hitSlop).toEqual({
+ bottom: 6,
+ left: 6,
+ right: 6,
+ top: 6,
+ });
+ });
+
+ it("opens visibility controls from the shared status row like the web card", async () => {
+ const onVisibilityPress = vi.fn();
+ const renderer = await renderComponent(
+ React.createElement(CapCard, {
+ cap,
+ onPress: vi.fn(),
+ onVisibilityPress,
+ now: new Date("2026-05-18T11:00:00.000Z"),
+ }),
+ );
+ const [shareState] = renderer.root.findAllByProps({
+ accessibilityLabel: "Change sharing for Launch review",
+ });
+ if (!shareState) throw new Error("Shared status action was not rendered");
+ const stopPropagation = vi.fn();
+
+ expect(shareState.props.accessibilityHint).toBe("Opens sharing settings");
+ expect(shareState.props.accessibilityState).toEqual({
+ busy: false,
+ disabled: false,
+ });
+ expect(shareState.props.hitSlop).toEqual({
+ bottom: 6,
+ left: 6,
+ right: 6,
+ top: 6,
+ });
+
+ await act(async () => {
+ shareState.props.onPress({ stopPropagation });
+ });
+
+ expect(stopPropagation).toHaveBeenCalled();
+ expect(onVisibilityPress).toHaveBeenCalledTimes(1);
+ });
+
+ it("shows a disabled sharing state while the card visibility is updating", async () => {
+ const renderer = await renderComponent(
+ React.createElement(CapCard, {
+ cap,
+ onPress: vi.fn(),
+ onVisibilityPress: vi.fn(),
+ visibilityBusy: true,
+ visibilityDisabled: true,
+ visibilityDisabledHint: "Sharing update is in progress",
+ visibilityAccessibilityValue: "Updating sharing for Launch review",
+ now: new Date("2026-05-18T11:00:00.000Z"),
+ }),
+ );
+ const [shareState] = renderer.root.findAllByProps({
+ accessibilityLabel: "Change sharing for Launch review",
+ });
+ if (!shareState) throw new Error("Shared status action was not rendered");
+
+ expect(getTextNodes(renderer.toJSON())).toContain("Shared");
+ expect(getTextNodes(renderer.toJSON())).not.toContain("Updating...");
+ expect(shareState.props.disabled).toBe(true);
+ expect(shareState.props.accessibilityHint).toBe(
+ "Sharing update is in progress",
+ );
+ expect(shareState.props.accessibilityState).toEqual({
+ busy: true,
+ disabled: true,
+ });
+ expect(shareState.props.accessibilityValue).toEqual({
+ text: "Updating sharing for Launch review",
+ });
+ expect(resolveStyle(shareState.props.style, true)).toMatchObject({
+ backgroundColor: "#f9f9f9",
+ });
+ });
+
+ it("opens analytics from the metrics row like the web card", async () => {
+ const onAnalyticsPress = vi.fn();
+ const renderer = await renderComponent(
+ React.createElement(CapCard, {
+ cap,
+ onAnalyticsPress,
+ onPress: vi.fn(),
+ now: new Date("2026-05-18T11:00:00.000Z"),
+ }),
+ );
+ const [metricsRow] = renderer.root.findAllByProps({
+ accessibilityLabel: "View analytics for Launch review",
+ });
+ if (!metricsRow) throw new Error("Analytics action was not rendered");
+ const stopPropagation = vi.fn();
+
+ expect(metricsRow.props.accessibilityHint).toBe(
+ "Opens analytics in a browser sheet",
+ );
+ expect(metricsRow.props.accessibilityState).toEqual({
+ disabled: false,
+ });
+
+ await act(async () => {
+ metricsRow.props.onPress({ stopPropagation });
+ });
+
+ expect(stopPropagation).toHaveBeenCalled();
+ expect(onAnalyticsPress).toHaveBeenCalledTimes(1);
+ });
+
+ it("marks metrics as disabled when analytics are informational only", async () => {
+ const renderer = await renderComponent(
+ React.createElement(CapCard, {
+ cap,
+ onPress: vi.fn(),
+ now: new Date("2026-05-18T11:00:00.000Z"),
+ }),
+ );
+ const [metricsRow] = renderer.root.findAllByProps({
+ accessibilityLabel: "View analytics for Launch review",
+ });
+ if (!metricsRow) throw new Error("Metrics row was not rendered");
+
+ expect(metricsRow.props.disabled).toBe(true);
+ expect(metricsRow.props.accessibilityHint).toBeUndefined();
+ expect(metricsRow.props.accessibilityState).toEqual({
+ disabled: true,
+ });
+ });
+});
diff --git a/apps/mobile/src/components/CapCard.tsx b/apps/mobile/src/components/CapCard.tsx
new file mode 100644
index 00000000000..16b698c5b4f
--- /dev/null
+++ b/apps/mobile/src/components/CapCard.tsx
@@ -0,0 +1,649 @@
+import { Image } from "expo-image";
+import { SymbolView } from "expo-symbols";
+import { useEffect, useRef, useState } from "react";
+import {
+ ActivityIndicator,
+ Pressable,
+ StyleSheet,
+ Text,
+ View,
+} from "react-native";
+import Svg, { Circle } from "react-native-svg";
+import type { MobileCapSummary } from "@/api/mobile";
+import { colors, fonts, radius, squircle } from "@/theme";
+import { getCapCardViewModel } from "./capCardViewModel";
+
+type CapCardProps = {
+ cap: MobileCapSummary;
+ onPress: () => void;
+ onCopyPress?: () => void;
+ onSharePress?: () => void;
+ onVisibilityPress?: () => void;
+ onAnalyticsPress?: () => void;
+ onMenuPress?: () => void;
+ visibilityBusy?: boolean;
+ visibilityDisabled?: boolean;
+ visibilityDisabledHint?: string;
+ visibilityValue?: string;
+ visibilityAccessibilityValue?: string;
+ now?: Date;
+};
+
+const progressSize = 18;
+const progressStrokeWidth = 3;
+const progressRadius = (progressSize - progressStrokeWidth) / 2;
+const progressCircumference = 2 * Math.PI * progressRadius;
+const compactHitSlop = { bottom: 6, left: 6, right: 6, top: 6 };
+
+const getProgressAccessibilityValue = (
+ progress: number | null,
+ indeterminate: boolean,
+ statusText: string,
+) => {
+ if (indeterminate || progress === null) {
+ return { text: statusText };
+ }
+
+ const clampedProgress = Math.min(100, Math.max(0, progress));
+
+ return {
+ max: 100,
+ min: 0,
+ now: clampedProgress,
+ text: `${clampedProgress}%`,
+ };
+};
+
+function CapThumbnailPlaceholder() {
+ return (
+
+
+
+
+
+
+ );
+}
+
+function UploadProgressIndicator({
+ progress,
+ indeterminate,
+ statusText,
+}: {
+ progress: number | null;
+ indeterminate: boolean;
+ statusText: string;
+}) {
+ const accessibilityValue = getProgressAccessibilityValue(
+ progress,
+ indeterminate,
+ statusText,
+ );
+
+ if (indeterminate || progress === null) {
+ return (
+
+
+
+ );
+ }
+
+ const strokeDashoffset =
+ progressCircumference -
+ (Math.min(100, Math.max(0, progress)) / 100) * progressCircumference;
+
+ return (
+
+
+
+
+
+ );
+}
+
+export function CapCard({
+ cap,
+ onPress,
+ onCopyPress,
+ onSharePress,
+ onVisibilityPress,
+ onAnalyticsPress,
+ onMenuPress,
+ visibilityBusy = false,
+ visibilityDisabled = false,
+ visibilityDisabledHint,
+ visibilityValue,
+ visibilityAccessibilityValue,
+ now,
+}: CapCardProps) {
+ const viewModel = getCapCardViewModel(cap, now);
+ const [copyPressed, setCopyPressed] = useState(false);
+ const copyResetTimer = useRef | null>(null);
+ const hasCopyAction = Boolean(onCopyPress);
+ const hasShareAction = Boolean(onSharePress);
+ const hasVisibleMenuAction = Boolean(onMenuPress);
+ const hasActions = hasCopyAction || hasShareAction || hasVisibleMenuAction;
+ const visibilityActionDisabled = visibilityDisabled || visibilityBusy;
+ const visibilityHint = visibilityActionDisabled
+ ? (visibilityDisabledHint ?? "Sharing update is in progress")
+ : "Opens sharing settings";
+ const visibilityText = visibilityValue ?? viewModel.visibility;
+ const uploadIndeterminate =
+ Boolean(cap.upload) &&
+ cap.upload?.phase !== "uploading" &&
+ (viewModel.uploadProgress ?? 0) === 0;
+
+ useEffect(
+ () => () => {
+ if (copyResetTimer.current) clearTimeout(copyResetTimer.current);
+ },
+ [],
+ );
+
+ const copyLink = () => {
+ if (!onCopyPress) return;
+ onCopyPress();
+ setCopyPressed(true);
+ if (copyResetTimer.current) clearTimeout(copyResetTimer.current);
+ copyResetTimer.current = setTimeout(() => {
+ setCopyPressed(false);
+ copyResetTimer.current = null;
+ }, 1400);
+ };
+
+ return (
+ [styles.card, pressed && styles.pressed]}
+ >
+
+ {hasActions ? (
+
+ {onCopyPress ? (
+ {
+ event.stopPropagation();
+ copyLink();
+ }}
+ style={({ pressed }) => [
+ styles.actionIconButton,
+ pressed && styles.actionIconButtonPressed,
+ ]}
+ >
+
+
+ ) : null}
+ {onSharePress ? (
+ {
+ event.stopPropagation();
+ onSharePress();
+ }}
+ style={({ pressed }) => [
+ styles.actionIconButton,
+ pressed && styles.actionIconButtonPressed,
+ ]}
+ >
+
+
+ ) : null}
+ {onMenuPress ? (
+ {
+ event.stopPropagation();
+ onMenuPress();
+ }}
+ style={({ pressed }) => [
+ styles.actionIconButton,
+ pressed && styles.actionIconButtonPressed,
+ ]}
+ >
+
+
+ ) : null}
+
+ ) : null}
+ {cap.thumbnailUrl ? (
+
+ ) : (
+
+ )}
+ {viewModel.uploadStatusText ? (
+
+
+
+ {viewModel.uploadStatusText}
+
+ {viewModel.uploadFailed ? null : (
+
+ )}
+
+
+ ) : null}
+ {cap.protected ? (
+
+
+
+ ) : null}
+ {viewModel.duration ? (
+
+ {viewModel.duration}
+
+ ) : null}
+
+
+
+
+ {cap.title}
+
+ {onVisibilityPress ? (
+ {
+ event.stopPropagation();
+ onVisibilityPress();
+ }}
+ style={({ pressed }) => [
+ styles.shareStateButton,
+ pressed &&
+ !visibilityActionDisabled &&
+ styles.shareStateButtonPressed,
+ visibilityActionDisabled && styles.shareStateButtonDisabled,
+ ]}
+ >
+
+ {visibilityText}
+
+
+
+ ) : (
+
+ {viewModel.visibility}
+
+ )}
+
+ {viewModel.date}
+
+
+ {
+ event.stopPropagation();
+ onAnalyticsPress?.();
+ }}
+ style={({ pressed }) => [
+ styles.metricsRow,
+ onAnalyticsPress && styles.metricsRowAction,
+ pressed && onAnalyticsPress && styles.metricsRowPressed,
+ ]}
+ >
+
+
+ {cap.viewCount}
+
+
+
+ {cap.commentCount}
+
+
+
+ {cap.reactionCount}
+
+ {onAnalyticsPress ? (
+ View analytics
+ ) : null}
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ card: {
+ backgroundColor: colors.gray1,
+ borderRadius: radius.md,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray3,
+ overflow: "hidden",
+ marginBottom: 16,
+ ...squircle,
+ },
+ pressed: {
+ backgroundColor: colors.gray2,
+ borderColor: colors.blue10,
+ },
+ thumbnailWrap: {
+ width: "100%",
+ aspectRatio: 16 / 9,
+ backgroundColor: colors.black,
+ position: "relative",
+ borderBottomWidth: StyleSheet.hairlineWidth,
+ borderBottomColor: colors.gray3,
+ },
+ thumbnail: {
+ width: "100%",
+ height: "100%",
+ },
+ emptyThumbnail: {
+ flex: 1,
+ alignItems: "center",
+ justifyContent: "center",
+ backgroundColor: colors.gray3,
+ overflow: "hidden",
+ },
+ placeholderSheen: {
+ position: "absolute",
+ top: -36,
+ left: -28,
+ width: "78%",
+ height: "140%",
+ backgroundColor: "rgba(255, 255, 255, 0.34)",
+ transform: [{ rotate: "18deg" }],
+ },
+ placeholderMark: {
+ width: 48,
+ height: 48,
+ borderRadius: radius.full,
+ alignItems: "center",
+ justifyContent: "center",
+ backgroundColor: "rgba(255, 255, 255, 0.72)",
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: "rgba(255, 255, 255, 0.95)",
+ ...squircle,
+ },
+ durationPill: {
+ position: "absolute",
+ left: 12,
+ bottom: 12,
+ minWidth: 46,
+ height: 23,
+ borderRadius: radius.full,
+ alignItems: "center",
+ justifyContent: "center",
+ backgroundColor: "rgba(0, 0, 0, 0.5)",
+ paddingHorizontal: 8,
+ ...squircle,
+ },
+ durationText: {
+ fontFamily: fonts.medium,
+ fontSize: 11,
+ color: colors.white,
+ },
+ lockBadge: {
+ position: "absolute",
+ right: 10,
+ top: 10,
+ zIndex: 2,
+ width: 28,
+ height: 28,
+ borderRadius: radius.full,
+ alignItems: "center",
+ justifyContent: "center",
+ backgroundColor: "rgba(0, 0, 0, 0.7)",
+ },
+ lockBadgeWithActions: {
+ right: 46,
+ },
+ actionStack: {
+ position: "absolute",
+ right: 10,
+ top: 10,
+ zIndex: 2,
+ gap: 8,
+ },
+ actionIconButton: {
+ width: 32,
+ height: 32,
+ borderRadius: radius.full,
+ alignItems: "center",
+ justifyContent: "center",
+ backgroundColor: colors.gray3,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray5,
+ shadowColor: colors.black,
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.1,
+ shadowRadius: 8,
+ elevation: 2,
+ ...squircle,
+ },
+ actionIconButtonPressed: {
+ backgroundColor: colors.gray5,
+ borderColor: colors.gray7,
+ },
+ uploadOverlay: {
+ ...StyleSheet.absoluteFillObject,
+ justifyContent: "flex-end",
+ backgroundColor: "rgba(0, 0, 0, 0.58)",
+ paddingHorizontal: 12,
+ paddingBottom: 12,
+ zIndex: 1,
+ },
+ uploadStatusRow: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 8,
+ paddingRight: 96,
+ },
+ uploadStatusText: {
+ fontFamily: fonts.medium,
+ fontSize: 14,
+ lineHeight: 19,
+ color: colors.white,
+ },
+ progressIndicator: {
+ width: progressSize,
+ height: progressSize,
+ alignItems: "center",
+ justifyContent: "center",
+ },
+ progressRing: {
+ transform: [{ rotate: "-90deg" }],
+ },
+ body: {
+ paddingHorizontal: 16,
+ paddingBottom: 16,
+ gap: 12,
+ },
+ title: {
+ fontFamily: fonts.medium,
+ fontSize: 16,
+ lineHeight: 21,
+ color: colors.gray12,
+ marginTop: 13,
+ marginBottom: 4,
+ },
+ shareState: {
+ fontFamily: fonts.regular,
+ fontSize: 14,
+ lineHeight: 19,
+ color: colors.gray10,
+ },
+ shareStateButton: {
+ alignSelf: "flex-start",
+ minHeight: 22,
+ maxWidth: "100%",
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 5,
+ marginBottom: 2,
+ borderRadius: radius.xs,
+ paddingHorizontal: 3,
+ marginLeft: -3,
+ ...squircle,
+ },
+ shareStateButtonPressed: {
+ backgroundColor: colors.gray3,
+ },
+ shareStateButtonDisabled: {
+ backgroundColor: colors.gray2,
+ },
+ shareStateDisabled: {
+ color: colors.gray8,
+ },
+ meta: {
+ fontFamily: fonts.regular,
+ fontSize: 14,
+ lineHeight: 20,
+ color: colors.gray10,
+ },
+ metricsRow: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 16,
+ minHeight: 24,
+ },
+ metricsRowAction: {
+ width: "100%",
+ maxWidth: "100%",
+ borderRadius: radius.xs,
+ paddingHorizontal: 3,
+ marginLeft: -3,
+ ...squircle,
+ },
+ metricsRowPressed: {
+ backgroundColor: colors.gray3,
+ },
+ metric: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 7,
+ },
+ metricText: {
+ fontFamily: fonts.regular,
+ fontSize: 14,
+ color: colors.gray12,
+ },
+ analyticsLink: {
+ marginLeft: "auto",
+ fontFamily: fonts.regular,
+ fontSize: 12,
+ lineHeight: 17,
+ color: colors.blue11,
+ },
+});
diff --git a/apps/mobile/src/components/CapLogoBadge.tsx b/apps/mobile/src/components/CapLogoBadge.tsx
new file mode 100644
index 00000000000..7393a5b2e3f
--- /dev/null
+++ b/apps/mobile/src/components/CapLogoBadge.tsx
@@ -0,0 +1,32 @@
+import Svg, { Path, Rect } from "react-native-svg";
+import { colors } from "@/theme";
+
+type CapLogoBadgeProps = {
+ size?: number;
+};
+
+export function CapLogoBadge({ size = 48 }: CapLogoBadgeProps) {
+ return (
+
+ );
+}
diff --git a/apps/mobile/src/components/CapRefreshControl.test.tsx b/apps/mobile/src/components/CapRefreshControl.test.tsx
new file mode 100644
index 00000000000..372a9adf588
--- /dev/null
+++ b/apps/mobile/src/components/CapRefreshControl.test.tsx
@@ -0,0 +1,57 @@
+import type { ReactElement, ReactNode } from "react";
+import { RefreshControl } from "react-native";
+import TestRenderer, { act, type ReactTestRenderer } from "react-test-renderer";
+import { describe, expect, it, vi } from "vitest";
+import { CapRefreshControl } from "./CapRefreshControl";
+
+type HostProps = {
+ children?: ReactNode;
+ [key: string]: unknown;
+};
+
+(
+ globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
+).IS_REACT_ACT_ENVIRONMENT = true;
+
+const renderComponent = async (
+ node: ReactElement,
+): Promise => {
+ let renderer: ReactTestRenderer | null = null;
+ await act(async () => {
+ renderer = TestRenderer.create(node);
+ });
+ return renderer as unknown as ReactTestRenderer;
+};
+
+vi.mock("react-native", async () => {
+ const React = await import("react");
+ const createHost =
+ (name: string) =>
+ ({ children, ...props }: HostProps) =>
+ React.createElement(name, props, children);
+
+ return {
+ RefreshControl: createHost("RefreshControl"),
+ StyleSheet: {
+ create: >(styles: T) => styles,
+ },
+ };
+});
+
+describe("CapRefreshControl", () => {
+ it("uses Cap web colors for native pull-to-refresh", async () => {
+ const onRefresh = vi.fn();
+ const renderer = await renderComponent(
+ ,
+ );
+ const refreshControl = renderer.root.findByType(RefreshControl);
+
+ expect(refreshControl.props).toMatchObject({
+ colors: ["#0d74ce"],
+ onRefresh,
+ progressBackgroundColor: "#fcfcfc",
+ refreshing: true,
+ tintColor: "#0d74ce",
+ });
+ });
+});
diff --git a/apps/mobile/src/components/CapRefreshControl.tsx b/apps/mobile/src/components/CapRefreshControl.tsx
new file mode 100644
index 00000000000..6c55078bc4a
--- /dev/null
+++ b/apps/mobile/src/components/CapRefreshControl.tsx
@@ -0,0 +1,22 @@
+import { RefreshControl } from "react-native";
+import { colors } from "@/theme";
+
+type CapRefreshControlProps = {
+ refreshing: boolean;
+ onRefresh: () => void;
+};
+
+export function CapRefreshControl({
+ refreshing,
+ onRefresh,
+}: CapRefreshControlProps) {
+ return (
+
+ );
+}
diff --git a/apps/mobile/src/components/GlassSurface.tsx b/apps/mobile/src/components/GlassSurface.tsx
new file mode 100644
index 00000000000..9ee85941ace
--- /dev/null
+++ b/apps/mobile/src/components/GlassSurface.tsx
@@ -0,0 +1,72 @@
+import {
+ GlassView,
+ isGlassEffectAPIAvailable,
+ isLiquidGlassAvailable,
+} from "expo-glass-effect";
+import type { ReactNode } from "react";
+import {
+ Platform,
+ type StyleProp,
+ StyleSheet,
+ View,
+ type ViewStyle,
+} from "react-native";
+import { colors } from "@/theme";
+
+type GlassSurfaceProps = {
+ children?: ReactNode;
+ style?: StyleProp;
+ fallbackStyle?: StyleProp;
+ glassEffectStyle?: "clear" | "regular" | "none";
+ tintColor?: string;
+ isInteractive?: boolean;
+};
+
+const getGlassAvailable = () => {
+ if (Platform.OS !== "ios") return false;
+ try {
+ return isGlassEffectAPIAvailable() && isLiquidGlassAvailable();
+ } catch {
+ return false;
+ }
+};
+
+const glassAvailable = getGlassAvailable();
+
+export function GlassSurface({
+ children,
+ style,
+ fallbackStyle,
+ glassEffectStyle = "regular",
+ tintColor = colors.glass,
+ isInteractive = false,
+}: GlassSurfaceProps) {
+ if (glassAvailable) {
+ return (
+
+ {children}
+
+ );
+ }
+
+ return (
+
+ {children}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ surface: {
+ overflow: "hidden",
+ },
+ fallback: {
+ backgroundColor: colors.glass,
+ },
+});
diff --git a/apps/mobile/src/components/OrgSwitcher.test.tsx b/apps/mobile/src/components/OrgSwitcher.test.tsx
new file mode 100644
index 00000000000..57f01ea78c0
--- /dev/null
+++ b/apps/mobile/src/components/OrgSwitcher.test.tsx
@@ -0,0 +1,188 @@
+import { Organisation, User } from "@cap/web-domain";
+import type { ReactElement, ReactNode } from "react";
+import TestRenderer, {
+ act,
+ type ReactTestRenderer,
+ type ReactTestRendererJSON,
+} from "react-test-renderer";
+import { describe, expect, it, vi } from "vitest";
+import type { MobileBootstrapResponse } from "@/api/mobile";
+import { OrgSwitcher } from "./OrgSwitcher";
+
+type HostProps = {
+ children?: ReactNode;
+ [key: string]: unknown;
+};
+
+type JsonNode = ReactTestRendererJSON | ReactTestRendererJSON[] | string | null;
+
+(
+ globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
+).IS_REACT_ACT_ENVIRONMENT = true;
+
+const renderTree = async (node: ReactElement): Promise => {
+ let renderer: ReactTestRenderer | null = null;
+ await act(async () => {
+ renderer = TestRenderer.create(node);
+ });
+ return (renderer as ReactTestRenderer | null)?.toJSON() ?? null;
+};
+
+const renderComponent = async (
+ node: ReactElement,
+): Promise => {
+ let renderer: ReactTestRenderer | null = null;
+ await act(async () => {
+ renderer = TestRenderer.create(node);
+ });
+ return renderer as unknown as ReactTestRenderer;
+};
+
+const hasImageSourceUri = (node: JsonNode, uri: string): boolean => {
+ if (!node || typeof node === "string") return false;
+ if (Array.isArray(node))
+ return node.some((item) => hasImageSourceUri(item, uri));
+ const source = node.props.source;
+ if (
+ source &&
+ typeof source === "object" &&
+ "uri" in source &&
+ source.uri === uri
+ ) {
+ return true;
+ }
+ return node.children?.some((child) => hasImageSourceUri(child, uri)) ?? false;
+};
+
+const resolveStyle = (
+ style: unknown,
+ pressed = false,
+): Record => {
+ const resolved = typeof style === "function" ? style({ pressed }) : style;
+ const styles = Array.isArray(resolved) ? resolved : [resolved];
+ return Object.assign({}, ...styles.filter(Boolean));
+};
+
+vi.mock("react-native", async () => {
+ const React = await import("react");
+ const createHost =
+ (name: string) =>
+ ({ children, ...props }: HostProps) =>
+ React.createElement(name, props, children);
+
+ return {
+ ActionSheetIOS: {
+ showActionSheetWithOptions: vi.fn(),
+ },
+ Modal: createHost("Modal"),
+ Platform: {
+ OS: "ios",
+ },
+ Pressable: createHost("Pressable"),
+ StyleSheet: {
+ create: >(styles: T) => styles,
+ hairlineWidth: 1,
+ },
+ Text: createHost("Text"),
+ View: createHost("View"),
+ };
+});
+
+vi.mock("expo-image", async () => {
+ const React = await import("react");
+ return {
+ Image: (props: Record) =>
+ React.createElement("Image", props),
+ };
+});
+
+vi.mock("expo-symbols", async () => {
+ const React = await import("react");
+ return {
+ SymbolView: (props: Record) =>
+ React.createElement("SymbolView", props),
+ };
+});
+
+const bootstrap: MobileBootstrapResponse = {
+ user: {
+ id: User.UserId.make("user_123"),
+ name: "Richie",
+ email: "richie@cap.so",
+ imageUrl: null,
+ activeOrganizationId: Organisation.OrganisationId.make("org_123"),
+ },
+ organizations: [
+ {
+ id: Organisation.OrganisationId.make("org_123"),
+ name: "Cap",
+ iconUrl: "https://cap.so/icon.png",
+ role: "owner",
+ },
+ {
+ id: Organisation.OrganisationId.make("org_456"),
+ name: "Design",
+ iconUrl: null,
+ role: "member",
+ },
+ ],
+ activeOrganizationId: Organisation.OrganisationId.make("org_123"),
+ rootFolders: [],
+};
+
+describe("OrgSwitcher", () => {
+ it("uses the organization icon when the active org has one", async () => {
+ const tree = await renderTree(
+ ,
+ );
+
+ expect(hasImageSourceUri(tree, "https://cap.so/icon.png")).toBe(true);
+ });
+
+ it("uses a native organization action sheet with roles and disabled active org", async () => {
+ const onChange = vi.fn(() => Promise.resolve());
+ const renderer = await renderComponent(
+ ,
+ );
+ const [trigger] = renderer.root.findAllByProps({
+ accessibilityLabel: "Switch organization",
+ });
+ if (!trigger) throw new Error("Organization switcher was not rendered");
+
+ expect(resolveStyle(trigger.props.style, true)).toMatchObject({
+ backgroundColor: "#f0f0f0",
+ borderColor: "#d9d9d9",
+ });
+
+ const { ActionSheetIOS } = await import("react-native");
+ const showActionSheetWithOptions = vi.mocked(
+ ActionSheetIOS.showActionSheetWithOptions,
+ );
+ showActionSheetWithOptions.mockClear();
+
+ await act(async () => {
+ trigger.props.onPress();
+ });
+
+ expect(showActionSheetWithOptions).toHaveBeenCalledWith(
+ expect.objectContaining({
+ cancelButtonIndex: 2,
+ disabledButtonIndices: [0],
+ disabledButtonTintColor: "#8d8d8d",
+ options: ["Cap (Owner)", "Design (Member)", "Cancel"],
+ title: "Organization",
+ userInterfaceStyle: "light",
+ }),
+ expect.any(Function),
+ );
+
+ const [, callback] = showActionSheetWithOptions.mock.calls[0] ?? [];
+ if (!callback) throw new Error("Organization action sheet did not open");
+
+ await act(async () => {
+ callback(1);
+ });
+
+ expect(onChange).toHaveBeenCalledWith("org_456");
+ });
+});
diff --git a/apps/mobile/src/components/OrgSwitcher.tsx b/apps/mobile/src/components/OrgSwitcher.tsx
new file mode 100644
index 00000000000..b87f5391fa8
--- /dev/null
+++ b/apps/mobile/src/components/OrgSwitcher.tsx
@@ -0,0 +1,246 @@
+import { Image } from "expo-image";
+import { SymbolView } from "expo-symbols";
+import { useState } from "react";
+import {
+ ActionSheetIOS,
+ Modal,
+ Platform,
+ Pressable,
+ StyleSheet,
+ Text,
+ View,
+} from "react-native";
+import type { MobileBootstrapResponse } from "@/api/mobile";
+import { colors, fonts, radius, shadows, squircle } from "@/theme";
+
+type OrgSwitcherProps = {
+ bootstrap: MobileBootstrapResponse;
+ onChange: (organizationId: string) => Promise;
+};
+
+type Organization = MobileBootstrapResponse["organizations"][number];
+
+const formatRole = (role: Organization["role"]) =>
+ role.slice(0, 1).toUpperCase() + role.slice(1);
+
+function OrgAvatar({ organization }: { organization: Organization }) {
+ return (
+
+ {organization.iconUrl ? (
+
+ ) : (
+
+ {organization.name.slice(0, 1).toUpperCase()}
+
+ )}
+
+ );
+}
+
+export function OrgSwitcher({ bootstrap, onChange }: OrgSwitcherProps) {
+ const [open, setOpen] = useState(false);
+ const activeOrganization =
+ bootstrap.organizations.find(
+ (org) => org.id === bootstrap.activeOrganizationId,
+ ) ?? bootstrap.organizations[0];
+
+ if (!activeOrganization) return null;
+
+ const openSwitcher = () => {
+ if (Platform.OS === "ios") {
+ const activeIndex = bootstrap.organizations.findIndex(
+ (organization) => organization.id === activeOrganization.id,
+ );
+ const options = [
+ ...bootstrap.organizations.map(
+ (organization) =>
+ `${organization.name} (${formatRole(organization.role)})`,
+ ),
+ "Cancel",
+ ];
+ ActionSheetIOS.showActionSheetWithOptions(
+ {
+ cancelButtonIndex: options.length - 1,
+ disabledButtonIndices: activeIndex >= 0 ? [activeIndex] : undefined,
+ disabledButtonTintColor: colors.gray9,
+ message: activeOrganization.name,
+ options,
+ title: "Organization",
+ tintColor: colors.blue11,
+ userInterfaceStyle: "light",
+ },
+ (index) => {
+ const organization = bootstrap.organizations[index];
+ if (organization && organization.id !== activeOrganization.id) {
+ void onChange(organization.id);
+ }
+ },
+ );
+ return;
+ }
+ setOpen(true);
+ };
+
+ return (
+ <>
+ [
+ styles.trigger,
+ pressed && styles.triggerPressed,
+ ]}
+ >
+
+
+ {activeOrganization.name}
+
+
+
+ setOpen(false)}
+ presentationStyle="overFullScreen"
+ transparent
+ visible={open}
+ >
+ setOpen(false)}>
+
+ Organization
+ {bootstrap.organizations.map((org) => {
+ const active = org.id === activeOrganization.id;
+ return (
+ {
+ setOpen(false);
+ if (!active) await onChange(org.id);
+ }}
+ style={styles.orgRow}
+ >
+
+
+
+ {org.name}
+
+ {org.role}
+
+ {active ? (
+
+ ) : null}
+
+ );
+ })}
+
+
+
+ >
+ );
+}
+
+const styles = StyleSheet.create({
+ trigger: {
+ height: 44,
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 9,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray5,
+ backgroundColor: colors.gray1,
+ borderRadius: radius.full,
+ paddingHorizontal: 10,
+ ...squircle,
+ },
+ triggerPressed: {
+ backgroundColor: colors.gray3,
+ borderColor: colors.gray6,
+ },
+ triggerText: {
+ flex: 1,
+ fontFamily: fonts.medium,
+ color: colors.gray12,
+ fontSize: 15,
+ },
+ avatar: {
+ width: 26,
+ height: 26,
+ borderRadius: radius.xs,
+ overflow: "hidden",
+ alignItems: "center",
+ justifyContent: "center",
+ backgroundColor: colors.blue3,
+ ...squircle,
+ },
+ avatarImage: {
+ width: "100%",
+ height: "100%",
+ },
+ avatarText: {
+ fontFamily: fonts.medium,
+ fontSize: 12,
+ color: colors.blue11,
+ },
+ overlay: {
+ flex: 1,
+ backgroundColor: colors.blackAlpha40,
+ justifyContent: "flex-end",
+ },
+ sheet: {
+ backgroundColor: colors.gray1,
+ borderTopLeftRadius: radius.xl,
+ borderTopRightRadius: radius.xl,
+ padding: 18,
+ paddingBottom: 32,
+ gap: 6,
+ ...shadows.popover,
+ ...squircle,
+ },
+ sheetTitle: {
+ fontFamily: fonts.medium,
+ fontSize: 20,
+ lineHeight: 26,
+ color: colors.gray12,
+ marginBottom: 6,
+ },
+ orgRow: {
+ minHeight: 58,
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 12,
+ borderRadius: radius.sm,
+ paddingHorizontal: 8,
+ ...squircle,
+ },
+ orgTextWrap: {
+ flex: 1,
+ minWidth: 0,
+ },
+ orgName: {
+ fontFamily: fonts.medium,
+ fontSize: 16,
+ color: colors.gray12,
+ },
+ orgRole: {
+ fontFamily: fonts.regular,
+ fontSize: 12,
+ color: colors.gray10,
+ textTransform: "capitalize",
+ },
+});
diff --git a/apps/mobile/src/components/Screen.test.tsx b/apps/mobile/src/components/Screen.test.tsx
new file mode 100644
index 00000000000..0e5fe245882
--- /dev/null
+++ b/apps/mobile/src/components/Screen.test.tsx
@@ -0,0 +1,109 @@
+import type React from "react";
+import TestRenderer, {
+ act,
+ type ReactTestRendererJSON,
+} from "react-test-renderer";
+import { describe, expect, it, vi } from "vitest";
+import { Screen } from "./Screen";
+
+type HostProps = {
+ children?: React.ReactNode;
+ [key: string]: unknown;
+};
+
+type JsonNode = ReactTestRendererJSON | ReactTestRendererJSON[] | string | null;
+
+(
+ globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
+).IS_REACT_ACT_ENVIRONMENT = true;
+
+const renderTree = async (): Promise => {
+ let renderer: TestRenderer.ReactTestRenderer | null = null;
+ await act(async () => {
+ renderer = TestRenderer.create(
+ ,
+ );
+ });
+ return (renderer as TestRenderer.ReactTestRenderer | null)?.toJSON() ?? null;
+};
+
+const findTextByValue = (
+ node: JsonNode,
+ value: string,
+): ReactTestRendererJSON | null => {
+ if (!node || typeof node === "string") return null;
+ if (Array.isArray(node)) {
+ for (const item of node) {
+ const match = findTextByValue(item, value);
+ if (match) return match;
+ }
+ return null;
+ }
+ if (node.type === "Text" && node.children?.includes(value)) return node;
+ for (const child of node.children ?? []) {
+ const match = findTextByValue(child, value);
+ if (match) return match;
+ }
+ return null;
+};
+
+const hasStyle = (
+ node: JsonNode,
+ expected: Record,
+): boolean => {
+ if (!node || typeof node === "string") return false;
+ if (Array.isArray(node)) return node.some((item) => hasStyle(item, expected));
+ const resolved = Array.isArray(node.props.style)
+ ? Object.assign({}, ...node.props.style.filter(Boolean))
+ : node.props.style;
+ if (
+ resolved &&
+ Object.entries(expected).every(([key, value]) => resolved[key] === value)
+ ) {
+ return true;
+ }
+ return node.children?.some((child) => hasStyle(child, expected)) ?? false;
+};
+
+vi.mock("react-native", async () => {
+ const React = await import("react");
+ const createHost =
+ (name: string) =>
+ ({ children, ...props }: HostProps) =>
+ React.createElement(name, props, children);
+
+ return {
+ ActivityIndicator: createHost("ActivityIndicator"),
+ RefreshControl: createHost("RefreshControl"),
+ ScrollView: createHost("ScrollView"),
+ StyleSheet: {
+ create: >(styles: T) => styles,
+ },
+ Text: createHost("Text"),
+ View: createHost("View"),
+ };
+});
+
+vi.mock("react-native-safe-area-context", async () => {
+ const React = await import("react");
+ return {
+ SafeAreaView: ({ children, ...props }: HostProps) =>
+ React.createElement("SafeAreaView", props, children),
+ };
+});
+
+describe("Screen", () => {
+ it("uses the Cap web subtitle scale", async () => {
+ const tree = await renderTree();
+ const subtitle = findTextByValue(
+ tree,
+ "Import videos from external sources.",
+ );
+
+ expect(subtitle?.props.style).toMatchObject({
+ fontSize: 14,
+ lineHeight: 20,
+ });
+ expect(hasStyle(tree, { paddingBottom: 32 })).toBe(true);
+ });
+});
diff --git a/apps/mobile/src/components/Screen.tsx b/apps/mobile/src/components/Screen.tsx
new file mode 100644
index 00000000000..2e2e456e8b8
--- /dev/null
+++ b/apps/mobile/src/components/Screen.tsx
@@ -0,0 +1,124 @@
+import type { ReactNode } from "react";
+import {
+ ActivityIndicator,
+ ScrollView,
+ StyleSheet,
+ Text,
+ View,
+} from "react-native";
+import { type Edge, SafeAreaView } from "react-native-safe-area-context";
+import { colors, fonts } from "@/theme";
+import { CapRefreshControl } from "./CapRefreshControl";
+
+type ScreenProps = {
+ children?: ReactNode;
+ title?: string;
+ subtitle?: string | null;
+ scroll?: boolean;
+ refreshing?: boolean;
+ onRefresh?: () => void;
+ loading?: boolean;
+ footer?: ReactNode;
+ safeEdges?: Edge[];
+};
+
+const defaultSafeEdges: Edge[] = ["top", "left", "right"];
+
+export function Screen({
+ children,
+ title,
+ subtitle,
+ scroll = false,
+ refreshing = false,
+ onRefresh,
+ loading = false,
+ footer,
+ safeEdges = defaultSafeEdges,
+}: ScreenProps) {
+ const content = (
+ <>
+ {title ? (
+
+ {title}
+ {subtitle ? {subtitle} : null}
+
+ ) : null}
+ {loading ? (
+
+
+
+ ) : (
+ children
+ )}
+ {footer}
+ >
+ );
+
+ return (
+
+ {scroll ? (
+
+ ) : undefined
+ }
+ >
+ {content}
+
+ ) : (
+ {content}
+ )}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ safeArea: {
+ flex: 1,
+ backgroundColor: colors.appBackground,
+ },
+ content: {
+ flex: 1,
+ paddingHorizontal: 20,
+ paddingBottom: 18,
+ },
+ scrollContent: {
+ flexGrow: 1,
+ paddingHorizontal: 20,
+ paddingBottom: 28,
+ },
+ header: {
+ paddingTop: 8,
+ paddingBottom: 16,
+ gap: 4,
+ },
+ headerWithSubtitle: {
+ paddingBottom: 32,
+ },
+ title: {
+ fontFamily: fonts.medium,
+ fontSize: 24,
+ lineHeight: 30,
+ color: colors.gray12,
+ },
+ subtitle: {
+ fontFamily: fonts.regular,
+ fontSize: 14,
+ lineHeight: 20,
+ color: colors.gray10,
+ },
+ loading: {
+ flex: 1,
+ alignItems: "center",
+ justifyContent: "center",
+ paddingVertical: 48,
+ },
+});
diff --git a/apps/mobile/src/components/capCardViewModel.ts b/apps/mobile/src/components/capCardViewModel.ts
new file mode 100644
index 00000000000..84aae8189ff
--- /dev/null
+++ b/apps/mobile/src/components/capCardViewModel.ts
@@ -0,0 +1,60 @@
+import type { MobileCapSummary } from "@/api/mobile";
+import { formatDuration, formatRelativeDate } from "../utils/format";
+
+const clampPercent = (value: number) => {
+ const safeValue = Number.isFinite(value) ? value : 0;
+ return Math.min(100, Math.max(0, Math.round(safeValue)));
+};
+
+const getUploadProgress = (cap: MobileCapSummary) => {
+ if (!cap.upload) return null;
+
+ if (cap.upload.phase === "uploading") {
+ return clampPercent(
+ (cap.upload.total > 0 ? cap.upload.uploaded / cap.upload.total : 0) * 100,
+ );
+ }
+
+ return clampPercent(cap.upload.processingProgress);
+};
+
+const getUploadStatusText = (cap: MobileCapSummary) => {
+ if (!cap.upload) return null;
+
+ switch (cap.upload.phase) {
+ case "processing":
+ return cap.upload.processingMessage ?? "Processing";
+ case "generating_thumbnail":
+ return cap.upload.processingMessage ?? "Finishing up";
+ case "complete":
+ return cap.upload.processingMessage ?? "Finishing up";
+ case "error":
+ return cap.upload.processingError ?? "Upload failed";
+ default:
+ return `${getUploadProgress(cap) ?? 0}% uploaded`;
+ }
+};
+
+export const getCapCardViewModel = (
+ cap: MobileCapSummary,
+ now = new Date(),
+) => {
+ const duration = formatDuration(cap.durationSeconds);
+ const date = formatRelativeDate(cap.createdAt, now);
+ const visibility = cap.public ? "Shared" : "Not shared";
+ const uploadStatusText = getUploadStatusText(cap);
+ const uploadProgress = getUploadProgress(cap);
+ const uploadFailed = cap.upload?.phase === "error";
+
+ return {
+ date,
+ duration,
+ visibility,
+ uploadStatusText,
+ uploadProgress,
+ uploadFailed,
+ accessibilityLabel: [cap.title, date, visibility, uploadStatusText]
+ .filter(Boolean)
+ .join(", "),
+ };
+};
diff --git a/apps/mobile/src/screens/account-settings.test.tsx b/apps/mobile/src/screens/account-settings.test.tsx
new file mode 100644
index 00000000000..ad331a8bc2d
--- /dev/null
+++ b/apps/mobile/src/screens/account-settings.test.tsx
@@ -0,0 +1,377 @@
+import React, { type ReactElement, type ReactNode } from "react";
+import TestRenderer, {
+ act,
+ type ReactTestRenderer,
+ type ReactTestRendererJSON,
+} from "react-test-renderer";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import AccountScreen from "../../app/(tabs)/account";
+
+type HostProps = {
+ children?: ReactNode;
+ [key: string]: unknown;
+};
+
+type JsonNode = ReactTestRendererJSON | ReactTestRendererJSON[] | string | null;
+
+const auth = vi.hoisted(() => ({
+ value: {
+ status: "signedIn" as const,
+ bootstrap: {
+ activeOrganizationId: "org_123",
+ user: {
+ email: "richie@cap.so",
+ imageUrl: null,
+ name: "Richie",
+ },
+ organizations: [
+ {
+ id: "org_123",
+ iconUrl: null,
+ name: "Cap",
+ role: "owner",
+ },
+ ],
+ rootFolders: [],
+ },
+ refresh: vi.fn(() => Promise.resolve()),
+ setActiveOrganization: vi.fn(() => Promise.resolve()),
+ signOut: vi.fn(() => Promise.resolve()),
+ },
+}));
+
+(
+ globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
+).IS_REACT_ACT_ENVIRONMENT = true;
+
+const renderComponent = async (
+ node: ReactElement,
+): Promise => {
+ let renderer: ReactTestRenderer | null = null;
+ await act(async () => {
+ renderer = TestRenderer.create(node);
+ });
+ return renderer as unknown as ReactTestRenderer;
+};
+
+const getTextNodes = (node: JsonNode): string[] => {
+ if (!node) return [];
+ if (typeof node === "string") return [node];
+ if (Array.isArray(node)) return node.flatMap(getTextNodes);
+ return node.children?.flatMap(getTextNodes) ?? [];
+};
+
+const createDeferred = () => {
+ let resolve!: (value: T | PromiseLike) => void;
+ const promise = new Promise((nextResolve) => {
+ resolve = nextResolve;
+ });
+ return { promise, resolve };
+};
+
+vi.mock("react-native", async () => {
+ const React = await import("react");
+ const createHost =
+ (name: string) =>
+ ({ children, ...props }: HostProps) =>
+ React.createElement(name, props, children);
+
+ return {
+ ActionSheetIOS: {
+ showActionSheetWithOptions: vi.fn(),
+ },
+ ActivityIndicator: createHost("ActivityIndicator"),
+ Alert: {
+ alert: vi.fn(),
+ },
+ Linking: {
+ openSettings: vi.fn(),
+ },
+ Platform: {
+ OS: "ios",
+ },
+ Pressable: createHost("Pressable"),
+ StyleSheet: {
+ create: >(styles: T) => styles,
+ hairlineWidth: 1,
+ },
+ Text: createHost("Text"),
+ View: createHost("View"),
+ };
+});
+
+vi.mock("expo-constants", () => ({
+ default: {
+ expoConfig: {
+ version: "0.1.0",
+ },
+ },
+}));
+
+vi.mock("expo-image", async () => {
+ const React = await import("react");
+ return {
+ Image: (props: Record) =>
+ React.createElement("Image", props),
+ };
+});
+
+vi.mock("expo-symbols", async () => {
+ const React = await import("react");
+ return {
+ SymbolView: (props: Record) =>
+ React.createElement("SymbolView", props),
+ };
+});
+
+vi.mock("expo-web-browser", () => ({
+ openBrowserAsync: vi.fn(),
+}));
+
+vi.mock("@/auth/AuthContext", () => ({
+ apiBaseUrl: "https://cap.so",
+ useAuth: () => auth.value,
+}));
+
+vi.mock("@/auth/SignInPanel", async () => {
+ const React = await import("react");
+ return {
+ SignInPanel: () => React.createElement("SignInPanel"),
+ };
+});
+
+vi.mock("@/components/GlassSurface", async () => {
+ const React = await import("react");
+ return {
+ GlassSurface: ({ children }: { children?: ReactNode }) =>
+ React.createElement("GlassSurface", null, children),
+ };
+});
+
+vi.mock("@/components/OrgSwitcher", async () => {
+ const React = await import("react");
+ return {
+ OrgSwitcher: () => React.createElement("OrgSwitcher"),
+ };
+});
+
+vi.mock("@/components/Screen", async () => {
+ const React = await import("react");
+ return {
+ Screen: ({
+ children,
+ subtitle,
+ title,
+ }: {
+ children?: ReactNode;
+ subtitle?: string | null;
+ title?: string;
+ }) =>
+ React.createElement(
+ "Screen",
+ null,
+ title ? React.createElement("Text", null, title) : null,
+ subtitle ? React.createElement("Text", null, subtitle) : null,
+ children,
+ ),
+ };
+});
+
+describe("AccountScreen", () => {
+ beforeEach(() => {
+ auth.value.refresh.mockReset();
+ auth.value.refresh.mockResolvedValue(undefined);
+ auth.value.setActiveOrganization.mockReset();
+ auth.value.setActiveOrganization.mockResolvedValue(undefined);
+ auth.value.signOut.mockReset();
+ auth.value.signOut.mockResolvedValue(undefined);
+ });
+
+ it("opens organization settings in the native browser sheet", async () => {
+ const renderer = await renderComponent(React.createElement(AccountScreen));
+ const text = getTextNodes(renderer.toJSON());
+ const [organizationSettings] = renderer.root.findAllByProps({
+ accessibilityLabel: "Organization Settings",
+ });
+ if (!organizationSettings) {
+ throw new Error("Organization Settings row was not rendered");
+ }
+
+ expect(text).toContain("Account");
+ expect(text).toContain("Organization Settings");
+ expect(organizationSettings.props.accessibilityHint).toBe(
+ "Opens organization settings in a browser sheet",
+ );
+
+ const WebBrowser = await import("expo-web-browser");
+ const openBrowserAsync = vi.mocked(WebBrowser.openBrowserAsync);
+ const openDeferred =
+ createDeferred>>();
+ openBrowserAsync.mockClear();
+ openBrowserAsync.mockReturnValueOnce(openDeferred.promise);
+
+ await act(async () => {
+ organizationSettings.props.onPress();
+ await Promise.resolve();
+ });
+
+ expect(openBrowserAsync).toHaveBeenCalledWith(
+ "https://cap.so/dashboard/settings/organization",
+ );
+ const [openingOrganizationSettings] = renderer.root.findAllByProps({
+ accessibilityLabel: "Organization Settings",
+ });
+ expect(openingOrganizationSettings?.props.accessibilityValue).toEqual({
+ text: "Opening organization settings",
+ });
+
+ await act(async () => {
+ openDeferred.resolve({
+ type: "dismiss",
+ } as Awaited>);
+ await openDeferred.promise;
+ });
+ });
+
+ it("marks app settings as opening with a native value", async () => {
+ const renderer = await renderComponent(React.createElement(AccountScreen));
+ const [appSettings] = renderer.root.findAllByProps({
+ accessibilityLabel: "App Settings",
+ });
+ if (!appSettings) throw new Error("App Settings row was not rendered");
+
+ const { Linking } = await import("react-native");
+ const openSettings = vi.mocked(Linking.openSettings);
+ const openDeferred = createDeferred();
+ openSettings.mockClear();
+ openSettings.mockReturnValueOnce(openDeferred.promise);
+
+ await act(async () => {
+ appSettings.props.onPress();
+ await Promise.resolve();
+ });
+
+ const [openingAppSettings] = renderer.root.findAllByProps({
+ accessibilityLabel: "App Settings",
+ });
+ expect(openingAppSettings?.props.accessibilityValue).toEqual({
+ text: "Opening iOS app settings",
+ });
+
+ await act(async () => {
+ openDeferred.resolve();
+ await openDeferred.promise;
+ });
+ });
+
+ it("locks account settings rows while refresh is in progress", async () => {
+ const refreshDeferred = createDeferred();
+ auth.value.refresh.mockReturnValueOnce(refreshDeferred.promise);
+ const renderer = await renderComponent(React.createElement(AccountScreen));
+ const [refreshRow] = renderer.root.findAllByProps({
+ accessibilityLabel: "Refresh",
+ });
+ if (!refreshRow) throw new Error("Refresh row was not rendered");
+
+ await act(async () => {
+ refreshRow.props.onPress();
+ await Promise.resolve();
+ });
+
+ const [loadingRefreshRow] = renderer.root.findAllByProps({
+ accessibilityLabel: "Refresh",
+ });
+ const [organizationSettings] = renderer.root.findAllByProps({
+ accessibilityLabel: "Organization Settings",
+ });
+ const [appSettings] = renderer.root.findAllByProps({
+ accessibilityLabel: "App Settings",
+ });
+ const [signOut] = renderer.root.findAllByProps({
+ accessibilityLabel: "Sign out",
+ });
+ if (
+ !loadingRefreshRow ||
+ !organizationSettings ||
+ !appSettings ||
+ !signOut
+ ) {
+ throw new Error("Account action rows were not rendered");
+ }
+
+ expect(loadingRefreshRow.props.accessibilityState).toEqual({
+ busy: true,
+ disabled: true,
+ });
+ expect(loadingRefreshRow.props.accessibilityHint).toBe(
+ "Refresh is in progress",
+ );
+ expect(loadingRefreshRow.props.accessibilityValue).toEqual({
+ text: "Refreshing account data",
+ });
+ expect(getTextNodes(renderer.toJSON())).toContain("Refreshing...");
+ for (const row of [organizationSettings, appSettings, signOut]) {
+ expect(row.props.disabled).toBe(true);
+ expect(row.props.accessibilityState).toEqual({
+ busy: false,
+ disabled: true,
+ });
+ expect(row.props.accessibilityHint).toBe("Refresh is in progress");
+ }
+
+ await act(async () => {
+ refreshDeferred.resolve();
+ await refreshDeferred.promise;
+ });
+ });
+
+ it("shows sign-out as busy after confirmation", async () => {
+ const signOutDeferred = createDeferred();
+ auth.value.signOut.mockReturnValueOnce(signOutDeferred.promise);
+ const renderer = await renderComponent(React.createElement(AccountScreen));
+ const [signOut] = renderer.root.findAllByProps({
+ accessibilityLabel: "Sign out",
+ });
+ if (!signOut) throw new Error("Sign out row was not rendered");
+
+ const { ActionSheetIOS } = await import("react-native");
+ const showActionSheetWithOptions = vi.mocked(
+ ActionSheetIOS.showActionSheetWithOptions,
+ );
+ showActionSheetWithOptions.mockClear();
+
+ await act(async () => {
+ signOut.props.onPress();
+ });
+
+ const [, callback] = showActionSheetWithOptions.mock.calls[0] ?? [];
+ if (!callback)
+ throw new Error("Sign-out confirmation callback was not set");
+
+ await act(async () => {
+ callback(0);
+ await Promise.resolve();
+ });
+
+ const [loadingSignOut] = renderer.root.findAllByProps({
+ accessibilityLabel: "Sign out",
+ });
+ if (!loadingSignOut) throw new Error("Sign out row was not rendered");
+ expect(loadingSignOut.props.accessibilityState).toEqual({
+ busy: true,
+ disabled: true,
+ });
+ expect(loadingSignOut.props.accessibilityHint).toBe(
+ "Sign out is in progress",
+ );
+ expect(loadingSignOut.props.accessibilityValue).toEqual({
+ text: "Signing out of Cap",
+ });
+ expect(getTextNodes(renderer.toJSON())).toContain("Signing out...");
+
+ await act(async () => {
+ signOutDeferred.resolve();
+ await signOutDeferred.promise;
+ });
+ });
+});
diff --git a/apps/mobile/src/screens/cap-detail.test.tsx b/apps/mobile/src/screens/cap-detail.test.tsx
new file mode 100644
index 00000000000..c987b6fae80
--- /dev/null
+++ b/apps/mobile/src/screens/cap-detail.test.tsx
@@ -0,0 +1,671 @@
+import { Comment, User, Video } from "@cap/web-domain";
+import React, { type ReactElement, type ReactNode } from "react";
+import TestRenderer, {
+ act,
+ type ReactTestRenderer,
+ type ReactTestRendererJSON,
+} from "react-test-renderer";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import type {
+ MobileCapDetail,
+ MobileComment,
+ MobilePlaybackResponse,
+} from "@/api/mobile";
+import CapDetailScreen from "../../app/caps/[id]";
+
+type HostProps = {
+ children?: ReactNode;
+ [key: string]: unknown;
+};
+
+type JsonNode = ReactTestRendererJSON | ReactTestRendererJSON[] | string | null;
+
+type AuthStub = {
+ status: "signedIn";
+ client: {
+ createComment: ReturnType;
+ createReaction: ReturnType;
+ deleteCap: ReturnType;
+ getCap: ReturnType;
+ getPlayback: ReturnType;
+ updateCapSharing: ReturnType;
+ };
+ refresh: ReturnType;
+};
+
+(
+ globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
+).IS_REACT_ACT_ENVIRONMENT = true;
+
+const detail: MobileCapDetail = {
+ cap: {
+ id: Video.VideoId.make("video_123"),
+ shareUrl: "https://cap.so/s/video_123",
+ title: "Launch review",
+ createdAt: "2026-05-18T10:00:00.000Z",
+ updatedAt: "2026-05-18T10:30:00.000Z",
+ ownerName: "Richie",
+ durationSeconds: 125,
+ thumbnailUrl: null,
+ folderId: null,
+ public: true,
+ protected: true,
+ viewCount: 17,
+ commentCount: 2,
+ reactionCount: 3,
+ upload: null,
+ },
+ summary: "A short launch walkthrough.",
+ chapters: [],
+ transcriptionStatus: "COMPLETE",
+ comments: [],
+ shareUrl: "https://cap.so/s/video_123",
+};
+
+const playback: MobilePlaybackResponse = {
+ kind: "mp4",
+ transcriptUrl: null,
+ url: "https://cap.so/video.mp4",
+};
+
+const createdComment = (content: string): MobileComment => ({
+ id: Comment.CommentId.make("comment_123"),
+ videoId: Video.VideoId.make("video_123"),
+ type: "text",
+ content,
+ timestamp: null,
+ parentCommentId: null,
+ createdAt: "2026-05-18T10:31:00.000Z",
+ updatedAt: "2026-05-18T10:31:00.000Z",
+ author: {
+ id: User.UserId.make("user_123"),
+ name: "Richie",
+ imageUrl: null,
+ },
+});
+
+const createDeferred = () => {
+ let resolve!: (value: T | PromiseLike) => void;
+ const promise = new Promise((nextResolve) => {
+ resolve = nextResolve;
+ });
+ return { promise, resolve };
+};
+
+const authState = vi.hoisted((): { value: AuthStub | null } => ({
+ value: null,
+}));
+
+const renderComponent = async (
+ node: ReactElement,
+): Promise => {
+ let renderer: ReactTestRenderer | null = null;
+ await act(async () => {
+ renderer = TestRenderer.create(node);
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+ return renderer as unknown as ReactTestRenderer;
+};
+
+const getTextNodes = (node: JsonNode): string[] => {
+ if (!node) return [];
+ if (typeof node === "string") return [node];
+ if (Array.isArray(node)) return node.flatMap(getTextNodes);
+ return node.children?.flatMap(getTextNodes) ?? [];
+};
+
+const hasProp = (node: JsonNode, prop: string, value: unknown): boolean => {
+ if (!node || typeof node === "string") return false;
+ if (Array.isArray(node))
+ return node.some((item) => hasProp(item, prop, value));
+ if (node.props[prop] === value) return true;
+ return node.children?.some((child) => hasProp(child, prop, value)) ?? false;
+};
+
+const resolveStyle = (
+ style: unknown,
+ pressed = false,
+): Record => {
+ const resolved =
+ typeof style === "function"
+ ? (style as (state: { pressed: boolean }) => unknown)({ pressed })
+ : style;
+ const styles = Array.isArray(resolved) ? resolved : [resolved];
+ return Object.assign({}, ...styles.filter(Boolean));
+};
+
+const createAuth = (): AuthStub => ({
+ status: "signedIn",
+ client: {
+ createComment: vi.fn(),
+ createReaction: vi.fn(),
+ deleteCap: vi.fn(),
+ getCap: vi.fn(() => Promise.resolve(detail)),
+ getPlayback: vi.fn(() => Promise.resolve(playback)),
+ updateCapSharing: vi.fn(),
+ },
+ refresh: vi.fn(() => Promise.resolve()),
+});
+
+vi.mock("react-native", async () => {
+ const React = await import("react");
+ const createHost =
+ (name: string) =>
+ ({ children, ...props }: HostProps) =>
+ React.createElement(name, props, children);
+
+ return {
+ ActionSheetIOS: {
+ showActionSheetWithOptions: vi.fn(),
+ },
+ Alert: {
+ alert: vi.fn(),
+ },
+ KeyboardAvoidingView: createHost("KeyboardAvoidingView"),
+ Linking: {
+ openSettings: vi.fn(),
+ },
+ Platform: {
+ OS: "ios",
+ },
+ Pressable: createHost("Pressable"),
+ Share: {
+ share: vi.fn(),
+ },
+ StyleSheet: {
+ absoluteFillObject: {
+ bottom: 0,
+ left: 0,
+ position: "absolute",
+ right: 0,
+ top: 0,
+ },
+ create: >(styles: T) => styles,
+ hairlineWidth: 1,
+ },
+ Text: createHost("Text"),
+ TextInput: createHost("TextInput"),
+ View: createHost("View"),
+ };
+});
+
+vi.mock("expo-clipboard", () => ({
+ setStringAsync: vi.fn(),
+}));
+
+vi.mock("expo-router", async () => {
+ const React = await import("react");
+ return {
+ router: {
+ back: vi.fn(),
+ },
+ Stack: {
+ Screen: (props: HostProps) =>
+ React.createElement("StackScreen", {
+ ...props,
+ testID: "stack-screen",
+ }),
+ },
+ useLocalSearchParams: () => ({ id: "video_123" }),
+ };
+});
+
+vi.mock("expo-symbols", async () => {
+ const React = await import("react");
+ return {
+ SymbolView: (props: Record) =>
+ React.createElement("SymbolView", props),
+ };
+});
+
+vi.mock("expo-video", async () => {
+ const React = await import("react");
+ return {
+ VideoView: (props: Record) =>
+ React.createElement("VideoView", props),
+ useVideoPlayer: () => ({
+ replace: vi.fn(),
+ }),
+ };
+});
+
+vi.mock("expo-web-browser", () => ({
+ openBrowserAsync: vi.fn(),
+}));
+
+vi.mock("@/auth/AuthContext", () => ({
+ apiBaseUrl: "https://cap.so",
+ useAuth: () => authState.value,
+}));
+
+vi.mock("@/auth/SignInPanel", async () => {
+ const React = await import("react");
+ return {
+ SignInPanel: () => React.createElement("SignInPanel"),
+ };
+});
+
+vi.mock("@/caps/CapSettingsSheet", async () => {
+ const React = await import("react");
+ return {
+ CapSettingsSheet: (props: Record) =>
+ React.createElement("CapSettingsSheet", {
+ ...props,
+ testID: "cap-settings-sheet",
+ }),
+ };
+});
+
+vi.mock("@/caps/passwordActions", () => ({
+ showCapPasswordActions: vi.fn(),
+}));
+
+vi.mock("@/caps/saveCapVideo", () => ({
+ PhotosPermissionDeniedError: class PhotosPermissionDeniedError extends Error {},
+ saveCapVideoToPhotos: vi.fn(),
+}));
+
+vi.mock("@/caps/titleActions", () => ({
+ showCapTitleActions: vi.fn(),
+}));
+
+vi.mock("@/components/ActionButton", async () => {
+ const React = await import("react");
+ return {
+ ActionButton: ({
+ children,
+ label,
+ onPress,
+ ...props
+ }: {
+ children?: ReactNode;
+ label: string;
+ onPress?: () => void;
+ [key: string]: unknown;
+ }) =>
+ React.createElement(
+ "ActionButton",
+ {
+ ...props,
+ accessibilityLabel: props.accessibilityLabel ?? label,
+ onPress,
+ },
+ children ?? label,
+ ),
+ };
+});
+
+vi.mock("@/components/GlassSurface", async () => {
+ const React = await import("react");
+ return {
+ GlassSurface: ({ children }: { children?: ReactNode }) =>
+ React.createElement("GlassSurface", null, children),
+ };
+});
+
+vi.mock("@/components/Screen", async () => {
+ const React = await import("react");
+ return {
+ Screen: ({
+ children,
+ loading,
+ }: {
+ children?: ReactNode;
+ loading?: boolean;
+ }) =>
+ React.createElement(
+ "Screen",
+ null,
+ loading ? React.createElement("Text", null, "Loading") : children,
+ ),
+ };
+});
+
+describe("Cap detail screen", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ authState.value = createAuth();
+ });
+
+ it("announces Cap detail load errors with a retry action", async () => {
+ const auth = createAuth();
+ auth.client.getCap = vi.fn(() =>
+ Promise.reject(new Error("Network unavailable")),
+ );
+ authState.value = auth;
+
+ const renderer = await renderComponent(
+ React.createElement(CapDetailScreen),
+ );
+ const tree = renderer.toJSON();
+ const text = getTextNodes(tree);
+
+ expect(text).toContain("Unable to load Cap");
+ expect(text).toContain("Network unavailable");
+ expect(hasProp(tree, "accessibilityRole", "alert")).toBe(true);
+ expect(hasProp(tree, "accessibilityLiveRegion", "polite")).toBe(true);
+ expect(
+ hasProp(
+ tree,
+ "accessibilityLabel",
+ "Cap detail error: Network unavailable",
+ ),
+ ).toBe(true);
+
+ const [retryButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Try again",
+ });
+ if (!retryButton)
+ throw new Error("Cap detail retry action was not rendered");
+ expect(retryButton.props.accessibilityHint).toBe("Reloads this Cap");
+
+ await act(async () => {
+ retryButton.props.onPress();
+ await Promise.resolve();
+ });
+
+ expect(auth.client.getCap).toHaveBeenCalledTimes(2);
+ });
+
+ it("shows web-matching sharing, analytics, and action labels", async () => {
+ const renderer = await renderComponent(
+ React.createElement(CapDetailScreen),
+ );
+ const tree = renderer.toJSON();
+ const text = getTextNodes(tree);
+
+ expect(text).toContain("Launch review");
+ expect(text).toContain("Shared");
+ expect(text).toContain("Password protected");
+ expect(text).toContain("17");
+ expect(text).toContain("2");
+ expect(text).toContain("3");
+ expect(text).toContain("Copy link");
+ expect(text).toContain("Save video");
+ expect(text).toContain("View analytics");
+ expect(hasProp(tree, "accessibilityHint", "Copies this Cap link")).toBe(
+ true,
+ );
+ expect(
+ hasProp(tree, "accessibilityHint", "Opens the native share sheet"),
+ ).toBe(true);
+ expect(
+ hasProp(tree, "accessibilityHint", "Saves this video to Photos"),
+ ).toBe(true);
+ expect(
+ hasProp(tree, "accessibilityLabel", "Change sharing for Launch review"),
+ ).toBe(true);
+ expect(hasProp(tree, "accessibilityHint", "Opens sharing settings")).toBe(
+ true,
+ );
+ expect(
+ hasProp(tree, "accessibilityLabel", "View analytics for Launch review"),
+ ).toBe(true);
+ expect(
+ hasProp(tree, "accessibilityHint", "Opens analytics in a browser sheet"),
+ ).toBe(true);
+ });
+
+ it("uses native affordances for the header menu and comment composer", async () => {
+ const renderer = await renderComponent(
+ React.createElement(CapDetailScreen),
+ );
+ const stackScreen = renderer.root.findByProps({
+ testID: "stack-screen",
+ });
+ const headerRight = stackScreen.props.options
+ .headerRight as () => ReactNode;
+ let headerRenderer: ReactTestRenderer | null = null;
+
+ await act(async () => {
+ headerRenderer = TestRenderer.create(headerRight() as ReactElement);
+ });
+
+ const headerAction = (
+ headerRenderer as unknown as ReactTestRenderer
+ ).root.findByProps({
+ accessibilityLabel: "More actions",
+ });
+ expect(headerAction.props.accessibilityState).toEqual({
+ disabled: false,
+ });
+ expect(headerAction.props.accessibilityHint).toBe("Opens Cap settings");
+ expect(headerAction.props.hitSlop).toBe(10);
+ expect(resolveStyle(headerAction.props.style, true)).toMatchObject({
+ backgroundColor: "#f0f0f0",
+ });
+
+ const [commentInput] = renderer.root.findAllByProps({
+ accessibilityLabel: "Comment",
+ });
+ if (!commentInput) throw new Error("Comment input was not rendered");
+
+ expect(commentInput.props.accessibilityState).toEqual({
+ disabled: false,
+ });
+ expect(commentInput.props.enablesReturnKeyAutomatically).toBe(true);
+ expect(commentInput.props.keyboardAppearance).toBe("light");
+ expect(commentInput.props.returnKeyType).toBe("send");
+ expect(commentInput.props.selectionColor).toBe("#0d74ce");
+ expect(commentInput.props.submitBehavior).toBe("blurAndSubmit");
+
+ const [disabledSend] = renderer.root.findAllByProps({
+ accessibilityLabel: "Send comment",
+ });
+ expect(disabledSend?.props.disabled).toBe(true);
+
+ await act(async () => {
+ commentInput.props.onChangeText("Ship it");
+ });
+
+ const [enabledSend] = renderer.root.findAllByProps({
+ accessibilityLabel: "Send comment",
+ });
+ expect(enabledSend?.props.disabled).toBe(false);
+ });
+
+ it("opens native settings from the sharing status", async () => {
+ const renderer = await renderComponent(
+ React.createElement(CapDetailScreen),
+ );
+ const [shareStatus] = renderer.root.findAllByProps({
+ accessibilityLabel: "Change sharing for Launch review",
+ });
+ if (!shareStatus) throw new Error("Sharing status row was not rendered");
+
+ await act(async () => {
+ shareStatus.props.onPress();
+ });
+
+ const [sheet] = renderer.root.findAllByProps({
+ testID: "cap-settings-sheet",
+ });
+ expect(sheet?.props.visible).toBe(true);
+ });
+
+ it("opens analytics in the native browser sheet", async () => {
+ const renderer = await renderComponent(
+ React.createElement(CapDetailScreen),
+ );
+ const [analytics] = renderer.root.findAllByProps({
+ accessibilityLabel: "View analytics for Launch review",
+ });
+ if (!analytics) throw new Error("Analytics row was not rendered");
+
+ const WebBrowser = await import("expo-web-browser");
+ const openBrowserAsync = vi.mocked(WebBrowser.openBrowserAsync);
+ openBrowserAsync.mockClear();
+
+ await act(async () => {
+ analytics.props.onPress();
+ });
+
+ expect(openBrowserAsync).toHaveBeenCalledWith(
+ "https://cap.so/dashboard/analytics?capId=video_123",
+ );
+ });
+
+ it("shows a save-specific busy state without blocking sharing as saving", async () => {
+ const saveDeferred = createDeferred();
+ const { saveCapVideoToPhotos } = await import("@/caps/saveCapVideo");
+ vi.mocked(saveCapVideoToPhotos).mockReturnValueOnce(saveDeferred.promise);
+ const renderer = await renderComponent(
+ React.createElement(CapDetailScreen),
+ );
+ const [saveButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Save video",
+ });
+ if (!saveButton) throw new Error("Save video button was not rendered");
+
+ await act(async () => {
+ saveButton.props.onPress();
+ await Promise.resolve();
+ });
+
+ const [savingButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Save video",
+ });
+ if (!savingButton) throw new Error("Saving button was not rendered");
+ expect(savingButton.props.loading).toBe(true);
+ expect(savingButton.props.accessibilityHint).toBe("Save is in progress");
+ expect(savingButton.props.accessibilityValue).toEqual({
+ text: "Saving video for Launch review",
+ });
+
+ const [sheet] = renderer.root.findAllByProps({
+ testID: "cap-settings-sheet",
+ });
+ expect(sheet?.props.saveDisabled).toBe(true);
+ expect(sheet?.props.saveDisabledHint).toBe("Save is in progress");
+ expect(sheet?.props.saveDisabledValue).toBeUndefined();
+ expect(sheet?.props.saveDisabledAccessibilityValue).toBe(
+ "Saving video for Launch review",
+ );
+ expect(sheet?.props.visibilityDisabled).toBe(true);
+ expect(sheet?.props.visibilityDisabledHint).toBe(
+ "Current Cap action is in progress",
+ );
+ expect(sheet?.props.visibilityDisabledValue).toBeUndefined();
+
+ await act(async () => {
+ saveDeferred.resolve("Launch review.mp4");
+ await saveDeferred.promise;
+ });
+
+ expect(getTextNodes(renderer.toJSON())).toContain("Saved");
+ });
+
+ it("keeps save idle while a comment is sending", async () => {
+ const commentDeferred = createDeferred();
+ const auth = createAuth();
+ auth.client.createComment.mockReturnValueOnce(commentDeferred.promise);
+ authState.value = auth;
+ const renderer = await renderComponent(
+ React.createElement(CapDetailScreen),
+ );
+ const [commentInput] = renderer.root.findAllByProps({
+ accessibilityLabel: "Comment",
+ });
+ if (!commentInput) throw new Error("Comment input was not rendered");
+
+ await act(async () => {
+ commentInput.props.onChangeText("Ship it");
+ });
+
+ const [sendButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Send comment",
+ });
+ if (!sendButton) throw new Error("Send button was not rendered");
+
+ await act(async () => {
+ sendButton.props.onPress();
+ await Promise.resolve();
+ });
+
+ const [sendingButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Sending comment on Launch review",
+ });
+ const [saveButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Save video",
+ });
+ if (!sendingButton) throw new Error("Sending button was not rendered");
+ if (!saveButton) throw new Error("Save video button was not rendered");
+
+ expect(sendingButton.props.loading).toBe(true);
+ expect(sendingButton.props.accessibilityHint).toBe("Comment is being sent");
+ expect(saveButton.props.loading).toBe(false);
+ expect(saveButton.props.disabled).toBe(true);
+ expect(saveButton.props.accessibilityHint).toBe(
+ "Current Cap action is in progress",
+ );
+ expect(getTextNodes(renderer.toJSON())).not.toContain("Saving...");
+
+ const [sheet] = renderer.root.findAllByProps({
+ testID: "cap-settings-sheet",
+ });
+ expect(sheet?.props.saveDisabledValue).toBe("Unavailable");
+ expect(sheet?.props.visibilityDisabledValue).toBeUndefined();
+
+ await act(async () => {
+ commentDeferred.resolve(createdComment("Ship it"));
+ await commentDeferred.promise;
+ });
+
+ expect(getTextNodes(renderer.toJSON())).toContain("Ship it");
+ });
+
+ it("marks the settings sheet sharing row as updating during visibility changes", async () => {
+ const sharingDeferred = createDeferred();
+ const auth = createAuth();
+ auth.client.updateCapSharing.mockReturnValueOnce(sharingDeferred.promise);
+ authState.value = auth;
+ const renderer = await renderComponent(
+ React.createElement(CapDetailScreen),
+ );
+ const [sheet] = renderer.root.findAllByProps({
+ testID: "cap-settings-sheet",
+ });
+ if (!sheet) throw new Error("Cap settings sheet was not rendered");
+
+ await act(async () => {
+ sheet.props.onVisibilityChange(detail.cap, false);
+ await Promise.resolve();
+ });
+
+ const [busySheet] = renderer.root.findAllByProps({
+ testID: "cap-settings-sheet",
+ });
+ const [sharingStatus] = renderer.root.findAllByProps({
+ accessibilityLabel: "Change sharing for Launch review",
+ });
+ expect(auth.client.updateCapSharing).toHaveBeenCalledWith("video_123", {
+ public: false,
+ });
+ expect(sharingStatus?.props.accessibilityHint).toBe(
+ "Sharing update is in progress",
+ );
+ expect(sharingStatus?.props.accessibilityState).toEqual({
+ disabled: true,
+ });
+ expect(sharingStatus?.props.accessibilityValue).toEqual({
+ text: "Updating sharing for Launch review",
+ });
+ expect(getTextNodes(renderer.toJSON())).toContain("Shared");
+ expect(getTextNodes(renderer.toJSON())).not.toContain("Updating...");
+ expect(busySheet?.props.visibilityDisabled).toBe(true);
+ expect(busySheet?.props.visibilityDisabledHint).toBe(
+ "Sharing update is in progress",
+ );
+ expect(busySheet?.props.visibilityDisabledValue).toBeUndefined();
+ expect(busySheet?.props.visibilityDisabledAccessibilityValue).toBe(
+ "Updating sharing for Launch review",
+ );
+ expect(busySheet?.props.saveDisabledValue).toBe("Unavailable");
+
+ await act(async () => {
+ sharingDeferred.resolve({ ...detail.cap, public: false });
+ await sharingDeferred.promise;
+ });
+ });
+});
diff --git a/apps/mobile/src/screens/dashboard-upload-visibility.test.tsx b/apps/mobile/src/screens/dashboard-upload-visibility.test.tsx
new file mode 100644
index 00000000000..b0880df9d60
--- /dev/null
+++ b/apps/mobile/src/screens/dashboard-upload-visibility.test.tsx
@@ -0,0 +1,2377 @@
+import React, { type ReactElement, type ReactNode } from "react";
+import TestRenderer, {
+ act,
+ type ReactTestRenderer,
+ type ReactTestRendererJSON,
+} from "react-test-renderer";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import CapsScreen from "../../app/(tabs)";
+import UploadScreen from "../../app/(tabs)/upload";
+
+type AuthStub = {
+ status: "signedIn";
+ bootstrap: {
+ activeOrganizationId: string;
+ user: {
+ email: string;
+ name: string | null;
+ };
+ };
+ client: {
+ createFolder: (input: { color?: string; name: string }) => Promise<{
+ color: string;
+ id: string;
+ name: string;
+ parentId: null;
+ videoCount: number;
+ }>;
+ getCap: (id: string) => Promise<{
+ cap: {
+ upload: null;
+ };
+ }>;
+ listCaps: () => Promise<{
+ caps: unknown[];
+ folders: unknown[];
+ pagination: {
+ hasNextPage: boolean;
+ page: number;
+ totalPages: number;
+ };
+ rootFolders: unknown[];
+ }>;
+ updateCapSharing: (
+ id: string,
+ input: { public: boolean },
+ ) => Promise;
+ };
+ refresh: () => Promise;
+};
+
+type HostProps = {
+ children?: ReactNode;
+ [key: string]: unknown;
+};
+
+type JsonNode = ReactTestRendererJSON | ReactTestRendererJSON[] | string | null;
+
+const createAuth = (): AuthStub => ({
+ status: "signedIn",
+ bootstrap: {
+ activeOrganizationId: "org_123",
+ user: {
+ email: "richie@cap.so",
+ name: "Richie",
+ },
+ },
+ client: {
+ createFolder: vi.fn((input: { color?: string; name: string }) =>
+ Promise.resolve({
+ id: "folder_123",
+ name: input.name,
+ color: input.color ?? "normal",
+ parentId: null,
+ videoCount: 0,
+ }),
+ ),
+ getCap: () =>
+ Promise.resolve({
+ cap: {
+ upload: null,
+ },
+ }),
+ listCaps: () =>
+ Promise.resolve({
+ caps: [],
+ folders: [],
+ pagination: {
+ hasNextPage: false,
+ page: 1,
+ totalPages: 1,
+ },
+ rootFolders: [],
+ }),
+ updateCapSharing: vi.fn((id: string, input: { public: boolean }) =>
+ Promise.resolve({
+ id,
+ public: input.public,
+ }),
+ ),
+ },
+ refresh: () => Promise.resolve(),
+});
+
+const createDeferred = () => {
+ let resolve!: (value: T | PromiseLike) => void;
+ const promise = new Promise((nextResolve) => {
+ resolve = nextResolve;
+ });
+ return { promise, resolve };
+};
+
+const authState = vi.hoisted((): { value: AuthStub | null } => ({
+ value: null,
+}));
+
+const uploadQueueState = vi.hoisted(
+ (): {
+ value: {
+ items: Array<{
+ capId: string | null;
+ contentType: string;
+ createdAt: string;
+ error: string | null;
+ fileName: string;
+ folderId: string | null;
+ id: string;
+ localUri: string;
+ organizationId: string | null;
+ progress: number;
+ processingMessage?: string | null;
+ rawFileKey: string | null;
+ size: number;
+ durationSeconds?: number;
+ status: "complete" | "failed" | "processing" | "queued" | "uploading";
+ updatedAt: string;
+ }>;
+ };
+ } => ({
+ value: {
+ items: [],
+ },
+ }),
+);
+
+const uploadQueueActionsState = vi.hoisted((): { value: unknown[] } => ({
+ value: [],
+}));
+
+(
+ globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
+).IS_REACT_ACT_ENVIRONMENT = true;
+
+const renderTree = async (node: ReactElement): Promise => {
+ let renderer: ReactTestRenderer | null = null;
+ await act(async () => {
+ renderer = TestRenderer.create(node);
+ });
+ return (renderer as ReactTestRenderer | null)?.toJSON() ?? null;
+};
+
+const renderComponent = async (
+ node: ReactElement,
+): Promise => {
+ let renderer: ReactTestRenderer | null = null;
+ await act(async () => {
+ renderer = TestRenderer.create(node);
+ });
+ return renderer as unknown as ReactTestRenderer;
+};
+
+const getTextNodes = (node: JsonNode): string[] => {
+ if (!node) return [];
+ if (typeof node === "string") return [node];
+ if (Array.isArray(node)) return node.flatMap(getTextNodes);
+ return node.children?.flatMap(getTextNodes) ?? [];
+};
+
+const hasProp = (node: JsonNode, prop: string, value: unknown): boolean => {
+ if (!node || typeof node === "string") return false;
+ if (Array.isArray(node))
+ return node.some((item) => hasProp(item, prop, value));
+ if (node.props[prop] === value) return true;
+ return node.children?.some((child) => hasProp(child, prop, value)) ?? false;
+};
+
+const propMatches = (actual: unknown, expected: unknown): boolean => {
+ if (
+ expected &&
+ typeof expected === "object" &&
+ !Array.isArray(expected) &&
+ actual &&
+ typeof actual === "object" &&
+ !Array.isArray(actual)
+ ) {
+ return Object.entries(expected).every(
+ ([key, value]) => (actual as Record)[key] === value,
+ );
+ }
+
+ return actual === expected;
+};
+
+const hasProps = (
+ node: JsonNode,
+ expected: Record,
+): boolean => {
+ if (!node || typeof node === "string") return false;
+ if (Array.isArray(node)) return node.some((item) => hasProps(item, expected));
+ if (
+ Object.entries(expected).every(([key, value]) =>
+ propMatches(node.props[key], value),
+ )
+ ) {
+ return true;
+ }
+ return node.children?.some((child) => hasProps(child, expected)) ?? false;
+};
+
+const hasStyle = (
+ node: JsonNode,
+ expected: Record,
+): boolean => {
+ if (!node || typeof node === "string") return false;
+ if (Array.isArray(node)) return node.some((item) => hasStyle(item, expected));
+ const style =
+ typeof node.props.style === "function"
+ ? node.props.style({ pressed: false })
+ : node.props.style;
+ const resolved = Array.isArray(style)
+ ? Object.assign({}, ...style.filter(Boolean))
+ : style;
+ if (
+ resolved &&
+ Object.entries(expected).every(([key, value]) => resolved[key] === value)
+ ) {
+ return true;
+ }
+ return node.children?.some((child) => hasStyle(child, expected)) ?? false;
+};
+
+const resolveStyle = (
+ style: unknown,
+ pressed = false,
+): Record => {
+ const resolved = typeof style === "function" ? style({ pressed }) : style;
+ const styles = Array.isArray(resolved) ? resolved : [resolved];
+ return Object.assign({}, ...styles.filter(Boolean));
+};
+
+vi.mock("react-native", async () => {
+ const React = await import("react");
+ const createHost =
+ (name: string) =>
+ ({ children, ...props }: HostProps) =>
+ React.createElement(name, props, children);
+
+ return {
+ ActionSheetIOS: {
+ showActionSheetWithOptions: vi.fn(),
+ },
+ ActivityIndicator: createHost("ActivityIndicator"),
+ Alert: {
+ alert: vi.fn(),
+ prompt: vi.fn(),
+ },
+ AppState: {
+ addEventListener: vi.fn(() => ({
+ remove: vi.fn(),
+ })),
+ },
+ Linking: {
+ openSettings: vi.fn(),
+ },
+ Modal: createHost("Modal"),
+ Platform: {
+ OS: "ios",
+ select: (values: { default?: T; ios?: T }) =>
+ values.ios ?? values.default,
+ },
+ Pressable: createHost("Pressable"),
+ RefreshControl: createHost("RefreshControl"),
+ Share: {
+ share: vi.fn(),
+ },
+ StyleSheet: {
+ absoluteFillObject: {
+ bottom: 0,
+ left: 0,
+ position: "absolute",
+ right: 0,
+ top: 0,
+ },
+ create: >(styles: T) => styles,
+ hairlineWidth: 1,
+ },
+ Switch: createHost("Switch"),
+ Text: createHost("Text"),
+ TextInput: createHost("TextInput"),
+ View: createHost("View"),
+ };
+});
+
+vi.mock("@shopify/flash-list", async () => {
+ const React = await import("react");
+ return {
+ FlashList: ({
+ data,
+ ListEmptyComponent,
+ renderItem,
+ }: {
+ data?: unknown[];
+ ListEmptyComponent?: ReactNode;
+ renderItem?: (info: { index: number; item: unknown }) => ReactNode;
+ }) =>
+ React.createElement(
+ "FlashList",
+ null,
+ data && data.length > 0
+ ? data.map((item, index) =>
+ React.createElement(
+ React.Fragment,
+ { key: index },
+ renderItem?.({ item, index }),
+ ),
+ )
+ : ListEmptyComponent,
+ ),
+ };
+});
+
+vi.mock("expo-clipboard", () => ({
+ setStringAsync: vi.fn(),
+}));
+
+vi.mock("expo-router", () => ({
+ router: {
+ push: vi.fn(),
+ },
+}));
+
+vi.mock("expo-web-browser", () => ({
+ openBrowserAsync: vi.fn(),
+}));
+
+vi.mock("@/auth/AuthContext", () => ({
+ apiBaseUrl: "https://cap.so",
+ useAuth: () => authState.value,
+}));
+
+vi.mock("@/auth/SignInPanel", async () => {
+ const React = await import("react");
+ return {
+ SignInPanel: () => React.createElement("SignInPanel"),
+ };
+});
+
+vi.mock("@/components/ActionButton", async () => {
+ const React = await import("react");
+ return {
+ ActionButton: ({
+ children,
+ label,
+ onPress,
+ ...props
+ }: {
+ children?: ReactNode;
+ label: string;
+ onPress?: () => void;
+ [key: string]: unknown;
+ }) =>
+ React.createElement(
+ "ActionButton",
+ { accessibilityLabel: label, onPress, ...props },
+ children ?? label,
+ ),
+ };
+});
+
+vi.mock("@/components/Screen", async () => {
+ const React = await import("react");
+
+ return {
+ Screen: ({
+ children,
+ loading,
+ subtitle,
+ title,
+ }: {
+ children?: ReactNode;
+ loading?: boolean;
+ subtitle?: string | null;
+ title?: string;
+ }) =>
+ React.createElement(
+ "Screen",
+ null,
+ title ? React.createElement("Text", null, title) : null,
+ subtitle ? React.createElement("Text", null, subtitle) : null,
+ loading ? React.createElement("Text", null, "Loading") : children,
+ ),
+ };
+});
+
+vi.mock("@/components/GlassSurface", async () => {
+ const React = await import("react");
+ return {
+ GlassSurface: ({ children }: { children?: ReactNode }) =>
+ React.createElement("GlassSurface", null, children),
+ };
+});
+
+vi.mock("@/components/CapCard", async () => {
+ const React = await import("react");
+ return {
+ CapCard: (props: HostProps) => React.createElement("CapCard", props),
+ };
+});
+
+vi.mock("@/components/OrgSwitcher", async () => {
+ const React = await import("react");
+ return {
+ OrgSwitcher: () => React.createElement("OrgSwitcher"),
+ };
+});
+
+vi.mock("expo-symbols", () => ({
+ SymbolView: () => null,
+}));
+
+vi.mock("react-native-svg", async () => {
+ const React = await import("react");
+ const createHost =
+ (name: string) =>
+ ({ children, ...props }: HostProps) =>
+ React.createElement(name, props, children);
+
+ return {
+ default: createHost("Svg"),
+ Path: createHost("Path"),
+ Rect: createHost("Rect"),
+ };
+});
+
+vi.mock("@/theme", () => ({
+ colors: {
+ appBackground: "#f9f9f9",
+ black: "#000000",
+ blackAlpha40: "rgba(18, 22, 31, 0.4)",
+ blue11: "#0d74ce",
+ blue3: "#edf6ff",
+ blue6: "#acd8fc",
+ blue9: "#0090ff",
+ buttonBlue: "#2563eb",
+ buttonBlueBorder: "#1e40af",
+ glass: "rgba(252, 252, 252, 0.72)",
+ gray1: "#fcfcfc",
+ gray10: "#838383",
+ gray12: "#202020",
+ gray2: "#f9f9f9",
+ gray3: "#f0f0f0",
+ gray4: "#e8e8e8",
+ gray5: "#e0e0e0",
+ gray6: "#d9d9d9",
+ gray9: "#8d8d8d",
+ red1: "#fffcfc",
+ red3: "#feebec",
+ red6: "#fdbdbe",
+ red9: "#e5484d",
+ white: "#ffffff",
+ yellow3: "#fffab8",
+ yellow5: "#ffe770",
+ yellow9: "#f5d90a",
+ },
+ fonts: {
+ bold: "NeueMontreal-Bold",
+ medium: "NeueMontreal-Medium",
+ regular: "NeueMontreal-Regular",
+ },
+ radius: {
+ full: 999,
+ lg: 16,
+ md: 12,
+ sm: 8,
+ xl: 20,
+ xs: 6,
+ },
+ shadows: {
+ card: {},
+ popover: {},
+ },
+ squircle: {
+ borderCurve: "continuous",
+ },
+}));
+
+vi.mock("expo-document-picker", () => ({
+ getDocumentAsync: vi.fn(),
+}));
+
+vi.mock("expo-file-system/legacy", () => ({
+ documentDirectory: "file:///tmp/",
+ downloadAsync: vi.fn(),
+}));
+
+vi.mock("expo-image-picker", () => ({
+ launchImageLibraryAsync: vi.fn(),
+ requestMediaLibraryPermissionsAsync: vi.fn(),
+}));
+
+vi.mock("expo-media-library", () => ({
+ requestPermissionsAsync: vi.fn(),
+ saveToLibraryAsync: vi.fn(),
+}));
+
+vi.mock("@/uploads/runMobileUpload", () => ({
+ runMobileUpload: vi.fn(),
+}));
+
+vi.mock("@/uploads/uploadQueue", async (importOriginal) => {
+ const actual = await importOriginal();
+
+ return {
+ ...actual,
+ emptyUploadQueue: uploadQueueState.value,
+ uploadProgressPercent: (progress: number) => Math.round(progress * 100),
+ uploadQueueReducer: (state: { items: unknown[] }, action: unknown) => {
+ uploadQueueActionsState.value.push(action);
+ return state;
+ },
+ uploadQueueStatusText: (item: {
+ processingMessage?: string | null;
+ progress: number;
+ status: string;
+ }) => {
+ if (item.status === "complete") return "Ready to view";
+ if (item.status === "failed") return "Upload failed";
+ if (item.status === "processing") {
+ return item.processingMessage ?? "Finishing up";
+ }
+ if (item.status === "uploading") {
+ return `Uploading ${Math.round(item.progress * 100)}%`;
+ }
+ return "Queued";
+ },
+ };
+});
+
+describe("upload and dashboard visibility", () => {
+ beforeEach(() => {
+ authState.value = createAuth();
+ uploadQueueState.value.items = [];
+ uploadQueueActionsState.value = [];
+ });
+
+ it("shows native upload entry points", async () => {
+ const tree = await renderTree(React.createElement(UploadScreen));
+
+ expect(getTextNodes(tree)).toContain("Import");
+ expect(getTextNodes(tree)).toContain("Upload File");
+ expect(getTextNodes(tree)).toContain("Browse Files");
+ expect(getTextNodes(tree)).toContain("Photos");
+ expect(getTextNodes(tree)).toContain("Import from Loom");
+ expect(getTextNodes(tree)).toContain("MP4, MOV, AVI, MKV, WebM, or M4V");
+ expect(hasStyle(tree, { height: 128, backgroundColor: "#f0f0f0" })).toBe(
+ true,
+ );
+ expect(
+ hasProps(tree, {
+ accessibilityHint: "Opens upload source options",
+ accessibilityLabel: "Choose upload source",
+ accessibilityState: { busy: false, disabled: false },
+ accessibilityValue: {
+ text: "MP4, MOV, AVI, MKV, WebM, or M4V",
+ },
+ }),
+ ).toBe(true);
+ expect(
+ hasProps(tree, {
+ accessibilityHint: "Opens Loom import in a browser sheet",
+ accessibilityLabel: "Open Loom import",
+ }),
+ ).toBe(true);
+ expect(
+ hasProps(tree, {
+ accessibilityHint: "Opens the native file picker",
+ accessibilityLabel: "Browse Files",
+ }),
+ ).toBe(true);
+ expect(
+ hasProps(tree, {
+ accessibilityHint: "Opens your photo library",
+ accessibilityLabel: "Photos",
+ }),
+ ).toBe(true);
+ });
+
+ it("opens the native iOS upload source sheet", async () => {
+ const renderer = await renderComponent(React.createElement(UploadScreen));
+ const [uploadSource] = renderer.root.findAllByProps({
+ accessibilityLabel: "Choose upload source",
+ });
+ if (!uploadSource) throw new Error("Upload source button was not rendered");
+
+ const { ActionSheetIOS } = await import("react-native");
+ const showActionSheetWithOptions = vi.mocked(
+ ActionSheetIOS.showActionSheetWithOptions,
+ );
+ showActionSheetWithOptions.mockClear();
+
+ await act(async () => {
+ uploadSource.props.onPress();
+ });
+
+ expect(showActionSheetWithOptions).toHaveBeenCalledWith(
+ expect.objectContaining({
+ cancelButtonIndex: 3,
+ options: ["Browse Files", "Photos", "Import from Loom", "Cancel"],
+ tintColor: "#0d74ce",
+ title: "Upload File",
+ userInterfaceStyle: "light",
+ }),
+ expect.any(Function),
+ );
+
+ const [, callback] = showActionSheetWithOptions.mock.calls[0] ?? [];
+ if (!callback) throw new Error("Upload source callback was not set");
+ const WebBrowser = await import("expo-web-browser");
+ const openBrowserAsync = vi.mocked(WebBrowser.openBrowserAsync);
+ openBrowserAsync.mockClear();
+
+ await act(async () => {
+ callback(2);
+ });
+
+ expect(openBrowserAsync).toHaveBeenCalledWith(
+ "https://cap.so/dashboard/import/loom",
+ );
+ });
+
+ it("opens Loom import in the native browser sheet", async () => {
+ const renderer = await renderComponent(React.createElement(UploadScreen));
+ const [loomAction] = renderer.root.findAllByProps({
+ accessibilityLabel: "Import from Loom",
+ });
+ const [loomImport] = renderer.root.findAllByProps({
+ accessibilityLabel: "Open Loom import",
+ });
+ if (!loomAction) throw new Error("Loom upload action was not rendered");
+ if (!loomImport) throw new Error("Loom import card was not rendered");
+
+ const WebBrowser = await import("expo-web-browser");
+ const openBrowserAsync = vi.mocked(WebBrowser.openBrowserAsync);
+ openBrowserAsync.mockClear();
+
+ expect(loomAction.props.accessibilityHint).toBe(
+ "Opens Loom import in a browser sheet",
+ );
+
+ await act(async () => {
+ loomAction.props.onPress();
+ });
+
+ expect(openBrowserAsync).toHaveBeenCalledWith(
+ "https://cap.so/dashboard/import/loom",
+ );
+ openBrowserAsync.mockClear();
+
+ await act(async () => {
+ loomImport.props.onPress();
+ });
+
+ expect(openBrowserAsync).toHaveBeenCalledWith(
+ "https://cap.so/dashboard/import/loom",
+ );
+ });
+
+ it("shows Loom import failures on the Loom card", async () => {
+ const WebBrowser = await import("expo-web-browser");
+ const openBrowserAsync = vi.mocked(WebBrowser.openBrowserAsync);
+ openBrowserAsync.mockClear();
+ openBrowserAsync.mockRejectedValueOnce(new Error("Loom unavailable"));
+
+ const renderer = await renderComponent(React.createElement(UploadScreen));
+ const [loomImport] = renderer.root.findAllByProps({
+ accessibilityLabel: "Open Loom import",
+ });
+ if (!loomImport) throw new Error("Loom import card was not rendered");
+
+ await act(async () => {
+ loomImport.props.onPress();
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+
+ expect(getTextNodes(renderer.toJSON())).toContain(
+ "Loom import unavailable",
+ );
+ expect(getTextNodes(renderer.toJSON())).toContain("Loom unavailable");
+ const [failedLoomImport] = renderer.root.findAllByProps({
+ accessibilityLabel: "Loom import unavailable",
+ });
+ if (!failedLoomImport) throw new Error("Loom error card was not rendered");
+ expect(failedLoomImport.props.accessibilityHint).toBe(
+ "Retries Loom import",
+ );
+ expect(failedLoomImport.props.accessibilityValue).toEqual({
+ text: "Loom unavailable",
+ });
+ expect(failedLoomImport.props.accessibilityState).toEqual({
+ busy: false,
+ disabled: false,
+ });
+ const [retryLoomAction] = renderer.root.findAllByProps({
+ accessibilityLabel: "Retry Loom",
+ });
+ if (!retryLoomAction) throw new Error("Retry Loom action was not rendered");
+ expect(retryLoomAction.props.accessibilityHint).toBe("Loom unavailable");
+ expect(retryLoomAction.props.accessibilityValue).toEqual({
+ text: "Loom unavailable",
+ });
+ expect(retryLoomAction.props.disabled).toBe(false);
+ const [uploadSource] = renderer.root.findAllByProps({
+ accessibilityLabel: "Choose upload source",
+ });
+ if (!uploadSource) throw new Error("Upload source card was not rendered");
+ expect(uploadSource.props.accessibilityValue).toEqual({
+ text: "MP4, MOV, AVI, MKV, WebM, or M4V",
+ });
+ expect(hasStyle(renderer.toJSON(), { color: "#e5484d" })).toBe(true);
+ expect(
+ hasProps(renderer.toJSON(), {
+ accessibilityLiveRegion: "polite",
+ accessibilityRole: "alert",
+ }),
+ ).toBe(true);
+ });
+
+ it("locks stale Loom import actions while the browser sheet is opening", async () => {
+ const WebBrowser = await import("expo-web-browser");
+ const openBrowserAsync = vi.mocked(WebBrowser.openBrowserAsync);
+ let resolveBrowser:
+ | ((
+ value: Awaited>,
+ ) => void)
+ | null = null;
+ openBrowserAsync.mockClear();
+ openBrowserAsync.mockImplementationOnce(
+ () =>
+ new Promise((resolve) => {
+ resolveBrowser = resolve;
+ }),
+ );
+
+ const renderer = await renderComponent(React.createElement(UploadScreen));
+ const [loomAction] = renderer.root.findAllByProps({
+ accessibilityLabel: "Import from Loom",
+ });
+ const [loomImport] = renderer.root.findAllByProps({
+ accessibilityLabel: "Open Loom import",
+ });
+ if (!loomAction) throw new Error("Loom upload action was not rendered");
+ if (!loomImport) throw new Error("Loom import card was not rendered");
+
+ await act(async () => {
+ void loomAction.props.onPress();
+ await Promise.resolve();
+ });
+
+ const [uploadSource] = renderer.root.findAllByProps({
+ accessibilityLabel: "Choose upload source",
+ });
+ const [loadingLoomImport] = renderer.root.findAllByProps({
+ accessibilityLabel: "Opening Loom",
+ });
+ const [browseButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Browse Files",
+ });
+ const [loadingLoomAction] = renderer.root.findAllByProps({
+ accessibilityLabel: "Import from Loom",
+ });
+ if (!uploadSource) throw new Error("Upload source button was not rendered");
+ if (!browseButton) throw new Error("Browse Files button was not rendered");
+ if (!loadingLoomAction)
+ throw new Error("Loom upload action was not rendered");
+ if (!loadingLoomImport)
+ throw new Error("Loom import card was not rendered");
+
+ const loadingText = getTextNodes(renderer.toJSON());
+ expect(loadingText.filter((item) => item === "Opening Loom")).toHaveLength(
+ 1,
+ );
+ expect(getTextNodes(renderer.toJSON())).not.toContain("Opening Loom...");
+ expect(
+ loadingText.filter(
+ (item) => item === "Continue in the browser sheet to import from Loom.",
+ ),
+ ).toHaveLength(1);
+ expect(uploadSource.props.accessibilityHint).toBe("Loom import is opening");
+ expect(uploadSource.props.accessibilityValue).toEqual({
+ text: "Opening Loom import",
+ });
+ expect(uploadSource.props.accessibilityState).toEqual({
+ busy: true,
+ disabled: true,
+ });
+ expect(browseButton.props.disabled).toBe(true);
+ expect(browseButton.props.accessibilityHint).toBe("Loom import is opening");
+ expect(browseButton.props.accessibilityValue).toEqual({
+ text: "Opening Loom import",
+ });
+ expect(loadingLoomAction.props.accessibilityHint).toBe(
+ "Loom import is opening",
+ );
+ expect(loadingLoomAction.props.accessibilityValue).toEqual({
+ text: "Opening Loom import",
+ });
+ expect(loadingLoomAction.props.loading).toBe(true);
+ expect(loadingLoomAction.props.disabled).toBe(false);
+ expect(loadingLoomImport.props.accessibilityHint).toBe(
+ "Loom import is opening",
+ );
+ expect(loadingLoomImport.props.accessibilityValue).toEqual({
+ text: "Opening Loom import",
+ });
+ expect(loadingLoomImport.props.disabled).toBe(true);
+ expect(openBrowserAsync).toHaveBeenCalledTimes(1);
+
+ openBrowserAsync.mockClear();
+
+ await act(async () => {
+ loomAction.props.onPress();
+ loomImport.props.onPress();
+ uploadSource.props.onPress();
+ await Promise.resolve();
+ });
+
+ expect(openBrowserAsync).not.toHaveBeenCalled();
+
+ await act(async () => {
+ resolveBrowser?.({
+ type: "dismiss",
+ } as Awaited>);
+ await Promise.resolve();
+ });
+ });
+
+ it("locks upload sources while the file picker is opening", async () => {
+ const DocumentPicker = await import("expo-document-picker");
+ let resolvePicker:
+ | ((
+ value: Awaited>,
+ ) => void)
+ | null = null;
+ vi.mocked(DocumentPicker.getDocumentAsync).mockImplementationOnce(
+ () =>
+ new Promise((resolve) => {
+ resolvePicker = resolve;
+ }),
+ );
+ const renderer = await renderComponent(React.createElement(UploadScreen));
+ const [browseButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Browse Files",
+ });
+ if (!browseButton) throw new Error("Browse Files button was not rendered");
+
+ await act(async () => {
+ void browseButton.props.onPress();
+ await Promise.resolve();
+ });
+
+ const [uploadSource] = renderer.root.findAllByProps({
+ accessibilityLabel: "Opening Files",
+ });
+ const [loadingBrowseButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Browse Files",
+ });
+ const [photosButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Photos",
+ });
+ const [loomAction] = renderer.root.findAllByProps({
+ accessibilityLabel: "Import from Loom",
+ });
+ const [loomImport] = renderer.root.findAllByProps({
+ accessibilityLabel: "Open Loom import",
+ });
+ if (
+ !uploadSource ||
+ !loadingBrowseButton ||
+ !photosButton ||
+ !loomAction ||
+ !loomImport
+ ) {
+ throw new Error("Upload source controls were not rendered");
+ }
+
+ expect(getTextNodes(renderer.toJSON())).toContain("Opening Files");
+ expect(getTextNodes(renderer.toJSON())).not.toContain("Opening Files...");
+ expect(getTextNodes(renderer.toJSON())).toContain(
+ "Choose a video from Files.",
+ );
+ expect(uploadSource.props.accessibilityHint).toBe(
+ "Upload source picker is opening",
+ );
+ expect(uploadSource.props.accessibilityValue).toEqual({
+ text: "Opening native file picker",
+ });
+ expect(uploadSource.props.accessibilityState).toEqual({
+ busy: true,
+ disabled: true,
+ });
+ expect(uploadSource.props.disabled).toBe(true);
+ expect(resolveStyle(uploadSource.props.style)).toMatchObject({
+ opacity: 0.58,
+ });
+ expect(loadingBrowseButton.props.loading).toBe(true);
+ expect(loadingBrowseButton.props.accessibilityHint).toBe(
+ "Upload source picker is opening",
+ );
+ expect(loadingBrowseButton.props.accessibilityValue).toEqual({
+ text: "Opening native file picker",
+ });
+ expect(photosButton.props.accessibilityHint).toBe(
+ "Another upload source is opening",
+ );
+ expect(photosButton.props.accessibilityValue).toEqual({
+ text: "Opening native file picker",
+ });
+ expect(photosButton.props.disabled).toBe(true);
+ expect(loomAction.props.accessibilityHint).toBe(
+ "Another upload source is opening",
+ );
+ expect(loomAction.props.accessibilityValue).toEqual({
+ text: "Opening native file picker",
+ });
+ expect(loomAction.props.disabled).toBe(true);
+ expect(loomImport.props.accessibilityHint).toBe(
+ "Upload source picker is opening",
+ );
+ expect(loomImport.props.accessibilityValue).toEqual({
+ text: "Opening native file picker",
+ });
+ expect(loomImport.props.disabled).toBe(true);
+
+ const { ActionSheetIOS } = await import("react-native");
+ const showActionSheetWithOptions = vi.mocked(
+ ActionSheetIOS.showActionSheetWithOptions,
+ );
+ const ImagePicker = await import("expo-image-picker");
+ const requestMediaLibraryPermissionsAsync = vi.mocked(
+ ImagePicker.requestMediaLibraryPermissionsAsync,
+ );
+ const WebBrowser = await import("expo-web-browser");
+ const openBrowserAsync = vi.mocked(WebBrowser.openBrowserAsync);
+ showActionSheetWithOptions.mockClear();
+ requestMediaLibraryPermissionsAsync.mockClear();
+ openBrowserAsync.mockClear();
+
+ await act(async () => {
+ uploadSource.props.onPress();
+ photosButton.props.onPress();
+ loomAction.props.onPress();
+ loomImport.props.onPress();
+ });
+
+ expect(showActionSheetWithOptions).not.toHaveBeenCalled();
+ expect(requestMediaLibraryPermissionsAsync).not.toHaveBeenCalled();
+ expect(openBrowserAsync).not.toHaveBeenCalled();
+ expect(DocumentPicker.getDocumentAsync).toHaveBeenCalledTimes(1);
+
+ await act(async () => {
+ resolvePicker?.({
+ assets: null,
+ canceled: true,
+ } as Awaited>);
+ await Promise.resolve();
+ });
+ });
+
+ it("shows the active Photos source as loading while the photo picker is opening", async () => {
+ const ImagePicker = await import("expo-image-picker");
+ const requestMediaLibraryPermissionsAsync = vi.mocked(
+ ImagePicker.requestMediaLibraryPermissionsAsync,
+ );
+ let resolvePermission:
+ | ((
+ value: Awaited<
+ ReturnType
+ >,
+ ) => void)
+ | null = null;
+ requestMediaLibraryPermissionsAsync.mockImplementationOnce(
+ () =>
+ new Promise((resolve) => {
+ resolvePermission = resolve;
+ }),
+ );
+ const { ActionSheetIOS } = await import("react-native");
+ const showActionSheetWithOptions = vi.mocked(
+ ActionSheetIOS.showActionSheetWithOptions,
+ );
+ showActionSheetWithOptions.mockClear();
+ const renderer = await renderComponent(React.createElement(UploadScreen));
+ const [photosButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Photos",
+ });
+ if (!photosButton) throw new Error("Photos button was not rendered");
+
+ await act(async () => {
+ void photosButton.props.onPress();
+ await Promise.resolve();
+ });
+
+ const [uploadSource] = renderer.root.findAllByProps({
+ accessibilityLabel: "Opening Photos",
+ });
+ const [browseButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Browse Files",
+ });
+ const [loadingPhotosButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Photos",
+ });
+ const [loomAction] = renderer.root.findAllByProps({
+ accessibilityLabel: "Import from Loom",
+ });
+ const [loomImport] = renderer.root.findAllByProps({
+ accessibilityLabel: "Open Loom import",
+ });
+ if (
+ !uploadSource ||
+ !browseButton ||
+ !loadingPhotosButton ||
+ !loomAction ||
+ !loomImport
+ ) {
+ throw new Error("Upload source controls were not rendered");
+ }
+
+ expect(getTextNodes(renderer.toJSON())).toContain("Opening Photos");
+ expect(getTextNodes(renderer.toJSON())).not.toContain("Opening Photos...");
+ expect(getTextNodes(renderer.toJSON())).toContain(
+ "Choose a video from Photos.",
+ );
+ expect(uploadSource.props.accessibilityHint).toBe(
+ "Upload source picker is opening",
+ );
+ expect(uploadSource.props.accessibilityValue).toEqual({
+ text: "Opening native photo picker",
+ });
+ expect(uploadSource.props.accessibilityState).toEqual({
+ busy: true,
+ disabled: true,
+ });
+ expect(uploadSource.props.disabled).toBe(true);
+ expect(resolveStyle(uploadSource.props.style)).toMatchObject({
+ opacity: 0.58,
+ });
+ expect(browseButton.props.accessibilityHint).toBe(
+ "Another upload source is opening",
+ );
+ expect(browseButton.props.accessibilityValue).toEqual({
+ text: "Opening native photo picker",
+ });
+ expect(browseButton.props.disabled).toBe(true);
+ expect(loadingPhotosButton.props.accessibilityHint).toBe(
+ "Upload source picker is opening",
+ );
+ expect(loadingPhotosButton.props.accessibilityValue).toEqual({
+ text: "Opening native photo picker",
+ });
+ expect(loadingPhotosButton.props.loading).toBe(true);
+ expect(loadingPhotosButton.props.disabled).toBe(false);
+ expect(loomAction.props.accessibilityHint).toBe(
+ "Another upload source is opening",
+ );
+ expect(loomAction.props.accessibilityValue).toEqual({
+ text: "Opening native photo picker",
+ });
+ expect(loomAction.props.disabled).toBe(true);
+ expect(loomImport.props.accessibilityHint).toBe(
+ "Upload source picker is opening",
+ );
+ expect(loomImport.props.accessibilityValue).toEqual({
+ text: "Opening native photo picker",
+ });
+ expect(loomImport.props.disabled).toBe(true);
+
+ await act(async () => {
+ resolvePermission?.({
+ granted: false,
+ } as Awaited<
+ ReturnType
+ >);
+ await Promise.resolve();
+ });
+
+ expect(showActionSheetWithOptions).toHaveBeenCalledWith(
+ expect.objectContaining({
+ cancelButtonIndex: 1,
+ message: "Allow Cap to read videos from Photos before uploading.",
+ options: ["Open Settings", "Cancel"],
+ title: "Photos access needed",
+ }),
+ expect.any(Function),
+ );
+ expect(getTextNodes(renderer.toJSON())).toContain(
+ "Upload source unavailable",
+ );
+ expect(getTextNodes(renderer.toJSON())).toContain(
+ "Allow Cap to read videos from Photos before uploading.",
+ );
+ const [failedUploadSource] = renderer.root.findAllByProps({
+ accessibilityLabel: "Upload source unavailable",
+ });
+ const [retryPhotosButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Retry Photos",
+ });
+ if (!failedUploadSource)
+ throw new Error("Upload source error state was not rendered");
+ if (!retryPhotosButton)
+ throw new Error("Retry Photos button was not rendered");
+ expect(failedUploadSource.props.accessibilityValue).toEqual({
+ text: "Allow Cap to read videos from Photos before uploading.",
+ });
+ expect(retryPhotosButton.props.accessibilityHint).toBe(
+ "Allow Cap to read videos from Photos before uploading.",
+ );
+ expect(retryPhotosButton.props.accessibilityValue).toEqual({
+ text: "Allow Cap to read videos from Photos before uploading.",
+ });
+ expect(retryPhotosButton.props.disabled).toBe(false);
+ });
+
+ it("deduplicates stale upload source actions while the file picker is opening", async () => {
+ const DocumentPicker = await import("expo-document-picker");
+ const getDocumentAsync = vi.mocked(DocumentPicker.getDocumentAsync);
+ let resolvePicker:
+ | ((
+ value: Awaited>,
+ ) => void)
+ | null = null;
+ getDocumentAsync.mockClear();
+ getDocumentAsync.mockImplementationOnce(
+ () =>
+ new Promise((resolve) => {
+ resolvePicker = resolve;
+ }),
+ );
+ const renderer = await renderComponent(React.createElement(UploadScreen));
+ const [uploadSource] = renderer.root.findAllByProps({
+ accessibilityLabel: "Choose upload source",
+ });
+ const [browseButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Browse Files",
+ });
+ const [photosButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Photos",
+ });
+ const [loomAction] = renderer.root.findAllByProps({
+ accessibilityLabel: "Import from Loom",
+ });
+ const [loomImport] = renderer.root.findAllByProps({
+ accessibilityLabel: "Open Loom import",
+ });
+ if (
+ !uploadSource ||
+ !browseButton ||
+ !photosButton ||
+ !loomAction ||
+ !loomImport
+ ) {
+ throw new Error("Upload source controls were not rendered");
+ }
+
+ await act(async () => {
+ void browseButton.props.onPress();
+ await Promise.resolve();
+ });
+
+ const { ActionSheetIOS } = await import("react-native");
+ const showActionSheetWithOptions = vi.mocked(
+ ActionSheetIOS.showActionSheetWithOptions,
+ );
+ const ImagePicker = await import("expo-image-picker");
+ const requestMediaLibraryPermissionsAsync = vi.mocked(
+ ImagePicker.requestMediaLibraryPermissionsAsync,
+ );
+ const WebBrowser = await import("expo-web-browser");
+ const openBrowserAsync = vi.mocked(WebBrowser.openBrowserAsync);
+ showActionSheetWithOptions.mockClear();
+ requestMediaLibraryPermissionsAsync.mockClear();
+ openBrowserAsync.mockClear();
+
+ await act(async () => {
+ uploadSource.props.onPress();
+ browseButton.props.onPress();
+ photosButton.props.onPress();
+ loomAction.props.onPress();
+ loomImport.props.onPress();
+ await Promise.resolve();
+ });
+
+ expect(getDocumentAsync).toHaveBeenCalledTimes(1);
+ expect(showActionSheetWithOptions).not.toHaveBeenCalled();
+ expect(requestMediaLibraryPermissionsAsync).not.toHaveBeenCalled();
+ expect(openBrowserAsync).not.toHaveBeenCalled();
+
+ await act(async () => {
+ resolvePicker?.({
+ assets: null,
+ canceled: true,
+ } as Awaited>);
+ await Promise.resolve();
+ });
+ });
+
+ it("locks Loom import while a device upload is active", async () => {
+ const DocumentPicker = await import("expo-document-picker");
+ const { runMobileUpload } = await import("@/uploads/runMobileUpload");
+ const uploadStartedAt = 1_763_440_800_000;
+ const dateNow = vi.spyOn(Date, "now").mockReturnValue(uploadStartedAt);
+ let resolveUpload:
+ | ((value: Awaited>) => void)
+ | null = null;
+ uploadQueueState.value.items = [
+ {
+ capId: null,
+ contentType: "video/mp4",
+ createdAt: "2026-05-18T10:00:00.000Z",
+ error: null,
+ fileName: "launch-review.mp4",
+ folderId: null,
+ id: `${uploadStartedAt}-launch-review.mp4`,
+ localUri: "file:///tmp/launch-review.mp4",
+ organizationId: "org_123",
+ progress: 0,
+ processingMessage: null,
+ rawFileKey: null,
+ size: 12_400_000,
+ status: "queued",
+ updatedAt: "2026-05-18T10:00:00.000Z",
+ },
+ ];
+ vi.mocked(DocumentPicker.getDocumentAsync).mockResolvedValueOnce({
+ assets: [
+ {
+ mimeType: "video/mp4",
+ name: "launch-review.mp4",
+ size: 12_400_000,
+ uri: "file:///tmp/launch-review.mp4",
+ },
+ ],
+ canceled: false,
+ } as Awaited>);
+ vi.mocked(runMobileUpload).mockImplementationOnce(
+ () =>
+ new Promise((resolve) => {
+ resolveUpload = resolve;
+ }),
+ );
+ const renderer = await renderComponent(React.createElement(UploadScreen));
+ const [browseButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Browse Files",
+ });
+ if (!browseButton) throw new Error("Browse Files button was not rendered");
+
+ await act(async () => {
+ void browseButton.props.onPress();
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+
+ const [uploadSource] = renderer.root.findAllByProps({
+ accessibilityLabel: "Choose upload source",
+ });
+ const [loomImport] = renderer.root.findAllByProps({
+ accessibilityLabel: "Open Loom import",
+ });
+ const [activeBrowseButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Browse Files",
+ });
+ const [photosButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Photos",
+ });
+ const [loomAction] = renderer.root.findAllByProps({
+ accessibilityLabel: "Import from Loom",
+ });
+ if (!uploadSource) throw new Error("Upload source button was not rendered");
+ if (!loomImport) throw new Error("Loom import card was not rendered");
+ if (!activeBrowseButton)
+ throw new Error("Browse Files button was not rendered");
+ if (!photosButton) throw new Error("Photos button was not rendered");
+ if (!loomAction) throw new Error("Loom upload action was not rendered");
+ const WebBrowser = await import("expo-web-browser");
+ const openBrowserAsync = vi.mocked(WebBrowser.openBrowserAsync);
+ openBrowserAsync.mockClear();
+
+ expect(getTextNodes(renderer.toJSON())).toContain("Upload File");
+ expect(getTextNodes(renderer.toJSON())).not.toContain("Preparing upload");
+ expect(getTextNodes(renderer.toJSON()).join("")).toContain(
+ "Preparing upload ยท 12 MB",
+ );
+ expect(getTextNodes(renderer.toJSON())).toContain("Import from Loom");
+ expect(getTextNodes(renderer.toJSON())).toContain(
+ "Finish preparing this upload before importing from Loom.",
+ );
+ expect(uploadSource.props.accessibilityHint).toBe("Preparing upload");
+ expect(uploadSource.props.accessibilityValue).toEqual({
+ text: "Preparing upload launch-review.mp4",
+ });
+ expect(uploadSource.props.disabled).toBe(true);
+ expect(resolveStyle(uploadSource.props.style)).toMatchObject({
+ opacity: 0.58,
+ });
+ expect(activeBrowseButton.props.loading).toBe(false);
+ expect(activeBrowseButton.props.accessibilityHint).toBe("Preparing upload");
+ expect(activeBrowseButton.props.accessibilityValue).toEqual({
+ text: "Preparing upload launch-review.mp4",
+ });
+ expect(activeBrowseButton.props.disabled).toBe(true);
+ expect(photosButton.props.loading).toBe(false);
+ expect(photosButton.props.accessibilityHint).toBe("Preparing upload");
+ expect(photosButton.props.accessibilityValue).toEqual({
+ text: "Preparing upload launch-review.mp4",
+ });
+ expect(photosButton.props.disabled).toBe(true);
+ expect(loomAction.props.loading).toBe(false);
+ expect(loomAction.props.accessibilityHint).toBe("Preparing upload");
+ expect(loomAction.props.accessibilityValue).toEqual({
+ text: "Preparing upload launch-review.mp4",
+ });
+ expect(loomAction.props.disabled).toBe(true);
+ expect(loomImport.props.accessibilityHint).toBe("Preparing upload");
+ expect(loomImport.props.accessibilityValue).toEqual({
+ text: "Preparing upload launch-review.mp4",
+ });
+ expect(loomImport.props.accessibilityState).toEqual({
+ busy: true,
+ disabled: true,
+ });
+ expect(loomImport.props.disabled).toBe(true);
+
+ await act(async () => {
+ loomImport.props.onPress();
+ });
+
+ expect(openBrowserAsync).not.toHaveBeenCalled();
+
+ await act(async () => {
+ resolveUpload?.({
+ id: "video_123",
+ } as Awaited>);
+ await Promise.resolve();
+ });
+ dateNow.mockRestore();
+ });
+
+ it("locks inactive upload queue rows while a device upload is active", async () => {
+ uploadQueueState.value.items = [
+ {
+ capId: null,
+ contentType: "video/mp4",
+ createdAt: "2026-05-18T10:00:00.000Z",
+ error: "Network unavailable",
+ fileName: "failed-upload.mp4",
+ folderId: null,
+ id: "failed-upload",
+ localUri: "file:///tmp/failed-upload.mp4",
+ organizationId: "org_123",
+ progress: 0.42,
+ rawFileKey: null,
+ size: 124_000,
+ durationSeconds: 125,
+ status: "failed",
+ updatedAt: "2026-05-18T10:00:00.000Z",
+ },
+ ];
+ const DocumentPicker = await import("expo-document-picker");
+ const { runMobileUpload } = await import("@/uploads/runMobileUpload");
+ let resolveUpload:
+ | ((value: Awaited>) => void)
+ | null = null;
+ vi.mocked(DocumentPicker.getDocumentAsync).mockResolvedValueOnce({
+ assets: [
+ {
+ mimeType: "video/mp4",
+ name: "launch-review.mp4",
+ size: 12_400_000,
+ uri: "file:///tmp/launch-review.mp4",
+ },
+ ],
+ canceled: false,
+ } as Awaited>);
+ vi.mocked(runMobileUpload).mockImplementationOnce(
+ () =>
+ new Promise((resolve) => {
+ resolveUpload = resolve;
+ }),
+ );
+ const renderer = await renderComponent(React.createElement(UploadScreen));
+ const [browseButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Browse Files",
+ });
+ if (!browseButton) throw new Error("Browse Files button was not rendered");
+
+ await act(async () => {
+ void browseButton.props.onPress();
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+
+ const [queueRow] = renderer.root.findAllByProps({
+ accessibilityLabel: "Upload failed-upload.mp4",
+ });
+ const [retryButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Retry upload failed-upload.mp4",
+ });
+ const queueMenus = renderer.root.findAllByProps({
+ accessibilityLabel: "More actions for failed-upload.mp4",
+ });
+ const [queueMenu] = queueMenus;
+ if (!queueRow) throw new Error("Upload queue row was not rendered");
+ if (!retryButton) throw new Error("Retry button was not rendered");
+ if (!queueMenu) throw new Error("Upload queue menu was not rendered");
+
+ expect(queueRow.props.accessibilityHint).toBe(
+ "Another upload is in progress",
+ );
+ expect(queueRow.props.accessibilityState).toEqual({
+ busy: false,
+ disabled: true,
+ });
+ expect(queueRow.props.accessibilityValue).toEqual({
+ text: "Preparing upload launch-review.mp4",
+ });
+ expect(queueRow.props.disabled).toBe(true);
+ expect(retryButton.props.disabled).toBe(true);
+ expect(retryButton.props.accessibilityHint).toBe(
+ "Another upload is in progress",
+ );
+ expect(retryButton.props.accessibilityValue).toEqual({
+ text: "Preparing upload launch-review.mp4",
+ });
+ expect(queueMenu.props.accessibilityHint).toBe(
+ "Another upload is in progress",
+ );
+ expect(queueMenu.props.accessibilityState).toEqual({
+ busy: false,
+ disabled: true,
+ });
+ expect(queueMenu.props.accessibilityValue).toEqual({
+ text: "Preparing upload launch-review.mp4",
+ });
+ expect(queueMenu.props.disabled).toBe(true);
+
+ const { ActionSheetIOS } = await import("react-native");
+ const showActionSheetWithOptions = vi.mocked(
+ ActionSheetIOS.showActionSheetWithOptions,
+ );
+ showActionSheetWithOptions.mockClear();
+
+ await act(async () => {
+ queueRow.props.onPress();
+ retryButton.props.onPress({ stopPropagation: vi.fn() });
+ queueMenu.props.onPress({ stopPropagation: vi.fn() });
+ });
+
+ expect(showActionSheetWithOptions).not.toHaveBeenCalled();
+ expect(uploadQueueActionsState.value).not.toContainEqual(
+ expect.objectContaining({
+ id: "failed-upload",
+ type: "retry",
+ }),
+ );
+
+ await act(async () => {
+ resolveUpload?.({
+ id: "video_123",
+ } as Awaited>);
+ await Promise.resolve();
+ });
+ });
+
+ it("locks stale upload queue view actions while a device upload is active", async () => {
+ uploadQueueState.value.items = [
+ {
+ capId: "video_complete",
+ contentType: "video/mp4",
+ createdAt: "2026-05-18T10:00:00.000Z",
+ error: null,
+ fileName: "processed-upload.mp4",
+ folderId: null,
+ id: "processed-upload",
+ localUri: "file:///tmp/processed-upload.mp4",
+ organizationId: "org_123",
+ progress: 1,
+ rawFileKey: "raw-file-key",
+ size: 124_000,
+ durationSeconds: 125,
+ status: "complete",
+ updatedAt: "2026-05-18T10:00:00.000Z",
+ },
+ ];
+ const renderer = await renderComponent(React.createElement(UploadScreen));
+ const [queueRow] = renderer.root.findAllByProps({
+ accessibilityLabel: "Upload processed-upload.mp4",
+ });
+ const [queueMenu] = renderer.root.findAllByProps({
+ accessibilityLabel: "More actions for processed-upload.mp4",
+ });
+ const [viewButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "View upload processed-upload.mp4",
+ });
+ if (!queueRow) throw new Error("Upload queue row was not rendered");
+ if (!queueMenu) throw new Error("Upload queue menu was not rendered");
+ if (!viewButton) throw new Error("View button was not rendered");
+ expect(queueRow.props.accessibilityHint).toBe(
+ "Ready to view. Opens upload actions",
+ );
+ expect(queueMenu.props.accessibilityHint).toBe(
+ "Opens view and remove actions",
+ );
+ expect(queueRow.props.accessibilityValue).toEqual({
+ text: "Ready to view ยท 124 KB ยท 2 mins",
+ });
+
+ const { ActionSheetIOS } = await import("react-native");
+ const showActionSheetWithOptions = vi.mocked(
+ ActionSheetIOS.showActionSheetWithOptions,
+ );
+ showActionSheetWithOptions.mockClear();
+
+ await act(async () => {
+ queueRow.props.onPress();
+ });
+
+ const [, callback] = showActionSheetWithOptions.mock.calls[0] ?? [];
+ if (!callback) throw new Error("Upload queue action callback was not set");
+
+ const DocumentPicker = await import("expo-document-picker");
+ const { runMobileUpload } = await import("@/uploads/runMobileUpload");
+ let resolveUpload:
+ | ((value: Awaited>) => void)
+ | null = null;
+ vi.mocked(DocumentPicker.getDocumentAsync).mockResolvedValueOnce({
+ assets: [
+ {
+ mimeType: "video/mp4",
+ name: "launch-review.mp4",
+ size: 12_400_000,
+ uri: "file:///tmp/launch-review.mp4",
+ },
+ ],
+ canceled: false,
+ } as Awaited>);
+ vi.mocked(runMobileUpload).mockImplementationOnce(
+ () =>
+ new Promise((resolve) => {
+ resolveUpload = resolve;
+ }),
+ );
+ const [browseButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Browse Files",
+ });
+ if (!browseButton) throw new Error("Browse Files button was not rendered");
+
+ await act(async () => {
+ void browseButton.props.onPress();
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+
+ const { router } = await import("expo-router");
+ const push = vi.mocked(router.push);
+ push.mockClear();
+ const [lockedViewButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "View upload processed-upload.mp4",
+ });
+ const [lockedQueueMenu] = renderer.root.findAllByProps({
+ accessibilityLabel: "More actions for processed-upload.mp4",
+ });
+ if (!lockedViewButton) throw new Error("View button was not rendered");
+ if (!lockedQueueMenu) throw new Error("Upload queue menu was not rendered");
+ expect(lockedViewButton.props.accessibilityHint).toBe(
+ "Another upload is in progress",
+ );
+ expect(lockedViewButton.props.accessibilityValue).toEqual({
+ text: "Preparing upload launch-review.mp4",
+ });
+ expect(lockedViewButton.props.disabled).toBe(true);
+ expect(lockedQueueMenu.props.accessibilityHint).toBe(
+ "Another upload is in progress",
+ );
+ expect(lockedQueueMenu.props.accessibilityState).toEqual({
+ busy: false,
+ disabled: true,
+ });
+ expect(lockedQueueMenu.props.accessibilityValue).toEqual({
+ text: "Preparing upload launch-review.mp4",
+ });
+ expect(lockedQueueMenu.props.disabled).toBe(true);
+
+ showActionSheetWithOptions.mockClear();
+ await act(async () => {
+ viewButton.props.onPress({ stopPropagation: vi.fn() });
+ lockedQueueMenu.props.onPress({ stopPropagation: vi.fn() });
+ callback(0);
+ });
+
+ expect(push).not.toHaveBeenCalled();
+ expect(showActionSheetWithOptions).not.toHaveBeenCalled();
+
+ await act(async () => {
+ resolveUpload?.({
+ id: "video_123",
+ } as Awaited>);
+ await Promise.resolve();
+ });
+ });
+
+ it("announces processing upload queue rows with their current status", async () => {
+ uploadQueueState.value.items = [
+ {
+ capId: "video_processing",
+ contentType: "video/mp4",
+ createdAt: "2026-05-18T10:00:00.000Z",
+ error: null,
+ fileName: "processing-upload.mp4",
+ folderId: null,
+ id: "processing-upload",
+ localUri: "file:///tmp/processing-upload.mp4",
+ organizationId: "org_123",
+ progress: 0.42,
+ processingMessage: "Processing frames",
+ rawFileKey: "raw-file-key",
+ size: 124_000,
+ durationSeconds: 125,
+ status: "processing",
+ updatedAt: "2026-05-18T10:00:00.000Z",
+ },
+ ];
+ const renderer = await renderComponent(React.createElement(UploadScreen));
+ const tree = renderer.toJSON();
+ const [queueRow] = renderer.root.findAllByProps({
+ accessibilityLabel: "Upload processing-upload.mp4",
+ });
+ const [queueMenu] = renderer.root.findAllByProps({
+ accessibilityLabel: "More actions for processing-upload.mp4",
+ });
+ const [viewButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "View upload processing-upload.mp4",
+ });
+ if (!queueRow) throw new Error("Processing upload row was not rendered");
+ if (!queueMenu) throw new Error("Upload queue menu was not rendered");
+ if (!viewButton) throw new Error("View button was not rendered");
+
+ expect(getTextNodes(tree).join("")).toContain(
+ "Processing frames ยท 124 KB ยท 2 mins",
+ );
+ expect(queueRow.props.accessibilityHint).toBe(
+ "Processing frames. Opens upload actions",
+ );
+ expect(queueMenu.props.accessibilityHint).toBe(
+ "Opens view and remove actions",
+ );
+ expect(queueRow.props.accessibilityValue).toEqual({
+ text: "Processing frames ยท 124 KB ยท 2 mins",
+ });
+ expect(
+ hasProps(tree, {
+ accessibilityLabel: "Upload progress for processing-upload.mp4",
+ accessibilityRole: "progressbar",
+ accessibilityValue: {
+ max: 100,
+ min: 0,
+ now: 42,
+ text: "42%",
+ },
+ }),
+ ).toBe(true);
+ expect(viewButton.props.accessibilityHint).toBe("Opens the uploaded Cap");
+ });
+
+ it("shows queued upload rows without premature progress", async () => {
+ uploadQueueState.value.items = [
+ {
+ capId: null,
+ contentType: "video/mp4",
+ createdAt: "2026-05-18T10:00:00.000Z",
+ error: null,
+ fileName: "queued-upload.mp4",
+ folderId: null,
+ id: "queued-upload",
+ localUri: "file:///tmp/queued-upload.mp4",
+ organizationId: "org_123",
+ progress: 0,
+ processingMessage: null,
+ rawFileKey: null,
+ size: 124_000,
+ durationSeconds: 125,
+ status: "queued",
+ updatedAt: "2026-05-18T10:00:00.000Z",
+ },
+ ];
+ const renderer = await renderComponent(React.createElement(UploadScreen));
+ const tree = renderer.toJSON();
+ const [queueRow] = renderer.root.findAllByProps({
+ accessibilityLabel: "Upload queued-upload.mp4",
+ });
+ const [queueMenu] = renderer.root.findAllByProps({
+ accessibilityLabel: "More actions for queued-upload.mp4",
+ });
+ if (!queueRow) throw new Error("Queued upload row was not rendered");
+ if (!queueMenu) throw new Error("Upload queue menu was not rendered");
+
+ expect(getTextNodes(tree).join("")).toContain("Queued ยท 124 KB ยท 2 mins");
+ expect(queueRow.props.accessibilityHint).toBe(
+ "Queued. Opens upload actions",
+ );
+ expect(queueRow.props.accessibilityValue).toEqual({
+ text: "Queued ยท 124 KB ยท 2 mins",
+ });
+ expect(queueMenu.props.accessibilityHint).toBe("Opens remove action");
+ expect(
+ hasProps(tree, {
+ accessibilityLabel: "Upload progress for queued-upload.mp4",
+ accessibilityRole: "progressbar",
+ }),
+ ).toBe(false);
+
+ const { ActionSheetIOS } = await import("react-native");
+ const showActionSheetWithOptions = vi.mocked(
+ ActionSheetIOS.showActionSheetWithOptions,
+ );
+ showActionSheetWithOptions.mockClear();
+
+ await act(async () => {
+ queueMenu.props.onPress({ stopPropagation: vi.fn() });
+ });
+
+ expect(showActionSheetWithOptions).toHaveBeenCalledWith(
+ expect.objectContaining({
+ cancelButtonIndex: 1,
+ destructiveButtonIndex: 0,
+ message: "Queued ยท 124 KB ยท 2 mins",
+ options: ["Remove from Queue", "Cancel"],
+ title: "queued-upload.mp4",
+ userInterfaceStyle: "light",
+ }),
+ expect.any(Function),
+ );
+ });
+
+ it("keeps uploaded files processing when the library refresh fails", async () => {
+ const auth = createAuth();
+ auth.refresh = vi.fn(() => Promise.reject(new Error("Refresh failed")));
+ authState.value = auth;
+ const DocumentPicker = await import("expo-document-picker");
+ const { runMobileUpload } = await import("@/uploads/runMobileUpload");
+ vi.mocked(DocumentPicker.getDocumentAsync).mockResolvedValueOnce({
+ assets: [
+ {
+ mimeType: "video/mp4",
+ name: "launch-review.mp4",
+ size: 12_400_000,
+ uri: "file:///tmp/launch-review.mp4",
+ },
+ ],
+ canceled: false,
+ } as Awaited>);
+ vi.mocked(runMobileUpload).mockResolvedValueOnce({
+ id: "video_123",
+ } as Awaited>);
+ const renderer = await renderComponent(React.createElement(UploadScreen));
+ const [browseButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Browse Files",
+ });
+ if (!browseButton) throw new Error("Browse Files button was not rendered");
+
+ await act(async () => {
+ await browseButton.props.onPress();
+ await Promise.resolve();
+ });
+
+ expect(uploadQueueActionsState.value).toContainEqual(
+ expect.objectContaining({
+ progress: 0,
+ type: "processing",
+ }),
+ );
+ expect(
+ uploadQueueActionsState.value.some(
+ (action) =>
+ typeof action === "object" &&
+ action !== null &&
+ "type" in action &&
+ action.type === "fail",
+ ),
+ ).toBe(false);
+ });
+
+ it("completes uploaded files when the final processing refresh fails", async () => {
+ vi.useFakeTimers();
+ try {
+ const auth = createAuth();
+ auth.refresh = vi.fn(() => Promise.reject(new Error("Refresh failed")));
+ auth.client.getCap = vi.fn(() =>
+ Promise.resolve({
+ cap: {
+ upload: null,
+ },
+ }),
+ );
+ authState.value = auth;
+ const DocumentPicker = await import("expo-document-picker");
+ const { runMobileUpload } = await import("@/uploads/runMobileUpload");
+ vi.mocked(DocumentPicker.getDocumentAsync).mockResolvedValueOnce({
+ assets: [
+ {
+ mimeType: "video/mp4",
+ name: "launch-review.mp4",
+ size: 12_400_000,
+ uri: "file:///tmp/launch-review.mp4",
+ },
+ ],
+ canceled: false,
+ } as Awaited>);
+ vi.mocked(runMobileUpload).mockResolvedValueOnce({
+ id: "video_123",
+ } as Awaited>);
+ const renderer = await renderComponent(React.createElement(UploadScreen));
+ const [browseButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Browse Files",
+ });
+ if (!browseButton)
+ throw new Error("Browse Files button was not rendered");
+
+ await act(async () => {
+ await browseButton.props.onPress();
+ await Promise.resolve();
+ });
+ await act(async () => {
+ await vi.advanceTimersByTimeAsync(1500);
+ });
+
+ expect(auth.client.getCap).toHaveBeenCalledWith("video_123");
+ expect(auth.refresh).toHaveBeenCalledTimes(2);
+ expect(uploadQueueActionsState.value).toContainEqual(
+ expect.objectContaining({
+ type: "complete",
+ }),
+ );
+ expect(
+ uploadQueueActionsState.value.some(
+ (action) =>
+ typeof action === "object" &&
+ action !== null &&
+ "type" in action &&
+ action.type === "fail",
+ ),
+ ).toBe(false);
+ } finally {
+ vi.useRealTimers();
+ }
+ });
+
+ it("opens the native iOS upload queue sheet", async () => {
+ uploadQueueState.value.items = [
+ {
+ capId: null,
+ contentType: "video/mp4",
+ createdAt: "2026-05-18T10:00:00.000Z",
+ error: "Network unavailable",
+ fileName: "failed-upload.mp4",
+ folderId: null,
+ id: "failed-upload",
+ localUri: "file:///tmp/failed-upload.mp4",
+ organizationId: "org_123",
+ progress: 0.42,
+ rawFileKey: null,
+ size: 124_000,
+ durationSeconds: 125,
+ status: "failed",
+ updatedAt: "2026-05-18T10:00:00.000Z",
+ },
+ ];
+ const renderer = await renderComponent(React.createElement(UploadScreen));
+ const tree = renderer.toJSON();
+ const [queueMenu] = renderer.root.findAllByProps({
+ accessibilityLabel: "More actions for failed-upload.mp4",
+ });
+ if (!queueMenu) throw new Error("Upload queue menu was not rendered");
+ const [queueRow] = renderer.root.findAllByProps({
+ accessibilityLabel: "Upload failed-upload.mp4",
+ });
+ if (!queueRow) throw new Error("Upload queue row was not rendered");
+ expect(getTextNodes(tree).join("")).toContain(
+ "Upload failed ยท Network unavailable ยท 124 KB ยท 2 mins",
+ );
+ expect(queueMenu.props.hitSlop).toBe(6);
+ expect(queueMenu.props.accessibilityHint).toBe(
+ "Opens retry and remove actions",
+ );
+ expect(
+ hasProps(tree, {
+ accessibilityHint: "Upload failed. Opens upload actions",
+ accessibilityLabel: "Upload failed-upload.mp4",
+ accessibilityState: { busy: false, disabled: false },
+ }),
+ ).toBe(true);
+ expect(queueRow.props.accessibilityValue).toEqual({
+ text: "Upload failed ยท Network unavailable ยท 124 KB ยท 2 mins",
+ });
+ expect(
+ hasProps(tree, {
+ accessibilityLabel: "Upload progress for failed-upload.mp4",
+ accessibilityRole: "progressbar",
+ }),
+ ).toBe(false);
+ expect(hasProp(tree, "accessibilityRole", "alert")).toBe(true);
+
+ const { ActionSheetIOS } = await import("react-native");
+ const showActionSheetWithOptions = vi.mocked(
+ ActionSheetIOS.showActionSheetWithOptions,
+ );
+ showActionSheetWithOptions.mockClear();
+
+ const menuStopPropagation = vi.fn();
+ await act(async () => {
+ queueMenu.props.onPress({ stopPropagation: menuStopPropagation });
+ });
+
+ expect(menuStopPropagation).toHaveBeenCalled();
+ expect(showActionSheetWithOptions).toHaveBeenCalledWith(
+ expect.objectContaining({
+ cancelButtonIndex: 2,
+ destructiveButtonIndex: 1,
+ message: "Upload failed ยท Network unavailable ยท 124 KB ยท 2 mins",
+ options: ["Retry", "Remove from Queue", "Cancel"],
+ title: "failed-upload.mp4",
+ userInterfaceStyle: "light",
+ }),
+ expect.any(Function),
+ );
+ showActionSheetWithOptions.mockClear();
+
+ await act(async () => {
+ queueRow.props.onPress();
+ });
+
+ expect(showActionSheetWithOptions).toHaveBeenCalledWith(
+ expect.objectContaining({
+ cancelButtonIndex: 2,
+ destructiveButtonIndex: 1,
+ message: "Upload failed ยท Network unavailable ยท 124 KB ยท 2 mins",
+ options: ["Retry", "Remove from Queue", "Cancel"],
+ title: "failed-upload.mp4",
+ userInterfaceStyle: "light",
+ }),
+ expect.any(Function),
+ );
+ });
+
+ it("announces picker errors as native alerts", async () => {
+ const DocumentPicker = await import("expo-document-picker");
+ vi.mocked(DocumentPicker.getDocumentAsync).mockRejectedValueOnce(
+ new Error("Files unavailable"),
+ );
+ const uploadRenderer = await renderComponent(
+ React.createElement(UploadScreen),
+ );
+ const [browseButton] = uploadRenderer.root.findAllByProps({
+ accessibilityLabel: "Browse Files",
+ });
+ if (!browseButton) throw new Error("Browse Files button was not rendered");
+
+ await act(async () => {
+ await browseButton.props.onPress();
+ });
+
+ expect(getTextNodes(uploadRenderer.toJSON())).toContain(
+ "Upload source unavailable",
+ );
+ expect(getTextNodes(uploadRenderer.toJSON())).toContain(
+ "Files unavailable",
+ );
+ const [uploadSource] = uploadRenderer.root.findAllByProps({
+ accessibilityLabel: "Upload source unavailable",
+ });
+ if (!uploadSource) throw new Error("Upload source button was not rendered");
+ expect(uploadSource.props.accessibilityHint).toBe(
+ "Retries upload source options",
+ );
+ expect(uploadSource.props.accessibilityValue).toEqual({
+ text: "Files unavailable",
+ });
+ const [retryFilesButton] = uploadRenderer.root.findAllByProps({
+ accessibilityLabel: "Retry Files",
+ });
+ if (!retryFilesButton)
+ throw new Error("Retry Files button was not rendered");
+ expect(retryFilesButton.props.accessibilityHint).toBe("Files unavailable");
+ expect(retryFilesButton.props.disabled).toBe(false);
+ const [loomImport] = uploadRenderer.root.findAllByProps({
+ accessibilityLabel: "Open Loom import",
+ });
+ if (!loomImport) throw new Error("Loom import card was not rendered");
+ expect(loomImport.props.accessibilityValue).toBeUndefined();
+ expect(hasStyle(uploadRenderer.toJSON(), { color: "#e5484d" })).toBe(true);
+ expect(
+ hasProps(uploadRenderer.toJSON(), {
+ accessibilityLiveRegion: "polite",
+ accessibilityRole: "alert",
+ }),
+ ).toBe(true);
+ });
+
+ it("shows dashboard import actions", async () => {
+ const tree = await renderTree(React.createElement(CapsScreen));
+ const text = getTextNodes(tree);
+
+ expect(text).toContain("My Caps");
+ expect(text.filter((item) => item === "New Folder").length).toBeGreaterThan(
+ 0,
+ );
+ expect(text).not.toContain("Record");
+ expect(
+ text.filter((item) => item === "Import Video").length,
+ ).toBeGreaterThan(0);
+ expect(hasStyle(tree, { marginBottom: 40 })).toBe(true);
+ expect(text.join("")).toContain("Hey Richie! Import your first Cap");
+ expect(hasProp(tree, "accessibilityLabel", "Cap logo")).toBe(true);
+ expect(
+ hasProps(tree, {
+ accessibilityHint: "Opens import options",
+ accessibilityLabel: "Import Video",
+ }),
+ ).toBe(true);
+ });
+
+ it("announces dashboard load errors with a retry action", async () => {
+ const auth = createAuth();
+ auth.client.listCaps = vi.fn(() =>
+ Promise.reject(new Error("Network unavailable")),
+ );
+ authState.value = auth;
+ const renderer = await renderComponent(React.createElement(CapsScreen));
+
+ await act(async () => {
+ await Promise.resolve();
+ });
+
+ const tree = renderer.toJSON();
+ const text = getTextNodes(tree);
+
+ expect(text).toContain("Unable to load Caps");
+ expect(text).toContain("Network unavailable");
+ expect(
+ hasProps(tree, {
+ accessibilityLabel: "Library error: Network unavailable",
+ accessibilityLiveRegion: "polite",
+ accessibilityRole: "alert",
+ }),
+ ).toBe(true);
+
+ const [retryButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Try again",
+ });
+ if (!retryButton)
+ throw new Error("Dashboard retry action was not rendered");
+ expect(retryButton.props.accessibilityHint).toBe(
+ "Reloads your Cap library",
+ );
+
+ await act(async () => {
+ await retryButton.props.onPress();
+ await Promise.resolve();
+ });
+
+ expect(auth.client.listCaps).toHaveBeenCalledTimes(2);
+ });
+
+ it("renders dashboard folders with native folder rows", async () => {
+ const auth = createAuth();
+ auth.client.listCaps = () =>
+ Promise.resolve({
+ caps: [],
+ folders: [
+ {
+ color: "blue",
+ id: "folder_123",
+ name: "Product",
+ parentId: null,
+ videoCount: 2,
+ },
+ ],
+ pagination: {
+ hasNextPage: false,
+ page: 1,
+ totalPages: 1,
+ },
+ rootFolders: [],
+ });
+ authState.value = auth;
+
+ const renderer = await renderComponent(React.createElement(CapsScreen));
+ await act(async () => {
+ await Promise.resolve();
+ });
+ const tree = renderer.toJSON();
+ const text = getTextNodes(tree);
+
+ expect(text).toContain("Folders");
+ expect(text).toContain("Product");
+ expect(text.join("")).toContain("2 videos");
+ expect(hasStyle(tree, { paddingBottom: 24 })).toBe(true);
+ expect(hasProp(tree, "accessibilityLabel", "Open folder Product")).toBe(
+ true,
+ );
+
+ const [folderRow] = renderer.root.findAllByProps({
+ accessibilityLabel: "Open folder Product",
+ });
+ if (!folderRow) throw new Error("Folder row was not rendered");
+
+ expect(resolveStyle(folderRow.props.style)).toMatchObject({
+ backgroundColor: "#f0f0f0",
+ borderColor: "#e0e0e0",
+ });
+ expect(resolveStyle(folderRow.props.style, true)).toMatchObject({
+ backgroundColor: "#e8e8e8",
+ borderColor: "#d9d9d9",
+ });
+
+ await act(async () => {
+ folderRow.props.onPress();
+ });
+
+ expect(
+ hasProp(renderer.toJSON(), "accessibilityLabel", "Back to My Caps"),
+ ).toBe(true);
+ });
+
+ it("marks the dashboard card sharing action busy while visibility is updating", async () => {
+ const auth = createAuth();
+ const cap = {
+ commentCount: 2,
+ createdAt: "2026-05-18T10:00:00.000Z",
+ durationSeconds: 125,
+ folderId: null,
+ id: "video_123",
+ ownerName: "Richie",
+ protected: false,
+ public: true,
+ reactionCount: 3,
+ shareUrl: "https://cap.so/s/video_123",
+ thumbnailUrl: null,
+ title: "Launch review",
+ updatedAt: "2026-05-18T10:30:00.000Z",
+ upload: null,
+ viewCount: 7,
+ };
+ const sharingDeferred = createDeferred();
+ auth.client.listCaps = vi.fn(() =>
+ Promise.resolve({
+ caps: [cap],
+ folders: [],
+ pagination: {
+ hasNextPage: false,
+ page: 1,
+ totalPages: 1,
+ },
+ rootFolders: [],
+ }),
+ );
+ auth.client.updateCapSharing = vi.fn(() => sharingDeferred.promise);
+ authState.value = auth;
+
+ const renderer = await renderComponent(React.createElement(CapsScreen));
+ await act(async () => {
+ await Promise.resolve();
+ });
+ const [capCard] = renderer.root.findAllByProps({ cap });
+ if (!capCard) throw new Error("Cap card was not rendered");
+
+ const { ActionSheetIOS } = await import("react-native");
+ const showActionSheetWithOptions = vi.mocked(
+ ActionSheetIOS.showActionSheetWithOptions,
+ );
+ showActionSheetWithOptions.mockClear();
+
+ await act(async () => {
+ capCard.props.onVisibilityPress();
+ });
+
+ const [, sharingCallback] = showActionSheetWithOptions.mock.calls[0] ?? [];
+ if (!sharingCallback) throw new Error("Sharing callback was not set");
+
+ await act(async () => {
+ sharingCallback(0);
+ await Promise.resolve();
+ });
+
+ const [busyCard] = renderer.root.findAllByProps({ cap });
+ if (!busyCard) throw new Error("Busy Cap card was not rendered");
+
+ expect(auth.client.updateCapSharing).toHaveBeenCalledWith("video_123", {
+ public: false,
+ });
+ expect(busyCard.props.visibilityBusy).toBe(true);
+ expect(busyCard.props.visibilityDisabled).toBe(true);
+ expect(busyCard.props.visibilityDisabledHint).toBe(
+ "Sharing update is in progress",
+ );
+ expect(busyCard.props.visibilityValue).toBeUndefined();
+ expect(busyCard.props.visibilityAccessibilityValue).toBe(
+ "Updating sharing for Launch review",
+ );
+
+ await act(async () => {
+ sharingDeferred.resolve({ ...cap, public: false });
+ await sharingDeferred.promise;
+ await Promise.resolve();
+ });
+ });
+
+ it("opens the native iOS folder creation prompt and color sheet", async () => {
+ const auth = createAuth();
+ authState.value = auth;
+ const renderer = await renderComponent(React.createElement(CapsScreen));
+ const [newFolder] = renderer.root.findAllByProps({
+ accessibilityLabel: "New Folder",
+ });
+ if (!newFolder) throw new Error("New Folder action was not rendered");
+
+ const { ActionSheetIOS, Alert } = await import("react-native");
+ const prompt = vi.mocked(Alert.prompt);
+ const showActionSheetWithOptions = vi.mocked(
+ ActionSheetIOS.showActionSheetWithOptions,
+ );
+ prompt.mockClear();
+ showActionSheetWithOptions.mockClear();
+
+ await act(async () => {
+ newFolder.props.onPress();
+ });
+
+ expect(prompt).toHaveBeenCalledWith(
+ "New Folder",
+ "Name this folder.",
+ expect.any(Array),
+ "plain-text",
+ );
+
+ const buttons = prompt.mock.calls[0]?.[2] as
+ | Array<{ onPress?: (value?: string) => void }>
+ | undefined;
+ if (!Array.isArray(buttons)) {
+ throw new Error("Folder prompt buttons were not provided");
+ }
+ const nextButton = buttons[1];
+ const nextAction = nextButton?.onPress;
+ if (typeof nextAction !== "function") {
+ throw new Error("Folder prompt next action was not provided");
+ }
+
+ await act(async () => {
+ nextAction("Product");
+ });
+
+ expect(showActionSheetWithOptions).toHaveBeenCalledWith(
+ expect.objectContaining({
+ cancelButtonIndex: 4,
+ message: "Product",
+ options: ["Normal", "Blue", "Red", "Yellow", "Cancel"],
+ title: "Folder color",
+ userInterfaceStyle: "light",
+ }),
+ expect.any(Function),
+ );
+
+ const [, colorCallback] = showActionSheetWithOptions.mock.calls[0] ?? [];
+ if (!colorCallback) throw new Error("Folder color callback was not set");
+
+ await act(async () => {
+ colorCallback(1);
+ await Promise.resolve();
+ });
+
+ expect(auth.client.createFolder).toHaveBeenCalledWith({
+ name: "Product",
+ color: "blue",
+ });
+ });
+
+ it("locks dashboard navigation while a folder is being created", async () => {
+ const auth = createAuth();
+ const folderDeferred =
+ createDeferred>>();
+ auth.client.createFolder = vi.fn(() => folderDeferred.promise);
+ authState.value = auth;
+ const renderer = await renderComponent(React.createElement(CapsScreen));
+ const [newFolder] = renderer.root.findAllByProps({
+ accessibilityLabel: "New Folder",
+ });
+ if (!newFolder) throw new Error("New Folder action was not rendered");
+
+ const { ActionSheetIOS, Alert } = await import("react-native");
+ const prompt = vi.mocked(Alert.prompt);
+ const showActionSheetWithOptions = vi.mocked(
+ ActionSheetIOS.showActionSheetWithOptions,
+ );
+ prompt.mockClear();
+ showActionSheetWithOptions.mockClear();
+
+ await act(async () => {
+ newFolder.props.onPress();
+ });
+
+ const buttons = prompt.mock.calls[0]?.[2] as
+ | Array<{ onPress?: (value?: string) => void }>
+ | undefined;
+ const nextAction = buttons?.[1]?.onPress;
+ if (typeof nextAction !== "function") {
+ throw new Error("Folder prompt next action was not provided");
+ }
+
+ await act(async () => {
+ nextAction("Product");
+ });
+
+ const [, colorCallback] = showActionSheetWithOptions.mock.calls[0] ?? [];
+ if (!colorCallback) throw new Error("Folder color callback was not set");
+
+ await act(async () => {
+ colorCallback(1);
+ await Promise.resolve();
+ });
+
+ const [creatingFolder] = renderer.root.findAllByProps({
+ accessibilityLabel: "New Folder",
+ });
+ if (!creatingFolder) {
+ throw new Error("Creating folder action was not rendered");
+ }
+ expect(getTextNodes(renderer.toJSON())).not.toContain("Creating...");
+ expect(creatingFolder.props.loading).toBe(true);
+ expect(creatingFolder.props.accessibilityHint).toBe(
+ "Folder creation is in progress",
+ );
+ expect(creatingFolder.props.accessibilityValue).toEqual({
+ text: "Creating folder Product",
+ });
+ for (const action of renderer.root.findAllByProps({
+ accessibilityLabel: "Import Video",
+ })) {
+ expect(action.props.disabled).toBe(true);
+ expect(action.props.accessibilityHint).toBe(
+ "Folder creation is in progress",
+ );
+ expect(action.props.accessibilityValue).toEqual({
+ text: "Creating folder Product",
+ });
+ }
+
+ await act(async () => {
+ folderDeferred.resolve({
+ id: "folder_123",
+ name: "Product",
+ color: "blue",
+ parentId: null,
+ videoCount: 0,
+ });
+ await folderDeferred.promise;
+ await Promise.resolve();
+ });
+ });
+});
diff --git a/apps/mobile/src/theme.test.ts b/apps/mobile/src/theme.test.ts
new file mode 100644
index 00000000000..13a0898d491
--- /dev/null
+++ b/apps/mobile/src/theme.test.ts
@@ -0,0 +1,63 @@
+import { describe, expect, it, vi } from "vitest";
+import { colors } from "./theme";
+
+vi.mock("react-native", () => ({
+ StyleSheet: {
+ create: >(styles: T) => styles,
+ },
+}));
+
+const webRadixColors = {
+ gray: {
+ gray1: "#fcfcfc",
+ gray2: "#f9f9f9",
+ gray3: "#f0f0f0",
+ gray4: "#e8e8e8",
+ gray5: "#e0e0e0",
+ gray6: "#d9d9d9",
+ gray7: "#cecece",
+ gray8: "#bbbbbb",
+ gray9: "#8d8d8d",
+ gray10: "#838383",
+ gray11: "#646464",
+ gray12: "#202020",
+ },
+ blue: {
+ blue1: "#fbfdff",
+ blue2: "#f4faff",
+ blue3: "#e6f4fe",
+ blue4: "#d5efff",
+ blue5: "#c2e5ff",
+ blue6: "#acd8fc",
+ blue7: "#8ec8f6",
+ blue8: "#5eb1ef",
+ blue9: "#0090ff",
+ blue10: "#0588f0",
+ blue11: "#0d74ce",
+ blue12: "#113264",
+ },
+ red: {
+ red1: "#fffcfc",
+ red2: "#fff7f7",
+ red3: "#feebec",
+ red4: "#ffdbdc",
+ red5: "#ffcdce",
+ red6: "#fdbdbe",
+ red7: "#f4a9aa",
+ red8: "#eb8e90",
+ red9: "#e5484d",
+ red10: "#dc3e42",
+ red11: "#ce2c31",
+ red12: "#641723",
+ },
+};
+
+describe("mobile theme", () => {
+ it("matches the Radix color scales imported by Cap web", () => {
+ expect(colors).toMatchObject({
+ ...webRadixColors.gray,
+ ...webRadixColors.blue,
+ ...webRadixColors.red,
+ });
+ });
+});
diff --git a/apps/mobile/src/theme.ts b/apps/mobile/src/theme.ts
new file mode 100644
index 00000000000..786269c6b82
--- /dev/null
+++ b/apps/mobile/src/theme.ts
@@ -0,0 +1,95 @@
+import { StyleSheet } from "react-native";
+
+export const colors = {
+ white: "#ffffff",
+ black: "#000000",
+ gray1: "#fcfcfc",
+ gray2: "#f9f9f9",
+ gray3: "#f0f0f0",
+ gray4: "#e8e8e8",
+ gray5: "#e0e0e0",
+ gray6: "#d9d9d9",
+ gray7: "#cecece",
+ gray8: "#bbbbbb",
+ gray9: "#8d8d8d",
+ gray10: "#838383",
+ gray11: "#646464",
+ gray12: "#202020",
+ appBackground: "#f9f9f9",
+ blue1: "#fbfdff",
+ blue2: "#f4faff",
+ blue3: "#e6f4fe",
+ blue4: "#d5efff",
+ blue5: "#c2e5ff",
+ blue6: "#acd8fc",
+ blue7: "#8ec8f6",
+ blue8: "#5eb1ef",
+ blue9: "#0090ff",
+ blue10: "#0588f0",
+ blue11: "#0d74ce",
+ blue12: "#113264",
+ red1: "#fffcfc",
+ red2: "#fff7f7",
+ red3: "#feebec",
+ red4: "#ffdbdc",
+ red5: "#ffcdce",
+ red6: "#fdbdbe",
+ red7: "#f4a9aa",
+ red8: "#eb8e90",
+ red9: "#e5484d",
+ red10: "#dc3e42",
+ red11: "#ce2c31",
+ red12: "#641723",
+ primary: "#005cb1",
+ primary2: "#004c93",
+ secondary: "#2eb4ff",
+ tertiary: "#c5eaff",
+ buttonBlue: "#2563eb",
+ buttonBlueHover: "#1d4ed8",
+ buttonBlueBorder: "#1e40af",
+ glass: "rgba(252, 252, 252, 0.72)",
+ blackAlpha5: "rgba(18, 22, 31, 0.05)",
+ blackAlpha10: "rgba(18, 22, 31, 0.1)",
+ blackAlpha40: "rgba(18, 22, 31, 0.4)",
+ blackAlpha60: "rgba(18, 22, 31, 0.6)",
+ green9: "#30a46c",
+ yellow3: "#fffab8",
+ yellow5: "#ffe770",
+ yellow9: "#f5d90a",
+};
+
+export const fonts = {
+ regular: "NeueMontreal-Regular",
+ medium: "NeueMontreal-Medium",
+ bold: "NeueMontreal-Bold",
+};
+
+export const radius = {
+ xs: 6,
+ sm: 8,
+ md: 12,
+ lg: 16,
+ xl: 20,
+ full: 999,
+};
+
+export const squircle = {
+ borderCurve: "continuous" as const,
+};
+
+export const shadows = StyleSheet.create({
+ card: {
+ shadowColor: colors.black,
+ shadowOffset: { width: 0, height: 1 },
+ shadowOpacity: 0.04,
+ shadowRadius: 2,
+ elevation: 1,
+ },
+ popover: {
+ shadowColor: colors.black,
+ shadowOffset: { width: 0, height: 16 },
+ shadowOpacity: 0.12,
+ shadowRadius: 32,
+ elevation: 10,
+ },
+});
diff --git a/apps/mobile/src/uploads/fileTypes.test.ts b/apps/mobile/src/uploads/fileTypes.test.ts
new file mode 100644
index 00000000000..d253cfb319c
--- /dev/null
+++ b/apps/mobile/src/uploads/fileTypes.test.ts
@@ -0,0 +1,27 @@
+import { describe, expect, it } from "vitest";
+import { contentTypeForUpload, contentTypeFromName } from "./fileTypes";
+
+describe("mobile upload file type inference", () => {
+ it.each([
+ ["demo.mp4", "video/mp4"],
+ ["demo.mov", "video/quicktime"],
+ ["demo.webm", "video/webm"],
+ ["demo.mkv", "video/x-matroska"],
+ ["demo.avi", "video/x-msvideo"],
+ ["demo.m4v", "video/x-m4v"],
+ ])("infers %s as %s", (name, contentType) => {
+ expect(contentTypeFromName(name)).toBe(contentType);
+ });
+
+ it("keeps picker-provided video content types", () => {
+ expect(contentTypeForUpload("demo.mkv", "video/custom")).toBe(
+ "video/custom",
+ );
+ });
+
+ it("falls back to the filename when the picker returns an opaque type", () => {
+ expect(contentTypeForUpload("demo.mkv", "application/octet-stream")).toBe(
+ "video/x-matroska",
+ );
+ });
+});
diff --git a/apps/mobile/src/uploads/fileTypes.ts b/apps/mobile/src/uploads/fileTypes.ts
new file mode 100644
index 00000000000..b7539305617
--- /dev/null
+++ b/apps/mobile/src/uploads/fileTypes.ts
@@ -0,0 +1,24 @@
+const videoContentTypesByExtension: Record = {
+ avi: "video/x-msvideo",
+ m4v: "video/x-m4v",
+ mkv: "video/x-matroska",
+ mov: "video/quicktime",
+ mp4: "video/mp4",
+ webm: "video/webm",
+};
+
+const extensionFromName = (name: string) => {
+ const extension = name.split(".").at(-1)?.toLowerCase();
+ return extension && extension !== name.toLowerCase() ? extension : null;
+};
+
+export const contentTypeFromName = (name: string) =>
+ videoContentTypesByExtension[extensionFromName(name) ?? ""] ?? "video/mp4";
+
+export const contentTypeForUpload = (
+ name: string,
+ contentType?: string | null,
+) => {
+ if (contentType?.startsWith("video/")) return contentType;
+ return contentTypeFromName(name);
+};
diff --git a/apps/mobile/src/uploads/runMobileUpload.test.ts b/apps/mobile/src/uploads/runMobileUpload.test.ts
new file mode 100644
index 00000000000..3b664f1338c
--- /dev/null
+++ b/apps/mobile/src/uploads/runMobileUpload.test.ts
@@ -0,0 +1,173 @@
+import { Folder, Organisation, Video } from "@cap/web-domain";
+import { describe, expect, it, vi } from "vitest";
+import type { MobileApiClient, UploadFile } from "@/api/mobile";
+import { runMobileUpload } from "./runMobileUpload";
+
+const uploadMock = vi.hoisted(() => ({
+ uploadToTarget: vi.fn(
+ async (
+ _target: unknown,
+ _file: UploadFile,
+ onProgress?: (progress: { loaded: number; total: number }) => void,
+ ) => {
+ onProgress?.({ loaded: 40, total: 80 });
+ },
+ ),
+}));
+
+vi.mock("@/api/mobile", () => ({
+ uploadToTarget: uploadMock.uploadToTarget,
+}));
+
+describe("runMobileUpload", () => {
+ it("passes native video metadata through upload creation and retry-safe progress", async () => {
+ const createUpload = vi.fn(async () => ({
+ id: Video.VideoId.make("video_123"),
+ shareUrl: "https://cap.so/s/video_123",
+ rawFileKey: "user_123/video_123/raw-upload.mov",
+ upload: {
+ type: "put" as const,
+ url: "https://uploads.example/video",
+ headers: {
+ "Content-Type": "video/quicktime",
+ },
+ },
+ cap: {
+ id: Video.VideoId.make("video_123"),
+ shareUrl: "https://cap.so/s/video_123",
+ title: "video",
+ createdAt: "2026-05-18T10:00:00.000Z",
+ updatedAt: "2026-05-18T10:00:00.000Z",
+ ownerName: "Richie",
+ durationSeconds: 12.5,
+ thumbnailUrl: null,
+ folderId: null,
+ public: true,
+ protected: false,
+ viewCount: 0,
+ commentCount: 0,
+ reactionCount: 0,
+ upload: null,
+ },
+ }));
+ const updateUploadProgress = vi.fn(async () => ({
+ success: true as const,
+ }));
+ const completeUpload = vi.fn(async () => ({ success: true as const }));
+ const client = {
+ createUpload,
+ updateUploadProgress,
+ completeUpload,
+ } as unknown as MobileApiClient;
+ const file: UploadFile = {
+ uri: "file:///tmp/video.mov",
+ name: "video.mov",
+ type: "video/quicktime",
+ size: 80,
+ durationSeconds: 12.5,
+ width: 1920,
+ height: 1080,
+ };
+ const onProgress = vi.fn();
+
+ await runMobileUpload({
+ client,
+ file,
+ organizationId: Organisation.OrganisationId.make("org_123"),
+ folderId: Folder.FolderId.make("folder_123"),
+ onProgress,
+ });
+
+ expect(createUpload).toHaveBeenCalledWith({
+ organizationId: "org_123",
+ folderId: "folder_123",
+ fileName: "video.mov",
+ contentType: "video/quicktime",
+ contentLength: 80,
+ durationSeconds: 12.5,
+ width: 1920,
+ height: 1080,
+ });
+ expect(updateUploadProgress).toHaveBeenCalledWith("video_123", {
+ uploaded: 40,
+ total: 80,
+ });
+ expect(completeUpload).toHaveBeenCalledWith("video_123", {
+ rawFileKey: "user_123/video_123/raw-upload.mov",
+ contentLength: 80,
+ });
+ expect(onProgress).toHaveBeenCalledWith(0.5);
+ });
+
+ it("normalizes non-finite native upload progress", async () => {
+ uploadMock.uploadToTarget.mockImplementationOnce(
+ async (
+ _target: unknown,
+ _file: UploadFile,
+ onProgress?: (progress: { loaded: number; total: number }) => void,
+ ) => {
+ onProgress?.({ loaded: Number.NaN, total: Number.NaN });
+ },
+ );
+ const createUpload = vi.fn(async () => ({
+ id: Video.VideoId.make("video_123"),
+ shareUrl: "https://cap.so/s/video_123",
+ rawFileKey: "user_123/video_123/raw-upload.mov",
+ upload: {
+ type: "put" as const,
+ url: "https://uploads.example/video",
+ headers: {
+ "Content-Type": "video/quicktime",
+ },
+ },
+ cap: {
+ id: Video.VideoId.make("video_123"),
+ shareUrl: "https://cap.so/s/video_123",
+ title: "video",
+ createdAt: "2026-05-18T10:00:00.000Z",
+ updatedAt: "2026-05-18T10:00:00.000Z",
+ ownerName: "Richie",
+ durationSeconds: 12.5,
+ thumbnailUrl: null,
+ folderId: null,
+ public: true,
+ protected: false,
+ viewCount: 0,
+ commentCount: 0,
+ reactionCount: 0,
+ upload: null,
+ },
+ }));
+ const updateUploadProgress = vi.fn(async () => ({
+ success: true as const,
+ }));
+ const completeUpload = vi.fn(async () => ({ success: true as const }));
+ const client = {
+ createUpload,
+ updateUploadProgress,
+ completeUpload,
+ } as unknown as MobileApiClient;
+ const file: UploadFile = {
+ uri: "file:///tmp/video.mov",
+ name: "video.mov",
+ type: "video/quicktime",
+ size: 80,
+ durationSeconds: 12.5,
+ width: 1920,
+ height: 1080,
+ };
+ const onProgress = vi.fn();
+
+ await runMobileUpload({
+ client,
+ file,
+ onProgress,
+ });
+
+ expect(updateUploadProgress).toHaveBeenCalledWith("video_123", {
+ uploaded: 0,
+ total: 80,
+ });
+ expect(onProgress).toHaveBeenCalledWith(0);
+ });
+});
diff --git a/apps/mobile/src/uploads/runMobileUpload.ts b/apps/mobile/src/uploads/runMobileUpload.ts
new file mode 100644
index 00000000000..a295bc38803
--- /dev/null
+++ b/apps/mobile/src/uploads/runMobileUpload.ts
@@ -0,0 +1,71 @@
+import { Folder, Organisation } from "@cap/web-domain";
+import type { MobileApiClient, UploadFile } from "@/api/mobile";
+import { uploadToTarget } from "@/api/mobile";
+
+type RunMobileUploadInput = {
+ client: MobileApiClient;
+ file: UploadFile;
+ organizationId?: string | null;
+ folderId?: string | null;
+ onCreated?: (capId: string, rawFileKey: string) => void;
+ onProgress?: (progress: number) => void;
+};
+
+const nonNegativeFiniteNumber = (value: number | null | undefined) =>
+ typeof value === "number" && Number.isFinite(value) ? Math.max(0, value) : 0;
+
+const positiveFiniteNumber = (value: number | null | undefined) =>
+ typeof value === "number" && Number.isFinite(value) && value > 0
+ ? value
+ : null;
+
+const clampProgress = (progress: number) => {
+ const safeProgress = Number.isFinite(progress) ? progress : 0;
+ return Math.min(1, Math.max(0, safeProgress));
+};
+
+export const runMobileUpload = async ({
+ client,
+ file,
+ organizationId,
+ folderId,
+ onCreated,
+ onProgress,
+}: RunMobileUploadInput) => {
+ const created = await client.createUpload({
+ organizationId: organizationId
+ ? Organisation.OrganisationId.make(organizationId)
+ : undefined,
+ folderId: folderId ? Folder.FolderId.make(folderId) : undefined,
+ fileName: file.name,
+ contentType: file.type,
+ contentLength: file.size,
+ durationSeconds: file.durationSeconds,
+ width: file.width,
+ height: file.height,
+ });
+ onCreated?.(created.id, created.rawFileKey);
+
+ await uploadToTarget(created.upload, file, ({ loaded, total }) => {
+ const safeLoaded = nonNegativeFiniteNumber(loaded);
+ const safeTotal =
+ positiveFiniteNumber(total) ??
+ positiveFiniteNumber(file.size) ??
+ safeLoaded;
+ const progress = safeTotal > 0 ? safeLoaded / safeTotal : 0;
+ onProgress?.(clampProgress(progress));
+ client
+ .updateUploadProgress(created.id, {
+ uploaded: safeLoaded,
+ total: safeTotal,
+ })
+ .catch(() => {});
+ });
+
+ await client.completeUpload(created.id, {
+ rawFileKey: created.rawFileKey,
+ contentLength: file.size,
+ });
+
+ return created;
+};
diff --git a/apps/mobile/src/uploads/uploadQueue.test.ts b/apps/mobile/src/uploads/uploadQueue.test.ts
new file mode 100644
index 00000000000..aa446cad28e
--- /dev/null
+++ b/apps/mobile/src/uploads/uploadQueue.test.ts
@@ -0,0 +1,318 @@
+import { describe, expect, it } from "vitest";
+import {
+ emptyUploadQueue,
+ isTerminalUploadQueueAction,
+ uploadProgressPercent,
+ uploadQueueActionFromCapUpload,
+ uploadQueueReducer,
+ uploadQueueStatusText,
+} from "./uploadQueue";
+
+const item = {
+ id: "local-1",
+ localUri: "file:///tmp/video.mp4",
+ fileName: "video.mp4",
+ contentType: "video/mp4",
+ size: 100,
+ folderId: null,
+ organizationId: "org_123",
+ status: "queued" as const,
+ progress: 0,
+ error: null,
+ capId: null,
+ rawFileKey: null,
+ processingMessage: null,
+};
+
+describe("uploadQueueReducer", () => {
+ it("preserves failed uploads for retry", () => {
+ const queued = uploadQueueReducer(emptyUploadQueue, {
+ type: "enqueue",
+ item,
+ });
+ const failed = uploadQueueReducer(queued, {
+ type: "fail",
+ id: item.id,
+ error: "Network unavailable",
+ });
+ expect(failed.items[0]?.status).toBe("failed");
+ expect(failed.items[0]?.error).toBe("Network unavailable");
+
+ const retrying = uploadQueueReducer(failed, {
+ type: "retry",
+ id: item.id,
+ });
+ expect(retrying.items[0]?.status).toBe("queued");
+ expect(retrying.items[0]?.error).toBeNull();
+ expect(retrying.items[0]?.localUri).toBe(item.localUri);
+ });
+
+ it("clears stale server upload metadata before retrying", () => {
+ const queued = uploadQueueReducer(emptyUploadQueue, {
+ type: "enqueue",
+ item,
+ });
+ const uploading = uploadQueueReducer(queued, {
+ type: "start",
+ id: item.id,
+ capId: "cap_123",
+ rawFileKey: "raw/video.mp4",
+ });
+ const failed = uploadQueueReducer(uploading, {
+ type: "fail",
+ id: item.id,
+ error: "Upload target rejected the file",
+ });
+ const retrying = uploadQueueReducer(failed, {
+ type: "retry",
+ id: item.id,
+ });
+
+ expect(retrying.items[0]?.capId).toBeNull();
+ expect(retrying.items[0]?.rawFileKey).toBeNull();
+ });
+
+ it("keeps the created Cap id after upload completion", () => {
+ const queued = uploadQueueReducer(emptyUploadQueue, {
+ type: "enqueue",
+ item,
+ });
+ const uploading = uploadQueueReducer(queued, {
+ type: "start",
+ id: item.id,
+ capId: "cap_123",
+ rawFileKey: "raw/video.mp4",
+ });
+ const complete = uploadQueueReducer(uploading, {
+ type: "complete",
+ id: item.id,
+ });
+
+ expect(complete.items[0]).toMatchObject({
+ status: "complete",
+ capId: "cap_123",
+ rawFileKey: "raw/video.mp4",
+ });
+ });
+
+ it("uses the web finishing label while processing after upload", () => {
+ const queued = uploadQueueReducer(emptyUploadQueue, {
+ type: "enqueue",
+ item,
+ });
+ const uploading = uploadQueueReducer(queued, {
+ type: "start",
+ id: item.id,
+ capId: "cap_123",
+ rawFileKey: "raw/video.mp4",
+ });
+ const processing = uploadQueueReducer(uploading, {
+ type: "processing",
+ id: item.id,
+ progress: 0,
+ });
+
+ expect(processing.items[0]).toMatchObject({
+ status: "processing",
+ progress: 0,
+ capId: "cap_123",
+ rawFileKey: "raw/video.mp4",
+ });
+ expect(
+ processing.items[0] ? uploadQueueStatusText(processing.items[0]) : null,
+ ).toBe("Finishing up");
+ });
+
+ it("uses server processing progress and messages in the queue row", () => {
+ const queued = uploadQueueReducer(emptyUploadQueue, {
+ type: "enqueue",
+ item,
+ });
+ const processing = uploadQueueReducer(queued, {
+ type: "processing",
+ id: item.id,
+ progress: 0.42,
+ message: "Processing frames",
+ });
+
+ expect(processing.items[0]).toMatchObject({
+ status: "processing",
+ progress: 0.42,
+ processingMessage: "Processing frames",
+ });
+ expect(
+ processing.items[0] ? uploadQueueStatusText(processing.items[0]) : null,
+ ).toBe("Processing frames");
+ });
+
+ it("restores uploading status when progress arrives after processing", () => {
+ const queued = uploadQueueReducer(emptyUploadQueue, {
+ type: "enqueue",
+ item,
+ });
+ const processing = uploadQueueReducer(queued, {
+ type: "processing",
+ id: item.id,
+ progress: 0.25,
+ message: "Processing frames",
+ });
+ const uploading = uploadQueueReducer(processing, {
+ type: "progress",
+ id: item.id,
+ progress: 0.5,
+ });
+
+ expect(uploading.items[0]).toMatchObject({
+ status: "uploading",
+ progress: 0.5,
+ error: null,
+ processingMessage: null,
+ });
+ expect(
+ uploading.items[0] ? uploadQueueStatusText(uploading.items[0]) : null,
+ ).toBe("Uploading 50%");
+ });
+
+ it("keeps invalid queue progress display-safe", () => {
+ const queued = uploadQueueReducer(emptyUploadQueue, {
+ type: "enqueue",
+ item,
+ });
+ const invalidUploadProgress = uploadQueueReducer(queued, {
+ type: "progress",
+ id: item.id,
+ progress: Number.NaN,
+ });
+ const invalidProcessingProgress = uploadQueueReducer(queued, {
+ type: "processing",
+ id: item.id,
+ progress: Number.POSITIVE_INFINITY,
+ message: "Processing frames",
+ });
+
+ expect(invalidUploadProgress.items[0]).toMatchObject({
+ status: "uploading",
+ progress: 0,
+ });
+ expect(
+ invalidUploadProgress.items[0]
+ ? uploadQueueStatusText(invalidUploadProgress.items[0])
+ : null,
+ ).toBe("Uploading 0%");
+ expect(invalidProcessingProgress.items[0]).toMatchObject({
+ status: "processing",
+ progress: 0,
+ });
+ expect(uploadProgressPercent(Number.NaN)).toBe(0);
+ expect(uploadProgressPercent(Number.POSITIVE_INFINITY)).toBe(0);
+ });
+
+ it("maps settled server upload state back to local queue actions", () => {
+ expect(uploadQueueActionFromCapUpload(item.id, null)).toEqual({
+ type: "complete",
+ id: item.id,
+ });
+ expect(
+ uploadQueueActionFromCapUpload(item.id, {
+ uploaded: 100,
+ total: 100,
+ phase: "complete",
+ processingProgress: 100,
+ processingMessage: null,
+ processingError: null,
+ }),
+ ).toEqual({
+ type: "complete",
+ id: item.id,
+ });
+ expect(
+ uploadQueueActionFromCapUpload(item.id, {
+ uploaded: 100,
+ total: 100,
+ phase: "error",
+ processingProgress: 40,
+ processingMessage: null,
+ processingError: "Transcode failed",
+ }),
+ ).toEqual({
+ type: "fail",
+ id: item.id,
+ error: "Transcode failed",
+ });
+ });
+
+ it("maps active server upload state back to local queue progress", () => {
+ expect(
+ uploadQueueActionFromCapUpload(item.id, {
+ uploaded: 25,
+ total: 100,
+ phase: "uploading",
+ processingProgress: 0,
+ processingMessage: null,
+ processingError: null,
+ }),
+ ).toEqual({
+ type: "progress",
+ id: item.id,
+ progress: 0.25,
+ });
+ expect(
+ uploadQueueActionFromCapUpload(item.id, {
+ uploaded: 100,
+ total: 100,
+ phase: "processing",
+ processingProgress: 42,
+ processingMessage: "Processing frames",
+ processingError: null,
+ }),
+ ).toEqual({
+ type: "processing",
+ id: item.id,
+ progress: 0.42,
+ message: "Processing frames",
+ });
+ expect(
+ uploadQueueActionFromCapUpload(item.id, {
+ uploaded: 100,
+ total: 100,
+ phase: "generating_thumbnail",
+ processingProgress: 88,
+ processingMessage: null,
+ processingError: null,
+ }),
+ ).toEqual({
+ type: "processing",
+ id: item.id,
+ progress: 0.88,
+ message: "Finishing up",
+ });
+ });
+
+ it("keeps polling for non-terminal upload queue actions", () => {
+ expect(isTerminalUploadQueueAction({ type: "complete", id: item.id })).toBe(
+ true,
+ );
+ expect(
+ isTerminalUploadQueueAction({
+ type: "fail",
+ id: item.id,
+ error: "Transcode failed",
+ }),
+ ).toBe(true);
+ expect(
+ isTerminalUploadQueueAction({
+ type: "progress",
+ id: item.id,
+ progress: 0.25,
+ }),
+ ).toBe(false);
+ expect(
+ isTerminalUploadQueueAction({
+ type: "processing",
+ id: item.id,
+ progress: 0.42,
+ message: "Processing frames",
+ }),
+ ).toBe(false);
+ });
+});
diff --git a/apps/mobile/src/uploads/uploadQueue.ts b/apps/mobile/src/uploads/uploadQueue.ts
new file mode 100644
index 00000000000..5a3162f7596
--- /dev/null
+++ b/apps/mobile/src/uploads/uploadQueue.ts
@@ -0,0 +1,201 @@
+import type { MobileCapSummary } from "@/api/mobile";
+
+export type UploadQueueStatus =
+ | "queued"
+ | "uploading"
+ | "processing"
+ | "failed"
+ | "complete";
+
+export type UploadQueueItem = {
+ id: string;
+ localUri: string;
+ fileName: string;
+ contentType: string;
+ size: number;
+ durationSeconds?: number;
+ width?: number;
+ height?: number;
+ folderId: string | null;
+ organizationId: string | null;
+ status: UploadQueueStatus;
+ progress: number;
+ error: string | null;
+ capId: string | null;
+ rawFileKey: string | null;
+ processingMessage: string | null;
+ createdAt: string;
+ updatedAt: string;
+};
+
+export type UploadQueueAction =
+ | { type: "enqueue"; item: Omit }
+ | { type: "start"; id: string; capId: string; rawFileKey: string }
+ | { type: "progress"; id: string; progress: number }
+ | {
+ type: "processing";
+ id: string;
+ progress?: number;
+ message?: string | null;
+ }
+ | { type: "complete"; id: string }
+ | { type: "fail"; id: string; error: string }
+ | { type: "remove"; id: string }
+ | { type: "retry"; id: string };
+
+export type UploadQueueState = {
+ items: UploadQueueItem[];
+};
+
+const clampProgress = (progress: number) => {
+ if (!Number.isFinite(progress)) return 0;
+ return Math.min(1, Math.max(0, progress));
+};
+
+export const uploadProgressPercent = (progress: number) =>
+ Math.round(clampProgress(progress) * 100);
+
+export const isTerminalUploadQueueAction = (action: UploadQueueAction) =>
+ action.type === "complete" || action.type === "fail";
+
+export const uploadQueueStatusText = (item: UploadQueueItem) => {
+ switch (item.status) {
+ case "queued":
+ return "Queued";
+ case "uploading":
+ return `Uploading ${uploadProgressPercent(item.progress)}%`;
+ case "processing":
+ return item.processingMessage ?? "Finishing up";
+ case "complete":
+ return "Ready to view";
+ case "failed":
+ return "Upload failed";
+ }
+};
+
+export const uploadQueueActionFromCapUpload = (
+ id: string,
+ upload: MobileCapSummary["upload"],
+): UploadQueueAction | null => {
+ if (!upload || upload.phase === "complete") return { type: "complete", id };
+ if (upload.phase === "error") {
+ return {
+ type: "fail",
+ id,
+ error: upload.processingError ?? "Processing failed",
+ };
+ }
+ if (upload.phase === "uploading") {
+ return {
+ type: "progress",
+ id,
+ progress: upload.total > 0 ? upload.uploaded / upload.total : 0,
+ };
+ }
+ return {
+ type: "processing",
+ id,
+ progress: upload.processingProgress / 100,
+ message:
+ upload.processingMessage ??
+ (upload.phase === "processing" ? "Processing" : "Finishing up"),
+ };
+};
+
+const nowIso = () => new Date().toISOString();
+
+const updateItem = (
+ state: UploadQueueState,
+ id: string,
+ update: (item: UploadQueueItem) => UploadQueueItem,
+): UploadQueueState => ({
+ items: state.items.map((item) => (item.id === id ? update(item) : item)),
+});
+
+export const emptyUploadQueue: UploadQueueState = {
+ items: [],
+};
+
+export const uploadQueueReducer = (
+ state: UploadQueueState,
+ action: UploadQueueAction,
+): UploadQueueState => {
+ const updatedAt = nowIso();
+
+ switch (action.type) {
+ case "enqueue":
+ return {
+ items: [
+ ...state.items,
+ {
+ ...action.item,
+ createdAt: updatedAt,
+ updatedAt,
+ },
+ ],
+ };
+ case "start":
+ return updateItem(state, action.id, (item) => ({
+ ...item,
+ status: "uploading",
+ progress: 0,
+ error: null,
+ capId: action.capId,
+ rawFileKey: action.rawFileKey,
+ processingMessage: null,
+ updatedAt,
+ }));
+ case "progress":
+ return updateItem(state, action.id, (item) => ({
+ ...item,
+ status: "uploading",
+ progress: clampProgress(action.progress),
+ error: null,
+ processingMessage: null,
+ updatedAt,
+ }));
+ case "processing":
+ return updateItem(state, action.id, (item) => ({
+ ...item,
+ status: "processing",
+ progress:
+ action.progress !== undefined
+ ? clampProgress(action.progress)
+ : item.progress,
+ processingMessage: action.message ?? null,
+ updatedAt,
+ }));
+ case "complete":
+ return updateItem(state, action.id, (item) => ({
+ ...item,
+ status: "complete",
+ progress: 1,
+ error: null,
+ processingMessage: null,
+ updatedAt,
+ }));
+ case "fail":
+ return updateItem(state, action.id, (item) => ({
+ ...item,
+ status: "failed",
+ error: action.error,
+ processingMessage: null,
+ updatedAt,
+ }));
+ case "remove":
+ return {
+ items: state.items.filter((item) => item.id !== action.id),
+ };
+ case "retry":
+ return updateItem(state, action.id, (item) => ({
+ ...item,
+ status: "queued",
+ progress: 0,
+ error: null,
+ capId: null,
+ rawFileKey: null,
+ processingMessage: null,
+ updatedAt,
+ }));
+ }
+};
diff --git a/apps/mobile/src/utils/format.test.ts b/apps/mobile/src/utils/format.test.ts
new file mode 100644
index 00000000000..1ef39850004
--- /dev/null
+++ b/apps/mobile/src/utils/format.test.ts
@@ -0,0 +1,35 @@
+import { describe, expect, it } from "vitest";
+import { formatDuration, formatFileSize, formatRelativeDate } from "./format";
+
+describe("mobile formatters", () => {
+ it("formats card dates like Cap web", () => {
+ const now = new Date("2026-05-18T11:00:00.000Z");
+
+ expect(formatRelativeDate("2026-05-18T10:30:00.000Z", now)).toBe(
+ "30 minutes ago",
+ );
+ expect(formatRelativeDate("2026-05-18T09:45:00.000Z", now)).toBe(
+ "an hour ago",
+ );
+ expect(formatRelativeDate("2026-05-16T11:00:00.000Z", now)).toBe(
+ "2 days ago",
+ );
+ });
+
+ it("formats card durations like Cap web thumbnails", () => {
+ expect(formatDuration(0)).toBe("< 1 sec");
+ expect(formatDuration(8)).toBe("8 secs");
+ expect(formatDuration(61)).toBe("1 min");
+ expect(formatDuration(125)).toBe("2 mins");
+ expect(formatDuration(7200)).toBe("2 hrs");
+ });
+
+ it("formats native upload file sizes", () => {
+ expect(formatFileSize(null)).toBeNull();
+ expect(formatFileSize(0)).toBeNull();
+ expect(formatFileSize(640)).toBe("640 B");
+ expect(formatFileSize(124_000)).toBe("124 KB");
+ expect(formatFileSize(12_400_000)).toBe("12 MB");
+ expect(formatFileSize(2_300_000_000)).toBe("2 GB");
+ });
+});
diff --git a/apps/mobile/src/utils/format.ts b/apps/mobile/src/utils/format.ts
new file mode 100644
index 00000000000..a8b43469f65
--- /dev/null
+++ b/apps/mobile/src/utils/format.ts
@@ -0,0 +1,51 @@
+export const formatRelativeDate = (input: string, now = new Date()) => {
+ const date = new Date(input);
+ const diffMs = now.getTime() - date.getTime();
+ const diffSeconds = Math.max(0, Math.round(diffMs / 1000));
+ if (diffSeconds < 45) return "a few seconds ago";
+ if (diffSeconds < 90) return "a minute ago";
+
+ const diffMinutes = Math.round(diffSeconds / 60);
+ if (diffMinutes < 45) return `${diffMinutes} minutes ago`;
+ if (diffMinutes < 90) return "an hour ago";
+
+ const diffHours = Math.round(diffMinutes / 60);
+ if (diffHours < 22) return `${diffHours} hours ago`;
+ if (diffHours < 36) return "a day ago";
+
+ const diffDays = Math.round(diffHours / 24);
+ if (diffDays < 26) return `${diffDays} days ago`;
+ if (diffDays < 45) return "a month ago";
+
+ const diffMonths = Math.round(diffDays / 30);
+ if (diffDays < 320) return `${diffMonths} months ago`;
+ if (diffDays < 548) return "a year ago";
+
+ const diffYears = Math.round(diffDays / 365);
+ return `${diffYears} years ago`;
+};
+
+export const formatDuration = (seconds: number | null) => {
+ if (seconds === null || !Number.isFinite(seconds)) return null;
+ const safeSeconds = Math.max(0, Math.ceil(seconds));
+ const hours = Math.floor(safeSeconds / 3600);
+ const minutes = Math.floor(safeSeconds / 60);
+ const remainingSeconds = safeSeconds % 60;
+ if (hours > 0) return `${hours} hr${hours > 1 ? "s" : ""}`;
+ if (minutes > 0) return `${minutes} min${minutes > 1 ? "s" : ""}`;
+ if (remainingSeconds > 0) {
+ return `${remainingSeconds} sec${remainingSeconds === 1 ? "" : "s"}`;
+ }
+ return "< 1 sec";
+};
+
+export const formatFileSize = (bytes: number | null | undefined) => {
+ if (bytes === null || bytes === undefined || !Number.isFinite(bytes)) {
+ return null;
+ }
+ if (bytes <= 0) return null;
+ if (bytes >= 1_000_000_000) return `${Math.round(bytes / 1_000_000_000)} GB`;
+ if (bytes >= 1_000_000) return `${Math.round(bytes / 1_000_000)} MB`;
+ if (bytes >= 1_000) return `${Math.round(bytes / 1_000)} KB`;
+ return `${Math.round(bytes)} B`;
+};
diff --git a/apps/mobile/tsconfig.json b/apps/mobile/tsconfig.json
new file mode 100644
index 00000000000..495df8487cb
--- /dev/null
+++ b/apps/mobile/tsconfig.json
@@ -0,0 +1,22 @@
+{
+ "extends": "expo/tsconfig.base",
+ "compilerOptions": {
+ "strict": true,
+ "allowImportingTsExtensions": true,
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["src/*"],
+ "@modules/*": ["modules/*"]
+ },
+ "types": ["vitest/globals"]
+ },
+ "include": [
+ "app",
+ "src",
+ "modules",
+ "plugins",
+ "expo-env.d.ts",
+ "*.js",
+ ".expo/types/**/*.ts"
+ ]
+}
diff --git a/apps/mobile/vitest.config.ts b/apps/mobile/vitest.config.ts
new file mode 100644
index 00000000000..d60d49c3b9c
--- /dev/null
+++ b/apps/mobile/vitest.config.ts
@@ -0,0 +1,14 @@
+import { fileURLToPath } from "node:url";
+import { defineConfig } from "vitest/config";
+
+export default defineConfig({
+ esbuild: {
+ jsx: "automatic",
+ jsxImportSource: "react",
+ },
+ resolve: {
+ alias: {
+ "@": fileURLToPath(new URL("./src", import.meta.url)),
+ },
+ },
+});
diff --git a/apps/web/__tests__/unit/mobile-api-contract.test.ts b/apps/web/__tests__/unit/mobile-api-contract.test.ts
new file mode 100644
index 00000000000..c336c187c1a
--- /dev/null
+++ b/apps/web/__tests__/unit/mobile-api-contract.test.ts
@@ -0,0 +1,174 @@
+import { Folder, Mobile, Organisation, User, Video } from "@cap/web-domain";
+import { Schema } from "effect";
+import { describe, expect, it } from "vitest";
+
+describe("mobile API contract schemas", () => {
+ it("decodes bootstrap responses without exposing database rows", () => {
+ const decoded = Schema.decodeUnknownSync(Mobile.MobileBootstrapResponse)({
+ user: {
+ id: User.UserId.make("user_123"),
+ name: "Richie",
+ email: "richie@example.com",
+ imageUrl: null,
+ activeOrganizationId: Organisation.OrganisationId.make("org_123"),
+ },
+ organizations: [
+ {
+ id: Organisation.OrganisationId.make("org_123"),
+ name: "Cap",
+ iconUrl: null,
+ role: "owner",
+ },
+ ],
+ activeOrganizationId: Organisation.OrganisationId.make("org_123"),
+ rootFolders: [
+ {
+ id: Folder.FolderId.make("folder_123"),
+ name: "Product",
+ color: "blue",
+ parentId: null,
+ videoCount: 4,
+ },
+ ],
+ });
+
+ expect(decoded.user.email).toBe("richie@example.com");
+ expect(decoded.rootFolders[0]?.videoCount).toBe(4);
+ });
+
+ it("decodes auth provider availability", () => {
+ const decoded = Schema.decodeUnknownSync(Mobile.MobileAuthConfigResponse)({
+ googleAuthAvailable: true,
+ workosAuthAvailable: false,
+ });
+
+ expect(decoded.googleAuthAvailable).toBe(true);
+ expect(decoded.workosAuthAvailable).toBe(false);
+ });
+
+ it("accepts Google and WorkOS mobile session providers", () => {
+ expect(
+ Schema.decodeUnknownSync(Mobile.MobileSessionRequestParams)({
+ redirectUri: "cap://auth",
+ provider: "google",
+ }).provider,
+ ).toBe("google");
+ expect(
+ Schema.decodeUnknownSync(Mobile.MobileSessionRequestParams)({
+ redirectUri: "cap://auth",
+ provider: "workos",
+ organizationId: "org_123",
+ }).organizationId,
+ ).toBe("org_123");
+ });
+
+ it("decodes Cap sharing visibility updates", () => {
+ const decoded = Schema.decodeUnknownSync(Mobile.MobileCapSharingInput)({
+ public: false,
+ });
+
+ expect(decoded.public).toBe(false);
+ });
+
+ it("decodes Cap title updates", () => {
+ const decoded = Schema.decodeUnknownSync(Mobile.MobileCapTitleInput)({
+ title: "Roadmap review",
+ });
+
+ expect(decoded.title).toBe("Roadmap review");
+ });
+
+ it("decodes Cap password updates", () => {
+ expect(
+ Schema.decodeUnknownSync(Mobile.MobileCapPasswordInput)({
+ password: "secret",
+ }).password,
+ ).toBe("secret");
+ expect(
+ Schema.decodeUnknownSync(Mobile.MobileCapPasswordInput)({
+ password: null,
+ }).password,
+ ).toBeNull();
+ });
+
+ it("decodes mobile folder creation inputs", () => {
+ const decoded = Schema.decodeUnknownSync(Mobile.MobileFolderCreateInput)({
+ name: "Product",
+ color: "blue",
+ });
+
+ expect(decoded).toEqual({
+ name: "Product",
+ color: "blue",
+ });
+ });
+
+ it("requires mobile caps dates to be serialized strings", () => {
+ expect(() =>
+ Schema.decodeUnknownSync(Mobile.MobileCapSummary)({
+ id: Video.VideoId.make("video_123"),
+ shareUrl: "https://cap.so/s/video_123",
+ title: "Launch review",
+ createdAt: new Date("2026-05-18T10:00:00.000Z"),
+ updatedAt: "2026-05-18T10:30:00.000Z",
+ ownerName: "Richie",
+ durationSeconds: 125,
+ thumbnailUrl: null,
+ folderId: null,
+ public: true,
+ protected: false,
+ viewCount: 7,
+ commentCount: 2,
+ reactionCount: 3,
+ upload: null,
+ }),
+ ).toThrow();
+ });
+
+ it("decodes signed playback and upload targets", () => {
+ const playback = Schema.decodeUnknownSync(Mobile.MobilePlaybackResponse)({
+ kind: "mp4",
+ url: "https://signed.example/video.mp4",
+ transcriptUrl: "https://signed.example/transcript.vtt",
+ });
+ const upload = Schema.decodeUnknownSync(Mobile.MobileUploadCreateResponse)({
+ id: Video.VideoId.make("video_123"),
+ shareUrl: "https://cap.so/s/video_123",
+ rawFileKey: "user_123/video_123/raw-upload.mp4",
+ upload: {
+ type: "put",
+ url: "https://signed.example/upload",
+ headers: {
+ "Content-Type": "video/mp4",
+ },
+ },
+ cap: {
+ id: Video.VideoId.make("video_123"),
+ shareUrl: "https://cap.so/s/video_123",
+ title: "Launch review",
+ createdAt: "2026-05-18T10:00:00.000Z",
+ updatedAt: "2026-05-18T10:30:00.000Z",
+ ownerName: "Richie",
+ durationSeconds: null,
+ thumbnailUrl: null,
+ folderId: null,
+ public: true,
+ protected: false,
+ viewCount: 0,
+ commentCount: 0,
+ reactionCount: 0,
+ upload: {
+ uploaded: 0,
+ total: 0,
+ phase: "uploading",
+ processingProgress: 0,
+ processingMessage: null,
+ processingError: null,
+ },
+ },
+ });
+
+ expect(playback.url).toContain("signed.example");
+ expect(upload.upload.type).toBe("put");
+ });
+});
diff --git a/apps/web/app/(org)/login/form.tsx b/apps/web/app/(org)/login/form.tsx
index 50ffff2733f..5d1b81860d7 100644
--- a/apps/web/app/(org)/login/form.tsx
+++ b/apps/web/app/(org)/login/form.tsx
@@ -15,7 +15,14 @@ import Image from "next/image";
import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation";
import { signIn } from "next-auth/react";
-import { Suspense, useEffect, useState } from "react";
+import {
+ Suspense,
+ useCallback,
+ useEffect,
+ useId,
+ useRef,
+ useState,
+} from "react";
import { toast } from "sonner";
import { getOrganizationSSOData } from "@/actions/organization/get-organization-sso-data";
import { trackEvent } from "@/app/utils/analytics";
@@ -40,13 +47,11 @@ export function LoginForm() {
const [lastEmailSentTime, setLastEmailSentTime] = useState(
null,
);
+ const mobileGoogleSignInStarted = useRef(false);
const theme = Cookies.get("theme") || "light";
useEffect(() => {
- theme === "dark"
- ? (document.body.className = "dark")
- : (document.body.className = "light");
- //remove the dark mode when we leave the dashboard
+ document.body.className = theme === "dark" ? "dark" : "light";
return () => {
document.body.className = "light";
};
@@ -104,7 +109,7 @@ export function LoginForm() {
}
}, [emailSent]);
- const handleGoogleSignIn = () => {
+ const handleGoogleSignIn = useCallback(() => {
trackEvent("auth_started", {
method: "google",
is_signup: false,
@@ -113,7 +118,53 @@ export function LoginForm() {
signIn("google", {
...(next && next.length > 0 ? { callbackUrl: next } : {}),
});
- };
+ }, [next]);
+
+ const handleWorkosSignIn = useCallback(
+ async (orgId: string) => {
+ const data = await getOrganizationSSOData(
+ Organisation.OrganisationId.make(orgId),
+ );
+ setOrganizationName(data.name);
+
+ signIn(
+ "workos",
+ next && next.length > 0 ? { callbackUrl: next } : undefined,
+ {
+ organization: data.organizationId,
+ connection: data.connectionId,
+ },
+ );
+ },
+ [next],
+ );
+
+ useEffect(() => {
+ if (searchParams?.get("mobileProvider") === "google") {
+ if (mobileGoogleSignInStarted.current) return;
+ mobileGoogleSignInStarted.current = true;
+ handleGoogleSignIn();
+ return;
+ }
+
+ if (searchParams?.get("mobileProvider") !== "workos") return;
+ const mobileOrganizationId = searchParams.get("organizationId");
+ if (!mobileOrganizationId) {
+ setShowOrgInput(true);
+ return;
+ }
+
+ let active = true;
+ handleWorkosSignIn(mobileOrganizationId).catch(() => {
+ if (!active) return;
+ setOrganizationId(mobileOrganizationId);
+ setShowOrgInput(true);
+ toast.error("Organization not found or SSO not configured");
+ });
+ return () => {
+ active = false;
+ };
+ }, [handleGoogleSignIn, handleWorkosSignIn, searchParams]);
const handleOrganizationLookup = async (e: React.FormEvent) => {
e.preventDefault();
@@ -123,15 +174,7 @@ export function LoginForm() {
}
try {
- const data = await getOrganizationSSOData(
- Organisation.OrganisationId.make(organizationId),
- );
- setOrganizationName(data.name);
-
- signIn("workos", undefined, {
- organization: data.organizationId,
- connection: data.connectionId,
- });
+ await handleWorkosSignIn(organizationId);
} catch (error) {
console.error("Lookup Error:", error);
toast.error("Organization not found or SSO not configured");
@@ -372,6 +415,8 @@ const LoginWithSSO = ({
setOrganizationId: (organizationId: string) => void;
organizationName: string | null;
}) => {
+ const organizationIdInputId = useId();
+
return (
setOrganizationId(e.target.value)}
@@ -415,12 +460,13 @@ const NormalLogin = ({
handleGoogleSignIn: () => void;
}) => {
const publicEnv = usePublicEnv();
+ const emailInputId = useId();
return (
value.toISOString();
+
+const normalizeEmail = (email: string) => email.trim().toLowerCase();
+
+const getAffectedRows = (result: unknown) => {
+ if (Array.isArray(result)) {
+ return (
+ (result[0] as { affectedRows?: number } | undefined)?.affectedRows ?? 0
+ );
+ }
+
+ return (result as { affectedRows?: number } | undefined)?.affectedRows ?? 0;
+};
+
+const hashEmailCode = (code: string) =>
+ crypto
+ .createHash("sha256")
+ .update(`${code}${serverEnv().NEXTAUTH_SECRET}`)
+ .digest("hex");
+
+const sendMobileEmailCode = async (email: string, code: string) => {
+ if (!serverEnv().RESEND_API_KEY) {
+ if (process.env.NODE_ENV === "production") {
+ throw new Error("RESEND_API_KEY is required to send mobile email codes");
+ }
+ console.log("");
+ console.log("Cap mobile verification code");
+ console.log(`Email: ${email}`);
+ console.log(`Code: ${code}`);
+ console.log("Expires in: 10 minutes");
+ console.log("");
+ return;
+ }
+
+ await sendEmail({
+ email,
+ subject: "Your Cap Verification Code",
+ react: OTPEmail({ code, email }),
+ });
+};
+
+const getMobileRedirectUrl = (redirectUri: string) => {
+ try {
+ const redirectUrl = new URL(redirectUri);
+ if (!mobileRedirectProtocols.has(redirectUrl.protocol)) return null;
+ return redirectUrl;
+ } catch {
+ return null;
+ }
+};
+
+const getEmailAuthAdapter = () => {
+ const adapter = authOptions().adapter;
+ const { createUser, getUserByEmail, updateUser } = adapter ?? {};
+
+ if (!createUser || !getUserByEmail || !updateUser) {
+ throw new Error("Email auth adapter is not configured");
+ }
+
+ return { createUser, getUserByEmail, updateUser };
+};
+
+const createOrUpdateEmailUser = async (email: string) => {
+ const { createUser, getUserByEmail, updateUser } = getEmailAuthAdapter();
+ const existingUser = await getUserByEmail(email);
+
+ if (existingUser) {
+ return updateUser({
+ id: existingUser.id,
+ emailVerified: new Date(),
+ });
+ }
+
+ return createUser({
+ email,
+ emailVerified: new Date(),
+ image: null,
+ name: null,
+ });
+};
+
+const parseBearerToken = (authorization: string | undefined) => {
+ if (!authorization) return null;
+ const [scheme, token] = authorization.split(" ");
+ if (scheme?.toLowerCase() !== "bearer" || !token) return null;
+ return token;
+};
+
+const parsePositiveInteger = (
+ value: string | undefined,
+ fallback: number,
+ max: number,
+) => {
+ const parsed = Number(value);
+ if (!Number.isFinite(parsed) || parsed < 1) return fallback;
+ return Math.min(Math.trunc(parsed), max);
+};
+
+const getMetadataRecord = (metadata: unknown): Record => {
+ if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
+ return {};
+ }
+ return metadata as Record;
+};
+
+const getMetadataString = (metadata: Record, key: string) => {
+ const value = metadata[key];
+ return typeof value === "string" && value.length > 0 ? value : null;
+};
+
+const getMetadataChapters = (metadata: Record) => {
+ const chapters = metadata.chapters;
+ if (!Array.isArray(chapters)) return [];
+
+ return chapters.flatMap((chapter) => {
+ if (!chapter || typeof chapter !== "object" || Array.isArray(chapter)) {
+ return [];
+ }
+ const value = chapter as Record;
+ const title = value.title;
+ const start = value.start;
+ if (typeof title !== "string" || typeof start !== "number") return [];
+ return [{ title, start }];
+ });
+};
+
+const getDeploymentOrigin = () => {
+ const webUrl = serverEnv().WEB_URL;
+ const vercelEnv = serverEnv().VERCEL_ENV;
+
+ if (!vercelEnv || vercelEnv === "production") return webUrl;
+
+ if (vercelEnv === "preview") {
+ const branchHost = serverEnv().VERCEL_BRANCH_URL_HOST;
+ if (branchHost?.endsWith(".vercel.app")) return `https://${branchHost}`;
+ }
+
+ return webUrl;
+};
+
+const getFileExtension = (input: MobileUploadCreateInput) => {
+ const fileNameExtension = input.fileName.split(".").at(-1)?.toLowerCase();
+ if (
+ fileNameExtension &&
+ fileNameExtension !== input.fileName.toLowerCase() &&
+ /^[a-z0-9]+$/.test(fileNameExtension)
+ ) {
+ return fileNameExtension;
+ }
+
+ if (input.contentType.includes("quicktime")) return "mov";
+ if (input.contentType.includes("webm")) return "webm";
+ if (input.contentType.includes("matroska")) return "mkv";
+ if (input.contentType.includes("x-msvideo")) return "avi";
+ if (input.contentType.includes("x-m4v")) return "m4v";
+ return "mp4";
+};
+
+const getUploadTitle = (fileName: string) => {
+ const title = fileName.replace(/\.[^/.]+$/, "").trim();
+ return title.length > 0 ? title : "Mobile Upload";
+};
+
+const toMobileCapSummary = (
+ row: CapRow,
+ thumbnailUrl: string | null,
+ viewCount: number,
+): MobileCapSummary => ({
+ id: row.id,
+ shareUrl: `${serverEnv().WEB_URL}/s/${row.id}`,
+ title: row.name,
+ createdAt: toIsoString(row.createdAt),
+ updatedAt: toIsoString(row.updatedAt),
+ ownerName: row.ownerName ?? "",
+ durationSeconds: row.duration,
+ thumbnailUrl,
+ folderId: row.folderId,
+ public: row.public,
+ protected: row.hasPassword,
+ viewCount,
+ commentCount: Number(row.commentCount),
+ reactionCount: Number(row.reactionCount),
+ upload: row.uploadVideoId
+ ? {
+ uploaded: Number(row.uploadUploaded ?? 0),
+ total: Number(row.uploadTotal ?? 0),
+ phase: row.uploadPhase ?? "uploading",
+ processingProgress: Number(row.processingProgress ?? 0),
+ processingMessage: row.processingMessage,
+ processingError: row.processingError,
+ }
+ : null,
+});
+
+const withMappedErrors = (effect: Effect.Effect) =>
+ effect.pipe(
+ Effect.catchTags({
+ DatabaseError: () => new HttpApiError.InternalServerError(),
+ NoSuchElementException: () => new HttpApiError.NotFound(),
+ PolicyDenied: () => new HttpApiError.Forbidden(),
+ S3Error: () => new HttpApiError.InternalServerError(),
+ StorageError: () => new HttpApiError.InternalServerError(),
+ UnknownException: () => new HttpApiError.InternalServerError(),
+ VerifyVideoPasswordError: () => new HttpApiError.Forbidden(),
+ VideoNotFoundError: () => new HttpApiError.NotFound(),
+ }),
+ );
+
+const ensureEmailSignInAllowed = Effect.fn("Mobile.ensureEmailSignInAllowed")(
+ function* (email: string) {
+ if (!emailPattern.test(email)) {
+ return yield* Effect.fail(new HttpApiError.BadRequest());
+ }
+
+ const allowedDomains = serverEnv().CAP_ALLOWED_SIGNUP_DOMAINS;
+ if (!allowedDomains) return;
+
+ const database = yield* Database;
+ const [existingUser] = yield* database.use((db) =>
+ db
+ .select({ id: Db.users.id })
+ .from(Db.users)
+ .where(eq(Db.users.email, email))
+ .limit(1),
+ );
+
+ if (!existingUser && !isEmailAllowedForSignup(email, allowedDomains)) {
+ return yield* Effect.fail(new HttpApiError.Forbidden());
+ }
+ },
+);
+
+const createMobileApiKey = Effect.fn("Mobile.createMobileApiKey")(function* (
+ userId: User.UserId,
+) {
+ const database = yield* Database;
+ const apiKey = crypto.randomUUID();
+ yield* database.use((db) =>
+ db.insert(Db.authApiKeys).values({
+ id: apiKey,
+ userId,
+ }),
+ );
+
+ return {
+ type: "api_key" as const,
+ apiKey,
+ userId,
+ };
+});
+
+const requestEmailSession = Effect.fn("Mobile.requestEmailSession")(function* (
+ rawEmail: string,
+) {
+ const email = normalizeEmail(rawEmail);
+ yield* ensureEmailSignInAllowed(email);
+
+ const code = crypto.randomInt(100000, 1000000).toString();
+ const token = hashEmailCode(code);
+ const expires = new Date(Date.now() + emailCodeTtlMs);
+ const database = yield* Database;
+
+ yield* database.use(async (db) => {
+ const [existingToken] = await db
+ .select({ identifier: Db.verificationTokens.identifier })
+ .from(Db.verificationTokens)
+ .where(eq(Db.verificationTokens.identifier, email))
+ .limit(1);
+
+ if (existingToken) {
+ await db
+ .update(Db.verificationTokens)
+ .set({ token, expires })
+ .where(eq(Db.verificationTokens.identifier, email));
+ return;
+ }
+
+ await db.insert(Db.verificationTokens).values({
+ identifier: email,
+ token,
+ expires,
+ });
+ });
+
+ yield* Effect.tryPromise({
+ try: () => sendMobileEmailCode(email, code),
+ catch: () => new HttpApiError.InternalServerError(),
+ });
+
+ return { success: true as const };
+});
+
+const verifyEmailSession = Effect.fn("Mobile.verifyEmailSession")(function* ({
+ email: rawEmail,
+ code: rawCode,
+}: (typeof Mobile.MobileEmailSessionVerifyInput)["Type"]) {
+ const email = normalizeEmail(rawEmail);
+ const code = rawCode.trim();
+
+ if (!emailPattern.test(email) || !emailCodePattern.test(code)) {
+ return yield* Effect.fail(new HttpApiError.BadRequest());
+ }
+
+ yield* ensureEmailSignInAllowed(email);
+
+ const database = yield* Database;
+ const token = hashEmailCode(code);
+ const verificationStatus = yield* database.use(async (db) => {
+ const [verificationToken] = await db
+ .select()
+ .from(Db.verificationTokens)
+ .where(eq(Db.verificationTokens.identifier, email))
+ .limit(1);
+
+ if (!verificationToken) return "missing" as const;
+
+ if (verificationToken.expires.valueOf() < Date.now()) {
+ await db
+ .delete(Db.verificationTokens)
+ .where(eq(Db.verificationTokens.identifier, email));
+ return "expired" as const;
+ }
+
+ if (verificationToken.token !== token) {
+ await db
+ .delete(Db.verificationTokens)
+ .where(eq(Db.verificationTokens.identifier, email));
+ return "invalid" as const;
+ }
+
+ const result = await db
+ .delete(Db.verificationTokens)
+ .where(
+ and(
+ eq(Db.verificationTokens.identifier, email),
+ eq(Db.verificationTokens.token, token),
+ ),
+ );
+
+ return getAffectedRows(result) === 1
+ ? ("verified" as const)
+ : ("used" as const);
+ });
+
+ if (verificationStatus !== "verified") {
+ return yield* Effect.fail(new HttpApiError.Forbidden());
+ }
+
+ const user = yield* Effect.tryPromise({
+ try: () => createOrUpdateEmailUser(email),
+ catch: () => new HttpApiError.InternalServerError(),
+ });
+
+ return yield* createMobileApiKey(User.UserId.make(user.id));
+});
+
+const getAccessibleOrganizations = Effect.fn(
+ "Mobile.getAccessibleOrganizations",
+)(function* (userId: User.UserId) {
+ const database = yield* Database;
+ const imageUploads = yield* ImageUploads;
+
+ const rows = yield* database.use((db) =>
+ db
+ .select({
+ id: Db.organizations.id,
+ name: Db.organizations.name,
+ ownerId: Db.organizations.ownerId,
+ iconUrl: Db.organizations.iconUrl,
+ role: Db.organizationMembers.role,
+ })
+ .from(Db.organizations)
+ .leftJoin(
+ Db.organizationMembers,
+ and(
+ eq(Db.organizationMembers.organizationId, Db.organizations.id),
+ eq(Db.organizationMembers.userId, userId),
+ ),
+ )
+ .where(
+ and(
+ isNull(Db.organizations.tombstoneAt),
+ or(
+ eq(Db.organizations.ownerId, userId),
+ eq(Db.organizationMembers.userId, userId),
+ ),
+ ),
+ ),
+ );
+
+ return yield* Effect.forEach(
+ rows,
+ (row) =>
+ Effect.gen(function* () {
+ const role: MobileOrganization["role"] =
+ row.ownerId === userId ? "owner" : (row.role ?? "member");
+ const iconUrl = row.iconUrl
+ ? yield* imageUploads.resolveImageUrl(row.iconUrl)
+ : null;
+
+ return {
+ id: row.id,
+ name: row.name,
+ iconUrl,
+ role,
+ };
+ }),
+ { concurrency: 5 },
+ );
+});
+
+const getRootFolders = Effect.fn("Mobile.getRootFolders")(function* (
+ organizationId: Organisation.OrganisationId,
+) {
+ const user = yield* CurrentUser;
+ const database = yield* Database;
+
+ const rows = yield* database.use((db) =>
+ db
+ .select({
+ id: Db.folders.id,
+ name: Db.folders.name,
+ color: Db.folders.color,
+ parentId: Db.folders.parentId,
+ videoCount: sql`(
+ SELECT COUNT(*)
+ FROM ${Db.videos}
+ WHERE ${Db.videos.folderId} = ${Db.folders.id}
+ AND ${Db.videos.ownerId} = ${user.id}
+ AND ${Db.videos.orgId} = ${organizationId}
+ )`,
+ })
+ .from(Db.folders)
+ .where(
+ and(
+ eq(Db.folders.organizationId, organizationId),
+ eq(Db.folders.createdById, user.id),
+ isNull(Db.folders.parentId),
+ isNull(Db.folders.spaceId),
+ ),
+ ),
+ );
+
+ return rows satisfies MobileFolder[];
+});
+
+const assertOrganizationAccess = Effect.fn("Mobile.assertOrganizationAccess")(
+ function* (organizationId: Organisation.OrganisationId) {
+ const user = yield* CurrentUser;
+ const organizations = yield* getAccessibleOrganizations(user.id);
+ const hasAccess = organizations.some((org) => org.id === organizationId);
+ if (!hasAccess) return yield* Effect.fail(new HttpApiError.Forbidden());
+ },
+);
+
+const getBootstrap = Effect.fn("Mobile.getBootstrap")(function* () {
+ const user = yield* CurrentUser;
+ const database = yield* Database;
+ const imageUploads = yield* ImageUploads;
+
+ const [userRow] = yield* database.use((db) =>
+ db
+ .select({
+ id: Db.users.id,
+ name: Db.users.name,
+ email: Db.users.email,
+ image: Db.users.image,
+ activeOrganizationId: Db.users.activeOrganizationId,
+ })
+ .from(Db.users)
+ .where(eq(Db.users.id, user.id)),
+ );
+ if (!userRow) return yield* Effect.fail(new HttpApiError.Unauthorized());
+
+ const organizations = yield* getAccessibleOrganizations(user.id);
+ const activeOrganization =
+ organizations.find((org) => org.id === userRow.activeOrganizationId) ??
+ organizations[0] ??
+ null;
+ const activeOrganizationId = activeOrganization?.id ?? null;
+ const rootFolders = activeOrganizationId
+ ? yield* getRootFolders(activeOrganizationId)
+ : [];
+ const imageUrl = userRow.image
+ ? yield* imageUploads.resolveImageUrl(userRow.image)
+ : null;
+
+ return {
+ user: {
+ id: userRow.id,
+ name: userRow.name,
+ email: userRow.email,
+ imageUrl,
+ activeOrganizationId: activeOrganizationId ?? user.activeOrganizationId,
+ },
+ organizations,
+ activeOrganizationId,
+ rootFolders,
+ };
+});
+
+const getCapRows = Effect.fn("Mobile.getCapRows")(function* ({
+ folderId,
+ page,
+ limit,
+}: {
+ folderId: Folder.FolderId | null;
+ page: number;
+ limit: number;
+}) {
+ const user = yield* CurrentUser;
+ const database = yield* Database;
+ const offset = (page - 1) * limit;
+ const folderFilter = folderId
+ ? eq(Db.videos.folderId, folderId)
+ : isNull(Db.videos.folderId);
+ const whereClause = and(
+ eq(Db.videos.ownerId, user.id),
+ eq(Db.videos.orgId, user.activeOrganizationId),
+ folderFilter,
+ isNull(Db.organizations.tombstoneAt),
+ );
+
+ const [totalRow] = yield* database.use((db) =>
+ db
+ .select({ value: count() })
+ .from(Db.videos)
+ .leftJoin(Db.organizations, eq(Db.videos.orgId, Db.organizations.id))
+ .where(whereClause),
+ );
+
+ const rows = yield* database.use((db) =>
+ db
+ .select({
+ id: Db.videos.id,
+ name: Db.videos.name,
+ createdAt: Db.videos.createdAt,
+ updatedAt: Db.videos.updatedAt,
+ ownerName: Db.users.name,
+ duration: Db.videos.duration,
+ folderId: Db.videos.folderId,
+ public: Db.videos.public,
+ hasPassword: sql`${Db.videos.password} IS NOT NULL`.mapWith(
+ Boolean,
+ ),
+ commentCount: sql`COUNT(DISTINCT CASE WHEN ${Db.comments.type} = 'text' THEN ${Db.comments.id} END)`,
+ reactionCount: sql`COUNT(DISTINCT CASE WHEN ${Db.comments.type} = 'emoji' THEN ${Db.comments.id} END)`,
+ uploadVideoId: Db.videoUploads.videoId,
+ uploadUploaded: Db.videoUploads.uploaded,
+ uploadTotal: Db.videoUploads.total,
+ uploadPhase: Db.videoUploads.phase,
+ processingProgress: Db.videoUploads.processingProgress,
+ processingMessage: Db.videoUploads.processingMessage,
+ processingError: Db.videoUploads.processingError,
+ metadata: Db.videos.metadata,
+ transcriptionStatus: Db.videos.transcriptionStatus,
+ })
+ .from(Db.videos)
+ .leftJoin(Db.comments, eq(Db.videos.id, Db.comments.videoId))
+ .leftJoin(Db.users, eq(Db.videos.ownerId, Db.users.id))
+ .leftJoin(Db.videoUploads, eq(Db.videos.id, Db.videoUploads.videoId))
+ .leftJoin(Db.organizations, eq(Db.videos.orgId, Db.organizations.id))
+ .where(whereClause)
+ .groupBy(
+ Db.videos.id,
+ Db.videos.name,
+ Db.videos.createdAt,
+ Db.videos.updatedAt,
+ Db.users.name,
+ Db.videos.duration,
+ Db.videos.folderId,
+ Db.videos.public,
+ Db.videos.password,
+ Db.videoUploads.videoId,
+ Db.videoUploads.uploaded,
+ Db.videoUploads.total,
+ Db.videoUploads.phase,
+ Db.videoUploads.processingProgress,
+ Db.videoUploads.processingMessage,
+ Db.videoUploads.processingError,
+ Db.videos.metadata,
+ Db.videos.transcriptionStatus,
+ )
+ .orderBy(desc(Db.videos.effectiveCreatedAt))
+ .limit(limit)
+ .offset(offset),
+ );
+
+ return { rows, total: totalRow?.value ?? 0 };
+});
+
+const getCapsList = Effect.fn("Mobile.getCapsList")(function* (
+ params: (typeof Mobile.MobileCapsListParams)["Type"],
+) {
+ const page = parsePositiveInteger(params.page, 1, 10_000);
+ const limit = parsePositiveInteger(params.limit, 20, 50);
+ const folderId = params.folderId
+ ? Folder.FolderId.make(params.folderId)
+ : null;
+ const videos = yield* Videos;
+ const user = yield* CurrentUser;
+
+ const [{ rows, total }, folders] = yield* Effect.all([
+ getCapRows({ folderId, page, limit }),
+ folderId ? Effect.succeed([]) : getRootFolders(user.activeOrganizationId),
+ ]);
+ const analyticsExits = yield* videos
+ .getAnalyticsBulk(rows.map((row) => row.id))
+ .pipe(Effect.catchAll(() => Effect.succeed([])));
+ const viewCounts = new Map();
+
+ rows.forEach((row, index) => {
+ const result = analyticsExits[index];
+ viewCounts.set(
+ row.id,
+ result && Exit.isSuccess(result) ? result.value.count : 0,
+ );
+ });
+
+ const caps = yield* Effect.forEach(
+ rows,
+ (row) =>
+ videos.getThumbnailURL(row.id).pipe(
+ Effect.map(Option.getOrNull),
+ Effect.catchAll(() => Effect.succeed(null)),
+ Effect.map((thumbnailUrl) =>
+ toMobileCapSummary(row, thumbnailUrl, viewCounts.get(row.id) ?? 0),
+ ),
+ ),
+ { concurrency: 5 },
+ );
+
+ return {
+ folders,
+ caps,
+ page,
+ limit,
+ total,
+ hasMore: page * limit < total,
+ };
+});
+
+const createMobileFolder = Effect.fn("Mobile.createFolder")(function* (
+ input: MobileFolderCreateInput,
+) {
+ const user = yield* CurrentUser;
+ const name = input.name.trim();
+ if (!name) return yield* Effect.fail(new HttpApiError.BadRequest());
+
+ const organizationId = user.activeOrganizationId;
+ yield* assertOrganizationAccess(organizationId);
+
+ const color = input.color ?? "normal";
+ const id = Folder.FolderId.make(nanoId());
+ const database = yield* Database;
+
+ yield* database.use((db) =>
+ db.insert(Db.folders).values({
+ id,
+ name,
+ color,
+ organizationId,
+ createdById: user.id,
+ parentId: null,
+ spaceId: null,
+ }),
+ );
+
+ yield* Effect.sync(() => {
+ revalidatePath("/dashboard/caps");
+ });
+
+ return {
+ id,
+ name,
+ color,
+ parentId: null,
+ videoCount: 0,
+ };
+});
+
+const getCapById = Effect.fn("Mobile.getCapById")(function* (
+ videoId: Video.VideoId,
+) {
+ const user = yield* CurrentUser;
+ const database = yield* Database;
+ const videos = yield* Videos;
+
+ const [row] = yield* database.use((db) =>
+ db
+ .select({
+ id: Db.videos.id,
+ name: Db.videos.name,
+ createdAt: Db.videos.createdAt,
+ updatedAt: Db.videos.updatedAt,
+ ownerName: Db.users.name,
+ duration: Db.videos.duration,
+ folderId: Db.videos.folderId,
+ public: Db.videos.public,
+ hasPassword: sql`${Db.videos.password} IS NOT NULL`.mapWith(
+ Boolean,
+ ),
+ commentCount: sql`COUNT(DISTINCT CASE WHEN ${Db.comments.type} = 'text' THEN ${Db.comments.id} END)`,
+ reactionCount: sql`COUNT(DISTINCT CASE WHEN ${Db.comments.type} = 'emoji' THEN ${Db.comments.id} END)`,
+ uploadVideoId: Db.videoUploads.videoId,
+ uploadUploaded: Db.videoUploads.uploaded,
+ uploadTotal: Db.videoUploads.total,
+ uploadPhase: Db.videoUploads.phase,
+ processingProgress: Db.videoUploads.processingProgress,
+ processingMessage: Db.videoUploads.processingMessage,
+ processingError: Db.videoUploads.processingError,
+ metadata: Db.videos.metadata,
+ transcriptionStatus: Db.videos.transcriptionStatus,
+ })
+ .from(Db.videos)
+ .leftJoin(Db.comments, eq(Db.videos.id, Db.comments.videoId))
+ .leftJoin(Db.users, eq(Db.videos.ownerId, Db.users.id))
+ .leftJoin(Db.videoUploads, eq(Db.videos.id, Db.videoUploads.videoId))
+ .where(and(eq(Db.videos.id, videoId), eq(Db.videos.ownerId, user.id)))
+ .groupBy(
+ Db.videos.id,
+ Db.videos.name,
+ Db.videos.createdAt,
+ Db.videos.updatedAt,
+ Db.users.name,
+ Db.videos.duration,
+ Db.videos.folderId,
+ Db.videos.public,
+ Db.videos.password,
+ Db.videoUploads.videoId,
+ Db.videoUploads.uploaded,
+ Db.videoUploads.total,
+ Db.videoUploads.phase,
+ Db.videoUploads.processingProgress,
+ Db.videoUploads.processingMessage,
+ Db.videoUploads.processingError,
+ Db.videos.metadata,
+ Db.videos.transcriptionStatus,
+ ),
+ );
+
+ if (!row) return yield* Effect.fail(new HttpApiError.NotFound());
+
+ const thumbnailUrl = yield* videos.getThumbnailURL(row.id).pipe(
+ Effect.map(Option.getOrNull),
+ Effect.catchAll(() => Effect.succeed(null)),
+ );
+ const analytics = yield* videos.getAnalytics(row.id).pipe(
+ Effect.map((result) => result.count),
+ Effect.catchAll(() => Effect.succeed(0)),
+ );
+
+ return { row, cap: toMobileCapSummary(row, thumbnailUrl, analytics) };
+});
+
+const getComments = Effect.fn("Mobile.getComments")(function* (
+ videoId: Video.VideoId,
+) {
+ const database = yield* Database;
+ const imageUploads = yield* ImageUploads;
+
+ const rows = yield* database.use((db) =>
+ db
+ .select({
+ id: Db.comments.id,
+ videoId: Db.comments.videoId,
+ type: Db.comments.type,
+ content: Db.comments.content,
+ timestamp: Db.comments.timestamp,
+ parentCommentId: Db.comments.parentCommentId,
+ createdAt: Db.comments.createdAt,
+ updatedAt: Db.comments.updatedAt,
+ authorId: Db.comments.authorId,
+ authorName: Db.users.name,
+ authorImage: Db.users.image,
+ })
+ .from(Db.comments)
+ .leftJoin(Db.users, eq(Db.comments.authorId, Db.users.id))
+ .where(eq(Db.comments.videoId, videoId))
+ .orderBy(Db.comments.createdAt),
+ );
+
+ return yield* Effect.forEach(
+ rows,
+ (row) =>
+ Effect.gen(function* () {
+ const imageUrl = row.authorImage
+ ? yield* imageUploads
+ .resolveImageUrl(row.authorImage)
+ .pipe(Effect.catchAll(() => Effect.succeed(null)))
+ : null;
+
+ return {
+ id: row.id,
+ videoId: row.videoId,
+ type: row.type,
+ content: row.content,
+ timestamp: row.timestamp,
+ parentCommentId: row.parentCommentId,
+ createdAt: toIsoString(row.createdAt),
+ updatedAt: toIsoString(row.updatedAt),
+ author: {
+ id: row.authorId,
+ name: row.authorName,
+ imageUrl,
+ },
+ };
+ }),
+ { concurrency: 5 },
+ );
+});
+
+const getCapDetail = Effect.fn("Mobile.getCapDetail")(function* (
+ videoId: Video.VideoId,
+) {
+ const { row, cap } = yield* getCapById(videoId);
+ const metadata = getMetadataRecord(row.metadata);
+ const comments = yield* getComments(videoId);
+
+ return {
+ cap,
+ summary: getMetadataString(metadata, "summary"),
+ chapters: getMetadataChapters(metadata),
+ transcriptionStatus: row.transcriptionStatus,
+ comments,
+ shareUrl: `${serverEnv().WEB_URL}/s/${videoId}`,
+ };
+});
+
+const createMobileComment = Effect.fn("Mobile.createComment")(function* ({
+ videoId,
+ content,
+ timestamp,
+ parentCommentId,
+ type,
+}: {
+ videoId: Video.VideoId;
+ content: string;
+ timestamp: number | null;
+ parentCommentId: Comment.CommentId | null;
+ type: "text" | "emoji";
+}) {
+ const user = yield* CurrentUser;
+ yield* getCapById(videoId);
+
+ const trimmedContent = content.trim();
+ if (trimmedContent.length === 0) {
+ return yield* Effect.fail(new HttpApiError.BadRequest());
+ }
+
+ const id = Comment.CommentId.make(nanoId());
+ const now = new Date();
+ const database = yield* Database;
+ yield* database.use((db) =>
+ db.insert(Db.comments).values({
+ id,
+ authorId: user.id,
+ type,
+ content: trimmedContent,
+ videoId,
+ timestamp,
+ parentCommentId,
+ createdAt: now,
+ updatedAt: now,
+ }),
+ );
+
+ const notificationType = parentCommentId
+ ? "reply"
+ : type === "emoji"
+ ? "reaction"
+ : "comment";
+
+ yield* Effect.tryPromise(() =>
+ createNotification({
+ type: notificationType,
+ videoId,
+ authorId: user.id,
+ comment: { id, content: trimmedContent },
+ parentCommentId: parentCommentId ?? undefined,
+ }),
+ ).pipe(Effect.catchAll(() => Effect.void));
+
+ const comments = yield* getComments(videoId);
+ const created = comments.find((comment) => comment.id === id);
+ if (!created)
+ return yield* Effect.fail(new HttpApiError.InternalServerError());
+ return created;
+});
+
+const getPlayback = Effect.fn("Mobile.getPlayback")(function* (
+ videoId: Video.VideoId,
+) {
+ const user = yield* CurrentUser;
+ const videos = yield* Videos;
+ const storage = yield* Storage;
+ const [video] = yield* videos.getByIdForViewing(videoId).pipe(
+ Effect.flatten,
+ Effect.catchTag("NoSuchElementException", () => new Video.NotFoundError()),
+ );
+
+ if (video.ownerId !== user.id) {
+ return yield* Effect.fail(new HttpApiError.NotFound());
+ }
+
+ const [bucket] = yield* storage.getAccessForVideo(video);
+ const source = Video.Video.getSource(video);
+
+ const transcriptKey = `${video.ownerId}/${video.id}/transcription.vtt`;
+ const transcriptUrl = yield* bucket.headObject(transcriptKey).pipe(
+ Effect.flatMap(() => bucket.getSignedObjectUrl(transcriptKey)),
+ Effect.catchAll(() => Effect.succeed(null)),
+ );
+
+ if (source instanceof Video.Mp4Source) {
+ const url = yield* bucket.getSignedObjectUrl(source.getFileKey());
+ return { kind: "mp4" as const, url, transcriptUrl };
+ }
+
+ if (source instanceof Video.M3U8Source) {
+ const url = yield* bucket.getSignedObjectUrl(source.getPlaylistFileKey());
+ return { kind: "hls" as const, url, transcriptUrl };
+ }
+
+ if (source instanceof Video.SegmentsSource) {
+ return {
+ kind: "hls" as const,
+ url: `${serverEnv().WEB_URL}/api/playlist?videoId=${video.id}&videoType=segments-master`,
+ transcriptUrl,
+ };
+ }
+
+ return yield* Effect.fail(new HttpApiError.NotFound());
+});
+
+const createUpload = Effect.fn("Mobile.createUpload")(function* (
+ input: MobileUploadCreateInput,
+) {
+ const user = yield* CurrentUser;
+ const organizationId = input.organizationId ?? user.activeOrganizationId;
+ yield* assertOrganizationAccess(organizationId);
+
+ if (!input.contentType.startsWith("video/")) {
+ return yield* Effect.fail(new HttpApiError.BadRequest());
+ }
+
+ const database = yield* Database;
+ const storage = yield* Storage;
+ const repo = yield* VideosRepo;
+ const folderId = input.folderId;
+
+ if (folderId) {
+ const [folder] = yield* database.use((db) =>
+ db
+ .select({ id: Db.folders.id })
+ .from(Db.folders)
+ .where(
+ and(
+ eq(Db.folders.id, folderId),
+ eq(Db.folders.organizationId, organizationId),
+ eq(Db.folders.createdById, user.id),
+ isNull(Db.folders.spaceId),
+ ),
+ ),
+ );
+ if (!folder) return yield* Effect.fail(new HttpApiError.NotFound());
+ }
+
+ const writable = yield* storage.getWritableAccessForUser(
+ user.id,
+ organizationId,
+ );
+ const videoId = yield* repo.create({
+ ownerId: user.id,
+ orgId: organizationId,
+ name: getUploadTitle(input.fileName),
+ public: serverEnv().CAP_VIDEOS_DEFAULT_PUBLIC,
+ source: { type: "webMP4" },
+ bucketId: writable.bucketId,
+ storageIntegrationId: writable.storageIntegrationId,
+ folderId: Option.fromNullable(folderId),
+ width: Option.fromNullable(input.width),
+ height: Option.fromNullable(input.height),
+ duration: Option.fromNullable(input.durationSeconds),
+ metadata: Option.none(),
+ transcriptionStatus: Option.none(),
+ });
+
+ yield* database.use((db) =>
+ db.insert(Db.videoUploads).values({
+ videoId,
+ total: input.contentLength ?? 0,
+ mode: "singlepart",
+ }),
+ );
+
+ const rawFileKey = `${user.id}/${videoId}/raw-upload.${getFileExtension(input)}`;
+ const upload = yield* writable.access.createUploadTarget(rawFileKey, {
+ contentType: input.contentType,
+ method: "put",
+ fields: {
+ "Content-Type": input.contentType,
+ "x-amz-meta-userid": user.id,
+ "x-amz-meta-source": "cap-mobile-ios",
+ },
+ });
+ const { cap } = yield* getCapById(videoId);
+
+ return {
+ id: videoId,
+ shareUrl: `${serverEnv().WEB_URL}/s/${videoId}`,
+ rawFileKey,
+ upload,
+ cap,
+ };
+});
+
+const ApiLive = HttpApiBuilder.api(Mobile.MobileApiContract).pipe(
+ Layer.provide(
+ HttpApiBuilder.group(Mobile.MobileApiContract, "mobile", (handlers) =>
+ Effect.gen(function* () {
+ const videos = yield* Videos;
+ const database = yield* Database;
+
+ return handlers
+ .handle("getAuthConfig", () =>
+ Effect.succeed({
+ googleAuthAvailable: Boolean(serverEnv().GOOGLE_CLIENT_ID),
+ workosAuthAvailable: Boolean(serverEnv().WORKOS_CLIENT_ID),
+ }),
+ )
+ .handle("requestSession", ({ request, urlParams }) =>
+ withMappedErrors(
+ Effect.gen(function* () {
+ const user = yield* getCurrentUser;
+ if (Option.isNone(user)) {
+ const redirectOrigin = getDeploymentOrigin();
+ const requestUrl = new URL(request.url);
+ const loginRedirectUrl = new URL(`${redirectOrigin}/login`);
+ loginRedirectUrl.searchParams.set(
+ "next",
+ new URL(
+ `${redirectOrigin}${requestUrl.pathname}${requestUrl.search}`,
+ ).toString(),
+ );
+ if (urlParams.provider === "google") {
+ loginRedirectUrl.searchParams.set(
+ "mobileProvider",
+ "google",
+ );
+ } else if (urlParams.provider === "workos") {
+ loginRedirectUrl.searchParams.set(
+ "mobileProvider",
+ "workos",
+ );
+ if (urlParams.organizationId) {
+ loginRedirectUrl.searchParams.set(
+ "organizationId",
+ urlParams.organizationId,
+ );
+ }
+ }
+ return HttpServerResponse.redirect(
+ loginRedirectUrl.toString(),
+ );
+ }
+
+ const session = yield* createMobileApiKey(user.value.id);
+
+ if (urlParams.redirectUri) {
+ const redirectUrl = getMobileRedirectUrl(
+ urlParams.redirectUri,
+ );
+ if (!redirectUrl) {
+ return yield* Effect.fail(new HttpApiError.BadRequest());
+ }
+
+ redirectUrl.searchParams.set("api_key", session.apiKey);
+ redirectUrl.searchParams.set("user_id", user.value.id);
+ return HttpServerResponse.redirect(redirectUrl.toString());
+ }
+
+ return session;
+ }),
+ ),
+ )
+ .handle("requestEmailSession", ({ payload }) =>
+ withMappedErrors(requestEmailSession(payload.email)),
+ )
+ .handle("verifyEmailSession", ({ payload }) =>
+ withMappedErrors(verifyEmailSession(payload)),
+ )
+ .handle("revokeSession", ({ headers }) =>
+ withMappedErrors(
+ Effect.gen(function* () {
+ const token = parseBearerToken(headers.authorization);
+ if (!token)
+ return yield* Effect.fail(new HttpApiError.Unauthorized());
+ yield* database.use((db) =>
+ db.delete(Db.authApiKeys).where(eq(Db.authApiKeys.id, token)),
+ );
+ return { success: true as const };
+ }),
+ ),
+ )
+ .handle("bootstrap", () => withMappedErrors(getBootstrap()))
+ .handle("setActiveOrganization", ({ payload }) =>
+ withMappedErrors(
+ Effect.gen(function* () {
+ const user = yield* CurrentUser;
+ yield* assertOrganizationAccess(payload.organizationId);
+ yield* database.use((db) =>
+ db
+ .update(Db.users)
+ .set({ activeOrganizationId: payload.organizationId })
+ .where(eq(Db.users.id, user.id)),
+ );
+ return yield* getBootstrap();
+ }),
+ ),
+ )
+ .handle("listCaps", ({ urlParams }) =>
+ withMappedErrors(getCapsList(urlParams)),
+ )
+ .handle("createFolder", ({ payload }) =>
+ withMappedErrors(createMobileFolder(payload)),
+ )
+ .handle("getCap", ({ path }) =>
+ withMappedErrors(getCapDetail(path.id)),
+ )
+ .handle("updateCapSharing", ({ path, payload }) =>
+ withMappedErrors(
+ Effect.gen(function* () {
+ const user = yield* CurrentUser;
+ yield* getCapById(path.id);
+ yield* database.use((db) =>
+ db
+ .update(Db.videos)
+ .set({ public: payload.public })
+ .where(
+ and(
+ eq(Db.videos.id, path.id),
+ eq(Db.videos.ownerId, user.id),
+ ),
+ ),
+ );
+ const { cap } = yield* getCapById(path.id);
+ return cap;
+ }),
+ ),
+ )
+ .handle("updateCapTitle", ({ path, payload }) =>
+ withMappedErrors(
+ Effect.gen(function* () {
+ const user = yield* CurrentUser;
+ yield* getCapById(path.id);
+ const title = payload.title.trim();
+ if (!title) {
+ return yield* Effect.fail(new HttpApiError.BadRequest());
+ }
+
+ yield* database.use((db) =>
+ db
+ .update(Db.videos)
+ .set({ name: title })
+ .where(
+ and(
+ eq(Db.videos.id, path.id),
+ eq(Db.videos.ownerId, user.id),
+ ),
+ ),
+ );
+ yield* Effect.sync(() => {
+ revalidatePath("/dashboard/caps");
+ revalidatePath("/dashboard/shared-caps");
+ revalidatePath(`/s/${path.id}`);
+ });
+ const { cap } = yield* getCapById(path.id);
+ return cap;
+ }),
+ ),
+ )
+ .handle("updateCapPassword", ({ path, payload }) =>
+ withMappedErrors(
+ Effect.gen(function* () {
+ const user = yield* CurrentUser;
+ yield* getCapById(path.id);
+ const trimmedPassword = payload.password?.trim() ?? null;
+ const nextPassword = trimmedPassword
+ ? yield* Effect.tryPromise({
+ try: () => hashPassword(trimmedPassword),
+ catch: () => new HttpApiError.InternalServerError(),
+ })
+ : null;
+
+ yield* database.use((db) =>
+ db
+ .update(Db.videos)
+ .set({ password: nextPassword })
+ .where(
+ and(
+ eq(Db.videos.id, path.id),
+ eq(Db.videos.ownerId, user.id),
+ ),
+ ),
+ );
+ const { cap } = yield* getCapById(path.id);
+ return cap;
+ }),
+ ),
+ )
+ .handle("deleteCap", ({ path }) =>
+ withMappedErrors(
+ videos
+ .delete(path.id)
+ .pipe(Effect.map(() => ({ success: true as const }))),
+ ),
+ )
+ .handle("getPlayback", ({ path }) =>
+ withMappedErrors(getPlayback(path.id)),
+ )
+ .handle("getDownload", ({ path }) =>
+ withMappedErrors(
+ videos.getDownloadInfo(path.id).pipe(
+ Effect.flatMap(
+ Option.match({
+ onNone: () => Effect.fail(new HttpApiError.NotFound()),
+ onSome: (info) =>
+ Effect.succeed({
+ fileName: info.fileName,
+ url: info.downloadUrl,
+ }),
+ }),
+ ),
+ ),
+ ),
+ )
+ .handle("createComment", ({ path, payload }) =>
+ withMappedErrors(
+ createMobileComment({
+ videoId: path.id,
+ content: payload.content,
+ timestamp: payload.timestamp,
+ parentCommentId: payload.parentCommentId ?? null,
+ type: "text",
+ }),
+ ),
+ )
+ .handle("deleteComment", ({ path }) =>
+ withMappedErrors(
+ Effect.gen(function* () {
+ const user = yield* CurrentUser;
+ const result = yield* database.use((db) =>
+ db
+ .delete(Db.comments)
+ .where(
+ and(
+ eq(Db.comments.id, path.id),
+ eq(Db.comments.authorId, user.id),
+ ),
+ ),
+ );
+ const affectedRows = getAffectedRows(result);
+ if (affectedRows === 0) {
+ return yield* Effect.fail(new HttpApiError.NotFound());
+ }
+ return { success: true as const };
+ }),
+ ),
+ )
+ .handle("createReaction", ({ path, payload }) =>
+ withMappedErrors(
+ createMobileComment({
+ videoId: path.id,
+ content: payload.content,
+ timestamp: payload.timestamp,
+ parentCommentId: null,
+ type: "emoji",
+ }),
+ ),
+ )
+ .handle("createUpload", ({ payload }) =>
+ withMappedErrors(createUpload(payload)),
+ )
+ .handle("updateUploadProgress", ({ path, payload }) =>
+ withMappedErrors(
+ videos
+ .updateUploadProgress({
+ videoId: path.id,
+ uploaded: Math.max(0, Math.trunc(payload.uploaded)),
+ total: Math.max(0, Math.trunc(payload.total)),
+ updatedAt: new Date(),
+ })
+ .pipe(Effect.map(() => ({ success: true as const }))),
+ ),
+ )
+ .handle("completeUpload", ({ path, payload }) =>
+ withMappedErrors(
+ Effect.gen(function* () {
+ const user = yield* CurrentUser;
+ const [video] = yield* database.use((db) =>
+ db
+ .select({
+ id: Db.videos.id,
+ ownerId: Db.videos.ownerId,
+ bucketId: Db.videos.bucket,
+ })
+ .from(Db.videos)
+ .where(
+ and(
+ eq(Db.videos.id, path.id),
+ eq(Db.videos.ownerId, user.id),
+ ),
+ ),
+ );
+ if (!video)
+ return yield* Effect.fail(new HttpApiError.NotFound());
+
+ const prefix = `${user.id}/${path.id}/`;
+ if (!payload.rawFileKey.startsWith(prefix)) {
+ return yield* Effect.fail(new HttpApiError.BadRequest());
+ }
+
+ if (payload.contentLength !== undefined) {
+ yield* database.use((db) =>
+ db
+ .update(Db.videoUploads)
+ .set({
+ uploaded: payload.contentLength,
+ total: payload.contentLength,
+ updatedAt: new Date(),
+ })
+ .where(eq(Db.videoUploads.videoId, path.id)),
+ );
+ }
+
+ yield* Effect.tryPromise(() =>
+ startVideoProcessingWorkflow({
+ videoId: path.id,
+ userId: user.id,
+ rawFileKey: payload.rawFileKey,
+ bucketId: video.bucketId,
+ processingMessage: "Starting video processing...",
+ startFailureMessage:
+ "Video uploaded, but processing could not start.",
+ mode: "singlepart",
+ }),
+ ).pipe(
+ Effect.catchAll((error) =>
+ Effect.logError(error).pipe(
+ Effect.flatMap(() =>
+ Effect.fail(new HttpApiError.InternalServerError()),
+ ),
+ ),
+ ),
+ );
+
+ return { success: true as const };
+ }),
+ ),
+ );
+ }),
+ ),
+ ),
+);
+
+const handler = apiToHandler(ApiLive);
+
+export const GET = handler;
+export const POST = handler;
+export const PATCH = handler;
+export const DELETE = handler;
diff --git a/package.json b/package.json
index 8ae22dd6d83..9af939bb312 100644
--- a/package.json
+++ b/package.json
@@ -18,6 +18,7 @@
"dev": "pnpm run docker:up && trap 'pnpm run docker:stop' EXIT && dotenv -e .env -- turbo run dev --env-mode=loose --ui tui",
"dev:desktop": "pnpm run --filter=@cap/desktop dev",
"dev:manual": "pnpm run docker:up && trap 'pnpm run docker:stop' EXIT && dotenv -e .env -- turbo run dev --filter=!@cap/storybook --no-cache --concurrency 1",
+ "dev:mobile": "pnpm run docker:up && trap 'pnpm run docker:stop' EXIT && CAP_MOBILE_DISABLE_ASSOCIATED_DOMAINS=1 EXPO_PUBLIC_CAP_WEB_URL=${EXPO_PUBLIC_CAP_WEB_URL:-http://localhost:3000} dotenv -e .env -- turbo run dev --filter=@cap/web --filter=@cap/mobile --env-mode=loose --ui tui",
"dev:web": "pnpm dev --filter=!@cap/desktop",
"dev:windows": "start /b cmd /c \"pnpm run docker:up > nul\" && timeout /t 5 /nobreak > nul && dotenv -e .env -- turbo run dev --env-mode=loose --ui tui",
"docker:clean": "turbo run docker:clean",
@@ -59,6 +60,9 @@
"typescript": "^5.8.3"
},
"pnpm": {
+ "overrides": {
+ "react-native-worklets": "0.7.4"
+ },
"peerDependencyRules": {
"allowedVersions": {
"next-auth>next": ">=16.0.0"
diff --git a/packages/database/package.json b/packages/database/package.json
index a221a469a38..aa4bc320f88 100644
--- a/packages/database/package.json
+++ b/packages/database/package.json
@@ -58,6 +58,7 @@
"exports": {
".": "./index.ts",
"./auth/auth-options": "./auth/auth-options.ts",
+ "./auth/domain-utils": "./auth/domain-utils.ts",
"./auth/session": "./auth/session.ts",
"./schema": "./schema.ts",
"./crypto": "./crypto.ts",
@@ -71,6 +72,7 @@
"exports": {
".": "./dist/index.js",
"./auth/auth-options": "./dist/auth/auth-options.js",
+ "./auth/domain-utils": "./dist/auth/domain-utils.js",
"./auth/session": "./dist/auth/session.js",
"./schema": "./dist/schema.js",
"./crypto": "./dist/crypto.js",
diff --git a/packages/web-domain/src/Mobile.ts b/packages/web-domain/src/Mobile.ts
new file mode 100644
index 00000000000..3dfafc652c2
--- /dev/null
+++ b/packages/web-domain/src/Mobile.ts
@@ -0,0 +1,435 @@
+import {
+ HttpApi,
+ HttpApiEndpoint,
+ HttpApiError,
+ HttpApiGroup,
+ OpenApi,
+} from "@effect/platform";
+import { Schema } from "effect";
+import { HttpAuthMiddleware } from "./Authentication.ts";
+import { CommentId } from "./Comment.ts";
+import { FolderColor, FolderId } from "./Folder.ts";
+import { OrganisationId } from "./Organisation.ts";
+import { UploadTarget } from "./Storage.ts";
+import { UserId } from "./User.ts";
+import { UploadPhase, VideoId } from "./Video.ts";
+
+export const MobileApiKeyResponse = Schema.Struct({
+ type: Schema.Literal("api_key"),
+ apiKey: Schema.String,
+ userId: UserId,
+});
+
+export const MobileSuccessResponse = Schema.Struct({
+ success: Schema.Literal(true),
+});
+
+export const MobileAuthConfigResponse = Schema.Struct({
+ googleAuthAvailable: Schema.Boolean,
+ workosAuthAvailable: Schema.Boolean,
+});
+
+export const MobileSessionRequestParams = Schema.Struct({
+ redirectUri: Schema.optional(Schema.String),
+ provider: Schema.optional(Schema.Literal("google", "workos")),
+ organizationId: Schema.optional(Schema.String),
+});
+
+export const MobileEmailSessionRequestInput = Schema.Struct({
+ email: Schema.String,
+});
+
+export const MobileEmailSessionVerifyInput = Schema.Struct({
+ email: Schema.String,
+ code: Schema.String,
+});
+
+export const MobileAuthHeaders = Schema.Struct({
+ authorization: Schema.optional(Schema.String),
+});
+
+export const MobileUser = Schema.Struct({
+ id: UserId,
+ name: Schema.NullOr(Schema.String),
+ email: Schema.String,
+ imageUrl: Schema.NullOr(Schema.String),
+ activeOrganizationId: OrganisationId,
+});
+
+export const MobileOrganization = Schema.Struct({
+ id: OrganisationId,
+ name: Schema.String,
+ iconUrl: Schema.NullOr(Schema.String),
+ role: Schema.Literal("owner", "admin", "member"),
+});
+
+export const MobileFolder = Schema.Struct({
+ id: FolderId,
+ name: Schema.String,
+ color: FolderColor,
+ parentId: Schema.NullOr(FolderId),
+ videoCount: Schema.Number,
+});
+
+export const MobileUploadProgress = Schema.Struct({
+ uploaded: Schema.Number,
+ total: Schema.Number,
+ phase: UploadPhase,
+ processingProgress: Schema.Number,
+ processingMessage: Schema.NullOr(Schema.String),
+ processingError: Schema.NullOr(Schema.String),
+});
+
+export const MobileCapSummary = Schema.Struct({
+ id: VideoId,
+ shareUrl: Schema.String,
+ title: Schema.String,
+ createdAt: Schema.String,
+ updatedAt: Schema.String,
+ ownerName: Schema.String,
+ durationSeconds: Schema.NullOr(Schema.Number),
+ thumbnailUrl: Schema.NullOr(Schema.String),
+ folderId: Schema.NullOr(FolderId),
+ public: Schema.Boolean,
+ protected: Schema.Boolean,
+ viewCount: Schema.Number,
+ commentCount: Schema.Number,
+ reactionCount: Schema.Number,
+ upload: Schema.NullOr(MobileUploadProgress),
+});
+
+export const MobileComment = Schema.Struct({
+ id: CommentId,
+ videoId: VideoId,
+ type: Schema.Literal("text", "emoji"),
+ content: Schema.String,
+ timestamp: Schema.NullOr(Schema.Number),
+ parentCommentId: Schema.NullOr(CommentId),
+ createdAt: Schema.String,
+ updatedAt: Schema.String,
+ author: Schema.Struct({
+ id: UserId,
+ name: Schema.NullOr(Schema.String),
+ imageUrl: Schema.NullOr(Schema.String),
+ }),
+});
+
+export const MobileChapter = Schema.Struct({
+ title: Schema.String,
+ start: Schema.Number,
+});
+
+export const MobileCapDetail = Schema.Struct({
+ cap: MobileCapSummary,
+ summary: Schema.NullOr(Schema.String),
+ chapters: Schema.Array(MobileChapter),
+ transcriptionStatus: Schema.NullOr(
+ Schema.Literal("PROCESSING", "COMPLETE", "ERROR", "SKIPPED", "NO_AUDIO"),
+ ),
+ comments: Schema.Array(MobileComment),
+ shareUrl: Schema.String,
+});
+
+export const MobileCapsListParams = Schema.Struct({
+ folderId: Schema.optional(Schema.String),
+ page: Schema.optional(Schema.String),
+ limit: Schema.optional(Schema.String),
+});
+
+export const MobileCapsListResponse = Schema.Struct({
+ folders: Schema.Array(MobileFolder),
+ caps: Schema.Array(MobileCapSummary),
+ page: Schema.Number,
+ limit: Schema.Number,
+ total: Schema.Number,
+ hasMore: Schema.Boolean,
+});
+
+export const MobileBootstrapResponse = Schema.Struct({
+ user: MobileUser,
+ organizations: Schema.Array(MobileOrganization),
+ activeOrganizationId: Schema.NullOr(OrganisationId),
+ rootFolders: Schema.Array(MobileFolder),
+});
+
+export const MobileActiveOrganizationInput = Schema.Struct({
+ organizationId: OrganisationId,
+});
+
+export const MobileCapSharingInput = Schema.Struct({
+ public: Schema.Boolean,
+});
+
+export const MobileCapTitleInput = Schema.Struct({
+ title: Schema.String,
+});
+
+export const MobileCapPasswordInput = Schema.Struct({
+ password: Schema.NullOr(Schema.String),
+});
+
+export const MobileFolderCreateInput = Schema.Struct({
+ name: Schema.String,
+ color: Schema.optional(FolderColor),
+});
+
+export const MobileVideoPath = Schema.Struct({
+ id: VideoId,
+});
+
+export const MobileCommentPath = Schema.Struct({
+ id: CommentId,
+});
+
+export const MobileUploadPath = Schema.Struct({
+ id: VideoId,
+});
+
+export const MobileCommentCreateInput = Schema.Struct({
+ content: Schema.String,
+ timestamp: Schema.NullOr(Schema.Number),
+ parentCommentId: Schema.optional(Schema.NullOr(CommentId)),
+});
+
+export const MobileReactionCreateInput = Schema.Struct({
+ content: Schema.String,
+ timestamp: Schema.NullOr(Schema.Number),
+});
+
+export const MobilePlaybackResponse = Schema.Struct({
+ kind: Schema.Literal("mp4", "hls"),
+ url: Schema.String,
+ transcriptUrl: Schema.NullOr(Schema.String),
+});
+
+export const MobileDownloadResponse = Schema.Struct({
+ fileName: Schema.String,
+ url: Schema.String,
+});
+
+export const MobileUploadCreateInput = Schema.Struct({
+ organizationId: Schema.optional(OrganisationId),
+ folderId: Schema.optional(FolderId),
+ fileName: Schema.String,
+ contentType: Schema.String,
+ contentLength: Schema.optional(Schema.Number),
+ durationSeconds: Schema.optional(Schema.Number),
+ width: Schema.optional(Schema.Number),
+ height: Schema.optional(Schema.Number),
+ fps: Schema.optional(Schema.Number),
+});
+
+export const MobileUploadCreateResponse = Schema.Struct({
+ id: VideoId,
+ shareUrl: Schema.String,
+ rawFileKey: Schema.String,
+ upload: UploadTarget,
+ cap: MobileCapSummary,
+});
+
+export const MobileUploadProgressInput = Schema.Struct({
+ uploaded: Schema.Number,
+ total: Schema.Number,
+});
+
+export const MobileUploadCompleteInput = Schema.Struct({
+ rawFileKey: Schema.String,
+ contentLength: Schema.optional(Schema.Number),
+});
+
+export class MobileHttpApi extends HttpApiGroup.make("mobile")
+ .add(
+ HttpApiEndpoint.get("getAuthConfig", "/session/config").addSuccess(
+ MobileAuthConfigResponse,
+ ),
+ )
+ .add(
+ HttpApiEndpoint.get("requestSession", "/session/request")
+ .setUrlParams(MobileSessionRequestParams)
+ .addSuccess(MobileApiKeyResponse)
+ .addError(HttpApiError.InternalServerError)
+ .addError(HttpApiError.BadRequest)
+ .addError(HttpApiError.Forbidden)
+ .addError(HttpApiError.NotFound),
+ )
+ .add(
+ HttpApiEndpoint.post("requestEmailSession", "/session/email/request")
+ .setPayload(MobileEmailSessionRequestInput)
+ .addSuccess(MobileSuccessResponse)
+ .addError(HttpApiError.InternalServerError)
+ .addError(HttpApiError.BadRequest)
+ .addError(HttpApiError.Forbidden)
+ .addError(HttpApiError.NotFound),
+ )
+ .add(
+ HttpApiEndpoint.post("verifyEmailSession", "/session/email/verify")
+ .setPayload(MobileEmailSessionVerifyInput)
+ .addSuccess(MobileApiKeyResponse)
+ .addError(HttpApiError.InternalServerError)
+ .addError(HttpApiError.BadRequest)
+ .addError(HttpApiError.Forbidden)
+ .addError(HttpApiError.NotFound),
+ )
+ .add(
+ HttpApiEndpoint.post("revokeSession", "/session/revoke")
+ .setHeaders(MobileAuthHeaders)
+ .addSuccess(MobileSuccessResponse)
+ .middleware(HttpAuthMiddleware)
+ .addError(HttpApiError.Forbidden)
+ .addError(HttpApiError.NotFound),
+ )
+ .add(
+ HttpApiEndpoint.get("bootstrap", "/bootstrap")
+ .addSuccess(MobileBootstrapResponse)
+ .middleware(HttpAuthMiddleware)
+ .addError(HttpApiError.Forbidden)
+ .addError(HttpApiError.NotFound),
+ )
+ .add(
+ HttpApiEndpoint.patch("setActiveOrganization", "/user/active-organization")
+ .setPayload(MobileActiveOrganizationInput)
+ .addSuccess(MobileBootstrapResponse)
+ .middleware(HttpAuthMiddleware)
+ .addError(HttpApiError.Forbidden)
+ .addError(HttpApiError.NotFound),
+ )
+ .add(
+ HttpApiEndpoint.get("listCaps", "/caps")
+ .setUrlParams(MobileCapsListParams)
+ .addSuccess(MobileCapsListResponse)
+ .middleware(HttpAuthMiddleware)
+ .addError(HttpApiError.Forbidden)
+ .addError(HttpApiError.NotFound),
+ )
+ .add(
+ HttpApiEndpoint.post("createFolder", "/folders")
+ .setPayload(MobileFolderCreateInput)
+ .addSuccess(MobileFolder)
+ .middleware(HttpAuthMiddleware)
+ .addError(HttpApiError.BadRequest)
+ .addError(HttpApiError.Forbidden)
+ .addError(HttpApiError.NotFound),
+ )
+ .add(
+ HttpApiEndpoint.get("getCap", "/caps/:id")
+ .setPath(MobileVideoPath)
+ .addSuccess(MobileCapDetail)
+ .middleware(HttpAuthMiddleware)
+ .addError(HttpApiError.Forbidden)
+ .addError(HttpApiError.NotFound),
+ )
+ .add(
+ HttpApiEndpoint.patch("updateCapSharing", "/caps/:id/sharing")
+ .setPath(MobileVideoPath)
+ .setPayload(MobileCapSharingInput)
+ .addSuccess(MobileCapSummary)
+ .middleware(HttpAuthMiddleware)
+ .addError(HttpApiError.Forbidden)
+ .addError(HttpApiError.NotFound),
+ )
+ .add(
+ HttpApiEndpoint.patch("updateCapTitle", "/caps/:id/title")
+ .setPath(MobileVideoPath)
+ .setPayload(MobileCapTitleInput)
+ .addSuccess(MobileCapSummary)
+ .middleware(HttpAuthMiddleware)
+ .addError(HttpApiError.BadRequest)
+ .addError(HttpApiError.Forbidden)
+ .addError(HttpApiError.NotFound),
+ )
+ .add(
+ HttpApiEndpoint.patch("updateCapPassword", "/caps/:id/password")
+ .setPath(MobileVideoPath)
+ .setPayload(MobileCapPasswordInput)
+ .addSuccess(MobileCapSummary)
+ .middleware(HttpAuthMiddleware)
+ .addError(HttpApiError.BadRequest)
+ .addError(HttpApiError.Forbidden)
+ .addError(HttpApiError.NotFound),
+ )
+ .add(
+ HttpApiEndpoint.del("deleteCap", "/caps/:id")
+ .setPath(MobileVideoPath)
+ .addSuccess(MobileSuccessResponse)
+ .middleware(HttpAuthMiddleware)
+ .addError(HttpApiError.Forbidden)
+ .addError(HttpApiError.NotFound),
+ )
+ .add(
+ HttpApiEndpoint.get("getPlayback", "/caps/:id/playback")
+ .setPath(MobileVideoPath)
+ .addSuccess(MobilePlaybackResponse)
+ .middleware(HttpAuthMiddleware)
+ .addError(HttpApiError.Forbidden)
+ .addError(HttpApiError.NotFound),
+ )
+ .add(
+ HttpApiEndpoint.get("getDownload", "/caps/:id/download")
+ .setPath(MobileVideoPath)
+ .addSuccess(MobileDownloadResponse)
+ .middleware(HttpAuthMiddleware)
+ .addError(HttpApiError.Forbidden)
+ .addError(HttpApiError.NotFound),
+ )
+ .add(
+ HttpApiEndpoint.post("createComment", "/caps/:id/comments")
+ .setPath(MobileVideoPath)
+ .setPayload(MobileCommentCreateInput)
+ .addSuccess(MobileComment)
+ .middleware(HttpAuthMiddleware)
+ .addError(HttpApiError.Forbidden)
+ .addError(HttpApiError.NotFound),
+ )
+ .add(
+ HttpApiEndpoint.del("deleteComment", "/comments/:id")
+ .setPath(MobileCommentPath)
+ .addSuccess(MobileSuccessResponse)
+ .middleware(HttpAuthMiddleware)
+ .addError(HttpApiError.Forbidden)
+ .addError(HttpApiError.NotFound),
+ )
+ .add(
+ HttpApiEndpoint.post("createReaction", "/caps/:id/reactions")
+ .setPath(MobileVideoPath)
+ .setPayload(MobileReactionCreateInput)
+ .addSuccess(MobileComment)
+ .middleware(HttpAuthMiddleware)
+ .addError(HttpApiError.Forbidden)
+ .addError(HttpApiError.NotFound),
+ )
+ .add(
+ HttpApiEndpoint.post("createUpload", "/uploads")
+ .setPayload(MobileUploadCreateInput)
+ .addSuccess(MobileUploadCreateResponse)
+ .middleware(HttpAuthMiddleware)
+ .addError(HttpApiError.Forbidden)
+ .addError(HttpApiError.NotFound),
+ )
+ .add(
+ HttpApiEndpoint.post("updateUploadProgress", "/uploads/:id/progress")
+ .setPath(MobileUploadPath)
+ .setPayload(MobileUploadProgressInput)
+ .addSuccess(MobileSuccessResponse)
+ .middleware(HttpAuthMiddleware)
+ .addError(HttpApiError.Forbidden)
+ .addError(HttpApiError.NotFound),
+ )
+ .add(
+ HttpApiEndpoint.post("completeUpload", "/uploads/:id/complete")
+ .setPath(MobileUploadPath)
+ .setPayload(MobileUploadCompleteInput)
+ .addSuccess(MobileSuccessResponse)
+ .middleware(HttpAuthMiddleware)
+ .addError(HttpApiError.Forbidden)
+ .addError(HttpApiError.NotFound),
+ ) {}
+
+export class MobileApiContract extends HttpApi.make("cap-mobile-api")
+ .add(MobileHttpApi)
+ .annotateContext(
+ OpenApi.annotations({
+ title: "Cap Mobile API",
+ description: "Authenticated API used by the Cap iOS app",
+ }),
+ )
+ .prefix("/api/mobile") {}
diff --git a/packages/web-domain/src/index.ts b/packages/web-domain/src/index.ts
index b8aca7049a4..dc3db577ddd 100644
--- a/packages/web-domain/src/index.ts
+++ b/packages/web-domain/src/index.ts
@@ -9,6 +9,7 @@ export * as ImageUpload from "./ImageUpload.ts";
export * as Language from "./Language.ts";
export * from "./Language.ts";
export * as Loom from "./Loom.ts";
+export * as Mobile from "./Mobile.ts";
export * as Organisation from "./Organisation.ts";
export * from "./Organisation.ts";
export * as Policy from "./Policy.ts";
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 2810356dc57..565e0ffe18b 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -4,6 +4,9 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
+overrides:
+ react-native-worklets: 0.7.4
+
importers:
.:
@@ -115,7 +118,7 @@ importers:
version: 0.14.10(solid-js@1.9.6)
'@solidjs/start':
specifier: ^1.1.3
- version: 1.1.3(@testing-library/jest-dom@6.5.0)(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(solid-js@1.9.6)(terser@5.44.0)(vinxi@0.5.6(@planetscale/database@1.19.0)(@types/node@22.15.17)(db0@0.3.2(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(mysql2@3.15.2))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(ioredis@5.6.1)(jiti@2.6.1)(lightningcss@1.32.0)(mysql2@3.15.2)(rolldown@1.0.1)(terser@5.44.0)(xml2js@0.6.2)(yaml@2.8.1))(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1)
+ version: 1.1.3(@testing-library/jest-dom@6.5.0)(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(solid-js@1.9.6)(terser@5.44.0)(vinxi@0.5.6(@planetscale/database@1.19.0)(@types/node@22.15.17)(db0@0.3.2(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(mysql2@3.15.2))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(ioredis@5.6.1)(jiti@2.6.1)(lightningcss@1.32.0)(mysql2@3.15.2)(rolldown@1.0.3)(terser@5.44.0)(xml2js@0.6.2)(yaml@2.8.1))(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1)
'@tanstack/solid-query':
specifier: ^5.51.21
version: 5.75.4(solid-js@1.9.6)
@@ -205,7 +208,7 @@ importers:
version: 9.0.1
vinxi:
specifier: ^0.5.6
- version: 0.5.6(@planetscale/database@1.19.0)(@types/node@22.15.17)(db0@0.3.2(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(mysql2@3.15.2))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(ioredis@5.6.1)(jiti@2.6.1)(lightningcss@1.32.0)(mysql2@3.15.2)(rolldown@1.0.1)(terser@5.44.0)(xml2js@0.6.2)(yaml@2.8.1)
+ version: 0.5.6(@planetscale/database@1.19.0)(@types/node@22.15.17)(db0@0.3.2(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(mysql2@3.15.2))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(ioredis@5.6.1)(jiti@2.6.1)(lightningcss@1.32.0)(mysql2@3.15.2)(rolldown@1.0.3)(terser@5.44.0)(xml2js@0.6.2)(yaml@2.8.1)
webcodecs:
specifier: ^0.1.0
version: 0.1.0
@@ -224,7 +227,7 @@ importers:
version: 4.3.0
'@tailwindcss/typography':
specifier: ^0.5.9
- version: 0.5.16(tailwindcss@4.3.0)
+ version: 0.5.16(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@20.17.43)(typescript@5.8.3)))
'@tauri-apps/cli':
specifier: '>=2.1.0'
version: 2.11.0
@@ -330,6 +333,133 @@ importers:
specifier: latest
version: 1.3.14
+ apps/mobile:
+ dependencies:
+ '@cap/web-domain':
+ specifier: workspace:*
+ version: link:../../packages/web-domain
+ '@expo/config-plugins':
+ specifier: ~55.0.9
+ version: 55.0.10
+ '@expo/metro-runtime':
+ specifier: ~55.0.11
+ version: 55.0.11(@expo/dom-webview@55.0.6)(expo@55.0.26)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ '@shopify/flash-list':
+ specifier: 2.0.2
+ version: 2.0.2(@babel/runtime@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ effect:
+ specifier: ^3.18.4
+ version: 3.18.4
+ expo:
+ specifier: ~55.0.24
+ version: 55.0.26(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.16)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ expo-clipboard:
+ specifier: ~55.0.13
+ version: 55.0.13(expo@55.0.26)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ expo-constants:
+ specifier: ~55.0.16
+ version: 55.0.16(expo@55.0.26)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))
+ expo-dev-client:
+ specifier: ~55.0.34
+ version: 55.0.35(expo@55.0.26)
+ expo-document-picker:
+ specifier: ~55.0.13
+ version: 55.0.13(expo@55.0.26)
+ expo-file-system:
+ specifier: ~55.0.20
+ version: 55.0.22(expo@55.0.26)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))
+ expo-font:
+ specifier: ~55.0.7
+ version: 55.0.8(expo@55.0.26)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ expo-glass-effect:
+ specifier: ~55.0.11
+ version: 55.0.11(expo@55.0.26)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ expo-image:
+ specifier: ~55.0.10
+ version: 55.0.11(expo@55.0.26)(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ expo-image-picker:
+ specifier: ~55.0.20
+ version: 55.0.20(expo@55.0.26)
+ expo-linking:
+ specifier: ~55.0.15
+ version: 55.0.15(expo@55.0.26)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ expo-media-library:
+ specifier: ~55.0.17
+ version: 55.0.17(expo@55.0.26)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))
+ expo-modules-core:
+ specifier: ~55.0.25
+ version: 55.0.25(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ expo-router:
+ specifier: ~55.0.14
+ version: 55.0.16(d69165032b49371b387000fe555903ea)
+ expo-secure-store:
+ specifier: ~55.0.14
+ version: 55.0.14(expo@55.0.26)
+ expo-sharing:
+ specifier: ~55.0.19
+ version: 55.0.20(expo@55.0.26)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ expo-symbols:
+ specifier: ~55.0.8
+ version: 55.0.9(expo-font@55.0.8)(expo@55.0.26)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ expo-video:
+ specifier: ~55.0.17
+ version: 55.0.17(expo@55.0.26)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ expo-web-browser:
+ specifier: ~55.0.16
+ version: 55.0.16(expo@55.0.26)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))
+ react:
+ specifier: 19.2.0
+ version: 19.2.0
+ react-dom:
+ specifier: 19.2.0
+ version: 19.2.0(react@19.2.0)
+ react-native:
+ specifier: 0.83.6
+ version: 0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0)
+ react-native-gesture-handler:
+ specifier: ~2.30.0
+ version: 2.30.1(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ react-native-reanimated:
+ specifier: 4.2.1
+ version: 4.2.1(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ react-native-safe-area-context:
+ specifier: ~5.6.2
+ version: 5.6.2(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ react-native-screens:
+ specifier: ~4.23.0
+ version: 4.23.0(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ react-native-svg:
+ specifier: 15.15.5
+ version: 15.15.5(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ react-native-web:
+ specifier: ~0.21.0
+ version: 0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ react-native-worklets:
+ specifier: 0.7.4
+ version: 0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ devDependencies:
+ '@testing-library/react-native':
+ specifier: ^13.3.3
+ version: 13.3.3(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react-test-renderer@19.2.0(react@19.2.0))(react@19.2.0)
+ '@types/react':
+ specifier: 19.2.14
+ version: 19.2.14
+ '@types/react-test-renderer':
+ specifier: ^19.1.0
+ version: 19.1.0
+ babel-preset-expo:
+ specifier: ~55.0.21
+ version: 55.0.22(@babel/core@7.27.1)(@babel/runtime@7.27.1)(expo@55.0.26)(react-refresh@0.14.2)
+ react-test-renderer:
+ specifier: 19.2.0
+ version: 19.2.0(react@19.2.0)
+ typescript:
+ specifier: ~5.9.2
+ version: 5.9.3
+ vitest:
+ specifier: ^3.2.0
+ version: 3.2.4(@types/debug@4.1.12)(@types/node@22.15.17)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)
+
apps/storybook:
dependencies:
'@cap/ui-solid':
@@ -590,7 +720,7 @@ importers:
version: 2.0.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@workos-inc/node':
specifier: ^7.34.0
- version: 7.50.0(express@5.1.0)(next@16.2.1(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))
+ version: 7.50.0(express@5.1.0)(next@16.2.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))
aws-sdk:
specifier: ^2.1530.0
version: 2.1692.0
@@ -638,7 +768,7 @@ importers:
version: 11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
geist:
specifier: ^1.3.1
- version: 1.4.2(next@16.2.1(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))
+ version: 1.4.2(next@16.2.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))
gif.js:
specifier: 0.2.0
version: 0.2.0
@@ -677,10 +807,10 @@ importers:
version: 12.20.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
next:
specifier: 16.2.1
- version: 16.2.1(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ version: 16.2.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
next-auth:
specifier: ^4.24.5
- version: 4.24.11(next@16.2.1(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(nodemailer@6.10.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ version: 4.24.11(next@16.2.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(nodemailer@6.10.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
next-mdx-remote:
specifier: ^6.0.0
version: 6.0.0(@types/react@19.2.14)(acorn@8.16.0)(react@19.2.4)
@@ -725,7 +855,7 @@ importers:
version: 5.28.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
recharts:
specifier: ^3.3.0
- version: 3.4.1(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react-is@17.0.2)(react@19.2.4)(redux@5.0.1)
+ version: 3.4.1(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react-is@19.2.6)(react@19.2.4)(redux@5.0.1)
rehype-pretty-code:
specifier: ^0.14.1
version: 0.14.1(shiki@3.23.0)
@@ -770,7 +900,7 @@ importers:
version: 9.0.1
workflow:
specifier: 4.2.0-beta.73
- version: 4.2.0-beta.73(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(@opentelemetry/api@1.9.0)(@swc/cli@0.8.0(@swc/core@1.15.3(@swc/helpers@0.5.17))(chokidar@5.0.0))(@swc/core@1.15.3(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(magicast@0.3.5)(next@16.2.1(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.8.3)
+ version: 4.2.0-beta.73(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(@opentelemetry/api@1.9.0)(@swc/cli@0.8.0(@swc/core@1.15.3(@swc/helpers@0.5.17))(chokidar@5.0.0))(@swc/core@1.15.3(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(magicast@0.3.5)(next@16.2.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.8.3)
zod:
specifier: ^3.25.76
version: 3.25.76
@@ -888,10 +1018,10 @@ importers:
version: 1.0.0-beta.42
tsdown:
specifier: ^0.15.6
- version: 0.15.6(typescript@5.8.3)
+ version: 0.15.6(typescript@5.9.3)
tsup:
specifier: ^8.5.0
- version: 8.5.0(@swc/core@1.15.5(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.14)(typescript@5.8.3)(yaml@2.8.1)
+ version: 8.5.0(@swc/core@1.15.5(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.14)(typescript@5.9.3)(yaml@2.8.1)
devDependencies:
concurrently:
specifier: ^9.2.1
@@ -904,13 +1034,13 @@ importers:
dependencies:
'@pulumi/github':
specifier: ^6.7.0
- version: 6.7.2(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.8.3))(typescript@5.8.3)
+ version: 6.7.2(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.9.3))(typescript@5.9.3)
'@pulumi/pulumi':
specifier: ^3.201.0
- version: 3.201.0(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.8.3))(typescript@5.8.3)
+ version: 3.201.0(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.9.3))(typescript@5.9.3)
'@pulumiverse/vercel':
specifier: ^1.14.3
- version: 1.14.3(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.8.3))(typescript@5.8.3)
+ version: 1.14.3(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.9.3))(typescript@5.9.3)
zod:
specifier: ^3
version: 3.25.76
@@ -929,7 +1059,7 @@ importers:
version: 6.3.5(@types/node@20.17.43)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)
vite-tsconfig-paths:
specifier: ^4.2.0
- version: 4.3.2(typescript@5.8.3)(vite@6.3.5(@types/node@20.17.43)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1))
+ version: 4.3.2(typescript@5.9.3)(vite@6.3.5(@types/node@20.17.43)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1))
zod:
specifier: ^3
version: 3.25.76
@@ -939,16 +1069,16 @@ importers:
version: 20.17.43
'@typescript-eslint/eslint-plugin':
specifier: ^5.59.6
- version: 5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3)
+ version: 5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)
'@typescript-eslint/parser':
specifier: ^5.59.6
- version: 5.62.0(eslint@8.57.1)(typescript@5.8.3)
+ version: 5.62.0(eslint@8.57.1)(typescript@5.9.3)
eslint:
specifier: ^8.41.0
version: 8.57.1
eslint-config-next:
specifier: 13.3.0
- version: 13.3.0(eslint@8.57.1)(typescript@5.8.3)
+ version: 13.3.0(eslint@8.57.1)(typescript@5.9.3)
eslint-config-prettier:
specifier: ^8.8.0
version: 8.10.0(eslint@8.57.1)
@@ -1020,13 +1150,13 @@ importers:
version: 5.1.5
next:
specifier: 15.5.9
- version: 15.5.9(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ version: 15.5.9(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
next-auth:
specifier: ^4.24.5
- version: 4.24.11(next@15.5.9(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(nodemailer@6.10.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ version: 4.24.11(next@15.5.9(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(nodemailer@6.10.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
react-email:
specifier: ^4.0.16
- version: 4.0.16(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ version: 4.0.16(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
resend:
specifier: 4.6.0
version: 4.6.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
@@ -1078,7 +1208,7 @@ importers:
dependencies:
'@t3-oss/env-nextjs':
specifier: ^0.12.0
- version: 0.12.0(typescript@5.8.3)(valibot@1.0.0-rc.1(typescript@5.8.3))(zod@3.25.76)
+ version: 0.12.0(typescript@5.9.3)(valibot@1.0.0-rc.1(typescript@5.9.3))(zod@3.25.76)
zod:
specifier: ^3.25.76
version: 3.25.76
@@ -1412,7 +1542,7 @@ importers:
version: 3.18.4
next:
specifier: 15.5.9
- version: 15.5.9(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ version: 15.5.9(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
server-only:
specifier: ^0.0.1
version: 0.0.1
@@ -1821,10 +1951,18 @@ packages:
resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
engines: {node: '>=6.9.0'}
+ '@babel/code-frame@7.29.7':
+ resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
+ engines: {node: '>=6.9.0'}
+
'@babel/compat-data@7.27.2':
resolution: {integrity: sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==}
engines: {node: '>=6.9.0'}
+ '@babel/compat-data@7.29.7':
+ resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==}
+ engines: {node: '>=6.9.0'}
+
'@babel/core@7.27.1':
resolution: {integrity: sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==}
engines: {node: '>=6.9.0'}
@@ -1841,14 +1979,51 @@ packages:
resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==}
engines: {node: '>=6.9.0'}
+ '@babel/generator@7.29.7':
+ resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-annotate-as-pure@7.29.7':
+ resolution: {integrity: sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==}
+ engines: {node: '>=6.9.0'}
+
'@babel/helper-compilation-targets@7.27.2':
resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==}
engines: {node: '>=6.9.0'}
+ '@babel/helper-compilation-targets@7.29.7':
+ resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-create-class-features-plugin@7.29.7':
+ resolution: {integrity: sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-create-regexp-features-plugin@7.29.7':
+ resolution: {integrity: sha512-907Uymvqgg1dwUA+7IGwFAOSYzQOuzPXKNJ1yxzwPffzkYFg2q2eHi1fIOs6sXkG9NbIUMunnUlkYsfRFNvomg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-define-polyfill-provider@0.6.8':
+ resolution: {integrity: sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==}
+ peerDependencies:
+ '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
+
'@babel/helper-globals@7.28.0':
resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==}
engines: {node: '>=6.9.0'}
+ '@babel/helper-globals@7.29.7':
+ resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-member-expression-to-functions@7.29.7':
+ resolution: {integrity: sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==}
+ engines: {node: '>=6.9.0'}
+
'@babel/helper-module-imports@7.18.6':
resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==}
engines: {node: '>=6.9.0'}
@@ -1857,28 +2032,78 @@ packages:
resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==}
engines: {node: '>=6.9.0'}
+ '@babel/helper-module-imports@7.29.7':
+ resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==}
+ engines: {node: '>=6.9.0'}
+
'@babel/helper-module-transforms@7.27.1':
resolution: {integrity: sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0
+ '@babel/helper-module-transforms@7.29.7':
+ resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-optimise-call-expression@7.29.7':
+ resolution: {integrity: sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==}
+ engines: {node: '>=6.9.0'}
+
'@babel/helper-plugin-utils@7.27.1':
resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==}
engines: {node: '>=6.9.0'}
+ '@babel/helper-plugin-utils@7.29.7':
+ resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-remap-async-to-generator@7.29.7':
+ resolution: {integrity: sha512-16AMiW26DbXWBbr3B8wNozKM0ydMLB892vaOaJW/fPJdnT8vJk5sdkQcU/isqUxyCE0cEoa8wZOcbgDuC4b6Og==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-replace-supers@7.29.7':
+ resolution: {integrity: sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-skip-transparent-expression-wrappers@7.29.7':
+ resolution: {integrity: sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==}
+ engines: {node: '>=6.9.0'}
+
'@babel/helper-string-parser@7.27.1':
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
engines: {node: '>=6.9.0'}
+ '@babel/helper-string-parser@7.29.7':
+ resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==}
+ engines: {node: '>=6.9.0'}
+
'@babel/helper-validator-identifier@7.27.1':
resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==}
engines: {node: '>=6.9.0'}
+ '@babel/helper-validator-identifier@7.29.7':
+ resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
+ engines: {node: '>=6.9.0'}
+
'@babel/helper-validator-option@7.27.1':
resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
engines: {node: '>=6.9.0'}
+ '@babel/helper-validator-option@7.29.7':
+ resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-wrap-function@7.29.7':
+ resolution: {integrity: sha512-iES0Skag9ERIF68aXadpO6dbXa03mNWK3sEqJaMnLNs/eC3l0lkImdfoy6Y09/SfkpawdAB4RjQ7PVA7TcVGdw==}
+ engines: {node: '>=6.9.0'}
+
'@babel/helpers@7.27.1':
resolution: {integrity: sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==}
engines: {node: '>=6.9.0'}
@@ -1902,18 +2127,341 @@ packages:
engines: {node: '>=6.0.0'}
hasBin: true
+ '@babel/parser@7.29.7':
+ resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ '@babel/plugin-proposal-decorators@7.29.7':
+ resolution: {integrity: sha512-EtU0Hi3GvrTqD56xKmZvV/uCXK2ZbwVNPNLAquVItcAZpUhkXwWlo3Fmj0c2LxgSf2I8IDULeAepwNP1OefLXg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-proposal-export-default-from@7.29.7':
+ resolution: {integrity: sha512-p+G5BNXDcy3bOXplhY4HybQ1GxH3i2Tppmdm/3epyRu2VgJJZuUlZ61MqRTg582Q7ZLBdP7fePYvsumSEkMxcQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-async-generators@7.8.4':
+ resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-bigint@7.8.3':
+ resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-class-properties@7.12.13':
+ resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-class-static-block@7.14.5':
+ resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-decorators@7.29.7':
+ resolution: {integrity: sha512-9MTTLbF39X6sqM92JPEsoI7++26hjZvzkxKZy64aMhWLH2mPkJ/Q3AV4QLmls3R14FpSpkOwQQfUh962JGQxxg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-dynamic-import@7.8.3':
+ resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-export-default-from@7.29.7':
+ resolution: {integrity: sha512-foag0BB37ROhdeIX9O8G0jX7hw0UekJc04cHMrYLOnrErsnBKqJGHJ8eDRpoCFZBvEPPygmmtw4qyU97qa4oOw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-flow@7.29.7':
+ resolution: {integrity: sha512-ajMX6QPcyomotqwpzhkYGxcK2i/us0rs1Qo9QvUpa+Fca0FTmqrzKrctoIYLMxcOhGZldGT/BAVkRGTWBiR8gQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-import-attributes@7.29.7':
+ resolution: {integrity: sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-import-meta@7.10.4':
+ resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-json-strings@7.8.3':
+ resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
'@babel/plugin-syntax-jsx@7.27.1':
resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
+ '@babel/plugin-syntax-jsx@7.29.7':
+ resolution: {integrity: sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-logical-assignment-operators@7.10.4':
+ resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3':
+ resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-numeric-separator@7.10.4':
+ resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-object-rest-spread@7.8.3':
+ resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-optional-catch-binding@7.8.3':
+ resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-optional-chaining@7.8.3':
+ resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-private-property-in-object@7.14.5':
+ resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-top-level-await@7.14.5':
+ resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
'@babel/plugin-syntax-typescript@7.27.1':
resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
+ '@babel/plugin-syntax-typescript@7.29.7':
+ resolution: {integrity: sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-arrow-functions@7.27.1':
+ resolution: {integrity: sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-arrow-functions@7.29.7':
+ resolution: {integrity: sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-async-generator-functions@7.29.7':
+ resolution: {integrity: sha512-d98gXZkgswvkyohMBABkhm3GeXhYj8psWfwQ2C7gtfrKGTykQa/iOIi+JJhwMjPlZ6Vm2XN+DCf3Es1EoG4ZLA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-async-to-generator@7.29.7':
+ resolution: {integrity: sha512-pcUb2SS+RMo9TWVBwKGI5ShtoG7R+zBsFmCKDa6fe8c+hPr3XJlZgoE5j6i8W7gDjhyvy+85vmYexanvXh3d1w==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-block-scoping@7.29.7':
+ resolution: {integrity: sha512-ONyr4+AZhKh8yKWInVxU9AXA9EbsyeLcL6V0dJy6M2/62vuvpGm29zzuymbTpdc451GEpDIdAyPLP3r+P61yKQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-class-properties@7.27.1':
+ resolution: {integrity: sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-class-properties@7.29.7':
+ resolution: {integrity: sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-class-static-block@7.29.7':
+ resolution: {integrity: sha512-kibJgmEdX2iMwsHY2tSZNDgj8PwIlCQz7FK9KuGKO8zsuoUwSEhoNnNVp/emKWrbY4HeO6kkXfdMqRKKKXBm2A==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.12.0
+
+ '@babel/plugin-transform-classes@7.28.4':
+ resolution: {integrity: sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-classes@7.29.7':
+ resolution: {integrity: sha512-qV0OGGBVacduzQHE649JyCneOFI/maT+YKsO+K4Yi3xv2wTPNjM/W2o2gdzMwEAZz7fXNTHAe0NcSg30bIN69g==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-computed-properties@7.29.7':
+ resolution: {integrity: sha512-RK7/IyU5phpuCdBAuig5VkzG/EnbDaui5SQGdU9BFrHdV+mV4cUjLMQ9lJDjLNtWHsqtiefpGZUXQP2BiTYMsA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-destructuring@7.29.7':
+ resolution: {integrity: sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-export-namespace-from@7.29.7':
+ resolution: {integrity: sha512-24B2nOy2TeJSMheqwPD4DDQOV/elLSIlKxjZt4i05H5AgdPdWR3n18HnNrcJ+j76WJd9gbwb9jPjNYUy6RautA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-flow-strip-types@7.29.7':
+ resolution: {integrity: sha512-wRHeUjUjCZnMHmiO5bRgjFLcoEh7JyTdByOW11ahhwNa4V0bmeGEaIvt51yq0zQp2yWIpqfxXXPyUP6GFJZHOQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-for-of@7.29.7':
+ resolution: {integrity: sha512-zeSIHh0+E1Um1WJRXCFlHQYu2ieJNdivLLjlBEp+dIBu3S51n+SZZmIXjxnItw6pz56Cn+KvK68BIBVsxq2JiQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-function-name@7.29.7':
+ resolution: {integrity: sha512-otRWaHXE6fbAGkePvaj/kvs3HsqXfPhlnzwSOlnFgbqCPMd975dW+4wZ00WFBt+/YlBGcJwNrARQTOJOb4ZrIg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-literals@7.29.7':
+ resolution: {integrity: sha512-DZ/oLP21ZuWx1vKqnoNv6/tvEK48AQOBRai40CX9dTjGluvT/YZCyY3rryDtyUqCEoyNroy5KKPwX2iQCiRvyw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-logical-assignment-operators@7.29.7':
+ resolution: {integrity: sha512-A0H91hh6W8MFRkp5TqJmMr39jzGD1A1E1Ysiv2O06Sfbhkapm+XyIzxWCEh5kqwOZ1/8QZ0dY3SeQ7XBqfJd5Q==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-modules-commonjs@7.29.7':
+ resolution: {integrity: sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-named-capturing-groups-regex@7.29.7':
+ resolution: {integrity: sha512-vuFoLwr4qnv2xbZ16SQd6uPcH5FNrLHhk/Jzo++0XJFcaDsr4gjJVg6j398oMHiC+83k/GiBzviwF5KBJkPUtQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/plugin-transform-nullish-coalescing-operator@7.27.1':
+ resolution: {integrity: sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-nullish-coalescing-operator@7.29.7':
+ resolution: {integrity: sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-numeric-separator@7.29.7':
+ resolution: {integrity: sha512-zR7fv/z14OjgHl4AgRtkDBvBMhIzCxqV/qN/2BCRC7LjFwvuzjYe7gDWxC4Wl/SNsLM6SE1IWvRPYMgSJaUvNw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-object-rest-spread@7.29.7':
+ resolution: {integrity: sha512-Ld98jn4c0smUywL57m7SgsHq3OpThOa6LqZJif3G6jYOovPleoFhVrBJ1WegRApSFB2wu4+RelAj9AC9G08Z4A==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-optional-catch-binding@7.29.7':
+ resolution: {integrity: sha512-sLsyndxK2VwX6yNUOakMb7Sh553ZTe/vVM1XJ+9Z5aW1ytsc8xOIwmyk05NNjN60vkc5/KqoTH6hB4V41LJhng==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-optional-chaining@7.27.1':
+ resolution: {integrity: sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-optional-chaining@7.29.7':
+ resolution: {integrity: sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-parameters@7.29.7':
+ resolution: {integrity: sha512-ZDOBqV/qLYJI0YElr8DcENEyARsFQeESqWXH6gZlghYXuPPjvweuDhP4VyEi4BlUBlLRFZVjxoZDMjxhLW766g==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-private-methods@7.29.7':
+ resolution: {integrity: sha512-/6Rz4DK1ETDEM/bWHsPHcaEe7ZaT1EqSXjtSP/L0DijOYuaUhiRiOKcwpZ8P7zR4xXEHc2ITdiCgBm9Tpyv9ug==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-private-property-in-object@7.29.7':
+ resolution: {integrity: sha512-+BNo06dnrzdNNqCm1X6YUaVv0DKk8Q+JYcoZfOkLhYWNCXzlwTSRq8zGWayT1csjcpNXV9CQTBRRbmTLZac5cA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-react-display-name@7.29.7':
+ resolution: {integrity: sha512-+1wdDMGNb4UPeY3Q4L5yLiYe6TXPXubs4NjrgRFw13hPRLJfEMw2Q5OXkee6/IfdqePIeW4Jjwe3aBh7SdKz4Q==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-react-jsx-development@7.29.7':
+ resolution: {integrity: sha512-Xfy3UVMF04+ypnFbkhvfqtmvwfe92qwQdbGZVonhE+6v35GzlofmOnA1szaZqzb9xYWr0nl1e5EMmzi0DNON1g==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
'@babel/plugin-transform-react-jsx-self@7.27.1':
resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==}
engines: {node: '>=6.9.0'}
@@ -1926,6 +2474,96 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
+ '@babel/plugin-transform-react-jsx@7.29.7':
+ resolution: {integrity: sha512-WsZulLVBUHXVj2cUcPVx6UE21TpalB6bHbSFErKT0Ib++ax24jjXe73FqlWvdylFOjiuPHYi6VCcgRad1ItN+A==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-react-pure-annotations@7.29.7':
+ resolution: {integrity: sha512-H5E+HBgDpr6Q5t+Aj11tL7XkIui1jhbIoArVQnqjgXo5/3YxkN7ZEBcWF4RQlB0T4rrxJQbXS6kiFV6B7XTqUA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-regenerator@7.29.7':
+ resolution: {integrity: sha512-rNNFV0DBAJp988xW2DOntfDoYn1eR8GGF5AT5vYc+rjyfaQkM242c9tZUHHPe7KYaiJizXPWhQTzzdbXySyhBw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-runtime@7.29.7':
+ resolution: {integrity: sha512-xmAscdE/AsqRW7vutbPNoUmu/nF5SrLKPs7aoJgEjo35lLKA/Bc0i2rMv/hr1+Y0o1bQCiVtith3u2vdgRL39Q==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-shorthand-properties@7.27.1':
+ resolution: {integrity: sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-shorthand-properties@7.29.7':
+ resolution: {integrity: sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-spread@7.29.7':
+ resolution: {integrity: sha512-/u5K1QWada7tbYNqTjMh96718g9NTwh9tfPJMsSmVsQwGT447FskV+KcfeXkXq2GWki4EM/MuTdmBec+hOuVTQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-sticky-regex@7.29.7':
+ resolution: {integrity: sha512-BCHzNYJGe9l7EpwwDBN/ztlL2NYFFq8hp9ddjtUEM9f2O7S7kKV/lL6Fwo7IF7NSkYhPK2vO+86nIGltA90MsA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-template-literals@7.27.1':
+ resolution: {integrity: sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-typescript@7.29.7':
+ resolution: {integrity: sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-unicode-regex@7.27.1':
+ resolution: {integrity: sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-unicode-regex@7.29.7':
+ resolution: {integrity: sha512-7D/x/23/d/3VqZ0QA+LGbZMlGwZjztBygSWWWsfTPoQ1oQ6Q1P6Mr3d0kk42XabyUVw+fha3LqdRsFqeKqvCyA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/preset-react@7.29.7':
+ resolution: {integrity: sha512-C+PV1TFUPTmBQGoPBL8j2QmLpZ117YTCwxIZeJOM96GbYMFSc7/pOXU5lVykwnZxyTqQxRsvoRk6f2FktZgGHA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/preset-typescript@7.27.1':
+ resolution: {integrity: sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/preset-typescript@7.29.7':
+ resolution: {integrity: sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
'@babel/runtime@7.27.1':
resolution: {integrity: sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==}
engines: {node: '>=6.9.0'}
@@ -1938,6 +2576,10 @@ packages:
resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
engines: {node: '>=6.9.0'}
+ '@babel/template@7.29.7':
+ resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==}
+ engines: {node: '>=6.9.0'}
+
'@babel/traverse@7.27.4':
resolution: {integrity: sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==}
engines: {node: '>=6.9.0'}
@@ -1946,6 +2588,10 @@ packages:
resolution: {integrity: sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==}
engines: {node: '>=6.9.0'}
+ '@babel/traverse@7.29.7':
+ resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==}
+ engines: {node: '>=6.9.0'}
+
'@babel/types@7.27.1':
resolution: {integrity: sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==}
engines: {node: '>=6.9.0'}
@@ -1958,6 +2604,10 @@ packages:
resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==}
engines: {node: '>=6.9.0'}
+ '@babel/types@7.29.7':
+ resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==}
+ engines: {node: '>=6.9.0'}
+
'@bcoe/v8-coverage@1.0.2':
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
engines: {node: '>=18'}
@@ -2341,6 +2991,10 @@ packages:
'@effect/rpc': ^0.71.0
effect: ^3.18.1
+ '@egjs/hammerjs@2.0.17':
+ resolution: {integrity: sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==}
+ engines: {node: '>=0.8.0'}
+
'@emnapi/core@1.10.0':
resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==}
@@ -3608,6 +4262,181 @@ packages:
resolution: {integrity: sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ '@expo-google-fonts/material-symbols@0.4.38':
+ resolution: {integrity: sha512-IJkBtN1o8u9BW5fvSii1MyHPQ7Q0HxbWcVBvOrOzgMLpVtZw7R2w94wBTVR7kZwv3w1JNTESMmLA5Sqn1+Z36A==}
+
+ '@expo/cli@55.0.32':
+ resolution: {integrity: sha512-fq+/yUYBVw5ZudT4igNyJ3WaF17R39iS7EZlrkfHkLI7Y1kmUlivabwKviLoAfepJOKjKODKpViti9EPfmG3SQ==}
+ hasBin: true
+ peerDependencies:
+ expo: '*'
+ expo-router: '*'
+ react-native: '*'
+ peerDependenciesMeta:
+ expo-router:
+ optional: true
+ react-native:
+ optional: true
+
+ '@expo/code-signing-certificates@0.0.6':
+ resolution: {integrity: sha512-iNe0puxwBNEcuua9gmTGzq+SuMDa0iATai1FlFTMHJ/vUmKvN/V//drXoLJkVb5i5H3iE/n/qIJxyoBnXouD0w==}
+
+ '@expo/config-plugins@55.0.10':
+ resolution: {integrity: sha512-1txnRnMLIO5lM/Of/VyvDkCwZap0YFvCyfSTIlUQamhwhx6Rh7r8TXfcIstaDYUQ7X6GTMkNxLXWbcYS6ZAFDw==}
+
+ '@expo/config-types@55.0.5':
+ resolution: {integrity: sha512-sCmSUZG4mZ/ySXvfyyBdhjivz8Q539X1NondwDdYG7s3SBsk+wsgPJzYsqgAG/P9+l0xWjUD2F+kQ1cAJ6NNLg==}
+
+ '@expo/config@55.0.17':
+ resolution: {integrity: sha512-Y3VaRg7Jllg3MhlUOTQqHm6/dttsqcjYlnS9enhAllZvPUpTHnRA4YPETtUZlxkdMJy6y3UZe986pd/KfJ6OTg==}
+
+ '@expo/devcert@1.2.1':
+ resolution: {integrity: sha512-qC4eaxmKMTmJC2ahwyui6ud8f3W60Ss7pMkpBq40Hu3zyiAaugPXnZ24145U7K36qO9UHdZUVxsCvIpz2RYYCA==}
+
+ '@expo/devtools@55.0.3':
+ resolution: {integrity: sha512-KoIDgo0NoXeWLsIcOdZqtAG/1LlsM+JL0DA3bo0vCYaOYTBLXi/ZvRBqa20Ub8D2vKLNa+FgRQW0gRg04Ps1Pg==}
+ peerDependencies:
+ react: '*'
+ react-native: '*'
+ peerDependenciesMeta:
+ react:
+ optional: true
+ react-native:
+ optional: true
+
+ '@expo/dom-webview@55.0.6':
+ resolution: {integrity: sha512-ZNm8tiNEZysxrr36J0x4mOCGyJDcaIvL/3tMxBz0VJIJDcV19xjuJAhJQxHovu+jKx6s9tRyEAINa1mdrzV39g==}
+ peerDependencies:
+ expo: '*'
+ react: '*'
+ react-native: '*'
+
+ '@expo/env@2.1.2':
+ resolution: {integrity: sha512-RJtGFfj/ygO/6zcVbV3cckHf4THcEkv5IZft1GjCB3dfT6axvzvIwXE9EiQqQYmGHcQ+ZrvC8xZcIhiHba0pYg==}
+ engines: {node: '>=20.12.0'}
+
+ '@expo/env@2.3.0':
+ resolution: {integrity: sha512-9HnnIbzwTTdbwSjNLXTk0fPm9ZwMJ7c1/31tsni8HZ8Q62KzYCyspahH+V365vg5J6lr001DzNwBxVWSaYCQLg==}
+ engines: {node: '>=20.12.0'}
+
+ '@expo/fingerprint@0.16.7':
+ resolution: {integrity: sha512-BH8sicYOqZ1iBMwCVEGIz6uTTfylosjc49FoMmCYIzKOiYdiVehsfoYBwyfxwWIiya1VMhm1gv0cgOP8fxHpDw==}
+ hasBin: true
+
+ '@expo/image-utils@0.8.14':
+ resolution: {integrity: sha512-5Sn+jG4Cw+shC2wDMXoqSAJnvERbiwzHn05FpWtD5IBflfTIs5gUmjzwiGVyjOdlMSQhgRrw/AymPbmO9h9mpQ==}
+
+ '@expo/json-file@10.0.15':
+ resolution: {integrity: sha512-xLtsy1820Rf2myhhIc7WmfoUg5cWEJB9tEylhgGhRF/acYGuUXUVkKHYoHY31GbYf6CIZNvipTFxuvWRpVlXTw==}
+
+ '@expo/json-file@10.2.0':
+ resolution: {integrity: sha512-S6XzKe3R9GQeHiUPXc3xJjOv2VJhOEwFYf7xdC2z2cUqt3kZJ9mSO877sNQloVdnW/SUCtPY3bexlM7nwq+CAQ==}
+
+ '@expo/local-build-cache-provider@55.0.13':
+ resolution: {integrity: sha512-Vg5BE10UL+0yg3BVtIeiSoeHU31Qe1m3UxhBPS478ACY1zzKuxZE30x2sym/B2OIWypjmPzXDRt8J9TOGFuFNw==}
+
+ '@expo/log-box@55.0.12':
+ resolution: {integrity: sha512-f9ARS8J60cq3LLNdIqmUjYwyerBzVS5Ecp7KjIf3GOIPjW0571rkcwLz4/U18l/1DeSkSzIkYsNl2TC9oTdWaQ==}
+ peerDependencies:
+ '@expo/dom-webview': ^55.0.6
+ expo: '*'
+ react: '*'
+ react-native: '*'
+
+ '@expo/metro-config@55.0.23':
+ resolution: {integrity: sha512-Mkw3Ss/1LFlafH3iie3r9E13yKMyJgZqGTEkGviGf6LYp51eY5fR8ATbXrNsH69wVc2z+ty4lT/8lEA18YJv7g==}
+ peerDependencies:
+ expo: '*'
+ peerDependenciesMeta:
+ expo:
+ optional: true
+
+ '@expo/metro-runtime@55.0.11':
+ resolution: {integrity: sha512-4KKi/jGrIEXi2YGu0hYTVr0CEeRJy5SXbCrz9+KDZkuD3ROwKNpM1DBawni5rhPVovFnR323HBck9GaxhnfrRw==}
+ peerDependencies:
+ expo: '*'
+ react: '*'
+ react-dom: '*'
+ react-native: '*'
+ peerDependenciesMeta:
+ react-dom:
+ optional: true
+
+ '@expo/metro@55.1.1':
+ resolution: {integrity: sha512-/wfXo5hTuAVpVLG/4hzlmD9NBGJkzkmBEMm/4VICajYRbj7y8OmqqPWbbymzHiBiHB6tI9BnsyXpQM6zVZEECg==}
+
+ '@expo/osascript@2.6.0':
+ resolution: {integrity: sha512-QvqDBlJXa8CS2vRORJ4wEflY1m0vVI07uSJdIRgBrLxRPBcsrXxrtU7+wXRXMqfq9zLwNP9XbvRsXF2omoDylg==}
+ engines: {node: '>=12'}
+
+ '@expo/package-manager@1.12.0':
+ resolution: {integrity: sha512-SWr6093nwBjn94cvElsYZNUnhvs+XtUatUz3h0vAn0IbaWG0B6l/V5ZfOBptX/xq6rMpFG5ibIf/eckLSXw8Gg==}
+
+ '@expo/plist@0.5.4':
+ resolution: {integrity: sha512-Jqppj0FULNq6Zp5JtQrFICl8TtpMjwwUbxEcEC2T3z7m+TOrTQEHZXz3D3Ay7vhbmvD+VMgfWJ4ARclJXeN8Eg==}
+
+ '@expo/prebuild-config@55.0.18':
+ resolution: {integrity: sha512-2oKXyy5pyM87DJqXW5Z+Sakle6rApFFtpPhWOiNsOdoh6rOAD+EqVgyrs2OEEic8CE0tTt27w3SRfSZe/PZrxg==}
+ peerDependencies:
+ expo: '*'
+
+ '@expo/require-utils@55.0.5':
+ resolution: {integrity: sha512-U4K/CQ2VpXuwfNGsN+daKmYOt15hCP8v/pXaYH6eut7kdYZo6SfJ1yr67BIcJ+1Gzzs+QzTxswAZChKpXmceyw==}
+ peerDependencies:
+ typescript: ^5.0.0 || ^5.0.0-0
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ '@expo/router-server@55.0.18':
+ resolution: {integrity: sha512-W0VsvIiR48OvdlAOUlag4qspGYT/DV4srfYowlbYxwZh5Qw0MjiZAID4Zt7F0qynGZZxx8OZPpFhIX7XsqtRmg==}
+ peerDependencies:
+ '@expo/metro-runtime': ^55.0.11
+ expo: '*'
+ expo-constants: ^55.0.16
+ expo-font: ^55.0.8
+ expo-router: '*'
+ expo-server: ^55.0.11
+ react: '*'
+ react-dom: '*'
+ react-server-dom-webpack: ~19.0.1 || ~19.1.2 || ~19.2.1
+ peerDependenciesMeta:
+ '@expo/metro-runtime':
+ optional: true
+ expo-router:
+ optional: true
+ react-dom:
+ optional: true
+ react-server-dom-webpack:
+ optional: true
+
+ '@expo/schema-utils@55.0.4':
+ resolution: {integrity: sha512-65IdeeE8dAZR3n3J5Eq7LYiQ8BFGeEYCWPBCzycvafL7PkskbCyIclTQarRwf/HXFoRvezKCjaLwy/8v9Prk6g==}
+
+ '@expo/sdk-runtime-versions@1.0.0':
+ resolution: {integrity: sha512-Doz2bfiPndXYFPMRwPyGa1k5QaKDVpY806UJj570epIiMzWaYyCtobasyfC++qfIXVb5Ocy7r3tP9d62hAQ7IQ==}
+
+ '@expo/spawn-async@1.8.0':
+ resolution: {integrity: sha512-eb9xxd/LbuEGSdua4NumCu/McVB9EM+F/JxB9pWgnERw4HQ9XyTNH1KapG6oqLWR8TuRK2LQfzJlmNi94CVobw==}
+ engines: {node: '>=12'}
+
+ '@expo/sudo-prompt@9.3.2':
+ resolution: {integrity: sha512-HHQigo3rQWKMDzYDLkubN5WQOYXJJE2eNqIQC2axC2iO3mHdwnIR7FgZVvHWtBwAdzBgAP0ECp8KqS8TiMKvgw==}
+
+ '@expo/vector-icons@15.1.1':
+ resolution: {integrity: sha512-Iu2VkcoI5vygbtYngm7jb4ifxElNVXQYdDrYkT7UCEIiKLeWnQY0wf2ZhHZ+Wro6Sc5TaumpKUOqDRpLi5rkvw==}
+ peerDependencies:
+ expo-font: '>=14.0.4'
+ react: '*'
+ react-native: '*'
+
+ '@expo/ws-tunnel@1.0.6':
+ resolution: {integrity: sha512-nDRbLmSrJar7abvUjp3smDwH8HcbZcoOEa5jVPUv9/9CajgmWw20JNRwTuBRzWIWIkEJDkz20GoNA+tSwUqk0Q==}
+
+ '@expo/xcpretty@4.4.4':
+ resolution: {integrity: sha512-4aQzz9vgxcNXFfo/iyNgDDYfsU5XGKKxWxZopw0cVotHiW+U8IJbIxMaxsINs6bHhtkG3StKNPcOrn3eBuxKPw==}
+ hasBin: true
+
'@fastify/busboy@2.1.1':
resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==}
engines: {node: '>=14'}
@@ -4014,10 +4843,54 @@ packages:
'@isaacs/string-locale-compare@1.1.0':
resolution: {integrity: sha512-SQ7Kzhh9+D+ZW9MA0zkYv3VXhIDNx+LzM6EJ+/65I3QY+enU6Itte7E5XX7EWrqLW2FN4n06GWzBnPoC3th2aQ==}
+ '@isaacs/ttlcache@1.4.1':
+ resolution: {integrity: sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==}
+ engines: {node: '>=12'}
+
+ '@istanbuljs/load-nyc-config@1.1.0':
+ resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==}
+ engines: {node: '>=8'}
+
'@istanbuljs/schema@0.1.3':
resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==}
engines: {node: '>=8'}
+ '@jest/create-cache-key-function@29.7.0':
+ resolution: {integrity: sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+
+ '@jest/diff-sequences@30.4.0':
+ resolution: {integrity: sha512-zOpzlfUs45l6u7jm39qr87JCHUDsaeCtvL+kQe/Vn9jSnRB4/5IPXISm0h9I1vZW/o00Kn4UTJ2MOlhnUGwv3g==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+
+ '@jest/environment@29.7.0':
+ resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+
+ '@jest/fake-timers@29.7.0':
+ resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+
+ '@jest/get-type@30.1.0':
+ resolution: {integrity: sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+
+ '@jest/schemas@29.6.3':
+ resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+
+ '@jest/schemas@30.4.1':
+ resolution: {integrity: sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+
+ '@jest/transform@29.7.0':
+ resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+
+ '@jest/types@29.6.3':
+ resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+
'@jridgewell/gen-mapping@0.3.13':
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
@@ -4907,8 +5780,8 @@ packages:
resolution: {integrity: sha512-JD6DerIKdJGmRp4jQyX5FlrQjA4tjOw1cvfsPAZXfOOEErMUHjPcPSICS+6WnM0nB0efSFARh0KAZss+bvExOA==}
engines: {node: '>=14'}
- '@oxc-project/types@0.130.0':
- resolution: {integrity: sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==}
+ '@oxc-project/types@0.133.0':
+ resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==}
'@oxc-project/types@0.94.0':
resolution: {integrity: sha512-+UgQT/4o59cZfH6Cp7G0hwmqEQ0wE+AdIwhikdwnhWI9Dp8CgSY081+Q3O67/wq3VJu8mgUEB93J9EHHn70fOw==}
@@ -5146,6 +6019,9 @@ packages:
'@radix-ui/primitive@1.1.2':
resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==}
+ '@radix-ui/primitive@1.1.3':
+ resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
+
'@radix-ui/react-arrow@1.1.6':
resolution: {integrity: sha512-2JMfHJf/eVnwq+2dewT3C0acmCWD3XiVA1Da+jTDqo342UlU13WvXtqHhG+yJw5JeQmu4ue2eMy6gcEArLBlcw==}
peerDependencies:
@@ -5488,6 +6364,19 @@ packages:
'@types/react-dom':
optional: true
+ '@radix-ui/react-presence@1.1.5':
+ resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
'@radix-ui/react-primitive@1.0.0':
resolution: {integrity: sha512-EyXe6mnRlHZ8b6f4ilTDrXmkLShICIuOTTj0GX4w1rp+wSxf3+TD05u1UOITC8VsJ2a9nwHvdXtOXEOl0Cw/zQ==}
peerDependencies:
@@ -5520,6 +6409,19 @@ packages:
'@types/react-dom':
optional: true
+ '@radix-ui/react-roving-focus@1.1.11':
+ resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
'@radix-ui/react-roving-focus@1.1.9':
resolution: {integrity: sha512-ZzrIFnMYHHCNqSNCsuN6l7wlewBEq0O0BCSBkabJMFXVO51LRUTq71gLP1UxFvmrXElqmPjA5VX7IqC9VpazAQ==}
peerDependencies:
@@ -5595,6 +6497,19 @@ packages:
'@types/react-dom':
optional: true
+ '@radix-ui/react-tabs@1.1.13':
+ resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
'@radix-ui/react-tooltip@1.2.6':
resolution: {integrity: sha512-zYb+9dc9tkoN2JjBDIIPLQtk3gGyz8FMKoqYTb8EMVQ5a5hBcdHPECrsZVI4NpPAUOixhkoqg7Hj5ry5USowfA==}
peerDependencies:
@@ -5854,6 +6769,119 @@ packages:
peerDependencies:
react: ^18.0 || ^19.0 || ^19.0.0-rc
+ '@react-native/assets-registry@0.83.6':
+ resolution: {integrity: sha512-iljb4ue1yWJ3EhySz7EjV6CzSVrI2uNtR8BI2jzP5+QS5E4Cl3fdIJRmVwDEx1pu8uE97PGEusGRHnoaZ9Q3jg==}
+ engines: {node: '>= 20.19.4'}
+
+ '@react-native/babel-plugin-codegen@0.83.6':
+ resolution: {integrity: sha512-qfRXsHGeucT5c6mK+8Q7v4Ly3zmygfVmFlEtkiq7q07W1OTreld6nib4rJ/DBEeNiKBoBTuHjWliYGNuDjLFQA==}
+ engines: {node: '>= 20.19.4'}
+
+ '@react-native/babel-preset@0.83.6':
+ resolution: {integrity: sha512-4/fXFDUvGOObETZq4+SUFkafld6OGgQWut5cQiqVghlhCB5z/p2lVhPgEUr/aTxTzeS3AmN+ztC+GpYPQ7tsTw==}
+ engines: {node: '>= 20.19.4'}
+ peerDependencies:
+ '@babel/core': '*'
+
+ '@react-native/codegen@0.83.6':
+ resolution: {integrity: sha512-doB/Pq6Cf6IjF3wlQXTIiZOnsX9X8mEEk+CdGfyuCwZjWrf7IB8KaZEXXckJmfUcIwvJ9u/a72ZoTTCIoxAc9A==}
+ engines: {node: '>= 20.19.4'}
+ peerDependencies:
+ '@babel/core': '*'
+
+ '@react-native/community-cli-plugin@0.83.6':
+ resolution: {integrity: sha512-Mko6mywoHYJmpBnjwAC95vQWaUUh//71knFadH0BrhHDq2m7i/IrpLwcQsPAy8855ucXflBs5zQyGTpNbPBAaw==}
+ engines: {node: '>= 20.19.4'}
+ peerDependencies:
+ '@react-native-community/cli': '*'
+ '@react-native/metro-config': '*'
+ peerDependenciesMeta:
+ '@react-native-community/cli':
+ optional: true
+ '@react-native/metro-config':
+ optional: true
+
+ '@react-native/debugger-frontend@0.83.6':
+ resolution: {integrity: sha512-TyWXEpAjVundrc87fPWg91piOUg75+X9iutcfDe7cO3NrAEYCsl7Z09rKHuiAGkxfG9/rFD13dPsYIixUFkSFA==}
+ engines: {node: '>= 20.19.4'}
+
+ '@react-native/debugger-shell@0.83.6':
+ resolution: {integrity: sha512-684TJMBCU0l0ZjJWzrnK0HH+ERaM9KLyxyArE1k7BrP+gVl4X9GO0Pi94RoInOxvW/nyV65sOU6Ip1F3ygS0cg==}
+ engines: {node: '>= 20.19.4'}
+
+ '@react-native/dev-middleware@0.83.6':
+ resolution: {integrity: sha512-22xoddLTelpcVnF385SNH2hdP7X2av5pu7yRl/WnM5jBznbcl0+M9Ce94cj+WVeomsoUF/vlfuB0Ooy+RMlRiA==}
+ engines: {node: '>= 20.19.4'}
+
+ '@react-native/gradle-plugin@0.83.6':
+ resolution: {integrity: sha512-5prXv7WWR1RgZ/kWGZP+mi7/y/IE2ymfOHIZO5Pv14tMOmRAcQSgSYogcRmOiWw5mJs2K0UFeMiQD49ZO9oCug==}
+ engines: {node: '>= 20.19.4'}
+
+ '@react-native/js-polyfills@0.83.6':
+ resolution: {integrity: sha512-VSev0LV2i5X0ibduHBSLqKj0YU2F+waCgjl2uvaGHMGCSV1ZRKNFX/vJFqvLwjvdzLbkAZoFT1Rg7k7jDv44UA==}
+ engines: {node: '>= 20.19.4'}
+
+ '@react-native/normalize-colors@0.74.89':
+ resolution: {integrity: sha512-qoMMXddVKVhZ8PA1AbUCk83trpd6N+1nF2A6k1i6LsQObyS92fELuk8kU/lQs6M7BsMHwqyLCpQJ1uFgNvIQXg==}
+
+ '@react-native/normalize-colors@0.83.6':
+ resolution: {integrity: sha512-bTM24b5v4qN3h52oflnv+OujFORn/kVi06WaWhnQQw14/ycilPqIsqsa+DpIBqdBrXxvLa9fXtCRrQtGATZCEw==}
+
+ '@react-native/virtualized-lists@0.83.6':
+ resolution: {integrity: sha512-gNSFXeb4P7qHtauLvl+zESroULIyX6Ltpvau3dhwy/QmfanBv0KUcrIU/7aVXxtWcXgp+54oWJyu2LIrsZ9+LQ==}
+ engines: {node: '>= 20.19.4'}
+ peerDependencies:
+ '@types/react': ^19.2.0
+ react: '*'
+ react-native: '*'
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@react-navigation/bottom-tabs@7.16.2':
+ resolution: {integrity: sha512-Lbp++BGMc7SQXnyKuO/JrQJIhFH0zyB5v4kIEbnzDJLJfgubd5hoSe+QfCqy4YHfLA4phC4Xf/6Q2Ic8x7datQ==}
+ peerDependencies:
+ '@react-navigation/native': ^7.2.5
+ react: '>= 18.2.0'
+ react-native: '*'
+ react-native-safe-area-context: '>= 4.0.0'
+ react-native-screens: '>= 4.0.0'
+
+ '@react-navigation/core@7.17.5':
+ resolution: {integrity: sha512-6fDCwDTWC7DJn0SDb9DJGRlipaygHIc+2elpZBJI6Crl/2Pu+Z1d6W4jMJ2gZO6iHKf+Pe5sUiQ/uwepGprZtg==}
+ peerDependencies:
+ react: '>= 18.2.0'
+
+ '@react-navigation/elements@2.9.19':
+ resolution: {integrity: sha512-gBUvCZuUkOGw1KpLQEZIkByUz8RYPwXeoA6mZFJy9K1mxd8GdqHDMFCIoB0lfPz9rgrHj99RvtdlGZ/ZzkZv2A==}
+ peerDependencies:
+ '@react-native-masked-view/masked-view': '>= 0.2.0'
+ '@react-navigation/native': ^7.2.5
+ react: '>= 18.2.0'
+ react-native: '*'
+ react-native-safe-area-context: '>= 4.0.0'
+ peerDependenciesMeta:
+ '@react-native-masked-view/masked-view':
+ optional: true
+
+ '@react-navigation/native-stack@7.16.0':
+ resolution: {integrity: sha512-wM21rHYR2XifjDnKLrr3HeHUeGsWQZJRwPqEzy1Vp/a9k3ieiwTGpmpDItD/jtERH9qkYESwDPO6oEtrVBEpQg==}
+ peerDependencies:
+ '@react-navigation/native': ^7.2.5
+ react: '>= 18.2.0'
+ react-native: '*'
+ react-native-safe-area-context: '>= 4.0.0'
+ react-native-screens: '>= 4.0.0'
+
+ '@react-navigation/native@7.2.5':
+ resolution: {integrity: sha512-01AAUQiiHQAfTabq+ZyU1/ZWq+AbB/J3v0CB0UTJSON6M6cuadWNsbChzrZUdqQvHrXvg96U5i2PQLJzK3+zpg==}
+ peerDependencies:
+ react: '>= 18.2.0'
+ react-native: '*'
+
+ '@react-navigation/routers@7.5.5':
+ resolution: {integrity: sha512-9/hhMte12Kgu+pMnLfA4EWJ0OQmIEAMVMX06FPH2yGkEQSQ3JhhCN/GkcRikzQhtEi97VYYQA15umptBUShcOQ==}
+
'@reduxjs/toolkit@2.10.1':
resolution: {integrity: sha512-/U17EXQ9Do9Yx4DlNGU6eVNfZvFJfYpUtRRdLf19PbPjdWBxNlxGZXywQZ1p1Nz8nMkWplTI7iD/23m07nolDA==}
peerDependencies:
@@ -5895,8 +6923,8 @@ packages:
cpu: [arm64]
os: [android]
- '@rolldown/binding-android-arm64@1.0.1':
- resolution: {integrity: sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==}
+ '@rolldown/binding-android-arm64@1.0.3':
+ resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [android]
@@ -5907,8 +6935,8 @@ packages:
cpu: [arm64]
os: [darwin]
- '@rolldown/binding-darwin-arm64@1.0.1':
- resolution: {integrity: sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg==}
+ '@rolldown/binding-darwin-arm64@1.0.3':
+ resolution: {integrity: sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [darwin]
@@ -5919,8 +6947,8 @@ packages:
cpu: [x64]
os: [darwin]
- '@rolldown/binding-darwin-x64@1.0.1':
- resolution: {integrity: sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg==}
+ '@rolldown/binding-darwin-x64@1.0.3':
+ resolution: {integrity: sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [darwin]
@@ -5931,8 +6959,8 @@ packages:
cpu: [x64]
os: [freebsd]
- '@rolldown/binding-freebsd-x64@1.0.1':
- resolution: {integrity: sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw==}
+ '@rolldown/binding-freebsd-x64@1.0.3':
+ resolution: {integrity: sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [freebsd]
@@ -5943,8 +6971,8 @@ packages:
cpu: [arm]
os: [linux]
- '@rolldown/binding-linux-arm-gnueabihf@1.0.1':
- resolution: {integrity: sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ==}
+ '@rolldown/binding-linux-arm-gnueabihf@1.0.3':
+ resolution: {integrity: sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
@@ -5955,8 +6983,8 @@ packages:
cpu: [arm64]
os: [linux]
- '@rolldown/binding-linux-arm64-gnu@1.0.1':
- resolution: {integrity: sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==}
+ '@rolldown/binding-linux-arm64-gnu@1.0.3':
+ resolution: {integrity: sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
@@ -5967,20 +6995,20 @@ packages:
cpu: [arm64]
os: [linux]
- '@rolldown/binding-linux-arm64-musl@1.0.1':
- resolution: {integrity: sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==}
+ '@rolldown/binding-linux-arm64-musl@1.0.3':
+ resolution: {integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
- '@rolldown/binding-linux-ppc64-gnu@1.0.1':
- resolution: {integrity: sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==}
+ '@rolldown/binding-linux-ppc64-gnu@1.0.3':
+ resolution: {integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
- '@rolldown/binding-linux-s390x-gnu@1.0.1':
- resolution: {integrity: sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==}
+ '@rolldown/binding-linux-s390x-gnu@1.0.3':
+ resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
@@ -5991,8 +7019,8 @@ packages:
cpu: [x64]
os: [linux]
- '@rolldown/binding-linux-x64-gnu@1.0.1':
- resolution: {integrity: sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==}
+ '@rolldown/binding-linux-x64-gnu@1.0.3':
+ resolution: {integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
@@ -6003,8 +7031,8 @@ packages:
cpu: [x64]
os: [linux]
- '@rolldown/binding-linux-x64-musl@1.0.1':
- resolution: {integrity: sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==}
+ '@rolldown/binding-linux-x64-musl@1.0.3':
+ resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
@@ -6015,8 +7043,8 @@ packages:
cpu: [arm64]
os: [openharmony]
- '@rolldown/binding-openharmony-arm64@1.0.1':
- resolution: {integrity: sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ==}
+ '@rolldown/binding-openharmony-arm64@1.0.3':
+ resolution: {integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [openharmony]
@@ -6026,8 +7054,8 @@ packages:
engines: {node: '>=14.0.0'}
cpu: [wasm32]
- '@rolldown/binding-wasm32-wasi@1.0.1':
- resolution: {integrity: sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==}
+ '@rolldown/binding-wasm32-wasi@1.0.3':
+ resolution: {integrity: sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [wasm32]
@@ -6037,8 +7065,8 @@ packages:
cpu: [arm64]
os: [win32]
- '@rolldown/binding-win32-arm64-msvc@1.0.1':
- resolution: {integrity: sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw==}
+ '@rolldown/binding-win32-arm64-msvc@1.0.3':
+ resolution: {integrity: sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [win32]
@@ -6055,8 +7083,8 @@ packages:
cpu: [x64]
os: [win32]
- '@rolldown/binding-win32-x64-msvc@1.0.1':
- resolution: {integrity: sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==}
+ '@rolldown/binding-win32-x64-msvc@1.0.3':
+ resolution: {integrity: sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [win32]
@@ -6351,6 +7379,13 @@ packages:
'@shinyoshiaki/jspack@0.0.6':
resolution: {integrity: sha512-SdsNhLjQh4onBlyPrn4ia1Pdx5bXT88G/LIEpOYAjx2u4xeY/m/HB5yHqlkJB1uQR3Zw4R3hBWLj46STRAN0rg==}
+ '@shopify/flash-list@2.0.2':
+ resolution: {integrity: sha512-zhlrhA9eiuEzja4wxVvotgXHtqd3qsYbXkQ3rsBfOgbFA9BVeErpDE/yEwtlIviRGEqpuFj/oU5owD6ByaNX+w==}
+ peerDependencies:
+ '@babel/runtime': '*'
+ react: '*'
+ react-native: '*'
+
'@sigstore/bundle@2.3.2':
resolution: {integrity: sha512-wueKWDk70QixNLB363yHc2D2ItTgYiMTdPwK8D9dKQMR3ZQ0c35IxP5xnwQ8cNLoCgCRcHf14kE+CLIvNX1zmA==}
engines: {node: ^16.14.0 || >=18.0.0}
@@ -6375,6 +7410,12 @@ packages:
resolution: {integrity: sha512-8iKx79/F73DKbGfRf7+t4dqrc0bRr0thdPrxAtCKWRm/F0tG71i6O1rvlnScncJLLBZHn3h8M3c1BSUAb9yu8g==}
engines: {node: ^16.14.0 || >=18.0.0}
+ '@sinclair/typebox@0.27.10':
+ resolution: {integrity: sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==}
+
+ '@sinclair/typebox@0.34.49':
+ resolution: {integrity: sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==}
+
'@sindresorhus/is@4.6.0':
resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==}
engines: {node: '>=10'}
@@ -6391,6 +7432,12 @@ packages:
resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==}
engines: {node: '>=18'}
+ '@sinonjs/commons@3.0.1':
+ resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==}
+
+ '@sinonjs/fake-timers@10.3.0':
+ resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==}
+
'@smithy/abort-controller@4.0.2':
resolution: {integrity: sha512-Sl/78VDtgqKxN2+1qduaVE140XF+Xg+TafkncspwM4jFP/LHr76ZHmIY/y3V1M0mMLNk+Je6IGbzxy23RSToMw==}
engines: {node: '>=18.0.0'}
@@ -7191,10 +8238,10 @@ packages:
react-dom:
optional: true
- '@storybook/builder-vite@10.5.0-alpha.0':
- resolution: {integrity: sha512-4iCteRr03HHLbeD3osey14D8ZQS8hu5OkamlmARR4JLJUF/o6hjxckZ9xD+Ci7jjeGrlGLom71Y+TneN1iLx4g==}
+ '@storybook/builder-vite@10.5.0-alpha.3':
+ resolution: {integrity: sha512-ke/DOtGeLiV6cpNHrM+9gEEsWfplH8gsTQt35prQqaBerPkd2AXsUylpOBUbZdUxtdTdbYWJvYvZpqAeaGiq0A==}
peerDependencies:
- storybook: ^10.5.0-alpha.0
+ storybook: ^10.5.0-alpha.3
vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
'@storybook/core@8.6.12':
@@ -7205,12 +8252,12 @@ packages:
prettier:
optional: true
- '@storybook/csf-plugin@10.5.0-alpha.0':
- resolution: {integrity: sha512-XO6PgW7aldty1BsvwL7HFljELSJK4noZsGqgt+nHXvq46mXyl2OXJUpO+z06DoN8KabQGup//y49aYh4uBm/uw==}
+ '@storybook/csf-plugin@10.5.0-alpha.3':
+ resolution: {integrity: sha512-X5XbPt6Z8zp+xeoCJw91GVnhYE/+y+RbeKIl+DvK6fr9rV6jFSFiXyAivOiOmmxEuD017k8BKWwSmZ2KyI2hmg==}
peerDependencies:
esbuild: '*'
rollup: '*'
- storybook: ^10.5.0-alpha.0
+ storybook: ^10.5.0-alpha.3
vite: '*'
webpack: '*'
peerDependenciesMeta:
@@ -7861,6 +8908,18 @@ packages:
resolution: {integrity: sha512-xGGHpBXYSHUUr6XsKBfs85TWlYKpTc37cSBBVrXcib2MkHLboWlkClhWF37JKlDb9KEq3dHs+f2xR7XJEWGBxA==}
engines: {node: '>=14', npm: '>=6', yarn: '>=1'}
+ '@testing-library/react-native@13.3.3':
+ resolution: {integrity: sha512-k6Mjsd9dbZgvY4Bl7P1NIpePQNi+dfYtlJ5voi9KQlynxSyQkfOgJmYGCYmw/aSgH/rUcFvG8u5gd4npzgRDyg==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ jest: '>=29.0.0'
+ react: '>=18.2.0'
+ react-native: '>=0.71'
+ react-test-renderer: '>=18.2.0'
+ peerDependenciesMeta:
+ jest:
+ optional: true
+
'@testing-library/user-event@14.5.2':
resolution: {integrity: sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==}
engines: {node: '>=12', npm: '>=6'}
@@ -8053,6 +9112,12 @@ packages:
'@types/google-protobuf@3.15.12':
resolution: {integrity: sha512-40um9QqwHjRS92qnOaDpL7RmDK15NuZYo9HihiJRbYkMQZlWnuH8AdvbMy8/o6lgLmKbDUKa+OALCltHdbOTpQ==}
+ '@types/graceful-fs@4.1.9':
+ resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==}
+
+ '@types/hammerjs@2.0.46':
+ resolution: {integrity: sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==}
+
'@types/hast@3.0.4':
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
@@ -8065,6 +9130,15 @@ packages:
'@types/http-errors@2.0.4':
resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==}
+ '@types/istanbul-lib-coverage@2.0.6':
+ resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==}
+
+ '@types/istanbul-lib-report@3.0.3':
+ resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==}
+
+ '@types/istanbul-reports@3.0.4':
+ resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==}
+
'@types/js-cookie@3.0.6':
resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==}
@@ -8156,6 +9230,9 @@ packages:
'@types/react-responsive-masonry@2.6.0':
resolution: {integrity: sha512-MF2ql1CjzOoL9fLWp6L3ABoyzBUP/YV71wyb3Fx+cViYNj7+tq3gDCllZHbLg1LQfGOQOEGbV2P7TOcUeGiR6w==}
+ '@types/react-test-renderer@19.1.0':
+ resolution: {integrity: sha512-XD0WZrHqjNrxA/MaR9O22w/RNidWR9YZmBdRGI7wcnWGrv/3dA8wKCJ8m63Sn+tLJhcjmuhOi629N66W6kgWzQ==}
+
'@types/react-tooltip@4.2.4':
resolution: {integrity: sha512-UzjzmgY/VH3Str6DcAGTLMA1mVVhGOyARNTANExrirtp+JgxhaIOVDxq4TIRmpSi4voLv+w4HA9CC5GvhhCA0A==}
deprecated: This is a stub types definition. react-tooltip provides its own type definitions, so you do not need this installed.
@@ -8184,6 +9261,9 @@ packages:
'@types/shimmer@1.2.0':
resolution: {integrity: sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==}
+ '@types/stack-utils@2.0.3':
+ resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==}
+
'@types/tmp@0.2.6':
resolution: {integrity: sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==}
@@ -8211,6 +9291,12 @@ packages:
'@types/uuid@9.0.8':
resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==}
+ '@types/yargs-parser@21.0.3':
+ resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==}
+
+ '@types/yargs@17.0.35':
+ resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==}
+
'@types/yauzl@2.10.3':
resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==}
@@ -8810,6 +9896,14 @@ packages:
resolution: {integrity: sha512-siPY6BD5dQ2SZPl3I0OZBHL27ZqZvLEosObsZRQ1NUB8qcxegwt0T9eKtV96JMFQpIz1elhkzqOg4c/Ri6Dp9A==}
engines: {node: ^14.14.0 || >=16.0.0}
+ '@xmldom/xmldom@0.8.13':
+ resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==}
+ engines: {node: '>=10.0.0'}
+
+ '@xmldom/xmldom@0.9.10':
+ resolution: {integrity: sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==}
+ engines: {node: '>=14.6'}
+
'@xtuc/ieee754@1.2.0':
resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==}
@@ -8935,6 +10029,9 @@ packages:
ajv@8.17.1:
resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
+ anser@1.4.10:
+ resolution: {integrity: sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==}
+
ansi-align@3.0.1:
resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==}
@@ -8950,6 +10047,10 @@ packages:
resolution: {integrity: sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==}
engines: {node: '>=18'}
+ ansi-regex@4.1.1:
+ resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==}
+ engines: {node: '>=6'}
+
ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
@@ -9092,6 +10193,9 @@ packages:
as-table@1.0.55:
resolution: {integrity: sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==}
+ asap@2.0.6:
+ resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==}
+
asn1js@3.0.6:
resolution: {integrity: sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA==}
engines: {node: '>=12.0.0'}
@@ -9185,11 +10289,81 @@ packages:
babel-dead-code-elimination@1.0.10:
resolution: {integrity: sha512-DV5bdJZTzZ0zn0DC24v3jD7Mnidh6xhKa4GfKCbq3sfW8kaWhDdZjP3i81geA8T33tdYqWKw4D3fVv0CwEgKVA==}
+ babel-jest@29.7.0:
+ resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+ peerDependencies:
+ '@babel/core': ^7.8.0
+
+ babel-plugin-istanbul@6.1.1:
+ resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==}
+ engines: {node: '>=8'}
+
+ babel-plugin-jest-hoist@29.6.3:
+ resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+
babel-plugin-jsx-dom-expressions@0.39.8:
resolution: {integrity: sha512-/MVOIIjonylDXnrWmG23ZX82m9mtKATsVHB7zYlPfDR9Vdd/NBE48if+wv27bSkBtyO7EPMUlcUc4J63QwuACQ==}
peerDependencies:
'@babel/core': ^7.20.12
+ babel-plugin-polyfill-corejs2@0.4.17:
+ resolution: {integrity: sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==}
+ peerDependencies:
+ '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
+
+ babel-plugin-polyfill-corejs3@0.13.0:
+ resolution: {integrity: sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==}
+ peerDependencies:
+ '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
+
+ babel-plugin-polyfill-regenerator@0.6.8:
+ resolution: {integrity: sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==}
+ peerDependencies:
+ '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
+
+ babel-plugin-react-compiler@1.0.0:
+ resolution: {integrity: sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==}
+
+ babel-plugin-react-native-web@0.21.2:
+ resolution: {integrity: sha512-SPD0J6qjJn8231i0HZhlAGH6NORe+QvRSQM2mwQEzJ2Fb3E4ruWTiiicPlHjmeWShDXLcvoorOCXjeR7k/lyWA==}
+
+ babel-plugin-syntax-hermes-parser@0.32.0:
+ resolution: {integrity: sha512-m5HthL++AbyeEA2FcdwOLfVFvWYECOBObLHNqdR8ceY4TsEdn4LdX2oTvbB2QJSSElE2AWA/b2MXZ/PF/CqLZg==}
+
+ babel-plugin-syntax-hermes-parser@0.32.1:
+ resolution: {integrity: sha512-HgErPZTghW76Rkq9uqn5ESeiD97FbqpZ1V170T1RG2RDp+7pJVQV2pQJs7y5YzN0/gcT6GM5ci9apRnIwuyPdQ==}
+
+ babel-plugin-transform-flow-enums@0.0.2:
+ resolution: {integrity: sha512-g4aaCrDDOsWjbm0PUUeVnkcVd6AKJsVc/MbnPhEotEpkeJQP6b8nzewohQi7+QS8UyPehOhGWn0nOwjvWpmMvQ==}
+
+ babel-preset-current-node-syntax@1.2.0:
+ resolution: {integrity: sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==}
+ peerDependencies:
+ '@babel/core': ^7.0.0 || ^8.0.0-0
+
+ babel-preset-expo@55.0.22:
+ resolution: {integrity: sha512-Se6kPnvCNN13jJVIa6JJvlmImVoVRzu9stagAbivCPcfrq2VNrsEiYpJZ1+H32kXinKW/y797/wctGuxPy0APw==}
+ peerDependencies:
+ '@babel/runtime': ^7.20.0
+ expo: '*'
+ expo-widgets: ^55.0.19
+ react-refresh: '>=0.14.0 <1.0.0'
+ peerDependenciesMeta:
+ '@babel/runtime':
+ optional: true
+ expo:
+ optional: true
+ expo-widgets:
+ optional: true
+
+ babel-preset-jest@29.6.3:
+ resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
babel-preset-solid@1.9.6:
resolution: {integrity: sha512-HXTK9f93QxoH8dYn1M2mJdOlWgMsR88Lg/ul6QCZGkNTktjTE5HAf93YxQumHoCudLEtZrU1cFCMFOVho6GqFg==}
peerDependencies:
@@ -9220,6 +10394,11 @@ packages:
engines: {node: '>=6.0.0'}
hasBin: true
+ baseline-browser-mapping@2.10.32:
+ resolution: {integrity: sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
baseline-browser-mapping@2.8.16:
resolution: {integrity: sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw==}
hasBin: true
@@ -9237,6 +10416,10 @@ packages:
bezier-easing@2.1.0:
resolution: {integrity: sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig==}
+ big-integer@1.6.52:
+ resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==}
+ engines: {node: '>=0.6'}
+
bin-links@4.0.4:
resolution: {integrity: sha512-cMtq4W5ZsEwcutJrVId+a/tjt8GSbS+h0oNkdl6+6rBuEv8Ot33Bevj5KPm40t309zuhVic8NjpuL42QCiJWWA==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
@@ -9279,6 +10462,9 @@ packages:
resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==}
engines: {node: '>=18'}
+ boolbase@1.0.0:
+ resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
+
bottleneck@2.19.5:
resolution: {integrity: sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==}
@@ -9289,6 +10475,17 @@ packages:
resolution: {integrity: sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==}
engines: {node: '>=18'}
+ bplist-creator@0.1.0:
+ resolution: {integrity: sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg==}
+
+ bplist-parser@0.3.1:
+ resolution: {integrity: sha512-PyJxiNtA5T2PlLIeBot4lbp7rj4OadzjnMZD/G5zuBNt8ei/yCU7+wW0h2bag9vr8c+/WuRWmSxbqAl9hL1rBA==}
+ engines: {node: '>= 5.10.0'}
+
+ bplist-parser@0.3.2:
+ resolution: {integrity: sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==}
+ engines: {node: '>= 5.10.0'}
+
brace-expansion@1.1.11:
resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
@@ -9316,6 +10513,14 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
+ browserslist@4.28.2:
+ resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==}
+ engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
+ hasBin: true
+
+ bser@2.1.1:
+ resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==}
+
buffer-crc32@0.2.13:
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
@@ -9423,6 +10628,14 @@ packages:
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
engines: {node: '>= 6'}
+ camelcase@5.3.1:
+ resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
+ engines: {node: '>=6'}
+
+ camelcase@6.3.0:
+ resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==}
+ engines: {node: '>=10'}
+
camelcase@8.0.0:
resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==}
engines: {node: '>=16'}
@@ -9433,6 +10646,9 @@ packages:
caniuse-lite@1.0.30001750:
resolution: {integrity: sha512-cuom0g5sdX6rw00qOoLNSFCJ9/mYIsuSOA+yzpDw8eopiFqcVwQvZHqov0vmEighRxX++cfC0Vg1G+1Iy/mSpQ==}
+ caniuse-lite@1.0.30001793:
+ resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==}
+
canvas-confetti@1.9.3:
resolution: {integrity: sha512-rFfTURMvmVEX1gyXFgn5QMn81bYk70qa0HLzcIOSVEyl57n6o9ItHeBtUSWdvKAPY0xlvBHno4/v3QPrT83q9g==}
@@ -9534,10 +10750,25 @@ packages:
'@chromatic-com/playwright':
optional: true
+ chrome-launcher@0.15.2:
+ resolution: {integrity: sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ==}
+ engines: {node: '>=12.13.0'}
+ hasBin: true
+
chrome-trace-event@1.0.4:
resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==}
engines: {node: '>=6.0'}
+ chromium-edge-launcher@0.2.0:
+ resolution: {integrity: sha512-JfJjUnq25y9yg4FABRRVPmBGWPZZi+AQXT4mxupb67766/0UlhG8PAZCz6xzEMXTbW3CsSoE8PcCWA49n35mKg==}
+
+ ci-info@2.0.0:
+ resolution: {integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==}
+
+ ci-info@3.9.0:
+ resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==}
+ engines: {node: '>=8'}
+
citty@0.1.6:
resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==}
@@ -9562,6 +10793,10 @@ packages:
resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==}
engines: {node: '>=10'}
+ cli-cursor@2.1.0:
+ resolution: {integrity: sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==}
+ engines: {node: '>=4'}
+
cli-cursor@5.0.0:
resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==}
engines: {node: '>=18'}
@@ -9654,6 +10889,10 @@ packages:
resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==}
engines: {node: '>=14'}
+ commander@12.1.0:
+ resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==}
+ engines: {node: '>=18'}
+
commander@13.1.0:
resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==}
engines: {node: '>=18'}
@@ -9669,6 +10908,10 @@ packages:
resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==}
engines: {node: '>= 6'}
+ commander@7.2.0:
+ resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==}
+ engines: {node: '>= 10'}
+
commander@8.3.0:
resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==}
engines: {node: '>= 12'}
@@ -9693,6 +10936,14 @@ packages:
resolution: {integrity: sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==}
engines: {node: '>= 14'}
+ compressible@2.0.18:
+ resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==}
+ engines: {node: '>= 0.6'}
+
+ compression@1.8.1:
+ resolution: {integrity: sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==}
+ engines: {node: '>= 0.8.0'}
+
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
@@ -9711,6 +10962,10 @@ packages:
confbox@0.2.2:
resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==}
+ connect@3.7.0:
+ resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==}
+ engines: {node: '>= 0.10.0'}
+
consola@3.4.2:
resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==}
engines: {node: ^14.18.0 || >=16.10.0}
@@ -9761,6 +11016,9 @@ packages:
cookies-next@4.3.0:
resolution: {integrity: sha512-XxeCwLR30cWwRd94sa9X5lRCDLVujtx73tv+N0doQCFIDl83fuuYdxbu/WQUt9aSV7EJx7bkMvJldjvzuFqr4w==}
+ core-js-compat@3.49.0:
+ resolution: {integrity: sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==}
+
core-js@3.42.0:
resolution: {integrity: sha512-Sz4PP4ZA+Rq4II21qkNqOEDTDrCvcANId3xpIgB34NDkWc3UduWj2dqEtN9yZIq8Dk3HyPI33x9sqqU5C8sr0g==}
@@ -9817,6 +11075,20 @@ packages:
crossws@0.3.5:
resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==}
+ css-in-js-utils@3.1.0:
+ resolution: {integrity: sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==}
+
+ css-select@5.2.2:
+ resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==}
+
+ css-tree@1.1.3:
+ resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==}
+ engines: {node: '>=8.0.0'}
+
+ css-what@6.2.2:
+ resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==}
+ engines: {node: '>= 6'}
+
css.escape@1.5.1:
resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==}
@@ -10013,6 +11285,10 @@ packages:
decode-named-character-reference@1.1.0:
resolution: {integrity: sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==}
+ decode-uri-component@0.2.2:
+ resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==}
+ engines: {node: '>=0.10'}
+
decompress-response@6.0.0:
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
engines: {node: '>=10'}
@@ -10191,6 +11467,9 @@ packages:
resolution: {integrity: sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==}
engines: {node: '>=6'}
+ dnssd-advertise@1.1.4:
+ resolution: {integrity: sha512-AmGyK9WpNf06WeP5TjHZq/wNzP76OuEeaiTlKr9E/EEelYLczywUKoqRz+DPRq/ErssjT4lU+/W7wzJW+7K/ZA==}
+
doctrine@2.1.0:
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
engines: {node: '>=0.10.0'}
@@ -10405,6 +11684,9 @@ packages:
electron-to-chromium@1.5.234:
resolution: {integrity: sha512-RXfEp2x+VRYn8jbKfQlRImzoJU01kyDvVPBmG39eU2iuRVhuS6vQNocB8J0/8GrIMLnPzgz4eW6WiRnJkTuNWg==}
+ electron-to-chromium@1.5.363:
+ resolution: {integrity: sha512-VjUKPyWzGnT1fujlkEGC/BvN70Hh70KXtAqcmniXviYlJC/ivcT+BWGPyxWVbJZLfvtKR6dqg1L7T7pgAMBtWA==}
+
emoji-regex-xs@1.0.0:
resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==}
@@ -10597,6 +11879,10 @@ packages:
resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
engines: {node: '>=0.8.0'}
+ escape-string-regexp@2.0.0:
+ resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==}
+ engines: {node: '>=8'}
+
escape-string-regexp@4.0.0:
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
engines: {node: '>=10'}
@@ -10944,6 +12230,226 @@ packages:
resolution: {integrity: sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==}
engines: {node: '>=12.0.0'}
+ expo-asset@55.0.17:
+ resolution: {integrity: sha512-pK9HHJuFqjE8kDUcbMFsZj3Cz8WdXpvZHZmYl7ouFQp59P83BvHln6VnqPDGlO+/4929G0Lm8ZUzbONuNRhi9w==}
+ peerDependencies:
+ expo: '*'
+ react: '*'
+ react-native: '*'
+
+ expo-clipboard@55.0.13:
+ resolution: {integrity: sha512-PrOmmuVsGW4bAkNQmGKtxMXj3invsfN+jfIKmQxHwE/dn7ODqwFWviUTa+PMUjP3XZmYCDLyu/i0GLeu7HF9Ew==}
+ peerDependencies:
+ expo: '*'
+ react: '*'
+ react-native: '*'
+
+ expo-constants@55.0.16:
+ resolution: {integrity: sha512-Z15/No94UHoogD+pulxjudGAeOHTEIWZgb/vnX48Wx5D+apWTeCbnKxQZZtGQlosvduYL5kaic2/W8U+NHfBQQ==}
+ peerDependencies:
+ expo: '*'
+ react-native: '*'
+
+ expo-dev-client@55.0.35:
+ resolution: {integrity: sha512-DN50x9gqWYAfnJpxgiJm3zK2bFvDhxJ5JjFq0wFot7o4knZ7H3BVwiL6zZMHG29g6gfxdgpzGG69WPiSR/Ipgg==}
+ peerDependencies:
+ expo: '*'
+
+ expo-dev-launcher@55.0.36:
+ resolution: {integrity: sha512-Dn2om4J71aavWqi1jLzK3QlGZjDiFv7nIBZkQyzy2zW62IOD9kLwOOvHHj07Ra/6n9cqFEpNYzwpPkR7KHuYZA==}
+ peerDependencies:
+ expo: '*'
+
+ expo-dev-menu-interface@55.0.2:
+ resolution: {integrity: sha512-DomUNvGzY/xliwnMdbAYY780sCv19N7zIbifc0ClcoCzJZpNSCkvJ2qGIFRPyM/7DmqmlHGCKi8di7kYYLKNEg==}
+ peerDependencies:
+ expo: '*'
+
+ expo-dev-menu@55.0.30:
+ resolution: {integrity: sha512-uwDI4cEPzpRemf06Ts5O41azJcz8BBcE6QOkNaTX8JlzdJ05eq9jWxmbA1WhoSoE5C+NFo8njHSvmHqUqTpOng==}
+ peerDependencies:
+ expo: '*'
+
+ expo-document-picker@55.0.13:
+ resolution: {integrity: sha512-IhswJElhdzs3fKDEKW8KXYRoFkWGEsXRMYAZT46Yo56zqqy8yQXrczo33RSwD2hFzNQBdLT97SJL9N311UyS3g==}
+ peerDependencies:
+ expo: '*'
+
+ expo-file-system@55.0.22:
+ resolution: {integrity: sha512-T5Rfv3vqcFyhVrl/tEEeglc/J8LJbcZQgC3TMT5jxzIgUgWmIgJEgncGYqB/YNXFgUTL2LiuCvqrU51Dzp83NQ==}
+ peerDependencies:
+ expo: '*'
+ react-native: '*'
+
+ expo-font@55.0.8:
+ resolution: {integrity: sha512-WyP75pnKqhLNktYwDn3xKAUNt5rLihRDv8XWGhhz6VEhVqypixpT86NA3uGtiDTlM3gGjhrYCY7o7ypXgCUOZg==}
+ peerDependencies:
+ expo: '*'
+ react: '*'
+ react-native: '*'
+
+ expo-glass-effect@55.0.11:
+ resolution: {integrity: sha512-wqq7GUOqSkfoFJzreZvBG0jzjsq5c582m3glhWSjcmIuByxXXWp6j6GY6hyFuYKzpOXhbuvusVxGCQi0yWnp3g==}
+ peerDependencies:
+ expo: '*'
+ react: '*'
+ react-native: '*'
+
+ expo-image-loader@55.0.1:
+ resolution: {integrity: sha512-o8gCo1j59XpXDh0/llgNYPcnfecYQhafQAO0yw5pb+kukPizvNoEqea8tFQIIQmNYqxd6Ljgs7lLXed0gXpOdQ==}
+ peerDependencies:
+ expo: '*'
+
+ expo-image-picker@55.0.20:
+ resolution: {integrity: sha512-lfWt/0rPWdKz8AdDEGmGHZIJSNlVc720Dlx5bfou10FU16ZV5wAbTU63nm2jkXd8hbXke4a/2Ha1dzxCVA+LQQ==}
+ peerDependencies:
+ expo: '*'
+
+ expo-image@55.0.11:
+ resolution: {integrity: sha512-PVIBYQJW/h1f6Zb9xnoWlgfqyOPVm2yb6eo6ZogaKbvMrhb/Q/fiERbagi4oqmR6IPljWPEpkXXQyFBUh7TjpQ==}
+ peerDependencies:
+ expo: '*'
+ react: '*'
+ react-native: '*'
+ react-native-web: '*'
+ peerDependenciesMeta:
+ react-native-web:
+ optional: true
+
+ expo-json-utils@55.0.2:
+ resolution: {integrity: sha512-QJMOZOPOG7CTnKcrdVaiummn2va1MCO56z++eyWkDv3GBRODldM6MFMDf/jTREWthFc2Nxo6TuyWRrEV9S6n/Q==}
+
+ expo-keep-awake@55.0.8:
+ resolution: {integrity: sha512-PfIpMfM+STOBwkR5XOE+yVtER86c44MD+W8QD8JxuO0sT9pF7Y1SJYakWlpvX8xsGA+bjKLxftm9403s9kQhKA==}
+ peerDependencies:
+ expo: '*'
+ react: '*'
+
+ expo-linking@55.0.15:
+ resolution: {integrity: sha512-/RQh2vkNqV8Bim9Owm/evVqn2fqTvCDYHkpYPoSKbLAdydSGdHC2xZNw7Odl4wu1i1/3L4Xz//LKd3NsPWYWBQ==}
+ peerDependencies:
+ react: '*'
+ react-native: '*'
+
+ expo-manifests@55.0.17:
+ resolution: {integrity: sha512-vKZvFivX3usVJKfBODKQcFHso0g38zlGbRGqGAppz+il0zKvG6umpJ47OZbzLod7iJpjd+ZDD2AGuOxacixonA==}
+ peerDependencies:
+ expo: '*'
+
+ expo-media-library@55.0.17:
+ resolution: {integrity: sha512-x/8bdVZAjjB/yitlYZs87qXxxCpJdQBhJ0juXcGHPXCkijNz2sef6BmnbbK5FXg8jxf5nHkG0bIUQgo223hOvw==}
+ peerDependencies:
+ expo: '*'
+ react-native: '*'
+
+ expo-modules-autolinking@55.0.24:
+ resolution: {integrity: sha512-A0OyMbTPZqibYrwqj98HFYTNSvl4NSS4Zt+R5A8qiAx3nM0mc81e6Iqw7Wl4J8M/t36lJ+cT3WuVTz5Oszj6Hw==}
+ hasBin: true
+
+ expo-modules-core@55.0.25:
+ resolution: {integrity: sha512-yXpfg7aHLbuqoXocK34Vua6Aey5SCyqLygAsXAMbul9P8vfBjLpaOPiTJ5cLVF7Drfq8ownqVJO6qpGEtZ6GOw==}
+ peerDependencies:
+ react: '*'
+ react-native: '*'
+ react-native-worklets: 0.7.4
+ peerDependenciesMeta:
+ react-native-worklets:
+ optional: true
+
+ expo-router@55.0.16:
+ resolution: {integrity: sha512-xVwWsDz3Ar2+3hRpMMrZMYFzkJak322vCA5/XCP7WOL0hEXnWhgQGhv5IEYZyz/TXZbl2IYD6/1MnH9mBhjwKQ==}
+ peerDependencies:
+ '@expo/log-box': 55.0.12
+ '@expo/metro-runtime': ^55.0.11
+ '@react-navigation/drawer': ^7.9.4
+ '@testing-library/react-native': '>= 13.2.0'
+ expo: '*'
+ expo-constants: ^55.0.16
+ expo-linking: ^55.0.15
+ react: '*'
+ react-dom: '*'
+ react-native: '*'
+ react-native-gesture-handler: '*'
+ react-native-reanimated: '*'
+ react-native-safe-area-context: '>= 5.4.0'
+ react-native-screens: '*'
+ react-native-web: '*'
+ react-server-dom-webpack: ~19.0.4 || ~19.1.5 || ~19.2.4
+ peerDependenciesMeta:
+ '@react-navigation/drawer':
+ optional: true
+ '@testing-library/react-native':
+ optional: true
+ react-dom:
+ optional: true
+ react-native-gesture-handler:
+ optional: true
+ react-native-reanimated:
+ optional: true
+ react-native-web:
+ optional: true
+ react-server-dom-webpack:
+ optional: true
+
+ expo-secure-store@55.0.14:
+ resolution: {integrity: sha512-OKp9pDiTa4kgChop8+pTRJGBPhkJUcAxP5c6JbivNr4bmx3I+gKmAj1ov4KOXkY95TpWdHO+GQ4+0BgSY2P3JQ==}
+ peerDependencies:
+ expo: '*'
+
+ expo-server@55.0.11:
+ resolution: {integrity: sha512-AxRdHqcv0H1g4s923vu+5n1Nrhne23bjXbP+Vl7+Lwfpe7MG9PuU1IS95IJK6a+7BVV1mRN6QlZvs8Yv7EEXNQ==}
+ engines: {node: '>=20.16.0'}
+
+ expo-sharing@55.0.20:
+ resolution: {integrity: sha512-KhDCsqPmDFFEWM+NlJe+nn0Z48hYeprNpwtNa5wQ4JgXxr2FL7i+AXjLh6v6kdHO2+r6+gkDft/u4fTDgYXdtA==}
+ peerDependencies:
+ expo: '*'
+ react: '*'
+ react-native: '*'
+
+ expo-symbols@55.0.9:
+ resolution: {integrity: sha512-F85C/8ExQjd2gYjasLVKMT8wPj+1+19TVTqg4jAeVjVZklqiQtLO72io9Ji1xAjYNgmDeUI0diVHlFMMTC4Ekg==}
+ peerDependencies:
+ expo: '*'
+ expo-font: '*'
+ react: '*'
+ react-native: '*'
+
+ expo-updates-interface@55.1.6:
+ resolution: {integrity: sha512-evxNpagCkjT3lE6bGV570TFzRtKuIuLY8I37RYHoriXCJ+ZKCN1hbmklK29uAixya+BxGpeTI2K4FqYeJLvfrw==}
+ peerDependencies:
+ expo: '*'
+
+ expo-video@55.0.17:
+ resolution: {integrity: sha512-z2Fqg1WkctD2jpsUoMQU9y6jWYlV+Lwb7nIMB+3fOcKMVCiUwSWS9xq/MQnRImZe6t9gfRFVMvw2Xz8Lk/kUPw==}
+ peerDependencies:
+ expo: '*'
+ react: '*'
+ react-native: '*'
+
+ expo-web-browser@55.0.16:
+ resolution: {integrity: sha512-eeGs3439ewO/Q56Pzg3qbAVZSE0oH/R7XW9VCXI59k0m78ZIYbBtPT4PMFL/+sBgRkXm546Lq/DFcJQPTOfXJg==}
+ peerDependencies:
+ expo: '*'
+ react-native: '*'
+
+ expo@55.0.26:
+ resolution: {integrity: sha512-MuVW6Uzd/Jh6E37ICOYAiTOm9nflNMUNzf6wH5ld/IXFyuF2Lo86a8fCSMgHcvTGsSjRsJ5Uxhf+WHZcvGPfrg==}
+ hasBin: true
+ peerDependencies:
+ '@expo/dom-webview': '*'
+ '@expo/metro-runtime': '*'
+ react: '*'
+ react-native: '*'
+ react-native-webview: '*'
+ peerDependenciesMeta:
+ '@expo/dom-webview':
+ optional: true
+ '@expo/metro-runtime':
+ optional: true
+ react-native-webview:
+ optional: true
+
exponential-backoff@3.1.2:
resolution: {integrity: sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==}
@@ -11043,6 +12549,20 @@ packages:
fastq@1.19.1:
resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
+ fb-dotslash@0.5.8:
+ resolution: {integrity: sha512-XHYLKk9J4BupDxi9bSEhkfss0m+Vr9ChTrjhf9l2iw3jB5C7BnY4GVPoMcqbrTutsKJso6yj2nAB6BI/F2oZaA==}
+ engines: {node: '>=20'}
+ hasBin: true
+
+ fb-watchman@2.0.2:
+ resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==}
+
+ fbjs-css-vars@1.0.2:
+ resolution: {integrity: sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==}
+
+ fbjs@3.0.5:
+ resolution: {integrity: sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==}
+
fd-slicer@1.1.0:
resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==}
@@ -11070,6 +12590,9 @@ packages:
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
engines: {node: ^12.20 || >= 14.13}
+ fetch-nodeshim@0.4.10:
+ resolution: {integrity: sha512-m6I8ALe4L4XpdETy7MJZWs6L1IVMbjs99bwbpIKphxX+0CTns4IKDWJY0LWfr4YsFjfg+z1TjzTMU8lKl8rG0w==}
+
fflate@0.4.8:
resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==}
@@ -11121,10 +12644,18 @@ packages:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
+ filter-obj@1.1.0:
+ resolution: {integrity: sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==}
+ engines: {node: '>=0.10.0'}
+
filter-obj@5.1.0:
resolution: {integrity: sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==}
engines: {node: '>=14.16'}
+ finalhandler@1.1.2:
+ resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==}
+ engines: {node: '>= 0.8'}
+
finalhandler@1.3.2:
resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==}
engines: {node: '>= 0.8'}
@@ -11140,6 +12671,10 @@ packages:
resolution: {integrity: sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==}
engines: {node: '>=18'}
+ find-up@4.1.0:
+ resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
+ engines: {node: '>=8'}
+
find-up@5.0.0:
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
engines: {node: '>=10'}
@@ -11170,6 +12705,9 @@ packages:
flatted@3.3.3:
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
+ flow-enums-runtime@0.0.6:
+ resolution: {integrity: sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==}
+
fn.name@1.1.0:
resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==}
@@ -11182,6 +12720,9 @@ packages:
debug:
optional: true
+ fontfaceobserver@2.3.0:
+ resolution: {integrity: sha512-6FPvD/IVyT4ZlNe7Wcn5Fb/4ChigpucKYSvD6a+0iMoLn2inpo711eyIcKjmDtE5XNcgAkSH9uN/nfAeZzHEfg==}
+
for-each@0.3.5:
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
engines: {node: '>= 0.4'}
@@ -11361,6 +12902,10 @@ packages:
get-tsconfig@4.11.0:
resolution: {integrity: sha512-sNsqf7XKQ38IawiVGPOoAlqZo1DMrO7TU+ZcZwi7yLl7/7S0JwmoBMKz/IkUPhSoXM0Ng3vT0yB1iCe5XavDeQ==}
+ getenv@2.0.0:
+ resolution: {integrity: sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ==}
+ engines: {node: '>=6'}
+
gif.js@0.2.0:
resolution: {integrity: sha512-bYxCoT8OZKmbxY8RN4qDiYuj4nrQDTzgLRcFVovyona1PTWNePzI4nzOmotnlOFIzTk/ZxAHtv+TfVLiBWj/hw==}
@@ -11390,6 +12935,10 @@ packages:
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
hasBin: true
+ glob@13.0.6:
+ resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==}
+ engines: {node: 18 || 20 || >=22}
+
glob@7.1.7:
resolution: {integrity: sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==}
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
@@ -11547,12 +13096,33 @@ packages:
hastscript@9.0.1:
resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==}
+ hermes-compiler@0.14.1:
+ resolution: {integrity: sha512-+RPPQlayoZ9n6/KXKt5SFILWXCGJ/LV5d24L5smXrvTDrPS4L6dSctPczXauuvzFP3QEJbD1YO7Z3Ra4a+4IhA==}
+
hermes-estree@0.25.1:
resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==}
+ hermes-estree@0.32.0:
+ resolution: {integrity: sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ==}
+
+ hermes-estree@0.32.1:
+ resolution: {integrity: sha512-ne5hkuDxheNBAikDjqvCZCwihnz0vVu9YsBzAEO1puiyFR4F1+PAz/SiPHSsNTuOveCYGRMX8Xbx4LOubeC0Qg==}
+
+ hermes-estree@0.35.0:
+ resolution: {integrity: sha512-xVx5Opwy8Oo1I5yGpVRhCvWL/iV3M+ylksSKVNlxxD90cpDpR/AR1jLYqK8HWihm065a6UI3HeyAmYzwS8NOOg==}
+
hermes-parser@0.25.1:
resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==}
+ hermes-parser@0.32.0:
+ resolution: {integrity: sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw==}
+
+ hermes-parser@0.32.1:
+ resolution: {integrity: sha512-175dz634X/W5AiwrpLdoMl/MOb17poLHyIqgyExlE8D9zQ1OPnoORnGMB5ltRKnpvQzBjMYvT2rN/sHeIfZW5Q==}
+
+ hermes-parser@0.35.0:
+ resolution: {integrity: sha512-9JLjeHxBx8T4CAsydZR49PNZUaix+WpQJwu9p2010lu+7Kwl6D/7wYFFJxoz+aXkaaClp9Zfg6W6/zVlSJORaA==}
+
hls.js@0.14.17:
resolution: {integrity: sha512-25A7+m6qqp6UVkuzUQ//VVh2EEOPYlOBg32ypr34bcPO7liBMOkKFvbjbCBfiPAOTA/7BSx1Dujft3Th57WyFg==}
@@ -11562,6 +13132,9 @@ packages:
hls.js@1.6.2:
resolution: {integrity: sha512-rx+pETSCJEDThm/JCm8CuadcAC410cVjb1XVXFNDKFuylaayHk1+tFxhkjvnMDAfqsJHxZXDAJ3Uc2d5xQyWlQ==}
+ hoist-non-react-statics@3.3.2:
+ resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
+
hono@4.12.12:
resolution: {integrity: sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==}
engines: {node: '>=16.9.0'}
@@ -11667,6 +13240,9 @@ packages:
humanize-ms@1.2.1:
resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
+ hyphenate-style-name@1.1.0:
+ resolution: {integrity: sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==}
+
iconv-lite@0.4.24:
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
engines: {node: '>=0.10.0'}
@@ -11705,6 +13281,11 @@ packages:
resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
engines: {node: '>= 4'}
+ image-size@1.2.1:
+ resolution: {integrity: sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==}
+ engines: {node: '>=16.x'}
+ hasBin: true
+
immediate@3.0.6:
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
@@ -11751,6 +13332,9 @@ packages:
inline-style-parser@0.2.4:
resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==}
+ inline-style-prefixer@7.0.1:
+ resolution: {integrity: sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw==}
+
inspect-with-kind@1.0.5:
resolution: {integrity: sha512-MAQUJuIo7Xqk8EVNP+6d3CKq9c80hi4tjIbIAT6lmGW9W6WzlHiu9PS8uSuUYU+Do+j1baiFp3H25XEVxDIG2g==}
@@ -11765,6 +13349,9 @@ packages:
resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
engines: {node: '>=12'}
+ invariant@2.2.4:
+ resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==}
+
ioredis@5.6.1:
resolution: {integrity: sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA==}
engines: {node: '>=12.22.0'}
@@ -12060,6 +13647,10 @@ packages:
resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==}
engines: {node: '>=8'}
+ istanbul-lib-instrument@5.2.1:
+ resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==}
+ engines: {node: '>=8'}
+
istanbul-lib-report@3.0.1:
resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==}
engines: {node: '>=10'}
@@ -12092,10 +13683,57 @@ packages:
engines: {node: '>=10'}
hasBin: true
+ jest-diff@30.4.1:
+ resolution: {integrity: sha512-CRpFK0RtLriVDGcPPAnR6HMVI8bSR2jnUIgralhauzYQZIb4RH9AtEInTuQr65LmmGggGcRT6HIASxwqsVsmlA==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+
+ jest-environment-node@29.7.0:
+ resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+
+ jest-get-type@29.6.3:
+ resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+
+ jest-haste-map@29.7.0:
+ resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+
+ jest-matcher-utils@30.4.1:
+ resolution: {integrity: sha512-zvYfX5CaeEkFrrLS9suWe9rvJrm9J1Iv3ua8kIBv9GEPzcnsfBf0bob37la7s67fs0nlBC3EuvkOLnXQKxtx4A==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+
+ jest-message-util@29.7.0:
+ resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+
+ jest-mock@29.7.0:
+ resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+
+ jest-regex-util@29.6.3:
+ resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+
+ jest-util@29.7.0:
+ resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+
+ jest-validate@29.7.0:
+ resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+
jest-worker@27.5.1:
resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==}
engines: {node: '>= 10.13.0'}
+ jest-worker@29.7.0:
+ resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+
+ jimp-compact@0.16.1:
+ resolution: {integrity: sha512-dZ6Ra7u1G8c4Letq/B5EzAxj4tLFHL+cGtdpR+PVm4yzPDj+lCk+AbivWt1eOM+ikzkowtyV7qSqX6qr3t71Ww==}
+
jiti@1.21.7:
resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
hasBin: true
@@ -12149,6 +13787,9 @@ packages:
jsbn@1.1.0:
resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==}
+ jsc-safe-url@0.2.4:
+ resolution: {integrity: sha512-0wM3YBWtYePOjfyXQH5MWQ8H7sdk5EXSwZvmSLKk2RboVQ2Bu239jycHDz5J/8Blf3K0Qnoy2b6xD+z10MFB+Q==}
+
jsdoc-type-pratt-parser@4.1.0:
resolution: {integrity: sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==}
engines: {node: '>=12.0.0'}
@@ -12259,6 +13900,10 @@ packages:
engines: {node: '>=8'}
hasBin: true
+ lan-network@0.2.1:
+ resolution: {integrity: sha512-ONPnazC96VKDntab9j9JKwIWhZ4ZUceB4A9Epu4Ssg0hYFmtHZSeQ+n15nIwTFmcBUKtExOer8WTJ4GF9MO64A==}
+ hasBin: true
+
language-subtag-registry@0.3.23:
resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==}
@@ -12276,6 +13921,10 @@ packages:
leb@1.0.0:
resolution: {integrity: sha512-Y3c3QZfvKWHX60BVOQPhLCvVGmDYWyJEiINE3drOog6KCyN2AOwvuQQzlS3uJg1J85kzpILXIUwRXULWavir+w==}
+ leven@3.1.0:
+ resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==}
+ engines: {node: '>=6'}
+
levn@0.4.1:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'}
@@ -12283,6 +13932,9 @@ packages:
lie@3.3.0:
resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
+ lighthouse-logger@1.4.2:
+ resolution: {integrity: sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==}
+
lightningcss-android-arm64@1.32.0:
resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
engines: {node: '>= 12.0.0'}
@@ -12384,6 +14036,10 @@ packages:
resolution: {integrity: sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==}
engines: {node: '>=14'}
+ locate-path@5.0.0:
+ resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
+ engines: {node: '>=8'}
+
locate-path@6.0.0:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
@@ -12425,6 +14081,9 @@ packages:
lodash.sortby@4.7.0:
resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==}
+ lodash.throttle@4.1.1:
+ resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==}
+
lodash.truncate@4.4.2:
resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==}
@@ -12434,6 +14093,10 @@ packages:
lodash@4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
+ log-symbols@2.2.0:
+ resolution: {integrity: sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==}
+ engines: {node: '>=4'}
+
log-symbols@6.0.0:
resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==}
engines: {node: '>=18'}
@@ -12546,6 +14209,9 @@ packages:
resolution: {integrity: sha512-cKTUFc/rbKUd/9meOvgrpJ2WrNzymt6jfRDdwg5UCnVzv9dTpEj9JS5m3wtziXVCjluIXyL8pcaukYqezIzZQA==}
engines: {node: ^16.14.0 || >=18.0.0}
+ makeerror@1.0.12:
+ resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==}
+
map-or-similar@1.5.0:
resolution: {integrity: sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==}
@@ -12561,6 +14227,9 @@ packages:
engines: {node: '>= 16'}
hasBin: true
+ marky@1.3.0:
+ resolution: {integrity: sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==}
+
math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
@@ -12618,6 +14287,9 @@ packages:
mdast-util-to-string@4.0.0:
resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==}
+ mdn-data@2.0.14:
+ resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==}
+
media-chrome@4.12.0:
resolution: {integrity: sha512-leItqyy2jn1nk66KGmzeH0z6JXeVUvK95O7ouQTSVdvrTXB/8jtLEI964eXlq5oCZfyPLnPUiuosKrDEfmWDqQ==}
@@ -12638,6 +14310,12 @@ packages:
mediabunny@1.45.2:
resolution: {integrity: sha512-lm34wGClgC263x8SEH5+79Z6aeDcHetoCKMSAeqDhn6Qn4a3A24Bs8uJf9Lxt9h0MEa/uJqZ/5soial/V9TSwQ==}
+ memoize-one@5.2.1:
+ resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==}
+
+ memoize-one@6.0.0:
+ resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==}
+
memoizerific@1.11.3:
resolution: {integrity: sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog==}
@@ -12667,6 +14345,64 @@ packages:
resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==}
engines: {node: '>= 0.6'}
+ metro-babel-transformer@0.83.7:
+ resolution: {integrity: sha512-sBqBkt6kNut/88bv+Ucvm4yqdPetbvAEsHzi3MAgJEifOSYYzX5Z5Kgw3TFOrwf/mHJTOBG2ONlaMHoyfP15TA==}
+ engines: {node: '>=20.19.4'}
+
+ metro-cache-key@0.83.7:
+ resolution: {integrity: sha512-W1c2Nmx8MiJTJt+eWhMO08z9VKi3kZOaz99IYGdqeqDgY9j+yZjXl62rUav4Di0heZfh4/n2s722PqRL1OODeg==}
+ engines: {node: '>=20.19.4'}
+
+ metro-cache@0.83.7:
+ resolution: {integrity: sha512-E9SRePXQ1Zvlj79VcOk57q7VC7rMHMFQ+jhmPHBiq+dJ0bJB5BL87lWZF6oh5X76Cci5tpDuQNaDwwuSCToEeg==}
+ engines: {node: '>=20.19.4'}
+
+ metro-config@0.83.7:
+ resolution: {integrity: sha512-83mjWFbFOt2GeJ6pFIum5mSnc1uTsZJAtD8o4ej0s4NVsYsA7fB+pHvTfHhFrpeMONaobu2riKavkPei05Er/Q==}
+ engines: {node: '>=20.19.4'}
+
+ metro-core@0.83.7:
+ resolution: {integrity: sha512-6yn3w1wnltT6RQl7p7YES2l95ArC+mWrOssEiH8p5/DDrJS65/szf9LsC9JrBv8c5DdvSY3V3f0GRYg0Ox7hCg==}
+ engines: {node: '>=20.19.4'}
+
+ metro-file-map@0.83.7:
+ resolution: {integrity: sha512-+j0F1m+FQYVAQ6syf+mwhIPV5GoFQrkInX8bppuc50IzNsZbMrp8R5H/Sx/K2daQ3YEa9F/XwkeZT8gzJfgeCw==}
+ engines: {node: '>=20.19.4'}
+
+ metro-minify-terser@0.83.7:
+ resolution: {integrity: sha512-MfJar2IS4tBRuLb9svwb0Gu5l9BsH+pcRm8eGcEi/wy8MzZinfinh5dFLt2nWkocnulIgtGB5NkFDdbXqMXKhQ==}
+ engines: {node: '>=20.19.4'}
+
+ metro-resolver@0.83.7:
+ resolution: {integrity: sha512-WSJIENlMcoSsuz66IfBHOkgfp3KJt2UW2TnEHPf1b8pIG2eEXNOVmo2+03A0H17WY2XGXWgxL0CG7FAopqgB1A==}
+ engines: {node: '>=20.19.4'}
+
+ metro-runtime@0.83.7:
+ resolution: {integrity: sha512-9GKkJURaB2iyYoEExKnedzAHzxmKtSi+k0tsZUvMoU27tBZJElchYt7JH/Ai/XzYAI9lCAaV7u5HZSI8J5Z+wQ==}
+ engines: {node: '>=20.19.4'}
+
+ metro-source-map@0.83.7:
+ resolution: {integrity: sha512-JgA1h7oc1a1jydBe1GhVFsUoMYo3wLPk7oRA32rjlDsq+sP2JLt9x2p2lWbNSxTm/u8NV4VRid3hvEJgcX8tKw==}
+ engines: {node: '>=20.19.4'}
+
+ metro-symbolicate@0.83.7:
+ resolution: {integrity: sha512-g4suyxw20WOHWI680c+Kq4wC/NF+Hx5pRH9afrMp+sMTxqLeKcPR1Xf4wMhsjlbvx7LbIREdke6q928jEjvJWw==}
+ engines: {node: '>=20.19.4'}
+ hasBin: true
+
+ metro-transform-plugins@0.83.7:
+ resolution: {integrity: sha512-Ss0FpBiZDjX2kwhukMDl5sNdYK8T/06IPqxNE4H6PTlRlfs9q11cef13c/xESY/Pm4VCkp1yJUZO3kXzvMxQFA==}
+ engines: {node: '>=20.19.4'}
+
+ metro-transform-worker@0.83.7:
+ resolution: {integrity: sha512-UegCo7ygB2fT64mRK2nbAjQVJ1zSwIIHy8d96jJv2nKZFDaViYBiughEdu5HM/Ceq0WN3LZrZk3zhl9aoiLYFw==}
+ engines: {node: '>=20.19.4'}
+
+ metro@0.83.7:
+ resolution: {integrity: sha512-SPaPEyvTsTmd0LpT7RaZciQyDw2i/JB7+iY9L5VfBo72+psescFxBqpI1TL9dnL+pmnfkU+l/J1mEEGLeF65EQ==}
+ engines: {node: '>=20.19.4'}
+ hasBin: true
+
micro-api-client@3.3.0:
resolution: {integrity: sha512-y0y6CUB9RLVsy3kfgayU28746QrNMpSm9O/AYGNsBgOkJr/X/Jk0VLGoO8Ude7Bpa8adywzF+MzXNZRFRsNPhg==}
@@ -12810,6 +14546,10 @@ packages:
engines: {node: '>=16'}
hasBin: true
+ mimic-fn@1.2.0:
+ resolution: {integrity: sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==}
+ engines: {node: '>=4'}
+
mimic-fn@2.1.0:
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
engines: {node: '>=6'}
@@ -12902,6 +14642,10 @@ packages:
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
engines: {node: '>=16 || 14 >=14.17'}
+ minipass@7.1.3:
+ resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==}
+ engines: {node: '>=16 || 14 >=14.17'}
+
minizlib@2.1.2:
resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==}
engines: {node: '>= 8'}
@@ -13001,6 +14745,9 @@ packages:
multipasta@0.2.7:
resolution: {integrity: sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA==}
+ multitars@1.0.0:
+ resolution: {integrity: sha512-H/J4fMLedtudftaYMOg7ajzLYgT3/rwbWVJbqr/iUgB8DQztn38ys5HOqI1CzSxx8QhXXwOOnnBvd4v3jG5+Mg==}
+
mustache@4.2.0:
resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==}
hasBin: true
@@ -13170,6 +14917,10 @@ packages:
resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==}
engines: {node: '>= 6.13.0'}
+ node-forge@1.4.0:
+ resolution: {integrity: sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==}
+ engines: {node: '>= 6.13.0'}
+
node-gyp-build-optional-packages@5.1.1:
resolution: {integrity: sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==}
hasBin: true
@@ -13199,6 +14950,10 @@ packages:
node-releases@2.0.23:
resolution: {integrity: sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==}
+ node-releases@2.0.46:
+ resolution: {integrity: sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==}
+ engines: {node: '>=18'}
+
node-source-walk@6.0.2:
resolution: {integrity: sha512-jn9vOIK/nfqoFCcpK89/VCVaLg1IHE6UVfDOzvqmANaJ/rWCTEdH8RZ1V278nv2jr36BJdyQXIAavBLXpzdlag==}
engines: {node: '>=14'}
@@ -13286,6 +15041,12 @@ packages:
resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==}
deprecated: This package is no longer supported.
+ nth-check@2.1.1:
+ resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
+
+ nullthrows@1.1.1:
+ resolution: {integrity: sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==}
+
number-flow@0.5.7:
resolution: {integrity: sha512-P83Y9rBgN3Xpz5677YDNtuQHZpIldw6WXeWRg0+edrfFthhV7QqRdABas5gtu07QPLvbA8XhfO69rIvbKRzYIg==}
@@ -13300,6 +15061,10 @@ packages:
oauth@0.9.15:
resolution: {integrity: sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==}
+ ob1@0.83.7:
+ resolution: {integrity: sha512-9M5kpuOLyTPogMtZiQUIxdAZxl7Dxs6tVBbJErSumsqGMuhVSoUbkfeZ3XNPpLpwBBtqY5QDUzGwggLHX3slQg==}
+ engines: {node: '>=20.19.4'}
+
object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
@@ -13365,16 +15130,28 @@ packages:
resolution: {integrity: sha512-D7EmwxJV6DsEB6vOFLrBM2OzsVgQzgPWyHlV2OOAVj772n+WTXpudC9e9u5BVKQnYwaD30Ivhi9b+4UeBcGu9g==}
engines: {node: ^10.13.0 || >=12.0.0}
+ on-finished@2.3.0:
+ resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==}
+ engines: {node: '>= 0.8'}
+
on-finished@2.4.1:
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
engines: {node: '>= 0.8'}
+ on-headers@1.1.0:
+ resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==}
+ engines: {node: '>= 0.8'}
+
once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
one-time@1.0.0:
resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==}
+ onetime@2.0.1:
+ resolution: {integrity: sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==}
+ engines: {node: '>=4'}
+
onetime@5.1.2:
resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
engines: {node: '>=6'}
@@ -13400,6 +15177,10 @@ packages:
resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==}
engines: {node: '>=18'}
+ open@7.4.2:
+ resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==}
+ engines: {node: '>=8'}
+
open@8.4.0:
resolution: {integrity: sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==}
engines: {node: '>=12'}
@@ -13422,6 +15203,10 @@ packages:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
+ ora@3.4.0:
+ resolution: {integrity: sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg==}
+ engines: {node: '>=6'}
+
ora@8.2.0:
resolution: {integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==}
engines: {node: '>=18'}
@@ -13446,6 +15231,10 @@ packages:
resolution: {integrity: sha512-dd589iCQ7m1L0bmC5NLlVYfy3TbBEsMUfWx9PyAgPeIcFZ/E2yaTZ4Rz4MiBmmJShviiftHVXOqfnfzJ6kyMrQ==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+ p-limit@2.3.0:
+ resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
+ engines: {node: '>=6'}
+
p-limit@3.1.0:
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
engines: {node: '>=10'}
@@ -13454,6 +15243,10 @@ packages:
resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+ p-locate@4.1.0:
+ resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
+ engines: {node: '>=8'}
+
p-locate@5.0.0:
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
engines: {node: '>=10'}
@@ -13478,6 +15271,10 @@ packages:
resolution: {integrity: sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==}
engines: {node: '>=14.16'}
+ p-try@2.2.0:
+ resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
+ engines: {node: '>=6'}
+
p-wait-for@5.0.2:
resolution: {integrity: sha512-lwx6u1CotQYPVju77R+D0vFomni/AqRfqLmqQ8hekklqZ6gAY9rONh7lBQ0uxWMkC2AuX9b2DVAl8To0NyP1JA==}
engines: {node: '>=12'}
@@ -13528,6 +15325,10 @@ packages:
parse-numeric-range@1.3.0:
resolution: {integrity: sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==}
+ parse-png@2.1.0:
+ resolution: {integrity: sha512-Nt/a5SfCLiTnQAjx3fHlqp8hRgTL3z7kTQZzvIMS9uCAepnCyjpdEc6M/sz69WqMBdaDBw9sF1F1UaHROYzGkQ==}
+ engines: {node: '>=10'}
+
parse5@7.3.0:
resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
@@ -13573,6 +15374,10 @@ packages:
resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==}
engines: {node: 20 || >=22}
+ path-scurry@2.0.2:
+ resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==}
+ engines: {node: 18 || 20 || >=22}
+
path-to-regexp@0.1.13:
resolution: {integrity: sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==}
@@ -13662,10 +15467,18 @@ packages:
player.style@0.1.8:
resolution: {integrity: sha512-r/k2gX1IS3tIH/MQJsXQ0pt54s0NqQ70jSePQSKBlu1Rwf/qqdFUmSKACL10ebS48B0VioclwbvKzBnqgb52Tw==}
+ plist@3.1.1:
+ resolution: {integrity: sha512-ZIfcLJC+7E7FBFnDxm9MPmt7D+DidyQ26lewieO75AdhA2ayMtsJSES0iWzqJQbcVRSrTufQoy0DR94xHue0oA==}
+ engines: {node: '>=10.4.0'}
+
pluralize@8.0.0:
resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
engines: {node: '>=4'}
+ pngjs@3.4.0:
+ resolution: {integrity: sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==}
+ engines: {node: '>=4.0.0'}
+
polished@4.3.1:
resolution: {integrity: sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA==}
engines: {node: '>=10'}
@@ -13806,9 +15619,17 @@ packages:
resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
+ pretty-format@29.7.0:
+ resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+
pretty-format@3.8.0:
resolution: {integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==}
+ pretty-format@30.4.1:
+ resolution: {integrity: sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+
printable-characters@1.0.42:
resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==}
@@ -13858,6 +15679,12 @@ packages:
resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==}
engines: {node: '>=10'}
+ promise@7.3.1:
+ resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==}
+
+ promise@8.3.0:
+ resolution: {integrity: sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==}
+
prompts@2.4.2:
resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
engines: {node: '>= 6'}
@@ -13912,6 +15739,10 @@ packages:
quansync@0.2.11:
resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==}
+ query-string@7.1.3:
+ resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==}
+ engines: {node: '>=6'}
+
querystring@0.2.0:
resolution: {integrity: sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==}
engines: {node: '>=0.4.x'}
@@ -13920,6 +15751,9 @@ packages:
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
+ queue@6.0.2:
+ resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==}
+
quick-lru@5.1.1:
resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==}
engines: {node: '>=10'}
@@ -13963,11 +15797,19 @@ packages:
peerDependencies:
react: ^16.3.0 || ^17.0.1 || ^18.0.0 || ^19.0.0
+ react-devtools-core@6.1.5:
+ resolution: {integrity: sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==}
+
react-dom@19.1.1:
resolution: {integrity: sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==}
peerDependencies:
react: ^19.1.1
+ react-dom@19.2.0:
+ resolution: {integrity: sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==}
+ peerDependencies:
+ react: ^19.2.0
+
react-dom@19.2.4:
resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==}
peerDependencies:
@@ -13989,6 +15831,15 @@ packages:
engines: {node: '>=18.0.0'}
hasBin: true
+ react-fast-compare@3.2.2:
+ resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}
+
+ react-freeze@1.0.4:
+ resolution: {integrity: sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ react: '>=17.0.0'
+
react-hls-player@3.0.7:
resolution: {integrity: sha512-i5QWNyLmaUhV/mgnpljRJT0CBfJnylClV/bne8aiXO3ZqU0+D3U/jtTDwdXM4i5qHhyFy9lemyZ179IgadKd0Q==}
peerDependencies:
@@ -14023,6 +15874,12 @@ packages:
react-is@17.0.2:
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
+ react-is@18.3.1:
+ resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
+
+ react-is@19.2.6:
+ resolution: {integrity: sha512-XjBR15BhXuylgWGuslhDKqlSayuqvqBX91BP8pauG8kd1zY8kotkNWbXksTCNRarse4kuGbe2kIY05ARtwNIvw==}
+
react-loading-skeleton@3.5.0:
resolution: {integrity: sha512-gxxSyLbrEAdXTKgfbpBEFZCO/P153DnqSCQau2+o6lNy1jgMRr2MmRmOzMmyrwSaSYLRB8g7b0waYPmUjz7IhQ==}
peerDependencies:
@@ -14034,6 +15891,73 @@ packages:
'@types/react': '>=18'
react: '>=18'
+ react-native-gesture-handler@2.30.1:
+ resolution: {integrity: sha512-xIUBDo5ktmJs++0fZlavQNvDEE4PsihWhSeJsJtoz4Q6p0MiTM9TgrTgfEgzRR36qGPytFoeq+ShLrVwGdpUdA==}
+ peerDependencies:
+ react: '*'
+ react-native: '*'
+
+ react-native-is-edge-to-edge@1.2.1:
+ resolution: {integrity: sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q==}
+ peerDependencies:
+ react: '*'
+ react-native: '*'
+
+ react-native-is-edge-to-edge@1.3.1:
+ resolution: {integrity: sha512-NIXU/iT5+ORyCc7p0z2nnlkouYKX425vuU1OEm6bMMtWWR9yvb+Xg5AZmImTKoF9abxCPqrKC3rOZsKzUYgYZA==}
+ peerDependencies:
+ react: '*'
+ react-native: '*'
+
+ react-native-reanimated@4.2.1:
+ resolution: {integrity: sha512-/NcHnZMyOvsD/wYXug/YqSKw90P9edN0kEPL5lP4PFf1aQ4F1V7MKe/E0tvfkXKIajy3Qocp5EiEnlcrK/+BZg==}
+ peerDependencies:
+ react: '*'
+ react-native: '*'
+ react-native-worklets: 0.7.4
+
+ react-native-safe-area-context@5.6.2:
+ resolution: {integrity: sha512-4XGqMNj5qjUTYywJqpdWZ9IG8jgkS3h06sfVjfw5yZQZfWnRFXczi0GnYyFyCc2EBps/qFmoCH8fez//WumdVg==}
+ peerDependencies:
+ react: '*'
+ react-native: '*'
+
+ react-native-screens@4.23.0:
+ resolution: {integrity: sha512-XhO3aK0UeLpBn4kLecd+J+EDeRRJlI/Ro9Fze06vo1q163VeYtzfU9QS09/VyDFMWR1qxDC1iazCArTPSFFiPw==}
+ peerDependencies:
+ react: '*'
+ react-native: '*'
+
+ react-native-svg@15.15.5:
+ resolution: {integrity: sha512-L4go5jA+GWutdJ/JucuN20cjAbMg1HmMtAP+wZ+3JLCf6Jd0bhXQHxciRP/AQm/FlrIEZwkMcHNZP+FXAiic0w==}
+ peerDependencies:
+ react: '*'
+ react-native: '*'
+
+ react-native-web@0.21.2:
+ resolution: {integrity: sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg==}
+ peerDependencies:
+ react: ^18.0.0 || ^19.0.0
+ react-dom: ^18.0.0 || ^19.0.0
+
+ react-native-worklets@0.7.4:
+ resolution: {integrity: sha512-NYOdM1MwBb3n+AtMqy1tFy3Mn8DliQtd8sbzAVRf9Gc+uvQ0zRfxN7dS8ZzoyX7t6cyQL5THuGhlnX+iFlQTag==}
+ peerDependencies:
+ '@babel/core': '*'
+ react: '*'
+ react-native: '*'
+
+ react-native@0.83.6:
+ resolution: {integrity: sha512-H513+8VzviNFXOdPnStRzX9S3/jiJGg++QZ1zd+ROyAvBEKqFqKUPHH0d82y3QyRPct5qKjdOa7J6vNehCvXYA==}
+ engines: {node: '>= 20.19.4'}
+ hasBin: true
+ peerDependencies:
+ '@types/react': ^19.1.1
+ react: ^19.2.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
react-promise-suspense@0.3.4:
resolution: {integrity: sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==}
@@ -14049,6 +15973,10 @@ packages:
redux:
optional: true
+ react-refresh@0.14.2:
+ resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
+ engines: {node: '>=0.10.0'}
+
react-refresh@0.17.0:
resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
engines: {node: '>=0.10.0'}
@@ -14119,6 +16047,11 @@ packages:
'@types/react':
optional: true
+ react-test-renderer@19.2.0:
+ resolution: {integrity: sha512-zLCFMHFE9vy/w3AxO0zNxy6aAupnCuLSVOJYDe/Tp+ayGI1f2PLQsFVPANSD42gdSbmYx5oN+1VWDhcXtq7hAQ==}
+ peerDependencies:
+ react: ^19.2.0
+
react-tooltip@5.28.1:
resolution: {integrity: sha512-ZA4oHwoIIK09TS7PvSLFcRlje1wGZaxw6xHvfrzn6T82UcMEfEmHVCad16Gnr4NDNDh93HyN037VK4HDi5odfQ==}
peerDependencies:
@@ -14129,6 +16062,10 @@ packages:
resolution: {integrity: sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==}
engines: {node: '>=0.10.0'}
+ react@19.2.0:
+ resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==}
+ engines: {node: '>=0.10.0'}
+
react@19.2.4:
resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==}
engines: {node: '>=0.10.0'}
@@ -14229,6 +16166,16 @@ packages:
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
engines: {node: '>= 0.4'}
+ regenerate-unicode-properties@10.2.2:
+ resolution: {integrity: sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==}
+ engines: {node: '>=4'}
+
+ regenerate@1.4.2:
+ resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==}
+
+ regenerator-runtime@0.13.11:
+ resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}
+
regex-recursion@5.1.1:
resolution: {integrity: sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w==}
@@ -14252,6 +16199,17 @@ packages:
resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==}
engines: {node: '>=8'}
+ regexpu-core@6.4.0:
+ resolution: {integrity: sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==}
+ engines: {node: '>=4'}
+
+ regjsgen@0.8.0:
+ resolution: {integrity: sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==}
+
+ regjsparser@0.13.1:
+ resolution: {integrity: sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw==}
+ hasBin: true
+
rehype-parse@9.0.1:
resolution: {integrity: sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==}
@@ -14325,11 +16283,19 @@ packages:
resolve-pkg-maps@1.0.0:
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
+ resolve-workspace-root@2.0.1:
+ resolution: {integrity: sha512-nR23LHAvaI6aHtMg6RWoaHpdR4D881Nydkzi2CixINyg9T00KgaJdJI6Vwty+Ps8WLxZHuxsS0BseWjxSA4C+w==}
+
resolve@1.22.10:
resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==}
engines: {node: '>= 0.4'}
hasBin: true
+ resolve@1.22.12:
+ resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==}
+ engines: {node: '>= 0.4'}
+ hasBin: true
+
resolve@2.0.0-next.5:
resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==}
hasBin: true
@@ -14341,6 +16307,10 @@ packages:
resolution: {integrity: sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==}
engines: {node: '>=14.16'}
+ restore-cursor@2.0.0:
+ resolution: {integrity: sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==}
+ engines: {node: '>=4'}
+
restore-cursor@5.1.0:
resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==}
engines: {node: '>=18'}
@@ -14382,8 +16352,8 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
- rolldown@1.0.1:
- resolution: {integrity: sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==}
+ rolldown@1.0.3:
+ resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
@@ -14502,6 +16472,11 @@ packages:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
hasBin: true
+ semver@7.6.3:
+ resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==}
+ engines: {node: '>=10'}
+ hasBin: true
+
semver@7.7.1:
resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==}
engines: {node: '>=10'}
@@ -14533,6 +16508,10 @@ packages:
seq-queue@0.0.5:
resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==}
+ serialize-error@2.1.0:
+ resolution: {integrity: sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==}
+ engines: {node: '>=0.10.0'}
+
serialize-javascript@6.0.2:
resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==}
@@ -14581,6 +16560,13 @@ packages:
setprototypeof@1.2.0:
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
+ sf-symbols-typescript@2.2.0:
+ resolution: {integrity: sha512-TPbeg0b7ylrswdGCji8FRGFAKuqbpQlLbL8SOle3j1iHSs5Ob5mhvMAxWN2UItOjgALAB5Zp3fmMfj8mbWvXKw==}
+ engines: {node: '>=10'}
+
+ shallowequal@1.1.0:
+ resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==}
+
sharp@0.33.5:
resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
@@ -14640,6 +16626,9 @@ packages:
resolution: {integrity: sha512-8G+/XDU8wNsJOQS5ysDVO0Etg9/2uA5gR9l4ZwijjlwxBcrU6RPfwi2+jJmbP+Ap1Hlp/nVAaEO4Fj22/SL2gQ==}
engines: {node: ^16.14.0 || >=18.0.0}
+ simple-plist@1.3.1:
+ resolution: {integrity: sha512-iMSw5i0XseMnrhtIzRb7XpQEXepa9xhWxGUojHBL43SIpQuDQkh3Wpy67ZbDzZVr6EKxvwVChnVpdl8hEVLDiw==}
+
simple-swizzle@0.2.2:
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
@@ -14662,6 +16651,10 @@ packages:
resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==}
engines: {node: '>=10'}
+ slugify@1.6.9:
+ resolution: {integrity: sha512-vZ7rfeehZui7wQs438JXBckYLkIIdfHOXsaVEUMyS5fHo1483l1bMdo0EDSWYclY0yZKFOipDy4KHuKs6ssvdg==}
+ engines: {node: '>=8.0.0'}
+
smart-buffer@4.2.0:
resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
@@ -14760,6 +16753,10 @@ packages:
source-map-support@0.5.21:
resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
+ source-map@0.5.7:
+ resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==}
+ engines: {node: '>=0.10.0'}
+
source-map@0.6.1:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
engines: {node: '>=0.10.0'}
@@ -14792,6 +16789,10 @@ packages:
spdx-license-ids@3.0.21:
resolution: {integrity: sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==}
+ split-on-first@1.1.0:
+ resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==}
+ engines: {node: '>=6'}
+
sprintf-js@1.0.3:
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
@@ -14856,18 +16857,30 @@ packages:
stack-trace@0.0.10:
resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==}
+ stack-utils@2.0.6:
+ resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==}
+ engines: {node: '>=10'}
+
stackback@0.0.2:
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
stackframe@1.3.4:
resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==}
+ stacktrace-parser@0.1.11:
+ resolution: {integrity: sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==}
+ engines: {node: '>=6'}
+
stacktracey@2.1.8:
resolution: {integrity: sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==}
standard-as-callback@2.1.0:
resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==}
+ statuses@1.5.0:
+ resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==}
+ engines: {node: '>= 0.6'}
+
statuses@2.0.1:
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
engines: {node: '>= 0.8'}
@@ -14921,9 +16934,17 @@ packages:
prettier:
optional: true
+ stream-buffers@2.2.0:
+ resolution: {integrity: sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==}
+ engines: {node: '>= 0.10.0'}
+
streamx@2.22.0:
resolution: {integrity: sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw==}
+ strict-uri-encode@2.0.0:
+ resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==}
+ engines: {node: '>=4'}
+
string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
@@ -14968,6 +16989,10 @@ packages:
stringify-entities@4.0.4:
resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
+ strip-ansi@5.2.0:
+ resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==}
+ engines: {node: '>=6'}
+
strip-ansi@6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
@@ -15022,6 +17047,9 @@ packages:
resolution: {integrity: sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==}
engines: {node: '>=18'}
+ structured-headers@0.4.1:
+ resolution: {integrity: sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg==}
+
style-to-js@1.1.16:
resolution: {integrity: sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw==}
@@ -15044,6 +17072,9 @@ packages:
babel-plugin-macros:
optional: true
+ styleq@0.1.3:
+ resolution: {integrity: sha512-3ZUifmCDCQanjeej1f6kyl/BeP/Vae5EYkQ9iJfUm/QwZvlgnZzyflqAsAWYURdtea8Vkvswu2GrC57h3qffcA==}
+
subtitles-parser-vtt@0.1.0:
resolution: {integrity: sha512-+y3GOvLL+71JLMFFjqSi4p0J9ddSbhpXKaWG6vHUT8PqPZmlhyAsfu0LP248FdVGfwNIj77wIgVkfQ2xwCZ4+Q==}
@@ -15071,6 +17102,10 @@ packages:
resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
engines: {node: '>=10'}
+ supports-hyperlinks@2.3.0:
+ resolution: {integrity: sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==}
+ engines: {node: '>=8'}
+
supports-hyperlinks@4.4.0:
resolution: {integrity: sha512-UKbpT93hN5Nr9go5UY7bopIB9YQlMz9nm/ct4IXt/irb5YRkn9WaqrOBJGZ5Pwvsd5FQzSVeYlGdXoCAPQZrPg==}
engines: {node: '>=20'}
@@ -15149,6 +17184,10 @@ packages:
tauri-plugin-positioner-api@0.2.7:
resolution: {integrity: sha512-jwqRHo59UU3aJbffEFkWVhBorjQg1WNeDa4W4eWVnaTqLals+/fqgHdNwTGzG1+LLdaJSS2FUy4XSwEDAWvERQ==}
+ terminal-link@2.1.1:
+ resolution: {integrity: sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==}
+ engines: {node: '>=8'}
+
terminal-link@5.0.0:
resolution: {integrity: sha512-qFAy10MTMwjzjU8U16YS4YoZD+NQLHzLssFMNqgravjbvIPNiqkGFR4yjhJfmY9R5OFU7+yHxc6y+uGHkKwLRA==}
engines: {node: '>=20'}
@@ -15185,6 +17224,10 @@ packages:
engines: {node: '>=10'}
hasBin: true
+ test-exclude@6.0.0:
+ resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==}
+ engines: {node: '>=8'}
+
test-exclude@7.0.1:
resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==}
engines: {node: '>=18'}
@@ -15205,6 +17248,9 @@ packages:
thenify@3.3.1:
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
+ throat@5.0.0:
+ resolution: {integrity: sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==}
+
through@2.3.8:
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
@@ -15280,6 +17326,9 @@ packages:
resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==}
engines: {node: '>=14.14'}
+ tmpl@1.0.5:
+ resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==}
+
to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
@@ -15299,6 +17348,9 @@ packages:
toml@3.0.0:
resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==}
+ toqr@0.1.1:
+ resolution: {integrity: sha512-FWAPzCIHZHnrE/5/w9MPk0kK25hSQSH2IKhYh9PyjS3SG/+IEMvlwIHbhz+oF7xl54I+ueZlVnMjyzdSwLmAwA==}
+
totalist@3.0.1:
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
engines: {node: '>=6'}
@@ -15490,6 +17542,10 @@ packages:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
+ type-detect@4.0.8:
+ resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==}
+ engines: {node: '>=4'}
+
type-fest@0.20.2:
resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==}
engines: {node: '>=10'}
@@ -15498,6 +17554,10 @@ packages:
resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==}
engines: {node: '>=10'}
+ type-fest@0.7.1:
+ resolution: {integrity: sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==}
+ engines: {node: '>=8'}
+
type-fest@4.41.0:
resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==}
engines: {node: '>=16'}
@@ -15541,6 +17601,11 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
+ typescript@5.9.3:
+ resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
ua-parser-js@1.0.41:
resolution: {integrity: sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==}
hasBin: true
@@ -15625,6 +17690,22 @@ packages:
unenv@2.0.0-rc.15:
resolution: {integrity: sha512-J/rEIZU8w6FOfLNz/hNKsnY+fFHWnu9MH4yRbSZF3xbbGHovcetXPs7sD+9p8L6CeNC//I9bhRYAOsBt2u7/OA==}
+ unicode-canonical-property-names-ecmascript@2.0.1:
+ resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==}
+ engines: {node: '>=4'}
+
+ unicode-match-property-ecmascript@2.0.0:
+ resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==}
+ engines: {node: '>=4'}
+
+ unicode-match-property-value-ecmascript@2.2.1:
+ resolution: {integrity: sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==}
+ engines: {node: '>=4'}
+
+ unicode-property-aliases-ecmascript@2.2.0:
+ resolution: {integrity: sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==}
+ engines: {node: '>=4'}
+
unicorn-magic@0.1.0:
resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==}
engines: {node: '>=18'}
@@ -15856,6 +17937,12 @@ packages:
peerDependencies:
browserslist: '>= 4.21.0'
+ update-browserslist-db@1.2.3:
+ resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
+ hasBin: true
+ peerDependencies:
+ browserslist: '>= 4.21.0'
+
uqr@0.1.2:
resolution: {integrity: sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA==}
@@ -15889,6 +17976,11 @@ packages:
peerDependencies:
react: '>=16.8.0'
+ use-latest-callback@0.2.6:
+ resolution: {integrity: sha512-FvRG9i1HSo0wagmX63Vrm8SnlUU3LMM3WyZkQ76RnslpBrX694AdG4A0zQBx2B3ZifFA0yv/BaEHGBnEax5rZg==}
+ peerDependencies:
+ react: '>=16.8'
+
use-resize-observer@9.1.0:
resolution: {integrity: sha512-R25VqO9Wb3asSD4eqtcxk8sJalvIOYBqS8MNZlpDSQ4l4xMQxC/J7Id9HoTqPq8FwULIn0PVW+OAqF2dyYbjow==}
peerDependencies:
@@ -15929,6 +18021,11 @@ packages:
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
hasBin: true
+ uuid@7.0.3:
+ resolution: {integrity: sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==}
+ deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
+ hasBin: true
+
uuid@8.0.0:
resolution: {integrity: sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==}
deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
@@ -15972,6 +18069,12 @@ packages:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'}
+ vaul@1.1.2:
+ resolution: {integrity: sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==}
+ peerDependencies:
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc
+
vfile-location@5.0.3:
resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==}
@@ -16209,6 +18312,9 @@ packages:
jsdom:
optional: true
+ vlq@1.0.1:
+ resolution: {integrity: sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==}
+
w3c-xmlserializer@5.0.0:
resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
engines: {node: '>=18'}
@@ -16216,6 +18322,12 @@ packages:
walk-up-path@3.0.1:
resolution: {integrity: sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA==}
+ walker@1.0.8:
+ resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==}
+
+ warn-once@0.1.1:
+ resolution: {integrity: sha512-VkQZJbO8zVImzYFteBXvBOZEl1qL175WH8VmZcxF2fZAoudNhNDvHi+doCaAEdU2l2vtcIwa2zn0QK5+I1HQ3Q==}
+
watchpack@2.5.1:
resolution: {integrity: sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==}
engines: {node: '>=10.13.0'}
@@ -16298,10 +18410,16 @@ packages:
engines: {node: '>=18'}
deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation
+ whatwg-fetch@3.6.20:
+ resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==}
+
whatwg-mimetype@4.0.0:
resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
engines: {node: '>=18'}
+ whatwg-url-minimum@0.1.2:
+ resolution: {integrity: sha512-XPEm0XFQWNVG292lII1PrRRJl3sItrs7CettZ4ncYxuDVpLyy+NwlGyut2hXI0JswcJUxeCH+CyOJK0ZzAXD6A==}
+
whatwg-url@14.2.0:
resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==}
engines: {node: '>=18'}
@@ -16423,6 +18541,10 @@ packages:
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
+ write-file-atomic@4.0.2:
+ resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==}
+ engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
+
write-file-atomic@5.0.1:
resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
@@ -16431,6 +18553,18 @@ packages:
resolution: {integrity: sha512-GmqrO8WJ1NuzJ2DrziEI2o57jKAVIQNf8a18W3nCYU3H7PNWqCCVTeH6/NQE93CIllIgQS98rrmVkYgTX9fFJQ==}
engines: {node: ^18.17.0 || >=20.5.0}
+ ws@7.5.11:
+ resolution: {integrity: sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==}
+ engines: {node: '>=8.3.0'}
+ peerDependencies:
+ bufferutil: ^4.0.1
+ utf-8-validate: ^5.0.2
+ peerDependenciesMeta:
+ bufferutil:
+ optional: true
+ utf-8-validate:
+ optional: true
+
ws@8.17.1:
resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==}
engines: {node: '>=10.0.0'}
@@ -16483,6 +18617,10 @@ packages:
resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==}
engines: {node: '>=18'}
+ xcode@3.0.1:
+ resolution: {integrity: sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA==}
+ engines: {node: '>=10.0.0'}
+
xdg-app-paths@5.1.0:
resolution: {integrity: sha512-RAQ3WkPf4KTU1A8RtFx3gWywzVKe00tfOPFfl2NDGqbIFENQO4kqAJp7mhQjNj/33W5x5hiWWUdyfPq/5SU3QA==}
engines: {node: '>=6'}
@@ -16495,6 +18633,10 @@ packages:
resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==}
engines: {node: '>=18'}
+ xml2js@0.6.0:
+ resolution: {integrity: sha512-eLTh0kA8uHceqesPqSE+VvO1CDDJWMwlQfB6LuN6T8w6MaDJ8Txm8P7s5cHD0miF0V+GGTZrDQfxPZQVsur33w==}
+ engines: {node: '>=4.0.0'}
+
xml2js@0.6.2:
resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==}
engines: {node: '>=4.0.0'}
@@ -16503,6 +18645,10 @@ packages:
resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==}
engines: {node: '>=4.0'}
+ xmlbuilder@15.1.1:
+ resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==}
+ engines: {node: '>=8.0'}
+
xmlchars@2.2.0:
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
@@ -17773,8 +19919,16 @@ snapshots:
js-tokens: 4.0.0
picocolors: 1.1.1
+ '@babel/code-frame@7.29.7':
+ dependencies:
+ '@babel/helper-validator-identifier': 7.29.7
+ js-tokens: 4.0.0
+ picocolors: 1.1.1
+
'@babel/compat-data@7.27.2': {}
+ '@babel/compat-data@7.29.7': {}
+
'@babel/core@7.27.1':
dependencies:
'@ampproject/remapping': 2.3.0
@@ -17819,6 +19973,18 @@ snapshots:
'@jridgewell/trace-mapping': 0.3.31
jsesc: 3.1.0
+ '@babel/generator@7.29.7':
+ dependencies:
+ '@babel/parser': 7.29.7
+ '@babel/types': 7.29.7
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+ jsesc: 3.1.0
+
+ '@babel/helper-annotate-as-pure@7.29.7':
+ dependencies:
+ '@babel/types': 7.29.7
+
'@babel/helper-compilation-targets@7.27.2':
dependencies:
'@babel/compat-data': 7.27.2
@@ -17827,11 +19993,59 @@ snapshots:
lru-cache: 5.1.1
semver: 6.3.1
+ '@babel/helper-compilation-targets@7.29.7':
+ dependencies:
+ '@babel/compat-data': 7.29.7
+ '@babel/helper-validator-option': 7.29.7
+ browserslist: 4.26.3
+ lru-cache: 5.1.1
+ semver: 6.3.1
+
+ '@babel/helper-create-class-features-plugin@7.29.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-annotate-as-pure': 7.29.7
+ '@babel/helper-member-expression-to-functions': 7.29.7
+ '@babel/helper-optimise-call-expression': 7.29.7
+ '@babel/helper-replace-supers': 7.29.7(@babel/core@7.27.1)
+ '@babel/helper-skip-transparent-expression-wrappers': 7.29.7
+ '@babel/traverse': 7.29.7
+ semver: 6.3.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-create-regexp-features-plugin@7.29.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-annotate-as-pure': 7.29.7
+ regexpu-core: 6.4.0
+ semver: 6.3.1
+
+ '@babel/helper-define-polyfill-provider@0.6.8(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-compilation-targets': 7.29.7
+ '@babel/helper-plugin-utils': 7.29.7
+ debug: 4.4.3(supports-color@8.1.1)
+ lodash.debounce: 4.0.8
+ resolve: 1.22.12
+ transitivePeerDependencies:
+ - supports-color
+
'@babel/helper-globals@7.28.0': {}
+ '@babel/helper-globals@7.29.7': {}
+
+ '@babel/helper-member-expression-to-functions@7.29.7':
+ dependencies:
+ '@babel/traverse': 7.29.7
+ '@babel/types': 7.29.7
+ transitivePeerDependencies:
+ - supports-color
+
'@babel/helper-module-imports@7.18.6':
dependencies:
- '@babel/types': 7.27.6
+ '@babel/types': 7.28.4
'@babel/helper-module-imports@7.27.1':
dependencies:
@@ -17840,6 +20054,13 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@babel/helper-module-imports@7.29.7':
+ dependencies:
+ '@babel/traverse': 7.29.7
+ '@babel/types': 7.29.7
+ transitivePeerDependencies:
+ - supports-color
+
'@babel/helper-module-transforms@7.27.1(@babel/core@7.27.1)':
dependencies:
'@babel/core': 7.27.1
@@ -17849,14 +20070,68 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@babel/helper-module-transforms@7.29.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-module-imports': 7.29.7
+ '@babel/helper-validator-identifier': 7.29.7
+ '@babel/traverse': 7.29.7
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-optimise-call-expression@7.29.7':
+ dependencies:
+ '@babel/types': 7.29.7
+
'@babel/helper-plugin-utils@7.27.1': {}
+ '@babel/helper-plugin-utils@7.29.7': {}
+
+ '@babel/helper-remap-async-to-generator@7.29.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-annotate-as-pure': 7.29.7
+ '@babel/helper-wrap-function': 7.29.7
+ '@babel/traverse': 7.29.7
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-replace-supers@7.29.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-member-expression-to-functions': 7.29.7
+ '@babel/helper-optimise-call-expression': 7.29.7
+ '@babel/traverse': 7.29.7
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-skip-transparent-expression-wrappers@7.29.7':
+ dependencies:
+ '@babel/traverse': 7.29.7
+ '@babel/types': 7.29.7
+ transitivePeerDependencies:
+ - supports-color
+
'@babel/helper-string-parser@7.27.1': {}
+ '@babel/helper-string-parser@7.29.7': {}
+
'@babel/helper-validator-identifier@7.27.1': {}
+ '@babel/helper-validator-identifier@7.29.7': {}
+
'@babel/helper-validator-option@7.27.1': {}
+ '@babel/helper-validator-option@7.29.7': {}
+
+ '@babel/helper-wrap-function@7.29.7':
+ dependencies:
+ '@babel/template': 7.29.7
+ '@babel/traverse': 7.29.7
+ '@babel/types': 7.29.7
+ transitivePeerDependencies:
+ - supports-color
+
'@babel/helpers@7.27.1':
dependencies:
'@babel/template': 7.27.2
@@ -17881,16 +20156,367 @@ snapshots:
dependencies:
'@babel/types': 7.28.4
+ '@babel/parser@7.29.7':
+ dependencies:
+ '@babel/types': 7.29.7
+
+ '@babel/plugin-proposal-decorators@7.29.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.27.1)
+ '@babel/helper-plugin-utils': 7.29.7
+ '@babel/plugin-syntax-decorators': 7.29.7(@babel/core@7.27.1)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-proposal-export-default-from@7.29.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.29.7
+
+ '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.27.1
+
+ '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.27.1
+
+ '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.27.1
+
+ '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.27.1
+
+ '@babel/plugin-syntax-decorators@7.29.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.29.7
+
+ '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.27.1
+
+ '@babel/plugin-syntax-export-default-from@7.29.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.29.7
+
+ '@babel/plugin-syntax-flow@7.29.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.29.7
+
+ '@babel/plugin-syntax-import-attributes@7.29.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.29.7
+
+ '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.27.1
+
+ '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.27.1
+
'@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.27.1)':
dependencies:
'@babel/core': 7.27.1
'@babel/helper-plugin-utils': 7.27.1
+ '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.29.7
+
+ '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.27.1
+
+ '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.27.1
+
+ '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.27.1
+
+ '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.27.1
+
+ '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.27.1
+
+ '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.27.1
+
+ '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.27.1
+
+ '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.27.1
+
'@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.27.1)':
dependencies:
'@babel/core': 7.27.1
'@babel/helper-plugin-utils': 7.27.1
+ '@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.29.7
+
+ '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.27.1
+
+ '@babel/plugin-transform-arrow-functions@7.29.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.29.7
+
+ '@babel/plugin-transform-async-generator-functions@7.29.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.29.7
+ '@babel/helper-remap-async-to-generator': 7.29.7(@babel/core@7.27.1)
+ '@babel/traverse': 7.29.7
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-async-to-generator@7.29.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-module-imports': 7.29.7
+ '@babel/helper-plugin-utils': 7.29.7
+ '@babel/helper-remap-async-to-generator': 7.29.7(@babel/core@7.27.1)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-block-scoping@7.29.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.29.7
+
+ '@babel/plugin-transform-class-properties@7.27.1(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.27.1)
+ '@babel/helper-plugin-utils': 7.27.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-class-properties@7.29.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.27.1)
+ '@babel/helper-plugin-utils': 7.29.7
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-class-static-block@7.29.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.27.1)
+ '@babel/helper-plugin-utils': 7.29.7
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-classes@7.28.4(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-annotate-as-pure': 7.29.7
+ '@babel/helper-compilation-targets': 7.27.2
+ '@babel/helper-globals': 7.28.0
+ '@babel/helper-plugin-utils': 7.27.1
+ '@babel/helper-replace-supers': 7.29.7(@babel/core@7.27.1)
+ '@babel/traverse': 7.28.4
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-classes@7.29.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-annotate-as-pure': 7.29.7
+ '@babel/helper-compilation-targets': 7.29.7
+ '@babel/helper-globals': 7.29.7
+ '@babel/helper-plugin-utils': 7.29.7
+ '@babel/helper-replace-supers': 7.29.7(@babel/core@7.27.1)
+ '@babel/traverse': 7.29.7
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-computed-properties@7.29.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.29.7
+ '@babel/template': 7.29.7
+
+ '@babel/plugin-transform-destructuring@7.29.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.29.7
+ '@babel/traverse': 7.29.7
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-export-namespace-from@7.29.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.29.7
+
+ '@babel/plugin-transform-flow-strip-types@7.29.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.29.7
+ '@babel/plugin-syntax-flow': 7.29.7(@babel/core@7.27.1)
+
+ '@babel/plugin-transform-for-of@7.29.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.29.7
+ '@babel/helper-skip-transparent-expression-wrappers': 7.29.7
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-function-name@7.29.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-compilation-targets': 7.29.7
+ '@babel/helper-plugin-utils': 7.29.7
+ '@babel/traverse': 7.29.7
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-literals@7.29.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.29.7
+
+ '@babel/plugin-transform-logical-assignment-operators@7.29.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.29.7
+
+ '@babel/plugin-transform-modules-commonjs@7.29.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-module-transforms': 7.29.7(@babel/core@7.27.1)
+ '@babel/helper-plugin-utils': 7.29.7
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-named-capturing-groups-regex@7.29.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.27.1)
+ '@babel/helper-plugin-utils': 7.29.7
+
+ '@babel/plugin-transform-nullish-coalescing-operator@7.27.1(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.27.1
+
+ '@babel/plugin-transform-nullish-coalescing-operator@7.29.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.29.7
+
+ '@babel/plugin-transform-numeric-separator@7.29.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.29.7
+
+ '@babel/plugin-transform-object-rest-spread@7.29.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-compilation-targets': 7.29.7
+ '@babel/helper-plugin-utils': 7.29.7
+ '@babel/plugin-transform-destructuring': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-transform-parameters': 7.29.7(@babel/core@7.27.1)
+ '@babel/traverse': 7.29.7
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-optional-catch-binding@7.29.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.29.7
+
+ '@babel/plugin-transform-optional-chaining@7.27.1(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.27.1
+ '@babel/helper-skip-transparent-expression-wrappers': 7.29.7
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-optional-chaining@7.29.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.29.7
+ '@babel/helper-skip-transparent-expression-wrappers': 7.29.7
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-parameters@7.29.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.29.7
+
+ '@babel/plugin-transform-private-methods@7.29.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.27.1)
+ '@babel/helper-plugin-utils': 7.29.7
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-private-property-in-object@7.29.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-annotate-as-pure': 7.29.7
+ '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.27.1)
+ '@babel/helper-plugin-utils': 7.29.7
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-react-display-name@7.29.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.29.7
+
+ '@babel/plugin-transform-react-jsx-development@7.29.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/plugin-transform-react-jsx': 7.29.7(@babel/core@7.27.1)
+ transitivePeerDependencies:
+ - supports-color
+
'@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.27.1)':
dependencies:
'@babel/core': 7.27.1
@@ -17901,6 +20527,125 @@ snapshots:
'@babel/core': 7.27.1
'@babel/helper-plugin-utils': 7.27.1
+ '@babel/plugin-transform-react-jsx@7.29.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-annotate-as-pure': 7.29.7
+ '@babel/helper-module-imports': 7.29.7
+ '@babel/helper-plugin-utils': 7.29.7
+ '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.27.1)
+ '@babel/types': 7.29.7
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-react-pure-annotations@7.29.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-annotate-as-pure': 7.29.7
+ '@babel/helper-plugin-utils': 7.29.7
+
+ '@babel/plugin-transform-regenerator@7.29.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.29.7
+
+ '@babel/plugin-transform-runtime@7.29.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-module-imports': 7.29.7
+ '@babel/helper-plugin-utils': 7.29.7
+ babel-plugin-polyfill-corejs2: 0.4.17(@babel/core@7.27.1)
+ babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.27.1)
+ babel-plugin-polyfill-regenerator: 0.6.8(@babel/core@7.27.1)
+ semver: 6.3.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.27.1
+
+ '@babel/plugin-transform-shorthand-properties@7.29.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.29.7
+
+ '@babel/plugin-transform-spread@7.29.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.29.7
+ '@babel/helper-skip-transparent-expression-wrappers': 7.29.7
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-sticky-regex@7.29.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.29.7
+
+ '@babel/plugin-transform-template-literals@7.27.1(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.27.1
+
+ '@babel/plugin-transform-typescript@7.29.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-annotate-as-pure': 7.29.7
+ '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.27.1)
+ '@babel/helper-plugin-utils': 7.29.7
+ '@babel/helper-skip-transparent-expression-wrappers': 7.29.7
+ '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.27.1)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-unicode-regex@7.27.1(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.27.1)
+ '@babel/helper-plugin-utils': 7.27.1
+
+ '@babel/plugin-transform-unicode-regex@7.29.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.27.1)
+ '@babel/helper-plugin-utils': 7.29.7
+
+ '@babel/preset-react@7.29.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.29.7
+ '@babel/helper-validator-option': 7.29.7
+ '@babel/plugin-transform-react-display-name': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-transform-react-jsx': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-transform-react-jsx-development': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-transform-react-pure-annotations': 7.29.7(@babel/core@7.27.1)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/preset-typescript@7.27.1(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.27.1
+ '@babel/helper-validator-option': 7.27.1
+ '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.27.1)
+ '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.27.1)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/preset-typescript@7.29.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.29.7
+ '@babel/helper-validator-option': 7.29.7
+ '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.27.1)
+ transitivePeerDependencies:
+ - supports-color
+
'@babel/runtime@7.27.1': {}
'@babel/standalone@7.27.2': {}
@@ -17911,6 +20656,12 @@ snapshots:
'@babel/parser': 7.27.5
'@babel/types': 7.27.1
+ '@babel/template@7.29.7':
+ dependencies:
+ '@babel/code-frame': 7.29.7
+ '@babel/parser': 7.29.7
+ '@babel/types': 7.29.7
+
'@babel/traverse@7.27.4':
dependencies:
'@babel/code-frame': 7.27.1
@@ -17935,6 +20686,18 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@babel/traverse@7.29.7':
+ dependencies:
+ '@babel/code-frame': 7.29.7
+ '@babel/generator': 7.29.7
+ '@babel/helper-globals': 7.29.7
+ '@babel/parser': 7.29.7
+ '@babel/template': 7.29.7
+ '@babel/types': 7.29.7
+ debug: 4.4.3(supports-color@8.1.1)
+ transitivePeerDependencies:
+ - supports-color
+
'@babel/types@7.27.1':
dependencies:
'@babel/helper-string-parser': 7.27.1
@@ -17950,6 +20713,11 @@ snapshots:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.27.1
+ '@babel/types@7.29.7':
+ dependencies:
+ '@babel/helper-string-parser': 7.29.7
+ '@babel/helper-validator-identifier': 7.29.7
+
'@bcoe/v8-coverage@1.0.2': {}
'@biomejs/biome@2.2.0':
@@ -18304,6 +21072,10 @@ snapshots:
'@effect/rpc': 0.71.0(@effect/platform@0.92.1(effect@3.18.4))(effect@3.18.4)
effect: 3.18.4
+ '@egjs/hammerjs@2.0.17':
+ dependencies:
+ '@types/hammerjs': 2.0.46
+
'@emnapi/core@1.10.0':
dependencies:
'@emnapi/wasi-threads': 1.2.1
@@ -19033,6 +21805,366 @@ snapshots:
'@eslint/core': 0.15.1
levn: 0.4.1
+ '@expo-google-fonts/material-symbols@0.4.38': {}
+
+ '@expo/cli@55.0.32(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-constants@55.0.16)(expo-font@55.0.8)(expo-router@55.0.16)(expo@55.0.26)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)':
+ dependencies:
+ '@expo/code-signing-certificates': 0.0.6
+ '@expo/config': 55.0.17(typescript@5.9.3)
+ '@expo/config-plugins': 55.0.10
+ '@expo/devcert': 1.2.1
+ '@expo/env': 2.1.2
+ '@expo/image-utils': 0.8.14(typescript@5.9.3)
+ '@expo/json-file': 10.2.0
+ '@expo/log-box': 55.0.12(@expo/dom-webview@55.0.6)(expo@55.0.26)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ '@expo/metro': 55.1.1
+ '@expo/metro-config': 55.0.23(expo@55.0.26)(typescript@5.9.3)
+ '@expo/osascript': 2.6.0
+ '@expo/package-manager': 1.12.0
+ '@expo/plist': 0.5.4
+ '@expo/prebuild-config': 55.0.18(expo@55.0.26)(typescript@5.9.3)
+ '@expo/require-utils': 55.0.5(typescript@5.9.3)
+ '@expo/router-server': 55.0.18(@expo/metro-runtime@55.0.11)(expo-constants@55.0.16)(expo-font@55.0.8)(expo-router@55.0.16)(expo-server@55.0.11)(expo@55.0.26)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ '@expo/schema-utils': 55.0.4
+ '@expo/spawn-async': 1.8.0
+ '@expo/ws-tunnel': 1.0.6
+ '@expo/xcpretty': 4.4.4
+ '@react-native/dev-middleware': 0.83.6
+ accepts: 1.3.8
+ arg: 5.0.2
+ better-opn: 3.0.2
+ bplist-creator: 0.1.0
+ bplist-parser: 0.3.2
+ chalk: 4.1.2
+ ci-info: 3.9.0
+ compression: 1.8.1
+ connect: 3.7.0
+ debug: 4.4.3(supports-color@8.1.1)
+ dnssd-advertise: 1.1.4
+ expo: 55.0.26(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.16)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ expo-server: 55.0.11
+ fetch-nodeshim: 0.4.10
+ getenv: 2.0.0
+ glob: 13.0.6
+ lan-network: 0.2.1
+ multitars: 1.0.0
+ node-forge: 1.4.0
+ npm-package-arg: 11.0.3
+ ora: 3.4.0
+ picomatch: 4.0.3
+ pretty-format: 29.7.0
+ progress: 2.0.3
+ prompts: 2.4.2
+ resolve-from: 5.0.0
+ semver: 7.7.4
+ send: 0.19.0
+ slugify: 1.6.9
+ source-map-support: 0.5.21
+ stacktrace-parser: 0.1.11
+ structured-headers: 0.4.1
+ terminal-link: 2.1.1
+ toqr: 0.1.1
+ wrap-ansi: 7.0.0
+ ws: 8.18.3
+ zod: 3.25.76
+ optionalDependencies:
+ expo-router: 55.0.16(d69165032b49371b387000fe555903ea)
+ react-native: 0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0)
+ transitivePeerDependencies:
+ - '@expo/dom-webview'
+ - '@expo/metro-runtime'
+ - bufferutil
+ - expo-constants
+ - expo-font
+ - react
+ - react-dom
+ - react-server-dom-webpack
+ - supports-color
+ - typescript
+ - utf-8-validate
+
+ '@expo/code-signing-certificates@0.0.6':
+ dependencies:
+ node-forge: 1.4.0
+
+ '@expo/config-plugins@55.0.10':
+ dependencies:
+ '@expo/config-types': 55.0.5
+ '@expo/json-file': 10.0.15
+ '@expo/plist': 0.5.4
+ '@expo/sdk-runtime-versions': 1.0.0
+ chalk: 4.1.2
+ debug: 4.4.3(supports-color@8.1.1)
+ getenv: 2.0.0
+ glob: 13.0.6
+ resolve-from: 5.0.0
+ semver: 7.7.4
+ slugify: 1.6.9
+ xcode: 3.0.1
+ xml2js: 0.6.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@expo/config-types@55.0.5': {}
+
+ '@expo/config@55.0.17(typescript@5.9.3)':
+ dependencies:
+ '@expo/config-plugins': 55.0.10
+ '@expo/config-types': 55.0.5
+ '@expo/json-file': 10.2.0
+ '@expo/require-utils': 55.0.5(typescript@5.9.3)
+ deepmerge: 4.3.1
+ getenv: 2.0.0
+ glob: 13.0.6
+ resolve-workspace-root: 2.0.1
+ semver: 7.7.4
+ slugify: 1.6.9
+ transitivePeerDependencies:
+ - supports-color
+ - typescript
+
+ '@expo/devcert@1.2.1':
+ dependencies:
+ '@expo/sudo-prompt': 9.3.2
+ debug: 3.2.7
+ transitivePeerDependencies:
+ - supports-color
+
+ '@expo/devtools@55.0.3(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ chalk: 4.1.2
+ optionalDependencies:
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0)
+
+ '@expo/dom-webview@55.0.6(expo@55.0.26)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ expo: 55.0.26(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.16)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0)
+
+ '@expo/env@2.1.2':
+ dependencies:
+ chalk: 4.1.2
+ debug: 4.4.3(supports-color@8.1.1)
+ getenv: 2.0.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@expo/env@2.3.0':
+ dependencies:
+ chalk: 4.1.2
+ debug: 4.4.3(supports-color@8.1.1)
+ getenv: 2.0.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@expo/fingerprint@0.16.7':
+ dependencies:
+ '@expo/env': 2.3.0
+ '@expo/spawn-async': 1.8.0
+ arg: 5.0.2
+ chalk: 4.1.2
+ debug: 4.4.3(supports-color@8.1.1)
+ getenv: 2.0.0
+ glob: 13.0.6
+ ignore: 5.3.2
+ minimatch: 10.2.4
+ resolve-from: 5.0.0
+ semver: 7.7.4
+ transitivePeerDependencies:
+ - supports-color
+
+ '@expo/image-utils@0.8.14(typescript@5.9.3)':
+ dependencies:
+ '@expo/require-utils': 55.0.5(typescript@5.9.3)
+ '@expo/spawn-async': 1.8.0
+ chalk: 4.1.2
+ getenv: 2.0.0
+ jimp-compact: 0.16.1
+ parse-png: 2.1.0
+ semver: 7.7.4
+ transitivePeerDependencies:
+ - supports-color
+ - typescript
+
+ '@expo/json-file@10.0.15':
+ dependencies:
+ '@babel/code-frame': 7.27.1
+ json5: 2.2.3
+
+ '@expo/json-file@10.2.0':
+ dependencies:
+ '@babel/code-frame': 7.27.1
+ json5: 2.2.3
+
+ '@expo/local-build-cache-provider@55.0.13(typescript@5.9.3)':
+ dependencies:
+ '@expo/config': 55.0.17(typescript@5.9.3)
+ chalk: 4.1.2
+ transitivePeerDependencies:
+ - supports-color
+ - typescript
+
+ '@expo/log-box@55.0.12(@expo/dom-webview@55.0.6)(expo@55.0.26)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ '@expo/dom-webview': 55.0.6(expo@55.0.26)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ anser: 1.4.10
+ expo: 55.0.26(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.16)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0)
+ stacktrace-parser: 0.1.11
+
+ '@expo/metro-config@55.0.23(expo@55.0.26)(typescript@5.9.3)':
+ dependencies:
+ '@babel/code-frame': 7.27.1
+ '@babel/core': 7.27.1
+ '@babel/generator': 7.28.3
+ '@expo/config': 55.0.17(typescript@5.9.3)
+ '@expo/env': 2.1.2
+ '@expo/json-file': 10.0.15
+ '@expo/metro': 55.1.1
+ '@expo/spawn-async': 1.8.0
+ browserslist: 4.26.3
+ chalk: 4.1.2
+ debug: 4.4.3(supports-color@8.1.1)
+ getenv: 2.0.0
+ glob: 13.0.6
+ hermes-parser: 0.32.1
+ jsc-safe-url: 0.2.4
+ lightningcss: 1.32.0
+ picomatch: 4.0.3
+ postcss: 8.5.14
+ resolve-from: 5.0.0
+ optionalDependencies:
+ expo: 55.0.26(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.16)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ transitivePeerDependencies:
+ - bufferutil
+ - supports-color
+ - typescript
+ - utf-8-validate
+
+ '@expo/metro-runtime@55.0.11(@expo/dom-webview@55.0.6)(expo@55.0.26)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ '@expo/log-box': 55.0.12(@expo/dom-webview@55.0.6)(expo@55.0.26)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ anser: 1.4.10
+ expo: 55.0.26(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.16)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ pretty-format: 29.7.0
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0)
+ stacktrace-parser: 0.1.11
+ whatwg-fetch: 3.6.20
+ optionalDependencies:
+ react-dom: 19.2.0(react@19.2.0)
+ transitivePeerDependencies:
+ - '@expo/dom-webview'
+
+ '@expo/metro@55.1.1':
+ dependencies:
+ metro: 0.83.7
+ metro-babel-transformer: 0.83.7
+ metro-cache: 0.83.7
+ metro-cache-key: 0.83.7
+ metro-config: 0.83.7
+ metro-core: 0.83.7
+ metro-file-map: 0.83.7
+ metro-minify-terser: 0.83.7
+ metro-resolver: 0.83.7
+ metro-runtime: 0.83.7
+ metro-source-map: 0.83.7
+ metro-symbolicate: 0.83.7
+ metro-transform-plugins: 0.83.7
+ metro-transform-worker: 0.83.7
+ transitivePeerDependencies:
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+
+ '@expo/osascript@2.6.0':
+ dependencies:
+ '@expo/spawn-async': 1.8.0
+
+ '@expo/package-manager@1.12.0':
+ dependencies:
+ '@expo/json-file': 10.2.0
+ '@expo/spawn-async': 1.8.0
+ chalk: 4.1.2
+ npm-package-arg: 11.0.3
+ ora: 3.4.0
+ resolve-workspace-root: 2.0.1
+
+ '@expo/plist@0.5.4':
+ dependencies:
+ '@xmldom/xmldom': 0.8.13
+ base64-js: 1.5.1
+ xmlbuilder: 15.1.1
+
+ '@expo/prebuild-config@55.0.18(expo@55.0.26)(typescript@5.9.3)':
+ dependencies:
+ '@expo/config': 55.0.17(typescript@5.9.3)
+ '@expo/config-plugins': 55.0.10
+ '@expo/config-types': 55.0.5
+ '@expo/image-utils': 0.8.14(typescript@5.9.3)
+ '@expo/json-file': 10.2.0
+ '@react-native/normalize-colors': 0.83.6
+ debug: 4.4.3(supports-color@8.1.1)
+ expo: 55.0.26(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.16)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ resolve-from: 5.0.0
+ semver: 7.7.4
+ xml2js: 0.6.0
+ transitivePeerDependencies:
+ - supports-color
+ - typescript
+
+ '@expo/require-utils@55.0.5(typescript@5.9.3)':
+ dependencies:
+ '@babel/code-frame': 7.27.1
+ '@babel/core': 7.27.1
+ '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.27.1)
+ optionalDependencies:
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@expo/router-server@55.0.18(@expo/metro-runtime@55.0.11)(expo-constants@55.0.16)(expo-font@55.0.8)(expo-router@55.0.16)(expo-server@55.0.11)(expo@55.0.26)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ debug: 4.4.3(supports-color@8.1.1)
+ expo: 55.0.26(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.16)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ expo-constants: 55.0.16(expo@55.0.26)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))
+ expo-font: 55.0.8(expo@55.0.26)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ expo-server: 55.0.11
+ react: 19.2.0
+ optionalDependencies:
+ '@expo/metro-runtime': 55.0.11(@expo/dom-webview@55.0.6)(expo@55.0.26)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ expo-router: 55.0.16(d69165032b49371b387000fe555903ea)
+ react-dom: 19.2.0(react@19.2.0)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@expo/schema-utils@55.0.4': {}
+
+ '@expo/sdk-runtime-versions@1.0.0': {}
+
+ '@expo/spawn-async@1.8.0':
+ dependencies:
+ cross-spawn: 7.0.6
+
+ '@expo/sudo-prompt@9.3.2': {}
+
+ '@expo/vector-icons@15.1.1(expo-font@55.0.8)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ expo-font: 55.0.8(expo@55.0.26)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0)
+
+ '@expo/ws-tunnel@1.0.6': {}
+
+ '@expo/xcpretty@4.4.4':
+ dependencies:
+ '@babel/code-frame': 7.27.1
+ chalk: 4.1.2
+ js-yaml: 4.1.0
+
'@fastify/busboy@2.1.1': {}
'@fastify/busboy@3.1.1': {}
@@ -19372,8 +22504,79 @@ snapshots:
'@isaacs/string-locale-compare@1.1.0': {}
+ '@isaacs/ttlcache@1.4.1': {}
+
+ '@istanbuljs/load-nyc-config@1.1.0':
+ dependencies:
+ camelcase: 5.3.1
+ find-up: 4.1.0
+ get-package-type: 0.1.0
+ js-yaml: 3.14.1
+ resolve-from: 5.0.0
+
'@istanbuljs/schema@0.1.3': {}
+ '@jest/create-cache-key-function@29.7.0':
+ dependencies:
+ '@jest/types': 29.6.3
+
+ '@jest/diff-sequences@30.4.0': {}
+
+ '@jest/environment@29.7.0':
+ dependencies:
+ '@jest/fake-timers': 29.7.0
+ '@jest/types': 29.6.3
+ '@types/node': 20.19.21
+ jest-mock: 29.7.0
+
+ '@jest/fake-timers@29.7.0':
+ dependencies:
+ '@jest/types': 29.6.3
+ '@sinonjs/fake-timers': 10.3.0
+ '@types/node': 20.19.21
+ jest-message-util: 29.7.0
+ jest-mock: 29.7.0
+ jest-util: 29.7.0
+
+ '@jest/get-type@30.1.0': {}
+
+ '@jest/schemas@29.6.3':
+ dependencies:
+ '@sinclair/typebox': 0.27.10
+
+ '@jest/schemas@30.4.1':
+ dependencies:
+ '@sinclair/typebox': 0.34.49
+
+ '@jest/transform@29.7.0':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@jest/types': 29.6.3
+ '@jridgewell/trace-mapping': 0.3.31
+ babel-plugin-istanbul: 6.1.1
+ chalk: 4.1.2
+ convert-source-map: 2.0.0
+ fast-json-stable-stringify: 2.1.0
+ graceful-fs: 4.2.11
+ jest-haste-map: 29.7.0
+ jest-regex-util: 29.6.3
+ jest-util: 29.7.0
+ micromatch: 4.0.8
+ pirates: 4.0.7
+ slash: 3.0.0
+ write-file-atomic: 4.0.2
+ transitivePeerDependencies:
+ - supports-color
+
+ '@jest/types@29.6.3':
+ dependencies:
+ '@jest/schemas': 29.6.3
+ '@types/istanbul-lib-coverage': 2.0.6
+ '@types/istanbul-reports': 3.0.4
+ '@types/node': 20.19.21
+ '@types/yargs': 17.0.35
+ chalk: 4.1.2
+
'@jridgewell/gen-mapping@0.3.13':
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@@ -19390,7 +22593,6 @@ snapshots:
dependencies:
'@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.31
- optional: true
'@jridgewell/source-map@0.3.6':
dependencies:
@@ -19962,7 +23164,7 @@ snapshots:
'@npmcli/fs@3.1.1':
dependencies:
- semver: 7.7.3
+ semver: 7.7.4
'@npmcli/git@5.0.8':
dependencies:
@@ -19996,7 +23198,7 @@ snapshots:
json-parse-even-better-errors: 3.0.2
pacote: 18.0.6
proc-log: 4.2.0
- semver: 7.7.3
+ semver: 7.7.4
transitivePeerDependencies:
- bluebird
- supports-color
@@ -20013,7 +23215,7 @@ snapshots:
json-parse-even-better-errors: 3.0.2
normalize-package-data: 6.0.2
proc-log: 4.2.0
- semver: 7.7.3
+ semver: 7.7.4
transitivePeerDependencies:
- bluebird
@@ -20496,7 +23698,7 @@ snapshots:
'@opentelemetry/semantic-conventions@1.37.0': {}
- '@oxc-project/types@0.130.0': {}
+ '@oxc-project/types@0.133.0': {}
'@oxc-project/types@0.94.0': {}
@@ -20730,16 +23932,16 @@ snapshots:
'@protobufjs/utf8@1.1.0': {}
- '@pulumi/github@6.7.2(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.8.3))(typescript@5.8.3)':
+ '@pulumi/github@6.7.2(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.9.3))(typescript@5.9.3)':
dependencies:
- '@pulumi/pulumi': 3.201.0(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.8.3))(typescript@5.8.3)
+ '@pulumi/pulumi': 3.201.0(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.9.3))(typescript@5.9.3)
transitivePeerDependencies:
- bluebird
- supports-color
- ts-node
- typescript
- '@pulumi/pulumi@3.201.0(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.8.3))(typescript@5.8.3)':
+ '@pulumi/pulumi@3.201.0(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.9.3))(typescript@5.9.3)':
dependencies:
'@grpc/grpc-js': 1.13.3
'@logdna/tail-file': 2.2.0
@@ -20770,15 +23972,15 @@ snapshots:
tmp: 0.2.5
upath: 1.2.0
optionalDependencies:
- ts-node: 10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.8.3)
- typescript: 5.8.3
+ ts-node: 10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.9.3)
+ typescript: 5.9.3
transitivePeerDependencies:
- bluebird
- supports-color
- '@pulumiverse/vercel@1.14.3(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.8.3))(typescript@5.8.3)':
+ '@pulumiverse/vercel@1.14.3(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.9.3))(typescript@5.9.3)':
dependencies:
- '@pulumi/pulumi': 3.201.0(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.8.3))(typescript@5.8.3)
+ '@pulumi/pulumi': 3.201.0(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.9.3))(typescript@5.9.3)
transitivePeerDependencies:
- bluebird
- supports-color
@@ -20799,6 +24001,8 @@ snapshots:
'@radix-ui/primitive@1.1.2': {}
+ '@radix-ui/primitive@1.1.3': {}
+
'@radix-ui/react-arrow@1.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -20829,6 +24033,18 @@ snapshots:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
+ '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.0)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.0)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.0)
+ react: 19.2.0
+ react-dom: 19.2.0(react@19.2.0)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
'@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
@@ -20846,6 +24062,12 @@ snapshots:
'@babel/runtime': 7.27.1
react: 19.2.4
+ '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.14)(react@19.2.0)':
+ dependencies:
+ react: 19.2.0
+ optionalDependencies:
+ '@types/react': 19.2.14
+
'@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.14)(react@19.2.4)':
dependencies:
react: 19.2.4
@@ -20857,6 +24079,12 @@ snapshots:
'@babel/runtime': 7.27.1
react: 19.2.4
+ '@radix-ui/react-context@1.1.2(@types/react@19.2.14)(react@19.2.0)':
+ dependencies:
+ react: 19.2.0
+ optionalDependencies:
+ '@types/react': 19.2.14
+
'@radix-ui/react-context@1.1.2(@types/react@19.2.14)(react@19.2.4)':
dependencies:
react: 19.2.4
@@ -20885,6 +24113,28 @@ snapshots:
transitivePeerDependencies:
- '@types/react'
+ '@radix-ui/react-dialog@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.2
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.0)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.0)
+ '@radix-ui/react-dismissable-layer': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ '@radix-ui/react-focus-guards': 1.1.2(@types/react@19.2.14)(react@19.2.0)
+ '@radix-ui/react-focus-scope': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.0)
+ '@radix-ui/react-portal': 1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ '@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ '@radix-ui/react-slot': 1.2.2(@types/react@19.2.14)(react@19.2.0)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.0)
+ aria-hidden: 1.2.4
+ react: 19.2.0
+ react-dom: 19.2.0(react@19.2.0)
+ react-remove-scroll: 2.6.3(@types/react@19.2.14)(react@19.2.0)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
'@radix-ui/react-dialog@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@radix-ui/primitive': 1.1.2
@@ -20907,6 +24157,12 @@ snapshots:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
+ '@radix-ui/react-direction@1.1.1(@types/react@19.2.14)(react@19.2.0)':
+ dependencies:
+ react: 19.2.0
+ optionalDependencies:
+ '@types/react': 19.2.14
+
'@radix-ui/react-direction@1.1.1(@types/react@19.2.14)(react@19.2.4)':
dependencies:
react: 19.2.4
@@ -20937,6 +24193,19 @@ snapshots:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
+ '@radix-ui/react-dismissable-layer@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.2
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.0)
+ '@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.0)
+ '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.14)(react@19.2.0)
+ react: 19.2.0
+ react-dom: 19.2.0(react@19.2.0)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
'@radix-ui/react-dismissable-layer@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@radix-ui/primitive': 1.1.2
@@ -20970,6 +24239,12 @@ snapshots:
'@babel/runtime': 7.27.1
react: 19.2.4
+ '@radix-ui/react-focus-guards@1.1.2(@types/react@19.2.14)(react@19.2.0)':
+ dependencies:
+ react: 19.2.0
+ optionalDependencies:
+ '@types/react': 19.2.14
+
'@radix-ui/react-focus-guards@1.1.2(@types/react@19.2.14)(react@19.2.4)':
dependencies:
react: 19.2.4
@@ -20985,6 +24260,17 @@ snapshots:
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-focus-scope@1.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.0)
+ '@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.0)
+ react: 19.2.0
+ react-dom: 19.2.0(react@19.2.0)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
'@radix-ui/react-focus-scope@1.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
@@ -21013,6 +24299,13 @@ snapshots:
'@radix-ui/react-use-layout-effect': 1.0.0(react@19.2.4)
react: 19.2.4
+ '@radix-ui/react-id@1.1.1(@types/react@19.2.14)(react@19.2.0)':
+ dependencies:
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.0)
+ react: 19.2.0
+ optionalDependencies:
+ '@types/react': 19.2.14
+
'@radix-ui/react-id@1.1.1(@types/react@19.2.14)(react@19.2.4)':
dependencies:
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
@@ -21143,6 +24436,16 @@ snapshots:
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-portal@1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ '@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.0)
+ react: 19.2.0
+ react-dom: 19.2.0(react@19.2.0)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
'@radix-ui/react-portal@1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -21171,6 +24474,16 @@ snapshots:
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-presence@1.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.0)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.0)
+ react: 19.2.0
+ react-dom: 19.2.0(react@19.2.0)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
'@radix-ui/react-presence@1.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
@@ -21181,6 +24494,16 @@ snapshots:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
+ '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.0)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.0)
+ react: 19.2.0
+ react-dom: 19.2.0(react@19.2.0)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
'@radix-ui/react-primitive@1.0.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@babel/runtime': 7.27.1
@@ -21188,6 +24511,15 @@ snapshots:
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-primitive@2.1.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ '@radix-ui/react-slot': 1.2.2(@types/react@19.2.14)(react@19.2.0)
+ react: 19.2.0
+ react-dom: 19.2.0(react@19.2.0)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
'@radix-ui/react-primitive@2.1.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@radix-ui/react-slot': 1.2.2(@types/react@19.2.14)(react@19.2.4)
@@ -21197,6 +24529,15 @@ snapshots:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
+ '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.0)
+ react: 19.2.0
+ react-dom: 19.2.0(react@19.2.0)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
'@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4)
@@ -21206,6 +24547,23 @@ snapshots:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
+ '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.0)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.0)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.0)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.0)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.0)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.0)
+ react: 19.2.0
+ react-dom: 19.2.0(react@19.2.0)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
'@radix-ui/react-roving-focus@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@radix-ui/primitive': 1.1.2
@@ -21277,6 +24635,13 @@ snapshots:
'@radix-ui/react-compose-refs': 1.0.0(react@19.2.4)
react: 19.2.4
+ '@radix-ui/react-slot@1.2.2(@types/react@19.2.14)(react@19.2.0)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.0)
+ react: 19.2.0
+ optionalDependencies:
+ '@types/react': 19.2.14
+
'@radix-ui/react-slot@1.2.2(@types/react@19.2.14)(react@19.2.4)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
@@ -21284,6 +24649,13 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.14
+ '@radix-ui/react-slot@1.2.3(@types/react@19.2.14)(react@19.2.0)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.0)
+ react: 19.2.0
+ optionalDependencies:
+ '@types/react': 19.2.14
+
'@radix-ui/react-slot@1.2.3(@types/react@19.2.14)(react@19.2.4)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
@@ -21306,6 +24678,22 @@ snapshots:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
+ '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.0)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.0)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.0)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.0)
+ react: 19.2.0
+ react-dom: 19.2.0(react@19.2.0)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
'@radix-ui/react-tooltip@1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@radix-ui/primitive': 1.1.2
@@ -21331,6 +24719,12 @@ snapshots:
'@babel/runtime': 7.27.1
react: 19.2.4
+ '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.14)(react@19.2.0)':
+ dependencies:
+ react: 19.2.0
+ optionalDependencies:
+ '@types/react': 19.2.14
+
'@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.14)(react@19.2.4)':
dependencies:
react: 19.2.4
@@ -21343,6 +24737,14 @@ snapshots:
'@radix-ui/react-use-callback-ref': 1.0.0(react@19.2.4)
react: 19.2.4
+ '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.14)(react@19.2.0)':
+ dependencies:
+ '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.0)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.0)
+ react: 19.2.0
+ optionalDependencies:
+ '@types/react': 19.2.14
+
'@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.14)(react@19.2.4)':
dependencies:
'@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.4)
@@ -21351,6 +24753,13 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.14
+ '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.14)(react@19.2.0)':
+ dependencies:
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.0)
+ react: 19.2.0
+ optionalDependencies:
+ '@types/react': 19.2.14
+
'@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.14)(react@19.2.4)':
dependencies:
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
@@ -21364,6 +24773,13 @@ snapshots:
'@radix-ui/react-use-callback-ref': 1.0.0(react@19.2.4)
react: 19.2.4
+ '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.14)(react@19.2.0)':
+ dependencies:
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.0)
+ react: 19.2.0
+ optionalDependencies:
+ '@types/react': 19.2.14
+
'@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.14)(react@19.2.4)':
dependencies:
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4)
@@ -21376,6 +24792,12 @@ snapshots:
'@babel/runtime': 7.27.1
react: 19.2.4
+ '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.14)(react@19.2.0)':
+ dependencies:
+ react: 19.2.0
+ optionalDependencies:
+ '@types/react': 19.2.14
+
'@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.14)(react@19.2.4)':
dependencies:
react: 19.2.4
@@ -21646,6 +25068,196 @@ snapshots:
dependencies:
react: 19.2.4
+ '@react-native/assets-registry@0.83.6': {}
+
+ '@react-native/babel-plugin-codegen@0.83.6(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/traverse': 7.28.4
+ '@react-native/codegen': 0.83.6(@babel/core@7.27.1)
+ transitivePeerDependencies:
+ - '@babel/core'
+ - supports-color
+
+ '@react-native/babel-preset@0.83.6(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/plugin-proposal-export-default-from': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.27.1)
+ '@babel/plugin-syntax-export-default-from': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.27.1)
+ '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.27.1)
+ '@babel/plugin-transform-arrow-functions': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-transform-async-generator-functions': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-transform-async-to-generator': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-transform-block-scoping': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-transform-class-properties': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-transform-classes': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-transform-computed-properties': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-transform-destructuring': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-transform-flow-strip-types': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-transform-for-of': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-transform-function-name': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-transform-literals': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-transform-logical-assignment-operators': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-transform-named-capturing-groups-regex': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-transform-nullish-coalescing-operator': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-transform-numeric-separator': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-transform-object-rest-spread': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-transform-optional-catch-binding': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-transform-optional-chaining': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-transform-parameters': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-transform-private-methods': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-transform-private-property-in-object': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-transform-react-display-name': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-transform-react-jsx': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.1)
+ '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.27.1)
+ '@babel/plugin-transform-regenerator': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-transform-runtime': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-transform-shorthand-properties': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-transform-spread': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-transform-sticky-regex': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-transform-unicode-regex': 7.29.7(@babel/core@7.27.1)
+ '@babel/template': 7.27.2
+ '@react-native/babel-plugin-codegen': 0.83.6(@babel/core@7.27.1)
+ babel-plugin-syntax-hermes-parser: 0.32.0
+ babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.27.1)
+ react-refresh: 0.14.2
+ transitivePeerDependencies:
+ - supports-color
+
+ '@react-native/codegen@0.83.6(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/parser': 7.28.4
+ glob: 7.2.3
+ hermes-parser: 0.32.0
+ invariant: 2.2.4
+ nullthrows: 1.1.1
+ yargs: 17.7.2
+
+ '@react-native/community-cli-plugin@0.83.6':
+ dependencies:
+ '@react-native/dev-middleware': 0.83.6
+ debug: 4.4.3(supports-color@8.1.1)
+ invariant: 2.2.4
+ metro: 0.83.7
+ metro-config: 0.83.7
+ metro-core: 0.83.7
+ semver: 7.7.4
+ transitivePeerDependencies:
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+
+ '@react-native/debugger-frontend@0.83.6': {}
+
+ '@react-native/debugger-shell@0.83.6':
+ dependencies:
+ cross-spawn: 7.0.6
+ fb-dotslash: 0.5.8
+
+ '@react-native/dev-middleware@0.83.6':
+ dependencies:
+ '@isaacs/ttlcache': 1.4.1
+ '@react-native/debugger-frontend': 0.83.6
+ '@react-native/debugger-shell': 0.83.6
+ chrome-launcher: 0.15.2
+ chromium-edge-launcher: 0.2.0
+ connect: 3.7.0
+ debug: 4.4.3(supports-color@8.1.1)
+ invariant: 2.2.4
+ nullthrows: 1.1.1
+ open: 7.4.2
+ serve-static: 1.16.2
+ ws: 7.5.11
+ transitivePeerDependencies:
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+
+ '@react-native/gradle-plugin@0.83.6': {}
+
+ '@react-native/js-polyfills@0.83.6': {}
+
+ '@react-native/normalize-colors@0.74.89': {}
+
+ '@react-native/normalize-colors@0.83.6': {}
+
+ '@react-native/virtualized-lists@0.83.6(@types/react@19.2.14)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ invariant: 2.2.4
+ nullthrows: 1.1.1
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0)
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ '@react-navigation/bottom-tabs@7.16.2(@react-navigation/native@7.2.5(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-safe-area-context@5.6.2(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-screens@4.23.0(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ '@react-navigation/elements': 2.9.19(@react-navigation/native@7.2.5(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-safe-area-context@5.6.2(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ '@react-navigation/native': 7.2.5(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ color: 4.2.3
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0)
+ react-native-safe-area-context: 5.6.2(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ react-native-screens: 4.23.0(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ sf-symbols-typescript: 2.2.0
+ transitivePeerDependencies:
+ - '@react-native-masked-view/masked-view'
+
+ '@react-navigation/core@7.17.5(react@19.2.0)':
+ dependencies:
+ '@react-navigation/routers': 7.5.5
+ escape-string-regexp: 4.0.0
+ fast-deep-equal: 3.1.3
+ nanoid: 3.3.11
+ query-string: 7.1.3
+ react: 19.2.0
+ react-is: 19.2.6
+ use-latest-callback: 0.2.6(react@19.2.0)
+ use-sync-external-store: 1.5.0(react@19.2.0)
+
+ '@react-navigation/elements@2.9.19(@react-navigation/native@7.2.5(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-safe-area-context@5.6.2(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ '@react-navigation/native': 7.2.5(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ color: 4.2.3
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0)
+ react-native-safe-area-context: 5.6.2(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ use-latest-callback: 0.2.6(react@19.2.0)
+ use-sync-external-store: 1.5.0(react@19.2.0)
+
+ '@react-navigation/native-stack@7.16.0(@react-navigation/native@7.2.5(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-safe-area-context@5.6.2(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-screens@4.23.0(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ '@react-navigation/elements': 2.9.19(@react-navigation/native@7.2.5(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-safe-area-context@5.6.2(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ '@react-navigation/native': 7.2.5(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ color: 4.2.3
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0)
+ react-native-safe-area-context: 5.6.2(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ react-native-screens: 4.23.0(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ sf-symbols-typescript: 2.2.0
+ warn-once: 0.1.1
+ transitivePeerDependencies:
+ - '@react-native-masked-view/masked-view'
+
+ '@react-navigation/native@7.2.5(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ '@react-navigation/core': 7.17.5(react@19.2.0)
+ escape-string-regexp: 4.0.0
+ fast-deep-equal: 3.1.3
+ nanoid: 3.3.11
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0)
+ use-latest-callback: 0.2.6(react@19.2.0)
+
+ '@react-navigation/routers@7.5.5':
+ dependencies:
+ nanoid: 3.3.11
+
'@reduxjs/toolkit@2.10.1(react-redux@9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1))(react@19.2.4)':
dependencies:
'@standard-schema/spec': 1.0.0
@@ -21681,67 +25293,67 @@ snapshots:
'@rolldown/binding-android-arm64@1.0.0-beta.42':
optional: true
- '@rolldown/binding-android-arm64@1.0.1':
+ '@rolldown/binding-android-arm64@1.0.3':
optional: true
'@rolldown/binding-darwin-arm64@1.0.0-beta.42':
optional: true
- '@rolldown/binding-darwin-arm64@1.0.1':
+ '@rolldown/binding-darwin-arm64@1.0.3':
optional: true
'@rolldown/binding-darwin-x64@1.0.0-beta.42':
optional: true
- '@rolldown/binding-darwin-x64@1.0.1':
+ '@rolldown/binding-darwin-x64@1.0.3':
optional: true
'@rolldown/binding-freebsd-x64@1.0.0-beta.42':
optional: true
- '@rolldown/binding-freebsd-x64@1.0.1':
+ '@rolldown/binding-freebsd-x64@1.0.3':
optional: true
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.42':
optional: true
- '@rolldown/binding-linux-arm-gnueabihf@1.0.1':
+ '@rolldown/binding-linux-arm-gnueabihf@1.0.3':
optional: true
'@rolldown/binding-linux-arm64-gnu@1.0.0-beta.42':
optional: true
- '@rolldown/binding-linux-arm64-gnu@1.0.1':
+ '@rolldown/binding-linux-arm64-gnu@1.0.3':
optional: true
'@rolldown/binding-linux-arm64-musl@1.0.0-beta.42':
optional: true
- '@rolldown/binding-linux-arm64-musl@1.0.1':
+ '@rolldown/binding-linux-arm64-musl@1.0.3':
optional: true
- '@rolldown/binding-linux-ppc64-gnu@1.0.1':
+ '@rolldown/binding-linux-ppc64-gnu@1.0.3':
optional: true
- '@rolldown/binding-linux-s390x-gnu@1.0.1':
+ '@rolldown/binding-linux-s390x-gnu@1.0.3':
optional: true
'@rolldown/binding-linux-x64-gnu@1.0.0-beta.42':
optional: true
- '@rolldown/binding-linux-x64-gnu@1.0.1':
+ '@rolldown/binding-linux-x64-gnu@1.0.3':
optional: true
'@rolldown/binding-linux-x64-musl@1.0.0-beta.42':
optional: true
- '@rolldown/binding-linux-x64-musl@1.0.1':
+ '@rolldown/binding-linux-x64-musl@1.0.3':
optional: true
'@rolldown/binding-openharmony-arm64@1.0.0-beta.42':
optional: true
- '@rolldown/binding-openharmony-arm64@1.0.1':
+ '@rolldown/binding-openharmony-arm64@1.0.3':
optional: true
'@rolldown/binding-wasm32-wasi@1.0.0-beta.42':
@@ -21749,7 +25361,7 @@ snapshots:
'@napi-rs/wasm-runtime': 1.0.6
optional: true
- '@rolldown/binding-wasm32-wasi@1.0.1':
+ '@rolldown/binding-wasm32-wasi@1.0.3':
dependencies:
'@emnapi/core': 1.10.0
'@emnapi/runtime': 1.10.0
@@ -21759,7 +25371,7 @@ snapshots:
'@rolldown/binding-win32-arm64-msvc@1.0.0-beta.42':
optional: true
- '@rolldown/binding-win32-arm64-msvc@1.0.1':
+ '@rolldown/binding-win32-arm64-msvc@1.0.3':
optional: true
'@rolldown/binding-win32-ia32-msvc@1.0.0-beta.42':
@@ -21768,7 +25380,7 @@ snapshots:
'@rolldown/binding-win32-x64-msvc@1.0.0-beta.42':
optional: true
- '@rolldown/binding-win32-x64-msvc@1.0.1':
+ '@rolldown/binding-win32-x64-msvc@1.0.3':
optional: true
'@rolldown/pluginutils@1.0.0': {}
@@ -21786,7 +25398,7 @@ snapshots:
estree-walker: 2.0.2
fdir: 6.5.0(picomatch@4.0.3)
is-reference: 1.2.1
- magic-string: 0.30.19
+ magic-string: 0.30.21
picomatch: 4.0.3
optionalDependencies:
rollup: 4.40.2
@@ -21795,7 +25407,7 @@ snapshots:
dependencies:
'@rollup/pluginutils': 5.1.4(rollup@4.40.2)
estree-walker: 2.0.2
- magic-string: 0.30.19
+ magic-string: 0.30.21
optionalDependencies:
rollup: 4.40.2
@@ -21818,7 +25430,7 @@ snapshots:
'@rollup/plugin-replace@6.0.2(rollup@4.40.2)':
dependencies:
'@rollup/pluginutils': 5.1.4(rollup@4.40.2)
- magic-string: 0.30.19
+ magic-string: 0.30.21
optionalDependencies:
rollup: 4.40.2
@@ -22008,6 +25620,13 @@ snapshots:
'@shinyoshiaki/jspack@0.0.6': {}
+ '@shopify/flash-list@2.0.2(@babel/runtime@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ '@babel/runtime': 7.27.1
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0)
+ tslib: 2.8.1
+
'@sigstore/bundle@2.3.2':
dependencies:
'@sigstore/protobuf-specs': 0.3.3
@@ -22040,6 +25659,10 @@ snapshots:
'@sigstore/core': 1.1.0
'@sigstore/protobuf-specs': 0.3.3
+ '@sinclair/typebox@0.27.10': {}
+
+ '@sinclair/typebox@0.34.49': {}
+
'@sindresorhus/is@4.6.0': {}
'@sindresorhus/is@5.6.0': {}
@@ -22048,6 +25671,14 @@ snapshots:
'@sindresorhus/merge-streams@2.3.0': {}
+ '@sinonjs/commons@3.0.1':
+ dependencies:
+ type-detect: 4.0.8
+
+ '@sinonjs/fake-timers@10.3.0':
+ dependencies:
+ '@sinonjs/commons': 3.0.1
+
'@smithy/abort-controller@4.0.2':
dependencies:
'@smithy/types': 4.3.1
@@ -23081,11 +26712,11 @@ snapshots:
dependencies:
solid-js: 1.9.6
- '@solidjs/start@1.1.3(@testing-library/jest-dom@6.5.0)(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(solid-js@1.9.6)(terser@5.44.0)(vinxi@0.5.6(@planetscale/database@1.19.0)(@types/node@22.15.17)(db0@0.3.2(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(mysql2@3.15.2))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(ioredis@5.6.1)(jiti@2.6.1)(lightningcss@1.32.0)(mysql2@3.15.2)(rolldown@1.0.1)(terser@5.44.0)(xml2js@0.6.2)(yaml@2.8.1))(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1)':
+ '@solidjs/start@1.1.3(@testing-library/jest-dom@6.5.0)(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(solid-js@1.9.6)(terser@5.44.0)(vinxi@0.5.6(@planetscale/database@1.19.0)(@types/node@22.15.17)(db0@0.3.2(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(mysql2@3.15.2))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(ioredis@5.6.1)(jiti@2.6.1)(lightningcss@1.32.0)(mysql2@3.15.2)(rolldown@1.0.3)(terser@5.44.0)(xml2js@0.6.2)(yaml@2.8.1))(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1)':
dependencies:
'@tanstack/server-functions-plugin': 1.119.2(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)
- '@vinxi/plugin-directives': 0.5.1(vinxi@0.5.6(@planetscale/database@1.19.0)(@types/node@22.15.17)(db0@0.3.2(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(mysql2@3.15.2))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(ioredis@5.6.1)(jiti@2.6.1)(lightningcss@1.32.0)(mysql2@3.15.2)(rolldown@1.0.1)(terser@5.44.0)(xml2js@0.6.2)(yaml@2.8.1))
- '@vinxi/server-components': 0.5.1(vinxi@0.5.6(@planetscale/database@1.19.0)(@types/node@22.15.17)(db0@0.3.2(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(mysql2@3.15.2))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(ioredis@5.6.1)(jiti@2.6.1)(lightningcss@1.32.0)(mysql2@3.15.2)(rolldown@1.0.1)(terser@5.44.0)(xml2js@0.6.2)(yaml@2.8.1))
+ '@vinxi/plugin-directives': 0.5.1(vinxi@0.5.6(@planetscale/database@1.19.0)(@types/node@22.15.17)(db0@0.3.2(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(mysql2@3.15.2))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(ioredis@5.6.1)(jiti@2.6.1)(lightningcss@1.32.0)(mysql2@3.15.2)(rolldown@1.0.3)(terser@5.44.0)(xml2js@0.6.2)(yaml@2.8.1))
+ '@vinxi/server-components': 0.5.1(vinxi@0.5.6(@planetscale/database@1.19.0)(@types/node@22.15.17)(db0@0.3.2(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(mysql2@3.15.2))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(ioredis@5.6.1)(jiti@2.6.1)(lightningcss@1.32.0)(mysql2@3.15.2)(rolldown@1.0.3)(terser@5.44.0)(xml2js@0.6.2)(yaml@2.8.1))
defu: 6.1.4
error-stack-parser: 2.1.4
html-to-image: 1.11.13
@@ -23096,7 +26727,7 @@ snapshots:
source-map-js: 1.2.1
terracotta: 1.0.6(solid-js@1.9.6)
tinyglobby: 0.2.13
- vinxi: 0.5.6(@planetscale/database@1.19.0)(@types/node@22.15.17)(db0@0.3.2(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(mysql2@3.15.2))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(ioredis@5.6.1)(jiti@2.6.1)(lightningcss@1.32.0)(mysql2@3.15.2)(rolldown@1.0.1)(terser@5.44.0)(xml2js@0.6.2)(yaml@2.8.1)
+ vinxi: 0.5.6(@planetscale/database@1.19.0)(@types/node@22.15.17)(db0@0.3.2(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(mysql2@3.15.2))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(ioredis@5.6.1)(jiti@2.6.1)(lightningcss@1.32.0)(mysql2@3.15.2)(rolldown@1.0.3)(terser@5.44.0)(xml2js@0.6.2)(yaml@2.8.1)
vite-plugin-solid: 2.11.6(@testing-library/jest-dom@6.5.0)(solid-js@1.9.6)(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1))
transitivePeerDependencies:
- '@testing-library/jest-dom'
@@ -23226,9 +26857,9 @@ snapshots:
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
- '@storybook/builder-vite@10.5.0-alpha.0(esbuild@0.25.5)(rollup@4.40.2)(storybook@8.6.12(prettier@3.7.4))(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1))(webpack@5.101.3(esbuild@0.25.5))':
+ '@storybook/builder-vite@10.5.0-alpha.3(esbuild@0.25.5)(rollup@4.40.2)(storybook@8.6.12(prettier@3.7.4))(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1))(webpack@5.101.3(esbuild@0.25.5))':
dependencies:
- '@storybook/csf-plugin': 10.5.0-alpha.0(esbuild@0.25.5)(rollup@4.40.2)(storybook@8.6.12(prettier@3.7.4))(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1))(webpack@5.101.3(esbuild@0.25.5))
+ '@storybook/csf-plugin': 10.5.0-alpha.3(esbuild@0.25.5)(rollup@4.40.2)(storybook@8.6.12(prettier@3.7.4))(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1))(webpack@5.101.3(esbuild@0.25.5))
storybook: 8.6.12(prettier@3.7.4)
ts-dedent: 2.2.0
vite: 6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)
@@ -23258,7 +26889,7 @@ snapshots:
- supports-color
- utf-8-validate
- '@storybook/csf-plugin@10.5.0-alpha.0(esbuild@0.25.5)(rollup@4.40.2)(storybook@8.6.12(prettier@3.7.4))(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1))(webpack@5.101.3(esbuild@0.25.5))':
+ '@storybook/csf-plugin@10.5.0-alpha.3(esbuild@0.25.5)(rollup@4.40.2)(storybook@8.6.12(prettier@3.7.4))(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1))(webpack@5.101.3(esbuild@0.25.5))':
dependencies:
storybook: 8.6.12(prettier@3.7.4)
unplugin: 2.3.11
@@ -23472,6 +27103,12 @@ snapshots:
valibot: 1.0.0-rc.1(typescript@5.8.3)
zod: 3.25.76
+ '@t3-oss/env-core@0.12.0(typescript@5.9.3)(valibot@1.0.0-rc.1(typescript@5.9.3))(zod@3.25.76)':
+ optionalDependencies:
+ typescript: 5.9.3
+ valibot: 1.0.0-rc.1(typescript@5.9.3)
+ zod: 3.25.76
+
'@t3-oss/env-nextjs@0.12.0(typescript@5.8.3)(valibot@1.0.0-rc.1(typescript@5.8.3))(zod@3.25.76)':
dependencies:
'@t3-oss/env-core': 0.12.0(typescript@5.8.3)(valibot@1.0.0-rc.1(typescript@5.8.3))(zod@3.25.76)
@@ -23480,6 +27117,14 @@ snapshots:
valibot: 1.0.0-rc.1(typescript@5.8.3)
zod: 3.25.76
+ '@t3-oss/env-nextjs@0.12.0(typescript@5.9.3)(valibot@1.0.0-rc.1(typescript@5.9.3))(zod@3.25.76)':
+ dependencies:
+ '@t3-oss/env-core': 0.12.0(typescript@5.9.3)(valibot@1.0.0-rc.1(typescript@5.9.3))(zod@3.25.76)
+ optionalDependencies:
+ typescript: 5.9.3
+ valibot: 1.0.0-rc.1(typescript@5.9.3)
+ zod: 3.25.76
+
'@tailwindcss/node@4.3.0':
dependencies:
'@jridgewell/remapping': 2.3.5
@@ -23893,6 +27538,16 @@ snapshots:
lodash: 4.17.21
redent: 3.0.0
+ '@testing-library/react-native@13.3.3(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react-test-renderer@19.2.0(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ jest-matcher-utils: 30.4.1
+ picocolors: 1.1.1
+ pretty-format: 30.4.1
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0)
+ react-test-renderer: 19.2.0(react@19.2.0)
+ redent: 3.0.0
+
'@testing-library/user-event@14.5.2(@testing-library/dom@10.4.0)':
dependencies:
'@testing-library/dom': 10.4.0
@@ -24112,6 +27767,12 @@ snapshots:
'@types/google-protobuf@3.15.12': {}
+ '@types/graceful-fs@4.1.9':
+ dependencies:
+ '@types/node': 20.19.21
+
+ '@types/hammerjs@2.0.46': {}
+
'@types/hast@3.0.4':
dependencies:
'@types/unist': 3.0.3
@@ -24122,6 +27783,16 @@ snapshots:
'@types/http-errors@2.0.4': {}
+ '@types/istanbul-lib-coverage@2.0.6': {}
+
+ '@types/istanbul-lib-report@3.0.3':
+ dependencies:
+ '@types/istanbul-lib-coverage': 2.0.6
+
+ '@types/istanbul-reports@3.0.4':
+ dependencies:
+ '@types/istanbul-lib-report': 3.0.3
+
'@types/js-cookie@3.0.6': {}
'@types/jsdom@21.1.7':
@@ -24221,6 +27892,10 @@ snapshots:
dependencies:
'@types/react': 19.2.14
+ '@types/react-test-renderer@19.1.0':
+ dependencies:
+ '@types/react': 19.2.14
+
'@types/react-tooltip@4.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
react-tooltip: 5.28.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -24257,6 +27932,8 @@ snapshots:
'@types/shimmer@1.2.0': {}
+ '@types/stack-utils@2.0.3': {}
+
'@types/tmp@0.2.6': {}
'@types/tough-cookie@4.0.5': {}
@@ -24276,27 +27953,33 @@ snapshots:
'@types/uuid@9.0.8': {}
+ '@types/yargs-parser@21.0.3': {}
+
+ '@types/yargs@17.0.35':
+ dependencies:
+ '@types/yargs-parser': 21.0.3
+
'@types/yauzl@2.10.3':
dependencies:
'@types/node': 20.19.21
optional: true
- '@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3)':
+ '@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)':
dependencies:
'@eslint-community/regexpp': 4.12.1
- '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.8.3)
+ '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.9.3)
'@typescript-eslint/scope-manager': 5.62.0
- '@typescript-eslint/type-utils': 5.62.0(eslint@8.57.1)(typescript@5.8.3)
- '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@5.8.3)
+ '@typescript-eslint/type-utils': 5.62.0(eslint@8.57.1)(typescript@5.9.3)
+ '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@5.9.3)
debug: 4.4.0
eslint: 8.57.1
graphemer: 1.4.0
ignore: 5.3.2
natural-compare-lite: 1.4.0
semver: 7.7.1
- tsutils: 3.21.0(typescript@5.8.3)
+ tsutils: 3.21.0(typescript@5.9.3)
optionalDependencies:
- typescript: 5.8.3
+ typescript: 5.9.3
transitivePeerDependencies:
- supports-color
@@ -24316,15 +27999,15 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3)':
+ '@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.9.3)':
dependencies:
'@typescript-eslint/scope-manager': 5.62.0
'@typescript-eslint/types': 5.62.0
- '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.8.3)
+ '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.9.3)
debug: 4.4.0
eslint: 8.57.1
optionalDependencies:
- typescript: 5.8.3
+ typescript: 5.9.3
transitivePeerDependencies:
- supports-color
@@ -24363,15 +28046,15 @@ snapshots:
dependencies:
typescript: 5.8.3
- '@typescript-eslint/type-utils@5.62.0(eslint@8.57.1)(typescript@5.8.3)':
+ '@typescript-eslint/type-utils@5.62.0(eslint@8.57.1)(typescript@5.9.3)':
dependencies:
- '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.8.3)
- '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@5.8.3)
+ '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.9.3)
+ '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@5.9.3)
debug: 4.4.3(supports-color@8.1.1)
eslint: 8.57.1
- tsutils: 3.21.0(typescript@5.8.3)
+ tsutils: 3.21.0(typescript@5.9.3)
optionalDependencies:
- typescript: 5.8.3
+ typescript: 5.9.3
transitivePeerDependencies:
- supports-color
@@ -24391,7 +28074,7 @@ snapshots:
'@typescript-eslint/types@8.57.2': {}
- '@typescript-eslint/typescript-estree@5.62.0(typescript@5.8.3)':
+ '@typescript-eslint/typescript-estree@5.62.0(typescript@5.9.3)':
dependencies:
'@typescript-eslint/types': 5.62.0
'@typescript-eslint/visitor-keys': 5.62.0
@@ -24399,9 +28082,9 @@ snapshots:
globby: 11.1.0
is-glob: 4.0.3
semver: 7.7.1
- tsutils: 3.21.0(typescript@5.8.3)
+ tsutils: 3.21.0(typescript@5.9.3)
optionalDependencies:
- typescript: 5.8.3
+ typescript: 5.9.3
transitivePeerDependencies:
- supports-color
@@ -24420,14 +28103,14 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/utils@5.62.0(eslint@8.57.1)(typescript@5.8.3)':
+ '@typescript-eslint/utils@5.62.0(eslint@8.57.1)(typescript@5.9.3)':
dependencies:
'@eslint-community/eslint-utils': 4.7.0(eslint@8.57.1)
'@types/json-schema': 7.0.15
'@types/semver': 7.7.0
'@typescript-eslint/scope-manager': 5.62.0
'@typescript-eslint/types': 5.62.0
- '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.8.3)
+ '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.9.3)
eslint: 8.57.1
eslint-scope: 5.1.1
semver: 7.7.1
@@ -24627,7 +28310,7 @@ snapshots:
untun: 0.1.3
uqr: 0.1.2
- '@vinxi/plugin-directives@0.5.1(vinxi@0.5.6(@planetscale/database@1.19.0)(@types/node@22.15.17)(db0@0.3.2(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(mysql2@3.15.2))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(ioredis@5.6.1)(jiti@2.6.1)(lightningcss@1.32.0)(mysql2@3.15.2)(rolldown@1.0.1)(terser@5.44.0)(xml2js@0.6.2)(yaml@2.8.1))':
+ '@vinxi/plugin-directives@0.5.1(vinxi@0.5.6(@planetscale/database@1.19.0)(@types/node@22.15.17)(db0@0.3.2(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(mysql2@3.15.2))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(ioredis@5.6.1)(jiti@2.6.1)(lightningcss@1.32.0)(mysql2@3.15.2)(rolldown@1.0.3)(terser@5.44.0)(xml2js@0.6.2)(yaml@2.8.1))':
dependencies:
'@babel/parser': 7.27.2
acorn: 8.14.1
@@ -24638,18 +28321,18 @@ snapshots:
magicast: 0.2.11
recast: 0.23.11
tslib: 2.8.1
- vinxi: 0.5.6(@planetscale/database@1.19.0)(@types/node@22.15.17)(db0@0.3.2(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(mysql2@3.15.2))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(ioredis@5.6.1)(jiti@2.6.1)(lightningcss@1.32.0)(mysql2@3.15.2)(rolldown@1.0.1)(terser@5.44.0)(xml2js@0.6.2)(yaml@2.8.1)
+ vinxi: 0.5.6(@planetscale/database@1.19.0)(@types/node@22.15.17)(db0@0.3.2(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(mysql2@3.15.2))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(ioredis@5.6.1)(jiti@2.6.1)(lightningcss@1.32.0)(mysql2@3.15.2)(rolldown@1.0.3)(terser@5.44.0)(xml2js@0.6.2)(yaml@2.8.1)
- '@vinxi/server-components@0.5.1(vinxi@0.5.6(@planetscale/database@1.19.0)(@types/node@22.15.17)(db0@0.3.2(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(mysql2@3.15.2))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(ioredis@5.6.1)(jiti@2.6.1)(lightningcss@1.32.0)(mysql2@3.15.2)(rolldown@1.0.1)(terser@5.44.0)(xml2js@0.6.2)(yaml@2.8.1))':
+ '@vinxi/server-components@0.5.1(vinxi@0.5.6(@planetscale/database@1.19.0)(@types/node@22.15.17)(db0@0.3.2(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(mysql2@3.15.2))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(ioredis@5.6.1)(jiti@2.6.1)(lightningcss@1.32.0)(mysql2@3.15.2)(rolldown@1.0.3)(terser@5.44.0)(xml2js@0.6.2)(yaml@2.8.1))':
dependencies:
- '@vinxi/plugin-directives': 0.5.1(vinxi@0.5.6(@planetscale/database@1.19.0)(@types/node@22.15.17)(db0@0.3.2(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(mysql2@3.15.2))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(ioredis@5.6.1)(jiti@2.6.1)(lightningcss@1.32.0)(mysql2@3.15.2)(rolldown@1.0.1)(terser@5.44.0)(xml2js@0.6.2)(yaml@2.8.1))
+ '@vinxi/plugin-directives': 0.5.1(vinxi@0.5.6(@planetscale/database@1.19.0)(@types/node@22.15.17)(db0@0.3.2(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(mysql2@3.15.2))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(ioredis@5.6.1)(jiti@2.6.1)(lightningcss@1.32.0)(mysql2@3.15.2)(rolldown@1.0.3)(terser@5.44.0)(xml2js@0.6.2)(yaml@2.8.1))
acorn: 8.14.1
acorn-loose: 8.5.0
acorn-typescript: 1.4.13(acorn@8.14.1)
astring: 1.9.0
magicast: 0.2.11
recast: 0.23.11
- vinxi: 0.5.6(@planetscale/database@1.19.0)(@types/node@22.15.17)(db0@0.3.2(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(mysql2@3.15.2))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(ioredis@5.6.1)(jiti@2.6.1)(lightningcss@1.32.0)(mysql2@3.15.2)(rolldown@1.0.1)(terser@5.44.0)(xml2js@0.6.2)(yaml@2.8.1)
+ vinxi: 0.5.6(@planetscale/database@1.19.0)(@types/node@22.15.17)(db0@0.3.2(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(mysql2@3.15.2))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(ioredis@5.6.1)(jiti@2.6.1)(lightningcss@1.32.0)(mysql2@3.15.2)(rolldown@1.0.3)(terser@5.44.0)(xml2js@0.6.2)(yaml@2.8.1)
'@virtual-grid/core@2.0.1': {}
@@ -24731,10 +28414,18 @@ snapshots:
dependencies:
'@vitest/spy': 3.2.4
estree-walker: 3.0.3
- magic-string: 0.30.19
+ magic-string: 0.30.21
optionalDependencies:
vite: 6.3.5(@types/node@20.17.43)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)
+ '@vitest/mocker@3.2.4(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1))':
+ dependencies:
+ '@vitest/spy': 3.2.4
+ estree-walker: 3.0.3
+ magic-string: 0.30.21
+ optionalDependencies:
+ vite: 6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)
+
'@vitest/pretty-format@2.0.5':
dependencies:
tinyrainbow: 1.2.0
@@ -24767,7 +28458,7 @@ snapshots:
'@vitest/snapshot@3.2.4':
dependencies:
'@vitest/pretty-format': 3.2.4
- magic-string: 0.30.19
+ magic-string: 0.30.21
pathe: 2.0.3
'@vitest/spy@2.0.5':
@@ -24791,7 +28482,7 @@ snapshots:
sirv: 3.0.2
tinyglobby: 0.2.15
tinyrainbow: 2.0.0
- vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.43)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)
+ vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.15.17)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)
'@vitest/utils@2.0.5':
dependencies:
@@ -25052,7 +28743,7 @@ snapshots:
- aws-crt
- supports-color
- '@workflow/next@4.0.1-beta.69(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.17)(next@16.2.1(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))':
+ '@workflow/next@4.0.1-beta.69(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.17)(next@16.2.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))':
dependencies:
'@swc/core': 1.15.3(@swc/helpers@0.5.17)
'@workflow/builders': 4.0.1-beta.64(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.17)
@@ -25061,7 +28752,7 @@ snapshots:
semver: 7.7.4
watchpack: 2.5.1
optionalDependencies:
- next: 16.2.1(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ next: 16.2.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
transitivePeerDependencies:
- '@opentelemetry/api'
- '@swc/helpers'
@@ -25182,9 +28873,9 @@ snapshots:
ulid: 3.0.1
zod: 4.3.6
- '@workos-inc/node@7.50.0(express@5.1.0)(next@16.2.1(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))':
+ '@workos-inc/node@7.50.0(express@5.1.0)(next@16.2.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))':
dependencies:
- iron-session: 6.3.1(express@5.1.0)(next@16.2.1(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))
+ iron-session: 6.3.1(express@5.1.0)(next@16.2.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))
jose: 5.6.3
leb: 1.0.0
pluralize: 8.0.0
@@ -25276,6 +28967,10 @@ snapshots:
dependencies:
arch: 3.0.0
+ '@xmldom/xmldom@0.8.13': {}
+
+ '@xmldom/xmldom@0.9.10': {}
+
'@xtuc/ieee754@1.2.0':
optional: true
@@ -25400,6 +29095,8 @@ snapshots:
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
+ anser@1.4.10: {}
+
ansi-align@3.0.1:
dependencies:
string-width: 4.2.3
@@ -25414,6 +29111,8 @@ snapshots:
dependencies:
environment: 1.1.0
+ ansi-regex@4.1.1: {}
+
ansi-regex@5.0.1: {}
ansi-regex@6.1.0: {}
@@ -25615,6 +29314,8 @@ snapshots:
dependencies:
printable-characters: 1.0.42
+ asap@2.0.6: {}
+
asn1js@3.0.6:
dependencies:
pvtsutils: 1.3.6
@@ -25724,6 +29425,36 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ babel-jest@29.7.0(@babel/core@7.27.1):
+ dependencies:
+ '@babel/core': 7.27.1
+ '@jest/transform': 29.7.0
+ '@types/babel__core': 7.20.5
+ babel-plugin-istanbul: 6.1.1
+ babel-preset-jest: 29.6.3(@babel/core@7.27.1)
+ chalk: 4.1.2
+ graceful-fs: 4.2.11
+ slash: 3.0.0
+ transitivePeerDependencies:
+ - supports-color
+
+ babel-plugin-istanbul@6.1.1:
+ dependencies:
+ '@babel/helper-plugin-utils': 7.27.1
+ '@istanbuljs/load-nyc-config': 1.1.0
+ '@istanbuljs/schema': 0.1.3
+ istanbul-lib-instrument: 5.2.1
+ test-exclude: 6.0.0
+ transitivePeerDependencies:
+ - supports-color
+
+ babel-plugin-jest-hoist@29.6.3:
+ dependencies:
+ '@babel/template': 7.27.2
+ '@babel/types': 7.28.4
+ '@types/babel__core': 7.20.5
+ '@types/babel__traverse': 7.20.7
+
babel-plugin-jsx-dom-expressions@0.39.8(@babel/core@7.27.1):
dependencies:
'@babel/core': 7.27.1
@@ -25734,6 +29465,108 @@ snapshots:
parse5: 7.3.0
validate-html-nesting: 1.2.2
+ babel-plugin-polyfill-corejs2@0.4.17(@babel/core@7.27.1):
+ dependencies:
+ '@babel/compat-data': 7.29.7
+ '@babel/core': 7.27.1
+ '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.27.1)
+ semver: 6.3.1
+ transitivePeerDependencies:
+ - supports-color
+
+ babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.27.1):
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.27.1)
+ core-js-compat: 3.49.0
+ transitivePeerDependencies:
+ - supports-color
+
+ babel-plugin-polyfill-regenerator@0.6.8(@babel/core@7.27.1):
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.27.1)
+ transitivePeerDependencies:
+ - supports-color
+
+ babel-plugin-react-compiler@1.0.0:
+ dependencies:
+ '@babel/types': 7.28.4
+
+ babel-plugin-react-native-web@0.21.2: {}
+
+ babel-plugin-syntax-hermes-parser@0.32.0:
+ dependencies:
+ hermes-parser: 0.32.0
+
+ babel-plugin-syntax-hermes-parser@0.32.1:
+ dependencies:
+ hermes-parser: 0.32.1
+
+ babel-plugin-transform-flow-enums@0.0.2(@babel/core@7.27.1):
+ dependencies:
+ '@babel/plugin-syntax-flow': 7.29.7(@babel/core@7.27.1)
+ transitivePeerDependencies:
+ - '@babel/core'
+
+ babel-preset-current-node-syntax@1.2.0(@babel/core@7.27.1):
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.27.1)
+ '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.27.1)
+ '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.27.1)
+ '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.27.1)
+ '@babel/plugin-syntax-import-attributes': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.27.1)
+ '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.27.1)
+ '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.27.1)
+ '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.27.1)
+ '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.27.1)
+ '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.27.1)
+ '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.27.1)
+ '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.27.1)
+ '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.27.1)
+ '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.27.1)
+
+ babel-preset-expo@55.0.22(@babel/core@7.27.1)(@babel/runtime@7.27.1)(expo@55.0.26)(react-refresh@0.14.2):
+ dependencies:
+ '@babel/generator': 7.28.3
+ '@babel/helper-module-imports': 7.27.1
+ '@babel/plugin-proposal-decorators': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-proposal-export-default-from': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-syntax-export-default-from': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-transform-class-static-block': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-transform-export-namespace-from': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-transform-flow-strip-types': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-transform-object-rest-spread': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-transform-parameters': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-transform-private-methods': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-transform-private-property-in-object': 7.29.7(@babel/core@7.27.1)
+ '@babel/plugin-transform-runtime': 7.29.7(@babel/core@7.27.1)
+ '@babel/preset-react': 7.29.7(@babel/core@7.27.1)
+ '@babel/preset-typescript': 7.29.7(@babel/core@7.27.1)
+ '@react-native/babel-preset': 0.83.6(@babel/core@7.27.1)
+ babel-plugin-react-compiler: 1.0.0
+ babel-plugin-react-native-web: 0.21.2
+ babel-plugin-syntax-hermes-parser: 0.32.1
+ babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.27.1)
+ debug: 4.4.3(supports-color@8.1.1)
+ react-refresh: 0.14.2
+ resolve-from: 5.0.0
+ optionalDependencies:
+ '@babel/runtime': 7.27.1
+ expo: 55.0.26(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.16)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ transitivePeerDependencies:
+ - '@babel/core'
+ - supports-color
+
+ babel-preset-jest@29.6.3(@babel/core@7.27.1):
+ dependencies:
+ '@babel/core': 7.27.1
+ babel-plugin-jest-hoist: 29.6.3
+ babel-preset-current-node-syntax: 1.2.0(@babel/core@7.27.1)
+
babel-preset-solid@1.9.6(@babel/core@7.27.1):
dependencies:
'@babel/core': 7.27.1
@@ -25754,6 +29587,8 @@ snapshots:
baseline-browser-mapping@2.10.11: {}
+ baseline-browser-mapping@2.10.32: {}
+
baseline-browser-mapping@2.8.16: {}
before-after-hook@2.2.3: {}
@@ -25766,6 +29601,8 @@ snapshots:
bezier-easing@2.1.0: {}
+ big-integer@1.6.52: {}
+
bin-links@4.0.4:
dependencies:
cmd-shim: 6.0.3
@@ -25835,6 +29672,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ boolbase@1.0.0: {}
+
bottleneck@2.19.5: {}
bowser@2.11.0: {}
@@ -25850,6 +29689,18 @@ snapshots:
widest-line: 5.0.0
wrap-ansi: 9.0.0
+ bplist-creator@0.1.0:
+ dependencies:
+ stream-buffers: 2.2.0
+
+ bplist-parser@0.3.1:
+ dependencies:
+ big-integer: 1.6.52
+
+ bplist-parser@0.3.2:
+ dependencies:
+ big-integer: 1.6.52
+
brace-expansion@1.1.11:
dependencies:
balanced-match: 1.0.2
@@ -25884,6 +29735,18 @@ snapshots:
node-releases: 2.0.23
update-browserslist-db: 1.1.3(browserslist@4.26.3)
+ browserslist@4.28.2:
+ dependencies:
+ baseline-browser-mapping: 2.10.32
+ caniuse-lite: 1.0.30001793
+ electron-to-chromium: 1.5.363
+ node-releases: 2.0.46
+ update-browserslist-db: 1.2.3(browserslist@4.28.2)
+
+ bser@2.1.1:
+ dependencies:
+ node-int64: 0.4.0
+
buffer-crc32@0.2.13: {}
buffer-crc32@1.0.0: {}
@@ -26023,12 +29886,18 @@ snapshots:
camelcase-css@2.0.1: {}
+ camelcase@5.3.1: {}
+
+ camelcase@6.3.0: {}
+
camelcase@8.0.0: {}
caniuse-lite@1.0.30001717: {}
caniuse-lite@1.0.30001750: {}
+ caniuse-lite@1.0.30001793: {}
+
canvas-confetti@1.9.3: {}
caseless@0.12.0: {}
@@ -26127,9 +29996,33 @@ snapshots:
chromatic@11.28.2: {}
+ chrome-launcher@0.15.2:
+ dependencies:
+ '@types/node': 20.19.21
+ escape-string-regexp: 4.0.0
+ is-wsl: 2.2.0
+ lighthouse-logger: 1.4.2
+ transitivePeerDependencies:
+ - supports-color
+
chrome-trace-event@1.0.4:
optional: true
+ chromium-edge-launcher@0.2.0:
+ dependencies:
+ '@types/node': 20.19.21
+ escape-string-regexp: 4.0.0
+ is-wsl: 2.2.0
+ lighthouse-logger: 1.4.2
+ mkdirp: 1.0.4
+ rimraf: 3.0.2
+ transitivePeerDependencies:
+ - supports-color
+
+ ci-info@2.0.0: {}
+
+ ci-info@3.9.0: {}
+
citty@0.1.6:
dependencies:
consola: 3.4.2
@@ -26150,6 +30043,10 @@ snapshots:
cli-boxes@3.0.0: {}
+ cli-cursor@2.1.0:
+ dependencies:
+ restore-cursor: 2.0.0
+
cli-cursor@5.0.0:
dependencies:
restore-cursor: 5.1.0
@@ -26174,8 +30071,7 @@ snapshots:
dependencies:
mimic-response: 1.0.1
- clone@1.0.4:
- optional: true
+ clone@1.0.4: {}
clsx@1.2.1: {}
@@ -26223,7 +30119,6 @@ snapshots:
dependencies:
color-convert: 2.0.1
color-string: 1.9.1
- optional: true
colorspace@1.1.4:
dependencies:
@@ -26238,6 +30133,8 @@ snapshots:
commander@10.0.1: {}
+ commander@12.1.0: {}
+
commander@13.1.0: {}
commander@2.20.3: {}
@@ -26246,6 +30143,8 @@ snapshots:
commander@6.2.1: {}
+ commander@7.2.0: {}
+
commander@8.3.0: {}
common-ancestor-path@1.0.1: {}
@@ -26271,6 +30170,22 @@ snapshots:
normalize-path: 3.0.0
readable-stream: 4.7.0
+ compressible@2.0.18:
+ dependencies:
+ mime-db: 1.54.0
+
+ compression@1.8.1:
+ 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
+ transitivePeerDependencies:
+ - supports-color
+
concat-map@0.0.1: {}
concat-stream@2.0.0:
@@ -26293,6 +30208,15 @@ snapshots:
confbox@0.2.2: {}
+ connect@3.7.0:
+ dependencies:
+ debug: 2.6.9
+ finalhandler: 1.1.2
+ parseurl: 1.3.3
+ utils-merge: 1.0.1
+ transitivePeerDependencies:
+ - supports-color
+
consola@3.4.2: {}
console-control-strings@1.1.0: {}
@@ -26328,6 +30252,10 @@ snapshots:
'@types/cookie': 0.6.0
cookie: 0.7.2
+ core-js-compat@3.49.0:
+ dependencies:
+ browserslist: 4.28.2
+
core-js@3.42.0: {}
core-util-is@1.0.3: {}
@@ -26386,6 +30314,25 @@ snapshots:
dependencies:
uncrypto: 0.1.3
+ css-in-js-utils@3.1.0:
+ dependencies:
+ hyphenate-style-name: 1.1.0
+
+ css-select@5.2.2:
+ dependencies:
+ boolbase: 1.0.0
+ css-what: 6.2.2
+ domhandler: 5.0.3
+ domutils: 3.2.2
+ nth-check: 2.1.1
+
+ css-tree@1.1.3:
+ dependencies:
+ mdn-data: 2.0.14
+ source-map: 0.6.1
+
+ css-what@6.2.2: {}
+
css.escape@1.5.1: {}
cssesc@3.0.0: {}
@@ -26526,6 +30473,8 @@ snapshots:
dependencies:
character-entities: 2.0.2
+ decode-uri-component@0.2.2: {}
+
decompress-response@6.0.0:
dependencies:
mimic-response: 3.1.0
@@ -26569,7 +30518,6 @@ snapshots:
defaults@1.0.4:
dependencies:
clone: 1.0.4
- optional: true
defaults@2.0.2: {}
@@ -26654,10 +30602,10 @@ snapshots:
detective-typescript@11.2.0:
dependencies:
- '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.8.3)
+ '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.9.3)
ast-module-types: 5.0.0
node-source-walk: 6.0.2
- typescript: 5.8.3
+ typescript: 5.9.3
transitivePeerDependencies:
- supports-color
@@ -26688,6 +30636,8 @@ snapshots:
dependencies:
'@leichtgewicht/ip-codec': 2.0.5
+ dnssd-advertise@1.1.4: {}
+
doctrine@2.1.0:
dependencies:
esutils: 2.0.3
@@ -26816,6 +30766,8 @@ snapshots:
electron-to-chromium@1.5.234: {}
+ electron-to-chromium@1.5.363: {}
+
emoji-regex-xs@1.0.0: {}
emoji-regex@10.4.0: {}
@@ -27317,6 +31269,8 @@ snapshots:
escape-string-regexp@1.0.5: {}
+ escape-string-regexp@2.0.0: {}
+
escape-string-regexp@4.0.0: {}
escape-string-regexp@5.0.0: {}
@@ -27329,20 +31283,20 @@ snapshots:
optionalDependencies:
source-map: 0.6.1
- eslint-config-next@13.3.0(eslint@8.57.1)(typescript@5.8.3):
+ eslint-config-next@13.3.0(eslint@8.57.1)(typescript@5.9.3):
dependencies:
'@next/eslint-plugin-next': 13.3.0
'@rushstack/eslint-patch': 1.11.0
- '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.8.3)
+ '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.9.3)
eslint: 8.57.1
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@8.57.1)
- eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
+ eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1)
eslint-plugin-react: 7.37.5(eslint@8.57.1)
eslint-plugin-react-hooks: 4.6.2(eslint@8.57.1)
optionalDependencies:
- typescript: 5.8.3
+ typescript: 5.9.3
transitivePeerDependencies:
- eslint-import-resolver-webpack
- eslint-plugin-import-x
@@ -27396,7 +31350,7 @@ snapshots:
tinyglobby: 0.2.15
unrs-resolver: 1.7.2
optionalDependencies:
- eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
+ eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
transitivePeerDependencies:
- supports-color
@@ -27415,11 +31369,11 @@ snapshots:
transitivePeerDependencies:
- supports-color
- eslint-module-utils@2.12.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1):
+ eslint-module-utils@2.12.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1):
dependencies:
debug: 3.2.7
optionalDependencies:
- '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.8.3)
+ '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.9.3)
eslint: 8.57.1
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@8.57.1)
@@ -27437,7 +31391,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1):
+ eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.8
@@ -27448,7 +31402,7 @@ snapshots:
doctrine: 2.1.0
eslint: 8.57.1
eslint-import-resolver-node: 0.3.9
- eslint-module-utils: 2.12.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
+ eslint-module-utils: 2.12.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3
@@ -27460,7 +31414,7 @@ snapshots:
string.prototype.trimend: 1.0.9
tsconfig-paths: 3.15.0
optionalDependencies:
- '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.8.3)
+ '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.9.3)
transitivePeerDependencies:
- eslint-import-resolver-typescript
- eslint-import-resolver-webpack
@@ -27906,6 +31860,273 @@ snapshots:
expect-type@1.2.1: {}
+ expo-asset@55.0.17(expo@55.0.26)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3):
+ dependencies:
+ '@expo/image-utils': 0.8.14(typescript@5.9.3)
+ expo: 55.0.26(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.16)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ expo-constants: 55.0.16(expo@55.0.26)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0)
+ transitivePeerDependencies:
+ - supports-color
+ - typescript
+
+ expo-clipboard@55.0.13(expo@55.0.26)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
+ dependencies:
+ expo: 55.0.26(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.16)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0)
+
+ expo-constants@55.0.16(expo@55.0.26)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0)):
+ dependencies:
+ '@expo/env': 2.1.2
+ expo: 55.0.26(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.16)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ react-native: 0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0)
+ transitivePeerDependencies:
+ - supports-color
+
+ expo-dev-client@55.0.35(expo@55.0.26):
+ dependencies:
+ expo: 55.0.26(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.16)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ expo-dev-launcher: 55.0.36(expo@55.0.26)
+ expo-dev-menu: 55.0.30(expo@55.0.26)
+ expo-dev-menu-interface: 55.0.2(expo@55.0.26)
+ expo-manifests: 55.0.17(expo@55.0.26)
+ expo-updates-interface: 55.1.6(expo@55.0.26)
+
+ expo-dev-launcher@55.0.36(expo@55.0.26):
+ dependencies:
+ '@expo/schema-utils': 55.0.4
+ expo: 55.0.26(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.16)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ expo-dev-menu: 55.0.30(expo@55.0.26)
+ expo-manifests: 55.0.17(expo@55.0.26)
+
+ expo-dev-menu-interface@55.0.2(expo@55.0.26):
+ dependencies:
+ expo: 55.0.26(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.16)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+
+ expo-dev-menu@55.0.30(expo@55.0.26):
+ dependencies:
+ expo: 55.0.26(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.16)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ expo-dev-menu-interface: 55.0.2(expo@55.0.26)
+
+ expo-document-picker@55.0.13(expo@55.0.26):
+ dependencies:
+ expo: 55.0.26(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.16)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+
+ expo-file-system@55.0.22(expo@55.0.26)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0)):
+ dependencies:
+ expo: 55.0.26(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.16)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ react-native: 0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0)
+
+ expo-font@55.0.8(expo@55.0.26)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
+ dependencies:
+ expo: 55.0.26(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.16)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ fontfaceobserver: 2.3.0
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0)
+
+ expo-glass-effect@55.0.11(expo@55.0.26)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
+ dependencies:
+ expo: 55.0.26(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.16)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0)
+
+ expo-image-loader@55.0.1(expo@55.0.26):
+ dependencies:
+ expo: 55.0.26(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.16)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+
+ expo-image-picker@55.0.20(expo@55.0.26):
+ dependencies:
+ expo: 55.0.26(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.16)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ expo-image-loader: 55.0.1(expo@55.0.26)
+
+ expo-image@55.0.11(expo@55.0.26)(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
+ dependencies:
+ expo: 55.0.26(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.16)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0)
+ sf-symbols-typescript: 2.2.0
+ optionalDependencies:
+ react-native-web: 0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+
+ expo-json-utils@55.0.2: {}
+
+ expo-keep-awake@55.0.8(expo@55.0.26)(react@19.2.0):
+ dependencies:
+ expo: 55.0.26(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.16)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ react: 19.2.0
+
+ expo-linking@55.0.15(expo@55.0.26)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
+ dependencies:
+ expo-constants: 55.0.16(expo@55.0.26)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))
+ invariant: 2.2.4
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0)
+ transitivePeerDependencies:
+ - expo
+ - supports-color
+
+ expo-manifests@55.0.17(expo@55.0.26):
+ dependencies:
+ expo: 55.0.26(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.16)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ expo-json-utils: 55.0.2
+
+ expo-media-library@55.0.17(expo@55.0.26)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0)):
+ dependencies:
+ expo: 55.0.26(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.16)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ react-native: 0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0)
+
+ expo-modules-autolinking@55.0.24(typescript@5.9.3):
+ dependencies:
+ '@expo/require-utils': 55.0.5(typescript@5.9.3)
+ '@expo/spawn-async': 1.8.0
+ chalk: 4.1.2
+ commander: 7.2.0
+ transitivePeerDependencies:
+ - supports-color
+ - typescript
+
+ expo-modules-core@55.0.25(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
+ dependencies:
+ invariant: 2.2.4
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0)
+ optionalDependencies:
+ react-native-worklets: 0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+
+ expo-router@55.0.16(d69165032b49371b387000fe555903ea):
+ dependencies:
+ '@expo/log-box': 55.0.12(@expo/dom-webview@55.0.6)(expo@55.0.26)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ '@expo/metro-runtime': 55.0.11(@expo/dom-webview@55.0.6)(expo@55.0.26)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ '@expo/schema-utils': 55.0.4
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.0)
+ '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ '@react-navigation/bottom-tabs': 7.16.2(@react-navigation/native@7.2.5(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-safe-area-context@5.6.2(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-screens@4.23.0(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ '@react-navigation/native': 7.2.5(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ '@react-navigation/native-stack': 7.16.0(@react-navigation/native@7.2.5(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-safe-area-context@5.6.2(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-screens@4.23.0(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ client-only: 0.0.1
+ debug: 4.4.3(supports-color@8.1.1)
+ escape-string-regexp: 4.0.0
+ expo: 55.0.26(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.16)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ expo-constants: 55.0.16(expo@55.0.26)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))
+ expo-glass-effect: 55.0.11(expo@55.0.26)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ expo-image: 55.0.11(expo@55.0.26)(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ expo-linking: 55.0.15(expo@55.0.26)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ expo-server: 55.0.11
+ expo-symbols: 55.0.9(expo-font@55.0.8)(expo@55.0.26)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ fast-deep-equal: 3.1.3
+ invariant: 2.2.4
+ nanoid: 3.3.11
+ query-string: 7.1.3
+ react: 19.2.0
+ react-fast-compare: 3.2.2
+ react-native: 0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0)
+ react-native-is-edge-to-edge: 1.3.1(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ react-native-safe-area-context: 5.6.2(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ react-native-screens: 4.23.0(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ semver: 7.6.3
+ server-only: 0.0.1
+ sf-symbols-typescript: 2.2.0
+ shallowequal: 1.1.0
+ use-latest-callback: 0.2.6(react@19.2.0)
+ vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ optionalDependencies:
+ '@testing-library/react-native': 13.3.3(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react-test-renderer@19.2.0(react@19.2.0))(react@19.2.0)
+ react-dom: 19.2.0(react@19.2.0)
+ react-native-gesture-handler: 2.30.1(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ react-native-reanimated: 4.2.1(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ react-native-web: 0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ transitivePeerDependencies:
+ - '@react-native-masked-view/masked-view'
+ - '@types/react'
+ - '@types/react-dom'
+ - expo-font
+ - supports-color
+
+ expo-secure-store@55.0.14(expo@55.0.26):
+ dependencies:
+ expo: 55.0.26(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.16)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+
+ expo-server@55.0.11: {}
+
+ expo-sharing@55.0.20(expo@55.0.26)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
+ dependencies:
+ '@expo/config-plugins': 55.0.10
+ '@expo/config-types': 55.0.5
+ '@expo/plist': 0.5.4
+ expo: 55.0.26(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.16)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0)
+ transitivePeerDependencies:
+ - supports-color
+
+ expo-symbols@55.0.9(expo-font@55.0.8)(expo@55.0.26)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
+ dependencies:
+ '@expo-google-fonts/material-symbols': 0.4.38
+ expo: 55.0.26(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.16)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ expo-font: 55.0.8(expo@55.0.26)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0)
+ sf-symbols-typescript: 2.2.0
+
+ expo-updates-interface@55.1.6(expo@55.0.26):
+ dependencies:
+ expo: 55.0.26(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.16)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+
+ expo-video@55.0.17(expo@55.0.26)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
+ dependencies:
+ expo: 55.0.26(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.16)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0)
+
+ expo-web-browser@55.0.16(expo@55.0.26)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0)):
+ dependencies:
+ expo: 55.0.26(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.16)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ react-native: 0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0)
+
+ expo@55.0.26(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.16)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3):
+ dependencies:
+ '@babel/runtime': 7.27.1
+ '@expo/cli': 55.0.32(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-constants@55.0.16)(expo-font@55.0.8)(expo-router@55.0.16)(expo@55.0.26)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ '@expo/config': 55.0.17(typescript@5.9.3)
+ '@expo/config-plugins': 55.0.10
+ '@expo/devtools': 55.0.3(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ '@expo/fingerprint': 0.16.7
+ '@expo/local-build-cache-provider': 55.0.13(typescript@5.9.3)
+ '@expo/log-box': 55.0.12(@expo/dom-webview@55.0.6)(expo@55.0.26)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ '@expo/metro': 55.1.1
+ '@expo/metro-config': 55.0.23(expo@55.0.26)(typescript@5.9.3)
+ '@expo/vector-icons': 15.1.1(expo-font@55.0.8)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ '@ungap/structured-clone': 1.3.0
+ babel-preset-expo: 55.0.22(@babel/core@7.27.1)(@babel/runtime@7.27.1)(expo@55.0.26)(react-refresh@0.14.2)
+ expo-asset: 55.0.17(expo@55.0.26)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ expo-constants: 55.0.16(expo@55.0.26)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))
+ expo-file-system: 55.0.22(expo@55.0.26)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))
+ expo-font: 55.0.8(expo@55.0.26)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ expo-keep-awake: 55.0.8(expo@55.0.26)(react@19.2.0)
+ expo-modules-autolinking: 55.0.24(typescript@5.9.3)
+ expo-modules-core: 55.0.25(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ pretty-format: 29.7.0
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0)
+ react-refresh: 0.14.2
+ whatwg-url-minimum: 0.1.2
+ optionalDependencies:
+ '@expo/dom-webview': 55.0.6(expo@55.0.26)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ '@expo/metro-runtime': 55.0.11(@expo/dom-webview@55.0.6)(expo@55.0.26)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ transitivePeerDependencies:
+ - '@babel/core'
+ - bufferutil
+ - expo-router
+ - expo-widgets
+ - react-dom
+ - react-native-worklets
+ - react-server-dom-webpack
+ - supports-color
+ - typescript
+ - utf-8-validate
+
exponential-backoff@3.1.2: {}
express-rate-limit@7.5.1(express@5.1.0):
@@ -28067,6 +32288,26 @@ snapshots:
dependencies:
reusify: 1.1.0
+ fb-dotslash@0.5.8: {}
+
+ fb-watchman@2.0.2:
+ dependencies:
+ bser: 2.1.1
+
+ fbjs-css-vars@1.0.2: {}
+
+ fbjs@3.0.5(encoding@0.1.13):
+ dependencies:
+ cross-fetch: 3.2.0(encoding@0.1.13)
+ fbjs-css-vars: 1.0.2
+ loose-envify: 1.4.0
+ object-assign: 4.1.1
+ promise: 7.3.1
+ setimmediate: 1.0.5
+ ua-parser-js: 1.0.41
+ transitivePeerDependencies:
+ - encoding
+
fd-slicer@1.1.0:
dependencies:
pend: 1.2.0
@@ -28094,6 +32335,8 @@ snapshots:
node-domexception: 1.0.0
web-streams-polyfill: 3.3.3
+ fetch-nodeshim@0.4.10: {}
+
fflate@0.4.8: {}
fflate@0.8.2: {}
@@ -28153,8 +32396,22 @@ snapshots:
dependencies:
to-regex-range: 5.0.1
+ filter-obj@1.1.0: {}
+
filter-obj@5.1.0: {}
+ finalhandler@1.1.2:
+ dependencies:
+ debug: 2.6.9
+ encodeurl: 1.0.2
+ escape-html: 1.0.3
+ on-finished: 2.3.0
+ parseurl: 1.3.3
+ statuses: 1.5.0
+ unpipe: 1.0.0
+ transitivePeerDependencies:
+ - supports-color
+
finalhandler@1.3.2:
dependencies:
debug: 2.6.9
@@ -28182,6 +32439,11 @@ snapshots:
find-up-simple@1.0.1: {}
+ find-up@4.1.0:
+ dependencies:
+ locate-path: 5.0.0
+ path-exists: 4.0.0
+
find-up@5.0.0:
dependencies:
locate-path: 6.0.0
@@ -28221,10 +32483,14 @@ snapshots:
flatted@3.3.3: {}
+ flow-enums-runtime@0.0.6: {}
+
fn.name@1.1.0: {}
follow-redirects@1.15.9: {}
+ fontfaceobserver@2.3.0: {}
+
for-each@0.3.5:
dependencies:
is-callable: 1.2.7
@@ -28328,9 +32594,9 @@ snapshots:
strip-ansi: 6.0.1
wide-align: 1.1.5
- geist@1.4.2(next@16.2.1(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)):
+ geist@1.4.2(next@16.2.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)):
dependencies:
- next: 16.2.1(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ next: 16.2.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
generate-function@2.3.1:
dependencies:
@@ -28398,6 +32664,8 @@ snapshots:
dependencies:
resolve-pkg-maps: 1.0.0
+ getenv@2.0.0: {}
+
gif.js@0.2.0: {}
giget@2.0.0:
@@ -28437,6 +32705,12 @@ snapshots:
package-json-from-dist: 1.0.1
path-scurry: 2.0.0
+ glob@13.0.6:
+ dependencies:
+ minimatch: 10.2.4
+ minipass: 7.1.3
+ path-scurry: 2.0.2
+
glob@7.1.7:
dependencies:
fs.realpath: 1.0.0
@@ -28707,12 +32981,32 @@ snapshots:
property-information: 7.0.0
space-separated-tokens: 2.0.2
+ hermes-compiler@0.14.1: {}
+
hermes-estree@0.25.1: {}
+ hermes-estree@0.32.0: {}
+
+ hermes-estree@0.32.1: {}
+
+ hermes-estree@0.35.0: {}
+
hermes-parser@0.25.1:
dependencies:
hermes-estree: 0.25.1
+ hermes-parser@0.32.0:
+ dependencies:
+ hermes-estree: 0.32.0
+
+ hermes-parser@0.32.1:
+ dependencies:
+ hermes-estree: 0.32.1
+
+ hermes-parser@0.35.0:
+ dependencies:
+ hermes-estree: 0.35.0
+
hls.js@0.14.17:
dependencies:
eventemitter3: 4.0.7
@@ -28722,6 +33016,10 @@ snapshots:
hls.js@1.6.2: {}
+ hoist-non-react-statics@3.3.2:
+ dependencies:
+ react-is: 16.13.1
+
hono@4.12.12: {}
hono@4.7.4: {}
@@ -28838,6 +33136,8 @@ snapshots:
dependencies:
ms: 2.1.3
+ hyphenate-style-name@1.1.0: {}
+
iconv-lite@0.4.24:
dependencies:
safer-buffer: 2.1.2
@@ -28866,6 +33166,10 @@ snapshots:
ignore@7.0.5: {}
+ image-size@1.2.1:
+ dependencies:
+ queue: 6.0.2
+
immediate@3.0.6: {}
immer@10.2.0: {}
@@ -28903,6 +33207,10 @@ snapshots:
inline-style-parser@0.2.4: {}
+ inline-style-prefixer@7.0.1:
+ dependencies:
+ css-in-js-utils: 3.1.0
+
inspect-with-kind@1.0.5:
dependencies:
kind-of: 6.0.3
@@ -28917,6 +33225,10 @@ snapshots:
internmap@2.0.3: {}
+ invariant@2.2.4:
+ dependencies:
+ loose-envify: 1.4.0
+
ioredis@5.6.1:
dependencies:
'@ioredis/commands': 1.2.0
@@ -28940,7 +33252,7 @@ snapshots:
ipaddr.js@1.9.1: {}
- iron-session@6.3.1(express@5.1.0)(next@16.2.1(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)):
+ iron-session@6.3.1(express@5.1.0)(next@16.2.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)):
dependencies:
'@peculiar/webcrypto': 1.5.0
'@types/cookie': 0.5.4
@@ -28951,7 +33263,7 @@ snapshots:
iron-webcrypto: 0.2.8
optionalDependencies:
express: 5.1.0
- next: 16.2.1(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ next: 16.2.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
iron-webcrypto@0.2.8:
dependencies:
@@ -29176,6 +33488,16 @@ snapshots:
istanbul-lib-coverage@3.2.2: {}
+ istanbul-lib-instrument@5.2.1:
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/parser': 7.28.4
+ '@istanbuljs/schema': 0.1.3
+ istanbul-lib-coverage: 3.2.2
+ semver: 6.3.1
+ transitivePeerDependencies:
+ - supports-color
+
istanbul-lib-report@3.0.1:
dependencies:
istanbul-lib-coverage: 3.2.2
@@ -29222,6 +33544,85 @@ snapshots:
filelist: 1.0.4
picocolors: 1.1.1
+ jest-diff@30.4.1:
+ dependencies:
+ '@jest/diff-sequences': 30.4.0
+ '@jest/get-type': 30.1.0
+ chalk: 4.1.2
+ pretty-format: 30.4.1
+
+ jest-environment-node@29.7.0:
+ dependencies:
+ '@jest/environment': 29.7.0
+ '@jest/fake-timers': 29.7.0
+ '@jest/types': 29.6.3
+ '@types/node': 20.19.21
+ jest-mock: 29.7.0
+ jest-util: 29.7.0
+
+ jest-get-type@29.6.3: {}
+
+ jest-haste-map@29.7.0:
+ dependencies:
+ '@jest/types': 29.6.3
+ '@types/graceful-fs': 4.1.9
+ '@types/node': 20.19.21
+ anymatch: 3.1.3
+ fb-watchman: 2.0.2
+ graceful-fs: 4.2.11
+ jest-regex-util: 29.6.3
+ jest-util: 29.7.0
+ jest-worker: 29.7.0
+ micromatch: 4.0.8
+ walker: 1.0.8
+ optionalDependencies:
+ fsevents: 2.3.3
+
+ jest-matcher-utils@30.4.1:
+ dependencies:
+ '@jest/get-type': 30.1.0
+ chalk: 4.1.2
+ jest-diff: 30.4.1
+ pretty-format: 30.4.1
+
+ jest-message-util@29.7.0:
+ dependencies:
+ '@babel/code-frame': 7.27.1
+ '@jest/types': 29.6.3
+ '@types/stack-utils': 2.0.3
+ chalk: 4.1.2
+ graceful-fs: 4.2.11
+ micromatch: 4.0.8
+ pretty-format: 29.7.0
+ slash: 3.0.0
+ stack-utils: 2.0.6
+
+ jest-mock@29.7.0:
+ dependencies:
+ '@jest/types': 29.6.3
+ '@types/node': 20.19.21
+ jest-util: 29.7.0
+
+ jest-regex-util@29.6.3: {}
+
+ jest-util@29.7.0:
+ dependencies:
+ '@jest/types': 29.6.3
+ '@types/node': 20.19.21
+ chalk: 4.1.2
+ ci-info: 3.9.0
+ graceful-fs: 4.2.11
+ picomatch: 2.3.1
+
+ jest-validate@29.7.0:
+ dependencies:
+ '@jest/types': 29.6.3
+ camelcase: 6.3.0
+ chalk: 4.1.2
+ jest-get-type: 29.6.3
+ leven: 3.1.0
+ pretty-format: 29.7.0
+
jest-worker@27.5.1:
dependencies:
'@types/node': 20.19.21
@@ -29229,6 +33630,15 @@ snapshots:
supports-color: 8.1.1
optional: true
+ jest-worker@29.7.0:
+ dependencies:
+ '@types/node': 20.19.21
+ jest-util: 29.7.0
+ merge-stream: 2.0.0
+ supports-color: 8.1.1
+
+ jimp-compact@0.16.1: {}
+
jiti@1.21.7: {}
jiti@2.4.2: {}
@@ -29264,6 +33674,8 @@ snapshots:
jsbn@1.1.0: {}
+ jsc-safe-url@0.2.4: {}
+
jsdoc-type-pratt-parser@4.1.0: {}
jsdom@26.1.0:
@@ -29370,6 +33782,8 @@ snapshots:
dotenv: 16.5.0
winston: 3.17.0
+ lan-network@0.2.1: {}
+
language-subtag-registry@0.3.23: {}
language-tags@1.0.9:
@@ -29384,6 +33798,8 @@ snapshots:
leb@1.0.0: {}
+ leven@3.1.0: {}
+
levn@0.4.1:
dependencies:
prelude-ls: 1.2.1
@@ -29393,6 +33809,13 @@ snapshots:
dependencies:
immediate: 3.0.6
+ lighthouse-logger@1.4.2:
+ dependencies:
+ debug: 2.6.9
+ marky: 1.3.0
+ transitivePeerDependencies:
+ - supports-color
+
lightningcss-android-arm64@1.32.0:
optional: true
@@ -29485,6 +33908,10 @@ snapshots:
pkg-types: 2.1.0
quansync: 0.2.10
+ locate-path@5.0.0:
+ dependencies:
+ p-locate: 4.1.0
+
locate-path@6.0.0:
dependencies:
p-locate: 5.0.0
@@ -29515,12 +33942,18 @@ snapshots:
lodash.sortby@4.7.0: {}
+ lodash.throttle@4.1.1: {}
+
lodash.truncate@4.4.2: {}
lodash.union@4.6.0: {}
lodash@4.17.21: {}
+ log-symbols@2.2.0:
+ dependencies:
+ chalk: 2.4.2
+
log-symbols@6.0.0:
dependencies:
chalk: 5.6.2
@@ -29642,6 +34075,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ makeerror@1.0.12:
+ dependencies:
+ tmpl: 1.0.5
+
map-or-similar@1.5.0: {}
markdown-extensions@2.0.0: {}
@@ -29650,6 +34087,8 @@ snapshots:
marked@7.0.4: {}
+ marky@1.3.0: {}
+
math-intrinsics@1.1.0: {}
md-to-react-email@5.0.5(react@19.1.1):
@@ -29825,6 +34264,8 @@ snapshots:
dependencies:
'@types/mdast': 4.0.4
+ mdn-data@2.0.14: {}
+
media-chrome@4.12.0(react@19.2.4):
dependencies:
ce-la-react: 0.3.0(react@19.2.4)
@@ -29849,6 +34290,10 @@ snapshots:
'@types/dom-mediacapture-transform': 0.1.11
'@types/dom-webcodecs': 0.1.13
+ memoize-one@5.2.1: {}
+
+ memoize-one@6.0.0: {}
+
memoizerific@1.11.3:
dependencies:
map-or-similar: 1.5.0
@@ -29871,6 +34316,180 @@ snapshots:
methods@1.1.2: {}
+ metro-babel-transformer@0.83.7:
+ dependencies:
+ '@babel/core': 7.27.1
+ flow-enums-runtime: 0.0.6
+ hermes-parser: 0.35.0
+ metro-cache-key: 0.83.7
+ nullthrows: 1.1.1
+ transitivePeerDependencies:
+ - supports-color
+
+ metro-cache-key@0.83.7:
+ dependencies:
+ flow-enums-runtime: 0.0.6
+
+ metro-cache@0.83.7:
+ dependencies:
+ exponential-backoff: 3.1.2
+ flow-enums-runtime: 0.0.6
+ https-proxy-agent: 7.0.6
+ metro-core: 0.83.7
+ transitivePeerDependencies:
+ - supports-color
+
+ metro-config@0.83.7:
+ dependencies:
+ connect: 3.7.0
+ flow-enums-runtime: 0.0.6
+ jest-validate: 29.7.0
+ metro: 0.83.7
+ metro-cache: 0.83.7
+ metro-core: 0.83.7
+ metro-runtime: 0.83.7
+ yaml: 2.8.1
+ transitivePeerDependencies:
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+
+ metro-core@0.83.7:
+ dependencies:
+ flow-enums-runtime: 0.0.6
+ lodash.throttle: 4.1.1
+ metro-resolver: 0.83.7
+
+ metro-file-map@0.83.7:
+ dependencies:
+ debug: 4.4.3(supports-color@8.1.1)
+ fb-watchman: 2.0.2
+ flow-enums-runtime: 0.0.6
+ graceful-fs: 4.2.11
+ invariant: 2.2.4
+ jest-worker: 29.7.0
+ micromatch: 4.0.8
+ nullthrows: 1.1.1
+ walker: 1.0.8
+ transitivePeerDependencies:
+ - supports-color
+
+ metro-minify-terser@0.83.7:
+ dependencies:
+ flow-enums-runtime: 0.0.6
+ terser: 5.44.0
+
+ metro-resolver@0.83.7:
+ dependencies:
+ flow-enums-runtime: 0.0.6
+
+ metro-runtime@0.83.7:
+ dependencies:
+ '@babel/runtime': 7.27.1
+ flow-enums-runtime: 0.0.6
+
+ metro-source-map@0.83.7:
+ dependencies:
+ '@babel/traverse': 7.29.7
+ '@babel/types': 7.29.7
+ flow-enums-runtime: 0.0.6
+ invariant: 2.2.4
+ metro-symbolicate: 0.83.7
+ nullthrows: 1.1.1
+ ob1: 0.83.7
+ source-map: 0.5.7
+ vlq: 1.0.1
+ transitivePeerDependencies:
+ - supports-color
+
+ metro-symbolicate@0.83.7:
+ dependencies:
+ flow-enums-runtime: 0.0.6
+ invariant: 2.2.4
+ metro-source-map: 0.83.7
+ nullthrows: 1.1.1
+ source-map: 0.5.7
+ vlq: 1.0.1
+ transitivePeerDependencies:
+ - supports-color
+
+ metro-transform-plugins@0.83.7:
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/generator': 7.29.7
+ '@babel/template': 7.29.7
+ '@babel/traverse': 7.29.7
+ flow-enums-runtime: 0.0.6
+ nullthrows: 1.1.1
+ transitivePeerDependencies:
+ - supports-color
+
+ metro-transform-worker@0.83.7:
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/generator': 7.29.7
+ '@babel/parser': 7.29.7
+ '@babel/types': 7.29.7
+ flow-enums-runtime: 0.0.6
+ metro: 0.83.7
+ metro-babel-transformer: 0.83.7
+ metro-cache: 0.83.7
+ metro-cache-key: 0.83.7
+ metro-minify-terser: 0.83.7
+ metro-source-map: 0.83.7
+ metro-transform-plugins: 0.83.7
+ nullthrows: 1.1.1
+ transitivePeerDependencies:
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+
+ metro@0.83.7:
+ dependencies:
+ '@babel/code-frame': 7.29.7
+ '@babel/core': 7.27.1
+ '@babel/generator': 7.29.7
+ '@babel/parser': 7.29.7
+ '@babel/template': 7.29.7
+ '@babel/traverse': 7.29.7
+ '@babel/types': 7.29.7
+ accepts: 2.0.0
+ ci-info: 2.0.0
+ connect: 3.7.0
+ debug: 4.4.3(supports-color@8.1.1)
+ error-stack-parser: 2.1.4
+ flow-enums-runtime: 0.0.6
+ graceful-fs: 4.2.11
+ hermes-parser: 0.35.0
+ image-size: 1.2.1
+ invariant: 2.2.4
+ jest-worker: 29.7.0
+ jsc-safe-url: 0.2.4
+ lodash.throttle: 4.1.1
+ metro-babel-transformer: 0.83.7
+ metro-cache: 0.83.7
+ metro-cache-key: 0.83.7
+ metro-config: 0.83.7
+ metro-core: 0.83.7
+ metro-file-map: 0.83.7
+ metro-resolver: 0.83.7
+ metro-runtime: 0.83.7
+ metro-source-map: 0.83.7
+ metro-symbolicate: 0.83.7
+ metro-transform-plugins: 0.83.7
+ metro-transform-worker: 0.83.7
+ mime-types: 3.0.1
+ nullthrows: 1.1.1
+ serialize-error: 2.1.0
+ source-map: 0.5.7
+ throat: 5.0.0
+ ws: 7.5.11
+ yargs: 17.7.2
+ transitivePeerDependencies:
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+
micro-api-client@3.3.0: {}
micromark-core-commonmark@2.0.3:
@@ -30160,6 +34779,8 @@ snapshots:
mime@4.0.7: {}
+ mimic-fn@1.2.0: {}
+
mimic-fn@2.1.0: {}
mimic-fn@4.0.0: {}
@@ -30262,6 +34883,8 @@ snapshots:
minipass@7.1.2: {}
+ minipass@7.1.3: {}
+
minizlib@2.1.2:
dependencies:
minipass: 3.3.6
@@ -30360,6 +34983,8 @@ snapshots:
multipasta@0.2.7: {}
+ multitars@1.0.0: {}
+
mustache@4.2.0: {}
mux-embed@5.9.0: {}
@@ -30418,13 +35043,13 @@ snapshots:
p-wait-for: 5.0.2
qs: 6.14.0
- next-auth@4.24.11(next@15.5.9(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(nodemailer@6.10.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
+ next-auth@4.24.11(next@15.5.9(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(nodemailer@6.10.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
dependencies:
'@babel/runtime': 7.27.1
'@panva/hkdf': 1.2.1
cookie: 0.7.2
jose: 4.15.9
- next: 15.5.9(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ next: 15.5.9(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
oauth: 0.9.15
openid-client: 5.7.1
preact: 10.26.6
@@ -30435,13 +35060,13 @@ snapshots:
optionalDependencies:
nodemailer: 6.10.1
- next-auth@4.24.11(next@16.2.1(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(nodemailer@6.10.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
+ next-auth@4.24.11(next@16.2.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(nodemailer@6.10.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
'@babel/runtime': 7.27.1
'@panva/hkdf': 1.2.1
cookie: 0.7.2
jose: 4.15.9
- next: 16.2.1(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ next: 16.2.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
oauth: 0.9.15
openid-client: 5.7.1
preact: 10.26.6
@@ -30467,7 +35092,7 @@ snapshots:
- acorn
- supports-color
- next@15.5.9(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
+ next@15.5.9(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
dependencies:
'@next/env': 15.5.9
'@swc/helpers': 0.5.15
@@ -30486,12 +35111,13 @@ snapshots:
'@next/swc-win32-arm64-msvc': 15.5.7
'@next/swc-win32-x64-msvc': 15.5.7
'@opentelemetry/api': 1.9.0
+ babel-plugin-react-compiler: 1.0.0
sharp: 0.34.5
transitivePeerDependencies:
- '@babel/core'
- babel-plugin-macros
- next@15.5.9(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
+ next@15.5.9(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
'@next/env': 15.5.9
'@swc/helpers': 0.5.15
@@ -30510,12 +35136,13 @@ snapshots:
'@next/swc-win32-arm64-msvc': 15.5.7
'@next/swc-win32-x64-msvc': 15.5.7
'@opentelemetry/api': 1.9.0
+ babel-plugin-react-compiler: 1.0.0
sharp: 0.34.5
transitivePeerDependencies:
- '@babel/core'
- babel-plugin-macros
- next@16.2.1(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
+ next@16.2.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
'@next/env': 16.2.1
'@swc/helpers': 0.5.15
@@ -30535,12 +35162,13 @@ snapshots:
'@next/swc-win32-arm64-msvc': 16.2.1
'@next/swc-win32-x64-msvc': 16.2.1
'@opentelemetry/api': 1.9.0
+ babel-plugin-react-compiler: 1.0.0
sharp: 0.34.5
transitivePeerDependencies:
- '@babel/core'
- babel-plugin-macros
- nitropack@2.11.11(@planetscale/database@1.19.0)(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(mysql2@3.15.2)(rolldown@1.0.1)(xml2js@0.6.2):
+ nitropack@2.11.11(@planetscale/database@1.19.0)(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(mysql2@3.15.2)(rolldown@1.0.3)(xml2js@0.6.2):
dependencies:
'@cloudflare/kv-asset-handler': 0.4.0
'@netlify/functions': 3.1.5(encoding@0.1.13)(rollup@4.40.2)
@@ -30594,7 +35222,7 @@ snapshots:
pretty-bytes: 6.1.1
radix3: 1.1.2
rollup: 4.40.2
- rollup-plugin-visualizer: 5.14.0(rolldown@1.0.1)(rollup@4.40.2)
+ rollup-plugin-visualizer: 5.14.0(rolldown@1.0.3)(rollup@4.40.2)
scule: 1.3.0
semver: 7.7.2
serve-placeholder: 2.0.2
@@ -30678,6 +35306,8 @@ snapshots:
node-forge@1.3.1: {}
+ node-forge@1.4.0: {}
+
node-gyp-build-optional-packages@5.1.1:
dependencies:
detect-libc: 2.1.2
@@ -30713,6 +35343,8 @@ snapshots:
node-releases@2.0.23: {}
+ node-releases@2.0.46: {}
+
node-source-walk@6.0.2:
dependencies:
'@babel/parser': 7.28.4
@@ -30755,7 +35387,7 @@ snapshots:
npm-install-checks@6.3.0:
dependencies:
- semver: 7.7.3
+ semver: 7.7.4
npm-normalize-package-bin@3.0.1: {}
@@ -30763,7 +35395,7 @@ snapshots:
dependencies:
hosted-git-info: 7.0.2
proc-log: 4.2.0
- semver: 7.7.3
+ semver: 7.7.4
validate-npm-package-name: 5.0.1
npm-packlist@8.0.2:
@@ -30775,7 +35407,7 @@ snapshots:
npm-install-checks: 6.3.0
npm-normalize-package-bin: 3.0.1
npm-package-arg: 11.0.3
- semver: 7.7.3
+ semver: 7.7.4
npm-registry-fetch@17.1.0:
dependencies:
@@ -30805,6 +35437,12 @@ snapshots:
gauge: 3.0.2
set-blocking: 2.0.0
+ nth-check@2.1.1:
+ dependencies:
+ boolbase: 1.0.0
+
+ nullthrows@1.1.1: {}
+
number-flow@0.5.7:
dependencies:
esm-env: 1.2.2
@@ -30821,6 +35459,10 @@ snapshots:
oauth@0.9.15: {}
+ ob1@0.83.7:
+ dependencies:
+ flow-enums-runtime: 0.0.6
+
object-assign@4.1.1: {}
object-hash@2.2.0: {}
@@ -30899,10 +35541,16 @@ snapshots:
oidc-token-hash@5.1.1: {}
+ on-finished@2.3.0:
+ dependencies:
+ ee-first: 1.1.1
+
on-finished@2.4.1:
dependencies:
ee-first: 1.1.1
+ on-headers@1.1.0: {}
+
once@1.4.0:
dependencies:
wrappy: 1.0.2
@@ -30911,6 +35559,10 @@ snapshots:
dependencies:
fn.name: 1.1.0
+ onetime@2.0.1:
+ dependencies:
+ mimic-fn: 1.2.0
+
onetime@5.1.2:
dependencies:
mimic-fn: 2.1.0
@@ -30944,6 +35596,11 @@ snapshots:
is-inside-container: 1.0.0
wsl-utils: 0.1.0
+ open@7.4.2:
+ dependencies:
+ is-docker: 2.2.1
+ is-wsl: 2.2.0
+
open@8.4.0:
dependencies:
define-lazy-prop: 2.0.0
@@ -30989,6 +35646,15 @@ snapshots:
type-check: 0.4.0
word-wrap: 1.2.5
+ ora@3.4.0:
+ dependencies:
+ chalk: 2.4.2
+ cli-cursor: 2.1.0
+ cli-spinners: 2.9.2
+ log-symbols: 2.2.0
+ strip-ansi: 5.2.0
+ wcwidth: 1.0.1
+
ora@8.2.0:
dependencies:
chalk: 5.4.1
@@ -31017,6 +35683,10 @@ snapshots:
dependencies:
p-timeout: 5.1.0
+ p-limit@2.3.0:
+ dependencies:
+ p-try: 2.2.0
+
p-limit@3.1.0:
dependencies:
yocto-queue: 0.1.0
@@ -31025,6 +35695,10 @@ snapshots:
dependencies:
yocto-queue: 1.2.1
+ p-locate@4.1.0:
+ dependencies:
+ p-limit: 2.3.0
+
p-locate@5.0.0:
dependencies:
p-limit: 3.1.0
@@ -31043,6 +35717,8 @@ snapshots:
p-timeout@6.1.4: {}
+ p-try@2.2.0: {}
+
p-wait-for@5.0.2:
dependencies:
p-timeout: 6.1.4
@@ -31116,6 +35792,10 @@ snapshots:
parse-numeric-range@1.3.0: {}
+ parse-png@2.1.0:
+ dependencies:
+ pngjs: 3.4.0
+
parse5@7.3.0:
dependencies:
entities: 6.0.0
@@ -31151,6 +35831,11 @@ snapshots:
lru-cache: 11.1.0
minipass: 7.1.2
+ path-scurry@2.0.2:
+ dependencies:
+ lru-cache: 11.1.0
+ minipass: 7.1.3
+
path-to-regexp@0.1.13: {}
path-to-regexp@6.3.0: {}
@@ -31223,8 +35908,16 @@ snapshots:
transitivePeerDependencies:
- react
+ plist@3.1.1:
+ dependencies:
+ '@xmldom/xmldom': 0.9.10
+ base64-js: 1.5.1
+ xmlbuilder: 15.1.1
+
pluralize@8.0.0: {}
+ pngjs@3.4.0: {}
+
polished@4.3.1:
dependencies:
'@babel/runtime': 7.27.1
@@ -31366,8 +36059,21 @@ snapshots:
ansi-styles: 5.2.0
react-is: 17.0.2
+ pretty-format@29.7.0:
+ dependencies:
+ '@jest/schemas': 29.6.3
+ ansi-styles: 5.2.0
+ react-is: 18.3.1
+
pretty-format@3.8.0: {}
+ pretty-format@30.4.1:
+ dependencies:
+ '@jest/schemas': 30.4.1
+ ansi-styles: 5.2.0
+ react-is-18: react-is@18.3.1
+ react-is-19: react-is@19.2.6
+
printable-characters@1.0.42: {}
prism-react-renderer@2.4.1(react@19.2.4):
@@ -31399,6 +36105,14 @@ snapshots:
err-code: 2.0.3
retry: 0.12.0
+ promise@7.3.1:
+ dependencies:
+ asap: 2.0.6
+
+ promise@8.3.0:
+ dependencies:
+ asap: 2.0.6
+
prompts@2.4.2:
dependencies:
kleur: 3.0.3
@@ -31461,10 +36175,21 @@ snapshots:
quansync@0.2.11: {}
+ query-string@7.1.3:
+ dependencies:
+ decode-uri-component: 0.2.2
+ filter-obj: 1.1.0
+ split-on-first: 1.1.0
+ strict-uri-encode: 2.0.0
+
querystring@0.2.0: {}
queue-microtask@1.2.3: {}
+ queue@6.0.2:
+ dependencies:
+ inherits: 2.0.4
+
quick-lru@5.1.1: {}
quote-unquote@1.0.0: {}
@@ -31511,11 +36236,24 @@ snapshots:
react: 19.2.4
tween-functions: 1.2.0
+ react-devtools-core@6.1.5:
+ dependencies:
+ shell-quote: 1.8.3
+ ws: 7.5.11
+ transitivePeerDependencies:
+ - bufferutil
+ - utf-8-validate
+
react-dom@19.1.1(react@19.1.1):
dependencies:
react: 19.1.1
scheduler: 0.26.0
+ react-dom@19.2.0(react@19.2.0):
+ dependencies:
+ react: 19.2.0
+ scheduler: 0.27.0
+
react-dom@19.2.4(react@19.2.4):
dependencies:
react: 19.2.4
@@ -31528,7 +36266,7 @@ snapshots:
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
- react-email@4.0.16(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
+ react-email@4.0.16(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
dependencies:
'@babel/parser': 7.27.5
'@babel/traverse': 7.27.4
@@ -31540,7 +36278,7 @@ snapshots:
glob: 11.0.3
log-symbols: 7.0.1
mime-types: 3.0.1
- next: 15.5.9(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ next: 15.5.9(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
normalize-path: 3.0.0
ora: 8.2.0
socket.io: 4.8.1
@@ -31580,6 +36318,12 @@ snapshots:
- supports-color
- utf-8-validate
+ react-fast-compare@3.2.2: {}
+
+ react-freeze@1.0.4(react@19.2.0):
+ dependencies:
+ react: 19.2.0
+
react-hls-player@3.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
hls.js: 0.14.17
@@ -31607,6 +36351,10 @@ snapshots:
react-is@17.0.2: {}
+ react-is@18.3.1: {}
+
+ react-is@19.2.6: {}
+
react-loading-skeleton@3.5.0(react@19.2.4):
dependencies:
react: 19.2.4
@@ -31629,6 +36377,133 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ react-native-gesture-handler@2.30.1(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
+ dependencies:
+ '@egjs/hammerjs': 2.0.17
+ hoist-non-react-statics: 3.3.2
+ invariant: 2.2.4
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0)
+
+ react-native-is-edge-to-edge@1.2.1(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
+ dependencies:
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0)
+
+ react-native-is-edge-to-edge@1.3.1(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
+ dependencies:
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0)
+
+ react-native-reanimated@4.2.1(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
+ dependencies:
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0)
+ react-native-is-edge-to-edge: 1.2.1(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ react-native-worklets: 0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ semver: 7.7.3
+
+ react-native-safe-area-context@5.6.2(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
+ dependencies:
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0)
+
+ react-native-screens@4.23.0(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
+ dependencies:
+ react: 19.2.0
+ react-freeze: 1.0.4(react@19.2.0)
+ react-native: 0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0)
+ warn-once: 0.1.1
+
+ react-native-svg@15.15.5(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
+ dependencies:
+ css-select: 5.2.2
+ css-tree: 1.1.3
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0)
+
+ react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
+ dependencies:
+ '@babel/runtime': 7.27.1
+ '@react-native/normalize-colors': 0.74.89
+ fbjs: 3.0.5(encoding@0.1.13)
+ inline-style-prefixer: 7.0.1
+ memoize-one: 6.0.0
+ nullthrows: 1.1.1
+ postcss-value-parser: 4.2.0
+ react: 19.2.0
+ react-dom: 19.2.0(react@19.2.0)
+ styleq: 0.1.3
+ transitivePeerDependencies:
+ - encoding
+
+ react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.27.1)
+ '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.27.1)
+ '@babel/plugin-transform-classes': 7.28.4(@babel/core@7.27.1)
+ '@babel/plugin-transform-nullish-coalescing-operator': 7.27.1(@babel/core@7.27.1)
+ '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.27.1)
+ '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.27.1)
+ '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.27.1)
+ '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.27.1)
+ '@babel/preset-typescript': 7.27.1(@babel/core@7.27.1)
+ convert-source-map: 2.0.0
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0)
+ semver: 7.7.3
+ transitivePeerDependencies:
+ - supports-color
+
+ react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0):
+ dependencies:
+ '@jest/create-cache-key-function': 29.7.0
+ '@react-native/assets-registry': 0.83.6
+ '@react-native/codegen': 0.83.6(@babel/core@7.27.1)
+ '@react-native/community-cli-plugin': 0.83.6
+ '@react-native/gradle-plugin': 0.83.6
+ '@react-native/js-polyfills': 0.83.6
+ '@react-native/normalize-colors': 0.83.6
+ '@react-native/virtualized-lists': 0.83.6(@types/react@19.2.14)(react-native@0.83.6(@babel/core@7.27.1)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ abort-controller: 3.0.0
+ anser: 1.4.10
+ ansi-regex: 5.0.1
+ babel-jest: 29.7.0(@babel/core@7.27.1)
+ babel-plugin-syntax-hermes-parser: 0.32.0
+ base64-js: 1.5.1
+ commander: 12.1.0
+ flow-enums-runtime: 0.0.6
+ glob: 7.2.3
+ hermes-compiler: 0.14.1
+ invariant: 2.2.4
+ jest-environment-node: 29.7.0
+ memoize-one: 5.2.1
+ metro-runtime: 0.83.7
+ metro-source-map: 0.83.7
+ nullthrows: 1.1.1
+ pretty-format: 29.7.0
+ promise: 8.3.0
+ react: 19.2.0
+ react-devtools-core: 6.1.5
+ react-refresh: 0.14.2
+ regenerator-runtime: 0.13.11
+ scheduler: 0.27.0
+ semver: 7.7.4
+ stacktrace-parser: 0.1.11
+ whatwg-fetch: 3.6.20
+ ws: 7.5.11
+ yargs: 17.7.2
+ optionalDependencies:
+ '@types/react': 19.2.14
+ transitivePeerDependencies:
+ - '@babel/core'
+ - '@react-native-community/cli'
+ - '@react-native/metro-config'
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+
react-promise-suspense@0.3.4:
dependencies:
fast-deep-equal: 2.0.1
@@ -31642,8 +36517,18 @@ snapshots:
'@types/react': 19.2.14
redux: 5.0.1
+ react-refresh@0.14.2: {}
+
react-refresh@0.17.0: {}
+ react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.0):
+ dependencies:
+ react: 19.2.0
+ react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.0)
+ tslib: 2.8.1
+ optionalDependencies:
+ '@types/react': 19.2.14
+
react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.4):
dependencies:
react: 19.2.4
@@ -31663,6 +36548,17 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.14
+ react-remove-scroll@2.6.3(@types/react@19.2.14)(react@19.2.0):
+ dependencies:
+ react: 19.2.0
+ react-remove-scroll-bar: 2.3.8(@types/react@19.2.14)(react@19.2.0)
+ react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.0)
+ tslib: 2.8.1
+ use-callback-ref: 1.3.3(@types/react@19.2.14)(react@19.2.0)
+ use-sidecar: 1.1.3(@types/react@19.2.14)(react@19.2.0)
+ optionalDependencies:
+ '@types/react': 19.2.14
+
react-remove-scroll@2.6.3(@types/react@19.2.14)(react@19.2.4):
dependencies:
react: 19.2.4
@@ -31712,6 +36608,14 @@ snapshots:
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
+ react-style-singleton@2.2.3(@types/react@19.2.14)(react@19.2.0):
+ dependencies:
+ get-nonce: 1.0.1
+ react: 19.2.0
+ tslib: 2.8.1
+ optionalDependencies:
+ '@types/react': 19.2.14
+
react-style-singleton@2.2.3(@types/react@19.2.14)(react@19.2.4):
dependencies:
get-nonce: 1.0.1
@@ -31720,6 +36624,12 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.14
+ react-test-renderer@19.2.0(react@19.2.0):
+ dependencies:
+ react: 19.2.0
+ react-is: 19.2.6
+ scheduler: 0.27.0
+
react-tooltip@5.28.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
'@floating-ui/dom': 1.7.0
@@ -31729,6 +36639,8 @@ snapshots:
react@19.1.1: {}
+ react@19.2.0: {}
+
react@19.2.4: {}
read-cache@1.0.0:
@@ -31800,7 +36712,7 @@ snapshots:
tiny-invariant: 1.3.3
tslib: 2.8.1
- recharts@3.4.1(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react-is@17.0.2)(react@19.2.4)(redux@5.0.1):
+ recharts@3.4.1(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react-is@19.2.6)(react@19.2.4)(redux@5.0.1):
dependencies:
'@reduxjs/toolkit': 2.10.1(react-redux@9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1))(react@19.2.4)
clsx: 2.1.1
@@ -31810,7 +36722,7 @@ snapshots:
immer: 10.2.0
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
- react-is: 17.0.2
+ react-is: 19.2.6
react-redux: 9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1)
reselect: 5.1.1
tiny-invariant: 1.3.3
@@ -31880,6 +36792,14 @@ snapshots:
get-proto: 1.0.1
which-builtin-type: 1.2.1
+ regenerate-unicode-properties@10.2.2:
+ dependencies:
+ regenerate: 1.4.2
+
+ regenerate@1.4.2: {}
+
+ regenerator-runtime@0.13.11: {}
+
regex-recursion@5.1.1:
dependencies:
regex: 5.1.1
@@ -31910,6 +36830,21 @@ snapshots:
regexpp@3.2.0: {}
+ regexpu-core@6.4.0:
+ dependencies:
+ regenerate: 1.4.2
+ regenerate-unicode-properties: 10.2.2
+ regjsgen: 0.8.0
+ regjsparser: 0.13.1
+ unicode-match-property-ecmascript: 2.0.0
+ unicode-match-property-value-ecmascript: 2.2.1
+
+ regjsgen@0.8.0: {}
+
+ regjsparser@0.13.1:
+ dependencies:
+ jsesc: 3.1.0
+
rehype-parse@9.0.1:
dependencies:
'@types/hast': 3.0.4
@@ -32021,12 +36956,21 @@ snapshots:
resolve-pkg-maps@1.0.0: {}
+ resolve-workspace-root@2.0.1: {}
+
resolve@1.22.10:
dependencies:
is-core-module: 2.16.1
path-parse: 1.0.7
supports-preserve-symlinks-flag: 1.0.0
+ resolve@1.22.12:
+ dependencies:
+ es-errors: 1.3.0
+ is-core-module: 2.16.1
+ path-parse: 1.0.7
+ supports-preserve-symlinks-flag: 1.0.0
+
resolve@2.0.0-next.5:
dependencies:
is-core-module: 2.16.1
@@ -32041,6 +36985,11 @@ snapshots:
dependencies:
lowercase-keys: 3.0.0
+ restore-cursor@2.0.0:
+ dependencies:
+ onetime: 2.0.1
+ signal-exit: 3.0.7
+
restore-cursor@5.1.0:
dependencies:
onetime: 7.0.0
@@ -32054,7 +37003,7 @@ snapshots:
dependencies:
glob: 7.2.3
- rolldown-plugin-dts@0.16.11(rolldown@1.0.1)(typescript@5.8.3):
+ rolldown-plugin-dts@0.16.11(rolldown@1.0.3)(typescript@5.8.3):
dependencies:
'@babel/generator': 7.28.3
'@babel/parser': 7.28.4
@@ -32064,14 +37013,32 @@ snapshots:
debug: 4.4.3(supports-color@8.1.1)
dts-resolver: 2.1.2
get-tsconfig: 4.11.0
- magic-string: 0.30.19
- rolldown: 1.0.1
+ magic-string: 0.30.21
+ rolldown: 1.0.3
optionalDependencies:
typescript: 5.8.3
transitivePeerDependencies:
- oxc-resolver
- supports-color
+ rolldown-plugin-dts@0.16.11(rolldown@1.0.3)(typescript@5.9.3):
+ dependencies:
+ '@babel/generator': 7.28.3
+ '@babel/parser': 7.28.4
+ '@babel/types': 7.28.4
+ ast-kit: 2.1.3
+ birpc: 2.6.1
+ debug: 4.4.3(supports-color@8.1.1)
+ dts-resolver: 2.1.2
+ get-tsconfig: 4.11.0
+ magic-string: 0.30.21
+ rolldown: 1.0.3
+ optionalDependencies:
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - oxc-resolver
+ - supports-color
+
rolldown@1.0.0-beta.42:
dependencies:
'@oxc-project/types': 0.94.0
@@ -32093,26 +37060,26 @@ snapshots:
'@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.42
'@rolldown/binding-win32-x64-msvc': 1.0.0-beta.42
- rolldown@1.0.1:
+ rolldown@1.0.3:
dependencies:
- '@oxc-project/types': 0.130.0
+ '@oxc-project/types': 0.133.0
'@rolldown/pluginutils': 1.0.0
optionalDependencies:
- '@rolldown/binding-android-arm64': 1.0.1
- '@rolldown/binding-darwin-arm64': 1.0.1
- '@rolldown/binding-darwin-x64': 1.0.1
- '@rolldown/binding-freebsd-x64': 1.0.1
- '@rolldown/binding-linux-arm-gnueabihf': 1.0.1
- '@rolldown/binding-linux-arm64-gnu': 1.0.1
- '@rolldown/binding-linux-arm64-musl': 1.0.1
- '@rolldown/binding-linux-ppc64-gnu': 1.0.1
- '@rolldown/binding-linux-s390x-gnu': 1.0.1
- '@rolldown/binding-linux-x64-gnu': 1.0.1
- '@rolldown/binding-linux-x64-musl': 1.0.1
- '@rolldown/binding-openharmony-arm64': 1.0.1
- '@rolldown/binding-wasm32-wasi': 1.0.1
- '@rolldown/binding-win32-arm64-msvc': 1.0.1
- '@rolldown/binding-win32-x64-msvc': 1.0.1
+ '@rolldown/binding-android-arm64': 1.0.3
+ '@rolldown/binding-darwin-arm64': 1.0.3
+ '@rolldown/binding-darwin-x64': 1.0.3
+ '@rolldown/binding-freebsd-x64': 1.0.3
+ '@rolldown/binding-linux-arm-gnueabihf': 1.0.3
+ '@rolldown/binding-linux-arm64-gnu': 1.0.3
+ '@rolldown/binding-linux-arm64-musl': 1.0.3
+ '@rolldown/binding-linux-ppc64-gnu': 1.0.3
+ '@rolldown/binding-linux-s390x-gnu': 1.0.3
+ '@rolldown/binding-linux-x64-gnu': 1.0.3
+ '@rolldown/binding-linux-x64-musl': 1.0.3
+ '@rolldown/binding-openharmony-arm64': 1.0.3
+ '@rolldown/binding-wasm32-wasi': 1.0.3
+ '@rolldown/binding-win32-arm64-msvc': 1.0.3
+ '@rolldown/binding-win32-x64-msvc': 1.0.3
rollup-plugin-inject@3.0.2:
dependencies:
@@ -32124,14 +37091,14 @@ snapshots:
dependencies:
rollup-plugin-inject: 3.0.2
- rollup-plugin-visualizer@5.14.0(rolldown@1.0.1)(rollup@4.40.2):
+ rollup-plugin-visualizer@5.14.0(rolldown@1.0.3)(rollup@4.40.2):
dependencies:
open: 8.4.2
picomatch: 4.0.3
source-map: 0.7.4
yargs: 17.7.2
optionalDependencies:
- rolldown: 1.0.1
+ rolldown: 1.0.3
rollup: 4.40.2
rollup-pluginutils@2.8.2:
@@ -32253,6 +37220,8 @@ snapshots:
semver@6.3.1: {}
+ semver@7.6.3: {}
+
semver@7.7.1: {}
semver@7.7.2: {}
@@ -32297,6 +37266,8 @@ snapshots:
seq-queue@0.0.5: {}
+ serialize-error@2.1.0: {}
+
serialize-javascript@6.0.2:
dependencies:
randombytes: 2.1.0
@@ -32359,6 +37330,10 @@ snapshots:
setprototypeof@1.2.0: {}
+ sf-symbols-typescript@2.2.0: {}
+
+ shallowequal@1.1.0: {}
+
sharp@0.33.5:
dependencies:
color: 4.2.3
@@ -32495,6 +37470,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ simple-plist@1.3.1:
+ dependencies:
+ bplist-creator: 0.1.0
+ bplist-parser: 0.3.1
+ plist: 3.1.1
+
simple-swizzle@0.2.2:
dependencies:
is-arrayish: 0.3.2
@@ -32517,6 +37498,8 @@ snapshots:
astral-regex: 2.0.0
is-fullwidth-code-point: 3.0.0
+ slugify@1.6.9: {}
+
smart-buffer@4.2.0: {}
smob@1.5.0: {}
@@ -32648,6 +37631,8 @@ snapshots:
buffer-from: 1.1.2
source-map: 0.6.1
+ source-map@0.5.7: {}
+
source-map@0.6.1: {}
source-map@0.7.4: {}
@@ -32674,6 +37659,8 @@ snapshots:
spdx-license-ids@3.0.21: {}
+ split-on-first@1.1.0: {}
+
sprintf-js@1.0.3: {}
sprintf-js@1.1.3: {}
@@ -32731,10 +37718,18 @@ snapshots:
stack-trace@0.0.10: {}
+ stack-utils@2.0.6:
+ dependencies:
+ escape-string-regexp: 2.0.0
+
stackback@0.0.2: {}
stackframe@1.3.4: {}
+ stacktrace-parser@0.1.11:
+ dependencies:
+ type-fest: 0.7.1
+
stacktracey@2.1.8:
dependencies:
as-table: 1.0.55
@@ -32742,6 +37737,8 @@ snapshots:
standard-as-callback@2.1.0: {}
+ statuses@1.5.0: {}
+
statuses@2.0.1: {}
statuses@2.0.2: {}
@@ -32759,7 +37756,7 @@ snapshots:
storybook-solidjs-vite@1.0.0-beta.7(@storybook/test@8.6.12(storybook@8.6.12(prettier@3.7.4)))(esbuild@0.25.5)(rollup@4.40.2)(solid-js@1.9.6)(storybook@8.6.12(prettier@3.7.4))(vite-plugin-solid@2.11.6(@testing-library/jest-dom@6.5.0)(solid-js@1.9.6)(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)))(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1))(webpack@5.101.3(esbuild@0.25.5)):
dependencies:
- '@storybook/builder-vite': 10.5.0-alpha.0(esbuild@0.25.5)(rollup@4.40.2)(storybook@8.6.12(prettier@3.7.4))(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1))(webpack@5.101.3(esbuild@0.25.5))
+ '@storybook/builder-vite': 10.5.0-alpha.3(esbuild@0.25.5)(rollup@4.40.2)(storybook@8.6.12(prettier@3.7.4))(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1))(webpack@5.101.3(esbuild@0.25.5))
'@storybook/types': 9.0.0-alpha.1(storybook@8.6.12(prettier@3.7.4))
magic-string: 0.30.17
solid-js: 1.9.6
@@ -32797,6 +37794,8 @@ snapshots:
- supports-color
- utf-8-validate
+ stream-buffers@2.2.0: {}
+
streamx@2.22.0:
dependencies:
fast-fifo: 1.3.2
@@ -32804,6 +37803,8 @@ snapshots:
optionalDependencies:
bare-events: 2.5.4
+ strict-uri-encode@2.0.0: {}
+
string-width@4.2.3:
dependencies:
emoji-regex: 8.0.0
@@ -32885,6 +37886,10 @@ snapshots:
character-entities-html4: 2.1.0
character-entities-legacy: 3.0.0
+ strip-ansi@5.2.0:
+ dependencies:
+ ansi-regex: 4.1.1
+
strip-ansi@6.0.1:
dependencies:
ansi-regex: 5.0.1
@@ -32933,6 +37938,8 @@ snapshots:
dependencies:
'@tokenizer/token': 0.3.0
+ structured-headers@0.4.1: {}
+
style-to-js@1.1.16:
dependencies:
style-to-object: 1.0.8
@@ -32955,6 +37962,8 @@ snapshots:
client-only: 0.0.1
react: 19.2.4
+ styleq@0.1.3: {}
+
subtitles-parser-vtt@0.1.0: {}
sucrase@3.35.0:
@@ -32983,6 +37992,11 @@ snapshots:
dependencies:
has-flag: 4.0.0
+ supports-hyperlinks@2.3.0:
+ dependencies:
+ has-flag: 4.0.0
+ supports-color: 7.2.0
+
supports-hyperlinks@4.4.0:
dependencies:
has-flag: 5.0.1
@@ -33123,6 +38137,11 @@ snapshots:
dependencies:
'@tauri-apps/api': 1.6.0
+ terminal-link@2.1.1:
+ dependencies:
+ ansi-escapes: 4.3.2
+ supports-hyperlinks: 2.3.0
+
terminal-link@5.0.0:
dependencies:
ansi-escapes: 7.2.0
@@ -33158,7 +38177,12 @@ snapshots:
acorn: 8.16.0
commander: 2.20.3
source-map-support: 0.5.21
- optional: true
+
+ test-exclude@6.0.0:
+ dependencies:
+ '@istanbuljs/schema': 0.1.3
+ glob: 7.2.3
+ minimatch: 3.1.2
test-exclude@7.0.1:
dependencies:
@@ -33182,6 +38206,8 @@ snapshots:
dependencies:
any-promise: 1.3.0
+ throat@5.0.0: {}
+
through@2.3.8: {}
thunky@1.1.0: {}
@@ -33236,6 +38262,8 @@ snapshots:
tmp@0.2.5: {}
+ tmpl@1.0.5: {}
+
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0
@@ -33252,6 +38280,8 @@ snapshots:
toml@3.0.0: {}
+ toqr@0.1.1: {}
+
totalist@3.0.1: {}
tough-cookie@5.1.2:
@@ -33330,7 +38360,7 @@ snapshots:
'@swc/wasm': 1.15.5
optional: true
- ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.8.3):
+ ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.9.3):
dependencies:
'@cspotcode/source-map-support': 0.8.1
'@tsconfig/node10': 1.0.11
@@ -33344,7 +38374,7 @@ snapshots:
create-require: 1.1.1
diff: 4.0.2
make-error: 1.3.6
- typescript: 5.8.3
+ typescript: 5.9.3
v8-compile-cache-lib: 3.0.1
yn: 3.1.1
optionalDependencies:
@@ -33356,6 +38386,10 @@ snapshots:
optionalDependencies:
typescript: 5.8.3
+ tsconfck@3.1.5(typescript@5.9.3):
+ optionalDependencies:
+ typescript: 5.9.3
+
tsconfig-paths@3.15.0:
dependencies:
'@types/json5': 0.0.29
@@ -33378,8 +38412,8 @@ snapshots:
diff: 8.0.2
empathic: 2.0.0
hookable: 5.5.3
- rolldown: 1.0.1
- rolldown-plugin-dts: 0.16.11(rolldown@1.0.1)(typescript@5.8.3)
+ rolldown: 1.0.3
+ rolldown-plugin-dts: 0.16.11(rolldown@1.0.3)(typescript@5.8.3)
semver: 7.7.2
tinyexec: 1.0.1
tinyglobby: 0.2.15
@@ -33394,6 +38428,31 @@ snapshots:
- supports-color
- vue-tsc
+ tsdown@0.15.6(typescript@5.9.3):
+ dependencies:
+ ansis: 4.2.0
+ cac: 6.7.14
+ chokidar: 4.0.3
+ debug: 4.4.3(supports-color@8.1.1)
+ diff: 8.0.2
+ empathic: 2.0.0
+ hookable: 5.5.3
+ rolldown: 1.0.3
+ rolldown-plugin-dts: 0.16.11(rolldown@1.0.3)(typescript@5.9.3)
+ semver: 7.7.2
+ tinyexec: 1.0.1
+ tinyglobby: 0.2.15
+ tree-kill: 1.2.2
+ unconfig: 7.3.3
+ optionalDependencies:
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - '@ts-macro/tsc'
+ - '@typescript/native-preview'
+ - oxc-resolver
+ - supports-color
+ - vue-tsc
+
tslib@1.14.1: {}
tslib@2.6.2: {}
@@ -33429,10 +38488,39 @@ snapshots:
- tsx
- yaml
- tsutils@3.21.0(typescript@5.8.3):
+ tsup@8.5.0(@swc/core@1.15.5(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.14)(typescript@5.9.3)(yaml@2.8.1):
+ dependencies:
+ bundle-require: 5.1.0(esbuild@0.25.12)
+ cac: 6.7.14
+ chokidar: 4.0.3
+ consola: 3.4.2
+ debug: 4.4.3(supports-color@8.1.1)
+ esbuild: 0.25.12
+ fix-dts-default-cjs-exports: 1.0.1
+ joycon: 3.1.1
+ picocolors: 1.1.1
+ postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.14)(yaml@2.8.1)
+ resolve-from: 5.0.0
+ rollup: 4.40.2
+ source-map: 0.8.0-beta.0
+ sucrase: 3.35.0
+ tinyexec: 0.3.2
+ tinyglobby: 0.2.15
+ tree-kill: 1.2.2
+ optionalDependencies:
+ '@swc/core': 1.15.5(@swc/helpers@0.5.17)
+ postcss: 8.5.14
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - jiti
+ - supports-color
+ - tsx
+ - yaml
+
+ tsutils@3.21.0(typescript@5.9.3):
dependencies:
tslib: 1.14.1
- typescript: 5.8.3
+ typescript: 5.9.3
tsyringe@4.10.0:
dependencies:
@@ -33483,10 +38571,14 @@ snapshots:
dependencies:
prelude-ls: 1.2.1
+ type-detect@4.0.8: {}
+
type-fest@0.20.2: {}
type-fest@0.21.3: {}
+ type-fest@0.7.1: {}
+
type-fest@4.41.0: {}
type-is@1.6.18:
@@ -33548,6 +38640,8 @@ snapshots:
typescript@5.8.3: {}
+ typescript@5.9.3: {}
+
ua-parser-js@1.0.41: {}
ufo@1.6.1: {}
@@ -33651,6 +38745,17 @@ snapshots:
pathe: 2.0.3
ufo: 1.6.1
+ unicode-canonical-property-names-ecmascript@2.0.1: {}
+
+ unicode-match-property-ecmascript@2.0.0:
+ dependencies:
+ unicode-canonical-property-names-ecmascript: 2.0.1
+ unicode-property-aliases-ecmascript: 2.2.0
+
+ unicode-match-property-value-ecmascript@2.2.1: {}
+
+ unicode-property-aliases-ecmascript@2.2.0: {}
+
unicorn-magic@0.1.0: {}
unicorn-magic@0.3.0: {}
@@ -33690,7 +38795,7 @@ snapshots:
escape-string-regexp: 5.0.0
estree-walker: 3.0.3
local-pkg: 1.1.1
- magic-string: 0.30.19
+ magic-string: 0.30.21
mlly: 1.8.0
pathe: 2.0.3
picomatch: 4.0.3
@@ -33897,7 +39002,7 @@ snapshots:
unwasm@0.3.9:
dependencies:
knitwork: 1.2.0
- magic-string: 0.30.19
+ magic-string: 0.30.21
mlly: 1.8.0
pathe: 1.1.2
pkg-types: 1.3.1
@@ -33925,6 +39030,12 @@ snapshots:
escalade: 3.2.0
picocolors: 1.1.1
+ update-browserslist-db@1.2.3(browserslist@4.28.2):
+ dependencies:
+ browserslist: 4.28.2
+ escalade: 3.2.0
+ picocolors: 1.1.1
+
uqr@0.1.2: {}
uri-js@4.4.1:
@@ -33942,6 +39053,13 @@ snapshots:
urlpattern-polyfill@8.0.2: {}
+ use-callback-ref@1.3.3(@types/react@19.2.14)(react@19.2.0):
+ dependencies:
+ react: 19.2.0
+ tslib: 2.8.1
+ optionalDependencies:
+ '@types/react': 19.2.14
+
use-callback-ref@1.3.3(@types/react@19.2.14)(react@19.2.4):
dependencies:
react: 19.2.4
@@ -33954,12 +39072,24 @@ snapshots:
dequal: 2.0.3
react: 19.2.4
+ use-latest-callback@0.2.6(react@19.2.0):
+ dependencies:
+ react: 19.2.0
+
use-resize-observer@9.1.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
'@juggle/resize-observer': 3.4.0
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
+ use-sidecar@1.1.3(@types/react@19.2.14)(react@19.2.0):
+ dependencies:
+ detect-node-es: 1.1.0
+ react: 19.2.0
+ tslib: 2.8.1
+ optionalDependencies:
+ '@types/react': 19.2.14
+
use-sidecar@1.1.3(@types/react@19.2.14)(react@19.2.4):
dependencies:
detect-node-es: 1.1.0
@@ -33968,6 +39098,10 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.14
+ use-sync-external-store@1.5.0(react@19.2.0):
+ dependencies:
+ react: 19.2.0
+
use-sync-external-store@1.5.0(react@19.2.4):
dependencies:
react: 19.2.4
@@ -33988,6 +39122,8 @@ snapshots:
uuid@11.1.0: {}
+ uuid@7.0.3: {}
+
uuid@8.0.0: {}
uuid@8.3.2: {}
@@ -34003,6 +39139,11 @@ snapshots:
optionalDependencies:
typescript: 5.8.3
+ valibot@1.0.0-rc.1(typescript@5.9.3):
+ optionalDependencies:
+ typescript: 5.9.3
+ optional: true
+
validate-html-nesting@1.2.2: {}
validate-npm-package-license@3.0.4:
@@ -34014,6 +39155,15 @@ snapshots:
vary@1.1.2: {}
+ vaul@1.1.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
+ dependencies:
+ '@radix-ui/react-dialog': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ react: 19.2.0
+ react-dom: 19.2.0(react@19.2.0)
+ transitivePeerDependencies:
+ - '@types/react'
+ - '@types/react-dom'
+
vfile-location@5.0.3:
dependencies:
'@types/unist': 3.0.3
@@ -34051,7 +39201,7 @@ snapshots:
d3-time: 3.1.0
d3-timer: 3.0.1
- vinxi@0.5.6(@planetscale/database@1.19.0)(@types/node@22.15.17)(db0@0.3.2(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(mysql2@3.15.2))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(ioredis@5.6.1)(jiti@2.6.1)(lightningcss@1.32.0)(mysql2@3.15.2)(rolldown@1.0.1)(terser@5.44.0)(xml2js@0.6.2)(yaml@2.8.1):
+ vinxi@0.5.6(@planetscale/database@1.19.0)(@types/node@22.15.17)(db0@0.3.2(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(mysql2@3.15.2))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(ioredis@5.6.1)(jiti@2.6.1)(lightningcss@1.32.0)(mysql2@3.15.2)(rolldown@1.0.3)(terser@5.44.0)(xml2js@0.6.2)(yaml@2.8.1):
dependencies:
'@babel/core': 7.27.1
'@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.27.1)
@@ -34073,7 +39223,7 @@ snapshots:
hookable: 5.5.3
http-proxy: 1.18.1
micromatch: 4.0.8
- nitropack: 2.11.11(@planetscale/database@1.19.0)(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(mysql2@3.15.2)(rolldown@1.0.1)(xml2js@0.6.2)
+ nitropack: 2.11.11(@planetscale/database@1.19.0)(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(mysql2@3.15.2)(rolldown@1.0.3)(xml2js@0.6.2)
node-fetch-native: 1.6.6
path-to-regexp: 6.3.0
pathe: 1.1.2
@@ -34168,6 +39318,27 @@ snapshots:
- tsx
- yaml
+ vite-node@3.2.4(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1):
+ dependencies:
+ cac: 6.7.14
+ debug: 4.4.3(supports-color@8.1.1)
+ es-module-lexer: 1.7.0
+ pathe: 2.0.3
+ vite: 6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)
+ transitivePeerDependencies:
+ - '@types/node'
+ - jiti
+ - less
+ - lightningcss
+ - sass
+ - sass-embedded
+ - stylus
+ - sugarss
+ - supports-color
+ - terser
+ - tsx
+ - yaml
+
vite-plugin-solid@2.11.6(@testing-library/jest-dom@6.5.0)(solid-js@1.9.6)(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)):
dependencies:
'@babel/core': 7.27.1
@@ -34209,6 +39380,17 @@ snapshots:
- supports-color
- typescript
+ vite-tsconfig-paths@4.3.2(typescript@5.9.3)(vite@6.3.5(@types/node@20.17.43)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)):
+ dependencies:
+ debug: 4.4.0
+ globrex: 0.1.2
+ tsconfck: 3.1.5(typescript@5.9.3)
+ optionalDependencies:
+ vite: 6.3.5(@types/node@20.17.43)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)
+ transitivePeerDependencies:
+ - supports-color
+ - typescript
+
vite-tsconfig-paths@5.1.4(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)):
dependencies:
debug: 4.4.0
@@ -34329,7 +39511,7 @@ snapshots:
chai: 5.2.0
debug: 4.4.3(supports-color@8.1.1)
expect-type: 1.2.1
- magic-string: 0.30.19
+ magic-string: 0.30.21
pathe: 2.0.3
picomatch: 4.0.3
std-env: 3.9.0
@@ -34360,12 +39542,64 @@ snapshots:
- tsx
- yaml
+ vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.15.17)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1):
+ dependencies:
+ '@types/chai': 5.2.3
+ '@vitest/expect': 3.2.4
+ '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1))
+ '@vitest/pretty-format': 3.2.4
+ '@vitest/runner': 3.2.4
+ '@vitest/snapshot': 3.2.4
+ '@vitest/spy': 3.2.4
+ '@vitest/utils': 3.2.4
+ chai: 5.2.0
+ debug: 4.4.3(supports-color@8.1.1)
+ expect-type: 1.2.1
+ magic-string: 0.30.21
+ pathe: 2.0.3
+ picomatch: 4.0.3
+ std-env: 3.9.0
+ tinybench: 2.9.0
+ tinyexec: 0.3.2
+ tinyglobby: 0.2.15
+ tinypool: 1.1.1
+ tinyrainbow: 2.0.0
+ vite: 6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)
+ vite-node: 3.2.4(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)
+ why-is-node-running: 2.3.0
+ optionalDependencies:
+ '@types/debug': 4.1.12
+ '@types/node': 22.15.17
+ '@vitest/ui': 3.2.4(vitest@3.2.4)
+ jsdom: 26.1.0
+ transitivePeerDependencies:
+ - jiti
+ - less
+ - lightningcss
+ - msw
+ - sass
+ - sass-embedded
+ - stylus
+ - sugarss
+ - supports-color
+ - terser
+ - tsx
+ - yaml
+
+ vlq@1.0.1: {}
+
w3c-xmlserializer@5.0.0:
dependencies:
xml-name-validator: 5.0.0
walk-up-path@3.0.1: {}
+ walker@1.0.8:
+ dependencies:
+ makeerror: 1.0.12
+
+ warn-once@0.1.1: {}
+
watchpack@2.5.1:
dependencies:
glob-to-regexp: 0.4.1
@@ -34374,7 +39608,6 @@ snapshots:
wcwidth@1.0.1:
dependencies:
defaults: 1.0.4
- optional: true
web-namespaces@2.0.1: {}
@@ -34511,8 +39744,12 @@ snapshots:
dependencies:
iconv-lite: 0.6.3
+ whatwg-fetch@3.6.20: {}
+
whatwg-mimetype@4.0.0: {}
+ whatwg-url-minimum@0.1.2: {}
+
whatwg-url@14.2.0:
dependencies:
tr46: 5.1.1
@@ -34635,14 +39872,14 @@ snapshots:
'@cloudflare/workerd-linux-arm64': 1.20250408.0
'@cloudflare/workerd-windows-64': 1.20250408.0
- workflow@4.2.0-beta.73(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(@opentelemetry/api@1.9.0)(@swc/cli@0.8.0(@swc/core@1.15.3(@swc/helpers@0.5.17))(chokidar@5.0.0))(@swc/core@1.15.3(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(magicast@0.3.5)(next@16.2.1(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.8.3):
+ workflow@4.2.0-beta.73(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(@opentelemetry/api@1.9.0)(@swc/cli@0.8.0(@swc/core@1.15.3(@swc/helpers@0.5.17))(chokidar@5.0.0))(@swc/core@1.15.3(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(magicast@0.3.5)(next@16.2.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.8.3):
dependencies:
'@workflow/astro': 4.0.0-beta.47(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.17)
'@workflow/cli': 4.2.0-beta.73(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.17)
'@workflow/core': 4.2.0-beta.73(@opentelemetry/api@1.9.0)
'@workflow/errors': 4.1.0-beta.19
'@workflow/nest': 0.0.0-beta.22(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(@opentelemetry/api@1.9.0)(@swc/cli@0.8.0(@swc/core@1.15.3(@swc/helpers@0.5.17))(chokidar@5.0.0))(@swc/core@1.15.3(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)
- '@workflow/next': 4.0.1-beta.69(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.17)(next@16.2.1(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))
+ '@workflow/next': 4.0.1-beta.69(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.17)(next@16.2.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))
'@workflow/nitro': 4.0.1-beta.68(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.17)
'@workflow/nuxt': 4.0.1-beta.57(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.17)(magicast@0.3.5)
'@workflow/rollup': 4.0.0-beta.30(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.17)
@@ -34723,6 +39960,11 @@ snapshots:
wrappy@1.0.2: {}
+ write-file-atomic@4.0.2:
+ dependencies:
+ imurmurhash: 0.1.4
+ signal-exit: 3.0.7
+
write-file-atomic@5.0.1:
dependencies:
imurmurhash: 0.1.4
@@ -34733,6 +39975,8 @@ snapshots:
imurmurhash: 0.1.4
signal-exit: 4.1.0
+ ws@7.5.11: {}
+
ws@8.17.1: {}
ws@8.18.0: {}
@@ -34745,6 +39989,11 @@ snapshots:
dependencies:
is-wsl: 3.1.0
+ xcode@3.0.1:
+ dependencies:
+ simple-plist: 1.3.1
+ uuid: 7.0.3
+
xdg-app-paths@5.1.0:
dependencies:
xdg-portable: 7.3.0
@@ -34755,6 +40004,11 @@ snapshots:
xml-name-validator@5.0.0: {}
+ xml2js@0.6.0:
+ dependencies:
+ sax: 1.2.1
+ xmlbuilder: 11.0.1
+
xml2js@0.6.2:
dependencies:
sax: 1.2.1
@@ -34762,6 +40016,8 @@ snapshots:
xmlbuilder@11.0.1: {}
+ xmlbuilder@15.1.1: {}
+
xmlchars@2.2.0: {}
y18n@5.0.8: {}