Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion apps/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@
"expo-location": "~57.0.12",
"expo-notifications": "~57.0.13",
"expo-router": "~57.0.15",
"expo-screen-capture": "57.0.2",
"expo-screen-corner-radius": "1.1.0",
"expo-secure-store": "~57.0.1",
"expo-share-intent": "8.0.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import { Stack } from 'expo-router';
import { appUnlockScreenLayout } from '@/components/app-unlock-screen';

import { privacyScreenLayout } from '@/components/privacy-cover-overlay';
import { useFormSheetDetents } from '@/lib/form-sheet';

const screenLayout: typeof appUnlockScreenLayout = props =>
appUnlockScreenLayout({ ...props, children: privacyScreenLayout(props) });

export default function OrganizationLayout() {
const { fullSheetDetent } = useFormSheetDetents();

Expand All @@ -18,7 +14,7 @@ export default function OrganizationLayout() {
};

return (
<Stack screenLayout={screenLayout} screenOptions={{ headerShown: false }}>
<Stack screenLayout={appUnlockScreenLayout} screenOptions={{ headerShown: false }}>
<Stack.Screen name="invite-member" options={sheetOptions} />
<Stack.Screen name="member-limit" options={sheetOptions} />
<Stack.Screen name="low-balance-alert" options={sheetOptions} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,9 @@ import { appUnlockScreenLayout } from '@/components/app-unlock-screen';

import { InvalidRouteState } from '@/components/invalid-route-state';
import { SecurityAgentCommandObserver } from '@/components/security-agent/security-agent-command-observer';
import { privacyScreenLayout } from '@/components/privacy-cover-overlay';
import { useFormSheetDetents } from '@/lib/form-sheet';
import { parseParam } from '@/lib/route-params';

const screenLayout: typeof appUnlockScreenLayout = props =>
appUnlockScreenLayout({ ...props, children: privacyScreenLayout(props) });

// Mounts exactly one command observer per scope alongside a headerless Stack,
// so it stays mounted across Dashboard/Findings/Settings navigation without
// ever running twice for the same scope. Also the single validation point
Expand All @@ -31,7 +27,7 @@ export default function SecurityAgentScopeLayout() {
{/* Anchored entry needs a valid scope root, not a parameterless dismissal route. */}
<Stack
initialRouteName="index"
screenLayout={screenLayout}
screenLayout={appUnlockScreenLayout}
screenOptions={{ headerShown: false }}
>
<Stack.Screen
Expand Down
4 changes: 2 additions & 2 deletions apps/mobile/src/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ import { AppRootProviders } from '@/components/app-root-providers';
import { BootstrapErrorScreen } from '@/components/bootstrap-error-screen';
import { StateSurface } from '@/components/centered-state-surface';
import { LanguageReloadErrorScreen } from '@/components/language-reload-error-screen';
import { PrivacyCoverOverlay } from '@/components/privacy-cover-overlay';
import { QueryError } from '@/components/query-error';
import { splashContentScale } from '@/components/splash-reveal';
import { announceForA11y, moveA11yFocus } from '@/lib/a11y/announce';
Expand All @@ -67,6 +66,7 @@ import { useForceUpdate } from '@/lib/hooks/use-force-update';
import { useCurrentUserId } from '@/lib/hooks/use-current-user-id';
import { useRestoreErrorHold } from '@/lib/hooks/use-restore-error-hold';
import { useScreenTracking } from '@/lib/hooks/use-screen-tracking';
import { preloadHideBalancePreference } from '@/lib/hooks/use-hide-balance-preference';
import { useNavigationTheme } from '@/lib/hooks/use-theme-colors';
import {
applyThemePreference,
Expand Down Expand Up @@ -163,6 +163,7 @@ checkInitialNotification();
captureLaunchDeepLink();
prefetchCurrentUser();
preloadThemePreference();
preloadHideBalancePreference();
preloadLanguagePreference();
preloadStartupFonts();

Expand Down Expand Up @@ -980,7 +981,6 @@ function RootLayoutNav({
pointerEvents={hidden || showRestoreError ? 'none' : 'auto'}
>
<Slot />
<PrivacyCoverOverlay segments={segments} />
</View>
{showRestoreError ? (
<View className="absolute inset-0">
Expand Down
11 changes: 0 additions & 11 deletions apps/mobile/src/components/agents/markdown-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ import { moveA11yFocus } from '@/lib/a11y/announce';
import { i18n } from '@/i18n';
import { formatNumber } from '@/lib/format';
import { useThemeColors } from '@/lib/hooks/use-theme-colors';
import { subscribePrivacyCover } from '@/lib/privacy-cover-events';
import { AccessibleStatus } from '@/components/ui/accessible-status';
import { CenteredState } from '@/components/centered-state';
import { StateSurface } from '@/components/centered-state-surface';
Expand Down Expand Up @@ -155,16 +154,6 @@ export function MarkdownTable({
}
}, [open, savedScale, scale, session]);

// Close when the privacy cover fires (app backgrounds on a covered route):
// a native Modal renders above the overlay, so it must close itself.
useEffect(
() =>
subscribePrivacyCover(() => {
setOpen(false);
}),
[]
);

// Natural (unscaled) table size — a transform does not change layout, so this
// measurement stays valid at every zoom level. Epsilon guard stops a re-render loop.
const handleTableLayout = useCallback((event: LayoutChangeEvent) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { Button } from '@/components/ui/button';
import { Text } from '@/components/ui/text';
import { i18n } from '@/i18n';
import { type AgentSessionFilters } from '@/lib/agent-session-filters';
import { emitPrivacyCover } from '@/lib/privacy-cover-events';
import { renderWithProviders } from '@/test/render-with-providers';
import { SessionFilterModal } from './platform-filter-modal';

Expand Down Expand Up @@ -224,7 +223,7 @@ describe('SessionFilterModal', () => {
expect(props.onClose).toHaveBeenCalledOnce();
});

it.each(['cancel', 'backdrop', 'native', 'privacy'] as const)(
it.each(['cancel', 'backdrop', 'native'] as const)(
'dismisses through %s without applying draft selections',
async dismissal => {
const { renderer, props } = await renderModal();
Expand All @@ -235,9 +234,7 @@ describe('SessionFilterModal', () => {
pressButton(renderer, i18n.t('common.cancel'));
} else {
act(() => {
if (dismissal === 'privacy') {
emitPrivacyCover();
} else if (dismissal === 'native') {
if (dismissal === 'native') {
(renderer.root.findByType(Modal).props.onRequestClose as () => void)();
} else {
const backdrop = renderer.root.findAllByType(Pressable)[0];
Expand Down
6 changes: 1 addition & 5 deletions apps/mobile/src/components/agents/platform-filter-modal.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Check } from '@/components/ui/icons';
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { Modal, Pressable, ScrollView, View } from 'react-native';
import { useTranslation } from 'react-i18next';

Expand All @@ -14,7 +14,6 @@ import { Text } from '@/components/ui/text';
import { type AgentSessionFilters } from '@/lib/agent-session-filters';
import { platformLabel } from '@/lib/platform-label';
import { useThemeColors } from '@/lib/hooks/use-theme-colors';
import { subscribePrivacyCover } from '@/lib/privacy-cover-events';
import { cn } from '@/lib/utils';

export { type ProjectFilterOption };
Expand Down Expand Up @@ -120,9 +119,6 @@ export function SessionFilterModal({
);
};

// Close when the privacy cover fires (app backgrounds on a covered route).
useEffect(() => subscribePrivacyCover(onClose), [onClose]);

return (
<Modal visible transparent animationType="fade" onRequestClose={onClose}>
<Pressable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,13 @@ import { createElement, type ReactElement } from 'react';
import TestRenderer, { act } from 'react-test-renderer';
import { beforeEach, describe, expect, it, vi } from 'vitest';

import { emitPrivacyCover } from '@/lib/privacy-cover-events';

import { SessionPageSheet } from './session-page-sheet';

// Mutated between tests so one suite can prove both platform branches and the
// Android top-inset padding.
const reactNativeMock = vi.hoisted(() => {
const listeners = new Set<(state: string) => void>();
return {
Platform: { OS: 'ios' as string },
AppState: {
addEventListener: (_event: string, listener: (state: string) => void) => {
listeners.add(listener);
return {
remove: () => {
listeners.delete(listener);
},
};
},
},
emitAppState: (state: string) => {
for (const listener of listeners) {
listener(state);
}
},
};
});
const reactNativeMock = vi.hoisted(() => ({
Platform: { OS: 'ios' as string },
}));
const safeAreaMock = vi.hoisted(() => ({
useSafeAreaInsets: vi.fn(() => ({ top: 0, bottom: 0 })),
}));
Expand All @@ -41,7 +21,6 @@ vi.mock('react-native', () => ({
Modal: 'Modal',
View: 'View',
Platform: reactNativeMock.Platform,
AppState: reactNativeMock.AppState,
}));
vi.mock('react-native-safe-area-context', () => ({
useSafeAreaInsets: safeAreaMock.useSafeAreaInsets,
Expand Down Expand Up @@ -193,36 +172,6 @@ describe('SessionPageSheet mounted', () => {
renderer.unmount();
});

it.each(['ios', 'android'])(
'force-closes the %s Modal on privacy cover and restores it on foreground',
async platform => {
reactNativeMock.Platform.OS = platform;
// MessageDetailsSheet passes a stacked closer that only pops its inner
// select-text view, so `visible` stays true and the native Modal would
// otherwise stay in the Recents snapshot.
const onClose = vi.fn<() => void>();
const renderer = await mountSheet({ onClose });
expect(modal(renderer.root).props.visible).toBe(true);

await act(async () => {
await Promise.resolve();
emitPrivacyCover();
});
expect(onClose).toHaveBeenCalledTimes(1);
expect(modal(renderer.root).props.visible).toBe(false);

// The forced close releases on the next foreground, so the caller is not
// left holding a sheet that can never show again.
await act(async () => {
await Promise.resolve();
reactNativeMock.emitAppState('active');
});
expect(modal(renderer.root).props.visible).toBe(true);

renderer.unmount();
}
);

it('keeps SheetHeader as the first surface child and routes Done to onClose', async () => {
reactNativeMock.Platform.OS = 'android';
const onClose = vi.fn<() => void>();
Expand Down
41 changes: 4 additions & 37 deletions apps/mobile/src/components/agents/session-page-sheet.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { type ReactNode, useEffect, useState } from 'react';
import { AppState, Modal, Platform } from 'react-native';
import { type ReactNode } from 'react';
import { Modal, Platform } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

import { StateSurface } from '@/components/centered-state-surface';
import { useThemeColors } from '@/lib/hooks/use-theme-colors';
import { subscribePrivacyCover } from '@/lib/privacy-cover-events';

type SessionPageSheetProps = {
visible: boolean;
Expand All @@ -30,43 +29,11 @@ export function SessionPageSheet({
}: Readonly<SessionPageSheetProps>) {
const insets = useSafeAreaInsets();
const colors = useThemeColors();
const [coverClosed, setCoverClosed] = useState(false);

// Close when the privacy cover fires (app backgrounds on a covered route).
// The caller `onClose` can be a stacked closer that only pops an inner view
// and leaves `visible` true, so tear the native Modal down here too instead
// of trusting the caller to do it.
useEffect(
() =>
subscribePrivacyCover(() => {
setCoverClosed(true);
onClose();
}),
[onClose]
);

// Release the forced close on the next foreground, so a caller that kept
// `visible` true is not left holding a sheet that can never show again.
useEffect(() => {
if (!coverClosed) {
return undefined;
}
const subscription = AppState.addEventListener('change', state => {
if (state === 'active') {
setCoverClosed(false);
}
});
return () => {
subscription.remove();
};
}, [coverClosed]);

const open = visible && !coverClosed;

if (Platform.OS === 'ios') {
return (
<Modal
visible={open}
visible={visible}
// RN Modal paints its container white. Android unmounts the children
// before the slide-out ends, so the container shows as a white flash.
backdropColor={colors.background}
Expand All @@ -84,7 +51,7 @@ export function SessionPageSheet({

return (
<Modal
visible={open}
visible={visible}
backdropColor={colors.background}
animationType="slide"
onRequestClose={onClose}
Expand Down
4 changes: 0 additions & 4 deletions apps/mobile/src/components/app-unlock-screen.mounted.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,6 @@ it.each([false, true])(
const boundary = root().findByType(Layout);
expect(boundary.findAllByType('Scene' as ElementType)).toHaveLength(1);
}
for (const Layout of [OrganizationLayout, SecurityAgentScopeLayout]) {
const wrapper = root().findByType(Layout).findByProps({ pointerEvents: 'none' });
expect(wrapper.findAllByType('PrivacyCover' as ElementType)).toHaveLength(1);
}
const observer = root().findByType('SecurityAgentCommandObserver' as ElementType);
const scene = root().findByType(KiloClawLayout);
const draft = scene.findByType('Draft' as ElementType);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,10 @@ vi.mock('react-native-safe-area-context', () => ({
useSafeAreaInsets: () => ({ top: 24, bottom: 12, left: 0, right: 0 }),
}));
vi.mock('react-native-reanimated', () => ({
default: { View: 'PrivacyCover' },
default: { View: 'Animated.View' },
useSharedValue: (value: number) => ({ value }),
useAnimatedStyle: (build: () => unknown) => build(),
}));
vi.mock('expo-screen-capture', () => ({}));
vi.mock('@/components/centered-state', () => ({ CenteredState: 'CenteredState' }));
vi.mock('@/components/centered-state-surface', () => ({
NativeStateSurface: ({ children }: { children: ReactElement }) => children,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ import { createElement, type ReactNode } from 'react';
import TestRenderer, { act } from 'react-test-renderer';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { emitPrivacyCover } from '@/lib/privacy-cover-events';

import { ReviewDetailScreen } from './review-detail-screen';

const detail = vi.hoisted(() => ({
Expand Down Expand Up @@ -588,20 +586,6 @@ describe('ReviewDetailScreen transcript sheet', () => {
}
);

it('closes the transcript and destroys the stream when the privacy cover activates', async () => {
const connection = { connect: vi.fn(), destroy: vi.fn() };
spectatorStream.createReviewSpectatorStream.mockResolvedValue(connection);
mountScreen(true);
await act(async () => {
await Promise.resolve();
});

act(emitPrivacyCover);

expect(modalRenders.list.at(-1)?.visible).toBe(false);
expect(connection.destroy).toHaveBeenCalledTimes(1);
});

it('destroys a pending stream connection if the sheet closes before it arrives', async () => {
const connection = { connect: vi.fn(), destroy: vi.fn() };
let resolveConnection: ((value: typeof connection) => void) | undefined = undefined;
Expand Down
5 changes: 0 additions & 5 deletions apps/mobile/src/components/image-viewer-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { AccessibleStatus } from '@/components/ui/accessible-status';
import { Image } from '@/components/ui/image';
import { Text } from '@/components/ui/text';
import { useThemeColors } from '@/lib/hooks/use-theme-colors';
import { subscribePrivacyCover } from '@/lib/privacy-cover-events';

type ImageViewerModalProps = {
visible: boolean;
Expand Down Expand Up @@ -86,10 +85,6 @@ export function ImageViewerModal({
setImageError(false);
}, [visible]);

// Close when the privacy cover fires (app backgrounds on a covered route):
// a native Modal renders above the overlay, so it must close itself.
useEffect(() => subscribePrivacyCover(onClose), [onClose]);

// eslint-disable-next-line new-cap -- RNGH's gesture builder API is Gesture.Pinch().
const pinch = Gesture.Pinch()
.onUpdate(event => {
Expand Down
Loading