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
4 changes: 2 additions & 2 deletions apps/mobile/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const config: ExpoConfig = {
slug: "splidly",
scheme: "splidly",
version: "1.0.0",
orientation: "portrait",
orientation: "default",
userInterfaceStyle: "automatic",
experiments: { typedRoutes: true },
extra: {
Expand Down Expand Up @@ -54,7 +54,7 @@ const config: ExpoConfig = {
ios: {
icon: "./assets/icons/splidly.icon",
bundleIdentifier: buildConfig.iosBundleIdentifier,
supportsTablet: false,
supportsTablet: true,
usesAppleSignIn: true,
associatedDomains: [`applinks:${host}`],
privacyManifests: {
Expand Down
53 changes: 48 additions & 5 deletions apps/mobile/src/__tests__/friend-detail.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,20 @@ let mockFriendSettlements: Array<{
};
}> = [];

jest.mock("expo-router", () => ({
router: { push: jest.fn() },
useLocalSearchParams: () => ({ id: "friendship-1" }),
Stack: { Screen: () => null },
}));
jest.mock("expo-router", () => {
const { Text } = require("react-native") as typeof import("react-native");
return {
router: { push: jest.fn() },
useLocalSearchParams: () => ({ id: "friendship-1" }),
Stack: {
Screen: ({ options }: { options?: { title?: string } }) => (
<Text selectable={false} testID="friend-navigation-title">
{options?.title ?? ""}
</Text>
),
},
};
});

jest.mock("../lib/trpc", () => ({
api: {
Expand Down Expand Up @@ -89,6 +98,40 @@ describe("FriendDetailScreen", () => {
jest.clearAllMocks();
});

it("shows the navigation title only after the profile name scrolls away", async () => {
const view = await render(
<SafeAreaInsetsContext.Provider
value={{ top: 0, right: 0, bottom: 0, left: 0 }}
>
<FriendDetailScreen />
</SafeAreaInsetsContext.Provider>,
);
expect(view.getByTestId("friend-navigation-title").props.children).toBe("");
await fireEvent(view.getByTestId("friend-identity-header"), "layout", {
nativeEvent: { layout: { x: 0, y: 0, width: 300, height: 120 } },
});
const [scrollView] = view.container.queryAll(
(instance) => instance.props.contentInsetAdjustmentBehavior === "automatic",
);
if (!scrollView) throw new Error("Friend ScrollView was not rendered");
const scroll = async (y: number) =>
fireEvent.scroll(scrollView, {
nativeEvent: {
contentInset: { top: 0, left: 0, bottom: 0, right: 0 },
contentOffset: { x: 0, y },
contentSize: { width: 300, height: 900 },
layoutMeasurement: { width: 300, height: 700 },
zoomScale: 1,
},
});
await scroll(120);
expect(view.getByTestId("friend-navigation-title").props.children).toBe(
"Demo User",
);
await scroll(0);
expect(view.getByTestId("friend-navigation-title").props.children).toBe("");
});

it("allows a payment even when there are no open balances", async () => {
const view = await render(
<SafeAreaInsetsContext.Provider
Expand Down
17 changes: 13 additions & 4 deletions apps/mobile/src/__tests__/settlement.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,17 @@ describe("NewSettlementScreen group context", () => {
expect(view.getByLabelText("Paid by: Alex")).toBeTruthy();
expect(view.getByLabelText("Paid to: You")).toBeTruthy();
expect(view.getByDisplayValue("12.34")).toBeTruthy();
await fireEvent(view.getByTestId("settlement-paid-by-slot"), "layout", {
nativeEvent: { layout: { x: 0, y: 0, width: 220, height: 110 } },
});
expect(
StyleSheet.flatten(view.getByTestId("settlement-paid-by").props.style)
.width,
).toBe(220);
expect(
StyleSheet.flatten(view.getByTestId("settlement-paid-to").props.style)
.width,
).toBe(220);
expect(
StyleSheet.flatten(view.getByTestId("settlement-paid-by-slot").props.style)
.flex,
Expand Down Expand Up @@ -345,7 +356,7 @@ describe("NewSettlementScreen group context", () => {
).toEqual({ disabled: true });
});

it("keeps a growing note tied to the focused-input scroll behavior", async () => {
it("does not scroll the sheet after every note size change", async () => {
const view = await render(
<SafeAreaInsetsContext.Provider
value={{ top: 0, right: 0, bottom: 0, left: 0 }}
Expand All @@ -357,9 +368,7 @@ describe("NewSettlementScreen group context", () => {

await fireEvent.press(view.getByText("Add a note"));

expect(view.getByLabelText("Notes").props.onContentSizeChange).toEqual(
expect.any(Function),
);
expect(view.getByLabelText("Notes").props.onContentSizeChange).toBeUndefined();
});

it("can always be cancelled from the native sheet toolbar", async () => {
Expand Down
39 changes: 31 additions & 8 deletions apps/mobile/src/app/(tabs)/friends/[id].tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import type { CurrencyCode } from "@splidly/shared";
import { Stack, router, useLocalSearchParams, type Href } from "expo-router";
import { HeaderHeightContext } from "expo-router/build/react-navigation/elements/Header/HeaderHeightContext";
import { use, useRef, useState } from "react";
import { Linking, Text, View } from "react-native";
import { ActivityTimeline } from "../../../components/activity-timeline";
import {
Avatar,
BalanceText,
EmptyState,
ErrorState,
Intro,
ListRow,
LoadingState,
PrimaryButton,
Expand All @@ -26,6 +27,12 @@ import { useTheme } from "../../../theme";
export default function FriendDetailScreen() {
const { id } = useLocalSearchParams<{ id: string }>();
const theme = useTheme();
const headerHeight = use(HeaderHeightContext) ?? 0;
const [compactTitleVisible, setCompactTitleVisible] = useState(
process.env.EXPO_OS !== "ios",
);
const compactTitleVisibleRef = useRef(process.env.EXPO_OS !== "ios");
const identityBottomRef = useRef(0);
const detail = api.friends.detail.useQuery({ friendshipId: id });
const list = api.friends.list.useQuery();
const profile = api.profile.me.useQuery();
Expand Down Expand Up @@ -68,8 +75,28 @@ export default function FriendDetailScreen() {
const activityGroups = groupActivityByDate(activity);
return (
<>
<Screen>
<View style={{ alignItems: "center", gap: 10, paddingVertical: 8 }}>
<Screen
onScroll={(event) => {
if (process.env.EXPO_OS !== "ios") return;
const visibleContentTop =
event.nativeEvent.contentOffset.y +
Math.max(event.nativeEvent.contentInset.top, headerHeight);
const nextVisible =
identityBottomRef.current > 0 &&
visibleContentTop >= identityBottomRef.current;
if (nextVisible === compactTitleVisibleRef.current) return;
compactTitleVisibleRef.current = nextVisible;
setCompactTitleVisible(nextVisible);
}}
>
<View
testID="friend-identity-header"
onLayout={(event) => {
const { y, height } = event.nativeEvent.layout;
identityBottomRef.current = y + height;
}}
style={{ alignItems: "center", gap: 10, paddingVertical: 8 }}
>
<Avatar
name={name}
colorKey={detail.data.friend?.userId ?? id}
Expand Down Expand Up @@ -116,10 +143,6 @@ export default function FriendDetailScreen() {
/>
</View>
</View>
<Intro>
Direct and group balances stay separated so every amount remains
traceable to its original ledger.
</Intro>
{summary?.balances.length ? (
<Section title="Open balances">
{summary.balances.map((balance, index) => (
Expand Down Expand Up @@ -231,7 +254,7 @@ export default function FriendDetailScreen() {
/>
</Section>
</Screen>
<Stack.Screen options={{ title: name }} />
<Stack.Screen options={{ title: compactTitleVisible ? name : "" }} />
</>
);
}
4 changes: 3 additions & 1 deletion apps/mobile/src/app/(tabs)/profile/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Stack } from "expo-router";
import { Platform } from "react-native";
import {
formSheetOptions,
inlineLargeTitleOptions,
Expand All @@ -8,6 +9,7 @@ import { useTheme } from "../../../theme";

export default function ProfileStackLayout() {
const theme = useTheme();
const tablet = process.env.EXPO_OS === "ios" && "isPad" in Platform && Platform.isPad;
return (
<Stack
screenOptions={{
Expand All @@ -20,7 +22,7 @@ export default function ProfileStackLayout() {
<Stack.Screen
name="index"
options={{
title: "Profile",
title: tablet ? "" : "Profile",
...inlineLargeTitleOptions(theme.text),
}}
/>
Expand Down
5 changes: 3 additions & 2 deletions apps/mobile/src/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
ThemeProvider,
} from "expo-router";
import { StatusBar } from "expo-status-bar";
import { useColorScheme } from "react-native";
import { Platform, useColorScheme } from "react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import {
expenseDetailSheetOptions,
Expand All @@ -24,6 +24,7 @@ import { NotificationCoordinator } from "../components/notification-coordinator"
function Navigation() {
const theme = useTheme();
const colorScheme = useColorScheme();
const ipad = process.env.EXPO_OS === "ios" && "isPad" in Platform && Platform.isPad;
return (
<ThemeProvider
value={colorScheme === "dark" ? DarkTheme : DefaultTheme}
Expand Down Expand Up @@ -117,7 +118,7 @@ function Navigation() {
title: "Record Payment",
...formSheetOptions(theme.sheet),
sheetAllowedDetents: [0.82, 1],
sheetInitialDetentIndex: 0,
sheetInitialDetentIndex: ipad ? 1 : 0,
sheetGrabberVisible: true,
}}
/>
Expand Down
6 changes: 2 additions & 4 deletions apps/mobile/src/app/settlement/new.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export default function NewSettlementScreen() {
keyboardClearance,
focusInput: focusBottomInput,
blurInput: blurBottomInput,
revealFocusedInput: revealBottomInput,
onScroll: onBottomScroll,
} = useKeyboardFocusScroll(screenRef, settlementOverlayHeight + 16);

let signedCanonicalMinor: bigint | undefined;
Expand Down Expand Up @@ -649,9 +649,6 @@ export default function NewSettlementScreen() {
multiline
onFocus={() => focusBottomInput(notesInputRef.current)}
onBlur={() => blurBottomInput(notesInputRef.current)}
onContentSizeChange={() =>
requestAnimationFrame(revealBottomInput)
}
textAlignVertical="top"
style={{
color: theme.text,
Expand Down Expand Up @@ -723,6 +720,7 @@ export default function NewSettlementScreen() {
<>
<Screen
scrollViewRef={screenRef}
onScroll={onBottomScroll}
transientBottomClearance={keyboardClearance}
background="sheet"
contentContainerStyle={{ paddingTop: 16, gap: 20 }}
Expand Down
17 changes: 11 additions & 6 deletions apps/mobile/src/components/allocation-floating-summary.ios.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import {
progressViewStyle,
tint,
} from "@expo/ui/swift-ui/modifiers";
import { useColorScheme, useWindowDimensions, View } from "react-native";
import { useState } from "react";
import { useColorScheme, View } from "react-native";

export function AllocationFloatingSummary({
title,
Expand All @@ -28,8 +29,7 @@ export function AllocationFloatingSummary({
complete: boolean;
}) {
const colorScheme = useColorScheme();
const { width: windowWidth } = useWindowDimensions();
const width = windowWidth - 32;
const [width, setWidth] = useState(0);
const statusColor = complete
? colorScheme === "dark"
? "#55D6A0"
Expand All @@ -56,8 +56,13 @@ export function AllocationFloatingSummary({
max: 100,
now: Math.round(Math.max(0, Math.min(1, progress)) * 100),
}}
onLayout={(event) => {
const nextWidth = event.nativeEvent.layout.width;
setWidth((current) => current === nextWidth ? current : nextWidth);
}}
style={{
width,
width: "100%",
maxWidth: 768,
height: 76,
overflow: "hidden",
borderRadius: 24,
Expand All @@ -72,7 +77,7 @@ export function AllocationFloatingSummary({
: "0 10px 28px rgba(0, 0, 0, 0.18)",
}}
>
<Host
{width > 0 ? <Host
colorScheme={colorScheme === "dark" ? "dark" : "light"}
style={{ width, height: 76 }}
>
Expand Down Expand Up @@ -115,7 +120,7 @@ export function AllocationFloatingSummary({
]}
/>
</VStack>
</Host>
</Host> : null}
</View>
);
}
6 changes: 3 additions & 3 deletions apps/mobile/src/components/allocation-floating-summary.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Text, useWindowDimensions, View } from "react-native";
import { Text, View } from "react-native";
import { useTheme } from "../theme";

/**
Expand All @@ -15,7 +15,6 @@ export function AllocationFloatingSummary({
complete: boolean;
}) {
const theme = useTheme();
const { width: windowWidth } = useWindowDimensions();
const progressWidth =
`${Math.max(0, Math.min(1, progress)) * 100}%` as const;

Expand All @@ -33,7 +32,8 @@ export function AllocationFloatingSummary({
now: Math.round(Math.max(0, Math.min(1, progress)) * 100),
}}
style={{
width: windowWidth - 32,
width: "100%",
maxWidth: 768,
height: 76,
paddingHorizontal: 16,
paddingVertical: 13,
Expand Down
7 changes: 4 additions & 3 deletions apps/mobile/src/components/expense-allocation-ui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,9 @@ export function InlineAmountInput({
const digitCount = value.replace(/\D/g, "").length;
const fontSize = digitCount > 9 ? 13 : digitCount > 6 ? 14 : 15;
return (
<View
<Pressable
accessible={false}
onPress={() => inputRef.current?.focus()}
style={{
width,
minHeight: 34,
Expand All @@ -196,7 +198,6 @@ export function InlineAmountInput({
onFocus={() => onFocus?.(inputRef.current)}
onBlur={() => onBlur?.(inputRef.current)}
keyboardType={keyboardType}
returnKeyType="done"
selectTextOnFocus
numberOfLines={1}
placeholder={placeholder}
Expand All @@ -221,7 +222,7 @@ export function InlineAmountInput({
>
{suffix}
</Text>
</View>
</Pressable>
);
}

Expand Down
Loading